create-dwy 0.28.0 → 0.29.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # create-dwy
2
2
 
3
+ ## [0.29.0] - 2026-11-08
4
+
5
+ ### Added
6
+
7
+ - 同步时从 GitHub develop 获取最新 AI 模板,Skills 更新无需单独升级 CLI;网络不可用时回退本地缓存或随包模板。
8
+ - 成功检查模板更新后显示所用仓库提交时间,方便确认模板来源的新旧。
9
+
10
+ ---
11
+
12
+ ## [0.28.1] - 2026-11-08
13
+
14
+ ### Changed
15
+
16
+ - 项目同步在确认后清理可识别的旧 Rules 托管内容,不再因旧规则自动勾选替代 Skills;个人内容和无法确认归属的文件继续保留。
17
+
18
+ ---
19
+
3
20
  ## [0.28.0] - 2026-11-08
4
21
 
5
22
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli-help.js CHANGED
@@ -17,7 +17,7 @@ export function buildHelpText(version) {
17
17
  ' dwy 选择范围、配置和 CLI,预览清单后同步',
18
18
  ' dwy sync 同上',
19
19
  ' dwy upgrade 升级到 npm 最新正式版,并刷新共享 Skills',
20
- ' dwy refresh 用当前 CLI 包内模板刷新已安装的共享 Skills',
20
+ ' dwy refresh 用最新模板刷新已安装的共享 Skills',
21
21
  ' dwy migrate 一键迁移老项目已有的托管 Skills,原目录先备份',
22
22
  ' dwy --help 显示本说明',
23
23
  ' dwy --version 显示版本',
@@ -63,7 +63,7 @@ export function buildHelpText(version) {
63
63
  '全局 Hook 的脚本与对应事件、matcher、完整命令注册均有效时,项目禁止重复安装。',
64
64
  '任一适用 CLI 已有 dwy 全局通用规范时,项目继承并清理重复基线;其他 CLI 缺失的全局规范另选全局范围补齐。',
65
65
  '手写 AGENTS.md 本身不算全局已安装;通用规范仅管理其托管块,保留手写内容。',
66
- '旧托管 Rules 在选中替代 Skills 并确认后迁移,无法确认归属的内容保留。',
66
+ '旧托管 Rules 确认后清理,Skills 自行选择;无法确认归属的内容保留。',
67
67
  '全局启用会明确列出当前项目重复项清理;全局移除不会自动向项目补装。',
68
68
  '项目同步只清理项目重复入口,不改写有效全局提供者;真实目录先备份。',
69
69
  '确认前不改项目/全局配置或已安装共享内容;确认后才同步配置,并刷新所选共享 Skills。',
@@ -75,9 +75,9 @@ export function buildHelpText(version) {
75
75
  'Hooks 的模板更新仍需执行 dwy sync;已有会话缓存的 Skill 可能需要重新加载。',
76
76
  '',
77
77
  '模板更新:',
78
- ' 模板随 CLI 包分发,启动不联网检查模板。',
79
- ' DWY_TEMPLATE_SOURCE_DIR 开发覆盖优先;未设置时读取当前 CLI 的 dwy-cli/templates/ai-tools。',
80
- ' Skills 新增或变更需要 CLI 发版;dwy upgrade 更新程序与包内模板。',
78
+ ' 同步从 GitHub develop 获取模板;离线优先使用缓存,无有效缓存时使用随包模板。',
79
+ ' DWY_TEMPLATE_SOURCE_DIR 开发覆盖优先;设置后直接使用指定本地模板。',
80
+ ' Skills 新增或变更不需要 CLI 发版;dwy upgrade 更新 CLI 程序。',
81
81
  '',
82
82
  ].join('\n')
83
83
  }
package/src/sync-all.js CHANGED
@@ -182,11 +182,6 @@ const RULE_SKILL_MIGRATIONS = {
182
182
  'dwy-prod-branch.md': ['dwy-publish'],
183
183
  }
184
184
 
185
- /** 将旧托管规则映射为可选的对应技能,保留用户最终选择。 */
186
- export function migratedRuleSkills(names) {
187
- return new Set([...names].flatMap(name => RULE_SKILL_MIGRATIONS[name] || []))
188
- }
189
-
190
185
  function staleEntries(scans, state, existing, platforms) {
191
186
  const current = Object.fromEntries(['skills', 'rules', 'hooks'].map(type => [type, new Set(scans[type].map(item => item.name))]))
192
187
  const result = {}
@@ -330,10 +325,6 @@ export async function syncAll({
330
325
  unionSets(...platforms.map(platform => platform === 'cursor' && type === 'rules' ? cursorRules : existing[platform][type]))]))
331
326
  const defaults = buildSelectionDefaultsFromSyncState(current, targetState, platforms)
332
327
  for (const type of ['skills', 'rules', 'hooks']) defaults[type] = unionSets(defaults[type], current[type])
333
- if (!global && contentTypes.includes('skills')) {
334
- const oldRules = unionSets(current.rules, ...platforms.map(platform => new Set(targetState.platforms[platform]?.rules || [])))
335
- defaults.skills = unionSets(defaults.skills, migratedRuleSkills(oldRules))
336
- }
337
328
  const choices = !global && contentTypes.includes('skills')
338
329
  ? { ...scans, skills: await projectSkillChoices(scans.skills, platforms, { homeDir, codexHome }) } : scans
339
330
  const selected = selectedOverride
@@ -385,15 +376,13 @@ export async function syncAll({
385
376
  const stale = staleEntries(scans, targetState, existing, platforms)
386
377
  const removals = normalizeRemovals(removalsOverride || stale)
387
378
  if (!removalsOverride) {
388
- const availableSkills = new Set(scans.skills.map(skill => skill.name))
389
- const selectedSkills = new Set(selected.skills.map(skill => skill.name).filter(name => availableSkills.has(name)))
390
379
  for (const platform of platforms) for (const name of removals[platform]?.rules || []) {
391
- const required = RULE_SKILL_MIGRATIONS[name]
392
- if (!required && !scans.rules.length) {
380
+ const knownRule = RULE_SKILL_MIGRATIONS[name]
381
+ if (!knownRule && !scans.rules.length) {
393
382
  removals[platform].rules.delete(name)
394
383
  continue
395
384
  }
396
- if (required && (platform === 'cursor' || !contentTypes.includes('skills') || required.some(skill => !selectedSkills.has(skill)))) {
385
+ if (knownRule && platform === 'cursor') {
397
386
  removals[platform].rules.delete(name)
398
387
  }
399
388
  }
@@ -430,7 +419,7 @@ export async function syncAll({
430
419
  if (contentTypes.includes('skills')) console.log(`Skills:${selected.skills.map(item => item.name).join('、') || '无项目安装选择;已有全局入口仅去重'}`)
431
420
  if (contentTypes.includes('rules')) console.log(scans.rules.length
432
421
  ? `Rules:${selected.rules.map(item => item.name).join('、') || '移除当前项目托管规则'}`
433
- : '旧 Rules:仅迁移已选择替代 Skill 的托管项,其他内容保留')
422
+ : '旧 Rules:清理可识别的托管项,其他内容保留;Skills 自行选择')
434
423
  if (contentTypes.includes('baseline')) console.log(`通用规范:${contentActions.baseline === 'remove' ? '显式移除' : baseline.split('\n').filter(line => /^#/.test(line)).slice(0,3).join(';')}`)
435
424
  for (const operation of plan.operations) {
436
425
  console.log(operation.label)
package/src/sync.js CHANGED
@@ -2,7 +2,8 @@ import fs from 'fs-extra'
2
2
  import path from 'path'
3
3
  import { isCancel } from '@clack/prompts'
4
4
  import { tabMultiselect } from './tab-multiselect.js'
5
- import { chalk, PACKAGE_ROOT } from './utils.js'
5
+ import { chalk } from './utils.js'
6
+ import { resolveTemplateSource } from './template-source.js'
6
7
  import { linkSkill, ensureSharedSkills, removeSkillEntry } from './skill-store.js'
7
8
 
8
9
  const CATEGORIES = [
@@ -391,14 +392,9 @@ async function syncSettings(sourceDir, targetDir, selectedHookNames, global, sta
391
392
  return 1
392
393
  }
393
394
 
394
- /** 只读取当前 CLI 的随包模板;开发时允许显式覆盖,不联网检查更新。 */
395
- export async function resolveSourceDir() {
396
- const override = process.env.DWY_TEMPLATE_SOURCE_DIR
397
- const sourceDir = override || path.join(PACKAGE_ROOT, 'templates', 'ai-tools')
398
- if (!await fs.pathExists(sourceDir)) {
399
- throw new Error(override ? `DWY_TEMPLATE_SOURCE_DIR not found: ${override}` : 'ai-tools templates not found in package')
400
- }
401
- return sourceDir
395
+ /** 获取已验证的远端模板;离线时使用缓存或随包版本。 */
396
+ export async function resolveSourceDir(options) {
397
+ return resolveTemplateSource(options)
402
398
  }
403
399
 
404
400
  /** 同步 Claude Skills 与 Hooks;基线和项目 Rules 由总同步入口分别处理。 */
@@ -0,0 +1,237 @@
1
+ import fs from 'fs-extra'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { execFile } from 'node:child_process'
5
+ import { promisify } from 'node:util'
6
+ import { randomUUID } from 'node:crypto'
7
+ import { spinner } from '@clack/prompts'
8
+ import { PACKAGE_ROOT } from './utils.js'
9
+
10
+ const execFileAsync = promisify(execFile)
11
+ const REMOTE = 'https://github.com/danweiyuancircle/dwy-ai-experience.git'
12
+ const TEMPLATE_PATH = ['dwy-cli', 'templates', 'ai-tools']
13
+ const REVISION_FILE = '.dwy-source-revision'
14
+
15
+ async function statEntry(file) {
16
+ return fs.lstat(file).catch(error => {
17
+ if (error.code === 'ENOENT') return null
18
+ throw error
19
+ })
20
+ }
21
+
22
+ async function requireDirectory(directory) {
23
+ const stat = await fs.lstat(directory)
24
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`模板目录不能是软链接或文件:${directory}`)
25
+ }
26
+
27
+ async function validateTemplates(directory) {
28
+ await requireDirectory(directory)
29
+ await validateTree(directory)
30
+ for (const name of ['skills', 'hooks']) await requireDirectory(path.join(directory, name))
31
+ const baseline = path.join(directory, 'AGENTS.md')
32
+ if (!(await fs.lstat(baseline)).isFile() || !(await fs.readFile(baseline, 'utf8')).trim()) {
33
+ throw new Error('模板缺少有效 AGENTS.md')
34
+ }
35
+ }
36
+
37
+ async function validateTree(directory) {
38
+ for (const entry of await fs.readdir(directory, { withFileTypes: true })) {
39
+ if (entry.isSymbolicLink()) throw new Error(`模板不能包含软链接:${entry.name}`)
40
+ if (entry.isDirectory()) await validateTree(path.join(directory, entry.name))
41
+ }
42
+ }
43
+
44
+ async function validateCache(directory) {
45
+ await validateTemplates(directory)
46
+ const revision = (await fs.readFile(path.join(directory, REVISION_FILE), 'utf8')).trim()
47
+ if (!/^[a-f\d]{40,64}$/.test(revision)) throw new Error('模板缓存缺少有效归属标记')
48
+ }
49
+
50
+ async function deadLockExists(lockPath) {
51
+ const stat = await statEntry(lockPath)
52
+ if (!stat) return false
53
+ if (!stat.isFile()) throw new Error(`模板锁无效,保留原文件:${lockPath}`)
54
+ const owner = await fs.readFile(lockPath, 'utf8').catch(error => {
55
+ if (error.code === 'ENOENT') return null
56
+ throw error
57
+ })
58
+ if (owner === null) return false
59
+ const pid = Number(owner.trim())
60
+ if (!/^[1-9]\d*\s*$/.test(owner) || !Number.isSafeInteger(pid) || pid > 2147483647) {
61
+ throw new Error(`模板锁无效,保留原文件:${lockPath}`)
62
+ }
63
+ try {
64
+ process.kill(pid, 0)
65
+ } catch (error) {
66
+ if (error.code === 'ESRCH') return true
67
+ if (error.code !== 'EPERM') throw error
68
+ }
69
+ throw new Error(`进程 ${pid} 正在更新模板,保留现有锁。`)
70
+ }
71
+
72
+ async function acquireLock(lockPath, candidate) {
73
+ while (true) {
74
+ try {
75
+ await fs.link(candidate, lockPath)
76
+ return
77
+ } catch (error) {
78
+ if (error.code !== 'EEXIST') throw error
79
+ }
80
+ if (!await deadLockExists(lockPath)) continue
81
+ // 回收也须互斥,避免两个进程删掉对方的新锁;回收中断时按同一协议恢复。
82
+ const recovery = `${lockPath}.recovery`
83
+ await acquireLock(recovery, candidate)
84
+ try {
85
+ if (await deadLockExists(lockPath)) await fs.unlink(lockPath)
86
+ try {
87
+ await fs.link(candidate, lockPath)
88
+ return
89
+ } catch (error) {
90
+ if (error.code !== 'EEXIST') throw error
91
+ }
92
+ } finally {
93
+ await fs.unlink(recovery)
94
+ }
95
+ }
96
+ }
97
+
98
+ async function git(args, cwd) {
99
+ try {
100
+ const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith('GIT_')))
101
+ const { stdout } = await execFileAsync('git', ['-c', 'core.hooksPath=/dev/null', ...args], {
102
+ cwd,
103
+ // 不加载用户安装的 hooks 或过滤器,避免检出远端内容时执行脚本。
104
+ env: { ...env, GIT_TERMINAL_PROMPT: '0', GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: '/dev/null' },
105
+ // 首次检出要获取整套模板,不能沿用轻量版本检查的短超时。
106
+ timeout: ['clone', 'checkout', 'fetch', 'reset'].includes(args[0]) ? 45000 : 8000,
107
+ maxBuffer: 1024 * 1024,
108
+ })
109
+ return stdout.trim()
110
+ } catch (error) {
111
+ throw new Error(`Git 获取模板失败(${error.code || error.signal})`)
112
+ }
113
+ }
114
+
115
+ async function refreshTemplates(root, cache, repositoryUrl) {
116
+ await fs.ensureDir(root)
117
+ await requireDirectory(root)
118
+ const lockPath = path.join(root, 'template-source.lock')
119
+ const candidate = path.join(root, `.template-source-lock-${randomUUID()}`)
120
+ await fs.writeFile(candidate, `${process.pid}\n`, { flag: 'wx' })
121
+ try {
122
+ await acquireLock(lockPath, candidate)
123
+ } finally {
124
+ await fs.unlink(candidate)
125
+ }
126
+ let staging
127
+ const previous = path.join(root, '.template-source.previous')
128
+ try {
129
+ if (await statEntry(cache)) {
130
+ await requireDirectory(cache)
131
+ const marker = await fs.readFile(path.join(cache, REVISION_FILE), 'utf8').catch(() => '')
132
+ if (!/^[a-f\d]{40,64}$/.test(marker.trim())) throw new Error('模板缓存缺少归属标记,保留原目录')
133
+ }
134
+ if (await statEntry(previous)) {
135
+ await validateCache(previous)
136
+ if (await statEntry(cache)) {
137
+ await validateCache(cache)
138
+ await fs.remove(previous)
139
+ } else {
140
+ await fs.rename(previous, cache)
141
+ }
142
+ }
143
+ const revision = (await git(['ls-remote', '--exit-code', repositoryUrl, 'refs/heads/develop'], root)).split(/\s/)[0]
144
+ if (!/^[a-f\d]{40,64}$/.test(revision)) throw new Error('远端 develop 版本无效')
145
+ if (await statEntry(cache)) {
146
+ try {
147
+ await validateCache(cache)
148
+ if ((await fs.readFile(path.join(cache, REVISION_FILE), 'utf8')).trim() === revision) return
149
+ } catch {
150
+ // 本地缓存不完整时重新获取,不能只凭版本标记跳过内容校验。
151
+ }
152
+ }
153
+ staging = await fs.mkdtemp(path.join(root, '.template-source-'))
154
+ const storedRepository = path.join(root, 'template-repository-v2')
155
+ const reuse = !!await statEntry(storedRepository)
156
+ const repository = reuse ? storedRepository : path.join(staging, 'repository')
157
+ if (reuse) {
158
+ await requireDirectory(repository)
159
+ await requireDirectory(path.join(repository, '.git'))
160
+ if ((await fs.readFile(path.join(repository, '.dwy-template-owner'), 'utf8')) !== `${repositoryUrl}\ndevelop\n`) throw new Error('模板 Git 缓存来源不符,保留原目录')
161
+ await git(['fetch', '--depth=1', repositoryUrl, 'develop'], repository)
162
+ await git(['reset', '--hard', 'FETCH_HEAD'], repository)
163
+ } else {
164
+ await git(['clone', '--depth=1', '--no-checkout', '--single-branch',
165
+ '--branch', 'develop', repositoryUrl, repository], staging)
166
+ await git(['sparse-checkout', 'set', '--cone', TEMPLATE_PATH.join('/')], repository)
167
+ await git(['checkout', '--force', 'develop'], repository)
168
+ }
169
+ let candidate = repository
170
+ for (const segment of TEMPLATE_PATH) {
171
+ candidate = path.join(candidate, segment)
172
+ await requireDirectory(candidate)
173
+ }
174
+ await validateTemplates(candidate)
175
+ const next = path.join(staging, 'templates')
176
+ await fs.copy(candidate, next)
177
+ await fs.writeFile(path.join(next, REVISION_FILE), await git(['rev-parse', 'HEAD'], repository))
178
+ if (!reuse) {
179
+ await fs.writeFile(path.join(repository, '.dwy-template-owner'), `${repositoryUrl}\ndevelop\n`)
180
+ await fs.rename(repository, storedRepository)
181
+ }
182
+ const hadCache = !!await statEntry(cache)
183
+ if (hadCache) await fs.rename(cache, previous)
184
+ try {
185
+ await fs.rename(next, cache)
186
+ } catch (error) {
187
+ if (hadCache) await fs.rename(previous, cache)
188
+ throw error
189
+ }
190
+ if (hadCache) await fs.remove(previous)
191
+ } finally {
192
+ try {
193
+ if (staging) await fs.remove(staging)
194
+ } finally {
195
+ await fs.unlink(lockPath)
196
+ }
197
+ }
198
+ }
199
+
200
+ /**
201
+ * 从固定公开仓库更新用户模板缓存;只读取已验证的目录,不执行模板脚本。
202
+ * 显式目录优先;DWY_HOME 优先于注入 homeDir,更新失败保留缓存,无有效缓存才用随包模板。
203
+ */
204
+ export async function resolveTemplateSource({ homeDir = os.homedir(), repository = REMOTE } = {}) {
205
+ const explicitSourceDir = process.env.DWY_TEMPLATE_SOURCE_DIR
206
+ if (explicitSourceDir) {
207
+ if (!await fs.pathExists(explicitSourceDir)) {
208
+ throw new Error(`DWY_TEMPLATE_SOURCE_DIR not found: ${explicitSourceDir}`)
209
+ }
210
+ return explicitSourceDir
211
+ }
212
+ const root = process.env.DWY_HOME || path.join(homeDir, '.dwy')
213
+ const cache = path.join(root, 'template-source')
214
+ const progress = process.stdout.isTTY ? spinner() : null
215
+ progress?.start('正在检查 AI 模板更新,首次获取可能较慢')
216
+ try {
217
+ await refreshTemplates(root, cache, repository)
218
+ progress?.stop('AI 模板已就绪')
219
+ const revision = (await fs.readFile(path.join(cache, REVISION_FILE), 'utf8')).trim()
220
+ const date = await git(['show', '-s', '--format=%cI', revision], path.join(root, 'template-repository-v2'))
221
+ .catch(() => '未知')
222
+ console.log(`模板仓库提交时间:${date}`)
223
+ return cache
224
+ } catch (error) {
225
+ progress?.stop('模板更新未完成,正在检查本地来源')
226
+ try {
227
+ await validateCache(cache)
228
+ } catch {
229
+ const bundled = path.join(PACKAGE_ROOT, 'templates', 'ai-tools')
230
+ await validateTemplates(bundled)
231
+ console.warn(`模板更新失败(${error.message});无有效缓存,回退随包模板。`)
232
+ return bundled
233
+ }
234
+ console.warn(`模板更新失败(${error.message});继续使用本地缓存。`)
235
+ return cache
236
+ }
237
+ }
@@ -23,11 +23,30 @@
23
23
  - PyPI 与 npm 的发布必须走 GitHub Actions,使用 OIDC 完成鉴权,禁止使用长期静态发布凭据。
24
24
  - 多平台打包必须走 GitHub Actions 工作流执行,不使用本地手工打包作为唯一流程。
25
25
 
26
+ ## 编码哲学:完成比完美好,可用优先
27
+
28
+ 完成比完美好,可用优先。优先交付需求范围内完整、可用、经过必要验证的功能闭环,再根据实际需要优化。
29
+
30
+ - **功能完整**:完成已约定的需求,不擅自缩减范围,不遗漏必要的调用方、配置、数据处理和使用说明。
31
+ - **流程闭环**:从真实入口到最终结果走通,处理关键失败路径;不能只完成局部代码,就把整个功能视为完成。
32
+ - **务实取舍**:选择最简单、足以满足当前需求的方案,不因追求理想架构、过度抽象或非必要打磨拖延交付。
33
+ - **验证完成**:用与改动范围匹配的测试或实际操作验证结果,明确未验证项和剩余限制;代码写完不等于任务完成。
34
+ - **守住底线**:安全、数据完整性、必要校验、错误处理和可访问性属于可用性的组成部分,不能以赶进度为由省略。
35
+
36
+ 以下简化与精准改动要求均以满足已约定需求和必要质量底线为前提。
37
+
38
+ ## 功能正确与应用稳定性
39
+
40
+ - 优先保证功能符合预期,并最大限度保持应用可用。不能为避免崩溃而改变业务语义、伪造成功或损坏数据。
41
+ - 对实际可能发生的异常,在合适边界处理并记录必要的警告或错误日志,避免局部失败导致整个应用崩溃;无法继续时,明确结束当前操作,保持其他功能可用。
42
+ - 不添加推测性的降级、重试、默认值或备用路径。只有需求明确要求,或现有调用链和故障证据证明必要时才添加,并明确适用条件。
43
+ - 优先修复根因,复用已有错误处理机制;禁止无差别捕获后忽略异常,也不在每层重复兜底。
44
+
26
45
  ## 减少常见 LLM 编码错误的行为指南
27
46
 
28
47
  与项目特定指令合并使用。
29
48
 
30
- 权衡:这些指南偏向谨慎而非速度。对于琐碎任务,自行判断。
49
+ 权衡:按任务风险投入必要的分析和验证,不让流程与非必要打磨拖延可用功能的交付。对于琐碎任务,自行判断。
31
50
 
32
51
  ### 1. 先思考再编码
33
52
 
@@ -47,7 +66,7 @@
47
66
  - 不为一次性使用的代码做抽象。
48
67
  - 不做未被要求的"灵活性"或"可配置性"。
49
68
  - 不为不可能发生的场景做错误处理。
50
- - 如果你写了 200 行但可以写成 50 行,重写它。
69
+ - 实现时优先选择清晰、简洁的写法;不为单纯减少行数重写已满足需求的代码。
51
70
  - 问自己:"一个资深工程师会说这过于复杂吗?"如果是,简化。
52
71
 
53
72
  ### 写代码前走决策阶梯
@@ -1,5 +1,16 @@
1
1
  # ai-tools 内部版本
2
2
 
3
+ ## 0.36.0 - 2026-11-08
4
+
5
+ ### Changed
6
+
7
+ - `skills/通用/dwy-scope-guard/SKILL.md` 将想法收集箱与版本规划分开,支持项目内独立存储、旧资料兼容、版本范围评估和下版开发提醒。
8
+
9
+ ## 0.35.1 - 2026-11-08
10
+
11
+ - 更新通用开发基线,强调功能闭环、稳定性和必要验证。
12
+ - Apple 开发资料补充模拟器 Keychain 签名排查,并明确 macOS Intel 和 Apple Silicon 构建验证。
13
+
3
14
  ## 0.35.0 - 2026-11-08
4
15
 
5
16
  - 新增 Android 开发与架构、Flutter、HarmonyOS、PostgreSQL、Redis 和 Alembic Skills,按任务加载迁移后的规范和踩坑。
@@ -1 +1 @@
1
- 0.35.0
1
+ 0.36.0
@@ -31,9 +31,9 @@
31
31
 
32
32
  ### Package.swift 的 .macOS 平台声明,别为「只支持手机」删掉
33
33
 
34
- - **现象**:想让 app 只支持 iPhone,看到 `Package.swift` 里 `platforms: [.iOS(.v17), .macOS(.v14)]` 就想删掉 `.macOS`。
34
+ - **现象**:想让 app 只支持 iPhone,看到 `Package.swift` 里 `platforms: [.iOS(.v17), .macOS(.v15)]` 就想删掉 `.macOS`。
35
35
  - **根因**:这是**纯逻辑库**的平台声明,作用是让命令行 `swift test` 能在 mac(host)上跑核心逻辑测试,和 iOS app 支持哪些设备**无关**。app 的设备类型由 app target 的 `TARGETED_DEVICE_FAMILY` 控制。删掉 `.macOS` 会让 `swift test` 在 mac 上跑不起来。
36
- - **正确做法**:保留 `.macOS(.v14)`。要 app 只支持手机,设 app target `TARGETED_DEVICE_FAMILY: "1"`(1=iPhone,2=iPad)。库的 platforms 是「最低支持平台」,不影响 app。
36
+ - **正确做法**:按项目实际支持范围保留 macOS 声明;新项目基线为 `.macOS(.v15)`。要 app 只支持手机,设 app target `TARGETED_DEVICE_FAMILY: "1"`(1=iPhone,2=iPad)。库的 platforms 是「最低支持平台」,不影响 app。
37
37
  - **适用**:两层结构(SPM 纯逻辑库 + iOS app)、靠命令行 `swift test` 验证核心逻辑的工程。
38
38
 
39
39
  ### XcodeGen 加 String Catalog,要靠 CFBundleLocalizations 才生成多语言 .lproj
@@ -104,6 +104,13 @@
104
104
 
105
105
  ## 证书 / Keychain / mTLS
106
106
 
107
+ ### 模拟器能启动但 Keychain 返回 -34018
108
+
109
+ - **现象**:模拟器可以启动、发现局域网设备,读取或创建 Keychain 项却返回 `-34018`(`errSecMissingEntitlement`);依赖密钥的配对和连接随之失败,真机正常。
110
+ - **范围与根因**:本次 iOS 17.5 模拟器使用 `CODE_SIGNING_ALLOWED=NO` 构建,应用缺少模拟器运行时的应用标识权限;编译成功和界面可见不能证明 Keychain 可用。`-34018` 也可能来自其他访问组配置错误,应以实际构建产物、查询参数和日志判断,不把所有场景归为未签名。
111
+ - **正确做法**:检查实际构建及安装产物的签名、模拟器模拟权限和 `application-identifier`,核对 Keychain 查询使用的访问组。需要验证 Keychain 时启用模拟器签名构建,再覆盖安装并重试真实的密钥读写;不要为了排错清除 Keychain、删除应用数据或复制真机私钥。参见 [Apple 对 -34018 的说明](https://developer.apple.com/forums/thread/114456)。
112
+ - **验证状态**:本次启用签名后,模拟器已成功生成密钥并向局域网电视发送 ADB 公钥,未再出现 `-34018`;电视未显示授权提示,端到端连接仍待验证。模拟器与真机的密钥独立,不能用真机已授权推断模拟器也已获授权。
113
+
107
114
  ### SecIdentityCreateWithCertificate 在 iOS 不存在,组装 SecIdentity 要走 keychain 查询
108
115
 
109
116
  - **现象**:用 swift-certificates 生成自签证书后想组装 `SecIdentity`,命令行 `swift test`(host 是 macOS)能过,但一编译 iOS(模拟器/真机)就报 `cannot find 'SecIdentityCreateWithCertificate' in scope`,阻断整个 app build。
@@ -9,6 +9,8 @@ description: 开发、排查、评审或适配 macOS 应用时使用,涵盖窗
9
9
 
10
10
  新项目默认最低 macOS 15;现有项目先读取实际 deployment target,发现差异时说明,不擅自升级。新 API 按 SDK 与运行时版本判断,保留低版本可用体验。
11
11
 
12
+ macOS 应用与库须支持 Intel `x86_64` 和 Apple Silicon `arm64`;检查源码、二进制依赖和构建设置,发布产物应包含两种架构。分别验证两架构构建,检查产物架构;实际运行和界面验收分别报告,不能以交叉编译代替 Intel 实机验证。
13
+
12
14
  按需阅读 [窗口与系统外观](references/ui-adaptation.md)、[弹窗差异](references/presentations.md) 和 [macOS 踩坑](references/pitfalls.md)。界面变更先确定目标系统、窗口尺寸、键鼠和辅助功能验证;签名或启动通过不等于目标系统交互与视觉验收。
13
15
 
14
16
  发现可复用新坑先查重:只在 macOS 证实的放本 Skill;双端均有证据时将共通部分放 Swift Skill,保留平台差异。记录现象、范围、根因及证据、处理方法、验证状态和未验证项;未确认标记待验证,项目专属问题留项目内。修改共享 Skill 必须已有来源仓库授权。
@@ -1,91 +1,49 @@
1
1
  ---
2
2
  name: dwy-scope-guard
3
- description: 用户提出新增功能、顺手扩展、收录想法,或开始规划下一个版本、查看待办想法、担心本期功能过多时使用。适用于持续迭代中的版本范围与想法管理;单纯修复既有行为、执行已确认范围内的任务时不重复评审。
3
+ description: 收集、查看、修改或删除产品想法;评估新增功能、本期范围和上线版本差异;规划或调整后续版本,或开始开发下一版本时使用。单纯修复既有行为或执行已确认范围时不重复评审。
4
4
  ---
5
5
 
6
- # 版本克制与想法收录
6
+ # 想法收集与版本规划
7
7
 
8
- 帮助用户判断想法是否值得现在做,让每个版本先完成可用闭环。敢于指出范围膨胀,把暂缓的想法可靠保存,并在新版本规划时重新提醒。
8
+ 帮助用户随时收集想法,并基于真实需求和交付成本控制版本范围,让产品尽快以可用的小版本上线。想法是收集箱;版本计划是经过取舍的动态安排,两者不能混为一谈。
9
9
 
10
- ## 工作边界
10
+ ## 触发与边界
11
11
 
12
- - 针对需求证据和交付取舍提出异议,不评价用户的性格或欲望,也不迎合每个想法。
13
- - 用户明确限定“只分析、先分析、不改文件”时不开发、不持久化,返回待收录内容;常规想法评估按第 3 节收录。明确要求实施时,给出一次简短范围建议后,在授权范围内执行。不能擅自删除已约定功能;用户听取建议后仍决定继续,不反复劝阻。
14
- - 安全、数据完整性、输入校验、错误处理、可访问性和必要验证属于可用性,不因控制范围而省略。
15
- - 提醒发生在本 Skill 被调用、识别到新增功能或新版本规划时。自动触发取决于宿主发现能力;不承诺后台监控、定时或跨会话主动推送,不自行配置通知服务。
12
+ - 用户说“记个想法、查看想法、修改或删除想法”时,直接处理想法收集箱;收录不等于安排开发。
13
+ - 用户提出新功能或顺手扩展、担心功能过多、要求比较线上版本时,评估当前范围和发布影响,提出可交付的最小范围及后续安排。
14
+ - 用户说“规划下一版、下个版本做什么、调整版本计划”或开始开发下一版本时,先读取版本计划和想法箱,提醒已规划内容与需要处理的遗留项,再按当前目标建议取舍。仅查看想法时不强行制定计划。
15
+ - 普通修复和已确认范围内的开发不重复评审;范围、证据或依赖明显变化时再提醒。用户坚持某项取舍时遵照执行,不反复劝阻。
16
+ - 只在 Skill 被宿主发现和调用时提醒,不承诺后台监控、定时或跨会话主动通知。用户要求只分析、不改文件或当前无写权限时只提供建议,不声称已经保存。
16
17
 
17
- ## 1. 先确认当前版本的负担
18
+ ## 项目存储
18
19
 
19
- 读取当前产品的版本计划、变更记录、验收记录及相关改动,优先复用已有资料。多包仓库按当前产品或包判断,不把其他包的工作算进来。
20
+ 以当前任务的项目根目录为准;多产品或多包仓库按当前产品区分,不混入其他产品。即使 Skill 全局安装,项目数据也只写入该项目:
20
21
 
21
- - 确认上一个已发布或已验收版本、本期目标和已承诺范围;最新提交或标签本身不能证明版本可用。
22
- - 区分本期已验证、已实现未验证、尚未完成的内容,关注新增用户能力、跨模块依赖、数据迁移和验收成本,不只数功能或代码行数。
23
- - 缺少基线或进度证据时明确说明,不编造版本差异、工期或完成率。仅在影响决策时问一个关键问题,其余给出带假设的建议。
22
+ - `.dwy/dwy-scope-guard/ideas.md`:想法收集箱。
23
+ - `.dwy/dwy-scope-guard/versions.md`:当前和后续版本计划。
24
24
 
25
- ## 2. 每个新想法先给判断
25
+ 这是需保留的项目资料,不作为可随意清理的临时缓存;不要写到 Skill 安装目录或用户全局目录,不记录凭据等敏感信息,也不擅自创建远端任务。只读取时不创建空文件。按需创建目录和对应文件;写入后回读并报告实际路径,失败时如实说明。
26
26
 
27
- 结合上下文简短分析:解决谁的什么问题、证据是什么、不做的影响、能否用现有能力满足,以及最小方案引入多少实现、运维和验证负担。无证据的收益标为假设,不机械追问一整套问题。
27
+ 先查项目已有的想法池、版本计划和旧版 `.dwy/ideas.md`,将其作为已有信息来源,不覆盖或删除。新旧资料并存时按内容和编号核对去重;需要沿用或迁入时保留来源和状态,核对后再在新目录维护,不能因迁移丢失历史。项目明确指定现有规划为权威来源时沿用该规划,不再创建冲突的第二份计划。
28
28
 
29
- 先给结论:**本期做 / 缩小后做 / 放到后续版本 / 先验证需求**,随后说明理由、最小范围和收尾标准。
29
+ ## 想法收集箱
30
30
 
31
- - 新想法支撑本期核心闭环、有实际需求依据且负担可控时,建议本期做。
32
- - 已有多项未完成或未验收内容、新增独立使用场景、引入较重依赖,或不断以“顺手、以后可能用到、既然做了就一起”扩张时,具体指出对交付的影响,优先建议收尾或拆阶段。
33
- - 需要拆阶段时,每阶段都要有独立可用结果和可判断的验收条件;后续增强进入清单,不作为本期交付的前置条件。
34
- - 本期确需增加范围时,说明新增成本,并建议替换或推迟什么;取舍由用户决定。不设任意功能数量上限,不为凑齐固定数量增加需求。
35
- - 已分析并确认过的同一想法,直接沿用结论;只有范围、证据或依赖发生变化时才重评。
31
+ 用户要求记录或讨论中形成需要保留的暂缓想法时,按用户原意收录;先查重,同一想法补充信息,不覆盖其他条目。用户可以随时增加、修改或删除想法,按其明确要求执行;删除前检查版本计划中的引用,保留计划中的必要描述和历史,不把删除想法误作取消版本安排。
36
32
 
37
- ## 3. 把想法存到项目里
33
+ 每条只记录当前已知的信息:稳定编号、名称、提出日期或来源、用户想解决的问题、补充说明,以及适用时的证据、建议或关联版本。未知写“待确认”;AI 建议与用户原意分开。无需为每条想法提前分配版本或编造优先级,用户可随时修改。
38
34
 
39
- 先查项目指定或已有的想法池、backlog、版本路线图,沿用其位置和格式;没有时在项目根创建 `.dwy/ideas.md`。不写进 Skill 安装目录、全局个人记忆或其他项目,不另建重复清单。多产品清单按产品或包分组。
35
+ ## 版本范围判断
40
36
 
41
- 用户要求收录,或常规分析产生暂缓想法时,在任务允许写文件的情况下直接记录,不逐条重复确认。明确限定仅分析、纯只读请求或没有写权限时只给待收录内容,明确尚未保存。不得把敏感数据、凭据写入清单,也不因已有外部系统而擅自创建远端任务。
37
+ 先查看当前产品线上实际发布版本、当前待发布版本、版本计划、变更与验收证据。标签、提交或已实现代码不能单独证明已经上线或验收;基线不明时标注未知,不编造功能差异、工作量或完成率。
42
38
 
43
- 记录前按问题和方案查重;已有同一想法时补充证据和决定,不重复新增,不覆盖无关记录。每项保留:
39
+ 相对线上版本梳理新增能力、未完成和未验收项,关注核心目标、用户价值、跨模块依赖、迁移、验证和发布成本。功能数量只是风险信号,不设机械上限。若积累过多,明确指出延迟上线的原因,建议保留能独立使用的最小闭环,将可拆出的内容推迟。安全、数据完整性、必要校验、可访问性与验收不能为了快而省略。
44
40
 
45
- - 稳定编号、简短名称、提出日期与来源。
46
- - 用户问题和需求证据;用户原意与 AI 建议分开,未知项写“待确认”。
47
- - 最小可用方案、暂缓原因与重评条件。
48
- - 状态:候选 / 本期已选 / 已交付 / 已放弃;目标版本未定就写“未定”,不自行承诺下版必做。
41
+ 对新功能给出简短建议:本期做、缩小后做、安排后续版本,或先收集证据;说明依据、最小范围和不做的影响。本期确需新增时,说明成本并建议用什么内容置换。最终范围由用户决定,不擅自删除已承诺功能。
49
42
 
50
- 没有既有格式时,可用下列 Markdown 结构,按真实信息填写:
43
+ ## 版本计划与调整
51
44
 
52
- ```markdown
53
- # 想法清单
45
+ 版本计划保存目标、线上基线、当前拟交付范围、已验证与未完成项、最小验收条件,以及后续版本的候选安排和取舍原因。使用真实版本号;尚未确定时写“下一版”或“版本待定”,不虚构版本号、日期或承诺。可引用想法编号并保留足够描述;直接规划的任务不强制先创建想法。
54
46
 
55
- ## 当前版本
47
+ 被推迟的功能先根据依赖、价值和发布节奏建议放入合适的后续版本;条件不足时放入“待排期”,不能默认全部挤入下一版。把建议和用户已确认的计划区分记录。用户明确要求规划或将某项移到后续时,可保存相应安排;没有确认的推荐保持建议状态,不视为承诺。
56
48
 
57
- - 上一可用版本:待确认
58
- - 本期目标与范围:待确认
59
- - 收尾与验收条件:待确认
60
-
61
- ## IDEA-001:想法名称
62
-
63
- - 提出日期与来源:实际日期;用户原话或需求链接
64
- - 状态 / 目标版本:候选 / 未定
65
- - 问题与证据:具体问题;无证据时注明假设
66
- - 最小方案:最小可用结果;AI 建议单独标明
67
- - 暂缓原因:当前取舍
68
- - 重评条件:下一版本规划,或具体反馈、依赖条件满足时
69
- ```
70
-
71
- 已有版本计划时引用它,不重复维护一份当前范围。实际写入后回读相关条目,报告保存位置和编号;写入失败不得声称已收录。仅更新清单不代表功能实现或版本交付。
72
-
73
- ## 4. 新版本规划时重新提醒
74
-
75
- 用户说“开始下个版本、下一版做什么、排版本、还有哪些想法”等时,先读取持久化清单,再提出计划,不依赖聊天记忆。
76
-
77
- 1. 核对上一版实际交付和未完成项;未完成项先明确处置,不默认为已完成或偷偷带入下一版。
78
- 2. 主动提醒候选想法,说明哪些到了重评时机、哪些仍缺证据或依赖;没有到期候选时如实说明。
79
- 3. 结合新反馈和本期目标推荐少量最相关项,解释取舍,避免把整个想法池塞进版本。用户未选定前保持候选状态。
80
- 4. 用户确定范围后,记录本期目标、已选编号、最小范围和收尾标准,更新对应状态及目标版本。用户决定暂缓或撤回本期项时恢复候选并保留原因;同一轮规划不反复提醒已决定的事项。
81
- 5. 只有对应验收证据成立才标为已交付;未验收的实现仍保留未完成说明。用户明确放弃时标为已放弃并保留原因,不删除历史、不再次推荐;用户主动重提时再更新。
82
-
83
- 清单不存在时说明没有可读取的历史想法,不编造遗漏记录。不要只因查询清单就创建空文件。
84
-
85
- ## 输出示例
86
-
87
- 用户:“记账功能还没验收,我还想这版一起加 AI 分析、家庭共享和多币种。”
88
-
89
- > **建议:先完成本期验收,三个想法放到后续版本。** 基础录入尚未验收,家庭共享会增加权限和同步,多币种会增加换算与数据处理,都会扩大本期验证范围。先走通“录入—保存—查看”及关键失败路径;三个想法分别收录,需求证据先标为待确认。开始规划下一版时,再结合反馈提醒并重新排序。
90
-
91
- 只有实际保存后才补充收录位置与编号;此示例不代表已经写入想法清单。
49
+ 开始下一版本开发时,核对上一版实际交付、未完成项和现有计划,提醒相关已规划项,再结合新反馈及用户目标推荐少量内容;用户可选择已有计划、调整顺序或重新规划。只有实际验收并有证据才标记已交付;未完成事项明确保留或调整,不能自动当作完成。查看、调整与删除计划均按用户意图执行,保留仍需要的历史和关联。