@weotro/dx 0.1.21 → 0.2.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.
Files changed (71) hide show
  1. package/README.md +76 -768
  2. package/bin/dx.js +100 -182
  3. package/example/.env.development +1 -0
  4. package/example/README.md +24 -0
  5. package/example/dx/scripts/build +2 -0
  6. package/example/dx/scripts/db +2 -0
  7. package/example/dx/scripts/deploy +2 -0
  8. package/example/dx/scripts/setup +2 -0
  9. package/example/dx/scripts/start +12 -0
  10. package/example/dx/scripts/test +2 -0
  11. package/lib/env.js +40 -257
  12. package/lib/logger.js +5 -238
  13. package/lib/setup.js +72 -0
  14. package/package.json +27 -13
  15. package/skills/ask_3th/SKILL.md +2 -0
  16. package/skills/delegate_3th/SKILL.md +4 -2
  17. package/skills/delegate_3th/scripts/delegate_3th.py +37 -41
  18. package/skills/delegate_3th/scripts/test_delegate_3th.py +43 -17
  19. package/bin/dx-with-version-env.js +0 -8
  20. package/lib/artifact-deploy/artifact-builder.js +0 -144
  21. package/lib/artifact-deploy/config.js +0 -180
  22. package/lib/artifact-deploy/remote-script.js +0 -301
  23. package/lib/artifact-deploy/remote-transport.js +0 -86
  24. package/lib/artifact-deploy.js +0 -70
  25. package/lib/backend-artifact-deploy/artifact-builder.js +0 -267
  26. package/lib/backend-artifact-deploy/config.js +0 -218
  27. package/lib/backend-artifact-deploy/path-utils.js +0 -18
  28. package/lib/backend-artifact-deploy/remote-phases.js +0 -14
  29. package/lib/backend-artifact-deploy/remote-result.js +0 -44
  30. package/lib/backend-artifact-deploy/remote-script.js +0 -507
  31. package/lib/backend-artifact-deploy/remote-transport.js +0 -123
  32. package/lib/backend-artifact-deploy/rollback.js +0 -5
  33. package/lib/backend-artifact-deploy/runtime-package.js +0 -46
  34. package/lib/backend-artifact-deploy.js +0 -91
  35. package/lib/backend-package.js +0 -674
  36. package/lib/cli/args.js +0 -38
  37. package/lib/cli/command-result.js +0 -1
  38. package/lib/cli/commands/contracts.js +0 -60
  39. package/lib/cli/commands/core.js +0 -533
  40. package/lib/cli/commands/db.js +0 -231
  41. package/lib/cli/commands/deploy.js +0 -175
  42. package/lib/cli/commands/env.js +0 -120
  43. package/lib/cli/commands/export.js +0 -39
  44. package/lib/cli/commands/package.js +0 -22
  45. package/lib/cli/commands/release.js +0 -55
  46. package/lib/cli/commands/stack.js +0 -427
  47. package/lib/cli/commands/start.js +0 -58
  48. package/lib/cli/commands/worktree.js +0 -145
  49. package/lib/cli/dx-cli.js +0 -1098
  50. package/lib/cli/flags.js +0 -123
  51. package/lib/cli/help-model.js +0 -234
  52. package/lib/cli/help-renderer.js +0 -137
  53. package/lib/cli/help-schema.js +0 -552
  54. package/lib/cli/help.js +0 -141
  55. package/lib/cli/index.js +0 -4
  56. package/lib/cli/nx-command.js +0 -13
  57. package/lib/confirm.js +0 -213
  58. package/lib/env-policy.js +0 -134
  59. package/lib/env-profile.js +0 -531
  60. package/lib/exec.js +0 -698
  61. package/lib/nx-ignore.js +0 -45
  62. package/lib/run-with-version-env.js +0 -163
  63. package/lib/sdk-build.js +0 -424
  64. package/lib/start-dev.js +0 -401
  65. package/lib/telegram-webhook.js +0 -431
  66. package/lib/validate-env.js +0 -317
  67. package/lib/vercel-deploy.js +0 -549
  68. package/lib/worktree.js +0 -1052
  69. package/skills/delegate_3th/scripts/__pycache__/delegate_3th.cpython-314.pyc +0 -0
  70. package/skills/delegate_3th/scripts/__pycache__/delegate_cc.cpython-314.pyc +0 -0
  71. package/skills/delegate_3th/scripts/__pycache__/test_delegate_cc.cpython-314.pyc +0 -0
@@ -1,531 +0,0 @@
1
- import { spawn, spawnSync } from 'node:child_process'
2
- import { createHash, randomUUID } from 'node:crypto'
3
- import {
4
- chmodSync,
5
- closeSync,
6
- existsSync,
7
- lstatSync,
8
- mkdirSync,
9
- openSync,
10
- readFileSync,
11
- realpathSync,
12
- renameSync,
13
- rmSync,
14
- statSync,
15
- writeFileSync,
16
- } from 'node:fs'
17
- import { tmpdir } from 'node:os'
18
- import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path'
19
- import { loadEnvPolicy, resolveTargetRequiredVars } from './env-policy.js'
20
-
21
- const PROFILE_CONFIG_FILE = 'env-profiles.json'
22
- const PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/
23
- const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
24
- const SIGNAL_EXIT_CODES = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 }
25
-
26
- function assert(condition, message) {
27
- if (!condition) throw new Error(message)
28
- }
29
-
30
- function parseJsonFile(filePath) {
31
- try {
32
- return JSON.parse(readFileSync(filePath, 'utf8'))
33
- } catch (error) {
34
- throw new Error(`无法解析 ${filePath}: ${error.message}`)
35
- }
36
- }
37
-
38
- export function loadEnvProfileConfig(configDir) {
39
- const filePath = join(configDir, PROFILE_CONFIG_FILE)
40
- assert(existsSync(filePath), `缺少配置文件 ${filePath}`)
41
- const config = parseJsonFile(filePath)
42
-
43
- assert(config.version === 1, `${PROFILE_CONFIG_FILE}.version 必须为 1`)
44
- assert(
45
- config.profiles && typeof config.profiles === 'object' && !Array.isArray(config.profiles),
46
- `${PROFILE_CONFIG_FILE}.profiles 必须为对象`,
47
- )
48
- const profileNames = Object.keys(config.profiles)
49
- assert(profileNames.length > 0, `${PROFILE_CONFIG_FILE}.profiles 不能为空`)
50
- for (const name of profileNames) {
51
- assert(PROFILE_NAME_PATTERN.test(name), `非法 profile 名称: ${name}`)
52
- const profile = config.profiles[name]
53
- assert(profile && typeof profile === 'object' && !Array.isArray(profile), `profile ${name} 必须为对象`)
54
- if (profile.label !== undefined) {
55
- assert(typeof profile.label === 'string' && profile.label.trim(), `profile ${name}.label 必须为非空字符串`)
56
- }
57
- }
58
-
59
- assert(Array.isArray(config.environments), `${PROFILE_CONFIG_FILE}.environments 必须为数组`)
60
- assert(config.environments.length > 0, `${PROFILE_CONFIG_FILE}.environments 不能为空`)
61
- for (const environment of config.environments) {
62
- assert(typeof environment === 'string' && environment.trim(), 'environment 必须为非空字符串')
63
- }
64
-
65
- const requiredLocalKeys = config.requiredLocalKeys || {}
66
- assert(
67
- requiredLocalKeys && typeof requiredLocalKeys === 'object' && !Array.isArray(requiredLocalKeys),
68
- `${PROFILE_CONFIG_FILE}.requiredLocalKeys 必须为对象`,
69
- )
70
- for (const [environment, keys] of Object.entries(requiredLocalKeys)) {
71
- assert(config.environments.includes(environment), `requiredLocalKeys 包含未声明环境: ${environment}`)
72
- assert(Array.isArray(keys), `requiredLocalKeys.${environment} 必须为数组`)
73
- for (const key of keys) {
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)
97
- }
98
- }
99
-
100
- return config
101
- }
102
-
103
- export function resolveEnvProfilePaths({ projectRoot, profile, environment }) {
104
- assert(PROFILE_NAME_PATTERN.test(profile || ''), `非法 profile 名称: ${profile || '<empty>'}`)
105
- assert(typeof environment === 'string' && environment.trim(), 'environment 不能为空')
106
- const profileDirectory = join(projectRoot, 'dx', 'env', 'templates', profile)
107
- return {
108
- source: join(profileDirectory, `${environment}.local`),
109
- target: join(projectRoot, `.env.${environment}.local`),
110
- template: join(profileDirectory, `${environment}.local.example`),
111
- }
112
- }
113
-
114
- function parseEnvContent(content, filePath) {
115
- const entries = new Map()
116
- const errors = []
117
- const lines = String(content).split(/\r?\n/)
118
-
119
- lines.forEach((rawLine, index) => {
120
- let line = rawLine.trim()
121
- if (!line || line.startsWith('#')) return
122
- if (line.startsWith('export ')) line = line.slice('export '.length).trim()
123
- const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/)
124
- if (!match) {
125
- errors.push(`${filePath}:${index + 1}: 不是合法的 KEY=VALUE`)
126
- return
127
- }
128
- if (entries.has(match[1])) {
129
- errors.push(`${filePath}:${index + 1}: 重复键 ${match[1]}`)
130
- return
131
- }
132
- entries.set(match[1], match[2])
133
- })
134
-
135
- if (errors.length > 0) throw new Error(errors.join('\n'))
136
- return entries
137
- }
138
-
139
- function normalizedValue(value) {
140
- const text = String(value ?? '').trim()
141
- if (
142
- text.length >= 2 &&
143
- ((text.startsWith('"') && text.endsWith('"')) ||
144
- (text.startsWith("'") && text.endsWith("'")))
145
- ) {
146
- return text.slice(1, -1).trim()
147
- }
148
- return text
149
- }
150
-
151
- function assertPrivatePermissions(filePath) {
152
- const linkStat = lstatSync(filePath)
153
- assert(!linkStat.isSymbolicLink(), `profile 不允许使用符号链接: ${filePath}`)
154
- const stat = statSync(filePath)
155
- assert(stat.isFile(), `profile 必须是普通文件: ${filePath}`)
156
- if (process.platform === 'win32') return
157
- const publicBits = stat.mode & 0o077
158
- assert(
159
- publicBits === 0,
160
- `${filePath} 权限过宽(当前 ${(stat.mode & 0o777).toString(8).padStart(3, '0')}),请执行 chmod 600 ${basename(filePath)}`,
161
- )
162
- }
163
-
164
- function assertGitIgnored(projectRoot, filePath) {
165
- const inside = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], {
166
- cwd: projectRoot,
167
- encoding: 'utf8',
168
- stdio: ['ignore', 'pipe', 'ignore'],
169
- })
170
- if (inside.status !== 0) return
171
-
172
- const ignored = spawnSync('git', ['check-ignore', '--quiet', '--', relative(projectRoot, filePath)], {
173
- cwd: projectRoot,
174
- stdio: 'ignore',
175
- })
176
- assert(
177
- ignored.status === 0,
178
- `${basename(filePath)} 未被 Git 忽略;为防止泄密,拒绝读取该 profile`,
179
- )
180
- }
181
-
182
- function mergeEntries(...maps) {
183
- const merged = new Map()
184
- for (const map of maps) {
185
- for (const [key, value] of map) merged.set(key, value)
186
- }
187
- return merged
188
- }
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
-
254
- export function validateEnvProfile({ projectRoot, configDir, profile, environment }) {
255
- const config = loadEnvProfileConfig(configDir)
256
- assert(config.profiles[profile], `未声明 env profile: ${profile}`)
257
- assert(config.environments.includes(environment), `profile 不支持环境: ${environment}`)
258
-
259
- const paths = resolveEnvProfilePaths({ projectRoot, profile, environment })
260
- assert(
261
- existsSync(paths.source),
262
- `缺少私有配置 ${basename(paths.source)};请从 ${relative(projectRoot, paths.template)} 复制并填写`,
263
- )
264
- assertPrivatePermissions(paths.source)
265
- assertGitIgnored(projectRoot, paths.source)
266
-
267
- const policy = loadEnvPolicy(configDir)
268
- const committedRuntimeEnv = resolveCommittedRuntimeEnv({
269
- projectRoot,
270
- config,
271
- policy,
272
- environment,
273
- })
274
- const placeholder = normalizedValue(policy.secretPlaceholder)
275
- const secretKeys = new Set(policy.keys?.secret || [])
276
- const allowedLocalKeys = new Set([
277
- ...(policy.keys?.secret || []),
278
- ...(policy.keys?.localOnly || []),
279
- ...(policy.keys?.localOverride || []),
280
- ])
281
- const profileEntries = parseEnvContent(readFileSync(paths.source, 'utf8'), paths.source)
282
- const errors = []
283
- const invalidKeys = new Set()
284
-
285
- for (const [key, value] of profileEntries) {
286
- if (!allowedLocalKeys.has(key)) {
287
- errors.push(`${basename(paths.source)}: 未在 env-policy keys.* 声明的键 ${key}`)
288
- invalidKeys.add(key)
289
- }
290
- const normalized = normalizedValue(value)
291
- if (secretKeys.has(key) && (!normalized || normalized === placeholder)) {
292
- errors.push(`${basename(paths.source)}: 机密键 ${key} 必须设置真实值`)
293
- invalidKeys.add(key)
294
- }
295
- }
296
-
297
- for (const key of config.requiredLocalKeys?.[environment] || []) {
298
- if (!profileEntries.has(key) || !normalizedValue(profileEntries.get(key))) {
299
- if (invalidKeys.has(key)) continue
300
- errors.push(`${basename(paths.source)}: 缺少 profile 必填键 ${key}`)
301
- invalidKeys.add(key)
302
- }
303
- }
304
-
305
- const checkedRequired = new Set()
306
- for (const [targetId, target] of Object.entries(policy.targets || {})) {
307
- const committedTemplate = target?.files?.committed
308
- if (typeof committedTemplate !== 'string') continue
309
- const committedFile = committedTemplate.replace(/\{env\}/g, environment)
310
- const committedPath = join(projectRoot, committedFile)
311
- const committedEntries = existsSync(committedPath)
312
- ? parseEnvContent(readFileSync(committedPath, 'utf8'), committedPath)
313
- : new Map()
314
- const effective = mergeEntries(committedEntries, profileEntries)
315
- for (const key of resolveTargetRequiredVars(policy, targetId, environment)) {
316
- const identity = `${targetId}:${key}`
317
- if (checkedRequired.has(identity)) continue
318
- checkedRequired.add(identity)
319
- const value = normalizedValue(effective.get(key))
320
- if (!value || value === placeholder) {
321
- if (invalidKeys.has(key)) continue
322
- errors.push(`${targetId}@${environment}: 必填键 ${key} 未由 committed env 或 profile 提供`)
323
- invalidKeys.add(key)
324
- }
325
- }
326
- }
327
-
328
- if (errors.length > 0) throw new Error(`env profile 校验未通过:\n${errors.join('\n')}`)
329
-
330
- return {
331
- profile,
332
- label: config.profiles[profile].label || profile,
333
- environment,
334
- source: paths.source,
335
- target: paths.target,
336
- keyCount: profileEntries.size,
337
- committedRuntimeEnv,
338
- }
339
- }
340
-
341
- function sameFileContent(left, right) {
342
- if (!existsSync(left) || !existsSync(right)) return false
343
- const digest = file => createHash('sha256').update(readFileSync(file)).digest('hex')
344
- return digest(left) === digest(right)
345
- }
346
-
347
- export function describeEnvProfiles({ projectRoot, configDir }) {
348
- const config = loadEnvProfileConfig(configDir)
349
- const rows = []
350
- for (const [profile, metadata] of Object.entries(config.profiles)) {
351
- for (const environment of config.environments) {
352
- const paths = resolveEnvProfilePaths({ projectRoot, profile, environment })
353
- let mode = '-'
354
- if (existsSync(paths.source) && process.platform !== 'win32') {
355
- mode = (statSync(paths.source).mode & 0o777).toString(8).padStart(3, '0')
356
- }
357
- rows.push({
358
- profile,
359
- label: metadata.label || profile,
360
- environment,
361
- exists: existsSync(paths.source),
362
- mode,
363
- active: sameFileContent(paths.source, paths.target),
364
- source: paths.source,
365
- })
366
- }
367
- }
368
- return rows
369
- }
370
-
371
- function lockPathFor(projectRoot, environment) {
372
- const key = createHash('sha256').update(`${projectRoot}\0${environment}`).digest('hex').slice(0, 20)
373
- return join(tmpdir(), `dx-env-profile-${key}.lock`)
374
- }
375
-
376
- function isProcessAlive(pid) {
377
- if (!Number.isInteger(pid) || pid <= 0) return false
378
- try {
379
- process.kill(pid, 0)
380
- return true
381
- } catch (error) {
382
- return error?.code === 'EPERM'
383
- }
384
- }
385
-
386
- function acquireLock(projectRoot, environment, profile) {
387
- const lockPath = lockPathFor(projectRoot, environment)
388
- for (let attempt = 0; attempt < 2; attempt += 1) {
389
- try {
390
- const fd = openSync(lockPath, 'wx', 0o600)
391
- writeFileSync(fd, JSON.stringify({ pid: process.pid, profile, environment }))
392
- closeSync(fd)
393
- return lockPath
394
- } catch (error) {
395
- if (error?.code !== 'EEXIST') throw error
396
- let owner = null
397
- try {
398
- owner = JSON.parse(readFileSync(lockPath, 'utf8'))
399
- } catch {}
400
- if (owner && isProcessAlive(owner.pid)) {
401
- throw new Error(
402
- `已有 env profile 操作运行中: pid=${owner.pid}, profile=${owner.profile}, environment=${owner.environment}`,
403
- )
404
- }
405
- recoverStaleTransaction(projectRoot, environment, owner?.transaction)
406
- rmSync(lockPath, { force: true })
407
- }
408
- }
409
- throw new Error(`无法取得 env profile 锁: ${lockPath}`)
410
- }
411
-
412
- function writeLockState(lockPath, state) {
413
- const temporary = `${lockPath}.${randomUUID()}.tmp`
414
- writeFileSync(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 })
415
- renameSync(temporary, lockPath)
416
- }
417
-
418
- function recoverStaleTransaction(projectRoot, environment, transaction) {
419
- if (!transaction || typeof transaction !== 'object') return
420
- const expectedTarget = join(projectRoot, `.env.${environment}.local`)
421
- if (transaction.target !== expectedTarget) return
422
- if (
423
- typeof transaction.temporary !== 'string' ||
424
- !transaction.temporary.startsWith(`${expectedTarget}.dx-profile-`)
425
- ) {
426
- return
427
- }
428
-
429
- rmSync(expectedTarget, { force: true })
430
- rmSync(transaction.temporary, { force: true })
431
- }
432
-
433
- function materializeProfile(source, target, onPrepared) {
434
- const suffix = randomUUID()
435
- const temporary = `${target}.dx-profile-${suffix}`
436
- assert(
437
- !existsSync(target),
438
- `不允许存在持久根配置 ${basename(target)};请迁移到品牌 profile 后删除该文件`,
439
- )
440
- const transaction = { target, temporary }
441
- onPrepared?.(transaction)
442
-
443
- writeFileSync(temporary, readFileSync(source), { flag: 'wx', mode: 0o600 })
444
- chmodSync(temporary, 0o600)
445
-
446
- try {
447
- renameSync(temporary, target)
448
- } catch (error) {
449
- rmSync(temporary, { force: true })
450
- throw error
451
- }
452
-
453
- return {
454
- transaction,
455
- restore() {
456
- rmSync(target, { force: true })
457
- rmSync(temporary, { force: true })
458
- },
459
- }
460
- }
461
-
462
- function runChild(command, args, options) {
463
- return new Promise((resolve, reject) => {
464
- const child = spawn(command, args, {
465
- cwd: options.cwd,
466
- env: options.env,
467
- stdio: 'inherit',
468
- shell: false,
469
- })
470
- const handlers = new Map()
471
- for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
472
- const handler = () => {
473
- if (child.exitCode === null && child.signalCode === null) child.kill(signal)
474
- }
475
- handlers.set(signal, handler)
476
- process.on(signal, handler)
477
- }
478
- const removeHandlers = () => {
479
- for (const [signal, handler] of handlers) process.off(signal, handler)
480
- }
481
-
482
- child.once('error', error => {
483
- removeHandlers()
484
- reject(error)
485
- })
486
- child.once('close', (code, signal) => {
487
- removeHandlers()
488
- resolve(code ?? SIGNAL_EXIT_CODES[signal] ?? 1)
489
- })
490
- })
491
- }
492
-
493
- export async function executeWithEnvProfile({
494
- projectRoot,
495
- configDir,
496
- profile,
497
- environment,
498
- command,
499
- args = [],
500
- }) {
501
- assert(typeof command === 'string' && command.trim(), 'env exec 必须在 -- 后提供要执行的命令')
502
- const validated = validateEnvProfile({ projectRoot, configDir, profile, environment })
503
- const lockPath = acquireLock(projectRoot, environment, profile)
504
- let materialized = null
505
-
506
- try {
507
- materialized = materializeProfile(validated.source, validated.target, transaction => {
508
- writeLockState(lockPath, { pid: process.pid, profile, environment, transaction })
509
- })
510
- const profileEntries = parseEnvContent(readFileSync(validated.source, 'utf8'), validated.source)
511
- const profileEnv = Object.fromEntries(
512
- [...profileEntries].map(([key, value]) => [key, normalizedValue(value)]),
513
- )
514
- return await runChild(command, args, {
515
- cwd: projectRoot,
516
- env: {
517
- ...process.env,
518
- ...profileEnv,
519
- ...validated.committedRuntimeEnv,
520
- DX_ENV_PROFILE: profile,
521
- DX_ENV_PROFILE_ENVIRONMENT: environment,
522
- },
523
- })
524
- } finally {
525
- try {
526
- materialized?.restore()
527
- } finally {
528
- rmSync(lockPath, { force: true })
529
- }
530
- }
531
- }