create-dwy 0.13.1 → 0.13.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 (66) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/bin/index.js +18 -0
  3. package/package.json +6 -2
  4. package/src/sync-all.js +154 -0
  5. package/src/sync-codex.js +27 -8
  6. package/src/sync.js +25 -8
  7. package/templates/claude-global/hooks/Git/pre-git-commit-backtick-check.sh +76 -0
  8. package/templates/claude-global/hooks/Git/pre-git-commit-sensitive-check.sh +23 -27
  9. package/templates/claude-global/rules/iOS/dwy-swift-style.md +96 -0
  10. package/templates/claude-global/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit-safe.md +66 -0
  11. package/templates/claude-global/settings.json +4 -0
  12. package/templates/claude-global/skills//345/205/203/345/267/245/345/205/267/dwy-claude-code-features/scripts/lint.py +0 -0
  13. package/templates/claude-global/skills//345/205/203/345/267/245/345/205/267/dwy-claude-code-features/scripts/refresh_docs.py +0 -0
  14. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/SKILL.md +225 -0
  15. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates/README.md +55 -0
  16. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//344/270/252/344/272/272/346/204/237/346/202/237.md +21 -0
  17. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//345/267/245/345/205/267/350/265/204/346/272/220/345/256/211/345/210/251.md +26 -0
  18. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//346/212/200/346/234/257/346/225/231/347/250/213.md +46 -0
  19. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//346/226/271/346/241/210/351/200/211/345/236/213/345/257/271/346/257/224.md +28 -0
  20. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//346/246/202/345/277/265/347/247/221/346/231/256.md +28 -0
  21. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//350/241/214/344/270/232/350/247/202/345/257/237/350/257/204/350/256/272.md +21 -0
  22. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//350/270/251/345/235/221/346/216/222/351/224/231/345/256/236/345/275/225.md +28 -0
  23. package/templates/claude-global/skills//345/206/205/345/256/271/345/210/233/344/275/234/dwy-article/templates//351/241/271/347/233/256/345/244/215/347/233/230.md +26 -0
  24. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/SKILL.md +4 -2
  25. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_comments.py +0 -0
  26. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_js_publish.sh +0 -0
  27. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_python_publish.sh +0 -0
  28. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/detect_sdk.sh +0 -0
  29. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/init_config.sh +0 -0
  30. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/SKILL.md +113 -0
  31. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-0to1/templates/STATE.md +56 -0
  32. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-market-analysis/SKILL.md +92 -0
  33. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/SKILL.md +72 -0
  34. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-coarse/templates/prd-coarse.md +61 -0
  35. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/SKILL.md +76 -0
  36. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prd-detailed/templates/prd-detailed.md +119 -0
  37. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-prototype/SKILL.md +104 -0
  38. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-spike/SKILL.md +81 -0
  39. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/SKILL.md +212 -0
  40. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-dev/templates/impl-plan.md +115 -0
  41. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-tech-feasibility/SKILL.md +105 -0
  42. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/SKILL.md +64 -0
  43. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-version-plan/templates/version-plan.md +72 -0
  44. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-cibuildwheel/SKILL.md +3 -1
  45. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_base.sh +0 -0
  46. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_capacity.sh +0 -0
  47. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_db.sh +0 -0
  48. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_docker.sh +0 -0
  49. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_env.sh +0 -0
  50. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_https.sh +0 -0
  51. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_logs.sh +0 -0
  52. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_nginx.sh +0 -0
  53. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_resilience.sh +0 -0
  54. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_secrets.sh +0 -0
  55. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_services.sh +0 -0
  56. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/check_ssh.sh +0 -0
  57. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/scripts/run_all.sh +0 -0
  58. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-docker/scripts/query_dockerhub.py +0 -0
  59. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-docker/scripts/scan_images.sh +0 -0
  60. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-mirror-source/scripts/apply_mirrors.py +0 -0
  61. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-mirror-source/scripts/check_mirrors.py +0 -0
  62. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-publish/SKILL.md +8 -0
  63. package/templates/codex-global/hooks/Git/pre-git-commit-backtick-check.sh +75 -0
  64. package/templates/codex-global/hooks/Git/pre-git-commit-sensitive-check.sh +1 -1
  65. package/templates/codex-global/hooks.json +5 -0
  66. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/scripts/__pycache__/scrape_docs.cpython-311.pyc +0 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # create-dwy
2
+
3
+ ## 0.13.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 新增顶层命令 `dwy sync`,一次选择后同时同步 Claude Code 与 Codex 配置。
8
+ - `dwy sync -i` 支持重新交互式选择;`dwy sync --dry-run` 支持预演;`dwy sync md` 同步 `CLAUDE.md` 到 `~/.claude/CLAUDE.md` 和 `~/.codex/AGENTS.md`。
9
+ - 复用现有 `dwy claude sync` / `dwy codex sync` 的扫描、缓存选择、删除未选模板项和写入逻辑;`commands` 仅同步到 `.claude/commands`,`skills` / `rules` / `hooks` 同步到两端。
10
+ - 新增 CLI 聚合同步回归测试,覆盖一次缓存选择同时写入 `.claude`、`.agents`、`.codex` 和 `AGENTS.md`。
package/bin/index.js CHANGED
@@ -7,6 +7,7 @@ import { program } from 'commander'
7
7
  import { createProject } from '../src/create.js'
8
8
  import { syncClaude } from '../src/sync.js'
9
9
  import { syncCodex } from '../src/sync-codex.js'
10
+ import { syncAll } from '../src/sync-all.js'
10
11
 
11
12
  const __dirname = dirname(fileURLToPath(import.meta.url))
12
13
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'))
@@ -18,6 +19,9 @@ program
18
19
  .addHelpText('after', `
19
20
  Examples:
20
21
  $ dwy create my-app 创建新项目
22
+ $ dwy sync 同步 Claude Code 与 Codex 配置(只选择一次)
23
+ $ dwy sync -i 重新交互式选择后同步到 Claude Code 与 Codex
24
+ $ dwy sync md 同步 CLAUDE.md 到 ~/.claude/ 与 ~/.codex/AGENTS.md
21
25
  $ dwy claude sync 同步 Claude 配置(基于已选覆盖)
22
26
  $ dwy claude sync -i 重新交互式选择
23
27
  $ dwy claude sync --dry-run 预演同步,不写入文件
@@ -37,6 +41,20 @@ Examples:
37
41
  $ dwy create my-app
38
42
  `)
39
43
 
44
+ program
45
+ .command('sync [target]')
46
+ .description('同步 Claude Code 与 Codex 配置(target=md 时同步 CLAUDE.md 到两个全局目录)')
47
+ .option('-i, --reselect', '忽略已选缓存,重新交互式选择(已选项默认勾选)')
48
+ .option('--dry-run', '预演模式,不实际写入文件')
49
+ .action((target, opts) => syncAll({ target, ...opts }))
50
+ .addHelpText('after', `
51
+ Examples:
52
+ $ dwy sync 基于已选覆盖同步到 Claude Code 与 Codex
53
+ $ dwy sync -i 重新交互式选择一次,同步到两端
54
+ $ dwy sync --dry-run 预演,不写入文件
55
+ $ dwy sync md 同步 CLAUDE.md 到 ~/.claude/ 与 ~/.codex/AGENTS.md
56
+ `)
57
+
40
58
  const claudeCmd = program
41
59
  .command('claude')
42
60
  .description('Claude Code 配置管理')
package/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "description": "项目脚手架与 Claude Code 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "dwy": "./bin/index.js"
8
8
  },
9
+ "scripts": {
10
+ "test": "node --test tests/*.test.js"
11
+ },
9
12
  "files": [
13
+ "CHANGELOG.md",
10
14
  "bin",
11
15
  "src",
12
16
  "templates"
@@ -32,4 +36,4 @@
32
36
  "registry": "https://registry.npmjs.org/",
33
37
  "access": "public"
34
38
  }
35
- }
39
+ }
@@ -0,0 +1,154 @@
1
+ import fs from 'fs-extra'
2
+ import path from 'path'
3
+ import inquirer from 'inquirer'
4
+ import { chalk } from './utils.js'
5
+ import {
6
+ interactiveSelect,
7
+ resolveSourceDir,
8
+ scanCommands,
9
+ scanExisting,
10
+ scanHooks,
11
+ scanRules,
12
+ scanSkills,
13
+ syncClaude,
14
+ } from './sync.js'
15
+ import {
16
+ parseManagedRuleNames,
17
+ scanExistingCodexHooks,
18
+ scanExistingCodexSkills,
19
+ syncCodex,
20
+ } from './sync-codex.js'
21
+
22
+ function unionSets(...sets) {
23
+ return new Set(sets.flatMap(set => [...set]))
24
+ }
25
+
26
+ function selectFromExisting(scans, existing) {
27
+ return {
28
+ skills: scans.skills.filter(item => existing.skills.has(item.name)),
29
+ rules: scans.rules.filter(item => existing.rules.has(item.name)),
30
+ commands: scans.commands.filter(item => existing.commands.has(item.name)),
31
+ hooks: existing.hooks.size > 0
32
+ ? scans.hooks.filter(item => existing.hooks.has(item.name))
33
+ : scans.hooks,
34
+ }
35
+ }
36
+
37
+ async function scanCombinedExisting(projectDir) {
38
+ const claudeDir = path.join(projectDir, '.claude')
39
+ const agentsMdPath = path.join(projectDir, 'AGENTS.md')
40
+ const agentsMdContent = await fs.pathExists(agentsMdPath) ? await fs.readFile(agentsMdPath, 'utf-8') : ''
41
+
42
+ return {
43
+ skills: unionSets(
44
+ await scanExisting(claudeDir, 'skills'),
45
+ await scanExistingCodexSkills(projectDir),
46
+ ),
47
+ rules: unionSets(
48
+ await scanExisting(claudeDir, 'rules'),
49
+ parseManagedRuleNames(agentsMdContent),
50
+ ),
51
+ commands: await scanExisting(claudeDir, 'commands'),
52
+ hooks: unionSets(
53
+ await scanExisting(claudeDir, 'hooks'),
54
+ await scanExistingCodexHooks(projectDir),
55
+ ),
56
+ }
57
+ }
58
+
59
+ async function shouldInitialize(projectDir, existing) {
60
+ const hasAnyExisting = existing.skills.size + existing.rules.size + existing.commands.size + existing.hooks.size > 0
61
+ if (hasAnyExisting) return true
62
+
63
+ const hasConfigDir = await fs.pathExists(path.join(projectDir, '.claude'))
64
+ || await fs.pathExists(path.join(projectDir, '.codex'))
65
+ || await fs.pathExists(path.join(projectDir, '.agents'))
66
+
67
+ if (hasConfigDir) return true
68
+
69
+ const { ok } = await inquirer.prompt([{
70
+ type: 'confirm',
71
+ name: 'ok',
72
+ message: `当前目录 ${chalk.yellow(projectDir)} 尚无 Claude Code/Codex 配置,是否在此初始化?`,
73
+ default: false,
74
+ }])
75
+ return ok
76
+ }
77
+
78
+ export async function syncAll({
79
+ target,
80
+ reselect = false,
81
+ dryRun = false,
82
+ sourceDir: sourceDirOverride,
83
+ projectDir: projectDirOverride,
84
+ } = {}) {
85
+ const sourceDir = sourceDirOverride || await resolveSourceDir()
86
+ if (!await fs.pathExists(sourceDir)) {
87
+ console.error(chalk.red('Error: claude-global templates not found in repo'))
88
+ process.exit(1)
89
+ }
90
+
91
+ if (target === 'md') {
92
+ await syncClaude({ target, dryRun, sourceDir })
93
+ await syncCodex({ target, dryRun, sourceDir })
94
+ return
95
+ }
96
+
97
+ if (target !== undefined) {
98
+ console.error(chalk.red(`Error: unknown sync target "${target}". Supported: (none) | md`))
99
+ process.exit(2)
100
+ }
101
+
102
+ const projectDir = projectDirOverride || process.cwd()
103
+
104
+ console.log(chalk.blue('\nScanning available Claude Code/Codex configuration...\n'))
105
+ const scans = {
106
+ skills: await scanSkills(sourceDir),
107
+ rules: await scanRules(sourceDir),
108
+ commands: await scanCommands(sourceDir),
109
+ hooks: await scanHooks(sourceDir),
110
+ }
111
+ console.log(chalk.yellow(`Found ${scans.skills.length} skills, ${scans.rules.length} rules, ${scans.commands.length} commands, ${scans.hooks.length} hooks\n`))
112
+
113
+ const existing = await scanCombinedExisting(projectDir)
114
+ if (!await shouldInitialize(projectDir, existing)) {
115
+ console.log(chalk.yellow('\n已取消同步。'))
116
+ return
117
+ }
118
+
119
+ const hasAnyExisting = existing.skills.size + existing.rules.size + existing.commands.size + existing.hooks.size > 0
120
+ let selected
121
+ if (!reselect && hasAnyExisting) {
122
+ selected = selectFromExisting(scans, existing)
123
+ console.log(chalk.gray('使用已选覆盖同步(如需调整请加 -i / --reselect):'))
124
+ console.log(chalk.gray(` ${selected.skills.length} skills, ${selected.rules.length} rules, ${selected.commands.length} commands, ${selected.hooks.length} hooks\n`))
125
+ } else {
126
+ if (!reselect && !hasAnyExisting) console.log(chalk.gray('首次同步,进入交互式选择...\n'))
127
+ const result = await interactiveSelect(scans, existing)
128
+ if (result === null) {
129
+ console.log(chalk.yellow('\n已取消同步。'))
130
+ return
131
+ }
132
+ selected = result
133
+ }
134
+
135
+ console.log(chalk.blue('\nSyncing Claude Code configuration...\n'))
136
+ await syncClaude({
137
+ sourceDir,
138
+ projectDir,
139
+ selected,
140
+ reselect,
141
+ dryRun,
142
+ skipInitPrompt: true,
143
+ })
144
+
145
+ console.log(chalk.blue('\nSyncing Codex configuration...\n'))
146
+ await syncCodex({
147
+ sourceDir,
148
+ projectDir,
149
+ selected,
150
+ reselect,
151
+ dryRun,
152
+ skipInitPrompt: true,
153
+ })
154
+ }
package/src/sync-codex.js CHANGED
@@ -54,7 +54,7 @@ async function ruleToSection(ruleItem) {
54
54
  return [marker, title, scopeLine, body].filter(Boolean).join('\n\n')
55
55
  }
56
56
 
57
- function parseManagedRuleNames(content) {
57
+ export function parseManagedRuleNames(content) {
58
58
  const block = content.match(BLOCK_RE)
59
59
  if (!block) return new Set()
60
60
  const names = new Set()
@@ -94,7 +94,7 @@ async function syncRulesToAgentsMd(selectedRules, proj, dryRun) {
94
94
 
95
95
  // ---------- skills → .agents/skills/ ----------
96
96
 
97
- async function scanExistingCodexSkills(proj) {
97
+ export async function scanExistingCodexSkills(proj) {
98
98
  const dir = path.join(proj, '.agents', 'skills')
99
99
  if (!await fs.pathExists(dir)) return new Set()
100
100
  const entries = await fs.readdir(dir, { withFileTypes: true })
@@ -129,7 +129,7 @@ async function removeUnselectedFlat(subdir, existingNames, selectedNames, templa
129
129
 
130
130
  // ---------- hooks → .codex/ ----------
131
131
 
132
- async function scanExistingCodexHooks(proj) {
132
+ export async function scanExistingCodexHooks(proj) {
133
133
  const dir = path.join(proj, '.codex', 'hooks')
134
134
  if (!await fs.pathExists(dir)) return new Set()
135
135
  const entries = await fs.readdir(dir)
@@ -258,8 +258,16 @@ async function interactiveSelect(scans, existing) {
258
258
  * target=undefined 同步到项目:rules→AGENTS.md / skills→.agents/skills / hooks→.codex
259
259
  * target='md' 仅同步 CLAUDE.md → 全局 ~/.codex/AGENTS.md
260
260
  */
261
- export async function syncCodex({ target, reselect = false, dryRun = false } = {}) {
262
- const claudeGlobal = await resolveSourceDir()
261
+ export async function syncCodex({
262
+ target,
263
+ reselect = false,
264
+ dryRun = false,
265
+ sourceDir: sourceDirOverride,
266
+ projectDir,
267
+ selected: selectedOverride,
268
+ skipInitPrompt = false,
269
+ } = {}) {
270
+ const claudeGlobal = sourceDirOverride || await resolveSourceDir()
263
271
  if (!await fs.pathExists(claudeGlobal)) {
264
272
  console.error(chalk.red('Error: claude-global templates not found in repo'))
265
273
  process.exit(1)
@@ -289,7 +297,7 @@ export async function syncCodex({ target, reselect = false, dryRun = false } = {
289
297
 
290
298
  const codexGlobal = path.join(claudeGlobal, '..', 'codex-global')
291
299
  const hooksJsonSrc = path.join(codexGlobal, 'hooks.json')
292
- const proj = process.cwd()
300
+ const proj = projectDir || process.cwd()
293
301
 
294
302
  console.log(chalk.blue('\nScanning available config for Codex...\n'))
295
303
 
@@ -309,7 +317,11 @@ export async function syncCodex({ target, reselect = false, dryRun = false } = {
309
317
  }
310
318
  const hasAnyExisting = existing.skills.size + existing.rules.size + existing.hooks.size > 0
311
319
 
312
- if (!hasAnyExisting && !await fs.pathExists(path.join(proj, '.codex')) && !await fs.pathExists(path.join(proj, '.agents'))) {
320
+ if (!selectedOverride
321
+ && !skipInitPrompt
322
+ && !hasAnyExisting
323
+ && !await fs.pathExists(path.join(proj, '.codex'))
324
+ && !await fs.pathExists(path.join(proj, '.agents'))) {
313
325
  const { ok } = await inquirer.prompt([{
314
326
  type: 'confirm',
315
327
  name: 'ok',
@@ -323,7 +335,14 @@ export async function syncCodex({ target, reselect = false, dryRun = false } = {
323
335
  }
324
336
 
325
337
  let selected
326
- if (!reselect && hasAnyExisting) {
338
+ if (selectedOverride) {
339
+ const selectedNames = key => new Set((selectedOverride[key] || []).map(item => item.name))
340
+ selected = {
341
+ skills: scans.skills.filter(item => selectedNames('skills').has(item.name)),
342
+ rules: scans.rules.filter(item => selectedNames('rules').has(item.name)),
343
+ hooks: scans.hooks.filter(item => selectedNames('hooks').has(item.name)),
344
+ }
345
+ } else if (!reselect && hasAnyExisting) {
327
346
  selected = {
328
347
  skills: scans.skills.filter(s => existing.skills.has(s.name)),
329
348
  rules: scans.rules.filter(r => existing.rules.has(r.name)),
package/src/sync.js CHANGED
@@ -103,7 +103,7 @@ export async function scanRules(sourceDir) {
103
103
  return rules.sort((a, b) => a.name.localeCompare(b.name))
104
104
  }
105
105
 
106
- async function scanCommands(sourceDir) {
106
+ export async function scanCommands(sourceDir) {
107
107
  const commandsDir = path.join(sourceDir, 'commands')
108
108
  if (!await fs.pathExists(commandsDir)) return []
109
109
 
@@ -163,7 +163,7 @@ async function promptSelection(items, label, defaultNames) {
163
163
  * 交互式选择:每步可回退到上一步,最后总览支持任意类别重选。
164
164
  * 返回 { skills, rules, commands, hooks } 或 null(取消)。
165
165
  */
166
- async function interactiveSelect(scans, existing) {
166
+ export async function interactiveSelect(scans, existing) {
167
167
  const sel = {}
168
168
 
169
169
  // 第一轮:顺序遍历,每步给导航
@@ -350,8 +350,16 @@ export async function resolveSourceDir() {
350
350
  * target=undefined 同步项目 .claude/(不动全局 CLAUDE.md)
351
351
  * target='md' 仅同步 CLAUDE.md → 全局 ~/.claude/
352
352
  */
353
- export async function syncClaude({ target, reselect = false, dryRun = false } = {}) {
354
- const sourceDir = await resolveSourceDir()
353
+ export async function syncClaude({
354
+ target,
355
+ reselect = false,
356
+ dryRun = false,
357
+ sourceDir: sourceDirOverride,
358
+ projectDir,
359
+ selected: selectedOverride,
360
+ skipInitPrompt = false,
361
+ } = {}) {
362
+ const sourceDir = sourceDirOverride || await resolveSourceDir()
355
363
  if (!await fs.pathExists(sourceDir)) {
356
364
  console.error(chalk.red('Error: claude-global templates not found in repo'))
357
365
  process.exit(1)
@@ -373,13 +381,14 @@ export async function syncClaude({ target, reselect = false, dryRun = false } =
373
381
  }
374
382
 
375
383
  console.log(chalk.blue('\nScanning available Claude configuration...\n'))
376
- const projectTargetDir = path.join(process.cwd(), '.claude')
384
+ const projectRoot = projectDir || process.cwd()
385
+ const projectTargetDir = path.join(projectRoot, '.claude')
377
386
 
378
- if (!await fs.pathExists(projectTargetDir)) {
387
+ if (!selectedOverride && !skipInitPrompt && !await fs.pathExists(projectTargetDir)) {
379
388
  const { shouldCreate } = await inquirer.prompt([{
380
389
  type: 'confirm',
381
390
  name: 'shouldCreate',
382
- message: `当前目录 ${chalk.yellow(process.cwd())} 未找到 .claude 目录,是否在此创建?`,
391
+ message: `当前目录 ${chalk.yellow(projectRoot)} 未找到 .claude 目录,是否在此创建?`,
383
392
  default: false,
384
393
  }])
385
394
  if (!shouldCreate) {
@@ -406,7 +415,15 @@ export async function syncClaude({ target, reselect = false, dryRun = false } =
406
415
 
407
416
  let selected
408
417
 
409
- if (!reselect && hasAnyExisting) {
418
+ if (selectedOverride) {
419
+ const selectedNames = key => new Set((selectedOverride[key] || []).map(item => item.name))
420
+ selected = {
421
+ skills: scans.skills.filter(item => selectedNames('skills').has(item.name)),
422
+ rules: scans.rules.filter(item => selectedNames('rules').has(item.name)),
423
+ commands: scans.commands.filter(item => selectedNames('commands').has(item.name)),
424
+ hooks: scans.hooks.filter(item => selectedNames('hooks').has(item.name)),
425
+ }
426
+ } else if (!reselect && hasAnyExisting) {
410
427
  // 缓存模式:模板 ∩ 已选
411
428
  selected = {
412
429
  skills: scans.skills.filter(s => existing.skills.has(s.name)),
@@ -0,0 +1,76 @@
1
+ #!/bin/bash
2
+ # Claude Code PreToolUse Hook
3
+ # 拦截 git commit message 中会被 shell 命令替换的反引号 / $(...)
4
+ #
5
+ # 背景:AI 生成的 commit message 爱用 Markdown 风格反引号包代码标识符,
6
+ # git commit -m "fix: support `bun install` on windows"
7
+ # 经 shell 执行时,双引号内的 `bun install` 会被当成命令替换 $(bun install) 实际执行,
8
+ # 输出塞进 message,甚至误执行破坏性命令导致丢代码。
9
+ # 单引号内 '...`...`...' 不展开,安全。
10
+ # 安全写法:git commit -F <file> / --file=- / 单引号。
11
+
12
+ # 从 stdin 读 JSON 取待执行命令(Claude Code 与 Codex 同样通过 stdin 传 .tool_input.command)
13
+ CMD=$(python3 -c 'import sys,json; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))')
14
+
15
+ # 只在含 git commit 的命令上检查
16
+ printf '%s' "$CMD" | grep -q 'git commit' || exit 0
17
+
18
+ # 逐字符做轻量 shell 词法扫描:跟踪引号状态,
19
+ # 只在「非单引号上下文」里出现裸反引号或 $( 时判危险(这些会被 shell 求值)。
20
+ # 命令经环境变量传入(stdin 已被 heredoc 脚本占用,不能再走管道)。
21
+ DANGER=$(CMD="$CMD" python3 <<'PY'
22
+ import os
23
+
24
+ s = os.environ.get("CMD", "")
25
+ in_single = False # 单引号内:所有字符字面,反引号安全
26
+ in_double = False # 双引号内:反引号 / $() 仍会命令替换
27
+ i, n = 0, len(s)
28
+ danger = False
29
+
30
+ while i < n:
31
+ c = s[i]
32
+ if c == '\\' and not in_single:
33
+ # 反斜杠转义下一个字符(单引号内反斜杠不转义)
34
+ i += 2
35
+ continue
36
+ if c == "'" and not in_double:
37
+ in_single = not in_single
38
+ i += 1
39
+ continue
40
+ if c == '"' and not in_single:
41
+ in_double = not in_double
42
+ i += 1
43
+ continue
44
+ if not in_single:
45
+ # 非单引号上下文(裸或双引号内):反引号 / $( 会被求值
46
+ if c == '`':
47
+ danger = True
48
+ break
49
+ if c == '$' and i + 1 < n and s[i + 1] == '(':
50
+ danger = True
51
+ break
52
+ i += 1
53
+
54
+ print("DANGER" if danger else "OK")
55
+ PY
56
+ )
57
+
58
+ [ "$DANGER" = "DANGER" ] || exit 0
59
+
60
+ echo "========================================"
61
+ echo " WARNING: 危险的 commit message 写法"
62
+ echo "========================================"
63
+ echo ""
64
+ echo "检测到 git commit message 中含会被 shell 命令替换的反引号 \` 或 \$(...)。"
65
+ echo "双引号内的 \`...\` 会被当成命令替换实际执行,可能误执行命令甚至丢代码。"
66
+ echo ""
67
+ echo "改用以下任一安全写法:"
68
+ echo " 1. 文件提交(推荐多行 / 含特殊字符):"
69
+ echo " git commit -F <msg-file>"
70
+ echo " printf '%s\\n' \"\$MSG\" | git commit --file=-"
71
+ echo " 2. 单引号包裹(单引号内反引号不展开):"
72
+ echo " git commit -m 'fix: support \`bun install\` on windows'"
73
+ echo " 3. 转义反引号:"
74
+ echo " git commit -m \"fix: support \\\`bun install\\\` on windows\""
75
+ echo "========================================"
76
+ exit 2 # 暂停等待人工审批
@@ -2,40 +2,36 @@
2
2
  # Claude Code PreToolUse Hook
3
3
  # 在 git commit 前检查暂存文件中是否包含敏感数据
4
4
 
5
+ # 从 stdin 读 JSON 并取出待执行命令(Claude Code 与 Codex 同样通过 stdin 传 .tool_input.command)
6
+ CMD=$(python3 -c 'import sys,json; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))')
7
+
5
8
  # 只拦截 git commit 命令
6
- echo "$TOOL_INPUT" | grep -q 'git commit' || exit 0
9
+ printf '%s' "$CMD" | grep -q 'git commit' || exit 0
7
10
 
8
11
  # 获取暂存文件列表
9
12
  STAGED=$(git diff --cached --diff-filter=ACMR --name-only 2>/dev/null)
10
13
  [ -z "$STAGED" ] && exit 0
11
14
 
12
- # 敏感数据匹配模式
15
+ # 敏感数据匹配模式(赋值/上下文形式,避免类名/变量名裸词误报)
13
16
  PATTERNS=(
14
- # 密码/密钥
15
- 'password'
16
- 'passwd'
17
- 'secret'
18
- 'api[_-]?key'
19
- 'access[_-]?token'
20
- 'private[_-]?key'
21
- 'auth[_-]?token'
22
- 'client[_-]?secret'
23
- 'bearer'
24
-
25
- # 数据库连接串
26
- 'jdbc:'
27
- 'mysql://'
28
- 'postgres://'
29
- 'mongodb://'
30
- 'redis://'
31
-
32
- # 私钥文件
33
- '-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY'
34
-
35
- # 平台 Token
36
- 'sk-[a-zA-Z0-9]{20,}' # OpenAI
37
- 'ghp_[a-zA-Z0-9]{36}' # GitHub
38
- 'AKIA[0-9A-Z]{16}' # AWS
17
+ # 口令/密钥/令牌赋值(词 + :或= + 引号值;PairingSecret / clientSecret 等标识符不命中)
18
+ "(password|passwd)[[:space:]]*[:=][[:space:]]*[\"'][^\"']{4,}"
19
+ "secret[[:space:]]*[:=][[:space:]]*[\"'][^\"']{8,}"
20
+ "api[_-]?key[[:space:]]*[:=][[:space:]]*[\"'][^\"']{8,}"
21
+ "(access|auth)[_-]?token[[:space:]]*[:=][[:space:]]*[\"'][^\"']{8,}"
22
+ "client[_-]?secret[[:space:]]*[:=][[:space:]]*[\"'][^\"']{8,}"
23
+ "[Bb]earer[[:space:]]+[a-zA-Z0-9._-]{20,}"
24
+
25
+ # 含凭证的连接串(scheme://user:pass@host)
26
+ "://[a-zA-Z0-9_.-]+:[^@[:space:]/]{3,}@"
27
+
28
+ # 私钥文件块
29
+ "-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY"
30
+
31
+ # 平台 Token(格式高特异,裸匹配)
32
+ "sk-[a-zA-Z0-9]{20,}" # OpenAI
33
+ "ghp_[a-zA-Z0-9]{36}" # GitHub
34
+ "AKIA[0-9A-Z]{16}" # AWS
39
35
  )
40
36
 
41
37
  # 拼接为正则
@@ -0,0 +1,96 @@
1
+ ---
2
+ paths:
3
+ - "**/*.swift"
4
+ ---
5
+
6
+ # Swift 代码规范
7
+
8
+ 编辑任何 `.swift` 时遵守。本规则仅覆盖代码写法层(缩进、命名、注释、控制流、UI token 化),是与具体业务无关的通用约定。
9
+
10
+ ## 1. 缩进与格式
11
+
12
+ - **4 空格缩进**,不用 Tab。
13
+ - 类型 / 函数声明参数过长时换行,每参数一行,右括号与返回类型 `) -> X` 单起一行。
14
+ - 一行只做一件事,避免把多个 `if` 挤进一行。
15
+
16
+ ## 2. 命名
17
+
18
+ - 类型 `UpperCamelCase`,属性 / 方法 / 局部变量 `lowerCamelCase`。
19
+ - **局部短生命周期变量可用短名**(`i` = index、`f` = formatter、`cal` = calendar),同一文件内保持一致。
20
+ - 名字表达意图,避免无信息量的 `data` / `info` / `tmp` 当主力变量名。
21
+
22
+ ## 3. 类型声明(iOS 17)
23
+
24
+ 状态对象统一 iOS 17 写法,不要 `ObservableObject / @Published / @StateObject`:
25
+
26
+ ```swift
27
+ @MainActor // 所有触碰 UI / 主线程隔离状态的类型都标
28
+ @Observable // 替代 ObservableObject
29
+ final class ListViewModel { // 非继承类一律 final
30
+ private let store: DataStore // 依赖私有 + 构造注入
31
+ }
32
+ ```
33
+
34
+ ## 4. 注释(中文,强制全量)
35
+
36
+ **所有代码都要加注释,包括每个类型和每个方法**。注释说明**意图**(为什么这么做、约束是什么),而非复述代码字面。
37
+
38
+ - **类型顶部必加** `///` 文档注释:说明该类型的职责、它在数据流里的位置或关键约束。`class` / `struct` / `enum` / `protocol` / 有逻辑的 `extension` 都算。
39
+ - **每个方法 / 函数必加** `///` 文档注释(公开和私有都要),说明它做什么、入参出参的语义、可能抛出的错误。
40
+ - 关键属性、反直觉分支、易踩坑处用行内 `//` 标注触发条件。
41
+ - 注释里**禁止 emoji**。
42
+ - 不写废话注释(`// 设置 name` 这种复述赋值的删掉),注释要补充代码没说出口的信息。
43
+
44
+ ```swift
45
+ /// 列表数据源。负责从持久层读取并按时间分组,供视图直接渲染。
46
+ @MainActor
47
+ @Observable
48
+ final class ListViewModel {
49
+
50
+ /// 重新加载全部数据并写回 `grouped`。
51
+ /// 失败时透传持久层错误,由调用方决定提示策略。
52
+ func reload() throws {
53
+ let items = try store.all() // 持久层已保证非 nil,无需兜底
54
+ grouped = group(items)
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## 5. 控制流:信任上游,不过度防御
60
+
61
+ 只在系统外边界(用户输入、文件 / 网络读入、外部 API)校验;内部调用信任类型签名。
62
+
63
+ ```swift
64
+ // 好:early return,无多余 else
65
+ guard !isBusy else { return }
66
+ withAnimation(.spring(response: 0.5, dampingFraction: 0.75)) { isBusy = true }
67
+ ```
68
+
69
+ - 不写 `value ?? default` 兜已有明确来源的值。
70
+ - 不写 `try? { ... }` 吞异常掩盖 bug;只 catch 预期的具体错误,其余让它抛。
71
+ - 持久 / IO 层方法用 `throws` 透传错误,由调用方决定处理,不在底层吞。
72
+
73
+ ## 6. 图标:矢量图,禁止 emoji
74
+
75
+ UI 图标**一律 SF Symbols 矢量图**(`Image(systemName:)`),任何视图、文案、注释都**禁止 emoji**:
76
+
77
+ ```swift
78
+ // 好:SF Symbols 矢量图标
79
+ Image(systemName: "checkmark")
80
+ .font(.system(size: 11, weight: .bold))
81
+ .foregroundStyle(.white)
82
+
83
+ // 禁止:emoji 当图标
84
+ Text("✅") // 不允许
85
+ ```
86
+
87
+ ## 7. 颜色 / 字体 / 动效 token 化
88
+
89
+ - 颜色集中到设计 token(如 `ColorTokens.*` / `theme.*`),**禁止在视图里散落硬编码颜色字面量**。
90
+ - 字体用 `.font(.system(size:weight:))` 或集中的字体 token。
91
+ - 动效统一用一组 `.spring(response:dampingFraction:)` 参数,不用 `.easeInOut`,保持全局手感一致。
92
+
93
+ ## 8. 文案与时间
94
+
95
+ - 所有 UI 文案走 `String(localized: "some_key")`,**禁止硬编码字面量**;key 命名风格全项目统一。
96
+ - 业务逻辑里「现在」作为依赖注入(`now: @escaping () -> Date = { Date() }`),方便单测固定时间,禁止在逻辑里直接散用 `Date()`。