@h-ai/deploy 0.1.0-alpha9 → 1.0.0-beta.2

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.d.ts CHANGED
@@ -1,24 +1,264 @@
1
1
  import { z } from 'zod';
2
2
  import * as _h_ai_core from '@h-ai/core';
3
- import { HaiResult } from '@h-ai/core';
3
+ import { HaiResult, HaiError } from '@h-ai/core';
4
+
5
+ /**
6
+ * 镜像制品清单,例如 `{ version: 4, primaryService: 'serv', appVersion: '1.2.3', buildId: '1.2.3-build1', partial: false, hasWeb: false, images: [...] }`。
7
+ * 根目录制品保留格式4;非根目录使用格式5,强制旧部署器拒绝而非静默忽略挂载路径。
8
+ * 完整产物包含primaryService,hasWeb记录浏览器端拓扑;结构验证不能代替真实归档校验。
9
+ * @example
10
+ * ```ts
11
+ * import { readFile } from 'node:fs/promises'
12
+ * import { DeploymentArtifactSchema } from '@h-ai/deploy'
13
+ * const content = await readFile('.hai/build/docker/build-id/manifest.json', 'utf8')
14
+ * const parsed = DeploymentArtifactSchema.safeParse(JSON.parse(content))
15
+ * if (!parsed.success) throw new Error('Invalid artifact manifest')
16
+ * // Schema 只验证结构;文件路径与 SHA-256 核验使用 readDeploymentArtifact。
17
+ * ```
18
+ */
19
+ declare const DeploymentArtifactSchema: z.ZodObject<{
20
+ version: z.ZodUnion<readonly [z.ZodLiteral<4>, z.ZodLiteral<5>]>;
21
+ primaryService: z.ZodEnum<{
22
+ serv: "serv";
23
+ web: "web";
24
+ }>;
25
+ appVersion: z.ZodString;
26
+ buildId: z.ZodString;
27
+ partial: z.ZodDefault<z.ZodBoolean>;
28
+ hasWeb: z.ZodDefault<z.ZodBoolean>;
29
+ webBasePath: z.ZodOptional<z.ZodString>;
30
+ servContainerPort: z.ZodOptional<z.ZodNumber>;
31
+ webContainerPort: z.ZodOptional<z.ZodNumber>;
32
+ images: z.ZodArray<z.ZodObject<{
33
+ name: z.ZodEnum<{
34
+ serv: "serv";
35
+ web: "web";
36
+ }>;
37
+ file: z.ZodString;
38
+ imageRef: z.ZodString;
39
+ sha256: z.ZodString;
40
+ containerPort: z.ZodOptional<z.ZodNumber>;
41
+ }, z.core.$strip>>;
42
+ }, z.core.$strip>;
43
+ /**
44
+ * 构建器与部署器共享的镜像交付结构;参见 DeploymentArtifactSchema 示例。
45
+ * @example
46
+ * ```ts
47
+ * import type { DeploymentArtifact } from '@h-ai/deploy'
48
+ * import { readDeploymentArtifact } from '@h-ai/deploy'
49
+ * const directory = '.hai/build/docker/build-id'
50
+ * const artifact = await readDeploymentArtifact(directory)
51
+ * const verified: DeploymentArtifact = artifact
52
+ * const references = verified.images.map(image => image.imageRef)
53
+ * ```
54
+ */
55
+ type DeploymentArtifact = z.infer<typeof DeploymentArtifactSchema>;
56
+ /**
57
+ * 流式计算制品校验和,避免将大镜像一次性读入内存。
58
+ * @throws Error - 文件读取失败;返回真实 tar 内容的 SHA-256,不校验清单其它字段。
59
+ * @param file - 真实镜像归档文件路径。
60
+ * @returns 文件内容的 SHA-256 十六进制摘要。
61
+ * @example
62
+ * ```ts
63
+ * import { artifactChecksum } from '@h-ai/deploy'
64
+ * const digest = await artifactChecksum('.hai/build/docker/build-id/serv-1.0.0.tar')
65
+ * ```
66
+ */
67
+ declare function artifactChecksum(file: string): Promise<string>;
68
+ /**
69
+ * 校验完整构建产物;缺失、损坏或非法清单均抛错,由部署边界转换为 HaiResult。
70
+ * @throws Error - 清单、路径、镜像 tar 或摘要校验失败;不能用未验证的清单绕过本函数。
71
+ * @param directory - 包含已准备制品或部署文件的目录;具体见函数用途。
72
+ * @returns 已经验证的构建制品清单。
73
+ * @example
74
+ * ```ts
75
+ * import { readDeploymentArtifact } from '@h-ai/deploy'
76
+ * const directory = '.hai/build/docker/build-id'
77
+ * const artifact = await readDeploymentArtifact(directory)
78
+ * const images = artifact.images.map(image => ({ name: image.name, imageRef: image.imageRef }))
79
+ * ```
80
+ */
81
+ declare function readDeploymentArtifact(directory: string): Promise<DeploymentArtifact>;
4
82
 
5
83
  /**
6
84
  * @h-ai/deploy — 部署配置 Schema
7
85
  *
8
- * 本文件定义部署模块的错误码、Zod Schema 和配置类型。
86
+ * 本文件定义部署模块的 Zod Schema 和配置类型;错误定义位于 deploy-types.ts。
9
87
  * @module deploy-config
10
88
  */
11
89
 
12
- /** 部署平台配置联合类型(后续可扩展更多平台) */
90
+ /** SSH 连接配置(docker-ssh Provider 使用 OpenSSH 客户端连接远程主机) */
91
+ declare const SshConfigSchema: z.ZodObject<{
92
+ host: z.ZodString;
93
+ port: z.ZodDefault<z.ZodNumber>;
94
+ username: z.ZodString;
95
+ auth: z.ZodOptional<z.ZodEnum<{
96
+ key: "key";
97
+ password: "password";
98
+ }>>;
99
+ password: z.ZodOptional<z.ZodString>;
100
+ identityFile: z.ZodOptional<z.ZodString>;
101
+ knownHostsFile: z.ZodOptional<z.ZodString>;
102
+ strictHostKeyChecking: z.ZodDefault<z.ZodBoolean>;
103
+ connectTimeoutSeconds: z.ZodDefault<z.ZodNumber>;
104
+ }, z.core.$strip>;
105
+ /** 端口直出暴露配置 */
106
+ declare const PortExposeSchema: z.ZodObject<{
107
+ type: z.ZodDefault<z.ZodLiteral<"port">>;
108
+ containerPort: z.ZodDefault<z.ZodNumber>;
109
+ hostPort: z.ZodNumber;
110
+ }, z.core.$strip>;
111
+ /** docker-ssh 部署平台配置 */
112
+ declare const DockerSshProviderConfigSchema: z.ZodObject<{
113
+ type: z.ZodLiteral<"docker-ssh">;
114
+ ssh: z.ZodOptional<z.ZodObject<{
115
+ host: z.ZodString;
116
+ port: z.ZodDefault<z.ZodNumber>;
117
+ username: z.ZodString;
118
+ auth: z.ZodOptional<z.ZodEnum<{
119
+ key: "key";
120
+ password: "password";
121
+ }>>;
122
+ password: z.ZodOptional<z.ZodString>;
123
+ identityFile: z.ZodOptional<z.ZodString>;
124
+ knownHostsFile: z.ZodOptional<z.ZodString>;
125
+ strictHostKeyChecking: z.ZodDefault<z.ZodBoolean>;
126
+ connectTimeoutSeconds: z.ZodDefault<z.ZodNumber>;
127
+ }, z.core.$strip>>;
128
+ remote: z.ZodDefault<z.ZodObject<{
129
+ baseDir: z.ZodDefault<z.ZodString>;
130
+ runtime: z.ZodDefault<z.ZodEnum<{
131
+ auto: "auto";
132
+ docker: "docker";
133
+ podman: "podman";
134
+ }>>;
135
+ }, z.core.$strip>>;
136
+ expose: z.ZodObject<{
137
+ type: z.ZodDefault<z.ZodLiteral<"port">>;
138
+ containerPort: z.ZodDefault<z.ZodNumber>;
139
+ hostPort: z.ZodNumber;
140
+ }, z.core.$strip>;
141
+ trustedEdgeProxies: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6, z.ZodCIDRv4, z.ZodCIDRv6]>>>;
142
+ }, z.core.$strip>;
143
+ /**
144
+ * 部署平台配置联合类型(后续可扩展更多平台)
145
+ * 按 type 区分 Vercel 与 docker-ssh;仅校验配置形状,不验证远端连通性或云凭据。
146
+ * @example
147
+ * ```ts
148
+ * import { ProviderConfigSchema } from '@h-ai/deploy'
149
+ * const result = ProviderConfigSchema.safeParse({ type: 'docker-ssh', expose: { hostPort: 8080 } })
150
+ * if (!result.success) throw new Error('Invalid deployment provider')
151
+ * const provider = result.data
152
+ * ```
153
+ */
13
154
  declare const ProviderConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
14
155
  type: z.ZodLiteral<"vercel">;
15
156
  token: z.ZodString;
157
+ }, z.core.$strip>, z.ZodObject<{
158
+ type: z.ZodLiteral<"docker-ssh">;
159
+ ssh: z.ZodOptional<z.ZodObject<{
160
+ host: z.ZodString;
161
+ port: z.ZodDefault<z.ZodNumber>;
162
+ username: z.ZodString;
163
+ auth: z.ZodOptional<z.ZodEnum<{
164
+ key: "key";
165
+ password: "password";
166
+ }>>;
167
+ password: z.ZodOptional<z.ZodString>;
168
+ identityFile: z.ZodOptional<z.ZodString>;
169
+ knownHostsFile: z.ZodOptional<z.ZodString>;
170
+ strictHostKeyChecking: z.ZodDefault<z.ZodBoolean>;
171
+ connectTimeoutSeconds: z.ZodDefault<z.ZodNumber>;
172
+ }, z.core.$strip>>;
173
+ remote: z.ZodDefault<z.ZodObject<{
174
+ baseDir: z.ZodDefault<z.ZodString>;
175
+ runtime: z.ZodDefault<z.ZodEnum<{
176
+ auto: "auto";
177
+ docker: "docker";
178
+ podman: "podman";
179
+ }>>;
180
+ }, z.core.$strip>>;
181
+ expose: z.ZodObject<{
182
+ type: z.ZodDefault<z.ZodLiteral<"port">>;
183
+ containerPort: z.ZodDefault<z.ZodNumber>;
184
+ hostPort: z.ZodNumber;
185
+ }, z.core.$strip>;
186
+ trustedEdgeProxies: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6, z.ZodCIDRv4, z.ZodCIDRv6]>>>;
16
187
  }, z.core.$strip>], "type">;
17
- /** 部署模块完整配置 Schema */
188
+ /**
189
+ * SSH 连接配置(parse 后)
190
+ * 这是 Schema 解析后的类型,默认字段已必填;auth 省略时非空密码选择 password,否则使用 ssh-agent/默认密钥;显式 key 忽略残留 password。
191
+ * @example
192
+ * ```ts
193
+ * import type { SshConfig } from '@h-ai/deploy'
194
+ * const config: SshConfig = { host: 'deploy.example.net', username: 'deploy', port: 22, strictHostKeyChecking: true, connectTimeoutSeconds: 10 }
195
+ * ```
196
+ */
197
+ type SshConfig = z.infer<typeof SshConfigSchema>;
198
+ /**
199
+ * 端口暴露配置(parse 后)
200
+ * @example
201
+ * ```ts
202
+ * import type { PortExposeConfig } from '@h-ai/deploy'
203
+ * const ports: PortExposeConfig = { type: 'port', hostPort: 8080, containerPort: 3000 }
204
+ * ```
205
+ */
206
+ type PortExposeConfig = z.infer<typeof PortExposeSchema>;
207
+ /**
208
+ * docker-ssh 部署平台配置(parse 后)
209
+ * SSH 可省略用于离线打包;真正在线发布必须提供 SSH 配置。
210
+ * @example
211
+ * ```ts
212
+ * import type { DockerSshProviderConfig } from '@h-ai/deploy'
213
+ * const provider: DockerSshProviderConfig = { type: 'docker-ssh', remote: { baseDir: '/opt/hai/apps', runtime: 'auto' }, expose: { type: 'port', hostPort: 8080, containerPort: 3000 } }
214
+ * ```
215
+ */
216
+ type DockerSshProviderConfig = z.infer<typeof DockerSshProviderConfigSchema>;
217
+ /**
218
+ * 部署模块完整配置 Schema
219
+ * @example
220
+ * ```ts
221
+ * import { DeployConfigSchema } from '@h-ai/deploy'
222
+ * const parsed = DeployConfigSchema.safeParse({ environment: 'prod', provider: { type: 'docker-ssh', expose: { hostPort: 8080 } } })
223
+ * if (!parsed.success) throw new Error('Invalid deployment configuration')
224
+ * const baseDir = parsed.data.provider.type === 'docker-ssh' ? parsed.data.provider.remote.baseDir : undefined
225
+ * ```
226
+ */
18
227
  declare const DeployConfigSchema: z.ZodObject<{
228
+ environment: z.ZodOptional<z.ZodString>;
19
229
  provider: z.ZodDiscriminatedUnion<[z.ZodObject<{
20
230
  type: z.ZodLiteral<"vercel">;
21
231
  token: z.ZodString;
232
+ }, z.core.$strip>, z.ZodObject<{
233
+ type: z.ZodLiteral<"docker-ssh">;
234
+ ssh: z.ZodOptional<z.ZodObject<{
235
+ host: z.ZodString;
236
+ port: z.ZodDefault<z.ZodNumber>;
237
+ username: z.ZodString;
238
+ auth: z.ZodOptional<z.ZodEnum<{
239
+ key: "key";
240
+ password: "password";
241
+ }>>;
242
+ password: z.ZodOptional<z.ZodString>;
243
+ identityFile: z.ZodOptional<z.ZodString>;
244
+ knownHostsFile: z.ZodOptional<z.ZodString>;
245
+ strictHostKeyChecking: z.ZodDefault<z.ZodBoolean>;
246
+ connectTimeoutSeconds: z.ZodDefault<z.ZodNumber>;
247
+ }, z.core.$strip>>;
248
+ remote: z.ZodDefault<z.ZodObject<{
249
+ baseDir: z.ZodDefault<z.ZodString>;
250
+ runtime: z.ZodDefault<z.ZodEnum<{
251
+ auto: "auto";
252
+ docker: "docker";
253
+ podman: "podman";
254
+ }>>;
255
+ }, z.core.$strip>>;
256
+ expose: z.ZodObject<{
257
+ type: z.ZodDefault<z.ZodLiteral<"port">>;
258
+ containerPort: z.ZodDefault<z.ZodNumber>;
259
+ hostPort: z.ZodNumber;
260
+ }, z.core.$strip>;
261
+ trustedEdgeProxies: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodIPv4, z.ZodIPv6, z.ZodCIDRv4, z.ZodCIDRv6]>>>;
22
262
  }, z.core.$strip>], "type">;
23
263
  services: z.ZodOptional<z.ZodObject<{
24
264
  db: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -32,113 +272,183 @@ declare const DeployConfigSchema: z.ZodObject<{
32
272
  }, z.core.$strip>], "provisioner">>;
33
273
  storage: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
34
274
  provisioner: z.ZodLiteral<"cloudflare-r2">;
275
+ accessKeyId: z.ZodString;
276
+ secretAccessKey: z.ZodString;
35
277
  accountId: z.ZodString;
36
278
  apiToken: z.ZodString;
37
279
  }, z.core.$strip>], "provisioner">>;
38
280
  email: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
39
281
  provisioner: z.ZodLiteral<"resend">;
282
+ from: z.ZodString;
40
283
  apiKey: z.ZodString;
41
284
  }, z.core.$strip>], "provisioner">>;
42
285
  sms: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
43
286
  provisioner: z.ZodLiteral<"aliyun">;
287
+ signName: z.ZodString;
44
288
  accessKeyId: z.ZodString;
45
289
  accessKeySecret: z.ZodString;
46
290
  }, z.core.$strip>], "provisioner">>;
47
291
  }, z.core.$strip>>;
292
+ container: z.ZodOptional<z.ZodObject<{
293
+ s3Engine: z.ZodDefault<z.ZodEnum<{
294
+ rustfs: "rustfs";
295
+ minio: "minio";
296
+ }>>;
297
+ }, z.core.$strip>>;
48
298
  }, z.core.$strip>;
49
- /** 部署配置类型(parse 后) */
299
+ /**
300
+ * 部署配置类型(parse 后)
301
+ * @example
302
+ * ```ts
303
+ * import type { DeployConfig } from '@h-ai/deploy'
304
+ * const config: DeployConfig = { environment: 'prod', provider: { type: 'docker-ssh', remote: { baseDir: '/opt/hai/apps', runtime: 'auto' }, expose: { type: 'port', hostPort: 8080, containerPort: 3000 } } }
305
+ * ```
306
+ */
50
307
  type DeployConfig = z.infer<typeof DeployConfigSchema>;
51
- /** 部署配置输入类型(用户输入) */
308
+ /**
309
+ * 部署配置输入类型(用户输入)
310
+ * @example
311
+ * ```ts
312
+ * import type { DeployConfigInput } from '@h-ai/deploy'
313
+ * const input: DeployConfigInput = { provider: { type: 'docker-ssh', expose: { hostPort: 8080 } } } // 由 init 应用 remote 等默认值。
314
+ * ```
315
+ */
52
316
  type DeployConfigInput = z.input<typeof DeployConfigSchema>;
53
317
 
318
+ /** 部署环境的加载和持久化;公开环境文件不保存真实凭据。 */
319
+
54
320
  /**
55
- * @h-ai/deploy — 凭证管理
56
- *
57
- * 管理全局凭证文件 `~/.hai/credentials.yml`。 CLI 在加载 `_deploy.yml` 之前调用 `loadCredentials()` 将凭证注入 process.env, 使 core 的 `interpolateEnv()` 能自动解析 `${HAI_DEPLOY_*}` 变量。
58
- * @module deploy-credentials
321
+ * 部署环境快照;例如 prod 的 targetValues 只来自 `.env.prod`,不包含基础/私有覆盖。
322
+ * @example
323
+ * ```ts
324
+ * import type { DeploymentEnvironment } from '@h-ai/deploy'
325
+ * const environment: DeploymentEnvironment = { environment: 'prod', envDir: '/workspace/shop', values: { HAI_DB_TYPE: 'postgresql' }, targetValues: { HAI_DB_TYPE: 'postgresql' } }
326
+ * // values 可能还含私有凭据,不能整体写入日志。
327
+ * ```
59
328
  */
60
-
329
+ interface DeploymentEnvironment {
330
+ /** 文件模式(dev/test/prod 或其他合法模式名)。 */
331
+ environment: string;
332
+ /** 环境文件的绝对目录。 */
333
+ envDir: string;
334
+ /** 合并后的部署变量,可能含私有值,不得直接记录日志。 */
335
+ values: Record<string, string>;
336
+ /** 仅目标环境文件显式声明的变量,用于判断能否“保持不变”。 */
337
+ targetValues: Record<string, string>;
338
+ }
61
339
  /**
62
- * 获取凭证文件路径
63
- *
64
- * @returns `~/.hai/credentials.yml` 的绝对路径
340
+ * 按来源归并环境变量;HAI 高层父配置清除旧叶,同层按父→叶处理。
341
+ * @param layers - 从低到高优先级的环境来源。
342
+ * @param prefix - 可选变量前缀,仅归并该模块。
343
+ * @returns 归并后的字面量环境变量,可能包含秘密。
344
+ * @example 模块内部上下文
345
+ * ```ts
346
+ * const env = mergeEnvironmentLayers([{ HAI_CACHE_TYPE: 'memory' }, { HAI_CACHE: '{"type":"redis"}' }])
347
+ * ```
65
348
  */
66
- declare function getCredentialsPath(): string;
349
+ declare function mergeEnvironmentLayers(layers: Array<Record<string, string | undefined>>, prefix?: string): Record<string, string>;
67
350
  /**
68
- * 加载全局凭证文件并注入到 process.env
69
- *
70
- * 读取 `~/.hai/credentials.yml` 中的所有键值对,
71
- * 将每个 key-value 设置到 `process.env` 中,
72
- * 使后续 core 配置加载时自动解析 `${HAI_DEPLOY_*}` 变量。
73
- *
74
- * @returns 成功返回加载的凭证键名列表
75
- *
351
+ * 按部署环境解析配置;未选择的可选 Provisioner 缺少凭据时保留空值,选择完成后由 Schema 校验。
352
+ * hai.json 只读取对应的根分区;运行时模块继续读取 YAML。不修改 core 缓存或进程变量。
353
+ * @param name - 模块配置名,例如 deploy/db/iam
354
+ * @param path - hai.json 或运行时 YAML 文件的绝对路径
355
+ * @param values - 已加载的目标环境变量
356
+ * @returns 配置对象;解析错误已脱敏
76
357
  * @example
77
358
  * ```ts
78
- * const result = loadCredentials()
79
- * if (result.success) {
80
- * console.log(`Loaded ${result.data.length} credentials`)
81
- * }
359
+ * import { loadDeploymentConfig, loadDeploymentEnvironment } from '@h-ai/deploy'
360
+ * const environment = loadDeploymentEnvironment({ environment: 'prod', envDir: process.cwd() })
361
+ * if (!environment.success) throw new Error(environment.error.message)
362
+ * const config = loadDeploymentConfig('deploy', `${process.cwd()}/hai.json`, environment.data.values)
363
+ * if (!config.success) throw new Error(config.error.message)
364
+ * // 返回对象仍需经 DeployConfigSchema 或 deploy.init 验证。
82
365
  * ```
83
366
  */
84
- declare function loadCredentials(): HaiResult<string[]>;
367
+ declare function loadDeploymentConfig(name: string, path: string, values: Record<string, string | undefined>): HaiResult<Record<string, unknown>>;
85
368
  /**
86
- * 保存单个凭证到全局凭证文件
87
- *
88
- * 读取已有凭证 → 合并新凭证 → 写回文件。已有同名 key 会被覆盖。
89
- *
90
- * @param key - 环境变量名(如 HAI_DEPLOY_VERCEL_TOKEN)
91
- * @param value - 凭证值
92
- *
369
+ * 加载部署环境;指定环境文件缺失时返回错误并要求用户创建。
370
+ * 部署目标按进程 > 模式私有 > 模式公共 > 旧全局私有 > 基础归并。
371
+ * 其它模块保留进程 > 模式私有 > 旧全局私有 > 模式公共 > 基础;targetValues 仅记录模式公共项。
372
+ * @param options - 模式、环境目录与是否强制存在目标文件
373
+ * @param options.environment - dev/test/prod 或自定义模式
374
+ * @param options.envDir - 环境文件所在目录
375
+ * @param options.requireFile - 默认 true;扫描无显式环境时可只读取已有基础文件
376
+ * @returns 环境快照,可能包含私有值,不得直接输出
93
377
  * @example
94
378
  * ```ts
95
- * saveCredential('HAI_DEPLOY_VERCEL_TOKEN', 'vel_xxx')
379
+ * import { loadDeploymentEnvironment } from '@h-ai/deploy'
380
+ * const loaded = loadDeploymentEnvironment({ environment: 'prod', envDir: process.cwd() })
381
+ * if (!loaded.success) throw new Error(loaded.error.message)
382
+ * const explicitlyConfigured = loaded.data.targetValues.HAI_DB_TYPE
96
383
  * ```
97
384
  */
98
- declare function saveCredential(key: string, value: string): HaiResult<void>;
385
+ declare function loadDeploymentEnvironment(options: {
386
+ environment: string;
387
+ envDir: string;
388
+ requireFile?: boolean;
389
+ }): HaiResult<DeploymentEnvironment>;
99
390
  /**
100
- * 批量保存凭证到全局凭证文件
101
- *
102
- * @param entries - 凭证键值对
103
- *
391
+ * 保存部署变量:公共值写目标环境文件,凭据写 `.env.<environment>.local`;旧共享私有文件不删除。
392
+ * 返回实际写入的变量名,调用者只展示名称或脱敏值。
393
+ * @param environment - 已加载的目标环境快照
394
+ * @param values - 需要更新的变量;未提供的项保持原状
395
+ * @param options - private 为 true 时所有值只保存在本机;适用于 SSH 地址、端口及密钥路径
396
+ * @param options.private - 是否将全部值保存到选中模式的私有文件
397
+ * @returns 已保存的变量名或脱敏错误
104
398
  * @example
105
399
  * ```ts
106
- * saveCredentials({
107
- * HAI_DEPLOY_VERCEL_TOKEN: 'vel_xxx',
108
- * HAI_DEPLOY_NEON_API_KEY: 'neon_xxx',
109
- * })
400
+ * import { loadDeploymentEnvironment, saveDeploymentEnvironment } from '@h-ai/deploy'
401
+ * const loaded = loadDeploymentEnvironment({ environment: 'prod', envDir: process.cwd() })
402
+ * if (!loaded.success) throw new Error(loaded.error.message)
403
+ * const saved = saveDeploymentEnvironment(loaded.data, { HAI_DB_HOST: 'db' })
404
+ * if (!saved.success) throw new Error(saved.error.message)
405
+ * // 密钥应以 { private: true } 写入被忽略的 .env.prod.local,不能放公共环境文件。
110
406
  * ```
111
407
  */
112
- declare function saveCredentials(entries: Record<string, string>): HaiResult<void>;
408
+ declare function saveDeploymentEnvironment(environment: DeploymentEnvironment, values: Record<string, string>, options?: {
409
+ private?: boolean;
410
+ }): HaiResult<string[]>;
113
411
 
412
+ /**
413
+ *
414
+ * @example
415
+ * ```ts
416
+ * import { err } from '@h-ai/core'
417
+ * import { HaiDeployError } from '@h-ai/deploy'
418
+ * const result = err(HaiDeployError.CONFIG_ERROR, 'Invalid deployment configuration')
419
+ * if (!result.success && result.error.code === HaiDeployError.CONFIG_ERROR.code) {
420
+ * process.stderr.write(result.error.message + '\n')
421
+ * }
422
+ * ```
423
+ */
114
424
  declare const HaiDeployError: {
115
425
  readonly DEPLOY_FAILED: _h_ai_core.HaiErrorDef;
116
426
  readonly PROJECT_CREATE_FAILED: _h_ai_core.HaiErrorDef;
117
- readonly BUILD_FAILED: _h_ai_core.HaiErrorDef;
118
427
  readonly UPLOAD_FAILED: _h_ai_core.HaiErrorDef;
119
428
  readonly AUTH_REQUIRED: _h_ai_core.HaiErrorDef;
120
429
  readonly AUTH_FAILED: _h_ai_core.HaiErrorDef;
121
430
  readonly PROVISION_FAILED: _h_ai_core.HaiErrorDef;
122
431
  readonly ADAPTER_MISSING: _h_ai_core.HaiErrorDef;
123
432
  readonly SCAN_FAILED: _h_ai_core.HaiErrorDef;
124
- readonly ENV_VAR_FAILED: _h_ai_core.HaiErrorDef;
125
433
  readonly NOT_INITIALIZED: _h_ai_core.HaiErrorDef;
434
+ readonly ENV_VAR_FAILED: _h_ai_core.HaiErrorDef;
126
435
  readonly UNSUPPORTED_TYPE: _h_ai_core.HaiErrorDef;
127
436
  readonly CONFIG_ERROR: _h_ai_core.HaiErrorDef;
128
437
  readonly CREDENTIAL_ERROR: _h_ai_core.HaiErrorDef;
438
+ readonly REMOTE_CONNECT_FAILED: _h_ai_core.HaiErrorDef;
439
+ readonly REMOTE_COMMAND_FAILED: _h_ai_core.HaiErrorDef;
440
+ readonly IMAGE_TRANSFER_FAILED: _h_ai_core.HaiErrorDef;
441
+ readonly HEALTH_CHECK_FAILED: _h_ai_core.HaiErrorDef;
129
442
  };
130
443
  /**
131
444
  * 部署错误接口
132
445
  *
133
- * 所有部署操作返回的错误都遵循此接口。
134
- *
446
+ * 用于组合错误定义、消息与可选 cause 的描述对象;业务操作返回的 error 遵循 core.HaiError,请按 success 分支读取 code/message。
135
447
  * @example
136
448
  * ```ts
137
- * const result = await deploy.deployApp('./apps/my-app')
138
- * if (!result.success) {
139
- * const error: DeployError = result.error
140
- * // 根据 error.code 处理
141
- * }
449
+ * import type { DeployError } from '@h-ai/deploy'
450
+ * import { HaiDeployError } from '@h-ai/deploy'
451
+ * const detail: DeployError = { def: HaiDeployError.DEPLOY_FAILED, message: 'Deployment failed' }
142
452
  * ```
143
453
  */
144
454
  interface DeployError {
@@ -149,29 +459,110 @@ interface DeployError {
149
459
  /** 原始错误(可选) */
150
460
  cause?: unknown;
151
461
  }
152
- /** 基础设施服务类型 */
153
- type ServiceType = 'db' | 'cache' | 'storage' | 'email' | 'sms';
462
+ /**
463
+ * 基础设施服务类型
464
+ * @example
465
+ * ```ts
466
+ * import type { ServiceType } from '@h-ai/deploy'
467
+ * const service: ServiceType = 'db'
468
+ * ```
469
+ */
470
+ type ServiceType = 'db' | 'cache' | 'storage' | 'email' | 'sms' | 'vecdb' | 'ldap';
471
+ /**
472
+ * 每项中间件的处理方式;keep 不修改配置,也不开通服务。
473
+ * @example
474
+ * ```ts
475
+ * import type { MiddlewareMode } from '@h-ai/deploy'
476
+ * const mode: MiddlewareMode = 'keep' // 复用已有服务,不修改连接配置或自动开通。
477
+ * ```
478
+ */
479
+ type MiddlewareMode = 'default' | 'custom' | 'keep';
480
+ /**
481
+ * 扫描环境;例如 `{ environment: 'test', envDir: '/workspace/app' }`。
482
+ * @example
483
+ * ```ts
484
+ * import type { DeployScanOptions } from '@h-ai/deploy'
485
+ * const options: DeployScanOptions = { environment: 'prod', envDir: '/workspace/shop' }
486
+ * ```
487
+ */
488
+ interface DeployScanOptions {
489
+ /** 环境文件模式。 */
490
+ environment?: string;
491
+ /** 环境文件目录;默认自动定位到应用或 workspace 根。 */
492
+ envDir?: string;
493
+ }
494
+ /**
495
+ * 依赖对应的中间件;例如 IAM 的 LDAP 位于 `iam.ldap`,可处于 disabled 状态。
496
+ * @example
497
+ * ```ts
498
+ * import type { MiddlewareRequirement } from '@h-ai/deploy'
499
+ * const requirement: MiddlewareRequirement = { service: 'db', backend: 'postgresql', source: '@h-ai/reldb', configName: 'db', configPath: [], envPrefix: 'HAI_DB', config: { host: 'db', database: 'shop' } }
500
+ * // config 可能含私有值;展示时只选择安全字段。
501
+ * ```
502
+ */
503
+ interface MiddlewareRequirement {
504
+ /** 中间件类别。 */
505
+ service: ServiceType;
506
+ /** 生效后端标识。 */
507
+ backend: string;
508
+ /** 检测来源(依赖包或配置名),用于解释为何需要此项。 */
509
+ source: string;
510
+ /** 模块配置名。 */
511
+ configName: string;
512
+ /** 模块配置内的路径;例如 LDAP 为 ['ldap']。 */
513
+ configPath: string[];
514
+ /** 约定变量前缀。 */
515
+ envPrefix: string;
516
+ /** 生效连接配置,可能含凭据,仅用于输入默认值,不得原样输出日志。 */
517
+ config: Record<string, unknown>;
518
+ }
154
519
  /**
155
520
  * 应用扫描结果
156
521
  *
157
522
  * 由 scanner 分析应用目录后返回,描述应用的框架类型、依赖和部署需求。
523
+ * 扫描是文件系统能力,不依赖 init,也不代表远程服务已经连通。
524
+ * @example
525
+ * ```ts
526
+ * import type { ScanResult } from '@h-ai/deploy'
527
+ * import { deploy } from '@h-ai/deploy'
528
+ * const result = await deploy.scan(process.cwd(), { environment: 'prod' })
529
+ * if (!result.success) throw new Error(result.error.message)
530
+ * const scan: ScanResult = result.data
531
+ * const services = scan.requiredServices
532
+ * ```
158
533
  */
159
534
  interface ScanResult {
160
- /** 应用名称(来自 package.json name) */
535
+ /** 调用者指定的部署目录,对应 lockfile 中的一个 importer。 */
536
+ appDir?: string;
537
+ /** 公共环境文件目录。 */
538
+ envDir?: string;
539
+ /** 依赖与配置共同确定的中间件详情。 */
540
+ middlewares?: MiddlewareRequirement[];
541
+ /** 从运行依赖图识别的 Hai 模块;peer 和开发依赖不启用模块。 */
542
+ haiModules?: string[];
543
+ /** 工程名称(工程根 package.json.name;缺失时报错)。 */
161
544
  appName: string;
162
545
  /** 是否为 SvelteKit 项目 */
163
546
  isSvelteKit: boolean;
164
547
  /** 目标平台 adapter 是否已安装(如 @sveltejs/adapter-vercel) */
165
548
  adapterInstalled: boolean;
549
+ /** 容器部署所需的 @sveltejs/adapter-node 是否已安装 */
550
+ nodeAdapterInstalled: boolean;
166
551
  /** 检测到的模块依赖需要的服务列表 */
167
552
  requiredServices: ServiceType[];
168
- /** 构建命令 */
169
- buildCommand: string;
553
+ /** 各服务检测到的后端类型(如 db→postgresql/mysql,vecdb→qdrant),用于生成匹配的 sidecar */
554
+ serviceBackends: Partial<Record<ServiceType, string>>;
170
555
  }
171
556
  /**
172
557
  * 部署结果
173
558
  *
174
559
  * 包含部署后的访问 URL、状态和已设置的环境变量。
560
+ * @example
561
+ * ```ts
562
+ * import type { DeployResult } from '@h-ai/deploy'
563
+ * const packaged: DeployResult = { url: '', deploymentId: 'deploy-shop-001', status: 'packaged', bundlePath: '/workspace/shop/.hai/deploy/shop-1.0.0.tar', envVarsSet: [], provider: 'docker-ssh', healthCheck: 'skipped' }
564
+ * // packaged 只代表交付包生成;不将其展示为服务已上线。
565
+ * ```
175
566
  */
176
567
  interface DeployResult {
177
568
  /** 部署后的访问 URL */
@@ -179,16 +570,32 @@ interface DeployResult {
179
570
  /** 部署 ID */
180
571
  deploymentId: string;
181
572
  /** 部署状态 */
182
- status: 'ready' | 'building' | 'error';
573
+ status: 'ready' | 'packaged' | 'building' | 'error';
574
+ /** Compose 部署 tar 的绝对路径;包含私有配置,应安全传输。 */
575
+ bundlePath?: string;
576
+ /** 手工搬运和启动说明;仅打包不代表服务器已运行。 */
577
+ instructions?: string;
183
578
  /** 已设置的环境变量名列表 */
184
579
  envVarsSet: string[];
580
+ /** 部署 Provider 名称(如 vercel / docker-ssh),容器部署时返回 */
581
+ provider?: string;
582
+ /** 健康检查结果:passed 表示 HTTP 探测成功,container-only 表示仅确认容器运行 */
583
+ healthCheck?: 'passed' | 'container-only' | 'skipped';
185
584
  }
186
585
  /**
187
586
  * 基础设施开通结果
188
587
  *
189
588
  * 由 Provisioner 开通服务后返回,包含要注入到部署平台的环境变量。
589
+ * @example
590
+ * ```ts
591
+ * import type { ProvisionResult } from '@h-ai/deploy'
592
+ * const reused: ProvisionResult = { resourceStatus: 'reused', serviceType: 'db', provisionerName: 'neon', envVars: { HAI_DB_TYPE: 'postgresql' }, resourceInfo: 'Existing shop database' }
593
+ * // envVars 可能含密码;不能直接输出。
594
+ * ```
190
595
  */
191
596
  interface ProvisionResult {
597
+ /** 本次新建或复用;凭证型服务始终为 reused */
598
+ resourceStatus: 'created' | 'reused';
192
599
  /** 服务类型 */
193
600
  serviceType: ServiceType;
194
601
  /** Provisioner 名称 */
@@ -199,130 +606,289 @@ interface ProvisionResult {
199
606
  resourceInfo: string;
200
607
  }
201
608
  /**
202
- * 部署平台 Provider 接口(Vercel / Cloudflare Pages 等)
609
+ * 部署失败后的安全恢复信息,不含环境变量值或原始异常。
610
+ * @example
611
+ * ```ts
612
+ * import type { DeployRecoveryInfo } from '@h-ai/deploy'
613
+ * const recovery: DeployRecoveryInfo = { projectName: 'shop', stage: 'provision', resources: [{ serviceType: 'db', provisionerName: 'neon', resourceInfo: 'shop', resourceStatus: 'unknown' }] }
614
+ * // 先在供应商控制台核对 unknown 资源;避免重复创建计费资源。
615
+ * ```
616
+ */
617
+ interface DeployRecoveryInfo {
618
+ projectName: string;
619
+ stage: string;
620
+ platformProjectId?: string;
621
+ resources: Array<{
622
+ serviceType: ServiceType;
623
+ provisionerName: string;
624
+ resourceInfo: string;
625
+ resourceStatus: 'created' | 'reused' | 'unknown';
626
+ }>;
627
+ }
628
+ /**
629
+ * 命令失败后的已核验执行状态;远端 finished 仅指部署脚本及恢复已完成,不代表容器停止。
630
+ * @example
631
+ * ```ts
632
+ * import type { DeployCommandState } from '@h-ai/deploy'
633
+ * const state: DeployCommandState = { reason: 'timeout', localTermination: 'confirmed', remoteExecution: 'unknown', deploymentId: 'deploy-shop-001', resultFile: '/opt/hai/apps/shop/results/deploy-shop-001' }
634
+ * if (state.remoteExecution === 'unknown') process.stderr.write('Verify the matching remote deployment result before retrying\n')
635
+ * ```
636
+ */
637
+ interface DeployCommandState {
638
+ /** 失败触发原因。 */
639
+ reason: 'timeout' | 'output' | 'input' | 'spawn' | 'exit' | 'configuration';
640
+ /** 本地受管理树已全部退出、无法证明,或尚未启动。 */
641
+ localTermination: 'confirmed' | 'unknown' | 'not-started';
642
+ /** SSH 失败默认 unknown;读取匹配部署 ID 的完整终态结果后才为 finished。 */
643
+ remoteExecution?: 'unknown' | 'finished';
644
+ /** 本次不可变部署 ID,可与服务器结果核对。 */
645
+ deploymentId?: string;
646
+ /** 服务器端结果文件;无凭据。 */
647
+ resultFile?: string;
648
+ /** 匹配终态结果中的真实脚本退出码。 */
649
+ remoteExitCode?: number;
650
+ /** 匹配终态结果中的最后阶段。 */
651
+ remoteStage?: string;
652
+ /** 匹配终态中的恢复结果;passed 表示旧服务和 HTTP 验证通过,不包含业务数据回退。 */
653
+ remoteRecovery?: 'not-needed' | 'passed' | 'failed' | 'stopped';
654
+ }
655
+ /**
656
+ * 部署选项
657
+ * 仅更新指定服务时仍受完整/部分制品与首次部署约束;bundleOnly 不触发 SSH。
658
+ * @example
659
+ * ```ts
660
+ * import type { DeployAppOptions } from '@h-ai/deploy'
661
+ * const options: DeployAppOptions = { environment: 'prod', bundleOnly: true, artifactDir: '.hai/build/docker/build-id', selectedServices: ['serv'], output: '.hai/deploy/shop.tar' }
662
+ * ```
663
+ */
664
+ interface DeployAppOptions {
665
+ /** 接收实际执行阶段,用于终端进度显示;不包含凭据或配置原文。 */
666
+ onProgress?: (stage: 'packaging' | 'preflight' | 'images' | 'dependencies' | 'start' | 'health' | 'complete') => void;
667
+ /** 只更新指定的 Compose 服务;省略时完整制品部署全部,部分制品只部署其镜像。 */
668
+ selectedServices?: string[];
669
+ /** 打包中间件镜像所用的本地引擎,auto 优先 Podman。 */
670
+ runtime?: 'auto' | 'podman' | 'docker';
671
+ /** 部署环境标识;显式指定时必须存在 .env.<environment>。 */
672
+ environment?: string;
673
+ /** 环境文件所在目录,默认自动定位。 */
674
+ envDir?: string;
675
+ /** 按服务或变量前缀选择处理方式;如 HAI_REACH_PROVIDERS_1 可独立于另一邮件连接。 */
676
+ serviceModes?: Partial<Record<string, MiddlewareMode>>;
677
+ /** 是否跳过全部中间件配置与开通,默认 false;例如配置完备时设为 true。 */
678
+ skipMiddleware?: boolean;
679
+ /** 已构建镜像目录;默认最近构建入口/锁文件目录下的 .hai/build/docker。 */
680
+ artifactDir?: string;
681
+ /** 只生成独立交付目录(含 tar 和部署脚本),不连接 SSH;仅 docker-ssh 支持。 */
682
+ bundleOnly?: boolean;
683
+ /** 交付包命名路径;默认在 .hai/deploy/<环境>/bundle-<项目>-<部署ID>-<版本>/ 中输出同名 tar 和脚本,自定义 release.tar 输出 release-<版本>/release-<版本>.tar;已有目录拒绝覆盖。 */
684
+ output?: string;
685
+ }
686
+ /**
687
+ * Deploy 凭证文件操作。
203
688
  *
204
- * 通过工厂函数创建(如 `createVercelProvider()`),负责将构建产物部署到目标平台。
689
+ * 这些方法仅操作 `~/.hai/credentials.yml`,不依赖 `deploy.init()`。
690
+ * @example
691
+ * ```ts
692
+ * import type { DeployCredentialOperations } from '@h-ai/deploy'
693
+ * import { deploy } from '@h-ai/deploy'
694
+ * const credentials: DeployCredentialOperations = deploy.credentials
695
+ * const path = credentials.getPath() // 无需先 init。
696
+ * ```
205
697
  */
206
- interface DeployProvider {
207
- /** Provider 名称 */
208
- readonly name: string;
698
+ interface DeployCredentialOperations {
209
699
  /**
210
- * 验证凭证有效性
211
- *
212
- * @param token - 平台 API Token
213
- * @returns 用户信息摘要(如邮箱/用户名)
700
+ * 获取凭证文件绝对路径。
701
+ * @example
702
+ * ```ts
703
+ * import { deploy } from '@h-ai/deploy'
704
+ * const path = deploy.credentials.getPath() // 当前用户的全局凭证文件。
705
+ * ```
214
706
  */
215
- authenticate: (token: string) => Promise<HaiResult<string>>;
707
+ getPath: () => string;
216
708
  /**
217
- * 创建平台项目(幂等:已存在则返回现有项目 ID)
218
- *
219
- * @param projectName - 项目名称
220
- * @returns 项目 ID
709
+ * 加载凭证文件并补齐 `process.env` 缺项;已有同名或父配置优先,返回文件中可识别的键名。
710
+ * @example
711
+ * ```ts
712
+ * import { deploy } from '@h-ai/deploy'
713
+ * const loaded = deploy.credentials.load()
714
+ * if (!loaded.success) throw new Error(loaded.error.message)
715
+ * const names = loaded.data // 不输出被注入的值。
716
+ * ```
221
717
  */
222
- createProject: (projectName: string) => Promise<HaiResult<string>>;
718
+ load: () => HaiResult<string[]>;
223
719
  /**
224
- * 批量设置项目环境变量
225
- *
226
- * @param projectId - 平台项目 ID
227
- * @param envVars - 环境变量键值对
720
+ * 保存单个凭证。
721
+ * @example
722
+ * ```ts
723
+ * import { deploy } from '@h-ai/deploy'
724
+ * const token = process.env.HAI_DEPLOY_PROVIDER_TOKEN
725
+ * if (!token) throw new Error('Provide HAI_DEPLOY_PROVIDER_TOKEN privately')
726
+ * const saved = deploy.credentials.save('HAI_DEPLOY_PROVIDER_TOKEN', token)
727
+ * if (!saved.success) throw new Error(saved.error.message)
728
+ * ```
228
729
  */
229
- setEnvVars: (projectId: string, envVars: Record<string, string>) => Promise<HaiResult<void>>;
730
+ save: (key: string, value: string) => HaiResult<void>;
230
731
  /**
231
- * 上传构建产物并触发部署
232
- *
233
- * @param projectId - 平台项目 ID
234
- * @param outputDir - 构建产物目录(如 .vercel/output/)
235
- * @returns 部署结果
732
+ * 批量保存凭证。
733
+ * @example
734
+ * ```ts
735
+ * import { deploy } from '@h-ai/deploy'
736
+ * const token = process.env.HAI_DEPLOY_PROVIDER_TOKEN
737
+ * if (!token) throw new Error('Provide HAI_DEPLOY_PROVIDER_TOKEN privately')
738
+ * const saved = deploy.credentials.saveAll({ HAI_DEPLOY_PROVIDER_TOKEN: token })
739
+ * if (!saved.success) throw new Error(saved.error.message)
740
+ * ```
236
741
  */
237
- deploy: (projectId: string, outputDir: string) => Promise<HaiResult<DeployResult>>;
238
- }
239
- /**
240
- * 基础设施服务 Provisioner 接口(Neon / Upstash / R2 / Resend / Aliyun)
241
- *
242
- * 通过工厂函数创建(如 `createNeonProvisioner(config)`),负责开通/验证云服务。
243
- */
244
- interface ServiceProvisioner {
245
- /** Provisioner 名称 */
246
- readonly name: string;
247
- /** 对应的服务类型 */
248
- readonly serviceType: ServiceType;
249
- /**
250
- * 验证凭证有效性
251
- *
252
- * @param credentials - 凭证键值对
253
- * @returns 账户标识信息
254
- */
255
- authenticate: (credentials: Record<string, string>) => Promise<HaiResult<string>>;
256
- /**
257
- * 开通/查找服务资源(幂等:按项目名查找已有资源)
258
- *
259
- * @param projectName - 项目名称,用作资源命名
260
- * @returns 开通结果(含环境变量映射)
261
- */
262
- provision: (projectName: string) => Promise<HaiResult<ProvisionResult>>;
263
- }
264
- /** 部署选项 */
265
- interface DeployAppOptions {
266
- /** 项目名称(默认从 package.json name 提取) */
267
- projectName?: string;
268
- /** 是否跳过基础设施开通 */
269
- skipProvision?: boolean;
270
- /** 是否跳过构建 */
271
- skipBuild?: boolean;
742
+ saveAll: (entries: Record<string, string>) => HaiResult<void>;
272
743
  }
273
744
  /**
274
745
  * Deploy 模块功能接口
275
746
  *
276
747
  * 提供自动化部署的完整 API:
748
+ * - `deploy.credentials.*()` — 凭证文件读写(不依赖 init)
277
749
  * - `deploy.init()` — 初始化(加载配置、创建 Provider 和 Provisioner)
278
- * - `deploy.scan()` — 扫描应用依赖
750
+ * - `deploy.scan()` — 扫描应用依赖(纯文件系统能力,不依赖 init)
279
751
  * - `deploy.provisionAll()` — 开通所有已配置的基础设施
280
752
  * - `deploy.deployApp()` — 完整部署流程
281
753
  * - `deploy.close()` — 关闭模块
282
- *
754
+ * scan 与 credentials 不需要 init;部署与开通遵循 init → 检查结果 → use → finally close。close 返回 Promise<void>,不伪造 HaiResult。
283
755
  * @example
284
756
  * ```ts
757
+ * import type { DeployFunctions } from '@h-ai/deploy'
285
758
  * import { deploy } from '@h-ai/deploy'
286
- *
287
- * await deploy.init({ provider: { type: 'vercel', token: 'xxx' } })
288
- * const result = await deploy.deployApp('./apps/my-app')
289
- * if (result.success) {
290
- * console.log(`Deployed: ${result.data.url}`)
291
- * }
292
- * await deploy.close()
759
+ * const operations: DeployFunctions = deploy
760
+ * const scan = await operations.scan(process.cwd())
761
+ * if (!scan.success) throw new Error(scan.error.message)
762
+ * const required = scan.data.requiredServices
293
763
  * ```
294
764
  */
295
765
  interface DeployFunctions {
296
- /** 初始化模块(加载配置、创建 Provider 和 Provisioner 实例) */
766
+ /** 全局部署凭证文件操作。 */
767
+ readonly credentials: DeployCredentialOperations;
768
+ /**
769
+ * 初始化模块(加载配置、创建 Provider 和 Provisioner 实例)
770
+ * @example
771
+ * ```ts
772
+ * import { deploy } from '@h-ai/deploy'
773
+ * const initialized = await deploy.init({ provider: { type: 'docker-ssh', expose: { hostPort: 8080 } } })
774
+ * if (!initialized.success) throw new Error(initialized.error.message)
775
+ * try {
776
+ * const ready = deploy.isInitialized
777
+ * }
778
+ * finally {
779
+ * await deploy.close()
780
+ * }
781
+ * ```
782
+ */
297
783
  init: (config: DeployConfigInput) => Promise<HaiResult<void>>;
298
- /** 关闭模块 */
784
+ /**
785
+ * 关闭模块
786
+ * @example
787
+ * ```ts
788
+ * import { deploy } from '@h-ai/deploy'
789
+ * const initialized = await deploy.init({ provider: { type: 'docker-ssh', expose: { hostPort: 8080 } } })
790
+ * if (!initialized.success) throw new Error(initialized.error.message)
791
+ * try {
792
+ * const provider = deploy.config?.provider.type
793
+ * }
794
+ * finally {
795
+ * await deploy.close() // Promise<void>;不检查不存在的 success 字段。
796
+ * }
797
+ * ```
798
+ */
299
799
  close: () => Promise<void>;
300
800
  /** 当前配置 */
301
801
  readonly config: DeployConfig | null;
302
802
  /** 是否已初始化 */
303
803
  readonly isInitialized: boolean;
304
804
  /**
305
- * 扫描应用目录,检测依赖和所需服务
805
+ * 扫描应用目录,检测依赖和所需服务。
806
+ *
807
+ * 这是纯文件系统分析能力,不依赖云端认证状态,可在 `deploy.init()` 之前调用。
306
808
  *
307
809
  * @param appDir - 应用根目录路径
810
+ * @example
811
+ * ```ts
812
+ * import { deploy } from '@h-ai/deploy'
813
+ * const scanned = await deploy.scan(process.cwd(), { environment: 'prod' })
814
+ * if (!scanned.success) throw new Error(scanned.error.message)
815
+ * const services = scanned.data.requiredServices // 无需 init。
816
+ * ```
308
817
  */
309
- scan: (appDir: string) => Promise<HaiResult<ScanResult>>;
818
+ scan: (appDir: string, options?: DeployScanOptions) => Promise<HaiResult<ScanResult>>;
310
819
  /**
311
820
  * 对所有已配置的 Provisioner 执行基础设施开通
312
821
  *
313
822
  * @param projectName - 用于资源命名的项目名
314
823
  * @returns 所有 Provisioner 的结果列表
824
+ * @example
825
+ * ```ts
826
+ * import { deploy, getDeployRecovery } from '@h-ai/deploy'
827
+ * const apiKey = process.env.HAI_DEPLOY_SERVICES_DB_APIKEY
828
+ * if (!apiKey) throw new Error('Provide HAI_DEPLOY_SERVICES_DB_APIKEY privately')
829
+ * const initialized = await deploy.init({ provider: { type: 'docker-ssh', expose: { hostPort: 8080 } }, services: { db: { provisioner: 'neon', apiKey } } })
830
+ * if (!initialized.success) throw new Error(initialized.error.message)
831
+ * try {
832
+ * // 会访问真实云服务并可能新建资源。
833
+ * const result = await deploy.provisionAll('shop', ['db'])
834
+ * if (!result.success) {
835
+ * const recovery = getDeployRecovery(result.error)
836
+ * if (recovery?.resources.some(resource => resource.resourceStatus === 'unknown')) {
837
+ * process.stderr.write('Verify unknown resources at the provider before retrying\n')
838
+ * }
839
+ * throw new Error(result.error.message)
840
+ * }
841
+ * }
842
+ * finally {
843
+ * await deploy.close()
844
+ * }
845
+ * ```
315
846
  */
316
- provisionAll: (projectName: string) => Promise<HaiResult<ProvisionResult[]>>;
847
+ provisionAll: (projectName: string, services?: ServiceType[]) => Promise<HaiResult<ProvisionResult[]>>;
317
848
  /**
318
- * 执行完整部署流程(provision → build → deploy)
849
+ * 部署已有构建产物(环境和中间件 → 打包 → 上传/启动);不会调用构建命令。
319
850
  *
320
851
  * @param appDir - 应用根目录路径
321
852
  * @param options - 部署选项
853
+ * @example
854
+ * ```ts
855
+ * import { deploy } from '@h-ai/deploy'
856
+ * const initialized = await deploy.init({ provider: { type: 'docker-ssh', expose: { hostPort: 8080 } } })
857
+ * if (!initialized.success) throw new Error(initialized.error.message)
858
+ * try {
859
+ * // 当前项目必须已有构建镜像清单与生产环境配置。
860
+ * const result = await deploy.deployApp(process.cwd(), { environment: 'prod', bundleOnly: true })
861
+ * if (!result.success) throw new Error(result.error.message)
862
+ * const status = result.data.status // packaged 不代表线上运行。
863
+ * }
864
+ * finally {
865
+ * await deploy.close()
866
+ * }
867
+ * ```
322
868
  */
323
869
  deployApp: (appDir: string, options?: DeployAppOptions) => Promise<HaiResult<DeployResult>>;
324
870
  }
325
871
 
872
+ /**
873
+ * @h-ai/deploy — 部署辅助函数
874
+ *
875
+ * 包含已有产物目录解析等与部署流程相关的辅助操作。 从 deploy-main.ts 中提取,遵循 main 仅做生命周期管理的规范。
876
+ * @module deploy-functions
877
+ */
878
+
879
+ /**
880
+ * 将工程根 package.json.name 归一化为镜像与部署资源共用的身份;移除 scope、转小写并合并非法字符。
881
+ * @throws Error - 名称为空或归一化后不含字母数字。
882
+ * @param name - 工程根 package.json.name。
883
+ * @returns 可用于 OCI 镜像和 Compose 资源的工程名。
884
+ * @example
885
+ * ```ts
886
+ * import { normalizeProjectName } from '@h-ai/deploy'
887
+ * const name = normalizeProjectName('@shop/orders')
888
+ * ```
889
+ */
890
+ declare function normalizeProjectName(name: string): string;
891
+
326
892
  /**
327
893
  * @h-ai/deploy — 模块主入口
328
894
  *
@@ -332,39 +898,262 @@ interface DeployFunctions {
332
898
 
333
899
  /**
334
900
  * Deploy 模块单例
335
- *
901
+ * 每次 init 检查 HaiResult;只打包不会连接 SSH,成功打包也不代表服务器已部署。远端 unknown 必须先核对匹配部署 ID 的结果,不能盲目重试。
336
902
  * @example
337
903
  * ```ts
338
- * import { deploy } from '@h-ai/deploy'
339
- *
340
- * await deploy.init({ provider: { type: 'vercel', token: 'xxx' } })
341
- * const result = await deploy.deployApp('./apps/my-app')
342
- * await deploy.close()
904
+ * import { deploy, getDeployCommandState } from '@h-ai/deploy'
905
+ * const initialized = await deploy.init({ provider: { type: 'docker-ssh', expose: { hostPort: 8080 } } })
906
+ * if (!initialized.success) throw new Error(initialized.error.message)
907
+ * try {
908
+ * // 当前项目已有镜像产物与部署环境;只打包,不连接服务器。
909
+ * const result = await deploy.deployApp(process.cwd(), { environment: 'prod', bundleOnly: true })
910
+ * if (!result.success) {
911
+ * const state = getDeployCommandState(result.error)
912
+ * if (state?.remoteExecution === 'unknown') {
913
+ * process.stderr.write('Inspect the matching server result before retrying\n')
914
+ * }
915
+ * throw new Error(result.error.message)
916
+ * }
917
+ * if (result.data.status === 'packaged') {
918
+ * const bundle = result.data.bundlePath
919
+ * }
920
+ * }
921
+ * finally {
922
+ * await deploy.close()
923
+ * }
343
924
  * ```
344
925
  */
345
926
  declare const deploy: DeployFunctions;
346
927
 
347
928
  /**
348
- * @h-ai/deploy — 应用扫描器
929
+ * @h-ai/deploy — i18n
349
930
  *
350
- * 扫描应用目录,检测框架类型、模块依赖和所需基础设施。
351
- * @module deploy-scanner
931
+ * 本文件提供部署模块的 i18n 文案访问入口。
932
+ * @module deploy-i18n
352
933
  */
934
+ /**
935
+ * 获取部署模块文案
936
+ * 使用 core 请求/全局 locale 的现有中英字典;不自行缓存调用时的语言。
937
+ * @param key - 文案 Key
938
+ * @param options - 插值参数与格式化选项
939
+ * @returns 本地化后的文案字符串
940
+ * @example 模块内部上下文
941
+ * ```ts
942
+ * const message = deployM('deploy_sshRequired')
943
+ * ```
944
+ */
945
+ declare const deployM: (key: "deploy_notInitialized" | "deploy_initFailed" | "deploy_unsupportedProviderType" | "deploy_unsupportedProvisionerType" | "deploy_configTokenRequired" | "deploy_authRequired" | "deploy_authFailed" | "deploy_projectCreateFailed" | "deploy_uploadFailed" | "deploy_deployFailed" | "deploy_provisionFailed" | "deploy_adapterMissing" | "deploy_scanFailed" | "deploy_envVarFailed" | "deploy_credentialError" | "deploy_credentialInvalidKey" | "deploy_credentialSaved" | "deploy_credentialLoaded" | "deploy_configError" | "deploy_credentialMissing" | "deploy_apiError" | "deploy_fileUploadFailed" | "deploy_provisionNoResult" | "deploy_recoveryFailure" | "deploy_recoverySuggestion" | "deploy_commandTimeout" | "deploy_commandFailed" | "deploy_remoteConnectFailed" | "deploy_remoteRuntimeMissing" | "deploy_imageTransferFailed" | "deploy_healthCheckFailed" | "deploy_invalidEnvironment" | "deploy_environmentMissing" | "deploy_environmentReadFailed" | "deploy_environmentWriteFailed" | "deploy_multipleApplications" | "deploy_purposeSqlite" | "deploy_purposeDatabase" | "deploy_purposeMemory" | "deploy_purposeLocal" | "deploy_purposeCache" | "deploy_purposeStorage" | "deploy_purposeVector" | "deploy_purposeDisabled" | "deploy_purposeLdap" | "deploy_purposeConsole" | "deploy_purposeEmail" | "deploy_purposeSms" | "deploy_webProxyInvalid" | "deploy_webBaseConflict" | "deploy_storageProxyInvalid" | "deploy_storagePublicEndpointInvalid" | "deploy_storageBackendRequired" | "deploy_artifactInvalid" | "deploy_sshRequired" | "deploy_sshPasswordInvalid" | "deploy_sshAskpassFailed" | "deploy_sshAskpassCleanupFailed" | "deploy_targetAuth" | "deploy_targetPasswordEnv" | "deploy_bundleInstructions" | "deploy_bundleMetadata" | "deploy_targetHost" | "deploy_targetPort" | "deploy_targetUsername" | "deploy_targetIdentityFile" | "deploy_targetKnownHostsFile" | "deploy_targetStrictHostKeyChecking" | "deploy_targetConnectTimeoutSeconds" | "deploy_targetDirectory" | "deploy_targetBundle" | "deploy_bundleOnlyContainer" | "deploy_selectedContainerOnly" | "deploy_selectionInvalid" | "deploy_imageVersionRequired" | "deploy_bundleRuntimeRequired" | "deploy_bundleFailed" | "deploy_configFileInvalid" | "deploy_configFileUnreadable" | "deploy_bundleNameInvalid" | "deploy_localStopped" | "deploy_localUnknown" | "deploy_localNotStarted" | "deploy_commandOutputFailed" | "deploy_commandInputFailed" | "deploy_commandTreeActive" | "deploy_commandExecutionFailed" | "deploy_remoteUnknown" | "deploy_remoteFinished" | "deploy_bundleDirectoryExists" | "deploy_bundleFull" | "deploy_bundlePartial" | "deploy_bundleSelection" | "deploy_transportBodyLimitInvalid" | "deploy_transportKeyRingInvalid" | "deploy_uploadPasswordPrompt" | "deploy_uploadPasswordUnavailable" | "deploy_uploadPasswordCancelled" | "deploy_uploadPasswordInvalid" | "deploy_managedSecretsInvalid" | "deploy_secretImportFailed" | "deploy_projectNameRequired" | "deploy_projectNameOverrideRemoved" | "deploy_requestAccessInvalid", options?: _h_ai_core.MessageOptions) => string;
353
946
 
354
947
  /**
355
- * 扫描应用目录,检测框架类型、模块依赖和所需基础设施
356
- *
357
- * @param appDir - 应用根目录绝对路径
358
- * @returns 扫描结果(ScanResult)
359
- *
948
+ * 部署中间件的后端与连接字段目录,供扫描、CLI 和容器规划共同使用。
949
+ * 字段名与对应模块公开 Schema 保持一致,不把模块依赖等同于必须创建外部服务。
950
+ * @module deploy-middleware
951
+ */
952
+
953
+ /**
954
+ * 连接字段;例如 `{ name: 'port', defaultValue: '5432', kind: 'port' }`。
955
+ * @example
956
+ * ```ts
957
+ * import type { MiddlewareField } from '@h-ai/deploy'
958
+ * const field: MiddlewareField = { name: 'port', defaultValue: '5432', required: true, kind: 'port' }
959
+ * ```
960
+ */
961
+ interface MiddlewareField {
962
+ /** 模块配置字段名,camelCase 不拆词。 */
963
+ name: string;
964
+ /** 无现有配置时使用的提示默认值。 */
965
+ defaultValue?: string;
966
+ /** 是否为连接所必需;可选项允许留空。 */
967
+ required?: boolean;
968
+ /** 密码输入与私有环境文件写入标记。 */
969
+ secret?: boolean;
970
+ /** 交互校验类型;端口范围为 1–65535。 */
971
+ kind?: 'port' | 'boolean' | 'number' | 'url';
972
+ /** 数值下限;未声明时为 0。 */
973
+ minimum?: number;
974
+ /** 数值上限;例如 Redis db 为 15。 */
975
+ maximum?: number;
976
+ }
977
+ /**
978
+ * 后端目录项;例如 PostgreSQL 对应 `backend: 'postgresql'`。
979
+ * @example
980
+ * ```ts
981
+ * import type { MiddlewareBackend } from '@h-ai/deploy'
982
+ * const backend: MiddlewareBackend = { backend: 'postgresql', label: 'PostgreSQL', purpose: 'deploy_purposeDatabase', fields: [{ name: 'database', required: true }] }
983
+ * ```
984
+ */
985
+ interface MiddlewareBackend {
986
+ /** 模块 Schema 接受的后端标识。 */
987
+ backend: string;
988
+ /** 可直接识别的产品名称。 */
989
+ label: string;
990
+ /** 用途说明的 i18n 消息键。 */
991
+ purpose: Parameters<typeof deployM>[0];
992
+ /** 云自动开通器;缺失表示不能自动开通该云服务。 */
993
+ provisioner?: 'neon' | 'upstash' | 'cloudflare-r2' | 'resend' | 'aliyun';
994
+ /** 自定义连接需要逐项输入的字段。 */
995
+ fields: MiddlewareField[];
996
+ }
997
+ /**
998
+ * 所有可部署中间件的连接目录。
999
+ * SQLite、memory、local 与 disabled 保留本地/禁用语义,不创建无关 sidecar。
1000
+ * 目录同时描述本机、禁用、自建与云后端;存在依赖不等于必须开通新资源。
360
1001
  * @example
361
1002
  * ```ts
362
- * const result = await scanApp('/path/to/my-app')
363
- * if (result.success) {
364
- * const { appName, isSvelteKit, requiredServices } = result.data
1003
+ * import { DEPLOY_MIDDLEWARE, describeMiddlewareBackend } from '@h-ai/deploy'
1004
+ * const choices = DEPLOY_MIDDLEWARE.db.map(backend => ({ value: backend.backend, description: describeMiddlewareBackend(backend) }))
1005
+ * ```
1006
+ */
1007
+ declare const DEPLOY_MIDDLEWARE: Record<ServiceType, MiddlewareBackend[]>;
1008
+ /**
1009
+ * 返回当前语言的后端用途说明。
1010
+ * @param backend - 部署目录中的后端条目。
1011
+ * @returns 当前 locale 的用途说明。
1012
+ * @example
1013
+ * ```ts
1014
+ * import { DEPLOY_MIDDLEWARE, describeMiddlewareBackend } from '@h-ai/deploy'
1015
+ * const backend = DEPLOY_MIDDLEWARE.db.find(item => item.backend === 'postgresql')
1016
+ * if (backend) {
1017
+ * const description = describeMiddlewareBackend(backend)
1018
+ * }
1019
+ * ```
1020
+ */
1021
+ declare function describeMiddlewareBackend(backend: MiddlewareBackend): string;
1022
+
1023
+ /**
1024
+ * 查看与执行入口相同的扫描、环境、项目身份和制品校验结果。远端 current 只报告为待执行时核验。
1025
+ * @example
1026
+ * ```ts
1027
+ * import { createDeploymentPlan } from '@h-ai/deploy'
1028
+ * const plan = await createDeploymentPlan('/workspace/shop', { provider: { type: 'docker-ssh', expose: { hostPort: 8080 } } }, { environment: 'prod' })
1029
+ * ```
1030
+ */
1031
+ declare function createDeploymentPlan(appDir: string, input: DeployConfigInput, options?: DeployAppOptions & {
1032
+ inspectRemote?: boolean;
1033
+ }): Promise<{
1034
+ success: false;
1035
+ error: _h_ai_core.HaiError;
1036
+ } | {
1037
+ success: true;
1038
+ data: {
1039
+ projectName: string;
1040
+ root: string;
1041
+ appDir: string;
1042
+ environment: string;
1043
+ target: {
1044
+ provider: "docker-ssh";
1045
+ host: string | undefined;
1046
+ port: number | undefined;
1047
+ directory: string | undefined;
1048
+ runtime: "auto" | "docker" | "podman";
1049
+ hostPort: number;
1050
+ } | {
1051
+ provider: "vercel";
1052
+ host?: undefined;
1053
+ port?: undefined;
1054
+ directory?: undefined;
1055
+ runtime?: undefined;
1056
+ hostPort?: undefined;
1057
+ };
1058
+ artifact: {
1059
+ directory: string;
1060
+ version: string;
1061
+ buildId: string;
1062
+ verified: boolean;
1063
+ images: {
1064
+ name: "serv" | "web";
1065
+ reference: string;
1066
+ sha256: string;
1067
+ }[];
1068
+ } | undefined;
1069
+ selected: string[];
1070
+ retained: string[];
1071
+ containers: string[];
1072
+ composeManagedServices: string[];
1073
+ logicalVolumes: string[];
1074
+ middleware: {
1075
+ service: ServiceType;
1076
+ backend: string;
1077
+ configuration: string;
1078
+ action: MiddlewareMode;
1079
+ provisioner: string | undefined;
1080
+ }[];
1081
+ configuration: {
1082
+ files: string[];
1083
+ environmentKeys: string[];
1084
+ };
1085
+ baseline: {
1086
+ required: boolean;
1087
+ state: string;
1088
+ current: string | undefined;
1089
+ release: string | undefined;
1090
+ };
1091
+ recovery: {
1092
+ releases: string | undefined;
1093
+ point: string;
1094
+ preservesVolumes: boolean;
1095
+ databaseRollback: boolean;
1096
+ };
1097
+ };
1098
+ }>;
1099
+
1100
+ /**
1101
+ * 读取命令失败的安全状态;不把本地 SSH 退出解释为远端停止。
1102
+ * @param error - 部署错误
1103
+ * @returns 通过结构校验的命令状态,或 undefined
1104
+ * @example
1105
+ * ```ts
1106
+ * import { getDeployCommandState, HaiDeployError } from '@h-ai/deploy'
1107
+ * const state = getDeployCommandState({ ...HaiDeployError.REMOTE_COMMAND_FAILED, message: 'SSH connection interrupted', ext: { commandExecution: { reason: 'exit', localTermination: 'confirmed', remoteExecution: 'unknown' } } })
1108
+ * if (state?.remoteExecution === 'unknown') process.stderr.write('Remote execution is unknown\n')
1109
+ * ```
1110
+ */
1111
+ declare function getDeployCommandState(error: HaiError): DeployCommandState | undefined;
1112
+ /**
1113
+ * 读取错误中的恢复清单;仅返回明确允许展示的字段。
1114
+ * @param error - 上层捕获的部署 HaiError;不直接展示其 cause。
1115
+ * @returns 安全恢复清单;结构不满足约定时为 undefined。
1116
+ * @example
1117
+ * ```ts
1118
+ * import { getDeployRecovery, HaiDeployError } from '@h-ai/deploy'
1119
+ * const recovery = getDeployRecovery({ ...HaiDeployError.PROVISION_FAILED, message: 'Provisioning failed', ext: { recovery: { projectName: 'shop', stage: 'provision', resources: [] } } })
1120
+ * if (recovery) {
1121
+ * const stage = recovery.stage
365
1122
  * }
366
1123
  * ```
367
1124
  */
368
- declare function scanApp(appDir: string): Promise<HaiResult<ScanResult>>;
1125
+ declare function getDeployRecovery(error: HaiError): DeployRecoveryInfo | undefined;
1126
+
1127
+ /** 旧部署凭据的显式、只读预检与原子导入;不轮换密码、不变更容器和数据卷。 */
1128
+
1129
+ /**
1130
+ * 在目标服务器导入前核验旧凭据;dryRun 默认 true,apply 需要 Linux flock 与运行中的原服务。
1131
+ * 输入文件必须由操作者明确指定,权限 0600;API 不读取旧部署包猜测密码。
1132
+ * @param options - 服务器项目目录、受保护的导入 JSON、容器引擎和只读开关。
1133
+ * @param options.directory - 目标 Linux 服务器上已存在的项目部署目录。
1134
+ * @param options.inputFile - 操作者提供的版本化私有 JSON 文件路径。
1135
+ * @param options.runtime - 实际使用的 Docker 或 Podman 引擎。
1136
+ * @param options.dryRun - 默认 true;只有显式 false 才在部署锁内保存状态。
1137
+ * @returns 仅包含项目、凭据组和卷名的回执,不包含密码;失败不推进 current 或改变服务。
1138
+ * @example
1139
+ * ```ts
1140
+ * import { importDeploymentSecrets } from '@h-ai/deploy'
1141
+ * const plan = await importDeploymentSecrets({ directory: '/opt/hai/apps/shop-test', inputFile: '/private/shop-import.json', runtime: 'docker' })
1142
+ * if (!plan.success) throw new Error(plan.error.message)
1143
+ * // 停止业务写入、备份数据后,用相同输入显式传 dryRun: false 导入。
1144
+ * ```
1145
+ */
1146
+ declare function importDeploymentSecrets(options: {
1147
+ directory: string;
1148
+ inputFile: string;
1149
+ runtime: 'docker' | 'podman';
1150
+ dryRun?: boolean;
1151
+ }): Promise<HaiResult<{
1152
+ projectName: string;
1153
+ stateVersion: 2;
1154
+ groups: string[];
1155
+ volumes: string[];
1156
+ changed: boolean;
1157
+ }>>;
369
1158
 
370
- export { type DeployAppOptions, type DeployConfig, type DeployConfigInput, DeployConfigSchema, type DeployError, type DeployFunctions, type DeployProvider, type DeployResult, HaiDeployError, ProviderConfigSchema, type ProvisionResult, type ScanResult, type ServiceProvisioner, type ServiceType, deploy, getCredentialsPath, loadCredentials, saveCredential, saveCredentials, scanApp };
1159
+ export { DEPLOY_MIDDLEWARE, type DeployAppOptions, type DeployCommandState, type DeployConfig, type DeployConfigInput, DeployConfigSchema, type DeployCredentialOperations, type DeployError, type DeployFunctions, type DeployRecoveryInfo, type DeployResult, type DeployScanOptions, type DeploymentArtifact, DeploymentArtifactSchema, type DeploymentEnvironment, type DockerSshProviderConfig, HaiDeployError, type MiddlewareBackend, type MiddlewareField, type MiddlewareMode, type MiddlewareRequirement, type PortExposeConfig, ProviderConfigSchema, type ProvisionResult, type ScanResult, type ServiceType, type SshConfig, artifactChecksum, createDeploymentPlan, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, importDeploymentSecrets, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, readDeploymentArtifact, saveDeploymentEnvironment };