@h-ai/deploy 0.1.0-alpha.67 → 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,13 +1,15 @@
1
- import { createHash, randomBytes } from 'crypto';
1
+ import { createHash, randomBytes, randomUUID } from 'crypto';
2
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
5
  import { core, err, ok } from '@h-ai/core';
6
- import process2 from 'process';
6
+ import process3 from 'process';
7
7
  import { parseEnv } from 'util';
8
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
@@ -109,8 +111,17 @@ var en_US_default = {
109
111
  deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
110
112
  deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
111
113
  deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
112
- deploy_bundleInstructions: "Bundle: {file}\nIncludes all images needed by selected services, including self-hosted middleware.\nWith SSH access, run deploy.ps1 (Windows) or deploy.sh (Linux/macOS) from the adjacent deployment directory.\nFor isolated servers, copy and extract the tar into a separate directory, then run sh apply.sh /opt/hai/apps/project-environment.\nLinux requires Docker/Podman, Compose, sh, tar, sha256sum, awk, curl and filesystem/container permissions.\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}",
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}",
113
115
  deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
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.",
114
125
  deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh.",
115
126
  deploy_selectedContainerOnly: "Selected-service deployment requires a docker-ssh Compose destination.",
116
127
  deploy_selectionInvalid: "Cannot deploy {service}: check the service name and build its image first.",
@@ -121,7 +132,102 @@ var en_US_default = {
121
132
  deploy_configFileUnreadable: "Cannot read configuration file {path}. Check its path and permissions.",
122
133
  deploy_bundleNameInvalid: "Bundle filenames may contain letters, numbers, dots, plus signs, hyphens and underscores; parent directories may contain spaces.",
123
134
  deploy_secretsInvalid: "Middleware credentials are damaged or incomplete: {path}. Restore a backup; passwords are not regenerated because existing data may become inaccessible.",
124
- 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."
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"
125
231
  };
126
232
 
127
233
  // messages/zh-CN.json
@@ -177,8 +283,17 @@ var zh_CN_default = {
177
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",
178
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",
179
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",
180
- 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\u5728\u65C1\u8FB9\u7684\u90E8\u7F72\u76EE\u5F55\u8FD0\u884C deploy.ps1\uFF08Windows\uFF09\u6216 deploy.sh\uFF08Linux/macOS\uFF09\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 /opt/hai/apps/\u9879\u76EE\u73AF\u5883\u3002\n\u670D\u52A1\u5668\u9700\u8981 Docker/Podman\u3001Compose\u3001sh\u3001tar\u3001sha256sum\u3001awk \u548C curl\uFF0C\u4EE5\u53CA\u76EE\u5F55\u3001\u5BB9\u5668\u8FD0\u884C\u6743\u9650\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}",
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}",
181
287
  deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
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",
182
297
  deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002",
183
298
  deploy_selectedContainerOnly: "\u6309\u670D\u52A1\u5C40\u90E8\u90E8\u7F72\u4EC5\u9002\u7528\u4E8E docker-ssh Compose \u76EE\u6807\u3002",
184
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",
@@ -189,13 +304,23 @@ var zh_CN_default = {
189
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",
190
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",
191
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",
192
- 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"
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"
193
317
  };
194
318
 
195
319
  // src/deploy-i18n.ts
196
320
  var deployM = core.i18n.createMessageGetter({
197
321
  "zh-CN": zh_CN_default,
198
- "en-US": en_US_default
322
+ "en-US": en_US_default,
323
+ "ja-JP": ja_JP_default
199
324
  });
200
325
 
201
326
  // src/deploy-config.ts
@@ -358,7 +483,7 @@ function loadDeploymentEnvironment(options) {
358
483
  return loaded;
359
484
  try {
360
485
  const values = { ...loaded.data };
361
- for (const [name, value] of Object.entries(process2.env)) {
486
+ for (const [name, value] of Object.entries(process3.env)) {
362
487
  if (/^(?:HAI_|PUBLIC_|VITE_)/.test(name) && value !== void 0)
363
488
  values[name] = value;
364
489
  }
@@ -546,7 +671,7 @@ function loadCredentials() {
546
671
  const data = readCredentialsFile();
547
672
  const keys = Object.keys(data);
548
673
  for (const key of keys) {
549
- process2.env[key] = String(data[key]);
674
+ process3.env[key] = String(data[key]);
550
675
  }
551
676
  logger.debug("Credentials loaded", { count: keys.length });
552
677
  return ok(keys);
@@ -567,7 +692,7 @@ function saveCredential(key, value) {
567
692
  const data = readCredentialsFile();
568
693
  data[key] = value;
569
694
  writeCredentialsFile(data);
570
- process2.env[key] = value;
695
+ process3.env[key] = value;
571
696
  logger.debug("Credential saved", { key });
572
697
  return ok(void 0);
573
698
  } catch (error) {
@@ -587,7 +712,7 @@ function saveCredentials(entries) {
587
712
  const data = readCredentialsFile();
588
713
  for (const [key, value] of Object.entries(entries)) {
589
714
  data[key] = value;
590
- process2.env[key] = value;
715
+ process3.env[key] = value;
591
716
  }
592
717
  writeCredentialsFile(data);
593
718
  logger.debug("Credentials saved", { count: Object.keys(entries).length });
@@ -603,17 +728,20 @@ function saveCredentials(entries) {
603
728
  );
604
729
  }
605
730
  }
606
- function resolveArtifactDirectory(appDir) {
731
+ function resolveProjectDirectory(appDir) {
607
732
  let directory = resolve(appDir);
608
733
  while (true) {
609
734
  if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
610
- return join(directory, ".hai/build/docker");
735
+ return directory;
611
736
  const parent = dirname(directory);
612
737
  if (parent === directory)
613
- return join(appDir, ".hai/build/docker");
738
+ return resolve(appDir);
614
739
  directory = parent;
615
740
  }
616
741
  }
742
+ function resolveArtifactDirectory(appDir) {
743
+ return join(resolveProjectDirectory(appDir), ".hai/build/docker");
744
+ }
617
745
  function usesDefaultMiddleware(scan, service, options) {
618
746
  const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
619
747
  return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
@@ -637,6 +765,19 @@ var RecoverySchema = z.object({
637
765
  resourceStatus: z.enum(["created", "reused", "unknown"])
638
766
  }))
639
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
+ }
640
781
  function getDeployRecovery(error) {
641
782
  const parsed = RecoverySchema.safeParse(error.ext?.recovery);
642
783
  return parsed.success ? parsed.data : void 0;
@@ -753,7 +894,7 @@ async function scanApp(inputDir, options) {
753
894
  for (const [index, item] of providers.entries()) {
754
895
  const provider = record(item);
755
896
  const type = String(provider.type ?? "");
756
- 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;
757
898
  if (service)
758
899
  add(service, type, "reach", ["providers", String(index)], provider, "@h-ai/reach");
759
900
  }
@@ -779,91 +920,575 @@ async function scanApp(inputDir, options) {
779
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" } }));
780
921
  }
781
922
  }
782
- var logger3 = core.logger.child({ module: "deploy", scope: "command" });
783
- function maskSecrets(text, secrets) {
784
- if (!secrets || secrets.length === 0)
785
- return text;
786
- let masked = text;
787
- for (const secret of secrets) {
788
- if (secret && secret.length > 0)
789
- 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;
790
1103
  }
791
- return masked;
792
1104
  }
793
- function runCommand(command, args, options = {}) {
1105
+ async function helper(command, args, timeoutMs) {
1106
+ if (timeoutMs <= 0)
1107
+ return false;
794
1108
  return new Promise((resolve6) => {
795
- const child = spawn(command, args, {
796
- windowsHide: true,
797
- cwd: options.cwd,
798
- env: options.env ? { ...process2.env, ...options.env } : process2.env,
799
- shell: false
800
- });
801
- let stdout = "";
802
- let stderr = "";
1109
+ const child = spawn(command, args, { windowsHide: true, shell: false, stdio: "ignore" });
803
1110
  let settled = false;
804
1111
  let timer;
805
- const finish = (result) => {
1112
+ const finish = (success) => {
806
1113
  if (settled)
807
1114
  return;
808
1115
  settled = true;
809
1116
  if (timer)
810
1117
  clearTimeout(timer);
811
- resolve6(result);
1118
+ resolve6(success);
812
1119
  };
813
- if (options.timeoutMs && options.timeoutMs > 0) {
814
- timer = setTimeout(() => {
815
- child.kill("SIGKILL");
816
- logger3.error("Command timed out", { command, timeoutMs: options.timeoutMs });
817
- finish(err(
818
- HaiDeployError.REMOTE_COMMAND_FAILED,
819
- deployM("deploy_commandTimeout", { params: { command } })
820
- ));
821
- }, options.timeoutMs);
822
- }
823
- child.stdout.on("data", (chunk) => {
824
- stdout += chunk.toString("utf-8");
825
- options.onOutput?.(maskSecrets(chunk.toString("utf-8"), options.secrets));
826
- });
827
- child.stderr.on("data", (chunk) => {
828
- stderr += chunk.toString("utf-8");
829
- });
830
- child.on("error", (error) => {
831
- logger3.error("Command failed to start", { command, error: maskSecrets(String(error), options.secrets) });
832
- finish(err(
833
- HaiDeployError.REMOTE_COMMAND_FAILED,
834
- deployM("deploy_commandFailed", {
835
- params: { command, error: maskSecrets(error.message, options.secrets) }
836
- }),
837
- error
838
- ));
839
- });
840
- child.on("close", (code) => {
841
- const exitCode = code ?? -1;
842
- if (exitCode === 0) {
843
- finish(ok({ code: exitCode, stdout, stderr }));
844
- 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
+ }
845
1169
  }
846
- logger3.error("Command exited with non-zero code", {
847
- command,
848
- code: exitCode,
849
- stderr: maskSecrets(stderr, options.secrets).slice(-500),
850
- stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
851
- });
852
- finish(err(
853
- HaiDeployError.REMOTE_COMMAND_FAILED,
854
- deployM("deploy_commandFailed", {
855
- params: { command, error: maskSecrets(`${stdout.slice(-2500)}
856
- ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
857
- })
858
- ));
859
- });
860
- if (options.input !== void 0) {
861
- child.stdin.write(options.input);
862
- child.stdin.end();
863
- } else {
864
- child.stdin.end();
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);
865
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 }
866
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);
1329
+ }
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") } }));
1362
+ });
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);
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
1471
+ }
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
+ `;
867
1492
  }
868
1493
 
869
1494
  // src/container/deploy-scripts.ts
@@ -906,6 +1531,7 @@ previous=''
906
1531
  restore=''
907
1532
  changed=false
908
1533
  locked=false
1534
+ interrupted=false
909
1535
  started=$(date +%s)
910
1536
  step_started=$started
911
1537
  step() {
@@ -985,15 +1611,22 @@ finish() {
985
1611
  outcome=passed
986
1612
  if [ "$code" -ne 0 ]; then outcome=failed; fi
987
1613
  printf '{"stage":"%s","status":"%s","durationSeconds":%s}\n' "$stage" "$outcome" "$(( $(date +%s) - step_started ))" >> "$root/steps-$id.jsonl"
988
- printf '{"deploymentId":"%s","stage":"%s","exitCode":%s,"durationSeconds":%s}\n' "$id" "$stage" "$code" "$elapsed" > "$root/result-$id.json"
989
- rmdir "$root/.deploy-lock" || true
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
990
1623
  fi
991
1624
  exit "$code"
992
1625
  }
993
1626
  trap finish EXIT
994
- trap 'exit 130' HUP INT TERM
1627
+ trap 'interrupted=true; exit 130' HUP INT TERM
995
1628
  step preflight 'Checking deployment tools and package'
996
- for tool in sha256sum tar awk curl; do command -v "$tool" >/dev/null || { printf 'Required command missing: %s\n' "$tool" >&2; exit 1; }; done
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
997
1630
  if [ "$runtime" = auto ]; then
998
1631
  if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
999
1632
  fi
@@ -1001,7 +1634,20 @@ case "$runtime" in docker|podman) ;; *) printf 'Runtime must be docker or podman
1001
1634
  "$runtime" version >/dev/null
1002
1635
  "$runtime" compose version >/dev/null
1003
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; }
1004
- mkdir "$root/.deploy-lock" 2>/dev/null || { printf 'Another deployment is running. Check %s/.deploy-lock before retrying.\n' "$root" >&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
1005
1651
  locked=true
1006
1652
  cd "$source_dir"
1007
1653
  sha256sum -c SHA256SUMS
@@ -1093,70 +1739,129 @@ mv "$root/current.next" "$root/current"
1093
1739
  printf '[PASS] %s\n' "$id"
1094
1740
  `.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]);
1095
1741
  }
1096
- function createUploadScripts(config, bundle, projectName) {
1097
- const ssh = config.ssh;
1098
- const host = ssh?.host ?? "SERVER_HOST";
1099
- const user = ssh?.username ?? "deploy";
1100
- const remote = `${config.remote.baseDir}/${projectName}`;
1101
- const port = ssh?.port ?? 22;
1742
+ function createUploadScripts() {
1102
1743
  const sh = String.raw`#!/bin/sh
1103
1744
  set -eu
1104
- cd "$(dirname "$0")"
1105
- host=@@HOST@@
1106
- user=@@USER@@
1107
- port=@@PORT@@
1108
- remote=@@REMOTE@@
1109
- bundle=@@BUNDLE@@
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)
1110
1760
  if [ "$#" -ge 1 ]; then host=$1; fi
1111
1761
  if [ "$#" -ge 2 ]; then port=$2; fi
1112
1762
  if [ "$#" -ge 3 ]; then user=$3; fi
1113
- identity=$(printenv HAI_DEPLOY_IDENTITY_FILE || true)
1114
- if [ -z "$identity" ]; then identity=@@IDENTITY@@; 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; }
1115
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; }
1116
1779
  for tool in ssh scp; do command -v "$tool" >/dev/null || { printf 'Install OpenSSH (%s).\n' "$tool" >&2; exit 1; }; done
1117
- set -- -o BatchMode=yes -o StrictHostKeyChecking=@@STRICT@@ -o ConnectTimeout=10
1118
- if [ -n "$identity" ]; then set -- "$@" -i "$identity"; fi
1119
- known_hosts=@@KNOWN@@
1120
- if value=$(printenv HAI_DEPLOY_KNOWN_HOSTS_FILE); then known_hosts=$value; fi
1121
- if [ -n "$known_hosts" ]; then set -- "$@" -o "UserKnownHostsFile=$known_hosts"; fi
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
1122
1786
  printf '[1/3] Connecting to %s\n' "$host"
1123
1787
  ssh "$@" -p "$port" "$user@$host" "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"
1124
1788
  printf '[2/3] Uploading %s\n' "$bundle"
1125
1789
  scp_host=$host
1126
1790
  case "$host" in *:*) scp_host="[$host]";; esac
1127
- scp "$@" -P "$port" "$bundle" "$user@$scp_host:$remote/incoming/$bundle"
1791
+ scp "$@" -P "$port" "./$bundle" "$user@$scp_host:$remote/incoming/$bundle"
1128
1792
  printf '[3/3] Applying package and checking HTTP access\n'
1129
- ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"
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
1130
1800
  `;
1131
1801
  const ps1 = String.raw`param([string]$IdentityFile = $env:HAI_DEPLOY_IDENTITY_FILE)
1132
1802
  $ErrorActionPreference = 'Stop'
1133
- Set-Location -LiteralPath $PSScriptRoot
1134
- $target = Get-Content -LiteralPath './deploy-target.json' -Raw | ConvertFrom-Json
1803
+ Set-Location -LiteralPath (Split-Path -Parent $PSScriptRoot)
1804
+ $target = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'deploy-target.json') -Raw -Encoding UTF8 | ConvertFrom-Json
1135
1805
  if ($target.host -eq 'SERVER_HOST') { throw 'Set the server host in deploy-target.json.' }
1136
- if ($target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]+$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar$') { throw 'Invalid deployment target.' }
1137
- $options = @('-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'ConnectTimeout=10')
1138
- if ($target.strictHostKeyChecking -eq $false) { $options[3] = 'StrictHostKeyChecking=no' }
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")
1139
1814
  if (-not $IdentityFile) { $IdentityFile = $target.identityFile }
1140
- if ($IdentityFile) { $options += @('-i', $IdentityFile) }
1141
- if ($target.knownHostsFile) { $options += @('-o', "UserKnownHostsFile=$($target.knownHostsFile)") }
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
+ }
1142
1821
  $destination = "$($target.username)@$($target.host)"
1143
1822
  $scpDestination = if ($target.host.Contains(':')) { "$($target.username)@[$($target.host)]" } else { $destination }
1144
1823
  $remote = $target.directory
1145
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
+ }
1146
1832
  function Invoke-Step([string]$Name, [string]$Command, [string[]]$Arguments) {
1147
1833
  Write-Host "[$Name]"
1148
- & $Command @Arguments
1149
- if ($LASTEXITCODE -ne 0) { throw "$Name failed (exit $LASTEXITCODE)." }
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)." }
1150
1850
  }
1851
+ $applying = $false
1151
1852
  try {
1152
1853
  foreach ($tool in @('ssh', 'scp')) { Get-Command $tool -ErrorAction Stop | Out-Null }
1153
- Invoke-Step '1/3 Connect' 'ssh' ($options + @('-p', [string]$target.port, $destination, "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"))
1154
- Invoke-Step '2/3 Upload' 'scp' ($options + @('-P', [string]$target.port, $bundle, "$($scpDestination):$remote/incoming/$bundle"))
1155
- Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$target.port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"))
1156
- } catch { Write-Error $_; exit 1 }
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
+ }
1157
1863
  `;
1158
- const values = { HOST: shellArgument(host), USER: shellArgument(user), PORT: String(port), REMOTE: shellArgument(remote), BUNDLE: shellArgument(bundle), IDENTITY: shellArgument(ssh?.identityFile ?? ""), KNOWN: shellArgument(ssh?.knownHostsFile ?? ""), STRICT: ssh?.strictHostKeyChecking === false ? "no" : "accept-new" };
1159
- return { sh: sh.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]), ps1 };
1864
+ return { sh, ps1, reader: createTargetReader() };
1160
1865
  }
1161
1866
 
1162
1867
  // src/container/deploy-bundle.ts
@@ -1247,7 +1952,8 @@ async function createDeploymentBundle(options) {
1247
1952
  if (selected.includes("storage-init"))
1248
1953
  put("initialize-storage", "");
1249
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 }));
1250
- const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output), start: "apply.sh", images: [...references].join("\n"), compose: "compose.json" } });
1955
+ const instructionParams = { file: basename(output), start: "apply.sh", images: [...references].join("\n"), compose: "compose.json" };
1956
+ const instructions = deployM("deploy_bundleInstructions", { params: instructionParams });
1251
1957
  put("README.txt", `${deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: "compose.json" } })}
1252
1958
  ${instructions}
1253
1959
  `);
@@ -1261,15 +1967,38 @@ ${instructions}
1261
1967
  throw new Error(tar.error.message);
1262
1968
  chmodSync(temporaryTar, 384);
1263
1969
  renameSync(temporaryTar, output);
1264
- const upload = createUploadScripts(config, basename(output), projectName);
1970
+ const upload = createUploadScripts();
1265
1971
  const scripts = `${output.slice(0, -4)}-deploy`;
1266
1972
  mkdirSync(scripts, { recursive: true, mode: 448 });
1267
- writeFileSync(join(scripts, "deploy.sh"), upload.sh.replace('cd "$(dirname "$0")"', 'cd "$(dirname "$0")/.."'), { mode: 448 });
1268
- writeFileSync(join(scripts, "deploy.ps1"), upload.ps1.replace("Set-Location -LiteralPath $PSScriptRoot", "Set-Location -LiteralPath (Split-Path -Parent $PSScriptRoot)").replace("'./deploy-target.json'", "(Join-Path $PSScriptRoot 'deploy-target.json')"));
1269
- writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...config.ssh, host: config.ssh?.host ?? "SERVER_HOST", username: config.ssh?.username ?? "deploy", port: config.ssh?.port ?? 22, directory: `${config.remote.baseDir}/${projectName}`, bundle: basename(output) }, null, 2)}
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)}
1270
1999
  `);
1271
2000
  writeFileSync(join(scripts, "README.txt"), instructions);
1272
- return { bundlePath: output, instructions: `${instructions}
2001
+ return { bundlePath: output, instructions: `${deployM("deploy_bundleInstructions", { params: { ...instructionParams, file: output } })}
1273
2002
  ${scripts}`, startName: "apply.sh" };
1274
2003
  } finally {
1275
2004
  rmSync(staging, { recursive: true, force: true });
@@ -1633,6 +2362,7 @@ function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
1633
2362
  listen ${webPort};
1634
2363
  server_name _;
1635
2364
  root /usr/share/nginx/html;
2365
+ large_client_header_buffers 4 32k;
1636
2366
  location / {
1637
2367
  try_files $uri $uri/ /index.html;
1638
2368
  }
@@ -1650,11 +2380,15 @@ function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
1650
2380
  proxy_set_header Host $http_host;
1651
2381
  }
1652
2382
  location ${apiPrefix.replace(/\/$/, "")}/ {
2383
+ client_max_body_size 2129920;
1653
2384
  proxy_pass http://app:${port};
1654
2385
  proxy_http_version 1.1;
1655
2386
  proxy_set_header Host $http_host;
1656
2387
  proxy_set_header X-Forwarded-Proto $scheme;
2388
+ proxy_request_buffering off;
1657
2389
  proxy_buffering off;
2390
+ proxy_buffer_size 32k;
2391
+ proxy_buffers 8 32k;
1658
2392
  }
1659
2393
  }
1660
2394
  `;
@@ -1679,8 +2413,10 @@ function buildConnectOptions(config) {
1679
2413
  const args = ["-o", "BatchMode=yes"];
1680
2414
  args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
1681
2415
  args.push("-o", `StrictHostKeyChecking=${config.strictHostKeyChecking ? "accept-new" : "no"}`);
1682
- if (config.knownHostsFile)
1683
- args.push("-o", `UserKnownHostsFile=${config.knownHostsFile}`);
2416
+ if (config.knownHostsFile) {
2417
+ const path = config.knownHostsFile.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
2418
+ args.push("-o", `UserKnownHostsFile="${path}"`);
2419
+ }
1684
2420
  if (config.identityFile) {
1685
2421
  args.push("-i", config.identityFile);
1686
2422
  args.push("-o", "IdentitiesOnly=yes");
@@ -1707,21 +2443,23 @@ function buildScpUploadArgs(config, localPath, remotePath) {
1707
2443
  }
1708
2444
  function createSshExecutor(config) {
1709
2445
  const exec = async (remoteCommand, options) => {
1710
- return runCommand("ssh", buildSshExecArgs(config, remoteCommand), {
2446
+ const result = await runCommand("ssh", buildSshExecArgs(config, remoteCommand), {
1711
2447
  timeoutMs: options?.timeoutMs,
2448
+ terminationTimeoutMs: options?.terminationTimeoutMs,
2449
+ maxCaptureChars: options?.maxCaptureChars,
1712
2450
  secrets: options?.secrets,
1713
2451
  onOutput: options?.onOutput
1714
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" } } });
1715
2457
  };
1716
2458
  const upload = async (localPath, remotePath) => {
1717
2459
  const args = buildScpUploadArgs(config, localPath, remotePath);
1718
- const result = await runCommand("scp", args, {});
2460
+ const result = await runCommand("scp", args, { timeoutMs: 6e5 });
1719
2461
  if (!result.success) {
1720
- return err(
1721
- HaiDeployError.IMAGE_TRANSFER_FAILED,
1722
- deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }),
1723
- result.error
1724
- );
2462
+ return err({ ...HaiDeployError.IMAGE_TRANSFER_FAILED, message: deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }), ext: result.error.ext });
1725
2463
  }
1726
2464
  return ok(void 0);
1727
2465
  };
@@ -1729,11 +2467,7 @@ function createSshExecutor(config) {
1729
2467
  logger4.debug("SSH preflight", { host: config.host, port: config.port });
1730
2468
  const result = await exec("true", { timeoutMs: 2e4 });
1731
2469
  if (!result.success) {
1732
- return err(
1733
- HaiDeployError.REMOTE_CONNECT_FAILED,
1734
- deployM("deploy_remoteConnectFailed", { params: { host: config.host, error: result.error.message } }),
1735
- result.error
1736
- );
2470
+ return err({ ...HaiDeployError.REMOTE_CONNECT_FAILED, message: deployM("deploy_remoteConnectFailed", { params: { host: config.host, error: result.error.message } }), ext: result.error.ext });
1737
2471
  }
1738
2472
  return ok(void 0);
1739
2473
  };
@@ -1744,6 +2478,12 @@ function createSshExecutor(config) {
1744
2478
  var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
1745
2479
  var HEALTH_POLL_INTERVAL = 3e3;
1746
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
+ });
1747
2487
  function makeDeploymentId() {
1748
2488
  const now = /* @__PURE__ */ new Date();
1749
2489
  const pad = (n) => String(n).padStart(2, "0");
@@ -1791,7 +2531,7 @@ function createDockerSshProvider(config, container) {
1791
2531
  const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
1792
2532
  const deploymentId = makeDeploymentId();
1793
2533
  const hostPort = config.expose.hostPort;
1794
- const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
2534
+ const artifactDir = join(resolveProjectDirectory(appDir), ".hai", "deploy", options?.environment ?? "prod");
1795
2535
  mkdirSync(artifactDir, { recursive: true, mode: 448 });
1796
2536
  chmodSync(artifactDir, 448);
1797
2537
  const artifact = context.artifact;
@@ -1900,18 +2640,43 @@ function createDockerSshProvider(config, container) {
1900
2640
  const started = await ssh.exec(
1901
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)}`,
1902
2642
  { timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output) => {
1903
- pendingOutput += output;
1904
- const lines = pendingOutput.split("\n");
1905
- pendingOutput = lines.pop() ?? "";
1906
- for (const line of lines) {
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 = "";
1907
2650
  const stage = /^\[(preflight|images|dependencies|start|health|complete)\]/.exec(line)?.[1];
1908
2651
  if (stage)
1909
2652
  options?.onProgress?.(stage);
1910
2653
  }
1911
2654
  } }
1912
2655
  );
1913
- if (!started.success)
1914
- return started;
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
+ }
1915
2680
  logger5.info("Running health check", { url });
1916
2681
  const healthy = await healthCheck(`${url}${!hasWeb && scan.isSvelteKit ? "/" : "/health"}`) && (!hasWeb || await healthCheck(`${url}/_hai/web-health`) && await healthCheck(`${url}/`));
1917
2682
  if (!healthy) {
@@ -1936,9 +2701,14 @@ function createDockerSshProvider(config, container) {
1936
2701
  try {
1937
2702
  return await deploy2(context);
1938
2703
  } catch (error) {
1939
- logger5.error("Deployment bundle failed", { error });
1940
2704
  const reason = error instanceof Error ? error.message : String(error);
1941
- return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", { params: { reason: maskSecrets(reason, Object.values(context.appEnv ?? {}).filter((value) => value.length > 6)) } }), 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 } }));
1942
2712
  }
1943
2713
  };
1944
2714
  return { name: "docker-ssh", deploy: deploySafely };
@@ -3064,13 +3834,15 @@ var DEPLOY_MIDDLEWARE = {
3064
3834
  { backend: "api", label: "HTTP notification API", purpose: "deploy_purposeEmail", fields: [required("url", "", "url"), field("method", "POST"), field("headers"), field("timeout", "10000", "number")] }
3065
3835
  ],
3066
3836
  sms: [
3067
- { 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")] }
3068
3840
  ]
3069
3841
  };
3070
3842
  function describeMiddlewareBackend(backend) {
3071
3843
  return deployM(backend.purpose);
3072
3844
  }
3073
3845
 
3074
- 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 };
3075
3847
  //# sourceMappingURL=index.js.map
3076
3848
  //# sourceMappingURL=index.js.map