@h-ai/deploy 0.1.0-alpha.64 → 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,15 +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
- import { core, err, ok } from '@h-ai/core';
3
- import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, statSync } from 'fs';
5
+ import { core, ok, err } from '@h-ai/core';
6
+ import process2 from 'process';
7
+ import { parseEnv } from 'util';
8
+ import { parse, stringify, parseAllDocuments } from 'yaml';
4
9
  import { homedir } from 'os';
5
- import { join, dirname, relative, sep } from 'path';
6
- import process from 'process';
7
- import { parse, stringify } from 'yaml';
8
- import { execSync, spawn } from 'child_process';
9
- import { randomBytes, createHash } from 'crypto';
10
+ import { spawn } from 'child_process';
10
11
  import { Buffer } from 'buffer';
11
12
 
12
- // 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
+ }
13
50
 
14
51
  // messages/en-US.json
15
52
  var en_US_default = {
@@ -21,7 +58,6 @@ var en_US_default = {
21
58
  deploy_authRequired: "Authentication required",
22
59
  deploy_authFailed: "Authentication failed: {error}",
23
60
  deploy_projectCreateFailed: "Project creation failed: {error}",
24
- deploy_buildFailed: "Application build failed: {error}",
25
61
  deploy_uploadFailed: "Build output upload failed: {error}",
26
62
  deploy_deployFailed: "Deployment failed: {error}",
27
63
  deploy_provisionFailed: "Service provisioning failed: {service} - {error}",
@@ -41,12 +77,33 @@ var en_US_default = {
41
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.",
42
78
  deploy_commandTimeout: "Command timed out: {command}",
43
79
  deploy_commandFailed: "Command failed ({command}): {error}",
44
- deploy_containerRuntimeNotFound: "No usable container runtime found: {runtime}",
45
- deploy_containerBuildFailed: "Container image build failed: {error}",
46
80
  deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
47
81
  deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
48
82
  deploy_imageTransferFailed: "Image transfer failed: {error}",
49
- deploy_healthCheckFailed: "Health check failed, app not ready at {url}"
83
+ deploy_healthCheckFailed: "Health check failed, app not ready at {url}",
84
+ deploy_invalidEnvironment: "Use letters, numbers, underscores or hyphens for the environment; local/example are reserved.",
85
+ deploy_environmentMissing: "Environment file missing: {path}. Create and configure it before deploying.",
86
+ deploy_environmentReadFailed: "Cannot read deployment environment files; check syntax and permissions.",
87
+ deploy_environmentWriteFailed: "Cannot save deployment configuration; check file syntax and permissions.",
88
+ deploy_multipleApplications: "Multiple service packages found; specify the application directory.",
89
+ deploy_purposeSqlite: "Local relational database persisted in a volume",
90
+ deploy_purposeDatabase: "Relational database service for business data",
91
+ deploy_purposeMemory: "Per-process memory cache; cleared on restart, no external service",
92
+ deploy_purposeLocal: "Local filesystem storage persisted in a volume",
93
+ deploy_purposeCache: "Shared cache, sessions and distributed locks",
94
+ deploy_purposeStorage: "S3-compatible object storage for uploaded files",
95
+ deploy_purposeVector: "Vector indexing and similarity search",
96
+ deploy_purposeDisabled: "LDAP login is disabled; no directory service is created",
97
+ deploy_purposeLdap: "Directory account authentication; the default directory has no business users",
98
+ deploy_purposeConsole: "Messages go to application logs without delivery",
99
+ deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
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."
50
107
  };
51
108
 
52
109
  // messages/zh-CN.json
@@ -59,7 +116,6 @@ var zh_CN_default = {
59
116
  deploy_authRequired: "\u8BF7\u5148\u5B8C\u6210\u8BA4\u8BC1",
60
117
  deploy_authFailed: "\u8BA4\u8BC1\u5931\u8D25\uFF1A{error}",
61
118
  deploy_projectCreateFailed: "\u9879\u76EE\u521B\u5EFA\u5931\u8D25\uFF1A{error}",
62
- deploy_buildFailed: "\u5E94\u7528\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
63
119
  deploy_uploadFailed: "\u6784\u5EFA\u4EA7\u7269\u4E0A\u4F20\u5931\u8D25\uFF1A{error}",
64
120
  deploy_deployFailed: "\u90E8\u7F72\u5931\u8D25\uFF1A{error}",
65
121
  deploy_provisionFailed: "\u57FA\u7840\u8BBE\u65BD\u5F00\u901A\u5931\u8D25\uFF1A{service} - {error}",
@@ -79,12 +135,33 @@ var zh_CN_default = {
79
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",
80
136
  deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
81
137
  deploy_commandFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08{command}\uFF09\uFF1A{error}",
82
- deploy_containerRuntimeNotFound: "\u672A\u627E\u5230\u53EF\u7528\u7684\u5BB9\u5668\u8FD0\u884C\u65F6\uFF1A{runtime}",
83
- deploy_containerBuildFailed: "\u5BB9\u5668\u955C\u50CF\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
84
138
  deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
85
139
  deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
86
140
  deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
87
- deploy_healthCheckFailed: "\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF0C\u5E94\u7528\u672A\u5728 {url} \u5C31\u7EEA"
141
+ deploy_healthCheckFailed: "\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF0C\u5E94\u7528\u672A\u5728 {url} \u5C31\u7EEA",
142
+ deploy_invalidEnvironment: "\u73AF\u5883\u540D\u79F0\u4EC5\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u8FDE\u5B57\u7B26\uFF0C\u4E14\u4E0D\u80FD\u4F7F\u7528 local/example\u3002",
143
+ deploy_environmentMissing: "\u7F3A\u5C11\u73AF\u5883\u914D\u7F6E {path}\uFF1B\u8BF7\u5148\u521B\u5EFA\u8BE5\u6587\u4EF6\u5E76\u5B8C\u6210\u914D\u7F6E\uFF0C\u518D\u91CD\u65B0\u90E8\u7F72\u3002",
144
+ deploy_environmentReadFailed: "\u90E8\u7F72\u73AF\u5883\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u683C\u5F0F\u4E0E\u6743\u9650\u3002",
145
+ deploy_environmentWriteFailed: "\u90E8\u7F72\u914D\u7F6E\u5199\u5165\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u683C\u5F0F\u4E0E\u6743\u9650\u3002",
146
+ deploy_multipleApplications: "\u53D1\u73B0\u591A\u4E2A\u670D\u52A1\u5305\uFF0C\u8BF7\u6307\u5B9A\u8981\u90E8\u7F72\u7684\u5E94\u7528\u76EE\u5F55\u3002",
147
+ deploy_purposeSqlite: "\u672C\u5730\u6587\u4EF6\u5173\u7CFB\u6570\u636E\u5E93\uFF0C\u4F7F\u7528\u6301\u4E45\u5316\u5377",
148
+ deploy_purposeDatabase: "\u72EC\u7ACB\u5173\u7CFB\u6570\u636E\u5E93\u670D\u52A1\uFF0C\u4FDD\u5B58\u4E1A\u52A1\u6570\u636E",
149
+ deploy_purposeMemory: "\u5355\u8FDB\u7A0B\u5185\u5B58\u7F13\u5B58\uFF0C\u91CD\u542F\u540E\u6E05\u7A7A\uFF0C\u4E0D\u521B\u5EFA\u5916\u90E8\u670D\u52A1",
150
+ deploy_purposeLocal: "\u672C\u5730\u6587\u4EF6\u5B58\u50A8\uFF0C\u4F7F\u7528\u6301\u4E45\u5316\u5377",
151
+ deploy_purposeCache: "\u5171\u4EAB\u7F13\u5B58\u3001\u4F1A\u8BDD\u4E0E\u5206\u5E03\u5F0F\u9501",
152
+ deploy_purposeStorage: "S3 \u517C\u5BB9\u5BF9\u8C61\u5B58\u50A8\uFF0C\u4FDD\u5B58\u4E0A\u4F20\u6587\u4EF6",
153
+ deploy_purposeVector: "\u5411\u91CF\u7D22\u5F15\u4E0E\u76F8\u4F3C\u5EA6\u641C\u7D22",
154
+ deploy_purposeDisabled: "LDAP \u767B\u5F55\u672A\u542F\u7528\uFF0C\u4E0D\u521B\u5EFA\u76EE\u5F55\u670D\u52A1",
155
+ deploy_purposeLdap: "\u4F01\u4E1A\u76EE\u5F55\u8D26\u53F7\u8BA4\u8BC1\uFF1B\u9ED8\u8BA4\u76EE\u5F55\u4E0D\u5305\u542B\u4E1A\u52A1\u7528\u6237",
156
+ deploy_purposeConsole: "\u6D88\u606F\u8F93\u51FA\u5230\u5E94\u7528\u65E5\u5FD7\uFF0C\u4E0D\u5B9E\u9645\u6295\u9012",
157
+ deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
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"
88
165
  };
89
166
 
90
167
  // src/deploy-i18n.ts
@@ -117,15 +194,15 @@ var SshConfigSchema = z.object({
117
194
  var PortExposeSchema = z.object({
118
195
  type: z.literal("port").default("port"),
119
196
  /** 容器内监听端口(adapter-node 默认 3000) */
120
- containerPort: z.number().int().positive().default(3e3),
197
+ containerPort: z.number().int().min(1).max(65535).default(3e3),
121
198
  /** 宿主机映射端口 */
122
- hostPort: z.number().int().positive()
199
+ hostPort: z.number().int().min(1).max(65535)
123
200
  });
124
201
  var ExposeSchema = PortExposeSchema;
125
202
  var DockerSshProviderConfigSchema = z.object({
126
203
  type: z.literal("docker-ssh"),
127
204
  /** SSH 连接参数 */
128
- ssh: SshConfigSchema,
205
+ ssh: SshConfigSchema.optional(),
129
206
  /** 远程主机运行参数 */
130
207
  remote: z.object({
131
208
  /** 远程应用根目录 */
@@ -175,17 +252,19 @@ var ServicesConfigSchema = z.object({
175
252
  sms: z.discriminatedUnion("provisioner", [AliyunSmsServiceSchema]).optional()
176
253
  }).optional();
177
254
  var DeployConfigSchema = z.object({
255
+ /** 部署环境文件模式;dev/test/prod 或自定义模式,禁止 local/example。 */
256
+ environment: z.string().regex(/^[a-z0-9][\w-]*$/i).refine((value) => !["local", "example"].includes(value.toLowerCase())).optional(),
178
257
  provider: ProviderConfigSchema,
179
258
  services: ServicesConfigSchema,
180
- /** 本地容器构建参数(docker-ssh 使用;auto 优先 podman) */
259
+ /** Docker Compose 部署参数;构建配置位于项目 hai.build.json。 */
181
260
  container: z.object({
182
- runtime: z.enum(["auto", "docker", "podman"]).default("auto")
261
+ /** 自建 S3 服务实现;协议仍为 storage.type=s3。 */
262
+ s3Engine: z.enum(["rustfs", "minio"]).default("rustfs")
183
263
  }).optional()
184
264
  });
185
265
  var DeployErrorInfo = {
186
266
  DEPLOY_FAILED: "001:500",
187
267
  PROJECT_CREATE_FAILED: "002:500",
188
- BUILD_FAILED: "003:500",
189
268
  UPLOAD_FAILED: "004:500",
190
269
  AUTH_REQUIRED: "005:401",
191
270
  AUTH_FAILED: "006:401",
@@ -197,8 +276,6 @@ var DeployErrorInfo = {
197
276
  UNSUPPORTED_TYPE: "012:400",
198
277
  CONFIG_ERROR: "013:500",
199
278
  CREDENTIAL_ERROR: "014:500",
200
- CONTAINER_RUNTIME_NOT_FOUND: "015:500",
201
- CONTAINER_BUILD_FAILED: "016:500",
202
279
  REMOTE_CONNECT_FAILED: "017:500",
203
280
  REMOTE_COMMAND_FAILED: "018:500",
204
281
  IMAGE_TRANSFER_FAILED: "019:500",
@@ -206,7 +283,172 @@ var DeployErrorInfo = {
206
283
  };
207
284
  var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
208
285
 
209
- // src/deploy-credentials.ts
286
+ // src/deploy-environment.ts
287
+ function environmentValue(value) {
288
+ try {
289
+ return parse(value) ?? value;
290
+ } catch {
291
+ return value;
292
+ }
293
+ }
294
+ function resolveConfig(value, prefix, env) {
295
+ if (env[prefix] !== void 0)
296
+ return environmentValue(env[prefix]);
297
+ if (Array.isArray(value))
298
+ return value.map((item, index) => resolveConfig(item, `${prefix}_${index}`, env));
299
+ if (value && typeof value === "object")
300
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, resolveConfig(nested, `${prefix}_${key.toUpperCase()}`, env)]));
301
+ if (typeof value !== "string")
302
+ return value;
303
+ const resolved = value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, name, fallback) => env[name] ?? fallback ?? "");
304
+ return /^\$\{[^}]+\}$/.test(value) ? environmentValue(resolved) : resolved;
305
+ }
306
+ function loadDeploymentConfig(name, path, values) {
307
+ try {
308
+ const config = resolveConfig(parse(readFileSync(path, "utf8")), `HAI_${name.toUpperCase()}`, values);
309
+ return ok(config && typeof config === "object" && !Array.isArray(config) ? config : {});
310
+ } catch {
311
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
312
+ }
313
+ }
314
+ function loadDeploymentEnvironment(options) {
315
+ const environment = options.environment === "production" ? "prod" : options.environment === "development" ? "dev" : options.environment;
316
+ if (!/^[a-z0-9][\w-]*$/i.test(environment) || ["local", "example"].includes(environment.toLowerCase()))
317
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_invalidEnvironment"));
318
+ const envDir = resolve(options.envDir);
319
+ const target = join(envDir, `.env.${environment}`);
320
+ if (options.requireFile !== false && !existsSync(target))
321
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentMissing", { params: { path: target } }));
322
+ const loaded = core.config.loadEnvFiles({ mode: environment, envDir });
323
+ if (!loaded.success)
324
+ return loaded;
325
+ try {
326
+ const values = { ...loaded.data };
327
+ for (const [name, value] of Object.entries(process2.env)) {
328
+ if (/^(?:HAI_|PUBLIC_|VITE_)/.test(name) && value !== void 0)
329
+ values[name] = value;
330
+ }
331
+ values.HAI_DEPLOY_ENVIRONMENT = environment;
332
+ values.HAI_CORE_ENV ??= environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
333
+ const targetValues = {};
334
+ if (existsSync(target)) {
335
+ for (const [name, value] of Object.entries(parseEnv(readFileSync(target, "utf8")))) {
336
+ if (value !== void 0)
337
+ targetValues[name] = value;
338
+ }
339
+ }
340
+ return ok({ environment, envDir, values, targetValues });
341
+ } catch {
342
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
343
+ }
344
+ }
345
+ function isSecret(name, value) {
346
+ if (/PASSWORD|SECRET|TOKEN|APIKEY|PRIVATEKEY|ACCESSKEY|AUTHORIZATION/i.test(name.replace(/[^a-z0-9]/gi, "")) || /(?:^|_)PASS$/i.test(name) || value.includes("PRIVATE KEY"))
347
+ return true;
348
+ try {
349
+ const url = new URL(value);
350
+ if (url.username || url.password || [...url.searchParams.keys()].some((key) => /password|secret|token|key|authorization/i.test(key)))
351
+ return true;
352
+ } catch {
353
+ }
354
+ try {
355
+ const parsed = JSON.parse(value);
356
+ if (parsed && typeof parsed === "object")
357
+ return Object.entries(parsed).some(([key, nested]) => isSecret(key, typeof nested === "string" ? nested : JSON.stringify(nested)));
358
+ } catch {
359
+ }
360
+ return false;
361
+ }
362
+ function publicConfig(value) {
363
+ if (Array.isArray(value))
364
+ return value.map(publicConfig);
365
+ if (value && typeof value === "object")
366
+ return Object.fromEntries(Object.entries(value).filter(([name, item]) => !isSecret(name, typeof item === "string" ? item : "")).map(([name, item]) => [name, publicConfig(item)]));
367
+ return value;
368
+ }
369
+ function quoteEnv(value) {
370
+ try {
371
+ const parsed = JSON.parse(value);
372
+ if (parsed && typeof parsed === "object")
373
+ return `'${JSON.stringify(parsed).replaceAll("'", "\\u0027")}'`;
374
+ } catch {
375
+ }
376
+ if (!value.includes("'"))
377
+ return `'${value}'`;
378
+ if (!value.includes('"'))
379
+ return `"${value}"`;
380
+ if (!value.includes("`"))
381
+ return `\`${value}\``;
382
+ if (!/[\r\n#]/.test(value))
383
+ return value;
384
+ throw new Error("Unsupported dotenv value");
385
+ }
386
+ function updateFile(path, values, privateFile) {
387
+ let content = existsSync(path) ? readFileSync(path, "utf8") : "# Local private deployment configuration; do not commit.\n";
388
+ for (const [name, value] of Object.entries(values)) {
389
+ if (!/^[A-Z][A-Z0-9_]*$/.test(name))
390
+ throw new Error("Invalid environment variable name");
391
+ const line = `${name}=${quoteEnv(value)}`;
392
+ const pattern = new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"[^"]*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*`, "gm");
393
+ if (pattern.test(content))
394
+ content = content.replace(pattern, () => line);
395
+ else
396
+ content = `${content.trimEnd()}
397
+ ${line}
398
+ `;
399
+ }
400
+ writeFileSync(path, content, { mode: privateFile ? 384 : 420 });
401
+ if (privateFile)
402
+ chmodSync(path, 384);
403
+ }
404
+ function removeOverrides(path, names, privateFile = true) {
405
+ if (!existsSync(path) || names.length === 0)
406
+ return;
407
+ let content = readFileSync(path, "utf8");
408
+ for (const name of names)
409
+ content = content.replace(new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"[^"]*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
410
+ writeFileSync(path, content, { mode: privateFile ? 384 : 420 });
411
+ chmodSync(path, privateFile ? 384 : 420);
412
+ }
413
+ function saveDeploymentEnvironment(environment, values, options = {}) {
414
+ try {
415
+ const shared = {};
416
+ const privateValues = {};
417
+ for (const [name, value] of Object.entries(values)) {
418
+ if (options.private) {
419
+ privateValues[name] = value;
420
+ } else if (isSecret(name, value)) {
421
+ privateValues[name] = value;
422
+ try {
423
+ const parsed = JSON.parse(value);
424
+ if (parsed && typeof parsed === "object")
425
+ shared[name] = JSON.stringify(publicConfig(parsed));
426
+ } catch {
427
+ }
428
+ } else {
429
+ shared[name] = value;
430
+ }
431
+ }
432
+ if (Object.keys(shared).length)
433
+ updateFile(join(environment.envDir, `.env.${environment.environment}`), shared, false);
434
+ if (Object.keys(privateValues).length)
435
+ updateFile(join(environment.envDir, ".env.local"), privateValues, true);
436
+ removeOverrides(join(environment.envDir, `.env.${environment.environment}`), Object.keys(privateValues).filter((name) => !(name in shared)), false);
437
+ removeOverrides(join(environment.envDir, ".env.local"), Object.keys(shared).filter((name) => !(name in privateValues)));
438
+ if (Object.keys(privateValues).length) {
439
+ const ignorePath = join(environment.envDir, ".gitignore");
440
+ const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
441
+ if (!ignore.split(/\r?\n/).includes(".env.local"))
442
+ writeFileSync(ignorePath, `${ignore.trimEnd()}
443
+ .env.local
444
+ .hai/deploy/
445
+ `);
446
+ }
447
+ return ok(Object.keys(values));
448
+ } catch {
449
+ return err(HaiDeployError.ENV_VAR_FAILED, deployM("deploy_environmentWriteFailed"));
450
+ }
451
+ }
210
452
  var logger = core.logger.child({ module: "deploy", scope: "credentials" });
211
453
  var DEPLOY_CREDENTIAL_KEY_PATTERN = /^HAI_DEPLOY_[A-Z0-9_]+$/;
212
454
  function isAllowedCredentialKey(key) {
@@ -270,7 +512,7 @@ function loadCredentials() {
270
512
  const data = readCredentialsFile();
271
513
  const keys = Object.keys(data);
272
514
  for (const key of keys) {
273
- process.env[key] = String(data[key]);
515
+ process2.env[key] = String(data[key]);
274
516
  }
275
517
  logger.debug("Credentials loaded", { count: keys.length });
276
518
  return ok(keys);
@@ -291,7 +533,7 @@ function saveCredential(key, value) {
291
533
  const data = readCredentialsFile();
292
534
  data[key] = value;
293
535
  writeCredentialsFile(data);
294
- process.env[key] = value;
536
+ process2.env[key] = value;
295
537
  logger.debug("Credential saved", { key });
296
538
  return ok(void 0);
297
539
  } catch (error) {
@@ -311,7 +553,7 @@ function saveCredentials(entries) {
311
553
  const data = readCredentialsFile();
312
554
  for (const [key, value] of Object.entries(entries)) {
313
555
  data[key] = value;
314
- process.env[key] = value;
556
+ process2.env[key] = value;
315
557
  }
316
558
  writeCredentialsFile(data);
317
559
  logger.debug("Credentials saved", { count: Object.keys(entries).length });
@@ -327,35 +569,25 @@ function saveCredentials(entries) {
327
569
  );
328
570
  }
329
571
  }
330
- var logger2 = core.logger.child({ module: "deploy", scope: "functions" });
331
- function buildApp(appDir, buildCommand, envVars = {}) {
332
- logger2.info("Building application", { appDir, buildCommand });
333
- try {
334
- execSync(buildCommand, {
335
- cwd: appDir,
336
- stdio: "inherit",
337
- env: { ...process.env, ...envVars }
338
- });
339
- return ok(void 0);
340
- } catch (error) {
341
- return err(
342
- HaiDeployError.BUILD_FAILED,
343
- deployM("deploy_buildFailed", {
344
- params: { error: error instanceof Error ? error.message : String(error) }
345
- }),
346
- error
347
- );
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;
348
581
  }
349
582
  }
583
+ function usesDefaultMiddleware(scan, service, options) {
584
+ const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
585
+ return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
586
+ }
350
587
  function resolveOutputDir(appDir, providerType) {
351
588
  switch (providerType) {
352
- case "vercel": {
353
- const vercelOutput = join(appDir, ".vercel", "output");
354
- if (existsSync(vercelOutput)) {
355
- return vercelOutput;
356
- }
357
- return join(appDir, "build");
358
- }
589
+ case "vercel":
590
+ return join(appDir, ".vercel", "output");
359
591
  default:
360
592
  return join(appDir, "build");
361
593
  }
@@ -386,196 +618,300 @@ function recoveryFailure(error, recovery) {
386
618
  ext: { recovery }
387
619
  });
388
620
  }
389
- var logger3 = core.logger.child({ module: "deploy", scope: "scanner" });
390
- function readYaml(filePath) {
391
- if (!existsSync(filePath)) {
392
- return null;
621
+ var logger2 = core.logger.child({ module: "deploy", scope: "scanner" });
622
+ function record(value) {
623
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
624
+ }
625
+ function runtimeDependencies(appDir) {
626
+ let root = appDir;
627
+ while (!existsSync(join(root, "pnpm-lock.yaml"))) {
628
+ const parent = dirname(root);
629
+ if (parent === root)
630
+ throw new Error("Missing pnpm lockfile; run pnpm install");
631
+ root = parent;
393
632
  }
394
- try {
395
- const content = readFileSync(filePath, "utf-8");
396
- const parsed = parse(content);
397
- if (parsed === null || parsed === void 0 || typeof parsed !== "object") {
398
- return null;
633
+ const importerKey = relative(root, appDir).replaceAll("\\", "/") || ".";
634
+ const documents = parseAllDocuments(readFileSync(join(root, "pnpm-lock.yaml"), "utf8"));
635
+ if (documents.some((document) => document.errors.length))
636
+ throw new Error("Invalid pnpm lockfile");
637
+ const lock = documents.map((document) => record(document.toJS())).reverse().find((document) => importerKey in record(document.importers));
638
+ if (!lock)
639
+ throw new Error("Application is missing from pnpm lockfile; run pnpm install");
640
+ const importers = record(lock.importers);
641
+ const packages = record(lock.packages);
642
+ const snapshots = record(lock.snapshots);
643
+ const names = /* @__PURE__ */ new Set();
644
+ const visited = /* @__PURE__ */ new Set();
645
+ const visit = (id, node, importer) => {
646
+ if (visited.has(id))
647
+ return;
648
+ visited.add(id);
649
+ const metadata = importer === void 0 ? record(packages[id.split("(")[0]]) : {};
650
+ const peers = record(metadata.peerDependencies);
651
+ for (const [name, reference] of Object.entries({ ...record(node.dependencies), ...record(node.optionalDependencies) })) {
652
+ if (name in peers)
653
+ continue;
654
+ const version = typeof reference === "string" ? reference : record(reference).version;
655
+ if (typeof version !== "string")
656
+ throw new Error("Invalid pnpm dependency reference");
657
+ if (version.startsWith("link:")) {
658
+ if (importer === void 0)
659
+ throw new Error("Invalid workspace dependency");
660
+ const target = relative(root, resolve(root, importer, version.slice(5))).replaceAll("\\", "/") || ".";
661
+ const linked = record(importers[target]);
662
+ if (!Object.keys(linked).length)
663
+ throw new Error("Missing linked workspace importer");
664
+ names.add(name);
665
+ visit(`workspace:${target}`, linked, target);
666
+ continue;
667
+ }
668
+ const key = version in snapshots || version in packages ? version : `${name}@${version}`;
669
+ names.add(key.match(/^(@[^/]+\/[^@]+|[^@]+)@/)?.[1] ?? name);
670
+ const snapshot = record(snapshots[key] ?? packages[key]);
671
+ if (!Object.keys(snapshot).length && !(key in snapshots) && !(key in packages))
672
+ throw new Error("Missing pnpm dependency snapshot");
673
+ visit(key, snapshot);
399
674
  }
400
- return parsed;
401
- } catch {
402
- return null;
403
- }
675
+ };
676
+ visit(`workspace:${importerKey}`, record(importers[importerKey]), importerKey);
677
+ return { runtime: names, build: /* @__PURE__ */ new Set([...names, ...Object.keys(record(record(importers[importerKey]).devDependencies))]), root };
404
678
  }
405
- function readJson(filePath) {
406
- if (!existsSync(filePath)) {
407
- return null;
408
- }
679
+ async function scanApp(inputDir, options) {
409
680
  try {
410
- const content = readFileSync(filePath, "utf-8");
411
- return JSON.parse(content);
412
- } catch {
413
- return null;
414
- }
415
- }
416
- function hasDependency(pkg, name) {
417
- const deps = pkg.dependencies;
418
- const devDeps = pkg.devDependencies;
419
- return deps !== void 0 && name in deps || devDeps !== void 0 && name in devDeps;
420
- }
421
- function detectRequiredServices(configDir) {
422
- const services = [];
423
- const backends = {};
424
- if (!existsSync(configDir)) {
425
- return { services, backends };
426
- }
427
- const files = readdirSync(configDir);
428
- const dbFile = files.find((f) => f.startsWith("_db."));
429
- if (dbFile) {
430
- const dbConfig = readYaml(join(configDir, dbFile));
431
- if (dbConfig !== null) {
432
- services.push("db");
433
- backends.db = dbConfig.type ?? "postgresql";
434
- }
435
- }
436
- const cacheFile = files.find((f) => f.startsWith("_cache."));
437
- if (cacheFile) {
438
- const cacheConfig = readYaml(join(configDir, cacheFile));
439
- if (cacheConfig !== null && cacheConfig.type === "redis") {
440
- services.push("cache");
441
- backends.cache = "redis";
442
- }
443
- }
444
- const storageFile = files.find((f) => f.startsWith("_storage."));
445
- if (storageFile) {
446
- const storageConfig = readYaml(join(configDir, storageFile));
447
- if (storageConfig !== null && detectStorageIsS3(storageConfig)) {
448
- services.push("storage");
449
- backends.storage = "s3";
681
+ const appDir = resolve(inputDir);
682
+ const graph = runtimeDependencies(appDir);
683
+ const envDir = options?.envDir ? resolve(options.envDir) : existsSync(join(appDir, ".env")) ? appDir : graph.root;
684
+ const loaded = loadDeploymentEnvironment({ environment: options?.environment ?? "prod", envDir, requireFile: options?.environment !== void 0 });
685
+ if (!loaded.success)
686
+ return loaded;
687
+ const env = loaded.data.values;
688
+ const deps = graph.runtime;
689
+ const configs = {};
690
+ for (const [name, owner] of Object.entries({ db: "reldb", cache: "cache", storage: "storage", vecdb: "vecdb", iam: "iam", reach: "reach" })) {
691
+ configs[name] = {};
692
+ if (!deps.has(`@h-ai/${owner}`))
693
+ continue;
694
+ const configFile = ["yml", "yaml"].map((ext) => join(appDir, "config", `_${name}.${ext}`)).find(existsSync);
695
+ const config = configFile ? loadDeploymentConfig(name, configFile, env) : void 0;
696
+ if (config && !config.success)
697
+ return config;
698
+ configs[name] = config?.data ?? {};
450
699
  }
451
- }
452
- const vecdbFile = files.find((f) => f.startsWith("_vecdb."));
453
- if (vecdbFile) {
454
- const vecdbConfig = readYaml(join(configDir, vecdbFile));
455
- if (vecdbConfig !== null) {
456
- services.push("vecdb");
457
- backends.vecdb = vecdbConfig.type ?? "qdrant";
700
+ const middlewares = [];
701
+ const add = (service, backend, configName = service, configPath = [], config = configs[configName] ?? {}, source = `_${configName}.yml`) => {
702
+ middlewares.push({ service, backend, source, configName, configPath, envPrefix: ["HAI", configName, ...configPath].map((part) => part.toUpperCase()).join("_"), config });
703
+ };
704
+ const iam = deps.has("@h-ai/iam");
705
+ if (deps.has("@h-ai/reldb"))
706
+ add("db", String(configs.db.type ?? "sqlite"), "db", [], configs.db, "@h-ai/reldb / _db.yml");
707
+ if (deps.has("@h-ai/cache"))
708
+ add("cache", String(configs.cache.type ?? "memory"), "cache", [], configs.cache, "@h-ai/cache / _cache.yml");
709
+ if (deps.has("@h-ai/storage"))
710
+ add("storage", String(configs.storage.type ?? "local"));
711
+ if (deps.has("@h-ai/vecdb"))
712
+ add("vecdb", String(configs.vecdb.type ?? "lancedb"));
713
+ if (iam) {
714
+ const ldap = record(configs.iam.ldap);
715
+ add("ldap", record(configs.iam.login).ldap === true ? "ldap" : "disabled", "iam", ["ldap"], ldap, "@h-ai/iam (LDAP)");
458
716
  }
459
- }
460
- const reachFile = files.find((f) => f.startsWith("_reach."));
461
- if (reachFile) {
462
- const reachConfig = readYaml(join(configDir, reachFile));
463
- if (reachConfig !== null) {
464
- detectReachServices(reachConfig, services);
717
+ const providers = configs.reach.providers;
718
+ if (deps.has("@h-ai/reach") && Array.isArray(providers)) {
719
+ for (const [index, item] of providers.entries()) {
720
+ const provider = record(item);
721
+ const type = String(provider.type ?? "");
722
+ const service = type === "aliyun-sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
723
+ if (service)
724
+ add(service, type, "reach", ["providers", String(index)], provider, "@h-ai/reach");
725
+ }
465
726
  }
727
+ if (deps.has("@h-ai/reach") && !middlewares.some((item) => item.configName === "reach"))
728
+ add("email", "console", "reach", ["providers", "0"], { name: "email", type: "console" }, "@h-ai/reach");
729
+ const isSvelteKit = graph.build.has("@sveltejs/kit");
730
+ const scan = {
731
+ appDir,
732
+ envDir,
733
+ middlewares,
734
+ haiModules: [...deps].filter((name) => name.startsWith("@h-ai/")).sort(),
735
+ appName: basename(appDir),
736
+ isSvelteKit,
737
+ adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
738
+ nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
739
+ requiredServices: [...new Set(middlewares.map((item) => item.service))],
740
+ serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
741
+ };
742
+ logger2.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
743
+ return ok(scan);
744
+ } catch {
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" } }));
466
746
  }
467
- return { services, backends };
468
- }
469
- function detectStorageIsS3(config) {
470
- if (config.type === "s3") {
471
- return true;
472
- }
473
- const providers = config.providers;
474
- if (providers === void 0) {
475
- return false;
476
- }
477
- return Object.values(providers).some((p) => p?.type === "s3");
478
747
  }
479
- function detectReachServices(config, services) {
480
- const providers = config.providers;
481
- if (!Array.isArray(providers)) {
482
- return;
483
- }
484
- let hasEmail = false;
485
- let hasSms = false;
486
- for (const provider of providers) {
487
- const providerType = provider.type;
488
- if (!hasEmail && (providerType === "smtp" || providerType === "api")) {
489
- hasEmail = true;
490
- services.push("email");
491
- }
492
- if (!hasSms && providerType === "aliyun-sms") {
493
- hasSms = true;
494
- services.push("sms");
495
- }
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("***");
496
756
  }
757
+ return masked;
497
758
  }
498
- async function scanApp(appDir) {
499
- logger3.debug("Scanning application", { appDir });
500
- try {
501
- const pkg = readJson(join(appDir, "package.json"));
502
- if (pkg === null) {
503
- return err(
504
- HaiDeployError.SCAN_FAILED,
505
- deployM("deploy_scanFailed", { params: { error: "package.json not found" } })
506
- );
507
- }
508
- const appName = extractAppName(pkg);
509
- const isSvelteKit = existsSync(join(appDir, "svelte.config.js")) || existsSync(join(appDir, "svelte.config.ts"));
510
- const adapterInstalled = hasDependency(pkg, "@sveltejs/adapter-vercel");
511
- const nodeAdapterInstalled = hasDependency(pkg, "@sveltejs/adapter-node");
512
- const configDir = join(appDir, "config");
513
- const configDetection = detectRequiredServices(configDir);
514
- const depServices = detectServicesFromDependencies(pkg);
515
- const requiredServices = Array.from(/* @__PURE__ */ new Set([...configDetection.services, ...depServices]));
516
- const scripts = pkg.scripts;
517
- const buildCommand = scripts?.build ?? "pnpm build";
518
- const scanResult = {
519
- appName,
520
- isSvelteKit,
521
- adapterInstalled,
522
- nodeAdapterInstalled,
523
- requiredServices,
524
- serviceBackends: configDetection.backends,
525
- buildCommand
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);
526
777
  };
527
- logger3.info("Application scanned", {
528
- appName,
529
- isSvelteKit,
530
- adapterInstalled,
531
- nodeAdapterInstalled,
532
- requiredServices,
533
- serviceBackends: configDetection.backends
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");
534
790
  });
535
- return ok(scanResult);
536
- } catch (error) {
537
- logger3.error("Application scan failed", { appDir, error });
538
- return err(
539
- HaiDeployError.SCAN_FAILED,
540
- deployM("deploy_scanFailed", {
541
- params: { error: error instanceof Error ? error.message : String(error) }
542
- }),
543
- error
544
- );
545
- }
546
- }
547
- function detectServicesFromDependencies(pkg) {
548
- const depMap = {
549
- "@h-ai/reldb": "db",
550
- "@h-ai/cache": "cache",
551
- "@h-ai/storage": "storage",
552
- "@h-ai/vecdb": "vecdb"
553
- };
554
- const services = [];
555
- for (const [depName, serviceType] of Object.entries(depMap)) {
556
- if (hasDependency(pkg, depName)) {
557
- services.push(serviceType);
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();
558
829
  }
559
- }
560
- return services;
830
+ });
561
831
  }
562
- function extractAppName(pkg) {
563
- const name = pkg.name ?? "app";
564
- const slashIndex = name.indexOf("/");
565
- if (slashIndex >= 0) {
566
- return name.slice(slashIndex + 1);
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 });
567
902
  }
568
- return name;
569
903
  }
570
904
  var IMAGES = {
571
905
  postgres: "postgres:16-alpine",
572
906
  mysql: "mysql:8.4",
573
907
  redis: "redis:7-alpine",
574
- minio: "minio/minio:RELEASE.2025-09-07T16-13-09Z",
575
- mc: "minio/mc:RELEASE.2025-08-13T08-35-41Z",
908
+ rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
909
+ minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
576
910
  qdrant: "qdrant/qdrant:v1.12.4",
577
911
  pgvector: "pgvector/pgvector:pg17",
578
- chroma: "chromadb/chroma:0.5.23"
912
+ chroma: "chromadb/chroma:1.5.5",
913
+ ldap: "docker.io/osixia/openldap:1.5.0",
914
+ smtp: "axllent/mailpit:v1.27.4"
579
915
  };
580
916
  function generatePassword() {
581
917
  return randomBytes(24).toString("base64url");
@@ -590,7 +926,8 @@ function resolveSidecarSecrets(appDir, projectName) {
590
926
  }
591
927
  const secrets = {
592
928
  dbPassword: generatePassword(),
593
- storagePassword: generatePassword()
929
+ storagePassword: generatePassword(),
930
+ ldapPassword: generatePassword()
594
931
  };
595
932
  mkdirSync(dirname(secretFile), { recursive: true });
596
933
  writeFileSync(secretFile, JSON.stringify(secrets), { mode: 384 });
@@ -651,38 +988,57 @@ function planRedisCache(plan, projectName, net) {
651
988
  };
652
989
  plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
653
990
  }
654
- function planMinioStorage(plan, projectName, password, net) {
991
+ function planS3Storage(plan, projectName, password, net, engine) {
655
992
  plan.services.push("storage");
656
993
  plan.dependsOn.push("storage");
657
994
  plan.composeServices.storage = {
658
- image: IMAGES.minio,
995
+ image: IMAGES[engine],
659
996
  container_name: `hai-${projectName}-storage`,
660
997
  restart: "unless-stopped",
661
- command: ["server", "/data"],
998
+ ...engine === "minio" ? { command: ["server", "/data"] } : {},
662
999
  env_file: [".env.runtime"],
663
1000
  volumes: [`${projectName}-storage-data:/data`],
664
1001
  networks: net
665
1002
  };
666
1003
  plan.composeServices["storage-init"] = {
667
- image: IMAGES.mc,
668
1004
  depends_on: ["storage"],
669
1005
  env_file: [".env.runtime"],
670
1006
  entrypoint: [
671
- "sh",
672
- "-c",
673
- 'until mc alias set s3 http://storage:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; do sleep 1; done; mc mb -p s3/hai; exit 0'
1007
+ "node",
1008
+ "--input-type=module",
1009
+ "-e",
1010
+ // 复用应用 @h-ai/storage 已依赖的 SDK;不额外拉取失效的 mc 镜像,不把密钥拼进命令。
1011
+ `import { S3Client, HeadBucketCommand, CreateBucketCommand } from '@aws-sdk/client-s3';
1012
+ const config = JSON.parse(process.env.HAI_STORAGE);
1013
+ const client = new S3Client({ endpoint: config.endpoint, region: config.region, forcePathStyle: config.forcePathStyle, credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey } });
1014
+ let ready = false;
1015
+ for (let attempt = 0; attempt < 120; attempt++) {
1016
+ try {
1017
+ try { await client.send(new HeadBucketCommand({ Bucket: config.bucket })); }
1018
+ catch (error) {
1019
+ if (error.$$metadata?.httpStatusCode !== 404) throw error;
1020
+ await client.send(new CreateBucketCommand({ Bucket: config.bucket }));
1021
+ }
1022
+ ready = true; break;
1023
+ } catch { await new Promise(resolve => setTimeout(resolve, 1000)); }
1024
+ }
1025
+ client.destroy();
1026
+ if (!ready) throw new Error('S3 bucket initialization failed');`
674
1027
  ],
675
1028
  networks: net
676
1029
  };
677
1030
  plan.volumes[`${projectName}-storage-data`] = null;
678
- plan.env.MINIO_ROOT_USER = "hai";
679
- plan.env.MINIO_ROOT_PASSWORD = password;
1031
+ if (engine === "rustfs") {
1032
+ Object.assign(plan.env, { RUSTFS_ACCESS_KEY: "hai-storage", RUSTFS_SECRET_KEY: password, RUSTFS_ADDRESS: "0.0.0.0:9000", RUSTFS_VOLUMES: "/data", RUSTFS_CONSOLE_ENABLE: "false" });
1033
+ } else {
1034
+ Object.assign(plan.env, { MINIO_ROOT_USER: "hai-storage", MINIO_ROOT_PASSWORD: password });
1035
+ }
680
1036
  plan.env.HAI_STORAGE = JSON.stringify({
681
1037
  type: "s3",
682
1038
  endpoint: "http://storage:9000",
683
1039
  bucket: "hai",
684
1040
  region: "us-east-1",
685
- accessKeyId: "hai",
1041
+ accessKeyId: "hai-storage",
686
1042
  secretAccessKey: password,
687
1043
  forcePathStyle: true
688
1044
  });
@@ -732,7 +1088,7 @@ function planChromaVecdb(plan, projectName, net) {
732
1088
  plan.volumes[`${projectName}-vecdb-data`] = null;
733
1089
  plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
734
1090
  }
735
- function planSidecars(appDir, projectName, scan, provisionEnv) {
1091
+ function planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine = "rustfs") {
736
1092
  const plan = {
737
1093
  services: [],
738
1094
  composeServices: {},
@@ -742,10 +1098,14 @@ function planSidecars(appDir, projectName, scan, provisionEnv) {
742
1098
  dependsOn: [],
743
1099
  secrets: []
744
1100
  };
1101
+ if (!scan.requiredServices.some((service) => usesDefaultMiddleware(scan, service, options)))
1102
+ return plan;
745
1103
  const secrets = resolveSidecarSecrets(appDir, projectName);
746
1104
  const net = ["hai-app"];
747
1105
  const backends = scan.serviceBackends;
748
1106
  for (const service of scan.requiredServices) {
1107
+ if (!usesDefaultMiddleware(scan, service, options))
1108
+ continue;
749
1109
  if (service === "db" && !provisionEnv.HAI_DB) {
750
1110
  const backend = backends.db ?? "postgresql";
751
1111
  if (backend === "mysql")
@@ -755,9 +1115,13 @@ function planSidecars(appDir, projectName, scan, provisionEnv) {
755
1115
  else
756
1116
  planPostgresDb(plan, projectName, secrets.dbPassword, net);
757
1117
  } else if (service === "cache" && !provisionEnv.HAI_CACHE) {
758
- planRedisCache(plan, projectName, net);
1118
+ if ((backends.cache ?? "redis") === "redis")
1119
+ planRedisCache(plan, projectName, net);
759
1120
  } else if (service === "storage" && !provisionEnv.HAI_STORAGE) {
760
- planMinioStorage(plan, projectName, secrets.storagePassword, net);
1121
+ if ((backends.storage ?? "s3") === "s3")
1122
+ planS3Storage(plan, projectName, secrets.storagePassword, net, s3Engine);
1123
+ else
1124
+ addAppDataVolume(plan, projectName);
761
1125
  } else if (service === "vecdb" && !provisionEnv.HAI_VECDB) {
762
1126
  const backend = backends.vecdb ?? "qdrant";
763
1127
  if (backend === "pgvector")
@@ -768,16 +1132,39 @@ function planSidecars(appDir, projectName, scan, provisionEnv) {
768
1132
  addAppDataVolume(plan, projectName);
769
1133
  else
770
1134
  planQdrantVecdb(plan, projectName, net);
1135
+ } else if (service === "ldap" && backends.ldap === "ldap" && !provisionEnv.HAI_IAM_LDAP) {
1136
+ const password = secrets.ldapPassword ?? secrets.dbPassword;
1137
+ plan.services.push("ldap");
1138
+ plan.dependsOn.push("ldap");
1139
+ plan.composeServices.ldap = { image: IMAGES.ldap, restart: "unless-stopped", env_file: [".env.runtime"], networks: net, volumes: [`${projectName}-ldap-data:/var/lib/ldap`, `${projectName}-ldap-config:/etc/ldap/slapd.d`] };
1140
+ plan.volumes[`${projectName}-ldap-data`] = null;
1141
+ plan.volumes[`${projectName}-ldap-config`] = null;
1142
+ Object.assign(plan.env, { LDAP_DOMAIN: "example.org", LDAP_ORGANISATION: "HAI", LDAP_ADMIN_PASSWORD: password, LDAP_TLS: "false", HAI_IAM_LOGIN_LDAP: "true", HAI_IAM_LDAP: JSON.stringify({ url: "ldap://ldap:389", bindDn: "cn=admin,dc=example,dc=org", bindPassword: password, searchBase: "dc=example,dc=org" }) });
1143
+ plan.secrets.push(password);
1144
+ } else if (service === "email" && (scan.middlewares ? scan.middlewares.some((item) => item.service === "email" && item.backend === "smtp" && (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email ?? "default") === "default") : backends.email === "smtp") && !provisionEnv.HAI_REACH_PROVIDERS) {
1145
+ plan.services.push("email");
1146
+ plan.dependsOn.push("email");
1147
+ plan.composeServices.email = { image: IMAGES.smtp, restart: "unless-stopped", networks: net };
1148
+ const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
1149
+ if (requirements?.length) {
1150
+ for (const requirement of requirements)
1151
+ plan.env[requirement.envPrefix] = JSON.stringify({ name: requirement.config.name ?? "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
1152
+ } else if (!scan.middlewares) {
1153
+ plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({ name: "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
1154
+ }
771
1155
  }
772
1156
  }
773
1157
  return plan;
774
1158
  }
775
- function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort) {
776
- const sidecar = planSidecars(appDir, projectName, scan, provisionEnv);
1159
+ function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv = {}, s3Engine = "rustfs") {
1160
+ const sidecar = planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine);
777
1161
  const env = {
778
1162
  NODE_ENV: "production",
779
1163
  HOST: "0.0.0.0",
780
1164
  PORT: String(containerPort),
1165
+ HAI_SERV_SERVER_HOST: "0.0.0.0",
1166
+ HAI_SERV_SERVER_PORT: String(containerPort),
1167
+ ...appEnv,
781
1168
  ...sidecar.env,
782
1169
  ...provisionEnv
783
1170
  };
@@ -787,6 +1174,7 @@ function generateComposeFile(options) {
787
1174
  const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
788
1175
  const app = {
789
1176
  image: imageRef,
1177
+ labels: { "org.opencontainers.image.version": options.appVersion },
790
1178
  container_name: `hai-${projectName}-app`,
791
1179
  restart: "unless-stopped",
792
1180
  // 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
@@ -799,188 +1187,110 @@ function generateComposeFile(options) {
799
1187
  app.volumes = sidecar.appVolumes;
800
1188
  if (sidecar.dependsOn.length > 0)
801
1189
  app.depends_on = sidecar.dependsOn;
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
+ }
1207
+ if (services["storage-init"])
1208
+ services["storage-init"] = { ...services["storage-init"], image: imageRef };
802
1209
  const compose = {
803
- services: {
804
- app,
805
- ...sidecar.composeServices
806
- },
807
- networks: {
1210
+ "x-hai-version": options.appVersion,
1211
+ services,
1212
+ "networks": {
808
1213
  "hai-app": { name: `hai-${projectName}` }
809
1214
  }
810
1215
  };
811
1216
  if (Object.keys(sidecar.volumes).length > 0)
812
1217
  compose.volumes = sidecar.volumes;
813
- return stringify(compose);
814
- }
815
- function serializeEnvFile(env) {
816
- return `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n")}
817
- `;
818
- }
819
- function generateDockerignore() {
820
- return `**/node_modules
821
- **/dist
822
- **/build
823
- **/.turbo
824
- **/.svelte-kit
825
- **/coverage
826
- **/test-results
827
- **/playwright-report
828
- **/.hai/deploy
829
- .git
830
- `;
831
- }
832
- function generateFallbackDockerfile(packageName, containerPort) {
833
- return `# syntax=docker/dockerfile:1.7
834
- FROM node:24-bookworm-slim AS build
835
- ENV PNPM_HOME=/pnpm
836
- ENV PATH=$PNPM_HOME:$PATH
837
- ENV CI=true
838
- WORKDIR /workspace
839
- RUN corepack enable && corepack prepare pnpm@12.11.2 --activate
840
- COPY . .
841
- RUN pnpm install --frozen-lockfile --ignore-scripts
842
- RUN pnpm --filter ${packageName} build
843
- RUN pnpm pm deploy --filter ${packageName} --prod /prod/app
844
-
845
- FROM node:24-bookworm-slim AS runtime
846
- ENV NODE_ENV=production
847
- ENV HOST=0.0.0.0
848
- ENV PORT=${containerPort}
849
- WORKDIR /app
850
- COPY --from=build --chown=node:node /prod/app ./
851
- USER node
852
- EXPOSE ${containerPort}
853
- CMD ["node", "build"]
854
- `;
855
- }
856
- var logger4 = core.logger.child({ module: "deploy", scope: "command" });
857
- function maskSecrets(text, secrets) {
858
- if (!secrets || secrets.length === 0)
859
- return text;
860
- let masked = text;
861
- for (const secret of secrets) {
862
- if (secret && secret.length > 0)
863
- masked = masked.split(secret).join("***");
864
- }
865
- return masked;
866
- }
867
- function runCommand(command, args, options = {}) {
868
- return new Promise((resolve) => {
869
- const child = spawn(command, args, {
870
- cwd: options.cwd,
871
- env: options.env ? { ...process.env, ...options.env } : process.env,
872
- shell: false
873
- });
874
- let stdout = "";
875
- let stderr = "";
876
- let settled = false;
877
- let timer;
878
- const finish = (result) => {
879
- if (settled)
880
- return;
881
- settled = true;
882
- if (timer)
883
- clearTimeout(timer);
884
- resolve(result);
885
- };
886
- if (options.timeoutMs && options.timeoutMs > 0) {
887
- timer = setTimeout(() => {
888
- child.kill("SIGKILL");
889
- logger4.error("Command timed out", { command, timeoutMs: options.timeoutMs });
890
- finish(err(
891
- HaiDeployError.REMOTE_COMMAND_FAILED,
892
- deployM("deploy_commandTimeout", { params: { command } })
893
- ));
894
- }, options.timeoutMs);
895
- }
896
- child.stdout.on("data", (chunk) => {
897
- stdout += chunk.toString("utf-8");
898
- });
899
- child.stderr.on("data", (chunk) => {
900
- stderr += chunk.toString("utf-8");
901
- });
902
- child.on("error", (error) => {
903
- logger4.error("Command failed to start", { command, error: maskSecrets(String(error), options.secrets) });
904
- finish(err(
905
- HaiDeployError.REMOTE_COMMAND_FAILED,
906
- deployM("deploy_commandFailed", {
907
- params: { command, error: maskSecrets(error.message, options.secrets) }
908
- }),
909
- error
910
- ));
911
- });
912
- child.on("close", (code) => {
913
- const exitCode = code ?? -1;
914
- if (exitCode === 0) {
915
- finish(ok({ code: exitCode, stdout, stderr }));
916
- return;
1218
+ if (options.previousCompose && options.serviceModes) {
1219
+ const previous = parse(options.previousCompose);
1220
+ for (const [service, mode] of Object.entries(options.serviceModes)) {
1221
+ if (mode !== "keep")
1222
+ continue;
1223
+ for (const name of [service, `${service}-init`]) {
1224
+ if (previous.services?.[name]) {
1225
+ services[name] = previous.services[name];
1226
+ app.depends_on = [.../* @__PURE__ */ new Set([...app.depends_on ?? [], service])];
1227
+ }
917
1228
  }
918
- logger4.error("Command exited with non-zero code", {
919
- command,
920
- code: exitCode,
921
- stderr: maskSecrets(stderr, options.secrets).slice(-500)
922
- });
923
- finish(err(
924
- HaiDeployError.REMOTE_COMMAND_FAILED,
925
- deployM("deploy_commandFailed", {
926
- params: { command, error: maskSecrets(stderr || `exit ${exitCode}`, options.secrets).slice(-500) }
927
- })
928
- ));
929
- });
930
- if (options.input !== void 0) {
931
- child.stdin.write(options.input);
932
- child.stdin.end();
933
- } else {
934
- child.stdin.end();
935
- }
936
- });
937
- }
938
-
939
- // src/container/deploy-container-runtime.ts
940
- var logger5 = core.logger.child({ module: "deploy", scope: "container-runtime" });
941
- async function detectLocalRuntime(preferred = "auto") {
942
- const candidates = preferred === "auto" ? ["podman", "docker"] : [preferred];
943
- for (const runtime of candidates) {
944
- const probe = await runCommand(runtime, ["version"], { timeoutMs: 15e3 });
945
- if (probe.success) {
946
- logger5.info("Local container runtime detected", { runtime });
947
- return ok(runtime);
948
1229
  }
1230
+ const previousMounts = previous.services?.app?.volumes;
1231
+ if (Array.isArray(previousMounts) && Object.values(options.serviceModes).includes("keep"))
1232
+ app.volumes = [.../* @__PURE__ */ new Set([...app.volumes ?? [], ...previousMounts])];
1233
+ compose.volumes = { ...previous.volumes, ...sidecar.volumes };
949
1234
  }
950
- return err(
951
- HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND,
952
- deployM("deploy_containerRuntimeNotFound", { params: { runtime: candidates.join(" / ") } })
953
- );
954
- }
955
- async function buildImage(runtime, options) {
956
- const args = ["build", "-f", options.dockerfilePath, "-t", options.imageRef];
957
- if (runtime === "podman")
958
- args.push("--format", "docker", "--jobs", "1");
959
- args.push(options.contextDir);
960
- logger5.info("Building container image", { runtime, imageRef: options.imageRef });
961
- const result = await runCommand(runtime, args, { cwd: options.contextDir });
962
- if (!result.success) {
963
- return err(
964
- HaiDeployError.CONTAINER_BUILD_FAILED,
965
- deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
966
- result.error
967
- );
1235
+ if (services["storage-init"]) {
1236
+ app.depends_on = {
1237
+ ...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
1238
+ "storage-init": { condition: "service_completed_successfully" }
1239
+ };
968
1240
  }
969
- return ok(void 0);
1241
+ for (const service of Object.values(services))
1242
+ service.pull_policy = "never";
1243
+ return stringify(compose);
970
1244
  }
971
- async function saveImage(runtime, imageRef, tarPath) {
972
- logger5.info("Saving container image", { runtime, imageRef, tarPath });
973
- const result = await runCommand(runtime, ["save", "-o", tarPath, imageRef], {});
974
- if (!result.success) {
975
- return err(
976
- HaiDeployError.CONTAINER_BUILD_FAILED,
977
- deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
978
- result.error
979
- );
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;
980
1274
  }
981
- return ok(void 0);
982
1275
  }
983
- var logger6 = core.logger.child({ module: "deploy", scope: "ssh" });
1276
+ `;
1277
+ }
1278
+ function serializeEnvFile(env) {
1279
+ return `${Object.entries(env).map(([key, value]) => {
1280
+ let literal = value;
1281
+ if (/[\s#$'"]/.test(value)) {
1282
+ if (value.startsWith("{") || value.startsWith("["))
1283
+ literal = `'${value.replaceAll("'", "\\u0027")}'`;
1284
+ else if (!value.includes("'"))
1285
+ literal = `'${value}'`;
1286
+ else
1287
+ literal = JSON.stringify(value).replaceAll("$", "$$");
1288
+ }
1289
+ return `${key}=${literal}`;
1290
+ }).join("\n")}
1291
+ `;
1292
+ }
1293
+ var logger4 = core.logger.child({ module: "deploy", scope: "ssh" });
984
1294
  function buildConnectOptions(config) {
985
1295
  const args = ["-o", "BatchMode=yes"];
986
1296
  args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
@@ -1031,7 +1341,7 @@ function createSshExecutor(config) {
1031
1341
  return ok(void 0);
1032
1342
  };
1033
1343
  const ping = async () => {
1034
- logger6.debug("SSH preflight", { host: config.host, port: config.port });
1344
+ logger4.debug("SSH preflight", { host: config.host, port: config.port });
1035
1345
  const result = await exec("true", { timeoutMs: 2e4 });
1036
1346
  if (!result.success) {
1037
1347
  return err(
@@ -1046,7 +1356,7 @@ function createSshExecutor(config) {
1046
1356
  }
1047
1357
 
1048
1358
  // src/providers/deploy-provider-docker-ssh.ts
1049
- var logger7 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
1359
+ var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
1050
1360
  var HEALTH_POLL_INTERVAL = 3e3;
1051
1361
  var HEALTH_MAX_ATTEMPTS = 40;
1052
1362
  function makeDeploymentId() {
@@ -1055,38 +1365,15 @@ function makeDeploymentId() {
1055
1365
  const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
1056
1366
  return `${stamp}-${randomBytes(3).toString("hex")}`;
1057
1367
  }
1058
- function parseLoadedImageRef(stdout, fallback) {
1059
- const match = stdout.match(/Loaded image(?:\(s\))?:\s*(\S+)/);
1060
- return match ? match[1] : fallback;
1061
- }
1062
1368
  function sleep(ms) {
1063
- return new Promise((resolve) => setTimeout(resolve, ms));
1369
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
1064
1370
  }
1065
- function readPackageName(appDir) {
1066
- try {
1067
- const pkg = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8"));
1068
- return pkg.name ?? "app";
1069
- } catch {
1070
- return "app";
1071
- }
1072
- }
1073
- function resolveWorkspaceRoot(appDir) {
1074
- if (existsSync(join(appDir, "pnpm-workspace.yaml")))
1075
- return appDir;
1076
- let current = appDir;
1077
- let parent = dirname(current);
1078
- while (parent !== current) {
1079
- if (existsSync(join(parent, "pnpm-workspace.yaml")))
1080
- return parent;
1081
- current = parent;
1082
- parent = dirname(parent);
1083
- }
1084
- return appDir;
1085
- }
1086
- function createDockerSshProvider(config, localRuntimePref = "auto") {
1087
- const ssh = createSshExecutor(config.ssh);
1371
+ function createDockerSshProvider(config, container) {
1372
+ const ssh = config.ssh ? createSshExecutor(config.ssh) : void 0;
1088
1373
  const baseDir = config.remote.baseDir;
1089
1374
  async function detectRemoteRuntime() {
1375
+ if (!ssh)
1376
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
1090
1377
  const candidates = config.remote.runtime === "auto" ? ["podman", "docker"] : [config.remote.runtime];
1091
1378
  for (const runtime of candidates) {
1092
1379
  const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
@@ -1094,7 +1381,7 @@ function createDockerSshProvider(config, localRuntimePref = "auto") {
1094
1381
  continue;
1095
1382
  const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
1096
1383
  if (composeResult.success) {
1097
- logger7.info("Remote container runtime detected", { runtime });
1384
+ logger5.info("Remote container runtime detected", { runtime });
1098
1385
  return ok(runtime);
1099
1386
  }
1100
1387
  }
@@ -1116,98 +1403,103 @@ function createDockerSshProvider(config, localRuntimePref = "auto") {
1116
1403
  return false;
1117
1404
  }
1118
1405
  const deploy2 = async (context) => {
1119
- const { appDir, projectName, scan, provisionEnv } = context;
1406
+ const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
1120
1407
  const deploymentId = makeDeploymentId();
1121
- const containerPort = config.expose.containerPort;
1122
1408
  const hostPort = config.expose.hostPort;
1123
- const localImageTag = `hai-${projectName}:${deploymentId}`;
1124
- const runtimeResult = await detectLocalRuntime(localRuntimePref);
1125
- if (!runtimeResult.success)
1126
- return err(runtimeResult.error);
1127
- const localRuntime = runtimeResult.data;
1128
- const artifactDir = join(appDir, ".hai", "deploy");
1129
- mkdirSync(artifactDir, { recursive: true });
1130
- const appDockerfile = join(appDir, "Dockerfile");
1131
- let dockerfilePath;
1132
- let contextDir;
1133
- if (existsSync(appDockerfile)) {
1134
- dockerfilePath = appDockerfile;
1135
- contextDir = appDir;
1136
- } else {
1137
- dockerfilePath = join(artifactDir, "Dockerfile");
1138
- writeFileSync(dockerfilePath, generateFallbackDockerfile(readPackageName(appDir), containerPort));
1139
- writeFileSync(`${dockerfilePath}.dockerignore`, generateDockerignore());
1140
- contextDir = resolveWorkspaceRoot(appDir);
1141
- logger7.info("Generated fallback Dockerfile", { dockerfilePath, contextDir });
1409
+ const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
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}`;
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 ?? "*" } });
1142
1432
  }
1143
- const buildResult = await buildImage(localRuntime, {
1144
- contextDir,
1145
- dockerfilePath,
1146
- imageRef: localImageTag
1147
- });
1148
- if (!buildResult.success)
1149
- return err(buildResult.error);
1150
- const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort);
1151
1433
  const envPath = join(artifactDir, ".env.runtime");
1434
+ const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
1435
+ Object.assign(env, { ...previousEnv, ...env });
1152
1436
  writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
1153
- const tarPath = join(artifactDir, `${projectName}.tar`);
1154
- const saveResult = await saveImage(localRuntime, localImageTag, tarPath);
1155
- if (!saveResult.success)
1156
- return err(saveResult.error);
1157
- const pingResult = await ssh.ping();
1158
- if (!pingResult.success)
1159
- return err(pingResult.error);
1160
- const remoteRuntimeResult = await detectRemoteRuntime();
1161
- if (!remoteRuntimeResult.success)
1162
- return err(remoteRuntimeResult.error);
1163
- const remoteRuntime = remoteRuntimeResult.data;
1164
- const remoteDir = `${baseDir}/${projectName}`;
1165
- const prepareResult = await ssh.exec(`mkdir -p ${remoteDir} && chmod 700 ${remoteDir}`);
1166
- if (!prepareResult.success)
1167
- return err(prepareResult.error);
1168
- const remoteTar = `${remoteDir}/image.tar`;
1169
- const uploadTarResult = await ssh.upload(tarPath, remoteTar);
1170
- if (!uploadTarResult.success)
1171
- return err(uploadTarResult.error);
1172
- const loadResult = await ssh.exec(`${remoteRuntime} load -i ${remoteTar}`, { timeoutMs: 3e5 });
1173
- if (!loadResult.success)
1174
- return err(loadResult.error);
1175
- const loadedRef = parseLoadedImageRef(loadResult.data.stdout, localImageTag);
1176
- const remoteImageRef = remoteRuntime === "podman" ? `localhost/${localImageTag}` : localImageTag;
1177
- if (loadedRef !== remoteImageRef) {
1178
- const tagResult = await ssh.exec(`${remoteRuntime} tag ${loadedRef} ${remoteImageRef}`);
1179
- if (!tagResult.success)
1180
- return err(tagResult.error);
1437
+ if (options?.environment) {
1438
+ const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
1439
+ if (!environment.success)
1440
+ return environment;
1441
+ const saved = saveDeploymentEnvironment(environment.data, { HAI_DEPLOY_ENVIRONMENT: environment.data.environment, HAI_CORE_ENV: environment.data.values.HAI_CORE_ENV, ...sidecar.env, ...provisionEnv });
1442
+ if (!saved.success)
1443
+ return saved;
1181
1444
  }
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;
1182
1449
  const composeContent = generateComposeFile({
1183
1450
  projectName,
1184
- imageRef: remoteImageRef,
1451
+ appVersion: artifact.appVersion,
1452
+ imageRef: app.imageRef,
1185
1453
  hostPort,
1186
1454
  containerPort,
1187
- sidecar
1455
+ sidecar,
1456
+ web: web ? { imageRef: web.imageRef, containerPort: web.containerPort ?? 8080 } : void 0,
1457
+ serviceModes: options?.serviceModes,
1458
+ previousCompose: previousPath && existsSync(previousPath) ? readFileSync(previousPath, "utf8") : void 0
1188
1459
  });
1189
- const composePath = join(artifactDir, "compose.yml");
1460
+ const composePath = join(artifactDir, composeName);
1190
1461
  writeFileSync(composePath, composeContent);
1191
- const uploadComposeResult = await ssh.upload(composePath, `${remoteDir}/compose.yml`);
1192
- if (!uploadComposeResult.success)
1193
- return err(uploadComposeResult.error);
1194
- const uploadEnvResult = await ssh.upload(envPath, `${remoteDir}/.env.runtime`);
1195
- if (!uploadEnvResult.success)
1196
- return err(uploadEnvResult.error);
1197
- const secretMask = sidecar.secrets;
1198
- const chmodResult = await ssh.exec(`chmod 600 ${remoteDir}/.env.runtime`, { secrets: secretMask });
1199
- if (!chmodResult.success)
1200
- return err(chmodResult.error);
1201
- const upResult = await ssh.exec(
1202
- `cd ${remoteDir} && ${remoteRuntime} compose -p hai-${projectName} up -d`,
1203
- { 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 }
1204
1497
  );
1205
- if (!upResult.success)
1206
- return err(upResult.error);
1207
- await ssh.exec(`rm -f ${remoteTar}`);
1208
- const url = `http://${config.ssh.host}:${hostPort}`;
1209
- logger7.info("Running health check", { url });
1210
- const healthy = await healthCheck(`${url}/`);
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}/`));
1211
1503
  if (!healthy) {
1212
1504
  return err(
1213
1505
  HaiDeployError.HEALTH_CHECK_FAILED,
@@ -1220,14 +1512,23 @@ function createDockerSshProvider(config, localRuntimePref = "auto") {
1220
1512
  status: "ready",
1221
1513
  provider: "docker-ssh",
1222
1514
  healthCheck: "passed",
1223
- envVarsSet: Object.keys(env)
1515
+ envVarsSet: Object.keys(env),
1516
+ bundlePath: bundle.bundlePath
1224
1517
  };
1225
- logger7.info("docker-ssh deployment complete", { url, deploymentId });
1518
+ logger5.info("docker-ssh deployment complete", { url, deploymentId });
1226
1519
  return ok(result);
1227
1520
  };
1228
- 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 };
1229
1530
  }
1230
- var logger8 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
1531
+ var logger6 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
1231
1532
  var VERCEL_API = "https://api.vercel.com";
1232
1533
  var POLL_INTERVAL = 3e3;
1233
1534
  var MAX_POLL_ATTEMPTS = 60;
@@ -1280,7 +1581,7 @@ function createVercelProvider() {
1280
1581
  return {
1281
1582
  name: "vercel",
1282
1583
  async authenticate(apiToken) {
1283
- logger8.debug("Authenticating with Vercel");
1584
+ logger6.debug("Authenticating with Vercel");
1284
1585
  try {
1285
1586
  const user = await vercelFetch(
1286
1587
  apiToken,
@@ -1288,10 +1589,10 @@ function createVercelProvider() {
1288
1589
  );
1289
1590
  token = apiToken;
1290
1591
  const username = user.user.username || user.user.email;
1291
- logger8.info("Vercel authenticated", { user: username });
1592
+ logger6.info("Vercel authenticated", { user: username });
1292
1593
  return ok(username);
1293
1594
  } catch (error) {
1294
- logger8.error("Vercel authentication failed", { error });
1595
+ logger6.error("Vercel authentication failed", { error });
1295
1596
  return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error));
1296
1597
  }
1297
1598
  },
@@ -1302,11 +1603,11 @@ function createVercelProvider() {
1302
1603
  deployM("deploy_authRequired")
1303
1604
  );
1304
1605
  }
1305
- logger8.debug("Creating Vercel project", { projectName });
1606
+ logger6.debug("Creating Vercel project", { projectName });
1306
1607
  try {
1307
1608
  const existingId = await findExistingProject(token, projectName);
1308
1609
  if (existingId !== null) {
1309
- logger8.info("Vercel project found", { projectName, projectId: existingId });
1610
+ logger6.info("Vercel project found", { projectName, projectId: existingId });
1310
1611
  return ok(existingId);
1311
1612
  }
1312
1613
  try {
@@ -1321,13 +1622,13 @@ function createVercelProvider() {
1321
1622
  })
1322
1623
  }
1323
1624
  );
1324
- logger8.info("Vercel project created", { projectName, projectId: project.id });
1625
+ logger6.info("Vercel project created", { projectName, projectId: project.id });
1325
1626
  return ok(project.id);
1326
1627
  } catch (error) {
1327
1628
  if (error instanceof VercelApiError && error.status === 409) {
1328
1629
  const concurrentProjectId = await findExistingProject(token, projectName);
1329
1630
  if (concurrentProjectId !== null) {
1330
- logger8.info("Vercel project reused after create conflict", {
1631
+ logger6.info("Vercel project reused after create conflict", {
1331
1632
  projectName,
1332
1633
  projectId: concurrentProjectId
1333
1634
  });
@@ -1337,7 +1638,7 @@ function createVercelProvider() {
1337
1638
  throw error;
1338
1639
  }
1339
1640
  } catch (error) {
1340
- logger8.error("Vercel project creation failed", { projectName, error });
1641
+ logger6.error("Vercel project creation failed", { projectName, error });
1341
1642
  return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error));
1342
1643
  }
1343
1644
  },
@@ -1348,7 +1649,7 @@ function createVercelProvider() {
1348
1649
  deployM("deploy_authRequired")
1349
1650
  );
1350
1651
  }
1351
- 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 });
1352
1653
  try {
1353
1654
  const envEntries = Object.entries(envVars).map(([key, value]) => ({
1354
1655
  key,
@@ -1364,10 +1665,10 @@ function createVercelProvider() {
1364
1665
  body: JSON.stringify(envEntries)
1365
1666
  }
1366
1667
  );
1367
- logger8.info("Vercel env vars set", { projectId, count: envEntries.length });
1668
+ logger6.info("Vercel env vars set", { projectId, count: envEntries.length });
1368
1669
  return ok(void 0);
1369
1670
  } catch (error) {
1370
- logger8.error("Vercel env vars setup failed", { projectId, error });
1671
+ logger6.error("Vercel env vars setup failed", { projectId, error });
1371
1672
  return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error));
1372
1673
  }
1373
1674
  },
@@ -1378,8 +1679,11 @@ function createVercelProvider() {
1378
1679
  deployM("deploy_authRequired")
1379
1680
  );
1380
1681
  }
1381
- logger8.debug("Deploying to Vercel", { projectId, outputDir });
1682
+ logger6.debug("Deploying to Vercel", { projectId, outputDir });
1382
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"));
1383
1687
  const filePaths = collectFiles(outputDir, outputDir);
1384
1688
  const files = [];
1385
1689
  for (const filePath of filePaths) {
@@ -1388,18 +1692,20 @@ function createVercelProvider() {
1388
1692
  const sha = createHash("sha1").update(content).digest("hex");
1389
1693
  const size = statSync(fullPath).size;
1390
1694
  await uploadFile(token, content, sha);
1391
- files.push({ file: filePath, sha, size });
1695
+ files.push({ file: `.vercel/output/${filePath}`, sha, size });
1392
1696
  }
1393
- logger8.debug("Files uploaded", { count: files.length });
1697
+ logger6.debug("Files uploaded", { count: files.length });
1394
1698
  const deployment = await vercelFetch(
1395
1699
  token,
1396
- "/v13/deployments",
1700
+ "/v13/deployments?prebuilt=1",
1397
1701
  {
1398
1702
  method: "POST",
1399
1703
  body: JSON.stringify({
1400
1704
  name: projectId,
1401
1705
  files,
1402
- projectSettings: { framework: "sveltekit" }
1706
+ version: 2,
1707
+ project: projectId,
1708
+ projectSettings: { framework: null, buildCommand: "", installCommand: "" }
1403
1709
  })
1404
1710
  }
1405
1711
  );
@@ -1410,14 +1716,14 @@ function createVercelProvider() {
1410
1716
  status: finalStatus,
1411
1717
  envVarsSet: []
1412
1718
  };
1413
- logger8.info("Vercel deployment complete", {
1719
+ logger6.info("Vercel deployment complete", {
1414
1720
  deploymentId: deployment.id,
1415
1721
  url: result.url,
1416
1722
  status: finalStatus
1417
1723
  });
1418
1724
  return ok(result);
1419
1725
  } catch (error) {
1420
- logger8.error("Vercel deployment failed", { projectId, error });
1726
+ logger6.error("Vercel deployment failed", { projectId, error });
1421
1727
  return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error));
1422
1728
  }
1423
1729
  }
@@ -1462,11 +1768,11 @@ async function pollDeploymentStatus(token, deploymentId) {
1462
1768
  if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
1463
1769
  return "error";
1464
1770
  }
1465
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
1771
+ await new Promise((resolve6) => setTimeout(resolve6, POLL_INTERVAL));
1466
1772
  }
1467
1773
  return "building";
1468
1774
  }
1469
- var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
1775
+ var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
1470
1776
  function createAliyunProvisioner() {
1471
1777
  let accessKey = null;
1472
1778
  let secretKey = null;
@@ -1475,7 +1781,7 @@ function createAliyunProvisioner() {
1475
1781
  name: "aliyun",
1476
1782
  serviceType: "sms",
1477
1783
  async authenticate(credentials) {
1478
- logger9.debug("Authenticating with Aliyun");
1784
+ logger7.debug("Authenticating with Aliyun");
1479
1785
  try {
1480
1786
  const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
1481
1787
  const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
@@ -1485,10 +1791,10 @@ function createAliyunProvisioner() {
1485
1791
  accessKey = akId;
1486
1792
  secretKey = akSecret;
1487
1793
  signName = credentials.signName;
1488
- logger9.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
1794
+ logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
1489
1795
  return ok(akId);
1490
1796
  } catch (error) {
1491
- logger9.error("Aliyun authentication failed", { error });
1797
+ logger7.error("Aliyun authentication failed", { error });
1492
1798
  return err(
1493
1799
  HaiDeployError.AUTH_FAILED,
1494
1800
  deployM("deploy_authFailed", {
@@ -1505,7 +1811,7 @@ function createAliyunProvisioner() {
1505
1811
  deployM("deploy_authRequired")
1506
1812
  );
1507
1813
  }
1508
- logger9.debug("Provisioning Aliyun SMS service");
1814
+ logger7.debug("Provisioning Aliyun SMS service");
1509
1815
  return ok({
1510
1816
  serviceType: "sms",
1511
1817
  provisionerName: "aliyun",
@@ -1518,7 +1824,7 @@ function createAliyunProvisioner() {
1518
1824
  }
1519
1825
  };
1520
1826
  }
1521
- var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
1827
+ var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
1522
1828
  var NEON_API = "https://console.neon.tech/api/v2";
1523
1829
  var DEFAULT_NEON_DATABASE_NAME = "neondb";
1524
1830
  var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
@@ -1577,7 +1883,7 @@ function createNeonProvisioner() {
1577
1883
  name: "neon",
1578
1884
  serviceType: "db",
1579
1885
  async authenticate(credentials) {
1580
- logger10.debug("Authenticating with Neon");
1886
+ logger8.debug("Authenticating with Neon");
1581
1887
  try {
1582
1888
  const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
1583
1889
  if (!apiToken) {
@@ -1585,10 +1891,10 @@ function createNeonProvisioner() {
1585
1891
  }
1586
1892
  await listNeonProjects(apiToken);
1587
1893
  token = apiToken;
1588
- logger10.info("Neon authenticated");
1894
+ logger8.info("Neon authenticated");
1589
1895
  return ok("neon-user");
1590
1896
  } catch (error) {
1591
- logger10.error("Neon authentication failed", { error });
1897
+ logger8.error("Neon authentication failed", { error });
1592
1898
  return err(
1593
1899
  HaiDeployError.AUTH_FAILED,
1594
1900
  deployM("deploy_authFailed", {
@@ -1605,7 +1911,7 @@ function createNeonProvisioner() {
1605
1911
  deployM("deploy_authRequired")
1606
1912
  );
1607
1913
  }
1608
- logger10.debug("Provisioning Neon database", { appName });
1914
+ logger8.debug("Provisioning Neon database", { appName });
1609
1915
  let resource;
1610
1916
  try {
1611
1917
  const projectName = `${appName}-db`;
@@ -1613,7 +1919,7 @@ function createNeonProvisioner() {
1613
1919
  if (existingProject !== null) {
1614
1920
  resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
1615
1921
  const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
1616
- logger10.info("Neon project reused", {
1922
+ logger8.info("Neon project reused", {
1617
1923
  projectId: existingProject.id,
1618
1924
  projectName
1619
1925
  });
@@ -1635,10 +1941,10 @@ function createNeonProvisioner() {
1635
1941
  if (!projectId || !connectionUri) {
1636
1942
  throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
1637
1943
  }
1638
- logger10.info("Neon database provisioned", { projectId });
1944
+ logger8.info("Neon database provisioned", { projectId });
1639
1945
  return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
1640
1946
  } catch (error) {
1641
- logger10.error("Neon provisioning failed", { appName });
1947
+ logger8.error("Neon provisioning failed", { appName });
1642
1948
  if (resource) {
1643
1949
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
1644
1950
  projectName: appName,
@@ -1660,7 +1966,7 @@ function createNeonProvisioner() {
1660
1966
  }
1661
1967
  };
1662
1968
  }
1663
- var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
1969
+ var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
1664
1970
  var CF_API = "https://api.cloudflare.com/client/v4";
1665
1971
  function createR2Provisioner() {
1666
1972
  let token = null;
@@ -1671,7 +1977,7 @@ function createR2Provisioner() {
1671
1977
  name: "r2",
1672
1978
  serviceType: "storage",
1673
1979
  async authenticate(credentials) {
1674
- logger11.debug("Authenticating with Cloudflare");
1980
+ logger9.debug("Authenticating with Cloudflare");
1675
1981
  try {
1676
1982
  const acctId = credentials.accountId ?? credentials.account_id ?? "";
1677
1983
  const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
@@ -1688,10 +1994,10 @@ function createR2Provisioner() {
1688
1994
  token = apiTok;
1689
1995
  accessKey = credentials.accessKeyId;
1690
1996
  secretKey = credentials.secretAccessKey;
1691
- logger11.info("Cloudflare R2 authenticated", { accountId: acctId });
1997
+ logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
1692
1998
  return ok(acctId);
1693
1999
  } catch (error) {
1694
- logger11.error("Cloudflare R2 authentication failed", { error });
2000
+ logger9.error("Cloudflare R2 authentication failed", { error });
1695
2001
  return err(
1696
2002
  HaiDeployError.AUTH_FAILED,
1697
2003
  deployM("deploy_authFailed", {
@@ -1709,7 +2015,7 @@ function createR2Provisioner() {
1709
2015
  );
1710
2016
  }
1711
2017
  const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1712
- logger11.debug("Provisioning R2 bucket", { appName, bucketName });
2018
+ logger9.debug("Provisioning R2 bucket", { appName, bucketName });
1713
2019
  try {
1714
2020
  const createRes = await fetch(
1715
2021
  `${CF_API}/accounts/${accountId}/r2/buckets`,
@@ -1725,7 +2031,7 @@ function createR2Provisioner() {
1725
2031
  if (!createRes.ok && createRes.status !== 409) {
1726
2032
  throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
1727
2033
  }
1728
- logger11.info("R2 bucket provisioned", { bucketName });
2034
+ logger9.info("R2 bucket provisioned", { bucketName });
1729
2035
  return ok({
1730
2036
  serviceType: "storage",
1731
2037
  provisionerName: "r2",
@@ -1743,7 +2049,7 @@ function createR2Provisioner() {
1743
2049
  resourceInfo: `r2-bucket:${bucketName}`
1744
2050
  });
1745
2051
  } catch (error) {
1746
- logger11.error("R2 provisioning failed", { appName, error });
2052
+ logger9.error("R2 provisioning failed", { appName, error });
1747
2053
  return err(
1748
2054
  HaiDeployError.PROVISION_FAILED,
1749
2055
  deployM("deploy_provisionFailed", {
@@ -1758,7 +2064,7 @@ function createR2Provisioner() {
1758
2064
  }
1759
2065
  };
1760
2066
  }
1761
- var logger12 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
2067
+ var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
1762
2068
  var RESEND_API = "https://api.resend.com";
1763
2069
  function createResendProvisioner() {
1764
2070
  let token = null;
@@ -1767,7 +2073,7 @@ function createResendProvisioner() {
1767
2073
  name: "resend",
1768
2074
  serviceType: "email",
1769
2075
  async authenticate(credentials) {
1770
- logger12.debug("Authenticating with Resend");
2076
+ logger10.debug("Authenticating with Resend");
1771
2077
  try {
1772
2078
  const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
1773
2079
  if (!apiToken || !credentials.from) {
@@ -1781,10 +2087,10 @@ function createResendProvisioner() {
1781
2087
  }
1782
2088
  token = apiToken;
1783
2089
  from = credentials.from;
1784
- logger12.info("Resend authenticated");
2090
+ logger10.info("Resend authenticated");
1785
2091
  return ok("resend-user");
1786
2092
  } catch (error) {
1787
- logger12.error("Resend authentication failed", { error });
2093
+ logger10.error("Resend authentication failed", { error });
1788
2094
  return err(
1789
2095
  HaiDeployError.AUTH_FAILED,
1790
2096
  deployM("deploy_authFailed", {
@@ -1801,7 +2107,7 @@ function createResendProvisioner() {
1801
2107
  deployM("deploy_authRequired")
1802
2108
  );
1803
2109
  }
1804
- logger12.debug("Provisioning Resend email service");
2110
+ logger10.debug("Provisioning Resend email service");
1805
2111
  return ok({
1806
2112
  serviceType: "email",
1807
2113
  provisionerName: "resend",
@@ -1814,7 +2120,7 @@ function createResendProvisioner() {
1814
2120
  }
1815
2121
  };
1816
2122
  }
1817
- var logger13 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
2123
+ var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
1818
2124
  var UPSTASH_API = "https://api.upstash.com";
1819
2125
  function createUpstashAuthHeader(email, apiKey) {
1820
2126
  return `Basic ${Buffer.from(`${email}:${apiKey}`).toString("base64")}`;
@@ -1865,7 +2171,7 @@ function createUpstashProvisioner() {
1865
2171
  name: "upstash",
1866
2172
  serviceType: "cache",
1867
2173
  async authenticate(credentials) {
1868
- logger13.debug("Authenticating with Upstash");
2174
+ logger11.debug("Authenticating with Upstash");
1869
2175
  try {
1870
2176
  const userEmail = credentials.email ?? "";
1871
2177
  const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
@@ -1875,10 +2181,10 @@ function createUpstashProvisioner() {
1875
2181
  await listUpstashDatabases(userEmail, userKey);
1876
2182
  email = userEmail;
1877
2183
  apiKey = userKey;
1878
- logger13.info("Upstash authenticated", { email: userEmail });
2184
+ logger11.info("Upstash authenticated", { email: userEmail });
1879
2185
  return ok(userEmail);
1880
2186
  } catch (error) {
1881
- logger13.error("Upstash authentication failed", { error });
2187
+ logger11.error("Upstash authentication failed", { error });
1882
2188
  return err(
1883
2189
  HaiDeployError.AUTH_FAILED,
1884
2190
  deployM("deploy_authFailed", {
@@ -1895,7 +2201,7 @@ function createUpstashProvisioner() {
1895
2201
  deployM("deploy_authRequired")
1896
2202
  );
1897
2203
  }
1898
- logger13.debug("Provisioning Upstash Redis", { appName });
2204
+ logger11.debug("Provisioning Upstash Redis", { appName });
1899
2205
  let resource;
1900
2206
  try {
1901
2207
  const databaseName = `${appName}-cache`;
@@ -1907,7 +2213,7 @@ function createUpstashProvisioner() {
1907
2213
  }
1908
2214
  resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
1909
2215
  const detail = await getUpstashDatabase(email, apiKey, databaseId);
1910
- logger13.info("Upstash Redis reused", { databaseId, databaseName });
2216
+ logger11.info("Upstash Redis reused", { databaseId, databaseName });
1911
2217
  return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
1912
2218
  }
1913
2219
  const data = await upstashFetch(email, apiKey, "/v2/redis/database", {
@@ -1920,10 +2226,10 @@ function createUpstashProvisioner() {
1920
2226
  });
1921
2227
  if (data.database_id)
1922
2228
  resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
1923
- logger13.info("Upstash Redis provisioned", { databaseId: data.database_id });
2229
+ logger11.info("Upstash Redis provisioned", { databaseId: data.database_id });
1924
2230
  return ok(buildUpstashProvisionResult(data, "created"));
1925
2231
  } catch (error) {
1926
- logger13.error("Upstash provisioning failed", { appName });
2232
+ logger11.error("Upstash provisioning failed", { appName });
1927
2233
  if (resource) {
1928
2234
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
1929
2235
  projectName: appName,
@@ -1947,7 +2253,7 @@ function createUpstashProvisioner() {
1947
2253
  }
1948
2254
 
1949
2255
  // src/deploy-main.ts
1950
- var logger14 = core.logger.child({ module: "deploy", scope: "main" });
2256
+ var logger12 = core.logger.child({ module: "deploy", scope: "main" });
1951
2257
  var currentConfig = null;
1952
2258
  var currentProvider = null;
1953
2259
  var currentContainerProvider = null;
@@ -2000,15 +2306,31 @@ function normalizeProjectName(name) {
2000
2306
  const normalized = name.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
2001
2307
  return normalized || "app";
2002
2308
  }
2003
- function mergeProvisionEnv(results) {
2309
+ function mergeProvisionEnv(results, scan, options) {
2004
2310
  let env = {};
2005
- const reachProviders = [];
2311
+ const reachRequirements = scan?.middlewares?.filter((item) => item.configName === "reach") ?? [];
2312
+ const reachProviders = reachRequirements.map((item) => item.config);
2313
+ let hasReachOutput = false;
2006
2314
  for (const prov of results) {
2007
- if (prov.envVars.HAI_REACH_PROVIDERS)
2008
- reachProviders.push(...JSON.parse(prov.envVars.HAI_REACH_PROVIDERS));
2315
+ if (prov.envVars.HAI_REACH_PROVIDERS) {
2316
+ hasReachOutput = true;
2317
+ const supplied = JSON.parse(prov.envVars.HAI_REACH_PROVIDERS);
2318
+ for (const provider of supplied) {
2319
+ const targets = reachRequirements.filter((item) => {
2320
+ const mode = options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[item.service];
2321
+ return item.service === prov.serviceType && (!mode || mode === "default");
2322
+ });
2323
+ if (targets.length) {
2324
+ for (const target of targets)
2325
+ reachProviders[reachRequirements.indexOf(target)] = { ...provider, name: target.config.name ?? provider.name };
2326
+ } else {
2327
+ reachProviders.push(provider);
2328
+ }
2329
+ }
2330
+ }
2009
2331
  env = { ...env, ...prov.envVars };
2010
2332
  }
2011
- if (reachProviders.length > 0)
2333
+ if (hasReachOutput)
2012
2334
  env.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
2013
2335
  return env;
2014
2336
  }
@@ -2016,7 +2338,7 @@ var deploy = {
2016
2338
  credentials: deployCredentials,
2017
2339
  async init(config) {
2018
2340
  if (initInProgress) {
2019
- logger14.warn("Deploy init already in progress, skipping concurrent call");
2341
+ logger12.warn("Deploy init already in progress, skipping concurrent call");
2020
2342
  return err(
2021
2343
  HaiDeployError.CONFIG_ERROR,
2022
2344
  deployM("deploy_configError", { params: { error: "init already in progress" } })
@@ -2025,13 +2347,13 @@ var deploy = {
2025
2347
  initInProgress = true;
2026
2348
  try {
2027
2349
  if (currentProvider || currentContainerProvider) {
2028
- logger14.warn("Deploy module is already initialized, reinitializing");
2350
+ logger12.warn("Deploy module is already initialized, reinitializing");
2029
2351
  await deploy.close();
2030
2352
  }
2031
- logger14.info("Initializing deploy module");
2353
+ logger12.info("Initializing deploy module");
2032
2354
  const parseResult = DeployConfigSchema.safeParse(config);
2033
2355
  if (!parseResult.success) {
2034
- logger14.error("Deploy config validation failed", { error: parseResult.error.message });
2356
+ logger12.error("Deploy config validation failed", { error: parseResult.error.message });
2035
2357
  return err(
2036
2358
  HaiDeployError.CONFIG_ERROR,
2037
2359
  deployM("deploy_configError", { params: { error: parseResult.error.message } }),
@@ -2043,15 +2365,15 @@ var deploy = {
2043
2365
  let provider = null;
2044
2366
  let containerProvider = null;
2045
2367
  if (parsed.provider.type === "docker-ssh") {
2046
- containerProvider = createDockerSshProvider(parsed.provider, parsed.container?.runtime);
2047
- logger14.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh.host });
2368
+ containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
2369
+ logger12.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh?.host });
2048
2370
  } else {
2049
2371
  provider = createProviderByType(parsed.provider.type);
2050
2372
  const authResult = await provider.authenticate(parsed.provider.token);
2051
2373
  if (!authResult.success) {
2052
2374
  return err(authResult.error);
2053
2375
  }
2054
- logger14.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
2376
+ logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
2055
2377
  }
2056
2378
  currentProvisioners.clear();
2057
2379
  if (parsed.services) {
@@ -2063,14 +2385,14 @@ var deploy = {
2063
2385
  const credentials = extractCredentials(serviceConfig);
2064
2386
  const provAuthResult = await provisioner.authenticate(credentials);
2065
2387
  if (!provAuthResult.success) {
2066
- logger14.error("Provisioner authentication failed", {
2388
+ logger12.error("Provisioner authentication failed", {
2067
2389
  service: serviceType,
2068
2390
  provisioner: serviceConfig.provisioner
2069
2391
  });
2070
2392
  currentProvisioners.clear();
2071
2393
  return err(provAuthResult.error);
2072
2394
  }
2073
- logger14.info("Provisioner authenticated", {
2395
+ logger12.info("Provisioner authenticated", {
2074
2396
  service: serviceType,
2075
2397
  user: provAuthResult.data
2076
2398
  });
@@ -2080,7 +2402,7 @@ var deploy = {
2080
2402
  currentProvider = provider;
2081
2403
  currentContainerProvider = containerProvider;
2082
2404
  currentConfig = parsed;
2083
- logger14.info("Deploy module initialized", {
2405
+ logger12.info("Deploy module initialized", {
2084
2406
  provider: parsed.provider.type,
2085
2407
  provisioners: [...currentProvisioners.keys()]
2086
2408
  });
@@ -2090,7 +2412,7 @@ var deploy = {
2090
2412
  currentContainerProvider = null;
2091
2413
  currentConfig = null;
2092
2414
  currentProvisioners.clear();
2093
- logger14.error("Deploy module initialization failed", { error });
2415
+ logger12.error("Deploy module initialization failed", { error });
2094
2416
  return err(
2095
2417
  HaiDeployError.CONFIG_ERROR,
2096
2418
  deployM("deploy_configError", {
@@ -2105,15 +2427,15 @@ var deploy = {
2105
2427
  },
2106
2428
  async close() {
2107
2429
  if (!currentProvider && !currentContainerProvider) {
2108
- logger14.info("Deploy module already closed, skipping");
2430
+ logger12.info("Deploy module already closed, skipping");
2109
2431
  return;
2110
2432
  }
2111
- logger14.info("Closing deploy module");
2433
+ logger12.info("Closing deploy module");
2112
2434
  currentProvider = null;
2113
2435
  currentContainerProvider = null;
2114
2436
  currentConfig = null;
2115
2437
  currentProvisioners.clear();
2116
- logger14.info("Deploy module closed");
2438
+ logger12.info("Deploy module closed");
2117
2439
  },
2118
2440
  get config() {
2119
2441
  return currentConfig;
@@ -2121,21 +2443,23 @@ var deploy = {
2121
2443
  get isInitialized() {
2122
2444
  return currentProvider !== null || currentContainerProvider !== null;
2123
2445
  },
2124
- async scan(appDir) {
2125
- logger14.debug("Scanning application", { appDir });
2126
- return scanApp(appDir);
2446
+ async scan(appDir, options) {
2447
+ logger12.debug("Scanning application", { appDir });
2448
+ return scanApp(appDir, options);
2127
2449
  },
2128
- async provisionAll(projectName) {
2450
+ async provisionAll(projectName, services) {
2129
2451
  if (!currentProvider && !currentContainerProvider) {
2130
2452
  return notInitialized.result();
2131
2453
  }
2132
- logger14.info("Provisioning all services", { projectName, count: currentProvisioners.size });
2454
+ logger12.info("Provisioning all services", { projectName, count: currentProvisioners.size });
2133
2455
  const results = [];
2134
2456
  for (const [serviceType, provisioner] of currentProvisioners) {
2135
- logger14.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
2457
+ if (services && !services.includes(serviceType))
2458
+ continue;
2459
+ logger12.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
2136
2460
  const provResult = await provisioner.provision(projectName);
2137
2461
  if (!provResult.success) {
2138
- logger14.error("Service provisioning failed", {
2462
+ logger12.error("Service provisioning failed", {
2139
2463
  serviceType,
2140
2464
  provisioner: provisioner.name,
2141
2465
  code: provResult.error.code
@@ -2152,7 +2476,7 @@ var deploy = {
2152
2476
  });
2153
2477
  }
2154
2478
  results.push(provResult.data);
2155
- logger14.info("Service provisioned", { serviceType, provisioner: provisioner.name });
2479
+ logger12.info("Service provisioned", { serviceType, provisioner: provisioner.name });
2156
2480
  }
2157
2481
  return ok(results);
2158
2482
  },
@@ -2160,16 +2484,29 @@ var deploy = {
2160
2484
  if (!currentProvider && !currentContainerProvider) {
2161
2485
  return notInitialized.result();
2162
2486
  }
2163
- logger14.info("Starting deployment", { appDir, options });
2164
- const scanResult = await scanApp(appDir);
2487
+ logger12.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
2488
+ const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
2489
+ const scanResult = await scanApp(appDir, scanOptions);
2165
2490
  if (!scanResult.success) {
2166
2491
  return err(scanResult.error);
2167
2492
  }
2168
2493
  const scan = scanResult.data;
2169
- const projectName = options?.projectName ?? scan.appName;
2494
+ appDir = scan.appDir ?? appDir;
2495
+ const environment = loadDeploymentEnvironment({ environment: scanOptions.environment ?? "prod", envDir: scan.envDir ?? appDir, requireFile: scanOptions.environment !== void 0 });
2496
+ if (!environment.success)
2497
+ return environment;
2498
+ const appEnv = Object.fromEntries(Object.entries(environment.data.values).filter(([key]) => !key.startsWith("HAI_CLI_") && (!key.startsWith("HAI_DEPLOY_") || key === "HAI_DEPLOY_ENVIRONMENT")));
2499
+ const projectName = `${options?.projectName ?? scan.appName}${scanOptions.environment ? `-${environment.data.environment}` : ""}`;
2500
+ options = {
2501
+ ...options,
2502
+ ...scanOptions.environment ? { environment: environment.data.environment } : {},
2503
+ ...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
2504
+ };
2170
2505
  if (currentContainerProvider) {
2171
- return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options);
2506
+ return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
2172
2507
  }
2508
+ if (options?.bundleOnly)
2509
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
2173
2510
  if (!currentProvider) {
2174
2511
  return notInitialized.result();
2175
2512
  }
@@ -2181,15 +2518,22 @@ var deploy = {
2181
2518
  })
2182
2519
  );
2183
2520
  }
2184
- let allEnvVars = {};
2521
+ if (!existsSync(resolveOutputDir(appDir, "vercel")))
2522
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
2523
+ let allEnvVars = appEnv;
2185
2524
  const recovery = { projectName, stage: "provision", resources: [] };
2186
- if (!options?.skipProvision) {
2187
- const provResults = await deploy.provisionAll(projectName);
2525
+ if (!options?.skipMiddleware) {
2526
+ const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
2188
2527
  if (!provResults.success) {
2189
2528
  return err(provResults.error);
2190
2529
  }
2191
2530
  recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
2192
- allEnvVars = mergeProvisionEnv(provResults.data);
2531
+ allEnvVars = { ...allEnvVars, ...mergeProvisionEnv(provResults.data, scan, options) };
2532
+ if (options?.environment) {
2533
+ const saved = saveDeploymentEnvironment(environment.data, mergeProvisionEnv(provResults.data, scan, options));
2534
+ if (!saved.success)
2535
+ return recoveryFailure(saved.error, { ...recovery, stage: "save-env" });
2536
+ }
2193
2537
  }
2194
2538
  const projectResult = await currentProvider.createProject(projectName);
2195
2539
  if (!projectResult.success) {
@@ -2203,12 +2547,6 @@ var deploy = {
2203
2547
  return recoveryFailure(envResult.error, { ...recovery, stage: "set-env" });
2204
2548
  }
2205
2549
  }
2206
- if (!options?.skipBuild) {
2207
- const buildResult = buildApp(appDir, scan.buildCommand, allEnvVars);
2208
- if (!buildResult.success) {
2209
- return recoveryFailure(buildResult.error, { ...recovery, stage: "build" });
2210
- }
2211
- }
2212
2550
  const outputDir = resolveOutputDir(appDir, currentConfig?.provider.type ?? "vercel");
2213
2551
  const deployResult = await currentProvider.deploy(projectId, outputDir);
2214
2552
  if (!deployResult.success) {
@@ -2218,7 +2556,7 @@ var deploy = {
2218
2556
  ...deployResult.data,
2219
2557
  envVarsSet: Object.keys(allEnvVars)
2220
2558
  };
2221
- logger14.info("Deployment completed", {
2559
+ logger12.info("Deployment completed", {
2222
2560
  url: result.url,
2223
2561
  deploymentId: result.deploymentId,
2224
2562
  status: result.status
@@ -2226,25 +2564,79 @@ var deploy = {
2226
2564
  return ok(result);
2227
2565
  }
2228
2566
  };
2229
- async function deployContainerApp(provider, appDir, scan, rawProjectName, options) {
2567
+ async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
2230
2568
  const projectName = normalizeProjectName(rawProjectName);
2231
- if (scan.isSvelteKit && !scan.nodeAdapterInstalled) {
2232
- return err(
2233
- HaiDeployError.ADAPTER_MISSING,
2234
- deployM("deploy_adapterMissing", { params: { adapter: "@sveltejs/adapter-node" } })
2235
- );
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"));
2236
2581
  }
2582
+ if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
2583
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
2237
2584
  let provisionEnv = {};
2238
- if (!options?.skipProvision) {
2239
- const provResults = await deploy.provisionAll(projectName);
2585
+ if (!options?.skipMiddleware) {
2586
+ const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
2240
2587
  if (!provResults.success) {
2241
2588
  return err(provResults.error);
2242
2589
  }
2243
- provisionEnv = mergeProvisionEnv(provResults.data);
2590
+ provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
2244
2591
  }
2245
- return provider.deploy({ appDir, projectName, scan, provisionEnv });
2592
+ return provider.deploy({ appDir, artifact, artifactDir, projectName, scan, provisionEnv, appEnv, options });
2593
+ }
2594
+
2595
+ // src/deploy-middleware.ts
2596
+ var field = (name, defaultValue, kind, secret = false) => ({ name, defaultValue, kind, secret });
2597
+ var required = (name, defaultValue, kind) => ({ ...field(name, defaultValue, kind), required: true });
2598
+ var DEPLOY_MIDDLEWARE = {
2599
+ db: [
2600
+ { backend: "sqlite", label: "SQLite", purpose: "deploy_purposeSqlite", fields: [required("database", "./data/app.db")] },
2601
+ ...["postgresql", "mysql"].map((backend) => ({
2602
+ backend,
2603
+ label: backend === "mysql" ? "MySQL" : "PostgreSQL",
2604
+ purpose: "deploy_purposeDatabase",
2605
+ provisioner: backend === "postgresql" ? "neon" : void 0,
2606
+ fields: [field("url", "", "url", true), required("host", "localhost"), required("port", backend === "mysql" ? "3306" : "5432", "port"), required("database", "hai"), field("user", "hai"), field("password", "", void 0, true), field("ssl", "false")]
2607
+ }))
2608
+ ],
2609
+ cache: [
2610
+ { backend: "memory", label: "Memory", purpose: "deploy_purposeMemory", fields: [] },
2611
+ { backend: "redis", label: "Redis", purpose: "deploy_purposeCache", provisioner: "upstash", fields: [field("url", "", "url", true), required("host", "localhost"), required("port", "6379", "port"), field("password", "", void 0, true), { ...field("db", "0", "number"), maximum: 15 }, field("tls", "false", "boolean"), field("keyPrefix"), field("connectTimeout", "10000", "number"), field("commandTimeout", "5000", "number")] }
2612
+ ],
2613
+ storage: [
2614
+ { backend: "local", label: "Local filesystem", purpose: "deploy_purposeLocal", fields: [required("root", "./data/storage")] },
2615
+ { backend: "s3", label: "S3 / RustFS / MinIO / Cloudflare R2", purpose: "deploy_purposeStorage", provisioner: "cloudflare-r2", fields: [field("endpoint", "", "url"), required("bucket", "hai"), required("region", "us-east-1"), required("accessKeyId"), { ...required("secretAccessKey"), secret: true }, field("forcePathStyle", "true", "boolean")] }
2616
+ ],
2617
+ vecdb: [
2618
+ { backend: "lancedb", label: "LanceDB", purpose: "deploy_purposeLocal", fields: [required("path", "./data/vectors")] },
2619
+ { backend: "qdrant", label: "Qdrant", purpose: "deploy_purposeVector", fields: [required("url", "http://localhost:6333", "url"), field("apiKey", "", void 0, true)] },
2620
+ { backend: "pgvector", label: "PostgreSQL / pgvector", purpose: "deploy_purposeVector", fields: [field("url", "", "url", true), required("host", "localhost"), required("port", "5432", "port"), required("database", "hai"), field("user", "hai"), field("password", "", void 0, true)] },
2621
+ { backend: "chroma", label: "Chroma", purpose: "deploy_purposeVector", fields: [required("url", "http://localhost:8000", "url")] }
2622
+ ],
2623
+ ldap: [
2624
+ { backend: "disabled", label: "Disabled", purpose: "deploy_purposeDisabled", fields: [] },
2625
+ { backend: "ldap", label: "LDAP / OpenLDAP", purpose: "deploy_purposeLdap", fields: [required("url", "ldap://localhost:389", "url"), required("bindDn", "cn=admin,dc=example,dc=org"), { ...required("bindPassword"), secret: true }, required("searchBase", "dc=example,dc=org"), field("searchFilter", "(uid={{username}})"), field("usernameAttribute", "uid"), field("emailAttribute", "mail"), field("displayNameAttribute", "cn"), field("useTls", "false", "boolean"), { ...field("connectTimeout", "5000", "number"), minimum: 1e3 }] }
2626
+ ],
2627
+ email: [
2628
+ { backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
2629
+ { backend: "smtp", label: "SMTP", purpose: "deploy_purposeEmail", provisioner: "resend", fields: [required("host", "localhost"), required("port", "465", "port"), field("secure", "true", "boolean"), field("user"), field("pass", "", void 0, true), required("from", "noreply@example.com")] },
2630
+ { backend: "api", label: "HTTP notification API", purpose: "deploy_purposeEmail", fields: [required("url", "", "url"), field("method", "POST"), field("headers"), field("timeout", "10000", "number")] }
2631
+ ],
2632
+ sms: [
2633
+ { backend: "aliyun-sms", label: "Aliyun SMS", purpose: "deploy_purposeSms", provisioner: "aliyun", fields: [required("accessKeyId"), { ...required("accessKeySecret"), secret: true }, required("signName"), field("endpoint", "dysmsapi.aliyuncs.com")] }
2634
+ ]
2635
+ };
2636
+ function describeMiddlewareBackend(backend) {
2637
+ return deployM(backend.purpose);
2246
2638
  }
2247
2639
 
2248
- export { DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, getDeployRecovery };
2640
+ export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
2249
2641
  //# sourceMappingURL=index.js.map
2250
2642
  //# sourceMappingURL=index.js.map