@h-ai/cli 0.1.0-alpha.65 → 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 (141) hide show
  1. package/README.en.md +66 -251
  2. package/README.ja.md +67 -252
  3. package/README.md +66 -251
  4. package/dist/{chunk-47V5OT22.js → chunk-7DSURTJ4.js} +32 -115
  5. package/dist/chunk-7DSURTJ4.js.map +1 -0
  6. package/dist/{chunk-IPG3QNYK.js → chunk-AOQ4N657.js} +212 -8
  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-SJ2MBIR7.js.map → cli-config-templates-6AHNUGBN.js.map} +1 -1
  10. package/dist/{cli-skill-templates-A5VIR5J6.js → cli-skill-templates-FC3QRKDO.js} +3 -3
  11. package/dist/{cli-skill-templates-A5VIR5J6.js.map → cli-skill-templates-FC3QRKDO.js.map} +1 -1
  12. package/dist/index.js +644 -391
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src-6PPBKQ6Q.js → src-2V6VP4I5.js} +972 -520
  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 +18 -12
  20. package/templates/apps/api/.gitignore.hbs +9 -1
  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 -3
  24. package/templates/apps/api/hai.build.json.hbs +1 -0
  25. package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
  26. package/templates/apps/api/package.json.hbs +10 -8
  27. package/templates/apps/fullstack/.dockerignore.hbs +18 -14
  28. package/templates/apps/fullstack/.gitignore.hbs +9 -1
  29. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +23 -8
  30. package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
  31. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +0 -2
  32. package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
  33. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +3 -3
  34. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +25 -10
  35. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +2 -3
  36. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/lib.rs.hbs +73 -2
  37. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +2 -2
  38. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +2 -2
  39. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
  40. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +24 -10
  41. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +0 -2
  42. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +3 -3
  43. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +12 -1
  44. package/templates/apps/fullstack/hai.build.json.hbs +7 -0
  45. package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
  46. package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
  47. package/templates/apps/fullstack/package.json.hbs +7 -12
  48. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
  49. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +28 -7
  50. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +0 -1
  51. package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
  52. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +33 -12
  53. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +2 -2
  54. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +3 -3
  55. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +2 -2
  56. package/templates/apps/h5/src/routes/+layout.svelte.hbs +2 -2
  57. package/templates/apps/mobile-app/index.html.hbs +1 -0
  58. package/templates/apps/mobile-app/public/runtime-config.js +1 -0
  59. package/templates/apps/mobile-app/src/App.svelte.hbs +2 -2
  60. package/templates/apps/mobile-app/src/lib/api.ts.hbs +2 -1
  61. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +3 -2
  62. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
  63. package/templates/apps/website/src/routes/+layout.svelte.hbs +3 -3
  64. package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
  65. package/templates/base/.dockerignore.hbs +18 -13
  66. package/templates/base/.gitignore.hbs +9 -1
  67. package/templates/base/e2e/app.spec.ts.hbs +10 -0
  68. package/templates/base/hai.build.json.hbs +1 -0
  69. package/templates/base/images/app.Dockerfile.hbs +64 -0
  70. package/templates/base/{docker → images/web}/nginx.conf.hbs +6 -0
  71. package/templates/base/package.json.hbs +20 -19
  72. package/templates/base/svelte.config.js.hbs +2 -0
  73. package/templates/base/vite.config.ts.hbs +9 -3
  74. package/templates/shared/.gitattributes +11 -0
  75. package/templates/shared/container-runtime-config.sh +30 -0
  76. package/templates/shared/run-e2e.mjs +65 -0
  77. package/templates/shared/vite-runtime-config.ts +54 -0
  78. package/templates/skills/AGENTS.md +3 -3
  79. package/templates/skills/bridges/admin/AGENTS.md +1 -1
  80. package/templates/skills/bridges/api/AGENTS.md +1 -1
  81. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +1 -1
  82. package/templates/skills/bridges/h5/AGENTS.md +1 -1
  83. package/templates/skills/bridges/mobile-app/AGENTS.md +1 -1
  84. package/templates/skills/bridges/website/AGENTS.md +1 -1
  85. package/templates/skills/hai-api-client/SKILL.md +14 -161
  86. package/templates/skills/hai-api-client/reference.md +166 -0
  87. package/templates/skills/hai-api-contract/SKILL.md +15 -273
  88. package/templates/skills/hai-api-contract/reference.md +278 -0
  89. package/templates/skills/hai-app-create/SKILL.md +2 -2
  90. package/templates/skills/hai-app-create/development.md +6 -4
  91. package/templates/skills/hai-app-create/reference.md +5 -4
  92. package/templates/skills/hai-app-review/SKILL.md +1 -1
  93. package/templates/skills/hai-app-tests/SKILL.md +4 -3
  94. package/templates/skills/hai-app-tests/reference.md +2 -2
  95. package/templates/skills/hai-app-tests/validation.md +31 -0
  96. package/templates/skills/hai-audit/SKILL.md +17 -142
  97. package/templates/skills/hai-audit/reference.md +145 -0
  98. package/templates/skills/hai-build/SKILL.md +24 -3
  99. package/templates/skills/hai-build/reference.md +10 -9
  100. package/templates/skills/hai-cache/SKILL.md +15 -182
  101. package/templates/skills/hai-cache/reference.md +187 -0
  102. package/templates/skills/hai-capacitor/SKILL.md +4 -3
  103. package/templates/skills/hai-capacitor/reference.md +6 -4
  104. package/templates/skills/hai-ci/SKILL.md +2 -0
  105. package/templates/skills/hai-core/SKILL.md +3 -3
  106. package/templates/skills/hai-crypto/SKILL.md +16 -252
  107. package/templates/skills/hai-crypto/reference.md +256 -0
  108. package/templates/skills/hai-datapipe/SKILL.md +15 -196
  109. package/templates/skills/hai-datapipe/reference.md +202 -0
  110. package/templates/skills/hai-deploy/SKILL.md +28 -271
  111. package/templates/skills/hai-deploy/middleware.md +6 -18
  112. package/templates/skills/hai-framework-sync/SKILL.md +2 -2
  113. package/templates/skills/hai-framework-sync/sync.md +1 -1
  114. package/templates/skills/hai-iam/SKILL.md +1 -3
  115. package/templates/skills/hai-kit/SKILL.md +12 -167
  116. package/templates/skills/hai-kit/reference.md +172 -0
  117. package/templates/skills/hai-payment/SKILL.md +19 -251
  118. package/templates/skills/hai-payment/reference.md +256 -0
  119. package/templates/skills/hai-pr-review/SKILL.md +4 -4
  120. package/templates/skills/hai-reach/SKILL.md +2 -2
  121. package/templates/skills/hai-reldb/SKILL.md +1 -1
  122. package/templates/skills/hai-scheduler/SKILL.md +15 -195
  123. package/templates/skills/hai-scheduler/reference.md +202 -0
  124. package/templates/skills/hai-serv/SKILL.md +2 -2
  125. package/templates/skills/hai-spec-implement/SKILL.md +1 -1
  126. package/templates/skills/hai-spec-implement/workflow.md +3 -3
  127. package/templates/skills/hai-storage/SKILL.md +17 -238
  128. package/templates/skills/hai-storage/reference.md +243 -0
  129. package/templates/skills/hai-ui/SKILL.md +1 -1
  130. package/templates/skills/hai-ui/reference.md +11 -5
  131. package/templates/skills/hai-vecdb/SKILL.md +2 -2
  132. package/dist/chunk-47V5OT22.js.map +0 -1
  133. package/dist/chunk-IPG3QNYK.js.map +0 -1
  134. package/dist/cli-config-templates-SJ2MBIR7.js +0 -5
  135. package/dist/src-6PPBKQ6Q.js.map +0 -1
  136. package/templates/apps/api/Dockerfile.hbs +0 -48
  137. package/templates/apps/fullstack/Dockerfile.hbs +0 -69
  138. package/templates/base/Dockerfile.hbs +0 -79
  139. /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
  140. /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
  141. /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { __export } from './chunk-2H7UOFLK.js';
3
- import { core, ok, err } from '@h-ai/core';
4
- import { readFileSync, existsSync, writeFileSync, chmodSync, mkdirSync, statSync, readdirSync } from 'fs';
3
+ import { createHash, randomBytes } from 'crypto';
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, err, ok } from '@h-ai/core';
6
7
  import process2 from 'process';
7
8
  import { parseEnv } from 'util';
8
- import { parse as parse$1, stringify, parseAllDocuments } from 'yaml';
9
+ import { LineCounter, parseDocument, parse as parse$1, stringify, parseAllDocuments } from 'yaml';
9
10
  import { homedir } from 'os';
10
- import { execSync, spawn } from 'child_process';
11
- import { randomBytes, createHash } from 'crypto';
11
+ import { spawn } from 'child_process';
12
12
  import { Buffer } from 'buffer';
13
13
 
14
14
  // ../../node_modules/zod/v4/classic/external.js
@@ -1398,10 +1398,10 @@ function defineLazyInternal(inst, key, compute) {
1398
1398
  Object.defineProperty(this, key, { configurable: true, writable: true, value });
1399
1399
  broke = broke || outer;
1400
1400
  return value;
1401
- } catch (err17) {
1401
+ } catch (err15) {
1402
1402
  delete this[key];
1403
1403
  broke = broke || outer;
1404
- throw err17;
1404
+ throw err15;
1405
1405
  }
1406
1406
  },
1407
1407
  set(value) {
@@ -5037,7 +5037,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5037
5037
  var NONE = 0;
5038
5038
  var ASSUMED = 1;
5039
5039
  var PROVEN = 2;
5040
- function isRecursive(inst, stack, resolve3) {
5040
+ function isRecursive(inst, stack, resolve6) {
5041
5041
  const cached2 = recursive.get(inst);
5042
5042
  if (cached2 !== void 0)
5043
5043
  return cached2 ? PROVEN : NONE;
@@ -5047,7 +5047,7 @@ function isRecursive(inst, stack, resolve3) {
5047
5047
  let result = NONE;
5048
5048
  const check2 = (child) => {
5049
5049
  if (result !== PROVEN && child?._zod) {
5050
- const answer = isRecursive(child, stack, resolve3);
5050
+ const answer = isRecursive(child, stack, resolve6);
5051
5051
  if (answer > result)
5052
5052
  result = answer;
5053
5053
  }
@@ -5058,7 +5058,7 @@ function isRecursive(inst, stack, resolve3) {
5058
5058
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5059
5059
  if (!desc.enumerable)
5060
5060
  continue;
5061
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
5061
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
5062
5062
  if (child > answer)
5063
5063
  answer = child;
5064
5064
  }
@@ -5122,7 +5122,7 @@ function isRecursive(inst, stack, resolve3) {
5122
5122
  break;
5123
5123
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5124
5124
  case "lazy": {
5125
- const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
5125
+ const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
5126
5126
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5127
5127
  break;
5128
5128
  }
@@ -12940,9 +12940,9 @@ function compile(schema, options) {
12940
12940
  const clone2 = withParser(schema, parser);
12941
12941
  clone2._zod.bag.validator = compileValidator(schema, parser);
12942
12942
  return clone2;
12943
- } catch (err17) {
12943
+ } catch (err15) {
12944
12944
  if (options?.strict)
12945
- throw err17;
12945
+ throw err15;
12946
12946
  return schema;
12947
12947
  }
12948
12948
  }
@@ -13032,8 +13032,8 @@ ${code}
13032
13032
  try {
13033
13033
  const factory = new F(...constantNames, factoryCode);
13034
13034
  fn = factory(...constantValues);
13035
- } catch (err17) {
13036
- throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${err17.message})`);
13035
+ } catch (err15) {
13036
+ throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${err15.message})`);
13037
13037
  }
13038
13038
  if (options?.debug) {
13039
13039
  fn.code = fullCode;
@@ -13071,9 +13071,9 @@ function compileChild(doc, ctx, schema, accessor, needsValue = true) {
13071
13071
  const varCounter = ctx.varCounter;
13072
13072
  try {
13073
13073
  return generateCheck(doc, ctx, schema, accessor, needsValue);
13074
- } catch (err17) {
13075
- if (!(err17 instanceof ZodCompileUnsupportedError) || !err17.islandable)
13076
- throw err17;
13074
+ } catch (err15) {
13075
+ if (!(err15 instanceof ZodCompileUnsupportedError) || !err15.islandable)
13076
+ throw err15;
13077
13077
  doc.content.length = contentLen;
13078
13078
  if (ctx.constants.size > constantCount) {
13079
13079
  const trailing = Array.from(ctx.constants.keys()).slice(constantCount);
@@ -19668,6 +19668,52 @@ function date4(params) {
19668
19668
  return _coercedDate(ZodDate, params);
19669
19669
  }
19670
19670
 
19671
+ // ../deploy/src/deploy-artifact.ts
19672
+ var DeploymentArtifactSchema = external_exports.object({
19673
+ /** 清单格式版本,与应用版本和部署环境无关。 */
19674
+ version: external_exports.literal(3),
19675
+ /** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
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
+ /** 本次构建标识;同一应用版本允许重复构建。 */
19678
+ buildId: external_exports.string().regex(/^[a-z0-9][\w.+-]*$/i),
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
+ /** 本次构建选择的镜像归档。 */
19688
+ images: external_exports.array(external_exports.object({
19689
+ /** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
19690
+ name: external_exports.enum(["app", "web"]),
19691
+ /** 相对于清单目录的 tar 文件名。 */
19692
+ file: external_exports.string().regex(/^[\w.+-]+\.tar$/),
19693
+ /** 归档导入后可直接使用的不可变镜像引用。 */
19694
+ imageRef: external_exports.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
19695
+ /** tar 内容的 SHA-256,用于部署前校验。 */
19696
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
19697
+ /** 容器内部监听端口;服务器公开端口由部署选择。 */
19698
+ containerPort: external_exports.number().int().min(1).max(65535).optional()
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"));
19701
+ async function artifactChecksum(file2) {
19702
+ const hash2 = createHash("sha256");
19703
+ for await (const chunk of createReadStream(file2))
19704
+ hash2.update(chunk);
19705
+ return hash2.digest("hex");
19706
+ }
19707
+ async function readDeploymentArtifact(directory) {
19708
+ const artifact = DeploymentArtifactSchema.parse(JSON.parse(readFileSync(resolve(directory, "manifest.json"), "utf8")));
19709
+ for (const image of artifact.images) {
19710
+ const file2 = resolve(directory, image.file);
19711
+ if (!existsSync(file2) || !lstatSync(file2).isFile() || lstatSync(file2).isSymbolicLink() || await artifactChecksum(file2) !== image.sha256)
19712
+ throw new Error(`Invalid artifact: ${image.file}`);
19713
+ }
19714
+ return artifact;
19715
+ }
19716
+
19671
19717
  // ../deploy/messages/en-US.json
19672
19718
  var en_US_default = {
19673
19719
  deploy_notInitialized: "Deploy module not initialized, call deploy.init() first",
@@ -19678,7 +19724,6 @@ var en_US_default = {
19678
19724
  deploy_authRequired: "Authentication required",
19679
19725
  deploy_authFailed: "Authentication failed: {error}",
19680
19726
  deploy_projectCreateFailed: "Project creation failed: {error}",
19681
- deploy_buildFailed: "Application build failed: {error}",
19682
19727
  deploy_uploadFailed: "Build output upload failed: {error}",
19683
19728
  deploy_deployFailed: "Deployment failed: {error}",
19684
19729
  deploy_provisionFailed: "Service provisioning failed: {service} - {error}",
@@ -19698,8 +19743,6 @@ var en_US_default = {
19698
19743
  deploy_recoverySuggestion: "Inspect listed resources in the provider console before retrying with the same project name. Unknown means the request may have created a resource. Reused resources are never deleted automatically; remove newly created resources manually only after checking their use.",
19699
19744
  deploy_commandTimeout: "Command timed out: {command}",
19700
19745
  deploy_commandFailed: "Command failed ({command}): {error}",
19701
- deploy_containerRuntimeNotFound: "No usable container runtime found: {runtime}",
19702
- deploy_containerBuildFailed: "Container image build failed: {error}",
19703
19746
  deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
19704
19747
  deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
19705
19748
  deploy_imageTransferFailed: "Image transfer failed: {error}",
@@ -19720,7 +19763,23 @@ var en_US_default = {
19720
19763
  deploy_purposeLdap: "Directory account authentication; the default directory has no business users",
19721
19764
  deploy_purposeConsole: "Messages go to application logs without delivery",
19722
19765
  deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
19723
- deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota"
19766
+ deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
19767
+ deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
19768
+ deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
19769
+ deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
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}",
19771
+ deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
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."
19724
19783
  };
19725
19784
 
19726
19785
  // ../deploy/messages/zh-CN.json
@@ -19733,7 +19792,6 @@ var zh_CN_default2 = {
19733
19792
  deploy_authRequired: "\u8BF7\u5148\u5B8C\u6210\u8BA4\u8BC1",
19734
19793
  deploy_authFailed: "\u8BA4\u8BC1\u5931\u8D25\uFF1A{error}",
19735
19794
  deploy_projectCreateFailed: "\u9879\u76EE\u521B\u5EFA\u5931\u8D25\uFF1A{error}",
19736
- deploy_buildFailed: "\u5E94\u7528\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
19737
19795
  deploy_uploadFailed: "\u6784\u5EFA\u4EA7\u7269\u4E0A\u4F20\u5931\u8D25\uFF1A{error}",
19738
19796
  deploy_deployFailed: "\u90E8\u7F72\u5931\u8D25\uFF1A{error}",
19739
19797
  deploy_provisionFailed: "\u57FA\u7840\u8BBE\u65BD\u5F00\u901A\u5931\u8D25\uFF1A{service} - {error}",
@@ -19753,8 +19811,6 @@ var zh_CN_default2 = {
19753
19811
  deploy_recoverySuggestion: "\u91CD\u8BD5\u524D\u5728\u670D\u52A1\u5546\u63A7\u5236\u53F0\u6838\u5BF9\u6E05\u5355\uFF0C\u5E76\u4F7F\u7528\u76F8\u540C\u9879\u76EE\u540D\u3002unknown \u8868\u793A\u8BF7\u6C42\u53EF\u80FD\u5DF2\u521B\u5EFA\u8D44\u6E90\u3002\u590D\u7528\u8D44\u6E90\u4E0D\u4F1A\u81EA\u52A8\u5220\u9664\uFF1B\u65B0\u5EFA\u8D44\u6E90\u4EC5\u5728\u786E\u8BA4\u672A\u88AB\u4F7F\u7528\u540E\u4EBA\u5DE5\u6E05\u7406\u3002",
19754
19812
  deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
19755
19813
  deploy_commandFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08{command}\uFF09\uFF1A{error}",
19756
- deploy_containerRuntimeNotFound: "\u672A\u627E\u5230\u53EF\u7528\u7684\u5BB9\u5668\u8FD0\u884C\u65F6\uFF1A{runtime}",
19757
- deploy_containerBuildFailed: "\u5BB9\u5668\u955C\u50CF\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
19758
19814
  deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
19759
19815
  deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
19760
19816
  deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
@@ -19775,7 +19831,23 @@ var zh_CN_default2 = {
19775
19831
  deploy_purposeLdap: "\u4F01\u4E1A\u76EE\u5F55\u8D26\u53F7\u8BA4\u8BC1\uFF1B\u9ED8\u8BA4\u76EE\u5F55\u4E0D\u5305\u542B\u4E1A\u52A1\u7528\u6237",
19776
19832
  deploy_purposeConsole: "\u6D88\u606F\u8F93\u51FA\u5230\u5E94\u7528\u65E5\u5FD7\uFF0C\u4E0D\u5B9E\u9645\u6295\u9012",
19777
19833
  deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
19778
- deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6"
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",
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",
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",
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",
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}",
19839
+ deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
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"
19779
19851
  };
19780
19852
 
19781
19853
  // ../deploy/src/deploy-i18n.ts
@@ -19791,11 +19863,11 @@ var VercelProviderConfigSchema = external_exports.object({
19791
19863
  });
19792
19864
  var SshConfigSchema = external_exports.object({
19793
19865
  /** 远程主机地址或 ~/.ssh/config 中的 Host 别名 */
19794
- host: external_exports.string().min(1),
19866
+ host: external_exports.string().regex(/^[a-z0-9][a-z0-9.:-]*$/i),
19795
19867
  /** SSH 端口 */
19796
19868
  port: external_exports.number().int().min(1).max(65535).default(22),
19797
19869
  /** SSH 用户名 */
19798
- username: external_exports.string().min(1),
19870
+ username: external_exports.string().regex(/^[a-z_][\w-]*$/i),
19799
19871
  /** 私钥文件路径(省略时使用 ssh-agent 或 ~/.ssh 默认密钥) */
19800
19872
  identityFile: external_exports.string().min(1).optional(),
19801
19873
  /** known_hosts 文件路径(省略时使用默认;配合 CI/临时环境隔离主机指纹) */
@@ -19816,11 +19888,11 @@ var ExposeSchema = PortExposeSchema;
19816
19888
  var DockerSshProviderConfigSchema = external_exports.object({
19817
19889
  type: external_exports.literal("docker-ssh"),
19818
19890
  /** SSH 连接参数 */
19819
- ssh: SshConfigSchema,
19891
+ ssh: SshConfigSchema.optional(),
19820
19892
  /** 远程主机运行参数 */
19821
19893
  remote: external_exports.object({
19822
19894
  /** 远程应用根目录 */
19823
- 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"),
19824
19896
  /** 远程容器运行时(auto 优先 podman) */
19825
19897
  runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto")
19826
19898
  }).default({ baseDir: "/opt/hai/apps", runtime: "auto" }),
@@ -19870,24 +19942,15 @@ var DeployConfigSchema = external_exports.object({
19870
19942
  environment: external_exports.string().regex(/^[a-z0-9][\w-]*$/i).refine((value) => !["local", "example"].includes(value.toLowerCase())).optional(),
19871
19943
  provider: ProviderConfigSchema,
19872
19944
  services: ServicesConfigSchema,
19873
- /** 本地容器构建参数(docker-ssh 使用;auto 优先 podman) */
19945
+ /** Docker Compose 部署参数;构建配置位于项目 hai.build.json。 */
19874
19946
  container: external_exports.object({
19875
- runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto"),
19876
- /** Podman 构建是否继承代理;容器不能访问宿主机 localhost 代理时设为 false。 */
19877
- httpProxy: external_exports.boolean().default(true),
19878
19947
  /** 自建 S3 服务实现;协议仍为 storage.type=s3。 */
19879
- s3Engine: external_exports.enum(["rustfs", "minio"]).default("rustfs"),
19880
- /** SQLite 预编译包镜像根地址;省略时从官方 GitHub Release 下载,不允许嵌入凭据。 */
19881
- sqlitePrebuildMirror: external_exports.url().refine((value) => {
19882
- const url2 = new URL(value);
19883
- return ["http:", "https:"].includes(url2.protocol) && !url2.username && !url2.password && !url2.search && !url2.hash;
19884
- }).optional()
19948
+ s3Engine: external_exports.enum(["rustfs", "minio"]).default("rustfs")
19885
19949
  }).optional()
19886
19950
  });
19887
19951
  var DeployErrorInfo = {
19888
19952
  DEPLOY_FAILED: "001:500",
19889
19953
  PROJECT_CREATE_FAILED: "002:500",
19890
- BUILD_FAILED: "003:500",
19891
19954
  UPLOAD_FAILED: "004:500",
19892
19955
  AUTH_REQUIRED: "005:401",
19893
19956
  AUTH_FAILED: "006:401",
@@ -19899,8 +19962,6 @@ var DeployErrorInfo = {
19899
19962
  UNSUPPORTED_TYPE: "012:400",
19900
19963
  CONFIG_ERROR: "013:500",
19901
19964
  CREDENTIAL_ERROR: "014:500",
19902
- CONTAINER_RUNTIME_NOT_FOUND: "015:500",
19903
- CONTAINER_BUILD_FAILED: "016:500",
19904
19965
  REMOTE_CONNECT_FAILED: "017:500",
19905
19966
  REMOTE_COMMAND_FAILED: "018:500",
19906
19967
  IMAGE_TRANSFER_FAILED: "019:500",
@@ -19909,9 +19970,16 @@ var DeployErrorInfo = {
19909
19970
  var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
19910
19971
 
19911
19972
  // ../deploy/src/deploy-environment.ts
19973
+ function environmentValue(value) {
19974
+ try {
19975
+ return parse$1(value) ?? value;
19976
+ } catch {
19977
+ return value;
19978
+ }
19979
+ }
19912
19980
  function resolveConfig(value, prefix, env) {
19913
19981
  if (env[prefix] !== void 0)
19914
- return parse$1(env[prefix]) ?? env[prefix];
19982
+ return environmentValue(env[prefix]);
19915
19983
  if (Array.isArray(value))
19916
19984
  return value.map((item, index) => resolveConfig(item, `${prefix}_${index}`, env));
19917
19985
  if (value && typeof value === "object")
@@ -19919,14 +19987,20 @@ function resolveConfig(value, prefix, env) {
19919
19987
  if (typeof value !== "string")
19920
19988
  return value;
19921
19989
  const resolved = value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, name, fallback) => env[name] ?? fallback ?? "");
19922
- return /^\$\{[^}]+\}$/.test(value) ? parse$1(resolved) ?? resolved : resolved;
19990
+ return /^\$\{[^}]+\}$/.test(value) ? environmentValue(resolved) : resolved;
19923
19991
  }
19924
19992
  function loadDeploymentConfig(name, path, values) {
19925
19993
  try {
19926
- 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" } }));
19927
20001
  return ok(config2 && typeof config2 === "object" && !Array.isArray(config2) ? config2 : {});
19928
20002
  } catch {
19929
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
20003
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", { params: { path } }));
19930
20004
  }
19931
20005
  }
19932
20006
  function loadDeploymentEnvironment(options) {
@@ -20187,39 +20261,25 @@ function saveCredentials(entries) {
20187
20261
  );
20188
20262
  }
20189
20263
  }
20190
- var logger2 = core.logger.child({ module: "deploy", scope: "functions" });
20264
+ function resolveArtifactDirectory(appDir) {
20265
+ let directory = resolve(appDir);
20266
+ while (true) {
20267
+ if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
20268
+ return join(directory, ".hai/build/docker");
20269
+ const parent = dirname(directory);
20270
+ if (parent === directory)
20271
+ return join(appDir, ".hai/build/docker");
20272
+ directory = parent;
20273
+ }
20274
+ }
20191
20275
  function usesDefaultMiddleware(scan, service, options) {
20192
20276
  const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
20193
20277
  return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
20194
20278
  }
20195
- function buildApp(appDir, buildCommand, envVars = {}) {
20196
- logger2.info("Building application", { appDir, buildCommand });
20197
- try {
20198
- execSync(buildCommand, {
20199
- cwd: appDir,
20200
- stdio: "inherit",
20201
- env: { ...process2.env, ...envVars }
20202
- });
20203
- return ok(void 0);
20204
- } catch (error62) {
20205
- return err(
20206
- HaiDeployError.BUILD_FAILED,
20207
- deployM("deploy_buildFailed", {
20208
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
20209
- }),
20210
- error62
20211
- );
20212
- }
20213
- }
20214
20279
  function resolveOutputDir(appDir, providerType) {
20215
20280
  switch (providerType) {
20216
- case "vercel": {
20217
- const vercelOutput = join(appDir, ".vercel", "output");
20218
- if (existsSync(vercelOutput)) {
20219
- return vercelOutput;
20220
- }
20221
- return join(appDir, "build");
20222
- }
20281
+ case "vercel":
20282
+ return join(appDir, ".vercel", "output");
20223
20283
  default:
20224
20284
  return join(appDir, "build");
20225
20285
  }
@@ -20250,7 +20310,7 @@ function recoveryFailure(error62, recovery) {
20250
20310
  ext: { recovery }
20251
20311
  });
20252
20312
  }
20253
- var logger3 = core.logger.child({ module: "deploy", scope: "scanner" });
20313
+ var logger2 = core.logger.child({ module: "deploy", scope: "scanner" });
20254
20314
  function record2(value) {
20255
20315
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20256
20316
  }
@@ -20369,26 +20429,522 @@ async function scanApp(inputDir, options) {
20369
20429
  adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
20370
20430
  nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
20371
20431
  requiredServices: [...new Set(middlewares.map((item) => item.service))],
20372
- serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend])),
20373
- buildCommand: "pnpm build"
20432
+ serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
20374
20433
  };
20375
- logger3.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
20434
+ logger2.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
20376
20435
  return ok(scan);
20377
20436
  } catch {
20378
20437
  return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", { params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" } }));
20379
20438
  }
20380
20439
  }
20440
+ var logger3 = core.logger.child({ module: "deploy", scope: "command" });
20441
+ function maskSecrets(text, secrets) {
20442
+ if (!secrets || secrets.length === 0)
20443
+ return text;
20444
+ let masked = text;
20445
+ for (const secret of secrets) {
20446
+ if (secret && secret.length > 0)
20447
+ masked = masked.split(secret).join("***");
20448
+ }
20449
+ return masked;
20450
+ }
20451
+ function runCommand(command, args, options = {}) {
20452
+ return new Promise((resolve6) => {
20453
+ const child = spawn(command, args, {
20454
+ windowsHide: true,
20455
+ cwd: options.cwd,
20456
+ env: options.env ? { ...process2.env, ...options.env } : process2.env,
20457
+ shell: false
20458
+ });
20459
+ let stdout = "";
20460
+ let stderr = "";
20461
+ let settled = false;
20462
+ let timer;
20463
+ const finish = (result) => {
20464
+ if (settled)
20465
+ return;
20466
+ settled = true;
20467
+ if (timer)
20468
+ clearTimeout(timer);
20469
+ resolve6(result);
20470
+ };
20471
+ if (options.timeoutMs && options.timeoutMs > 0) {
20472
+ timer = setTimeout(() => {
20473
+ child.kill("SIGKILL");
20474
+ logger3.error("Command timed out", { command, timeoutMs: options.timeoutMs });
20475
+ finish(err(
20476
+ HaiDeployError.REMOTE_COMMAND_FAILED,
20477
+ deployM("deploy_commandTimeout", { params: { command } })
20478
+ ));
20479
+ }, options.timeoutMs);
20480
+ }
20481
+ child.stdout.on("data", (chunk) => {
20482
+ stdout += chunk.toString("utf-8");
20483
+ options.onOutput?.(maskSecrets(chunk.toString("utf-8"), options.secrets));
20484
+ });
20485
+ child.stderr.on("data", (chunk) => {
20486
+ stderr += chunk.toString("utf-8");
20487
+ });
20488
+ child.on("error", (error62) => {
20489
+ logger3.error("Command failed to start", { command, error: maskSecrets(String(error62), options.secrets) });
20490
+ finish(err(
20491
+ HaiDeployError.REMOTE_COMMAND_FAILED,
20492
+ deployM("deploy_commandFailed", {
20493
+ params: { command, error: maskSecrets(error62.message, options.secrets) }
20494
+ }),
20495
+ error62
20496
+ ));
20497
+ });
20498
+ child.on("close", (code) => {
20499
+ const exitCode = code ?? -1;
20500
+ if (exitCode === 0) {
20501
+ finish(ok({ code: exitCode, stdout, stderr }));
20502
+ return;
20503
+ }
20504
+ logger3.error("Command exited with non-zero code", {
20505
+ command,
20506
+ code: exitCode,
20507
+ stderr: maskSecrets(stderr, options.secrets).slice(-500),
20508
+ stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
20509
+ });
20510
+ finish(err(
20511
+ HaiDeployError.REMOTE_COMMAND_FAILED,
20512
+ deployM("deploy_commandFailed", {
20513
+ params: { command, error: maskSecrets(`${stdout.slice(-2500)}
20514
+ ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
20515
+ })
20516
+ ));
20517
+ });
20518
+ if (options.input !== void 0) {
20519
+ child.stdin.write(options.input);
20520
+ child.stdin.end();
20521
+ } else {
20522
+ child.stdin.end();
20523
+ }
20524
+ });
20525
+ }
20526
+
20527
+ // ../deploy/src/container/deploy-scripts.ts
20528
+ function shellArgument(value) {
20529
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
20530
+ }
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
20544
+ set -eu
20545
+ umask 077
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
20655
+ if [ "$runtime" = auto ]; then
20656
+ if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
20657
+ fi
20658
+ case "$runtime" in docker|podman) ;; *) printf 'Runtime must be docker or podman\n' >&2; exit 1;; esac
20659
+ "$runtime" version >/dev/null
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"
20665
+ sha256sum -c SHA256SUMS
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; }
20747
+ done
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 }
20815
+ `;
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" } })}
20910
+ ${instructions}
20911
+ `);
20912
+ const checksums = [];
20913
+ for (const file2 of files)
20914
+ checksums.push(`${await artifactChecksum(join(staging, file2))} ${file2}`);
20915
+ writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
20916
+ `);
20917
+ const tar = await runCommand("tar", ["-cf", temporaryTar, "-C", staging, "."]);
20918
+ if (!tar.success)
20919
+ throw new Error(tar.error.message);
20920
+ chmodSync(temporaryTar, 384);
20921
+ renameSync(temporaryTar, output2);
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" };
20932
+ } finally {
20933
+ rmSync(staging, { recursive: true, force: true });
20934
+ rmSync(temporaryTar, { force: true });
20935
+ }
20936
+ }
20381
20937
  var IMAGES = {
20382
- postgres: "postgres:16-alpine",
20383
- mysql: "mysql:8.4",
20384
- 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",
20385
20941
  rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
20386
20942
  minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
20387
- qdrant: "qdrant/qdrant:v1.12.4",
20388
- pgvector: "pgvector/pgvector:pg17",
20389
- 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",
20390
20946
  ldap: "docker.io/osixia/openldap:1.5.0",
20391
- smtp: "axllent/mailpit:v1.27.4"
20947
+ smtp: "docker.io/axllent/mailpit:v1.27.4"
20392
20948
  };
20393
20949
  function generatePassword() {
20394
20950
  return randomBytes(24).toString("base64url");
@@ -20397,8 +20953,12 @@ function resolveSidecarSecrets(appDir, projectName) {
20397
20953
  const secretFile = join(appDir, ".hai", "deploy", "runtime", `${projectName}.secrets.json`);
20398
20954
  if (existsSync(secretFile)) {
20399
20955
  try {
20400
- 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;
20401
20960
  } catch {
20961
+ throw new Error(deployM("deploy_secretsInvalid", { params: { path: secretFile } }));
20402
20962
  }
20403
20963
  }
20404
20964
  const secrets = {
@@ -20566,6 +21126,28 @@ function planChromaVecdb(plan, projectName, net) {
20566
21126
  plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
20567
21127
  }
20568
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 };
20569
21151
  const plan = {
20570
21152
  services: [],
20571
21153
  composeServices: {},
@@ -20651,6 +21233,7 @@ function generateComposeFile(options) {
20651
21233
  const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
20652
21234
  const app = {
20653
21235
  image: imageRef,
21236
+ labels: { "org.opencontainers.image.version": options.appVersion },
20654
21237
  container_name: `hai-${projectName}-app`,
20655
21238
  restart: "unless-stopped",
20656
21239
  // 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
@@ -20664,41 +21247,76 @@ function generateComposeFile(options) {
20664
21247
  if (sidecar.dependsOn.length > 0)
20665
21248
  app.depends_on = sidecar.dependsOn;
20666
21249
  const services = { app, ...sidecar.composeServices };
21250
+ if (options.web) {
21251
+ delete app.ports;
21252
+ services.web = {
21253
+ image: options.web.imageRef,
21254
+ labels: { "org.opencontainers.image.version": options.appVersion },
21255
+ container_name: `hai-${projectName}-web`,
21256
+ restart: "unless-stopped",
21257
+ pull_policy: "never",
21258
+ ports: [`${hostPort}:${options.web.containerPort}`],
21259
+ environment: { PUBLIC_API_BASE: "" },
21260
+ healthcheck: { test: ["CMD", "wget", "-Y", "off", "-q", "-O", "/dev/null", `http://127.0.0.1:${options.web.containerPort}/_hai/web-health`], interval: "30s", timeout: "5s", retries: 3 },
21261
+ volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
21262
+ depends_on: ["app"],
21263
+ networks: ["hai-app"]
21264
+ };
21265
+ }
20667
21266
  if (services["storage-init"])
20668
21267
  services["storage-init"] = { ...services["storage-init"], image: imageRef };
20669
21268
  const compose = {
21269
+ "x-hai-version": options.appVersion,
20670
21270
  services,
20671
- networks: {
21271
+ "networks": {
20672
21272
  "hai-app": { name: `hai-${projectName}` }
20673
21273
  }
20674
21274
  };
20675
21275
  if (Object.keys(sidecar.volumes).length > 0)
20676
21276
  compose.volumes = sidecar.volumes;
20677
- if (options.previousCompose && options.serviceModes) {
20678
- const previous = parse$1(options.previousCompose);
20679
- for (const [service, mode] of Object.entries(options.serviceModes)) {
20680
- if (mode !== "keep")
20681
- continue;
20682
- for (const name of [service, `${service}-init`]) {
20683
- if (previous.services?.[name]) {
20684
- services[name] = previous.services[name];
20685
- app.depends_on = [.../* @__PURE__ */ new Set([...app.depends_on ?? [], service])];
20686
- }
20687
- }
20688
- }
20689
- const previousMounts = previous.services?.app?.volumes;
20690
- if (Array.isArray(previousMounts) && Object.values(options.serviceModes).includes("keep"))
20691
- app.volumes = [.../* @__PURE__ */ new Set([...app.volumes ?? [], ...previousMounts])];
20692
- compose.volumes = { ...previous.volumes, ...sidecar.volumes };
20693
- }
20694
21277
  if (services["storage-init"]) {
20695
21278
  app.depends_on = {
20696
21279
  ...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
20697
21280
  "storage-init": { condition: "service_completed_successfully" }
20698
21281
  };
20699
21282
  }
21283
+ for (const service of Object.values(services))
21284
+ service.pull_policy = "never";
20700
21285
  return stringify(compose);
20701
21286
  }
21287
+ function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
21288
+ if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535))
21289
+ throw new Error("Invalid web proxy target");
21290
+ return `server {
21291
+ listen ${webPort};
21292
+ server_name _;
21293
+ root /usr/share/nginx/html;
21294
+ location / {
21295
+ try_files $uri $uri/ /index.html;
21296
+ }
21297
+ location = /runtime-config.js {
21298
+ add_header Cache-Control "no-store";
21299
+ default_type application/javascript;
21300
+ return 200 'globalThis.__HAI_PUBLIC_CONFIG__={apiBase:globalThis.location.origin+"${apiPrefix}"};';
21301
+ }
21302
+ location = /_hai/web-health {
21303
+ access_log off;
21304
+ return 200 'ok';
21305
+ }
21306
+ location = /health {
21307
+ proxy_pass http://app:${port};
21308
+ proxy_set_header Host $http_host;
21309
+ }
21310
+ location ${apiPrefix.replace(/\/$/, "")}/ {
21311
+ proxy_pass http://app:${port};
21312
+ proxy_http_version 1.1;
21313
+ proxy_set_header Host $http_host;
21314
+ proxy_set_header X-Forwarded-Proto $scheme;
21315
+ proxy_buffering off;
21316
+ }
21317
+ }
21318
+ `;
21319
+ }
20702
21320
  function serializeEnvFile(env) {
20703
21321
  return `${Object.entries(env).map(([key, value]) => {
20704
21322
  let literal2 = value;
@@ -20714,194 +21332,7 @@ function serializeEnvFile(env) {
20714
21332
  }).join("\n")}
20715
21333
  `;
20716
21334
  }
20717
- function generateDockerignore() {
20718
- return `**/node_modules
20719
- **/dist
20720
- **/build
20721
- **/.turbo
20722
- **/.svelte-kit
20723
- **/coverage
20724
- **/test-results
20725
- **/playwright-report
20726
- **/.hai/deploy
20727
- **/.env.local
20728
- **/.env.*.local
20729
- **/.env.runtime
20730
- .git
20731
- `;
20732
- }
20733
- function generateFallbackDockerfile(packageName, containerPort, options = {}) {
20734
- if (!/^@?[a-z0-9][\w./-]*$/i.test(packageName) || !/^[a-z0-9][\w-]*$/i.test(options.environment ?? "prod"))
20735
- throw new Error("Invalid container build target");
20736
- const environment = options.environment ?? "prod";
20737
- const coreEnv = environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
20738
- return `# syntax=docker/dockerfile:1.7
20739
- FROM node:24-bookworm-slim AS build
20740
- ARG SQLITE_PREBUILD_MIRROR
20741
- ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
20742
- ENV PNPM_HOME=/pnpm
20743
- ENV PATH=$PNPM_HOME:$PATH
20744
- ENV CI=true
20745
- ENV HAI_DEPLOY_ENVIRONMENT=${environment}
20746
- ENV HAI_CORE_ENV=${coreEnv}
20747
- WORKDIR /workspace
20748
- RUN corepack enable && corepack prepare pnpm@12.11.2 --activate
20749
- COPY . .
20750
- RUN pnpm install --frozen-lockfile --ignore-scripts
20751
- RUN pnpm --filter ${packageName}... build
20752
- RUN if [ -d node_modules/better-sqlite3 ]; then cd node_modules/better-sqlite3; for attempt in 1 2 3; do node -e "require('node:child_process').execFileSync(process.execPath,[require.resolve('prebuild-install/bin.js'),'--verbose'],{stdio:'inherit'})" && break; [ "$attempt" = 3 ] && exit 1; sleep 3; done; fi
20753
- RUN pnpm pm deploy --filter ${packageName} --prod /prod/app
20754
-
20755
- FROM node:24-bookworm-slim AS runtime
20756
- ENV NODE_ENV=production
20757
- ENV HAI_DEPLOY_ENVIRONMENT=${environment}
20758
- ENV HAI_CORE_ENV=${coreEnv}
20759
- ENV HOST=0.0.0.0
20760
- ENV PORT=${containerPort}
20761
- WORKDIR /app
20762
- COPY --from=build --chown=node:node /prod/app ./
20763
- RUN mkdir -p data && chown node:node data
20764
- USER node
20765
- EXPOSE ${containerPort}
20766
- CMD ["node", ${JSON.stringify(options.entrypoint ?? "build")}]
20767
- `;
20768
- }
20769
- var logger4 = core.logger.child({ module: "deploy", scope: "command" });
20770
- function maskSecrets(text, secrets) {
20771
- if (!secrets || secrets.length === 0)
20772
- return text;
20773
- let masked = text;
20774
- for (const secret of secrets) {
20775
- if (secret && secret.length > 0)
20776
- masked = masked.split(secret).join("***");
20777
- }
20778
- return masked;
20779
- }
20780
- function runCommand(command, args, options = {}) {
20781
- return new Promise((resolve3) => {
20782
- const child = spawn(command, args, {
20783
- cwd: options.cwd,
20784
- env: options.env ? { ...process2.env, ...options.env } : process2.env,
20785
- shell: false
20786
- });
20787
- let stdout = "";
20788
- let stderr = "";
20789
- let settled = false;
20790
- let timer;
20791
- const finish = (result) => {
20792
- if (settled)
20793
- return;
20794
- settled = true;
20795
- if (timer)
20796
- clearTimeout(timer);
20797
- resolve3(result);
20798
- };
20799
- if (options.timeoutMs && options.timeoutMs > 0) {
20800
- timer = setTimeout(() => {
20801
- child.kill("SIGKILL");
20802
- logger4.error("Command timed out", { command, timeoutMs: options.timeoutMs });
20803
- finish(err(
20804
- HaiDeployError.REMOTE_COMMAND_FAILED,
20805
- deployM("deploy_commandTimeout", { params: { command } })
20806
- ));
20807
- }, options.timeoutMs);
20808
- }
20809
- child.stdout.on("data", (chunk) => {
20810
- stdout += chunk.toString("utf-8");
20811
- });
20812
- child.stderr.on("data", (chunk) => {
20813
- stderr += chunk.toString("utf-8");
20814
- });
20815
- child.on("error", (error62) => {
20816
- logger4.error("Command failed to start", { command, error: maskSecrets(String(error62), options.secrets) });
20817
- finish(err(
20818
- HaiDeployError.REMOTE_COMMAND_FAILED,
20819
- deployM("deploy_commandFailed", {
20820
- params: { command, error: maskSecrets(error62.message, options.secrets) }
20821
- }),
20822
- error62
20823
- ));
20824
- });
20825
- child.on("close", (code) => {
20826
- const exitCode = code ?? -1;
20827
- if (exitCode === 0) {
20828
- finish(ok({ code: exitCode, stdout, stderr }));
20829
- return;
20830
- }
20831
- logger4.error("Command exited with non-zero code", {
20832
- command,
20833
- code: exitCode,
20834
- stderr: maskSecrets(stderr, options.secrets).slice(-500),
20835
- stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
20836
- });
20837
- finish(err(
20838
- HaiDeployError.REMOTE_COMMAND_FAILED,
20839
- deployM("deploy_commandFailed", {
20840
- params: { command, error: maskSecrets(`${stdout.slice(-2500)}
20841
- ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
20842
- })
20843
- ));
20844
- });
20845
- if (options.input !== void 0) {
20846
- child.stdin.write(options.input);
20847
- child.stdin.end();
20848
- } else {
20849
- child.stdin.end();
20850
- }
20851
- });
20852
- }
20853
-
20854
- // ../deploy/src/container/deploy-container-runtime.ts
20855
- var logger5 = core.logger.child({ module: "deploy", scope: "container-runtime" });
20856
- async function detectLocalRuntime(preferred = "auto") {
20857
- const candidates = preferred === "auto" ? ["podman", "docker"] : [preferred];
20858
- for (const runtime of candidates) {
20859
- const probe = await runCommand(runtime, ["version"], { timeoutMs: 15e3 });
20860
- if (probe.success) {
20861
- logger5.info("Local container runtime detected", { runtime });
20862
- return ok(runtime);
20863
- }
20864
- }
20865
- return err(
20866
- HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND,
20867
- deployM("deploy_containerRuntimeNotFound", { params: { runtime: candidates.join(" / ") } })
20868
- );
20869
- }
20870
- async function buildImage(runtime, options) {
20871
- const args = ["build", "-f", options.dockerfilePath, "-t", options.imageRef];
20872
- if (runtime === "podman")
20873
- args.push("--format", "docker", "--jobs", "1");
20874
- if (runtime === "podman" && existsSync(`${options.dockerfilePath}.dockerignore`))
20875
- args.push("--ignorefile", `${options.dockerfilePath}.dockerignore`);
20876
- if (runtime === "podman" && options.httpProxy === false)
20877
- args.push("--http-proxy=false");
20878
- for (const [key, value] of Object.entries(options.buildArgs ?? {}))
20879
- args.push("--build-arg", `${key}=${value}`);
20880
- args.push(options.contextDir);
20881
- logger5.info("Building container image", { runtime, imageRef: options.imageRef });
20882
- const result = await runCommand(runtime, args, { cwd: options.contextDir });
20883
- if (!result.success) {
20884
- return err(
20885
- HaiDeployError.CONTAINER_BUILD_FAILED,
20886
- deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
20887
- result.error
20888
- );
20889
- }
20890
- return ok(void 0);
20891
- }
20892
- async function saveImage(runtime, imageRef, tarPath) {
20893
- logger5.info("Saving container image", { runtime, imageRef, tarPath });
20894
- const result = await runCommand(runtime, ["save", "-o", tarPath, imageRef], {});
20895
- if (!result.success) {
20896
- return err(
20897
- HaiDeployError.CONTAINER_BUILD_FAILED,
20898
- deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
20899
- result.error
20900
- );
20901
- }
20902
- return ok(void 0);
20903
- }
20904
- var logger6 = core.logger.child({ module: "deploy", scope: "ssh" });
21335
+ var logger4 = core.logger.child({ module: "deploy", scope: "ssh" });
20905
21336
  function buildConnectOptions(config2) {
20906
21337
  const args = ["-o", "BatchMode=yes"];
20907
21338
  args.push("-o", `ConnectTimeout=${config2.connectTimeoutSeconds}`);
@@ -20929,14 +21360,15 @@ function buildScpUploadArgs(config2, localPath, remotePath) {
20929
21360
  "-P",
20930
21361
  String(config2.port),
20931
21362
  localPath,
20932
- `${config2.username}@${config2.host}:${remotePath}`
21363
+ `${config2.username}@${config2.host.includes(":") ? `[${config2.host}]` : config2.host}:${remotePath}`
20933
21364
  ];
20934
21365
  }
20935
21366
  function createSshExecutor(config2) {
20936
21367
  const exec = async (remoteCommand, options) => {
20937
21368
  return runCommand("ssh", buildSshExecArgs(config2, remoteCommand), {
20938
21369
  timeoutMs: options?.timeoutMs,
20939
- secrets: options?.secrets
21370
+ secrets: options?.secrets,
21371
+ onOutput: options?.onOutput
20940
21372
  });
20941
21373
  };
20942
21374
  const upload = async (localPath, remotePath) => {
@@ -20952,7 +21384,7 @@ function createSshExecutor(config2) {
20952
21384
  return ok(void 0);
20953
21385
  };
20954
21386
  const ping = async () => {
20955
- logger6.debug("SSH preflight", { host: config2.host, port: config2.port });
21387
+ logger4.debug("SSH preflight", { host: config2.host, port: config2.port });
20956
21388
  const result = await exec("true", { timeoutMs: 2e4 });
20957
21389
  if (!result.success) {
20958
21390
  return err(
@@ -20967,52 +21399,24 @@ function createSshExecutor(config2) {
20967
21399
  }
20968
21400
 
20969
21401
  // ../deploy/src/providers/deploy-provider-docker-ssh.ts
20970
- var logger7 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
21402
+ var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
20971
21403
  var HEALTH_POLL_INTERVAL = 3e3;
20972
21404
  var HEALTH_MAX_ATTEMPTS = 40;
20973
- function shellArgument(value) {
20974
- return `'${value.replaceAll("'", `'"'"'`)}'`;
20975
- }
20976
21405
  function makeDeploymentId() {
20977
21406
  const now = /* @__PURE__ */ new Date();
20978
21407
  const pad = (n) => String(n).padStart(2, "0");
20979
21408
  const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
20980
21409
  return `${stamp}-${randomBytes(3).toString("hex")}`;
20981
21410
  }
20982
- function parseLoadedImageRef(stdout, fallback) {
20983
- const match = stdout.match(/Loaded image(?:\(s\))?:\s*(\S+)/);
20984
- return match ? match[1] : fallback;
20985
- }
20986
21411
  function sleep(ms) {
20987
- return new Promise((resolve3) => setTimeout(resolve3, ms));
20988
- }
20989
- function readPackageName(appDir) {
20990
- try {
20991
- const pkg = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8"));
20992
- return pkg.name ?? "app";
20993
- } catch {
20994
- return "app";
20995
- }
20996
- }
20997
- function resolveWorkspaceRoot(appDir) {
20998
- if (existsSync(join(appDir, "pnpm-workspace.yaml")))
20999
- return appDir;
21000
- let current = appDir;
21001
- let parent = dirname(current);
21002
- while (parent !== current) {
21003
- if (existsSync(join(parent, "pnpm-workspace.yaml")))
21004
- return parent;
21005
- current = parent;
21006
- parent = dirname(parent);
21007
- }
21008
- return appDir;
21412
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
21009
21413
  }
21010
21414
  function createDockerSshProvider(config2, container) {
21011
- const localRuntimePref = container?.runtime ?? "auto";
21012
- const httpProxy = container?.httpProxy ?? true;
21013
- const ssh = createSshExecutor(config2.ssh);
21415
+ const ssh = config2.ssh ? createSshExecutor(config2.ssh) : void 0;
21014
21416
  const baseDir = config2.remote.baseDir;
21015
21417
  async function detectRemoteRuntime() {
21418
+ if (!ssh)
21419
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
21016
21420
  const candidates = config2.remote.runtime === "auto" ? ["podman", "docker"] : [config2.remote.runtime];
21017
21421
  for (const runtime of candidates) {
21018
21422
  const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
@@ -21020,7 +21424,7 @@ function createDockerSshProvider(config2, container) {
21020
21424
  continue;
21021
21425
  const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
21022
21426
  if (composeResult.success) {
21023
- logger7.info("Remote container runtime detected", { runtime });
21427
+ logger5.info("Remote container runtime detected", { runtime });
21024
21428
  return ok(runtime);
21025
21429
  }
21026
21430
  }
@@ -21044,32 +21448,44 @@ function createDockerSshProvider(config2, container) {
21044
21448
  const deploy2 = async (context) => {
21045
21449
  const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
21046
21450
  const deploymentId = makeDeploymentId();
21047
- const containerPort = config2.expose.containerPort;
21048
21451
  const hostPort = config2.expose.hostPort;
21049
- const localImageTag = `hai-${projectName}:${deploymentId}`;
21050
- const runtimeResult = await detectLocalRuntime(localRuntimePref);
21051
- if (!runtimeResult.success)
21052
- return err(runtimeResult.error);
21053
- const localRuntime = runtimeResult.data;
21054
21452
  const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
21055
- mkdirSync(artifactDir, { recursive: true });
21056
- const appDockerfile = join(appDir, "Dockerfile");
21057
- let dockerfilePath;
21058
- let contextDir;
21059
- if (existsSync(appDockerfile)) {
21060
- dockerfilePath = appDockerfile;
21061
- contextDir = appDir;
21062
- } else {
21063
- dockerfilePath = join(artifactDir, "Dockerfile");
21064
- writeFileSync(dockerfilePath, generateFallbackDockerfile(readPackageName(appDir), containerPort, { environment: options?.environment, entrypoint: scan.isSvelteKit ? "build" : "dist/index.js" }));
21065
- writeFileSync(`${dockerfilePath}.dockerignore`, generateDockerignore());
21066
- contextDir = resolveWorkspaceRoot(appDir);
21067
- logger7.info("Generated fallback Dockerfile", { dockerfilePath, contextDir });
21068
- }
21453
+ mkdirSync(artifactDir, { recursive: true, mode: 448 });
21454
+ chmodSync(artifactDir, 448);
21455
+ const artifact = context.artifact;
21456
+ const resolvedBuildDir = context.artifactDir;
21457
+ const images = artifact.images;
21458
+ const web = images.find((image) => image.name === "web");
21459
+ const app = images.find((image) => image.name === "app");
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}`;
21069
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"));
21475
+ let webProxy;
21476
+ if (hasWeb) {
21477
+ const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
21478
+ if (!server.success)
21479
+ return server;
21480
+ const http = server.data.http;
21481
+ const cors = server.data.cors;
21482
+ const apiPrefix = http?.apiPrefix ?? "/api/v1";
21483
+ if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
21484
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
21485
+ webProxy = generateWebProxyConfig(containerPort, apiPrefix, webContainerPort);
21486
+ env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config2.ssh ? url2 : cors?.origin ?? "*" } });
21487
+ }
21070
21488
  const envPath = join(artifactDir, ".env.runtime");
21071
- const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
21072
- Object.assign(env, { ...previousEnv, ...env });
21073
21489
  writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
21074
21490
  if (options?.environment) {
21075
21491
  const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
@@ -21079,88 +21495,83 @@ function createDockerSshProvider(config2, container) {
21079
21495
  if (!saved.success)
21080
21496
  return saved;
21081
21497
  }
21082
- const buildResult = await buildImage(localRuntime, {
21083
- contextDir,
21084
- dockerfilePath,
21085
- imageRef: localImageTag,
21086
- buildArgs: { HAI_DEPLOY_ENVIRONMENT: options?.environment ?? "prod", HAI_CORE_ENV: env.HAI_CORE_ENV ?? "production", SQLITE_PREBUILD_MIRROR: container?.sqlitePrebuildMirror ?? "" },
21087
- httpProxy
21088
- });
21089
- if (!buildResult.success)
21090
- return err(buildResult.error);
21091
- const tarPath = join(artifactDir, `${projectName}.tar`);
21092
- const saveResult = await saveImage(localRuntime, localImageTag, tarPath);
21093
- if (!saveResult.success)
21094
- return err(saveResult.error);
21095
- const pingResult = await ssh.ping();
21096
- if (!pingResult.success)
21097
- return err(pingResult.error);
21098
- const remoteRuntimeResult = await detectRemoteRuntime();
21099
- if (!remoteRuntimeResult.success)
21100
- return err(remoteRuntimeResult.error);
21101
- const remoteRuntime = remoteRuntimeResult.data;
21102
- const remoteDir = `${baseDir}/${projectName}`;
21103
- const prepareResult = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
21104
- if (!prepareResult.success)
21105
- return err(prepareResult.error);
21106
- const remoteTar = `${remoteDir}/image.tar`;
21107
- const uploadTarResult = await ssh.upload(tarPath, remoteTar);
21108
- if (!uploadTarResult.success)
21109
- return err(uploadTarResult.error);
21110
- const loadResult = await ssh.exec(`${remoteRuntime} load -i ${shellArgument(remoteTar)}`, { timeoutMs: 3e5 });
21111
- if (!loadResult.success)
21112
- return err(loadResult.error);
21113
- const loadedRef = parseLoadedImageRef(loadResult.data.stdout, localImageTag);
21114
- const remoteImageRef = remoteRuntime === "podman" ? `localhost/${localImageTag}` : localImageTag;
21115
- if (loadedRef !== remoteImageRef) {
21116
- const tagResult = await ssh.exec(`${remoteRuntime} tag ${shellArgument(loadedRef)} ${shellArgument(remoteImageRef)}`);
21117
- if (!tagResult.success)
21118
- return err(tagResult.error);
21119
- }
21498
+ const composeName = `docker-compose-${artifact.appVersion}.yml`;
21120
21499
  const composeContent = generateComposeFile({
21121
21500
  projectName,
21122
- imageRef: remoteImageRef,
21501
+ appVersion: artifact.appVersion,
21502
+ imageRef: app?.imageRef ?? "localhost/hai-unselected:0",
21123
21503
  hostPort,
21124
21504
  containerPort,
21125
21505
  sidecar,
21126
- serviceModes: options?.serviceModes,
21127
- previousCompose: existsSync(join(artifactDir, "compose.yml")) ? readFileSync(join(artifactDir, "compose.yml"), "utf8") : void 0
21506
+ web: hasWeb ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0
21128
21507
  });
21129
- const composePath = join(artifactDir, "compose.yml");
21508
+ const composePath = join(artifactDir, composeName);
21130
21509
  writeFileSync(composePath, composeContent);
21131
- const uploadComposeResult = await ssh.upload(composePath, `${remoteDir}/compose.yml`);
21132
- if (!uploadComposeResult.success)
21133
- return err(uploadComposeResult.error);
21134
- const uploadEnvResult = await ssh.upload(envPath, `${remoteDir}/.env.runtime`);
21135
- if (!uploadEnvResult.success)
21136
- return err(uploadEnvResult.error);
21137
- const startScript = `#!/bin/sh
21138
- set -eu
21139
- cd "$(dirname "$0")"
21140
- export HAI_DEPLOY_ENVIRONMENT=${options?.environment ?? "prod"}
21141
- export HAI_CORE_ENV=${shellArgument(env.HAI_CORE_ENV ?? "production")}
21142
- exec ${remoteRuntime} compose --env-file .env.runtime -p hai-${projectName} up -d
21143
- `;
21144
- const startName = `start-${options?.environment ?? "prod"}.sh`;
21145
- const startPath = join(artifactDir, startName);
21146
- writeFileSync(startPath, startScript, { mode: 448 });
21147
- const uploadStart = await ssh.upload(startPath, `${remoteDir}/${startName}`);
21148
- if (!uploadStart.success)
21149
- return uploadStart;
21150
- const secretMask = sidecar.secrets;
21151
- const chmodResult = await ssh.exec(`chmod 600 -- ${shellArgument(`${remoteDir}/.env.runtime`)}`, { secrets: secretMask });
21152
- if (!chmodResult.success)
21153
- return err(chmodResult.error);
21154
- const upResult = await ssh.exec(
21155
- `cd ${shellArgument(remoteDir)} && sh ${shellArgument(startName)}`,
21156
- { timeoutMs: 3e5, secrets: secretMask }
21510
+ if (webProxy)
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");
21522
+ const bundle = await createDeploymentBundle({
21523
+ directory: artifactDir,
21524
+ artifactDir: resolvedBuildDir,
21525
+ artifact,
21526
+ projectName,
21527
+ environment: options?.environment ?? "prod",
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"],
21537
+ output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
21538
+ });
21539
+ if (options?.bundleOnly)
21540
+ return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
21541
+ if (!ssh)
21542
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
21543
+ const ping = await ssh.ping();
21544
+ if (!ping.success)
21545
+ return ping;
21546
+ const remoteRuntime = await detectRemoteRuntime();
21547
+ if (!remoteRuntime.success)
21548
+ return remoteRuntime;
21549
+ const remoteDir = `${baseDir}/${projectName}`;
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`)}`);
21552
+ if (!prepared.success)
21553
+ return prepared;
21554
+ const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
21555
+ if (!uploaded.success)
21556
+ return uploaded;
21557
+ let pendingOutput = "";
21558
+ const started = await ssh.exec(
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
+ } }
21157
21570
  );
21158
- if (!upResult.success)
21159
- return err(upResult.error);
21160
- await ssh.exec(`rm -f -- ${shellArgument(remoteTar)}`);
21161
- const url2 = `http://${config2.ssh.host}:${hostPort}`;
21162
- logger7.info("Running health check", { url: url2 });
21163
- const healthy = await healthCheck(`${url2}${scan.isSvelteKit ? "/" : "/health"}`);
21571
+ if (!started.success)
21572
+ return started;
21573
+ logger5.info("Running health check", { url: url2 });
21574
+ const healthy = await healthCheck(`${url2}${!hasWeb && scan.isSvelteKit ? "/" : "/health"}`) && (!hasWeb || await healthCheck(`${url2}/_hai/web-health`) && await healthCheck(`${url2}/`));
21164
21575
  if (!healthy) {
21165
21576
  return err(
21166
21577
  HaiDeployError.HEALTH_CHECK_FAILED,
@@ -21173,14 +21584,24 @@ exec ${remoteRuntime} compose --env-file .env.runtime -p hai-${projectName} up -
21173
21584
  status: "ready",
21174
21585
  provider: "docker-ssh",
21175
21586
  healthCheck: "passed",
21176
- envVarsSet: Object.keys(env)
21587
+ envVarsSet: Object.keys(env),
21588
+ bundlePath: bundle.bundlePath
21177
21589
  };
21178
- logger7.info("docker-ssh deployment complete", { url: url2, deploymentId });
21590
+ logger5.info("docker-ssh deployment complete", { url: url2, deploymentId });
21179
21591
  return ok(result);
21180
21592
  };
21181
- return { name: "docker-ssh", deploy: deploy2 };
21593
+ const deploySafely = async (context) => {
21594
+ try {
21595
+ return await deploy2(context);
21596
+ } catch (error62) {
21597
+ logger5.error("Deployment bundle failed", { error: 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);
21600
+ }
21601
+ };
21602
+ return { name: "docker-ssh", deploy: deploySafely };
21182
21603
  }
21183
- var logger8 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
21604
+ var logger6 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
21184
21605
  var VERCEL_API = "https://api.vercel.com";
21185
21606
  var POLL_INTERVAL = 3e3;
21186
21607
  var MAX_POLL_ATTEMPTS = 60;
@@ -21233,7 +21654,7 @@ function createVercelProvider() {
21233
21654
  return {
21234
21655
  name: "vercel",
21235
21656
  async authenticate(apiToken) {
21236
- logger8.debug("Authenticating with Vercel");
21657
+ logger6.debug("Authenticating with Vercel");
21237
21658
  try {
21238
21659
  const user = await vercelFetch(
21239
21660
  apiToken,
@@ -21241,10 +21662,10 @@ function createVercelProvider() {
21241
21662
  );
21242
21663
  token = apiToken;
21243
21664
  const username = user.user.username || user.user.email;
21244
- logger8.info("Vercel authenticated", { user: username });
21665
+ logger6.info("Vercel authenticated", { user: username });
21245
21666
  return ok(username);
21246
21667
  } catch (error62) {
21247
- logger8.error("Vercel authentication failed", { error: error62 });
21668
+ logger6.error("Vercel authentication failed", { error: error62 });
21248
21669
  return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error62));
21249
21670
  }
21250
21671
  },
@@ -21255,11 +21676,11 @@ function createVercelProvider() {
21255
21676
  deployM("deploy_authRequired")
21256
21677
  );
21257
21678
  }
21258
- logger8.debug("Creating Vercel project", { projectName });
21679
+ logger6.debug("Creating Vercel project", { projectName });
21259
21680
  try {
21260
21681
  const existingId = await findExistingProject(token, projectName);
21261
21682
  if (existingId !== null) {
21262
- logger8.info("Vercel project found", { projectName, projectId: existingId });
21683
+ logger6.info("Vercel project found", { projectName, projectId: existingId });
21263
21684
  return ok(existingId);
21264
21685
  }
21265
21686
  try {
@@ -21274,13 +21695,13 @@ function createVercelProvider() {
21274
21695
  })
21275
21696
  }
21276
21697
  );
21277
- logger8.info("Vercel project created", { projectName, projectId: project.id });
21698
+ logger6.info("Vercel project created", { projectName, projectId: project.id });
21278
21699
  return ok(project.id);
21279
21700
  } catch (error62) {
21280
21701
  if (error62 instanceof VercelApiError && error62.status === 409) {
21281
21702
  const concurrentProjectId = await findExistingProject(token, projectName);
21282
21703
  if (concurrentProjectId !== null) {
21283
- logger8.info("Vercel project reused after create conflict", {
21704
+ logger6.info("Vercel project reused after create conflict", {
21284
21705
  projectName,
21285
21706
  projectId: concurrentProjectId
21286
21707
  });
@@ -21290,7 +21711,7 @@ function createVercelProvider() {
21290
21711
  throw error62;
21291
21712
  }
21292
21713
  } catch (error62) {
21293
- logger8.error("Vercel project creation failed", { projectName, error: error62 });
21714
+ logger6.error("Vercel project creation failed", { projectName, error: error62 });
21294
21715
  return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error62));
21295
21716
  }
21296
21717
  },
@@ -21301,7 +21722,7 @@ function createVercelProvider() {
21301
21722
  deployM("deploy_authRequired")
21302
21723
  );
21303
21724
  }
21304
- logger8.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
21725
+ logger6.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
21305
21726
  try {
21306
21727
  const envEntries = Object.entries(envVars).map(([key, value]) => ({
21307
21728
  key,
@@ -21317,10 +21738,10 @@ function createVercelProvider() {
21317
21738
  body: JSON.stringify(envEntries)
21318
21739
  }
21319
21740
  );
21320
- logger8.info("Vercel env vars set", { projectId, count: envEntries.length });
21741
+ logger6.info("Vercel env vars set", { projectId, count: envEntries.length });
21321
21742
  return ok(void 0);
21322
21743
  } catch (error62) {
21323
- logger8.error("Vercel env vars setup failed", { projectId, error: error62 });
21744
+ logger6.error("Vercel env vars setup failed", { projectId, error: error62 });
21324
21745
  return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error62));
21325
21746
  }
21326
21747
  },
@@ -21331,8 +21752,11 @@ function createVercelProvider() {
21331
21752
  deployM("deploy_authRequired")
21332
21753
  );
21333
21754
  }
21334
- logger8.debug("Deploying to Vercel", { projectId, outputDir });
21755
+ logger6.debug("Deploying to Vercel", { projectId, outputDir });
21335
21756
  try {
21757
+ const outputConfig = JSON.parse(readFileSync(join(outputDir, "config.json"), "utf8"));
21758
+ if (outputConfig.version !== 3)
21759
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
21336
21760
  const filePaths = collectFiles(outputDir, outputDir);
21337
21761
  const files = [];
21338
21762
  for (const filePath of filePaths) {
@@ -21341,18 +21765,20 @@ function createVercelProvider() {
21341
21765
  const sha = createHash("sha1").update(content).digest("hex");
21342
21766
  const size = statSync(fullPath).size;
21343
21767
  await uploadFile(token, content, sha);
21344
- files.push({ file: filePath, sha, size });
21768
+ files.push({ file: `.vercel/output/${filePath}`, sha, size });
21345
21769
  }
21346
- logger8.debug("Files uploaded", { count: files.length });
21770
+ logger6.debug("Files uploaded", { count: files.length });
21347
21771
  const deployment = await vercelFetch(
21348
21772
  token,
21349
- "/v13/deployments",
21773
+ "/v13/deployments?prebuilt=1",
21350
21774
  {
21351
21775
  method: "POST",
21352
21776
  body: JSON.stringify({
21353
21777
  name: projectId,
21354
21778
  files,
21355
- projectSettings: { framework: "sveltekit" }
21779
+ version: 2,
21780
+ project: projectId,
21781
+ projectSettings: { framework: null, buildCommand: "", installCommand: "" }
21356
21782
  })
21357
21783
  }
21358
21784
  );
@@ -21363,14 +21789,14 @@ function createVercelProvider() {
21363
21789
  status: finalStatus,
21364
21790
  envVarsSet: []
21365
21791
  };
21366
- logger8.info("Vercel deployment complete", {
21792
+ logger6.info("Vercel deployment complete", {
21367
21793
  deploymentId: deployment.id,
21368
21794
  url: result.url,
21369
21795
  status: finalStatus
21370
21796
  });
21371
21797
  return ok(result);
21372
21798
  } catch (error62) {
21373
- logger8.error("Vercel deployment failed", { projectId, error: error62 });
21799
+ logger6.error("Vercel deployment failed", { projectId, error: error62 });
21374
21800
  return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error62));
21375
21801
  }
21376
21802
  }
@@ -21415,11 +21841,11 @@ async function pollDeploymentStatus(token, deploymentId) {
21415
21841
  if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
21416
21842
  return "error";
21417
21843
  }
21418
- await new Promise((resolve3) => setTimeout(resolve3, POLL_INTERVAL));
21844
+ await new Promise((resolve6) => setTimeout(resolve6, POLL_INTERVAL));
21419
21845
  }
21420
21846
  return "building";
21421
21847
  }
21422
- var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
21848
+ var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
21423
21849
  function createAliyunProvisioner() {
21424
21850
  let accessKey = null;
21425
21851
  let secretKey = null;
@@ -21428,7 +21854,7 @@ function createAliyunProvisioner() {
21428
21854
  name: "aliyun",
21429
21855
  serviceType: "sms",
21430
21856
  async authenticate(credentials) {
21431
- logger9.debug("Authenticating with Aliyun");
21857
+ logger7.debug("Authenticating with Aliyun");
21432
21858
  try {
21433
21859
  const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
21434
21860
  const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
@@ -21438,10 +21864,10 @@ function createAliyunProvisioner() {
21438
21864
  accessKey = akId;
21439
21865
  secretKey = akSecret;
21440
21866
  signName = credentials.signName;
21441
- logger9.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
21867
+ logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
21442
21868
  return ok(akId);
21443
21869
  } catch (error62) {
21444
- logger9.error("Aliyun authentication failed", { error: error62 });
21870
+ logger7.error("Aliyun authentication failed", { error: error62 });
21445
21871
  return err(
21446
21872
  HaiDeployError.AUTH_FAILED,
21447
21873
  deployM("deploy_authFailed", {
@@ -21458,7 +21884,7 @@ function createAliyunProvisioner() {
21458
21884
  deployM("deploy_authRequired")
21459
21885
  );
21460
21886
  }
21461
- logger9.debug("Provisioning Aliyun SMS service");
21887
+ logger7.debug("Provisioning Aliyun SMS service");
21462
21888
  return ok({
21463
21889
  serviceType: "sms",
21464
21890
  provisionerName: "aliyun",
@@ -21471,7 +21897,7 @@ function createAliyunProvisioner() {
21471
21897
  }
21472
21898
  };
21473
21899
  }
21474
- var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
21900
+ var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
21475
21901
  var NEON_API = "https://console.neon.tech/api/v2";
21476
21902
  var DEFAULT_NEON_DATABASE_NAME = "neondb";
21477
21903
  var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
@@ -21530,7 +21956,7 @@ function createNeonProvisioner() {
21530
21956
  name: "neon",
21531
21957
  serviceType: "db",
21532
21958
  async authenticate(credentials) {
21533
- logger10.debug("Authenticating with Neon");
21959
+ logger8.debug("Authenticating with Neon");
21534
21960
  try {
21535
21961
  const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
21536
21962
  if (!apiToken) {
@@ -21538,10 +21964,10 @@ function createNeonProvisioner() {
21538
21964
  }
21539
21965
  await listNeonProjects(apiToken);
21540
21966
  token = apiToken;
21541
- logger10.info("Neon authenticated");
21967
+ logger8.info("Neon authenticated");
21542
21968
  return ok("neon-user");
21543
21969
  } catch (error62) {
21544
- logger10.error("Neon authentication failed", { error: error62 });
21970
+ logger8.error("Neon authentication failed", { error: error62 });
21545
21971
  return err(
21546
21972
  HaiDeployError.AUTH_FAILED,
21547
21973
  deployM("deploy_authFailed", {
@@ -21558,7 +21984,7 @@ function createNeonProvisioner() {
21558
21984
  deployM("deploy_authRequired")
21559
21985
  );
21560
21986
  }
21561
- logger10.debug("Provisioning Neon database", { appName });
21987
+ logger8.debug("Provisioning Neon database", { appName });
21562
21988
  let resource;
21563
21989
  try {
21564
21990
  const projectName = `${appName}-db`;
@@ -21566,7 +21992,7 @@ function createNeonProvisioner() {
21566
21992
  if (existingProject !== null) {
21567
21993
  resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
21568
21994
  const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
21569
- logger10.info("Neon project reused", {
21995
+ logger8.info("Neon project reused", {
21570
21996
  projectId: existingProject.id,
21571
21997
  projectName
21572
21998
  });
@@ -21588,10 +22014,10 @@ function createNeonProvisioner() {
21588
22014
  if (!projectId || !connectionUri) {
21589
22015
  throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
21590
22016
  }
21591
- logger10.info("Neon database provisioned", { projectId });
22017
+ logger8.info("Neon database provisioned", { projectId });
21592
22018
  return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
21593
22019
  } catch (error62) {
21594
- logger10.error("Neon provisioning failed", { appName });
22020
+ logger8.error("Neon provisioning failed", { appName });
21595
22021
  if (resource) {
21596
22022
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
21597
22023
  projectName: appName,
@@ -21613,7 +22039,7 @@ function createNeonProvisioner() {
21613
22039
  }
21614
22040
  };
21615
22041
  }
21616
- var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
22042
+ var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
21617
22043
  var CF_API = "https://api.cloudflare.com/client/v4";
21618
22044
  function createR2Provisioner() {
21619
22045
  let token = null;
@@ -21624,7 +22050,7 @@ function createR2Provisioner() {
21624
22050
  name: "r2",
21625
22051
  serviceType: "storage",
21626
22052
  async authenticate(credentials) {
21627
- logger11.debug("Authenticating with Cloudflare");
22053
+ logger9.debug("Authenticating with Cloudflare");
21628
22054
  try {
21629
22055
  const acctId = credentials.accountId ?? credentials.account_id ?? "";
21630
22056
  const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
@@ -21641,10 +22067,10 @@ function createR2Provisioner() {
21641
22067
  token = apiTok;
21642
22068
  accessKey = credentials.accessKeyId;
21643
22069
  secretKey = credentials.secretAccessKey;
21644
- logger11.info("Cloudflare R2 authenticated", { accountId: acctId });
22070
+ logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
21645
22071
  return ok(acctId);
21646
22072
  } catch (error62) {
21647
- logger11.error("Cloudflare R2 authentication failed", { error: error62 });
22073
+ logger9.error("Cloudflare R2 authentication failed", { error: error62 });
21648
22074
  return err(
21649
22075
  HaiDeployError.AUTH_FAILED,
21650
22076
  deployM("deploy_authFailed", {
@@ -21662,7 +22088,7 @@ function createR2Provisioner() {
21662
22088
  );
21663
22089
  }
21664
22090
  const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
21665
- logger11.debug("Provisioning R2 bucket", { appName, bucketName });
22091
+ logger9.debug("Provisioning R2 bucket", { appName, bucketName });
21666
22092
  try {
21667
22093
  const createRes = await fetch(
21668
22094
  `${CF_API}/accounts/${accountId}/r2/buckets`,
@@ -21678,7 +22104,7 @@ function createR2Provisioner() {
21678
22104
  if (!createRes.ok && createRes.status !== 409) {
21679
22105
  throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
21680
22106
  }
21681
- logger11.info("R2 bucket provisioned", { bucketName });
22107
+ logger9.info("R2 bucket provisioned", { bucketName });
21682
22108
  return ok({
21683
22109
  serviceType: "storage",
21684
22110
  provisionerName: "r2",
@@ -21696,7 +22122,7 @@ function createR2Provisioner() {
21696
22122
  resourceInfo: `r2-bucket:${bucketName}`
21697
22123
  });
21698
22124
  } catch (error62) {
21699
- logger11.error("R2 provisioning failed", { appName, error: error62 });
22125
+ logger9.error("R2 provisioning failed", { appName, error: error62 });
21700
22126
  return err(
21701
22127
  HaiDeployError.PROVISION_FAILED,
21702
22128
  deployM("deploy_provisionFailed", {
@@ -21711,7 +22137,7 @@ function createR2Provisioner() {
21711
22137
  }
21712
22138
  };
21713
22139
  }
21714
- var logger12 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
22140
+ var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
21715
22141
  var RESEND_API = "https://api.resend.com";
21716
22142
  function createResendProvisioner() {
21717
22143
  let token = null;
@@ -21720,7 +22146,7 @@ function createResendProvisioner() {
21720
22146
  name: "resend",
21721
22147
  serviceType: "email",
21722
22148
  async authenticate(credentials) {
21723
- logger12.debug("Authenticating with Resend");
22149
+ logger10.debug("Authenticating with Resend");
21724
22150
  try {
21725
22151
  const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
21726
22152
  if (!apiToken || !credentials.from) {
@@ -21734,10 +22160,10 @@ function createResendProvisioner() {
21734
22160
  }
21735
22161
  token = apiToken;
21736
22162
  from = credentials.from;
21737
- logger12.info("Resend authenticated");
22163
+ logger10.info("Resend authenticated");
21738
22164
  return ok("resend-user");
21739
22165
  } catch (error62) {
21740
- logger12.error("Resend authentication failed", { error: error62 });
22166
+ logger10.error("Resend authentication failed", { error: error62 });
21741
22167
  return err(
21742
22168
  HaiDeployError.AUTH_FAILED,
21743
22169
  deployM("deploy_authFailed", {
@@ -21754,7 +22180,7 @@ function createResendProvisioner() {
21754
22180
  deployM("deploy_authRequired")
21755
22181
  );
21756
22182
  }
21757
- logger12.debug("Provisioning Resend email service");
22183
+ logger10.debug("Provisioning Resend email service");
21758
22184
  return ok({
21759
22185
  serviceType: "email",
21760
22186
  provisionerName: "resend",
@@ -21767,7 +22193,7 @@ function createResendProvisioner() {
21767
22193
  }
21768
22194
  };
21769
22195
  }
21770
- var logger13 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
22196
+ var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
21771
22197
  var UPSTASH_API = "https://api.upstash.com";
21772
22198
  function createUpstashAuthHeader(email3, apiKey) {
21773
22199
  return `Basic ${Buffer.from(`${email3}:${apiKey}`).toString("base64")}`;
@@ -21818,7 +22244,7 @@ function createUpstashProvisioner() {
21818
22244
  name: "upstash",
21819
22245
  serviceType: "cache",
21820
22246
  async authenticate(credentials) {
21821
- logger13.debug("Authenticating with Upstash");
22247
+ logger11.debug("Authenticating with Upstash");
21822
22248
  try {
21823
22249
  const userEmail = credentials.email ?? "";
21824
22250
  const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
@@ -21828,10 +22254,10 @@ function createUpstashProvisioner() {
21828
22254
  await listUpstashDatabases(userEmail, userKey);
21829
22255
  email3 = userEmail;
21830
22256
  apiKey = userKey;
21831
- logger13.info("Upstash authenticated", { email: userEmail });
22257
+ logger11.info("Upstash authenticated", { email: userEmail });
21832
22258
  return ok(userEmail);
21833
22259
  } catch (error62) {
21834
- logger13.error("Upstash authentication failed", { error: error62 });
22260
+ logger11.error("Upstash authentication failed", { error: error62 });
21835
22261
  return err(
21836
22262
  HaiDeployError.AUTH_FAILED,
21837
22263
  deployM("deploy_authFailed", {
@@ -21848,7 +22274,7 @@ function createUpstashProvisioner() {
21848
22274
  deployM("deploy_authRequired")
21849
22275
  );
21850
22276
  }
21851
- logger13.debug("Provisioning Upstash Redis", { appName });
22277
+ logger11.debug("Provisioning Upstash Redis", { appName });
21852
22278
  let resource;
21853
22279
  try {
21854
22280
  const databaseName = `${appName}-cache`;
@@ -21860,7 +22286,7 @@ function createUpstashProvisioner() {
21860
22286
  }
21861
22287
  resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
21862
22288
  const detail = await getUpstashDatabase(email3, apiKey, databaseId);
21863
- logger13.info("Upstash Redis reused", { databaseId, databaseName });
22289
+ logger11.info("Upstash Redis reused", { databaseId, databaseName });
21864
22290
  return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
21865
22291
  }
21866
22292
  const data = await upstashFetch(email3, apiKey, "/v2/redis/database", {
@@ -21873,10 +22299,10 @@ function createUpstashProvisioner() {
21873
22299
  });
21874
22300
  if (data.database_id)
21875
22301
  resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
21876
- logger13.info("Upstash Redis provisioned", { databaseId: data.database_id });
22302
+ logger11.info("Upstash Redis provisioned", { databaseId: data.database_id });
21877
22303
  return ok(buildUpstashProvisionResult(data, "created"));
21878
22304
  } catch (error62) {
21879
- logger13.error("Upstash provisioning failed", { appName });
22305
+ logger11.error("Upstash provisioning failed", { appName });
21880
22306
  if (resource) {
21881
22307
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
21882
22308
  projectName: appName,
@@ -21900,7 +22326,7 @@ function createUpstashProvisioner() {
21900
22326
  }
21901
22327
 
21902
22328
  // ../deploy/src/deploy-main.ts
21903
- var logger14 = core.logger.child({ module: "deploy", scope: "main" });
22329
+ var logger12 = core.logger.child({ module: "deploy", scope: "main" });
21904
22330
  var currentConfig = null;
21905
22331
  var currentProvider = null;
21906
22332
  var currentContainerProvider = null;
@@ -21985,7 +22411,7 @@ var deploy = {
21985
22411
  credentials: deployCredentials,
21986
22412
  async init(config2) {
21987
22413
  if (initInProgress) {
21988
- logger14.warn("Deploy init already in progress, skipping concurrent call");
22414
+ logger12.warn("Deploy init already in progress, skipping concurrent call");
21989
22415
  return err(
21990
22416
  HaiDeployError.CONFIG_ERROR,
21991
22417
  deployM("deploy_configError", { params: { error: "init already in progress" } })
@@ -21994,13 +22420,13 @@ var deploy = {
21994
22420
  initInProgress = true;
21995
22421
  try {
21996
22422
  if (currentProvider || currentContainerProvider) {
21997
- logger14.warn("Deploy module is already initialized, reinitializing");
22423
+ logger12.warn("Deploy module is already initialized, reinitializing");
21998
22424
  await deploy.close();
21999
22425
  }
22000
- logger14.info("Initializing deploy module");
22426
+ logger12.info("Initializing deploy module");
22001
22427
  const parseResult = DeployConfigSchema.safeParse(config2);
22002
22428
  if (!parseResult.success) {
22003
- logger14.error("Deploy config validation failed", { error: parseResult.error.message });
22429
+ logger12.error("Deploy config validation failed", { error: parseResult.error.message });
22004
22430
  return err(
22005
22431
  HaiDeployError.CONFIG_ERROR,
22006
22432
  deployM("deploy_configError", { params: { error: parseResult.error.message } }),
@@ -22013,14 +22439,14 @@ var deploy = {
22013
22439
  let containerProvider = null;
22014
22440
  if (parsed.provider.type === "docker-ssh") {
22015
22441
  containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
22016
- logger14.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh.host });
22442
+ logger12.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh?.host });
22017
22443
  } else {
22018
22444
  provider = createProviderByType(parsed.provider.type);
22019
22445
  const authResult = await provider.authenticate(parsed.provider.token);
22020
22446
  if (!authResult.success) {
22021
22447
  return err(authResult.error);
22022
22448
  }
22023
- logger14.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
22449
+ logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
22024
22450
  }
22025
22451
  currentProvisioners.clear();
22026
22452
  if (parsed.services) {
@@ -22032,14 +22458,14 @@ var deploy = {
22032
22458
  const credentials = extractCredentials(serviceConfig);
22033
22459
  const provAuthResult = await provisioner.authenticate(credentials);
22034
22460
  if (!provAuthResult.success) {
22035
- logger14.error("Provisioner authentication failed", {
22461
+ logger12.error("Provisioner authentication failed", {
22036
22462
  service: serviceType,
22037
22463
  provisioner: serviceConfig.provisioner
22038
22464
  });
22039
22465
  currentProvisioners.clear();
22040
22466
  return err(provAuthResult.error);
22041
22467
  }
22042
- logger14.info("Provisioner authenticated", {
22468
+ logger12.info("Provisioner authenticated", {
22043
22469
  service: serviceType,
22044
22470
  user: provAuthResult.data
22045
22471
  });
@@ -22049,7 +22475,7 @@ var deploy = {
22049
22475
  currentProvider = provider;
22050
22476
  currentContainerProvider = containerProvider;
22051
22477
  currentConfig = parsed;
22052
- logger14.info("Deploy module initialized", {
22478
+ logger12.info("Deploy module initialized", {
22053
22479
  provider: parsed.provider.type,
22054
22480
  provisioners: [...currentProvisioners.keys()]
22055
22481
  });
@@ -22059,7 +22485,7 @@ var deploy = {
22059
22485
  currentContainerProvider = null;
22060
22486
  currentConfig = null;
22061
22487
  currentProvisioners.clear();
22062
- logger14.error("Deploy module initialization failed", { error: error62 });
22488
+ logger12.error("Deploy module initialization failed", { error: error62 });
22063
22489
  return err(
22064
22490
  HaiDeployError.CONFIG_ERROR,
22065
22491
  deployM("deploy_configError", {
@@ -22074,15 +22500,15 @@ var deploy = {
22074
22500
  },
22075
22501
  async close() {
22076
22502
  if (!currentProvider && !currentContainerProvider) {
22077
- logger14.info("Deploy module already closed, skipping");
22503
+ logger12.info("Deploy module already closed, skipping");
22078
22504
  return;
22079
22505
  }
22080
- logger14.info("Closing deploy module");
22506
+ logger12.info("Closing deploy module");
22081
22507
  currentProvider = null;
22082
22508
  currentContainerProvider = null;
22083
22509
  currentConfig = null;
22084
22510
  currentProvisioners.clear();
22085
- logger14.info("Deploy module closed");
22511
+ logger12.info("Deploy module closed");
22086
22512
  },
22087
22513
  get config() {
22088
22514
  return currentConfig;
@@ -22091,22 +22517,22 @@ var deploy = {
22091
22517
  return currentProvider !== null || currentContainerProvider !== null;
22092
22518
  },
22093
22519
  async scan(appDir, options) {
22094
- logger14.debug("Scanning application", { appDir });
22520
+ logger12.debug("Scanning application", { appDir });
22095
22521
  return scanApp(appDir, options);
22096
22522
  },
22097
22523
  async provisionAll(projectName, services) {
22098
22524
  if (!currentProvider && !currentContainerProvider) {
22099
22525
  return notInitialized.result();
22100
22526
  }
22101
- logger14.info("Provisioning all services", { projectName, count: currentProvisioners.size });
22527
+ logger12.info("Provisioning all services", { projectName, count: currentProvisioners.size });
22102
22528
  const results = [];
22103
22529
  for (const [serviceType, provisioner] of currentProvisioners) {
22104
22530
  if (services && !services.includes(serviceType))
22105
22531
  continue;
22106
- logger14.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
22532
+ logger12.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
22107
22533
  const provResult = await provisioner.provision(projectName);
22108
22534
  if (!provResult.success) {
22109
- logger14.error("Service provisioning failed", {
22535
+ logger12.error("Service provisioning failed", {
22110
22536
  serviceType,
22111
22537
  provisioner: provisioner.name,
22112
22538
  code: provResult.error.code
@@ -22123,7 +22549,7 @@ var deploy = {
22123
22549
  });
22124
22550
  }
22125
22551
  results.push(provResult.data);
22126
- logger14.info("Service provisioned", { serviceType, provisioner: provisioner.name });
22552
+ logger12.info("Service provisioned", { serviceType, provisioner: provisioner.name });
22127
22553
  }
22128
22554
  return ok(results);
22129
22555
  },
@@ -22131,7 +22557,7 @@ var deploy = {
22131
22557
  if (!currentProvider && !currentContainerProvider) {
22132
22558
  return notInitialized.result();
22133
22559
  }
22134
- logger14.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
22560
+ logger12.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
22135
22561
  const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
22136
22562
  const scanResult = await scanApp(appDir, scanOptions);
22137
22563
  if (!scanResult.success) {
@@ -22147,11 +22573,21 @@ var deploy = {
22147
22573
  options = {
22148
22574
  ...options,
22149
22575
  ...scanOptions.environment ? { environment: environment.data.environment } : {},
22150
- ...options?.skipProvision ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
22576
+ ...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
22151
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
+ }
22152
22584
  if (currentContainerProvider) {
22153
22585
  return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
22154
22586
  }
22587
+ if (options?.bundleOnly)
22588
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
22589
+ if (options.selectedServices)
22590
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectedContainerOnly"));
22155
22591
  if (!currentProvider) {
22156
22592
  return notInitialized.result();
22157
22593
  }
@@ -22163,9 +22599,11 @@ var deploy = {
22163
22599
  })
22164
22600
  );
22165
22601
  }
22602
+ if (!existsSync(resolveOutputDir(appDir, "vercel")))
22603
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
22166
22604
  let allEnvVars = appEnv;
22167
22605
  const recovery = { projectName, stage: "provision", resources: [] };
22168
- if (!options?.skipProvision) {
22606
+ if (!options?.skipMiddleware) {
22169
22607
  const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
22170
22608
  if (!provResults.success) {
22171
22609
  return err(provResults.error);
@@ -22190,12 +22628,6 @@ var deploy = {
22190
22628
  return recoveryFailure(envResult.error, { ...recovery, stage: "set-env" });
22191
22629
  }
22192
22630
  }
22193
- if (!options?.skipBuild) {
22194
- const buildResult = buildApp(appDir, scan.buildCommand, allEnvVars);
22195
- if (!buildResult.success) {
22196
- return recoveryFailure(buildResult.error, { ...recovery, stage: "build" });
22197
- }
22198
- }
22199
22631
  const outputDir = resolveOutputDir(appDir, currentConfig?.provider.type ?? "vercel");
22200
22632
  const deployResult = await currentProvider.deploy(projectId, outputDir);
22201
22633
  if (!deployResult.success) {
@@ -22205,7 +22637,7 @@ var deploy = {
22205
22637
  ...deployResult.data,
22206
22638
  envVarsSet: Object.keys(allEnvVars)
22207
22639
  };
22208
- logger14.info("Deployment completed", {
22640
+ logger12.info("Deployment completed", {
22209
22641
  url: result.url,
22210
22642
  deploymentId: result.deploymentId,
22211
22643
  status: result.status
@@ -22215,21 +22647,41 @@ var deploy = {
22215
22647
  };
22216
22648
  async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
22217
22649
  const projectName = normalizeProjectName(rawProjectName);
22218
- if (scan.isSvelteKit && !scan.nodeAdapterInstalled) {
22219
- return err(
22220
- HaiDeployError.ADAPTER_MISSING,
22221
- deployM("deploy_adapterMissing", { params: { adapter: "@sveltejs/adapter-node" } })
22222
- );
22650
+ let artifact;
22651
+ let artifactDir = options?.artifactDir ?? resolveArtifactDirectory(appDir);
22652
+ try {
22653
+ if (existsSync(join(artifactDir, "latest"))) {
22654
+ const latest = readFileSync(join(artifactDir, "latest"), "utf8").trim();
22655
+ if (!/^[a-z0-9][\w.+-]*$/i.test(latest))
22656
+ throw new Error("Invalid artifact pointer");
22657
+ artifactDir = join(artifactDir, latest);
22658
+ }
22659
+ artifact = await readDeploymentArtifact(artifactDir);
22660
+ } catch {
22661
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
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
+ };
22223
22673
  }
22674
+ if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
22675
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
22224
22676
  let provisionEnv = {};
22225
- if (!options?.skipProvision) {
22677
+ if (!options?.skipMiddleware) {
22226
22678
  const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
22227
22679
  if (!provResults.success) {
22228
22680
  return err(provResults.error);
22229
22681
  }
22230
22682
  provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
22231
22683
  }
22232
- return provider.deploy({ appDir, projectName, scan, provisionEnv, appEnv, options });
22684
+ return provider.deploy({ appDir, artifact, artifactDir, projectName, scan, provisionEnv, appEnv, options });
22233
22685
  }
22234
22686
 
22235
22687
  // ../deploy/src/deploy-middleware.ts
@@ -22277,6 +22729,6 @@ function describeMiddlewareBackend(backend) {
22277
22729
  return deployM(backend.purpose);
22278
22730
  }
22279
22731
 
22280
- export { DEPLOY_MIDDLEWARE, DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, saveDeploymentEnvironment };
22281
- //# sourceMappingURL=src-6PPBKQ6Q.js.map
22282
- //# sourceMappingURL=src-6PPBKQ6Q.js.map
22732
+ export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
22733
+ //# sourceMappingURL=src-2V6VP4I5.js.map
22734
+ //# sourceMappingURL=src-2V6VP4I5.js.map