create-dwy 0.9.1 → 0.11.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/package.json +3 -2
- package/src/create.js +5 -2
- package/src/prompts/grouped-checkbox.js +223 -0
- package/src/sync.js +45 -19
- package/templates/claude-global/hooks/pre-git-commit-sensitive-check.sh +0 -0
- package/templates/claude-global/rules/dwy-android-core.md +1 -0
- package/templates/claude-global/rules/dwy-db-migration.md +1 -0
- package/templates/claude-global/rules/dwy-flutter-core.md +1 -0
- package/templates/claude-global/rules/dwy-flutter-network.md +1 -0
- package/templates/claude-global/rules/dwy-flutter-router.md +1 -0
- package/templates/claude-global/rules/dwy-flutter-testing.md +1 -0
- package/templates/claude-global/rules/dwy-postgres.md +1 -0
- package/templates/claude-global/rules/dwy-python-backend.md +44 -3
- package/templates/claude-global/rules/dwy-python-core.md +36 -2
- package/templates/claude-global/rules/dwy-python-testing.md +1 -0
- package/templates/claude-global/rules/dwy-redis.md +1 -0
- package/templates/claude-global/rules/dwy-tdd-development.md +1 -0
- package/templates/claude-global/rules/dwy-vue-core.md +3 -43
- package/templates/claude-global/rules/dwy-vue-pinia.md +1 -0
- package/templates/claude-global/rules/dwy-vue-testing.md +1 -0
- package/templates/claude-global/skills/dwy-cibuildwheel/SKILL.md +355 -0
- package/templates/claude-global/skills/dwy-cibuildwheel/references/release-workflow-template.yml +99 -0
- package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +0 -0
- package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +0 -0
- package/templates/claude-global/skills/dwy-dev-qa/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-docker/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +0 -0
- package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +0 -0
- package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +123 -76
- package/templates/claude-global/skills/dwy-dolphindb/references/create-database-table.md +219 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/crud-operations.md +281 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/data-import.md +271 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/docker-deployment.md +63 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/performance-tuning.md +223 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/pitfalls-and-best-practices.md +352 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/python-api.md +306 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/setup-and-connection.md +143 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/sources.md +46 -0
- package/templates/claude-global/skills/dwy-dolphindb/references/sql-batch-compute.md +206 -0
- package/templates/claude-global/skills/dwy-eapi/SKILL.md +134 -402
- package/templates/claude-global/skills/dwy-ekit/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-eui/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +14 -7
- package/templates/claude-global/skills/dwy-git-commit/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +0 -0
- package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +0 -0
- package/templates/claude-global/skills/dwy-pentest/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-publish/SKILL.md +1 -0
- package/templates/claude-global/skills/dwy-tech-stack/SKILL.md +1 -0
- package/templates/project/fullstack-monorepo/CLAUDE.md.hbs +21 -9
- package/templates/project/fullstack-monorepo/backend/Dockerfile.prod +40 -0
- package/templates/project/fullstack-monorepo/backend/_env.example.hbs +26 -0
- package/templates/project/fullstack-monorepo/docker-compose.dev.yml.hbs +4 -4
- package/templates/project/fullstack-monorepo/docker-compose.prod.yml.hbs +116 -0
- package/templates/project/fullstack-monorepo/docs/.gitkeep +0 -0
- package/templates/project/fullstack-monorepo/frontend/_env.example.hbs +19 -0
- package/templates/project/fullstack-monorepo/{dev.sh.hbs → scripts/dev.sh.hbs} +2 -2
- package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +0 -67
- package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +0 -441
- package/templates/claude-global/skills/dwy-eapi/references/health.md +0 -47
- package/templates/claude-global/skills/dwy-eapi/references/logger.md +0 -60
- package/templates/project/fullstack-monorepo/_env.example.hbs +0 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-dwy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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
|
+
}
|
package/src/create.js
CHANGED
|
@@ -154,11 +154,14 @@ export async function createProject(name) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
if (context.template === 'fullstack-monorepo') {
|
|
157
|
-
console.log(chalk.gray(' cp
|
|
157
|
+
console.log(chalk.gray(' cp backend/.env.example backend/.env'))
|
|
158
|
+
if (context.includeFrontend) {
|
|
159
|
+
console.log(chalk.gray(' cp frontend/.env.example frontend/.env'))
|
|
160
|
+
}
|
|
158
161
|
console.log(chalk.gray(' cd backend && uv sync --dev && cd ..'))
|
|
159
162
|
if (context.includeFrontend) {
|
|
160
163
|
console.log(chalk.gray(' cd frontend && pnpm install && cd ..'))
|
|
161
164
|
}
|
|
162
|
-
console.log(chalk.gray(' ./dev.sh'))
|
|
165
|
+
console.log(chalk.gray(' ./scripts/dev.sh'))
|
|
163
166
|
}
|
|
164
167
|
}
|
|
@@ -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' },
|
|
@@ -11,12 +12,38 @@ const CATEGORIES = [
|
|
|
11
12
|
{ key: 'hooks', label: 'Hooks' },
|
|
12
13
|
]
|
|
13
14
|
|
|
14
|
-
function
|
|
15
|
+
function extractFrontmatter(content) {
|
|
15
16
|
const match = content.match(/^---\s*\n([\s\S]*?)\n---/)
|
|
16
|
-
if (!match) return ''
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
17
|
+
if (!match) return { description: '', category: '其他' }
|
|
18
|
+
const lines = match[1].split('\n')
|
|
19
|
+
const result = { description: '', category: '其他' }
|
|
20
|
+
let i = 0
|
|
21
|
+
while (i < lines.length) {
|
|
22
|
+
const kv = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/)
|
|
23
|
+
if (!kv) { i++; continue }
|
|
24
|
+
const [, key, value] = kv
|
|
25
|
+
if (key === 'category') {
|
|
26
|
+
result.category = value.trim().replace(/^["']|["']$/g, '') || '其他'
|
|
27
|
+
i++
|
|
28
|
+
} else if (key === 'description') {
|
|
29
|
+
const trimmed = value.trim()
|
|
30
|
+
if (trimmed === '>' || trimmed === '|') {
|
|
31
|
+
i++
|
|
32
|
+
const parts = []
|
|
33
|
+
while (i < lines.length && (lines[i].startsWith(' ') || lines[i].trim() === '')) {
|
|
34
|
+
parts.push(lines[i].trim())
|
|
35
|
+
i++
|
|
36
|
+
}
|
|
37
|
+
result.description = parts.filter(Boolean).join(' ')
|
|
38
|
+
} else {
|
|
39
|
+
result.description = trimmed.replace(/^["']|["']$/g, '')
|
|
40
|
+
i++
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
i++
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result
|
|
20
47
|
}
|
|
21
48
|
|
|
22
49
|
async function scanSkills(sourceDir) {
|
|
@@ -31,9 +58,11 @@ async function scanSkills(sourceDir) {
|
|
|
31
58
|
const skillMd = path.join(skillPath, 'SKILL.md')
|
|
32
59
|
if (!await fs.pathExists(skillMd)) continue
|
|
33
60
|
const content = await fs.readFile(skillMd, 'utf-8')
|
|
61
|
+
const fm = extractFrontmatter(content)
|
|
34
62
|
skills.push({
|
|
35
63
|
name: entry.name,
|
|
36
|
-
description:
|
|
64
|
+
description: fm.description || '(无描述)',
|
|
65
|
+
category: fm.category,
|
|
37
66
|
sourcePath: skillPath,
|
|
38
67
|
type: 'skill',
|
|
39
68
|
})
|
|
@@ -51,9 +80,11 @@ async function scanRules(sourceDir) {
|
|
|
51
80
|
if (!entry.isFile() || !entry.name.endsWith('.md')) continue
|
|
52
81
|
const rulePath = path.join(rulesDir, entry.name)
|
|
53
82
|
const content = await fs.readFile(rulePath, 'utf-8')
|
|
83
|
+
const fm = extractFrontmatter(content)
|
|
54
84
|
rules.push({
|
|
55
85
|
name: entry.name,
|
|
56
|
-
description:
|
|
86
|
+
description: fm.description || '(无描述)',
|
|
87
|
+
category: fm.category,
|
|
57
88
|
sourcePath: rulePath,
|
|
58
89
|
type: 'rule',
|
|
59
90
|
})
|
|
@@ -71,6 +102,7 @@ async function scanCommands(sourceDir) {
|
|
|
71
102
|
.map(e => ({
|
|
72
103
|
name: e.name,
|
|
73
104
|
description: e.isDirectory() ? '命令目录' : '命令文件',
|
|
105
|
+
category: '其他',
|
|
74
106
|
sourcePath: path.join(commandsDir, e.name),
|
|
75
107
|
type: 'command',
|
|
76
108
|
}))
|
|
@@ -87,6 +119,7 @@ async function scanHooks(sourceDir) {
|
|
|
87
119
|
.map(e => ({
|
|
88
120
|
name: e.name,
|
|
89
121
|
description: e.isDirectory() ? '钩子目录' : '钩子脚本',
|
|
122
|
+
category: '其他',
|
|
90
123
|
sourcePath: path.join(hooksDir, e.name),
|
|
91
124
|
type: 'hook',
|
|
92
125
|
}))
|
|
@@ -102,19 +135,12 @@ async function scanExisting(projectTargetDir, typePlural) {
|
|
|
102
135
|
|
|
103
136
|
async function promptSelection(items, label, defaultNames) {
|
|
104
137
|
if (items.length === 0) return []
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
})),
|
|
138
|
+
return groupedCheckbox({
|
|
139
|
+
message: `选择 ${label}`,
|
|
140
|
+
items,
|
|
141
|
+
defaultSelected: defaultNames,
|
|
115
142
|
pageSize: 15,
|
|
116
|
-
}
|
|
117
|
-
return selected
|
|
143
|
+
})
|
|
118
144
|
}
|
|
119
145
|
|
|
120
146
|
/**
|
|
File without changes
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Python 后端(FastAPI + SQLAlchemy)应用层开发规范 — 路由 / Schema / 依赖注入 / ORM / 安全编码 / 注入防护 / 限流 / 审计 / 违规检测清单
|
|
3
|
+
category: Python
|
|
3
4
|
paths:
|
|
4
5
|
- "**/*.py"
|
|
5
6
|
---
|
|
@@ -200,7 +201,7 @@ class UserBase(BaseModel):
|
|
|
200
201
|
email: EmailStr
|
|
201
202
|
|
|
202
203
|
class UserCreate(UserBase):
|
|
203
|
-
password: str = Field(min_length=8, max_length=
|
|
204
|
+
password: str = Field(min_length=8, max_length=72) # 72 = bcrypt 输入上限
|
|
204
205
|
|
|
205
206
|
class UserUpdate(BaseModel):
|
|
206
207
|
name: str | None = Field(default=None, max_length=50)
|
|
@@ -330,15 +331,55 @@ user = result.scalar_one_or_none()
|
|
|
330
331
|
|------|------|
|
|
331
332
|
| 存储算法 | **只允许 bcrypt**,必须走 dwyeapi 安全模块,**禁止**手写 bcrypt 调用或使用 MD5 / SHA / 明文 |
|
|
332
333
|
| 接口返回 | **永远不返回**密码字段,Response Schema 中不包含 |
|
|
333
|
-
| 密码强度 |
|
|
334
|
+
| 密码强度 | **长度 ≥ 8 字符**,且**必须**包含 **大写字母、小写字母、数字、特殊符号** 中的至少 **3 类** |
|
|
335
|
+
| 密码长度上限 | Schema 必须 `max_length=72`(bcrypt 输入硬上限,超过会被静默截断),除非 dwyeapi 已做 SHA-256 预哈希 |
|
|
334
336
|
| 登录错误 | 返回"用户名或密码错误",**不区分**是用户名不存在还是密码错误 |
|
|
337
|
+
| 强度校验位置 | Pydantic Schema 层用 `field_validator` 校验复杂度,**禁止**只校长度 |
|
|
338
|
+
|
|
339
|
+
**密码复杂度校验示例**:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
import re
|
|
343
|
+
from pydantic import BaseModel, Field, field_validator
|
|
344
|
+
|
|
345
|
+
PASSWORD_PATTERNS = [
|
|
346
|
+
re.compile(r"[A-Z]"), # 大写
|
|
347
|
+
re.compile(r"[a-z]"), # 小写
|
|
348
|
+
re.compile(r"\d"), # 数字
|
|
349
|
+
re.compile(r"[^A-Za-z0-9]"), # 特殊符号
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
class UserCreate(BaseModel):
|
|
353
|
+
password: str = Field(min_length=8, max_length=72)
|
|
354
|
+
|
|
355
|
+
@field_validator("password")
|
|
356
|
+
@classmethod
|
|
357
|
+
def check_complexity(cls, v: str) -> str:
|
|
358
|
+
"""密码必须包含大写/小写/数字/特殊符号中至少 3 类。"""
|
|
359
|
+
matched = sum(1 for p in PASSWORD_PATTERNS if p.search(v))
|
|
360
|
+
if matched < 3:
|
|
361
|
+
raise ValueError("密码必须包含大写、小写、数字、特殊符号中至少 3 类")
|
|
362
|
+
return v
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
> 该 validator 建议下沉到 dwyeapi 提供统一的 `PasswordStr` 类型,避免每个项目重复实现。
|
|
335
366
|
|
|
336
367
|
### 7.3 接口认证与授权
|
|
337
368
|
|
|
338
369
|
- 所有 API **必须** JWT 认证(白名单除外:`/health` / `/auth/login` / `/auth/register`)
|
|
339
370
|
- JWT 创建与解码**必须**走 dwyeapi 安全模块,**禁止**直接调用 PyJWT
|
|
340
371
|
- Token 黑名单存 Redis(登出后失效,TTL = 剩余有效期)
|
|
341
|
-
- Secret Key
|
|
372
|
+
- Secret Key 必须从环境变量读取,**禁止**硬编码
|
|
373
|
+
- Secret Key 长度与生成方式(按字节算,不按字符算):
|
|
374
|
+
|
|
375
|
+
| JWT 算法 | 最小长度 | 推荐生成命令 |
|
|
376
|
+
|---------|---------|------------|
|
|
377
|
+
| HS256 | **32 字节 / 256 bit** | `openssl rand -hex 32`(输出 64 个十六进制字符 = 32 字节) |
|
|
378
|
+
| HS512 | **64 字节 / 512 bit** | `openssl rand -hex 64`(输出 128 个十六进制字符 = 64 字节) |
|
|
379
|
+
| Python 等价 | — | `python -c "import secrets; print(secrets.token_urlsafe(32))"` |
|
|
380
|
+
|
|
381
|
+
- **禁止**使用:`uuid.uuid4().hex`(熵不足且非密码学 PRNG 推荐用途)、`random` 模块、手写字符串拼接
|
|
382
|
+
- 轮换策略:每 90 天 / 安全事件后立即轮换;新旧 Key 共存 1 个 Access Token 有效期窗口,避免在线用户被强制登出
|
|
342
383
|
|
|
343
384
|
| Token 类型 | 有效期 |
|
|
344
385
|
|------------|--------|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Python 通用代码规范
|
|
3
|
+
category: Python
|
|
3
4
|
paths:
|
|
4
5
|
- "**/*.py"
|
|
5
6
|
- "**/pyproject.toml"
|
|
@@ -18,7 +19,10 @@ paths:
|
|
|
18
19
|
### 环境变量
|
|
19
20
|
|
|
20
21
|
- **禁止**在 shell 前台 `export FOO=...` 注入环境变量
|
|
21
|
-
-
|
|
22
|
+
- **禁止**在代码中硬编码以下配置值:
|
|
23
|
+
- **基础设施**:数据库地址、Redis / MQ 地址、密钥、端口、外部 API 地址
|
|
24
|
+
- **业务参数**:缓存 TTL、HTTP 超时、重试次数 / 间隔、批处理大小、限流阈值(rate / burst)、分页默认值与上限、文件大小上限、Token 有效期、定时任务周期
|
|
25
|
+
- 判定标准:**任何"运维 / 业务可能想调整、且不希望发版"的值,都必须走配置**。代码中只允许出现:算法常量(PI、HTTP 状态码)、协议规定的固定值、纯类型枚举(如 `Status.ACTIVE = 1`)
|
|
22
26
|
- 环境变量统一通过 `.env` 文件加载,由 Pydantic Settings 读取(见「配置管理」节)
|
|
23
27
|
- `.env` 必须列入 `.gitignore`,提交 `.env.example` 作为模板
|
|
24
28
|
|
|
@@ -149,6 +153,36 @@ class Settings(BaseSettings):
|
|
|
149
153
|
| 只有 1 个字段 | 保持顶层,不必嵌套 |
|
|
150
154
|
| 3 个以上同前缀字段 | 必须嵌套 |
|
|
151
155
|
|
|
156
|
+
### 什么必须走配置(判定表)
|
|
157
|
+
|
|
158
|
+
写代码时遇到字面量数字 / 字符串,先对照此表判断是否抽到 Settings:
|
|
159
|
+
|
|
160
|
+
| 类别 | 示例 | 是否走配置 |
|
|
161
|
+
|------|------|-----------|
|
|
162
|
+
| 基础设施地址 | `postgresql://...` / `redis://...` / `https://api.xxx.com` | ✅ 必须 |
|
|
163
|
+
| 凭证 | SECRET_KEY / API_KEY / DB 密码 / OSS AK·SK | ✅ 必须(致命) |
|
|
164
|
+
| 缓存 TTL | `redis.setex(key, 43200, v)` 中的 `43200` | ✅ 必须 → `settings.cache.user_token_ttl` |
|
|
165
|
+
| HTTP 超时 / 重试 | `httpx.AsyncClient(timeout=30)` 中的 `30` | ✅ 必须 |
|
|
166
|
+
| 限流阈值 | `@rate_limit(100, per=60)` 中的 `100` / `60` | ✅ 必须 |
|
|
167
|
+
| 分页默认 / 上限 | `page_size: int = 20` / `le=100` 的 `100` | ✅ 必须 |
|
|
168
|
+
| 文件大小上限 | `if size > 10 * 1024 * 1024` 的 `10MB` | ✅ 必须 |
|
|
169
|
+
| Token 有效期 | JWT `exp` 的 7 天 / 30 天 | ✅ 必须 |
|
|
170
|
+
| 定时任务周期 | Celery / APScheduler 的 cron / interval | ✅ 必须 |
|
|
171
|
+
| 算法常量 | `PI = 3.14159` / `EARTH_RADIUS = 6371` | ❌ 不走配置 |
|
|
172
|
+
| 协议规定值 | HTTP 状态码 `200` / `404` | ❌ 不走配置 |
|
|
173
|
+
| 纯枚举值 | `class Status: ACTIVE = 1` | ❌ 不走配置 |
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
# ❌ 错误:缓存 TTL 硬编码
|
|
177
|
+
await redis.setex(f"user:{uid}", 43200, json.dumps(user))
|
|
178
|
+
|
|
179
|
+
# ✅ 正确:走 Settings
|
|
180
|
+
class CacheConfig(BaseModel):
|
|
181
|
+
user_ttl: int = 43200 # 默认 12h,可通过 CACHE__USER_TTL 覆盖
|
|
182
|
+
|
|
183
|
+
await redis.setex(f"user:{uid}", settings.cache.user_ttl, json.dumps(user))
|
|
184
|
+
```
|
|
185
|
+
|
|
152
186
|
## 六、函数与类设计
|
|
153
187
|
|
|
154
188
|
### 强制规则
|
|
@@ -290,6 +324,6 @@ user = cache.get_user(user_id)
|
|
|
290
324
|
| 8 | 嵌套不超过 3 层,函数体不超过 50 行 |
|
|
291
325
|
| 9 | 所有模块/类/函数有 docstring(Google 风格) |
|
|
292
326
|
| 10 | 外部输入用 Pydantic 校验,不手动校验 dict |
|
|
293
|
-
| 11 | 配置必须 Pydantic Settings
|
|
327
|
+
| 11 | 配置必须 Pydantic Settings 嵌套分组;缓存 TTL / 超时 / 重试 / 限流 / 分页上限 / 文件上限 / Token 有效期等业务参数禁止硬编码 |
|
|
294
328
|
|
|
295
329
|
**不执行自检就提交代码 = 违规。**
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Vue 3 + TS + Vite + Tailwind 应用层规范(强制使用 eui/ekit、业务聚合结构、组件 / TS / 路由 / API / 模板 / 样式 / 错误处理 / 注释)
|
|
3
|
+
category: Vue
|
|
3
4
|
paths:
|
|
4
5
|
- "**/*.vue"
|
|
6
|
+
- "**/*.ts"
|
|
5
7
|
- "**/vite.config.*"
|
|
6
8
|
---
|
|
7
9
|
|
|
@@ -108,12 +110,6 @@ src/
|
|
|
108
110
|
- 单文件超过约 400 行时再考虑拆分,**禁止**未到规模就预拆分
|
|
109
111
|
- 模块间引用通过明确 import,**禁止**循环依赖
|
|
110
112
|
|
|
111
|
-
### 例外
|
|
112
|
-
|
|
113
|
-
- 小型项目(< 3 个业务模块)允许扁平结构(`src/views/` + `src/components/`),但**不得**长大后保持扁平
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
113
|
## 二、组件规范
|
|
118
114
|
|
|
119
115
|
### 强制使用 Composition API + `<script setup>`
|
|
@@ -550,43 +546,7 @@ export function formatMoney(value: number, currency = '¥'): string {
|
|
|
550
546
|
|
|
551
547
|
---
|
|
552
548
|
|
|
553
|
-
##
|
|
554
|
-
|
|
555
|
-
代码只写**必要的逻辑**,不写"以防万一"的冗余代码。信任 TypeScript 类型系统与框架保证,只在系统边界(用户输入、外部 API)做校验。
|
|
556
|
-
|
|
557
|
-
### 禁止的冗余模式
|
|
558
|
-
|
|
559
|
-
```typescript
|
|
560
|
-
// 反例:类型已保证非空,多余 fallback
|
|
561
|
-
const name = props.title ?? '' // props.title: string
|
|
562
|
-
|
|
563
|
-
// 正例
|
|
564
|
-
const name = props.title
|
|
565
|
-
|
|
566
|
-
// 反例:值一定存在
|
|
567
|
-
const id = user?.id // user 来自必传 prop
|
|
568
|
-
|
|
569
|
-
// 正例
|
|
570
|
-
const id = user.id
|
|
571
|
-
|
|
572
|
-
// 反例:空 catch 吞异常
|
|
573
|
-
try { await api.getUsers() } catch {}
|
|
574
|
-
|
|
575
|
-
// 正例:让异常暴露或给用户反馈
|
|
576
|
-
try { await api.getUsers() } catch { toast.error('加载失败') }
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
### 判断标准
|
|
580
|
-
|
|
581
|
-
写每行防御代码前自问:**这个情况在当前上下文真的会发生吗?**
|
|
582
|
-
|
|
583
|
-
- 会 → 写防御,注释说明触发条件
|
|
584
|
-
- 不会 → 不写,信任上游保证
|
|
585
|
-
- 不确定 → 查类型定义与调用链确认
|
|
586
|
-
|
|
587
|
-
---
|
|
588
|
-
|
|
589
|
-
## 十五、违规检测清单
|
|
549
|
+
## 十四、违规检测清单
|
|
590
550
|
|
|
591
551
|
AI 编写或审查 Vue 代码时,**必须**检查以下违规模式:
|
|
592
552
|
|