@weotro/dx 0.1.3 → 0.1.5

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 CHANGED
@@ -202,6 +202,10 @@ target(端)不写死,由 `env-policy.jsonc.targets` 定义;`commands.jso
202
202
  "primary": { "label": "Primary brand" },
203
203
  "secondary": { "label": "Secondary brand" }
204
204
  },
205
+ "committedRuntime": {
206
+ "target": "backend",
207
+ "keys": ["SENTRY_ORG"]
208
+ },
205
209
  "requiredLocalKeys": {
206
210
  "staging": ["DATABASE_URL"],
207
211
  "production": ["DATABASE_URL"]
@@ -218,7 +222,13 @@ dx env validate secondary --staging
218
222
  dx env exec secondary --staging -- dx deploy backend
219
223
  ```
220
224
 
221
- `dx env exec` 会加锁、原子装配 `.env.<environment>.local`、把同一份值注入子进程,并在成功、
225
+ `committedRuntime` 是可选的公开 runtime allowlist。`target` 必须指向 `env-policy.jsonc.targets`
226
+ 中的 target;`keys` 只会从该 target 当前环境的 committed 文件读取。未配置时默认空 allowlist,
227
+ 行为与旧版本一致。allowlist 不接受 secret/localOnly 键、空值或机密占位符,也不允许 committed
228
+ 文件越出项目根目录。合并优先级为调用进程环境 < private profile < committed allowlist,因此仓库
229
+ 已审查的 committed 公共值最终生效,未列入 allowlist 的 committed 字段不会被导出。
230
+
231
+ `dx env exec` 会加锁、原子装配 `.env.<environment>.local`、把合并后的值注入直接子进程,并在成功、
222
232
  失败或中断后删除临时文件。根目录不允许持久保存 staging/production `.local`;发现旧文件时命令
223
233
  会直接报错,必须先迁移到品牌 profile。内部 `dx` 命令未指定环境时会自动补齐;指定冲突环境时
224
234
  直接拒绝。该命令只操作本机文件和子进程,不包含上传、同步或修改 GitHub Environment 的能力。
@@ -8,17 +8,19 @@ import {
8
8
  mkdirSync,
9
9
  openSync,
10
10
  readFileSync,
11
+ realpathSync,
11
12
  renameSync,
12
13
  rmSync,
13
14
  statSync,
14
15
  writeFileSync,
15
16
  } from 'node:fs'
16
17
  import { tmpdir } from 'node:os'
17
- import { basename, join, relative } from 'node:path'
18
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path'
18
19
  import { loadEnvPolicy, resolveTargetRequiredVars } from './env-policy.js'
19
20
 
20
21
  const PROFILE_CONFIG_FILE = 'env-profiles.json'
21
22
  const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/
23
+ const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
22
24
  const SIGNAL_EXIT_CODES = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 }
23
25
 
24
26
  function assert(condition, message) {
@@ -69,7 +71,29 @@ export function loadEnvProfileConfig(configDir) {
69
71
  assert(config.environments.includes(environment), `requiredLocalKeys 包含未声明环境: ${environment}`)
70
72
  assert(Array.isArray(keys), `requiredLocalKeys.${environment} 必须为数组`)
71
73
  for (const key of keys) {
72
- assert(/^[A-Za-z_][A-Za-z0-9_]*$/.test(key), `requiredLocalKeys.${environment} 包含非法键: ${key}`)
74
+ assert(ENV_KEY_PATTERN.test(key), `requiredLocalKeys.${environment} 包含非法键: ${key}`)
75
+ }
76
+ }
77
+
78
+ if (config.committedRuntime !== undefined) {
79
+ const committedRuntime = config.committedRuntime
80
+ assert(
81
+ committedRuntime && typeof committedRuntime === 'object' && !Array.isArray(committedRuntime),
82
+ `${PROFILE_CONFIG_FILE}.committedRuntime 必须为对象`,
83
+ )
84
+ assert(
85
+ typeof committedRuntime.target === 'string' && committedRuntime.target.trim(),
86
+ `${PROFILE_CONFIG_FILE}.committedRuntime.target 必须为非空字符串`,
87
+ )
88
+ assert(Array.isArray(committedRuntime.keys), `${PROFILE_CONFIG_FILE}.committedRuntime.keys 必须为数组`)
89
+ const seenKeys = new Set()
90
+ for (const key of committedRuntime.keys) {
91
+ assert(
92
+ typeof key === 'string' && ENV_KEY_PATTERN.test(key),
93
+ `${PROFILE_CONFIG_FILE}.committedRuntime.keys 包含非法键: ${String(key)}`,
94
+ )
95
+ assert(!seenKeys.has(key), `${PROFILE_CONFIG_FILE}.committedRuntime.keys 包含重复键: ${key}`)
96
+ seenKeys.add(key)
73
97
  }
74
98
  }
75
99
 
@@ -163,6 +187,70 @@ function mergeEntries(...maps) {
163
187
  return merged
164
188
  }
165
189
 
190
+ function resolveCommittedRuntimeEnv({ projectRoot, config, policy, environment }) {
191
+ const committedRuntime = config.committedRuntime
192
+ if (!committedRuntime || committedRuntime.keys.length === 0) return {}
193
+
194
+ const targetId = committedRuntime.target
195
+ const target = policy.targets?.[targetId]
196
+ assert(target, `${PROFILE_CONFIG_FILE}.committedRuntime.target 指向不存在的 policy target: ${targetId}`)
197
+ assert(
198
+ policy.environments.includes(environment),
199
+ `${PROFILE_CONFIG_FILE}.committedRuntime 不支持 policy 未声明的环境: ${environment}`,
200
+ )
201
+
202
+ const committedTemplate = target.files.committed
203
+ assert(
204
+ committedTemplate.includes('{env}'),
205
+ `env-policy.jsonc.targets.${targetId}.files.committed 必须包含 {env} 才能用于 committed runtime`,
206
+ )
207
+ const projectBoundary = resolve(projectRoot)
208
+ const committedPath = resolve(projectBoundary, committedTemplate.replace(/\{env\}/g, environment))
209
+ const pathWithinProject = relative(projectBoundary, committedPath)
210
+ assert(
211
+ pathWithinProject &&
212
+ pathWithinProject !== '..' &&
213
+ !pathWithinProject.startsWith(`..${sep}`) &&
214
+ !isAbsolute(pathWithinProject),
215
+ `committed runtime 文件必须位于项目根目录内: ${committedTemplate}`,
216
+ )
217
+
218
+ const secretKeys = new Set(policy.keys?.secret || [])
219
+ const localOnlyKeys = new Set(policy.keys?.localOnly || [])
220
+ for (const key of committedRuntime.keys) {
221
+ assert(!secretKeys.has(key), `committed runtime 不允许导出机密键: ${key}`)
222
+ assert(!localOnlyKeys.has(key), `committed runtime 不允许导出 localOnly 键: ${key}`)
223
+ }
224
+
225
+ assert(existsSync(committedPath), `committed runtime 文件不存在: ${pathWithinProject}`)
226
+ const linkStat = lstatSync(committedPath)
227
+ assert(!linkStat.isSymbolicLink(), `committed runtime 文件不允许使用符号链接: ${pathWithinProject}`)
228
+ assert(statSync(committedPath).isFile(), `committed runtime 路径必须是普通文件: ${pathWithinProject}`)
229
+ const realProjectBoundary = realpathSync(projectBoundary)
230
+ const realCommittedPath = realpathSync(committedPath)
231
+ const realPathWithinProject = relative(realProjectBoundary, realCommittedPath)
232
+ assert(
233
+ realPathWithinProject &&
234
+ realPathWithinProject !== '..' &&
235
+ !realPathWithinProject.startsWith(`..${sep}`) &&
236
+ !isAbsolute(realPathWithinProject),
237
+ `committed runtime 文件解析后必须位于项目根目录内: ${pathWithinProject}`,
238
+ )
239
+
240
+ const placeholder = normalizedValue(policy.secretPlaceholder)
241
+ const committedEntries = parseEnvContent(readFileSync(committedPath, 'utf8'), committedPath)
242
+ const committedEnv = {}
243
+
244
+ for (const key of committedRuntime.keys) {
245
+ assert(committedEntries.has(key), `${pathWithinProject}: 缺少 committed runtime 键 ${key}`)
246
+ const value = normalizedValue(committedEntries.get(key))
247
+ assert(value && value !== placeholder, `${pathWithinProject}: committed runtime 键 ${key} 不能是空值或占位符`)
248
+ committedEnv[key] = value
249
+ }
250
+
251
+ return committedEnv
252
+ }
253
+
166
254
  export function validateEnvProfile({ projectRoot, configDir, profile, environment }) {
167
255
  const config = loadEnvProfileConfig(configDir)
168
256
  assert(config.profiles[profile], `未声明 env profile: ${profile}`)
@@ -177,6 +265,12 @@ export function validateEnvProfile({ projectRoot, configDir, profile, environmen
177
265
  assertGitIgnored(projectRoot, paths.source)
178
266
 
179
267
  const policy = loadEnvPolicy(configDir)
268
+ const committedRuntimeEnv = resolveCommittedRuntimeEnv({
269
+ projectRoot,
270
+ config,
271
+ policy,
272
+ environment,
273
+ })
180
274
  const placeholder = normalizedValue(policy.secretPlaceholder)
181
275
  const secretKeys = new Set(policy.keys?.secret || [])
182
276
  const allowedLocalKeys = new Set([
@@ -240,6 +334,7 @@ export function validateEnvProfile({ projectRoot, configDir, profile, environmen
240
334
  source: paths.source,
241
335
  target: paths.target,
242
336
  keyCount: profileEntries.size,
337
+ committedRuntimeEnv,
243
338
  }
244
339
  }
245
340
 
@@ -421,6 +516,7 @@ export async function executeWithEnvProfile({
421
516
  env: {
422
517
  ...process.env,
423
518
  ...profileEnv,
519
+ ...validated.committedRuntimeEnv,
424
520
  DX_ENV_PROFILE: profile,
425
521
  DX_ENV_PROFILE_ENVIRONMENT: environment,
426
522
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weotro/dx",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,7 +36,7 @@ description: ""
36
36
  | 当前宿主 | reviewer |
37
37
  |---|---|
38
38
  | Codex | 已暴露且可直接调用 Claude Code 时使用 Claude Code;否则派一个 fresh-context reviewer subagent |
39
- | Claude Code | 有匹配的 `codex:*` 技能时使用该技能;代码审查优先 `codex:review`,否则派一个 fresh-context reviewer subagent |
39
+ | Claude Code | 有匹配的 `codex:*` 技能时使用该技能;代码审查优先 `codex:rescue`,否则派一个 fresh-context reviewer subagent |
40
40
  | 其他宿主 | 派一个 fresh-context reviewer subagent |
41
41
 
42
42
  一次任务只派发一个 reviewer。调用失败或超时后不重试、不改派第二个 reviewer;返回 `UNAVAILABLE`。reviewer 不得递归委派。