@ranger1/dx 0.1.90 → 0.1.92
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/README.md +2 -2
- package/lib/cli/commands/core.js +92 -0
- package/lib/cli/help.js +2 -1
- package/lib/codex-initial.js +19 -215
- package/package.json +2 -2
- package/skills/backend-layering-audit-fixer/SKILL.md +180 -0
- package/{codex/skills → skills}/doctor/SKILL.md +2 -9
- package/{codex/skills → skills}/doctor/scripts/doctor.sh +2 -253
- package/skills/git-pr-ship/SKILL.md +481 -0
- package/skills/naming-audit-fixer/SKILL.md +149 -0
- package/skills/naming-audit-fixer/references/fix-guide.md +93 -0
- package/skills/naming-audit-fixer/scripts/audit_naming.py +534 -0
- package/codex/agents/fixer.toml +0 -37
- package/codex/agents/orchestrator.toml +0 -11
- package/codex/agents/reviewer.toml +0 -52
- package/codex/agents/spark.toml +0 -18
- package/codex/skills/pr-review-loop/SKILL.md +0 -209
- package/codex/skills/pr-review-loop/agents/openai.yaml +0 -4
- package/codex/skills/pr-review-loop/references/agents/pr-context.md +0 -73
- package/codex/skills/pr-review-loop/references/agents/pr-precheck.md +0 -161
- package/codex/skills/pr-review-loop/references/agents/pr-review-aggregate.md +0 -188
- package/codex/skills/pr-review-loop/references/skill-layout.md +0 -25
- package/codex/skills/pr-review-loop/scripts/gh_review_harvest.py +0 -292
- package/codex/skills/pr-review-loop/scripts/pr_context.py +0 -351
- package/codex/skills/pr-review-loop/scripts/pr_review_aggregate.py +0 -951
- package/codex/skills/pr-review-loop/scripts/test_pr_review_aggregate.py +0 -876
- package/codex/skills/pr-review-loop/scripts/test_validate_reviewer_prompts.py +0 -92
- package/codex/skills/pr-review-loop/scripts/validate_reviewer_prompts.py +0 -87
- /package/{codex/skills → skills}/doctor/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/e2e-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/e2e-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/e2e-audit-fixer/scripts/e2e_e2e_audit.py +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/references/bootstrap-env-foundation.md +0 -0
- /package/{codex/skills → skills}/env-accessor-audit-fixer/scripts/env_accessor_audit.py +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/references/error-handling-standard.md +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/references/foundation-bootstrap.md +0 -0
- /package/{codex/skills → skills}/error-handling-audit-fixer/scripts/error_handling_audit.py +0 -0
- /package/{codex/skills → skills}/gh-dependabot-cleanup/SKILL.md +0 -0
- /package/{codex/skills → skills}/gh-dependabot-cleanup/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/git-commit-and-pr/SKILL.md +0 -0
- /package/{codex/skills → skills}/git-commit-and-pr/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/git-release/SKILL.md +0 -0
- /package/{codex/skills → skills}/git-release/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/online-debug-guard/SKILL.md +0 -0
- /package/{codex/skills → skills}/online-debug-guard/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/SKILL.md +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/agents/openai.yaml +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/references/pagination-standard.md +0 -0
- /package/{codex/skills → skills}/pagination-dto-audit-fixer/scripts/pagination_dto_audit.py +0 -0
package/README.md
CHANGED
|
@@ -148,8 +148,8 @@ target(端)不写死,由 `env-policy.jsonc.targets` 定义;`commands.jso
|
|
|
148
148
|
|
|
149
149
|
技能入口与说明见:
|
|
150
150
|
|
|
151
|
-
- `
|
|
152
|
-
- `
|
|
151
|
+
- `skills/pr-review-loop/SKILL.md`
|
|
152
|
+
- `skills/pr-review-loop/references/agents/*.md`
|
|
153
153
|
|
|
154
154
|
### 工作流概览
|
|
155
155
|
|
package/lib/cli/commands/core.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join, relative } from 'node:path'
|
|
1
3
|
import { logger } from '../../logger.js'
|
|
2
4
|
import { confirmManager } from '../../confirm.js'
|
|
3
5
|
import { execManager } from '../../exec.js'
|
|
@@ -132,6 +134,35 @@ export async function handleTest(cli, args) {
|
|
|
132
134
|
: `运行单个E2E测试文件: ${testPath}`
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
if (testNamePattern) {
|
|
138
|
+
logger.step(`运行 ${type} 测试用例: ${testNamePattern} (文件: ${testPath})`)
|
|
139
|
+
} else {
|
|
140
|
+
logger.step(`运行单个 ${type} 测试: ${testPath}`)
|
|
141
|
+
}
|
|
142
|
+
} else if (type === 'unit' && testPath) {
|
|
143
|
+
let command = String(testConfig.command).trim()
|
|
144
|
+
const useDirectPathArg = shouldUseDirectPathArg(command)
|
|
145
|
+
const normalizedTestPath = useDirectPathArg
|
|
146
|
+
? normalizeUnitTestPathForCommand(cli, command, testPath)
|
|
147
|
+
: testPath
|
|
148
|
+
const forwardedArgs = useDirectPathArg
|
|
149
|
+
? [shellEscape(normalizedTestPath)]
|
|
150
|
+
: [`--runTestsByPath ${shellEscape(normalizedTestPath)}`]
|
|
151
|
+
|
|
152
|
+
if (testNamePattern) {
|
|
153
|
+
forwardedArgs.push(`-t ${shellEscape(testNamePattern)}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
command += ` ${forwardedArgs.join(' ')}`
|
|
157
|
+
|
|
158
|
+
testConfig = {
|
|
159
|
+
...testConfig,
|
|
160
|
+
command,
|
|
161
|
+
description: testNamePattern
|
|
162
|
+
? `运行单个单元测试文件的特定用例: ${testPath} -> ${testNamePattern}`
|
|
163
|
+
: `运行单个单元测试文件: ${testPath}`,
|
|
164
|
+
}
|
|
165
|
+
|
|
135
166
|
if (testNamePattern) {
|
|
136
167
|
logger.step(`运行 ${type} 测试用例: ${testNamePattern} (文件: ${testPath})`)
|
|
137
168
|
} else {
|
|
@@ -148,6 +179,67 @@ function shellEscape(value) {
|
|
|
148
179
|
return `'${String(value).replace(/'/g, `'\\''`)}'`
|
|
149
180
|
}
|
|
150
181
|
|
|
182
|
+
function shouldUseDirectPathArg(command) {
|
|
183
|
+
const text = String(command || '')
|
|
184
|
+
return (
|
|
185
|
+
/\bnx\s+test\b/.test(text) ||
|
|
186
|
+
/\bnx\.js\s+test\b/.test(text) ||
|
|
187
|
+
/\bvitest\s+run\b/.test(text)
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeUnitTestPathForCommand(cli, command, testPath) {
|
|
192
|
+
const rawPath = String(testPath || '')
|
|
193
|
+
if (!rawPath) return rawPath
|
|
194
|
+
|
|
195
|
+
if (/\bvitest\s+run\b/.test(String(command || ''))) {
|
|
196
|
+
return rawPath
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const vitestProjectCwd = resolveNxVitestProjectCwd(cli, command)
|
|
200
|
+
if (!vitestProjectCwd) return rawPath
|
|
201
|
+
|
|
202
|
+
const projectRoot = cli?.projectRoot || process.cwd()
|
|
203
|
+
const absoluteProjectCwd = join(projectRoot, vitestProjectCwd)
|
|
204
|
+
const absoluteTestPath = join(projectRoot, rawPath)
|
|
205
|
+
const relativePath = relative(absoluteProjectCwd, absoluteTestPath)
|
|
206
|
+
|
|
207
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
208
|
+
return rawPath
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return relativePath
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function resolveNxVitestProjectCwd(cli, command) {
|
|
215
|
+
const projectRoot = cli?.projectRoot || process.cwd()
|
|
216
|
+
const nxTarget = extractNxTestTarget(command)
|
|
217
|
+
if (!nxTarget) return null
|
|
218
|
+
|
|
219
|
+
const projectConfigPath = join(projectRoot, 'apps', nxTarget, 'project.json')
|
|
220
|
+
if (!existsSync(projectConfigPath)) return null
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const projectConfig = JSON.parse(readFileSync(projectConfigPath, 'utf8'))
|
|
224
|
+
const testTarget = projectConfig?.targets?.test
|
|
225
|
+
const command = String(testTarget?.options?.command || '')
|
|
226
|
+
const cwd = testTarget?.options?.cwd
|
|
227
|
+
if (!/\bvitest\s+run\b/.test(command)) return null
|
|
228
|
+
if (typeof cwd !== 'string' || cwd.trim().length === 0) return null
|
|
229
|
+
return cwd
|
|
230
|
+
} catch {
|
|
231
|
+
return null
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function extractNxTestTarget(command) {
|
|
236
|
+
const text = String(command || '').trim()
|
|
237
|
+
const match =
|
|
238
|
+
text.match(/\bnx(?:\.js)?\s+test\s+([^\s]+)/) ||
|
|
239
|
+
text.match(/\bnx(?:\.js)?\s+run\s+([^:\s]+):test\b/)
|
|
240
|
+
return match?.[1] || null
|
|
241
|
+
}
|
|
242
|
+
|
|
151
243
|
export async function handleLint(cli, args) {
|
|
152
244
|
void args
|
|
153
245
|
const baseConfig = cli.commands.lint
|
package/lib/cli/help.js
CHANGED
|
@@ -69,7 +69,7 @@ export function showHelp() {
|
|
|
69
69
|
'',
|
|
70
70
|
' status 查看系统状态',
|
|
71
71
|
'',
|
|
72
|
-
' initial
|
|
72
|
+
' initial 同步根目录 skills 到 ~/.codex/skills 和 ~/.claude/skills(覆盖同名文件)',
|
|
73
73
|
'',
|
|
74
74
|
'选项:',
|
|
75
75
|
' --dev, --development 使用开发环境',
|
|
@@ -95,6 +95,7 @@ export function showHelp() {
|
|
|
95
95
|
' dx test e2e backend apps/backend/e2e/activity/activity.admin.e2e-spec.ts # 运行单个E2E测试文件',
|
|
96
96
|
' dx test e2e backend apps/backend/e2e/activity/activity.admin.e2e-spec.ts -t "should list all activity definitions" # 运行特定测试用例',
|
|
97
97
|
' dx test e2e quantify apps/quantify/e2e/health/health.e2e-spec.ts # 运行 Quantify E2E 文件',
|
|
98
|
+
' dx test unit backend apps/backend/src/modules/chat/chat.service.spec.ts # 运行单个后端单测文件',
|
|
98
99
|
' dx test e2e all # 不受支持,必须指定 target 和 path',
|
|
99
100
|
' dx deploy front --staging # 部署前端到 Vercel(staging)',
|
|
100
101
|
' dx deploy backend --prod # 构建 backend 制品并上传/部署到远端主机',
|
package/lib/codex-initial.js
CHANGED
|
@@ -4,49 +4,6 @@ import os from 'node:os'
|
|
|
4
4
|
|
|
5
5
|
import { logger } from './logger.js'
|
|
6
6
|
|
|
7
|
-
const REQUIRED_CODEX_CONFIG = [
|
|
8
|
-
{
|
|
9
|
-
section: 'features',
|
|
10
|
-
values: {
|
|
11
|
-
multi_agent: 'true',
|
|
12
|
-
},
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
section: 'agents',
|
|
16
|
-
values: {
|
|
17
|
-
max_threads: '15',
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
section: 'agents.fixer',
|
|
22
|
-
values: {
|
|
23
|
-
description: '"bugfix 代理"',
|
|
24
|
-
config_file: '"agents/fixer.toml"',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
section: 'agents.orchestrator',
|
|
29
|
-
values: {
|
|
30
|
-
description: '"pr 修复流程编排代理"',
|
|
31
|
-
config_file: '"agents/orchestrator.toml"',
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
section: 'agents.reviewer',
|
|
36
|
-
values: {
|
|
37
|
-
description: '"代码评审代理"',
|
|
38
|
-
config_file: '"agents/reviewer.toml"',
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
section: 'agents.spark',
|
|
43
|
-
values: {
|
|
44
|
-
description: '"通用执行代理"',
|
|
45
|
-
config_file: '"agents/spark.toml"',
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
]
|
|
49
|
-
|
|
50
7
|
async function collectAllFiles(dir) {
|
|
51
8
|
const out = []
|
|
52
9
|
|
|
@@ -68,6 +25,7 @@ async function collectAllFiles(dir) {
|
|
|
68
25
|
continue
|
|
69
26
|
}
|
|
70
27
|
if (!entry.isFile()) continue
|
|
28
|
+
|
|
71
29
|
const lowerName = entry.name.toLowerCase()
|
|
72
30
|
if (lowerName.endsWith('.pyc') || lowerName.endsWith('.pyo') || lowerName.endsWith('.pyd')) continue
|
|
73
31
|
out.push(full)
|
|
@@ -82,110 +40,6 @@ async function ensureDir(path) {
|
|
|
82
40
|
await fs.mkdir(path, { recursive: true })
|
|
83
41
|
}
|
|
84
42
|
|
|
85
|
-
function escapeRegExp(input) {
|
|
86
|
-
return String(input).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function ensureTrailingNewline(text) {
|
|
90
|
-
if (!text) return ''
|
|
91
|
-
return text.endsWith('\n') ? text : `${text}\n`
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function upsertTomlSection(content, { section, values }) {
|
|
95
|
-
const header = `[${section}]`
|
|
96
|
-
const sectionPattern = new RegExp(`^\\[${escapeRegExp(section)}\\]\\s*$`, 'm')
|
|
97
|
-
const sectionHeaderMatch = content.match(sectionPattern)
|
|
98
|
-
let nextContent = content
|
|
99
|
-
let changedKeys = 0
|
|
100
|
-
let createdSection = false
|
|
101
|
-
|
|
102
|
-
if (!sectionHeaderMatch) {
|
|
103
|
-
const blockLines = [header, ...Object.entries(values).map(([key, value]) => `${key} = ${value}`), '']
|
|
104
|
-
nextContent = ensureTrailingNewline(content)
|
|
105
|
-
if (nextContent.length > 0 && !nextContent.endsWith('\n\n')) {
|
|
106
|
-
nextContent += '\n'
|
|
107
|
-
}
|
|
108
|
-
nextContent += `${blockLines.join('\n')}\n`
|
|
109
|
-
return {
|
|
110
|
-
content: nextContent,
|
|
111
|
-
changedKeys: Object.keys(values).length,
|
|
112
|
-
createdSection: true,
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const sectionStart = sectionHeaderMatch.index
|
|
117
|
-
const sectionBodyStart = sectionStart + sectionHeaderMatch[0].length
|
|
118
|
-
const remaining = content.slice(sectionBodyStart)
|
|
119
|
-
const nextHeaderMatch = remaining.match(/\n(?=\[[^\]]+\]\s*$)/m)
|
|
120
|
-
const sectionEnd =
|
|
121
|
-
nextHeaderMatch && typeof nextHeaderMatch.index === 'number'
|
|
122
|
-
? sectionBodyStart + nextHeaderMatch.index + 1
|
|
123
|
-
: content.length
|
|
124
|
-
|
|
125
|
-
const beforeSection = content.slice(0, sectionStart)
|
|
126
|
-
const originalSectionText = content.slice(sectionStart, sectionEnd)
|
|
127
|
-
const trailing = content.slice(sectionEnd)
|
|
128
|
-
const sectionLines = originalSectionText.split('\n')
|
|
129
|
-
|
|
130
|
-
for (const [key, value] of Object.entries(values)) {
|
|
131
|
-
const desiredLine = `${key} = ${value}`
|
|
132
|
-
const keyPattern = new RegExp(`^${escapeRegExp(key)}\\s*=`, 'm')
|
|
133
|
-
const lineIndex = sectionLines.findIndex(line => keyPattern.test(line.trim()))
|
|
134
|
-
|
|
135
|
-
if (lineIndex === -1) {
|
|
136
|
-
let insertIndex = sectionLines.length
|
|
137
|
-
while (insertIndex > 1 && sectionLines[insertIndex - 1] === '') {
|
|
138
|
-
insertIndex--
|
|
139
|
-
}
|
|
140
|
-
sectionLines.splice(insertIndex, 0, desiredLine)
|
|
141
|
-
changedKeys++
|
|
142
|
-
continue
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (sectionLines[lineIndex].trim() !== desiredLine) {
|
|
146
|
-
sectionLines[lineIndex] = desiredLine
|
|
147
|
-
changedKeys++
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const updatedSectionText = ensureTrailingNewline(sectionLines.join('\n'))
|
|
152
|
-
nextContent = `${beforeSection}${updatedSectionText}${trailing}`
|
|
153
|
-
|
|
154
|
-
return { content: nextContent, changedKeys, createdSection }
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function ensureCodexConfig({ codexDir }) {
|
|
158
|
-
const configPath = join(codexDir, 'config.toml')
|
|
159
|
-
let content = ''
|
|
160
|
-
|
|
161
|
-
try {
|
|
162
|
-
content = await fs.readFile(configPath, 'utf8')
|
|
163
|
-
} catch (error) {
|
|
164
|
-
if (error?.code !== 'ENOENT') throw error
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let changedKeys = 0
|
|
168
|
-
let createdSections = 0
|
|
169
|
-
let nextContent = content
|
|
170
|
-
|
|
171
|
-
for (const sectionConfig of REQUIRED_CODEX_CONFIG) {
|
|
172
|
-
const result = upsertTomlSection(nextContent, sectionConfig)
|
|
173
|
-
nextContent = result.content
|
|
174
|
-
changedKeys += result.changedKeys
|
|
175
|
-
if (result.createdSection) createdSections++
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (nextContent !== content || content === '') {
|
|
179
|
-
await fs.writeFile(configPath, ensureTrailingNewline(nextContent), 'utf8')
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
configPath,
|
|
184
|
-
changedKeys,
|
|
185
|
-
createdSections,
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
43
|
async function assertDirExists(path, label) {
|
|
190
44
|
try {
|
|
191
45
|
const st = await fs.stat(path)
|
|
@@ -198,35 +52,6 @@ async function assertDirExists(path, label) {
|
|
|
198
52
|
}
|
|
199
53
|
}
|
|
200
54
|
|
|
201
|
-
async function copyTemplateTree({ srcDir, dstDir }) {
|
|
202
|
-
const files = await collectTemplateFiles(srcDir)
|
|
203
|
-
let mdCount = 0
|
|
204
|
-
let pyCount = 0
|
|
205
|
-
let jsonCount = 0
|
|
206
|
-
|
|
207
|
-
for (const file of files) {
|
|
208
|
-
const rel = relative(srcDir, file)
|
|
209
|
-
const target = join(dstDir, rel)
|
|
210
|
-
await ensureDir(dirname(target))
|
|
211
|
-
await fs.copyFile(file, target)
|
|
212
|
-
|
|
213
|
-
if (file.endsWith('.md')) {
|
|
214
|
-
mdCount++
|
|
215
|
-
} else if (file.endsWith('.py')) {
|
|
216
|
-
pyCount++
|
|
217
|
-
try {
|
|
218
|
-
await fs.chmod(target, 0o755)
|
|
219
|
-
} catch {
|
|
220
|
-
// 忽略权限设置失败
|
|
221
|
-
}
|
|
222
|
-
} else if (file.endsWith('.json')) {
|
|
223
|
-
jsonCount++
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return { total: files.length, md: mdCount, py: pyCount, json: jsonCount }
|
|
228
|
-
}
|
|
229
|
-
|
|
230
55
|
async function copyDirMerge({ srcDir, dstDir }) {
|
|
231
56
|
const files = await collectAllFiles(srcDir)
|
|
232
57
|
|
|
@@ -240,49 +65,28 @@ async function copyDirMerge({ srcDir, dstDir }) {
|
|
|
240
65
|
return { fileCount: files.length }
|
|
241
66
|
}
|
|
242
67
|
|
|
243
|
-
async function copySkillsDirectories({ srcSkillsDir, dstSkillsDir }) {
|
|
244
|
-
const entries = await fs.readdir(srcSkillsDir, { withFileTypes: true })
|
|
245
|
-
let copiedDirs = 0
|
|
246
|
-
let copiedFiles = 0
|
|
247
|
-
|
|
248
|
-
for (const entry of entries) {
|
|
249
|
-
if (!entry.isDirectory()) continue
|
|
250
|
-
const srcDir = join(srcSkillsDir, entry.name)
|
|
251
|
-
const dstDir = join(dstSkillsDir, entry.name)
|
|
252
|
-
await ensureDir(dstDir)
|
|
253
|
-
const stats = await copyDirMerge({ srcDir, dstDir })
|
|
254
|
-
copiedDirs++
|
|
255
|
-
copiedFiles += stats.fileCount
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return { copiedDirs, copiedFiles }
|
|
259
|
-
}
|
|
260
|
-
|
|
261
68
|
export async function runCodexInitial(options = {}) {
|
|
262
69
|
const packageRoot = options.packageRoot
|
|
263
70
|
if (!packageRoot) throw new Error('runCodexInitial: 缺少 packageRoot')
|
|
264
71
|
|
|
265
72
|
const homeDir = options.homeDir || os.homedir()
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
await assertDirExists(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const configStats = await ensureCodexConfig({ codexDir })
|
|
73
|
+
const srcSkillsDir = join(packageRoot, 'skills')
|
|
74
|
+
const targets = [
|
|
75
|
+
{ name: 'codex', dir: join(homeDir, '.codex', 'skills') },
|
|
76
|
+
{ name: 'claude', dir: join(homeDir, '.claude', 'skills') },
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
await assertDirExists(srcSkillsDir, '模板目录 skills')
|
|
80
|
+
|
|
81
|
+
const stats = []
|
|
82
|
+
for (const target of targets) {
|
|
83
|
+
await ensureDir(target.dir)
|
|
84
|
+
const copyStats = await copyDirMerge({ srcDir: srcSkillsDir, dstDir: target.dir })
|
|
85
|
+
stats.push({ ...target, ...copyStats })
|
|
86
|
+
}
|
|
281
87
|
|
|
282
|
-
logger.success(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
`config.toml: 修复 ${configStats.changedKeys} 个配置项,新增 ${configStats.createdSections} 个分组 -> ${configStats.configPath}`,
|
|
287
|
-
)
|
|
88
|
+
logger.success('已初始化 skills 模板')
|
|
89
|
+
for (const target of stats) {
|
|
90
|
+
logger.info(`${target.name} skills: 覆盖复制 ${target.fileCount} 个文件 -> ${target.dir}`)
|
|
91
|
+
}
|
|
288
92
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ranger1/dx",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.92",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"bin/",
|
|
20
20
|
"lib/",
|
|
21
|
-
"
|
|
21
|
+
"skills/",
|
|
22
22
|
"LICENSE",
|
|
23
23
|
"README.md",
|
|
24
24
|
"package.json"
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: backend-layering-audit-fixer
|
|
3
|
+
description: Use when backend、NestJS 分层架构或事务规范审查中,需要检查 Service 是否绕过 Repository 直接访问数据库、Controller 是否跨层注入 Repository、Service 是否伪装 Repository、事务装饰器是否匹配 afterCommit 使用、非 HTTP 场景事务模式是否正确、SSE 端点是否误加事务装饰器、以及是否存在直接 prisma.$transaction() 调用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 后端三层架构与事务规范合规检查
|
|
7
|
+
|
|
8
|
+
## 概览
|
|
9
|
+
|
|
10
|
+
扫描后端代码是否遵守 **Controller → Service → Repository** 三层架构约束及事务规范。默认只输出审计结果和修复建议;用户明确要求时才自动修复。
|
|
11
|
+
|
|
12
|
+
规则来源:`ruler/conventions.md` 第 4 节 "NestJS/Prisma 约定" + 第 5 节 "事务规范"。
|
|
13
|
+
|
|
14
|
+
## 检查项
|
|
15
|
+
|
|
16
|
+
### 分层架构
|
|
17
|
+
|
|
18
|
+
| 编号 | 违规类型 | 说明 |
|
|
19
|
+
|------|----------|------|
|
|
20
|
+
| L1 | Service 直接访问数据库 | Service 注入 `PrismaService` 并调用 `this.prisma.*` |
|
|
21
|
+
| L2 | Service 伪装为 Repository | Service 文件含 `getClient()` / `txHost.tx` 模式,实质是 Repository |
|
|
22
|
+
| L3 | Service 死依赖 | Service 注入 `PrismaService` 但从未使用 |
|
|
23
|
+
| L4 | Controller 跨层调用 | Controller 直接注入 Repository |
|
|
24
|
+
|
|
25
|
+
### 事务规范
|
|
26
|
+
|
|
27
|
+
| 编号 | 违规类型 | 说明 |
|
|
28
|
+
|------|----------|------|
|
|
29
|
+
| T1 | afterCommit 未被排空 | Controller 用 `@Transactional()` 但调用链中存在 `txEvents.afterCommit()` 回调(应改用 `@TransactionalWithAfterCommit()`) |
|
|
30
|
+
| T2 | Service 违规传播类型 | Service 使用 `Propagation.Required` / `RequiresNew` / `Nested`(禁止 Service 自行创建事务) |
|
|
31
|
+
| T3 | 直接 `prisma.$transaction()` | 绕过 `TransactionHost` 抽象,直接调用 `prisma.$transaction()` |
|
|
32
|
+
| T4 | SSE/流式端点加事务装饰器 | `@Sse()` 或流式返回的方法上使用了 `@Transactional()` / `@TransactionalWithAfterCommit()`(会导致 afterCommit 提前排空) |
|
|
33
|
+
| T5 | 非 HTTP 场景事务模式错误 | Subscriber/Scheduler 使用了 `@Transactional()` 装饰器而非 `txHost.withTransaction()` 或 `txEvents.withAfterCommit()` |
|
|
34
|
+
| T6 | 非 HTTP 场景缺少 CLS 作用域 | Scheduler 调用 `txHost.withTransaction()` 前未用 `cls.run()` 创建 CLS 作用域 |
|
|
35
|
+
|
|
36
|
+
## 审计命令
|
|
37
|
+
|
|
38
|
+
**分层架构扫描(并行执行):**
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# L1 + L2 + L3:Service 注入 PrismaService
|
|
42
|
+
rg "PrismaService" apps/backend/src/modules --glob '*.service.ts' -l
|
|
43
|
+
|
|
44
|
+
# L2:Service 含 Repository 模式
|
|
45
|
+
rg "getClient|txHost\.tx" apps/backend/src/modules --glob '*.service.ts' -l
|
|
46
|
+
|
|
47
|
+
# L4:Controller 注入 Repository
|
|
48
|
+
rg "Repository" apps/backend/src/modules --glob '*.controller.ts' -l
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**事务规范扫描(并行执行):**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# T1:找出所有只用 @Transactional() 的 Controller 方法(排除 @TransactionalWithAfterCommit)
|
|
55
|
+
rg "@Transactional\(\)" apps/backend/src/modules --glob '*.controller.ts' -l
|
|
56
|
+
|
|
57
|
+
# T2:Service 违规传播类型
|
|
58
|
+
rg "Propagation\.(Required|RequiresNew|Nested)" apps/backend/src/modules --glob '*.service.ts' -l
|
|
59
|
+
|
|
60
|
+
# T3:直接 prisma.$transaction()
|
|
61
|
+
rg "prisma\.\$transaction" apps/backend/src/modules -l
|
|
62
|
+
|
|
63
|
+
# T4:SSE 端点是否带事务装饰器(需人工确认上下文)
|
|
64
|
+
rg "@Sse\(\)" apps/backend/src/modules --glob '*.controller.ts' -l
|
|
65
|
+
|
|
66
|
+
# T5:Subscriber/Scheduler 误用 @Transactional 装饰器
|
|
67
|
+
rg "@Transactional" apps/backend/src/modules --glob '*.subscriber.ts' --glob '*.task.ts' --glob '*.scheduler*.ts' -l
|
|
68
|
+
|
|
69
|
+
# T6:Scheduler 使用 txHost.withTransaction 但未包 cls.run
|
|
70
|
+
rg "txHost\.withTransaction" apps/backend/src/modules --glob '*.scheduler*.ts' --glob '*.task.ts' -l
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## 执行流程
|
|
74
|
+
|
|
75
|
+
1. 执行上述审计命令,收集命中文件列表
|
|
76
|
+
2. **分层检查**:对每个命中文件,读取构造函数和使用处,判定违规类型:
|
|
77
|
+
- 注入了 `PrismaService` 且有 `this.prisma.*` 调用 → **L1**
|
|
78
|
+
- 注入了 `PrismaService`/`txHost` 且有 `getClient()` → **L2**
|
|
79
|
+
- 注入了 `PrismaService` 但无任何 `this.prisma.*` 调用 → **L3**
|
|
80
|
+
- Controller 文件 import/注入 Repository → **L4**
|
|
81
|
+
3. **事务检查**:
|
|
82
|
+
- T1:对只用 `@Transactional()` 的 Controller,追踪其调用的 Service 方法,检查是否存在 `txEvents.afterCommit()` 调用
|
|
83
|
+
- T2:直接匹配即违规
|
|
84
|
+
- T3:直接匹配即违规
|
|
85
|
+
- T4:对 `@Sse()` 方法,检查同一方法上是否有事务装饰器
|
|
86
|
+
- T5:直接匹配即违规(Subscriber/Scheduler 不应用装饰器声明事务)
|
|
87
|
+
- T6:对命中文件,检查 `txHost.withTransaction()` 调用是否在 `cls.run()` 回调内部
|
|
88
|
+
4. 输出审计报告(按模块分组,标注违规类型和行号)
|
|
89
|
+
5. 仅当用户明确要求修复时,才执行修复
|
|
90
|
+
|
|
91
|
+
## 修复策略
|
|
92
|
+
|
|
93
|
+
### 分层架构
|
|
94
|
+
|
|
95
|
+
**L1:Service 直接访问数据库**
|
|
96
|
+
- 将数据库查询逻辑提取到对应 Repository(新建或复用已有)
|
|
97
|
+
- Service 改为注入 Repository 调用
|
|
98
|
+
|
|
99
|
+
**L2:Service 伪装为 Repository**
|
|
100
|
+
- 重命名文件为 `*.repository.ts`,类名改为 `*Repository`
|
|
101
|
+
- 更新所有引用(import、Module providers、注入处)
|
|
102
|
+
- 确认 Service 层消费者改为注入新 Repository
|
|
103
|
+
|
|
104
|
+
**L3:Service 死依赖**
|
|
105
|
+
- 移除构造函数中 `PrismaService` 注入
|
|
106
|
+
- 移除对应 import 语句
|
|
107
|
+
- 运行 lint 确认无残留
|
|
108
|
+
|
|
109
|
+
**L4:Controller 跨层调用**
|
|
110
|
+
- Controller 中的 Repository 调用下沉到 Service
|
|
111
|
+
- Controller 改为调用 Service 方法
|
|
112
|
+
|
|
113
|
+
### 事务规范
|
|
114
|
+
|
|
115
|
+
**T1:afterCommit 未被排空**
|
|
116
|
+
- 将 Controller 方法的 `@Transactional()` 改为 `@TransactionalWithAfterCommit()`
|
|
117
|
+
- 或确认调用链中确实不存在 `afterCommit()` 回调(则无需修改)
|
|
118
|
+
|
|
119
|
+
**T2:Service 违规传播类型**
|
|
120
|
+
- 改为 `Propagation.Mandatory`(必须在事务中调用)或 `Propagation.Supports`(有事务则加入)
|
|
121
|
+
- 事务边界上移到 Controller 层
|
|
122
|
+
|
|
123
|
+
**T3:直接 `prisma.$transaction()`**
|
|
124
|
+
- 改用 `txHost.withTransaction()` 或由 Controller 层 `@Transactional()` 声明事务边界
|
|
125
|
+
- Repository 内通过 `txHost.tx` 自动参与事务
|
|
126
|
+
|
|
127
|
+
**T4:SSE/流式端点加事务装饰器**
|
|
128
|
+
- 移除事务装饰器
|
|
129
|
+
- 如需事务操作,在流式逻辑内部用 `cls.run()` + `txHost.withTransaction()` 局部处理
|
|
130
|
+
|
|
131
|
+
**T5:非 HTTP 场景误用 @Transactional 装饰器**
|
|
132
|
+
- Subscriber:改用 `txEvents.withAfterCommit()` 包裹处理逻辑
|
|
133
|
+
- Scheduler/Task:改用 `cls.run()` + `txHost.withTransaction()`
|
|
134
|
+
|
|
135
|
+
**T6:非 HTTP 场景缺少 CLS 作用域**
|
|
136
|
+
- 在 `txHost.withTransaction()` 外层包裹 `cls.run()`:
|
|
137
|
+
```typescript
|
|
138
|
+
await this.cls.run(async () => {
|
|
139
|
+
await this.txHost.withTransaction(async () => { /* ... */ })
|
|
140
|
+
})
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## 排除项
|
|
144
|
+
|
|
145
|
+
以下场景不视为违规:
|
|
146
|
+
- `*.repository.ts` 文件中使用 `PrismaService` / `getClient()` / `txHost`(Repository 正常职责)
|
|
147
|
+
- `prisma/` 目录下的基础设施文件(`prisma.service.ts` 等)
|
|
148
|
+
- `common/` 目录下的事务基础设施(`TransactionEventsService`、`AfterCommitInterceptor` 等)
|
|
149
|
+
- `*.spec.ts` / `e2e/` 测试文件
|
|
150
|
+
- Subscriber / Scheduler 中通过 `txHost.withTransaction()` 管理事务(正确模式,但其中数据库查询仍应走 Repository)
|
|
151
|
+
|
|
152
|
+
## 审计报告模板
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
## 后端分层架构与事务规范审计报告
|
|
156
|
+
|
|
157
|
+
### 违规汇总
|
|
158
|
+
|
|
159
|
+
**分层架构:**
|
|
160
|
+
- L1(Service 直接访问 DB):N 处
|
|
161
|
+
- L2(Service 伪装 Repository):N 处
|
|
162
|
+
- L3(Service 死依赖):N 处
|
|
163
|
+
- L4(Controller 跨层调用):N 处
|
|
164
|
+
|
|
165
|
+
**事务规范:**
|
|
166
|
+
- T1(afterCommit 未被排空):N 处
|
|
167
|
+
- T2(Service 违规传播类型):N 处
|
|
168
|
+
- T3(直接 prisma.$transaction):N 处
|
|
169
|
+
- T4(SSE 端点加事务装饰器):N 处
|
|
170
|
+
- T5(非 HTTP 场景误用装饰器):N 处
|
|
171
|
+
- T6(非 HTTP 缺少 CLS 作用域):N 处
|
|
172
|
+
|
|
173
|
+
### 详细列表
|
|
174
|
+
|
|
175
|
+
#### [模块名]
|
|
176
|
+
| 文件 | 违规类型 | 行号 | 说明 |
|
|
177
|
+
|------|----------|------|------|
|
|
178
|
+
| ... | L1 | 39 | `this.prisma.user.findMany()` |
|
|
179
|
+
| ... | T1 | 85 | `@Transactional()` 但调用链含 `afterCommit()` |
|
|
180
|
+
```
|
|
@@ -52,18 +52,11 @@ bash "$CODEX_HOME/skills/doctor/scripts/doctor.sh" --max-rounds 3
|
|
|
52
52
|
|
|
53
53
|
## 脚本职责
|
|
54
54
|
|
|
55
|
-
- 并行检测:`python3`、`python` 别名、`pnpm`、`dx`、`agent-browser`、`rg`、`multi_agent
|
|
55
|
+
- 并行检测:`python3`、`python` 别名、`pnpm`、`dx`、`agent-browser`、`rg`、`multi_agent`。
|
|
56
56
|
- 自动修复:按平台选择安装器修复缺失项。
|
|
57
|
-
- 自动修复:确保 `~/.codex/config.toml` 含以下目标值(缺失补齐、值不符覆盖):
|
|
58
|
-
- `[features] multi_agent = true`
|
|
59
|
-
- `[agents] max_threads = 15`
|
|
60
|
-
- `[agents.fixer] description/config_file`
|
|
61
|
-
- `[agents.orchestrator] description/config_file`
|
|
62
|
-
- `[agents.reviewer] description/config_file`
|
|
63
|
-
- `[agents.spark] description/config_file`
|
|
64
57
|
- 强制执行:每轮都运行 `pnpm add -g @ranger1/dx@latest && dx initial`。
|
|
65
58
|
- agent-browser:安装/升级并执行 Chromium 安装。
|
|
66
|
-
-
|
|
59
|
+
- 结果输出:展示每项状态、版本、关键信息;全部通过则退出 0,否则最多三轮后退出 1。
|
|
67
60
|
|
|
68
61
|
## 注意
|
|
69
62
|
|