@weotro/dx 0.1.19 → 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.
- package/README.md +77 -718
- package/bin/dx.js +100 -173
- package/example/.env.development +1 -0
- package/example/README.md +24 -0
- package/example/dx/scripts/build +2 -0
- package/example/dx/scripts/db +2 -0
- package/example/dx/scripts/deploy +2 -0
- package/example/dx/scripts/setup +2 -0
- package/example/dx/scripts/start +12 -0
- package/example/dx/scripts/test +2 -0
- package/lib/env.js +40 -257
- package/lib/logger.js +5 -238
- package/lib/setup.js +72 -0
- package/package.json +27 -13
- package/skills/ask_3th/SKILL.md +2 -0
- package/skills/delegate_3th/SKILL.md +4 -2
- package/skills/delegate_3th/scripts/delegate_3th.py +37 -41
- package/skills/delegate_3th/scripts/test_delegate_3th.py +43 -17
- package/bin/dx-with-version-env.js +0 -8
- package/lib/artifact-deploy/artifact-builder.js +0 -144
- package/lib/artifact-deploy/config.js +0 -180
- package/lib/artifact-deploy/remote-script.js +0 -301
- package/lib/artifact-deploy/remote-transport.js +0 -86
- package/lib/artifact-deploy.js +0 -70
- package/lib/backend-artifact-deploy/artifact-builder.js +0 -267
- package/lib/backend-artifact-deploy/config.js +0 -218
- package/lib/backend-artifact-deploy/path-utils.js +0 -18
- package/lib/backend-artifact-deploy/remote-phases.js +0 -14
- package/lib/backend-artifact-deploy/remote-result.js +0 -44
- package/lib/backend-artifact-deploy/remote-script.js +0 -507
- package/lib/backend-artifact-deploy/remote-transport.js +0 -123
- package/lib/backend-artifact-deploy/rollback.js +0 -5
- package/lib/backend-artifact-deploy/runtime-package.js +0 -46
- package/lib/backend-artifact-deploy.js +0 -91
- package/lib/backend-package.js +0 -674
- package/lib/cli/args.js +0 -38
- package/lib/cli/command-result.js +0 -1
- package/lib/cli/commands/contracts.js +0 -60
- package/lib/cli/commands/core.js +0 -533
- package/lib/cli/commands/db.js +0 -231
- package/lib/cli/commands/deploy.js +0 -175
- package/lib/cli/commands/env.js +0 -120
- package/lib/cli/commands/export.js +0 -39
- package/lib/cli/commands/package.js +0 -22
- package/lib/cli/commands/release.js +0 -55
- package/lib/cli/commands/stack.js +0 -427
- package/lib/cli/commands/start.js +0 -58
- package/lib/cli/commands/worktree.js +0 -145
- package/lib/cli/dx-cli.js +0 -1072
- package/lib/cli/flags.js +0 -123
- package/lib/cli/help-model.js +0 -222
- package/lib/cli/help-renderer.js +0 -137
- package/lib/cli/help-schema.js +0 -552
- package/lib/cli/help.js +0 -141
- package/lib/cli/index.js +0 -4
- package/lib/cli/nx-command.js +0 -13
- package/lib/confirm.js +0 -213
- package/lib/env-policy.js +0 -134
- package/lib/env-profile.js +0 -531
- package/lib/exec.js +0 -692
- package/lib/nx-ignore.js +0 -45
- package/lib/run-with-version-env.js +0 -163
- package/lib/sdk-build.js +0 -424
- package/lib/start-dev.js +0 -401
- package/lib/telegram-webhook.js +0 -431
- package/lib/validate-env.js +0 -317
- package/lib/vercel-deploy.js +0 -549
- package/lib/worktree.js +0 -1052
- package/skills/delegate_3th/scripts/__pycache__/delegate_3th.cpython-314.pyc +0 -0
- package/skills/delegate_3th/scripts/__pycache__/delegate_cc.cpython-314.pyc +0 -0
- package/skills/delegate_3th/scripts/__pycache__/test_delegate_cc.cpython-314.pyc +0 -0
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process'
|
|
2
|
-
import { basename } from 'node:path'
|
|
3
|
-
import { parseRemoteResult } from '../backend-artifact-deploy/remote-result.js'
|
|
4
|
-
import { buildRemoteDeployScript } from './remote-script.js'
|
|
5
|
-
|
|
6
|
-
function runProcess(command, args, options = {}) {
|
|
7
|
-
return new Promise((resolve, reject) => {
|
|
8
|
-
const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], ...options })
|
|
9
|
-
let stdout = ''
|
|
10
|
-
let stderr = ''
|
|
11
|
-
child.stdout.on('data', chunk => { stdout += String(chunk) })
|
|
12
|
-
child.stderr.on('data', chunk => { stderr += String(chunk) })
|
|
13
|
-
child.on('error', reject)
|
|
14
|
-
child.on('close', exitCode => resolve({ stdout, stderr, exitCode }))
|
|
15
|
-
})
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function escapeShellArg(value) {
|
|
19
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function buildEnsureRemoteBaseDirsCommand(baseDir) {
|
|
23
|
-
const normalizedBaseDir = String(baseDir).replace(/\/+$/, '') || '/'
|
|
24
|
-
const directories = ['releases', 'shared', 'uploads'].map(name => `${normalizedBaseDir}/${name}`)
|
|
25
|
-
return `mkdir -p ${directories.map(escapeShellArg).join(' ')}`
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function defaultEnsureRemoteBaseDirs(remote) {
|
|
29
|
-
const target = `${remote.user}@${remote.host}`
|
|
30
|
-
const result = await runProcess('ssh', [
|
|
31
|
-
'-p',
|
|
32
|
-
String(remote.port || 22),
|
|
33
|
-
target,
|
|
34
|
-
buildEnsureRemoteBaseDirsCommand(remote.baseDir),
|
|
35
|
-
])
|
|
36
|
-
if (result.exitCode !== 0) throw new Error(result.stderr || `ssh mkdir failed (${result.exitCode})`)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async function defaultUploadBundle(remote, bundlePath) {
|
|
40
|
-
const target = `${remote.user}@${remote.host}:${remote.baseDir}/uploads/${basename(bundlePath)}`
|
|
41
|
-
const result = await runProcess('scp', ['-P', String(remote.port || 22), bundlePath, target])
|
|
42
|
-
if (result.exitCode !== 0) throw new Error(result.stderr || `scp failed (${result.exitCode})`)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function defaultRunRemoteScript(remote, script) {
|
|
46
|
-
const target = `${remote.user}@${remote.host}`
|
|
47
|
-
return new Promise((resolve, reject) => {
|
|
48
|
-
const child = spawn('ssh', ['-p', String(remote.port || 22), target, 'bash -s'], {
|
|
49
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
50
|
-
})
|
|
51
|
-
let stdout = ''
|
|
52
|
-
let stderr = ''
|
|
53
|
-
child.stdout.on('data', chunk => { stdout += String(chunk) })
|
|
54
|
-
child.stderr.on('data', chunk => { stderr += String(chunk) })
|
|
55
|
-
child.on('error', reject)
|
|
56
|
-
child.on('close', exitCode => resolve({ stdout, stderr, exitCode }))
|
|
57
|
-
child.stdin.write(script)
|
|
58
|
-
child.stdin.end()
|
|
59
|
-
})
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function deployArtifactRemotely(config, bundle, deps = {}) {
|
|
63
|
-
const ensureRemoteBaseDirs = deps.ensureRemoteBaseDirs || defaultEnsureRemoteBaseDirs
|
|
64
|
-
const uploadBundle = deps.uploadBundle || defaultUploadBundle
|
|
65
|
-
const runRemoteScript = deps.runRemoteScript || defaultRunRemoteScript
|
|
66
|
-
|
|
67
|
-
await ensureRemoteBaseDirs(config.remote)
|
|
68
|
-
await uploadBundle(config.remote, bundle.bundlePath)
|
|
69
|
-
const payload = {
|
|
70
|
-
environment: config.environment,
|
|
71
|
-
versionName: bundle.versionName,
|
|
72
|
-
uploadedBundlePath: `${config.remote.baseDir}/uploads/${basename(bundle.bundlePath)}`,
|
|
73
|
-
remote: config.remote,
|
|
74
|
-
artifact: {
|
|
75
|
-
innerArchiveName: bundle.innerArchiveName || `${bundle.versionName}.tgz`,
|
|
76
|
-
checksumName: bundle.checksumName || `${bundle.versionName}.tgz.sha256`,
|
|
77
|
-
},
|
|
78
|
-
startup: config.startup,
|
|
79
|
-
deploy: config.deploy,
|
|
80
|
-
verify: config.verify,
|
|
81
|
-
}
|
|
82
|
-
const commandResult = await runRemoteScript(config.remote, buildRemoteDeployScript(payload))
|
|
83
|
-
const result = parseRemoteResult(commandResult)
|
|
84
|
-
if (!result.ok) throw new Error(`远端部署失败(${result.phase}): ${result.message}`)
|
|
85
|
-
return result
|
|
86
|
-
}
|
package/lib/artifact-deploy.js
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { access } from 'node:fs/promises'
|
|
2
|
-
import { basename, resolve } from 'node:path'
|
|
3
|
-
import { buildArtifact } from './artifact-deploy/artifact-builder.js'
|
|
4
|
-
import { resolveArtifactDeployConfig } from './artifact-deploy/config.js'
|
|
5
|
-
import { deployArtifactRemotely } from './artifact-deploy/remote-transport.js'
|
|
6
|
-
import { logger as defaultLogger } from './logger.js'
|
|
7
|
-
|
|
8
|
-
export async function loadArtifact(config, artifactPath, deps = {}) {
|
|
9
|
-
const ensureReadable = deps.ensureArtifactReadable || access
|
|
10
|
-
const bundlePath = resolve(config.projectRoot, artifactPath)
|
|
11
|
-
await ensureReadable(bundlePath)
|
|
12
|
-
|
|
13
|
-
const bundleFile = basename(bundlePath)
|
|
14
|
-
const prefix = `${config.artifact.bundleName}-v`
|
|
15
|
-
if (!bundleFile.startsWith(prefix) || !bundleFile.endsWith('.tgz')) {
|
|
16
|
-
throw new Error(`制品文件名必须匹配 ${prefix}<version>-<timestamp>.tgz`)
|
|
17
|
-
}
|
|
18
|
-
const versionTag = bundleFile.slice(prefix.length, -'.tgz'.length)
|
|
19
|
-
if (!versionTag) throw new Error('无法从制品文件名解析版本')
|
|
20
|
-
const versionName = `${config.artifact.releaseName}-v${versionTag}`
|
|
21
|
-
return {
|
|
22
|
-
bundlePath,
|
|
23
|
-
versionName,
|
|
24
|
-
innerArchiveName: `${versionName}.tgz`,
|
|
25
|
-
checksumName: `${versionName}.tgz.sha256`,
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function printSuccessfulDeploySummary(result, logger) {
|
|
30
|
-
const summary = result?.summary
|
|
31
|
-
if (!summary) return
|
|
32
|
-
logger.success(`制品部署成功: ${summary.releaseName || 'unknown-release'}`)
|
|
33
|
-
if (summary.currentRelease) logger.info(`[deploy-summary] current=${summary.currentRelease}`)
|
|
34
|
-
if (summary.serviceName || summary.startupMode) {
|
|
35
|
-
logger.info(
|
|
36
|
-
`[deploy-summary] service=${summary.serviceName || 'custom-command'} mode=${summary.startupMode || 'unknown'}`,
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
if (summary.healthUrl) logger.info(`[deploy-summary] health=${summary.healthUrl}`)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function runArtifactDeploy({ cli, target, args, environment, deps = {} }) {
|
|
43
|
-
const logger = deps.logger || defaultLogger
|
|
44
|
-
const resolveConfig = deps.resolveConfig || resolveArtifactDeployConfig
|
|
45
|
-
const build = deps.buildArtifact || buildArtifact
|
|
46
|
-
const deployRemotely = deps.deployRemotely || deployArtifactRemotely
|
|
47
|
-
const config = resolveConfig({
|
|
48
|
-
cli,
|
|
49
|
-
target,
|
|
50
|
-
targetConfig: cli?.commands?.deploy?.[target],
|
|
51
|
-
environment,
|
|
52
|
-
flags: cli?.flags || {},
|
|
53
|
-
args,
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
const artifactPath = cli?.flags?.artifact
|
|
57
|
-
if (cli?.flags?.buildOnly && artifactPath) {
|
|
58
|
-
throw new Error('--build-only 与 --artifact 不能同时使用')
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const load = deps.loadArtifact || loadArtifact
|
|
62
|
-
const bundle = artifactPath
|
|
63
|
-
? await load(config, artifactPath, deps)
|
|
64
|
-
: await build(config, deps)
|
|
65
|
-
if (cli?.flags?.buildOnly) return bundle
|
|
66
|
-
|
|
67
|
-
const result = await deployRemotely(config, bundle, deps)
|
|
68
|
-
if (result?.ok) printSuccessfulDeploySummary(result, logger)
|
|
69
|
-
return result
|
|
70
|
-
}
|
|
@@ -1,267 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,218 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|