create-dwy 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/package.json +3 -2
  2. package/src/prompts/grouped-checkbox.js +223 -0
  3. package/src/sync.js +61 -39
  4. package/templates/claude-global/hooks/pre-git-commit-sensitive-check.sh +0 -0
  5. package/templates/claude-global/rules/{dwy-python-backend.md → Python/dwy-python-backend.md} +43 -3
  6. package/templates/claude-global/rules/{dwy-python-core.md → Python/dwy-python-core.md} +35 -2
  7. package/templates/claude-global/skills//345/237/272/347/241/200/345/272/223/dwy-eapi/SKILL.md +226 -0
  8. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/SKILL.md +144 -0
  9. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/create-database-table.md +219 -0
  10. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/crud-operations.md +281 -0
  11. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/data-import.md +271 -0
  12. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/docker-deployment.md +63 -0
  13. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/performance-tuning.md +223 -0
  14. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/pitfalls-and-best-practices.md +352 -0
  15. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/python-api.md +306 -0
  16. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/setup-and-connection.md +143 -0
  17. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/sources.md +46 -0
  18. package/templates/claude-global/skills//346/225/260/346/215/256/345/272/223/dwy-dolphindb/references/sql-batch-compute.md +206 -0
  19. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-cibuildwheel/SKILL.md +354 -0
  20. package/templates/claude-global/skills//350/277/220/347/273/264/345/217/221/345/270/203/dwy-cibuildwheel/references/release-workflow-template.yml +99 -0
  21. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_base.sh +0 -0
  22. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_capacity.sh +0 -0
  23. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_db.sh +0 -0
  24. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_docker.sh +0 -0
  25. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_env.sh +0 -0
  26. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_https.sh +0 -0
  27. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_logs.sh +0 -0
  28. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_nginx.sh +0 -0
  29. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_resilience.sh +0 -0
  30. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_secrets.sh +0 -0
  31. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_services.sh +0 -0
  32. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/check_ssh.sh +0 -0
  33. package/templates/claude-global/skills/{dwy-deploy-audit → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit}/scripts/run_all.sh +0 -0
  34. package/templates/claude-global/skills/{dwy-docker → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-docker}/scripts/query_dockerhub.py +0 -0
  35. package/templates/claude-global/skills/{dwy-docker → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-docker}/scripts/scan_images.sh +0 -0
  36. package/templates/claude-global/skills/{dwy-mirror-source → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-mirror-source}/scripts/apply_mirrors.py +0 -0
  37. package/templates/claude-global/skills/{dwy-mirror-source → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-mirror-source}/scripts/check_mirrors.py +0 -0
  38. package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +0 -98
  39. package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +0 -67
  40. package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +0 -441
  41. package/templates/claude-global/skills/dwy-eapi/SKILL.md +0 -495
  42. package/templates/claude-global/skills/dwy-eapi/references/health.md +0 -47
  43. package/templates/claude-global/skills/dwy-eapi/references/logger.md +0 -60
  44. /package/templates/claude-global/rules/{dwy-android-core.md → Android/dwy-android-core.md} +0 -0
  45. /package/templates/claude-global/rules/{dwy-flutter-core.md → Flutter/dwy-flutter-core.md} +0 -0
  46. /package/templates/claude-global/rules/{dwy-flutter-network.md → Flutter/dwy-flutter-network.md} +0 -0
  47. /package/templates/claude-global/rules/{dwy-flutter-router.md → Flutter/dwy-flutter-router.md} +0 -0
  48. /package/templates/claude-global/rules/{dwy-flutter-testing.md → Flutter/dwy-flutter-testing.md} +0 -0
  49. /package/templates/claude-global/rules/{dwy-python-testing.md → Python/dwy-python-testing.md} +0 -0
  50. /package/templates/claude-global/rules/{dwy-vue-core.md → Vue/dwy-vue-core.md} +0 -0
  51. /package/templates/claude-global/rules/{dwy-vue-pinia.md → Vue/dwy-vue-pinia.md} +0 -0
  52. /package/templates/claude-global/rules/{dwy-vue-testing.md → Vue/dwy-vue-testing.md} +0 -0
  53. /package/templates/claude-global/rules/{dwy-tdd-development.md → /345/274/200/345/217/221/346/265/201/347/250/213/dwy-tdd-development.md"} +0 -0
  54. /package/templates/claude-global/rules/{dwy-db-migration.md → /346/225/260/346/215/256/345/272/223/dwy-db-migration.md"} +0 -0
  55. /package/templates/claude-global/rules/{dwy-postgres.md → /346/225/260/346/215/256/345/272/223/dwy-postgres.md"} +0 -0
  56. /package/templates/claude-global/rules/{dwy-redis.md → /346/225/260/346/215/256/345/272/223/dwy-redis.md"} +0 -0
  57. /package/templates/claude-global/skills/{dwy-eapi/references/tasks-integration-guide.md → /345/237/272/347/241/200/345/272/223/dwy-eapi/references/tasks-integration-guide.md"} +0 -0
  58. /package/templates/claude-global/skills/{dwy-ekit/SKILL.md → /345/237/272/347/241/200/345/272/223/dwy-ekit/SKILL.md"} +0 -0
  59. /package/templates/claude-global/skills/{dwy-eui/SKILL.md → /345/237/272/347/241/200/345/272/223/dwy-eui/SKILL.md"} +0 -0
  60. /package/templates/claude-global/skills/{dwy-eui/references/eui-design-guide.md → /345/237/272/347/241/200/345/272/223/dwy-eui/references/eui-design-guide.md"} +0 -0
  61. /package/templates/claude-global/skills/{dwy-eui/references/eui-integration-guide.md → /345/237/272/347/241/200/345/272/223/dwy-eui/references/eui-integration-guide.md"} +0 -0
  62. /package/templates/claude-global/skills/{dwy-eui/references/eui-landing-design-guide.md → /345/237/272/347/241/200/345/272/223/dwy-eui/references/eui-landing-design-guide.md"} +0 -0
  63. /package/templates/claude-global/skills/{dwy-pentest/SKILL.md → /345/256/211/345/205/250/dwy-pentest/SKILL.md"} +0 -0
  64. /package/templates/claude-global/skills/{dwy-pentest/scripts/.python-version → /345/256/211/345/205/250/dwy-pentest/scripts/.python-version"} +0 -0
  65. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/__init__.py → /345/256/211/345/205/250/dwy-pentest/scripts/lib/__init__.py"} +0 -0
  66. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc → /345/256/211/345/205/250/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc"} +0 -0
  67. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc → /345/256/211/345/205/250/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc"} +0 -0
  68. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc → /345/256/211/345/205/250/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc"} +0 -0
  69. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc → /345/256/211/345/205/250/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc"} +0 -0
  70. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc → /345/256/211/345/205/250/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc"} +0 -0
  71. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/config.py → /345/256/211/345/205/250/dwy-pentest/scripts/lib/config.py"} +0 -0
  72. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/executor.py → /345/256/211/345/205/250/dwy-pentest/scripts/lib/executor.py"} +0 -0
  73. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/severity.py → /345/256/211/345/205/250/dwy-pentest/scripts/lib/severity.py"} +0 -0
  74. /package/templates/claude-global/skills/{dwy-pentest/scripts/lib/validator.py → /345/256/211/345/205/250/dwy-pentest/scripts/lib/validator.py"} +0 -0
  75. /package/templates/claude-global/skills/{dwy-pentest/scripts/pentest_auth.py → /345/256/211/345/205/250/dwy-pentest/scripts/pentest_auth.py"} +0 -0
  76. /package/templates/claude-global/skills/{dwy-pentest/scripts/pentest_recon.py → /345/256/211/345/205/250/dwy-pentest/scripts/pentest_recon.py"} +0 -0
  77. /package/templates/claude-global/skills/{dwy-pentest/scripts/pentest_report.py → /345/256/211/345/205/250/dwy-pentest/scripts/pentest_report.py"} +0 -0
  78. /package/templates/claude-global/skills/{dwy-pentest/scripts/pentest_scan.py → /345/256/211/345/205/250/dwy-pentest/scripts/pentest_scan.py"} +0 -0
  79. /package/templates/claude-global/skills/{dwy-pentest/scripts/pentest_ssl.py → /345/256/211/345/205/250/dwy-pentest/scripts/pentest_ssl.py"} +0 -0
  80. /package/templates/claude-global/skills/{dwy-pentest/scripts/pentest_vuln.py → /345/256/211/345/205/250/dwy-pentest/scripts/pentest_vuln.py"} +0 -0
  81. /package/templates/claude-global/skills/{dwy-pentest/scripts/pentest_web.py → /345/256/211/345/205/250/dwy-pentest/scripts/pentest_web.py"} +0 -0
  82. /package/templates/claude-global/skills/{dwy-pentest/scripts/pyproject.toml → /345/256/211/345/205/250/dwy-pentest/scripts/pyproject.toml"} +0 -0
  83. /package/templates/claude-global/skills/{dwy-pentest/scripts/setup_tools.sh → /345/256/211/345/205/250/dwy-pentest/scripts/setup_tools.sh"} +0 -0
  84. /package/templates/claude-global/skills/{dwy-pentest/scripts/uv.lock → /345/256/211/345/205/250/dwy-pentest/scripts/uv.lock"} +0 -0
  85. /package/templates/claude-global/skills/{dwy-dev-qa/SKILL.md → /345/274/200/345/217/221/346/265/201/347/250/213/dwy-dev-qa/SKILL.md"} +0 -0
  86. /package/templates/claude-global/skills/{dwy-git-commit/SKILL.md → /345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit/SKILL.md"} +0 -0
  87. /package/templates/claude-global/skills/{dwy-fullstack-scaffold/SKILL.md → /350/204/232/346/211/213/346/236/266/dwy-fullstack-scaffold/SKILL.md"} +0 -0
  88. /package/templates/claude-global/skills/{dwy-tech-stack/SKILL.md → /350/204/232/346/211/213/346/236/266/dwy-tech-stack/SKILL.md"} +0 -0
  89. /package/templates/claude-global/skills/{dwy-deploy-audit/SKILL.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/SKILL.md"} +0 -0
  90. /package/templates/claude-global/skills/{dwy-deploy-audit/references/checks-data.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/references/checks-data.md"} +0 -0
  91. /package/templates/claude-global/skills/{dwy-deploy-audit/references/checks-network.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/references/checks-network.md"} +0 -0
  92. /package/templates/claude-global/skills/{dwy-deploy-audit/references/checks-runtime.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-deploy-audit/references/checks-runtime.md"} +0 -0
  93. /package/templates/claude-global/skills/{dwy-docker/SKILL.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-docker/SKILL.md"} +0 -0
  94. /package/templates/claude-global/skills/{dwy-docker/references/ask-templates.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-docker/references/ask-templates.md"} +0 -0
  95. /package/templates/claude-global/skills/{dwy-docker/references/templates.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-docker/references/templates.md"} +0 -0
  96. /package/templates/claude-global/skills/{dwy-docker/references/version-rules.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-docker/references/version-rules.md"} +0 -0
  97. /package/templates/claude-global/skills/{dwy-mirror-source/SKILL.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-mirror-source/SKILL.md"} +0 -0
  98. /package/templates/claude-global/skills/{dwy-mirror-source/references/dockerfile-snippets.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-mirror-source/references/dockerfile-snippets.md"} +0 -0
  99. /package/templates/claude-global/skills/{dwy-mirror-source/references/mirror-providers.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-mirror-source/references/mirror-providers.md"} +0 -0
  100. /package/templates/claude-global/skills/{dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc"} +0 -0
  101. /package/templates/claude-global/skills/{dwy-publish/SKILL.md → /350/277/220/347/273/264/345/217/221/345/270/203/dwy-publish/SKILL.md"} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "项目脚手架与 Claude Code 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "license": "MIT",
21
21
  "author": "dwydev",
22
22
  "dependencies": {
23
+ "@inquirer/core": "^10.0.0",
23
24
  "commander": "^12.0.0",
24
25
  "inquirer": "^9.0.0",
25
26
  "handlebars": "^4.7.0",
@@ -31,4 +32,4 @@
31
32
  "registry": "https://registry.npmjs.org/",
32
33
  "access": "public"
33
34
  }
34
- }
35
+ }
@@ -0,0 +1,223 @@
1
+ import {
2
+ createPrompt,
3
+ useState,
4
+ useMemo,
5
+ useKeypress,
6
+ usePrefix,
7
+ usePagination,
8
+ isUpKey,
9
+ isDownKey,
10
+ isSpaceKey,
11
+ isBackspaceKey,
12
+ isEnterKey,
13
+ makeTheme,
14
+ } from '@inquirer/core'
15
+ import chalk from 'chalk'
16
+
17
+ const OTHER = '其他'
18
+
19
+ function sortCategories(cats) {
20
+ const others = cats.includes(OTHER) ? [OTHER] : []
21
+ const rest = cats.filter(c => c !== OTHER).sort((a, b) => a.localeCompare(b, 'zh'))
22
+ return [...rest, ...others]
23
+ }
24
+
25
+ function buildVisibleRows(items, filter, selected) {
26
+ const kw = filter.toLowerCase()
27
+ const filtered = kw
28
+ ? items.filter(it => it.name.toLowerCase().includes(kw)
29
+ || (it.description || '').toLowerCase().includes(kw))
30
+ : items
31
+ const groups = new Map()
32
+ for (const it of filtered) {
33
+ const cat = it.category || OTHER
34
+ if (!groups.has(cat)) groups.set(cat, [])
35
+ groups.get(cat).push(it)
36
+ }
37
+ const rows = []
38
+ for (const cat of sortCategories([...groups.keys()])) {
39
+ const groupItems = groups.get(cat).sort((a, b) => a.name.localeCompare(b.name))
40
+ const selectedInGroup = groupItems.filter(it => selected.has(it.name)).length
41
+ rows.push({ type: 'header', category: cat, total: groupItems.length, selectedInGroup, items: groupItems })
42
+ for (const it of groupItems) rows.push({ type: 'item', item: it, category: cat })
43
+ }
44
+ return rows
45
+ }
46
+
47
+ function isPrintable(key) {
48
+ return typeof key.sequence === 'string'
49
+ && key.sequence.length === 1
50
+ && key.sequence >= ' '
51
+ && key.sequence <= '~'
52
+ && !key.ctrl
53
+ && !key.meta
54
+ && key.name !== 'space'
55
+ && key.name !== 'return'
56
+ && key.name !== 'enter'
57
+ }
58
+
59
+ function isEscapeKey(key) {
60
+ return key.name === 'escape'
61
+ }
62
+
63
+ function isCtrlA(key) {
64
+ return key.ctrl && key.name === 'a'
65
+ }
66
+
67
+ function nextCursor(rows, cursor, delta) {
68
+ if (rows.length === 0) return 0
69
+ const n = rows.length
70
+ let i = ((cursor + delta) % n + n) % n
71
+ // 防止死循环(理论上 rows 永不为空时不会触发)
72
+ return i
73
+ }
74
+
75
+ export const groupedCheckbox = createPrompt((config, done) => {
76
+ const theme = makeTheme(config.theme)
77
+ const pageSize = config.pageSize ?? 15
78
+ const allItems = config.items
79
+ const totalCount = allItems.length
80
+
81
+ const [filter, setFilter] = useState('')
82
+ const [cursor, setCursor] = useState(0)
83
+ const [selected, setSelected] = useState(new Set(config.defaultSelected ?? []))
84
+ const [status, setStatus] = useState('idle') // 'idle' | 'done'
85
+
86
+ const rows = useMemo(
87
+ () => buildVisibleRows(allItems, filter, selected),
88
+ [filter, selected],
89
+ )
90
+
91
+ // cursor 越界保护
92
+ const safeCursor = rows.length === 0 ? 0 : Math.min(cursor, rows.length - 1)
93
+
94
+ useKeypress((key) => {
95
+ if (status === 'done') return
96
+
97
+ if (isEnterKey(key)) {
98
+ const result = allItems.filter(it => selected.has(it.name))
99
+ setStatus('done')
100
+ done(result)
101
+ return
102
+ }
103
+
104
+ if (isEscapeKey(key)) {
105
+ if (filter) {
106
+ setFilter('')
107
+ setCursor(0)
108
+ } else {
109
+ setStatus('done')
110
+ done([])
111
+ }
112
+ return
113
+ }
114
+
115
+ if (isUpKey(key)) {
116
+ setCursor(nextCursor(rows, safeCursor, -1))
117
+ return
118
+ }
119
+ if (isDownKey(key)) {
120
+ setCursor(nextCursor(rows, safeCursor, +1))
121
+ return
122
+ }
123
+
124
+ if (isCtrlA(key)) {
125
+ // 全选/全不选当前可见 item
126
+ const visibleItems = rows.filter(r => r.type === 'item').map(r => r.item)
127
+ if (visibleItems.length === 0) return
128
+ const allSelected = visibleItems.every(it => selected.has(it.name))
129
+ const next = new Set(selected)
130
+ if (allSelected) visibleItems.forEach(it => next.delete(it.name))
131
+ else visibleItems.forEach(it => next.add(it.name))
132
+ setSelected(next)
133
+ return
134
+ }
135
+
136
+ if (isSpaceKey(key)) {
137
+ if (rows.length === 0) return
138
+ const row = rows[safeCursor]
139
+ const next = new Set(selected)
140
+ if (row.type === 'item') {
141
+ if (next.has(row.item.name)) next.delete(row.item.name)
142
+ else next.add(row.item.name)
143
+ } else {
144
+ // header:组内全选/全不选
145
+ const allSelected = row.items.every(it => next.has(it.name))
146
+ if (allSelected) row.items.forEach(it => next.delete(it.name))
147
+ else row.items.forEach(it => next.add(it.name))
148
+ }
149
+ setSelected(next)
150
+ return
151
+ }
152
+
153
+ if (isBackspaceKey(key)) {
154
+ if (filter) {
155
+ setFilter(filter.slice(0, -1))
156
+ setCursor(0)
157
+ }
158
+ return
159
+ }
160
+
161
+ if (isPrintable(key)) {
162
+ setFilter(filter + key.sequence)
163
+ setCursor(0)
164
+ return
165
+ }
166
+ })
167
+
168
+ const prefix = usePrefix({ theme })
169
+
170
+ if (status === 'done') {
171
+ const picked = allItems.filter(it => selected.has(it.name))
172
+ const summary = picked.length === 0
173
+ ? chalk.gray('(未选)')
174
+ : chalk.green(`已选 ${picked.length} 项`)
175
+ return `${prefix} ${theme.style.message(config.message, 'done')} ${summary}`
176
+ }
177
+
178
+ const filterLine = filter
179
+ ? `${chalk.cyan('🔍')} ${chalk.yellow(filter)}${chalk.gray('▏')} ${chalk.gray(`${rows.filter(r => r.type === 'item').length}/${totalCount} 匹配`)}`
180
+ : `${chalk.gray('🔍 输入即过滤 ')}${chalk.gray(`${totalCount} 项`)}`
181
+
182
+ const hint = chalk.gray('↑↓ 移动 · space 切换/组全选 · ctrl+a 全选可见 · enter 确认 · esc 清搜索/退出')
183
+
184
+ const renderItem = ({ item: row, isActive }) => {
185
+ if (row.type === 'header') {
186
+ const fullSelected = row.selectedInGroup === row.total && row.total > 0
187
+ const partial = row.selectedInGroup > 0 && !fullSelected
188
+ const marker = fullSelected ? chalk.green('✓') : partial ? chalk.yellow('◐') : chalk.gray('○')
189
+ const arrow = isActive ? chalk.cyan('▶') : ' '
190
+ const label = chalk.bold(`${arrow} ${marker} ${row.category}`)
191
+ const count = chalk.gray(`[${row.selectedInGroup}/${row.total}]`)
192
+ return isActive ? chalk.bgGray.white(`${label} ${count}`) : `${label} ${count}`
193
+ }
194
+ const checked = selected.has(row.item.name)
195
+ const box = checked ? chalk.green('◉') : chalk.gray('◯')
196
+ const cursorMark = isActive ? chalk.cyan('❯') : ' '
197
+ const name = checked ? chalk.green(row.item.name) : chalk.cyan(row.item.name)
198
+ const desc = row.item.description || ''
199
+ const descShort = desc.length > 60 ? desc.slice(0, 60) + '…' : desc
200
+ const line = ` ${cursorMark} ${box} ${name} ${chalk.gray('- ' + descShort)}`
201
+ return isActive ? chalk.bgBlackBright(line) : line
202
+ }
203
+
204
+ const paginated = usePagination({
205
+ items: rows.length > 0 ? rows : [{ type: 'empty' }],
206
+ active: safeCursor,
207
+ renderItem: (ctx) => ctx.item.type === 'empty'
208
+ ? chalk.gray(' (无匹配项,按 esc 清空搜索)')
209
+ : renderItem(ctx),
210
+ pageSize,
211
+ loop: false,
212
+ })
213
+ const page = paginated
214
+
215
+ const message = theme.style.message(config.message, 'idle')
216
+
217
+ return [
218
+ `${prefix} ${message}`,
219
+ filterLine,
220
+ page,
221
+ hint,
222
+ ].join('\n')
223
+ })
package/src/sync.js CHANGED
@@ -3,6 +3,7 @@ import path from 'path'
3
3
  import os from 'os'
4
4
  import inquirer from 'inquirer'
5
5
  import { ensureRepoCache, chalk } from './utils.js'
6
+ import { groupedCheckbox } from './prompts/grouped-checkbox.js'
6
7
 
7
8
  const CATEGORIES = [
8
9
  { key: 'skills', label: 'Skills' },
@@ -14,29 +15,51 @@ const CATEGORIES = [
14
15
  function extractDescription(content) {
15
16
  const match = content.match(/^---\s*\n([\s\S]*?)\n---/)
16
17
  if (!match) return ''
17
- const frontmatter = match[1]
18
- const descMatch = frontmatter.match(/description:\s*(.+)/)
19
- return descMatch ? descMatch[1].trim().replace(/^["']|["']$/g, '') : ''
18
+ const lines = match[1].split('\n')
19
+ let i = 0
20
+ while (i < lines.length) {
21
+ const kv = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/)
22
+ if (!kv) { i++; continue }
23
+ if (kv[1] === 'description') {
24
+ const trimmed = kv[2].trim()
25
+ if (trimmed === '>' || trimmed === '|') {
26
+ i++
27
+ const parts = []
28
+ while (i < lines.length && (lines[i].startsWith(' ') || lines[i].trim() === '')) {
29
+ parts.push(lines[i].trim())
30
+ i++
31
+ }
32
+ return parts.filter(Boolean).join(' ')
33
+ }
34
+ return trimmed.replace(/^["']|["']$/g, '')
35
+ }
36
+ i++
37
+ }
38
+ return ''
20
39
  }
21
40
 
22
41
  async function scanSkills(sourceDir) {
23
42
  const skillsDir = path.join(sourceDir, 'skills')
24
43
  if (!await fs.pathExists(skillsDir)) return []
25
44
 
26
- const entries = await fs.readdir(skillsDir, { withFileTypes: true })
27
45
  const skills = []
28
- for (const entry of entries) {
29
- if (!entry.isDirectory()) continue
30
- const skillPath = path.join(skillsDir, entry.name)
31
- const skillMd = path.join(skillPath, 'SKILL.md')
32
- if (!await fs.pathExists(skillMd)) continue
33
- const content = await fs.readFile(skillMd, 'utf-8')
34
- skills.push({
35
- name: entry.name,
36
- description: extractDescription(content) || '(无描述)',
37
- sourcePath: skillPath,
38
- type: 'skill',
39
- })
46
+ for (const cat of await fs.readdir(skillsDir, { withFileTypes: true })) {
47
+ if (!cat.isDirectory()) continue
48
+ const catDir = path.join(skillsDir, cat.name)
49
+ for (const entry of await fs.readdir(catDir, { withFileTypes: true })) {
50
+ if (!entry.isDirectory()) continue
51
+ const skillPath = path.join(catDir, entry.name)
52
+ const skillMd = path.join(skillPath, 'SKILL.md')
53
+ if (!await fs.pathExists(skillMd)) continue
54
+ const content = await fs.readFile(skillMd, 'utf-8')
55
+ skills.push({
56
+ name: entry.name,
57
+ description: extractDescription(content) || '(无描述)',
58
+ category: cat.name,
59
+ sourcePath: skillPath,
60
+ type: 'skill',
61
+ })
62
+ }
40
63
  }
41
64
  return skills.sort((a, b) => a.name.localeCompare(b.name))
42
65
  }
@@ -45,18 +68,22 @@ async function scanRules(sourceDir) {
45
68
  const rulesDir = path.join(sourceDir, 'rules')
46
69
  if (!await fs.pathExists(rulesDir)) return []
47
70
 
48
- const entries = await fs.readdir(rulesDir, { withFileTypes: true })
49
71
  const rules = []
50
- for (const entry of entries) {
51
- if (!entry.isFile() || !entry.name.endsWith('.md')) continue
52
- const rulePath = path.join(rulesDir, entry.name)
53
- const content = await fs.readFile(rulePath, 'utf-8')
54
- rules.push({
55
- name: entry.name,
56
- description: extractDescription(content) || '(无描述)',
57
- sourcePath: rulePath,
58
- type: 'rule',
59
- })
72
+ for (const cat of await fs.readdir(rulesDir, { withFileTypes: true })) {
73
+ if (!cat.isDirectory()) continue
74
+ const catDir = path.join(rulesDir, cat.name)
75
+ for (const entry of await fs.readdir(catDir, { withFileTypes: true })) {
76
+ if (!entry.isFile() || !entry.name.endsWith('.md')) continue
77
+ const rulePath = path.join(catDir, entry.name)
78
+ const content = await fs.readFile(rulePath, 'utf-8')
79
+ rules.push({
80
+ name: entry.name,
81
+ description: extractDescription(content) || '(无描述)',
82
+ category: cat.name,
83
+ sourcePath: rulePath,
84
+ type: 'rule',
85
+ })
86
+ }
60
87
  }
61
88
  return rules.sort((a, b) => a.name.localeCompare(b.name))
62
89
  }
@@ -71,6 +98,7 @@ async function scanCommands(sourceDir) {
71
98
  .map(e => ({
72
99
  name: e.name,
73
100
  description: e.isDirectory() ? '命令目录' : '命令文件',
101
+ category: '其他',
74
102
  sourcePath: path.join(commandsDir, e.name),
75
103
  type: 'command',
76
104
  }))
@@ -87,6 +115,7 @@ async function scanHooks(sourceDir) {
87
115
  .map(e => ({
88
116
  name: e.name,
89
117
  description: e.isDirectory() ? '钩子目录' : '钩子脚本',
118
+ category: '其他',
90
119
  sourcePath: path.join(hooksDir, e.name),
91
120
  type: 'hook',
92
121
  }))
@@ -102,19 +131,12 @@ async function scanExisting(projectTargetDir, typePlural) {
102
131
 
103
132
  async function promptSelection(items, label, defaultNames) {
104
133
  if (items.length === 0) return []
105
- const { selected } = await inquirer.prompt([{
106
- type: 'checkbox',
107
- name: 'selected',
108
- message: `选择 ${label}(空格勾选,回车确认):`,
109
- choices: items.map(item => ({
110
- name: `${chalk.cyan(item.name)} ${chalk.gray('- ' + item.description.slice(0, 60))}${item.description.length > 60 ? '...' : ''}`,
111
- value: item,
112
- short: item.name,
113
- checked: defaultNames.has(item.name),
114
- })),
134
+ return groupedCheckbox({
135
+ message: `选择 ${label}`,
136
+ items,
137
+ defaultSelected: defaultNames,
115
138
  pageSize: 15,
116
- }])
117
- return selected
139
+ })
118
140
  }
119
141
 
120
142
  /**
@@ -200,7 +200,7 @@ class UserBase(BaseModel):
200
200
  email: EmailStr
201
201
 
202
202
  class UserCreate(UserBase):
203
- password: str = Field(min_length=8, max_length=128)
203
+ password: str = Field(min_length=8, max_length=72) # 72 = bcrypt 输入上限
204
204
 
205
205
  class UserUpdate(BaseModel):
206
206
  name: str | None = Field(default=None, max_length=50)
@@ -330,15 +330,55 @@ user = result.scalar_one_or_none()
330
330
  |------|------|
331
331
  | 存储算法 | **只允许 bcrypt**,必须走 dwyeapi 安全模块,**禁止**手写 bcrypt 调用或使用 MD5 / SHA / 明文 |
332
332
  | 接口返回 | **永远不返回**密码字段,Response Schema 中不包含 |
333
- | 密码强度 | 用户密码最少 8 位,管理员密码最少 12 位 |
333
+ | 密码强度 | **长度 ≥ 8 字符**,且**必须**包含 **大写字母、小写字母、数字、特殊符号** 中的至少 **3 类** |
334
+ | 密码长度上限 | Schema 必须 `max_length=72`(bcrypt 输入硬上限,超过会被静默截断),除非 dwyeapi 已做 SHA-256 预哈希 |
334
335
  | 登录错误 | 返回"用户名或密码错误",**不区分**是用户名不存在还是密码错误 |
336
+ | 强度校验位置 | Pydantic Schema 层用 `field_validator` 校验复杂度,**禁止**只校长度 |
337
+
338
+ **密码复杂度校验示例**:
339
+
340
+ ```python
341
+ import re
342
+ from pydantic import BaseModel, Field, field_validator
343
+
344
+ PASSWORD_PATTERNS = [
345
+ re.compile(r"[A-Z]"), # 大写
346
+ re.compile(r"[a-z]"), # 小写
347
+ re.compile(r"\d"), # 数字
348
+ re.compile(r"[^A-Za-z0-9]"), # 特殊符号
349
+ ]
350
+
351
+ class UserCreate(BaseModel):
352
+ password: str = Field(min_length=8, max_length=72)
353
+
354
+ @field_validator("password")
355
+ @classmethod
356
+ def check_complexity(cls, v: str) -> str:
357
+ """密码必须包含大写/小写/数字/特殊符号中至少 3 类。"""
358
+ matched = sum(1 for p in PASSWORD_PATTERNS if p.search(v))
359
+ if matched < 3:
360
+ raise ValueError("密码必须包含大写、小写、数字、特殊符号中至少 3 类")
361
+ return v
362
+ ```
363
+
364
+ > 该 validator 建议下沉到 dwyeapi 提供统一的 `PasswordStr` 类型,避免每个项目重复实现。
335
365
 
336
366
  ### 7.3 接口认证与授权
337
367
 
338
368
  - 所有 API **必须** JWT 认证(白名单除外:`/health` / `/auth/login` / `/auth/register`)
339
369
  - JWT 创建与解码**必须**走 dwyeapi 安全模块,**禁止**直接调用 PyJWT
340
370
  - Token 黑名单存 Redis(登出后失效,TTL = 剩余有效期)
341
- - Secret Key 必须从环境变量读取,最少 32 位随机字符串,**禁止**硬编码
371
+ - Secret Key 必须从环境变量读取,**禁止**硬编码
372
+ - Secret Key 长度与生成方式(按字节算,不按字符算):
373
+
374
+ | JWT 算法 | 最小长度 | 推荐生成命令 |
375
+ |---------|---------|------------|
376
+ | HS256 | **32 字节 / 256 bit** | `openssl rand -hex 32`(输出 64 个十六进制字符 = 32 字节) |
377
+ | HS512 | **64 字节 / 512 bit** | `openssl rand -hex 64`(输出 128 个十六进制字符 = 64 字节) |
378
+ | Python 等价 | — | `python -c "import secrets; print(secrets.token_urlsafe(32))"` |
379
+
380
+ - **禁止**使用:`uuid.uuid4().hex`(熵不足且非密码学 PRNG 推荐用途)、`random` 模块、手写字符串拼接
381
+ - 轮换策略:每 90 天 / 安全事件后立即轮换;新旧 Key 共存 1 个 Access Token 有效期窗口,避免在线用户被强制登出
342
382
 
343
383
  | Token 类型 | 有效期 |
344
384
  |------------|--------|
@@ -18,7 +18,10 @@ paths:
18
18
  ### 环境变量
19
19
 
20
20
  - **禁止**在 shell 前台 `export FOO=...` 注入环境变量
21
- - **禁止**在代码中硬编码配置值(数据库地址、密钥、端口等)
21
+ - **禁止**在代码中硬编码以下配置值:
22
+ - **基础设施**:数据库地址、Redis / MQ 地址、密钥、端口、外部 API 地址
23
+ - **业务参数**:缓存 TTL、HTTP 超时、重试次数 / 间隔、批处理大小、限流阈值(rate / burst)、分页默认值与上限、文件大小上限、Token 有效期、定时任务周期
24
+ - 判定标准:**任何"运维 / 业务可能想调整、且不希望发版"的值,都必须走配置**。代码中只允许出现:算法常量(PI、HTTP 状态码)、协议规定的固定值、纯类型枚举(如 `Status.ACTIVE = 1`)
22
25
  - 环境变量统一通过 `.env` 文件加载,由 Pydantic Settings 读取(见「配置管理」节)
23
26
  - `.env` 必须列入 `.gitignore`,提交 `.env.example` 作为模板
24
27
 
@@ -149,6 +152,36 @@ class Settings(BaseSettings):
149
152
  | 只有 1 个字段 | 保持顶层,不必嵌套 |
150
153
  | 3 个以上同前缀字段 | 必须嵌套 |
151
154
 
155
+ ### 什么必须走配置(判定表)
156
+
157
+ 写代码时遇到字面量数字 / 字符串,先对照此表判断是否抽到 Settings:
158
+
159
+ | 类别 | 示例 | 是否走配置 |
160
+ |------|------|-----------|
161
+ | 基础设施地址 | `postgresql://...` / `redis://...` / `https://api.xxx.com` | ✅ 必须 |
162
+ | 凭证 | SECRET_KEY / API_KEY / DB 密码 / OSS AK·SK | ✅ 必须(致命) |
163
+ | 缓存 TTL | `redis.setex(key, 43200, v)` 中的 `43200` | ✅ 必须 → `settings.cache.user_token_ttl` |
164
+ | HTTP 超时 / 重试 | `httpx.AsyncClient(timeout=30)` 中的 `30` | ✅ 必须 |
165
+ | 限流阈值 | `@rate_limit(100, per=60)` 中的 `100` / `60` | ✅ 必须 |
166
+ | 分页默认 / 上限 | `page_size: int = 20` / `le=100` 的 `100` | ✅ 必须 |
167
+ | 文件大小上限 | `if size > 10 * 1024 * 1024` 的 `10MB` | ✅ 必须 |
168
+ | Token 有效期 | JWT `exp` 的 7 天 / 30 天 | ✅ 必须 |
169
+ | 定时任务周期 | Celery / APScheduler 的 cron / interval | ✅ 必须 |
170
+ | 算法常量 | `PI = 3.14159` / `EARTH_RADIUS = 6371` | ❌ 不走配置 |
171
+ | 协议规定值 | HTTP 状态码 `200` / `404` | ❌ 不走配置 |
172
+ | 纯枚举值 | `class Status: ACTIVE = 1` | ❌ 不走配置 |
173
+
174
+ ```python
175
+ # ❌ 错误:缓存 TTL 硬编码
176
+ await redis.setex(f"user:{uid}", 43200, json.dumps(user))
177
+
178
+ # ✅ 正确:走 Settings
179
+ class CacheConfig(BaseModel):
180
+ user_ttl: int = 43200 # 默认 12h,可通过 CACHE__USER_TTL 覆盖
181
+
182
+ await redis.setex(f"user:{uid}", settings.cache.user_ttl, json.dumps(user))
183
+ ```
184
+
152
185
  ## 六、函数与类设计
153
186
 
154
187
  ### 强制规则
@@ -290,6 +323,6 @@ user = cache.get_user(user_id)
290
323
  | 8 | 嵌套不超过 3 层,函数体不超过 50 行 |
291
324
  | 9 | 所有模块/类/函数有 docstring(Google 风格) |
292
325
  | 10 | 外部输入用 Pydantic 校验,不手动校验 dict |
293
- | 11 | 配置必须 Pydantic Settings 嵌套分组,不在代码硬编码 |
326
+ | 11 | 配置必须 Pydantic Settings 嵌套分组;缓存 TTL / 超时 / 重试 / 限流 / 分页上限 / 文件上限 / Token 有效期等业务参数禁止硬编码 |
294
327
 
295
328
  **不执行自检就提交代码 = 违规。**