@ranger1/dx 0.1.9 → 0.1.11
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 +16 -8
- package/lib/backend-package.js +4 -11
- package/lib/cli/commands/ai.js +168 -0
- package/lib/cli/dx-cli.js +8 -12
- package/lib/cli/help.js +150 -109
- package/lib/env-policy.js +5 -3
- package/lib/env.js +0 -39
- package/lib/exec.js +1 -7
- package/lib/validate-env.js +2 -180
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
## 安装
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
必须全局安装,并始终使用最新版本:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
pnpm add -g @ranger1/dx
|
|
12
|
+
pnpm add -g @ranger1/dx@latest
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
安装后即可在任意目录使用:
|
|
@@ -19,11 +19,10 @@ dx --help
|
|
|
19
19
|
dx status
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
升级到最新版本:
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
pnpm
|
|
26
|
-
pnpm exec dx --help
|
|
25
|
+
pnpm update -g @ranger1/dx
|
|
27
26
|
```
|
|
28
27
|
|
|
29
28
|
## 使用条件(必须满足)
|
|
@@ -32,7 +31,7 @@ pnpm exec dx --help
|
|
|
32
31
|
- 包管理器:pnpm(dx 内部会调用 `pnpm`)
|
|
33
32
|
- 构建系统:Nx(dx 默认命令配置里大量使用 `npx nx ...`)
|
|
34
33
|
- 环境加载:建议项目依赖 `dotenv-cli`(dx 会用 `pnpm exec dotenv ...` 包裹命令来注入 `.env.*`)
|
|
35
|
-
-
|
|
34
|
+
- 项目结构:推荐按 `apps/backend` / `apps/front` / `apps/admin-front` 这类布局组织;如有自定义目录结构,请通过 `dx/config/commands.json` 适配
|
|
36
35
|
|
|
37
36
|
如果你的 monorepo 不完全一致,也能用:关键是你在 `dx/config/commands.json` 里把命令写成适配你项目的形式。
|
|
38
37
|
|
|
@@ -123,7 +122,7 @@ DX_CONFIG_DIR=/path/to/your-repo/dx/config dx status
|
|
|
123
122
|
|
|
124
123
|
target(端)不写死,由 `env-policy.jsonc.targets` 定义;`commands.json` 里的 `app` 通过 `env-policy.jsonc.appToTarget` 映射到某个 target。
|
|
125
124
|
|
|
126
|
-
|
|
125
|
+
注:`env-policy.jsonc` 为必需配置;未提供时 dx 将直接报错。
|
|
127
126
|
|
|
128
127
|
## 示例工程
|
|
129
128
|
|
|
@@ -159,6 +158,15 @@ dx test e2e backend
|
|
|
159
158
|
- 需要的前置构建(例如 `shared`、`api-contracts`、OpenAPI 导出、后端构建等)应由项目自己的 Nx 依赖图(`dependsOn`/项目依赖)或 Vercel 的 `buildCommand` 负责。
|
|
160
159
|
- 这样 dx deploy 不会强依赖 `apps/sdk` 等目录结构,更容易适配不同 monorepo。
|
|
161
160
|
|
|
161
|
+
## 依赖关系约定
|
|
162
|
+
|
|
163
|
+
dx 不负责管理「工程之间的构建依赖关系」。如果多个工程之间存在依赖(例如 `front/admin` 依赖 `shared` 或 `api-contracts`),必须由 Nx 的依赖图来表达并自动拉起:
|
|
164
|
+
|
|
165
|
+
- 使用 Nx 的项目依赖(基于 import graph 或 `implicitDependencies`)
|
|
166
|
+
- 使用 `nx.json` 的 `targetDefaults.dependsOn` / `targetDependencies`
|
|
167
|
+
|
|
168
|
+
dx 只会执行你在 `dx/config/commands.json` 中配置的命令,不会在执行过程中额外硬编码插入依赖构建。
|
|
169
|
+
|
|
162
170
|
## 给 Nx target 注入版本信息(可选)
|
|
163
171
|
|
|
164
172
|
本包提供 `dx-with-version-env`,用于在 `nx:run-commands` 中注入版本/sha/构建时间等环境变量:
|
|
@@ -176,7 +184,7 @@ dx test e2e backend
|
|
|
176
184
|
当前版本面向 pnpm + nx 的 monorepo,默认假设:
|
|
177
185
|
|
|
178
186
|
- 使用 pnpm + nx
|
|
179
|
-
- 项目布局包含 `apps/backend`、`apps/front`、`apps/admin-front
|
|
187
|
+
- 项目布局包含 `apps/backend`、`apps/front`、`apps/admin-front`(如有差异,通过 `dx/config/commands.json` 适配)
|
|
180
188
|
- 版本注入脚本 `dx-with-version-env` 默认支持 app: `backend` / `front` / `admin`
|
|
181
189
|
|
|
182
190
|
## 发布到 npm(准备工作)
|
package/lib/backend-package.js
CHANGED
|
@@ -239,18 +239,11 @@ class BackendPackager {
|
|
|
239
239
|
async prepareEnvSnapshot() {
|
|
240
240
|
logger.info('校验并快照环境变量')
|
|
241
241
|
const policy = loadEnvPolicy(envManager.configDir)
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
if (!targetId) {
|
|
246
|
-
throw new Error(
|
|
247
|
-
'env-policy.jsonc 已启用,但缺少 appToTarget.backend 配置(dx 内置逻辑需要 backend target)',
|
|
248
|
-
)
|
|
249
|
-
}
|
|
250
|
-
requiredVars = resolveTargetRequiredVars(policy, targetId, this.layerEnv)
|
|
251
|
-
} else {
|
|
252
|
-
requiredVars = envManager.getRequiredEnvVars(this.layerEnv, 'backend')
|
|
242
|
+
const targetId = resolvePolicyTargetId(policy, 'backend')
|
|
243
|
+
if (!targetId) {
|
|
244
|
+
throw new Error('缺少 appToTarget.backend 配置(dx 内置逻辑需要 backend target)')
|
|
253
245
|
}
|
|
246
|
+
const requiredVars = resolveTargetRequiredVars(policy, targetId, this.layerEnv)
|
|
254
247
|
const collected = envManager.collectEnvFromLayers('backend', this.layerEnv)
|
|
255
248
|
const effectiveEnv = { ...collected, ...process.env }
|
|
256
249
|
const { valid, missing, placeholders } = envManager.validateRequiredVars(
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
4
|
+
import { logger } from '../../logger.js'
|
|
5
|
+
import { confirmManager } from '../../confirm.js'
|
|
6
|
+
import { getPassthroughArgs } from '../args.js'
|
|
7
|
+
|
|
8
|
+
function ensureOpencodeAvailable() {
|
|
9
|
+
const result = spawnSync('opencode', ['--version'], { stdio: 'ignore' })
|
|
10
|
+
if (result?.error?.code === 'ENOENT') {
|
|
11
|
+
return { ok: false, reason: 'missing' }
|
|
12
|
+
}
|
|
13
|
+
return { ok: true }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveAiConfig(cli, name) {
|
|
17
|
+
const raw = cli.commands?.ai?.[name]
|
|
18
|
+
if (!raw) return null
|
|
19
|
+
|
|
20
|
+
const environment = cli.determineEnvironment()
|
|
21
|
+
const envKey = cli.normalizeEnvKey(environment)
|
|
22
|
+
let config = raw
|
|
23
|
+
|
|
24
|
+
// 允许按环境分支(保持与 build/start/export 一致)
|
|
25
|
+
if (typeof config === 'object' && config && !config.promptFile && !config.command) {
|
|
26
|
+
if (config[envKey]) config = config[envKey]
|
|
27
|
+
else if (envKey === 'staging' && config.prod) config = config.prod
|
|
28
|
+
else config = config.dev || config
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return config
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function handleAi(cli, args = []) {
|
|
35
|
+
const name = args[0]
|
|
36
|
+
if (!name) {
|
|
37
|
+
const names = Object.keys(cli.commands?.ai || {})
|
|
38
|
+
logger.error('请指定 ai 任务名称')
|
|
39
|
+
logger.info(`用法: ${cli.invocation} ai <name> [-- <opencode flags>...]`)
|
|
40
|
+
if (names.length > 0) {
|
|
41
|
+
logger.info(`可用任务: ${names.join(', ')}`)
|
|
42
|
+
}
|
|
43
|
+
process.exitCode = 1
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const config = resolveAiConfig(cli, name)
|
|
48
|
+
if (!config) {
|
|
49
|
+
const names = Object.keys(cli.commands?.ai || {})
|
|
50
|
+
logger.error(`未找到 ai 任务配置: ${name}`)
|
|
51
|
+
if (names.length > 0) {
|
|
52
|
+
logger.info(`可用任务: ${names.join(', ')}`)
|
|
53
|
+
}
|
|
54
|
+
process.exitCode = 1
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const availability = ensureOpencodeAvailable()
|
|
59
|
+
if (!availability.ok) {
|
|
60
|
+
logger.error('未找到 opencode 可执行文件(PATH 中不存在 opencode)')
|
|
61
|
+
logger.info('请先安装并确保 `opencode --version` 可用')
|
|
62
|
+
process.exitCode = 1
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const promptFile = config?.promptFile
|
|
67
|
+
if (!promptFile) {
|
|
68
|
+
logger.error(`ai.${name} 缺少 promptFile 配置(指向一个 .md 文件)`)
|
|
69
|
+
process.exitCode = 1
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const promptPath = resolve(process.cwd(), String(promptFile))
|
|
74
|
+
if (!existsSync(promptPath)) {
|
|
75
|
+
logger.error(`未找到提示词文件: ${promptFile}`)
|
|
76
|
+
logger.info(`解析后的路径: ${promptPath}`)
|
|
77
|
+
process.exitCode = 1
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let promptText = ''
|
|
82
|
+
try {
|
|
83
|
+
promptText = readFileSync(promptPath, 'utf8')
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.error(`读取提示词文件失败: ${promptFile}`)
|
|
86
|
+
logger.error(error?.message || String(error))
|
|
87
|
+
process.exitCode = 1
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 固定全权限:这会让 opencode 在当前目录拥有 bash/edit 等工具的自动执行权
|
|
92
|
+
if (!cli.flags.Y) {
|
|
93
|
+
const confirmed = await confirmManager.confirmDangerous(
|
|
94
|
+
`ai.${name}(将以 OPENCODE_PERMISSION="allow" 运行,全权限)`,
|
|
95
|
+
'当前目录',
|
|
96
|
+
false,
|
|
97
|
+
)
|
|
98
|
+
if (!confirmed) {
|
|
99
|
+
logger.info('操作已取消')
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const model = config?.model ? String(config.model) : null
|
|
105
|
+
const agent = config?.agent ? String(config.agent) : null
|
|
106
|
+
const format = config?.format ? String(config.format) : null
|
|
107
|
+
const attach = config?.attach ? String(config.attach) : null
|
|
108
|
+
|
|
109
|
+
const passthrough = getPassthroughArgs(cli.args)
|
|
110
|
+
const configPassthrough = Array.isArray(config?.passthrough)
|
|
111
|
+
? config.passthrough.map(v => String(v))
|
|
112
|
+
: []
|
|
113
|
+
|
|
114
|
+
const opencodeArgs = ['run']
|
|
115
|
+
if (model) opencodeArgs.push('--model', model)
|
|
116
|
+
if (agent) opencodeArgs.push('--agent', agent)
|
|
117
|
+
if (format) opencodeArgs.push('--format', format)
|
|
118
|
+
if (attach) opencodeArgs.push('--attach', attach)
|
|
119
|
+
if (configPassthrough.length > 0) opencodeArgs.push(...configPassthrough)
|
|
120
|
+
if (Array.isArray(passthrough) && passthrough.length > 0) opencodeArgs.push(...passthrough)
|
|
121
|
+
opencodeArgs.push(promptText)
|
|
122
|
+
|
|
123
|
+
logger.step(`ai ${name}`)
|
|
124
|
+
logger.command(`opencode ${opencodeArgs.filter(a => a !== promptText).join(' ')} <prompt-from-file>`)
|
|
125
|
+
|
|
126
|
+
await new Promise(resolvePromise => {
|
|
127
|
+
const child = spawn('opencode', opencodeArgs, {
|
|
128
|
+
cwd: process.cwd(),
|
|
129
|
+
stdio: 'inherit',
|
|
130
|
+
env: {
|
|
131
|
+
...process.env,
|
|
132
|
+
// OpenCode expects OPENCODE_PERMISSION to be JSON (it JSON.parse's the value).
|
|
133
|
+
OPENCODE_PERMISSION: '"allow"',
|
|
134
|
+
},
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const forwardSignal = signal => {
|
|
138
|
+
try {
|
|
139
|
+
child.kill(signal)
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const onSigint = () => forwardSignal('SIGINT')
|
|
144
|
+
const onSigterm = () => forwardSignal('SIGTERM')
|
|
145
|
+
process.on('SIGINT', onSigint)
|
|
146
|
+
process.on('SIGTERM', onSigterm)
|
|
147
|
+
|
|
148
|
+
const cleanup = () => {
|
|
149
|
+
process.off('SIGINT', onSigint)
|
|
150
|
+
process.off('SIGTERM', onSigterm)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
child.on('error', error => {
|
|
154
|
+
cleanup()
|
|
155
|
+
logger.error(error?.message || String(error))
|
|
156
|
+
process.exitCode = 1
|
|
157
|
+
resolvePromise()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
child.on('exit', code => {
|
|
161
|
+
cleanup()
|
|
162
|
+
if (typeof code === 'number' && code !== 0) {
|
|
163
|
+
process.exitCode = code
|
|
164
|
+
}
|
|
165
|
+
resolvePromise()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
}
|
package/lib/cli/dx-cli.js
CHANGED
|
@@ -33,6 +33,7 @@ import { handlePackage } from './commands/package.js'
|
|
|
33
33
|
import { handleExport } from './commands/export.js'
|
|
34
34
|
import { handleContracts } from './commands/contracts.js'
|
|
35
35
|
import { handleRelease } from './commands/release.js'
|
|
36
|
+
import { handleAi } from './commands/ai.js'
|
|
36
37
|
|
|
37
38
|
class DxCli {
|
|
38
39
|
constructor(options = {}) {
|
|
@@ -67,6 +68,7 @@ class DxCli {
|
|
|
67
68
|
export: args => handleExport(this, args),
|
|
68
69
|
contracts: args => handleContracts(this, args),
|
|
69
70
|
release: args => handleRelease(this, args),
|
|
71
|
+
ai: args => handleAi(this, args),
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
this.flagDefinitions = FLAG_DEFINITIONS
|
|
@@ -221,18 +223,11 @@ class DxCli {
|
|
|
221
223
|
if (process.env.CI !== '1') {
|
|
222
224
|
const effectiveEnv = { ...process.env, ...layeredEnv }
|
|
223
225
|
const policy = loadEnvPolicy(envManager.configDir)
|
|
224
|
-
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
if (!targetId) {
|
|
228
|
-
throw new Error(
|
|
229
|
-
'env-policy.jsonc 已启用,但缺少 appToTarget.backend 配置(dx 内置逻辑需要 backend target)',
|
|
230
|
-
)
|
|
231
|
-
}
|
|
232
|
-
requiredVars = resolveTargetRequiredVars(policy, targetId, environment)
|
|
233
|
-
} else {
|
|
234
|
-
requiredVars = envManager.getRequiredEnvVars(environment, 'backend')
|
|
226
|
+
const targetId = resolvePolicyTargetId(policy, 'backend')
|
|
227
|
+
if (!targetId) {
|
|
228
|
+
throw new Error('缺少 appToTarget.backend 配置(dx 内置逻辑需要 backend target)')
|
|
235
229
|
}
|
|
230
|
+
const requiredVars = resolveTargetRequiredVars(policy, targetId, environment)
|
|
236
231
|
if (requiredVars.length > 0) {
|
|
237
232
|
const { valid, missing, placeholders } = envManager.validateRequiredVars(
|
|
238
233
|
requiredVars,
|
|
@@ -304,7 +299,8 @@ class DxCli {
|
|
|
304
299
|
// - help: printing help should never require env vars.
|
|
305
300
|
// - status: should be available even when env is incomplete.
|
|
306
301
|
// - release: only edits package.json versions.
|
|
307
|
-
|
|
302
|
+
// - ai: wraps external opencode CLI, should not require project deps/prisma/env.
|
|
303
|
+
const skipStartupChecks = new Set(['help', 'status', 'release', 'ai'])
|
|
308
304
|
if (skipStartupChecks.has(this.command)) {
|
|
309
305
|
await this.routeCommand()
|
|
310
306
|
return
|
package/lib/cli/help.js
CHANGED
|
@@ -2,115 +2,124 @@ import { getPackageVersion } from '../version.js'
|
|
|
2
2
|
|
|
3
3
|
export function showHelp() {
|
|
4
4
|
const version = getPackageVersion()
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
service
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
target
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
target
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
action
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
dx db
|
|
38
|
-
dx db deploy --
|
|
39
|
-
dx db
|
|
40
|
-
dx db script fix-
|
|
41
|
-
dx db script
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
type
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
action
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
target
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
action
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
--
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
dx start
|
|
87
|
-
dx
|
|
88
|
-
dx
|
|
89
|
-
dx
|
|
90
|
-
dx
|
|
91
|
-
dx
|
|
92
|
-
dx
|
|
93
|
-
dx
|
|
94
|
-
dx
|
|
95
|
-
dx
|
|
96
|
-
dx
|
|
97
|
-
dx
|
|
98
|
-
dx
|
|
99
|
-
dx
|
|
100
|
-
dx
|
|
101
|
-
dx
|
|
102
|
-
|
|
103
|
-
#
|
|
104
|
-
dx
|
|
105
|
-
dx
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
dx
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
5
|
+
const lines = [
|
|
6
|
+
'',
|
|
7
|
+
`DX CLI v${version} - 统一开发环境管理工具`,
|
|
8
|
+
'',
|
|
9
|
+
'用法:',
|
|
10
|
+
' dx <命令> [选项] [参数...]',
|
|
11
|
+
'',
|
|
12
|
+
'命令:',
|
|
13
|
+
' start [service] [环境标志] 启动/桥接服务',
|
|
14
|
+
' service: backend, front, admin, all, dev, stack, stagewise-front, stagewise-admin (默认: dev)',
|
|
15
|
+
' stack: PM2 交互式服务栈管理(推荐)- 同时启动三个服务并提供交互式命令',
|
|
16
|
+
' 环境标志: --dev, --staging, --prod, --test, --e2e(支持别名 --development、--production 等)',
|
|
17
|
+
" 说明: 传入 --staging 时会加载 '.env.staging(.local)' 层,同时复用生产构建/启动流程",
|
|
18
|
+
'',
|
|
19
|
+
' build [target] [环境标志] 构建应用',
|
|
20
|
+
' target: backend, shared, front, admin, mobile, all, sdk, affected (默认: all)',
|
|
21
|
+
' 环境标志: --dev, --staging, --prod, --test, --e2e(未指定时默认 --dev)',
|
|
22
|
+
'',
|
|
23
|
+
' deploy <target> [环境标志] 部署前端到 Vercel',
|
|
24
|
+
' target: front, admin, telegram-bot, all',
|
|
25
|
+
' 环境标志: --dev, --staging, --prod(默认 --staging)',
|
|
26
|
+
'',
|
|
27
|
+
' install 安装依赖(使用 frozen-lockfile 确保版本一致)',
|
|
28
|
+
'',
|
|
29
|
+
' package backend [环境标志] 构建后端部署包(生成 backend-<version>-<sha>.tar.gz)',
|
|
30
|
+
' 环境标志: --dev, --staging, --prod, --test, --e2e(默认 --dev)',
|
|
31
|
+
' 产物位置: dist/backend/backend-*.tar.gz',
|
|
32
|
+
' 内含: dist/、node_modules(生产依赖)、prisma/、config/.env.runtime、bin/start.sh',
|
|
33
|
+
'',
|
|
34
|
+
' db [action] [环境标志] 数据库操作',
|
|
35
|
+
' action: generate, migrate, deploy, reset, seed, format, script',
|
|
36
|
+
' 用法示例:',
|
|
37
|
+
' dx db migrate --dev --name add_user_table # 创建新的迁移(开发环境需指定名称)',
|
|
38
|
+
' dx db deploy --dev # 应用开发环境已有迁移',
|
|
39
|
+
' dx db deploy --prod # 生产环境迁移(复用 deploy 流程,需确认)',
|
|
40
|
+
' dx db script fix-email-verified-status --dev # 运行数据库脚本(开发环境)',
|
|
41
|
+
' dx db script fix-pending-transfer-status --prod # 运行数据库脚本(生产环境,需确认)',
|
|
42
|
+
' dx db script my-script --dev -- --arg1 --arg2 # 向脚本传递额外参数(-- 后面的部分)',
|
|
43
|
+
'',
|
|
44
|
+
' test [type] [target] [path] [-t pattern] 运行测试',
|
|
45
|
+
' type: e2e, unit (默认: e2e)',
|
|
46
|
+
' target: backend, all (默认: all)',
|
|
47
|
+
' path: 测试文件路径 (可选,仅支持e2e backend)',
|
|
48
|
+
' -t pattern: 指定测试用例名称模式 (可选,需要和path一起使用)',
|
|
49
|
+
'',
|
|
50
|
+
' worktree [action] [num...] Git Worktree管理',
|
|
51
|
+
' action: make, del, list, clean',
|
|
52
|
+
' num: issue编号 (make时需要1个,del时支持多个)',
|
|
53
|
+
' 支持批量删除: dx worktree del 123 456 789',
|
|
54
|
+
' 支持非交互式: dx worktree del 123 -Y',
|
|
55
|
+
' 注意:该封装与原生 git worktree 行为不同,勿混用',
|
|
56
|
+
'',
|
|
57
|
+
' lint 运行代码检查',
|
|
58
|
+
'',
|
|
59
|
+
' contracts [generate] 导出后端 OpenAPI 并生成 Zod 合约(packages/api-contracts)',
|
|
60
|
+
'',
|
|
61
|
+
' release version <semver> 统一同步 backend/front/admin-front 的版本号',
|
|
62
|
+
'',
|
|
63
|
+
' clean [target] 清理操作',
|
|
64
|
+
' target: all, deps (默认: all)',
|
|
65
|
+
'',
|
|
66
|
+
' cache [action] 缓存清理',
|
|
67
|
+
' action: clear (默认: clear)',
|
|
68
|
+
'',
|
|
69
|
+
' status 查看系统状态',
|
|
70
|
+
'',
|
|
71
|
+
' ai <name> 运行一个预配置的 opencode 任务(从 commands.json 读取)',
|
|
72
|
+
' 透传: -- 后的参数将原样传给 opencode run',
|
|
73
|
+
'',
|
|
74
|
+
'选项:',
|
|
75
|
+
' --dev, --development 使用开发环境',
|
|
76
|
+
' --prod, --production 使用生产环境',
|
|
77
|
+
' --staging, --stage 使用预发环境(加载 .env.staging*.,复用生产流程)',
|
|
78
|
+
' --test 使用测试环境',
|
|
79
|
+
' --e2e 使用E2E测试环境',
|
|
80
|
+
' -Y, --yes 跳过所有确认提示',
|
|
81
|
+
' -v, --verbose 详细输出',
|
|
82
|
+
' -h, --help 显示此帮助信息',
|
|
83
|
+
' -V, --version 显示版本号',
|
|
84
|
+
'',
|
|
85
|
+
'示例:',
|
|
86
|
+
' dx start stack # PM2 交互式服务栈(推荐)- 同时管理三个服务',
|
|
87
|
+
' dx start backend --dev # 启动后端开发服务',
|
|
88
|
+
' dx start front --dev # 启动用户前端开发服务',
|
|
89
|
+
' dx start admin --dev # 启动管理后台开发服务',
|
|
90
|
+
' dx start all # 同时启动所有开发服务(默认 --dev)',
|
|
91
|
+
' dx build all --prod # 构建所有应用(生产环境)',
|
|
92
|
+
' dx db deploy --dev # 应用开发环境数据库迁移',
|
|
93
|
+
' dx db reset --prod -Y # 重置生产数据库(跳过确认)',
|
|
94
|
+
' dx test e2e backend # 运行后端E2E测试',
|
|
95
|
+
' dx test e2e backend e2e/activity/activity.admin.e2e-spec.ts # 运行单个E2E测试文件',
|
|
96
|
+
' dx test e2e backend e2e/activity/activity.admin.e2e-spec.ts -t "should list all activity definitions" # 运行特定测试用例',
|
|
97
|
+
' dx deploy front --staging # 部署前端到 Vercel(staging)',
|
|
98
|
+
' dx worktree make 88 # 为issue #88创建worktree',
|
|
99
|
+
' dx worktree del 88 # 删除issue #88的worktree',
|
|
100
|
+
' dx worktree del 88 89 90 -Y # 批量删除多个worktree(非交互式)',
|
|
101
|
+
' dx worktree list # 列出所有worktree',
|
|
102
|
+
' dx clean deps # 清理并重新安装依赖',
|
|
103
|
+
' dx cache clear # 清除 Nx 与依赖缓存',
|
|
104
|
+
' dx contracts # 导出 OpenAPI 并生成 Zod 合约',
|
|
105
|
+
' dx release version 1.2.3 # 同步 backend/front/admin-front 版本号',
|
|
106
|
+
'',
|
|
107
|
+
' dx ai review',
|
|
108
|
+
' dx ai review -- --share --title "my run"',
|
|
109
|
+
'',
|
|
110
|
+
' # Stagewise 桥接(固定端口,自动清理占用)',
|
|
111
|
+
' dx start stagewise-front # 桥接 front: 3001 -> 3002(工作目录 apps/front)',
|
|
112
|
+
' dx start stagewise-admin # 桥接 admin-front: 3500 -> 3501(工作目录 apps/admin-front)',
|
|
113
|
+
'',
|
|
114
|
+
' # Start 用法示例',
|
|
115
|
+
' dx start backend --prod # 以生产环境变量启动后端',
|
|
116
|
+
' dx start backend --dev # 以开发环境变量启动后端',
|
|
117
|
+
' dx start backend --e2e # 以 E2E 环境变量启动后端',
|
|
118
|
+
'',
|
|
119
|
+
'',
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
console.log(lines.join('\n'))
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
export function showCommandHelp(command) {
|
|
@@ -248,6 +257,38 @@ release 命令用法:
|
|
|
248
257
|
`)
|
|
249
258
|
return
|
|
250
259
|
|
|
260
|
+
case 'ai':
|
|
261
|
+
console.log(
|
|
262
|
+
[
|
|
263
|
+
'',
|
|
264
|
+
'ai 命令用法:',
|
|
265
|
+
' dx ai <name> [-- <opencode flags>...]',
|
|
266
|
+
'',
|
|
267
|
+
'说明:',
|
|
268
|
+
' - 从 dx/config/commands.json 的 ai.<name> 读取一个固定配置并执行 opencode run',
|
|
269
|
+
' - 本次运行固定注入 OPENCODE_PERMISSION="allow"(全权限)',
|
|
270
|
+
'',
|
|
271
|
+
'commands.json 示例:',
|
|
272
|
+
' {',
|
|
273
|
+
' "ai": {',
|
|
274
|
+
' "review": {',
|
|
275
|
+
' "promptFile": "./prompts/review.md",',
|
|
276
|
+
' "model": "openai/gpt-4.1",',
|
|
277
|
+
' "agent": "general",',
|
|
278
|
+
' "format": "default",',
|
|
279
|
+
' "passthrough": ["--share"]',
|
|
280
|
+
' }',
|
|
281
|
+
' }',
|
|
282
|
+
' }',
|
|
283
|
+
'',
|
|
284
|
+
'示例:',
|
|
285
|
+
' dx ai review',
|
|
286
|
+
' dx ai review -- --title "my run"',
|
|
287
|
+
'',
|
|
288
|
+
].join('\n'),
|
|
289
|
+
)
|
|
290
|
+
return
|
|
291
|
+
|
|
251
292
|
default:
|
|
252
293
|
showHelp()
|
|
253
294
|
}
|
package/lib/env-policy.js
CHANGED
|
@@ -28,9 +28,11 @@ export function loadEnvPolicy(configDir) {
|
|
|
28
28
|
|
|
29
29
|
const policyPath = join(configDir, DEFAULT_POLICY_PATH)
|
|
30
30
|
if (!existsSync(policyPath)) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
throw new Error(
|
|
32
|
+
`缺少配置文件 ${DEFAULT_POLICY_PATH}。
|
|
33
|
+
请在目标工程根目录创建 dx/config/${DEFAULT_POLICY_PATH}(或用 DX_CONFIG_DIR / --config-dir 指定配置目录)。
|
|
34
|
+
当前配置目录: ${configDir}`,
|
|
35
|
+
)
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
let parsed
|
package/lib/env.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
-
import stripJsonComments from 'strip-json-comments'
|
|
4
3
|
|
|
5
4
|
function resolveProjectRoot() {
|
|
6
5
|
return process.env.DX_PROJECT_ROOT || process.cwd()
|
|
@@ -16,7 +15,6 @@ export class EnvManager {
|
|
|
16
15
|
this.projectRoot = resolveProjectRoot()
|
|
17
16
|
this.configDir = resolveConfigDir()
|
|
18
17
|
this.envLayers = this.loadEnvLayers()
|
|
19
|
-
this.requiredEnvConfig = null
|
|
20
18
|
this.latestEnvWarnings = []
|
|
21
19
|
|
|
22
20
|
// APP_ENV → NODE_ENV 映射(用于运行时行为和工具链,如 Nx/Next)
|
|
@@ -103,43 +101,6 @@ export class EnvManager {
|
|
|
103
101
|
return layers.map(layer => layer.replace('{app}', app))
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
loadRequiredEnvConfig() {
|
|
107
|
-
if (this.requiredEnvConfig) return this.requiredEnvConfig
|
|
108
|
-
const configPath = join(this.configDir, 'required-env.jsonc')
|
|
109
|
-
if (!existsSync(configPath)) {
|
|
110
|
-
this.requiredEnvConfig = { _common: [] }
|
|
111
|
-
return this.requiredEnvConfig
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const raw = readFileSync(configPath, 'utf8')
|
|
116
|
-
const sanitized = stripJsonComments(raw)
|
|
117
|
-
this.requiredEnvConfig = JSON.parse(sanitized || '{}') || { _common: [] }
|
|
118
|
-
} catch (error) {
|
|
119
|
-
throw new Error(`无法解析 required-env.jsonc: ${error.message}`)
|
|
120
|
-
}
|
|
121
|
-
return this.requiredEnvConfig
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
getRequiredEnvVars(environment, appType = null) {
|
|
125
|
-
const config = this.loadRequiredEnvConfig()
|
|
126
|
-
const base = Array.isArray(config._common) ? config._common : []
|
|
127
|
-
const envSpecific = Array.isArray(config[environment]) ? config[environment] : []
|
|
128
|
-
|
|
129
|
-
// 按应用类型添加对应的环境变量组
|
|
130
|
-
let appSpecific = []
|
|
131
|
-
if (appType) {
|
|
132
|
-
const appTypes = Array.isArray(appType) ? appType : [appType]
|
|
133
|
-
for (const type of appTypes) {
|
|
134
|
-
if (Array.isArray(config[type])) {
|
|
135
|
-
appSpecific = appSpecific.concat(config[type])
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return Array.from(new Set([...base, ...envSpecific, ...appSpecific]))
|
|
141
|
-
}
|
|
142
|
-
|
|
143
104
|
// 构建dotenv命令参数
|
|
144
105
|
buildEnvFlags(app, environment) {
|
|
145
106
|
return this.getResolvedEnvLayers(app, environment)
|
package/lib/exec.js
CHANGED
|
@@ -80,7 +80,7 @@ export class ExecManager {
|
|
|
80
80
|
const policy = loadEnvPolicy(envManager.configDir)
|
|
81
81
|
let requiredVars = []
|
|
82
82
|
|
|
83
|
-
if (!isCI &&
|
|
83
|
+
if (!isCI && app) {
|
|
84
84
|
const targetId = resolvePolicyTargetId(policy, app)
|
|
85
85
|
if (!targetId) {
|
|
86
86
|
throw new Error(
|
|
@@ -88,12 +88,6 @@ export class ExecManager {
|
|
|
88
88
|
)
|
|
89
89
|
}
|
|
90
90
|
requiredVars = resolveTargetRequiredVars(policy, targetId, environment)
|
|
91
|
-
} else if (!policy) {
|
|
92
|
-
// Backward-compatible behavior
|
|
93
|
-
// CI 环境跳过后端环境变量校验(CI 中 build backend 只生成 OpenAPI,不需要数据库连接)
|
|
94
|
-
// 根据 app 参数确定需要检查的环境变量组
|
|
95
|
-
const appType = isCI ? null : (app === 'backend' ? 'backend' : app ? 'frontend' : null)
|
|
96
|
-
requiredVars = envManager.getRequiredEnvVars(environment, appType)
|
|
97
91
|
}
|
|
98
92
|
|
|
99
93
|
if (requiredVars.length > 0) {
|
package/lib/validate-env.js
CHANGED
|
@@ -16,21 +16,7 @@ const EXTRA_ENV_IGNORED_DIRS = new Set([
|
|
|
16
16
|
'.schaltwerk',
|
|
17
17
|
])
|
|
18
18
|
const EXTRA_ENV_ALLOWED_PATHS = new Set(['docker/.env', 'docker/.env.example'])
|
|
19
|
-
const LOCAL_ALLOWLIST_CONFIG = join(CONFIG_DIR, 'local-env-allowlist.jsonc')
|
|
20
|
-
const EXEMPTED_KEYS_CONFIG = join(CONFIG_DIR, 'exempted-keys.jsonc')
|
|
21
19
|
const ENV_EXAMPLE_FILE = join(ROOT_DIR, '.env.example')
|
|
22
|
-
const PLACEHOLDER_TOKEN = '__SET_IN_env.local__'
|
|
23
|
-
|
|
24
|
-
const LOCAL_ENV_FILES = [
|
|
25
|
-
'.env.development.local',
|
|
26
|
-
'.env.production.local',
|
|
27
|
-
'.env.test.local',
|
|
28
|
-
'.env.e2e.local',
|
|
29
|
-
'.env.staging.local',
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
let cachedAllowlist = null
|
|
33
|
-
let cachedExemptedKeys = null
|
|
34
20
|
|
|
35
21
|
export function validateEnvironment() {
|
|
36
22
|
if (!process.env.NODE_ENV) {
|
|
@@ -47,15 +33,8 @@ export function validateEnvironment() {
|
|
|
47
33
|
enforceRootOnlyEnvFiles(policy)
|
|
48
34
|
enforceGlobalLocalFileProhibited()
|
|
49
35
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
enforceEnvExamplePolicy(policy)
|
|
53
|
-
} else {
|
|
54
|
-
// Backward-compatible behavior
|
|
55
|
-
enforceLocalSecretWhitelist()
|
|
56
|
-
enforceNonLocalSecretPlaceholders()
|
|
57
|
-
enforceEnvExampleSecrets()
|
|
58
|
-
}
|
|
36
|
+
enforceSecretPolicy(policy)
|
|
37
|
+
enforceEnvExamplePolicy(policy)
|
|
59
38
|
|
|
60
39
|
return { nodeEnv: process.env.NODE_ENV, appEnv: process.env.APP_ENV }
|
|
61
40
|
}
|
|
@@ -114,36 +93,6 @@ function enforceRootOnlyEnvFiles(policy) {
|
|
|
114
93
|
}
|
|
115
94
|
}
|
|
116
95
|
|
|
117
|
-
function enforceLocalSecretWhitelist() {
|
|
118
|
-
const allowlist = loadLocalAllowlist()
|
|
119
|
-
const errors = []
|
|
120
|
-
|
|
121
|
-
for (const file of LOCAL_ENV_FILES) {
|
|
122
|
-
const fullPath = join(ROOT_DIR, file)
|
|
123
|
-
if (!existsSync(fullPath)) continue
|
|
124
|
-
|
|
125
|
-
const entries = parseEnvFile(fullPath)
|
|
126
|
-
const invalidKeys = []
|
|
127
|
-
|
|
128
|
-
for (const key of entries.keys()) {
|
|
129
|
-
if (!allowlist.has(key)) {
|
|
130
|
-
invalidKeys.push(key)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (invalidKeys.length > 0) {
|
|
135
|
-
errors.push(`${file}: ${invalidKeys.join(', ')}`)
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (errors.length > 0) {
|
|
140
|
-
const message = errors.join('\n')
|
|
141
|
-
throw new Error(
|
|
142
|
-
`检测到 *.local 文件包含非白名单键:\n${message}\n请将这些键迁移到对应的 .env.<env> 文件,仅保留白名单内的机密信息在 *.local 中。`,
|
|
143
|
-
)
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
96
|
function enforceGlobalLocalFileProhibited() {
|
|
148
97
|
const legacyLocal = join(ROOT_DIR, '.env.local')
|
|
149
98
|
if (existsSync(legacyLocal)) {
|
|
@@ -320,80 +269,6 @@ function findOverlaps(namedSets) {
|
|
|
320
269
|
return overlaps
|
|
321
270
|
}
|
|
322
271
|
|
|
323
|
-
function enforceNonLocalSecretPlaceholders() {
|
|
324
|
-
const allowlist = loadLocalAllowlist()
|
|
325
|
-
const exemptedKeys = loadExemptedKeys()
|
|
326
|
-
const violations = []
|
|
327
|
-
|
|
328
|
-
for (const file of listRootEnvFiles()) {
|
|
329
|
-
if (file === '.env.example') continue
|
|
330
|
-
if (file.includes('.local')) continue
|
|
331
|
-
|
|
332
|
-
const fullPath = join(ROOT_DIR, file)
|
|
333
|
-
if (!existsSync(fullPath)) continue
|
|
334
|
-
|
|
335
|
-
const entries = parseEnvFile(fullPath)
|
|
336
|
-
|
|
337
|
-
for (const [key, value] of entries.entries()) {
|
|
338
|
-
if (!allowlist.has(key)) continue
|
|
339
|
-
if (exemptedKeys.has(key)) continue // 豁免的键允许使用非占位符值
|
|
340
|
-
|
|
341
|
-
const normalized = value.trim()
|
|
342
|
-
if (normalized !== PLACEHOLDER_TOKEN) {
|
|
343
|
-
violations.push(`${file}: ${key}`)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (violations.length > 0) {
|
|
349
|
-
const message = violations.join('\n')
|
|
350
|
-
throw new Error(
|
|
351
|
-
`检测到非 *.local 文件包含敏感键但未使用占位符 ${PLACEHOLDER_TOKEN}:\n${message}\n` +
|
|
352
|
-
`请仅在 .env.<env>.local 系列中设置真实值,并在其他文件中使用占位符。`,
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function enforceEnvExampleSecrets() {
|
|
358
|
-
if (!existsSync(ENV_EXAMPLE_FILE)) return
|
|
359
|
-
|
|
360
|
-
const allowlist = loadLocalAllowlist()
|
|
361
|
-
const entries = parseEnvFile(ENV_EXAMPLE_FILE)
|
|
362
|
-
const disallowedKeys = []
|
|
363
|
-
const invalidPlaceholders = []
|
|
364
|
-
|
|
365
|
-
for (const [key, value] of entries.entries()) {
|
|
366
|
-
const normalized = value.trim()
|
|
367
|
-
|
|
368
|
-
if (!allowlist.has(key)) {
|
|
369
|
-
disallowedKeys.push(key)
|
|
370
|
-
continue
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (normalized !== PLACEHOLDER_TOKEN) {
|
|
374
|
-
invalidPlaceholders.push(key)
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (disallowedKeys.length > 0) {
|
|
379
|
-
throw new Error(
|
|
380
|
-
`.env.example 仅允许包含 scripts/config/local-env-allowlist.jsonc 中的键,检测到非法键: ${disallowedKeys.join(', ')}`,
|
|
381
|
-
)
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (invalidPlaceholders.length > 0) {
|
|
385
|
-
throw new Error(
|
|
386
|
-
`.env.example 中以下键未使用占位符 ${PLACEHOLDER_TOKEN}: ${invalidPlaceholders.join(', ')}`,
|
|
387
|
-
)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function listRootEnvFiles() {
|
|
392
|
-
return readdirSync(ROOT_DIR, { withFileTypes: true })
|
|
393
|
-
.filter(entry => entry.isFile() && entry.name.startsWith('.env'))
|
|
394
|
-
.map(entry => entry.name)
|
|
395
|
-
}
|
|
396
|
-
|
|
397
272
|
function parseEnvFile(filePath) {
|
|
398
273
|
const content = readFileSync(filePath, 'utf8')
|
|
399
274
|
const map = new Map()
|
|
@@ -415,57 +290,4 @@ function parseEnvFile(filePath) {
|
|
|
415
290
|
return map
|
|
416
291
|
}
|
|
417
292
|
|
|
418
|
-
function loadLocalAllowlist() {
|
|
419
|
-
if (cachedAllowlist) return cachedAllowlist
|
|
420
|
-
|
|
421
|
-
if (!existsSync(LOCAL_ALLOWLIST_CONFIG)) {
|
|
422
|
-
throw new Error('缺少配置文件 scripts/config/local-env-allowlist.jsonc,请补充后重试')
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const raw = readFileSync(LOCAL_ALLOWLIST_CONFIG, 'utf8')
|
|
426
|
-
const sanitized = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
|
427
|
-
const parsed = JSON.parse(sanitized || '{}') || {}
|
|
428
|
-
|
|
429
|
-
const allowedValues = Array.isArray(parsed.allowed) ? parsed.allowed : []
|
|
430
|
-
if (allowedValues.length === 0) {
|
|
431
|
-
throw new Error('local-env-allowlist.jsonc 中的 allowed 不能为空,请至少保留一个键')
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const invalid = allowedValues.filter(
|
|
435
|
-
value => typeof value !== 'string' || value.trim().length === 0,
|
|
436
|
-
)
|
|
437
|
-
if (invalid.length > 0) {
|
|
438
|
-
throw new Error('local-env-allowlist.jsonc.allowed 中存在非法键,请使用非空字符串')
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
cachedAllowlist = new Set(allowedValues)
|
|
442
|
-
return cachedAllowlist
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function loadExemptedKeys() {
|
|
446
|
-
if (cachedExemptedKeys) return cachedExemptedKeys
|
|
447
|
-
|
|
448
|
-
// 如果豁免配置文件不存在,返回空集合(可选配置)
|
|
449
|
-
if (!existsSync(EXEMPTED_KEYS_CONFIG)) {
|
|
450
|
-
cachedExemptedKeys = new Set()
|
|
451
|
-
return cachedExemptedKeys
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const raw = readFileSync(EXEMPTED_KEYS_CONFIG, 'utf8')
|
|
455
|
-
const sanitized = raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
|
|
456
|
-
const parsed = JSON.parse(sanitized || '{}') || {}
|
|
457
|
-
|
|
458
|
-
const exemptedValues = Array.isArray(parsed.exempted) ? parsed.exempted : []
|
|
459
|
-
|
|
460
|
-
const invalid = exemptedValues.filter(
|
|
461
|
-
value => typeof value !== 'string' || value.trim().length === 0,
|
|
462
|
-
)
|
|
463
|
-
if (invalid.length > 0) {
|
|
464
|
-
throw new Error('exempted-keys.jsonc.exempted 中存在非法键,请使用非空字符串')
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
cachedExemptedKeys = new Set(exemptedValues)
|
|
468
|
-
return cachedExemptedKeys
|
|
469
|
-
}
|
|
470
|
-
|
|
471
293
|
export default { validateEnvironment }
|