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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.en.md +13 -7
  2. package/README.ja.md +13 -7
  3. package/README.md +14 -8
  4. package/dist/{chunk-IPG3QNYK.js → chunk-FWN5B54L.js} +36 -4
  5. package/dist/chunk-FWN5B54L.js.map +1 -0
  6. package/dist/{chunk-47V5OT22.js → chunk-QYX6V7H2.js} +17 -43
  7. package/dist/chunk-QYX6V7H2.js.map +1 -0
  8. package/dist/cli-config-templates-3XSFIXBP.js +5 -0
  9. package/dist/{cli-config-templates-SJ2MBIR7.js.map → cli-config-templates-3XSFIXBP.js.map} +1 -1
  10. package/dist/{cli-skill-templates-A5VIR5J6.js → cli-skill-templates-TSYEQTCY.js} +3 -3
  11. package/dist/{cli-skill-templates-A5VIR5J6.js.map → cli-skill-templates-TSYEQTCY.js.map} +1 -1
  12. package/dist/index.js +273 -208
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src-6PPBKQ6Q.js → src-RDQ2EIBT.js} +506 -488
  15. package/dist/src-RDQ2EIBT.js.map +1 -0
  16. package/package.json +8 -8
  17. package/templates/apps/api/.dockerignore.hbs +4 -5
  18. package/templates/apps/api/.gitignore.hbs +2 -1
  19. package/templates/apps/api/Dockerfile.hbs +2 -15
  20. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +2 -2
  21. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +0 -1
  22. package/templates/apps/api/hai.build.json.hbs +1 -0
  23. package/templates/apps/api/package.json.hbs +5 -8
  24. package/templates/apps/fullstack/.dockerignore.hbs +4 -5
  25. package/templates/apps/fullstack/.gitignore.hbs +2 -1
  26. package/templates/apps/fullstack/Dockerfile.hbs +3 -15
  27. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +0 -3
  28. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +0 -2
  29. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +2 -2
  30. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +3 -6
  31. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +2 -3
  32. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/lib.rs.hbs +73 -2
  33. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +2 -2
  34. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +2 -2
  35. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -6
  36. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +0 -2
  37. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +2 -2
  38. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +1 -0
  39. package/templates/apps/fullstack/hai.build.json.hbs +7 -0
  40. package/templates/apps/fullstack/package.json.hbs +5 -12
  41. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +2 -2
  42. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +0 -1
  43. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +1 -1
  44. package/templates/apps/mobile-app/index.html.hbs +1 -0
  45. package/templates/apps/mobile-app/public/runtime-config.js +1 -0
  46. package/templates/apps/mobile-app/src/lib/api.ts.hbs +2 -1
  47. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +3 -2
  48. package/templates/base/.dockerignore.hbs +4 -5
  49. package/templates/base/.gitignore.hbs +2 -1
  50. package/templates/base/Dockerfile.hbs +5 -22
  51. package/templates/base/docker/nginx.conf.hbs +6 -0
  52. package/templates/base/hai.build.json.hbs +1 -0
  53. package/templates/base/package.json.hbs +13 -17
  54. package/templates/base/svelte.config.js.hbs +2 -0
  55. package/templates/base/vite.config.ts.hbs +6 -2
  56. package/templates/shared/.gitattributes +5 -0
  57. package/templates/shared/container-runtime-config.sh +30 -0
  58. package/templates/shared/run-e2e.mjs +65 -0
  59. package/templates/shared/vite-runtime-config.ts +54 -0
  60. package/templates/skills/hai-app-create/development.md +3 -1
  61. package/templates/skills/hai-app-create/reference.md +2 -2
  62. package/templates/skills/hai-build/SKILL.md +16 -1
  63. package/templates/skills/hai-deploy/SKILL.md +52 -260
  64. package/templates/skills/hai-deploy/middleware.md +8 -14
  65. package/templates/skills/hai-ui/reference.md +2 -0
  66. package/dist/chunk-47V5OT22.js.map +0 -1
  67. package/dist/chunk-IPG3QNYK.js.map +0 -1
  68. package/dist/cli-config-templates-SJ2MBIR7.js +0 -5
  69. package/dist/src-6PPBKQ6Q.js.map +0 -1
@@ -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, rmSync, statSync, readdirSync } from 'fs';
5
5
  import { resolve, join, basename, dirname, relative, sep } from 'path';
6
+ import { core, ok, err } from '@h-ai/core';
6
7
  import process2 from 'process';
7
8
  import { parseEnv } from 'util';
8
9
  import { 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) {
@@ -1856,18 +1856,18 @@ var validateAsync = async (schema, value, _ctx) => {
1856
1856
  return result.issues.length === 0;
1857
1857
  };
1858
1858
  var _encode = (_Err) => {
1859
- const parse6 = _parse(_Err);
1859
+ const parse7 = _parse(_Err);
1860
1860
  const fn = (schema, value, _ctx, _params) => {
1861
1861
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
1862
- return parse6(schema, value, ctx, finalizeParams(fn, _params));
1862
+ return parse7(schema, value, ctx, finalizeParams(fn, _params));
1863
1863
  };
1864
1864
  return fn;
1865
1865
  };
1866
1866
  var encode = /* @__PURE__ */ _encode($ZodRealError);
1867
1867
  var _decode = (_Err) => {
1868
- const parse6 = _parse(_Err);
1868
+ const parse7 = _parse(_Err);
1869
1869
  const fn = (schema, value, _ctx, _params) => {
1870
- return parse6(schema, value, _ctx, finalizeParams(fn, _params));
1870
+ return parse7(schema, value, _ctx, finalizeParams(fn, _params));
1871
1871
  };
1872
1872
  return fn;
1873
1873
  };
@@ -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,44 @@ 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(2),
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
+ /** 一次完整构建的标识,app/web 必须来自同一份清单。 */
19678
+ buildId: external_exports.string().regex(/^[a-z0-9][\w.+-]*$/i),
19679
+ /** API/独立应用和可选 Web 的镜像归档,至少包含一个 app。 */
19680
+ images: external_exports.array(external_exports.object({
19681
+ /** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
19682
+ name: external_exports.enum(["app", "web"]),
19683
+ /** 相对于清单目录的 tar 文件名。 */
19684
+ file: external_exports.string().regex(/^[\w.+-]+\.tar$/),
19685
+ /** 归档导入后可直接使用的不可变镜像引用。 */
19686
+ imageRef: external_exports.string().regex(/^[a-z0-9][a-z0-9./:_-]+$/),
19687
+ /** tar 内容的 SHA-256,用于部署前校验。 */
19688
+ sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
19689
+ /** 容器内部监听端口;服务器公开端口由部署选择。 */
19690
+ containerPort: external_exports.number().int().min(1).max(65535).optional()
19691
+ })).min(1).max(2).refine((images) => images.filter((image) => image.name === "app").length === 1 && new Set(images.map((image) => image.name)).size === images.length)
19692
+ }).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`));
19693
+ async function artifactChecksum(file2) {
19694
+ const hash2 = createHash("sha256");
19695
+ for await (const chunk of createReadStream(file2))
19696
+ hash2.update(chunk);
19697
+ return hash2.digest("hex");
19698
+ }
19699
+ async function readDeploymentArtifact(directory) {
19700
+ const artifact = DeploymentArtifactSchema.parse(JSON.parse(readFileSync(resolve(directory, "manifest.json"), "utf8")));
19701
+ for (const image of artifact.images) {
19702
+ const file2 = resolve(directory, image.file);
19703
+ if (!existsSync(file2) || !lstatSync(file2).isFile() || lstatSync(file2).isSymbolicLink() || await artifactChecksum(file2) !== image.sha256)
19704
+ throw new Error(`Invalid artifact: ${image.file}`);
19705
+ }
19706
+ return artifact;
19707
+ }
19708
+
19671
19709
  // ../deploy/messages/en-US.json
19672
19710
  var en_US_default = {
19673
19711
  deploy_notInitialized: "Deploy module not initialized, call deploy.init() first",
@@ -19678,7 +19716,6 @@ var en_US_default = {
19678
19716
  deploy_authRequired: "Authentication required",
19679
19717
  deploy_authFailed: "Authentication failed: {error}",
19680
19718
  deploy_projectCreateFailed: "Project creation failed: {error}",
19681
- deploy_buildFailed: "Application build failed: {error}",
19682
19719
  deploy_uploadFailed: "Build output upload failed: {error}",
19683
19720
  deploy_deployFailed: "Deployment failed: {error}",
19684
19721
  deploy_provisionFailed: "Service provisioning failed: {service} - {error}",
@@ -19698,8 +19735,6 @@ var en_US_default = {
19698
19735
  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
19736
  deploy_commandTimeout: "Command timed out: {command}",
19700
19737
  deploy_commandFailed: "Command failed ({command}): {error}",
19701
- deploy_containerRuntimeNotFound: "No usable container runtime found: {runtime}",
19702
- deploy_containerBuildFailed: "Container image build failed: {error}",
19703
19738
  deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
19704
19739
  deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
19705
19740
  deploy_imageTransferFailed: "Image transfer failed: {error}",
@@ -19720,7 +19755,13 @@ var en_US_default = {
19720
19755
  deploy_purposeLdap: "Directory account authentication; the default directory has no business users",
19721
19756
  deploy_purposeConsole: "Messages go to application logs without delivery",
19722
19757
  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"
19758
+ deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
19759
+ deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
19760
+ deploy_artifactInvalid: "Build artifacts are missing, corrupt, or could not be packaged. Run pnpm build --target docker at the project root and point --artifact-dir to a complete build directory.",
19761
+ deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
19762
+ deploy_bundleInstructions: "This bundle contains private configuration. Transfer it securely and restrict access.\n1. Copy {file} to Linux using removable media, a private network or scp.\n2. Install Docker/Podman with compose, tar, sha256sum and sh. Load middleware images using docker load/podman load with the exact tags below.\n3. Create a private release directory: mkdir -p release && chmod 700 release\n4. From the bundle directory run tar -xf {file} -C release\n5. Run cd release && sh {start}. Set HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=docker or podman to select the runtime explicitly.\n6. Check docker compose -f {compose} --env-file .env.runtime ps (or logs; use podman instead of docker for Podman), Web and /health after startup. Bundle generation does not perform server health checks.\nRequired images (app/web are included; prepare middleware separately):\n{images}\nBuilt images can be reused across environments; this bundle contains configuration for the selected environment only.",
19763
+ deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
19764
+ deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh."
19724
19765
  };
19725
19766
 
19726
19767
  // ../deploy/messages/zh-CN.json
@@ -19733,7 +19774,6 @@ var zh_CN_default2 = {
19733
19774
  deploy_authRequired: "\u8BF7\u5148\u5B8C\u6210\u8BA4\u8BC1",
19734
19775
  deploy_authFailed: "\u8BA4\u8BC1\u5931\u8D25\uFF1A{error}",
19735
19776
  deploy_projectCreateFailed: "\u9879\u76EE\u521B\u5EFA\u5931\u8D25\uFF1A{error}",
19736
- deploy_buildFailed: "\u5E94\u7528\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
19737
19777
  deploy_uploadFailed: "\u6784\u5EFA\u4EA7\u7269\u4E0A\u4F20\u5931\u8D25\uFF1A{error}",
19738
19778
  deploy_deployFailed: "\u90E8\u7F72\u5931\u8D25\uFF1A{error}",
19739
19779
  deploy_provisionFailed: "\u57FA\u7840\u8BBE\u65BD\u5F00\u901A\u5931\u8D25\uFF1A{service} - {error}",
@@ -19753,8 +19793,6 @@ var zh_CN_default2 = {
19753
19793
  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
19794
  deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
19755
19795
  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
19796
  deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
19759
19797
  deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
19760
19798
  deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
@@ -19775,7 +19813,13 @@ var zh_CN_default2 = {
19775
19813
  deploy_purposeLdap: "\u4F01\u4E1A\u76EE\u5F55\u8D26\u53F7\u8BA4\u8BC1\uFF1B\u9ED8\u8BA4\u76EE\u5F55\u4E0D\u5305\u542B\u4E1A\u52A1\u7528\u6237",
19776
19814
  deploy_purposeConsole: "\u6D88\u606F\u8F93\u51FA\u5230\u5E94\u7528\u65E5\u5FD7\uFF0C\u4E0D\u5B9E\u9645\u6295\u9012",
19777
19815
  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"
19816
+ deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6",
19817
+ deploy_webProxyInvalid: "API \u524D\u7F00\u5FC5\u987B\u662F\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26\u548C\u659C\u6760\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
19818
+ deploy_artifactInvalid: "\u6784\u5EFA\u4EA7\u7269\u7F3A\u5931\u3001\u635F\u574F\u6216\u90E8\u7F72\u6253\u5305\u5931\u8D25\u3002\u8BF7\u5148\u5728\u5DE5\u7A0B\u6839\u8FD0\u884C pnpm build --target docker\uFF0C\u5E76\u68C0\u67E5 --artifact-dir \u6307\u5411\u5B8C\u6574\u7684\u6784\u5EFA\u76EE\u5F55\u3002",
19819
+ deploy_sshRequired: "\u81EA\u52A8\u90E8\u7F72\u9700\u8981 SSH \u914D\u7F6E\uFF1B\u65E0\u7F51\u7EDC\u8FDE\u63A5\u65F6\u4F7F\u7528 --bundle-only \u751F\u6210\u90E8\u7F72\u5305\u5E76\u624B\u5DE5\u642C\u8FD0\u3002",
19820
+ deploy_bundleInstructions: "\u6B64\u5305\u5305\u542B\u79C1\u6709\u914D\u7F6E\uFF0C\u8BF7\u5B89\u5168\u642C\u8FD0\u5E76\u9650\u5236\u8BBF\u95EE\u6743\u9650\u3002\n1. \u5C06 {file} \u590D\u5236\u5230 Linux \u670D\u52A1\u5668\uFF08U \u76D8\u3001\u5185\u7F51\u4F20\u8F93\u6216 scp \u5747\u53EF\uFF09\u3002\n2. \u51C6\u5907 Docker/Podman\u3001\u5BF9\u5E94 compose \u547D\u4EE4\u3001tar\u3001sha256sum \u548C sh\uFF1B\u5C06\u6240\u9700\u4E2D\u95F4\u4EF6\u955C\u50CF\u7528 docker load/podman load \u5BFC\u5165\uFF0C\u955C\u50CF\u6807\u7B7E\u987B\u4E0E\u4E0B\u5217\u6E05\u5355\u5B8C\u5168\u4E00\u81F4\u3002\n3. \u65B0\u5EFA\u4EC5\u90E8\u7F72\u7528\u6237\u53EF\u8BBF\u95EE\u7684\u76EE\u5F55\uFF1Amkdir -p release && chmod 700 release\n4. \u5728\u5305\u6240\u5728\u76EE\u5F55\u6267\u884C tar -xf {file} -C release\n5. \u6267\u884C cd release && sh {start}\uFF1B\u53EF\u7528 HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=docker \u6216 podman \u663E\u5F0F\u9009\u62E9\u8FD0\u884C\u65F6\u3002\n6. \u542F\u52A8\u540E\u7528 docker compose -f {compose} --env-file .env.runtime ps\uFF08\u6216 logs\uFF1BPodman \u5C06 docker \u6362\u4E3A podman\uFF09\u68C0\u67E5\u72B6\u6001\u5E76\u9A8C\u8BC1 Web\u3001/health\uFF1B\u4EC5\u6253\u5305\u4E0D\u4F1A\u6267\u884C\u670D\u52A1\u5668\u5065\u5EB7\u68C0\u67E5\u3002\n\u6240\u9700\u955C\u50CF\uFF08app/web \u5DF2\u542B\u5728\u5305\u4E2D\uFF0C\u4E2D\u95F4\u4EF6\u9700\u53E6\u884C\u51C6\u5907\uFF09\uFF1A\n{images}\n\u6784\u5EFA\u955C\u50CF\u53EF\u7528\u4E8E\u591A\u73AF\u5883\uFF1B\u6B64\u5305\u7684\u73AF\u5883\u914D\u7F6E\u4EC5\u7528\u4E8E\u672C\u6B21\u6240\u9009\u73AF\u5883\u3002",
19821
+ deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
19822
+ deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002"
19779
19823
  };
19780
19824
 
19781
19825
  // ../deploy/src/deploy-i18n.ts
@@ -19816,7 +19860,7 @@ var ExposeSchema = PortExposeSchema;
19816
19860
  var DockerSshProviderConfigSchema = external_exports.object({
19817
19861
  type: external_exports.literal("docker-ssh"),
19818
19862
  /** SSH 连接参数 */
19819
- ssh: SshConfigSchema,
19863
+ ssh: SshConfigSchema.optional(),
19820
19864
  /** 远程主机运行参数 */
19821
19865
  remote: external_exports.object({
19822
19866
  /** 远程应用根目录 */
@@ -19870,24 +19914,15 @@ var DeployConfigSchema = external_exports.object({
19870
19914
  environment: external_exports.string().regex(/^[a-z0-9][\w-]*$/i).refine((value) => !["local", "example"].includes(value.toLowerCase())).optional(),
19871
19915
  provider: ProviderConfigSchema,
19872
19916
  services: ServicesConfigSchema,
19873
- /** 本地容器构建参数(docker-ssh 使用;auto 优先 podman) */
19917
+ /** Docker Compose 部署参数;构建配置位于项目 hai.build.json。 */
19874
19918
  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
19919
  /** 自建 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()
19920
+ s3Engine: external_exports.enum(["rustfs", "minio"]).default("rustfs")
19885
19921
  }).optional()
19886
19922
  });
19887
19923
  var DeployErrorInfo = {
19888
19924
  DEPLOY_FAILED: "001:500",
19889
19925
  PROJECT_CREATE_FAILED: "002:500",
19890
- BUILD_FAILED: "003:500",
19891
19926
  UPLOAD_FAILED: "004:500",
19892
19927
  AUTH_REQUIRED: "005:401",
19893
19928
  AUTH_FAILED: "006:401",
@@ -19899,8 +19934,6 @@ var DeployErrorInfo = {
19899
19934
  UNSUPPORTED_TYPE: "012:400",
19900
19935
  CONFIG_ERROR: "013:500",
19901
19936
  CREDENTIAL_ERROR: "014:500",
19902
- CONTAINER_RUNTIME_NOT_FOUND: "015:500",
19903
- CONTAINER_BUILD_FAILED: "016:500",
19904
19937
  REMOTE_CONNECT_FAILED: "017:500",
19905
19938
  REMOTE_COMMAND_FAILED: "018:500",
19906
19939
  IMAGE_TRANSFER_FAILED: "019:500",
@@ -19909,9 +19942,16 @@ var DeployErrorInfo = {
19909
19942
  var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
19910
19943
 
19911
19944
  // ../deploy/src/deploy-environment.ts
19945
+ function environmentValue(value) {
19946
+ try {
19947
+ return parse$1(value) ?? value;
19948
+ } catch {
19949
+ return value;
19950
+ }
19951
+ }
19912
19952
  function resolveConfig(value, prefix, env) {
19913
19953
  if (env[prefix] !== void 0)
19914
- return parse$1(env[prefix]) ?? env[prefix];
19954
+ return environmentValue(env[prefix]);
19915
19955
  if (Array.isArray(value))
19916
19956
  return value.map((item, index) => resolveConfig(item, `${prefix}_${index}`, env));
19917
19957
  if (value && typeof value === "object")
@@ -19919,7 +19959,7 @@ function resolveConfig(value, prefix, env) {
19919
19959
  if (typeof value !== "string")
19920
19960
  return value;
19921
19961
  const resolved = value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, name, fallback) => env[name] ?? fallback ?? "");
19922
- return /^\$\{[^}]+\}$/.test(value) ? parse$1(resolved) ?? resolved : resolved;
19962
+ return /^\$\{[^}]+\}$/.test(value) ? environmentValue(resolved) : resolved;
19923
19963
  }
19924
19964
  function loadDeploymentConfig(name, path, values) {
19925
19965
  try {
@@ -20187,39 +20227,25 @@ function saveCredentials(entries) {
20187
20227
  );
20188
20228
  }
20189
20229
  }
20190
- var logger2 = core.logger.child({ module: "deploy", scope: "functions" });
20230
+ function resolveArtifactDirectory(appDir) {
20231
+ let directory = resolve(appDir);
20232
+ while (true) {
20233
+ if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
20234
+ return join(directory, ".hai/build/docker");
20235
+ const parent = dirname(directory);
20236
+ if (parent === directory)
20237
+ return join(appDir, ".hai/build/docker");
20238
+ directory = parent;
20239
+ }
20240
+ }
20191
20241
  function usesDefaultMiddleware(scan, service, options) {
20192
20242
  const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
20193
20243
  return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
20194
20244
  }
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
20245
  function resolveOutputDir(appDir, providerType) {
20215
20246
  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
- }
20247
+ case "vercel":
20248
+ return join(appDir, ".vercel", "output");
20223
20249
  default:
20224
20250
  return join(appDir, "build");
20225
20251
  }
@@ -20250,7 +20276,7 @@ function recoveryFailure(error62, recovery) {
20250
20276
  ext: { recovery }
20251
20277
  });
20252
20278
  }
20253
- var logger3 = core.logger.child({ module: "deploy", scope: "scanner" });
20279
+ var logger2 = core.logger.child({ module: "deploy", scope: "scanner" });
20254
20280
  function record2(value) {
20255
20281
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20256
20282
  }
@@ -20369,15 +20395,170 @@ async function scanApp(inputDir, options) {
20369
20395
  adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
20370
20396
  nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
20371
20397
  requiredServices: [...new Set(middlewares.map((item) => item.service))],
20372
- serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend])),
20373
- buildCommand: "pnpm build"
20398
+ serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
20374
20399
  };
20375
- logger3.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
20400
+ logger2.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
20376
20401
  return ok(scan);
20377
20402
  } catch {
20378
20403
  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
20404
  }
20380
20405
  }
20406
+ var logger3 = core.logger.child({ module: "deploy", scope: "command" });
20407
+ function maskSecrets(text, secrets) {
20408
+ if (!secrets || secrets.length === 0)
20409
+ return text;
20410
+ let masked = text;
20411
+ for (const secret of secrets) {
20412
+ if (secret && secret.length > 0)
20413
+ masked = masked.split(secret).join("***");
20414
+ }
20415
+ return masked;
20416
+ }
20417
+ function runCommand(command, args, options = {}) {
20418
+ return new Promise((resolve6) => {
20419
+ const child = spawn(command, args, {
20420
+ cwd: options.cwd,
20421
+ env: options.env ? { ...process2.env, ...options.env } : process2.env,
20422
+ shell: false
20423
+ });
20424
+ let stdout = "";
20425
+ let stderr = "";
20426
+ let settled = false;
20427
+ let timer;
20428
+ const finish = (result) => {
20429
+ if (settled)
20430
+ return;
20431
+ settled = true;
20432
+ if (timer)
20433
+ clearTimeout(timer);
20434
+ resolve6(result);
20435
+ };
20436
+ if (options.timeoutMs && options.timeoutMs > 0) {
20437
+ timer = setTimeout(() => {
20438
+ child.kill("SIGKILL");
20439
+ logger3.error("Command timed out", { command, timeoutMs: options.timeoutMs });
20440
+ finish(err(
20441
+ HaiDeployError.REMOTE_COMMAND_FAILED,
20442
+ deployM("deploy_commandTimeout", { params: { command } })
20443
+ ));
20444
+ }, options.timeoutMs);
20445
+ }
20446
+ child.stdout.on("data", (chunk) => {
20447
+ stdout += chunk.toString("utf-8");
20448
+ });
20449
+ child.stderr.on("data", (chunk) => {
20450
+ stderr += chunk.toString("utf-8");
20451
+ });
20452
+ child.on("error", (error62) => {
20453
+ logger3.error("Command failed to start", { command, error: maskSecrets(String(error62), options.secrets) });
20454
+ finish(err(
20455
+ HaiDeployError.REMOTE_COMMAND_FAILED,
20456
+ deployM("deploy_commandFailed", {
20457
+ params: { command, error: maskSecrets(error62.message, options.secrets) }
20458
+ }),
20459
+ error62
20460
+ ));
20461
+ });
20462
+ child.on("close", (code) => {
20463
+ const exitCode = code ?? -1;
20464
+ if (exitCode === 0) {
20465
+ finish(ok({ code: exitCode, stdout, stderr }));
20466
+ return;
20467
+ }
20468
+ logger3.error("Command exited with non-zero code", {
20469
+ command,
20470
+ code: exitCode,
20471
+ stderr: maskSecrets(stderr, options.secrets).slice(-500),
20472
+ stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
20473
+ });
20474
+ finish(err(
20475
+ HaiDeployError.REMOTE_COMMAND_FAILED,
20476
+ deployM("deploy_commandFailed", {
20477
+ params: { command, error: maskSecrets(`${stdout.slice(-2500)}
20478
+ ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
20479
+ })
20480
+ ));
20481
+ });
20482
+ if (options.input !== void 0) {
20483
+ child.stdin.write(options.input);
20484
+ child.stdin.end();
20485
+ } else {
20486
+ child.stdin.end();
20487
+ }
20488
+ });
20489
+ }
20490
+
20491
+ // ../deploy/src/container/deploy-bundle.ts
20492
+ function shellArgument(value) {
20493
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
20494
+ }
20495
+ async function createDeploymentBundle(options) {
20496
+ const { directory, artifactDir, artifact, projectName, environment, runtime, coreEnv } = options;
20497
+ const stem = basename(options.output).replace(/\.tar$/i, "");
20498
+ const versionSuffix = `-${artifact.appVersion}`;
20499
+ const output2 = resolve(dirname(options.output), `${stem.endsWith(versionSuffix) ? stem : `${stem}${versionSuffix}`}.tar`);
20500
+ const composeName = `docker-compose-${artifact.appVersion}.yml`;
20501
+ mkdirSync(dirname(output2), { recursive: true, mode: 448 });
20502
+ const staging = mkdtempSync(join(directory, ".bundle-"));
20503
+ chmodSync(staging, 448);
20504
+ const temporaryTar = `${output2}.partial`;
20505
+ const startName = `start-${environment}.sh`;
20506
+ try {
20507
+ mkdirSync(join(staging, "images"));
20508
+ for (const image of artifact.images)
20509
+ copyFileSync(join(artifactDir, image.file), join(staging, "images", image.file));
20510
+ for (const file2 of [composeName, ".env.runtime", ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : []])
20511
+ copyFileSync(join(directory, file2), join(staging, file2));
20512
+ const compose = parse$1(readFileSync(join(directory, composeName), "utf8"));
20513
+ const images = [...new Set(Object.values(compose.services).flatMap((service) => service.image ? [service.image] : []))];
20514
+ const start = `#!/bin/sh
20515
+ set -eu
20516
+ cd "$(dirname "$0")"
20517
+ umask 077
20518
+ export HAI_DEPLOY_ENVIRONMENT=${shellArgument(environment)}
20519
+ export HAI_CORE_ENV=${shellArgument(coreEnv)}
20520
+ runtime=${shellArgument(runtime)}
20521
+ if [ -n "\${HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME:-}" ]; then runtime="$HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME"; fi
20522
+ if [ "$runtime" = auto ]; then
20523
+ if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
20524
+ fi
20525
+ case "$runtime" in docker|podman) ;; *) echo 'HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME must be docker or podman' >&2; exit 1;; esac
20526
+ "$runtime" version >/dev/null
20527
+ "$runtime" compose version >/dev/null
20528
+ sha256sum -c SHA256SUMS
20529
+ chmod 600 .env.runtime
20530
+ ${artifact.images.map((image) => `"$runtime" load -i ${shellArgument(`images/${image.file}`)}`).join("\n")}
20531
+ # \u79BB\u7EBF\u670D\u52A1\u5668\u5E94\u5148\u5BFC\u5165\u5168\u90E8\u4E2D\u95F4\u4EF6\u955C\u50CF\uFF1B\u7981\u6B62\u9759\u9ED8\u5C1D\u8BD5\u8054\u7F51\u3002
20532
+ for image in ${images.map(shellArgument).join(" ")}; do
20533
+ "$runtime" image inspect "$image" >/dev/null 2>&1 || { echo "Missing image: $image. Load the matching image archive before retrying." >&2; exit 1; }
20534
+ done
20535
+ exec "$runtime" compose -f ${shellArgument(composeName)} --env-file .env.runtime -p ${shellArgument(`hai-${projectName}`)} up -d
20536
+ `;
20537
+ writeFileSync(join(directory, startName), start, { mode: 448 });
20538
+ writeFileSync(join(staging, startName), start, { mode: 448 });
20539
+ const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output2), start: startName, images: images.join("\n"), compose: composeName } });
20540
+ const metadata = deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: composeName } });
20541
+ writeFileSync(join(staging, "README.txt"), `${metadata}
20542
+ ${instructions}
20543
+ `);
20544
+ copyFileSync(join(artifactDir, "manifest.json"), join(staging, "manifest.json"));
20545
+ const checksums = [];
20546
+ for (const file2 of [composeName, "manifest.json", ".env.runtime", startName, ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : [], ...artifact.images.map((image) => `images/${image.file}`)])
20547
+ checksums.push(`${await artifactChecksum(join(staging, file2))} ${file2}`);
20548
+ writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
20549
+ `);
20550
+ const tar = await runCommand("tar", ["-cf", temporaryTar, "-C", staging, "."]);
20551
+ if (!tar.success)
20552
+ throw new Error(tar.error.message);
20553
+ chmodSync(temporaryTar, 384);
20554
+ const { renameSync } = await import('fs');
20555
+ renameSync(temporaryTar, output2);
20556
+ return { bundlePath: output2, instructions, startName };
20557
+ } finally {
20558
+ rmSync(staging, { recursive: true, force: true });
20559
+ rmSync(temporaryTar, { force: true });
20560
+ }
20561
+ }
20381
20562
  var IMAGES = {
20382
20563
  postgres: "postgres:16-alpine",
20383
20564
  mysql: "mysql:8.4",
@@ -20651,6 +20832,7 @@ function generateComposeFile(options) {
20651
20832
  const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
20652
20833
  const app = {
20653
20834
  image: imageRef,
20835
+ labels: { "org.opencontainers.image.version": options.appVersion },
20654
20836
  container_name: `hai-${projectName}-app`,
20655
20837
  restart: "unless-stopped",
20656
20838
  // 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
@@ -20664,11 +20846,28 @@ function generateComposeFile(options) {
20664
20846
  if (sidecar.dependsOn.length > 0)
20665
20847
  app.depends_on = sidecar.dependsOn;
20666
20848
  const services = { app, ...sidecar.composeServices };
20849
+ if (options.web) {
20850
+ delete app.ports;
20851
+ services.web = {
20852
+ image: options.web.imageRef,
20853
+ labels: { "org.opencontainers.image.version": options.appVersion },
20854
+ container_name: `hai-${projectName}-web`,
20855
+ restart: "unless-stopped",
20856
+ pull_policy: "never",
20857
+ ports: [`${hostPort}:${options.web.containerPort}`],
20858
+ environment: { PUBLIC_API_BASE: "" },
20859
+ 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 },
20860
+ volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
20861
+ depends_on: ["app"],
20862
+ networks: ["hai-app"]
20863
+ };
20864
+ }
20667
20865
  if (services["storage-init"])
20668
20866
  services["storage-init"] = { ...services["storage-init"], image: imageRef };
20669
20867
  const compose = {
20868
+ "x-hai-version": options.appVersion,
20670
20869
  services,
20671
- networks: {
20870
+ "networks": {
20672
20871
  "hai-app": { name: `hai-${projectName}` }
20673
20872
  }
20674
20873
  };
@@ -20697,8 +20896,43 @@ function generateComposeFile(options) {
20697
20896
  "storage-init": { condition: "service_completed_successfully" }
20698
20897
  };
20699
20898
  }
20899
+ for (const service of Object.values(services))
20900
+ service.pull_policy = "never";
20700
20901
  return stringify(compose);
20701
20902
  }
20903
+ function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
20904
+ if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535))
20905
+ throw new Error("Invalid web proxy target");
20906
+ return `server {
20907
+ listen ${webPort};
20908
+ server_name _;
20909
+ root /usr/share/nginx/html;
20910
+ location / {
20911
+ try_files $uri $uri/ /index.html;
20912
+ }
20913
+ location = /runtime-config.js {
20914
+ add_header Cache-Control "no-store";
20915
+ default_type application/javascript;
20916
+ return 200 'globalThis.__HAI_PUBLIC_CONFIG__={apiBase:globalThis.location.origin+"${apiPrefix}"};';
20917
+ }
20918
+ location = /_hai/web-health {
20919
+ access_log off;
20920
+ return 200 'ok';
20921
+ }
20922
+ location = /health {
20923
+ proxy_pass http://app:${port};
20924
+ proxy_set_header Host $http_host;
20925
+ }
20926
+ location ${apiPrefix.replace(/\/$/, "")}/ {
20927
+ proxy_pass http://app:${port};
20928
+ proxy_http_version 1.1;
20929
+ proxy_set_header Host $http_host;
20930
+ proxy_set_header X-Forwarded-Proto $scheme;
20931
+ proxy_buffering off;
20932
+ }
20933
+ }
20934
+ `;
20935
+ }
20702
20936
  function serializeEnvFile(env) {
20703
20937
  return `${Object.entries(env).map(([key, value]) => {
20704
20938
  let literal2 = value;
@@ -20714,194 +20948,7 @@ function serializeEnvFile(env) {
20714
20948
  }).join("\n")}
20715
20949
  `;
20716
20950
  }
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" });
20951
+ var logger4 = core.logger.child({ module: "deploy", scope: "ssh" });
20905
20952
  function buildConnectOptions(config2) {
20906
20953
  const args = ["-o", "BatchMode=yes"];
20907
20954
  args.push("-o", `ConnectTimeout=${config2.connectTimeoutSeconds}`);
@@ -20952,7 +20999,7 @@ function createSshExecutor(config2) {
20952
20999
  return ok(void 0);
20953
21000
  };
20954
21001
  const ping = async () => {
20955
- logger6.debug("SSH preflight", { host: config2.host, port: config2.port });
21002
+ logger4.debug("SSH preflight", { host: config2.host, port: config2.port });
20956
21003
  const result = await exec("true", { timeoutMs: 2e4 });
20957
21004
  if (!result.success) {
20958
21005
  return err(
@@ -20967,52 +21014,24 @@ function createSshExecutor(config2) {
20967
21014
  }
20968
21015
 
20969
21016
  // ../deploy/src/providers/deploy-provider-docker-ssh.ts
20970
- var logger7 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
21017
+ var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
20971
21018
  var HEALTH_POLL_INTERVAL = 3e3;
20972
21019
  var HEALTH_MAX_ATTEMPTS = 40;
20973
- function shellArgument(value) {
20974
- return `'${value.replaceAll("'", `'"'"'`)}'`;
20975
- }
20976
21020
  function makeDeploymentId() {
20977
21021
  const now = /* @__PURE__ */ new Date();
20978
21022
  const pad = (n) => String(n).padStart(2, "0");
20979
21023
  const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
20980
21024
  return `${stamp}-${randomBytes(3).toString("hex")}`;
20981
21025
  }
20982
- function parseLoadedImageRef(stdout, fallback) {
20983
- const match = stdout.match(/Loaded image(?:\(s\))?:\s*(\S+)/);
20984
- return match ? match[1] : fallback;
20985
- }
20986
21026
  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;
21027
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
21009
21028
  }
21010
21029
  function createDockerSshProvider(config2, container) {
21011
- const localRuntimePref = container?.runtime ?? "auto";
21012
- const httpProxy = container?.httpProxy ?? true;
21013
- const ssh = createSshExecutor(config2.ssh);
21030
+ const ssh = config2.ssh ? createSshExecutor(config2.ssh) : void 0;
21014
21031
  const baseDir = config2.remote.baseDir;
21015
21032
  async function detectRemoteRuntime() {
21033
+ if (!ssh)
21034
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
21016
21035
  const candidates = config2.remote.runtime === "auto" ? ["podman", "docker"] : [config2.remote.runtime];
21017
21036
  for (const runtime of candidates) {
21018
21037
  const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
@@ -21020,7 +21039,7 @@ function createDockerSshProvider(config2, container) {
21020
21039
  continue;
21021
21040
  const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
21022
21041
  if (composeResult.success) {
21023
- logger7.info("Remote container runtime detected", { runtime });
21042
+ logger5.info("Remote container runtime detected", { runtime });
21024
21043
  return ok(runtime);
21025
21044
  }
21026
21045
  }
@@ -21044,29 +21063,31 @@ function createDockerSshProvider(config2, container) {
21044
21063
  const deploy2 = async (context) => {
21045
21064
  const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
21046
21065
  const deploymentId = makeDeploymentId();
21047
- const containerPort = config2.expose.containerPort;
21048
21066
  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
21067
  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
- }
21068
+ mkdirSync(artifactDir, { recursive: true, mode: 448 });
21069
+ chmodSync(artifactDir, 448);
21070
+ const artifact = context.artifact;
21071
+ const resolvedBuildDir = context.artifactDir;
21072
+ const images = artifact.images;
21073
+ const web = images.find((image) => image.name === "web");
21074
+ const app = images.find((image) => image.name === "app");
21075
+ const containerPort = app.containerPort ?? config2.expose.containerPort;
21076
+ const url2 = config2.ssh ? `http://${config2.ssh.host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
21069
21077
  const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
21078
+ let webProxy;
21079
+ if (web) {
21080
+ const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
21081
+ if (!server.success)
21082
+ return server;
21083
+ const http = server.data.http;
21084
+ const cors = server.data.cors;
21085
+ const apiPrefix = http?.apiPrefix ?? "/api/v1";
21086
+ if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
21087
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
21088
+ webProxy = generateWebProxyConfig(containerPort, apiPrefix, web.containerPort ?? 8080);
21089
+ env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config2.ssh ? url2 : cors?.origin ?? "*" } });
21090
+ }
21070
21091
  const envPath = join(artifactDir, ".env.runtime");
21071
21092
  const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
21072
21093
  Object.assign(env, { ...previousEnv, ...env });
@@ -21079,88 +21100,64 @@ function createDockerSshProvider(config2, container) {
21079
21100
  if (!saved.success)
21080
21101
  return saved;
21081
21102
  }
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
- }
21103
+ const composeName = `docker-compose-${artifact.appVersion}.yml`;
21104
+ const latestPath = join(artifactDir, "latest");
21105
+ const previousName = existsSync(latestPath) ? readFileSync(latestPath, "utf8").trim() : "";
21106
+ const previousPath = /^[\w.+-]+\.yml$/.test(previousName) ? join(artifactDir, previousName) : void 0;
21120
21107
  const composeContent = generateComposeFile({
21121
21108
  projectName,
21122
- imageRef: remoteImageRef,
21109
+ appVersion: artifact.appVersion,
21110
+ imageRef: app.imageRef,
21123
21111
  hostPort,
21124
21112
  containerPort,
21125
21113
  sidecar,
21114
+ web: web ? { imageRef: web.imageRef, containerPort: web.containerPort ?? 8080 } : void 0,
21126
21115
  serviceModes: options?.serviceModes,
21127
- previousCompose: existsSync(join(artifactDir, "compose.yml")) ? readFileSync(join(artifactDir, "compose.yml"), "utf8") : void 0
21116
+ previousCompose: previousPath && existsSync(previousPath) ? readFileSync(previousPath, "utf8") : void 0
21128
21117
  });
21129
- const composePath = join(artifactDir, "compose.yml");
21118
+ const composePath = join(artifactDir, composeName);
21130
21119
  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 }
21120
+ if (webProxy)
21121
+ writeFileSync(join(artifactDir, "web.conf"), webProxy);
21122
+ const bundle = await createDeploymentBundle({
21123
+ directory: artifactDir,
21124
+ artifactDir: resolvedBuildDir,
21125
+ artifact,
21126
+ projectName,
21127
+ environment: options?.environment ?? "prod",
21128
+ runtime: config2.remote.runtime,
21129
+ coreEnv: env.HAI_CORE_ENV ?? "production",
21130
+ output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
21131
+ });
21132
+ writeFileSync(latestPath, `${composeName}
21133
+ `);
21134
+ if (options?.bundleOnly)
21135
+ return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
21136
+ if (!ssh)
21137
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
21138
+ const ping = await ssh.ping();
21139
+ if (!ping.success)
21140
+ return ping;
21141
+ const remoteRuntime = await detectRemoteRuntime();
21142
+ if (!remoteRuntime.success)
21143
+ return remoteRuntime;
21144
+ const remoteDir = `${baseDir}/${projectName}`;
21145
+ const remoteTar = `${remoteDir}/${basename(bundle.bundlePath)}`;
21146
+ const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
21147
+ if (!prepared.success)
21148
+ return prepared;
21149
+ const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
21150
+ if (!uploaded.success)
21151
+ return uploaded;
21152
+ const started = await ssh.exec(
21153
+ `cd ${shellArgument(remoteDir)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} && HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME=${shellArgument(remoteRuntime.data)} sh ${shellArgument(bundle.startName)}`,
21154
+ { timeoutMs: 3e5, secrets: sidecar.secrets }
21157
21155
  );
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"}`);
21156
+ if (!started.success)
21157
+ return started;
21158
+ await ssh.exec(`rm -f -- ${shellArgument(remoteTar)} ${images.map((image) => shellArgument(`${remoteDir}/images/${image.file}`)).join(" ")}`);
21159
+ logger5.info("Running health check", { url: url2 });
21160
+ const healthy = await healthCheck(`${url2}${!web && scan.isSvelteKit ? "/" : "/health"}`) && (!web || await healthCheck(`${url2}/_hai/web-health`) && await healthCheck(`${url2}/`));
21164
21161
  if (!healthy) {
21165
21162
  return err(
21166
21163
  HaiDeployError.HEALTH_CHECK_FAILED,
@@ -21173,14 +21170,23 @@ exec ${remoteRuntime} compose --env-file .env.runtime -p hai-${projectName} up -
21173
21170
  status: "ready",
21174
21171
  provider: "docker-ssh",
21175
21172
  healthCheck: "passed",
21176
- envVarsSet: Object.keys(env)
21173
+ envVarsSet: Object.keys(env),
21174
+ bundlePath: bundle.bundlePath
21177
21175
  };
21178
- logger7.info("docker-ssh deployment complete", { url: url2, deploymentId });
21176
+ logger5.info("docker-ssh deployment complete", { url: url2, deploymentId });
21179
21177
  return ok(result);
21180
21178
  };
21181
- return { name: "docker-ssh", deploy: deploy2 };
21179
+ const deploySafely = async (context) => {
21180
+ try {
21181
+ return await deploy2(context);
21182
+ } catch (error62) {
21183
+ logger5.error("Deployment bundle failed", { error: error62 });
21184
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"), error62);
21185
+ }
21186
+ };
21187
+ return { name: "docker-ssh", deploy: deploySafely };
21182
21188
  }
21183
- var logger8 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
21189
+ var logger6 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
21184
21190
  var VERCEL_API = "https://api.vercel.com";
21185
21191
  var POLL_INTERVAL = 3e3;
21186
21192
  var MAX_POLL_ATTEMPTS = 60;
@@ -21233,7 +21239,7 @@ function createVercelProvider() {
21233
21239
  return {
21234
21240
  name: "vercel",
21235
21241
  async authenticate(apiToken) {
21236
- logger8.debug("Authenticating with Vercel");
21242
+ logger6.debug("Authenticating with Vercel");
21237
21243
  try {
21238
21244
  const user = await vercelFetch(
21239
21245
  apiToken,
@@ -21241,10 +21247,10 @@ function createVercelProvider() {
21241
21247
  );
21242
21248
  token = apiToken;
21243
21249
  const username = user.user.username || user.user.email;
21244
- logger8.info("Vercel authenticated", { user: username });
21250
+ logger6.info("Vercel authenticated", { user: username });
21245
21251
  return ok(username);
21246
21252
  } catch (error62) {
21247
- logger8.error("Vercel authentication failed", { error: error62 });
21253
+ logger6.error("Vercel authentication failed", { error: error62 });
21248
21254
  return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error62));
21249
21255
  }
21250
21256
  },
@@ -21255,11 +21261,11 @@ function createVercelProvider() {
21255
21261
  deployM("deploy_authRequired")
21256
21262
  );
21257
21263
  }
21258
- logger8.debug("Creating Vercel project", { projectName });
21264
+ logger6.debug("Creating Vercel project", { projectName });
21259
21265
  try {
21260
21266
  const existingId = await findExistingProject(token, projectName);
21261
21267
  if (existingId !== null) {
21262
- logger8.info("Vercel project found", { projectName, projectId: existingId });
21268
+ logger6.info("Vercel project found", { projectName, projectId: existingId });
21263
21269
  return ok(existingId);
21264
21270
  }
21265
21271
  try {
@@ -21274,13 +21280,13 @@ function createVercelProvider() {
21274
21280
  })
21275
21281
  }
21276
21282
  );
21277
- logger8.info("Vercel project created", { projectName, projectId: project.id });
21283
+ logger6.info("Vercel project created", { projectName, projectId: project.id });
21278
21284
  return ok(project.id);
21279
21285
  } catch (error62) {
21280
21286
  if (error62 instanceof VercelApiError && error62.status === 409) {
21281
21287
  const concurrentProjectId = await findExistingProject(token, projectName);
21282
21288
  if (concurrentProjectId !== null) {
21283
- logger8.info("Vercel project reused after create conflict", {
21289
+ logger6.info("Vercel project reused after create conflict", {
21284
21290
  projectName,
21285
21291
  projectId: concurrentProjectId
21286
21292
  });
@@ -21290,7 +21296,7 @@ function createVercelProvider() {
21290
21296
  throw error62;
21291
21297
  }
21292
21298
  } catch (error62) {
21293
- logger8.error("Vercel project creation failed", { projectName, error: error62 });
21299
+ logger6.error("Vercel project creation failed", { projectName, error: error62 });
21294
21300
  return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error62));
21295
21301
  }
21296
21302
  },
@@ -21301,7 +21307,7 @@ function createVercelProvider() {
21301
21307
  deployM("deploy_authRequired")
21302
21308
  );
21303
21309
  }
21304
- logger8.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
21310
+ logger6.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
21305
21311
  try {
21306
21312
  const envEntries = Object.entries(envVars).map(([key, value]) => ({
21307
21313
  key,
@@ -21317,10 +21323,10 @@ function createVercelProvider() {
21317
21323
  body: JSON.stringify(envEntries)
21318
21324
  }
21319
21325
  );
21320
- logger8.info("Vercel env vars set", { projectId, count: envEntries.length });
21326
+ logger6.info("Vercel env vars set", { projectId, count: envEntries.length });
21321
21327
  return ok(void 0);
21322
21328
  } catch (error62) {
21323
- logger8.error("Vercel env vars setup failed", { projectId, error: error62 });
21329
+ logger6.error("Vercel env vars setup failed", { projectId, error: error62 });
21324
21330
  return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error62));
21325
21331
  }
21326
21332
  },
@@ -21331,8 +21337,11 @@ function createVercelProvider() {
21331
21337
  deployM("deploy_authRequired")
21332
21338
  );
21333
21339
  }
21334
- logger8.debug("Deploying to Vercel", { projectId, outputDir });
21340
+ logger6.debug("Deploying to Vercel", { projectId, outputDir });
21335
21341
  try {
21342
+ const outputConfig = JSON.parse(readFileSync(join(outputDir, "config.json"), "utf8"));
21343
+ if (outputConfig.version !== 3)
21344
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
21336
21345
  const filePaths = collectFiles(outputDir, outputDir);
21337
21346
  const files = [];
21338
21347
  for (const filePath of filePaths) {
@@ -21341,18 +21350,20 @@ function createVercelProvider() {
21341
21350
  const sha = createHash("sha1").update(content).digest("hex");
21342
21351
  const size = statSync(fullPath).size;
21343
21352
  await uploadFile(token, content, sha);
21344
- files.push({ file: filePath, sha, size });
21353
+ files.push({ file: `.vercel/output/${filePath}`, sha, size });
21345
21354
  }
21346
- logger8.debug("Files uploaded", { count: files.length });
21355
+ logger6.debug("Files uploaded", { count: files.length });
21347
21356
  const deployment = await vercelFetch(
21348
21357
  token,
21349
- "/v13/deployments",
21358
+ "/v13/deployments?prebuilt=1",
21350
21359
  {
21351
21360
  method: "POST",
21352
21361
  body: JSON.stringify({
21353
21362
  name: projectId,
21354
21363
  files,
21355
- projectSettings: { framework: "sveltekit" }
21364
+ version: 2,
21365
+ project: projectId,
21366
+ projectSettings: { framework: null, buildCommand: "", installCommand: "" }
21356
21367
  })
21357
21368
  }
21358
21369
  );
@@ -21363,14 +21374,14 @@ function createVercelProvider() {
21363
21374
  status: finalStatus,
21364
21375
  envVarsSet: []
21365
21376
  };
21366
- logger8.info("Vercel deployment complete", {
21377
+ logger6.info("Vercel deployment complete", {
21367
21378
  deploymentId: deployment.id,
21368
21379
  url: result.url,
21369
21380
  status: finalStatus
21370
21381
  });
21371
21382
  return ok(result);
21372
21383
  } catch (error62) {
21373
- logger8.error("Vercel deployment failed", { projectId, error: error62 });
21384
+ logger6.error("Vercel deployment failed", { projectId, error: error62 });
21374
21385
  return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error62));
21375
21386
  }
21376
21387
  }
@@ -21415,11 +21426,11 @@ async function pollDeploymentStatus(token, deploymentId) {
21415
21426
  if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
21416
21427
  return "error";
21417
21428
  }
21418
- await new Promise((resolve3) => setTimeout(resolve3, POLL_INTERVAL));
21429
+ await new Promise((resolve6) => setTimeout(resolve6, POLL_INTERVAL));
21419
21430
  }
21420
21431
  return "building";
21421
21432
  }
21422
- var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
21433
+ var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
21423
21434
  function createAliyunProvisioner() {
21424
21435
  let accessKey = null;
21425
21436
  let secretKey = null;
@@ -21428,7 +21439,7 @@ function createAliyunProvisioner() {
21428
21439
  name: "aliyun",
21429
21440
  serviceType: "sms",
21430
21441
  async authenticate(credentials) {
21431
- logger9.debug("Authenticating with Aliyun");
21442
+ logger7.debug("Authenticating with Aliyun");
21432
21443
  try {
21433
21444
  const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
21434
21445
  const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
@@ -21438,10 +21449,10 @@ function createAliyunProvisioner() {
21438
21449
  accessKey = akId;
21439
21450
  secretKey = akSecret;
21440
21451
  signName = credentials.signName;
21441
- logger9.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
21452
+ logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
21442
21453
  return ok(akId);
21443
21454
  } catch (error62) {
21444
- logger9.error("Aliyun authentication failed", { error: error62 });
21455
+ logger7.error("Aliyun authentication failed", { error: error62 });
21445
21456
  return err(
21446
21457
  HaiDeployError.AUTH_FAILED,
21447
21458
  deployM("deploy_authFailed", {
@@ -21458,7 +21469,7 @@ function createAliyunProvisioner() {
21458
21469
  deployM("deploy_authRequired")
21459
21470
  );
21460
21471
  }
21461
- logger9.debug("Provisioning Aliyun SMS service");
21472
+ logger7.debug("Provisioning Aliyun SMS service");
21462
21473
  return ok({
21463
21474
  serviceType: "sms",
21464
21475
  provisionerName: "aliyun",
@@ -21471,7 +21482,7 @@ function createAliyunProvisioner() {
21471
21482
  }
21472
21483
  };
21473
21484
  }
21474
- var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
21485
+ var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
21475
21486
  var NEON_API = "https://console.neon.tech/api/v2";
21476
21487
  var DEFAULT_NEON_DATABASE_NAME = "neondb";
21477
21488
  var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
@@ -21530,7 +21541,7 @@ function createNeonProvisioner() {
21530
21541
  name: "neon",
21531
21542
  serviceType: "db",
21532
21543
  async authenticate(credentials) {
21533
- logger10.debug("Authenticating with Neon");
21544
+ logger8.debug("Authenticating with Neon");
21534
21545
  try {
21535
21546
  const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
21536
21547
  if (!apiToken) {
@@ -21538,10 +21549,10 @@ function createNeonProvisioner() {
21538
21549
  }
21539
21550
  await listNeonProjects(apiToken);
21540
21551
  token = apiToken;
21541
- logger10.info("Neon authenticated");
21552
+ logger8.info("Neon authenticated");
21542
21553
  return ok("neon-user");
21543
21554
  } catch (error62) {
21544
- logger10.error("Neon authentication failed", { error: error62 });
21555
+ logger8.error("Neon authentication failed", { error: error62 });
21545
21556
  return err(
21546
21557
  HaiDeployError.AUTH_FAILED,
21547
21558
  deployM("deploy_authFailed", {
@@ -21558,7 +21569,7 @@ function createNeonProvisioner() {
21558
21569
  deployM("deploy_authRequired")
21559
21570
  );
21560
21571
  }
21561
- logger10.debug("Provisioning Neon database", { appName });
21572
+ logger8.debug("Provisioning Neon database", { appName });
21562
21573
  let resource;
21563
21574
  try {
21564
21575
  const projectName = `${appName}-db`;
@@ -21566,7 +21577,7 @@ function createNeonProvisioner() {
21566
21577
  if (existingProject !== null) {
21567
21578
  resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
21568
21579
  const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
21569
- logger10.info("Neon project reused", {
21580
+ logger8.info("Neon project reused", {
21570
21581
  projectId: existingProject.id,
21571
21582
  projectName
21572
21583
  });
@@ -21588,10 +21599,10 @@ function createNeonProvisioner() {
21588
21599
  if (!projectId || !connectionUri) {
21589
21600
  throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
21590
21601
  }
21591
- logger10.info("Neon database provisioned", { projectId });
21602
+ logger8.info("Neon database provisioned", { projectId });
21592
21603
  return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
21593
21604
  } catch (error62) {
21594
- logger10.error("Neon provisioning failed", { appName });
21605
+ logger8.error("Neon provisioning failed", { appName });
21595
21606
  if (resource) {
21596
21607
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
21597
21608
  projectName: appName,
@@ -21613,7 +21624,7 @@ function createNeonProvisioner() {
21613
21624
  }
21614
21625
  };
21615
21626
  }
21616
- var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
21627
+ var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
21617
21628
  var CF_API = "https://api.cloudflare.com/client/v4";
21618
21629
  function createR2Provisioner() {
21619
21630
  let token = null;
@@ -21624,7 +21635,7 @@ function createR2Provisioner() {
21624
21635
  name: "r2",
21625
21636
  serviceType: "storage",
21626
21637
  async authenticate(credentials) {
21627
- logger11.debug("Authenticating with Cloudflare");
21638
+ logger9.debug("Authenticating with Cloudflare");
21628
21639
  try {
21629
21640
  const acctId = credentials.accountId ?? credentials.account_id ?? "";
21630
21641
  const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
@@ -21641,10 +21652,10 @@ function createR2Provisioner() {
21641
21652
  token = apiTok;
21642
21653
  accessKey = credentials.accessKeyId;
21643
21654
  secretKey = credentials.secretAccessKey;
21644
- logger11.info("Cloudflare R2 authenticated", { accountId: acctId });
21655
+ logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
21645
21656
  return ok(acctId);
21646
21657
  } catch (error62) {
21647
- logger11.error("Cloudflare R2 authentication failed", { error: error62 });
21658
+ logger9.error("Cloudflare R2 authentication failed", { error: error62 });
21648
21659
  return err(
21649
21660
  HaiDeployError.AUTH_FAILED,
21650
21661
  deployM("deploy_authFailed", {
@@ -21662,7 +21673,7 @@ function createR2Provisioner() {
21662
21673
  );
21663
21674
  }
21664
21675
  const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
21665
- logger11.debug("Provisioning R2 bucket", { appName, bucketName });
21676
+ logger9.debug("Provisioning R2 bucket", { appName, bucketName });
21666
21677
  try {
21667
21678
  const createRes = await fetch(
21668
21679
  `${CF_API}/accounts/${accountId}/r2/buckets`,
@@ -21678,7 +21689,7 @@ function createR2Provisioner() {
21678
21689
  if (!createRes.ok && createRes.status !== 409) {
21679
21690
  throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
21680
21691
  }
21681
- logger11.info("R2 bucket provisioned", { bucketName });
21692
+ logger9.info("R2 bucket provisioned", { bucketName });
21682
21693
  return ok({
21683
21694
  serviceType: "storage",
21684
21695
  provisionerName: "r2",
@@ -21696,7 +21707,7 @@ function createR2Provisioner() {
21696
21707
  resourceInfo: `r2-bucket:${bucketName}`
21697
21708
  });
21698
21709
  } catch (error62) {
21699
- logger11.error("R2 provisioning failed", { appName, error: error62 });
21710
+ logger9.error("R2 provisioning failed", { appName, error: error62 });
21700
21711
  return err(
21701
21712
  HaiDeployError.PROVISION_FAILED,
21702
21713
  deployM("deploy_provisionFailed", {
@@ -21711,7 +21722,7 @@ function createR2Provisioner() {
21711
21722
  }
21712
21723
  };
21713
21724
  }
21714
- var logger12 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
21725
+ var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
21715
21726
  var RESEND_API = "https://api.resend.com";
21716
21727
  function createResendProvisioner() {
21717
21728
  let token = null;
@@ -21720,7 +21731,7 @@ function createResendProvisioner() {
21720
21731
  name: "resend",
21721
21732
  serviceType: "email",
21722
21733
  async authenticate(credentials) {
21723
- logger12.debug("Authenticating with Resend");
21734
+ logger10.debug("Authenticating with Resend");
21724
21735
  try {
21725
21736
  const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
21726
21737
  if (!apiToken || !credentials.from) {
@@ -21734,10 +21745,10 @@ function createResendProvisioner() {
21734
21745
  }
21735
21746
  token = apiToken;
21736
21747
  from = credentials.from;
21737
- logger12.info("Resend authenticated");
21748
+ logger10.info("Resend authenticated");
21738
21749
  return ok("resend-user");
21739
21750
  } catch (error62) {
21740
- logger12.error("Resend authentication failed", { error: error62 });
21751
+ logger10.error("Resend authentication failed", { error: error62 });
21741
21752
  return err(
21742
21753
  HaiDeployError.AUTH_FAILED,
21743
21754
  deployM("deploy_authFailed", {
@@ -21754,7 +21765,7 @@ function createResendProvisioner() {
21754
21765
  deployM("deploy_authRequired")
21755
21766
  );
21756
21767
  }
21757
- logger12.debug("Provisioning Resend email service");
21768
+ logger10.debug("Provisioning Resend email service");
21758
21769
  return ok({
21759
21770
  serviceType: "email",
21760
21771
  provisionerName: "resend",
@@ -21767,7 +21778,7 @@ function createResendProvisioner() {
21767
21778
  }
21768
21779
  };
21769
21780
  }
21770
- var logger13 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
21781
+ var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
21771
21782
  var UPSTASH_API = "https://api.upstash.com";
21772
21783
  function createUpstashAuthHeader(email3, apiKey) {
21773
21784
  return `Basic ${Buffer.from(`${email3}:${apiKey}`).toString("base64")}`;
@@ -21818,7 +21829,7 @@ function createUpstashProvisioner() {
21818
21829
  name: "upstash",
21819
21830
  serviceType: "cache",
21820
21831
  async authenticate(credentials) {
21821
- logger13.debug("Authenticating with Upstash");
21832
+ logger11.debug("Authenticating with Upstash");
21822
21833
  try {
21823
21834
  const userEmail = credentials.email ?? "";
21824
21835
  const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
@@ -21828,10 +21839,10 @@ function createUpstashProvisioner() {
21828
21839
  await listUpstashDatabases(userEmail, userKey);
21829
21840
  email3 = userEmail;
21830
21841
  apiKey = userKey;
21831
- logger13.info("Upstash authenticated", { email: userEmail });
21842
+ logger11.info("Upstash authenticated", { email: userEmail });
21832
21843
  return ok(userEmail);
21833
21844
  } catch (error62) {
21834
- logger13.error("Upstash authentication failed", { error: error62 });
21845
+ logger11.error("Upstash authentication failed", { error: error62 });
21835
21846
  return err(
21836
21847
  HaiDeployError.AUTH_FAILED,
21837
21848
  deployM("deploy_authFailed", {
@@ -21848,7 +21859,7 @@ function createUpstashProvisioner() {
21848
21859
  deployM("deploy_authRequired")
21849
21860
  );
21850
21861
  }
21851
- logger13.debug("Provisioning Upstash Redis", { appName });
21862
+ logger11.debug("Provisioning Upstash Redis", { appName });
21852
21863
  let resource;
21853
21864
  try {
21854
21865
  const databaseName = `${appName}-cache`;
@@ -21860,7 +21871,7 @@ function createUpstashProvisioner() {
21860
21871
  }
21861
21872
  resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
21862
21873
  const detail = await getUpstashDatabase(email3, apiKey, databaseId);
21863
- logger13.info("Upstash Redis reused", { databaseId, databaseName });
21874
+ logger11.info("Upstash Redis reused", { databaseId, databaseName });
21864
21875
  return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
21865
21876
  }
21866
21877
  const data = await upstashFetch(email3, apiKey, "/v2/redis/database", {
@@ -21873,10 +21884,10 @@ function createUpstashProvisioner() {
21873
21884
  });
21874
21885
  if (data.database_id)
21875
21886
  resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
21876
- logger13.info("Upstash Redis provisioned", { databaseId: data.database_id });
21887
+ logger11.info("Upstash Redis provisioned", { databaseId: data.database_id });
21877
21888
  return ok(buildUpstashProvisionResult(data, "created"));
21878
21889
  } catch (error62) {
21879
- logger13.error("Upstash provisioning failed", { appName });
21890
+ logger11.error("Upstash provisioning failed", { appName });
21880
21891
  if (resource) {
21881
21892
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
21882
21893
  projectName: appName,
@@ -21900,7 +21911,7 @@ function createUpstashProvisioner() {
21900
21911
  }
21901
21912
 
21902
21913
  // ../deploy/src/deploy-main.ts
21903
- var logger14 = core.logger.child({ module: "deploy", scope: "main" });
21914
+ var logger12 = core.logger.child({ module: "deploy", scope: "main" });
21904
21915
  var currentConfig = null;
21905
21916
  var currentProvider = null;
21906
21917
  var currentContainerProvider = null;
@@ -21985,7 +21996,7 @@ var deploy = {
21985
21996
  credentials: deployCredentials,
21986
21997
  async init(config2) {
21987
21998
  if (initInProgress) {
21988
- logger14.warn("Deploy init already in progress, skipping concurrent call");
21999
+ logger12.warn("Deploy init already in progress, skipping concurrent call");
21989
22000
  return err(
21990
22001
  HaiDeployError.CONFIG_ERROR,
21991
22002
  deployM("deploy_configError", { params: { error: "init already in progress" } })
@@ -21994,13 +22005,13 @@ var deploy = {
21994
22005
  initInProgress = true;
21995
22006
  try {
21996
22007
  if (currentProvider || currentContainerProvider) {
21997
- logger14.warn("Deploy module is already initialized, reinitializing");
22008
+ logger12.warn("Deploy module is already initialized, reinitializing");
21998
22009
  await deploy.close();
21999
22010
  }
22000
- logger14.info("Initializing deploy module");
22011
+ logger12.info("Initializing deploy module");
22001
22012
  const parseResult = DeployConfigSchema.safeParse(config2);
22002
22013
  if (!parseResult.success) {
22003
- logger14.error("Deploy config validation failed", { error: parseResult.error.message });
22014
+ logger12.error("Deploy config validation failed", { error: parseResult.error.message });
22004
22015
  return err(
22005
22016
  HaiDeployError.CONFIG_ERROR,
22006
22017
  deployM("deploy_configError", { params: { error: parseResult.error.message } }),
@@ -22013,14 +22024,14 @@ var deploy = {
22013
22024
  let containerProvider = null;
22014
22025
  if (parsed.provider.type === "docker-ssh") {
22015
22026
  containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
22016
- logger14.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh.host });
22027
+ logger12.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh?.host });
22017
22028
  } else {
22018
22029
  provider = createProviderByType(parsed.provider.type);
22019
22030
  const authResult = await provider.authenticate(parsed.provider.token);
22020
22031
  if (!authResult.success) {
22021
22032
  return err(authResult.error);
22022
22033
  }
22023
- logger14.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
22034
+ logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
22024
22035
  }
22025
22036
  currentProvisioners.clear();
22026
22037
  if (parsed.services) {
@@ -22032,14 +22043,14 @@ var deploy = {
22032
22043
  const credentials = extractCredentials(serviceConfig);
22033
22044
  const provAuthResult = await provisioner.authenticate(credentials);
22034
22045
  if (!provAuthResult.success) {
22035
- logger14.error("Provisioner authentication failed", {
22046
+ logger12.error("Provisioner authentication failed", {
22036
22047
  service: serviceType,
22037
22048
  provisioner: serviceConfig.provisioner
22038
22049
  });
22039
22050
  currentProvisioners.clear();
22040
22051
  return err(provAuthResult.error);
22041
22052
  }
22042
- logger14.info("Provisioner authenticated", {
22053
+ logger12.info("Provisioner authenticated", {
22043
22054
  service: serviceType,
22044
22055
  user: provAuthResult.data
22045
22056
  });
@@ -22049,7 +22060,7 @@ var deploy = {
22049
22060
  currentProvider = provider;
22050
22061
  currentContainerProvider = containerProvider;
22051
22062
  currentConfig = parsed;
22052
- logger14.info("Deploy module initialized", {
22063
+ logger12.info("Deploy module initialized", {
22053
22064
  provider: parsed.provider.type,
22054
22065
  provisioners: [...currentProvisioners.keys()]
22055
22066
  });
@@ -22059,7 +22070,7 @@ var deploy = {
22059
22070
  currentContainerProvider = null;
22060
22071
  currentConfig = null;
22061
22072
  currentProvisioners.clear();
22062
- logger14.error("Deploy module initialization failed", { error: error62 });
22073
+ logger12.error("Deploy module initialization failed", { error: error62 });
22063
22074
  return err(
22064
22075
  HaiDeployError.CONFIG_ERROR,
22065
22076
  deployM("deploy_configError", {
@@ -22074,15 +22085,15 @@ var deploy = {
22074
22085
  },
22075
22086
  async close() {
22076
22087
  if (!currentProvider && !currentContainerProvider) {
22077
- logger14.info("Deploy module already closed, skipping");
22088
+ logger12.info("Deploy module already closed, skipping");
22078
22089
  return;
22079
22090
  }
22080
- logger14.info("Closing deploy module");
22091
+ logger12.info("Closing deploy module");
22081
22092
  currentProvider = null;
22082
22093
  currentContainerProvider = null;
22083
22094
  currentConfig = null;
22084
22095
  currentProvisioners.clear();
22085
- logger14.info("Deploy module closed");
22096
+ logger12.info("Deploy module closed");
22086
22097
  },
22087
22098
  get config() {
22088
22099
  return currentConfig;
@@ -22091,22 +22102,22 @@ var deploy = {
22091
22102
  return currentProvider !== null || currentContainerProvider !== null;
22092
22103
  },
22093
22104
  async scan(appDir, options) {
22094
- logger14.debug("Scanning application", { appDir });
22105
+ logger12.debug("Scanning application", { appDir });
22095
22106
  return scanApp(appDir, options);
22096
22107
  },
22097
22108
  async provisionAll(projectName, services) {
22098
22109
  if (!currentProvider && !currentContainerProvider) {
22099
22110
  return notInitialized.result();
22100
22111
  }
22101
- logger14.info("Provisioning all services", { projectName, count: currentProvisioners.size });
22112
+ logger12.info("Provisioning all services", { projectName, count: currentProvisioners.size });
22102
22113
  const results = [];
22103
22114
  for (const [serviceType, provisioner] of currentProvisioners) {
22104
22115
  if (services && !services.includes(serviceType))
22105
22116
  continue;
22106
- logger14.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
22117
+ logger12.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
22107
22118
  const provResult = await provisioner.provision(projectName);
22108
22119
  if (!provResult.success) {
22109
- logger14.error("Service provisioning failed", {
22120
+ logger12.error("Service provisioning failed", {
22110
22121
  serviceType,
22111
22122
  provisioner: provisioner.name,
22112
22123
  code: provResult.error.code
@@ -22123,7 +22134,7 @@ var deploy = {
22123
22134
  });
22124
22135
  }
22125
22136
  results.push(provResult.data);
22126
- logger14.info("Service provisioned", { serviceType, provisioner: provisioner.name });
22137
+ logger12.info("Service provisioned", { serviceType, provisioner: provisioner.name });
22127
22138
  }
22128
22139
  return ok(results);
22129
22140
  },
@@ -22131,7 +22142,7 @@ var deploy = {
22131
22142
  if (!currentProvider && !currentContainerProvider) {
22132
22143
  return notInitialized.result();
22133
22144
  }
22134
- logger14.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
22145
+ logger12.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
22135
22146
  const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
22136
22147
  const scanResult = await scanApp(appDir, scanOptions);
22137
22148
  if (!scanResult.success) {
@@ -22147,11 +22158,13 @@ var deploy = {
22147
22158
  options = {
22148
22159
  ...options,
22149
22160
  ...scanOptions.environment ? { environment: environment.data.environment } : {},
22150
- ...options?.skipProvision ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
22161
+ ...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
22151
22162
  };
22152
22163
  if (currentContainerProvider) {
22153
22164
  return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
22154
22165
  }
22166
+ if (options?.bundleOnly)
22167
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
22155
22168
  if (!currentProvider) {
22156
22169
  return notInitialized.result();
22157
22170
  }
@@ -22163,9 +22176,11 @@ var deploy = {
22163
22176
  })
22164
22177
  );
22165
22178
  }
22179
+ if (!existsSync(resolveOutputDir(appDir, "vercel")))
22180
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
22166
22181
  let allEnvVars = appEnv;
22167
22182
  const recovery = { projectName, stage: "provision", resources: [] };
22168
- if (!options?.skipProvision) {
22183
+ if (!options?.skipMiddleware) {
22169
22184
  const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
22170
22185
  if (!provResults.success) {
22171
22186
  return err(provResults.error);
@@ -22190,12 +22205,6 @@ var deploy = {
22190
22205
  return recoveryFailure(envResult.error, { ...recovery, stage: "set-env" });
22191
22206
  }
22192
22207
  }
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
22208
  const outputDir = resolveOutputDir(appDir, currentConfig?.provider.type ?? "vercel");
22200
22209
  const deployResult = await currentProvider.deploy(projectId, outputDir);
22201
22210
  if (!deployResult.success) {
@@ -22205,7 +22214,7 @@ var deploy = {
22205
22214
  ...deployResult.data,
22206
22215
  envVarsSet: Object.keys(allEnvVars)
22207
22216
  };
22208
- logger14.info("Deployment completed", {
22217
+ logger12.info("Deployment completed", {
22209
22218
  url: result.url,
22210
22219
  deploymentId: result.deploymentId,
22211
22220
  status: result.status
@@ -22215,21 +22224,30 @@ var deploy = {
22215
22224
  };
22216
22225
  async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
22217
22226
  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
- );
22227
+ let artifact;
22228
+ let artifactDir = options?.artifactDir ?? resolveArtifactDirectory(appDir);
22229
+ try {
22230
+ if (existsSync(join(artifactDir, "latest"))) {
22231
+ const latest = readFileSync(join(artifactDir, "latest"), "utf8").trim();
22232
+ if (!/^[a-z0-9][\w.+-]*$/i.test(latest))
22233
+ throw new Error("Invalid artifact pointer");
22234
+ artifactDir = join(artifactDir, latest);
22235
+ }
22236
+ artifact = await readDeploymentArtifact(artifactDir);
22237
+ } catch {
22238
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
22223
22239
  }
22240
+ if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
22241
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
22224
22242
  let provisionEnv = {};
22225
- if (!options?.skipProvision) {
22243
+ if (!options?.skipMiddleware) {
22226
22244
  const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
22227
22245
  if (!provResults.success) {
22228
22246
  return err(provResults.error);
22229
22247
  }
22230
22248
  provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
22231
22249
  }
22232
- return provider.deploy({ appDir, projectName, scan, provisionEnv, appEnv, options });
22250
+ return provider.deploy({ appDir, artifact, artifactDir, projectName, scan, provisionEnv, appEnv, options });
22233
22251
  }
22234
22252
 
22235
22253
  // ../deploy/src/deploy-middleware.ts
@@ -22277,6 +22295,6 @@ function describeMiddlewareBackend(backend) {
22277
22295
  return deployM(backend.purpose);
22278
22296
  }
22279
22297
 
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
22298
+ export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
22299
+ //# sourceMappingURL=src-RDQ2EIBT.js.map
22300
+ //# sourceMappingURL=src-RDQ2EIBT.js.map