@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,267 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
3
+ import { cp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'
4
+ import { basename, dirname, join, relative } from 'node:path'
5
+ import { promisify } from 'node:util'
6
+ import { execManager } from '../exec.js'
7
+ import { basenameOrThrow, resolveWithinBase } from './path-utils.js'
8
+ import { createRuntimePackage } from './runtime-package.js'
9
+
10
+ const execFileAsync = promisify(execFile)
11
+ const tarEnv = {
12
+ ...process.env,
13
+ COPYFILE_DISABLE: '1',
14
+ COPY_EXTENDED_ATTRIBUTES_DISABLE: '1',
15
+ }
16
+
17
+ function assertSafeNamePart(value, label) {
18
+ const text = String(value || '').trim()
19
+ if (!text || text.includes('/') || text.includes('\\') || text.includes('..')) {
20
+ throw new Error(`${label} 越界,已拒绝: ${text}`)
21
+ }
22
+ return text
23
+ }
24
+
25
+ function defaultNowTag() {
26
+ const now = new Date()
27
+ const pad = value => String(value).padStart(2, '0')
28
+ return [
29
+ now.getFullYear(),
30
+ pad(now.getMonth() + 1),
31
+ pad(now.getDate()),
32
+ ].join('') + `-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
33
+ }
34
+
35
+ async function defaultReadVersion(versionFile) {
36
+ const pkg = JSON.parse(await readFile(versionFile, 'utf8'))
37
+ return String(pkg.version || '').trim()
38
+ }
39
+
40
+ export function buildFlagsForEnvironment(environment) {
41
+ switch (environment || 'development') {
42
+ case 'production':
43
+ return { prod: true }
44
+ case 'staging':
45
+ return { staging: true }
46
+ case 'development':
47
+ default:
48
+ return { dev: true }
49
+ }
50
+ }
51
+
52
+ async function defaultRunBuild(build, environment = 'development') {
53
+ if (!build?.command) {
54
+ throw new Error('缺少构建命令: build.command')
55
+ }
56
+ await execManager.executeCommand(build.command, {
57
+ app: build.app || undefined,
58
+ flags: buildFlagsForEnvironment(environment),
59
+ })
60
+ }
61
+
62
+ async function defaultPrepareOutputDir(outputDir) {
63
+ await mkdir(outputDir, { recursive: true })
64
+ }
65
+
66
+ async function copyIntoDir(source, destinationDir) {
67
+ if (!existsSync(source)) {
68
+ throw new Error(`缺少必需文件或目录: ${source}`)
69
+ }
70
+ await cp(source, destinationDir, { recursive: true })
71
+ }
72
+
73
+ async function defaultStageFiles({ config, stageDir, stagePlan }) {
74
+ await rm(stageDir, { recursive: true, force: true })
75
+ await mkdir(stageDir, { recursive: true })
76
+
77
+ for (const entry of await readdir(stagePlan.dist.source)) {
78
+ await copyIntoDir(join(stagePlan.dist.source, entry), join(stageDir, entry))
79
+ }
80
+
81
+ const appPackage = JSON.parse(await readFile(stagePlan.appPackage.source, 'utf8'))
82
+ const rootPackage = JSON.parse(await readFile(stagePlan.rootPackage.source, 'utf8'))
83
+ const runtimePackage = createRuntimePackage({ appPackage, rootPackage })
84
+ await writeFile(join(stageDir, stagePlan.runtimePackage.destination), `${JSON.stringify(runtimePackage, null, 2)}\n`)
85
+
86
+ await copyIntoDir(stagePlan.lockfile.source, join(stageDir, stagePlan.lockfile.destination))
87
+
88
+ if (stagePlan.prismaSchema) {
89
+ await mkdir(join(stageDir, dirname(stagePlan.prismaSchema.destination)), { recursive: true })
90
+ await copyIntoDir(stagePlan.prismaSchema.source, join(stageDir, stagePlan.prismaSchema.destination))
91
+ }
92
+ if (stagePlan.prismaConfig) {
93
+ await mkdir(join(stageDir, dirname(stagePlan.prismaConfig.destination)), { recursive: true })
94
+ await copyIntoDir(stagePlan.prismaConfig.source, join(stageDir, stagePlan.prismaConfig.destination))
95
+ }
96
+ if (stagePlan.ecosystemConfig) {
97
+ await copyIntoDir(stagePlan.ecosystemConfig.source, join(stageDir, stagePlan.ecosystemConfig.destination))
98
+ }
99
+ }
100
+
101
+ async function defaultAssertNoEnvFiles(stageDir) {
102
+ const envFiles = []
103
+ const queue = ['.']
104
+
105
+ while (queue.length > 0) {
106
+ const currentRelativeDir = queue.shift()
107
+ const currentDir = currentRelativeDir === '.' ? stageDir : join(stageDir, currentRelativeDir)
108
+ const entries = await readdir(currentDir, { withFileTypes: true })
109
+
110
+ for (const entry of entries) {
111
+ const entryRelativePath =
112
+ currentRelativeDir === '.' ? entry.name : join(currentRelativeDir, entry.name)
113
+
114
+ if (entry.name.startsWith('.env')) {
115
+ envFiles.push(entryRelativePath.replace(/\\/g, '/'))
116
+ }
117
+
118
+ if (entry.isDirectory()) {
119
+ queue.push(entryRelativePath)
120
+ }
121
+ }
122
+ }
123
+
124
+ if (envFiles.length > 0) {
125
+ throw new Error(`制品目录包含 .env* 文件: ${envFiles.join(', ')}`)
126
+ }
127
+ }
128
+
129
+ async function defaultCreateInnerArchive({ stageDir, innerArchivePath }) {
130
+ await mkdir(dirname(innerArchivePath), { recursive: true })
131
+ await execFileAsync('tar', ['-czf', innerArchivePath, '.'], {
132
+ cwd: stageDir,
133
+ env: tarEnv,
134
+ })
135
+ }
136
+
137
+ async function defaultWriteChecksum({ archivePath, checksumPath }) {
138
+ const archiveName = basename(archivePath)
139
+ try {
140
+ const { stdout } = await execFileAsync('sha256sum', [archiveName], {
141
+ cwd: dirname(archivePath),
142
+ })
143
+ await writeFile(checksumPath, stdout)
144
+ } catch {
145
+ const { stdout } = await execFileAsync('shasum', ['-a', '256', archiveName], {
146
+ cwd: dirname(archivePath),
147
+ })
148
+ await writeFile(checksumPath, stdout)
149
+ }
150
+ }
151
+
152
+ async function defaultCreateBundle({ outputDir, bundlePath, innerArchivePath, checksumPath }) {
153
+ await execFileAsync(
154
+ 'tar',
155
+ ['-czf', bundlePath, basename(innerArchivePath), basename(checksumPath)],
156
+ {
157
+ cwd: outputDir,
158
+ env: tarEnv,
159
+ },
160
+ )
161
+ }
162
+
163
+ export function createArtifactNames({ version, timeTag, bundleName }) {
164
+ const safeVersion = assertSafeNamePart(version, 'version')
165
+ const safeTimeTag = assertSafeNamePart(timeTag, 'timeTag')
166
+ const safeBundleName = assertSafeNamePart(bundleName, 'bundleName')
167
+ const versionName = `backend-v${safeVersion}-${safeTimeTag}`
168
+ const innerArchiveName = `${versionName}.tgz`
169
+ return {
170
+ versionName,
171
+ innerArchiveName,
172
+ checksumName: `${innerArchiveName}.sha256`,
173
+ bundleName: `${safeBundleName}-v${safeVersion}-${safeTimeTag}.tgz`,
174
+ }
175
+ }
176
+
177
+ export function createStagePlan(config) {
178
+ const projectRoot = config.projectRoot || '/'
179
+ const relativeToProject = targetPath =>
180
+ relative(projectRoot, targetPath).replace(/\\/g, '/').replace(/^repo\//, '')
181
+ const plan = {
182
+ dist: {
183
+ source: config.build.distDir,
184
+ destination: '.',
185
+ },
186
+ runtimePackage: {
187
+ destination: 'package.json',
188
+ },
189
+ lockfile: {
190
+ source: config.runtime.lockfile,
191
+ destination: 'pnpm-lock.yaml',
192
+ },
193
+ appPackage: {
194
+ source: config.runtime.appPackage,
195
+ },
196
+ rootPackage: {
197
+ source: config.runtime.rootPackage,
198
+ },
199
+ }
200
+
201
+ if (config.runtime.prismaSchemaDir) {
202
+ plan.prismaSchema = {
203
+ source: config.runtime.prismaSchemaDir,
204
+ destination: relativeToProject(config.runtime.prismaSchemaDir),
205
+ }
206
+ }
207
+ if (config.runtime.prismaConfig) {
208
+ plan.prismaConfig = {
209
+ source: config.runtime.prismaConfig,
210
+ destination: relativeToProject(config.runtime.prismaConfig),
211
+ }
212
+ }
213
+ if (config.runtime.ecosystemConfig) {
214
+ plan.ecosystemConfig = {
215
+ source: config.runtime.ecosystemConfig,
216
+ destination: basenameOrThrow(config.runtime.ecosystemConfig, 'runtime.ecosystemConfig'),
217
+ }
218
+ }
219
+
220
+ return plan
221
+ }
222
+
223
+ export async function buildBackendArtifact(config, deps = {}) {
224
+ const nowTag = deps.nowTag || defaultNowTag
225
+ const readVersion = deps.readVersion || defaultReadVersion
226
+ const runBuild = deps.runBuild || defaultRunBuild
227
+ const prepareOutputDir = deps.prepareOutputDir || defaultPrepareOutputDir
228
+ const stageFiles = deps.stageFiles || defaultStageFiles
229
+ const assertNoEnvFiles = deps.assertNoEnvFiles || defaultAssertNoEnvFiles
230
+ const createInnerArchive = deps.createInnerArchive || defaultCreateInnerArchive
231
+ const writeChecksum = deps.writeChecksum || defaultWriteChecksum
232
+ const createBundle = deps.createBundle || defaultCreateBundle
233
+ const version = await readVersion(config.build.versionFile)
234
+ const timeTag = nowTag()
235
+ const names = createArtifactNames({
236
+ version,
237
+ timeTag,
238
+ bundleName: config.artifact.bundleName,
239
+ })
240
+
241
+ const outputDir = resolveWithinBase(config.artifact.outputDir, '.', 'artifact.outputDir')
242
+ const stageDir = resolveWithinBase(outputDir, names.versionName, 'stageDir')
243
+ const innerArchivePath = resolveWithinBase(outputDir, names.innerArchiveName, 'innerArchivePath')
244
+ const checksumPath = resolveWithinBase(outputDir, names.checksumName, 'checksumPath')
245
+ const bundlePath = resolveWithinBase(outputDir, names.bundleName, 'bundlePath')
246
+
247
+ await runBuild(config.build, config.environment)
248
+ await prepareOutputDir(outputDir)
249
+ await stageFiles({
250
+ config,
251
+ stageDir,
252
+ stagePlan: createStagePlan(config),
253
+ })
254
+ await assertNoEnvFiles(stageDir)
255
+ await createInnerArchive({ stageDir, innerArchivePath })
256
+ await writeChecksum({ archivePath: innerArchivePath, checksumPath })
257
+ await createBundle({ outputDir, bundlePath, innerArchivePath, checksumPath })
258
+
259
+ return {
260
+ version,
261
+ timeTag,
262
+ versionName: names.versionName,
263
+ bundlePath,
264
+ innerArchivePath,
265
+ checksumPath,
266
+ }
267
+ }
@@ -0,0 +1,218 @@
1
+ import { isAbsolute } from 'node:path'
2
+ import { resolveWithinBase } from './path-utils.js'
3
+
4
+ function requireString(value, fieldPath) {
5
+ if (typeof value !== 'string' || value.trim() === '') {
6
+ throw new Error(`缺少必填配置: ${fieldPath}`)
7
+ }
8
+ return value.trim()
9
+ }
10
+
11
+ function requirePositiveInteger(value, fieldPath) {
12
+ const parsed = Number(value)
13
+ if (!Number.isInteger(parsed) || parsed <= 0) {
14
+ throw new Error(`缺少必填配置: ${fieldPath}`)
15
+ }
16
+ return parsed
17
+ }
18
+
19
+ function requireEnvName(value, fieldPath) {
20
+ const name = requireString(value, fieldPath)
21
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
22
+ throw new Error(`缺少必填配置: ${fieldPath}`)
23
+ }
24
+ return name
25
+ }
26
+
27
+ function resolveHealthCheckEndpoint(healthCheckConfig) {
28
+ if (healthCheckConfig.url != null) {
29
+ const url = requireString(healthCheckConfig.url, 'verify.healthCheck.url')
30
+ try {
31
+ new URL(url)
32
+ } catch {
33
+ throw new Error(`缺少必填配置: verify.healthCheck.url`)
34
+ }
35
+ return { url }
36
+ }
37
+
38
+ if (healthCheckConfig.envPort != null) {
39
+ const rawPath = healthCheckConfig.path == null ? '/health' : requireString(healthCheckConfig.path, 'verify.healthCheck.path')
40
+ return {
41
+ envPort: requireEnvName(healthCheckConfig.envPort, 'verify.healthCheck.envPort'),
42
+ path: rawPath.startsWith('/') ? rawPath : `/${rawPath}`,
43
+ }
44
+ }
45
+
46
+ throw new Error(`缺少必填配置: verify.healthCheck.url`)
47
+ }
48
+
49
+ function resolveVerifyConfig(verifyConfig = {}) {
50
+ const healthCheckConfig = verifyConfig?.healthCheck
51
+ if (healthCheckConfig == null) {
52
+ return {
53
+ healthCheck: null,
54
+ }
55
+ }
56
+
57
+ return {
58
+ healthCheck: {
59
+ ...resolveHealthCheckEndpoint(healthCheckConfig),
60
+ timeoutSeconds:
61
+ healthCheckConfig.timeoutSeconds == null
62
+ ? 10
63
+ : requirePositiveInteger(healthCheckConfig.timeoutSeconds, 'verify.healthCheck.timeoutSeconds'),
64
+ maxWaitSeconds:
65
+ healthCheckConfig.maxWaitSeconds == null
66
+ ? 24
67
+ : requirePositiveInteger(healthCheckConfig.maxWaitSeconds, 'verify.healthCheck.maxWaitSeconds'),
68
+ retryIntervalSeconds:
69
+ healthCheckConfig.retryIntervalSeconds == null
70
+ ? 2
71
+ : requirePositiveInteger(
72
+ healthCheckConfig.retryIntervalSeconds,
73
+ 'verify.healthCheck.retryIntervalSeconds',
74
+ ),
75
+ },
76
+ }
77
+ }
78
+
79
+ function resolveBuildCommand(buildConfig, environment) {
80
+ if (buildConfig?.commands && typeof buildConfig.commands === 'object') {
81
+ const selected = buildConfig.commands[environment]
82
+ if (!selected || typeof selected !== 'string' || selected.trim() === '') {
83
+ throw new Error(`缺少必填配置: build.commands.${environment}`)
84
+ }
85
+ return selected.trim()
86
+ }
87
+
88
+ return requireString(buildConfig?.command, 'build.command')
89
+ }
90
+
91
+ function resolveProjectPath(projectRoot, relativePath, fieldPath) {
92
+ return resolveWithinBase(projectRoot, requireString(relativePath, fieldPath), fieldPath)
93
+ }
94
+
95
+ function requireRemoteBaseDir(value, fieldPath) {
96
+ const baseDir = requireString(value, fieldPath)
97
+ if (!isAbsolute(baseDir)) {
98
+ throw new Error(`${fieldPath} 必须是绝对路径: ${baseDir}`)
99
+ }
100
+ if (!/^\/[A-Za-z0-9._/-]*$/.test(baseDir)) {
101
+ throw new Error(`${fieldPath} 包含非法字符: ${baseDir}`)
102
+ }
103
+ return baseDir.replace(/\/+$/, '') || '/'
104
+ }
105
+
106
+ function resolveRemoteConfig(remoteConfig, environment) {
107
+ if (!remoteConfig || typeof remoteConfig !== 'object') {
108
+ return null
109
+ }
110
+
111
+ if (typeof remoteConfig.host === 'string') {
112
+ return remoteConfig
113
+ }
114
+
115
+ const selected = remoteConfig[environment]
116
+ if (!selected || typeof selected !== 'object') {
117
+ throw new Error(`缺少必填配置: remote.${environment}`)
118
+ }
119
+ return selected
120
+ }
121
+
122
+ export function resolveBackendDeployConfig({ cli, targetConfig, environment, flags = {} }) {
123
+ const deployConfig = targetConfig?.backendDeploy
124
+ if (!deployConfig || typeof deployConfig !== 'object') {
125
+ throw new Error('缺少必填配置: backendDeploy')
126
+ }
127
+
128
+ const buildConfig = deployConfig.build || {}
129
+ const runtimeConfig = deployConfig.runtime || {}
130
+ const artifactConfig = deployConfig.artifact || {}
131
+ const remoteConfig = resolveRemoteConfig(deployConfig.remote, environment)
132
+ const startupConfig = deployConfig.startup || {}
133
+ const runConfig = deployConfig.deploy || {}
134
+ const verifyConfig = deployConfig.verify || {}
135
+ const buildOnly = Boolean(flags.buildOnly)
136
+ const startupMode = String(startupConfig.mode || 'pm2').trim()
137
+ const prismaGenerate = runConfig.prismaGenerate !== false
138
+ const prismaMigrateDeploy = runConfig.prismaMigrateDeploy !== false
139
+ const prismaSeed = runConfig.prismaSeed === true
140
+
141
+ const normalized = {
142
+ projectRoot: cli.projectRoot,
143
+ environment,
144
+ build: {
145
+ app: typeof buildConfig.app === 'string' && buildConfig.app.trim() ? buildConfig.app.trim() : null,
146
+ command: resolveBuildCommand(buildConfig, environment),
147
+ distDir: resolveProjectPath(cli.projectRoot, buildConfig.distDir, 'build.distDir'),
148
+ versionFile: resolveProjectPath(cli.projectRoot, buildConfig.versionFile, 'build.versionFile'),
149
+ },
150
+ runtime: {
151
+ appPackage: resolveProjectPath(cli.projectRoot, runtimeConfig.appPackage, 'runtime.appPackage'),
152
+ rootPackage: resolveProjectPath(cli.projectRoot, runtimeConfig.rootPackage, 'runtime.rootPackage'),
153
+ lockfile: resolveProjectPath(cli.projectRoot, runtimeConfig.lockfile, 'runtime.lockfile'),
154
+ prismaSchemaDir: runtimeConfig.prismaSchemaDir
155
+ ? resolveProjectPath(cli.projectRoot, runtimeConfig.prismaSchemaDir, 'runtime.prismaSchemaDir')
156
+ : null,
157
+ prismaConfig: runtimeConfig.prismaConfig
158
+ ? resolveProjectPath(cli.projectRoot, runtimeConfig.prismaConfig, 'runtime.prismaConfig')
159
+ : null,
160
+ ecosystemConfig: runtimeConfig.ecosystemConfig
161
+ ? resolveProjectPath(cli.projectRoot, runtimeConfig.ecosystemConfig, 'runtime.ecosystemConfig')
162
+ : null,
163
+ },
164
+ artifact: {
165
+ outputDir: resolveProjectPath(cli.projectRoot, artifactConfig.outputDir, 'artifact.outputDir'),
166
+ bundleName: requireString(artifactConfig.bundleName, 'artifact.bundleName'),
167
+ },
168
+ remote: buildOnly
169
+ ? null
170
+ : {
171
+ host: requireString(remoteConfig?.host, 'remote.host'),
172
+ port: remoteConfig?.port == null ? 22 : requirePositiveInteger(remoteConfig.port, 'remote.port'),
173
+ user: requireString(remoteConfig?.user, 'remote.user'),
174
+ baseDir: requireRemoteBaseDir(remoteConfig?.baseDir, 'remote.baseDir'),
175
+ },
176
+ startup: {
177
+ mode: startupMode,
178
+ serviceName:
179
+ typeof startupConfig.serviceName === 'string' && startupConfig.serviceName.trim()
180
+ ? startupConfig.serviceName.trim()
181
+ : null,
182
+ entry:
183
+ typeof startupConfig.entry === 'string' && startupConfig.entry.trim()
184
+ ? startupConfig.entry.trim()
185
+ : null,
186
+ },
187
+ deploy: {
188
+ keepReleases:
189
+ runConfig.keepReleases == null ? 5 : requirePositiveInteger(runConfig.keepReleases, 'deploy.keepReleases'),
190
+ installCommand: requireString(
191
+ runConfig.installCommand || 'pnpm install --prod --no-frozen-lockfile --ignore-workspace',
192
+ 'deploy.installCommand',
193
+ ),
194
+ prismaGenerate,
195
+ prismaMigrateDeploy,
196
+ prismaSeed,
197
+ skipMigration: Boolean(flags.skipMigration),
198
+ },
199
+ verify: resolveVerifyConfig(verifyConfig),
200
+ }
201
+
202
+ if (!['pm2', 'direct'].includes(normalized.startup.mode)) {
203
+ throw new Error('缺少必填配置: startup.mode')
204
+ }
205
+ if (normalized.startup.mode === 'pm2') {
206
+ requireString(normalized.startup.serviceName, 'startup.serviceName')
207
+ requireString(normalized.runtime.ecosystemConfig, 'runtime.ecosystemConfig')
208
+ } else {
209
+ requireString(normalized.startup.entry, 'startup.entry')
210
+ }
211
+
212
+ if (normalized.deploy.prismaGenerate || normalized.deploy.prismaMigrateDeploy || normalized.deploy.prismaSeed) {
213
+ requireString(normalized.runtime.prismaSchemaDir, 'runtime.prismaSchemaDir')
214
+ requireString(normalized.runtime.prismaConfig, 'runtime.prismaConfig')
215
+ }
216
+
217
+ return normalized
218
+ }
@@ -0,0 +1,18 @@
1
+ import { basename, resolve, sep } from 'node:path'
2
+
3
+ export function resolveWithinBase(baseDir, targetPath, label = 'path') {
4
+ const absoluteBase = resolve(baseDir)
5
+ const absoluteTarget = resolve(absoluteBase, targetPath)
6
+ if (absoluteTarget !== absoluteBase && !absoluteTarget.startsWith(`${absoluteBase}${sep}`)) {
7
+ throw new Error(`${label} 越界,已拒绝: ${absoluteTarget}`)
8
+ }
9
+ return absoluteTarget
10
+ }
11
+
12
+ export function basenameOrThrow(filePath, label = 'path') {
13
+ const name = basename(String(filePath || '').trim())
14
+ if (!name || name === '.' || name === '..') {
15
+ throw new Error(`无效的 ${label}: ${filePath}`)
16
+ }
17
+ return name
18
+ }
@@ -0,0 +1,14 @@
1
+ export function createRemotePhaseModel(payload) {
2
+ return [
3
+ { phase: 'lock', payload },
4
+ { phase: 'extract', payload },
5
+ { phase: 'env', payload },
6
+ { phase: 'install', payload },
7
+ { phase: 'prisma-generate', payload },
8
+ { phase: 'prisma-migrate', payload },
9
+ { phase: 'switch-current', payload },
10
+ { phase: 'startup', payload },
11
+ { phase: 'verify', payload },
12
+ { phase: 'cleanup', payload },
13
+ ]
14
+ }
@@ -0,0 +1,44 @@
1
+ function parseResultLine(line) {
2
+ if (!line.startsWith('DX_REMOTE_RESULT=')) return null
3
+ return JSON.parse(line.slice('DX_REMOTE_RESULT='.length))
4
+ }
5
+
6
+ function getLastPhase(output = '') {
7
+ const lines = String(output).split('\n')
8
+ let phase = 'cleanup'
9
+ for (const line of lines) {
10
+ if (line.startsWith('DX_REMOTE_PHASE=')) {
11
+ phase = line.slice('DX_REMOTE_PHASE='.length).trim() || phase
12
+ }
13
+ }
14
+ return phase
15
+ }
16
+
17
+ export function parseRemoteResult({ stdout = '', stderr = '', exitCode = 0 }) {
18
+ const allLines = `${stdout}\n${stderr}`.trim().split('\n').filter(Boolean)
19
+ for (let index = allLines.length - 1; index >= 0; index -= 1) {
20
+ const parsed = parseResultLine(allLines[index])
21
+ if (parsed) return parsed
22
+ }
23
+
24
+ if (exitCode === 0) {
25
+ return {
26
+ ok: true,
27
+ phase: getLastPhase(stdout),
28
+ message: 'ok',
29
+ rollbackAttempted: false,
30
+ rollbackSucceeded: null,
31
+ summary: null,
32
+ }
33
+ }
34
+
35
+ const message = [stderr, stdout].filter(Boolean).join('\n').trim() || 'remote execution failed'
36
+ return {
37
+ ok: false,
38
+ phase: getLastPhase(`${stdout}\n${stderr}`),
39
+ message,
40
+ rollbackAttempted: false,
41
+ rollbackSucceeded: null,
42
+ summary: null,
43
+ }
44
+ }