@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,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runWithVersionEnv } from '../lib/run-with-version-env.js'
4
+
5
+ runWithVersionEnv(process.argv.slice(2)).catch(error => {
6
+ console.error('[dx-with-version-env] failed:', error?.message || String(error))
7
+ process.exit(1)
8
+ })
package/bin/dx.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync } from 'node:fs'
4
+ import { resolve, dirname, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ function parseConfigDir(argv) {
8
+ const envValue = process.env.DX_CONFIG_DIR
9
+ if (envValue) return String(envValue)
10
+
11
+ const args = Array.isArray(argv) ? argv : []
12
+ const idx = args.indexOf('--config-dir')
13
+ if (idx !== -1 && idx + 1 < args.length) return String(args[idx + 1])
14
+ for (const token of args) {
15
+ if (token.startsWith('--config-dir=')) return String(token.slice('--config-dir='.length))
16
+ }
17
+ return null
18
+ }
19
+
20
+ function stripConfigDirArgs(argv) {
21
+ const args = Array.isArray(argv) ? [...argv] : []
22
+ const out = []
23
+ for (let i = 0; i < args.length; i++) {
24
+ const token = args[i]
25
+ if (token === '--config-dir') {
26
+ i++
27
+ continue
28
+ }
29
+ if (token.startsWith('--config-dir=')) continue
30
+ out.push(token)
31
+ }
32
+ return out
33
+ }
34
+
35
+ function isVersionInvocation(argv) {
36
+ const raw = Array.isArray(argv) ? argv : []
37
+ const filtered = stripConfigDirArgs(raw)
38
+
39
+ const flags = []
40
+ const positionals = []
41
+ for (const token of filtered) {
42
+ if (token === '--') break
43
+ if (token.startsWith('-')) flags.push(token)
44
+ else positionals.push(token)
45
+ }
46
+
47
+ // Keep current semantics:
48
+ // - -V/--version always prints version and exits.
49
+ // - -v is verbose in commands, but `dx -v` is commonly expected to show version.
50
+ const hasCanonicalVersionFlag = flags.includes('-V') || flags.includes('--version')
51
+ if (hasCanonicalVersionFlag) return true
52
+
53
+ const isBareLowerV = flags.length === 1 && flags[0] === '-v' && positionals.length === 0
54
+ if (isBareLowerV) return true
55
+
56
+ const isVersionCommand = positionals.length === 1 && positionals[0] === 'version'
57
+ if (isVersionCommand) return true
58
+
59
+ return false
60
+ }
61
+
62
+ function isInitialInvocation(argv) {
63
+ const raw = Array.isArray(argv) ? argv : []
64
+ const filtered = stripConfigDirArgs(raw)
65
+
66
+ for (const token of filtered) {
67
+ if (token === '--') break
68
+ if (token.startsWith('-')) continue
69
+ return token === 'initial'
70
+ }
71
+
72
+ return false
73
+ }
74
+
75
+ function findProjectRootFrom(startDir) {
76
+ let current = resolve(startDir)
77
+ while (true) {
78
+ const marker = join(current, 'dx', 'config', 'commands.json')
79
+ if (existsSync(marker)) return current
80
+
81
+ const parent = dirname(current)
82
+ if (parent === current) return null
83
+ current = parent
84
+ }
85
+ }
86
+
87
+ function findRepoRootFrom(startDir) {
88
+ let current = resolve(startDir)
89
+ while (true) {
90
+ // ai-monorepo style marker
91
+ if (existsSync(join(current, 'pnpm-workspace.yaml'))) return current
92
+ // fallback marker
93
+ if (existsSync(join(current, 'package.json'))) return current
94
+
95
+ const parent = dirname(current)
96
+ if (parent === current) return null
97
+ current = parent
98
+ }
99
+ }
100
+
101
+ function inferProjectRootFromConfigDir(configDir, startDir) {
102
+ const normalized = String(configDir).replace(/\\/g, '/')
103
+ if (normalized.endsWith('/dx/config')) return resolve(configDir, '..', '..')
104
+ if (normalized.endsWith('/scripts/config')) return resolve(configDir, '..', '..')
105
+
106
+ return findRepoRootFrom(configDir) || findRepoRootFrom(startDir) || resolve(startDir)
107
+ }
108
+
109
+ async function main() {
110
+ const rawArgs = process.argv.slice(2)
111
+
112
+ if (isVersionInvocation(rawArgs)) {
113
+ const { getPackageVersion } = await import('../lib/version.js')
114
+ console.log(getPackageVersion())
115
+ return
116
+ }
117
+
118
+ if (isInitialInvocation(rawArgs)) {
119
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
120
+ const [{ logger }, { runCodexInitial }] = await Promise.all([
121
+ import('../lib/logger.js'),
122
+ import('../lib/codex-initial.js'),
123
+ ])
124
+
125
+ try {
126
+ await runCodexInitial({ packageRoot })
127
+ return
128
+ } catch (error) {
129
+ logger.error('initial 执行失败')
130
+ logger.error(error?.message || String(error))
131
+ process.exit(1)
132
+ }
133
+ }
134
+
135
+ const overrideConfigDir = parseConfigDir(rawArgs)
136
+ const filteredArgs = stripConfigDirArgs(rawArgs)
137
+
138
+ const startDir = process.cwd()
139
+ let projectRoot
140
+ let configDir
141
+
142
+ if (overrideConfigDir) {
143
+ // When dx is installed globally, users may prefer providing DX_CONFIG_DIR/--config-dir.
144
+ // In that case, do not require dx/config marker discovery.
145
+ configDir = resolve(startDir, overrideConfigDir)
146
+ if (!existsSync(join(configDir, 'commands.json'))) {
147
+ console.error(`dx: 配置目录无效: ${configDir}`)
148
+ console.error('dx: 期望存在 commands.json')
149
+ process.exit(1)
150
+ }
151
+ projectRoot = inferProjectRootFromConfigDir(configDir, startDir)
152
+ } else {
153
+ projectRoot = findProjectRootFrom(startDir)
154
+ if (!projectRoot) {
155
+ console.error('dx: 未找到项目配置目录: dx/config/commands.json')
156
+ console.error('dx: 请在项目目录内执行 dx,或先创建 dx/config 并放置 commands.json')
157
+ console.error('dx: 也可通过 DX_CONFIG_DIR 或 --config-dir 指定配置目录')
158
+ process.exit(1)
159
+ }
160
+ configDir = join(projectRoot, 'dx', 'config')
161
+ }
162
+
163
+ process.env.DX_PROJECT_ROOT = projectRoot
164
+ process.env.DX_CONFIG_DIR = configDir
165
+
166
+ process.chdir(projectRoot)
167
+
168
+ process.argv = [process.argv[0], process.argv[1], ...filteredArgs]
169
+
170
+ const [{ logger }, { DxCli }] = await Promise.all([
171
+ import('../lib/logger.js'),
172
+ import('../lib/cli/dx-cli.js'),
173
+ ])
174
+
175
+ const cli = new DxCli({ projectRoot, configDir: process.env.DX_CONFIG_DIR, invocation: 'dx' })
176
+ await cli.run().catch(error => {
177
+ logger.error('CLI启动失败')
178
+ logger.error(error?.message || String(error))
179
+ process.exit(1)
180
+ })
181
+ }
182
+
183
+ main().catch(error => {
184
+ console.error('dx: CLI启动失败')
185
+ console.error(error?.message || String(error))
186
+ process.exit(1)
187
+ })
@@ -0,0 +1,144 @@
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 } from 'node:path'
5
+ import { promisify } from 'node:util'
6
+ import { execManager } from '../exec.js'
7
+ import { resolveWithinBase } from '../backend-artifact-deploy/path-utils.js'
8
+
9
+ const execFileAsync = promisify(execFile)
10
+ const tarEnv = {
11
+ ...process.env,
12
+ COPYFILE_DISABLE: '1',
13
+ COPY_EXTENDED_ATTRIBUTES_DISABLE: '1',
14
+ }
15
+
16
+ function assertSafeNamePart(value, label) {
17
+ const text = String(value || '').trim()
18
+ if (!text || text.includes('/') || text.includes('\\') || text.includes('..')) {
19
+ throw new Error(`${label} 越界,已拒绝: ${text}`)
20
+ }
21
+ return text
22
+ }
23
+
24
+ function defaultNowTag() {
25
+ const now = new Date()
26
+ const pad = value => String(value).padStart(2, '0')
27
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`
28
+ }
29
+
30
+ async function defaultRunBuild(build, environment) {
31
+ await execManager.executeCommand(build.command, {
32
+ app: build.app || undefined,
33
+ skipEnvValidation: !build.app,
34
+ flags: environment === 'production'
35
+ ? { prod: true }
36
+ : environment === 'staging'
37
+ ? { staging: true }
38
+ : { dev: true },
39
+ })
40
+ }
41
+
42
+ async function defaultReadVersion(config) {
43
+ if (config.artifact.version) return config.artifact.version
44
+ if (config.build.versionCommand) {
45
+ const { stdout } = await execFileAsync('bash', ['-lc', config.build.versionCommand], {
46
+ cwd: config.projectRoot,
47
+ env: process.env,
48
+ })
49
+ return String(stdout).trim()
50
+ }
51
+
52
+ const parsed = JSON.parse(await readFile(config.build.versionFile, 'utf8'))
53
+ return String(parsed.version || '').trim()
54
+ }
55
+
56
+ async function defaultStageFiles(sourceDir, stageDir) {
57
+ if (!existsSync(sourceDir)) throw new Error(`缺少待打包目录: ${sourceDir}`)
58
+ await rm(stageDir, { recursive: true, force: true })
59
+ await mkdir(stageDir, { recursive: true })
60
+ for (const entry of await readdir(sourceDir)) {
61
+ await cp(join(sourceDir, entry), join(stageDir, entry), { recursive: true })
62
+ }
63
+ }
64
+
65
+ async function defaultAssertNoEnvFiles(stageDir) {
66
+ const queue = ['.']
67
+ const envFiles = []
68
+ while (queue.length > 0) {
69
+ const relativeDir = queue.shift()
70
+ const currentDir = relativeDir === '.' ? stageDir : join(stageDir, relativeDir)
71
+ for (const entry of await readdir(currentDir, { withFileTypes: true })) {
72
+ const relativePath = relativeDir === '.' ? entry.name : join(relativeDir, entry.name)
73
+ if (entry.name.startsWith('.env')) envFiles.push(relativePath.replace(/\\/g, '/'))
74
+ if (entry.isDirectory()) queue.push(relativePath)
75
+ }
76
+ }
77
+ if (envFiles.length > 0) throw new Error(`制品目录包含 .env* 文件: ${envFiles.join(', ')}`)
78
+ }
79
+
80
+ export function createArtifactNames({ version, timeTag, bundleName, releaseName }) {
81
+ const safeVersion = assertSafeNamePart(version, 'version')
82
+ const safeTimeTag = assertSafeNamePart(timeTag, 'timeTag')
83
+ const safeBundleName = assertSafeNamePart(bundleName, 'bundleName')
84
+ const safeReleaseName = assertSafeNamePart(releaseName, 'releaseName')
85
+ const versionName = `${safeReleaseName}-v${safeVersion}-${safeTimeTag}`
86
+ const innerArchiveName = `${versionName}.tgz`
87
+ return {
88
+ versionName,
89
+ innerArchiveName,
90
+ checksumName: `${innerArchiveName}.sha256`,
91
+ bundleName: `${safeBundleName}-v${safeVersion}-${safeTimeTag}.tgz`,
92
+ }
93
+ }
94
+
95
+ export async function buildArtifact(config, deps = {}) {
96
+ const runBuild = deps.runBuild || defaultRunBuild
97
+ const readVersion = deps.readVersion || defaultReadVersion
98
+ const nowTag = deps.nowTag || defaultNowTag
99
+ const stageFiles = deps.stageFiles || defaultStageFiles
100
+ const assertNoEnvFiles = deps.assertNoEnvFiles || defaultAssertNoEnvFiles
101
+ const version = await readVersion(config)
102
+ if (!version) throw new Error('无法解析制品版本')
103
+
104
+ const names = createArtifactNames({
105
+ version,
106
+ timeTag: nowTag(),
107
+ bundleName: config.artifact.bundleName,
108
+ releaseName: config.artifact.releaseName,
109
+ })
110
+ const outputDir = resolveWithinBase(config.artifact.outputDir, '.', 'artifact.outputDir')
111
+ const stageDir = resolveWithinBase(outputDir, names.versionName, 'stageDir')
112
+ const innerArchivePath = resolveWithinBase(outputDir, names.innerArchiveName, 'innerArchivePath')
113
+ const checksumPath = resolveWithinBase(outputDir, names.checksumName, 'checksumPath')
114
+ const bundlePath = resolveWithinBase(outputDir, names.bundleName, 'bundlePath')
115
+
116
+ await runBuild(config.build, config.environment)
117
+ await mkdir(outputDir, { recursive: true })
118
+ await stageFiles(config.build.sourceDir, stageDir)
119
+ await assertNoEnvFiles(stageDir)
120
+ await execFileAsync('tar', ['-czf', innerArchivePath, '.'], { cwd: stageDir, env: tarEnv })
121
+
122
+ const archiveName = basename(innerArchivePath)
123
+ let checksum
124
+ try {
125
+ checksum = await execFileAsync('sha256sum', [archiveName], { cwd: dirname(innerArchivePath) })
126
+ } catch {
127
+ checksum = await execFileAsync('shasum', ['-a', '256', archiveName], { cwd: dirname(innerArchivePath) })
128
+ }
129
+ await writeFile(checksumPath, checksum.stdout)
130
+ await execFileAsync('tar', ['-czf', bundlePath, archiveName, basename(checksumPath)], {
131
+ cwd: outputDir,
132
+ env: tarEnv,
133
+ })
134
+
135
+ return {
136
+ version,
137
+ versionName: names.versionName,
138
+ innerArchiveName: names.innerArchiveName,
139
+ checksumName: names.checksumName,
140
+ bundlePath,
141
+ innerArchivePath,
142
+ checksumPath,
143
+ }
144
+ }
@@ -0,0 +1,180 @@
1
+ import { isAbsolute } from 'node:path'
2
+ import { resolveWithinBase } from '../backend-artifact-deploy/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 optionalString(value) {
12
+ return typeof value === 'string' && value.trim() ? value.trim() : null
13
+ }
14
+
15
+ function requireSafeName(value, fieldPath) {
16
+ const name = requireString(value, fieldPath)
17
+ if (!/^[A-Za-z0-9._-]+$/.test(name) || name.includes('..')) {
18
+ throw new Error(`${fieldPath} 包含非法字符: ${name}`)
19
+ }
20
+ return name
21
+ }
22
+
23
+ function requirePositiveInteger(value, fieldPath) {
24
+ const parsed = Number(value)
25
+ if (!Number.isInteger(parsed) || parsed <= 0) {
26
+ throw new Error(`缺少必填配置: ${fieldPath}`)
27
+ }
28
+ return parsed
29
+ }
30
+
31
+ function resolveProjectPath(projectRoot, value, fieldPath) {
32
+ return resolveWithinBase(projectRoot, requireString(value, fieldPath), fieldPath)
33
+ }
34
+
35
+ function requireRemoteBaseDir(value, fieldPath) {
36
+ const baseDir = requireString(value, fieldPath)
37
+ if (!isAbsolute(baseDir)) throw new Error(`${fieldPath} 必须是绝对路径: ${baseDir}`)
38
+ if (!/^\/[A-Za-z0-9._/-]*$/.test(baseDir)) {
39
+ throw new Error(`${fieldPath} 包含非法字符: ${baseDir}`)
40
+ }
41
+ return baseDir.replace(/\/+$/, '') || '/'
42
+ }
43
+
44
+ function resolveRemoteConfig(remoteConfig, environment) {
45
+ if (!remoteConfig || typeof remoteConfig !== 'object') return null
46
+ if (typeof remoteConfig.host === 'string') return remoteConfig
47
+
48
+ const selected = remoteConfig[environment]
49
+ if (!selected || typeof selected !== 'object') {
50
+ throw new Error(`缺少必填配置: remote.${environment}`)
51
+ }
52
+ return selected
53
+ }
54
+
55
+ function resolveBuildCommand(buildConfig, environment) {
56
+ if (buildConfig?.commands && typeof buildConfig.commands === 'object') {
57
+ return requireString(buildConfig.commands[environment], `build.commands.${environment}`)
58
+ }
59
+ return requireString(buildConfig?.command, 'build.command')
60
+ }
61
+
62
+ function resolveHealthCheck(healthCheckConfig) {
63
+ if (healthCheckConfig == null) return null
64
+ const url = requireString(healthCheckConfig.url, 'verify.healthCheck.url')
65
+ try {
66
+ new URL(url)
67
+ } catch {
68
+ throw new Error('缺少必填配置: verify.healthCheck.url')
69
+ }
70
+
71
+ return {
72
+ url,
73
+ timeoutSeconds: healthCheckConfig.timeoutSeconds == null
74
+ ? 10
75
+ : requirePositiveInteger(healthCheckConfig.timeoutSeconds, 'verify.healthCheck.timeoutSeconds'),
76
+ maxWaitSeconds: healthCheckConfig.maxWaitSeconds == null
77
+ ? 24
78
+ : requirePositiveInteger(healthCheckConfig.maxWaitSeconds, 'verify.healthCheck.maxWaitSeconds'),
79
+ retryIntervalSeconds: healthCheckConfig.retryIntervalSeconds == null
80
+ ? 2
81
+ : requirePositiveInteger(
82
+ healthCheckConfig.retryIntervalSeconds,
83
+ 'verify.healthCheck.retryIntervalSeconds',
84
+ ),
85
+ }
86
+ }
87
+
88
+ export function resolveArtifactDeployConfig({ cli, target, targetConfig, environment, flags = {} }) {
89
+ const deployConfig = targetConfig?.artifactDeploy
90
+ if (!deployConfig || typeof deployConfig !== 'object') {
91
+ throw new Error('缺少必填配置: artifactDeploy')
92
+ }
93
+
94
+ const buildConfig = deployConfig.build || {}
95
+ const artifactConfig = deployConfig.artifact || {}
96
+ const startupConfig = deployConfig.startup || {}
97
+ const runConfig = deployConfig.deploy || {}
98
+ const verifyConfig = deployConfig.verify || {}
99
+ const buildOnly = Boolean(flags.buildOnly)
100
+ const remoteConfig = resolveRemoteConfig(deployConfig.remote, environment)
101
+ const startupMode = optionalString(startupConfig.mode) || 'command'
102
+
103
+ if (!['systemd', 'command'].includes(startupMode)) {
104
+ throw new Error('startup.mode 仅支持 systemd 或 command')
105
+ }
106
+
107
+ const versionFile = optionalString(buildConfig.versionFile)
108
+ const versionCommand = optionalString(buildConfig.versionCommand)
109
+ const version = optionalString(artifactConfig.version)
110
+ if (!versionFile && !versionCommand && !version) {
111
+ throw new Error('缺少必填配置: build.versionFile、build.versionCommand 或 artifact.version')
112
+ }
113
+
114
+ const serviceName = optionalString(startupConfig.serviceName)
115
+ const startupCommand = optionalString(startupConfig.command)
116
+ if (serviceName && !/^[A-Za-z0-9_.@-]+$/.test(serviceName)) {
117
+ throw new Error(`startup.serviceName 包含非法字符: ${serviceName}`)
118
+ }
119
+ if (startupMode === 'systemd' && !serviceName) {
120
+ throw new Error('缺少必填配置: startup.serviceName')
121
+ }
122
+ if (startupMode === 'command' && !startupCommand) {
123
+ throw new Error('缺少必填配置: startup.command')
124
+ }
125
+ const healthCheck = resolveHealthCheck(verifyConfig.healthCheck)
126
+
127
+ return {
128
+ projectRoot: cli.projectRoot,
129
+ target,
130
+ environment,
131
+ build: {
132
+ app: optionalString(buildConfig.app),
133
+ command: resolveBuildCommand(buildConfig, environment),
134
+ sourceDir: resolveProjectPath(cli.projectRoot, buildConfig.sourceDir, 'build.sourceDir'),
135
+ versionFile: versionFile
136
+ ? resolveProjectPath(cli.projectRoot, versionFile, 'build.versionFile')
137
+ : null,
138
+ versionCommand,
139
+ },
140
+ artifact: {
141
+ outputDir: resolveProjectPath(cli.projectRoot, artifactConfig.outputDir, 'artifact.outputDir'),
142
+ bundleName: requireSafeName(artifactConfig.bundleName, 'artifact.bundleName'),
143
+ releaseName: requireSafeName(
144
+ optionalString(artifactConfig.releaseName) || String(target),
145
+ 'artifact.releaseName',
146
+ ),
147
+ version,
148
+ },
149
+ remote: buildOnly
150
+ ? null
151
+ : {
152
+ host: requireString(remoteConfig?.host, 'remote.host'),
153
+ port: remoteConfig?.port == null ? 22 : requirePositiveInteger(remoteConfig.port, 'remote.port'),
154
+ user: requireString(remoteConfig?.user, 'remote.user'),
155
+ baseDir: requireRemoteBaseDir(remoteConfig?.baseDir, 'remote.baseDir'),
156
+ },
157
+ startup: {
158
+ mode: startupMode,
159
+ serviceName,
160
+ command: startupCommand,
161
+ rollbackCommand: optionalString(startupConfig.rollbackCommand),
162
+ },
163
+ deploy: {
164
+ keepReleases: runConfig.keepReleases == null
165
+ ? 5
166
+ : requirePositiveInteger(runConfig.keepReleases, 'deploy.keepReleases'),
167
+ installCommand: optionalString(runConfig.installCommand),
168
+ },
169
+ verify: {
170
+ command: optionalString(verifyConfig.command),
171
+ maxWaitSeconds: verifyConfig.maxWaitSeconds == null
172
+ ? healthCheck?.maxWaitSeconds || 24
173
+ : requirePositiveInteger(verifyConfig.maxWaitSeconds, 'verify.maxWaitSeconds'),
174
+ retryIntervalSeconds: verifyConfig.retryIntervalSeconds == null
175
+ ? healthCheck?.retryIntervalSeconds || 2
176
+ : requirePositiveInteger(verifyConfig.retryIntervalSeconds, 'verify.retryIntervalSeconds'),
177
+ healthCheck,
178
+ },
179
+ }
180
+ }