@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +755 -0
  3. package/bin/dx-with-version-env.js +8 -0
  4. package/bin/dx.js +187 -0
  5. package/lib/artifact-deploy/artifact-builder.js +144 -0
  6. package/lib/artifact-deploy/config.js +180 -0
  7. package/lib/artifact-deploy/remote-script.js +301 -0
  8. package/lib/artifact-deploy/remote-transport.js +86 -0
  9. package/lib/artifact-deploy.js +70 -0
  10. package/lib/backend-artifact-deploy/artifact-builder.js +267 -0
  11. package/lib/backend-artifact-deploy/config.js +218 -0
  12. package/lib/backend-artifact-deploy/path-utils.js +18 -0
  13. package/lib/backend-artifact-deploy/remote-phases.js +14 -0
  14. package/lib/backend-artifact-deploy/remote-result.js +44 -0
  15. package/lib/backend-artifact-deploy/remote-script.js +507 -0
  16. package/lib/backend-artifact-deploy/remote-transport.js +123 -0
  17. package/lib/backend-artifact-deploy/rollback.js +5 -0
  18. package/lib/backend-artifact-deploy/runtime-package.js +46 -0
  19. package/lib/backend-artifact-deploy.js +91 -0
  20. package/lib/backend-package.js +674 -0
  21. package/lib/cli/args.js +38 -0
  22. package/lib/cli/command-result.js +1 -0
  23. package/lib/cli/commands/contracts.js +60 -0
  24. package/lib/cli/commands/core.js +533 -0
  25. package/lib/cli/commands/db.js +231 -0
  26. package/lib/cli/commands/deploy.js +175 -0
  27. package/lib/cli/commands/env.js +120 -0
  28. package/lib/cli/commands/export.js +39 -0
  29. package/lib/cli/commands/package.js +22 -0
  30. package/lib/cli/commands/release.js +55 -0
  31. package/lib/cli/commands/stack.js +427 -0
  32. package/lib/cli/commands/start.js +58 -0
  33. package/lib/cli/commands/worktree.js +145 -0
  34. package/lib/cli/dx-cli.js +1072 -0
  35. package/lib/cli/flags.js +123 -0
  36. package/lib/cli/help-model.js +222 -0
  37. package/lib/cli/help-renderer.js +137 -0
  38. package/lib/cli/help-schema.js +552 -0
  39. package/lib/cli/help.js +141 -0
  40. package/lib/cli/index.js +4 -0
  41. package/lib/cli/nx-command.js +13 -0
  42. package/lib/codex-initial.js +271 -0
  43. package/lib/confirm.js +213 -0
  44. package/lib/env-policy.js +134 -0
  45. package/lib/env-profile.js +435 -0
  46. package/lib/env.js +261 -0
  47. package/lib/exec.js +692 -0
  48. package/lib/logger.js +239 -0
  49. package/lib/nx-ignore.js +45 -0
  50. package/lib/run-with-version-env.js +163 -0
  51. package/lib/sdk-build.js +424 -0
  52. package/lib/start-dev.js +401 -0
  53. package/lib/telegram-webhook.js +431 -0
  54. package/lib/validate-env.js +317 -0
  55. package/lib/vercel-deploy.js +549 -0
  56. package/lib/version.js +14 -0
  57. package/lib/worktree.js +1052 -0
  58. package/package.json +45 -0
  59. package/skills/create-issue/SKILL.md +90 -0
  60. package/skills/delivering-design-handoff/SKILL.md +290 -0
  61. package/skills/doctor/SKILL.md +76 -0
  62. package/skills/gh-dependabot-cleanup/SKILL.md +54 -0
  63. package/skills/gh-dependabot-cleanup/agents/openai.yaml +7 -0
  64. package/skills/git-release/SKILL.md +194 -0
  65. package/skills/git-release/agents/openai.yaml +7 -0
  66. package/skills/online-debug-guard/SKILL.md +111 -0
  67. package/skills/ship-issue-pr/SKILL.md +676 -0
  68. package/skills/stagewise-ui-debugging/SKILL.md +48 -0
@@ -0,0 +1,317 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { loadEnvPolicy } from './env-policy.js'
4
+
5
+ const ROOT_DIR = process.env.DX_PROJECT_ROOT || process.cwd()
6
+ const CONFIG_DIR = process.env.DX_CONFIG_DIR || join(ROOT_DIR, 'dx', 'config')
7
+ const ROOT_ENV_FILE = join(ROOT_DIR, '.env')
8
+ const EXTRA_ENV_IGNORED_DIRS = new Set([
9
+ '.cache',
10
+ '.claude',
11
+ '.codex',
12
+ '.git',
13
+ '.idea',
14
+ '.vercel',
15
+ 'node_modules',
16
+ '.nx',
17
+ '.omc',
18
+ '.omx',
19
+ '.opencode',
20
+ '.pytest_cache',
21
+ 'dist',
22
+ 'logs',
23
+ 'tmp',
24
+ '.schaltwerk',
25
+ ])
26
+ const EXTRA_ENV_ALLOWED_PATHS = new Set(['docker/.env', 'docker/.env.example'])
27
+ const ENV_EXAMPLE_FILE = join(ROOT_DIR, '.env.example')
28
+
29
+ export function validateEnvironment() {
30
+ if (!process.env.NODE_ENV) {
31
+ console.warn('⚠️ NODE_ENV 未设置,默认使用 development')
32
+ process.env.NODE_ENV = 'development'
33
+ }
34
+
35
+ if (typeof process.env.APP_ENV === 'string' && process.env.APP_ENV.trim() === '') {
36
+ delete process.env.APP_ENV
37
+ }
38
+
39
+ const policy = loadEnvPolicy(CONFIG_DIR)
40
+
41
+ enforceRootOnlyEnvFiles(policy)
42
+ enforceGlobalLocalFileProhibited()
43
+
44
+ enforceSecretPolicy(policy)
45
+ enforceEnvExamplePolicy(policy)
46
+
47
+ return { nodeEnv: process.env.NODE_ENV, appEnv: process.env.APP_ENV }
48
+ }
49
+
50
+ function enforceRootOnlyEnvFiles(policy) {
51
+ if (existsSync(ROOT_ENV_FILE)) {
52
+ throw new Error(
53
+ '检测到根目录存在 .env 文件,请迁移到 .env.<env> / .env.<env>.local 并删除 .env',
54
+ )
55
+ }
56
+
57
+ // 保留现有规则:禁止子目录出现任意 .env* 文件(除特例路径)
58
+ // 注:该规则对是否启用 env-policy 都有效。
59
+
60
+ const violations = []
61
+ const queue = ['.']
62
+
63
+ while (queue.length > 0) {
64
+ const current = queue.pop()
65
+ const dirPath = join(ROOT_DIR, current)
66
+ const entries = readdirSync(dirPath, { withFileTypes: true })
67
+
68
+ for (const entry of entries) {
69
+ if (entry.isDirectory()) {
70
+ if (EXTRA_ENV_IGNORED_DIRS.has(entry.name)) continue
71
+ const next = current === '.' ? entry.name : `${current}/${entry.name}`
72
+ queue.push(next)
73
+ continue
74
+ }
75
+
76
+ if (!entry.isFile()) continue
77
+ if (!entry.name.startsWith('.env')) continue
78
+
79
+ const relativePath = current === '.' ? entry.name : `${current}/${entry.name}`
80
+
81
+ if (!relativePath.includes('/')) {
82
+ continue
83
+ }
84
+
85
+ if (EXTRA_ENV_ALLOWED_PATHS.has(relativePath)) continue
86
+
87
+ if (policy) {
88
+ const globs = Array.isArray(policy.layout?.allowSubdirGlobs) ? policy.layout.allowSubdirGlobs : []
89
+ if (isAllowedBySimpleGlob(relativePath, globs)) continue
90
+ }
91
+
92
+ violations.push(relativePath)
93
+ }
94
+ }
95
+
96
+ if (violations.length > 0) {
97
+ const list = violations.join(', ')
98
+ throw new Error(
99
+ `检测到非根目录下的 env 文件: ${list}\n请将这些文件迁移到根目录或删除,再重试命令。`,
100
+ )
101
+ }
102
+ }
103
+
104
+ function enforceGlobalLocalFileProhibited() {
105
+ const legacyLocal = join(ROOT_DIR, '.env.local')
106
+ if (existsSync(legacyLocal)) {
107
+ throw new Error('项目已弃用 .env.local,请改用 .env.<env>.local 存放机密信息并删除 .env.local')
108
+ }
109
+ }
110
+
111
+ function enforceSecretPolicy(policy) {
112
+ const placeholder = String(policy.secretPlaceholder || '').trim()
113
+ const secretKeys = new Set(policy.keys?.secret || [])
114
+ const localOnlyKeys = new Set(policy.keys?.localOnly || [])
115
+ const localOverrideKeys = new Set(policy.keys?.localOverride || [])
116
+
117
+ const invalidOverlap = findOverlaps([
118
+ { name: 'keys.secret', set: secretKeys },
119
+ { name: 'keys.localOnly', set: localOnlyKeys },
120
+ { name: 'keys.localOverride', set: localOverrideKeys },
121
+ ])
122
+ if (invalidOverlap.length > 0) {
123
+ throw new Error(`env-policy.jsonc keys 分类存在重复: ${invalidOverlap.join(', ')}`)
124
+ }
125
+
126
+ const errors = []
127
+ const filePairs = listTargetEnvFilePairs(policy)
128
+
129
+ for (const envName of policy.environments || []) {
130
+ for (const pair of filePairs) {
131
+ const committed = replaceEnvToken(pair.committed, envName)
132
+ const local = replaceEnvToken(pair.local, envName)
133
+
134
+ const committedPath = join(ROOT_DIR, committed)
135
+ const localPath = join(ROOT_DIR, local)
136
+
137
+ const committedExists = existsSync(committedPath)
138
+ const localExists = existsSync(localPath)
139
+
140
+ if (!committedExists && !localExists) continue
141
+ if (!committedExists && localExists) {
142
+ errors.push(`${committed} 缺失(但存在 ${local}),请补充 committed 模板文件`)
143
+ continue
144
+ }
145
+
146
+ const committedEntries = committedExists ? parseEnvFile(committedPath) : new Map()
147
+ const localEntries = localExists ? parseEnvFile(localPath) : new Map()
148
+
149
+ // Committed: secret keys must exist and be placeholder.
150
+ for (const key of secretKeys) {
151
+ if (!committedEntries.has(key)) {
152
+ errors.push(`${committed}: 缺少机密键模板 ${key}`)
153
+ continue
154
+ }
155
+ const rawValue = String(committedEntries.get(key) ?? '')
156
+ if (rawValue.trim() !== placeholder) {
157
+ errors.push(`${committed}: 机密键 ${key} 必须使用占位符 ${placeholder}`)
158
+ }
159
+ }
160
+
161
+ // Committed: localOnly must not appear.
162
+ for (const key of localOnlyKeys) {
163
+ if (committedEntries.has(key)) {
164
+ errors.push(`${committed}: localOnly 键 ${key} 不允许出现在非 local 文件中`)
165
+ }
166
+ }
167
+
168
+ if (localExists) {
169
+ const allowedInLocal = new Set([...secretKeys, ...localOnlyKeys, ...localOverrideKeys])
170
+
171
+ for (const [key, value] of localEntries.entries()) {
172
+ if (!allowedInLocal.has(key)) {
173
+ errors.push(`${local}: 包含未声明的键 ${key}(请加入 env-policy.jsonc.keys.* 或迁移到 committed 文件)`)
174
+ continue
175
+ }
176
+
177
+ if (secretKeys.has(key)) {
178
+ if (String(value ?? '').trim() === placeholder) {
179
+ errors.push(`${local}: 机密键 ${key} 不允许使用占位符,请设置真实值`)
180
+ }
181
+ if (!committedEntries.has(key)) {
182
+ errors.push(`${committed}: 缺少机密键模板 ${key}(因为 ${local} 中存在该键)`)
183
+ }
184
+ }
185
+
186
+ if (localOnlyKeys.has(key) && committedEntries.has(key)) {
187
+ errors.push(`${committed}: localOnly 键 ${key} 不允许出现在 committed 文件中(已在 ${local} 中存在)`)
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ if (errors.length > 0) {
195
+ throw new Error(`环境变量机密策略校验未通过:\n${errors.join('\n')}`)
196
+ }
197
+ }
198
+
199
+ function enforceEnvExamplePolicy(policy) {
200
+ if (!existsSync(ENV_EXAMPLE_FILE)) return
201
+
202
+ const placeholder = String(policy.secretPlaceholder || '').trim()
203
+ const secretKeys = new Set(policy.keys?.secret || [])
204
+ const localOnlyKeys = new Set(policy.keys?.localOnly || [])
205
+ const entries = parseEnvFile(ENV_EXAMPLE_FILE)
206
+ const errors = []
207
+
208
+ for (const [key, value] of entries.entries()) {
209
+ if (localOnlyKeys.has(key)) {
210
+ errors.push(`.env.example 不允许包含 localOnly 键: ${key}`)
211
+ continue
212
+ }
213
+ if (secretKeys.has(key)) {
214
+ if (String(value ?? '').trim() !== placeholder) {
215
+ errors.push(`.env.example 中机密键 ${key} 必须使用占位符 ${placeholder}`)
216
+ }
217
+ }
218
+ }
219
+
220
+ if (errors.length > 0) {
221
+ throw new Error(errors.join('\n'))
222
+ }
223
+ }
224
+
225
+ function listTargetEnvFilePairs(policy) {
226
+ const pairs = []
227
+ const targets = policy.targets || {}
228
+ for (const target of Object.values(targets)) {
229
+ const committed = target?.files?.committed
230
+ const local = target?.files?.local
231
+ if (typeof committed !== 'string' || typeof local !== 'string') continue
232
+ pairs.push({ committed, local })
233
+ }
234
+
235
+ // De-dup
236
+ const seen = new Set()
237
+ return pairs.filter(p => {
238
+ const key = `${p.committed}@@${p.local}`
239
+ if (seen.has(key)) return false
240
+ seen.add(key)
241
+ return true
242
+ })
243
+ }
244
+
245
+ function replaceEnvToken(template, envName) {
246
+ return String(template).replace(/\{env\}/g, envName)
247
+ }
248
+
249
+ function isAllowedBySimpleGlob(path, globs) {
250
+ for (const raw of globs) {
251
+ const glob = String(raw)
252
+ if (!glob.includes('*')) {
253
+ if (glob === path) return true
254
+ continue
255
+ }
256
+ if (globToRegex(glob).test(path)) return true
257
+ }
258
+ return false
259
+ }
260
+
261
+ function globToRegex(glob) {
262
+ let pattern = ''
263
+ for (let i = 0; i < glob.length; i++) {
264
+ const ch = glob[i]
265
+ if (ch === '*') {
266
+ if (glob[i + 1] === '*') {
267
+ pattern += '.*'
268
+ i += 1
269
+ } else {
270
+ pattern += '[^/]*'
271
+ }
272
+ } else if ('\\^$+?.()|[]{}'.includes(ch)) {
273
+ pattern += `\\${ch}`
274
+ } else {
275
+ pattern += ch
276
+ }
277
+ }
278
+ return new RegExp(`^${pattern}$`)
279
+ }
280
+
281
+ function findOverlaps(namedSets) {
282
+ const seen = new Map()
283
+ const overlaps = []
284
+ for (const { name, set } of namedSets) {
285
+ for (const key of set) {
286
+ if (seen.has(key)) {
287
+ overlaps.push(`${key} (${seen.get(key)} & ${name})`)
288
+ } else {
289
+ seen.set(key, name)
290
+ }
291
+ }
292
+ }
293
+ return overlaps
294
+ }
295
+
296
+ function parseEnvFile(filePath) {
297
+ const content = readFileSync(filePath, 'utf8')
298
+ const map = new Map()
299
+ const lines = content.split(/\r?\n/)
300
+
301
+ for (const raw of lines) {
302
+ const trimmed = raw.trim()
303
+ if (!trimmed || trimmed.startsWith('#')) continue
304
+
305
+ const eqIdx = trimmed.indexOf('=')
306
+ if (eqIdx <= 0) continue
307
+
308
+ const key = trimmed.slice(0, eqIdx).trim()
309
+ if (!key) continue
310
+
311
+ map.set(key, trimmed.slice(eqIdx + 1))
312
+ }
313
+
314
+ return map
315
+ }
316
+
317
+ export default { validateEnvironment }