@h-ai/deploy 0.1.0-alpha.66 → 0.1.0-alpha.68

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/dist/index.js CHANGED
@@ -1,37 +1,47 @@
1
- import { createHash, randomBytes } from 'crypto';
2
- import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, rmSync, statSync, readdirSync } from 'fs';
1
+ import { createHash, randomBytes, randomUUID } from 'crypto';
2
+ import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, renameSync, rmSync, statSync, readdirSync } from 'fs';
3
3
  import { resolve, join, basename, dirname, relative, sep } from 'path';
4
4
  import { z } from 'zod';
5
- import { core, ok, err } from '@h-ai/core';
6
- import process2 from 'process';
5
+ import { core, err, ok } from '@h-ai/core';
6
+ import process3 from 'process';
7
7
  import { parseEnv } from 'util';
8
- import { parse, stringify, parseAllDocuments } from 'yaml';
8
+ import { LineCounter, parseDocument, parse, stringify, parseAllDocuments } from 'yaml';
9
9
  import { homedir } from 'os';
10
10
  import { spawn } from 'child_process';
11
+ import { readdir, readFile } from 'fs/promises';
12
+ import { setTimeout as setTimeout$1 } from 'timers/promises';
11
13
  import { Buffer } from 'buffer';
12
14
 
13
15
  // src/deploy-artifact.ts
14
16
  var DeploymentArtifactSchema = z.object({
15
17
  /** 清单格式版本,与应用版本和部署环境无关。 */
16
- version: z.literal(2),
18
+ version: z.literal(3),
17
19
  /** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
18
20
  appVersion: z.string().max(80).regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9a-z-]+(?:\.[0-9a-z-]+)*)?(?:\+[0-9a-z-]+(?:\.[0-9a-z-]+)*)?$/i),
19
- /** 一次完整构建的标识,app/web 必须来自同一份清单。 */
21
+ /** 本次构建标识;同一应用版本允许重复构建。 */
20
22
  buildId: z.string().regex(/^[a-z0-9][\w.+-]*$/i),
21
- /** API/独立应用和可选 Web 的镜像归档,至少包含一个 app。 */
23
+ /** 只包含所选镜像的构建不能用于首次安装。 */
24
+ partial: z.boolean().default(false),
25
+ /** API 单独构建时仍保留工程的 Web 拓扑。 */
26
+ hasWeb: z.boolean().default(false),
27
+ /** 工程显式配置的应用端口,部分构建未包含 app 镜像时仍保留;缺省由部署配置决定。 */
28
+ appContainerPort: z.number().int().min(1).max(65535).optional(),
29
+ /** 工程显式配置的 Web 端口,部分构建未包含 web 镜像时仍保留;缺省使用部署默认值。 */
30
+ webContainerPort: z.number().int().min(1).max(65535).optional(),
31
+ /** 本次构建选择的镜像归档。 */
22
32
  images: z.array(z.object({
23
33
  /** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
24
34
  name: z.enum(["app", "web"]),
25
35
  /** 相对于清单目录的 tar 文件名。 */
26
36
  file: z.string().regex(/^[\w.+-]+\.tar$/),
27
37
  /** 归档导入后可直接使用的不可变镜像引用。 */
28
- imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]+$/),
38
+ imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
29
39
  /** tar 内容的 SHA-256,用于部署前校验。 */
30
40
  sha256: z.string().regex(/^[a-f0-9]{64}$/),
31
41
  /** 容器内部监听端口;服务器公开端口由部署选择。 */
32
42
  containerPort: z.number().int().min(1).max(65535).optional()
33
- })).min(1).max(2).refine((images) => images.filter((image) => image.name === "app").length === 1 && new Set(images.map((image) => image.name)).size === images.length)
34
- }).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`));
43
+ })).min(1).max(2).refine((images) => new Set(images.map((image) => image.name)).size === images.length)
44
+ }).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`)).refine((artifact) => artifact.partial || artifact.images.some((image) => image.name === "app")).refine((artifact) => artifact.partial || !artifact.hasWeb || artifact.images.some((image) => image.name === "web"));
35
45
  async function artifactChecksum(file) {
36
46
  const hash = createHash("sha256");
37
47
  for await (const chunk of createReadStream(file))
@@ -99,11 +109,125 @@ var en_US_default = {
99
109
  deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
100
110
  deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
101
111
  deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
102
- deploy_artifactInvalid: "Build artifacts are missing, corrupt, or could not be packaged. Run pnpm build --target docker at the project root and point --artifact-dir to a complete build directory.",
112
+ deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
103
113
  deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
104
- deploy_bundleInstructions: "This bundle contains private configuration. Transfer it securely and restrict access.\n1. Copy {file} to Linux using removable media, a private network or scp.\n2. Install Docker/Podman with compose, tar, sha256sum and sh. Load middleware images using docker load/podman load with the exact tags below.\n3. Create a private release directory: mkdir -p release && chmod 700 release\n4. From the bundle directory run tar -xf {file} -C release\n5. Run cd release && sh {start}. Set HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=docker or podman to select the runtime explicitly.\n6. Check docker compose -f {compose} --env-file .env.runtime ps (or logs; use podman instead of docker for Podman), Web and /health after startup. Bundle generation does not perform server health checks.\nRequired images (app/web are included; prepare middleware separately):\n{images}\nBuilt images can be reused across environments; this bundle contains configuration for the selected environment only.",
114
+ deploy_bundleInstructions: "Bundle: {file}\nIncludes all images needed by selected services, including self-hosted middleware.\nWith SSH access, first edit deploy-target.json in the adjacent deployment directory using its _comments, then run deploy.ps1 (Windows) or deploy.sh (Linux/macOS) there. Both read this JSON; the sh entry also requires local awk.\nFor isolated servers, copy and extract the tar into a separate directory, then run sh apply.sh (uses the configured target directory).\nLinux requires Docker/Podman, Compose, sh, tar, sha256sum, awk, curl, util-linux flock and filesystem/container permissions on a filesystem supporting flock.\nNever delete or replace .deploy.lock; its lock releases after every holder exits or the server restarts. Inspect execution and recovery state after forced termination.\nPartial updates require a successful full installation with matching ports/layout. Steps return nonzero on failure; result-DEPLOYMENT_ID.json records the outcome.\nThe bundle contains private runtime configuration; transfer it securely.\nIncluded images:\n{images}",
105
115
  deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
106
- deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh."
116
+ deploy_targetHost: "Required: Linux server address or SSH alias. Replace SERVER_HOST before running either upload script.",
117
+ deploy_targetPort: "SSH port, integer 1-65535; default 22.",
118
+ deploy_targetUsername: "SSH login username; default deploy. Requires write access to directory and container runtime permissions.",
119
+ deploy_targetIdentityFile: "Optional private key path on the computer running the upload script. Prefer an absolute path; empty uses ssh-agent/default keys. HAI_DEPLOY_IDENTITY_FILE overrides it.",
120
+ deploy_targetKnownHostsFile: "Optional known_hosts path on the computer running the upload script. Prefer an absolute path; empty uses OpenSSH defaults. HAI_DEPLOY_KNOWN_HOSTS_FILE overrides it.",
121
+ deploy_targetStrictHostKeyChecking: "Boolean, default true: trust new host keys and reject changed keys. false disables host key checking.",
122
+ deploy_targetConnectTimeoutSeconds: "SSH connection timeout in seconds, integer 1-2147483647; default 10.",
123
+ deploy_targetDirectory: "Required absolute application directory on the Linux server; contains incoming packages, releases and successful current state. Parent-directory traversal is forbidden.",
124
+ deploy_targetBundle: "Required tar filename in the parent of this deployment directory. Keep the tar and this entire directory together when moving them.",
125
+ deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh.",
126
+ deploy_selectedContainerOnly: "Selected-service deployment requires a docker-ssh Compose destination.",
127
+ deploy_selectionInvalid: "Cannot deploy {service}: check the service name and build its image first.",
128
+ deploy_imageVersionRequired: "Specify an image version; latest is not allowed: {image}",
129
+ deploy_bundleRuntimeRequired: "Start local Podman/Docker to export middleware images, or prepare this complete offline bundle on the build machine.",
130
+ deploy_bundleFailed: "Package preparation or deployment failed: {reason}",
131
+ deploy_configFileInvalid: "Invalid YAML in {path}, line {line}. Check indentation, colons and quotes.",
132
+ deploy_configFileUnreadable: "Cannot read configuration file {path}. Check its path and permissions.",
133
+ deploy_bundleNameInvalid: "Bundle filenames may contain letters, numbers, dots, plus signs, hyphens and underscores; parent directories may contain spaces.",
134
+ deploy_secretsInvalid: "Middleware credentials are damaged or incomplete: {path}. Restore a backup; passwords are not regenerated because existing data may become inaccessible.",
135
+ deploy_storageArtifactRequired: "Storage initialization requires the application image. Select a complete artifact containing app with --artifact-dir, or run pnpm build --target docker --images app first.",
136
+ deploy_localStopped: "The local command process tree has been confirmed exited.",
137
+ deploy_localUnknown: "Local process tree termination is unconfirmed; inspect local execution state.",
138
+ deploy_localNotStarted: "The local command was not started.",
139
+ deploy_commandOutputFailed: "Command output or callback failed",
140
+ deploy_commandInputFailed: "Command input failed",
141
+ deploy_commandTreeActive: "Child processes remain active after the main command exited",
142
+ deploy_commandExecutionFailed: "Command execution failed",
143
+ deploy_remoteUnknown: "Remote execution is unknown. Local SSH termination does not prove remote deployment stopped; inspect the server result and actual containers.",
144
+ deploy_remoteFinished: "A matching server result confirms the deployment script and recovery finished: stage {stage}, exit {code}. Verify actual service health."
145
+ };
146
+
147
+ // messages/ja-JP.json
148
+ var ja_JP_default = {
149
+ deploy_notInitialized: "\u30C7\u30D7\u30ED\u30A4\u30E2\u30B8\u30E5\u30FC\u30EB\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306Bdeploy.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
150
+ deploy_initFailed: "\u30C7\u30D7\u30ED\u30A4\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
151
+ deploy_unsupportedProviderType: "\u672A\u5BFE\u5FDC\u306E\u30C7\u30D7\u30ED\u30A4\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306E\u7A2E\u985E\u3067\u3059: {type}",
152
+ deploy_unsupportedProvisionerType: "\u672A\u5BFE\u5FDC\u306E\u30B5\u30FC\u30D3\u30B9\u30D7\u30ED\u30D3\u30B8\u30E7\u30CA\u30FC\u306E\u7A2E\u985E\u3067\u3059: {type}",
153
+ deploy_configTokenRequired: "\u30C7\u30D7\u30ED\u30A4\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306E\u30C8\u30FC\u30AF\u30F3\u306F\u5FC5\u9808\u3067\u3059",
154
+ deploy_authRequired: "\u8A8D\u8A3C\u304C\u5FC5\u8981\u3067\u3059",
155
+ deploy_authFailed: "\u8A8D\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
156
+ deploy_projectCreateFailed: "\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306E\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
157
+ deploy_uploadFailed: "\u30D3\u30EB\u30C9\u6210\u679C\u7269\u306E\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
158
+ deploy_deployFailed: "\u30C7\u30D7\u30ED\u30A4\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
159
+ deploy_provisionFailed: "\u30B5\u30FC\u30D3\u30B9\u306E\u6E96\u5099\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {service} - {error}",
160
+ deploy_adapterMissing: "\u5BFE\u8C61\u30D7\u30E9\u30C3\u30C8\u30D5\u30A9\u30FC\u30E0\u306E\u30A2\u30C0\u30D7\u30BF\u30FC\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002{adapter}\u3092\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u3057\u3066\u304F\u3060\u3055\u3044",
161
+ deploy_scanFailed: "\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u306E\u30B9\u30AD\u30E3\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
162
+ deploy_envVarFailed: "\u74B0\u5883\u5909\u6570\u306E\u8A2D\u5B9A\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
163
+ deploy_credentialError: "\u8A8D\u8A3C\u60C5\u5831\u306E\u8AAD\u307F\u66F8\u304D\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
164
+ deploy_credentialInvalidKey: "\u672A\u5BFE\u5FDC\u306E\u8A8D\u8A3C\u60C5\u5831\u30AD\u30FC\u3067\u3059: {key}\u3002HAI_DEPLOY_*\u30AD\u30FC\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059",
165
+ deploy_credentialSaved: "\u8A8D\u8A3C\u60C5\u5831\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F",
166
+ deploy_credentialLoaded: "\u8A8D\u8A3C\u60C5\u5831\u3092\u8AAD\u307F\u8FBC\u307F\u307E\u3057\u305F",
167
+ deploy_configError: "\u30C7\u30D7\u30ED\u30A4\u8A2D\u5B9A\u30A8\u30E9\u30FC: {error}",
168
+ deploy_credentialMissing: "\u8A8D\u8A3C\u60C5\u5831\u306E\u9805\u76EE\u304C\u4E0D\u8DB3\u3057\u3066\u3044\u307E\u3059: {fields}",
169
+ deploy_apiError: "{service} API\u30A8\u30E9\u30FC\uFF08{status}\uFF09",
170
+ deploy_fileUploadFailed: "\u30D5\u30A1\u30A4\u30EB\u306E\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF08{status}\uFF09",
171
+ deploy_provisionNoResult: "{service}\u304B\u3089\u63A5\u7D9A\u60C5\u5831\u304C\u8FD4\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F",
172
+ deploy_recoveryFailure: "{stage}\u3067\u30C7\u30D7\u30ED\u30A4\u3092\u505C\u6B62\u3057\u307E\u3057\u305F\u3002\u5FA9\u65E7\u60C5\u5831\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
173
+ deploy_recoverySuggestion: "\u540C\u3058\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u540D\u3067\u518D\u8A66\u884C\u3059\u308B\u524D\u306B\u3001\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u306E\u30B3\u30F3\u30BD\u30FC\u30EB\u3067\u8A18\u8F09\u3055\u308C\u305F\u30EA\u30BD\u30FC\u30B9\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u300C\u4E0D\u660E\u300D\u306F\u3001\u30EA\u30AF\u30A8\u30B9\u30C8\u306B\u3088\u3063\u3066\u30EA\u30BD\u30FC\u30B9\u304C\u4F5C\u6210\u3055\u308C\u305F\u53EF\u80FD\u6027\u3092\u793A\u3057\u307E\u3059\u3002\u518D\u5229\u7528\u3057\u305F\u30EA\u30BD\u30FC\u30B9\u306F\u81EA\u52D5\u524A\u9664\u3055\u308C\u307E\u305B\u3093\u3002\u65B0\u898F\u30EA\u30BD\u30FC\u30B9\u306F\u4F7F\u7528\u72B6\u6CC1\u3092\u78BA\u8A8D\u3057\u3066\u304B\u3089\u624B\u52D5\u3067\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
174
+ deploy_commandTimeout: "\u30B3\u30DE\u30F3\u30C9\u304C\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u3057\u307E\u3057\u305F: {command}",
175
+ deploy_commandFailed: "\u30B3\u30DE\u30F3\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF08{command}\uFF09: {error}",
176
+ deploy_remoteConnectFailed: "\u30EA\u30E2\u30FC\u30C8\u30DB\u30B9\u30C8{host}\u306B\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093: {error}",
177
+ deploy_remoteRuntimeMissing: "\u30EA\u30E2\u30FC\u30C8\u30DB\u30B9\u30C8\u306B\u30B3\u30F3\u30C6\u30CA\u30FC\u30E9\u30F3\u30BF\u30A4\u30E0\u307E\u305F\u306Fcompose\u304C\u3042\u308A\u307E\u305B\u3093: {runtime}",
178
+ deploy_imageTransferFailed: "\u30A4\u30E1\u30FC\u30B8\u306E\u8EE2\u9001\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
179
+ deploy_healthCheckFailed: "\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AF\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002{url}\u3067\u30A2\u30D7\u30EA\u306E\u6E96\u5099\u304C\u5B8C\u4E86\u3057\u3066\u3044\u307E\u305B\u3093",
180
+ deploy_invalidEnvironment: "\u74B0\u5883\u540D\u306B\u306F\u82F1\u6570\u5B57\u3001\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3001\u30CF\u30A4\u30D5\u30F3\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002local/example\u306F\u4E88\u7D04\u3055\u308C\u3066\u3044\u307E\u3059\u3002",
181
+ deploy_environmentMissing: "\u74B0\u5883\u30D5\u30A1\u30A4\u30EB\u304C\u3042\u308A\u307E\u305B\u3093: {path}\u3002\u30C7\u30D7\u30ED\u30A4\u524D\u306B\u4F5C\u6210\u3057\u3066\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
182
+ deploy_environmentReadFailed: "\u30C7\u30D7\u30ED\u30A4\u7528\u306E\u74B0\u5883\u30D5\u30A1\u30A4\u30EB\u3092\u8AAD\u307F\u53D6\u308C\u307E\u305B\u3093\u3002\u69CB\u6587\u3068\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
183
+ deploy_environmentWriteFailed: "\u30C7\u30D7\u30ED\u30A4\u8A2D\u5B9A\u3092\u4FDD\u5B58\u3067\u304D\u307E\u305B\u3093\u3002\u30D5\u30A1\u30A4\u30EB\u306E\u69CB\u6587\u3068\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
184
+ deploy_multipleApplications: "\u30B5\u30FC\u30D3\u30B9\u30D1\u30C3\u30B1\u30FC\u30B8\u304C\u8907\u6570\u898B\u3064\u304B\u308A\u307E\u3057\u305F\u3002\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
185
+ deploy_purposeSqlite: "\u30DC\u30EA\u30E5\u30FC\u30E0\u306B\u6C38\u7D9A\u5316\u3059\u308B\u30ED\u30FC\u30AB\u30EB\u30EA\u30EC\u30FC\u30B7\u30E7\u30CA\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9",
186
+ deploy_purposeDatabase: "\u696D\u52D9\u30C7\u30FC\u30BF\u7528\u306E\u30EA\u30EC\u30FC\u30B7\u30E7\u30CA\u30EB\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u30B5\u30FC\u30D3\u30B9",
187
+ deploy_purposeMemory: "\u30D7\u30ED\u30BB\u30B9\u5358\u4F4D\u306E\u30E1\u30E2\u30EA\u30FC\u30AD\u30E3\u30C3\u30B7\u30E5\u3002\u518D\u8D77\u52D5\u6642\u306B\u6D88\u53BB\u3055\u308C\u3001\u5916\u90E8\u30B5\u30FC\u30D3\u30B9\u306F\u4F7F\u7528\u3057\u307E\u305B\u3093",
188
+ deploy_purposeLocal: "\u30DC\u30EA\u30E5\u30FC\u30E0\u306B\u6C38\u7D9A\u5316\u3059\u308B\u30ED\u30FC\u30AB\u30EB\u30D5\u30A1\u30A4\u30EB\u30B9\u30C8\u30EC\u30FC\u30B8",
189
+ deploy_purposeCache: "\u5171\u6709\u30AD\u30E3\u30C3\u30B7\u30E5\u3001\u30BB\u30C3\u30B7\u30E7\u30F3\u3001\u5206\u6563\u30ED\u30C3\u30AF",
190
+ deploy_purposeStorage: "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u30D5\u30A1\u30A4\u30EB\u7528\u306ES3\u4E92\u63DB\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u30B9\u30C8\u30EC\u30FC\u30B8",
191
+ deploy_purposeVector: "\u30D9\u30AF\u30C8\u30EB\u306E\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u4F5C\u6210\u3068\u985E\u4F3C\u691C\u7D22",
192
+ deploy_purposeDisabled: "LDAP\u30ED\u30B0\u30A4\u30F3\u306F\u7121\u52B9\u3067\u3059\u3002\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30B5\u30FC\u30D3\u30B9\u306F\u4F5C\u6210\u3057\u307E\u305B\u3093",
193
+ deploy_purposeLdap: "\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30A2\u30AB\u30A6\u30F3\u30C8\u8A8D\u8A3C\u3002\u65E2\u5B9A\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306B\u306F\u696D\u52D9\u30E6\u30FC\u30B6\u30FC\u306F\u3044\u307E\u305B\u3093",
194
+ deploy_purposeConsole: "\u914D\u4FE1\u305B\u305A\u3001\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u30ED\u30B0\u306B\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u51FA\u529B\u3057\u307E\u3059",
195
+ deploy_purposeEmail: "\u30E1\u30FC\u30EB/HTTP\u914D\u4FE1\u3002\u65E2\u5B9A\u306ESMTP\u30B3\u30F3\u30C6\u30CA\u30FC\u306F\u30C6\u30B9\u30C8\u30E1\u30C3\u30BB\u30FC\u30B8\u306E\u307F\u53D7\u4FE1\u3057\u307E\u3059",
196
+ deploy_purposeSms: "Aliyun\u306E\u8A8D\u8A3C\u60C5\u5831\u306B\u3088\u308BSMS\u3002\u65E2\u5B58\u306E\u7F72\u540D\u3068\u5229\u7528\u67A0\u304C\u5FC5\u8981\u3067\u3059",
197
+ deploy_webProxyInvalid: "API\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9\u306F\u3001\u82F1\u6570\u5B57\u3001\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3001\u30CF\u30A4\u30D5\u30F3\u3001\u30B9\u30E9\u30C3\u30B7\u30E5\u306E\u307F\u3092\u542B\u3080\u7D76\u5BFE\u30D1\u30B9\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
198
+ deploy_artifactInvalid: "\u30A4\u30E1\u30FC\u30B8\u6210\u679C\u7269\u304C\u5B58\u5728\u3057\u306A\u3044\u304B\u3001\u7121\u52B9\u3067\u3059\u3002pnpm build --target docker \u3092\u5B9F\u884C\u3059\u308B\u304B\u3001--artifact-dir \u3067\u6709\u52B9\u306A\u6210\u679C\u7269\u3092\u9078\u629E\u3059\u308B\u304B\u3001pnpm run deploy --build \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
199
+ deploy_sshRequired: "\u81EA\u52D5\u30C7\u30D7\u30ED\u30A4\u306B\u306FSSH\u8A2D\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002\u30B5\u30FC\u30D0\u30FC\u306B\u63A5\u7D9A\u3067\u304D\u306A\u3044\u5834\u5408\u306F--bundle-only\u3067\u624B\u52D5\u8EE2\u9001\u7528\u306E\u30D0\u30F3\u30C9\u30EB\u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
200
+ deploy_bundleInstructions: "\u30D0\u30F3\u30C9\u30EB\uFF1A{file}\n\u9078\u629E\u3057\u305F\u30B5\u30FC\u30D3\u30B9\u306B\u5FC5\u8981\u306A\u3059\u3079\u3066\u306E\u30A4\u30E1\u30FC\u30B8\u3092\u542B\u307F\u307E\u3059\uFF08\u30BB\u30EB\u30D5\u30DB\u30B9\u30C8\u306E\u30DF\u30C9\u30EB\u30A6\u30A7\u30A2\u3092\u542B\u3080\uFF09\u3002\nSSH \u3092\u4F7F\u7528\u3059\u308B\u5834\u5408\u3001\u96A3\u63A5\u3059\u308B\u30C7\u30D7\u30ED\u30A4\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E deploy-target.json \u3092 _comments \u306B\u5F93\u3063\u3066\u7DE8\u96C6\u3057\u3001\u540C\u3058\u5834\u6240\u3067 deploy.ps1\uFF08Windows\uFF09\u307E\u305F\u306F deploy.sh\uFF08Linux/macOS\uFF09\u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4E21\u65B9\u3068\u3082\u3053\u306E JSON \u3092\u8AAD\u307F\u8FBC\u307F\u307E\u3059\u3002sh \u306E\u5B9F\u884C\u306B\u306F\u30ED\u30FC\u30AB\u30EB\u306E awk \u3082\u5FC5\u8981\u3067\u3059\u3002\n\u9694\u96E2\u3055\u308C\u305F\u30B5\u30FC\u30D0\u30FC\u3067\u306F\u3001tar \u3092\u5225\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3078\u30B3\u30D4\u30FC\u3057\u3066\u5C55\u958B\u3057\u3001sh apply.sh \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\uFF08\u8A2D\u5B9A\u3055\u308C\u305F\u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3092\u4F7F\u7528\u3057\u307E\u3059\uFF09\u3002\nLinux \u306B\u306F Docker/Podman\u3001Compose\u3001sh\u3001tar\u3001sha256sum\u3001awk\u3001curl\u3001util-linux \u306E flock \u304C\u5FC5\u8981\u3067\u3059\u3002flock \u306B\u5BFE\u5FDC\u3057\u305F\u30D5\u30A1\u30A4\u30EB\u30B7\u30B9\u30C6\u30E0\u4E0A\u3067\u3001\u30D5\u30A1\u30A4\u30EB\u30B7\u30B9\u30C6\u30E0\u3068\u30B3\u30F3\u30C6\u30CA\u30FC\u306E\u64CD\u4F5C\u6A29\u9650\u3082\u5FC5\u8981\u3067\u3059\u3002\n.deploy.lock \u306F\u524A\u9664\u3082\u7F6E\u63DB\u3082\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002\u3059\u3079\u3066\u306E\u4FDD\u6301\u30D7\u30ED\u30BB\u30B9\u304C\u7D42\u4E86\u3059\u308B\u304B\u3001\u30B5\u30FC\u30D0\u30FC\u304C\u518D\u8D77\u52D5\u3059\u308B\u3068\u30ED\u30C3\u30AF\u304C\u89E3\u653E\u3055\u308C\u307E\u3059\u3002\u5F37\u5236\u7D42\u4E86\u5F8C\u306F\u5B9F\u884C\u72B6\u614B\u3068\u5FA9\u65E7\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\n\u90E8\u5206\u66F4\u65B0\u306B\u306F\u30DD\u30FC\u30C8\u3068\u69CB\u6210\u304C\u4E00\u81F4\u3059\u308B\u6B63\u5E38\u306A\u5B8C\u5168\u30A4\u30F3\u30B9\u30C8\u30FC\u30EB\u304C\u5FC5\u8981\u3067\u3059\u3002\u5404\u624B\u9806\u306F\u5931\u6557\u6642\u306B\u975E\u30BC\u30ED\u3067\u7D42\u4E86\u3057\u3001result-DEPLOYMENT_ID.json \u306B\u7D50\u679C\u3092\u8A18\u9332\u3057\u307E\u3059\u3002\n\u30D0\u30F3\u30C9\u30EB\u306B\u306F\u975E\u516C\u958B\u306E\u5B9F\u884C\u6642\u8A2D\u5B9A\u304C\u542B\u307E\u308C\u307E\u3059\u3002\u5B89\u5168\u306B\u8EE2\u9001\u3057\u3066\u304F\u3060\u3055\u3044\u3002\n\u540C\u68B1\u30A4\u30E1\u30FC\u30B8\uFF1A\n{images}",
201
+ deploy_bundleMetadata: "\u30A2\u30D7\u30EA\u306E\u30D0\u30FC\u30B8\u30E7\u30F3: {version}\n\u30D3\u30EB\u30C9ID: {buildId}\nCompose\u30D5\u30A1\u30A4\u30EB: {compose}",
202
+ deploy_targetHost: "\u5FC5\u9808\uFF1ALinux \u30B5\u30FC\u30D0\u30FC\u306E\u30A2\u30C9\u30EC\u30B9\u307E\u305F\u306F SSH \u30A8\u30A4\u30EA\u30A2\u30B9\u3002\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u30B9\u30AF\u30EA\u30D7\u30C8\u3092\u5B9F\u884C\u3059\u308B\u524D\u306B SERVER_HOST \u3092\u7F6E\u304D\u63DB\u3048\u3066\u304F\u3060\u3055\u3044\u3002",
203
+ deploy_targetPort: "SSH \u30DD\u30FC\u30C8\u30021\u301C65535\u306E\u6574\u6570\u3002\u65E2\u5B9A\u5024\u306F22\u3067\u3059\u3002",
204
+ deploy_targetUsername: "SSH \u30ED\u30B0\u30A4\u30F3\u30E6\u30FC\u30B6\u30FC\u540D\u3002\u65E2\u5B9A\u5024\u306F deploy \u3067\u3059\u3002\u5BFE\u8C61\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3078\u306E\u66F8\u304D\u8FBC\u307F\u6A29\u9650\u3068\u30B3\u30F3\u30C6\u30CA\u30FC\u30E9\u30F3\u30BF\u30A4\u30E0\u306E\u64CD\u4F5C\u6A29\u9650\u304C\u5FC5\u8981\u3067\u3059\u3002",
205
+ deploy_targetIdentityFile: "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u30B9\u30AF\u30EA\u30D7\u30C8\u3092\u5B9F\u884C\u3059\u308B\u7AEF\u672B\u4E0A\u306E\u79D8\u5BC6\u9375\u30D1\u30B9\uFF08\u7701\u7565\u53EF\uFF09\u3002\u7D76\u5BFE\u30D1\u30B9\u3092\u63A8\u5968\u3057\u307E\u3059\u3002\u7A7A\u6B04\u3067\u306F ssh-agent \u307E\u305F\u306F\u65E2\u5B9A\u306E\u9375\u3092\u4F7F\u7528\u3057\u307E\u3059\u3002HAI_DEPLOY_IDENTITY_FILE \u3067\u4E0A\u66F8\u304D\u3067\u304D\u307E\u3059\u3002",
206
+ deploy_targetKnownHostsFile: "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u30B9\u30AF\u30EA\u30D7\u30C8\u3092\u5B9F\u884C\u3059\u308B\u7AEF\u672B\u4E0A\u306E known_hosts \u30D1\u30B9\uFF08\u7701\u7565\u53EF\uFF09\u3002\u7D76\u5BFE\u30D1\u30B9\u3092\u63A8\u5968\u3057\u307E\u3059\u3002\u7A7A\u6B04\u3067\u306F OpenSSH \u306E\u65E2\u5B9A\u5024\u3092\u4F7F\u7528\u3057\u307E\u3059\u3002HAI_DEPLOY_KNOWN_HOSTS_FILE \u3067\u4E0A\u66F8\u304D\u3067\u304D\u307E\u3059\u3002",
207
+ deploy_targetStrictHostKeyChecking: "\u771F\u507D\u5024\u3002\u65E2\u5B9A\u5024\u306E true \u306F\u65B0\u3057\u3044\u30DB\u30B9\u30C8\u9375\u3092\u4FE1\u983C\u3057\u3001\u5909\u66F4\u3055\u308C\u305F\u9375\u3092\u62D2\u5426\u3057\u307E\u3059\u3002false \u306F\u30DB\u30B9\u30C8\u9375\u306E\u691C\u8A3C\u3092\u7121\u52B9\u306B\u3057\u307E\u3059\u3002",
208
+ deploy_targetConnectTimeoutSeconds: "SSH \u63A5\u7D9A\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\uFF08\u79D2\uFF09\u30021\u301C2147483647\u306E\u6574\u6570\u3002\u65E2\u5B9A\u5024\u306F10\u3067\u3059\u3002",
209
+ deploy_targetDirectory: "\u5FC5\u9808\uFF1ALinux \u30B5\u30FC\u30D0\u30FC\u4E0A\u306E\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u306E\u7D76\u5BFE\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3002\u53D7\u4FE1\u30D1\u30C3\u30B1\u30FC\u30B8\u3001\u30EA\u30EA\u30FC\u30B9\u3001\u6210\u529F\u6642\u306E current \u72B6\u614B\u3092\u4FDD\u5B58\u3057\u307E\u3059\u3002\u89AA\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u3078\u306E\u79FB\u52D5\u306F\u8A31\u53EF\u3055\u308C\u307E\u305B\u3093\u3002",
210
+ deploy_targetBundle: "\u5FC5\u9808\uFF1A\u3053\u306E\u30C7\u30D7\u30ED\u30A4\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306E\u89AA\u306B\u3042\u308B tar \u30D5\u30A1\u30A4\u30EB\u540D\u3002\u79FB\u52D5\u6642\u306F tar \u3068\u3053\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u5168\u4F53\u3092\u4E00\u7DD2\u306B\u79FB\u52D5\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
211
+ deploy_bundleOnlyContainer: "Compose\u30D0\u30F3\u30C9\u30EB\u306E\u751F\u6210\u306Fdocker-ssh\u306E\u307F\u3067\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u307E\u3059\u3002",
212
+ deploy_selectedContainerOnly: "\u30B5\u30FC\u30D3\u30B9\u3092\u9078\u629E\u3057\u305F\u30C7\u30D7\u30ED\u30A4\u306B\u306F docker-ssh Compose \u306E\u30C7\u30D7\u30ED\u30A4\u5148\u304C\u5FC5\u8981\u3067\u3059\u3002",
213
+ deploy_selectionInvalid: "{service} \u3092\u30C7\u30D7\u30ED\u30A4\u3067\u304D\u307E\u305B\u3093\u3002\u30B5\u30FC\u30D3\u30B9\u540D\u3092\u78BA\u8A8D\u3057\u3001\u5148\u306B\u30A4\u30E1\u30FC\u30B8\u3092\u30D3\u30EB\u30C9\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
214
+ deploy_imageVersionRequired: "\u30A4\u30E1\u30FC\u30B8\u306E\u30D0\u30FC\u30B8\u30E7\u30F3\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002latest \u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093\uFF1A{image}",
215
+ deploy_bundleRuntimeRequired: "\u30DF\u30C9\u30EB\u30A6\u30A7\u30A2\u30A4\u30E1\u30FC\u30B8\u3092\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8\u3059\u308B\u306B\u306F\u30ED\u30FC\u30AB\u30EB\u306E Podman/Docker \u3092\u8D77\u52D5\u3059\u308B\u304B\u3001\u30D3\u30EB\u30C9\u7AEF\u672B\u3067\u5B8C\u5168\u306A\u30AA\u30D5\u30E9\u30A4\u30F3\u30D0\u30F3\u30C9\u30EB\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
216
+ deploy_bundleFailed: "\u30D1\u30C3\u30B1\u30FC\u30B8\u306E\u6E96\u5099\u307E\u305F\u306F\u30C7\u30D7\u30ED\u30A4\u306B\u5931\u6557\u3057\u307E\u3057\u305F\uFF1A{reason}",
217
+ deploy_configFileInvalid: "{path} \u306E {line} \u884C\u76EE\u306E YAML \u304C\u7121\u52B9\u3067\u3059\u3002\u30A4\u30F3\u30C7\u30F3\u30C8\u3001\u30B3\u30ED\u30F3\u3001\u5F15\u7528\u7B26\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
218
+ deploy_configFileUnreadable: "\u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB {path} \u3092\u8AAD\u307F\u8FBC\u3081\u307E\u305B\u3093\u3002\u30D1\u30B9\u3068\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
219
+ deploy_bundleNameInvalid: "\u30D0\u30F3\u30C9\u30EB\u306E\u30D5\u30A1\u30A4\u30EB\u540D\u306B\u306F\u82F1\u6570\u5B57\u3001\u30C9\u30C3\u30C8\u3001\u30D7\u30E9\u30B9\u3001\u30CF\u30A4\u30D5\u30F3\u3001\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3092\u4F7F\u7528\u3067\u304D\u307E\u3059\u3002\u89AA\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u306B\u306F\u7A7A\u767D\u3092\u4F7F\u7528\u3067\u304D\u307E\u3059\u3002",
220
+ deploy_secretsInvalid: "\u30DF\u30C9\u30EB\u30A6\u30A7\u30A2\u306E\u8A8D\u8A3C\u60C5\u5831\u304C\u7834\u640D\u3057\u3066\u3044\u308B\u304B\u3001\u4E0D\u5B8C\u5168\u3067\u3059\uFF1A{path}\u3002\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u3092\u5FA9\u5143\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u65E2\u5B58\u30C7\u30FC\u30BF\u306B\u30A2\u30AF\u30BB\u30B9\u3067\u304D\u306A\u304F\u306A\u308B\u53EF\u80FD\u6027\u304C\u3042\u308B\u305F\u3081\u3001\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u518D\u751F\u6210\u3057\u307E\u305B\u3093\u3002",
221
+ deploy_storageArtifactRequired: "\u30B9\u30C8\u30EC\u30FC\u30B8\u306E\u521D\u671F\u5316\u306B\u306F\u30A2\u30D7\u30EA\u30B1\u30FC\u30B7\u30E7\u30F3\u30A4\u30E1\u30FC\u30B8\u304C\u5FC5\u8981\u3067\u3059\u3002--artifact-dir \u3067 app \u3092\u542B\u3080\u5B8C\u5168\u306A\u6210\u679C\u7269\u3092\u9078\u629E\u3059\u308B\u304B\u3001\u5148\u306B pnpm build --target docker --images app \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
222
+ deploy_localStopped: "\u30ED\u30FC\u30AB\u30EB\u30B3\u30DE\u30F3\u30C9\u306E\u30D7\u30ED\u30BB\u30B9\u30C4\u30EA\u30FC\u306E\u7D42\u4E86\u3092\u78BA\u8A8D\u3057\u307E\u3057\u305F\u3002",
223
+ deploy_localUnknown: "\u30ED\u30FC\u30AB\u30EB\u30D7\u30ED\u30BB\u30B9\u30C4\u30EA\u30FC\u306E\u7D42\u4E86\u306F\u672A\u78BA\u8A8D\u3067\u3059\u3002\u30ED\u30FC\u30AB\u30EB\u306E\u5B9F\u884C\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
224
+ deploy_localNotStarted: "\u30ED\u30FC\u30AB\u30EB\u30B3\u30DE\u30F3\u30C9\u306F\u8D77\u52D5\u3057\u3066\u3044\u307E\u305B\u3093\u3002",
225
+ deploy_commandOutputFailed: "\u30B3\u30DE\u30F3\u30C9\u51FA\u529B\u307E\u305F\u306F\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
226
+ deploy_commandInputFailed: "\u30B3\u30DE\u30F3\u30C9\u5165\u529B\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
227
+ deploy_commandTreeActive: "\u30E1\u30A4\u30F3\u30B3\u30DE\u30F3\u30C9\u7D42\u4E86\u5F8C\u3082\u5B50\u30D7\u30ED\u30BB\u30B9\u304C\u52D5\u4F5C\u3057\u3066\u3044\u307E\u3059",
228
+ deploy_commandExecutionFailed: "\u30B3\u30DE\u30F3\u30C9\u306E\u5B9F\u884C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
229
+ deploy_remoteUnknown: "\u30EA\u30E2\u30FC\u30C8\u306E\u5B9F\u884C\u72B6\u614B\u306F\u4E0D\u660E\u3067\u3059\u3002\u30ED\u30FC\u30AB\u30EB SSH \u306E\u7D42\u4E86\u306F\u30EA\u30E2\u30FC\u30C8\u30C7\u30D7\u30ED\u30A4\u306E\u505C\u6B62\u3092\u610F\u5473\u3057\u307E\u305B\u3093\u3002\u30B5\u30FC\u30D0\u30FC\u306E\u7D50\u679C\u3068\u5B9F\u969B\u306E\u30B3\u30F3\u30C6\u30CA\u30FC\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
230
+ deploy_remoteFinished: "\u5BFE\u5FDC\u3059\u308B\u30B5\u30FC\u30D0\u30FC\u306E\u7D50\u679C\u306B\u3088\u308A\u3001\u30C7\u30D7\u30ED\u30A4\u30B9\u30AF\u30EA\u30D7\u30C8\u3068\u5FA9\u65E7\u51E6\u7406\u306E\u7D42\u4E86\u3092\u78BA\u8A8D\u3057\u307E\u3057\u305F\uFF1A\u6BB5\u968E {stage}\u3001\u7D42\u4E86\u30B3\u30FC\u30C9 {code}\u3002\u5B9F\u969B\u306E\u30B5\u30FC\u30D3\u30B9\u306E\u7A3C\u50CD\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
107
231
  };
108
232
 
109
233
  // messages/zh-CN.json
@@ -157,17 +281,46 @@ var zh_CN_default = {
157
281
  deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
158
282
  deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6",
159
283
  deploy_webProxyInvalid: "API \u524D\u7F00\u5FC5\u987B\u662F\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26\u548C\u659C\u6760\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
160
- deploy_artifactInvalid: "\u6784\u5EFA\u4EA7\u7269\u7F3A\u5931\u3001\u635F\u574F\u6216\u90E8\u7F72\u6253\u5305\u5931\u8D25\u3002\u8BF7\u5148\u5728\u5DE5\u7A0B\u6839\u8FD0\u884C pnpm build --target docker\uFF0C\u5E76\u68C0\u67E5 --artifact-dir \u6307\u5411\u5B8C\u6574\u7684\u6784\u5EFA\u76EE\u5F55\u3002",
284
+ deploy_artifactInvalid: "\u955C\u50CF\u4EA7\u7269\u7F3A\u5931\u6216\u6821\u9A8C\u5931\u8D25\u3002\u8BF7\u5148\u6267\u884C pnpm build --target docker\uFF0C\u6216\u7528 --artifact-dir \u6307\u5B9A\u6709\u6548\u4EA7\u7269\uFF1B\u4E5F\u53EF\u7528 pnpm run deploy --build \u4E00\u6B21\u5B8C\u6210\u3002",
161
285
  deploy_sshRequired: "\u81EA\u52A8\u90E8\u7F72\u9700\u8981 SSH \u914D\u7F6E\uFF1B\u65E0\u7F51\u7EDC\u8FDE\u63A5\u65F6\u4F7F\u7528 --bundle-only \u751F\u6210\u90E8\u7F72\u5305\u5E76\u624B\u5DE5\u642C\u8FD0\u3002",
162
- deploy_bundleInstructions: "\u6B64\u5305\u5305\u542B\u79C1\u6709\u914D\u7F6E\uFF0C\u8BF7\u5B89\u5168\u642C\u8FD0\u5E76\u9650\u5236\u8BBF\u95EE\u6743\u9650\u3002\n1. \u5C06 {file} \u590D\u5236\u5230 Linux \u670D\u52A1\u5668\uFF08U \u76D8\u3001\u5185\u7F51\u4F20\u8F93\u6216 scp \u5747\u53EF\uFF09\u3002\n2. \u51C6\u5907 Docker/Podman\u3001\u5BF9\u5E94 compose \u547D\u4EE4\u3001tar\u3001sha256sum \u548C sh\uFF1B\u5C06\u6240\u9700\u4E2D\u95F4\u4EF6\u955C\u50CF\u7528 docker load/podman load \u5BFC\u5165\uFF0C\u955C\u50CF\u6807\u7B7E\u987B\u4E0E\u4E0B\u5217\u6E05\u5355\u5B8C\u5168\u4E00\u81F4\u3002\n3. \u65B0\u5EFA\u4EC5\u90E8\u7F72\u7528\u6237\u53EF\u8BBF\u95EE\u7684\u76EE\u5F55\uFF1Amkdir -p release && chmod 700 release\n4. \u5728\u5305\u6240\u5728\u76EE\u5F55\u6267\u884C tar -xf {file} -C release\n5. \u6267\u884C cd release && sh {start}\uFF1B\u53EF\u7528 HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=docker \u6216 podman \u663E\u5F0F\u9009\u62E9\u8FD0\u884C\u65F6\u3002\n6. \u542F\u52A8\u540E\u7528 docker compose -f {compose} --env-file .env.runtime ps\uFF08\u6216 logs\uFF1BPodman \u5C06 docker \u6362\u4E3A podman\uFF09\u68C0\u67E5\u72B6\u6001\u5E76\u9A8C\u8BC1 Web\u3001/health\uFF1B\u4EC5\u6253\u5305\u4E0D\u4F1A\u6267\u884C\u670D\u52A1\u5668\u5065\u5EB7\u68C0\u67E5\u3002\n\u6240\u9700\u955C\u50CF\uFF08app/web \u5DF2\u542B\u5728\u5305\u4E2D\uFF0C\u4E2D\u95F4\u4EF6\u9700\u53E6\u884C\u51C6\u5907\uFF09\uFF1A\n{images}\n\u6784\u5EFA\u955C\u50CF\u53EF\u7528\u4E8E\u591A\u73AF\u5883\uFF1B\u6B64\u5305\u7684\u73AF\u5883\u914D\u7F6E\u4EC5\u7528\u4E8E\u672C\u6B21\u6240\u9009\u73AF\u5883\u3002",
286
+ deploy_bundleInstructions: "\u90E8\u7F72\u5305\uFF1A{file}\n\u5305\u4E2D\u5DF2\u5305\u542B\u672C\u6B21\u9009\u62E9\u670D\u52A1\u9700\u8981\u7684\u955C\u50CF\uFF0C\u5305\u62EC\u81EA\u5EFA\u4E2D\u95F4\u4EF6\u3002\n\u6709 SSH \u8FDE\u63A5\u65F6\uFF0C\u5148\u6309 _comments \u8BF4\u660E\u4FEE\u6539\u65C1\u8FB9\u90E8\u7F72\u76EE\u5F55\u4E2D\u7684 deploy-target.json\uFF0C\u518D\u8FD0\u884C\u540C\u76EE\u5F55\u7684 deploy.ps1\uFF08Windows\uFF09\u6216 deploy.sh\uFF08Linux/macOS\uFF09\uFF1B\u4E24\u8005\u5747\u8BFB\u53D6\u6B64 JSON\uFF0Csh \u5165\u53E3\u8FD8\u9700\u672C\u673A\u5B89\u88C5 awk\u3002\n\u5B8C\u5168\u9694\u79BB\u65F6\uFF0C\u590D\u5236 tar \u5230 Linux \u670D\u52A1\u5668\uFF0C\u5728\u72EC\u7ACB\u76EE\u5F55\u89E3\u5305\uFF0C\u518D\u6267\u884C sh apply.sh\uFF08\u4F7F\u7528\u5305\u4E2D\u914D\u7F6E\u7684\u76EE\u6807\u76EE\u5F55\uFF09\u3002\n\u670D\u52A1\u5668\u9700\u8981 Docker/Podman\u3001Compose\u3001sh\u3001tar\u3001sha256sum\u3001awk\u3001curl\u3001util-linux flock\uFF0C\u4EE5\u53CA\u652F\u6301 flock \u7684\u6587\u4EF6\u7CFB\u7EDF\u548C\u76EE\u5F55\u3001\u5BB9\u5668\u8FD0\u884C\u6743\u9650\u3002\n\u56FA\u5B9A\u9501\u6587\u4EF6 .deploy.lock \u7981\u6B62\u5220\u9664\u6216\u66FF\u6362\uFF1B\u5168\u90E8\u6301\u6709\u8FDB\u7A0B\u9000\u51FA\u6216\u91CD\u542F\u540E\u81EA\u52A8\u91CA\u653E\u3002\u5F3A\u6740\u540E\u7684\u6267\u884C\u4E0E\u6062\u590D\u72B6\u6001\u987B\u4EBA\u5DE5\u6838\u5BF9\u3002\n\u90E8\u5206\u66F4\u65B0\u8981\u6C42\u670D\u52A1\u5668\u5DF2\u6709\u6210\u529F\u7684\u5B8C\u6574\u90E8\u7F72\uFF0C\u7AEF\u53E3\u53CA\u5E94\u7528\u5E03\u5C40\u4FDD\u6301\u4E00\u81F4\u3002\u6BCF\u6B65\u5931\u8D25\u4F1A\u8FD4\u56DE\u975E\u96F6\u9000\u51FA\u7801\uFF1B\u7ED3\u679C\u8BB0\u5F55\u5728\u90E8\u7F72\u76EE\u5F55 result-\u90E8\u7F72ID.json\u3002\n\u5305\u5305\u542B\u79C1\u6709\u8FD0\u884C\u914D\u7F6E\uFF0C\u8BF7\u5B89\u5168\u4F20\u8F93\u3002\n\u6240\u542B\u955C\u50CF\uFF1A\n{images}",
163
287
  deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
164
- deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002"
288
+ deploy_targetHost: "\u5FC5\u586B\uFF1ALinux \u670D\u52A1\u5668\u5730\u5740\u6216 SSH \u522B\u540D\u3002\u6267\u884C\u4EFB\u4E00\u4E0A\u4F20\u811A\u672C\u524D\uFF0C\u5C06 SERVER_HOST \u6539\u4E3A\u5B9E\u9645\u5730\u5740\u3002",
289
+ deploy_targetPort: "SSH \u7AEF\u53E3\uFF0C1-65535 \u7684\u6574\u6570\uFF0C\u9ED8\u8BA4 22\u3002",
290
+ deploy_targetUsername: "SSH \u767B\u5F55\u7528\u6237\u540D\uFF0C\u9ED8\u8BA4 deploy\uFF1B\u9700\u8981\u76EE\u6807\u76EE\u5F55\u5199\u5165\u6743\u9650\u53CA\u5BB9\u5668\u8FD0\u884C\u6743\u9650\u3002",
291
+ deploy_targetIdentityFile: "\u53EF\u9009\uFF1A\u8FD0\u884C\u4E0A\u4F20\u811A\u672C\u7684\u7535\u8111\u4E0A\u7684\u79C1\u94A5\u8DEF\u5F84\uFF0C\u5EFA\u8BAE\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u7559\u7A7A\u4F7F\u7528 ssh-agent \u6216\u9ED8\u8BA4\u5BC6\u94A5\u3002HAI_DEPLOY_IDENTITY_FILE \u53EF\u8986\u76D6\u3002",
292
+ deploy_targetKnownHostsFile: "\u53EF\u9009\uFF1A\u8FD0\u884C\u4E0A\u4F20\u811A\u672C\u7684\u7535\u8111\u4E0A\u7684 known_hosts \u8DEF\u5F84\uFF0C\u5EFA\u8BAE\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u7559\u7A7A\u4F7F\u7528 OpenSSH \u9ED8\u8BA4\u6587\u4EF6\u3002HAI_DEPLOY_KNOWN_HOSTS_FILE \u53EF\u8986\u76D6\u3002",
293
+ deploy_targetStrictHostKeyChecking: "\u5E03\u5C14\u503C\uFF0C\u9ED8\u8BA4 true\uFF1A\u9996\u6B21\u4FE1\u4EFB\u65B0\u4E3B\u673A\u5BC6\u94A5\uFF0C\u62D2\u7EDD\u5DF2\u53D8\u66F4\u5BC6\u94A5\uFF1Bfalse \u5173\u95ED\u4E3B\u673A\u5BC6\u94A5\u68C0\u67E5\u3002",
294
+ deploy_targetConnectTimeoutSeconds: "SSH \u8FDE\u63A5\u8D85\u65F6\u79D2\u6570\uFF0C1-2147483647 \u7684\u6574\u6570\uFF0C\u9ED8\u8BA4 10\u3002",
295
+ deploy_targetDirectory: "\u5FC5\u586B\uFF1ALinux \u670D\u52A1\u5668\u4E0A\u7684\u5E94\u7528\u7EDD\u5BF9\u76EE\u5F55\uFF0C\u4FDD\u5B58 incoming \u90E8\u7F72\u5305\u3001releases \u53CA\u6210\u529F\u72B6\u6001 current\uFF1B\u7981\u6B62 .. \u7236\u76EE\u5F55\u8DF3\u8F6C\u3002",
296
+ deploy_targetBundle: "\u5FC5\u586B\uFF1A\u672C\u90E8\u7F72\u64CD\u4F5C\u76EE\u5F55\u4E0A\u4E00\u7EA7\u7684 tar \u6587\u4EF6\u540D\uFF1B\u642C\u8FD0\u65F6\u8BF7\u5C06 tar \u548C\u672C\u76EE\u5F55\u5B8C\u6574\u653E\u5728\u4E00\u8D77\u3002",
297
+ deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002",
298
+ deploy_selectedContainerOnly: "\u6309\u670D\u52A1\u5C40\u90E8\u90E8\u7F72\u4EC5\u9002\u7528\u4E8E docker-ssh Compose \u76EE\u6807\u3002",
299
+ deploy_selectionInvalid: "\u65E0\u6CD5\u90E8\u7F72\u670D\u52A1 {service}\uFF1A\u8BF7\u68C0\u67E5\u670D\u52A1\u540D\u79F0\uFF0C\u5E76\u5148\u6784\u5EFA\u5BF9\u5E94\u955C\u50CF\u3002",
300
+ deploy_storageArtifactRequired: "\u5B58\u50A8\u521D\u59CB\u5316\u9700\u8981\u5E94\u7528\u955C\u50CF\u3002\u8BF7\u7528 --artifact-dir \u9009\u62E9\u542B app \u955C\u50CF\u7684\u5B8C\u6574\u5236\u54C1\uFF0C\u6216\u5148\u6267\u884C pnpm build --target docker --images app\u3002",
301
+ deploy_imageVersionRequired: "\u955C\u50CF\u5FC5\u987B\u6307\u5B9A\u7248\u672C\uFF0C\u4E0D\u80FD\u4F7F\u7528 latest\uFF1A{image}",
302
+ deploy_bundleRuntimeRequired: "\u5B8C\u6574\u79BB\u7EBF\u5305\u9700\u8981\u5BFC\u51FA\u4E2D\u95F4\u4EF6\u955C\u50CF\u3002\u8BF7\u542F\u52A8\u672C\u5730 Podman/Docker\uFF0C\u6216\u5728\u6784\u5EFA\u673A\u6253\u5305\u3002",
303
+ deploy_bundleFailed: "\u4EA4\u4ED8\u51C6\u5907\u6216\u90E8\u7F72\u5931\u8D25\uFF1A{reason}",
304
+ deploy_configFileInvalid: "\u914D\u7F6E\u6587\u4EF6 {path} \u7B2C {line} \u884C\u683C\u5F0F\u6709\u8BEF\u3002\u8BF7\u68C0\u67E5\u7F29\u8FDB\u3001\u5192\u53F7\u548C\u5F15\u53F7\u540E\u91CD\u8BD5\u3002",
305
+ deploy_configFileUnreadable: "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6 {path}\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u8DEF\u5F84\u4E0E\u8BFB\u53D6\u6743\u9650\u3002",
306
+ deploy_bundleNameInvalid: "\u90E8\u7F72\u5305\u6587\u4EF6\u540D\u53EA\u652F\u6301\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u52A0\u53F7\u3001\u77ED\u6A2A\u7EBF\u548C\u4E0B\u5212\u7EBF\uFF1B\u6240\u5728\u76EE\u5F55\u53EF\u4EE5\u5305\u542B\u7A7A\u683C\u3002",
307
+ deploy_secretsInvalid: "\u4E2D\u95F4\u4EF6\u51ED\u636E\u6587\u4EF6\u635F\u574F\u6216\u7F3A\u5C11\u5B57\u6BB5\uFF1A{path}\u3002\u8BF7\u4ECE\u5907\u4EFD\u6062\u590D\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u66F4\u6362\u5BC6\u7801\uFF0C\u4EE5\u514D\u5DF2\u6709\u6570\u636E\u65E0\u6CD5\u8BBF\u95EE\u3002",
308
+ deploy_localStopped: "\u5DF2\u786E\u8BA4\u672C\u5730\u547D\u4EE4\u8FDB\u7A0B\u6811\u5168\u90E8\u9000\u51FA\u3002",
309
+ deploy_localUnknown: "\u65E0\u6CD5\u786E\u8BA4\u672C\u5730\u547D\u4EE4\u8FDB\u7A0B\u6811\u5DF2\u5168\u90E8\u9000\u51FA\uFF1B\u8BF7\u68C0\u67E5\u672C\u673A\u6267\u884C\u72B6\u6001\u3002",
310
+ deploy_localNotStarted: "\u672C\u5730\u547D\u4EE4\u5C1A\u672A\u542F\u52A8\u3002",
311
+ deploy_commandOutputFailed: "\u547D\u4EE4\u8F93\u51FA\u6216\u56DE\u8C03\u5931\u8D25",
312
+ deploy_commandInputFailed: "\u547D\u4EE4\u8F93\u5165\u5931\u8D25",
313
+ deploy_commandTreeActive: "\u4E3B\u547D\u4EE4\u9000\u51FA\u540E\u4ECD\u6709\u6D3B\u8DC3\u7684\u5B50\u8FDB\u7A0B",
314
+ deploy_commandExecutionFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25",
315
+ deploy_remoteUnknown: "\u8FDC\u7AEF\u6267\u884C\u72B6\u6001\u672A\u77E5\uFF1B\u672C\u5730 SSH \u7ED3\u675F\u4E0D\u8BC1\u660E\u8FDC\u7AEF\u90E8\u7F72\u5DF2\u505C\u6B62\uFF0C\u8BF7\u6838\u5BF9\u670D\u52A1\u5668\u7ED3\u679C\u4E0E\u5B9E\u9645\u5BB9\u5668\u72B6\u6001\u3002",
316
+ deploy_remoteFinished: "\u5339\u914D\u7684\u670D\u52A1\u5668\u7ED3\u679C\u786E\u8BA4\u90E8\u7F72\u811A\u672C\u53CA\u6062\u590D\u5DF2\u7ED3\u675F\uFF1A\u9636\u6BB5 {stage}\uFF0C\u9000\u51FA\u7801 {code}\u3002\u8BF7\u6838\u5BF9\u5B9E\u9645\u670D\u52A1\u5065\u5EB7\u72B6\u6001\u3002"
165
317
  };
166
318
 
167
319
  // src/deploy-i18n.ts
168
320
  var deployM = core.i18n.createMessageGetter({
169
321
  "zh-CN": zh_CN_default,
170
- "en-US": en_US_default
322
+ "en-US": en_US_default,
323
+ "ja-JP": ja_JP_default
171
324
  });
172
325
 
173
326
  // src/deploy-config.ts
@@ -177,11 +330,11 @@ var VercelProviderConfigSchema = z.object({
177
330
  });
178
331
  var SshConfigSchema = z.object({
179
332
  /** 远程主机地址或 ~/.ssh/config 中的 Host 别名 */
180
- host: z.string().min(1),
333
+ host: z.string().regex(/^[a-z0-9][a-z0-9.:-]*$/i),
181
334
  /** SSH 端口 */
182
335
  port: z.number().int().min(1).max(65535).default(22),
183
336
  /** SSH 用户名 */
184
- username: z.string().min(1),
337
+ username: z.string().regex(/^[a-z_][\w-]*$/i),
185
338
  /** 私钥文件路径(省略时使用 ssh-agent 或 ~/.ssh 默认密钥) */
186
339
  identityFile: z.string().min(1).optional(),
187
340
  /** known_hosts 文件路径(省略时使用默认;配合 CI/临时环境隔离主机指纹) */
@@ -206,7 +359,7 @@ var DockerSshProviderConfigSchema = z.object({
206
359
  /** 远程主机运行参数 */
207
360
  remote: z.object({
208
361
  /** 远程应用根目录 */
209
- baseDir: z.string().min(1).default("/opt/hai/apps"),
362
+ baseDir: z.string().regex(/^\/[\w/. -]+$/).refine((value) => !value.split("/").includes("..")).default("/opt/hai/apps"),
210
363
  /** 远程容器运行时(auto 优先 podman) */
211
364
  runtime: z.enum(["auto", "docker", "podman"]).default("auto")
212
365
  }).default({ baseDir: "/opt/hai/apps", runtime: "auto" }),
@@ -305,10 +458,16 @@ function resolveConfig(value, prefix, env) {
305
458
  }
306
459
  function loadDeploymentConfig(name, path, values) {
307
460
  try {
308
- const config = resolveConfig(parse(readFileSync(path, "utf8")), `HAI_${name.toUpperCase()}`, values);
461
+ const lineCounter = new LineCounter();
462
+ const document = parseDocument(readFileSync(path, "utf8"), { lineCounter, prettyErrors: false });
463
+ if (document.errors.length)
464
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: String(lineCounter.linePos(document.errors[0].pos[0]).line) } }));
465
+ const config = resolveConfig(document.toJS(), `HAI_${name.toUpperCase()}`, values);
466
+ if (config !== null && (typeof config !== "object" || Array.isArray(config)))
467
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
309
468
  return ok(config && typeof config === "object" && !Array.isArray(config) ? config : {});
310
469
  } catch {
311
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
470
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", { params: { path } }));
312
471
  }
313
472
  }
314
473
  function loadDeploymentEnvironment(options) {
@@ -324,7 +483,7 @@ function loadDeploymentEnvironment(options) {
324
483
  return loaded;
325
484
  try {
326
485
  const values = { ...loaded.data };
327
- for (const [name, value] of Object.entries(process2.env)) {
486
+ for (const [name, value] of Object.entries(process3.env)) {
328
487
  if (/^(?:HAI_|PUBLIC_|VITE_)/.test(name) && value !== void 0)
329
488
  values[name] = value;
330
489
  }
@@ -512,7 +671,7 @@ function loadCredentials() {
512
671
  const data = readCredentialsFile();
513
672
  const keys = Object.keys(data);
514
673
  for (const key of keys) {
515
- process2.env[key] = String(data[key]);
674
+ process3.env[key] = String(data[key]);
516
675
  }
517
676
  logger.debug("Credentials loaded", { count: keys.length });
518
677
  return ok(keys);
@@ -533,7 +692,7 @@ function saveCredential(key, value) {
533
692
  const data = readCredentialsFile();
534
693
  data[key] = value;
535
694
  writeCredentialsFile(data);
536
- process2.env[key] = value;
695
+ process3.env[key] = value;
537
696
  logger.debug("Credential saved", { key });
538
697
  return ok(void 0);
539
698
  } catch (error) {
@@ -553,7 +712,7 @@ function saveCredentials(entries) {
553
712
  const data = readCredentialsFile();
554
713
  for (const [key, value] of Object.entries(entries)) {
555
714
  data[key] = value;
556
- process2.env[key] = value;
715
+ process3.env[key] = value;
557
716
  }
558
717
  writeCredentialsFile(data);
559
718
  logger.debug("Credentials saved", { count: Object.keys(entries).length });
@@ -569,17 +728,20 @@ function saveCredentials(entries) {
569
728
  );
570
729
  }
571
730
  }
572
- function resolveArtifactDirectory(appDir) {
731
+ function resolveProjectDirectory(appDir) {
573
732
  let directory = resolve(appDir);
574
733
  while (true) {
575
734
  if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
576
- return join(directory, ".hai/build/docker");
735
+ return directory;
577
736
  const parent = dirname(directory);
578
737
  if (parent === directory)
579
- return join(appDir, ".hai/build/docker");
738
+ return resolve(appDir);
580
739
  directory = parent;
581
740
  }
582
741
  }
742
+ function resolveArtifactDirectory(appDir) {
743
+ return join(resolveProjectDirectory(appDir), ".hai/build/docker");
744
+ }
583
745
  function usesDefaultMiddleware(scan, service, options) {
584
746
  const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
585
747
  return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
@@ -603,6 +765,19 @@ var RecoverySchema = z.object({
603
765
  resourceStatus: z.enum(["created", "reused", "unknown"])
604
766
  }))
605
767
  });
768
+ var CommandStateSchema = z.object({
769
+ reason: z.enum(["timeout", "output", "input", "spawn", "exit", "configuration"]),
770
+ localTermination: z.enum(["confirmed", "unknown", "not-started"]),
771
+ remoteExecution: z.enum(["unknown", "finished"]).optional(),
772
+ deploymentId: z.string().optional(),
773
+ resultFile: z.string().optional(),
774
+ remoteExitCode: z.number().int().optional(),
775
+ remoteStage: z.string().optional()
776
+ });
777
+ function getDeployCommandState(error) {
778
+ const parsed = CommandStateSchema.safeParse(error.ext?.commandExecution);
779
+ return parsed.success ? parsed.data : void 0;
780
+ }
606
781
  function getDeployRecovery(error) {
607
782
  const parsed = RecoverySchema.safeParse(error.ext?.recovery);
608
783
  return parsed.success ? parsed.data : void 0;
@@ -719,7 +894,7 @@ async function scanApp(inputDir, options) {
719
894
  for (const [index, item] of providers.entries()) {
720
895
  const provider = record(item);
721
896
  const type = String(provider.type ?? "");
722
- const service = type === "aliyun-sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
897
+ const service = type === "aliyun-sms" || ["console", "api"].includes(type) && provider.name === "sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
723
898
  if (service)
724
899
  add(service, type, "reach", ["providers", String(index)], provider, "@h-ai/reach");
725
900
  }
@@ -745,147 +920,1045 @@ async function scanApp(inputDir, options) {
745
920
  return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", { params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" } }));
746
921
  }
747
922
  }
748
- var logger3 = core.logger.child({ module: "deploy", scope: "command" });
749
- function maskSecrets(text, secrets) {
750
- if (!secrets || secrets.length === 0)
751
- return text;
752
- let masked = text;
753
- for (const secret of secrets) {
754
- if (secret && secret.length > 0)
755
- masked = masked.split(secret).join("***");
923
+ var nativeJob = String.raw`
924
+ using System;
925
+ using System.ComponentModel;
926
+ using System.Runtime.InteropServices;
927
+ using System.Text;
928
+ using System.Threading;
929
+
930
+ public static class HaiCommandJob {
931
+ [StructLayout(LayoutKind.Sequential)] struct BasicLimits {
932
+ public long ProcessTime, JobTime;
933
+ public uint Flags;
934
+ public UIntPtr MinimumWorkingSet, MaximumWorkingSet;
935
+ public uint ActiveProcessLimit;
936
+ public UIntPtr Affinity;
937
+ public uint PriorityClass, SchedulingClass;
938
+ }
939
+ [StructLayout(LayoutKind.Sequential)] struct IoCounters {
940
+ public ulong ReadOperations, WriteOperations, OtherOperations, ReadBytes, WriteBytes, OtherBytes;
941
+ }
942
+ [StructLayout(LayoutKind.Sequential)] struct ExtendedLimits {
943
+ public BasicLimits Basic;
944
+ public IoCounters Io;
945
+ public UIntPtr ProcessMemory, JobMemory, PeakProcessMemory, PeakJobMemory;
946
+ }
947
+ [StructLayout(LayoutKind.Sequential)] struct Accounting {
948
+ public long UserTime, KernelTime, PeriodUserTime, PeriodKernelTime;
949
+ public uint Faults, Total, Active, Terminated;
950
+ }
951
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct Startup {
952
+ public uint Size;
953
+ public string Reserved, Desktop, Title;
954
+ public uint X, Y, Width, Height, XChars, YChars, Fill, Flags;
955
+ public ushort Show, ReservedSize;
956
+ public IntPtr ReservedData, Input, Output, Error;
957
+ }
958
+ [StructLayout(LayoutKind.Sequential)] struct ProcessInfo {
959
+ public IntPtr Process, Thread;
960
+ public uint ProcessId, ThreadId;
961
+ }
962
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr CreateJobObject(IntPtr attributes, string name);
963
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr OpenJobObject(uint access, bool inherit, string name);
964
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetInformationJobObject(IntPtr job, int kind, ref ExtendedLimits information, uint size);
965
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool QueryInformationJobObject(IntPtr job, int kind, out Accounting information, uint size, IntPtr length);
966
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
967
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool CreateProcess(string application, StringBuilder command, IntPtr processAttributes, IntPtr threadAttributes, bool inherit, uint flags, IntPtr environment, string directory, ref Startup startup, out ProcessInfo information);
968
+ [DllImport("kernel32.dll", SetLastError = true)] static extern uint ResumeThread(IntPtr thread);
969
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateProcess(IntPtr process, uint code);
970
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateJobObject(IntPtr job, uint code);
971
+ [DllImport("kernel32.dll", SetLastError = true)] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
972
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool GetExitCodeProcess(IntPtr process, out uint code);
973
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr handle);
974
+ [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle(int kind);
975
+ [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags);
976
+
977
+ static void Check(bool success) { if (!success) throw new Win32Exception(Marshal.GetLastWin32Error()); }
978
+ static uint Active(IntPtr job) {
979
+ Accounting information;
980
+ Check(QueryInformationJobObject(job, 1, out information, (uint)Marshal.SizeOf(typeof(Accounting)), IntPtr.Zero));
981
+ return information.Active;
982
+ }
983
+ // Windows argv 编码:引号前与结尾的反斜杠加倍,其余字符原样传递,不经过命令 shell。
984
+ static string Quote(string value) {
985
+ StringBuilder result = new StringBuilder("\"");
986
+ int slashes = 0;
987
+ foreach (char current in value) {
988
+ if (current == '\\') { slashes++; continue; }
989
+ if (current == '"') result.Append('\\', slashes * 2 + 1);
990
+ else result.Append('\\', slashes);
991
+ result.Append(current);
992
+ slashes = 0;
993
+ }
994
+ result.Append('\\', slashes * 2).Append('"');
995
+ return result.ToString();
996
+ }
997
+ public static int Run(string name, string command, string[] arguments) {
998
+ IntPtr job = CreateJobObject(IntPtr.Zero, name);
999
+ if (job == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
1000
+ ProcessInfo process = new ProcessInfo();
1001
+ bool assigned = false;
1002
+ try {
1003
+ ExtendedLimits limits = new ExtendedLimits();
1004
+ limits.Basic.Flags = 0x2000; // JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;不允许 breakaway。
1005
+ Check(SetInformationJobObject(job, 9, ref limits, (uint)Marshal.SizeOf(typeof(ExtendedLimits))));
1006
+ StringBuilder line = new StringBuilder(Quote(command));
1007
+ foreach (string argument in arguments) line.Append(' ').Append(Quote(argument));
1008
+ Startup startup = new Startup();
1009
+ startup.Size = (uint)Marshal.SizeOf(typeof(Startup));
1010
+ startup.Flags = 0x100;
1011
+ startup.Input = GetStdHandle(-10); startup.Output = GetStdHandle(-11); startup.Error = GetStdHandle(-12);
1012
+ Check(SetHandleInformation(startup.Input, 1, 1));
1013
+ Check(SetHandleInformation(startup.Output, 1, 1));
1014
+ Check(SetHandleInformation(startup.Error, 1, 1));
1015
+ Check(CreateProcess(null, line, IntPtr.Zero, IntPtr.Zero, true, 0x08000004, IntPtr.Zero, null, ref startup, out process));
1016
+ Check(AssignProcessToJobObject(job, process.Process));
1017
+ assigned = true;
1018
+ if (ResumeThread(process.Thread) == 0xFFFFFFFF) throw new Win32Exception(Marshal.GetLastWin32Error());
1019
+ if (WaitForSingleObject(process.Process, 0xFFFFFFFF) != 0) throw new Win32Exception(Marshal.GetLastWin32Error());
1020
+ uint code;
1021
+ Check(GetExitCodeProcess(process.Process, out code));
1022
+ while (Active(job) != 0) Thread.Sleep(20);
1023
+ return unchecked((int)code);
1024
+ }
1025
+ finally {
1026
+ if (process.Process != IntPtr.Zero) {
1027
+ // 分配失败时进程仍挂起,直接终止,不允许裸进程恢复;Job 在其他异常时同样清空。
1028
+ if (!assigned) TerminateProcess(process.Process, 125);
1029
+ else TerminateJobObject(job, 125);
1030
+ WaitForSingleObject(process.Process, 5000);
1031
+ }
1032
+ if (process.Thread != IntPtr.Zero) CloseHandle(process.Thread);
1033
+ if (process.Process != IntPtr.Zero) CloseHandle(process.Process);
1034
+ CloseHandle(job);
1035
+ }
1036
+ }
1037
+ public static bool Empty(string name) {
1038
+ IntPtr job = OpenJobObject(4, false, name); // JOB_OBJECT_QUERY
1039
+ if (job == IntPtr.Zero) {
1040
+ if (Marshal.GetLastWin32Error() == 2) return true;
1041
+ throw new Win32Exception(Marshal.GetLastWin32Error());
1042
+ }
1043
+ try { return Active(job) == 0; }
1044
+ finally { CloseHandle(job); }
1045
+ }
1046
+ }
1047
+ `;
1048
+ function powershell(script) {
1049
+ return ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")];
1050
+ }
1051
+ function windowsCommand(command, args) {
1052
+ const name = `Local\\hai-deploy-${randomUUID()}`;
1053
+ const key = `__HAI_DEPLOY_JOB_${randomUUID().replaceAll("-", "")}`;
1054
+ const script = `$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
1055
+ try {
1056
+ Add-Type -TypeDefinition @'
1057
+ ${nativeJob}
1058
+ '@
1059
+ $inputConfig = [Environment]::GetEnvironmentVariable('${key}') | ConvertFrom-Json
1060
+ [Environment]::SetEnvironmentVariable('${key}', $null)
1061
+ exit ([HaiCommandJob]::Run([string]$inputConfig.name, [string]$inputConfig.command, [string[]]$inputConfig.args))
1062
+ } catch { [Console]::Error.WriteLine('Windows command job failed'); exit 125 }`;
1063
+ return { name, args: powershell(script), env: { [key]: JSON.stringify({ name, command, args }) } };
1064
+ }
1065
+ function windowsJobProbe(name) {
1066
+ return powershell(`$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
1067
+ try {
1068
+ Add-Type -TypeDefinition @'
1069
+ ${nativeJob}
1070
+ '@
1071
+ if ([HaiCommandJob]::Empty('${name}')) { exit 0 } else { exit 1 }
1072
+ } catch { exit 2 }`);
1073
+ }
1074
+
1075
+ // src/container/deploy-command-process.ts
1076
+ async function groupExited(pid) {
1077
+ try {
1078
+ process3.kill(-pid, 0);
1079
+ } catch (error) {
1080
+ return error instanceof Error && "code" in error && error.code === "ESRCH";
1081
+ }
1082
+ if (process3.platform !== "linux")
1083
+ return false;
1084
+ try {
1085
+ for (const entry of await readdir("/proc")) {
1086
+ if (!/^\d+$/.test(entry))
1087
+ continue;
1088
+ let stat;
1089
+ try {
1090
+ stat = await readFile(`/proc/${entry}/stat`, "utf8");
1091
+ } catch (error) {
1092
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
1093
+ continue;
1094
+ return false;
1095
+ }
1096
+ const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
1097
+ if (Number(fields[2]) === pid && fields[0] !== "Z" && fields[0] !== "X")
1098
+ return false;
1099
+ }
1100
+ return true;
1101
+ } catch {
1102
+ return false;
756
1103
  }
757
- return masked;
758
1104
  }
759
- function runCommand(command, args, options = {}) {
1105
+ async function helper(command, args, timeoutMs) {
1106
+ if (timeoutMs <= 0)
1107
+ return false;
760
1108
  return new Promise((resolve6) => {
761
- const child = spawn(command, args, {
762
- cwd: options.cwd,
763
- env: options.env ? { ...process2.env, ...options.env } : process2.env,
764
- shell: false
765
- });
766
- let stdout = "";
767
- let stderr = "";
1109
+ const child = spawn(command, args, { windowsHide: true, shell: false, stdio: "ignore" });
768
1110
  let settled = false;
769
1111
  let timer;
770
- const finish = (result) => {
1112
+ const finish = (success) => {
771
1113
  if (settled)
772
1114
  return;
773
1115
  settled = true;
774
1116
  if (timer)
775
1117
  clearTimeout(timer);
776
- resolve6(result);
1118
+ resolve6(success);
777
1119
  };
778
- if (options.timeoutMs && options.timeoutMs > 0) {
779
- timer = setTimeout(() => {
780
- child.kill("SIGKILL");
781
- logger3.error("Command timed out", { command, timeoutMs: options.timeoutMs });
782
- finish(err(
783
- HaiDeployError.REMOTE_COMMAND_FAILED,
784
- deployM("deploy_commandTimeout", { params: { command } })
785
- ));
786
- }, options.timeoutMs);
787
- }
788
- child.stdout.on("data", (chunk) => {
789
- stdout += chunk.toString("utf-8");
790
- });
791
- child.stderr.on("data", (chunk) => {
792
- stderr += chunk.toString("utf-8");
793
- });
794
- child.on("error", (error) => {
795
- logger3.error("Command failed to start", { command, error: maskSecrets(String(error), options.secrets) });
796
- finish(err(
797
- HaiDeployError.REMOTE_COMMAND_FAILED,
798
- deployM("deploy_commandFailed", {
799
- params: { command, error: maskSecrets(error.message, options.secrets) }
800
- }),
801
- error
802
- ));
803
- });
804
- child.on("close", (code) => {
805
- const exitCode = code ?? -1;
806
- if (exitCode === 0) {
807
- finish(ok({ code: exitCode, stdout, stderr }));
808
- return;
1120
+ timer = globalThis.setTimeout(() => {
1121
+ child.kill("SIGKILL");
1122
+ child.unref();
1123
+ finish(false);
1124
+ }, timeoutMs);
1125
+ child.once("error", () => finish(false));
1126
+ child.once("close", (code) => finish(code === 0));
1127
+ });
1128
+ }
1129
+ function spawnCommand(command, args, options) {
1130
+ const job = process3.platform === "win32" ? windowsCommand(command, args) : void 0;
1131
+ const child = spawn(job ? "powershell.exe" : command, job?.args ?? args, {
1132
+ ...options,
1133
+ env: job ? { ...options.env, ...job.env } : options.env,
1134
+ windowsHide: true,
1135
+ shell: false,
1136
+ detached: !job
1137
+ });
1138
+ let exited = false;
1139
+ child.once("exit", () => {
1140
+ exited = true;
1141
+ });
1142
+ child.once("close", () => {
1143
+ exited = true;
1144
+ });
1145
+ let termination;
1146
+ const treeExited = async () => {
1147
+ if (!child.pid)
1148
+ return true;
1149
+ if (job)
1150
+ return exited && helper("powershell.exe", windowsJobProbe(job.name), 5e3);
1151
+ return groupExited(child.pid);
1152
+ };
1153
+ const terminate = (timeoutMs) => {
1154
+ termination ??= (async () => {
1155
+ const deadline = performance.now() + timeoutMs;
1156
+ let windowsKillConfirmed = !job || exited;
1157
+ if (child.pid) {
1158
+ if (job) {
1159
+ if (!exited) {
1160
+ windowsKillConfirmed = await helper("taskkill", ["/pid", String(child.pid), "/t", "/f"], Math.max(1, deadline - performance.now()));
1161
+ child.kill("SIGKILL");
1162
+ }
1163
+ } else {
1164
+ try {
1165
+ process3.kill(-child.pid, "SIGKILL");
1166
+ } catch {
1167
+ }
1168
+ }
1169
+ }
1170
+ while (performance.now() < deadline) {
1171
+ if (exited)
1172
+ break;
1173
+ await setTimeout$1(20);
1174
+ }
1175
+ let verified = false;
1176
+ if (exited && job) {
1177
+ verified = await helper("powershell.exe", windowsJobProbe(job.name), deadline - performance.now());
1178
+ verified &&= windowsKillConfirmed || child.exitCode !== null;
1179
+ } else if (exited) {
1180
+ while (performance.now() < deadline) {
1181
+ if (await treeExited()) {
1182
+ verified = true;
1183
+ break;
1184
+ }
1185
+ await setTimeout$1(20);
1186
+ }
1187
+ }
1188
+ child.stdin.destroy();
1189
+ child.stdout.destroy();
1190
+ child.stderr.destroy();
1191
+ if (!verified)
1192
+ child.unref();
1193
+ return verified ? "confirmed" : "unknown";
1194
+ })();
1195
+ return termination;
1196
+ };
1197
+ return { child, treeExited, terminate };
1198
+ }
1199
+
1200
+ // src/container/deploy-secret-masker.ts
1201
+ function createSecretMasker(secrets = []) {
1202
+ const values = [...new Set(secrets.filter(Boolean))].sort((a, b) => b.length - a.length);
1203
+ if (values.reduce((total, value) => total + value.length, 0) > 1048576)
1204
+ throw new RangeError("Secret redaction input is too large");
1205
+ const pattern = values.length ? new RegExp(values.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g") : void 0;
1206
+ const tailLength = Math.max(0, (values[0]?.length ?? 1) - 1);
1207
+ let pending = "";
1208
+ let ended = false;
1209
+ function drain(final) {
1210
+ let end = final ? pending.length : Math.max(0, pending.length - tailLength);
1211
+ if (end > 0 && end < pending.length && /[\uD800-\uDBFF]/.test(pending[end - 1]) && /[\uDC00-\uDFFF]/.test(pending[end]))
1212
+ end--;
1213
+ if (end === 0)
1214
+ return "";
1215
+ let cursor = 0;
1216
+ let output = "";
1217
+ if (pattern) {
1218
+ pattern.lastIndex = 0;
1219
+ let match = pattern.exec(pending);
1220
+ while (match && match.index < end) {
1221
+ output += `${pending.slice(cursor, match.index)}***`;
1222
+ cursor = pattern.lastIndex;
1223
+ end = Math.max(end, cursor);
1224
+ match = pattern.exec(pending);
1225
+ }
1226
+ }
1227
+ output += pending.slice(cursor, end);
1228
+ pending = pending.slice(end);
1229
+ return output;
1230
+ }
1231
+ return {
1232
+ write(text) {
1233
+ if (ended)
1234
+ throw new Error("Secret masker has ended");
1235
+ pending += text;
1236
+ return drain(false);
1237
+ },
1238
+ end() {
1239
+ if (ended)
1240
+ return "";
1241
+ ended = true;
1242
+ return drain(true);
1243
+ }
1244
+ };
1245
+ }
1246
+
1247
+ // src/container/deploy-command.ts
1248
+ var logger3 = core.logger.child({ module: "deploy", scope: "command" });
1249
+ function maskSecrets(text, secrets = []) {
1250
+ const masker = createSecretMasker(secrets);
1251
+ return masker.write(text) + masker.end();
1252
+ }
1253
+ async function runCommand(command, args, options = {}) {
1254
+ const failure = (message, state) => err({
1255
+ ...HaiDeployError.REMOTE_COMMAND_FAILED,
1256
+ message: `${message}
1257
+ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state.localTermination === "not-started" ? "deploy_localNotStarted" : "deploy_localUnknown")}`,
1258
+ ext: { commandExecution: state }
1259
+ });
1260
+ try {
1261
+ return await new Promise((resolve6) => {
1262
+ if (typeof command !== "string" || !command || command.includes("\0") || args.some((argument) => typeof argument !== "string" || argument.includes("\0")))
1263
+ throw new TypeError("Invalid command arguments");
1264
+ if (options.input !== void 0 && typeof options.input !== "string")
1265
+ throw new TypeError("Invalid command input");
1266
+ const maxCapture = options.maxCaptureChars ?? 1048576;
1267
+ if (!Number.isSafeInteger(maxCapture) || maxCapture <= 0 || maxCapture > 16777216)
1268
+ throw new RangeError("Invalid command capture limit");
1269
+ const terminationTimeout = options.terminationTimeoutMs ?? 1e4;
1270
+ if (!Number.isSafeInteger(terminationTimeout) || terminationTimeout <= 0 || terminationTimeout > 6e4)
1271
+ throw new RangeError("Invalid command termination timeout");
1272
+ if (options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
1273
+ throw new RangeError("Invalid command timeout");
1274
+ const secrets = [...options.secrets ?? []];
1275
+ const safeCommand = maskSecrets(command, secrets);
1276
+ const outputMasker = createSecretMasker(secrets);
1277
+ const errorMasker = createSecretMasker(secrets);
1278
+ const control = spawnCommand(command, args, { cwd: options.cwd, env: options.env ? { ...process3.env, ...options.env } : process3.env });
1279
+ const child = control.child;
1280
+ child.stdout.setEncoding("utf8");
1281
+ child.stderr.setEncoding("utf8");
1282
+ let stdout = "";
1283
+ let stderr = "";
1284
+ let stdoutTruncated = false;
1285
+ let stderrTruncated = false;
1286
+ let settled = false;
1287
+ let interrupted = false;
1288
+ let interruption;
1289
+ let timer;
1290
+ const finish = (result) => {
1291
+ if (settled)
1292
+ return;
1293
+ settled = true;
1294
+ if (timer)
1295
+ clearTimeout(timer);
1296
+ resolve6(result);
1297
+ };
1298
+ const interrupt = (reason, message) => {
1299
+ interrupted = true;
1300
+ interruption ??= (async () => {
1301
+ if (timer)
1302
+ clearTimeout(timer);
1303
+ const localTermination = child.pid ? await control.terminate(terminationTimeout) : "not-started";
1304
+ logger3.error("Command termination checked", { command: safeCommand, reason, localTermination });
1305
+ finish(failure(message, { reason, localTermination }));
1306
+ })();
1307
+ return interruption;
1308
+ };
1309
+ const publish = async (text) => {
1310
+ if (!text || settled || interrupted)
1311
+ return;
1312
+ stdoutTruncated ||= stdout.length + text.length > maxCapture;
1313
+ stdout = (stdout + text).slice(-maxCapture);
1314
+ try {
1315
+ await options.onOutput?.(text);
1316
+ } catch {
1317
+ await interrupt("output", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandOutputFailed") } }));
1318
+ }
1319
+ };
1320
+ const captureError = (text) => {
1321
+ stderrTruncated ||= stderr.length + text.length > maxCapture;
1322
+ stderr = (stderr + text).slice(-maxCapture);
1323
+ };
1324
+ if (options.timeoutMs && options.timeoutMs > 0) {
1325
+ timer = setTimeout(() => {
1326
+ logger3.error("Command timed out", { command: safeCommand, timeoutMs: options.timeoutMs });
1327
+ void interrupt("timeout", deployM("deploy_commandTimeout", { params: { command: safeCommand } }));
1328
+ }, options.timeoutMs);
809
1329
  }
810
- logger3.error("Command exited with non-zero code", {
811
- command,
812
- code: exitCode,
813
- stderr: maskSecrets(stderr, options.secrets).slice(-500),
814
- stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
1330
+ const outputTask = (async () => {
1331
+ try {
1332
+ for await (const chunk of child.stdout) {
1333
+ if (settled || interrupted)
1334
+ return;
1335
+ if (typeof chunk !== "string")
1336
+ throw new TypeError("Command output must be UTF-8 decoded");
1337
+ await publish(outputMasker.write(chunk));
1338
+ }
1339
+ if (!settled && !interrupted)
1340
+ await publish(outputMasker.end());
1341
+ } catch {
1342
+ await interrupt("output", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandOutputFailed") } }));
1343
+ }
1344
+ })();
1345
+ const errorTask = (async () => {
1346
+ try {
1347
+ for await (const chunk of child.stderr) {
1348
+ if (settled || interrupted)
1349
+ return;
1350
+ if (typeof chunk !== "string")
1351
+ throw new TypeError("Command error output must be UTF-8 decoded");
1352
+ captureError(errorMasker.write(chunk));
1353
+ }
1354
+ if (!settled && !interrupted)
1355
+ captureError(errorMasker.end());
1356
+ } catch {
1357
+ await interrupt("output", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandOutputFailed") } }));
1358
+ }
1359
+ })();
1360
+ child.stdin.on("error", () => {
1361
+ void interrupt("input", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandInputFailed") } }));
815
1362
  });
816
- finish(err(
817
- HaiDeployError.REMOTE_COMMAND_FAILED,
818
- deployM("deploy_commandFailed", {
819
- params: { command, error: maskSecrets(`${stdout.slice(-2500)}
820
- ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
821
- })
822
- ));
823
- });
824
- if (options.input !== void 0) {
825
- child.stdin.write(options.input);
826
- child.stdin.end();
827
- } else {
1363
+ child.on("error", (error) => {
1364
+ const safeError = maskSecrets(error.message, secrets);
1365
+ logger3.error("Command failed to start", { command: safeCommand, error: safeError });
1366
+ void interrupt("spawn", deployM("deploy_commandFailed", { params: { command: safeCommand, error: safeError } }));
1367
+ });
1368
+ child.on("close", async (code) => {
1369
+ await Promise.all([outputTask, errorTask]);
1370
+ if (settled || interrupted)
1371
+ return;
1372
+ if (timer)
1373
+ clearTimeout(timer);
1374
+ if (!await control.treeExited()) {
1375
+ await interrupt("exit", deployM("deploy_commandFailed", { params: { command: safeCommand, error: deployM("deploy_commandTreeActive") } }));
1376
+ return;
1377
+ }
1378
+ const exitCode = code ?? -1;
1379
+ if (exitCode === 0) {
1380
+ finish(ok({ code: exitCode, stdout, stderr, stdoutTruncated, stderrTruncated }));
1381
+ return;
1382
+ }
1383
+ logger3.error("Command exited with non-zero code", { command: safeCommand, code: exitCode, stderr: maskSecrets(stderr, secrets).slice(-500), stdout: maskSecrets(stdout, secrets).slice(-3e3), stdoutTruncated, stderrTruncated });
1384
+ finish(failure(deployM("deploy_commandFailed", { params: { command: safeCommand, error: maskSecrets(`${stdout.slice(-2500)}
1385
+ ${stderr || `exit ${exitCode}`}`, secrets).slice(-3e3) } }), { reason: "exit", localTermination: "confirmed" }));
1386
+ });
1387
+ if (options.input !== void 0)
1388
+ child.stdin.write(options.input);
828
1389
  child.stdin.end();
1390
+ });
1391
+ } catch {
1392
+ return failure(deployM("deploy_commandFailed", { params: { command: "<command>", error: deployM("deploy_commandExecutionFailed") } }), { reason: "configuration", localTermination: "not-started" });
1393
+ }
1394
+ }
1395
+
1396
+ // src/container/deploy-target.ts
1397
+ function createTargetReader() {
1398
+ return String.raw`function fail(message) {
1399
+ print "Invalid deploy-target.json: " message > "/dev/stderr"
1400
+ exit 2
1401
+ }
1402
+ function whitespace() {
1403
+ while (substr(input, position, 1) ~ /^[ \t\r\n]$/) position++
1404
+ }
1405
+ function hexadecimal(text, result, offset, digit) {
1406
+ result = 0
1407
+ if (length(text) != 4) fail("invalid Unicode escape")
1408
+ for (offset = 1; offset <= 4; offset++) {
1409
+ digit = index("0123456789abcdef", tolower(substr(text, offset, 1))) - 1
1410
+ if (digit < 0) fail("invalid Unicode escape")
1411
+ result = result * 16 + digit
1412
+ }
1413
+ return result
1414
+ }
1415
+ function utf8(code) {
1416
+ if (code < 128) return sprintf("%c", code)
1417
+ if (code < 2048) return sprintf("%c%c", 192 + int(code / 64), 128 + code % 64)
1418
+ if (code < 65536) return sprintf("%c%c%c", 224 + int(code / 4096), 128 + int(code / 64) % 64, 128 + code % 64)
1419
+ return sprintf("%c%c%c%c", 240 + int(code / 262144), 128 + int(code / 4096) % 64, 128 + int(code / 64) % 64, 128 + code % 64)
1420
+ }
1421
+ function string_value( result, character, escaped, code, low) {
1422
+ if (substr(input, position++, 1) != "\"") fail("expected string")
1423
+ result = ""
1424
+ while (position <= length(input)) {
1425
+ character = substr(input, position++, 1)
1426
+ if (character == "\"") return result
1427
+ if (character ~ /[[:cntrl:]]/) fail("control character in string")
1428
+ if (character != "\\") { result = result character; continue }
1429
+ escaped = substr(input, position++, 1)
1430
+ if (escaped == "\"" || escaped == "\\" || escaped == "/") result = result escaped
1431
+ else if (escaped == "b") result = result sprintf("%c", 8)
1432
+ else if (escaped == "f") result = result sprintf("%c", 12)
1433
+ else if (escaped == "n") result = result "\n"
1434
+ else if (escaped == "r") result = result "\r"
1435
+ else if (escaped == "t") result = result "\t"
1436
+ else if (escaped == "u") {
1437
+ code = hexadecimal(substr(input, position, 4)); position += 4
1438
+ if (code >= 55296 && code <= 56319) {
1439
+ if (substr(input, position, 2) != "\\u") fail("missing low surrogate")
1440
+ position += 2
1441
+ low = hexadecimal(substr(input, position, 4)); position += 4
1442
+ if (low < 56320 || low > 57343) fail("invalid low surrogate")
1443
+ code = 65536 + (code - 55296) * 1024 + low - 56320
1444
+ } else if (code >= 56320 && code <= 57343) fail("unexpected low surrogate")
1445
+ if (code == 0) fail("NUL is not allowed")
1446
+ result = result utf8(code)
1447
+ } else fail("invalid escape")
1448
+ }
1449
+ fail("unterminated string")
1450
+ }
1451
+ function object_value(depth, key, character, value, kind) {
1452
+ if (depth > 16) fail("nesting is too deep")
1453
+ position++
1454
+ whitespace()
1455
+ if (substr(input, position, 1) == "}") { position++; return }
1456
+ while (position <= length(input)) {
1457
+ whitespace(); key = string_value(); whitespace()
1458
+ if (substr(input, position++, 1) != ":") fail("expected colon")
1459
+ whitespace(); character = substr(input, position, 1)
1460
+ if (character == "{") { object_value(depth + 1); value = ""; kind = "object" }
1461
+ else if (character == "\"") { value = string_value(); kind = "string" }
1462
+ else if (substr(input, position, 4) == "true") { value = "true"; kind = "boolean"; position += 4 }
1463
+ else if (substr(input, position, 5) == "false") { value = "false"; kind = "boolean"; position += 5 }
1464
+ else if (substr(input, position, 4) == "null") { value = ""; kind = "null"; position += 4 }
1465
+ else if (match(substr(input, position), /^-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?/)) {
1466
+ value = substr(input, position, RLENGTH); position += RLENGTH; kind = "number"
1467
+ } else fail("unsupported value")
1468
+ if (depth == 1) {
1469
+ if (key in values) fail("duplicate field: " key)
1470
+ values[key] = value; kinds[key] = kind
829
1471
  }
830
- });
1472
+ whitespace(); character = substr(input, position++, 1)
1473
+ if (character == "}") return
1474
+ if (character != ",") fail("expected comma or closing brace")
1475
+ }
1476
+ fail("unterminated object")
1477
+ }
1478
+ { input = input $0 "\n" }
1479
+ END {
1480
+ position = 1
1481
+ if (substr(input, 1, 3) == sprintf("%c%c%c", 239, 187, 191)) position = 4
1482
+ whitespace()
1483
+ if (substr(input, position, 1) != "{") fail("expected object")
1484
+ object_value(1); whitespace()
1485
+ if (position <= length(input)) fail("trailing content")
1486
+ if (!(field in values) || kinds[field] == "null") { print fallback; exit }
1487
+ if (kinds[field] != expected) fail("wrong type for " field)
1488
+ if (values[field] ~ /[[:cntrl:]]/) fail("control character in " field)
1489
+ print values[field]
1490
+ }
1491
+ `;
831
1492
  }
832
1493
 
833
- // src/container/deploy-bundle.ts
1494
+ // src/container/deploy-scripts.ts
834
1495
  function shellArgument(value) {
835
1496
  return `'${value.replaceAll("'", `'"'"'`)}'`;
836
1497
  }
837
- async function createDeploymentBundle(options) {
838
- const { directory, artifactDir, artifact, projectName, environment, runtime, coreEnv } = options;
839
- const stem = basename(options.output).replace(/\.tar$/i, "");
840
- const versionSuffix = `-${artifact.appVersion}`;
841
- const output = resolve(dirname(options.output), `${stem.endsWith(versionSuffix) ? stem : `${stem}${versionSuffix}`}.tar`);
842
- const composeName = `docker-compose-${artifact.appVersion}.yml`;
843
- mkdirSync(dirname(output), { recursive: true, mode: 448 });
844
- const staging = mkdtempSync(join(directory, ".bundle-"));
845
- chmodSync(staging, 448);
846
- const temporaryTar = `${output}.partial`;
847
- const startName = `start-${environment}.sh`;
848
- try {
849
- mkdirSync(join(staging, "images"));
850
- for (const image of artifact.images)
851
- copyFileSync(join(artifactDir, image.file), join(staging, "images", image.file));
852
- for (const file of [composeName, ".env.runtime", ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : []])
853
- copyFileSync(join(directory, file), join(staging, file));
854
- const compose = parse(readFileSync(join(directory, composeName), "utf8"));
855
- const images = [...new Set(Object.values(compose.services).flatMap((service) => service.image ? [service.image] : []))];
856
- const start = `#!/bin/sh
1498
+ function createApplyScript(options) {
1499
+ const values = {
1500
+ ID: shellArgument(options.deploymentId),
1501
+ PROJECT: shellArgument(`hai-${options.projectName}`),
1502
+ ENV: shellArgument(options.environment),
1503
+ RUNTIME: shellArgument(options.runtime),
1504
+ PARTIAL: options.partial ? "true" : "false",
1505
+ SELECTED: shellArgument(options.selected.join(" ")),
1506
+ ROOT: shellArgument(options.directory),
1507
+ PORT: String(options.hostPort),
1508
+ HEALTH: options.healthPaths.map(shellArgument).join(" ")
1509
+ };
1510
+ return String.raw`#!/bin/sh
857
1511
  set -eu
858
- cd "$(dirname "$0")"
859
1512
  umask 077
860
- export HAI_DEPLOY_ENVIRONMENT=${shellArgument(environment)}
861
- export HAI_CORE_ENV=${shellArgument(coreEnv)}
862
- runtime=${shellArgument(runtime)}
863
- if [ -n "\${HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME:-}" ]; then runtime="$HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME"; fi
1513
+ source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
1514
+ destination=@@ROOT@@
1515
+ if [ "$#" -gt 0 ]; then destination=$1; fi
1516
+ root=$(mkdir -p -- "$destination" && CDPATH= cd -- "$destination" && pwd)
1517
+ id=@@ID@@
1518
+ project=@@PROJECT@@
1519
+ export HAI_DEPLOY_ENVIRONMENT=@@ENV@@
1520
+ runtime=@@RUNTIME@@
1521
+ partial=@@PARTIAL@@
1522
+ orphan_option=''
1523
+ if [ "$partial" = false ]; then orphan_option=--remove-orphans; fi
1524
+ recreate_option=--force-recreate
1525
+ if [ "$partial" = true ]; then recreate_option=--no-recreate; fi
1526
+ selected=@@SELECTED@@
1527
+ release="$root/releases/$id"
1528
+ case "$release/" in "$source_dir/"*) printf 'Extract the package outside the deployment directory before applying it.\n' >&2; exit 1;; esac
1529
+ stage=''
1530
+ previous=''
1531
+ restore=''
1532
+ changed=false
1533
+ locked=false
1534
+ interrupted=false
1535
+ started=$(date +%s)
1536
+ step_started=$started
1537
+ step() {
1538
+ if [ -n "$stage" ]; then
1539
+ duration=$(( $(date +%s) - step_started ))
1540
+ printf '[PASS] %s (%ss)\n' "$stage" "$duration"
1541
+ printf '{"stage":"%s","status":"passed","durationSeconds":%s}\n' "$stage" "$duration" >> "$root/steps-$id.jsonl"
1542
+ fi
1543
+ stage="$1"
1544
+ step_started=$(date +%s)
1545
+ printf '\n[%s] %s\n' "$stage" "$2"
1546
+ }
1547
+ compose() {
1548
+ arguments_file="$release/compose-files"
1549
+ if [ -n "$restore" ]; then arguments_file="$root/releases/$restore/compose-files"; fi
1550
+ # -f 按顺序合并,由 Compose 保留未选服务、卷和环境文件,不自行拼装配置。
1551
+ command_args="$release/.compose-command"
1552
+ # 文件列表先反向读取,再向参数头部加入,保留原始合并顺序。
1553
+ awk '{lines[NR]=$0} END {for(i=NR;i>0;i--) print lines[i]}' "$arguments_file" > "$command_args"
1554
+ while IFS= read -r file; do set -- -f "$file" "$@"; done < "$command_args"
1555
+ "$runtime" compose -p "$project" "$@"
1556
+ }
1557
+ remove_selected() {
1558
+ test "$partial" = true || return 0
1559
+ # 直接替换选中容器,避免 Compose Provider 连带重建依赖它的未选服务;不删除卷。
1560
+ for service in "$@"; do
1561
+ containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service") || return 1
1562
+ for container in $containers; do
1563
+ "$runtime" rm -f "$container" || return 1
1564
+ done
1565
+ done
1566
+ }
1567
+ finish() {
1568
+ code=$?
1569
+ trap - EXIT HUP INT TERM
1570
+ if [ "$code" -ne 0 ]; then
1571
+ printf '[FAIL] %s (exit %s)\n' "$stage" "$code" >&2
1572
+ if [ "$changed" = true ]; then
1573
+ diagnostics="$root/diagnostics-$id.log"
1574
+ : > "$diagnostics"
1575
+ for service in $selected; do
1576
+ containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service" || true)
1577
+ for container in $containers; do
1578
+ "$runtime" inspect --format '{{.Name}} {{.State.Status}} exit={{.State.ExitCode}}' "$container" >> "$diagnostics" 2>&1 || true
1579
+ "$runtime" logs --tail 80 "$container" >> "$diagnostics" 2>&1 || true
1580
+ done
1581
+ done
1582
+ printf 'Startup diagnostics saved to %s (private server file).\n' "$diagnostics" >&2
1583
+ fi
1584
+ if [ -f "$release/rollback-images" ]; then
1585
+ while IFS=' ' read -r image reference; do
1586
+ test -n "$reference" || continue
1587
+ "$runtime" tag "$image" "$reference" || printf '[FAIL] Cannot restore image %s\n' "$reference" >&2
1588
+ done < "$release/rollback-images"
1589
+ fi
1590
+ if [ "$changed" = true ] && [ -n "$previous" ]; then
1591
+ printf '[RECOVER] Restoring previous selected services\n' >&2
1592
+ recover_selected=$selected
1593
+ if [ "$partial" = false ]; then recover_selected=$(grep -v -- '-init$' "$root/releases/$previous/services"); fi
1594
+ restore="$previous"
1595
+ if remove_selected $recover_selected && compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $recover_selected; then
1596
+ case " $selected " in *' app '*)
1597
+ if grep -qx web "$root/releases/$previous/services"; then compose exec -T web nginx -s reload || true; fi
1598
+ ;;
1599
+ esac
1600
+ printf '[RECOVER] Previous containers restored; verify application health\n' >&2
1601
+ else
1602
+ printf '[FAIL] Recovery failed. Use the previous release and inspect container logs.\n' >&2
1603
+ fi
1604
+ elif [ "$changed" = true ]; then
1605
+ compose stop $selected || true
1606
+ printf '[RECOVER] Failed first installation stopped; data volumes retained\n' >&2
1607
+ fi
1608
+ fi
1609
+ if [ "$locked" = true ]; then
1610
+ elapsed=$(( $(date +%s) - started ))
1611
+ outcome=passed
1612
+ if [ "$code" -ne 0 ]; then outcome=failed; fi
1613
+ printf '{"stage":"%s","status":"%s","durationSeconds":%s}\n' "$stage" "$outcome" "$(( $(date +%s) - step_started ))" >> "$root/steps-$id.jsonl"
1614
+ # 先完整写入再同目录原子发布,SSH 重连核对不能读取到半份终态结果。
1615
+ printf '{"deploymentId":"%s","stage":"%s","exitCode":%s,"durationSeconds":%s}\n' "$id" "$stage" "$code" "$elapsed" > "$root/result-$id.json.next"
1616
+ mv "$root/result-$id.json.next" "$root/result-$id.json"
1617
+ # 前台命令及同步恢复已结束;后台服务可继续运行,但不能使成功终态永久占锁。
1618
+ # 中断时仍可能有活跃子命令,必须由全部描述符持有者退出后释放。
1619
+ if [ "$interrupted" = false ]; then
1620
+ flock -u 9
1621
+ exec 9>&-
1622
+ fi
1623
+ fi
1624
+ exit "$code"
1625
+ }
1626
+ trap finish EXIT
1627
+ trap 'interrupted=true; exit 130' HUP INT TERM
1628
+ step preflight 'Checking deployment tools and package'
1629
+ for tool in sha256sum tar awk curl flock; do command -v "$tool" >/dev/null || { printf 'Required command missing: %s\n' "$tool" >&2; exit 1; }; done
864
1630
  if [ "$runtime" = auto ]; then
865
1631
  if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
866
1632
  fi
867
- case "$runtime" in docker|podman) ;; *) echo 'HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME must be docker or podman' >&2; exit 1;; esac
1633
+ case "$runtime" in docker|podman) ;; *) printf 'Runtime must be docker or podman\n' >&2; exit 1;; esac
868
1634
  "$runtime" version >/dev/null
869
1635
  "$runtime" compose version >/dev/null
1636
+ "$runtime" compose up --help | grep -q never || { printf 'Compose is too old. Install Docker Compose v2 or podman-compose 1.6.0 or newer (supports --pull never).\n' >&2; exit 1; }
1637
+ # 内核锁绑定同一文件描述符;绝不删除/替换锁文件,避免新 inode 绕过活跃部署。
1638
+ # 子命令继承 FD 9:即使父 shell 被强杀,仍执行的命令继续持锁;全部退出或重启后自动释放。
1639
+ exec 9>"$root/.deploy.lock"
1640
+ if flock -n 9; then
1641
+ :
1642
+ else
1643
+ lock_code=$?
1644
+ if [ "$lock_code" -eq 1 ]; then
1645
+ printf 'Another deployment is running. Wait for its commands to finish; do not remove %s/.deploy.lock.\n' "$root" >&2
1646
+ else
1647
+ printf 'Cannot acquire deployment lock (exit %s). Check filesystem flock support and permissions.\n' "$lock_code" >&2
1648
+ fi
1649
+ exit "$lock_code"
1650
+ fi
1651
+ locked=true
1652
+ cd "$source_dir"
870
1653
  sha256sum -c SHA256SUMS
871
- chmod 600 .env.runtime
872
- ${artifact.images.map((image) => `"$runtime" load -i ${shellArgument(`images/${image.file}`)}`).join("\n")}
873
- # \u79BB\u7EBF\u670D\u52A1\u5668\u5E94\u5148\u5BFC\u5165\u5168\u90E8\u4E2D\u95F4\u4EF6\u955C\u50CF\uFF1B\u7981\u6B62\u9759\u9ED8\u5C1D\u8BD5\u8054\u7F51\u3002
874
- for image in ${images.map(shellArgument).join(" ")}; do
875
- "$runtime" image inspect "$image" >/dev/null 2>&1 || { echo "Missing image: $image. Load the matching image archive before retrying." >&2; exit 1; }
1654
+ if [ -f "$root/current" ]; then
1655
+ previous=$(cat "$root/current")
1656
+ case "$previous" in ''|*[!a-zA-Z0-9._-]*) printf 'Invalid deployment state\n' >&2; exit 1;; esac
1657
+ test -f "$root/releases/$previous/compose-files"
1658
+ fi
1659
+ if [ "$partial" = true ]; then
1660
+ test -n "$previous" || { printf 'Partial deployment requires a successful full installation. Deploy a complete bundle first.\n' >&2; exit 1; }
1661
+ cmp -s layout "$root/releases/$previous/layout" || { printf 'Ports, API routes or application layout changed. Create a full deployment bundle.\n' >&2; exit 1; }
1662
+ fi
1663
+ test ! -e "$release" || { printf 'This deployment ID was already used. Create a new deployment bundle.\n' >&2; exit 1; }
1664
+ mkdir -p "$release"
1665
+ cp -R "$source_dir/." "$release/"
1666
+ # Nginx 以非 root 用户运行;只开放不含凭据的代理配置读取权限。
1667
+ if [ -f "$release/web.conf" ]; then chmod 644 "$release/web.conf"; fi
1668
+ : > "$release/compose-files"
1669
+ if [ "$partial" = true ]; then
1670
+ cp "$root/releases/$previous/compose-files" "$release/compose-files"
1671
+ cp "$root/releases/$previous/middleware.env" "$release/middleware.env"
1672
+ for service in $selected; do
1673
+ grep -qx "$service" "$root/releases/$previous/services" || { printf 'Service %s has no installed baseline. Use a full deployment.\n' "$service" >&2; exit 1; }
1674
+ done
1675
+ fi
1676
+ printf '%s\n' "$release/compose-entry.json" >> "$release/compose-files"
1677
+ compose config >/dev/null
1678
+ compose config --services > "$release/services"
1679
+ : > "$release/rollback-images"
1680
+ if [ -n "$previous" ]; then
1681
+ for service in $selected; do
1682
+ containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service")
1683
+ for container in $containers; do
1684
+ "$runtime" inspect --format '{{.Image}} {{.Config.Image}}' "$container" >> "$release/rollback-images"
1685
+ done
1686
+ done
1687
+ fi
1688
+ step images 'Loading packaged images (no registry access)'
1689
+ while IFS=' ' read -r archive reference; do
1690
+ test -n "$archive" || continue
1691
+ "$runtime" load -i "$release/images/$archive"
1692
+ "$runtime" image inspect "$reference" >/dev/null
1693
+ architecture=$("$runtime" image inspect --format '{{.Architecture}}' "$reference")
1694
+ host_arch=$(uname -m)
1695
+ case "$host_arch:$architecture" in x86_64:amd64|aarch64:arm64) ;; *) printf 'Image architecture %s does not match server %s\n' "$architecture" "$host_arch" >&2; exit 1;; esac
1696
+ done < "$release/image-list"
1697
+ step dependencies 'Checking dependencies of selected services'
1698
+ if [ "$partial" = true ]; then cat "$release/services" >> "$release/dependencies"; fi
1699
+ while IFS= read -r dependency; do
1700
+ test -n "$dependency" || continue
1701
+ case "$dependency" in *-init) continue;; esac
1702
+ case " $selected " in *" $dependency "*) continue;; esac
1703
+ running=$("$runtime" ps -q --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$dependency")
1704
+ test -n "$running" || { printf 'Dependency %s is not running. Include it in a full deployment.\n' "$dependency" >&2; exit 1; }
1705
+ for container in $running; do
1706
+ test "$("$runtime" inspect --format '{{.State.Running}}' "$container")" = true || { printf 'Dependency %s is stopped. Start it before retrying.\n' "$dependency" >&2; exit 1; }
1707
+ done
1708
+ done < "$release/dependencies"
1709
+ step start 'Updating selected services'
1710
+ changed=true
1711
+ remove_selected $selected
1712
+ if [ -f "$release/initialize-storage" ]; then
1713
+ remove_selected storage-init
1714
+ compose up -d --no-deps --no-build --pull never storage
1715
+ compose up --no-deps --no-build --pull never "$recreate_option" --exit-code-from storage-init storage-init
1716
+ fi
1717
+ compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $selected
1718
+ case " $selected " in
1719
+ *' app '*)
1720
+ case " $selected " in *' web '*) ;; *)
1721
+ if grep -qx web "$release/services"; then compose exec -T web nginx -s reload; fi
1722
+ esac
1723
+ ;;
1724
+ esac
1725
+ step health 'Checking HTTP access'
1726
+ for path in @@HEALTH@@; do
1727
+ ready=false
1728
+ attempt=0
1729
+ while [ "$attempt" -lt 40 ]; do
1730
+ if curl --noproxy '*' -fsS --max-time 5 "http://127.0.0.1:@@PORT@@$path" >/dev/null 2>&1; then ready=true; break; fi
1731
+ attempt=$((attempt + 1))
1732
+ sleep 3
1733
+ done
1734
+ test "$ready" = true || { printf 'HTTP check failed: %s. Inspect compose logs for selected services.\n' "$path" >&2; exit 1; }
876
1735
  done
877
- exec "$runtime" compose -f ${shellArgument(composeName)} --env-file .env.runtime -p ${shellArgument(`hai-${projectName}`)} up -d
1736
+ step complete 'Deployment passed all checks'
1737
+ printf '%s\n' "$id" > "$root/current.next"
1738
+ mv "$root/current.next" "$root/current"
1739
+ printf '[PASS] %s\n' "$id"
1740
+ `.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]);
1741
+ }
1742
+ function createUploadScripts() {
1743
+ const sh = String.raw`#!/bin/sh
1744
+ set -eu
1745
+ source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
1746
+ cd "$source_dir/.."
1747
+ command -v awk >/dev/null || { printf 'Install POSIX awk to read deploy-target.json.\n' >&2; exit 1; }
1748
+ read_target() {
1749
+ LC_ALL=C awk -v field="$1" -v expected="$2" -v fallback="$3" -f "$source_dir/read-target.awk" "$source_dir/deploy-target.json"
1750
+ }
1751
+ host=$(read_target host string SERVER_HOST)
1752
+ user=$(read_target username string deploy)
1753
+ port=$(read_target port number 22)
1754
+ remote=$(read_target directory string '')
1755
+ bundle=$(read_target bundle string '')
1756
+ identity=$(read_target identityFile string '')
1757
+ known_hosts=$(read_target knownHostsFile string '')
1758
+ strict=$(read_target strictHostKeyChecking boolean true)
1759
+ timeout=$(read_target connectTimeoutSeconds number 10)
1760
+ if [ "$#" -ge 1 ]; then host=$1; fi
1761
+ if [ "$#" -ge 2 ]; then port=$2; fi
1762
+ if [ "$#" -ge 3 ]; then user=$3; fi
1763
+ if value=$(printenv HAI_DEPLOY_IDENTITY_FILE) && [ -n "$value" ]; then identity=$value; fi
1764
+ if value=$(printenv HAI_DEPLOY_KNOWN_HOSTS_FILE) && [ -n "$value" ]; then known_hosts=$value; fi
1765
+ invalid_target() { printf 'Invalid deployment target. Edit deploy-target.json before running this script.\n' >&2; exit 1; }
1766
+ test "$host" != SERVER_HOST || { printf 'Specify the server: sh deploy.sh SERVER [PORT] [USER].\n' >&2; exit 1; }
1767
+ case "$host" in ''|*[!a-zA-Z0-9.:-]*|[!a-zA-Z0-9]*) invalid_target;; esac
1768
+ case "$user" in ''|*[!a-zA-Z0-9_-]*|[!a-zA-Z_]*) invalid_target;; esac
1769
+ case "$port" in ''|*[!0-9]*|???????????*) invalid_target;; esac
1770
+ test "$port" -ge 1 && test "$port" -le 65535 || invalid_target
1771
+ case "$timeout" in ''|*[!0-9]*|???????????*) invalid_target;; esac
1772
+ test "$timeout" -ge 1 && test "$timeout" -le 2147483647 || invalid_target
1773
+ case "$remote" in /*) ;; *) invalid_target;; esac
1774
+ case "$remote" in *[!a-zA-Z0-9/_.\ -]*|*/../*|*/..|/..) invalid_target;; esac
1775
+ case "$bundle" in *.tar) ;; *) invalid_target;; esac
1776
+ case "$bundle" in ''|*[!a-zA-Z0-9_.+-]*) invalid_target;; esac
1777
+ case "$strict" in true) strict=accept-new;; false) strict=no;; *) invalid_target;; esac
1778
+ test -f "./$bundle" || { printf 'Bundle missing: %s\n' "$bundle" >&2; exit 1; }
1779
+ for tool in ssh scp; do command -v "$tool" >/dev/null || { printf 'Install OpenSSH (%s).\n' "$tool" >&2; exit 1; }; done
1780
+ set -- -o BatchMode=yes -o "StrictHostKeyChecking=$strict" -o "ConnectTimeout=$timeout"
1781
+ if [ -n "$identity" ]; then set -- "$@" -i "$identity" -o IdentitiesOnly=yes; fi
1782
+ if [ -n "$known_hosts" ]; then
1783
+ known_hosts=$(printf '%s' "$known_hosts" | sed 's/[\\"]/\\&/g')
1784
+ set -- "$@" -o "UserKnownHostsFile=\"$known_hosts\""
1785
+ fi
1786
+ printf '[1/3] Connecting to %s\n' "$host"
1787
+ ssh "$@" -p "$port" "$user@$host" "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"
1788
+ printf '[2/3] Uploading %s\n' "$bundle"
1789
+ scp_host=$host
1790
+ case "$host" in *:*) scp_host="[$host]";; esac
1791
+ scp "$@" -P "$port" "./$bundle" "$user@$scp_host:$remote/incoming/$bundle"
1792
+ printf '[3/3] Applying package and checking HTTP access\n'
1793
+ if ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"; then
1794
+ :
1795
+ else
1796
+ code=$?
1797
+ printf '[UNKNOWN] Remote execution may continue. Inspect %s/result-<deploymentId>.json (ID in manifest.json), current and containers before retrying.\n' "$remote" >&2
1798
+ exit "$code"
1799
+ fi
1800
+ `;
1801
+ const ps1 = String.raw`param([string]$IdentityFile = $env:HAI_DEPLOY_IDENTITY_FILE)
1802
+ $ErrorActionPreference = 'Stop'
1803
+ Set-Location -LiteralPath (Split-Path -Parent $PSScriptRoot)
1804
+ $target = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'deploy-target.json') -Raw -Encoding UTF8 | ConvertFrom-Json
1805
+ if ($target.host -eq 'SERVER_HOST') { throw 'Set the server host in deploy-target.json.' }
1806
+ if ($target.host -isnot [string] -or $target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -isnot [string] -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -isnot [string] -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]*$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -isnot [string] -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar$') { throw 'Invalid deployment target.' }
1807
+ $port = if ($null -eq $target.port) { 22 } else { $target.port }
1808
+ $timeout = if ($null -eq $target.connectTimeoutSeconds) { 10 } else { $target.connectTimeoutSeconds }
1809
+ if ($port -is [string] -or [string]$port -notmatch '^[0-9]+$' -or $port -lt 1 -or $port -gt 65535 -or $timeout -is [string] -or [string]$timeout -notmatch '^[0-9]+$' -or $timeout -lt 1 -or $timeout -gt 2147483647) { throw 'Invalid deployment port or timeout.' }
1810
+ if ($null -ne $target.strictHostKeyChecking -and $target.strictHostKeyChecking -isnot [bool]) { throw 'Invalid host key policy.' }
1811
+ if (($null -ne $target.identityFile -and $target.identityFile -isnot [string]) -or ($null -ne $target.knownHostsFile -and $target.knownHostsFile -isnot [string])) { throw 'Invalid SSH file path.' }
1812
+ $strict = if ($target.strictHostKeyChecking -eq $false) { 'no' } else { 'accept-new' }
1813
+ $options = @('-o', 'BatchMode=yes', '-o', "StrictHostKeyChecking=$strict", '-o', "ConnectTimeout=$timeout")
1814
+ if (-not $IdentityFile) { $IdentityFile = $target.identityFile }
1815
+ if ($IdentityFile) { $options += @('-i', $IdentityFile, '-o', 'IdentitiesOnly=yes') }
1816
+ $knownHosts = if ($env:HAI_DEPLOY_KNOWN_HOSTS_FILE) { $env:HAI_DEPLOY_KNOWN_HOSTS_FILE } else { $target.knownHostsFile }
1817
+ if ($knownHosts) {
1818
+ $knownHosts = $knownHosts.Replace('\', '\\').Replace('"', '\"')
1819
+ $options += @('-o', ('UserKnownHostsFile="' + $knownHosts + '"'))
1820
+ }
1821
+ $destination = "$($target.username)@$($target.host)"
1822
+ $scpDestination = if ($target.host.Contains(':')) { "$($target.username)@[$($target.host)]" } else { $destination }
1823
+ $remote = $target.directory
1824
+ $bundle = $target.bundle
1825
+ if (-not (Test-Path -LiteralPath "./$bundle" -PathType Leaf)) { throw "Bundle missing: $bundle" }
1826
+ function ConvertTo-NativeArgument([string]$Value) {
1827
+ if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') { return $Value }
1828
+ $escaped = [regex]::Replace($Value, '(\\*)"', '$1$1\"')
1829
+ $escaped = [regex]::Replace($escaped, '(\\+)$', '$1$1')
1830
+ return '"' + $escaped + '"'
1831
+ }
1832
+ function Invoke-Step([string]$Name, [string]$Command, [string[]]$Arguments) {
1833
+ Write-Host "[$Name]"
1834
+ $nativeCommand = Get-Command $Command -ErrorAction Stop
1835
+ if ($PSVersionTable.PSVersion.Major -lt 6 -and $nativeCommand.CommandType -eq 'Application') {
1836
+ $start = New-Object System.Diagnostics.ProcessStartInfo
1837
+ $start.FileName = $nativeCommand.Source
1838
+ $start.WorkingDirectory = (Get-Location).ProviderPath
1839
+ $start.UseShellExecute = $false
1840
+ $start.CreateNoWindow = $true
1841
+ $start.Arguments = ($Arguments | ForEach-Object { ConvertTo-NativeArgument $_ }) -join ' '
1842
+ $child = [System.Diagnostics.Process]::Start($start)
1843
+ try { $child.WaitForExit(); $code = $child.ExitCode } finally { $child.Dispose() }
1844
+ } else {
1845
+ & $Command @Arguments
1846
+ $code = $LASTEXITCODE
1847
+ }
1848
+ $global:LASTEXITCODE = $code
1849
+ if ($code -ne 0) { throw "$Name failed (exit $code)." }
1850
+ }
1851
+ $applying = $false
1852
+ try {
1853
+ foreach ($tool in @('ssh', 'scp')) { Get-Command $tool -ErrorAction Stop | Out-Null }
1854
+ Invoke-Step '1/3 Connect' 'ssh' ($options + @('-p', [string]$port, $destination, "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"))
1855
+ Invoke-Step '2/3 Upload' 'scp' ($options + @('-P', [string]$port, "./$bundle", "$($scpDestination):$remote/incoming/$bundle"))
1856
+ $applying = $true
1857
+ Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"))
1858
+ } catch {
1859
+ if ($applying) { [Console]::Error.WriteLine("[UNKNOWN] Remote execution may continue. Inspect $remote/result-<deploymentId>.json (ID in manifest.json), current and containers before retrying.") }
1860
+ Write-Error $_
1861
+ exit 1
1862
+ }
878
1863
  `;
879
- writeFileSync(join(directory, startName), start, { mode: 448 });
880
- writeFileSync(join(staging, startName), start, { mode: 448 });
881
- const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output), start: startName, images: images.join("\n"), compose: composeName } });
882
- const metadata = deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: composeName } });
883
- writeFileSync(join(staging, "README.txt"), `${metadata}
1864
+ return { sh, ps1, reader: createTargetReader() };
1865
+ }
1866
+
1867
+ // src/container/deploy-bundle.ts
1868
+ async function createDeploymentBundle(options) {
1869
+ const { directory, artifactDir, artifact, projectName, environment, config, selected } = options;
1870
+ const stem = basename(options.output).replace(/\.tar$/i, "");
1871
+ const suffix = `-${artifact.appVersion}`;
1872
+ const output = resolve(dirname(options.output), `${stem.endsWith(suffix) ? stem : `${stem}${suffix}`}.tar`);
1873
+ if (!/^[\w.+-]+\.tar$/.test(basename(output)))
1874
+ throw new Error(deployM("deploy_bundleNameInvalid"));
1875
+ const staging = mkdtempSync(join(directory, ".bundle-"));
1876
+ const temporaryTar = `${output}.partial`;
1877
+ mkdirSync(dirname(output), { recursive: true, mode: 448 });
1878
+ chmodSync(staging, 448);
1879
+ const files = [];
1880
+ const put = (file, content) => {
1881
+ mkdirSync(dirname(join(staging, file)), { recursive: true });
1882
+ writeFileSync(join(staging, file), content, { mode: 384 });
1883
+ files.push(file);
1884
+ };
1885
+ try {
1886
+ mkdirSync(join(staging, "images"));
1887
+ const compose = parse(readFileSync(join(directory, `docker-compose-${artifact.appVersion}.yml`), "utf8"));
1888
+ const dependencies = /* @__PURE__ */ new Set();
1889
+ const references = /* @__PURE__ */ new Set();
1890
+ for (const name of selected) {
1891
+ const service = compose.services[name];
1892
+ if (!service)
1893
+ throw new Error(deployM("deploy_selectionInvalid", { params: { service: name } }));
1894
+ if (typeof service.image === "string")
1895
+ references.add(service.image);
1896
+ if (service.depends_on) {
1897
+ for (const dependency of Array.isArray(service.depends_on) ? service.depends_on : Object.keys(service.depends_on))
1898
+ dependencies.add(dependency);
1899
+ }
1900
+ if (service.env_file)
1901
+ service.env_file = [`../${options.deploymentId}/.env.runtime`, `../${options.deploymentId}/middleware.env`];
1902
+ if (Array.isArray(service.volumes))
1903
+ service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace("./web.conf:", `../${options.deploymentId}/web.conf:`) : mount);
1904
+ }
1905
+ put("compose.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, compose.services[name]])) }));
1906
+ put("compose-entry.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, { extends: { file: `../${options.deploymentId}/compose.json`, service: name } }])) }));
1907
+ let runtime;
1908
+ const imageList = [];
1909
+ for (const reference of references) {
1910
+ if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference))
1911
+ throw new Error(deployM("deploy_imageVersionRequired", { params: { image: reference } }));
1912
+ const built = artifact.images.find((image) => image.imageRef === reference);
1913
+ const file = built?.file ?? `middleware-${imageList.length + 1}.tar`;
1914
+ if (built) {
1915
+ copyFileSync(join(artifactDir, file), join(staging, "images", file));
1916
+ } else {
1917
+ if (!runtime) {
1918
+ const candidates = options.runtime === "auto" ? ["podman", "docker"] : [options.runtime];
1919
+ for (const candidate of candidates) {
1920
+ if ((await runCommand(candidate, ["version"])).success) {
1921
+ runtime = candidate;
1922
+ break;
1923
+ }
1924
+ }
1925
+ if (!runtime)
1926
+ throw new Error(deployM("deploy_bundleRuntimeRequired"));
1927
+ }
1928
+ if (!(await runCommand(runtime, ["image", "inspect", reference])).success) {
1929
+ const pulled = await runCommand(runtime, ["pull", reference], { timeoutMs: 6e5 });
1930
+ if (!pulled.success)
1931
+ throw new Error(pulled.error.message);
1932
+ }
1933
+ const saved = await runCommand(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", join(staging, "images", file), reference], { timeoutMs: 6e5 });
1934
+ if (!saved.success)
1935
+ throw new Error(saved.error.message);
1936
+ }
1937
+ files.push(`images/${file}`);
1938
+ imageList.push(`${file} ${reference}`);
1939
+ }
1940
+ put(".env.runtime", readFileSync(join(directory, ".env.runtime"), "utf8"));
1941
+ put("middleware.env", options.middlewareEnv);
1942
+ if (selected.includes("web"))
1943
+ put("web.conf", readFileSync(join(directory, "web.conf"), "utf8"));
1944
+ put("manifest.json", `${JSON.stringify({ ...artifact, images: artifact.images.filter((image) => references.has(image.imageRef)), deploymentId: options.deploymentId, selectedServices: selected, partial: options.partial }, null, 2)}
1945
+ `);
1946
+ put("layout", `${config.expose.hostPort}:${options.containerPort}:${options.hasWeb}:${options.hasWeb ? await artifactChecksum(join(directory, "web.conf")) : ""}
1947
+ `);
1948
+ put("image-list", `${imageList.join("\n")}
1949
+ `);
1950
+ put("dependencies", `${[...dependencies].join("\n")}
1951
+ `);
1952
+ if (selected.includes("storage-init"))
1953
+ put("initialize-storage", "");
1954
+ put("apply.sh", createApplyScript({ deploymentId: options.deploymentId, projectName, environment, directory: `${config.remote.baseDir}/${projectName}`, runtime: config.remote.runtime, partial: options.partial, selected: selected.filter((name) => name !== "storage-init"), hostPort: config.expose.hostPort, healthPaths: options.healthPaths }));
1955
+ const instructionParams = { file: basename(output), start: "apply.sh", images: [...references].join("\n"), compose: "compose.json" };
1956
+ const instructions = deployM("deploy_bundleInstructions", { params: instructionParams });
1957
+ put("README.txt", `${deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: "compose.json" } })}
884
1958
  ${instructions}
885
1959
  `);
886
- copyFileSync(join(artifactDir, "manifest.json"), join(staging, "manifest.json"));
887
1960
  const checksums = [];
888
- for (const file of [composeName, "manifest.json", ".env.runtime", startName, ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : [], ...artifact.images.map((image) => `images/${image.file}`)])
1961
+ for (const file of files)
889
1962
  checksums.push(`${await artifactChecksum(join(staging, file))} ${file}`);
890
1963
  writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
891
1964
  `);
@@ -893,25 +1966,56 @@ ${instructions}
893
1966
  if (!tar.success)
894
1967
  throw new Error(tar.error.message);
895
1968
  chmodSync(temporaryTar, 384);
896
- const { renameSync } = await import('fs');
897
1969
  renameSync(temporaryTar, output);
898
- return { bundlePath: output, instructions, startName };
1970
+ const upload = createUploadScripts();
1971
+ const scripts = `${output.slice(0, -4)}-deploy`;
1972
+ mkdirSync(scripts, { recursive: true, mode: 448 });
1973
+ writeFileSync(join(scripts, "deploy.sh"), upload.sh, { mode: 448 });
1974
+ writeFileSync(join(scripts, "deploy.ps1"), upload.ps1);
1975
+ writeFileSync(join(scripts, "read-target.awk"), upload.reader);
1976
+ const target = {
1977
+ host: config.ssh?.host ?? "SERVER_HOST",
1978
+ port: config.ssh?.port ?? 22,
1979
+ username: config.ssh?.username ?? "deploy",
1980
+ identityFile: config.ssh?.identityFile ?? "",
1981
+ knownHostsFile: config.ssh?.knownHostsFile ?? "",
1982
+ strictHostKeyChecking: config.ssh?.strictHostKeyChecking ?? true,
1983
+ connectTimeoutSeconds: config.ssh?.connectTimeoutSeconds ?? 10,
1984
+ directory: `${config.remote.baseDir}/${projectName}`,
1985
+ bundle: basename(output)
1986
+ };
1987
+ const comments = {
1988
+ host: deployM("deploy_targetHost"),
1989
+ port: deployM("deploy_targetPort"),
1990
+ username: deployM("deploy_targetUsername"),
1991
+ identityFile: deployM("deploy_targetIdentityFile"),
1992
+ knownHostsFile: deployM("deploy_targetKnownHostsFile"),
1993
+ strictHostKeyChecking: deployM("deploy_targetStrictHostKeyChecking"),
1994
+ connectTimeoutSeconds: deployM("deploy_targetConnectTimeoutSeconds"),
1995
+ directory: deployM("deploy_targetDirectory"),
1996
+ bundle: deployM("deploy_targetBundle")
1997
+ };
1998
+ writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...target, _comments: comments }, null, 2)}
1999
+ `);
2000
+ writeFileSync(join(scripts, "README.txt"), instructions);
2001
+ return { bundlePath: output, instructions: `${deployM("deploy_bundleInstructions", { params: { ...instructionParams, file: output } })}
2002
+ ${scripts}`, startName: "apply.sh" };
899
2003
  } finally {
900
2004
  rmSync(staging, { recursive: true, force: true });
901
2005
  rmSync(temporaryTar, { force: true });
902
2006
  }
903
2007
  }
904
2008
  var IMAGES = {
905
- postgres: "postgres:16-alpine",
906
- mysql: "mysql:8.4",
907
- redis: "redis:7-alpine",
2009
+ postgres: "docker.io/library/postgres:16.15-alpine",
2010
+ mysql: "docker.io/library/mysql:8.4.11",
2011
+ redis: "docker.io/library/redis:7.4.11-alpine",
908
2012
  rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
909
2013
  minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
910
- qdrant: "qdrant/qdrant:v1.12.4",
911
- pgvector: "pgvector/pgvector:pg17",
912
- chroma: "chromadb/chroma:1.5.5",
2014
+ qdrant: "docker.io/qdrant/qdrant:v1.19.2",
2015
+ pgvector: "docker.io/pgvector/pgvector:0.8.0-pg17",
2016
+ chroma: "docker.io/chromadb/chroma:1.5.5",
913
2017
  ldap: "docker.io/osixia/openldap:1.5.0",
914
- smtp: "axllent/mailpit:v1.27.4"
2018
+ smtp: "docker.io/axllent/mailpit:v1.27.4"
915
2019
  };
916
2020
  function generatePassword() {
917
2021
  return randomBytes(24).toString("base64url");
@@ -920,8 +2024,12 @@ function resolveSidecarSecrets(appDir, projectName) {
920
2024
  const secretFile = join(appDir, ".hai", "deploy", "runtime", `${projectName}.secrets.json`);
921
2025
  if (existsSync(secretFile)) {
922
2026
  try {
923
- return JSON.parse(readFileSync(secretFile, "utf-8"));
2027
+ const saved = JSON.parse(readFileSync(secretFile, "utf-8"));
2028
+ if (!saved || !["dbPassword", "storagePassword", "ldapPassword"].every((key) => typeof saved[key] === "string" && saved[key].length > 0))
2029
+ throw new Error("Invalid credentials");
2030
+ return saved;
924
2031
  } catch {
2032
+ throw new Error(deployM("deploy_secretsInvalid", { params: { path: secretFile } }));
925
2033
  }
926
2034
  }
927
2035
  const secrets = {
@@ -1089,6 +2197,28 @@ function planChromaVecdb(plan, projectName, net) {
1089
2197
  plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
1090
2198
  }
1091
2199
  function planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine = "rustfs") {
2200
+ const serviceModes = { ...options?.serviceModes };
2201
+ for (const requirement of scan.middlewares ?? []) {
2202
+ if (options?.selectedServices && !options.selectedServices.includes(requirement.service))
2203
+ continue;
2204
+ const mode = serviceModes[requirement.envPrefix] ?? serviceModes[requirement.service];
2205
+ if (mode !== "keep")
2206
+ continue;
2207
+ const endpoint = requirement.config.url ?? requirement.config.endpoint;
2208
+ let host = requirement.config.host;
2209
+ if (typeof endpoint === "string") {
2210
+ try {
2211
+ host = new URL(endpoint).hostname;
2212
+ } catch {
2213
+ continue;
2214
+ }
2215
+ }
2216
+ if (host === requirement.service || ["sqlite", "local", "memory", "lancedb"].includes(requirement.backend)) {
2217
+ serviceModes[requirement.envPrefix] = "default";
2218
+ serviceModes[requirement.service] = "default";
2219
+ }
2220
+ }
2221
+ options = { ...options, serviceModes };
1092
2222
  const plan = {
1093
2223
  services: [],
1094
2224
  composeServices: {},
@@ -1215,23 +2345,6 @@ function generateComposeFile(options) {
1215
2345
  };
1216
2346
  if (Object.keys(sidecar.volumes).length > 0)
1217
2347
  compose.volumes = sidecar.volumes;
1218
- if (options.previousCompose && options.serviceModes) {
1219
- const previous = parse(options.previousCompose);
1220
- for (const [service, mode] of Object.entries(options.serviceModes)) {
1221
- if (mode !== "keep")
1222
- continue;
1223
- for (const name of [service, `${service}-init`]) {
1224
- if (previous.services?.[name]) {
1225
- services[name] = previous.services[name];
1226
- app.depends_on = [.../* @__PURE__ */ new Set([...app.depends_on ?? [], service])];
1227
- }
1228
- }
1229
- }
1230
- const previousMounts = previous.services?.app?.volumes;
1231
- if (Array.isArray(previousMounts) && Object.values(options.serviceModes).includes("keep"))
1232
- app.volumes = [.../* @__PURE__ */ new Set([...app.volumes ?? [], ...previousMounts])];
1233
- compose.volumes = { ...previous.volumes, ...sidecar.volumes };
1234
- }
1235
2348
  if (services["storage-init"]) {
1236
2349
  app.depends_on = {
1237
2350
  ...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
@@ -1249,6 +2362,7 @@ function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
1249
2362
  listen ${webPort};
1250
2363
  server_name _;
1251
2364
  root /usr/share/nginx/html;
2365
+ large_client_header_buffers 4 32k;
1252
2366
  location / {
1253
2367
  try_files $uri $uri/ /index.html;
1254
2368
  }
@@ -1266,11 +2380,15 @@ function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
1266
2380
  proxy_set_header Host $http_host;
1267
2381
  }
1268
2382
  location ${apiPrefix.replace(/\/$/, "")}/ {
2383
+ client_max_body_size 2129920;
1269
2384
  proxy_pass http://app:${port};
1270
2385
  proxy_http_version 1.1;
1271
2386
  proxy_set_header Host $http_host;
1272
2387
  proxy_set_header X-Forwarded-Proto $scheme;
2388
+ proxy_request_buffering off;
1273
2389
  proxy_buffering off;
2390
+ proxy_buffer_size 32k;
2391
+ proxy_buffers 8 32k;
1274
2392
  }
1275
2393
  }
1276
2394
  `;
@@ -1295,8 +2413,10 @@ function buildConnectOptions(config) {
1295
2413
  const args = ["-o", "BatchMode=yes"];
1296
2414
  args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
1297
2415
  args.push("-o", `StrictHostKeyChecking=${config.strictHostKeyChecking ? "accept-new" : "no"}`);
1298
- if (config.knownHostsFile)
1299
- args.push("-o", `UserKnownHostsFile=${config.knownHostsFile}`);
2416
+ if (config.knownHostsFile) {
2417
+ const path = config.knownHostsFile.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
2418
+ args.push("-o", `UserKnownHostsFile="${path}"`);
2419
+ }
1300
2420
  if (config.identityFile) {
1301
2421
  args.push("-i", config.identityFile);
1302
2422
  args.push("-o", "IdentitiesOnly=yes");
@@ -1318,25 +2438,28 @@ function buildScpUploadArgs(config, localPath, remotePath) {
1318
2438
  "-P",
1319
2439
  String(config.port),
1320
2440
  localPath,
1321
- `${config.username}@${config.host}:${remotePath}`
2441
+ `${config.username}@${config.host.includes(":") ? `[${config.host}]` : config.host}:${remotePath}`
1322
2442
  ];
1323
2443
  }
1324
2444
  function createSshExecutor(config) {
1325
2445
  const exec = async (remoteCommand, options) => {
1326
- return runCommand("ssh", buildSshExecArgs(config, remoteCommand), {
2446
+ const result = await runCommand("ssh", buildSshExecArgs(config, remoteCommand), {
1327
2447
  timeoutMs: options?.timeoutMs,
1328
- secrets: options?.secrets
2448
+ terminationTimeoutMs: options?.terminationTimeoutMs,
2449
+ maxCaptureChars: options?.maxCaptureChars,
2450
+ secrets: options?.secrets,
2451
+ onOutput: options?.onOutput
1329
2452
  });
2453
+ if (result.success)
2454
+ return result;
2455
+ const state = getDeployCommandState(result.error);
2456
+ return err({ ...result.error, suggestion: deployM("deploy_remoteUnknown"), ext: { ...result.error.ext, commandExecution: { ...state, reason: state?.reason ?? "exit", localTermination: state?.localTermination ?? "unknown", remoteExecution: "unknown" } } });
1330
2457
  };
1331
2458
  const upload = async (localPath, remotePath) => {
1332
2459
  const args = buildScpUploadArgs(config, localPath, remotePath);
1333
- const result = await runCommand("scp", args, {});
2460
+ const result = await runCommand("scp", args, { timeoutMs: 6e5 });
1334
2461
  if (!result.success) {
1335
- return err(
1336
- HaiDeployError.IMAGE_TRANSFER_FAILED,
1337
- deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }),
1338
- result.error
1339
- );
2462
+ return err({ ...HaiDeployError.IMAGE_TRANSFER_FAILED, message: deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }), ext: result.error.ext });
1340
2463
  }
1341
2464
  return ok(void 0);
1342
2465
  };
@@ -1344,11 +2467,7 @@ function createSshExecutor(config) {
1344
2467
  logger4.debug("SSH preflight", { host: config.host, port: config.port });
1345
2468
  const result = await exec("true", { timeoutMs: 2e4 });
1346
2469
  if (!result.success) {
1347
- return err(
1348
- HaiDeployError.REMOTE_CONNECT_FAILED,
1349
- deployM("deploy_remoteConnectFailed", { params: { host: config.host, error: result.error.message } }),
1350
- result.error
1351
- );
2470
+ return err({ ...HaiDeployError.REMOTE_CONNECT_FAILED, message: deployM("deploy_remoteConnectFailed", { params: { host: config.host, error: result.error.message } }), ext: result.error.ext });
1352
2471
  }
1353
2472
  return ok(void 0);
1354
2473
  };
@@ -1359,6 +2478,12 @@ function createSshExecutor(config) {
1359
2478
  var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
1360
2479
  var HEALTH_POLL_INTERVAL = 3e3;
1361
2480
  var HEALTH_MAX_ATTEMPTS = 40;
2481
+ var ServerResultSchema = z.object({
2482
+ deploymentId: z.string(),
2483
+ stage: z.enum(["preflight", "images", "dependencies", "start", "health", "complete"]),
2484
+ exitCode: z.number().int().min(0).max(255),
2485
+ durationSeconds: z.number().int().nonnegative()
2486
+ });
1362
2487
  function makeDeploymentId() {
1363
2488
  const now = /* @__PURE__ */ new Date();
1364
2489
  const pad = (n) => String(n).padStart(2, "0");
@@ -1406,7 +2531,7 @@ function createDockerSshProvider(config, container) {
1406
2531
  const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
1407
2532
  const deploymentId = makeDeploymentId();
1408
2533
  const hostPort = config.expose.hostPort;
1409
- const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
2534
+ const artifactDir = join(resolveProjectDirectory(appDir), ".hai", "deploy", options?.environment ?? "prod");
1410
2535
  mkdirSync(artifactDir, { recursive: true, mode: 448 });
1411
2536
  chmodSync(artifactDir, 448);
1412
2537
  const artifact = context.artifact;
@@ -1414,11 +2539,23 @@ function createDockerSshProvider(config, container) {
1414
2539
  const images = artifact.images;
1415
2540
  const web = images.find((image) => image.name === "web");
1416
2541
  const app = images.find((image) => image.name === "app");
1417
- const containerPort = app.containerPort ?? config.expose.containerPort;
1418
- const url = config.ssh ? `http://${config.ssh.host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
2542
+ if (options?.selectedServices) {
2543
+ const available = [...images.map((image) => image.name), ...scan.requiredServices];
2544
+ const invalid = options.selectedServices.find((name) => !available.includes(name));
2545
+ if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid)
2546
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: invalid ?? options.selectedServices.join(", ") } }));
2547
+ }
2548
+ const hasWeb = artifact.hasWeb || !!web;
2549
+ const partial = artifact.partial || options?.selectedServices !== void 0;
2550
+ const containerPort = app?.containerPort ?? artifact.appContainerPort ?? config.expose.containerPort;
2551
+ const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
2552
+ const host = config.ssh?.host;
2553
+ const url = host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
1419
2554
  const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
2555
+ if (sidecar.composeServices["storage-init"] && !app && options?.selectedServices?.includes("storage"))
2556
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageArtifactRequired"));
1420
2557
  let webProxy;
1421
- if (web) {
2558
+ if (hasWeb) {
1422
2559
  const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
1423
2560
  if (!server.success)
1424
2561
  return server;
@@ -1427,12 +2564,10 @@ function createDockerSshProvider(config, container) {
1427
2564
  const apiPrefix = http?.apiPrefix ?? "/api/v1";
1428
2565
  if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
1429
2566
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
1430
- webProxy = generateWebProxyConfig(containerPort, apiPrefix, web.containerPort ?? 8080);
2567
+ webProxy = generateWebProxyConfig(containerPort, apiPrefix, webContainerPort);
1431
2568
  env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config.ssh ? url : cors?.origin ?? "*" } });
1432
2569
  }
1433
2570
  const envPath = join(artifactDir, ".env.runtime");
1434
- const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
1435
- Object.assign(env, { ...previousEnv, ...env });
1436
2571
  writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
1437
2572
  if (options?.environment) {
1438
2573
  const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
@@ -1443,36 +2578,46 @@ function createDockerSshProvider(config, container) {
1443
2578
  return saved;
1444
2579
  }
1445
2580
  const composeName = `docker-compose-${artifact.appVersion}.yml`;
1446
- const latestPath = join(artifactDir, "latest");
1447
- const previousName = existsSync(latestPath) ? readFileSync(latestPath, "utf8").trim() : "";
1448
- const previousPath = /^[\w.+-]+\.yml$/.test(previousName) ? join(artifactDir, previousName) : void 0;
1449
2581
  const composeContent = generateComposeFile({
1450
2582
  projectName,
1451
2583
  appVersion: artifact.appVersion,
1452
- imageRef: app.imageRef,
2584
+ imageRef: app?.imageRef ?? "localhost/hai-unselected:0",
1453
2585
  hostPort,
1454
2586
  containerPort,
1455
2587
  sidecar,
1456
- web: web ? { imageRef: web.imageRef, containerPort: web.containerPort ?? 8080 } : void 0,
1457
- serviceModes: options?.serviceModes,
1458
- previousCompose: previousPath && existsSync(previousPath) ? readFileSync(previousPath, "utf8") : void 0
2588
+ web: hasWeb ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0
1459
2589
  });
1460
2590
  const composePath = join(artifactDir, composeName);
1461
2591
  writeFileSync(composePath, composeContent);
1462
2592
  if (webProxy)
1463
2593
  writeFileSync(join(artifactDir, "web.conf"), webProxy);
2594
+ const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [...images.map((image) => image.name), ...Object.keys(sidecar.composeServices)];
2595
+ if (!selected.length || selected.some((name) => !/^[a-z][a-z0-9-]*$/.test(name)) || new Set(selected).size !== selected.length)
2596
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: selected.join(", ") } }));
2597
+ for (const name of selected) {
2598
+ if (["app", "web"].includes(name) && !images.some((image) => image.name === name))
2599
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: name } }));
2600
+ }
2601
+ if (selected.includes("storage") && sidecar.composeServices["storage-init"] && !selected.includes("storage-init"))
2602
+ selected.push("storage-init");
2603
+ options?.onProgress?.("packaging");
1464
2604
  const bundle = await createDeploymentBundle({
1465
2605
  directory: artifactDir,
1466
2606
  artifactDir: resolvedBuildDir,
1467
2607
  artifact,
1468
2608
  projectName,
1469
2609
  environment: options?.environment ?? "prod",
1470
- runtime: config.remote.runtime,
1471
- coreEnv: env.HAI_CORE_ENV ?? "production",
2610
+ deploymentId,
2611
+ config,
2612
+ runtime: options?.runtime ?? "auto",
2613
+ selected,
2614
+ partial,
2615
+ hasWeb,
2616
+ containerPort,
2617
+ middlewareEnv: serializeEnvFile(sidecar.env),
2618
+ healthPaths: hasWeb ? ["/health", "/_hai/web-health", "/"] : [scan.isSvelteKit ? "/" : "/health"],
1472
2619
  output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
1473
2620
  });
1474
- writeFileSync(latestPath, `${composeName}
1475
- `);
1476
2621
  if (options?.bundleOnly)
1477
2622
  return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
1478
2623
  if (!ssh)
@@ -1484,22 +2629,56 @@ function createDockerSshProvider(config, container) {
1484
2629
  if (!remoteRuntime.success)
1485
2630
  return remoteRuntime;
1486
2631
  const remoteDir = `${baseDir}/${projectName}`;
1487
- const remoteTar = `${remoteDir}/${basename(bundle.bundlePath)}`;
1488
- const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
2632
+ const remoteTar = `${remoteDir}/incoming/${basename(bundle.bundlePath)}`;
2633
+ const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(`${remoteDir}/incoming`)} && chmod 700 -- ${shellArgument(remoteDir)} ${shellArgument(`${remoteDir}/incoming`)}`);
1489
2634
  if (!prepared.success)
1490
2635
  return prepared;
1491
2636
  const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
1492
2637
  if (!uploaded.success)
1493
2638
  return uploaded;
2639
+ let pendingOutput = "";
1494
2640
  const started = await ssh.exec(
1495
- `cd ${shellArgument(remoteDir)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} && HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=${shellArgument(remoteRuntime.data)} sh ${shellArgument(bundle.startName)}`,
1496
- { timeoutMs: 3e5, secrets: sidecar.secrets }
2641
+ `mkdir -p ${shellArgument(`${remoteTar}.contents`)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} -C ${shellArgument(`${remoteTar}.contents`)} && sh ${shellArgument(`${remoteTar}.contents/${bundle.startName}`)} ${shellArgument(remoteDir)}`,
2642
+ { timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output) => {
2643
+ const parts = output.split("\n");
2644
+ for (let index = 0; index < parts.length; index++) {
2645
+ pendingOutput += parts[index].slice(0, 512 - pendingOutput.length);
2646
+ if (index === parts.length - 1)
2647
+ break;
2648
+ const line = pendingOutput;
2649
+ pendingOutput = "";
2650
+ const stage = /^\[(preflight|images|dependencies|start|health|complete)\]/.exec(line)?.[1];
2651
+ if (stage)
2652
+ options?.onProgress?.(stage);
2653
+ }
2654
+ } }
1497
2655
  );
1498
- if (!started.success)
1499
- return started;
1500
- await ssh.exec(`rm -f -- ${shellArgument(remoteTar)} ${images.map((image) => shellArgument(`${remoteDir}/images/${image.file}`)).join(" ")}`);
2656
+ if (!started.success) {
2657
+ const resultFile = `${remoteDir}/result-${deploymentId}.json`;
2658
+ const state = getDeployCommandState(started.error);
2659
+ const checked = await ssh.exec(`if [ -f ${shellArgument(resultFile)} ]; then cat -- ${shellArgument(resultFile)}; fi`, { timeoutMs: 2e4, maxCaptureChars: 8192 });
2660
+ let terminal;
2661
+ if (checked.success && !checked.data.stdoutTruncated) {
2662
+ try {
2663
+ const parsed = ServerResultSchema.safeParse(JSON.parse(checked.data.stdout));
2664
+ if (parsed.success && parsed.data.deploymentId === deploymentId)
2665
+ terminal = parsed.data;
2666
+ } catch {
2667
+ }
2668
+ }
2669
+ return err({ ...started.error, message: `${started.error.message}
2670
+ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage, code: String(terminal.exitCode) } }) : deployM("deploy_remoteUnknown")}`, suggestion: terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage, code: String(terminal.exitCode) } }) : deployM("deploy_remoteUnknown"), ext: { ...started.error.ext, commandExecution: {
2671
+ ...state,
2672
+ reason: state?.reason ?? "exit",
2673
+ localTermination: state?.localTermination ?? "unknown",
2674
+ remoteExecution: terminal ? "finished" : "unknown",
2675
+ deploymentId,
2676
+ resultFile,
2677
+ ...terminal ? { remoteExitCode: terminal.exitCode, remoteStage: terminal.stage } : {}
2678
+ } } });
2679
+ }
1501
2680
  logger5.info("Running health check", { url });
1502
- const healthy = await healthCheck(`${url}${!web && scan.isSvelteKit ? "/" : "/health"}`) && (!web || await healthCheck(`${url}/_hai/web-health`) && await healthCheck(`${url}/`));
2681
+ const healthy = await healthCheck(`${url}${!hasWeb && scan.isSvelteKit ? "/" : "/health"}`) && (!hasWeb || await healthCheck(`${url}/_hai/web-health`) && await healthCheck(`${url}/`));
1503
2682
  if (!healthy) {
1504
2683
  return err(
1505
2684
  HaiDeployError.HEALTH_CHECK_FAILED,
@@ -1522,8 +2701,14 @@ function createDockerSshProvider(config, container) {
1522
2701
  try {
1523
2702
  return await deploy2(context);
1524
2703
  } catch (error) {
1525
- logger5.error("Deployment bundle failed", { error });
1526
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"), error);
2704
+ const reason = error instanceof Error ? error.message : String(error);
2705
+ let safeReason = "Deployment bundle failed";
2706
+ try {
2707
+ safeReason = maskSecrets(reason, Object.values(context.appEnv ?? {}).filter(Boolean));
2708
+ } catch {
2709
+ }
2710
+ logger5.error("Deployment bundle failed", { reason: safeReason });
2711
+ return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", { params: { reason: safeReason } }));
1527
2712
  }
1528
2713
  };
1529
2714
  return { name: "docker-ssh", deploy: deploySafely };
@@ -2502,11 +3687,19 @@ var deploy = {
2502
3687
  ...scanOptions.environment ? { environment: environment.data.environment } : {},
2503
3688
  ...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
2504
3689
  };
3690
+ if (options.selectedServices) {
3691
+ options.serviceModes = {
3692
+ ...options.serviceModes,
3693
+ ...Object.fromEntries(scan.requiredServices.filter((service) => !options.selectedServices.includes(service)).map((service) => [service, "keep"]))
3694
+ };
3695
+ }
2505
3696
  if (currentContainerProvider) {
2506
3697
  return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
2507
3698
  }
2508
3699
  if (options?.bundleOnly)
2509
3700
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
3701
+ if (options.selectedServices)
3702
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectedContainerOnly"));
2510
3703
  if (!currentProvider) {
2511
3704
  return notInitialized.result();
2512
3705
  }
@@ -2579,6 +3772,17 @@ async function deployContainerApp(provider, appDir, scan, rawProjectName, option
2579
3772
  } catch {
2580
3773
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
2581
3774
  }
3775
+ if (artifact.partial && !options?.selectedServices) {
3776
+ const selectedServices = artifact.images.map((image) => image.name);
3777
+ options = {
3778
+ ...options,
3779
+ selectedServices,
3780
+ serviceModes: {
3781
+ ...options?.serviceModes,
3782
+ ...Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"]))
3783
+ }
3784
+ };
3785
+ }
2582
3786
  if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
2583
3787
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
2584
3788
  let provisionEnv = {};
@@ -2630,13 +3834,15 @@ var DEPLOY_MIDDLEWARE = {
2630
3834
  { backend: "api", label: "HTTP notification API", purpose: "deploy_purposeEmail", fields: [required("url", "", "url"), field("method", "POST"), field("headers"), field("timeout", "10000", "number")] }
2631
3835
  ],
2632
3836
  sms: [
2633
- { backend: "aliyun-sms", label: "Aliyun SMS", purpose: "deploy_purposeSms", provisioner: "aliyun", fields: [required("accessKeyId"), { ...required("accessKeySecret"), secret: true }, required("signName"), field("endpoint", "dysmsapi.aliyuncs.com")] }
3837
+ { backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
3838
+ { backend: "aliyun-sms", label: "Aliyun SMS", purpose: "deploy_purposeSms", provisioner: "aliyun", fields: [required("accessKeyId"), { ...required("accessKeySecret"), secret: true }, required("signName"), field("endpoint", "dysmsapi.aliyuncs.com")] },
3839
+ { backend: "api", label: "HTTP notification API", purpose: "deploy_purposeSms", fields: [required("url", "", "url"), field("method", "POST"), field("headers"), field("timeout", "10000", "number")] }
2634
3840
  ]
2635
3841
  };
2636
3842
  function describeMiddlewareBackend(backend) {
2637
3843
  return deployM(backend.purpose);
2638
3844
  }
2639
3845
 
2640
- export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
3846
+ export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
2641
3847
  //# sourceMappingURL=index.js.map
2642
3848
  //# sourceMappingURL=index.js.map