@h-ai/deploy 0.1.0-alpha.55 → 0.1.0-alpha.57

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/README.en.md ADDED
@@ -0,0 +1,243 @@
1
+ # @h-ai/deploy
2
+
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [Framework home](../../README.en.md) · Related modules: [@h-ai/core](../core/README.en.md)
6
+
7
+ An automated deployment module with two targets: deploy SvelteKit apps to Vercel with cloud infrastructure provisioning, or containerize and deploy to any Linux host over SSH (docker-ssh).
8
+
9
+ ## Deployment targets
10
+
11
+ | Provider | Target | Description |
12
+ | ------------ | --------------- | ------------------------------------------------------------- |
13
+ | `vercel` | Vercel platform | SvelteKit + adapter-vercel with cloud provisioners |
14
+ | `docker-ssh` | Any Linux host | SvelteKit + adapter-node, containerized and deployed over SSH |
15
+
16
+ ## Cloud services (provisioners)
17
+
18
+ | Service type | Provisioner | Description |
19
+ | ------------ | ------------- | ------------------------------- |
20
+ | Database | Neon | PostgreSQL Serverless |
21
+ | Cache | Upstash | Redis TCP + TLS |
22
+ | Storage | Cloudflare R2 | S3-compatible object storage |
23
+ | Email | Resend | SMTP (existing verified domain) |
24
+ | SMS | Alibaba Cloud | Alibaba Cloud SMS |
25
+
26
+ ## Quick start
27
+
28
+ ```typescript
29
+ import { deploy } from '@h-ai/deploy'
30
+
31
+ // 可选:从 ~/.hai/credentials.yml 注入 HAI_DEPLOY_* 环境变量
32
+ deploy.credentials.load()
33
+
34
+ // 初始化
35
+ await deploy.init({
36
+ provider: { type: 'vercel', token: 'vel_xxx' },
37
+ services: {
38
+ db: { provisioner: 'neon', apiKey: 'neon_xxx' },
39
+ cache: { provisioner: 'upstash', email: 'a@b.com', apiKey: 'up_xxx' },
40
+ },
41
+ })
42
+
43
+ // 扫描应用依赖
44
+ const scan = await deploy.scan('./apps/my-app')
45
+
46
+ // 开通基础设施
47
+ const provisions = await deploy.provisionAll('my-app')
48
+
49
+ // 完整部署
50
+ const result = await deploy.deployApp('./apps/my-app')
51
+
52
+ await deploy.close()
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ Keep Git-safe configuration in `config/_deploy.yml` and secrets in `~/.hai/credentials.yml`:
58
+
59
+ ```yaml
60
+ # config/_deploy.yml
61
+ provider:
62
+ type: vercel
63
+ token: ${HAI_DEPLOY_VERCEL_TOKEN}
64
+
65
+ services:
66
+ db:
67
+ provisioner: neon
68
+ apiKey: ${HAI_DEPLOY_NEON_API_KEY}
69
+ cache:
70
+ provisioner: upstash
71
+ email: ${HAI_DEPLOY_UPSTASH_EMAIL}
72
+ apiKey: ${HAI_DEPLOY_UPSTASH_API_KEY}
73
+ ```
74
+
75
+ These variables are explicit special mappings managed by `deploy.credentials`. If convention-based variables
76
+ such as `HAI_DEPLOY_PROVIDER_TOKEN` are also set, the convention-based variables take precedence.
77
+
78
+ Use `deploy.credentials.*` to manage credential files explicitly in scripts or the CLI:
79
+
80
+ ```typescript
81
+ deploy.credentials.load()
82
+ deploy.credentials.save('HAI_DEPLOY_VERCEL_TOKEN', 'vel_xxx')
83
+ deploy.credentials.saveAll({
84
+ HAI_DEPLOY_NEON_API_KEY: 'neon_xxx',
85
+ })
86
+ ```
87
+
88
+ > `deploy.scan()` and `deploy.credentials.*()` use the local filesystem and do not require `deploy.init()`.
89
+
90
+ ## docker-ssh deployment
91
+
92
+ Containerize the application and deploy over SSH to any Linux host; no cloud account is required.
93
+
94
+ ### Prerequisites
95
+
96
+ - **Local**: install docker or podman (`auto` prefers podman).
97
+ - **Remote host**: SSH access, docker/podman and compose installed, and a login user permitted to run containers.
98
+ - **Application**: SvelteKit must use `@sveltejs/adapter-node` (covered by the supplied `Dockerfile`); otherwise `ADAPTER_MISSING` is returned.
99
+
100
+ ### Steps
101
+
102
+ 1. Store the SSH private key path in the credential file, outside the repository. Edit `~/.hai/credentials.yml` or use the API:
103
+
104
+ ```typescript
105
+ deploy.credentials.save('HAI_DEPLOY_SSH_KEY', '~/.ssh/hai_deploy')
106
+ ```
107
+
108
+ 2. Declare the target host in `config/_deploy.yml`:
109
+
110
+ ```yaml
111
+ provider:
112
+ type: docker-ssh
113
+ ssh:
114
+ host: 10.0.0.20 # 主机 IP 或 ~/.ssh/config 别名
115
+ port: 22
116
+ username: deploy
117
+ identityFile: ${HAI_DEPLOY_SSH_KEY} # 私钥文件路径,省略则用 ssh-agent
118
+ remote:
119
+ baseDir: /opt/hai/apps # 远程部署根目录
120
+ runtime: auto # 远程运行时;auto 优先 podman,其次 docker
121
+ expose:
122
+ type: port
123
+ containerPort: 3000 # 容器内端口(adapter-node 默认 3000)
124
+ hostPort: 18080 # 宿主机端口 → 访问地址 http://10.0.0.20:18080
125
+
126
+ container:
127
+ runtime: auto # 本地构建运行时(可选)
128
+
129
+ services: {} # 启用的服务会自动生成 compose sidecar,见下
130
+ ```
131
+
132
+ 3. Deploy and verify with the CLI or API (equivalent behavior):
133
+
134
+ ```bash
135
+ hai deploy # 读取 config/_deploy.yml,输出访问地址
136
+ ```
137
+
138
+ ```typescript
139
+ import { deploy } from '@h-ai/deploy'
140
+
141
+ deploy.credentials.load() // 注入 HAI_DEPLOY_* 环境变量
142
+ await deploy.init(dockerSshConfig)
143
+ const result = await deploy.deployApp('./apps/my-app')
144
+ if (result.success) {
145
+ result.data.url // http://<host>:<hostPort>
146
+ result.data.healthCheck // 'passed'(HTTP 探测成功)
147
+ }
148
+ await deploy.close()
149
+ ```
150
+
151
+ ### How it works
152
+
153
+ For docker-ssh, `deployApp()` runs these steps:
154
+
155
+ 1. **Scan**: read `package.json` and `config/*.yml` to identify `@sveltejs/adapter-node`, required services, and backend types.
156
+ 2. **Local build**: use the project's `Dockerfile` to build `hai-<app>:<deploymentId>` (podman adds `--format docker --jobs=1`).
157
+ 3. **Export**: save the image as tar with `docker/podman save`.
158
+ 4. **Preflight**: verify SSH connectivity and remote `docker/podman version` / `compose version`.
159
+ 5. **Transfer/load**: upload tar with `scp`, load remotely, and retag under `localhost/` to avoid accidental registry pulls.
160
+ 6. **Runtime files**: generate `compose.yml` (app + sidecars) and `.env.runtime` (including `HAI_*` overrides and credentials), upload to `<baseDir>/<app>/`, and set `.env.runtime` permissions to `600`.
161
+ 7. **Start**: `compose up -d` with fixed project name `hai-<app>` for idempotent redeployment.
162
+ 8. **Health check**: poll `http://<host>:<hostPort>/`; only HTTP 200 yields `ready`.
163
+
164
+ > Images, `compose.yml`, and `.env.runtime` are stored in the application's `.hai/deploy/`, excluded by the template `.gitignore`.
165
+
166
+ ### Feature-based compose sidecars
167
+
168
+ `deploy.scan()` identifies services and their **actual backend types** (`type` in `_db.yml` / `_vecdb.yml`, etc.), generates matching sidecars, and injects `HAI_*` overrides:
169
+
170
+ | Detected service | Backend (`type`) | Added sidecar | Injected variables |
171
+ | ---------------- | ---------------- | ------------------------------- | ------------------ |
172
+ | `db` | `postgresql` | `postgres:16-alpine` | `HAI_DB` |
173
+ | `db` | `mysql` | `mysql:8.4` | `HAI_DB` |
174
+ | `db` | `sqlite` | None (mount `/app/data` volume) | — |
175
+ | `cache` | `redis` | `redis:7-alpine` | `HAI_CACHE` |
176
+ | `storage` | `s3` | `minio` + bucket initialization | `HAI_STORAGE` |
177
+ | `vecdb` | `qdrant` | `qdrant/qdrant` | `HAI_VECDB` |
178
+ | `vecdb` | `pgvector` | `pgvector/pgvector` | `HAI_VECDB` |
179
+ | `vecdb` | `chroma` | `chromadb/chroma` | `HAI_VECDB` |
180
+ | `vecdb` | `lancedb` | None (mount `/app/data` volume) | — |
181
+
182
+ - Memory `cache` and external email/SMS `reach` APIs do not generate sidecars.
183
+ - Existing cloud provisioners take precedence (for example Neon providing `HAI_DB`); no duplicate sidecar is created.
184
+ - Sidecar secrets such as passwords go only in remote `.env.runtime` (`600`), never `compose.yml`.
185
+
186
+ > Security: `identityFile` is a private key **path**, not key content. `strictHostKeyChecking: true` (default)
187
+ > uses accept-new: trust first use and reject changed keys. You may connect once manually with `ssh` for an unknown host.
188
+
189
+ ## Error handling
190
+
191
+ ```typescript
192
+ import { HaiDeployError } from '@h-ai/deploy'
193
+
194
+ const result = await deploy.deployApp('./apps/my-app')
195
+ if (!result.success) {
196
+ switch (result.error.code) {
197
+ case HaiDeployError.AUTH_FAILED.code:
198
+ // Vercel Token 无效
199
+ break
200
+ case HaiDeployError.BUILD_FAILED.code:
201
+ // 构建失败
202
+ break
203
+ }
204
+ }
205
+ ```
206
+
207
+ Common error codes:
208
+
209
+ | Error code | code | Description |
210
+ | --------------------------------- | ---------------- | ------------------------------------- |
211
+ | `HaiDeployError.DEPLOY_FAILED` | `hai:deploy:001` | Deployment failed |
212
+ | `HaiDeployError.BUILD_FAILED` | `hai:deploy:003` | Build failed |
213
+ | `HaiDeployError.AUTH_FAILED` | `hai:deploy:006` | Authentication failed |
214
+ | `HaiDeployError.PROVISION_FAILED` | `hai:deploy:007` | Provisioning failed |
215
+ | `HaiDeployError.NOT_INITIALIZED` | `hai:deploy:010` | Not initialized |
216
+ | `HaiDeployError.ENV_VAR_FAILED` | `hai:deploy:011` | Environment variable operation failed |
217
+
218
+ docker-ssh-specific error codes:
219
+
220
+ | Error code | code | Description |
221
+ | -------------------------------------------- | ---------------- | --------------------------- |
222
+ | `HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND` | `hai:deploy:015` | Container runtime not found |
223
+ | `HaiDeployError.CONTAINER_BUILD_FAILED` | `hai:deploy:016` | Image build failed |
224
+ | `HaiDeployError.REMOTE_CONNECT_FAILED` | `hai:deploy:017` | Remote connection failed |
225
+ | `HaiDeployError.REMOTE_COMMAND_FAILED` | `hai:deploy:018` | Remote command failed |
226
+ | `HaiDeployError.IMAGE_TRANSFER_FAILED` | `hai:deploy:019` | Image transfer failed |
227
+ | `HaiDeployError.HEALTH_CHECK_FAILED` | `hai:deploy:020` | Health check failed |
228
+
229
+ ## Tests
230
+
231
+ ```bash
232
+ pnpm --filter @h-ai/deploy test
233
+ ```
234
+
235
+ ## License
236
+
237
+ Apache-2.0
238
+
239
+ Provisioned configuration: Neon emits HAI_DB (PostgreSQL JSON), Upstash HAI_CACHE (Redis rediss URL JSON), and R2 HAI_STORAGE (flat S3 JSON). Email/SMS emit HAI_REACH_PROVIDERS; deployApp merges both channels while preserving YAML message templates, replacing the original providers array. Core supports whole-object/array overrides, with parents preceding leaves and schema validation retained. Build subprocesses and target runtimes share provisioned values. R2 needs existing S3 accessKeyId/secretAccessKey, Resend a verified-domain from address, and Alibaba Cloud approved signName/templates. Applications need matching `_db.yml` / `_cache.yml` / `_storage.yml` / `_reach.yml` loaded at startup; network clients require Node.
240
+
241
+ Configuration references: [Upstash TLS](https://upstash.com/docs/redis/features/security), [Resend SMTP](https://resend.com/docs/send-with-smtp). Deterministic acceptance generates real applications and validates Core and module schemas. It does not create cloud resources or send email/SMS, so it does not establish cloud read/write acceptance.
242
+
243
+ Deployment recovery: `ProvisionResult.resourceStatus` marks created/reused. `getDeployRecovery(result.error)` reads projectName, stage, platformProjectId, and resource lists; unknown means an uncertain request result that must first be checked in the provider console. Preserve returned IDs for failed resources. Recovery excludes envVars, raw exceptions, and credentials; the CLI shows resources and advice. Same-name retries reuse resources. Reused resources are never deleted automatically; clean up newly created resources only after manually confirming they are unused.
package/README.ja.md ADDED
@@ -0,0 +1,243 @@
1
+ # @h-ai/deploy
2
+
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
+
5
+ [フレームワークのホーム](../../README.ja.md) · 関連モジュール:[@h-ai/core](../core/README.ja.md)
6
+
7
+ 自動デプロイモジュールです。SvelteKit アプリをクラウド基盤の作成とともに Vercel へ展開する方式と、コンテナー化して任意の Linux ホストへ SSH で展開する方式(docker-ssh)に対応します。
8
+
9
+ ## 対応デプロイ方式
10
+
11
+ | Provider | 展開先 | 説明 |
12
+ | ------------ | ------------------- | ------------------------------------------------------- |
13
+ | `vercel` | Vercel | SvelteKit + adapter-vercel、クラウド Provisioner と連携 |
14
+ | `docker-ssh` | 任意の Linux ホスト | SvelteKit + adapter-node、コンテナー化して SSH で展開 |
15
+
16
+ ## 対応クラウドサービス(Provisioner)
17
+
18
+ | サービス種別 | Provisioner | 説明 |
19
+ | ------------ | ------------- | ------------------------------ |
20
+ | データベース | Neon | PostgreSQL Serverless |
21
+ | キャッシュ | Upstash | Redis TCP + TLS |
22
+ | ストレージ | Cloudflare R2 | S3 互換オブジェクトストレージ |
23
+ | メール | Resend | SMTP(検証済みドメインが必要) |
24
+ | SMS | Alibaba Cloud | Alibaba Cloud SMS |
25
+
26
+ ## クイックスタート
27
+
28
+ ```typescript
29
+ import { deploy } from '@h-ai/deploy'
30
+
31
+ // 可选:从 ~/.hai/credentials.yml 注入 HAI_DEPLOY_* 环境变量
32
+ deploy.credentials.load()
33
+
34
+ // 初始化
35
+ await deploy.init({
36
+ provider: { type: 'vercel', token: 'vel_xxx' },
37
+ services: {
38
+ db: { provisioner: 'neon', apiKey: 'neon_xxx' },
39
+ cache: { provisioner: 'upstash', email: 'a@b.com', apiKey: 'up_xxx' },
40
+ },
41
+ })
42
+
43
+ // 扫描应用依赖
44
+ const scan = await deploy.scan('./apps/my-app')
45
+
46
+ // 开通基础设施
47
+ const provisions = await deploy.provisionAll('my-app')
48
+
49
+ // 完整部署
50
+ const result = await deploy.deployApp('./apps/my-app')
51
+
52
+ await deploy.close()
53
+ ```
54
+
55
+ ## 設定
56
+
57
+ Git に保存できる設定は `config/_deploy.yml`、秘密情報は `~/.hai/credentials.yml` で管理します:
58
+
59
+ ```yaml
60
+ # config/_deploy.yml
61
+ provider:
62
+ type: vercel
63
+ token: ${HAI_DEPLOY_VERCEL_TOKEN}
64
+
65
+ services:
66
+ db:
67
+ provisioner: neon
68
+ apiKey: ${HAI_DEPLOY_NEON_API_KEY}
69
+ cache:
70
+ provisioner: upstash
71
+ email: ${HAI_DEPLOY_UPSTASH_EMAIL}
72
+ apiKey: ${HAI_DEPLOY_UPSTASH_API_KEY}
73
+ ```
74
+
75
+ ここでの変数は `deploy.credentials` が管理する明示的な特殊マッピングです。YAML パスから生成する
76
+ `HAI_DEPLOY_PROVIDER_TOKEN` なども指定した場合、規約に沿う変数が優先されます。
77
+
78
+ スクリプト・CLI で認証情報ファイルを明示的に管理するには `deploy.credentials.*` を使います:
79
+
80
+ ```typescript
81
+ deploy.credentials.load()
82
+ deploy.credentials.save('HAI_DEPLOY_VERCEL_TOKEN', 'vel_xxx')
83
+ deploy.credentials.saveAll({
84
+ HAI_DEPLOY_NEON_API_KEY: 'neon_xxx',
85
+ })
86
+ ```
87
+
88
+ > `deploy.scan()` と `deploy.credentials.*()` はローカルファイルシステムの機能で、`deploy.init()` に依存しません。
89
+
90
+ ## docker-ssh デプロイ
91
+
92
+ アプリをコンテナー化して SSH で任意の Linux ホストへ展開します。クラウドアカウントは不要です。
93
+
94
+ ### 前提条件
95
+
96
+ - **ローカル**:docker または podman をインストール(`auto` は podman を優先)。
97
+ - **リモート**:SSH 接続可能、docker/podman と compose が導入済み、ログインユーザーがコンテナーを実行可能。
98
+ - **アプリ**:SvelteKit に `@sveltejs/adapter-node` を使用(付属 `Dockerfile` は対応済み)。不足時は `ADAPTER_MISSING`。
99
+
100
+ ### 使用手順
101
+
102
+ 1. SSH 秘密鍵のパスをリポジトリー外の認証情報ファイルに保存します。`~/.hai/credentials.yml` を編集するか API を使います:
103
+
104
+ ```typescript
105
+ deploy.credentials.save('HAI_DEPLOY_SSH_KEY', '~/.ssh/hai_deploy')
106
+ ```
107
+
108
+ 2. `config/_deploy.yml` に対象ホストを定義します:
109
+
110
+ ```yaml
111
+ provider:
112
+ type: docker-ssh
113
+ ssh:
114
+ host: 10.0.0.20 # 主机 IP 或 ~/.ssh/config 别名
115
+ port: 22
116
+ username: deploy
117
+ identityFile: ${HAI_DEPLOY_SSH_KEY} # 私钥文件路径,省略则用 ssh-agent
118
+ remote:
119
+ baseDir: /opt/hai/apps # 远程部署根目录
120
+ runtime: auto # 远程运行时;auto 优先 podman,其次 docker
121
+ expose:
122
+ type: port
123
+ containerPort: 3000 # 容器内端口(adapter-node 默认 3000)
124
+ hostPort: 18080 # 宿主机端口 → 访问地址 http://10.0.0.20:18080
125
+
126
+ container:
127
+ runtime: auto # 本地构建运行时(可选)
128
+
129
+ services: {} # 启用的服务会自动生成 compose sidecar,见下
130
+ ```
131
+
132
+ 3. CLI または API で展開・検証します(動作は同じ):
133
+
134
+ ```bash
135
+ hai deploy # 读取 config/_deploy.yml,输出访问地址
136
+ ```
137
+
138
+ ```typescript
139
+ import { deploy } from '@h-ai/deploy'
140
+
141
+ deploy.credentials.load() // 注入 HAI_DEPLOY_* 环境变量
142
+ await deploy.init(dockerSshConfig)
143
+ const result = await deploy.deployApp('./apps/my-app')
144
+ if (result.success) {
145
+ result.data.url // http://<host>:<hostPort>
146
+ result.data.healthCheck // 'passed'(HTTP 探测成功)
147
+ }
148
+ await deploy.close()
149
+ ```
150
+
151
+ ### 処理の流れ
152
+
153
+ docker-ssh の `deployApp()` は次の順で処理します:
154
+
155
+ 1. **走査**:`package.json` と `config/*.yml` から `@sveltejs/adapter-node`、必要なサービス、バックエンド種別を判定。
156
+ 2. **ローカルビルド**:プロジェクトの `Dockerfile` で `hai-<app>:<deploymentId>` を作成(podman は `--format docker --jobs=1` を追加)。
157
+ 3. **出力**:`docker/podman save` で tar に保存。
158
+ 4. **事前確認**:SSH 接続とリモートの `docker/podman version` / `compose version` を確認。
159
+ 5. **転送・読み込み**:`scp` で tar を転送し、リモートで `load` 後、誤った registry 取得を防ぐため `localhost/` タグを付与。
160
+ 6. **実行定義**:走査結果から `compose.yml`(app + sidecar)と `.env.runtime`(`HAI_*` 上書き・認証情報)を生成し、`<baseDir>/<app>/` に転送。`.env.runtime` は `600` に設定。
161
+ 7. **起動**:固定プロジェクト名 `hai-<app>` で `compose up -d`。再展開は冪等。
162
+ 8. **ヘルスチェック**:`http://<host>:<hostPort>/` を監視し、200 応答でのみ `ready` と判定。
163
+
164
+ > イメージ、`compose.yml`、`.env.runtime` はアプリの `.hai/deploy/` に保存し、テンプレートの `.gitignore` で除外済みです。
165
+
166
+ ### 機能に応じた compose sidecar
167
+
168
+ `deploy.scan()` は必要なサービスと**実際のバックエンド種別**(`_db.yml` / `_vecdb.yml` などの `type`)を読み、対応する sidecar と `HAI_*` 上書き変数を生成します:
169
+
170
+ | 検出サービス | バックエンド(`type`) | 追加 sidecar | 注入する変数 |
171
+ | ------------ | ---------------------- | ---------------------------------------- | ------------- |
172
+ | `db` | `postgresql` | `postgres:16-alpine` | `HAI_DB` |
173
+ | `db` | `mysql` | `mysql:8.4` | `HAI_DB` |
174
+ | `db` | `sqlite` | なし(`/app/data` ボリュームをマウント) | — |
175
+ | `cache` | `redis` | `redis:7-alpine` | `HAI_CACHE` |
176
+ | `storage` | `s3` | `minio` + バケット初期化 | `HAI_STORAGE` |
177
+ | `vecdb` | `qdrant` | `qdrant/qdrant` | `HAI_VECDB` |
178
+ | `vecdb` | `pgvector` | `pgvector/pgvector` | `HAI_VECDB` |
179
+ | `vecdb` | `chroma` | `chromadb/chroma` | `HAI_VECDB` |
180
+ | `vecdb` | `lancedb` | なし(`/app/data` ボリュームをマウント) | — |
181
+
182
+ - `memory` の `cache` と外部 API の `reach`(メール・SMS)は sidecar を生成しません。
183
+ - 同じサービスにクラウド Provisioner がある場合(Neon の `HAI_DB` など)、クラウドを優先し sidecar を重複作成しません。
184
+ - sidecar のパスワードなどはリモートの `.env.runtime`(`600`)のみに保存し、`compose.yml` には含めません。
185
+
186
+ > セキュリティ:`identityFile` は秘密鍵の**パス**で、内容ではありません。`strictHostKeyChecking: true`(既定)は
187
+ > accept-new を使い、初回は信頼し、鍵変更は拒否します。未知のホストには先に手動で `ssh` 接続できます。
188
+
189
+ ## エラー処理
190
+
191
+ ```typescript
192
+ import { HaiDeployError } from '@h-ai/deploy'
193
+
194
+ const result = await deploy.deployApp('./apps/my-app')
195
+ if (!result.success) {
196
+ switch (result.error.code) {
197
+ case HaiDeployError.AUTH_FAILED.code:
198
+ // Vercel Token 无效
199
+ break
200
+ case HaiDeployError.BUILD_FAILED.code:
201
+ // 构建失败
202
+ break
203
+ }
204
+ }
205
+ ```
206
+
207
+ 主なエラーコード:
208
+
209
+ | エラーコード | code | 説明 |
210
+ | --------------------------------- | ---------------- | -------------------- |
211
+ | `HaiDeployError.DEPLOY_FAILED` | `hai:deploy:001` | デプロイに失敗 |
212
+ | `HaiDeployError.BUILD_FAILED` | `hai:deploy:003` | ビルドに失敗 |
213
+ | `HaiDeployError.AUTH_FAILED` | `hai:deploy:006` | 認証に失敗 |
214
+ | `HaiDeployError.PROVISION_FAILED` | `hai:deploy:007` | リソースの作成に失敗 |
215
+ | `HaiDeployError.NOT_INITIALIZED` | `hai:deploy:010` | 未初期化 |
216
+ | `HaiDeployError.ENV_VAR_FAILED` | `hai:deploy:011` | 環境変数の処理に失敗 |
217
+
218
+ docker-ssh 専用エラーコード:
219
+
220
+ | エラーコード | code | 説明 |
221
+ | -------------------------------------------- | ---------------- | ---------------------------------- |
222
+ | `HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND` | `hai:deploy:015` | コンテナーランタイムが見つからない |
223
+ | `HaiDeployError.CONTAINER_BUILD_FAILED` | `hai:deploy:016` | イメージのビルドに失敗 |
224
+ | `HaiDeployError.REMOTE_CONNECT_FAILED` | `hai:deploy:017` | リモート接続に失敗 |
225
+ | `HaiDeployError.REMOTE_COMMAND_FAILED` | `hai:deploy:018` | リモートコマンドに失敗 |
226
+ | `HaiDeployError.IMAGE_TRANSFER_FAILED` | `hai:deploy:019` | イメージ転送に失敗 |
227
+ | `HaiDeployError.HEALTH_CHECK_FAILED` | `hai:deploy:020` | ヘルスチェックに失敗 |
228
+
229
+ ## テスト
230
+
231
+ ```bash
232
+ pnpm --filter @h-ai/deploy test
233
+ ```
234
+
235
+ ## License
236
+
237
+ Apache-2.0
238
+
239
+ クラウド設定の利用:Neon は HAI_DB(PostgreSQL JSON)、Upstash は HAI_CACHE(Redis rediss URL JSON)、R2 は HAI_STORAGE(フラットな S3 JSON)を出力します。メール・SMS は HAI_REACH_PROVIDERS を出力し、deployApp が両チャネルと YAML のメッセージテンプレートを統合して元の providers 配列を置換します。Core はオブジェクト・配列全体の上書きをサポートし、親ノードを末端より優先して Schema 検証を維持します。ビルド子プロセスと対象環境は同じ値を使います。R2 は既存の S3 accessKeyId/secretAccessKey、Resend は検証済みドメインの from、Alibaba Cloud は承認済み signName・テンプレートが必要です。アプリは対応する `_db.yml` / `_cache.yml` / `_storage.yml` / `_reach.yml` を起動時に読み込み、ネットワーククライアントには Node が必要です。
240
+
241
+ 設定の参照:[Upstash TLS](https://upstash.com/docs/redis/features/security)、[Resend SMTP](https://resend.com/docs/send-with-smtp)。決定的な検証では実際のアプリを生成して Core・各モジュールの Schema を確認します。クラウドリソースの作成やメール・SMS 送信は行わず、クラウドの読み書き検証済みとは言えません。
242
+
243
+ デプロイ失敗からの復旧:`ProvisionResult.resourceStatus` は created/reused を示します。`getDeployRecovery(result.error)` で projectName、stage、platformProjectId、リソース一覧を取得し、unknown は結果不明なので先にサービス側の管理画面で確認します。失敗リソースの ID が返った場合は保持します。復旧情報に envVars・元の例外・接続認証情報は含まれず、CLI が一覧と提案を表示します。同名の再試行は既存リソースを再利用し、自動削除しません。新規作成分も未使用を人が確認した後にのみ削除します。
package/README.md CHANGED
@@ -1,12 +1,22 @@
1
1
  # @h-ai/deploy
2
2
 
3
- 自动化部署模块,支持一键将 SvelteKit 应用部署到 Vercel,并自动开通所需的基础设施服务。
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
4
 
5
- ## 支持的服务
5
+ [框架首页](../../README.md) · 相关模块:[@h-ai/core](../core/README.md)
6
+
7
+ 自动化部署模块,支持两种部署方式:一键将 SvelteKit 应用部署到 Vercel(并自动开通云基础设施),或将应用容器化后通过 SSH 部署到任意 Linux 主机(docker-ssh)。
8
+
9
+ ## 支持的部署方式
10
+
11
+ | Provider | 目标 | 说明 |
12
+ | ------------ | --------------- | ---------------------------------------------- |
13
+ | `vercel` | Vercel 平台 | SvelteKit + adapter-vercel,配合云 Provisioner |
14
+ | `docker-ssh` | 任意 Linux 主机 | SvelteKit + adapter-node,容器化后经 SSH 部署 |
15
+
16
+ ## 支持的云服务(Provisioner)
6
17
 
7
18
  | 服务类型 | Provisioner | 说明 |
8
19
  | -------- | ------------- | ---------------------- |
9
- | 部署 | Vercel | SvelteKit 应用部署 |
10
20
  | 数据库 | Neon | PostgreSQL Serverless |
11
21
  | 缓存 | Upstash | Redis TCP + TLS |
12
22
  | 存储 | Cloudflare R2 | S3 兼容对象存储 |
@@ -77,6 +87,105 @@ deploy.credentials.saveAll({
77
87
 
78
88
  > `deploy.scan()` 与 `deploy.credentials.*()` 都是本地文件系统能力,不依赖 `deploy.init()`。
79
89
 
90
+ ## docker-ssh 部署
91
+
92
+ 将应用容器化后通过 SSH 部署到任意 Linux 主机,无需云平台账号。
93
+
94
+ ### 前置条件
95
+
96
+ - **本地**:安装 docker 或 podman(`auto` 优先 podman)。
97
+ - **远程主机**:可 SSH 登录、已装 docker/podman 与 compose、登录用户可运行容器。
98
+ - **应用**:SvelteKit 使用 `@sveltejs/adapter-node`(工程自带的 `Dockerfile` 已满足);缺少时返回 `ADAPTER_MISSING`。
99
+
100
+ ### 使用步骤
101
+
102
+ 1)把 SSH 私钥路径存入凭证文件(不进仓库)。编辑 `~/.hai/credentials.yml`,或用 API:
103
+
104
+ ```typescript
105
+ deploy.credentials.save('HAI_DEPLOY_SSH_KEY', '~/.ssh/hai_deploy')
106
+ ```
107
+
108
+ 2)在 `config/_deploy.yml` 声明目标主机:
109
+
110
+ ```yaml
111
+ provider:
112
+ type: docker-ssh
113
+ ssh:
114
+ host: 10.0.0.20 # 主机 IP 或 ~/.ssh/config 别名
115
+ port: 22
116
+ username: deploy
117
+ identityFile: ${HAI_DEPLOY_SSH_KEY} # 私钥文件路径,省略则用 ssh-agent
118
+ remote:
119
+ baseDir: /opt/hai/apps # 远程部署根目录
120
+ runtime: auto # 远程运行时;auto 优先 podman,其次 docker
121
+ expose:
122
+ type: port
123
+ containerPort: 3000 # 容器内端口(adapter-node 默认 3000)
124
+ hostPort: 18080 # 宿主机端口 → 访问地址 http://10.0.0.20:18080
125
+
126
+ container:
127
+ runtime: auto # 本地构建运行时(可选)
128
+
129
+ services: {} # 启用的服务会自动生成 compose sidecar,见下
130
+ ```
131
+
132
+ 3)部署并验证(CLI 或 API,二者等价):
133
+
134
+ ```bash
135
+ hai deploy # 读取 config/_deploy.yml,输出访问地址
136
+ ```
137
+
138
+ ```typescript
139
+ import { deploy } from '@h-ai/deploy'
140
+
141
+ deploy.credentials.load() // 注入 HAI_DEPLOY_* 环境变量
142
+ await deploy.init(dockerSshConfig)
143
+ const result = await deploy.deployApp('./apps/my-app')
144
+ if (result.success) {
145
+ result.data.url // http://<host>:<hostPort>
146
+ result.data.healthCheck // 'passed'(HTTP 探测成功)
147
+ }
148
+ await deploy.close()
149
+ ```
150
+
151
+ ### 实现逻辑
152
+
153
+ `deployApp()` 对 docker-ssh 依次执行:
154
+
155
+ 1. **扫描**:读取 `package.json` 与 `config/*.yml`,识别 `@sveltejs/adapter-node` 与所需服务及其后端类型。
156
+ 2. **本地构建**:用工程自带的 `Dockerfile` 构建镜像 `hai-<app>:<部署ID>`(podman 追加 `--format docker --jobs=1`)。
157
+ 3. **导出**:`docker/podman save` 导出为 tar。
158
+ 4. **预检**:SSH 连通性 + 远程 `docker/podman version` 与 `compose version`。
159
+ 5. **传输加载**:`scp` 上传 tar → 远程 `load` → 重打为 `localhost/` 本地标签(避免误从 registry 拉取)。
160
+ 6. **生成运行描述**:按扫描结果生成 `compose.yml`(app + sidecar)与 `.env.runtime`(含 `HAI_*` 覆盖变量与凭据),上传到 `<baseDir>/<app>/`,`.env.runtime` 设为 `600`。
161
+ 7. **启动**:`compose up -d`(Compose 项目名固定 `hai-<app>`,重复部署幂等)。
162
+ 8. **健康检查**:轮询 `http://<host>:<hostPort>/`,返回 200 才判定 `ready`。
163
+
164
+ > 镜像、`compose.yml`、`.env.runtime` 落在应用 `.hai/deploy/`(已在模板 `.gitignore` 忽略)。
165
+
166
+ ### compose 按功能生成 sidecar
167
+
168
+ `deploy.scan()` 会识别所需服务及其**实际后端类型**(读取 `_db.yml` / `_vecdb.yml` 等的 `type`),生成匹配的 sidecar 并注入对应 `HAI_*` 覆盖变量:
169
+
170
+ | 扫描到的服务 | 后端 (`type`) | 追加的 sidecar | 注入变量 |
171
+ | ------------ | ------------- | ----------------------------- | ------------- |
172
+ | `db` | `postgresql` | `postgres:16-alpine` | `HAI_DB` |
173
+ | `db` | `mysql` | `mysql:8.4` | `HAI_DB` |
174
+ | `db` | `sqlite` | 无(挂载 `/app/data` 数据卷) | — |
175
+ | `cache` | `redis` | `redis:7-alpine` | `HAI_CACHE` |
176
+ | `storage` | `s3` | `minio` + 初始化 bucket | `HAI_STORAGE` |
177
+ | `vecdb` | `qdrant` | `qdrant/qdrant` | `HAI_VECDB` |
178
+ | `vecdb` | `pgvector` | `pgvector/pgvector` | `HAI_VECDB` |
179
+ | `vecdb` | `chroma` | `chromadb/chroma` | `HAI_VECDB` |
180
+ | `vecdb` | `lancedb` | 无(挂载 `/app/data` 数据卷) | — |
181
+
182
+ - `cache` 为 `memory`、`reach`(邮件/短信)为外部 API,均不生成 sidecar。
183
+ - 若同一服务已配置云 Provisioner(如 Neon 提供 `HAI_DB`),优先用云资源,不再自建 sidecar。
184
+ - sidecar 的密码等敏感值仅写入远程 `.env.runtime`(`600`),不会进入 `compose.yml`。
185
+
186
+ > 安全:`identityFile` 为私钥**路径**(非私钥内容);`strictHostKeyChecking: true`(默认)
187
+ > 采用 accept-new(首次信任、拒绝变更)。首次连接未知主机可先手动 `ssh` 一次。
188
+
80
189
  ## 错误处理
81
190
 
82
191
  ```typescript
@@ -106,6 +215,17 @@ if (!result.success) {
106
215
  | `HaiDeployError.NOT_INITIALIZED` | `hai:deploy:010` | 未初始化 |
107
216
  | `HaiDeployError.ENV_VAR_FAILED` | `hai:deploy:011` | 环境变量失败 |
108
217
 
218
+ docker-ssh 专属错误码:
219
+
220
+ | 错误码 | code | 说明 |
221
+ | -------------------------------------------- | ---------------- | ---------------- |
222
+ | `HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND` | `hai:deploy:015` | 未找到容器运行时 |
223
+ | `HaiDeployError.CONTAINER_BUILD_FAILED` | `hai:deploy:016` | 镜像构建失败 |
224
+ | `HaiDeployError.REMOTE_CONNECT_FAILED` | `hai:deploy:017` | 远程连接失败 |
225
+ | `HaiDeployError.REMOTE_COMMAND_FAILED` | `hai:deploy:018` | 远程命令失败 |
226
+ | `HaiDeployError.IMAGE_TRANSFER_FAILED` | `hai:deploy:019` | 镜像传输失败 |
227
+ | `HaiDeployError.HEALTH_CHECK_FAILED` | `hai:deploy:020` | 健康检查未通过 |
228
+
109
229
  ## 测试
110
230
 
111
231
  ```bash