@h-ai/cli 0.1.0-alpha.66 → 0.1.0-alpha.67

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.
Files changed (125) hide show
  1. package/README.en.md +66 -257
  2. package/README.ja.md +66 -257
  3. package/README.md +66 -257
  4. package/dist/{chunk-QYX6V7H2.js → chunk-7DSURTJ4.js} +22 -79
  5. package/dist/chunk-7DSURTJ4.js.map +1 -0
  6. package/dist/{chunk-FWN5B54L.js → chunk-AOQ4N657.js} +182 -10
  7. package/dist/chunk-AOQ4N657.js.map +1 -0
  8. package/dist/cli-config-templates-6AHNUGBN.js +5 -0
  9. package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-6AHNUGBN.js.map} +1 -1
  10. package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-FC3QRKDO.js} +3 -3
  11. package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-FC3QRKDO.js.map} +1 -1
  12. package/dist/index.js +435 -247
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src-RDQ2EIBT.js → src-2V6VP4I5.js} +553 -119
  15. package/dist/src-2V6VP4I5.js.map +1 -0
  16. package/package.json +8 -8
  17. package/templates/README.md +48 -145
  18. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +1 -1
  19. package/templates/apps/api/.dockerignore.hbs +14 -7
  20. package/templates/apps/api/.gitignore.hbs +7 -0
  21. package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
  22. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +28 -7
  23. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
  24. package/templates/apps/api/hai.build.json.hbs +1 -1
  25. package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
  26. package/templates/apps/api/package.json.hbs +5 -0
  27. package/templates/apps/fullstack/.dockerignore.hbs +14 -9
  28. package/templates/apps/fullstack/.gitignore.hbs +7 -0
  29. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +23 -5
  30. package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
  31. package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
  32. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +1 -1
  33. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +22 -4
  34. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
  35. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +23 -5
  36. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +1 -1
  37. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +11 -1
  38. package/templates/apps/fullstack/hai.build.json.hbs +2 -2
  39. package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
  40. package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
  41. package/templates/apps/fullstack/package.json.hbs +2 -0
  42. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
  43. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +27 -6
  44. package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
  45. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +32 -11
  46. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +2 -2
  47. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +3 -3
  48. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +2 -2
  49. package/templates/apps/h5/src/routes/+layout.svelte.hbs +2 -2
  50. package/templates/apps/mobile-app/src/App.svelte.hbs +2 -2
  51. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
  52. package/templates/apps/website/src/routes/+layout.svelte.hbs +3 -3
  53. package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
  54. package/templates/base/.dockerignore.hbs +14 -8
  55. package/templates/base/.gitignore.hbs +7 -0
  56. package/templates/base/e2e/app.spec.ts.hbs +10 -0
  57. package/templates/base/hai.build.json.hbs +1 -1
  58. package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
  59. package/templates/base/package.json.hbs +8 -3
  60. package/templates/base/vite.config.ts.hbs +3 -1
  61. package/templates/shared/.gitattributes +6 -0
  62. package/templates/skills/AGENTS.md +3 -3
  63. package/templates/skills/bridges/admin/AGENTS.md +1 -1
  64. package/templates/skills/bridges/api/AGENTS.md +1 -1
  65. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +1 -1
  66. package/templates/skills/bridges/h5/AGENTS.md +1 -1
  67. package/templates/skills/bridges/mobile-app/AGENTS.md +1 -1
  68. package/templates/skills/bridges/website/AGENTS.md +1 -1
  69. package/templates/skills/hai-api-client/SKILL.md +14 -161
  70. package/templates/skills/hai-api-client/reference.md +166 -0
  71. package/templates/skills/hai-api-contract/SKILL.md +15 -273
  72. package/templates/skills/hai-api-contract/reference.md +278 -0
  73. package/templates/skills/hai-app-create/SKILL.md +2 -2
  74. package/templates/skills/hai-app-create/development.md +4 -4
  75. package/templates/skills/hai-app-create/reference.md +5 -4
  76. package/templates/skills/hai-app-review/SKILL.md +1 -1
  77. package/templates/skills/hai-app-tests/SKILL.md +4 -3
  78. package/templates/skills/hai-app-tests/reference.md +2 -2
  79. package/templates/skills/hai-app-tests/validation.md +31 -0
  80. package/templates/skills/hai-audit/SKILL.md +17 -142
  81. package/templates/skills/hai-audit/reference.md +145 -0
  82. package/templates/skills/hai-build/SKILL.md +14 -8
  83. package/templates/skills/hai-build/reference.md +10 -9
  84. package/templates/skills/hai-cache/SKILL.md +15 -182
  85. package/templates/skills/hai-cache/reference.md +187 -0
  86. package/templates/skills/hai-capacitor/SKILL.md +4 -3
  87. package/templates/skills/hai-capacitor/reference.md +6 -4
  88. package/templates/skills/hai-ci/SKILL.md +2 -0
  89. package/templates/skills/hai-core/SKILL.md +3 -3
  90. package/templates/skills/hai-crypto/SKILL.md +16 -252
  91. package/templates/skills/hai-crypto/reference.md +256 -0
  92. package/templates/skills/hai-datapipe/SKILL.md +15 -196
  93. package/templates/skills/hai-datapipe/reference.md +202 -0
  94. package/templates/skills/hai-deploy/SKILL.md +20 -55
  95. package/templates/skills/hai-deploy/middleware.md +5 -11
  96. package/templates/skills/hai-framework-sync/SKILL.md +2 -2
  97. package/templates/skills/hai-framework-sync/sync.md +1 -1
  98. package/templates/skills/hai-iam/SKILL.md +1 -3
  99. package/templates/skills/hai-kit/SKILL.md +12 -167
  100. package/templates/skills/hai-kit/reference.md +172 -0
  101. package/templates/skills/hai-payment/SKILL.md +19 -251
  102. package/templates/skills/hai-payment/reference.md +256 -0
  103. package/templates/skills/hai-pr-review/SKILL.md +4 -4
  104. package/templates/skills/hai-reach/SKILL.md +2 -2
  105. package/templates/skills/hai-reldb/SKILL.md +1 -1
  106. package/templates/skills/hai-scheduler/SKILL.md +15 -195
  107. package/templates/skills/hai-scheduler/reference.md +202 -0
  108. package/templates/skills/hai-serv/SKILL.md +2 -2
  109. package/templates/skills/hai-spec-implement/SKILL.md +1 -1
  110. package/templates/skills/hai-spec-implement/workflow.md +3 -3
  111. package/templates/skills/hai-storage/SKILL.md +17 -238
  112. package/templates/skills/hai-storage/reference.md +243 -0
  113. package/templates/skills/hai-ui/SKILL.md +1 -1
  114. package/templates/skills/hai-ui/reference.md +9 -5
  115. package/templates/skills/hai-vecdb/SKILL.md +2 -2
  116. package/dist/chunk-FWN5B54L.js.map +0 -1
  117. package/dist/chunk-QYX6V7H2.js.map +0 -1
  118. package/dist/cli-config-templates-3XSFIXBP.js +0 -5
  119. package/dist/src-RDQ2EIBT.js.map +0 -1
  120. package/templates/apps/api/Dockerfile.hbs +0 -35
  121. package/templates/apps/fullstack/Dockerfile.hbs +0 -57
  122. /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
  123. /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
  124. /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
  125. /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { __export } from './chunk-2H7UOFLK.js';
3
3
  import { createHash, randomBytes } from 'crypto';
4
- import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, rmSync, statSync, readdirSync } from 'fs';
4
+ import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, renameSync, rmSync, statSync, readdirSync } from 'fs';
5
5
  import { resolve, join, basename, dirname, relative, sep } from 'path';
6
- import { core, ok, err } from '@h-ai/core';
6
+ import { core, err, ok } from '@h-ai/core';
7
7
  import process2 from 'process';
8
8
  import { parseEnv } from 'util';
9
- import { parse as parse$1, stringify, parseAllDocuments } from 'yaml';
9
+ import { LineCounter, parseDocument, parse as parse$1, stringify, parseAllDocuments } from 'yaml';
10
10
  import { homedir } from 'os';
11
11
  import { spawn } from 'child_process';
12
12
  import { Buffer } from 'buffer';
@@ -1856,18 +1856,18 @@ var validateAsync = async (schema, value, _ctx) => {
1856
1856
  return result.issues.length === 0;
1857
1857
  };
1858
1858
  var _encode = (_Err) => {
1859
- const parse7 = _parse(_Err);
1859
+ const parse6 = _parse(_Err);
1860
1860
  const fn = (schema, value, _ctx, _params) => {
1861
1861
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
1862
- return parse7(schema, value, ctx, finalizeParams(fn, _params));
1862
+ return parse6(schema, value, ctx, finalizeParams(fn, _params));
1863
1863
  };
1864
1864
  return fn;
1865
1865
  };
1866
1866
  var encode = /* @__PURE__ */ _encode($ZodRealError);
1867
1867
  var _decode = (_Err) => {
1868
- const parse7 = _parse(_Err);
1868
+ const parse6 = _parse(_Err);
1869
1869
  const fn = (schema, value, _ctx, _params) => {
1870
- return parse7(schema, value, _ctx, finalizeParams(fn, _params));
1870
+ return parse6(schema, value, _ctx, finalizeParams(fn, _params));
1871
1871
  };
1872
1872
  return fn;
1873
1873
  };
@@ -19671,25 +19671,33 @@ function date4(params) {
19671
19671
  // ../deploy/src/deploy-artifact.ts
19672
19672
  var DeploymentArtifactSchema = external_exports.object({
19673
19673
  /** 清单格式版本,与应用版本和部署环境无关。 */
19674
- version: external_exports.literal(2),
19674
+ version: external_exports.literal(3),
19675
19675
  /** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
19676
19676
  appVersion: external_exports.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),
19677
- /** 一次完整构建的标识,app/web 必须来自同一份清单。 */
19677
+ /** 本次构建标识;同一应用版本允许重复构建。 */
19678
19678
  buildId: external_exports.string().regex(/^[a-z0-9][\w.+-]*$/i),
19679
- /** API/独立应用和可选 Web 的镜像归档,至少包含一个 app。 */
19679
+ /** 只包含所选镜像的构建不能用于首次安装。 */
19680
+ partial: external_exports.boolean().default(false),
19681
+ /** API 单独构建时仍保留工程的 Web 拓扑。 */
19682
+ hasWeb: external_exports.boolean().default(false),
19683
+ /** 工程显式配置的应用端口,部分构建未包含 app 镜像时仍保留;缺省由部署配置决定。 */
19684
+ appContainerPort: external_exports.number().int().min(1).max(65535).optional(),
19685
+ /** 工程显式配置的 Web 端口,部分构建未包含 web 镜像时仍保留;缺省使用部署默认值。 */
19686
+ webContainerPort: external_exports.number().int().min(1).max(65535).optional(),
19687
+ /** 本次构建选择的镜像归档。 */
19680
19688
  images: external_exports.array(external_exports.object({
19681
19689
  /** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
19682
19690
  name: external_exports.enum(["app", "web"]),
19683
19691
  /** 相对于清单目录的 tar 文件名。 */
19684
19692
  file: external_exports.string().regex(/^[\w.+-]+\.tar$/),
19685
19693
  /** 归档导入后可直接使用的不可变镜像引用。 */
19686
- imageRef: external_exports.string().regex(/^[a-z0-9][a-z0-9./:_-]+$/),
19694
+ imageRef: external_exports.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
19687
19695
  /** tar 内容的 SHA-256,用于部署前校验。 */
19688
19696
  sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
19689
19697
  /** 容器内部监听端口;服务器公开端口由部署选择。 */
19690
19698
  containerPort: external_exports.number().int().min(1).max(65535).optional()
19691
- })).min(1).max(2).refine((images) => images.filter((image) => image.name === "app").length === 1 && new Set(images.map((image) => image.name)).size === images.length)
19692
- }).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`));
19699
+ })).min(1).max(2).refine((images) => new Set(images.map((image) => image.name)).size === images.length)
19700
+ }).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`)).refine((artifact) => artifact.partial || artifact.images.some((image) => image.name === "app")).refine((artifact) => artifact.partial || !artifact.hasWeb || artifact.images.some((image) => image.name === "web"));
19693
19701
  async function artifactChecksum(file2) {
19694
19702
  const hash2 = createHash("sha256");
19695
19703
  for await (const chunk of createReadStream(file2))
@@ -19757,11 +19765,21 @@ var en_US_default = {
19757
19765
  deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
19758
19766
  deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
19759
19767
  deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
19760
- 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.",
19768
+ deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
19761
19769
  deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
19762
- 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.",
19770
+ deploy_bundleInstructions: "Bundle: {file}\nIncludes all images needed by selected services, including self-hosted middleware.\nWith SSH access, run deploy.ps1 (Windows) or deploy.sh (Linux/macOS) from the adjacent deployment directory.\nFor isolated servers, copy and extract the tar into a separate directory, then run sh apply.sh /opt/hai/apps/project-environment.\nLinux requires Docker/Podman, Compose, sh, tar, sha256sum, awk, curl and filesystem/container permissions.\nPartial updates require a successful full installation with matching ports/layout. Steps return nonzero on failure; result-DEPLOYMENT_ID.json records the outcome.\nThe bundle contains private runtime configuration; transfer it securely.\nIncluded images:\n{images}",
19763
19771
  deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
19764
- deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh."
19772
+ deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh.",
19773
+ deploy_selectedContainerOnly: "Selected-service deployment requires a docker-ssh Compose destination.",
19774
+ deploy_selectionInvalid: "Cannot deploy {service}: check the service name and build its image first.",
19775
+ deploy_imageVersionRequired: "Specify an image version; latest is not allowed: {image}",
19776
+ deploy_bundleRuntimeRequired: "Start local Podman/Docker to export middleware images, or prepare this complete offline bundle on the build machine.",
19777
+ deploy_bundleFailed: "Package preparation or deployment failed: {reason}",
19778
+ deploy_configFileInvalid: "Invalid YAML in {path}, line {line}. Check indentation, colons and quotes.",
19779
+ deploy_configFileUnreadable: "Cannot read configuration file {path}. Check its path and permissions.",
19780
+ deploy_bundleNameInvalid: "Bundle filenames may contain letters, numbers, dots, plus signs, hyphens and underscores; parent directories may contain spaces.",
19781
+ deploy_secretsInvalid: "Middleware credentials are damaged or incomplete: {path}. Restore a backup; passwords are not regenerated because existing data may become inaccessible.",
19782
+ deploy_storageArtifactRequired: "Storage initialization requires the application image. Select a complete artifact containing app with --artifact-dir, or run pnpm build --target docker --images app first."
19765
19783
  };
19766
19784
 
19767
19785
  // ../deploy/messages/zh-CN.json
@@ -19815,11 +19833,21 @@ var zh_CN_default2 = {
19815
19833
  deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
19816
19834
  deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6",
19817
19835
  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",
19818
- 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",
19836
+ deploy_artifactInvalid: "\u955C\u50CF\u4EA7\u7269\u7F3A\u5931\u6216\u6821\u9A8C\u5931\u8D25\u3002\u8BF7\u5148\u6267\u884C pnpm build --target docker\uFF0C\u6216\u7528 --artifact-dir \u6307\u5B9A\u6709\u6548\u4EA7\u7269\uFF1B\u4E5F\u53EF\u7528 pnpm run deploy --build \u4E00\u6B21\u5B8C\u6210\u3002",
19819
19837
  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",
19820
- 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",
19838
+ deploy_bundleInstructions: "\u90E8\u7F72\u5305\uFF1A{file}\n\u5305\u4E2D\u5DF2\u5305\u542B\u672C\u6B21\u9009\u62E9\u670D\u52A1\u9700\u8981\u7684\u955C\u50CF\uFF0C\u5305\u62EC\u81EA\u5EFA\u4E2D\u95F4\u4EF6\u3002\n\u6709 SSH \u8FDE\u63A5\u65F6\uFF0C\u5728\u65C1\u8FB9\u7684\u90E8\u7F72\u76EE\u5F55\u8FD0\u884C deploy.ps1\uFF08Windows\uFF09\u6216 deploy.sh\uFF08Linux/macOS\uFF09\u3002\n\u5B8C\u5168\u9694\u79BB\u65F6\uFF0C\u590D\u5236 tar \u5230 Linux \u670D\u52A1\u5668\uFF0C\u5728\u72EC\u7ACB\u76EE\u5F55\u89E3\u5305\uFF0C\u518D\u6267\u884C sh apply.sh /opt/hai/apps/\u9879\u76EE\u73AF\u5883\u3002\n\u670D\u52A1\u5668\u9700\u8981 Docker/Podman\u3001Compose\u3001sh\u3001tar\u3001sha256sum\u3001awk \u548C curl\uFF0C\u4EE5\u53CA\u76EE\u5F55\u3001\u5BB9\u5668\u8FD0\u884C\u6743\u9650\u3002\n\u90E8\u5206\u66F4\u65B0\u8981\u6C42\u670D\u52A1\u5668\u5DF2\u6709\u6210\u529F\u7684\u5B8C\u6574\u90E8\u7F72\uFF0C\u7AEF\u53E3\u53CA\u5E94\u7528\u5E03\u5C40\u4FDD\u6301\u4E00\u81F4\u3002\u6BCF\u6B65\u5931\u8D25\u4F1A\u8FD4\u56DE\u975E\u96F6\u9000\u51FA\u7801\uFF1B\u7ED3\u679C\u8BB0\u5F55\u5728\u90E8\u7F72\u76EE\u5F55 result-\u90E8\u7F72ID.json\u3002\n\u5305\u5305\u542B\u79C1\u6709\u8FD0\u884C\u914D\u7F6E\uFF0C\u8BF7\u5B89\u5168\u4F20\u8F93\u3002\n\u6240\u542B\u955C\u50CF\uFF1A\n{images}",
19821
19839
  deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
19822
- deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002"
19840
+ deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002",
19841
+ deploy_selectedContainerOnly: "\u6309\u670D\u52A1\u5C40\u90E8\u90E8\u7F72\u4EC5\u9002\u7528\u4E8E docker-ssh Compose \u76EE\u6807\u3002",
19842
+ deploy_selectionInvalid: "\u65E0\u6CD5\u90E8\u7F72\u670D\u52A1 {service}\uFF1A\u8BF7\u68C0\u67E5\u670D\u52A1\u540D\u79F0\uFF0C\u5E76\u5148\u6784\u5EFA\u5BF9\u5E94\u955C\u50CF\u3002",
19843
+ deploy_storageArtifactRequired: "\u5B58\u50A8\u521D\u59CB\u5316\u9700\u8981\u5E94\u7528\u955C\u50CF\u3002\u8BF7\u7528 --artifact-dir \u9009\u62E9\u542B app \u955C\u50CF\u7684\u5B8C\u6574\u5236\u54C1\uFF0C\u6216\u5148\u6267\u884C pnpm build --target docker --images app\u3002",
19844
+ deploy_imageVersionRequired: "\u955C\u50CF\u5FC5\u987B\u6307\u5B9A\u7248\u672C\uFF0C\u4E0D\u80FD\u4F7F\u7528 latest\uFF1A{image}",
19845
+ deploy_bundleRuntimeRequired: "\u5B8C\u6574\u79BB\u7EBF\u5305\u9700\u8981\u5BFC\u51FA\u4E2D\u95F4\u4EF6\u955C\u50CF\u3002\u8BF7\u542F\u52A8\u672C\u5730 Podman/Docker\uFF0C\u6216\u5728\u6784\u5EFA\u673A\u6253\u5305\u3002",
19846
+ deploy_bundleFailed: "\u4EA4\u4ED8\u51C6\u5907\u6216\u90E8\u7F72\u5931\u8D25\uFF1A{reason}",
19847
+ deploy_configFileInvalid: "\u914D\u7F6E\u6587\u4EF6 {path} \u7B2C {line} \u884C\u683C\u5F0F\u6709\u8BEF\u3002\u8BF7\u68C0\u67E5\u7F29\u8FDB\u3001\u5192\u53F7\u548C\u5F15\u53F7\u540E\u91CD\u8BD5\u3002",
19848
+ deploy_configFileUnreadable: "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6 {path}\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u8DEF\u5F84\u4E0E\u8BFB\u53D6\u6743\u9650\u3002",
19849
+ deploy_bundleNameInvalid: "\u90E8\u7F72\u5305\u6587\u4EF6\u540D\u53EA\u652F\u6301\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u52A0\u53F7\u3001\u77ED\u6A2A\u7EBF\u548C\u4E0B\u5212\u7EBF\uFF1B\u6240\u5728\u76EE\u5F55\u53EF\u4EE5\u5305\u542B\u7A7A\u683C\u3002",
19850
+ deploy_secretsInvalid: "\u4E2D\u95F4\u4EF6\u51ED\u636E\u6587\u4EF6\u635F\u574F\u6216\u7F3A\u5C11\u5B57\u6BB5\uFF1A{path}\u3002\u8BF7\u4ECE\u5907\u4EFD\u6062\u590D\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u66F4\u6362\u5BC6\u7801\uFF0C\u4EE5\u514D\u5DF2\u6709\u6570\u636E\u65E0\u6CD5\u8BBF\u95EE\u3002"
19823
19851
  };
19824
19852
 
19825
19853
  // ../deploy/src/deploy-i18n.ts
@@ -19835,11 +19863,11 @@ var VercelProviderConfigSchema = external_exports.object({
19835
19863
  });
19836
19864
  var SshConfigSchema = external_exports.object({
19837
19865
  /** 远程主机地址或 ~/.ssh/config 中的 Host 别名 */
19838
- host: external_exports.string().min(1),
19866
+ host: external_exports.string().regex(/^[a-z0-9][a-z0-9.:-]*$/i),
19839
19867
  /** SSH 端口 */
19840
19868
  port: external_exports.number().int().min(1).max(65535).default(22),
19841
19869
  /** SSH 用户名 */
19842
- username: external_exports.string().min(1),
19870
+ username: external_exports.string().regex(/^[a-z_][\w-]*$/i),
19843
19871
  /** 私钥文件路径(省略时使用 ssh-agent 或 ~/.ssh 默认密钥) */
19844
19872
  identityFile: external_exports.string().min(1).optional(),
19845
19873
  /** known_hosts 文件路径(省略时使用默认;配合 CI/临时环境隔离主机指纹) */
@@ -19864,7 +19892,7 @@ var DockerSshProviderConfigSchema = external_exports.object({
19864
19892
  /** 远程主机运行参数 */
19865
19893
  remote: external_exports.object({
19866
19894
  /** 远程应用根目录 */
19867
- baseDir: external_exports.string().min(1).default("/opt/hai/apps"),
19895
+ baseDir: external_exports.string().regex(/^\/[\w/. -]+$/).refine((value) => !value.split("/").includes("..")).default("/opt/hai/apps"),
19868
19896
  /** 远程容器运行时(auto 优先 podman) */
19869
19897
  runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto")
19870
19898
  }).default({ baseDir: "/opt/hai/apps", runtime: "auto" }),
@@ -19963,10 +19991,16 @@ function resolveConfig(value, prefix, env) {
19963
19991
  }
19964
19992
  function loadDeploymentConfig(name, path, values) {
19965
19993
  try {
19966
- const config2 = resolveConfig(parse$1(readFileSync(path, "utf8")), `HAI_${name.toUpperCase()}`, values);
19994
+ const lineCounter = new LineCounter();
19995
+ const document = parseDocument(readFileSync(path, "utf8"), { lineCounter, prettyErrors: false });
19996
+ if (document.errors.length)
19997
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: String(lineCounter.linePos(document.errors[0].pos[0]).line) } }));
19998
+ const config2 = resolveConfig(document.toJS(), `HAI_${name.toUpperCase()}`, values);
19999
+ if (config2 !== null && (typeof config2 !== "object" || Array.isArray(config2)))
20000
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
19967
20001
  return ok(config2 && typeof config2 === "object" && !Array.isArray(config2) ? config2 : {});
19968
20002
  } catch {
19969
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
20003
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", { params: { path } }));
19970
20004
  }
19971
20005
  }
19972
20006
  function loadDeploymentEnvironment(options) {
@@ -20417,6 +20451,7 @@ function maskSecrets(text, secrets) {
20417
20451
  function runCommand(command, args, options = {}) {
20418
20452
  return new Promise((resolve6) => {
20419
20453
  const child = spawn(command, args, {
20454
+ windowsHide: true,
20420
20455
  cwd: options.cwd,
20421
20456
  env: options.env ? { ...process2.env, ...options.env } : process2.env,
20422
20457
  shell: false
@@ -20445,6 +20480,7 @@ function runCommand(command, args, options = {}) {
20445
20480
  }
20446
20481
  child.stdout.on("data", (chunk) => {
20447
20482
  stdout += chunk.toString("utf-8");
20483
+ options.onOutput?.(maskSecrets(chunk.toString("utf-8"), options.secrets));
20448
20484
  });
20449
20485
  child.stderr.on("data", (chunk) => {
20450
20486
  stderr += chunk.toString("utf-8");
@@ -20488,62 +20524,393 @@ ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
20488
20524
  });
20489
20525
  }
20490
20526
 
20491
- // ../deploy/src/container/deploy-bundle.ts
20527
+ // ../deploy/src/container/deploy-scripts.ts
20492
20528
  function shellArgument(value) {
20493
20529
  return `'${value.replaceAll("'", `'"'"'`)}'`;
20494
20530
  }
20495
- async function createDeploymentBundle(options) {
20496
- const { directory, artifactDir, artifact, projectName, environment, runtime, coreEnv } = options;
20497
- const stem = basename(options.output).replace(/\.tar$/i, "");
20498
- const versionSuffix = `-${artifact.appVersion}`;
20499
- const output2 = resolve(dirname(options.output), `${stem.endsWith(versionSuffix) ? stem : `${stem}${versionSuffix}`}.tar`);
20500
- const composeName = `docker-compose-${artifact.appVersion}.yml`;
20501
- mkdirSync(dirname(output2), { recursive: true, mode: 448 });
20502
- const staging = mkdtempSync(join(directory, ".bundle-"));
20503
- chmodSync(staging, 448);
20504
- const temporaryTar = `${output2}.partial`;
20505
- const startName = `start-${environment}.sh`;
20506
- try {
20507
- mkdirSync(join(staging, "images"));
20508
- for (const image of artifact.images)
20509
- copyFileSync(join(artifactDir, image.file), join(staging, "images", image.file));
20510
- for (const file2 of [composeName, ".env.runtime", ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : []])
20511
- copyFileSync(join(directory, file2), join(staging, file2));
20512
- const compose = parse$1(readFileSync(join(directory, composeName), "utf8"));
20513
- const images = [...new Set(Object.values(compose.services).flatMap((service) => service.image ? [service.image] : []))];
20514
- const start = `#!/bin/sh
20531
+ function createApplyScript(options) {
20532
+ const values = {
20533
+ ID: shellArgument(options.deploymentId),
20534
+ PROJECT: shellArgument(`hai-${options.projectName}`),
20535
+ ENV: shellArgument(options.environment),
20536
+ RUNTIME: shellArgument(options.runtime),
20537
+ PARTIAL: options.partial ? "true" : "false",
20538
+ SELECTED: shellArgument(options.selected.join(" ")),
20539
+ ROOT: shellArgument(options.directory),
20540
+ PORT: String(options.hostPort),
20541
+ HEALTH: options.healthPaths.map(shellArgument).join(" ")
20542
+ };
20543
+ return String.raw`#!/bin/sh
20515
20544
  set -eu
20516
- cd "$(dirname "$0")"
20517
20545
  umask 077
20518
- export HAI_DEPLOY_ENVIRONMENT=${shellArgument(environment)}
20519
- export HAI_CORE_ENV=${shellArgument(coreEnv)}
20520
- runtime=${shellArgument(runtime)}
20521
- if [ -n "\${HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME:-}" ]; then runtime="$HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME"; fi
20546
+ source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
20547
+ destination=@@ROOT@@
20548
+ if [ "$#" -gt 0 ]; then destination=$1; fi
20549
+ root=$(mkdir -p -- "$destination" && CDPATH= cd -- "$destination" && pwd)
20550
+ id=@@ID@@
20551
+ project=@@PROJECT@@
20552
+ export HAI_DEPLOY_ENVIRONMENT=@@ENV@@
20553
+ runtime=@@RUNTIME@@
20554
+ partial=@@PARTIAL@@
20555
+ orphan_option=''
20556
+ if [ "$partial" = false ]; then orphan_option=--remove-orphans; fi
20557
+ recreate_option=--force-recreate
20558
+ if [ "$partial" = true ]; then recreate_option=--no-recreate; fi
20559
+ selected=@@SELECTED@@
20560
+ release="$root/releases/$id"
20561
+ case "$release/" in "$source_dir/"*) printf 'Extract the package outside the deployment directory before applying it.\n' >&2; exit 1;; esac
20562
+ stage=''
20563
+ previous=''
20564
+ restore=''
20565
+ changed=false
20566
+ locked=false
20567
+ started=$(date +%s)
20568
+ step_started=$started
20569
+ step() {
20570
+ if [ -n "$stage" ]; then
20571
+ duration=$(( $(date +%s) - step_started ))
20572
+ printf '[PASS] %s (%ss)\n' "$stage" "$duration"
20573
+ printf '{"stage":"%s","status":"passed","durationSeconds":%s}\n' "$stage" "$duration" >> "$root/steps-$id.jsonl"
20574
+ fi
20575
+ stage="$1"
20576
+ step_started=$(date +%s)
20577
+ printf '\n[%s] %s\n' "$stage" "$2"
20578
+ }
20579
+ compose() {
20580
+ arguments_file="$release/compose-files"
20581
+ if [ -n "$restore" ]; then arguments_file="$root/releases/$restore/compose-files"; fi
20582
+ # -f 按顺序合并,由 Compose 保留未选服务、卷和环境文件,不自行拼装配置。
20583
+ command_args="$release/.compose-command"
20584
+ # 文件列表先反向读取,再向参数头部加入,保留原始合并顺序。
20585
+ awk '{lines[NR]=$0} END {for(i=NR;i>0;i--) print lines[i]}' "$arguments_file" > "$command_args"
20586
+ while IFS= read -r file; do set -- -f "$file" "$@"; done < "$command_args"
20587
+ "$runtime" compose -p "$project" "$@"
20588
+ }
20589
+ remove_selected() {
20590
+ test "$partial" = true || return 0
20591
+ # 直接替换选中容器,避免 Compose Provider 连带重建依赖它的未选服务;不删除卷。
20592
+ for service in "$@"; do
20593
+ containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service") || return 1
20594
+ for container in $containers; do
20595
+ "$runtime" rm -f "$container" || return 1
20596
+ done
20597
+ done
20598
+ }
20599
+ finish() {
20600
+ code=$?
20601
+ trap - EXIT HUP INT TERM
20602
+ if [ "$code" -ne 0 ]; then
20603
+ printf '[FAIL] %s (exit %s)\n' "$stage" "$code" >&2
20604
+ if [ "$changed" = true ]; then
20605
+ diagnostics="$root/diagnostics-$id.log"
20606
+ : > "$diagnostics"
20607
+ for service in $selected; do
20608
+ containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service" || true)
20609
+ for container in $containers; do
20610
+ "$runtime" inspect --format '{{.Name}} {{.State.Status}} exit={{.State.ExitCode}}' "$container" >> "$diagnostics" 2>&1 || true
20611
+ "$runtime" logs --tail 80 "$container" >> "$diagnostics" 2>&1 || true
20612
+ done
20613
+ done
20614
+ printf 'Startup diagnostics saved to %s (private server file).\n' "$diagnostics" >&2
20615
+ fi
20616
+ if [ -f "$release/rollback-images" ]; then
20617
+ while IFS=' ' read -r image reference; do
20618
+ test -n "$reference" || continue
20619
+ "$runtime" tag "$image" "$reference" || printf '[FAIL] Cannot restore image %s\n' "$reference" >&2
20620
+ done < "$release/rollback-images"
20621
+ fi
20622
+ if [ "$changed" = true ] && [ -n "$previous" ]; then
20623
+ printf '[RECOVER] Restoring previous selected services\n' >&2
20624
+ recover_selected=$selected
20625
+ if [ "$partial" = false ]; then recover_selected=$(grep -v -- '-init$' "$root/releases/$previous/services"); fi
20626
+ restore="$previous"
20627
+ if remove_selected $recover_selected && compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $recover_selected; then
20628
+ case " $selected " in *' app '*)
20629
+ if grep -qx web "$root/releases/$previous/services"; then compose exec -T web nginx -s reload || true; fi
20630
+ ;;
20631
+ esac
20632
+ printf '[RECOVER] Previous containers restored; verify application health\n' >&2
20633
+ else
20634
+ printf '[FAIL] Recovery failed. Use the previous release and inspect container logs.\n' >&2
20635
+ fi
20636
+ elif [ "$changed" = true ]; then
20637
+ compose stop $selected || true
20638
+ printf '[RECOVER] Failed first installation stopped; data volumes retained\n' >&2
20639
+ fi
20640
+ fi
20641
+ if [ "$locked" = true ]; then
20642
+ elapsed=$(( $(date +%s) - started ))
20643
+ outcome=passed
20644
+ if [ "$code" -ne 0 ]; then outcome=failed; fi
20645
+ printf '{"stage":"%s","status":"%s","durationSeconds":%s}\n' "$stage" "$outcome" "$(( $(date +%s) - step_started ))" >> "$root/steps-$id.jsonl"
20646
+ printf '{"deploymentId":"%s","stage":"%s","exitCode":%s,"durationSeconds":%s}\n' "$id" "$stage" "$code" "$elapsed" > "$root/result-$id.json"
20647
+ rmdir "$root/.deploy-lock" || true
20648
+ fi
20649
+ exit "$code"
20650
+ }
20651
+ trap finish EXIT
20652
+ trap 'exit 130' HUP INT TERM
20653
+ step preflight 'Checking deployment tools and package'
20654
+ for tool in sha256sum tar awk curl; do command -v "$tool" >/dev/null || { printf 'Required command missing: %s\n' "$tool" >&2; exit 1; }; done
20522
20655
  if [ "$runtime" = auto ]; then
20523
20656
  if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
20524
20657
  fi
20525
- case "$runtime" in docker|podman) ;; *) echo 'HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME must be docker or podman' >&2; exit 1;; esac
20658
+ case "$runtime" in docker|podman) ;; *) printf 'Runtime must be docker or podman\n' >&2; exit 1;; esac
20526
20659
  "$runtime" version >/dev/null
20527
20660
  "$runtime" compose version >/dev/null
20661
+ "$runtime" compose up --help | grep -q never || { printf 'Compose is too old. Install Docker Compose v2 or podman-compose 1.6.0 or newer (supports --pull never).\n' >&2; exit 1; }
20662
+ mkdir "$root/.deploy-lock" 2>/dev/null || { printf 'Another deployment is running. Check %s/.deploy-lock before retrying.\n' "$root" >&2; exit 1; }
20663
+ locked=true
20664
+ cd "$source_dir"
20528
20665
  sha256sum -c SHA256SUMS
20529
- chmod 600 .env.runtime
20530
- ${artifact.images.map((image) => `"$runtime" load -i ${shellArgument(`images/${image.file}`)}`).join("\n")}
20531
- # \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
20532
- for image in ${images.map(shellArgument).join(" ")}; do
20533
- "$runtime" image inspect "$image" >/dev/null 2>&1 || { echo "Missing image: $image. Load the matching image archive before retrying." >&2; exit 1; }
20666
+ if [ -f "$root/current" ]; then
20667
+ previous=$(cat "$root/current")
20668
+ case "$previous" in ''|*[!a-zA-Z0-9._-]*) printf 'Invalid deployment state\n' >&2; exit 1;; esac
20669
+ test -f "$root/releases/$previous/compose-files"
20670
+ fi
20671
+ if [ "$partial" = true ]; then
20672
+ test -n "$previous" || { printf 'Partial deployment requires a successful full installation. Deploy a complete bundle first.\n' >&2; exit 1; }
20673
+ cmp -s layout "$root/releases/$previous/layout" || { printf 'Ports, API routes or application layout changed. Create a full deployment bundle.\n' >&2; exit 1; }
20674
+ fi
20675
+ test ! -e "$release" || { printf 'This deployment ID was already used. Create a new deployment bundle.\n' >&2; exit 1; }
20676
+ mkdir -p "$release"
20677
+ cp -R "$source_dir/." "$release/"
20678
+ # Nginx 以非 root 用户运行;只开放不含凭据的代理配置读取权限。
20679
+ if [ -f "$release/web.conf" ]; then chmod 644 "$release/web.conf"; fi
20680
+ : > "$release/compose-files"
20681
+ if [ "$partial" = true ]; then
20682
+ cp "$root/releases/$previous/compose-files" "$release/compose-files"
20683
+ cp "$root/releases/$previous/middleware.env" "$release/middleware.env"
20684
+ for service in $selected; do
20685
+ grep -qx "$service" "$root/releases/$previous/services" || { printf 'Service %s has no installed baseline. Use a full deployment.\n' "$service" >&2; exit 1; }
20686
+ done
20687
+ fi
20688
+ printf '%s\n' "$release/compose-entry.json" >> "$release/compose-files"
20689
+ compose config >/dev/null
20690
+ compose config --services > "$release/services"
20691
+ : > "$release/rollback-images"
20692
+ if [ -n "$previous" ]; then
20693
+ for service in $selected; do
20694
+ containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service")
20695
+ for container in $containers; do
20696
+ "$runtime" inspect --format '{{.Image}} {{.Config.Image}}' "$container" >> "$release/rollback-images"
20697
+ done
20698
+ done
20699
+ fi
20700
+ step images 'Loading packaged images (no registry access)'
20701
+ while IFS=' ' read -r archive reference; do
20702
+ test -n "$archive" || continue
20703
+ "$runtime" load -i "$release/images/$archive"
20704
+ "$runtime" image inspect "$reference" >/dev/null
20705
+ architecture=$("$runtime" image inspect --format '{{.Architecture}}' "$reference")
20706
+ host_arch=$(uname -m)
20707
+ case "$host_arch:$architecture" in x86_64:amd64|aarch64:arm64) ;; *) printf 'Image architecture %s does not match server %s\n' "$architecture" "$host_arch" >&2; exit 1;; esac
20708
+ done < "$release/image-list"
20709
+ step dependencies 'Checking dependencies of selected services'
20710
+ if [ "$partial" = true ]; then cat "$release/services" >> "$release/dependencies"; fi
20711
+ while IFS= read -r dependency; do
20712
+ test -n "$dependency" || continue
20713
+ case "$dependency" in *-init) continue;; esac
20714
+ case " $selected " in *" $dependency "*) continue;; esac
20715
+ running=$("$runtime" ps -q --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$dependency")
20716
+ test -n "$running" || { printf 'Dependency %s is not running. Include it in a full deployment.\n' "$dependency" >&2; exit 1; }
20717
+ for container in $running; do
20718
+ test "$("$runtime" inspect --format '{{.State.Running}}' "$container")" = true || { printf 'Dependency %s is stopped. Start it before retrying.\n' "$dependency" >&2; exit 1; }
20719
+ done
20720
+ done < "$release/dependencies"
20721
+ step start 'Updating selected services'
20722
+ changed=true
20723
+ remove_selected $selected
20724
+ if [ -f "$release/initialize-storage" ]; then
20725
+ remove_selected storage-init
20726
+ compose up -d --no-deps --no-build --pull never storage
20727
+ compose up --no-deps --no-build --pull never "$recreate_option" --exit-code-from storage-init storage-init
20728
+ fi
20729
+ compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $selected
20730
+ case " $selected " in
20731
+ *' app '*)
20732
+ case " $selected " in *' web '*) ;; *)
20733
+ if grep -qx web "$release/services"; then compose exec -T web nginx -s reload; fi
20734
+ esac
20735
+ ;;
20736
+ esac
20737
+ step health 'Checking HTTP access'
20738
+ for path in @@HEALTH@@; do
20739
+ ready=false
20740
+ attempt=0
20741
+ while [ "$attempt" -lt 40 ]; do
20742
+ if curl --noproxy '*' -fsS --max-time 5 "http://127.0.0.1:@@PORT@@$path" >/dev/null 2>&1; then ready=true; break; fi
20743
+ attempt=$((attempt + 1))
20744
+ sleep 3
20745
+ done
20746
+ test "$ready" = true || { printf 'HTTP check failed: %s. Inspect compose logs for selected services.\n' "$path" >&2; exit 1; }
20534
20747
  done
20535
- exec "$runtime" compose -f ${shellArgument(composeName)} --env-file .env.runtime -p ${shellArgument(`hai-${projectName}`)} up -d
20748
+ step complete 'Deployment passed all checks'
20749
+ printf '%s\n' "$id" > "$root/current.next"
20750
+ mv "$root/current.next" "$root/current"
20751
+ printf '[PASS] %s\n' "$id"
20752
+ `.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]);
20753
+ }
20754
+ function createUploadScripts(config2, bundle, projectName) {
20755
+ const ssh = config2.ssh;
20756
+ const host = ssh?.host ?? "SERVER_HOST";
20757
+ const user = ssh?.username ?? "deploy";
20758
+ const remote = `${config2.remote.baseDir}/${projectName}`;
20759
+ const port = ssh?.port ?? 22;
20760
+ const sh = String.raw`#!/bin/sh
20761
+ set -eu
20762
+ cd "$(dirname "$0")"
20763
+ host=@@HOST@@
20764
+ user=@@USER@@
20765
+ port=@@PORT@@
20766
+ remote=@@REMOTE@@
20767
+ bundle=@@BUNDLE@@
20768
+ if [ "$#" -ge 1 ]; then host=$1; fi
20769
+ if [ "$#" -ge 2 ]; then port=$2; fi
20770
+ if [ "$#" -ge 3 ]; then user=$3; fi
20771
+ identity=$(printenv HAI_DEPLOY_IDENTITY_FILE || true)
20772
+ if [ -z "$identity" ]; then identity=@@IDENTITY@@; fi
20773
+ test "$host" != SERVER_HOST || { printf 'Specify the server: sh deploy.sh SERVER [PORT] [USER].\n' >&2; exit 1; }
20774
+ for tool in ssh scp; do command -v "$tool" >/dev/null || { printf 'Install OpenSSH (%s).\n' "$tool" >&2; exit 1; }; done
20775
+ set -- -o BatchMode=yes -o StrictHostKeyChecking=@@STRICT@@ -o ConnectTimeout=10
20776
+ if [ -n "$identity" ]; then set -- "$@" -i "$identity"; fi
20777
+ known_hosts=@@KNOWN@@
20778
+ if value=$(printenv HAI_DEPLOY_KNOWN_HOSTS_FILE); then known_hosts=$value; fi
20779
+ if [ -n "$known_hosts" ]; then set -- "$@" -o "UserKnownHostsFile=$known_hosts"; fi
20780
+ printf '[1/3] Connecting to %s\n' "$host"
20781
+ ssh "$@" -p "$port" "$user@$host" "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"
20782
+ printf '[2/3] Uploading %s\n' "$bundle"
20783
+ scp_host=$host
20784
+ case "$host" in *:*) scp_host="[$host]";; esac
20785
+ scp "$@" -P "$port" "$bundle" "$user@$scp_host:$remote/incoming/$bundle"
20786
+ printf '[3/3] Applying package and checking HTTP access\n'
20787
+ ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"
20788
+ `;
20789
+ const ps1 = String.raw`param([string]$IdentityFile = $env:HAI_DEPLOY_IDENTITY_FILE)
20790
+ $ErrorActionPreference = 'Stop'
20791
+ Set-Location -LiteralPath $PSScriptRoot
20792
+ $target = Get-Content -LiteralPath './deploy-target.json' -Raw | ConvertFrom-Json
20793
+ if ($target.host -eq 'SERVER_HOST') { throw 'Set the server host in deploy-target.json.' }
20794
+ if ($target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]+$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar$') { throw 'Invalid deployment target.' }
20795
+ $options = @('-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'ConnectTimeout=10')
20796
+ if ($target.strictHostKeyChecking -eq $false) { $options[3] = 'StrictHostKeyChecking=no' }
20797
+ if (-not $IdentityFile) { $IdentityFile = $target.identityFile }
20798
+ if ($IdentityFile) { $options += @('-i', $IdentityFile) }
20799
+ if ($target.knownHostsFile) { $options += @('-o', "UserKnownHostsFile=$($target.knownHostsFile)") }
20800
+ $destination = "$($target.username)@$($target.host)"
20801
+ $scpDestination = if ($target.host.Contains(':')) { "$($target.username)@[$($target.host)]" } else { $destination }
20802
+ $remote = $target.directory
20803
+ $bundle = $target.bundle
20804
+ function Invoke-Step([string]$Name, [string]$Command, [string[]]$Arguments) {
20805
+ Write-Host "[$Name]"
20806
+ & $Command @Arguments
20807
+ if ($LASTEXITCODE -ne 0) { throw "$Name failed (exit $LASTEXITCODE)." }
20808
+ }
20809
+ try {
20810
+ foreach ($tool in @('ssh', 'scp')) { Get-Command $tool -ErrorAction Stop | Out-Null }
20811
+ Invoke-Step '1/3 Connect' 'ssh' ($options + @('-p', [string]$target.port, $destination, "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"))
20812
+ Invoke-Step '2/3 Upload' 'scp' ($options + @('-P', [string]$target.port, $bundle, "$($scpDestination):$remote/incoming/$bundle"))
20813
+ Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$target.port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"))
20814
+ } catch { Write-Error $_; exit 1 }
20536
20815
  `;
20537
- writeFileSync(join(directory, startName), start, { mode: 448 });
20538
- writeFileSync(join(staging, startName), start, { mode: 448 });
20539
- const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output2), start: startName, images: images.join("\n"), compose: composeName } });
20540
- const metadata = deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: composeName } });
20541
- writeFileSync(join(staging, "README.txt"), `${metadata}
20816
+ const values = { HOST: shellArgument(host), USER: shellArgument(user), PORT: String(port), REMOTE: shellArgument(remote), BUNDLE: shellArgument(bundle), IDENTITY: shellArgument(ssh?.identityFile ?? ""), KNOWN: shellArgument(ssh?.knownHostsFile ?? ""), STRICT: ssh?.strictHostKeyChecking === false ? "no" : "accept-new" };
20817
+ return { sh: sh.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]), ps1 };
20818
+ }
20819
+
20820
+ // ../deploy/src/container/deploy-bundle.ts
20821
+ async function createDeploymentBundle(options) {
20822
+ const { directory, artifactDir, artifact, projectName, environment, config: config2, selected } = options;
20823
+ const stem = basename(options.output).replace(/\.tar$/i, "");
20824
+ const suffix = `-${artifact.appVersion}`;
20825
+ const output2 = resolve(dirname(options.output), `${stem.endsWith(suffix) ? stem : `${stem}${suffix}`}.tar`);
20826
+ if (!/^[\w.+-]+\.tar$/.test(basename(output2)))
20827
+ throw new Error(deployM("deploy_bundleNameInvalid"));
20828
+ const staging = mkdtempSync(join(directory, ".bundle-"));
20829
+ const temporaryTar = `${output2}.partial`;
20830
+ mkdirSync(dirname(output2), { recursive: true, mode: 448 });
20831
+ chmodSync(staging, 448);
20832
+ const files = [];
20833
+ const put = (file2, content) => {
20834
+ mkdirSync(dirname(join(staging, file2)), { recursive: true });
20835
+ writeFileSync(join(staging, file2), content, { mode: 384 });
20836
+ files.push(file2);
20837
+ };
20838
+ try {
20839
+ mkdirSync(join(staging, "images"));
20840
+ const compose = parse$1(readFileSync(join(directory, `docker-compose-${artifact.appVersion}.yml`), "utf8"));
20841
+ const dependencies = /* @__PURE__ */ new Set();
20842
+ const references = /* @__PURE__ */ new Set();
20843
+ for (const name of selected) {
20844
+ const service = compose.services[name];
20845
+ if (!service)
20846
+ throw new Error(deployM("deploy_selectionInvalid", { params: { service: name } }));
20847
+ if (typeof service.image === "string")
20848
+ references.add(service.image);
20849
+ if (service.depends_on) {
20850
+ for (const dependency of Array.isArray(service.depends_on) ? service.depends_on : Object.keys(service.depends_on))
20851
+ dependencies.add(dependency);
20852
+ }
20853
+ if (service.env_file)
20854
+ service.env_file = [`../${options.deploymentId}/.env.runtime`, `../${options.deploymentId}/middleware.env`];
20855
+ if (Array.isArray(service.volumes))
20856
+ service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace("./web.conf:", `../${options.deploymentId}/web.conf:`) : mount);
20857
+ }
20858
+ put("compose.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, compose.services[name]])) }));
20859
+ put("compose-entry.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, { extends: { file: `../${options.deploymentId}/compose.json`, service: name } }])) }));
20860
+ let runtime;
20861
+ const imageList = [];
20862
+ for (const reference of references) {
20863
+ if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference))
20864
+ throw new Error(deployM("deploy_imageVersionRequired", { params: { image: reference } }));
20865
+ const built = artifact.images.find((image) => image.imageRef === reference);
20866
+ const file2 = built?.file ?? `middleware-${imageList.length + 1}.tar`;
20867
+ if (built) {
20868
+ copyFileSync(join(artifactDir, file2), join(staging, "images", file2));
20869
+ } else {
20870
+ if (!runtime) {
20871
+ const candidates = options.runtime === "auto" ? ["podman", "docker"] : [options.runtime];
20872
+ for (const candidate of candidates) {
20873
+ if ((await runCommand(candidate, ["version"])).success) {
20874
+ runtime = candidate;
20875
+ break;
20876
+ }
20877
+ }
20878
+ if (!runtime)
20879
+ throw new Error(deployM("deploy_bundleRuntimeRequired"));
20880
+ }
20881
+ if (!(await runCommand(runtime, ["image", "inspect", reference])).success) {
20882
+ const pulled = await runCommand(runtime, ["pull", reference], { timeoutMs: 6e5 });
20883
+ if (!pulled.success)
20884
+ throw new Error(pulled.error.message);
20885
+ }
20886
+ const saved = await runCommand(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", join(staging, "images", file2), reference], { timeoutMs: 6e5 });
20887
+ if (!saved.success)
20888
+ throw new Error(saved.error.message);
20889
+ }
20890
+ files.push(`images/${file2}`);
20891
+ imageList.push(`${file2} ${reference}`);
20892
+ }
20893
+ put(".env.runtime", readFileSync(join(directory, ".env.runtime"), "utf8"));
20894
+ put("middleware.env", options.middlewareEnv);
20895
+ if (selected.includes("web"))
20896
+ put("web.conf", readFileSync(join(directory, "web.conf"), "utf8"));
20897
+ put("manifest.json", `${JSON.stringify({ ...artifact, images: artifact.images.filter((image) => references.has(image.imageRef)), deploymentId: options.deploymentId, selectedServices: selected, partial: options.partial }, null, 2)}
20898
+ `);
20899
+ put("layout", `${config2.expose.hostPort}:${options.containerPort}:${options.hasWeb}:${options.hasWeb ? await artifactChecksum(join(directory, "web.conf")) : ""}
20900
+ `);
20901
+ put("image-list", `${imageList.join("\n")}
20902
+ `);
20903
+ put("dependencies", `${[...dependencies].join("\n")}
20904
+ `);
20905
+ if (selected.includes("storage-init"))
20906
+ put("initialize-storage", "");
20907
+ put("apply.sh", createApplyScript({ deploymentId: options.deploymentId, projectName, environment, directory: `${config2.remote.baseDir}/${projectName}`, runtime: config2.remote.runtime, partial: options.partial, selected: selected.filter((name) => name !== "storage-init"), hostPort: config2.expose.hostPort, healthPaths: options.healthPaths }));
20908
+ const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output2), start: "apply.sh", images: [...references].join("\n"), compose: "compose.json" } });
20909
+ put("README.txt", `${deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: "compose.json" } })}
20542
20910
  ${instructions}
20543
20911
  `);
20544
- copyFileSync(join(artifactDir, "manifest.json"), join(staging, "manifest.json"));
20545
20912
  const checksums = [];
20546
- for (const file2 of [composeName, "manifest.json", ".env.runtime", startName, ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : [], ...artifact.images.map((image) => `images/${image.file}`)])
20913
+ for (const file2 of files)
20547
20914
  checksums.push(`${await artifactChecksum(join(staging, file2))} ${file2}`);
20548
20915
  writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
20549
20916
  `);
@@ -20551,25 +20918,33 @@ ${instructions}
20551
20918
  if (!tar.success)
20552
20919
  throw new Error(tar.error.message);
20553
20920
  chmodSync(temporaryTar, 384);
20554
- const { renameSync } = await import('fs');
20555
20921
  renameSync(temporaryTar, output2);
20556
- return { bundlePath: output2, instructions, startName };
20922
+ const upload = createUploadScripts(config2, basename(output2), projectName);
20923
+ const scripts = `${output2.slice(0, -4)}-deploy`;
20924
+ mkdirSync(scripts, { recursive: true, mode: 448 });
20925
+ writeFileSync(join(scripts, "deploy.sh"), upload.sh.replace('cd "$(dirname "$0")"', 'cd "$(dirname "$0")/.."'), { mode: 448 });
20926
+ writeFileSync(join(scripts, "deploy.ps1"), upload.ps1.replace("Set-Location -LiteralPath $PSScriptRoot", "Set-Location -LiteralPath (Split-Path -Parent $PSScriptRoot)").replace("'./deploy-target.json'", "(Join-Path $PSScriptRoot 'deploy-target.json')"));
20927
+ writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...config2.ssh, host: config2.ssh?.host ?? "SERVER_HOST", username: config2.ssh?.username ?? "deploy", port: config2.ssh?.port ?? 22, directory: `${config2.remote.baseDir}/${projectName}`, bundle: basename(output2) }, null, 2)}
20928
+ `);
20929
+ writeFileSync(join(scripts, "README.txt"), instructions);
20930
+ return { bundlePath: output2, instructions: `${instructions}
20931
+ ${scripts}`, startName: "apply.sh" };
20557
20932
  } finally {
20558
20933
  rmSync(staging, { recursive: true, force: true });
20559
20934
  rmSync(temporaryTar, { force: true });
20560
20935
  }
20561
20936
  }
20562
20937
  var IMAGES = {
20563
- postgres: "postgres:16-alpine",
20564
- mysql: "mysql:8.4",
20565
- redis: "redis:7-alpine",
20938
+ postgres: "docker.io/library/postgres:16.15-alpine",
20939
+ mysql: "docker.io/library/mysql:8.4.11",
20940
+ redis: "docker.io/library/redis:7.4.11-alpine",
20566
20941
  rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
20567
20942
  minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
20568
- qdrant: "qdrant/qdrant:v1.12.4",
20569
- pgvector: "pgvector/pgvector:pg17",
20570
- chroma: "chromadb/chroma:1.5.5",
20943
+ qdrant: "docker.io/qdrant/qdrant:v1.19.2",
20944
+ pgvector: "docker.io/pgvector/pgvector:0.8.0-pg17",
20945
+ chroma: "docker.io/chromadb/chroma:1.5.5",
20571
20946
  ldap: "docker.io/osixia/openldap:1.5.0",
20572
- smtp: "axllent/mailpit:v1.27.4"
20947
+ smtp: "docker.io/axllent/mailpit:v1.27.4"
20573
20948
  };
20574
20949
  function generatePassword() {
20575
20950
  return randomBytes(24).toString("base64url");
@@ -20578,8 +20953,12 @@ function resolveSidecarSecrets(appDir, projectName) {
20578
20953
  const secretFile = join(appDir, ".hai", "deploy", "runtime", `${projectName}.secrets.json`);
20579
20954
  if (existsSync(secretFile)) {
20580
20955
  try {
20581
- return JSON.parse(readFileSync(secretFile, "utf-8"));
20956
+ const saved = JSON.parse(readFileSync(secretFile, "utf-8"));
20957
+ if (!saved || !["dbPassword", "storagePassword", "ldapPassword"].every((key) => typeof saved[key] === "string" && saved[key].length > 0))
20958
+ throw new Error("Invalid credentials");
20959
+ return saved;
20582
20960
  } catch {
20961
+ throw new Error(deployM("deploy_secretsInvalid", { params: { path: secretFile } }));
20583
20962
  }
20584
20963
  }
20585
20964
  const secrets = {
@@ -20747,6 +21126,28 @@ function planChromaVecdb(plan, projectName, net) {
20747
21126
  plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
20748
21127
  }
20749
21128
  function planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine = "rustfs") {
21129
+ const serviceModes = { ...options?.serviceModes };
21130
+ for (const requirement of scan.middlewares ?? []) {
21131
+ if (options?.selectedServices && !options.selectedServices.includes(requirement.service))
21132
+ continue;
21133
+ const mode = serviceModes[requirement.envPrefix] ?? serviceModes[requirement.service];
21134
+ if (mode !== "keep")
21135
+ continue;
21136
+ const endpoint = requirement.config.url ?? requirement.config.endpoint;
21137
+ let host = requirement.config.host;
21138
+ if (typeof endpoint === "string") {
21139
+ try {
21140
+ host = new URL(endpoint).hostname;
21141
+ } catch {
21142
+ continue;
21143
+ }
21144
+ }
21145
+ if (host === requirement.service || ["sqlite", "local", "memory", "lancedb"].includes(requirement.backend)) {
21146
+ serviceModes[requirement.envPrefix] = "default";
21147
+ serviceModes[requirement.service] = "default";
21148
+ }
21149
+ }
21150
+ options = { ...options, serviceModes };
20750
21151
  const plan = {
20751
21152
  services: [],
20752
21153
  composeServices: {},
@@ -20873,23 +21274,6 @@ function generateComposeFile(options) {
20873
21274
  };
20874
21275
  if (Object.keys(sidecar.volumes).length > 0)
20875
21276
  compose.volumes = sidecar.volumes;
20876
- if (options.previousCompose && options.serviceModes) {
20877
- const previous = parse$1(options.previousCompose);
20878
- for (const [service, mode] of Object.entries(options.serviceModes)) {
20879
- if (mode !== "keep")
20880
- continue;
20881
- for (const name of [service, `${service}-init`]) {
20882
- if (previous.services?.[name]) {
20883
- services[name] = previous.services[name];
20884
- app.depends_on = [.../* @__PURE__ */ new Set([...app.depends_on ?? [], service])];
20885
- }
20886
- }
20887
- }
20888
- const previousMounts = previous.services?.app?.volumes;
20889
- if (Array.isArray(previousMounts) && Object.values(options.serviceModes).includes("keep"))
20890
- app.volumes = [.../* @__PURE__ */ new Set([...app.volumes ?? [], ...previousMounts])];
20891
- compose.volumes = { ...previous.volumes, ...sidecar.volumes };
20892
- }
20893
21277
  if (services["storage-init"]) {
20894
21278
  app.depends_on = {
20895
21279
  ...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
@@ -20976,14 +21360,15 @@ function buildScpUploadArgs(config2, localPath, remotePath) {
20976
21360
  "-P",
20977
21361
  String(config2.port),
20978
21362
  localPath,
20979
- `${config2.username}@${config2.host}:${remotePath}`
21363
+ `${config2.username}@${config2.host.includes(":") ? `[${config2.host}]` : config2.host}:${remotePath}`
20980
21364
  ];
20981
21365
  }
20982
21366
  function createSshExecutor(config2) {
20983
21367
  const exec = async (remoteCommand, options) => {
20984
21368
  return runCommand("ssh", buildSshExecArgs(config2, remoteCommand), {
20985
21369
  timeoutMs: options?.timeoutMs,
20986
- secrets: options?.secrets
21370
+ secrets: options?.secrets,
21371
+ onOutput: options?.onOutput
20987
21372
  });
20988
21373
  };
20989
21374
  const upload = async (localPath, remotePath) => {
@@ -21072,11 +21457,23 @@ function createDockerSshProvider(config2, container) {
21072
21457
  const images = artifact.images;
21073
21458
  const web = images.find((image) => image.name === "web");
21074
21459
  const app = images.find((image) => image.name === "app");
21075
- const containerPort = app.containerPort ?? config2.expose.containerPort;
21076
- const url2 = config2.ssh ? `http://${config2.ssh.host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
21460
+ if (options?.selectedServices) {
21461
+ const available = [...images.map((image) => image.name), ...scan.requiredServices];
21462
+ const invalid = options.selectedServices.find((name) => !available.includes(name));
21463
+ if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid)
21464
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: invalid ?? options.selectedServices.join(", ") } }));
21465
+ }
21466
+ const hasWeb = artifact.hasWeb || !!web;
21467
+ const partial2 = artifact.partial || options?.selectedServices !== void 0;
21468
+ const containerPort = app?.containerPort ?? artifact.appContainerPort ?? config2.expose.containerPort;
21469
+ const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
21470
+ const host = config2.ssh?.host;
21471
+ const url2 = host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
21077
21472
  const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
21473
+ if (sidecar.composeServices["storage-init"] && !app && options?.selectedServices?.includes("storage"))
21474
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageArtifactRequired"));
21078
21475
  let webProxy;
21079
- if (web) {
21476
+ if (hasWeb) {
21080
21477
  const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
21081
21478
  if (!server.success)
21082
21479
  return server;
@@ -21085,12 +21482,10 @@ function createDockerSshProvider(config2, container) {
21085
21482
  const apiPrefix = http?.apiPrefix ?? "/api/v1";
21086
21483
  if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
21087
21484
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
21088
- webProxy = generateWebProxyConfig(containerPort, apiPrefix, web.containerPort ?? 8080);
21485
+ webProxy = generateWebProxyConfig(containerPort, apiPrefix, webContainerPort);
21089
21486
  env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config2.ssh ? url2 : cors?.origin ?? "*" } });
21090
21487
  }
21091
21488
  const envPath = join(artifactDir, ".env.runtime");
21092
- const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
21093
- Object.assign(env, { ...previousEnv, ...env });
21094
21489
  writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
21095
21490
  if (options?.environment) {
21096
21491
  const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
@@ -21101,36 +21496,46 @@ function createDockerSshProvider(config2, container) {
21101
21496
  return saved;
21102
21497
  }
21103
21498
  const composeName = `docker-compose-${artifact.appVersion}.yml`;
21104
- const latestPath = join(artifactDir, "latest");
21105
- const previousName = existsSync(latestPath) ? readFileSync(latestPath, "utf8").trim() : "";
21106
- const previousPath = /^[\w.+-]+\.yml$/.test(previousName) ? join(artifactDir, previousName) : void 0;
21107
21499
  const composeContent = generateComposeFile({
21108
21500
  projectName,
21109
21501
  appVersion: artifact.appVersion,
21110
- imageRef: app.imageRef,
21502
+ imageRef: app?.imageRef ?? "localhost/hai-unselected:0",
21111
21503
  hostPort,
21112
21504
  containerPort,
21113
21505
  sidecar,
21114
- web: web ? { imageRef: web.imageRef, containerPort: web.containerPort ?? 8080 } : void 0,
21115
- serviceModes: options?.serviceModes,
21116
- previousCompose: previousPath && existsSync(previousPath) ? readFileSync(previousPath, "utf8") : void 0
21506
+ web: hasWeb ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0
21117
21507
  });
21118
21508
  const composePath = join(artifactDir, composeName);
21119
21509
  writeFileSync(composePath, composeContent);
21120
21510
  if (webProxy)
21121
21511
  writeFileSync(join(artifactDir, "web.conf"), webProxy);
21512
+ const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [...images.map((image) => image.name), ...Object.keys(sidecar.composeServices)];
21513
+ if (!selected.length || selected.some((name) => !/^[a-z][a-z0-9-]*$/.test(name)) || new Set(selected).size !== selected.length)
21514
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: selected.join(", ") } }));
21515
+ for (const name of selected) {
21516
+ if (["app", "web"].includes(name) && !images.some((image) => image.name === name))
21517
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: name } }));
21518
+ }
21519
+ if (selected.includes("storage") && sidecar.composeServices["storage-init"] && !selected.includes("storage-init"))
21520
+ selected.push("storage-init");
21521
+ options?.onProgress?.("packaging");
21122
21522
  const bundle = await createDeploymentBundle({
21123
21523
  directory: artifactDir,
21124
21524
  artifactDir: resolvedBuildDir,
21125
21525
  artifact,
21126
21526
  projectName,
21127
21527
  environment: options?.environment ?? "prod",
21128
- runtime: config2.remote.runtime,
21129
- coreEnv: env.HAI_CORE_ENV ?? "production",
21528
+ deploymentId,
21529
+ config: config2,
21530
+ runtime: options?.runtime ?? "auto",
21531
+ selected,
21532
+ partial: partial2,
21533
+ hasWeb,
21534
+ containerPort,
21535
+ middlewareEnv: serializeEnvFile(sidecar.env),
21536
+ healthPaths: hasWeb ? ["/health", "/_hai/web-health", "/"] : [scan.isSvelteKit ? "/" : "/health"],
21130
21537
  output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
21131
21538
  });
21132
- writeFileSync(latestPath, `${composeName}
21133
- `);
21134
21539
  if (options?.bundleOnly)
21135
21540
  return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
21136
21541
  if (!ssh)
@@ -21142,22 +21547,31 @@ function createDockerSshProvider(config2, container) {
21142
21547
  if (!remoteRuntime.success)
21143
21548
  return remoteRuntime;
21144
21549
  const remoteDir = `${baseDir}/${projectName}`;
21145
- const remoteTar = `${remoteDir}/${basename(bundle.bundlePath)}`;
21146
- const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
21550
+ const remoteTar = `${remoteDir}/incoming/${basename(bundle.bundlePath)}`;
21551
+ const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(`${remoteDir}/incoming`)} && chmod 700 -- ${shellArgument(remoteDir)} ${shellArgument(`${remoteDir}/incoming`)}`);
21147
21552
  if (!prepared.success)
21148
21553
  return prepared;
21149
21554
  const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
21150
21555
  if (!uploaded.success)
21151
21556
  return uploaded;
21557
+ let pendingOutput = "";
21152
21558
  const started = await ssh.exec(
21153
- `cd ${shellArgument(remoteDir)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} && HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=${shellArgument(remoteRuntime.data)} sh ${shellArgument(bundle.startName)}`,
21154
- { timeoutMs: 3e5, secrets: sidecar.secrets }
21559
+ `mkdir -p ${shellArgument(`${remoteTar}.contents`)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} -C ${shellArgument(`${remoteTar}.contents`)} && sh ${shellArgument(`${remoteTar}.contents/${bundle.startName}`)} ${shellArgument(remoteDir)}`,
21560
+ { timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output2) => {
21561
+ pendingOutput += output2;
21562
+ const lines = pendingOutput.split("\n");
21563
+ pendingOutput = lines.pop() ?? "";
21564
+ for (const line of lines) {
21565
+ const stage = /^\[(preflight|images|dependencies|start|health|complete)\]/.exec(line)?.[1];
21566
+ if (stage)
21567
+ options?.onProgress?.(stage);
21568
+ }
21569
+ } }
21155
21570
  );
21156
21571
  if (!started.success)
21157
21572
  return started;
21158
- await ssh.exec(`rm -f -- ${shellArgument(remoteTar)} ${images.map((image) => shellArgument(`${remoteDir}/images/${image.file}`)).join(" ")}`);
21159
21573
  logger5.info("Running health check", { url: url2 });
21160
- const healthy = await healthCheck(`${url2}${!web && scan.isSvelteKit ? "/" : "/health"}`) && (!web || await healthCheck(`${url2}/_hai/web-health`) && await healthCheck(`${url2}/`));
21574
+ const healthy = await healthCheck(`${url2}${!hasWeb && scan.isSvelteKit ? "/" : "/health"}`) && (!hasWeb || await healthCheck(`${url2}/_hai/web-health`) && await healthCheck(`${url2}/`));
21161
21575
  if (!healthy) {
21162
21576
  return err(
21163
21577
  HaiDeployError.HEALTH_CHECK_FAILED,
@@ -21181,7 +21595,8 @@ function createDockerSshProvider(config2, container) {
21181
21595
  return await deploy2(context);
21182
21596
  } catch (error62) {
21183
21597
  logger5.error("Deployment bundle failed", { error: error62 });
21184
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"), error62);
21598
+ const reason = error62 instanceof Error ? error62.message : String(error62);
21599
+ return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", { params: { reason: maskSecrets(reason, Object.values(context.appEnv ?? {}).filter((value) => value.length > 6)) } }), error62);
21185
21600
  }
21186
21601
  };
21187
21602
  return { name: "docker-ssh", deploy: deploySafely };
@@ -22160,11 +22575,19 @@ var deploy = {
22160
22575
  ...scanOptions.environment ? { environment: environment.data.environment } : {},
22161
22576
  ...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
22162
22577
  };
22578
+ if (options.selectedServices) {
22579
+ options.serviceModes = {
22580
+ ...options.serviceModes,
22581
+ ...Object.fromEntries(scan.requiredServices.filter((service) => !options.selectedServices.includes(service)).map((service) => [service, "keep"]))
22582
+ };
22583
+ }
22163
22584
  if (currentContainerProvider) {
22164
22585
  return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
22165
22586
  }
22166
22587
  if (options?.bundleOnly)
22167
22588
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
22589
+ if (options.selectedServices)
22590
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectedContainerOnly"));
22168
22591
  if (!currentProvider) {
22169
22592
  return notInitialized.result();
22170
22593
  }
@@ -22237,6 +22660,17 @@ async function deployContainerApp(provider, appDir, scan, rawProjectName, option
22237
22660
  } catch {
22238
22661
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
22239
22662
  }
22663
+ if (artifact.partial && !options?.selectedServices) {
22664
+ const selectedServices = artifact.images.map((image) => image.name);
22665
+ options = {
22666
+ ...options,
22667
+ selectedServices,
22668
+ serviceModes: {
22669
+ ...options?.serviceModes,
22670
+ ...Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"]))
22671
+ }
22672
+ };
22673
+ }
22240
22674
  if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
22241
22675
  return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
22242
22676
  let provisionEnv = {};
@@ -22296,5 +22730,5 @@ function describeMiddlewareBackend(backend) {
22296
22730
  }
22297
22731
 
22298
22732
  export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
22299
- //# sourceMappingURL=src-RDQ2EIBT.js.map
22300
- //# sourceMappingURL=src-RDQ2EIBT.js.map
22733
+ //# sourceMappingURL=src-2V6VP4I5.js.map
22734
+ //# sourceMappingURL=src-2V6VP4I5.js.map