@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.
- package/LICENSE +21 -0
- package/README.md +755 -0
- package/bin/dx-with-version-env.js +8 -0
- package/bin/dx.js +187 -0
- package/lib/artifact-deploy/artifact-builder.js +144 -0
- package/lib/artifact-deploy/config.js +180 -0
- package/lib/artifact-deploy/remote-script.js +301 -0
- package/lib/artifact-deploy/remote-transport.js +86 -0
- package/lib/artifact-deploy.js +70 -0
- package/lib/backend-artifact-deploy/artifact-builder.js +267 -0
- package/lib/backend-artifact-deploy/config.js +218 -0
- package/lib/backend-artifact-deploy/path-utils.js +18 -0
- package/lib/backend-artifact-deploy/remote-phases.js +14 -0
- package/lib/backend-artifact-deploy/remote-result.js +44 -0
- package/lib/backend-artifact-deploy/remote-script.js +507 -0
- package/lib/backend-artifact-deploy/remote-transport.js +123 -0
- package/lib/backend-artifact-deploy/rollback.js +5 -0
- package/lib/backend-artifact-deploy/runtime-package.js +46 -0
- package/lib/backend-artifact-deploy.js +91 -0
- package/lib/backend-package.js +674 -0
- package/lib/cli/args.js +38 -0
- package/lib/cli/command-result.js +1 -0
- package/lib/cli/commands/contracts.js +60 -0
- package/lib/cli/commands/core.js +533 -0
- package/lib/cli/commands/db.js +231 -0
- package/lib/cli/commands/deploy.js +175 -0
- package/lib/cli/commands/env.js +120 -0
- package/lib/cli/commands/export.js +39 -0
- package/lib/cli/commands/package.js +22 -0
- package/lib/cli/commands/release.js +55 -0
- package/lib/cli/commands/stack.js +427 -0
- package/lib/cli/commands/start.js +58 -0
- package/lib/cli/commands/worktree.js +145 -0
- package/lib/cli/dx-cli.js +1072 -0
- package/lib/cli/flags.js +123 -0
- package/lib/cli/help-model.js +222 -0
- package/lib/cli/help-renderer.js +137 -0
- package/lib/cli/help-schema.js +552 -0
- package/lib/cli/help.js +141 -0
- package/lib/cli/index.js +4 -0
- package/lib/cli/nx-command.js +13 -0
- package/lib/codex-initial.js +271 -0
- package/lib/confirm.js +213 -0
- package/lib/env-policy.js +134 -0
- package/lib/env-profile.js +435 -0
- package/lib/env.js +261 -0
- package/lib/exec.js +692 -0
- package/lib/logger.js +239 -0
- package/lib/nx-ignore.js +45 -0
- package/lib/run-with-version-env.js +163 -0
- package/lib/sdk-build.js +424 -0
- package/lib/start-dev.js +401 -0
- package/lib/telegram-webhook.js +431 -0
- package/lib/validate-env.js +317 -0
- package/lib/vercel-deploy.js +549 -0
- package/lib/version.js +14 -0
- package/lib/worktree.js +1052 -0
- package/package.json +45 -0
- package/skills/create-issue/SKILL.md +90 -0
- package/skills/delivering-design-handoff/SKILL.md +290 -0
- package/skills/doctor/SKILL.md +76 -0
- package/skills/gh-dependabot-cleanup/SKILL.md +54 -0
- package/skills/gh-dependabot-cleanup/agents/openai.yaml +7 -0
- package/skills/git-release/SKILL.md +194 -0
- package/skills/git-release/agents/openai.yaml +7 -0
- package/skills/online-debug-guard/SKILL.md +111 -0
- package/skills/ship-issue-pr/SKILL.md +676 -0
- 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
|
+
}
|