create-dwy 0.12.0 → 0.12.2

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 (23) hide show
  1. package/package.json +1 -1
  2. package/src/sync.js +29 -17
  3. package/templates/claude-global/rules/Python/dwy-python-core.md +105 -7
  4. package/templates/claude-global/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +255 -0
  5. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/SKILL.md +232 -0
  6. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/files-whitelist-snippet.json +22 -0
  7. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/js-commercial.npmignore +76 -0
  8. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/python-commercial-MANIFEST.in +37 -0
  9. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/python-commercial.gitignore +62 -0
  10. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/js-commercial-guide.md +133 -0
  11. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/publish-blacklist.md +106 -0
  12. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/python-commercial-guide.md +147 -0
  13. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/sensitive-patterns.md +112 -0
  14. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_comments.py +260 -0
  15. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_js_publish.sh +110 -0
  16. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_python_publish.sh +171 -0
  17. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/detect_sdk.sh +71 -0
  18. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/init_config.sh +97 -0
  19. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/SKILL.md +170 -0
  20. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/examples//351/205/215/351/242/235/350/256/241/347/256/227.md +64 -0
  21. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/templates/INDEX.md +7 -0
  22. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/templates/logic-doc.md +49 -0
  23. /package/templates/claude-global/hooks/{pre-git-commit-sensitive-check.sh → Git/pre-git-commit-sensitive-check.sh} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "项目脚手架与 Claude Code 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/sync.js CHANGED
@@ -109,17 +109,22 @@ async function scanHooks(sourceDir) {
109
109
  const hooksDir = path.join(sourceDir, 'hooks')
110
110
  if (!await fs.pathExists(hooksDir)) return []
111
111
 
112
- const entries = await fs.readdir(hooksDir, { withFileTypes: true })
113
- return entries
114
- .filter(e => e.name !== '.gitkeep')
115
- .map(e => ({
116
- name: e.name,
117
- description: e.isDirectory() ? '钩子目录' : '钩子脚本',
118
- category: '其他',
119
- sourcePath: path.join(hooksDir, e.name),
120
- type: 'hook',
121
- }))
122
- .sort((a, b) => a.name.localeCompare(b.name))
112
+ const hooks = []
113
+ for (const cat of await fs.readdir(hooksDir, { withFileTypes: true })) {
114
+ if (!cat.isDirectory()) continue
115
+ const catDir = path.join(hooksDir, cat.name)
116
+ for (const entry of await fs.readdir(catDir, { withFileTypes: true })) {
117
+ if (entry.name === '.gitkeep') continue
118
+ hooks.push({
119
+ name: entry.name,
120
+ description: entry.isDirectory() ? '钩子目录' : '钩子脚本',
121
+ category: cat.name,
122
+ sourcePath: path.join(catDir, entry.name),
123
+ type: 'hook',
124
+ })
125
+ }
126
+ }
127
+ return hooks.sort((a, b) => a.name.localeCompare(b.name))
123
128
  }
124
129
 
125
130
  async function scanExisting(projectTargetDir, typePlural) {
@@ -229,10 +234,12 @@ async function copyHooks(items, targetDir, dryRun) {
229
234
  return items.length
230
235
  }
231
236
 
232
- async function removeUnselected(typePlural, existingNames, selectedNames, targetDir, dryRun) {
237
+ async function removeUnselected(typePlural, existingNames, selectedNames, templateNames, targetDir, dryRun) {
233
238
  let count = 0
234
239
  for (const name of existingNames) {
235
240
  if (selectedNames.has(name)) continue
241
+ // 项目独有(模板未提供过)的条目永不删除,保留用户自定义
242
+ if (!templateNames.has(name)) continue
236
243
  const target = path.join(targetDir, typePlural, name)
237
244
  if (!dryRun) await fs.remove(target)
238
245
  logAction(dryRun, `${typePlural}/${name}`, 'red', '×')
@@ -420,12 +427,17 @@ export async function syncClaude({ target, reselect = false, dryRun = false } =
420
427
  syncedCount += await syncSettings(sourceDir, projectTargetDir, selectedHookNames, dryRun)
421
428
  syncedCount += await copyHooks(selected.hooks, projectTargetDir, dryRun)
422
429
 
423
- // 删除已存在但本次未勾选的(仅 reselect 模式:缓存模式默认 selected ⊇ existing 不会触发)
430
+ // 删除已存在但本次未勾选的(仅 reselect 模式:缓存模式默认 selected ⊇ existing 不会触发)
431
+ // 删除范围限定在模板提供过的条目内,项目自定义条目(scans 里没有的)始终保留
424
432
  if (reselect) {
425
- removedCount += await removeUnselected('skills', existing.skills, new Set(selected.skills.map(s => s.name)), projectTargetDir, dryRun)
426
- removedCount += await removeUnselected('rules', existing.rules, new Set(selected.rules.map(r => r.name)), projectTargetDir, dryRun)
427
- removedCount += await removeUnselected('commands', existing.commands, new Set(selected.commands.map(c => c.name)), projectTargetDir, dryRun)
428
- removedCount += await removeUnselected('hooks', existing.hooks, selectedHookNames, projectTargetDir, dryRun)
433
+ const skillTemplateNames = new Set(scans.skills.map(s => s.name))
434
+ const ruleTemplateNames = new Set(scans.rules.map(r => r.name))
435
+ const commandTemplateNames = new Set(scans.commands.map(c => c.name))
436
+ const hookTemplateNames = new Set(scans.hooks.map(h => h.name))
437
+ removedCount += await removeUnselected('skills', existing.skills, new Set(selected.skills.map(s => s.name)), skillTemplateNames, projectTargetDir, dryRun)
438
+ removedCount += await removeUnselected('rules', existing.rules, new Set(selected.rules.map(r => r.name)), ruleTemplateNames, projectTargetDir, dryRun)
439
+ removedCount += await removeUnselected('commands', existing.commands, new Set(selected.commands.map(c => c.name)), commandTemplateNames, projectTargetDir, dryRun)
440
+ removedCount += await removeUnselected('hooks', existing.hooks, selectedHookNames, hookTemplateNames, projectTargetDir, dryRun)
429
441
  }
430
442
 
431
443
  console.log(chalk.blue(`\n${dryRun ? 'Dry-run complete.' : 'Done!'} ${syncedCount} synced${removedCount > 0 ? `, ${removedCount} removed` : ''}.`))
@@ -64,9 +64,41 @@ select = [
64
64
  "B", # flake8-bugbear
65
65
  "SIM", # flake8-simplify
66
66
  "RUF", # ruff-specific
67
+ "D", # pydocstyle (docstring 规范,强制 Google 风格)
67
68
  ]
69
+ ignore = [
70
+ "D100", # 模块顶层 docstring:脚本/迁移等不强制(按需在文件头补)
71
+ "D104", # __init__.py 不强制 docstring
72
+ "D107", # __init__ 方法 docstring 写在类 docstring 里(与本规范一致)
73
+ "D203", # 与 D211 冲突,二选一(Google 风格选 D211:类 docstring 前不加空行)
74
+ "D213", # 与 D212 冲突,二选一(Google 风格选 D212:多行 docstring 首行紧跟 """)
75
+ "D401", # 中文 docstring 不要求祈使句开头
76
+ "D415", # 中文 docstring 不要求以 . ! ? 结尾(用中文句号 。)
77
+ ]
78
+
79
+ [tool.ruff.lint.pydocstyle]
80
+ convention = "google"
81
+
82
+ [tool.ruff.lint.per-file-ignores]
83
+ # 测试文件不强制 docstring(测试方法名已自描述)
84
+ "tests/**/*.py" = ["D100", "D101", "D102", "D103", "D104"]
85
+ "**/test_*.py" = ["D100", "D101", "D102", "D103", "D104"]
86
+ # 迁移脚本不强制 docstring
87
+ "**/migrations/**/*.py" = ["D"]
88
+ # CLI 入口脚本不强制模块 docstring
89
+ "**/__main__.py" = ["D100"]
68
90
  ```
69
91
 
92
+ 启用后 ruff 会强制检查:
93
+
94
+ - 公开模块/类/函数/方法必须有 docstring(D101 / D102 / D103)
95
+ - 私有方法以单下划线开头时不强制(pydocstyle 默认)
96
+ - Google 风格 section 格式(`Args:` / `Returns:` / `Raises:`)
97
+ - docstring 首行必须紧跟 `"""`(D212)
98
+ - 类 docstring 前不留空行(D211)
99
+
100
+ ⚠️ **D 规则只校验「有没有 docstring + 格式」,不校验「Args 是否包含类型/示例/边界」**。后者无法静态检查,靠 code review + 本规范第九节强制要求落实。
101
+
70
102
  ### 命名规范
71
103
 
72
104
  | 类型 | 风格 | 示例 |
@@ -265,17 +297,18 @@ class UserRepository:
265
297
  """初始化 UserRepository。
266
298
 
267
299
  Args:
268
- db: 数据库连接对象。
300
+ db (Database): 数据库连接对象。示例:``Database("postgresql+asyncpg://user:pass@localhost:5432/app")``。
269
301
  """
270
302
  self.db = db
271
303
 
272
- def get_by_id(self, user_id: int) -> User:
304
+ def get_by_id(self, user_id: int, *, page_size: int = 20) -> User:
273
305
  """根据 ID 查询用户。
274
306
 
275
307
  Args:
276
- user_id: 用户唯一标识。
308
+ user_id (int): 用户唯一标识。取值范围 ``[1, 2^31-1]``。示例:``1001``。
309
+ page_size (int): 关联数据分页大小。取值范围 ``[1, 100]``,默认 ``20``。示例:``50``。
277
310
  Returns:
278
- 匹配的用户对象。
311
+ User: 匹配的用户对象。
279
312
  Raises:
280
313
  UserNotFound: 用户不存在时抛出。
281
314
  """
@@ -285,7 +318,13 @@ class UserRepository:
285
318
  return user
286
319
 
287
320
  def _build_cache_key(self, user_id: int) -> str:
288
- """根据用户 ID 构建缓存键。"""
321
+ """根据用户 ID 构建缓存键。
322
+
323
+ Args:
324
+ user_id (int): 用户唯一标识。取值范围 ``[1, 2^31-1]``。示例:``1001``。
325
+ Returns:
326
+ str: 缓存键,形如 ``user:1001``。
327
+ """
289
328
  return f"user:{user_id}"
290
329
  ```
291
330
 
@@ -299,6 +338,64 @@ class UserRepository:
299
338
  | 魔术方法(`__str__` 等) | 必须有,说明行为 |
300
339
  | 私有方法 | 必须有,至少单行说明 |
301
340
 
341
+ ### Args 参数注释规范(强制)
342
+
343
+ 每个参数的 `Args` 注释**必须包含四要素**:参数名、类型、描述、示例;**有取值边界的必须显式标注边界**。
344
+
345
+ 格式模板:
346
+
347
+ ```
348
+ 参数名 (类型): 描述。[取值范围 `[min, max]` / 长度 `[min, max]` / 枚举 `{a, b, c}`,默认 `xxx`。]示例:`具体值`。
349
+ ```
350
+
351
+ | 要素 | 写法 | 示例 |
352
+ |---|---|---|
353
+ | 类型 | 紧跟参数名,括号包裹,与函数签名一致 | `user_id (int)` |
354
+ | 描述 | 一句话说明语义,不重复参数名 | `用户唯一标识` |
355
+ | 取值边界 | 数值用区间,字符串用长度,离散值用枚举 | `取值范围 [1, 100]` / `长度 [1, 64]` / `枚举 {"asc", "desc"}` |
356
+ | 默认值 | 与签名一致,显式写出 | `默认 20` |
357
+ | 示例 | 反引号包裹,给出真实可用值 | `示例:1001` |
358
+
359
+ **强制要求**:
360
+
361
+ - 凡是函数签名有 `int` / `float` / 字符串长度限制 / 时间窗口 / 分页 / 文件大小 / 比例阈值等**数值或长度型参数**,Args 注释**必须**写取值边界
362
+ - 凡是离散取值(状态枚举、排序方向、模式开关)**必须**列出可选值
363
+ - 凡是有副作用风险的参数(如 `force=True`、`delete=True`)**必须**在描述中提示风险
364
+
365
+ 正反例:
366
+
367
+ ```python
368
+ # ❌ 缺类型、缺示例、缺边界
369
+ def search(keyword, limit=20):
370
+ """搜索用户。
371
+
372
+ Args:
373
+ keyword: 搜索关键词。
374
+ limit: 返回数量。
375
+ """
376
+
377
+ # ❌ 有类型但没边界、没示例
378
+ def search(keyword: str, limit: int = 20) -> list[User]:
379
+ """搜索用户。
380
+
381
+ Args:
382
+ keyword (str): 搜索关键词。
383
+ limit (int): 返回数量。
384
+ """
385
+
386
+ # ✅ 四要素齐全,边界明确
387
+ def search(keyword: str, limit: int = 20, order: str = "desc") -> list[User]:
388
+ """搜索用户。
389
+
390
+ Args:
391
+ keyword (str): 搜索关键词。长度范围 ``[1, 64]``。示例:``"alice"``。
392
+ limit (int): 返回数量。取值范围 ``[1, 100]``,默认 ``20``。示例:``50``。
393
+ order (str): 排序方向。枚举 ``{"asc", "desc"}``,默认 ``"desc"``。示例:``"asc"``。
394
+ Returns:
395
+ list[User]: 匹配的用户列表,按创建时间排序。
396
+ """
397
+ ```
398
+
302
399
  ### 行内注释
303
400
 
304
401
  ```python
@@ -322,7 +419,8 @@ user = cache.get_user(user_id)
322
419
  | 7 | 无可变默认参数 `def f(items=[])` |
323
420
  | 8 | 嵌套不超过 3 层,函数体不超过 50 行 |
324
421
  | 9 | 所有模块/类/函数有 docstring(Google 风格) |
325
- | 10 | 外部输入用 Pydantic 校验,不手动校验 dict |
326
- | 11 | 配置必须 Pydantic Settings 嵌套分组;缓存 TTL / 超时 / 重试 / 限流 / 分页上限 / 文件上限 / Token 有效期等业务参数禁止硬编码 |
422
+ | 10 | 所有函数的 `Args` 注释包含「类型 + 描述 + 示例」三要素;数值/长度/枚举型参数必须显式标注取值边界 |
423
+ | 11 | 外部输入用 Pydantic 校验,不手动校验 dict |
424
+ | 12 | 配置必须 Pydantic Settings 嵌套分组;缓存 TTL / 超时 / 重试 / 限流 / 分页上限 / 文件上限 / Token 有效期等业务参数禁止硬编码 |
327
425
 
328
426
  **不执行自检就提交代码 = 违规。**
@@ -0,0 +1,255 @@
1
+ ---
2
+ name: dwy-shared
3
+ description: "管理 dwy-shared 模板仓库与 dwy CLI 工具。功能 1:把本机已有的 skill/rule/hook/command 文件加进 dwy-shared 仓库时,自动读现有分类目录、推荐归位、AskUserQuestion 确认后 cp/git mv + commit + push 到 Gitee,让别的项目用 dwy claude sync 拉到。功能 2:管理 dwy npm CLI(create-dwy)的安装/更新/卸载/版本查询/CDN 延迟应急。涉及以下任何场景必须使用此 skill:用户说「加 X 到 dwy-shared」「在 dwy-shared 新增 skill/rule/hook/command」「dwy 仓库整理」「dwy-shared 仓库管理」「换 dwy-shared 仓库路径」「重设 dwy-shared」「升级/装/卸/重装 dwy CLI」「dwy 拉不到最新版」「create-dwy 怎么装」「dwy --version 显示旧版」「dwy CDN 没传播」。"
4
+ ---
5
+
6
+ # dwy-shared 仓库管理
7
+
8
+ 管理 dwy-shared 模板仓库内 skills/rules/hooks/commands 的入库归类,以及 dwy CLI 的本机生命周期。
9
+
10
+ ## 何时触发
11
+
12
+ 下列任何场景**必须**用此 skill,不要直接动手:
13
+
14
+ - 用户给一个本机文件路径,说要加到 dwy-shared 仓库
15
+ - 用户说「在 dwy-shared 仓库新增 skill/rule/hook/command」
16
+ - 用户说「dwy 仓库整理 / dwy-shared 仓库管理」
17
+ - 用户说「升级/装/卸/重装 dwy CLI」
18
+ - 用户说「dwy 拉不到最新版 / dwy --version 是旧版」
19
+ - 用户说「换 dwy-shared 路径 / 重设 dwy-shared / dwy-shared 仓库挪了」
20
+
21
+ ## 工作流程 0:仓库位置配置(必走)
22
+
23
+ skill 与 dwy-shared 仓库深度绑定但**不硬编码路径**。每次执行流程 A/B 前先确保缓存就绪。
24
+
25
+ ### 缓存文件格式
26
+
27
+ 路径:`~/.config/dwy-shared/repo.json`
28
+
29
+ ```json
30
+ {
31
+ "repo_path": "/绝对/路径/到/dwy-shared",
32
+ "git_url": "https://gitee.com/snailyuanyuan/dwy-shared.git",
33
+ "configured_at": "2026-05-18T12:34:56Z"
34
+ }
35
+ ```
36
+
37
+ ### 读取与校验
38
+
39
+ ```bash
40
+ CONFIG="$HOME/.config/dwy-shared/repo.json"
41
+ if [ -f "$CONFIG" ]; then
42
+ REPO_PATH=$(jq -r .repo_path "$CONFIG" 2>/dev/null)
43
+ if [ -n "$REPO_PATH" ] && [ -d "$REPO_PATH/.git" ] && [ -d "$REPO_PATH/claude-cli/templates/claude-global" ]; then
44
+ echo "OK: $REPO_PATH"
45
+ else
46
+ echo "INVALID"
47
+ fi
48
+ else
49
+ echo "MISSING"
50
+ fi
51
+ ```
52
+
53
+ - `OK` → 继续后续流程
54
+ - `INVALID` 或 `MISSING` → 走「首次配置」
55
+
56
+ ### 首次配置流程
57
+
58
+ 1. 告知用户:「我需要知道 dwy-shared 仓库在你本机的 clone 位置」
59
+ 2. AskUserQuestion 问:「已经 clone 过 dwy-shared 仓库了吗?」
60
+ - **已 clone**:让用户告诉绝对路径(用 AskUserQuestion 的 Other 自由输入,或直接让用户回消息贴路径)
61
+ - **未 clone**:输出 git URL 让用户先 clone:
62
+
63
+ ```
64
+ git clone https://gitee.com/snailyuanyuan/dwy-shared.git <你想放的位置>
65
+ ```
66
+
67
+ 然后告诉路径
68
+ 3. 拿到路径后,**严格校验**:
69
+ - `test -d "$PATH"` 路径存在
70
+ - `test -d "$PATH/.git"` 是 git 仓库
71
+ - `git -C "$PATH" remote -v | grep -q dwy-shared` remote 指向 dwy-shared
72
+ - `test -d "$PATH/claude-cli/templates/claude-global"` 含特征目录
73
+ 4. 全部通过 → 写缓存:
74
+
75
+ ```bash
76
+ mkdir -p "$HOME/.config/dwy-shared"
77
+ cat > "$HOME/.config/dwy-shared/repo.json" <<EOF
78
+ {
79
+ "repo_path": "$PATH",
80
+ "git_url": "https://gitee.com/snailyuanyuan/dwy-shared.git",
81
+ "configured_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
82
+ }
83
+ EOF
84
+ ```
85
+
86
+ 5. 任一校验失败 → 列出失败项让用户重输
87
+
88
+ ### 修改路径流程
89
+
90
+ 用户说「换 dwy-shared 路径」「dwy-shared 挪了」「重设 dwy-shared」:
91
+
92
+ 1. 读现有 config,告诉用户「当前缓存路径是 X」
93
+ 2. 走首次配置流程,覆盖 config
94
+
95
+ ## 工作流程 A:往仓库加新模板
96
+
97
+ ### 步骤 1:拿仓库路径
98
+
99
+ 执行流程 0 的读取与校验。无效 → 先走首次配置,配完回到这里。
100
+
101
+ 不依赖 cwd —— 用户在 `/tmp` / `~/Desktop` 都行。后续命令统一用绝对路径,**不要 cd 到仓库**(避免污染用户 cwd)。
102
+
103
+ ### 步骤 2:识别类型
104
+
105
+ 根据用户给的源(绝对路径)判断属于哪类模板:
106
+
107
+ - **skill**:源是目录且含 `SKILL.md` 文件
108
+ - **rule**:源是 `.md` 文件,且 frontmatter 含 `paths:` 字段,或文件名形如 `dwy-<topic>-<aspect>.md`
109
+ - **hook**:源是 `.sh` 脚本,或文件名形如 `pre-*` / `post-*`
110
+ - **command**:源是 `.md` 文件,文件名是单词形式(如 `lint.md`、`deploy.md`),用作斜杠命令
111
+
112
+ 歧义时用 AskUserQuestion 让用户选 skill/rule/hook/command。
113
+
114
+ ### 步骤 3:读现有分类 + 准备推荐上下文
115
+
116
+ ```bash
117
+ TYPE_DIR="$REPO_PATH/claude-cli/templates/claude-global/<type>"
118
+ for cat in "$TYPE_DIR"/*/; do
119
+ echo "=== $(basename "$cat") ==="
120
+ # 读分类下第一个样例的 description 行(前 100 字)
121
+ first_sample=$(find "$cat" -name '*.md' | head -1)
122
+ test -n "$first_sample" && grep -m1 "^description" "$first_sample" | head -c 100
123
+ done
124
+ ```
125
+
126
+ 得到「现有分类 + 各分类的代表性描述」上下文。
127
+
128
+ ### 步骤 4:推荐归位
129
+
130
+ 读源文件的 description / 内容前 500 字,结合现有分类样例,推荐 1 个最匹配分类。
131
+
132
+ 如果新文件明显不属于任何现有分类(如全新技术栈),**主动建议新建分类**,提一个名字(中文优先,与现有风格一致:技术栈用英文 `Flutter`/`Vue`,业务域用中文 `运维发布`/`基础库`)。
133
+
134
+ ### 步骤 5:AskUserQuestion 确认分类
135
+
136
+ 选项:
137
+ - 推荐分类(标记「推荐」放第一位)
138
+ - 其他现有分类
139
+ - 「新建分类」(让用户在 Other 输入分类名)
140
+
141
+ ### 步骤 6:执行文件操作
142
+
143
+ ```bash
144
+ TARGET_DIR="$REPO_PATH/claude-cli/templates/claude-global/<type>/<category>"
145
+ mkdir -p "$TARGET_DIR"
146
+
147
+ # 源在仓库外:cp
148
+ cp -r <源> "$TARGET_DIR/<name>"
149
+
150
+ # 源在仓库内(如从其他分类移动):git mv
151
+ git -C "$REPO_PATH" mv <仓库内相对路径> <new-相对路径>
152
+ ```
153
+
154
+ **冲突处理**:目标已存在 → AskUserQuestion 让用户选「覆盖 / 改名 / 取消」。
155
+
156
+ ### 步骤 7:staged + 显示 diff
157
+
158
+ ```bash
159
+ git -C "$REPO_PATH" add <精准路径>
160
+ git -C "$REPO_PATH" status --short
161
+ git -C "$REPO_PATH" diff --cached --stat
162
+ ```
163
+
164
+ 把 status + diff stat 输出给用户看,让他看到将提交什么。
165
+
166
+ **禁止**用 `git add -A` 或 `git add .`,避免误捎带用户的在途工作。
167
+
168
+ ### 步骤 8:生成 commit message
169
+
170
+ 按项目 `CLAUDE.md` 中 Git Commit Scope 规范:
171
+ - scope: `cli`
172
+ - type: `feat`(新模板)/ `fix`(修复模板)/ `docs`(仅文档)
173
+ - subject: 中文动宾短语 ≤72 字符
174
+ - **禁** AI 署名 / Co-Authored-By trailer
175
+
176
+ 模板:
177
+ - `feat(cli): 新增 <name> skill 到 <分类> 分类`
178
+ - `feat(cli): 新增 <name> rule 到 <分类> 分类`
179
+ - `feat(cli): 新增 <分类>/<name> hook`
180
+
181
+ ### 步骤 9:AskUserQuestion 确认 commit + push
182
+
183
+ 选项:
184
+ - 「✓ 确认 commit + push」
185
+ - 「修改 commit message」(用户改完再 commit)
186
+ - 「✗ 取消」(保留 staged 状态让用户手动处理)
187
+
188
+ ### 步骤 10:执行
189
+
190
+ ```bash
191
+ git -C "$REPO_PATH" -c commit.gpgsign=false commit -m "$MSG"
192
+ git -C "$REPO_PATH" push origin master
193
+ ```
194
+
195
+ ### 步骤 11:完成提示
196
+
197
+ 告知用户:
198
+ - ✓ 已 push 到 Gitee
199
+ - 其他项目跑 `dwy claude sync` 即可拉到新模板(CLI 的 `ensureRepoCache` 会自动 `git pull`)
200
+ - 提醒:如果对方 dwy CLI <0.12.0,必须先升级 CLI 才能识别分类目录结构
201
+
202
+ ## 工作流程 B:dwy CLI 管理
203
+
204
+ 固定命令封装:
205
+
206
+ | 用户意图 | 命令 |
207
+ |---|---|
208
+ | 安装 / 升级到最新 | `npm i -g create-dwy@latest --registry=https://registry.npmjs.org/ --prefer-online` |
209
+ | 升级到指定版本 | `npm i -g create-dwy@<version> --registry=https://registry.npmjs.org/ --prefer-online` |
210
+ | 卸载 | `npm uninstall -g create-dwy` |
211
+ | 查本地版本 | `dwy --version` |
212
+ | 查 npm 上最新版 | `npm view create-dwy version` |
213
+ | 应急重装(cache 损坏) | `npm cache clean --force && npm i -g create-dwy@latest --registry=https://registry.npmjs.org/ --prefer-online` |
214
+
215
+ ### CDN 延迟应急
216
+
217
+ 如果用户刚 publish 完,跑 `npm view create-dwy version` 还显示旧版 / `npm i` 报 `ETARGET` —— 这是 npm CDN 传播延迟(30s ~ 几分钟),**不是失败**。
218
+
219
+ 直接打主 registry 旁路 CDN:
220
+
221
+ ```bash
222
+ curl -s https://registry.npmjs.org/create-dwy | jq '."dist-tags".latest'
223
+ ```
224
+
225
+ 如果显示是新版,等 30s 重试 `npm i` 或加 `--prefer-online --registry=https://registry.npmjs.org/`。
226
+
227
+ ## 错误处理速查
228
+
229
+ | 情况 | 处理 |
230
+ |---|---|
231
+ | 缓存路径不存在 | 走首次配置 |
232
+ | 缓存路径不是 git 仓库 / 不含特征目录 | 提示用户重设,走修改路径流程 |
233
+ | 源文件不存在 | 报错让用户确认路径 |
234
+ | 目标已存在同名文件/目录 | AskUserQuestion:覆盖 / 改名 / 取消 |
235
+ | 工作区有其他 uncommitted 改动 | 提示用户先 stash 或 commit 现有改动,避免误提(用 `git status --porcelain` 检测) |
236
+ | git push 鉴权失败 | 提示配 git credential helper / SSH key,告知不要在 skill 里存任何凭证 |
237
+ | git push 非快进失败 | 提示用户先 `git -C "$REPO_PATH" pull --rebase` 再重试 |
238
+ | dwy --version 报 command not found | 走流程 B 安装 |
239
+
240
+ ## 跨机器约束
241
+
242
+ skill 文件**零绝对路径**。换台机器装上后第一次用时自动走首次配置流程。
243
+
244
+ 需要用户机器满足:
245
+ 1. 装了 `git` / `jq` / `npm`(jq 用于解析缓存 JSON)
246
+ 2. clone 了 dwy-shared 仓库到任意位置(skill 引导)
247
+ 3. 对 Gitee origin 有 push 权限
248
+ 4. 缓存文件 `~/.config/dwy-shared/repo.json` 是机器本地配置,**不**随 skill 文件同步出去
249
+
250
+ ## 安全约束
251
+
252
+ - skill 不存任何 token / 凭证 / 私钥
253
+ - skill 不动 `~/.npmrc` / `~/.git-credentials`
254
+ - 仓库路径只存在用户机器本地 `~/.config/dwy-shared/repo.json`
255
+ - commit + push 前必须经 AskUserQuestion 确认,不静默推送