@weotro/dx 0.1.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/LICENSE +21 -0
- package/README.md +755 -0
- package/bin/dx-with-version-env.js +8 -0
- package/bin/dx.js +187 -0
- package/lib/artifact-deploy/artifact-builder.js +144 -0
- package/lib/artifact-deploy/config.js +180 -0
- package/lib/artifact-deploy/remote-script.js +301 -0
- package/lib/artifact-deploy/remote-transport.js +86 -0
- package/lib/artifact-deploy.js +70 -0
- package/lib/backend-artifact-deploy/artifact-builder.js +267 -0
- package/lib/backend-artifact-deploy/config.js +218 -0
- package/lib/backend-artifact-deploy/path-utils.js +18 -0
- package/lib/backend-artifact-deploy/remote-phases.js +14 -0
- package/lib/backend-artifact-deploy/remote-result.js +44 -0
- package/lib/backend-artifact-deploy/remote-script.js +507 -0
- package/lib/backend-artifact-deploy/remote-transport.js +123 -0
- package/lib/backend-artifact-deploy/rollback.js +5 -0
- package/lib/backend-artifact-deploy/runtime-package.js +46 -0
- package/lib/backend-artifact-deploy.js +91 -0
- package/lib/backend-package.js +674 -0
- package/lib/cli/args.js +38 -0
- package/lib/cli/command-result.js +1 -0
- package/lib/cli/commands/contracts.js +60 -0
- package/lib/cli/commands/core.js +533 -0
- package/lib/cli/commands/db.js +231 -0
- package/lib/cli/commands/deploy.js +175 -0
- package/lib/cli/commands/env.js +120 -0
- package/lib/cli/commands/export.js +39 -0
- package/lib/cli/commands/package.js +22 -0
- package/lib/cli/commands/release.js +55 -0
- package/lib/cli/commands/stack.js +427 -0
- package/lib/cli/commands/start.js +58 -0
- package/lib/cli/commands/worktree.js +145 -0
- package/lib/cli/dx-cli.js +1072 -0
- package/lib/cli/flags.js +123 -0
- package/lib/cli/help-model.js +222 -0
- package/lib/cli/help-renderer.js +137 -0
- package/lib/cli/help-schema.js +552 -0
- package/lib/cli/help.js +141 -0
- package/lib/cli/index.js +4 -0
- package/lib/cli/nx-command.js +13 -0
- package/lib/codex-initial.js +271 -0
- package/lib/confirm.js +213 -0
- package/lib/env-policy.js +134 -0
- package/lib/env-profile.js +435 -0
- package/lib/env.js +261 -0
- package/lib/exec.js +692 -0
- package/lib/logger.js +239 -0
- package/lib/nx-ignore.js +45 -0
- package/lib/run-with-version-env.js +163 -0
- package/lib/sdk-build.js +424 -0
- package/lib/start-dev.js +401 -0
- package/lib/telegram-webhook.js +431 -0
- package/lib/validate-env.js +317 -0
- package/lib/vercel-deploy.js +549 -0
- package/lib/version.js +14 -0
- package/lib/worktree.js +1052 -0
- package/package.json +45 -0
- package/skills/create-issue/SKILL.md +90 -0
- package/skills/delivering-design-handoff/SKILL.md +290 -0
- package/skills/doctor/SKILL.md +76 -0
- package/skills/gh-dependabot-cleanup/SKILL.md +54 -0
- package/skills/gh-dependabot-cleanup/agents/openai.yaml +7 -0
- package/skills/git-release/SKILL.md +194 -0
- package/skills/git-release/agents/openai.yaml +7 -0
- package/skills/online-debug-guard/SKILL.md +111 -0
- package/skills/ship-issue-pr/SKILL.md +676 -0
- package/skills/stagewise-ui-debugging/SKILL.md +48 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const COMMAND_NOT_HANDLED = Symbol('COMMAND_NOT_HANDLED')
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { logger } from '../../logger.js'
|
|
4
|
+
import { execManager } from '../../exec.js'
|
|
5
|
+
|
|
6
|
+
export async function handleContracts(cli, args = []) {
|
|
7
|
+
const action = args[0] || 'generate'
|
|
8
|
+
if (!['generate', 'pull'].includes(action)) {
|
|
9
|
+
logger.error(`不支持的 contracts 子命令: ${action}`)
|
|
10
|
+
logger.info(`用法: ${cli.invocation} contracts [generate|pull]`)
|
|
11
|
+
process.exitCode = 1
|
|
12
|
+
return
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
cli.ensureRepoRoot()
|
|
16
|
+
|
|
17
|
+
// starmomo/ai-monorepo compatibility: packages/api-contracts is expected
|
|
18
|
+
const contractsRoot = join(process.cwd(), 'packages', 'api-contracts')
|
|
19
|
+
if (!existsSync(contractsRoot)) {
|
|
20
|
+
logger.error(`未找到 contracts 目录: ${contractsRoot}`)
|
|
21
|
+
logger.info('期望存在 packages/api-contracts,用于输出生成的 Zod 合约。')
|
|
22
|
+
process.exitCode = 1
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger.step('导出 OpenAPI 并生成 Zod 合约')
|
|
27
|
+
|
|
28
|
+
// 1) Export OpenAPI spec to dist/openapi/backend.json
|
|
29
|
+
await execManager.executeCommand('npx nx run backend:swagger', {
|
|
30
|
+
app: 'backend',
|
|
31
|
+
flags: cli.flags,
|
|
32
|
+
env: { NX_CACHE: 'false', SKIP_PRISMA_CONNECT: 'true' },
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// 2) Generate zod client
|
|
36
|
+
const outputDir = join(contractsRoot, 'src', 'generated')
|
|
37
|
+
mkdirSync(outputDir, { recursive: true })
|
|
38
|
+
|
|
39
|
+
const baseUrl = process.env.OPENAPI_BASE_URL || 'http://localhost:3000/api/v1'
|
|
40
|
+
logger.info(`使用 API 基地址: ${baseUrl}`)
|
|
41
|
+
|
|
42
|
+
const generatorCommand = [
|
|
43
|
+
'pnpm exec openapi-zod-client',
|
|
44
|
+
'dist/openapi/backend.json',
|
|
45
|
+
'--output packages/api-contracts/src/generated/backend.ts',
|
|
46
|
+
'--api-client-name aiBackendClient',
|
|
47
|
+
`--base-url "${baseUrl}"`,
|
|
48
|
+
'--with-alias',
|
|
49
|
+
'--with-docs',
|
|
50
|
+
'--with-deprecated',
|
|
51
|
+
'--export-schemas',
|
|
52
|
+
'--prettier prettier.config.js',
|
|
53
|
+
].join(' ')
|
|
54
|
+
|
|
55
|
+
await execManager.executeCommand(generatorCommand, {
|
|
56
|
+
flags: cli.flags,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
logger.success('Zod 合约已更新(packages/api-contracts)')
|
|
60
|
+
}
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, relative } from 'node:path'
|
|
3
|
+
import { logger } from '../../logger.js'
|
|
4
|
+
import { confirmManager } from '../../confirm.js'
|
|
5
|
+
import { execManager } from '../../exec.js'
|
|
6
|
+
import { showHelp, showCommandHelp } from '../help.js'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TEST_WORKERS = 8
|
|
9
|
+
|
|
10
|
+
export function handleHelp(cli, args = []) {
|
|
11
|
+
if (args[0]) showCommandHelp(args[0], cli)
|
|
12
|
+
else showHelp(cli)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function handleBuild(cli, args) {
|
|
16
|
+
const target = args[0] || 'all'
|
|
17
|
+
const environment = cli.determineEnvironment()
|
|
18
|
+
const envKey = cli.normalizeEnvKey(environment)
|
|
19
|
+
const explicitEnv =
|
|
20
|
+
Boolean(cli.flags.dev || cli.flags.prod || cli.flags.staging || cli.flags.test || cli.flags.e2e)
|
|
21
|
+
|
|
22
|
+
const buildConfig = cli.commands.build[target]
|
|
23
|
+
if (!buildConfig) {
|
|
24
|
+
logger.error(`未找到构建目标: ${target}`)
|
|
25
|
+
process.exitCode = 1
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
logger.step(`构建 ${target} (${environment})`)
|
|
30
|
+
|
|
31
|
+
// 处理嵌套配置
|
|
32
|
+
let config = buildConfig
|
|
33
|
+
if (typeof config === 'object' && !config.command) {
|
|
34
|
+
const supportsCurrentEnv = Boolean(config[envKey])
|
|
35
|
+
if (explicitEnv && !supportsCurrentEnv) {
|
|
36
|
+
const envFlag = cli.getEnvironmentFlagExample(envKey) || `--${envKey}`
|
|
37
|
+
logger.error(`构建目标 ${target} 不支持 ${envFlag} 环境`)
|
|
38
|
+
logger.info('显式传入环境标志时,必须是该 target 实际支持的环境。')
|
|
39
|
+
const available = ['development', 'staging', 'production', 'test', 'e2e']
|
|
40
|
+
.filter(key => key in config)
|
|
41
|
+
.map(key => cli.getEnvironmentFlagExample(key) || `--${key}`)
|
|
42
|
+
if (available.length > 0) {
|
|
43
|
+
logger.info(`支持的环境: ${available.join(', ')}`)
|
|
44
|
+
logger.info(`示例: ${cli.invocation} build ${target} ${available[0]}`)
|
|
45
|
+
if (available.length > 1) {
|
|
46
|
+
logger.info(`示例: ${cli.invocation} build ${target} ${available[1]}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
process.exitCode = 1
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 严格按显式环境选择配置,不做环境回退
|
|
54
|
+
if (config[envKey]) config = config[envKey]
|
|
55
|
+
else config = null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!config) {
|
|
59
|
+
logger.error(`构建目标 ${target} 未提供 ${cli.getEnvironmentFlagExample(envKey) || envKey} 环境配置`)
|
|
60
|
+
process.exitCode = 1
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (config.concurrent) {
|
|
65
|
+
await cli.handleConcurrentCommands(config.commands, 'build', envKey)
|
|
66
|
+
} else if (config.sequential) {
|
|
67
|
+
await cli.handleSequentialCommands(config.commands, envKey)
|
|
68
|
+
} else {
|
|
69
|
+
await cli.executeCommand(config)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function handleTest(cli, args) {
|
|
74
|
+
const type = args[0] || 'e2e'
|
|
75
|
+
const target = args[1] || 'all'
|
|
76
|
+
const testPaths = args.slice(2) // unit 可选多个测试文件路径;e2e 只使用第一个
|
|
77
|
+
const testPath = testPaths[0]
|
|
78
|
+
|
|
79
|
+
// 解析 -t 参数用于指定特定测试用例(使用原始参数列表)
|
|
80
|
+
const allArgs = cli.args // 使用原始参数列表包含所有标志
|
|
81
|
+
const testNamePattern = resolveTestNamePattern(allArgs)
|
|
82
|
+
const passthroughArgs = resolvePassthroughArgs(allArgs)
|
|
83
|
+
|
|
84
|
+
// 根据测试类型自动设置环境标志
|
|
85
|
+
if (type === 'e2e' && !cli.flags.e2e) {
|
|
86
|
+
cli.flags.e2e = true
|
|
87
|
+
} else if (type === 'unit' && !cli.flags.test) {
|
|
88
|
+
cli.flags.test = true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const typeConfig = cli.commands.test[type]
|
|
92
|
+
let testConfig = typeConfig?.[target]
|
|
93
|
+
if (!testConfig && typeConfig?.command) {
|
|
94
|
+
testConfig = typeConfig
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!testConfig) {
|
|
98
|
+
logger.error(`未找到测试配置: ${type}.${target}`)
|
|
99
|
+
process.exit(1)
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (type === 'e2e' && testConfig.requiresPath && testPath) {
|
|
104
|
+
if (!testConfig.fileCommand) {
|
|
105
|
+
logger.error(`测试配置错误: test.${type}.${target} 已启用 requiresPath,必须配置 fileCommand`)
|
|
106
|
+
process.exit(1)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const fileCommand = String(testConfig.fileCommand)
|
|
110
|
+
if (!fileCommand.includes('{TEST_PATH}')) {
|
|
111
|
+
logger.error(`测试配置错误: test.${type}.${target} 的 fileCommand 必须包含 {TEST_PATH}`)
|
|
112
|
+
process.exit(1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let directTargetCommand = fileCommand
|
|
116
|
+
let directTarget = resolveNxTargetDirectCommand(cli, directTargetCommand, ['test:e2e'])
|
|
117
|
+
if (!directTarget && hasTrailingWorkerComment(fileCommand)) {
|
|
118
|
+
directTargetCommand = String(testConfig.command || '')
|
|
119
|
+
directTarget = resolveNxTargetDirectCommand(cli, directTargetCommand, ['test:e2e'])
|
|
120
|
+
}
|
|
121
|
+
const normalizedTestPath = normalizeE2eTestPathForCommand(
|
|
122
|
+
cli,
|
|
123
|
+
directTarget ? directTargetCommand : fileCommand,
|
|
124
|
+
testPath,
|
|
125
|
+
)
|
|
126
|
+
let command = directTarget
|
|
127
|
+
? `${directTarget.command} ${shellEscape(normalizedTestPath)}`
|
|
128
|
+
: fileCommand.replace('{TEST_PATH}', shellEscape(normalizedTestPath))
|
|
129
|
+
|
|
130
|
+
if (testNamePattern) {
|
|
131
|
+
command += ` -t ${shellEscape(testNamePattern)}`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (passthroughArgs.length > 0) {
|
|
135
|
+
command += ` ${passthroughArgs.map(shellEscape).join(' ')}`
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
testConfig = {
|
|
139
|
+
...testConfig,
|
|
140
|
+
command: command,
|
|
141
|
+
...(directTarget?.cwd ? { cwd: directTarget.cwd } : {}),
|
|
142
|
+
description: testNamePattern
|
|
143
|
+
? `运行单个E2E测试文件的特定用例: ${testPath} -> ${testNamePattern}`
|
|
144
|
+
: `运行单个E2E测试文件: ${testPath}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (testNamePattern) {
|
|
148
|
+
logger.step(`运行 ${type} 测试用例: ${testNamePattern} (文件: ${testPath})`)
|
|
149
|
+
} else {
|
|
150
|
+
logger.step(`运行单个 ${type} 测试: ${testPath}`)
|
|
151
|
+
}
|
|
152
|
+
} else if (type === 'unit' && testPaths.length > 0) {
|
|
153
|
+
let command = String(testConfig.command).trim()
|
|
154
|
+
const useDirectPathArg = shouldUseDirectPathArg(command)
|
|
155
|
+
const normalizedTestPaths = testPaths.map(path =>
|
|
156
|
+
useDirectPathArg
|
|
157
|
+
? normalizeUnitTestPathForCommand(cli, command, path)
|
|
158
|
+
: path
|
|
159
|
+
)
|
|
160
|
+
const forwardedArgs = useDirectPathArg
|
|
161
|
+
? normalizedTestPaths.map(shellEscape)
|
|
162
|
+
: [`--runTestsByPath ${normalizedTestPaths.map(shellEscape).join(' ')}`]
|
|
163
|
+
|
|
164
|
+
if (testNamePattern) {
|
|
165
|
+
forwardedArgs.push(`-t ${shellEscape(testNamePattern)}`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (passthroughArgs.length > 0) {
|
|
169
|
+
forwardedArgs.push(...passthroughArgs.map(shellEscape))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
command += ` ${forwardedArgs.join(' ')}`
|
|
173
|
+
|
|
174
|
+
testConfig = {
|
|
175
|
+
...testConfig,
|
|
176
|
+
command,
|
|
177
|
+
description: testNamePattern
|
|
178
|
+
? `运行单元测试文件的特定用例: ${testPaths.join(', ')} -> ${testNamePattern}`
|
|
179
|
+
: `运行单元测试文件: ${testPaths.join(', ')}`,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (testNamePattern) {
|
|
183
|
+
logger.step(`运行 ${type} 测试用例: ${testNamePattern} (文件: ${testPaths.join(', ')})`)
|
|
184
|
+
} else {
|
|
185
|
+
logger.step(`运行 ${type} 测试: ${testPaths.join(', ')}`)
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
logger.step(`运行 ${type} 测试`)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
testConfig = {
|
|
192
|
+
...testConfig,
|
|
193
|
+
command: appendDefaultTestWorkers(cli, testConfig.command, type),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await cli.executeCommand(testConfig)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function shellEscape(value) {
|
|
200
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function resolveTestNamePattern(args = []) {
|
|
204
|
+
const aliases = ['-t', '--name', '--test-name-pattern']
|
|
205
|
+
for (let i = 0; i < args.length; i++) {
|
|
206
|
+
if (!aliases.includes(args[i])) continue
|
|
207
|
+
if (i + 1 < args.length) return args[i + 1]
|
|
208
|
+
}
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolvePassthroughArgs(args = []) {
|
|
213
|
+
const index = args.indexOf('--')
|
|
214
|
+
if (index === -1) return []
|
|
215
|
+
return args.slice(index + 1)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function shouldUseDirectPathArg(command) {
|
|
219
|
+
const text = String(command || '')
|
|
220
|
+
return (
|
|
221
|
+
/\bnx\s+test\b/.test(text) ||
|
|
222
|
+
/\bnx\.js\s+test\b/.test(text) ||
|
|
223
|
+
/\bvitest\s+run\b/.test(text)
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function appendDefaultTestWorkers(cli, command, type) {
|
|
228
|
+
const text = String(command || '').trim()
|
|
229
|
+
const flag = getDefaultWorkerFlag(type, text)
|
|
230
|
+
if (!flag) return command
|
|
231
|
+
if (!text || hasWorkerFlag(text, flag)) return command
|
|
232
|
+
if (type === 'unit' && commandUsesRunInBand(cli, text)) return command
|
|
233
|
+
|
|
234
|
+
if (isNodeEvalCommandWithoutArgSeparator(text)) {
|
|
235
|
+
return `${text} -- ${flag}=${DEFAULT_TEST_WORKERS}`
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return `${text} ${flag}=${DEFAULT_TEST_WORKERS}`
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function getDefaultWorkerFlag(type, command = '') {
|
|
242
|
+
if (type === 'e2e') {
|
|
243
|
+
return /\bvitest\s+run\b/.test(String(command || '')) ? '--maxWorkers' : '--workers'
|
|
244
|
+
}
|
|
245
|
+
if (type === 'unit') return '--maxWorkers'
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function hasWorkerFlag(command, flag) {
|
|
250
|
+
const escapedFlag = flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
251
|
+
return new RegExp(`(^|[\\s'"])${escapedFlag}(=|\\s|['"]|$)`).test(String(command || ''))
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function hasTrailingWorkerComment(command) {
|
|
255
|
+
return /\s+#\s*--(?:maxWorkers|workers)(?:=\S+)?\s*$/.test(String(command || ''))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hasRunInBandFlag(command) {
|
|
259
|
+
return /(^|[\s'"])(--runInBand|--run-in-band|-i)(=|\s|['"]|$)/.test(String(command || ''))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function commandUsesRunInBand(cli, command) {
|
|
263
|
+
if (hasRunInBandFlag(command)) return true
|
|
264
|
+
|
|
265
|
+
const script = resolveNxUnitPackageTestScript(cli, command)
|
|
266
|
+
return hasRunInBandFlag(script)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function resolveNxUnitPackageTestScript(cli, command) {
|
|
270
|
+
const projectRoot = cli?.projectRoot || process.cwd()
|
|
271
|
+
const nxResolution = extractNxTarget(command, ['test'])
|
|
272
|
+
if (!nxResolution?.project) return null
|
|
273
|
+
|
|
274
|
+
const packagePath = join(projectRoot, 'apps', nxResolution.project, 'package.json')
|
|
275
|
+
if (!existsSync(packagePath)) return null
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'))
|
|
279
|
+
return typeof pkg?.scripts?.test === 'string' ? pkg.scripts.test : null
|
|
280
|
+
} catch {
|
|
281
|
+
return null
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function isNodeEvalCommandWithoutArgSeparator(command) {
|
|
286
|
+
return /(^|\s)node\s+-e\s/.test(String(command || '')) && !/\s--(\s|$)/.test(String(command || ''))
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeUnitTestPathForCommand(cli, command, testPath) {
|
|
290
|
+
const rawPath = String(testPath || '')
|
|
291
|
+
if (!rawPath) return rawPath
|
|
292
|
+
|
|
293
|
+
if (/\bvitest\s+run\b/.test(String(command || ''))) {
|
|
294
|
+
return rawPath
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const projectCwd = resolveNxTargetProjectCwd(cli, command, ['test'])
|
|
298
|
+
if (!projectCwd) return rawPath
|
|
299
|
+
|
|
300
|
+
const projectRoot = cli?.projectRoot || process.cwd()
|
|
301
|
+
const absoluteProjectCwd = join(projectRoot, projectCwd)
|
|
302
|
+
const absoluteTestPath = join(projectRoot, rawPath)
|
|
303
|
+
const relativePath = relative(absoluteProjectCwd, absoluteTestPath)
|
|
304
|
+
|
|
305
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
306
|
+
return rawPath
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return relativePath
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeE2eTestPathForCommand(cli, command, testPath) {
|
|
313
|
+
const rawPath = String(testPath || '')
|
|
314
|
+
if (!rawPath) return rawPath
|
|
315
|
+
|
|
316
|
+
const projectCwd = resolveNxTargetProjectCwd(cli, command, ['test:e2e'])
|
|
317
|
+
if (!projectCwd) return rawPath
|
|
318
|
+
|
|
319
|
+
const projectRoot = cli?.projectRoot || process.cwd()
|
|
320
|
+
const absoluteProjectCwd = join(projectRoot, projectCwd)
|
|
321
|
+
const absoluteTestPath = join(projectRoot, rawPath)
|
|
322
|
+
const relativePath = relative(absoluteProjectCwd, absoluteTestPath)
|
|
323
|
+
|
|
324
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
325
|
+
return rawPath
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return relativePath
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function resolveNxTargetDirectCommand(cli, command, targetNames = []) {
|
|
332
|
+
const projectRoot = cli?.projectRoot || process.cwd()
|
|
333
|
+
const nxResolution = extractNxTarget(command, targetNames)
|
|
334
|
+
if (!nxResolution) return null
|
|
335
|
+
|
|
336
|
+
const projectDir = join(projectRoot, 'apps', nxResolution.project)
|
|
337
|
+
const projectConfigPath = join(projectDir, 'project.json')
|
|
338
|
+
if (!existsSync(projectConfigPath)) return null
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const projectConfig = JSON.parse(readFileSync(projectConfigPath, 'utf8'))
|
|
342
|
+
const resolvedTarget = projectConfig?.targets?.[nxResolution.target]
|
|
343
|
+
const directCommand = resolvedTarget?.options?.command
|
|
344
|
+
if (typeof directCommand !== 'string' || directCommand.trim().length === 0) {
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
const cwd = resolvedTarget?.options?.cwd
|
|
348
|
+
return {
|
|
349
|
+
command: directCommand.trim(),
|
|
350
|
+
cwd: typeof cwd === 'string' && cwd.trim().length > 0 ? cwd.trim() : null,
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function resolveNxTargetProjectCwd(cli, command, targetNames = []) {
|
|
358
|
+
const projectRoot = cli?.projectRoot || process.cwd()
|
|
359
|
+
const nxResolution = extractNxTarget(command, targetNames)
|
|
360
|
+
if (!nxResolution) return null
|
|
361
|
+
|
|
362
|
+
const projectDir = join(projectRoot, 'apps', nxResolution.project)
|
|
363
|
+
const projectConfigPath = join(projectDir, 'project.json')
|
|
364
|
+
if (!existsSync(projectConfigPath)) return null
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
const projectConfig = JSON.parse(readFileSync(projectConfigPath, 'utf8'))
|
|
368
|
+
const resolvedTarget = projectConfig?.targets?.[nxResolution.target]
|
|
369
|
+
const cwd = resolvedTarget?.options?.cwd
|
|
370
|
+
if (typeof cwd === 'string' && cwd.trim().length > 0) {
|
|
371
|
+
return cwd
|
|
372
|
+
}
|
|
373
|
+
if (nxResolution.target === 'test:e2e') {
|
|
374
|
+
const e2eDir = join(projectDir, 'e2e')
|
|
375
|
+
if (existsSync(e2eDir)) {
|
|
376
|
+
return relative(projectRoot, e2eDir)
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return relative(projectRoot, dirname(projectConfigPath))
|
|
380
|
+
} catch {
|
|
381
|
+
return null
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function extractNxTarget(command, targetNames = []) {
|
|
386
|
+
const text = String(command || '').trim()
|
|
387
|
+
const names = Array.isArray(targetNames) && targetNames.length > 0 ? targetNames : ['test']
|
|
388
|
+
|
|
389
|
+
for (const targetName of names) {
|
|
390
|
+
if (targetName === 'test') {
|
|
391
|
+
const directMatch = text.match(/\bnx(?:\.js)?\s+test\s+([^\s]+)/)
|
|
392
|
+
if (directMatch?.[1]) {
|
|
393
|
+
return { project: directMatch[1], target: 'test' }
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const escapedTarget = targetName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
|
|
398
|
+
const runMatch = text.match(new RegExp(`\\bnx(?:\\.js)?\\s+run\\s+([^:\\s]+):${escapedTarget}\\b`))
|
|
399
|
+
if (runMatch?.[1]) {
|
|
400
|
+
return { project: runMatch[1], target: targetName }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const directColonMatch = text.match(new RegExp(`\\bnx(?:\\.js)?\\s+${escapedTarget}\\s+([^\\s]+)`))
|
|
404
|
+
if (directColonMatch?.[1]) {
|
|
405
|
+
return { project: directColonMatch[1], target: targetName }
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export async function handleLint(cli, args) {
|
|
413
|
+
void args
|
|
414
|
+
const baseConfig = cli.commands.lint
|
|
415
|
+
if (!baseConfig || !baseConfig.command) {
|
|
416
|
+
logger.error('未找到 lint 命令配置')
|
|
417
|
+
process.exitCode = 1
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const config = { ...baseConfig }
|
|
422
|
+
|
|
423
|
+
if (cli.flags.fix) {
|
|
424
|
+
logger.step('运行代码检查(自动修复模式: --fix)')
|
|
425
|
+
const cmd = String(config.command)
|
|
426
|
+
// 若已包含 ` -- ` 分隔符,直接在末尾追加 --fix;否则通过 `--` 传递给 Nx 下游
|
|
427
|
+
config.command = cmd.includes(' -- ')
|
|
428
|
+
? `${cmd} --fix`
|
|
429
|
+
: `${cmd} -- --fix`
|
|
430
|
+
} else {
|
|
431
|
+
logger.step('运行代码检查')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
await cli.executeCommand(config)
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function handleClean(cli, args) {
|
|
438
|
+
const target = args[0] || 'all'
|
|
439
|
+
const cleanConfig = cli.commands.clean[target]
|
|
440
|
+
|
|
441
|
+
if (!cleanConfig) {
|
|
442
|
+
logger.error(`未找到清理目标: ${target}`)
|
|
443
|
+
process.exitCode = 1
|
|
444
|
+
return
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 危险操作确认
|
|
448
|
+
if (cleanConfig.dangerous) {
|
|
449
|
+
const confirmed = await confirmManager.confirmDangerous(
|
|
450
|
+
`清理操作: ${target}`,
|
|
451
|
+
'当前环境',
|
|
452
|
+
cli.flags.Y
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if (!confirmed) {
|
|
456
|
+
logger.info('操作已取消')
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
logger.step(`清理 ${target}`)
|
|
462
|
+
await cli.executeCommand(cleanConfig)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function handleCache(cli, args) {
|
|
466
|
+
const action = args[0] || 'clear'
|
|
467
|
+
const cacheConfig = cli.commands.cache?.[action]
|
|
468
|
+
|
|
469
|
+
if (!cacheConfig) {
|
|
470
|
+
logger.error(`未找到缓存操作: ${action}`)
|
|
471
|
+
logger.info('用法: dx cache clear')
|
|
472
|
+
process.exitCode = 1
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 危险操作确认
|
|
477
|
+
if (cacheConfig.dangerous) {
|
|
478
|
+
const confirmed = await confirmManager.confirmDangerous(
|
|
479
|
+
`缓存清理: ${action}`,
|
|
480
|
+
'当前环境',
|
|
481
|
+
cli.flags.Y
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
if (!confirmed) {
|
|
485
|
+
logger.info('操作已取消')
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 二次确认(更醒目):强调将清理全局 pnpm store 与 ~/.pnpm-store
|
|
490
|
+
if (!cli.flags.Y && action === 'clear') {
|
|
491
|
+
const second = await confirmManager.confirm(
|
|
492
|
+
'二次确认:将清理全局 pnpm store 与 ~/.pnpm-store,可能影响其他项目,是否继续?',
|
|
493
|
+
false,
|
|
494
|
+
false
|
|
495
|
+
)
|
|
496
|
+
if (!second) {
|
|
497
|
+
logger.info('操作已取消')
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
logger.step(`执行缓存操作: ${action}`)
|
|
504
|
+
await cli.executeCommand(cacheConfig)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export async function handleInstall(cli, args) {
|
|
508
|
+
void args
|
|
509
|
+
const installConfig = cli.commands.install
|
|
510
|
+
if (!installConfig) {
|
|
511
|
+
logger.error('未找到 install 命令配置')
|
|
512
|
+
process.exitCode = 1
|
|
513
|
+
return
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
logger.step('安装依赖')
|
|
517
|
+
await cli.executeCommand(installConfig)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export async function handleStatus(cli, args) {
|
|
521
|
+
void args
|
|
522
|
+
logger.step('系统状态')
|
|
523
|
+
|
|
524
|
+
const status = execManager.getStatus()
|
|
525
|
+
console.log(`运行中的进程: ${status.runningProcesses}`)
|
|
526
|
+
|
|
527
|
+
if (status.processes.length > 0) {
|
|
528
|
+
logger.table(
|
|
529
|
+
status.processes.map(p => [p.id, p.command, `${Math.round(p.duration/1000)}s`]),
|
|
530
|
+
['进程ID', '命令', '运行时长']
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
}
|