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

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,16 +1,52 @@
1
+ import { createHash, randomBytes } from 'crypto';
2
+ import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, rmSync, statSync, readdirSync } from 'fs';
3
+ import { resolve, join, basename, dirname, relative, sep } from 'path';
1
4
  import { z } from 'zod';
2
5
  import { core, ok, err } from '@h-ai/core';
3
- import { readFileSync, existsSync, writeFileSync, chmodSync, mkdirSync, statSync, readdirSync } from 'fs';
4
- import { resolve, join, basename, dirname, relative, sep } from 'path';
5
6
  import process2 from 'process';
6
7
  import { parseEnv } from 'util';
7
8
  import { parse, stringify, parseAllDocuments } from 'yaml';
8
9
  import { homedir } from 'os';
9
- import { execSync, spawn } from 'child_process';
10
- import { randomBytes, createHash } from 'crypto';
10
+ import { spawn } from 'child_process';
11
11
  import { Buffer } from 'buffer';
12
12
 
13
- // src/deploy-config.ts
13
+ // src/deploy-artifact.ts
14
+ var DeploymentArtifactSchema = z.object({
15
+ /** 清单格式版本,与应用版本和部署环境无关。 */
16
+ version: z.literal(2),
17
+ /** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
18
+ appVersion: z.string().max(80).regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9a-z-]+(?:\.[0-9a-z-]+)*)?(?:\+[0-9a-z-]+(?:\.[0-9a-z-]+)*)?$/i),
19
+ /** 一次完整构建的标识,app/web 必须来自同一份清单。 */
20
+ buildId: z.string().regex(/^[a-z0-9][\w.+-]*$/i),
21
+ /** API/独立应用和可选 Web 的镜像归档,至少包含一个 app。 */
22
+ images: z.array(z.object({
23
+ /** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
24
+ name: z.enum(["app", "web"]),
25
+ /** 相对于清单目录的 tar 文件名。 */
26
+ file: z.string().regex(/^[\w.+-]+\.tar$/),
27
+ /** 归档导入后可直接使用的不可变镜像引用。 */
28
+ imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]+$/),
29
+ /** tar 内容的 SHA-256,用于部署前校验。 */
30
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
31
+ /** 容器内部监听端口;服务器公开端口由部署选择。 */
32
+ containerPort: z.number().int().min(1).max(65535).optional()
33
+ })).min(1).max(2).refine((images) => images.filter((image) => image.name === "app").length === 1 && new Set(images.map((image) => image.name)).size === images.length)
34
+ }).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`));
35
+ async function artifactChecksum(file) {
36
+ const hash = createHash("sha256");
37
+ for await (const chunk of createReadStream(file))
38
+ hash.update(chunk);
39
+ return hash.digest("hex");
40
+ }
41
+ async function readDeploymentArtifact(directory) {
42
+ const artifact = DeploymentArtifactSchema.parse(JSON.parse(readFileSync(resolve(directory, "manifest.json"), "utf8")));
43
+ for (const image of artifact.images) {
44
+ const file = resolve(directory, image.file);
45
+ if (!existsSync(file) || !lstatSync(file).isFile() || lstatSync(file).isSymbolicLink() || await artifactChecksum(file) !== image.sha256)
46
+ throw new Error(`Invalid artifact: ${image.file}`);
47
+ }
48
+ return artifact;
49
+ }
14
50
 
15
51
  // messages/en-US.json
16
52
  var en_US_default = {
@@ -22,7 +58,6 @@ var en_US_default = {
22
58
  deploy_authRequired: "Authentication required",
23
59
  deploy_authFailed: "Authentication failed: {error}",
24
60
  deploy_projectCreateFailed: "Project creation failed: {error}",
25
- deploy_buildFailed: "Application build failed: {error}",
26
61
  deploy_uploadFailed: "Build output upload failed: {error}",
27
62
  deploy_deployFailed: "Deployment failed: {error}",
28
63
  deploy_provisionFailed: "Service provisioning failed: {service} - {error}",
@@ -42,8 +77,6 @@ var en_US_default = {
42
77
  deploy_recoverySuggestion: "Inspect listed resources in the provider console before retrying with the same project name. Unknown means the request may have created a resource. Reused resources are never deleted automatically; remove newly created resources manually only after checking their use.",
43
78
  deploy_commandTimeout: "Command timed out: {command}",
44
79
  deploy_commandFailed: "Command failed ({command}): {error}",
45
- deploy_containerRuntimeNotFound: "No usable container runtime found: {runtime}",
46
- deploy_containerBuildFailed: "Container image build failed: {error}",
47
80
  deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
48
81
  deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
49
82
  deploy_imageTransferFailed: "Image transfer failed: {error}",
@@ -64,7 +97,13 @@ var en_US_default = {
64
97
  deploy_purposeLdap: "Directory account authentication; the default directory has no business users",
65
98
  deploy_purposeConsole: "Messages go to application logs without delivery",
66
99
  deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
67
- deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota"
100
+ deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
101
+ deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
102
+ deploy_artifactInvalid: "Build artifacts are missing, corrupt, or could not be packaged. Run pnpm build --target docker at the project root and point --artifact-dir to a complete build directory.",
103
+ deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
104
+ deploy_bundleInstructions: "This bundle contains private configuration. Transfer it securely and restrict access.\n1. Copy {file} to Linux using removable media, a private network or scp.\n2. Install Docker/Podman with compose, tar, sha256sum and sh. Load middleware images using docker load/podman load with the exact tags below.\n3. Create a private release directory: mkdir -p release && chmod 700 release\n4. From the bundle directory run tar -xf {file} -C release\n5. Run cd release && sh {start}. Set HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=docker or podman to select the runtime explicitly.\n6. Check docker compose -f {compose} --env-file .env.runtime ps (or logs; use podman instead of docker for Podman), Web and /health after startup. Bundle generation does not perform server health checks.\nRequired images (app/web are included; prepare middleware separately):\n{images}\nBuilt images can be reused across environments; this bundle contains configuration for the selected environment only.",
105
+ deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
106
+ deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh."
68
107
  };
69
108
 
70
109
  // messages/zh-CN.json
@@ -77,7 +116,6 @@ var zh_CN_default = {
77
116
  deploy_authRequired: "\u8BF7\u5148\u5B8C\u6210\u8BA4\u8BC1",
78
117
  deploy_authFailed: "\u8BA4\u8BC1\u5931\u8D25\uFF1A{error}",
79
118
  deploy_projectCreateFailed: "\u9879\u76EE\u521B\u5EFA\u5931\u8D25\uFF1A{error}",
80
- deploy_buildFailed: "\u5E94\u7528\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
81
119
  deploy_uploadFailed: "\u6784\u5EFA\u4EA7\u7269\u4E0A\u4F20\u5931\u8D25\uFF1A{error}",
82
120
  deploy_deployFailed: "\u90E8\u7F72\u5931\u8D25\uFF1A{error}",
83
121
  deploy_provisionFailed: "\u57FA\u7840\u8BBE\u65BD\u5F00\u901A\u5931\u8D25\uFF1A{service} - {error}",
@@ -97,8 +135,6 @@ var zh_CN_default = {
97
135
  deploy_recoverySuggestion: "\u91CD\u8BD5\u524D\u5728\u670D\u52A1\u5546\u63A7\u5236\u53F0\u6838\u5BF9\u6E05\u5355\uFF0C\u5E76\u4F7F\u7528\u76F8\u540C\u9879\u76EE\u540D\u3002unknown \u8868\u793A\u8BF7\u6C42\u53EF\u80FD\u5DF2\u521B\u5EFA\u8D44\u6E90\u3002\u590D\u7528\u8D44\u6E90\u4E0D\u4F1A\u81EA\u52A8\u5220\u9664\uFF1B\u65B0\u5EFA\u8D44\u6E90\u4EC5\u5728\u786E\u8BA4\u672A\u88AB\u4F7F\u7528\u540E\u4EBA\u5DE5\u6E05\u7406\u3002",
98
136
  deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
99
137
  deploy_commandFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08{command}\uFF09\uFF1A{error}",
100
- deploy_containerRuntimeNotFound: "\u672A\u627E\u5230\u53EF\u7528\u7684\u5BB9\u5668\u8FD0\u884C\u65F6\uFF1A{runtime}",
101
- deploy_containerBuildFailed: "\u5BB9\u5668\u955C\u50CF\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
102
138
  deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
103
139
  deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
104
140
  deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
@@ -119,7 +155,13 @@ var zh_CN_default = {
119
155
  deploy_purposeLdap: "\u4F01\u4E1A\u76EE\u5F55\u8D26\u53F7\u8BA4\u8BC1\uFF1B\u9ED8\u8BA4\u76EE\u5F55\u4E0D\u5305\u542B\u4E1A\u52A1\u7528\u6237",
120
156
  deploy_purposeConsole: "\u6D88\u606F\u8F93\u51FA\u5230\u5E94\u7528\u65E5\u5FD7\uFF0C\u4E0D\u5B9E\u9645\u6295\u9012",
121
157
  deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
122
- deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6"
158
+ deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6",
159
+ deploy_webProxyInvalid: "API \u524D\u7F00\u5FC5\u987B\u662F\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26\u548C\u659C\u6760\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
160
+ deploy_artifactInvalid: "\u6784\u5EFA\u4EA7\u7269\u7F3A\u5931\u3001\u635F\u574F\u6216\u90E8\u7F72\u6253\u5305\u5931\u8D25\u3002\u8BF7\u5148\u5728\u5DE5\u7A0B\u6839\u8FD0\u884C pnpm build --target docker\uFF0C\u5E76\u68C0\u67E5 --artifact-dir \u6307\u5411\u5B8C\u6574\u7684\u6784\u5EFA\u76EE\u5F55\u3002",
161
+ deploy_sshRequired: "\u81EA\u52A8\u90E8\u7F72\u9700\u8981 SSH \u914D\u7F6E\uFF1B\u65E0\u7F51\u7EDC\u8FDE\u63A5\u65F6\u4F7F\u7528 --bundle-only \u751F\u6210\u90E8\u7F72\u5305\u5E76\u624B\u5DE5\u642C\u8FD0\u3002",
162
+ deploy_bundleInstructions: "\u6B64\u5305\u5305\u542B\u79C1\u6709\u914D\u7F6E\uFF0C\u8BF7\u5B89\u5168\u642C\u8FD0\u5E76\u9650\u5236\u8BBF\u95EE\u6743\u9650\u3002\n1. \u5C06 {file} \u590D\u5236\u5230 Linux \u670D\u52A1\u5668\uFF08U \u76D8\u3001\u5185\u7F51\u4F20\u8F93\u6216 scp \u5747\u53EF\uFF09\u3002\n2. \u51C6\u5907 Docker/Podman\u3001\u5BF9\u5E94 compose \u547D\u4EE4\u3001tar\u3001sha256sum \u548C sh\uFF1B\u5C06\u6240\u9700\u4E2D\u95F4\u4EF6\u955C\u50CF\u7528 docker load/podman load \u5BFC\u5165\uFF0C\u955C\u50CF\u6807\u7B7E\u987B\u4E0E\u4E0B\u5217\u6E05\u5355\u5B8C\u5168\u4E00\u81F4\u3002\n3. \u65B0\u5EFA\u4EC5\u90E8\u7F72\u7528\u6237\u53EF\u8BBF\u95EE\u7684\u76EE\u5F55\uFF1Amkdir -p release && chmod 700 release\n4. \u5728\u5305\u6240\u5728\u76EE\u5F55\u6267\u884C tar -xf {file} -C release\n5. \u6267\u884C cd release && sh {start}\uFF1B\u53EF\u7528 HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=docker \u6216 podman \u663E\u5F0F\u9009\u62E9\u8FD0\u884C\u65F6\u3002\n6. \u542F\u52A8\u540E\u7528 docker compose -f {compose} --env-file .env.runtime ps\uFF08\u6216 logs\uFF1BPodman \u5C06 docker \u6362\u4E3A podman\uFF09\u68C0\u67E5\u72B6\u6001\u5E76\u9A8C\u8BC1 Web\u3001/health\uFF1B\u4EC5\u6253\u5305\u4E0D\u4F1A\u6267\u884C\u670D\u52A1\u5668\u5065\u5EB7\u68C0\u67E5\u3002\n\u6240\u9700\u955C\u50CF\uFF08app/web \u5DF2\u542B\u5728\u5305\u4E2D\uFF0C\u4E2D\u95F4\u4EF6\u9700\u53E6\u884C\u51C6\u5907\uFF09\uFF1A\n{images}\n\u6784\u5EFA\u955C\u50CF\u53EF\u7528\u4E8E\u591A\u73AF\u5883\uFF1B\u6B64\u5305\u7684\u73AF\u5883\u914D\u7F6E\u4EC5\u7528\u4E8E\u672C\u6B21\u6240\u9009\u73AF\u5883\u3002",
163
+ deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
164
+ deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002"
123
165
  };
124
166
 
125
167
  // src/deploy-i18n.ts
@@ -160,7 +202,7 @@ var ExposeSchema = PortExposeSchema;
160
202
  var DockerSshProviderConfigSchema = z.object({
161
203
  type: z.literal("docker-ssh"),
162
204
  /** SSH 连接参数 */
163
- ssh: SshConfigSchema,
205
+ ssh: SshConfigSchema.optional(),
164
206
  /** 远程主机运行参数 */
165
207
  remote: z.object({
166
208
  /** 远程应用根目录 */
@@ -214,24 +256,15 @@ var DeployConfigSchema = z.object({
214
256
  environment: z.string().regex(/^[a-z0-9][\w-]*$/i).refine((value) => !["local", "example"].includes(value.toLowerCase())).optional(),
215
257
  provider: ProviderConfigSchema,
216
258
  services: ServicesConfigSchema,
217
- /** 本地容器构建参数(docker-ssh 使用;auto 优先 podman) */
259
+ /** Docker Compose 部署参数;构建配置位于项目 hai.build.json。 */
218
260
  container: z.object({
219
- runtime: z.enum(["auto", "docker", "podman"]).default("auto"),
220
- /** Podman 构建是否继承代理;容器不能访问宿主机 localhost 代理时设为 false。 */
221
- httpProxy: z.boolean().default(true),
222
261
  /** 自建 S3 服务实现;协议仍为 storage.type=s3。 */
223
- s3Engine: z.enum(["rustfs", "minio"]).default("rustfs"),
224
- /** SQLite 预编译包镜像根地址;省略时从官方 GitHub Release 下载,不允许嵌入凭据。 */
225
- sqlitePrebuildMirror: z.url().refine((value) => {
226
- const url = new URL(value);
227
- return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password && !url.search && !url.hash;
228
- }).optional()
262
+ s3Engine: z.enum(["rustfs", "minio"]).default("rustfs")
229
263
  }).optional()
230
264
  });
231
265
  var DeployErrorInfo = {
232
266
  DEPLOY_FAILED: "001:500",
233
267
  PROJECT_CREATE_FAILED: "002:500",
234
- BUILD_FAILED: "003:500",
235
268
  UPLOAD_FAILED: "004:500",
236
269
  AUTH_REQUIRED: "005:401",
237
270
  AUTH_FAILED: "006:401",
@@ -243,8 +276,6 @@ var DeployErrorInfo = {
243
276
  UNSUPPORTED_TYPE: "012:400",
244
277
  CONFIG_ERROR: "013:500",
245
278
  CREDENTIAL_ERROR: "014:500",
246
- CONTAINER_RUNTIME_NOT_FOUND: "015:500",
247
- CONTAINER_BUILD_FAILED: "016:500",
248
279
  REMOTE_CONNECT_FAILED: "017:500",
249
280
  REMOTE_COMMAND_FAILED: "018:500",
250
281
  IMAGE_TRANSFER_FAILED: "019:500",
@@ -253,9 +284,16 @@ var DeployErrorInfo = {
253
284
  var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
254
285
 
255
286
  // src/deploy-environment.ts
287
+ function environmentValue(value) {
288
+ try {
289
+ return parse(value) ?? value;
290
+ } catch {
291
+ return value;
292
+ }
293
+ }
256
294
  function resolveConfig(value, prefix, env) {
257
295
  if (env[prefix] !== void 0)
258
- return parse(env[prefix]) ?? env[prefix];
296
+ return environmentValue(env[prefix]);
259
297
  if (Array.isArray(value))
260
298
  return value.map((item, index) => resolveConfig(item, `${prefix}_${index}`, env));
261
299
  if (value && typeof value === "object")
@@ -263,7 +301,7 @@ function resolveConfig(value, prefix, env) {
263
301
  if (typeof value !== "string")
264
302
  return value;
265
303
  const resolved = value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, name, fallback) => env[name] ?? fallback ?? "");
266
- return /^\$\{[^}]+\}$/.test(value) ? parse(resolved) ?? resolved : resolved;
304
+ return /^\$\{[^}]+\}$/.test(value) ? environmentValue(resolved) : resolved;
267
305
  }
268
306
  function loadDeploymentConfig(name, path, values) {
269
307
  try {
@@ -531,39 +569,25 @@ function saveCredentials(entries) {
531
569
  );
532
570
  }
533
571
  }
534
- var logger2 = core.logger.child({ module: "deploy", scope: "functions" });
572
+ function resolveArtifactDirectory(appDir) {
573
+ let directory = resolve(appDir);
574
+ while (true) {
575
+ if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
576
+ return join(directory, ".hai/build/docker");
577
+ const parent = dirname(directory);
578
+ if (parent === directory)
579
+ return join(appDir, ".hai/build/docker");
580
+ directory = parent;
581
+ }
582
+ }
535
583
  function usesDefaultMiddleware(scan, service, options) {
536
584
  const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
537
585
  return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
538
586
  }
539
- function buildApp(appDir, buildCommand, envVars = {}) {
540
- logger2.info("Building application", { appDir, buildCommand });
541
- try {
542
- execSync(buildCommand, {
543
- cwd: appDir,
544
- stdio: "inherit",
545
- env: { ...process2.env, ...envVars }
546
- });
547
- return ok(void 0);
548
- } catch (error) {
549
- return err(
550
- HaiDeployError.BUILD_FAILED,
551
- deployM("deploy_buildFailed", {
552
- params: { error: error instanceof Error ? error.message : String(error) }
553
- }),
554
- error
555
- );
556
- }
557
- }
558
587
  function resolveOutputDir(appDir, providerType) {
559
588
  switch (providerType) {
560
- case "vercel": {
561
- const vercelOutput = join(appDir, ".vercel", "output");
562
- if (existsSync(vercelOutput)) {
563
- return vercelOutput;
564
- }
565
- return join(appDir, "build");
566
- }
589
+ case "vercel":
590
+ return join(appDir, ".vercel", "output");
567
591
  default:
568
592
  return join(appDir, "build");
569
593
  }
@@ -594,7 +618,7 @@ function recoveryFailure(error, recovery) {
594
618
  ext: { recovery }
595
619
  });
596
620
  }
597
- var logger3 = core.logger.child({ module: "deploy", scope: "scanner" });
621
+ var logger2 = core.logger.child({ module: "deploy", scope: "scanner" });
598
622
  function record(value) {
599
623
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
600
624
  }
@@ -713,15 +737,170 @@ async function scanApp(inputDir, options) {
713
737
  adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
714
738
  nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
715
739
  requiredServices: [...new Set(middlewares.map((item) => item.service))],
716
- serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend])),
717
- buildCommand: "pnpm build"
740
+ serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
718
741
  };
719
- logger3.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
742
+ logger2.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
720
743
  return ok(scan);
721
744
  } catch {
722
745
  return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", { params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" } }));
723
746
  }
724
747
  }
748
+ var logger3 = core.logger.child({ module: "deploy", scope: "command" });
749
+ function maskSecrets(text, secrets) {
750
+ if (!secrets || secrets.length === 0)
751
+ return text;
752
+ let masked = text;
753
+ for (const secret of secrets) {
754
+ if (secret && secret.length > 0)
755
+ masked = masked.split(secret).join("***");
756
+ }
757
+ return masked;
758
+ }
759
+ function runCommand(command, args, options = {}) {
760
+ return new Promise((resolve6) => {
761
+ const child = spawn(command, args, {
762
+ cwd: options.cwd,
763
+ env: options.env ? { ...process2.env, ...options.env } : process2.env,
764
+ shell: false
765
+ });
766
+ let stdout = "";
767
+ let stderr = "";
768
+ let settled = false;
769
+ let timer;
770
+ const finish = (result) => {
771
+ if (settled)
772
+ return;
773
+ settled = true;
774
+ if (timer)
775
+ clearTimeout(timer);
776
+ resolve6(result);
777
+ };
778
+ if (options.timeoutMs && options.timeoutMs > 0) {
779
+ timer = setTimeout(() => {
780
+ child.kill("SIGKILL");
781
+ logger3.error("Command timed out", { command, timeoutMs: options.timeoutMs });
782
+ finish(err(
783
+ HaiDeployError.REMOTE_COMMAND_FAILED,
784
+ deployM("deploy_commandTimeout", { params: { command } })
785
+ ));
786
+ }, options.timeoutMs);
787
+ }
788
+ child.stdout.on("data", (chunk) => {
789
+ stdout += chunk.toString("utf-8");
790
+ });
791
+ child.stderr.on("data", (chunk) => {
792
+ stderr += chunk.toString("utf-8");
793
+ });
794
+ child.on("error", (error) => {
795
+ logger3.error("Command failed to start", { command, error: maskSecrets(String(error), options.secrets) });
796
+ finish(err(
797
+ HaiDeployError.REMOTE_COMMAND_FAILED,
798
+ deployM("deploy_commandFailed", {
799
+ params: { command, error: maskSecrets(error.message, options.secrets) }
800
+ }),
801
+ error
802
+ ));
803
+ });
804
+ child.on("close", (code) => {
805
+ const exitCode = code ?? -1;
806
+ if (exitCode === 0) {
807
+ finish(ok({ code: exitCode, stdout, stderr }));
808
+ return;
809
+ }
810
+ logger3.error("Command exited with non-zero code", {
811
+ command,
812
+ code: exitCode,
813
+ stderr: maskSecrets(stderr, options.secrets).slice(-500),
814
+ stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
815
+ });
816
+ finish(err(
817
+ HaiDeployError.REMOTE_COMMAND_FAILED,
818
+ deployM("deploy_commandFailed", {
819
+ params: { command, error: maskSecrets(`${stdout.slice(-2500)}
820
+ ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
821
+ })
822
+ ));
823
+ });
824
+ if (options.input !== void 0) {
825
+ child.stdin.write(options.input);
826
+ child.stdin.end();
827
+ } else {
828
+ child.stdin.end();
829
+ }
830
+ });
831
+ }
832
+
833
+ // src/container/deploy-bundle.ts
834
+ function shellArgument(value) {
835
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
836
+ }
837
+ async function createDeploymentBundle(options) {
838
+ const { directory, artifactDir, artifact, projectName, environment, runtime, coreEnv } = options;
839
+ const stem = basename(options.output).replace(/\.tar$/i, "");
840
+ const versionSuffix = `-${artifact.appVersion}`;
841
+ const output = resolve(dirname(options.output), `${stem.endsWith(versionSuffix) ? stem : `${stem}${versionSuffix}`}.tar`);
842
+ const composeName = `docker-compose-${artifact.appVersion}.yml`;
843
+ mkdirSync(dirname(output), { recursive: true, mode: 448 });
844
+ const staging = mkdtempSync(join(directory, ".bundle-"));
845
+ chmodSync(staging, 448);
846
+ const temporaryTar = `${output}.partial`;
847
+ const startName = `start-${environment}.sh`;
848
+ try {
849
+ mkdirSync(join(staging, "images"));
850
+ for (const image of artifact.images)
851
+ copyFileSync(join(artifactDir, image.file), join(staging, "images", image.file));
852
+ for (const file of [composeName, ".env.runtime", ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : []])
853
+ copyFileSync(join(directory, file), join(staging, file));
854
+ const compose = parse(readFileSync(join(directory, composeName), "utf8"));
855
+ const images = [...new Set(Object.values(compose.services).flatMap((service) => service.image ? [service.image] : []))];
856
+ const start = `#!/bin/sh
857
+ set -eu
858
+ cd "$(dirname "$0")"
859
+ umask 077
860
+ export HAI_DEPLOY_ENVIRONMENT=${shellArgument(environment)}
861
+ export HAI_CORE_ENV=${shellArgument(coreEnv)}
862
+ runtime=${shellArgument(runtime)}
863
+ if [ -n "\${HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME:-}" ]; then runtime="$HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME"; fi
864
+ if [ "$runtime" = auto ]; then
865
+ if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
866
+ fi
867
+ case "$runtime" in docker|podman) ;; *) echo 'HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME must be docker or podman' >&2; exit 1;; esac
868
+ "$runtime" version >/dev/null
869
+ "$runtime" compose version >/dev/null
870
+ sha256sum -c SHA256SUMS
871
+ chmod 600 .env.runtime
872
+ ${artifact.images.map((image) => `"$runtime" load -i ${shellArgument(`images/${image.file}`)}`).join("\n")}
873
+ # \u79BB\u7EBF\u670D\u52A1\u5668\u5E94\u5148\u5BFC\u5165\u5168\u90E8\u4E2D\u95F4\u4EF6\u955C\u50CF\uFF1B\u7981\u6B62\u9759\u9ED8\u5C1D\u8BD5\u8054\u7F51\u3002
874
+ for image in ${images.map(shellArgument).join(" ")}; do
875
+ "$runtime" image inspect "$image" >/dev/null 2>&1 || { echo "Missing image: $image. Load the matching image archive before retrying." >&2; exit 1; }
876
+ done
877
+ exec "$runtime" compose -f ${shellArgument(composeName)} --env-file .env.runtime -p ${shellArgument(`hai-${projectName}`)} up -d
878
+ `;
879
+ writeFileSync(join(directory, startName), start, { mode: 448 });
880
+ writeFileSync(join(staging, startName), start, { mode: 448 });
881
+ const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output), start: startName, images: images.join("\n"), compose: composeName } });
882
+ const metadata = deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: composeName } });
883
+ writeFileSync(join(staging, "README.txt"), `${metadata}
884
+ ${instructions}
885
+ `);
886
+ copyFileSync(join(artifactDir, "manifest.json"), join(staging, "manifest.json"));
887
+ const checksums = [];
888
+ for (const file of [composeName, "manifest.json", ".env.runtime", startName, ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : [], ...artifact.images.map((image) => `images/${image.file}`)])
889
+ checksums.push(`${await artifactChecksum(join(staging, file))} ${file}`);
890
+ writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
891
+ `);
892
+ const tar = await runCommand("tar", ["-cf", temporaryTar, "-C", staging, "."]);
893
+ if (!tar.success)
894
+ throw new Error(tar.error.message);
895
+ chmodSync(temporaryTar, 384);
896
+ const { renameSync } = await import('fs');
897
+ renameSync(temporaryTar, output);
898
+ return { bundlePath: output, instructions, startName };
899
+ } finally {
900
+ rmSync(staging, { recursive: true, force: true });
901
+ rmSync(temporaryTar, { force: true });
902
+ }
903
+ }
725
904
  var IMAGES = {
726
905
  postgres: "postgres:16-alpine",
727
906
  mysql: "mysql:8.4",
@@ -995,6 +1174,7 @@ function generateComposeFile(options) {
995
1174
  const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
996
1175
  const app = {
997
1176
  image: imageRef,
1177
+ labels: { "org.opencontainers.image.version": options.appVersion },
998
1178
  container_name: `hai-${projectName}-app`,
999
1179
  restart: "unless-stopped",
1000
1180
  // 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
@@ -1008,11 +1188,28 @@ function generateComposeFile(options) {
1008
1188
  if (sidecar.dependsOn.length > 0)
1009
1189
  app.depends_on = sidecar.dependsOn;
1010
1190
  const services = { app, ...sidecar.composeServices };
1191
+ if (options.web) {
1192
+ delete app.ports;
1193
+ services.web = {
1194
+ image: options.web.imageRef,
1195
+ labels: { "org.opencontainers.image.version": options.appVersion },
1196
+ container_name: `hai-${projectName}-web`,
1197
+ restart: "unless-stopped",
1198
+ pull_policy: "never",
1199
+ ports: [`${hostPort}:${options.web.containerPort}`],
1200
+ environment: { PUBLIC_API_BASE: "" },
1201
+ healthcheck: { test: ["CMD", "wget", "-Y", "off", "-q", "-O", "/dev/null", `http://127.0.0.1:${options.web.containerPort}/_hai/web-health`], interval: "30s", timeout: "5s", retries: 3 },
1202
+ volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
1203
+ depends_on: ["app"],
1204
+ networks: ["hai-app"]
1205
+ };
1206
+ }
1011
1207
  if (services["storage-init"])
1012
1208
  services["storage-init"] = { ...services["storage-init"], image: imageRef };
1013
1209
  const compose = {
1210
+ "x-hai-version": options.appVersion,
1014
1211
  services,
1015
- networks: {
1212
+ "networks": {
1016
1213
  "hai-app": { name: `hai-${projectName}` }
1017
1214
  }
1018
1215
  };
@@ -1041,8 +1238,43 @@ function generateComposeFile(options) {
1041
1238
  "storage-init": { condition: "service_completed_successfully" }
1042
1239
  };
1043
1240
  }
1241
+ for (const service of Object.values(services))
1242
+ service.pull_policy = "never";
1044
1243
  return stringify(compose);
1045
1244
  }
1245
+ function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
1246
+ if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535))
1247
+ throw new Error("Invalid web proxy target");
1248
+ return `server {
1249
+ listen ${webPort};
1250
+ server_name _;
1251
+ root /usr/share/nginx/html;
1252
+ location / {
1253
+ try_files $uri $uri/ /index.html;
1254
+ }
1255
+ location = /runtime-config.js {
1256
+ add_header Cache-Control "no-store";
1257
+ default_type application/javascript;
1258
+ return 200 'globalThis.__HAI_PUBLIC_CONFIG__={apiBase:globalThis.location.origin+"${apiPrefix}"};';
1259
+ }
1260
+ location = /_hai/web-health {
1261
+ access_log off;
1262
+ return 200 'ok';
1263
+ }
1264
+ location = /health {
1265
+ proxy_pass http://app:${port};
1266
+ proxy_set_header Host $http_host;
1267
+ }
1268
+ location ${apiPrefix.replace(/\/$/, "")}/ {
1269
+ proxy_pass http://app:${port};
1270
+ proxy_http_version 1.1;
1271
+ proxy_set_header Host $http_host;
1272
+ proxy_set_header X-Forwarded-Proto $scheme;
1273
+ proxy_buffering off;
1274
+ }
1275
+ }
1276
+ `;
1277
+ }
1046
1278
  function serializeEnvFile(env) {
1047
1279
  return `${Object.entries(env).map(([key, value]) => {
1048
1280
  let literal = value;
@@ -1058,194 +1290,7 @@ function serializeEnvFile(env) {
1058
1290
  }).join("\n")}
1059
1291
  `;
1060
1292
  }
1061
- function generateDockerignore() {
1062
- return `**/node_modules
1063
- **/dist
1064
- **/build
1065
- **/.turbo
1066
- **/.svelte-kit
1067
- **/coverage
1068
- **/test-results
1069
- **/playwright-report
1070
- **/.hai/deploy
1071
- **/.env.local
1072
- **/.env.*.local
1073
- **/.env.runtime
1074
- .git
1075
- `;
1076
- }
1077
- function generateFallbackDockerfile(packageName, containerPort, options = {}) {
1078
- if (!/^@?[a-z0-9][\w./-]*$/i.test(packageName) || !/^[a-z0-9][\w-]*$/i.test(options.environment ?? "prod"))
1079
- throw new Error("Invalid container build target");
1080
- const environment = options.environment ?? "prod";
1081
- const coreEnv = environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
1082
- return `# syntax=docker/dockerfile:1.7
1083
- FROM node:24-bookworm-slim AS build
1084
- ARG SQLITE_PREBUILD_MIRROR
1085
- ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
1086
- ENV PNPM_HOME=/pnpm
1087
- ENV PATH=$PNPM_HOME:$PATH
1088
- ENV CI=true
1089
- ENV HAI_DEPLOY_ENVIRONMENT=${environment}
1090
- ENV HAI_CORE_ENV=${coreEnv}
1091
- WORKDIR /workspace
1092
- RUN corepack enable && corepack prepare pnpm@12.11.2 --activate
1093
- COPY . .
1094
- RUN pnpm install --frozen-lockfile --ignore-scripts
1095
- RUN pnpm --filter ${packageName}... build
1096
- RUN if [ -d node_modules/better-sqlite3 ]; then cd node_modules/better-sqlite3; for attempt in 1 2 3; do node -e "require('node:child_process').execFileSync(process.execPath,[require.resolve('prebuild-install/bin.js'),'--verbose'],{stdio:'inherit'})" && break; [ "$attempt" = 3 ] && exit 1; sleep 3; done; fi
1097
- RUN pnpm pm deploy --filter ${packageName} --prod /prod/app
1098
-
1099
- FROM node:24-bookworm-slim AS runtime
1100
- ENV NODE_ENV=production
1101
- ENV HAI_DEPLOY_ENVIRONMENT=${environment}
1102
- ENV HAI_CORE_ENV=${coreEnv}
1103
- ENV HOST=0.0.0.0
1104
- ENV PORT=${containerPort}
1105
- WORKDIR /app
1106
- COPY --from=build --chown=node:node /prod/app ./
1107
- RUN mkdir -p data && chown node:node data
1108
- USER node
1109
- EXPOSE ${containerPort}
1110
- CMD ["node", ${JSON.stringify(options.entrypoint ?? "build")}]
1111
- `;
1112
- }
1113
- var logger4 = core.logger.child({ module: "deploy", scope: "command" });
1114
- function maskSecrets(text, secrets) {
1115
- if (!secrets || secrets.length === 0)
1116
- return text;
1117
- let masked = text;
1118
- for (const secret of secrets) {
1119
- if (secret && secret.length > 0)
1120
- masked = masked.split(secret).join("***");
1121
- }
1122
- return masked;
1123
- }
1124
- function runCommand(command, args, options = {}) {
1125
- return new Promise((resolve3) => {
1126
- const child = spawn(command, args, {
1127
- cwd: options.cwd,
1128
- env: options.env ? { ...process2.env, ...options.env } : process2.env,
1129
- shell: false
1130
- });
1131
- let stdout = "";
1132
- let stderr = "";
1133
- let settled = false;
1134
- let timer;
1135
- const finish = (result) => {
1136
- if (settled)
1137
- return;
1138
- settled = true;
1139
- if (timer)
1140
- clearTimeout(timer);
1141
- resolve3(result);
1142
- };
1143
- if (options.timeoutMs && options.timeoutMs > 0) {
1144
- timer = setTimeout(() => {
1145
- child.kill("SIGKILL");
1146
- logger4.error("Command timed out", { command, timeoutMs: options.timeoutMs });
1147
- finish(err(
1148
- HaiDeployError.REMOTE_COMMAND_FAILED,
1149
- deployM("deploy_commandTimeout", { params: { command } })
1150
- ));
1151
- }, options.timeoutMs);
1152
- }
1153
- child.stdout.on("data", (chunk) => {
1154
- stdout += chunk.toString("utf-8");
1155
- });
1156
- child.stderr.on("data", (chunk) => {
1157
- stderr += chunk.toString("utf-8");
1158
- });
1159
- child.on("error", (error) => {
1160
- logger4.error("Command failed to start", { command, error: maskSecrets(String(error), options.secrets) });
1161
- finish(err(
1162
- HaiDeployError.REMOTE_COMMAND_FAILED,
1163
- deployM("deploy_commandFailed", {
1164
- params: { command, error: maskSecrets(error.message, options.secrets) }
1165
- }),
1166
- error
1167
- ));
1168
- });
1169
- child.on("close", (code) => {
1170
- const exitCode = code ?? -1;
1171
- if (exitCode === 0) {
1172
- finish(ok({ code: exitCode, stdout, stderr }));
1173
- return;
1174
- }
1175
- logger4.error("Command exited with non-zero code", {
1176
- command,
1177
- code: exitCode,
1178
- stderr: maskSecrets(stderr, options.secrets).slice(-500),
1179
- stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
1180
- });
1181
- finish(err(
1182
- HaiDeployError.REMOTE_COMMAND_FAILED,
1183
- deployM("deploy_commandFailed", {
1184
- params: { command, error: maskSecrets(`${stdout.slice(-2500)}
1185
- ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
1186
- })
1187
- ));
1188
- });
1189
- if (options.input !== void 0) {
1190
- child.stdin.write(options.input);
1191
- child.stdin.end();
1192
- } else {
1193
- child.stdin.end();
1194
- }
1195
- });
1196
- }
1197
-
1198
- // src/container/deploy-container-runtime.ts
1199
- var logger5 = core.logger.child({ module: "deploy", scope: "container-runtime" });
1200
- async function detectLocalRuntime(preferred = "auto") {
1201
- const candidates = preferred === "auto" ? ["podman", "docker"] : [preferred];
1202
- for (const runtime of candidates) {
1203
- const probe = await runCommand(runtime, ["version"], { timeoutMs: 15e3 });
1204
- if (probe.success) {
1205
- logger5.info("Local container runtime detected", { runtime });
1206
- return ok(runtime);
1207
- }
1208
- }
1209
- return err(
1210
- HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND,
1211
- deployM("deploy_containerRuntimeNotFound", { params: { runtime: candidates.join(" / ") } })
1212
- );
1213
- }
1214
- async function buildImage(runtime, options) {
1215
- const args = ["build", "-f", options.dockerfilePath, "-t", options.imageRef];
1216
- if (runtime === "podman")
1217
- args.push("--format", "docker", "--jobs", "1");
1218
- if (runtime === "podman" && existsSync(`${options.dockerfilePath}.dockerignore`))
1219
- args.push("--ignorefile", `${options.dockerfilePath}.dockerignore`);
1220
- if (runtime === "podman" && options.httpProxy === false)
1221
- args.push("--http-proxy=false");
1222
- for (const [key, value] of Object.entries(options.buildArgs ?? {}))
1223
- args.push("--build-arg", `${key}=${value}`);
1224
- args.push(options.contextDir);
1225
- logger5.info("Building container image", { runtime, imageRef: options.imageRef });
1226
- const result = await runCommand(runtime, args, { cwd: options.contextDir });
1227
- if (!result.success) {
1228
- return err(
1229
- HaiDeployError.CONTAINER_BUILD_FAILED,
1230
- deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
1231
- result.error
1232
- );
1233
- }
1234
- return ok(void 0);
1235
- }
1236
- async function saveImage(runtime, imageRef, tarPath) {
1237
- logger5.info("Saving container image", { runtime, imageRef, tarPath });
1238
- const result = await runCommand(runtime, ["save", "-o", tarPath, imageRef], {});
1239
- if (!result.success) {
1240
- return err(
1241
- HaiDeployError.CONTAINER_BUILD_FAILED,
1242
- deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
1243
- result.error
1244
- );
1245
- }
1246
- return ok(void 0);
1247
- }
1248
- var logger6 = core.logger.child({ module: "deploy", scope: "ssh" });
1293
+ var logger4 = core.logger.child({ module: "deploy", scope: "ssh" });
1249
1294
  function buildConnectOptions(config) {
1250
1295
  const args = ["-o", "BatchMode=yes"];
1251
1296
  args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
@@ -1296,7 +1341,7 @@ function createSshExecutor(config) {
1296
1341
  return ok(void 0);
1297
1342
  };
1298
1343
  const ping = async () => {
1299
- logger6.debug("SSH preflight", { host: config.host, port: config.port });
1344
+ logger4.debug("SSH preflight", { host: config.host, port: config.port });
1300
1345
  const result = await exec("true", { timeoutMs: 2e4 });
1301
1346
  if (!result.success) {
1302
1347
  return err(
@@ -1311,52 +1356,24 @@ function createSshExecutor(config) {
1311
1356
  }
1312
1357
 
1313
1358
  // src/providers/deploy-provider-docker-ssh.ts
1314
- var logger7 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
1359
+ var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
1315
1360
  var HEALTH_POLL_INTERVAL = 3e3;
1316
1361
  var HEALTH_MAX_ATTEMPTS = 40;
1317
- function shellArgument(value) {
1318
- return `'${value.replaceAll("'", `'"'"'`)}'`;
1319
- }
1320
1362
  function makeDeploymentId() {
1321
1363
  const now = /* @__PURE__ */ new Date();
1322
1364
  const pad = (n) => String(n).padStart(2, "0");
1323
1365
  const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
1324
1366
  return `${stamp}-${randomBytes(3).toString("hex")}`;
1325
1367
  }
1326
- function parseLoadedImageRef(stdout, fallback) {
1327
- const match = stdout.match(/Loaded image(?:\(s\))?:\s*(\S+)/);
1328
- return match ? match[1] : fallback;
1329
- }
1330
1368
  function sleep(ms) {
1331
- return new Promise((resolve3) => setTimeout(resolve3, ms));
1332
- }
1333
- function readPackageName(appDir) {
1334
- try {
1335
- const pkg = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8"));
1336
- return pkg.name ?? "app";
1337
- } catch {
1338
- return "app";
1339
- }
1340
- }
1341
- function resolveWorkspaceRoot(appDir) {
1342
- if (existsSync(join(appDir, "pnpm-workspace.yaml")))
1343
- return appDir;
1344
- let current = appDir;
1345
- let parent = dirname(current);
1346
- while (parent !== current) {
1347
- if (existsSync(join(parent, "pnpm-workspace.yaml")))
1348
- return parent;
1349
- current = parent;
1350
- parent = dirname(parent);
1351
- }
1352
- return appDir;
1369
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
1353
1370
  }
1354
1371
  function createDockerSshProvider(config, container) {
1355
- const localRuntimePref = container?.runtime ?? "auto";
1356
- const httpProxy = container?.httpProxy ?? true;
1357
- const ssh = createSshExecutor(config.ssh);
1372
+ const ssh = config.ssh ? createSshExecutor(config.ssh) : void 0;
1358
1373
  const baseDir = config.remote.baseDir;
1359
1374
  async function detectRemoteRuntime() {
1375
+ if (!ssh)
1376
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
1360
1377
  const candidates = config.remote.runtime === "auto" ? ["podman", "docker"] : [config.remote.runtime];
1361
1378
  for (const runtime of candidates) {
1362
1379
  const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
@@ -1364,7 +1381,7 @@ function createDockerSshProvider(config, container) {
1364
1381
  continue;
1365
1382
  const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
1366
1383
  if (composeResult.success) {
1367
- logger7.info("Remote container runtime detected", { runtime });
1384
+ logger5.info("Remote container runtime detected", { runtime });
1368
1385
  return ok(runtime);
1369
1386
  }
1370
1387
  }
@@ -1388,29 +1405,31 @@ function createDockerSshProvider(config, container) {
1388
1405
  const deploy2 = async (context) => {
1389
1406
  const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
1390
1407
  const deploymentId = makeDeploymentId();
1391
- const containerPort = config.expose.containerPort;
1392
1408
  const hostPort = config.expose.hostPort;
1393
- const localImageTag = `hai-${projectName}:${deploymentId}`;
1394
- const runtimeResult = await detectLocalRuntime(localRuntimePref);
1395
- if (!runtimeResult.success)
1396
- return err(runtimeResult.error);
1397
- const localRuntime = runtimeResult.data;
1398
1409
  const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
1399
- mkdirSync(artifactDir, { recursive: true });
1400
- const appDockerfile = join(appDir, "Dockerfile");
1401
- let dockerfilePath;
1402
- let contextDir;
1403
- if (existsSync(appDockerfile)) {
1404
- dockerfilePath = appDockerfile;
1405
- contextDir = appDir;
1406
- } else {
1407
- dockerfilePath = join(artifactDir, "Dockerfile");
1408
- writeFileSync(dockerfilePath, generateFallbackDockerfile(readPackageName(appDir), containerPort, { environment: options?.environment, entrypoint: scan.isSvelteKit ? "build" : "dist/index.js" }));
1409
- writeFileSync(`${dockerfilePath}.dockerignore`, generateDockerignore());
1410
- contextDir = resolveWorkspaceRoot(appDir);
1411
- logger7.info("Generated fallback Dockerfile", { dockerfilePath, contextDir });
1412
- }
1410
+ mkdirSync(artifactDir, { recursive: true, mode: 448 });
1411
+ chmodSync(artifactDir, 448);
1412
+ const artifact = context.artifact;
1413
+ const resolvedBuildDir = context.artifactDir;
1414
+ const images = artifact.images;
1415
+ const web = images.find((image) => image.name === "web");
1416
+ const app = images.find((image) => image.name === "app");
1417
+ const containerPort = app.containerPort ?? config.expose.containerPort;
1418
+ const url = config.ssh ? `http://${config.ssh.host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
1413
1419
  const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
1420
+ let webProxy;
1421
+ if (web) {
1422
+ const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
1423
+ if (!server.success)
1424
+ return server;
1425
+ const http = server.data.http;
1426
+ const cors = server.data.cors;
1427
+ const apiPrefix = http?.apiPrefix ?? "/api/v1";
1428
+ if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
1429
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
1430
+ webProxy = generateWebProxyConfig(containerPort, apiPrefix, web.containerPort ?? 8080);
1431
+ env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config.ssh ? url : cors?.origin ?? "*" } });
1432
+ }
1414
1433
  const envPath = join(artifactDir, ".env.runtime");
1415
1434
  const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
1416
1435
  Object.assign(env, { ...previousEnv, ...env });
@@ -1423,88 +1442,64 @@ function createDockerSshProvider(config, container) {
1423
1442
  if (!saved.success)
1424
1443
  return saved;
1425
1444
  }
1426
- const buildResult = await buildImage(localRuntime, {
1427
- contextDir,
1428
- dockerfilePath,
1429
- imageRef: localImageTag,
1430
- buildArgs: { HAI_DEPLOY_ENVIRONMENT: options?.environment ?? "prod", HAI_CORE_ENV: env.HAI_CORE_ENV ?? "production", SQLITE_PREBUILD_MIRROR: container?.sqlitePrebuildMirror ?? "" },
1431
- httpProxy
1432
- });
1433
- if (!buildResult.success)
1434
- return err(buildResult.error);
1435
- const tarPath = join(artifactDir, `${projectName}.tar`);
1436
- const saveResult = await saveImage(localRuntime, localImageTag, tarPath);
1437
- if (!saveResult.success)
1438
- return err(saveResult.error);
1439
- const pingResult = await ssh.ping();
1440
- if (!pingResult.success)
1441
- return err(pingResult.error);
1442
- const remoteRuntimeResult = await detectRemoteRuntime();
1443
- if (!remoteRuntimeResult.success)
1444
- return err(remoteRuntimeResult.error);
1445
- const remoteRuntime = remoteRuntimeResult.data;
1446
- const remoteDir = `${baseDir}/${projectName}`;
1447
- const prepareResult = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
1448
- if (!prepareResult.success)
1449
- return err(prepareResult.error);
1450
- const remoteTar = `${remoteDir}/image.tar`;
1451
- const uploadTarResult = await ssh.upload(tarPath, remoteTar);
1452
- if (!uploadTarResult.success)
1453
- return err(uploadTarResult.error);
1454
- const loadResult = await ssh.exec(`${remoteRuntime} load -i ${shellArgument(remoteTar)}`, { timeoutMs: 3e5 });
1455
- if (!loadResult.success)
1456
- return err(loadResult.error);
1457
- const loadedRef = parseLoadedImageRef(loadResult.data.stdout, localImageTag);
1458
- const remoteImageRef = remoteRuntime === "podman" ? `localhost/${localImageTag}` : localImageTag;
1459
- if (loadedRef !== remoteImageRef) {
1460
- const tagResult = await ssh.exec(`${remoteRuntime} tag ${shellArgument(loadedRef)} ${shellArgument(remoteImageRef)}`);
1461
- if (!tagResult.success)
1462
- return err(tagResult.error);
1463
- }
1445
+ const composeName = `docker-compose-${artifact.appVersion}.yml`;
1446
+ const latestPath = join(artifactDir, "latest");
1447
+ const previousName = existsSync(latestPath) ? readFileSync(latestPath, "utf8").trim() : "";
1448
+ const previousPath = /^[\w.+-]+\.yml$/.test(previousName) ? join(artifactDir, previousName) : void 0;
1464
1449
  const composeContent = generateComposeFile({
1465
1450
  projectName,
1466
- imageRef: remoteImageRef,
1451
+ appVersion: artifact.appVersion,
1452
+ imageRef: app.imageRef,
1467
1453
  hostPort,
1468
1454
  containerPort,
1469
1455
  sidecar,
1456
+ web: web ? { imageRef: web.imageRef, containerPort: web.containerPort ?? 8080 } : void 0,
1470
1457
  serviceModes: options?.serviceModes,
1471
- previousCompose: existsSync(join(artifactDir, "compose.yml")) ? readFileSync(join(artifactDir, "compose.yml"), "utf8") : void 0
1458
+ previousCompose: previousPath && existsSync(previousPath) ? readFileSync(previousPath, "utf8") : void 0
1472
1459
  });
1473
- const composePath = join(artifactDir, "compose.yml");
1460
+ const composePath = join(artifactDir, composeName);
1474
1461
  writeFileSync(composePath, composeContent);
1475
- const uploadComposeResult = await ssh.upload(composePath, `${remoteDir}/compose.yml`);
1476
- if (!uploadComposeResult.success)
1477
- return err(uploadComposeResult.error);
1478
- const uploadEnvResult = await ssh.upload(envPath, `${remoteDir}/.env.runtime`);
1479
- if (!uploadEnvResult.success)
1480
- return err(uploadEnvResult.error);
1481
- const startScript = `#!/bin/sh
1482
- set -eu
1483
- cd "$(dirname "$0")"
1484
- export HAI_DEPLOY_ENVIRONMENT=${options?.environment ?? "prod"}
1485
- export HAI_CORE_ENV=${shellArgument(env.HAI_CORE_ENV ?? "production")}
1486
- exec ${remoteRuntime} compose --env-file .env.runtime -p hai-${projectName} up -d
1487
- `;
1488
- const startName = `start-${options?.environment ?? "prod"}.sh`;
1489
- const startPath = join(artifactDir, startName);
1490
- writeFileSync(startPath, startScript, { mode: 448 });
1491
- const uploadStart = await ssh.upload(startPath, `${remoteDir}/${startName}`);
1492
- if (!uploadStart.success)
1493
- return uploadStart;
1494
- const secretMask = sidecar.secrets;
1495
- const chmodResult = await ssh.exec(`chmod 600 -- ${shellArgument(`${remoteDir}/.env.runtime`)}`, { secrets: secretMask });
1496
- if (!chmodResult.success)
1497
- return err(chmodResult.error);
1498
- const upResult = await ssh.exec(
1499
- `cd ${shellArgument(remoteDir)} && sh ${shellArgument(startName)}`,
1500
- { timeoutMs: 3e5, secrets: secretMask }
1462
+ if (webProxy)
1463
+ writeFileSync(join(artifactDir, "web.conf"), webProxy);
1464
+ const bundle = await createDeploymentBundle({
1465
+ directory: artifactDir,
1466
+ artifactDir: resolvedBuildDir,
1467
+ artifact,
1468
+ projectName,
1469
+ environment: options?.environment ?? "prod",
1470
+ runtime: config.remote.runtime,
1471
+ coreEnv: env.HAI_CORE_ENV ?? "production",
1472
+ output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
1473
+ });
1474
+ writeFileSync(latestPath, `${composeName}
1475
+ `);
1476
+ if (options?.bundleOnly)
1477
+ return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
1478
+ if (!ssh)
1479
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
1480
+ const ping = await ssh.ping();
1481
+ if (!ping.success)
1482
+ return ping;
1483
+ const remoteRuntime = await detectRemoteRuntime();
1484
+ if (!remoteRuntime.success)
1485
+ return remoteRuntime;
1486
+ const remoteDir = `${baseDir}/${projectName}`;
1487
+ const remoteTar = `${remoteDir}/${basename(bundle.bundlePath)}`;
1488
+ const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
1489
+ if (!prepared.success)
1490
+ return prepared;
1491
+ const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
1492
+ if (!uploaded.success)
1493
+ return uploaded;
1494
+ const started = await ssh.exec(
1495
+ `cd ${shellArgument(remoteDir)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} && HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=${shellArgument(remoteRuntime.data)} sh ${shellArgument(bundle.startName)}`,
1496
+ { timeoutMs: 3e5, secrets: sidecar.secrets }
1501
1497
  );
1502
- if (!upResult.success)
1503
- return err(upResult.error);
1504
- await ssh.exec(`rm -f -- ${shellArgument(remoteTar)}`);
1505
- const url = `http://${config.ssh.host}:${hostPort}`;
1506
- logger7.info("Running health check", { url });
1507
- const healthy = await healthCheck(`${url}${scan.isSvelteKit ? "/" : "/health"}`);
1498
+ if (!started.success)
1499
+ return started;
1500
+ await ssh.exec(`rm -f -- ${shellArgument(remoteTar)} ${images.map((image) => shellArgument(`${remoteDir}/images/${image.file}`)).join(" ")}`);
1501
+ logger5.info("Running health check", { url });
1502
+ const healthy = await healthCheck(`${url}${!web && scan.isSvelteKit ? "/" : "/health"}`) && (!web || await healthCheck(`${url}/_hai/web-health`) && await healthCheck(`${url}/`));
1508
1503
  if (!healthy) {
1509
1504
  return err(
1510
1505
  HaiDeployError.HEALTH_CHECK_FAILED,
@@ -1517,14 +1512,23 @@ exec ${remoteRuntime} compose --env-file .env.runtime -p hai-${projectName} up -
1517
1512
  status: "ready",
1518
1513
  provider: "docker-ssh",
1519
1514
  healthCheck: "passed",
1520
- envVarsSet: Object.keys(env)
1515
+ envVarsSet: Object.keys(env),
1516
+ bundlePath: bundle.bundlePath
1521
1517
  };
1522
- logger7.info("docker-ssh deployment complete", { url, deploymentId });
1518
+ logger5.info("docker-ssh deployment complete", { url, deploymentId });
1523
1519
  return ok(result);
1524
1520
  };
1525
- return { name: "docker-ssh", deploy: deploy2 };
1521
+ const deploySafely = async (context) => {
1522
+ try {
1523
+ return await deploy2(context);
1524
+ } catch (error) {
1525
+ logger5.error("Deployment bundle failed", { error });
1526
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"), error);
1527
+ }
1528
+ };
1529
+ return { name: "docker-ssh", deploy: deploySafely };
1526
1530
  }
1527
- var logger8 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
1531
+ var logger6 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
1528
1532
  var VERCEL_API = "https://api.vercel.com";
1529
1533
  var POLL_INTERVAL = 3e3;
1530
1534
  var MAX_POLL_ATTEMPTS = 60;
@@ -1577,7 +1581,7 @@ function createVercelProvider() {
1577
1581
  return {
1578
1582
  name: "vercel",
1579
1583
  async authenticate(apiToken) {
1580
- logger8.debug("Authenticating with Vercel");
1584
+ logger6.debug("Authenticating with Vercel");
1581
1585
  try {
1582
1586
  const user = await vercelFetch(
1583
1587
  apiToken,
@@ -1585,10 +1589,10 @@ function createVercelProvider() {
1585
1589
  );
1586
1590
  token = apiToken;
1587
1591
  const username = user.user.username || user.user.email;
1588
- logger8.info("Vercel authenticated", { user: username });
1592
+ logger6.info("Vercel authenticated", { user: username });
1589
1593
  return ok(username);
1590
1594
  } catch (error) {
1591
- logger8.error("Vercel authentication failed", { error });
1595
+ logger6.error("Vercel authentication failed", { error });
1592
1596
  return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error));
1593
1597
  }
1594
1598
  },
@@ -1599,11 +1603,11 @@ function createVercelProvider() {
1599
1603
  deployM("deploy_authRequired")
1600
1604
  );
1601
1605
  }
1602
- logger8.debug("Creating Vercel project", { projectName });
1606
+ logger6.debug("Creating Vercel project", { projectName });
1603
1607
  try {
1604
1608
  const existingId = await findExistingProject(token, projectName);
1605
1609
  if (existingId !== null) {
1606
- logger8.info("Vercel project found", { projectName, projectId: existingId });
1610
+ logger6.info("Vercel project found", { projectName, projectId: existingId });
1607
1611
  return ok(existingId);
1608
1612
  }
1609
1613
  try {
@@ -1618,13 +1622,13 @@ function createVercelProvider() {
1618
1622
  })
1619
1623
  }
1620
1624
  );
1621
- logger8.info("Vercel project created", { projectName, projectId: project.id });
1625
+ logger6.info("Vercel project created", { projectName, projectId: project.id });
1622
1626
  return ok(project.id);
1623
1627
  } catch (error) {
1624
1628
  if (error instanceof VercelApiError && error.status === 409) {
1625
1629
  const concurrentProjectId = await findExistingProject(token, projectName);
1626
1630
  if (concurrentProjectId !== null) {
1627
- logger8.info("Vercel project reused after create conflict", {
1631
+ logger6.info("Vercel project reused after create conflict", {
1628
1632
  projectName,
1629
1633
  projectId: concurrentProjectId
1630
1634
  });
@@ -1634,7 +1638,7 @@ function createVercelProvider() {
1634
1638
  throw error;
1635
1639
  }
1636
1640
  } catch (error) {
1637
- logger8.error("Vercel project creation failed", { projectName, error });
1641
+ logger6.error("Vercel project creation failed", { projectName, error });
1638
1642
  return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error));
1639
1643
  }
1640
1644
  },
@@ -1645,7 +1649,7 @@ function createVercelProvider() {
1645
1649
  deployM("deploy_authRequired")
1646
1650
  );
1647
1651
  }
1648
- logger8.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
1652
+ logger6.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
1649
1653
  try {
1650
1654
  const envEntries = Object.entries(envVars).map(([key, value]) => ({
1651
1655
  key,
@@ -1661,10 +1665,10 @@ function createVercelProvider() {
1661
1665
  body: JSON.stringify(envEntries)
1662
1666
  }
1663
1667
  );
1664
- logger8.info("Vercel env vars set", { projectId, count: envEntries.length });
1668
+ logger6.info("Vercel env vars set", { projectId, count: envEntries.length });
1665
1669
  return ok(void 0);
1666
1670
  } catch (error) {
1667
- logger8.error("Vercel env vars setup failed", { projectId, error });
1671
+ logger6.error("Vercel env vars setup failed", { projectId, error });
1668
1672
  return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error));
1669
1673
  }
1670
1674
  },
@@ -1675,8 +1679,11 @@ function createVercelProvider() {
1675
1679
  deployM("deploy_authRequired")
1676
1680
  );
1677
1681
  }
1678
- logger8.debug("Deploying to Vercel", { projectId, outputDir });
1682
+ logger6.debug("Deploying to Vercel", { projectId, outputDir });
1679
1683
  try {
1684
+ const outputConfig = JSON.parse(readFileSync(join(outputDir, "config.json"), "utf8"));
1685
+ if (outputConfig.version !== 3)
1686
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
1680
1687
  const filePaths = collectFiles(outputDir, outputDir);
1681
1688
  const files = [];
1682
1689
  for (const filePath of filePaths) {
@@ -1685,18 +1692,20 @@ function createVercelProvider() {
1685
1692
  const sha = createHash("sha1").update(content).digest("hex");
1686
1693
  const size = statSync(fullPath).size;
1687
1694
  await uploadFile(token, content, sha);
1688
- files.push({ file: filePath, sha, size });
1695
+ files.push({ file: `.vercel/output/${filePath}`, sha, size });
1689
1696
  }
1690
- logger8.debug("Files uploaded", { count: files.length });
1697
+ logger6.debug("Files uploaded", { count: files.length });
1691
1698
  const deployment = await vercelFetch(
1692
1699
  token,
1693
- "/v13/deployments",
1700
+ "/v13/deployments?prebuilt=1",
1694
1701
  {
1695
1702
  method: "POST",
1696
1703
  body: JSON.stringify({
1697
1704
  name: projectId,
1698
1705
  files,
1699
- projectSettings: { framework: "sveltekit" }
1706
+ version: 2,
1707
+ project: projectId,
1708
+ projectSettings: { framework: null, buildCommand: "", installCommand: "" }
1700
1709
  })
1701
1710
  }
1702
1711
  );
@@ -1707,14 +1716,14 @@ function createVercelProvider() {
1707
1716
  status: finalStatus,
1708
1717
  envVarsSet: []
1709
1718
  };
1710
- logger8.info("Vercel deployment complete", {
1719
+ logger6.info("Vercel deployment complete", {
1711
1720
  deploymentId: deployment.id,
1712
1721
  url: result.url,
1713
1722
  status: finalStatus
1714
1723
  });
1715
1724
  return ok(result);
1716
1725
  } catch (error) {
1717
- logger8.error("Vercel deployment failed", { projectId, error });
1726
+ logger6.error("Vercel deployment failed", { projectId, error });
1718
1727
  return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error));
1719
1728
  }
1720
1729
  }
@@ -1759,11 +1768,11 @@ async function pollDeploymentStatus(token, deploymentId) {
1759
1768
  if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
1760
1769
  return "error";
1761
1770
  }
1762
- await new Promise((resolve3) => setTimeout(resolve3, POLL_INTERVAL));
1771
+ await new Promise((resolve6) => setTimeout(resolve6, POLL_INTERVAL));
1763
1772
  }
1764
1773
  return "building";
1765
1774
  }
1766
- var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
1775
+ var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
1767
1776
  function createAliyunProvisioner() {
1768
1777
  let accessKey = null;
1769
1778
  let secretKey = null;
@@ -1772,7 +1781,7 @@ function createAliyunProvisioner() {
1772
1781
  name: "aliyun",
1773
1782
  serviceType: "sms",
1774
1783
  async authenticate(credentials) {
1775
- logger9.debug("Authenticating with Aliyun");
1784
+ logger7.debug("Authenticating with Aliyun");
1776
1785
  try {
1777
1786
  const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
1778
1787
  const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
@@ -1782,10 +1791,10 @@ function createAliyunProvisioner() {
1782
1791
  accessKey = akId;
1783
1792
  secretKey = akSecret;
1784
1793
  signName = credentials.signName;
1785
- logger9.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
1794
+ logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
1786
1795
  return ok(akId);
1787
1796
  } catch (error) {
1788
- logger9.error("Aliyun authentication failed", { error });
1797
+ logger7.error("Aliyun authentication failed", { error });
1789
1798
  return err(
1790
1799
  HaiDeployError.AUTH_FAILED,
1791
1800
  deployM("deploy_authFailed", {
@@ -1802,7 +1811,7 @@ function createAliyunProvisioner() {
1802
1811
  deployM("deploy_authRequired")
1803
1812
  );
1804
1813
  }
1805
- logger9.debug("Provisioning Aliyun SMS service");
1814
+ logger7.debug("Provisioning Aliyun SMS service");
1806
1815
  return ok({
1807
1816
  serviceType: "sms",
1808
1817
  provisionerName: "aliyun",
@@ -1815,7 +1824,7 @@ function createAliyunProvisioner() {
1815
1824
  }
1816
1825
  };
1817
1826
  }
1818
- var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
1827
+ var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
1819
1828
  var NEON_API = "https://console.neon.tech/api/v2";
1820
1829
  var DEFAULT_NEON_DATABASE_NAME = "neondb";
1821
1830
  var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
@@ -1874,7 +1883,7 @@ function createNeonProvisioner() {
1874
1883
  name: "neon",
1875
1884
  serviceType: "db",
1876
1885
  async authenticate(credentials) {
1877
- logger10.debug("Authenticating with Neon");
1886
+ logger8.debug("Authenticating with Neon");
1878
1887
  try {
1879
1888
  const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
1880
1889
  if (!apiToken) {
@@ -1882,10 +1891,10 @@ function createNeonProvisioner() {
1882
1891
  }
1883
1892
  await listNeonProjects(apiToken);
1884
1893
  token = apiToken;
1885
- logger10.info("Neon authenticated");
1894
+ logger8.info("Neon authenticated");
1886
1895
  return ok("neon-user");
1887
1896
  } catch (error) {
1888
- logger10.error("Neon authentication failed", { error });
1897
+ logger8.error("Neon authentication failed", { error });
1889
1898
  return err(
1890
1899
  HaiDeployError.AUTH_FAILED,
1891
1900
  deployM("deploy_authFailed", {
@@ -1902,7 +1911,7 @@ function createNeonProvisioner() {
1902
1911
  deployM("deploy_authRequired")
1903
1912
  );
1904
1913
  }
1905
- logger10.debug("Provisioning Neon database", { appName });
1914
+ logger8.debug("Provisioning Neon database", { appName });
1906
1915
  let resource;
1907
1916
  try {
1908
1917
  const projectName = `${appName}-db`;
@@ -1910,7 +1919,7 @@ function createNeonProvisioner() {
1910
1919
  if (existingProject !== null) {
1911
1920
  resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
1912
1921
  const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
1913
- logger10.info("Neon project reused", {
1922
+ logger8.info("Neon project reused", {
1914
1923
  projectId: existingProject.id,
1915
1924
  projectName
1916
1925
  });
@@ -1932,10 +1941,10 @@ function createNeonProvisioner() {
1932
1941
  if (!projectId || !connectionUri) {
1933
1942
  throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
1934
1943
  }
1935
- logger10.info("Neon database provisioned", { projectId });
1944
+ logger8.info("Neon database provisioned", { projectId });
1936
1945
  return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
1937
1946
  } catch (error) {
1938
- logger10.error("Neon provisioning failed", { appName });
1947
+ logger8.error("Neon provisioning failed", { appName });
1939
1948
  if (resource) {
1940
1949
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
1941
1950
  projectName: appName,
@@ -1957,7 +1966,7 @@ function createNeonProvisioner() {
1957
1966
  }
1958
1967
  };
1959
1968
  }
1960
- var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
1969
+ var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
1961
1970
  var CF_API = "https://api.cloudflare.com/client/v4";
1962
1971
  function createR2Provisioner() {
1963
1972
  let token = null;
@@ -1968,7 +1977,7 @@ function createR2Provisioner() {
1968
1977
  name: "r2",
1969
1978
  serviceType: "storage",
1970
1979
  async authenticate(credentials) {
1971
- logger11.debug("Authenticating with Cloudflare");
1980
+ logger9.debug("Authenticating with Cloudflare");
1972
1981
  try {
1973
1982
  const acctId = credentials.accountId ?? credentials.account_id ?? "";
1974
1983
  const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
@@ -1985,10 +1994,10 @@ function createR2Provisioner() {
1985
1994
  token = apiTok;
1986
1995
  accessKey = credentials.accessKeyId;
1987
1996
  secretKey = credentials.secretAccessKey;
1988
- logger11.info("Cloudflare R2 authenticated", { accountId: acctId });
1997
+ logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
1989
1998
  return ok(acctId);
1990
1999
  } catch (error) {
1991
- logger11.error("Cloudflare R2 authentication failed", { error });
2000
+ logger9.error("Cloudflare R2 authentication failed", { error });
1992
2001
  return err(
1993
2002
  HaiDeployError.AUTH_FAILED,
1994
2003
  deployM("deploy_authFailed", {
@@ -2006,7 +2015,7 @@ function createR2Provisioner() {
2006
2015
  );
2007
2016
  }
2008
2017
  const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
2009
- logger11.debug("Provisioning R2 bucket", { appName, bucketName });
2018
+ logger9.debug("Provisioning R2 bucket", { appName, bucketName });
2010
2019
  try {
2011
2020
  const createRes = await fetch(
2012
2021
  `${CF_API}/accounts/${accountId}/r2/buckets`,
@@ -2022,7 +2031,7 @@ function createR2Provisioner() {
2022
2031
  if (!createRes.ok && createRes.status !== 409) {
2023
2032
  throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
2024
2033
  }
2025
- logger11.info("R2 bucket provisioned", { bucketName });
2034
+ logger9.info("R2 bucket provisioned", { bucketName });
2026
2035
  return ok({
2027
2036
  serviceType: "storage",
2028
2037
  provisionerName: "r2",
@@ -2040,7 +2049,7 @@ function createR2Provisioner() {
2040
2049
  resourceInfo: `r2-bucket:${bucketName}`
2041
2050
  });
2042
2051
  } catch (error) {
2043
- logger11.error("R2 provisioning failed", { appName, error });
2052
+ logger9.error("R2 provisioning failed", { appName, error });
2044
2053
  return err(
2045
2054
  HaiDeployError.PROVISION_FAILED,
2046
2055
  deployM("deploy_provisionFailed", {
@@ -2055,7 +2064,7 @@ function createR2Provisioner() {
2055
2064
  }
2056
2065
  };
2057
2066
  }
2058
- var logger12 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
2067
+ var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
2059
2068
  var RESEND_API = "https://api.resend.com";
2060
2069
  function createResendProvisioner() {
2061
2070
  let token = null;
@@ -2064,7 +2073,7 @@ function createResendProvisioner() {
2064
2073
  name: "resend",
2065
2074
  serviceType: "email",
2066
2075
  async authenticate(credentials) {
2067
- logger12.debug("Authenticating with Resend");
2076
+ logger10.debug("Authenticating with Resend");
2068
2077
  try {
2069
2078
  const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
2070
2079
  if (!apiToken || !credentials.from) {
@@ -2078,10 +2087,10 @@ function createResendProvisioner() {
2078
2087
  }
2079
2088
  token = apiToken;
2080
2089
  from = credentials.from;
2081
- logger12.info("Resend authenticated");
2090
+ logger10.info("Resend authenticated");
2082
2091
  return ok("resend-user");
2083
2092
  } catch (error) {
2084
- logger12.error("Resend authentication failed", { error });
2093
+ logger10.error("Resend authentication failed", { error });
2085
2094
  return err(
2086
2095
  HaiDeployError.AUTH_FAILED,
2087
2096
  deployM("deploy_authFailed", {
@@ -2098,7 +2107,7 @@ function createResendProvisioner() {
2098
2107
  deployM("deploy_authRequired")
2099
2108
  );
2100
2109
  }
2101
- logger12.debug("Provisioning Resend email service");
2110
+ logger10.debug("Provisioning Resend email service");
2102
2111
  return ok({
2103
2112
  serviceType: "email",
2104
2113
  provisionerName: "resend",
@@ -2111,7 +2120,7 @@ function createResendProvisioner() {
2111
2120
  }
2112
2121
  };
2113
2122
  }
2114
- var logger13 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
2123
+ var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
2115
2124
  var UPSTASH_API = "https://api.upstash.com";
2116
2125
  function createUpstashAuthHeader(email, apiKey) {
2117
2126
  return `Basic ${Buffer.from(`${email}:${apiKey}`).toString("base64")}`;
@@ -2162,7 +2171,7 @@ function createUpstashProvisioner() {
2162
2171
  name: "upstash",
2163
2172
  serviceType: "cache",
2164
2173
  async authenticate(credentials) {
2165
- logger13.debug("Authenticating with Upstash");
2174
+ logger11.debug("Authenticating with Upstash");
2166
2175
  try {
2167
2176
  const userEmail = credentials.email ?? "";
2168
2177
  const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
@@ -2172,10 +2181,10 @@ function createUpstashProvisioner() {
2172
2181
  await listUpstashDatabases(userEmail, userKey);
2173
2182
  email = userEmail;
2174
2183
  apiKey = userKey;
2175
- logger13.info("Upstash authenticated", { email: userEmail });
2184
+ logger11.info("Upstash authenticated", { email: userEmail });
2176
2185
  return ok(userEmail);
2177
2186
  } catch (error) {
2178
- logger13.error("Upstash authentication failed", { error });
2187
+ logger11.error("Upstash authentication failed", { error });
2179
2188
  return err(
2180
2189
  HaiDeployError.AUTH_FAILED,
2181
2190
  deployM("deploy_authFailed", {
@@ -2192,7 +2201,7 @@ function createUpstashProvisioner() {
2192
2201
  deployM("deploy_authRequired")
2193
2202
  );
2194
2203
  }
2195
- logger13.debug("Provisioning Upstash Redis", { appName });
2204
+ logger11.debug("Provisioning Upstash Redis", { appName });
2196
2205
  let resource;
2197
2206
  try {
2198
2207
  const databaseName = `${appName}-cache`;
@@ -2204,7 +2213,7 @@ function createUpstashProvisioner() {
2204
2213
  }
2205
2214
  resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
2206
2215
  const detail = await getUpstashDatabase(email, apiKey, databaseId);
2207
- logger13.info("Upstash Redis reused", { databaseId, databaseName });
2216
+ logger11.info("Upstash Redis reused", { databaseId, databaseName });
2208
2217
  return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
2209
2218
  }
2210
2219
  const data = await upstashFetch(email, apiKey, "/v2/redis/database", {
@@ -2217,10 +2226,10 @@ function createUpstashProvisioner() {
2217
2226
  });
2218
2227
  if (data.database_id)
2219
2228
  resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
2220
- logger13.info("Upstash Redis provisioned", { databaseId: data.database_id });
2229
+ logger11.info("Upstash Redis provisioned", { databaseId: data.database_id });
2221
2230
  return ok(buildUpstashProvisionResult(data, "created"));
2222
2231
  } catch (error) {
2223
- logger13.error("Upstash provisioning failed", { appName });
2232
+ logger11.error("Upstash provisioning failed", { appName });
2224
2233
  if (resource) {
2225
2234
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
2226
2235
  projectName: appName,
@@ -2244,7 +2253,7 @@ function createUpstashProvisioner() {
2244
2253
  }
2245
2254
 
2246
2255
  // src/deploy-main.ts
2247
- var logger14 = core.logger.child({ module: "deploy", scope: "main" });
2256
+ var logger12 = core.logger.child({ module: "deploy", scope: "main" });
2248
2257
  var currentConfig = null;
2249
2258
  var currentProvider = null;
2250
2259
  var currentContainerProvider = null;
@@ -2329,7 +2338,7 @@ var deploy = {
2329
2338
  credentials: deployCredentials,
2330
2339
  async init(config) {
2331
2340
  if (initInProgress) {
2332
- logger14.warn("Deploy init already in progress, skipping concurrent call");
2341
+ logger12.warn("Deploy init already in progress, skipping concurrent call");
2333
2342
  return err(
2334
2343
  HaiDeployError.CONFIG_ERROR,
2335
2344
  deployM("deploy_configError", { params: { error: "init already in progress" } })
@@ -2338,13 +2347,13 @@ var deploy = {
2338
2347
  initInProgress = true;
2339
2348
  try {
2340
2349
  if (currentProvider || currentContainerProvider) {
2341
- logger14.warn("Deploy module is already initialized, reinitializing");
2350
+ logger12.warn("Deploy module is already initialized, reinitializing");
2342
2351
  await deploy.close();
2343
2352
  }
2344
- logger14.info("Initializing deploy module");
2353
+ logger12.info("Initializing deploy module");
2345
2354
  const parseResult = DeployConfigSchema.safeParse(config);
2346
2355
  if (!parseResult.success) {
2347
- logger14.error("Deploy config validation failed", { error: parseResult.error.message });
2356
+ logger12.error("Deploy config validation failed", { error: parseResult.error.message });
2348
2357
  return err(
2349
2358
  HaiDeployError.CONFIG_ERROR,
2350
2359
  deployM("deploy_configError", { params: { error: parseResult.error.message } }),
@@ -2357,14 +2366,14 @@ var deploy = {
2357
2366
  let containerProvider = null;
2358
2367
  if (parsed.provider.type === "docker-ssh") {
2359
2368
  containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
2360
- logger14.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh.host });
2369
+ logger12.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh?.host });
2361
2370
  } else {
2362
2371
  provider = createProviderByType(parsed.provider.type);
2363
2372
  const authResult = await provider.authenticate(parsed.provider.token);
2364
2373
  if (!authResult.success) {
2365
2374
  return err(authResult.error);
2366
2375
  }
2367
- logger14.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
2376
+ logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
2368
2377
  }
2369
2378
  currentProvisioners.clear();
2370
2379
  if (parsed.services) {
@@ -2376,14 +2385,14 @@ var deploy = {
2376
2385
  const credentials = extractCredentials(serviceConfig);
2377
2386
  const provAuthResult = await provisioner.authenticate(credentials);
2378
2387
  if (!provAuthResult.success) {
2379
- logger14.error("Provisioner authentication failed", {
2388
+ logger12.error("Provisioner authentication failed", {
2380
2389
  service: serviceType,
2381
2390
  provisioner: serviceConfig.provisioner
2382
2391
  });
2383
2392
  currentProvisioners.clear();
2384
2393
  return err(provAuthResult.error);
2385
2394
  }
2386
- logger14.info("Provisioner authenticated", {
2395
+ logger12.info("Provisioner authenticated", {
2387
2396
  service: serviceType,
2388
2397
  user: provAuthResult.data
2389
2398
  });
@@ -2393,7 +2402,7 @@ var deploy = {
2393
2402
  currentProvider = provider;
2394
2403
  currentContainerProvider = containerProvider;
2395
2404
  currentConfig = parsed;
2396
- logger14.info("Deploy module initialized", {
2405
+ logger12.info("Deploy module initialized", {
2397
2406
  provider: parsed.provider.type,
2398
2407
  provisioners: [...currentProvisioners.keys()]
2399
2408
  });
@@ -2403,7 +2412,7 @@ var deploy = {
2403
2412
  currentContainerProvider = null;
2404
2413
  currentConfig = null;
2405
2414
  currentProvisioners.clear();
2406
- logger14.error("Deploy module initialization failed", { error });
2415
+ logger12.error("Deploy module initialization failed", { error });
2407
2416
  return err(
2408
2417
  HaiDeployError.CONFIG_ERROR,
2409
2418
  deployM("deploy_configError", {
@@ -2418,15 +2427,15 @@ var deploy = {
2418
2427
  },
2419
2428
  async close() {
2420
2429
  if (!currentProvider && !currentContainerProvider) {
2421
- logger14.info("Deploy module already closed, skipping");
2430
+ logger12.info("Deploy module already closed, skipping");
2422
2431
  return;
2423
2432
  }
2424
- logger14.info("Closing deploy module");
2433
+ logger12.info("Closing deploy module");
2425
2434
  currentProvider = null;
2426
2435
  currentContainerProvider = null;
2427
2436
  currentConfig = null;
2428
2437
  currentProvisioners.clear();
2429
- logger14.info("Deploy module closed");
2438
+ logger12.info("Deploy module closed");
2430
2439
  },
2431
2440
  get config() {
2432
2441
  return currentConfig;
@@ -2435,22 +2444,22 @@ var deploy = {
2435
2444
  return currentProvider !== null || currentContainerProvider !== null;
2436
2445
  },
2437
2446
  async scan(appDir, options) {
2438
- logger14.debug("Scanning application", { appDir });
2447
+ logger12.debug("Scanning application", { appDir });
2439
2448
  return scanApp(appDir, options);
2440
2449
  },
2441
2450
  async provisionAll(projectName, services) {
2442
2451
  if (!currentProvider && !currentContainerProvider) {
2443
2452
  return notInitialized.result();
2444
2453
  }
2445
- logger14.info("Provisioning all services", { projectName, count: currentProvisioners.size });
2454
+ logger12.info("Provisioning all services", { projectName, count: currentProvisioners.size });
2446
2455
  const results = [];
2447
2456
  for (const [serviceType, provisioner] of currentProvisioners) {
2448
2457
  if (services && !services.includes(serviceType))
2449
2458
  continue;
2450
- logger14.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
2459
+ logger12.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
2451
2460
  const provResult = await provisioner.provision(projectName);
2452
2461
  if (!provResult.success) {
2453
- logger14.error("Service provisioning failed", {
2462
+ logger12.error("Service provisioning failed", {
2454
2463
  serviceType,
2455
2464
  provisioner: provisioner.name,
2456
2465
  code: provResult.error.code
@@ -2467,7 +2476,7 @@ var deploy = {
2467
2476
  });
2468
2477
  }
2469
2478
  results.push(provResult.data);
2470
- logger14.info("Service provisioned", { serviceType, provisioner: provisioner.name });
2479
+ logger12.info("Service provisioned", { serviceType, provisioner: provisioner.name });
2471
2480
  }
2472
2481
  return ok(results);
2473
2482
  },
@@ -2475,7 +2484,7 @@ var deploy = {
2475
2484
  if (!currentProvider && !currentContainerProvider) {
2476
2485
  return notInitialized.result();
2477
2486
  }
2478
- logger14.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
2487
+ logger12.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
2479
2488
  const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
2480
2489
  const scanResult = await scanApp(appDir, scanOptions);
2481
2490
  if (!scanResult.success) {
@@ -2491,11 +2500,13 @@ var deploy = {
2491
2500
  options = {
2492
2501
  ...options,
2493
2502
  ...scanOptions.environment ? { environment: environment.data.environment } : {},
2494
- ...options?.skipProvision ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
2503
+ ...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
2495
2504
  };
2496
2505
  if (currentContainerProvider) {
2497
2506
  return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
2498
2507
  }
2508
+ if (options?.bundleOnly)
2509
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
2499
2510
  if (!currentProvider) {
2500
2511
  return notInitialized.result();
2501
2512
  }
@@ -2507,9 +2518,11 @@ var deploy = {
2507
2518
  })
2508
2519
  );
2509
2520
  }
2521
+ if (!existsSync(resolveOutputDir(appDir, "vercel")))
2522
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
2510
2523
  let allEnvVars = appEnv;
2511
2524
  const recovery = { projectName, stage: "provision", resources: [] };
2512
- if (!options?.skipProvision) {
2525
+ if (!options?.skipMiddleware) {
2513
2526
  const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
2514
2527
  if (!provResults.success) {
2515
2528
  return err(provResults.error);
@@ -2534,12 +2547,6 @@ var deploy = {
2534
2547
  return recoveryFailure(envResult.error, { ...recovery, stage: "set-env" });
2535
2548
  }
2536
2549
  }
2537
- if (!options?.skipBuild) {
2538
- const buildResult = buildApp(appDir, scan.buildCommand, allEnvVars);
2539
- if (!buildResult.success) {
2540
- return recoveryFailure(buildResult.error, { ...recovery, stage: "build" });
2541
- }
2542
- }
2543
2550
  const outputDir = resolveOutputDir(appDir, currentConfig?.provider.type ?? "vercel");
2544
2551
  const deployResult = await currentProvider.deploy(projectId, outputDir);
2545
2552
  if (!deployResult.success) {
@@ -2549,7 +2556,7 @@ var deploy = {
2549
2556
  ...deployResult.data,
2550
2557
  envVarsSet: Object.keys(allEnvVars)
2551
2558
  };
2552
- logger14.info("Deployment completed", {
2559
+ logger12.info("Deployment completed", {
2553
2560
  url: result.url,
2554
2561
  deploymentId: result.deploymentId,
2555
2562
  status: result.status
@@ -2559,21 +2566,30 @@ var deploy = {
2559
2566
  };
2560
2567
  async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
2561
2568
  const projectName = normalizeProjectName(rawProjectName);
2562
- if (scan.isSvelteKit && !scan.nodeAdapterInstalled) {
2563
- return err(
2564
- HaiDeployError.ADAPTER_MISSING,
2565
- deployM("deploy_adapterMissing", { params: { adapter: "@sveltejs/adapter-node" } })
2566
- );
2569
+ let artifact;
2570
+ let artifactDir = options?.artifactDir ?? resolveArtifactDirectory(appDir);
2571
+ try {
2572
+ if (existsSync(join(artifactDir, "latest"))) {
2573
+ const latest = readFileSync(join(artifactDir, "latest"), "utf8").trim();
2574
+ if (!/^[a-z0-9][\w.+-]*$/i.test(latest))
2575
+ throw new Error("Invalid artifact pointer");
2576
+ artifactDir = join(artifactDir, latest);
2577
+ }
2578
+ artifact = await readDeploymentArtifact(artifactDir);
2579
+ } catch {
2580
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
2567
2581
  }
2582
+ if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
2583
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
2568
2584
  let provisionEnv = {};
2569
- if (!options?.skipProvision) {
2585
+ if (!options?.skipMiddleware) {
2570
2586
  const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
2571
2587
  if (!provResults.success) {
2572
2588
  return err(provResults.error);
2573
2589
  }
2574
2590
  provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
2575
2591
  }
2576
- return provider.deploy({ appDir, projectName, scan, provisionEnv, appEnv, options });
2592
+ return provider.deploy({ appDir, artifact, artifactDir, projectName, scan, provisionEnv, appEnv, options });
2577
2593
  }
2578
2594
 
2579
2595
  // src/deploy-middleware.ts
@@ -2621,6 +2637,6 @@ function describeMiddlewareBackend(backend) {
2621
2637
  return deployM(backend.purpose);
2622
2638
  }
2623
2639
 
2624
- export { DEPLOY_MIDDLEWARE, DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, saveDeploymentEnvironment };
2640
+ export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
2625
2641
  //# sourceMappingURL=index.js.map
2626
2642
  //# sourceMappingURL=index.js.map