@h-ai/cli 0.1.0-alpha.64 → 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 (122) hide show
  1. package/README.en.md +31 -9
  2. package/README.ja.md +30 -8
  3. package/README.md +38 -16
  4. package/dist/{chunk-X3RSYNBG.js → chunk-FWN5B54L.js} +94 -4
  5. package/dist/chunk-FWN5B54L.js.map +1 -0
  6. package/dist/chunk-QYX6V7H2.js +5613 -0
  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-QRF6B5T4.js.map → cli-config-templates-3XSFIXBP.js.map} +1 -1
  10. package/dist/{cli-skill-templates-EZFW7D43.js → cli-skill-templates-TSYEQTCY.js} +3 -3
  11. package/dist/{cli-skill-templates-EZFW7D43.js.map → cli-skill-templates-TSYEQTCY.js.map} +1 -1
  12. package/dist/index.js +733 -210
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src-DUGLX4YB.js → src-RDQ2EIBT.js} +1054 -660
  15. package/dist/{src-DUGLX4YB.js.map → src-RDQ2EIBT.js.map} +1 -1
  16. package/package.json +9 -7
  17. package/templates/apps/admin/src/routes/+layout.svelte.hbs +4 -2
  18. package/templates/apps/api/.dockerignore.hbs +4 -0
  19. package/templates/apps/api/.gitignore.hbs +6 -5
  20. package/templates/apps/api/Dockerfile.hbs +5 -8
  21. package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +2 -0
  22. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +2 -2
  23. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +2 -2
  24. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +4 -4
  25. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +8 -2
  26. package/templates/apps/api/hai.build.json.hbs +1 -0
  27. package/templates/apps/api/package.json.hbs +13 -7
  28. package/templates/apps/fullstack/.dockerignore.hbs +4 -3
  29. package/templates/apps/fullstack/.gitignore.hbs +24 -10
  30. package/templates/apps/fullstack/Dockerfile.hbs +6 -8
  31. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +0 -3
  32. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +5 -1
  33. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +8 -4
  34. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +3 -6
  35. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +6 -2
  36. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/lib.rs.hbs +73 -2
  37. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +2 -2
  38. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +8 -4
  39. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -6
  40. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +5 -1
  41. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +8 -4
  42. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +24 -1
  43. package/templates/apps/fullstack/hai.build.json.hbs +7 -0
  44. package/templates/apps/fullstack/package.json.hbs +16 -11
  45. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +2 -2
  46. package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +2 -0
  47. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +1 -1
  48. package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +8 -14
  49. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +8 -6
  50. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -8
  51. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +3 -3
  52. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +8 -2
  53. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +4 -4
  54. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +4 -2
  55. package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +2 -0
  56. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +14 -12
  57. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +1 -1
  58. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +17 -3
  59. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +15 -5
  60. package/templates/apps/fullstack/playwright.config.ts.hbs +6 -1
  61. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +0 -1
  62. package/templates/apps/h5/src/routes/+layout.svelte.hbs +13 -13
  63. package/templates/apps/mobile-app/index.html.hbs +1 -0
  64. package/templates/apps/mobile-app/public/runtime-config.js +1 -0
  65. package/templates/apps/mobile-app/src/App.svelte.hbs +46 -117
  66. package/templates/apps/mobile-app/src/lib/api.ts.hbs +2 -1
  67. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +3 -2
  68. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +58 -0
  69. package/templates/apps/mobile-app/src/lib/pages/profile.svelte.hbs +13 -0
  70. package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +16 -0
  71. package/templates/base/.dockerignore.hbs +4 -0
  72. package/templates/base/.gitignore.hbs +20 -5
  73. package/templates/base/Dockerfile.hbs +10 -11
  74. package/templates/base/docker/nginx.conf.hbs +6 -0
  75. package/templates/base/e2e/app.spec.ts.hbs +14 -0
  76. package/templates/base/hai.build.json.hbs +1 -0
  77. package/templates/base/package.json.hbs +25 -17
  78. package/templates/base/svelte.config.js.hbs +2 -0
  79. package/templates/base/tests/scaffold.test.ts.hbs +9 -3
  80. package/templates/base/vite.config.ts.hbs +11 -5
  81. package/templates/shared/.gitattributes +5 -0
  82. package/templates/shared/container-runtime-config.sh +30 -0
  83. package/templates/shared/hash-router.svelte.ts +83 -0
  84. package/templates/shared/run-e2e.mjs +65 -0
  85. package/templates/shared/vite-runtime-config.ts +54 -0
  86. package/templates/skills/AGENTS.md +15 -1
  87. package/templates/skills/bridges/admin/AGENTS.md +17 -1
  88. package/templates/skills/bridges/api/AGENTS.md +17 -3
  89. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +35 -11
  90. package/templates/skills/bridges/h5/AGENTS.md +17 -1
  91. package/templates/skills/bridges/mobile-app/AGENTS.md +19 -3
  92. package/templates/skills/bridges/website/AGENTS.md +17 -1
  93. package/templates/skills/hai-ai/reference.md +2 -2
  94. package/templates/skills/hai-app-create/SKILL.md +5 -3
  95. package/templates/skills/hai-app-create/development.md +128 -2
  96. package/templates/skills/hai-app-create/reference.md +4 -4
  97. package/templates/skills/hai-app-review/SKILL.md +2 -2
  98. package/templates/skills/hai-app-tests/SKILL.md +2 -1
  99. package/templates/skills/hai-app-tests/reference.md +1 -1
  100. package/templates/skills/hai-build/SKILL.md +16 -1
  101. package/templates/skills/hai-core/SKILL.md +2 -1
  102. package/templates/skills/hai-deploy/SKILL.md +52 -252
  103. package/templates/skills/hai-deploy/middleware.md +88 -0
  104. package/templates/skills/hai-framework-sync/SKILL.md +1 -1
  105. package/templates/skills/hai-framework-sync/sync.md +1 -1
  106. package/templates/skills/hai-reach/reference.md +8 -8
  107. package/templates/skills/hai-spec-implement/SKILL.md +12 -9
  108. package/templates/skills/hai-spec-implement/changes.md +67 -0
  109. package/templates/skills/hai-spec-implement/templates.md +25 -3
  110. package/templates/skills/hai-spec-implement/workflow.md +7 -6
  111. package/templates/skills/hai-spec-review/SKILL.md +14 -5
  112. package/templates/skills/hai-spec-review/checklist.md +4 -3
  113. package/templates/skills/hai-storage/SKILL.md +2 -0
  114. package/templates/skills/hai-ui/reference.md +2 -0
  115. package/dist/chunk-WW2MIO6T.js +0 -992
  116. package/dist/chunk-WW2MIO6T.js.map +0 -1
  117. package/dist/chunk-X3RSYNBG.js.map +0 -1
  118. package/dist/cli-config-templates-QRF6B5T4.js +0 -5
  119. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/AuditManagement.svelte.hbs → pages/audit.svelte.hbs} +0 -0
  120. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/RoleManagement.svelte.hbs → pages/roles.svelte.hbs} +0 -0
  121. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/ProfileSettings.svelte.hbs → pages/settings.svelte.hbs} +0 -0
  122. /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/UserManagement.svelte.hbs → pages/users.svelte.hbs} +0 -0
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { __export } from './chunk-2H7UOFLK.js';
3
- import { core, err, ok } from '@h-ai/core';
4
- import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync, statSync } 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
+ import { resolve, join, basename, dirname, relative, sep } from 'path';
6
+ import { core, ok, err } from '@h-ai/core';
7
+ import process2 from 'process';
8
+ import { parseEnv } from 'util';
9
+ import { parse as parse$1, stringify, parseAllDocuments } from 'yaml';
5
10
  import { homedir } from 'os';
6
- import { join, dirname, relative, sep } from 'path';
7
- import process from 'process';
8
- import { parse as parse$1, stringify } from 'yaml';
9
- import { execSync, spawn } from 'child_process';
10
- import { randomBytes, createHash } from 'crypto';
11
+ import { spawn } from 'child_process';
11
12
  import { Buffer } from 'buffer';
12
13
 
13
14
  // ../../node_modules/zod/v4/classic/external.js
@@ -1397,10 +1398,10 @@ function defineLazyInternal(inst, key, compute) {
1397
1398
  Object.defineProperty(this, key, { configurable: true, writable: true, value });
1398
1399
  broke = broke || outer;
1399
1400
  return value;
1400
- } catch (err16) {
1401
+ } catch (err15) {
1401
1402
  delete this[key];
1402
1403
  broke = broke || outer;
1403
- throw err16;
1404
+ throw err15;
1404
1405
  }
1405
1406
  },
1406
1407
  set(value) {
@@ -1855,18 +1856,18 @@ var validateAsync = async (schema, value, _ctx) => {
1855
1856
  return result.issues.length === 0;
1856
1857
  };
1857
1858
  var _encode = (_Err) => {
1858
- const parse5 = _parse(_Err);
1859
+ const parse7 = _parse(_Err);
1859
1860
  const fn = (schema, value, _ctx, _params) => {
1860
1861
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
1861
- return parse5(schema, value, ctx, finalizeParams(fn, _params));
1862
+ return parse7(schema, value, ctx, finalizeParams(fn, _params));
1862
1863
  };
1863
1864
  return fn;
1864
1865
  };
1865
1866
  var encode = /* @__PURE__ */ _encode($ZodRealError);
1866
1867
  var _decode = (_Err) => {
1867
- const parse5 = _parse(_Err);
1868
+ const parse7 = _parse(_Err);
1868
1869
  const fn = (schema, value, _ctx, _params) => {
1869
- return parse5(schema, value, _ctx, finalizeParams(fn, _params));
1870
+ return parse7(schema, value, _ctx, finalizeParams(fn, _params));
1870
1871
  };
1871
1872
  return fn;
1872
1873
  };
@@ -3520,14 +3521,14 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
3520
3521
  const shape = zod.def.shape;
3521
3522
  const propValues = {};
3522
3523
  for (const key in shape) {
3523
- const field = shape[key]._zod;
3524
- if (field.values) {
3524
+ const field2 = shape[key]._zod;
3525
+ if (field2.values) {
3525
3526
  if (!Object.prototype.hasOwnProperty.call(propValues, key)) {
3526
3527
  assignProp(propValues, key, /* @__PURE__ */ new Set());
3527
3528
  }
3528
- for (const v of field.values)
3529
+ for (const v of field2.values)
3529
3530
  propValues[key].add(v);
3530
- if (field.optin !== void 0)
3531
+ if (field2.optin !== void 0)
3531
3532
  propValues[key].add(void 0);
3532
3533
  }
3533
3534
  }
@@ -5036,7 +5037,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5036
5037
  var NONE = 0;
5037
5038
  var ASSUMED = 1;
5038
5039
  var PROVEN = 2;
5039
- function isRecursive(inst, stack, resolve) {
5040
+ function isRecursive(inst, stack, resolve6) {
5040
5041
  const cached2 = recursive.get(inst);
5041
5042
  if (cached2 !== void 0)
5042
5043
  return cached2 ? PROVEN : NONE;
@@ -5046,7 +5047,7 @@ function isRecursive(inst, stack, resolve) {
5046
5047
  let result = NONE;
5047
5048
  const check2 = (child) => {
5048
5049
  if (result !== PROVEN && child?._zod) {
5049
- const answer = isRecursive(child, stack, resolve);
5050
+ const answer = isRecursive(child, stack, resolve6);
5050
5051
  if (answer > result)
5051
5052
  result = answer;
5052
5053
  }
@@ -5057,7 +5058,7 @@ function isRecursive(inst, stack, resolve) {
5057
5058
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5058
5059
  if (!desc.enumerable)
5059
5060
  continue;
5060
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve) : NONE;
5061
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
5061
5062
  if (child > answer)
5062
5063
  answer = child;
5063
5064
  }
@@ -5121,7 +5122,7 @@ function isRecursive(inst, stack, resolve) {
5121
5122
  break;
5122
5123
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5123
5124
  case "lazy": {
5124
- const inner = def._cachedInner ?? (resolve ? inst._zod.innerType : void 0);
5125
+ const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
5125
5126
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5126
5127
  break;
5127
5128
  }
@@ -12939,9 +12940,9 @@ function compile(schema, options) {
12939
12940
  const clone2 = withParser(schema, parser);
12940
12941
  clone2._zod.bag.validator = compileValidator(schema, parser);
12941
12942
  return clone2;
12942
- } catch (err16) {
12943
+ } catch (err15) {
12943
12944
  if (options?.strict)
12944
- throw err16;
12945
+ throw err15;
12945
12946
  return schema;
12946
12947
  }
12947
12948
  }
@@ -13031,8 +13032,8 @@ ${code}
13031
13032
  try {
13032
13033
  const factory = new F(...constantNames, factoryCode);
13033
13034
  fn = factory(...constantValues);
13034
- } catch (err16) {
13035
- throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${err16.message})`);
13035
+ } catch (err15) {
13036
+ throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${err15.message})`);
13036
13037
  }
13037
13038
  if (options?.debug) {
13038
13039
  fn.code = fullCode;
@@ -13070,9 +13071,9 @@ function compileChild(doc, ctx, schema, accessor, needsValue = true) {
13070
13071
  const varCounter = ctx.varCounter;
13071
13072
  try {
13072
13073
  return generateCheck(doc, ctx, schema, accessor, needsValue);
13073
- } catch (err16) {
13074
- if (!(err16 instanceof ZodCompileUnsupportedError) || !err16.islandable)
13075
- throw err16;
13074
+ } catch (err15) {
13075
+ if (!(err15 instanceof ZodCompileUnsupportedError) || !err15.islandable)
13076
+ throw err15;
13076
13077
  doc.content.length = contentLen;
13077
13078
  if (ctx.constants.size > constantCount) {
13078
13079
  const trailing = Array.from(ctx.constants.keys()).slice(constantCount);
@@ -15806,7 +15807,7 @@ function foldObjects(members2) {
15806
15807
  objects.push(member);
15807
15808
  }
15808
15809
  const properties = {};
15809
- const required2 = /* @__PURE__ */ new Set();
15810
+ const required3 = /* @__PURE__ */ new Set();
15810
15811
  for (const object2 of objects) {
15811
15812
  for (const key in object2.properties) {
15812
15813
  if (Object.prototype.hasOwnProperty.call(properties, key))
@@ -15823,11 +15824,11 @@ function foldObjects(members2) {
15823
15824
  assignProp(properties, key, merged);
15824
15825
  }
15825
15826
  for (const key of object2.required ?? [])
15826
- required2.add(key);
15827
+ required3.add(key);
15827
15828
  }
15828
15829
  const folded = { type: "object", properties };
15829
- if (required2.size)
15830
- folded.required = [...required2];
15830
+ if (required3.size)
15831
+ folded.required = [...required3];
15831
15832
  if (objects.every((object2) => object2.additionalProperties === false)) {
15832
15833
  folded.additionalProperties = false;
15833
15834
  } else {
@@ -16445,8 +16446,8 @@ var objectProcessor = (schema, ctx, _json, params) => {
16445
16446
  }
16446
16447
  const requiredKeys = [];
16447
16448
  for (const key of Object.keys(shape)) {
16448
- const field = def.shape[key];
16449
- if (ctx.io === "input" ? inputOptin(field) === void 0 : field._zod.optout === void 0) {
16449
+ const field2 = def.shape[key];
16450
+ if (ctx.io === "input" ? inputOptin(field2) === void 0 : field2._zod.optout === void 0) {
16450
16451
  requiredKeys.push(key);
16451
16452
  }
16452
16453
  }
@@ -16604,8 +16605,8 @@ function rewriteKeyNames(ctx) {
16604
16605
  bySchema.set(entry.schema, entry);
16605
16606
  }
16606
16607
  const rewrites = /* @__PURE__ */ new Map();
16607
- for (const record2 of pendingRecords.get(ctx) ?? []) {
16608
- const seen = ctx.seen.get(record2);
16608
+ for (const record3 of pendingRecords.get(ctx) ?? []) {
16609
+ const seen = ctx.seen.get(record3);
16609
16610
  const names = (seen?.def ?? seen?.schema)?.propertyNames;
16610
16611
  if (!names || names === true || rewrites.has(names))
16611
16612
  continue;
@@ -19667,6 +19668,44 @@ function date4(params) {
19667
19668
  return _coercedDate(ZodDate, params);
19668
19669
  }
19669
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
+
19670
19709
  // ../deploy/messages/en-US.json
19671
19710
  var en_US_default = {
19672
19711
  deploy_notInitialized: "Deploy module not initialized, call deploy.init() first",
@@ -19677,7 +19716,6 @@ var en_US_default = {
19677
19716
  deploy_authRequired: "Authentication required",
19678
19717
  deploy_authFailed: "Authentication failed: {error}",
19679
19718
  deploy_projectCreateFailed: "Project creation failed: {error}",
19680
- deploy_buildFailed: "Application build failed: {error}",
19681
19719
  deploy_uploadFailed: "Build output upload failed: {error}",
19682
19720
  deploy_deployFailed: "Deployment failed: {error}",
19683
19721
  deploy_provisionFailed: "Service provisioning failed: {service} - {error}",
@@ -19697,12 +19735,33 @@ var en_US_default = {
19697
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.",
19698
19736
  deploy_commandTimeout: "Command timed out: {command}",
19699
19737
  deploy_commandFailed: "Command failed ({command}): {error}",
19700
- deploy_containerRuntimeNotFound: "No usable container runtime found: {runtime}",
19701
- deploy_containerBuildFailed: "Container image build failed: {error}",
19702
19738
  deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
19703
19739
  deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
19704
19740
  deploy_imageTransferFailed: "Image transfer failed: {error}",
19705
- deploy_healthCheckFailed: "Health check failed, app not ready at {url}"
19741
+ deploy_healthCheckFailed: "Health check failed, app not ready at {url}",
19742
+ deploy_invalidEnvironment: "Use letters, numbers, underscores or hyphens for the environment; local/example are reserved.",
19743
+ deploy_environmentMissing: "Environment file missing: {path}. Create and configure it before deploying.",
19744
+ deploy_environmentReadFailed: "Cannot read deployment environment files; check syntax and permissions.",
19745
+ deploy_environmentWriteFailed: "Cannot save deployment configuration; check file syntax and permissions.",
19746
+ deploy_multipleApplications: "Multiple service packages found; specify the application directory.",
19747
+ deploy_purposeSqlite: "Local relational database persisted in a volume",
19748
+ deploy_purposeDatabase: "Relational database service for business data",
19749
+ deploy_purposeMemory: "Per-process memory cache; cleared on restart, no external service",
19750
+ deploy_purposeLocal: "Local filesystem storage persisted in a volume",
19751
+ deploy_purposeCache: "Shared cache, sessions and distributed locks",
19752
+ deploy_purposeStorage: "S3-compatible object storage for uploaded files",
19753
+ deploy_purposeVector: "Vector indexing and similarity search",
19754
+ deploy_purposeDisabled: "LDAP login is disabled; no directory service is created",
19755
+ deploy_purposeLdap: "Directory account authentication; the default directory has no business users",
19756
+ deploy_purposeConsole: "Messages go to application logs without delivery",
19757
+ deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
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."
19706
19765
  };
19707
19766
 
19708
19767
  // ../deploy/messages/zh-CN.json
@@ -19715,7 +19774,6 @@ var zh_CN_default2 = {
19715
19774
  deploy_authRequired: "\u8BF7\u5148\u5B8C\u6210\u8BA4\u8BC1",
19716
19775
  deploy_authFailed: "\u8BA4\u8BC1\u5931\u8D25\uFF1A{error}",
19717
19776
  deploy_projectCreateFailed: "\u9879\u76EE\u521B\u5EFA\u5931\u8D25\uFF1A{error}",
19718
- deploy_buildFailed: "\u5E94\u7528\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
19719
19777
  deploy_uploadFailed: "\u6784\u5EFA\u4EA7\u7269\u4E0A\u4F20\u5931\u8D25\uFF1A{error}",
19720
19778
  deploy_deployFailed: "\u90E8\u7F72\u5931\u8D25\uFF1A{error}",
19721
19779
  deploy_provisionFailed: "\u57FA\u7840\u8BBE\u65BD\u5F00\u901A\u5931\u8D25\uFF1A{service} - {error}",
@@ -19735,12 +19793,33 @@ var zh_CN_default2 = {
19735
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",
19736
19794
  deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
19737
19795
  deploy_commandFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08{command}\uFF09\uFF1A{error}",
19738
- deploy_containerRuntimeNotFound: "\u672A\u627E\u5230\u53EF\u7528\u7684\u5BB9\u5668\u8FD0\u884C\u65F6\uFF1A{runtime}",
19739
- deploy_containerBuildFailed: "\u5BB9\u5668\u955C\u50CF\u6784\u5EFA\u5931\u8D25\uFF1A{error}",
19740
19796
  deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
19741
19797
  deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
19742
19798
  deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
19743
- deploy_healthCheckFailed: "\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF0C\u5E94\u7528\u672A\u5728 {url} \u5C31\u7EEA"
19799
+ deploy_healthCheckFailed: "\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF0C\u5E94\u7528\u672A\u5728 {url} \u5C31\u7EEA",
19800
+ deploy_invalidEnvironment: "\u73AF\u5883\u540D\u79F0\u4EC5\u5141\u8BB8\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u548C\u8FDE\u5B57\u7B26\uFF0C\u4E14\u4E0D\u80FD\u4F7F\u7528 local/example\u3002",
19801
+ deploy_environmentMissing: "\u7F3A\u5C11\u73AF\u5883\u914D\u7F6E {path}\uFF1B\u8BF7\u5148\u521B\u5EFA\u8BE5\u6587\u4EF6\u5E76\u5B8C\u6210\u914D\u7F6E\uFF0C\u518D\u91CD\u65B0\u90E8\u7F72\u3002",
19802
+ deploy_environmentReadFailed: "\u90E8\u7F72\u73AF\u5883\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u683C\u5F0F\u4E0E\u6743\u9650\u3002",
19803
+ deploy_environmentWriteFailed: "\u90E8\u7F72\u914D\u7F6E\u5199\u5165\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u683C\u5F0F\u4E0E\u6743\u9650\u3002",
19804
+ deploy_multipleApplications: "\u53D1\u73B0\u591A\u4E2A\u670D\u52A1\u5305\uFF0C\u8BF7\u6307\u5B9A\u8981\u90E8\u7F72\u7684\u5E94\u7528\u76EE\u5F55\u3002",
19805
+ deploy_purposeSqlite: "\u672C\u5730\u6587\u4EF6\u5173\u7CFB\u6570\u636E\u5E93\uFF0C\u4F7F\u7528\u6301\u4E45\u5316\u5377",
19806
+ deploy_purposeDatabase: "\u72EC\u7ACB\u5173\u7CFB\u6570\u636E\u5E93\u670D\u52A1\uFF0C\u4FDD\u5B58\u4E1A\u52A1\u6570\u636E",
19807
+ deploy_purposeMemory: "\u5355\u8FDB\u7A0B\u5185\u5B58\u7F13\u5B58\uFF0C\u91CD\u542F\u540E\u6E05\u7A7A\uFF0C\u4E0D\u521B\u5EFA\u5916\u90E8\u670D\u52A1",
19808
+ deploy_purposeLocal: "\u672C\u5730\u6587\u4EF6\u5B58\u50A8\uFF0C\u4F7F\u7528\u6301\u4E45\u5316\u5377",
19809
+ deploy_purposeCache: "\u5171\u4EAB\u7F13\u5B58\u3001\u4F1A\u8BDD\u4E0E\u5206\u5E03\u5F0F\u9501",
19810
+ deploy_purposeStorage: "S3 \u517C\u5BB9\u5BF9\u8C61\u5B58\u50A8\uFF0C\u4FDD\u5B58\u4E0A\u4F20\u6587\u4EF6",
19811
+ deploy_purposeVector: "\u5411\u91CF\u7D22\u5F15\u4E0E\u76F8\u4F3C\u5EA6\u641C\u7D22",
19812
+ deploy_purposeDisabled: "LDAP \u767B\u5F55\u672A\u542F\u7528\uFF0C\u4E0D\u521B\u5EFA\u76EE\u5F55\u670D\u52A1",
19813
+ deploy_purposeLdap: "\u4F01\u4E1A\u76EE\u5F55\u8D26\u53F7\u8BA4\u8BC1\uFF1B\u9ED8\u8BA4\u76EE\u5F55\u4E0D\u5305\u542B\u4E1A\u52A1\u7528\u6237",
19814
+ deploy_purposeConsole: "\u6D88\u606F\u8F93\u51FA\u5230\u5E94\u7528\u65E5\u5FD7\uFF0C\u4E0D\u5B9E\u9645\u6295\u9012",
19815
+ deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
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"
19744
19823
  };
19745
19824
 
19746
19825
  // ../deploy/src/deploy-i18n.ts
@@ -19773,15 +19852,15 @@ var SshConfigSchema = external_exports.object({
19773
19852
  var PortExposeSchema = external_exports.object({
19774
19853
  type: external_exports.literal("port").default("port"),
19775
19854
  /** 容器内监听端口(adapter-node 默认 3000) */
19776
- containerPort: external_exports.number().int().positive().default(3e3),
19855
+ containerPort: external_exports.number().int().min(1).max(65535).default(3e3),
19777
19856
  /** 宿主机映射端口 */
19778
- hostPort: external_exports.number().int().positive()
19857
+ hostPort: external_exports.number().int().min(1).max(65535)
19779
19858
  });
19780
19859
  var ExposeSchema = PortExposeSchema;
19781
19860
  var DockerSshProviderConfigSchema = external_exports.object({
19782
19861
  type: external_exports.literal("docker-ssh"),
19783
19862
  /** SSH 连接参数 */
19784
- ssh: SshConfigSchema,
19863
+ ssh: SshConfigSchema.optional(),
19785
19864
  /** 远程主机运行参数 */
19786
19865
  remote: external_exports.object({
19787
19866
  /** 远程应用根目录 */
@@ -19831,17 +19910,19 @@ var ServicesConfigSchema = external_exports.object({
19831
19910
  sms: external_exports.discriminatedUnion("provisioner", [AliyunSmsServiceSchema]).optional()
19832
19911
  }).optional();
19833
19912
  var DeployConfigSchema = external_exports.object({
19913
+ /** 部署环境文件模式;dev/test/prod 或自定义模式,禁止 local/example。 */
19914
+ environment: external_exports.string().regex(/^[a-z0-9][\w-]*$/i).refine((value) => !["local", "example"].includes(value.toLowerCase())).optional(),
19834
19915
  provider: ProviderConfigSchema,
19835
19916
  services: ServicesConfigSchema,
19836
- /** 本地容器构建参数(docker-ssh 使用;auto 优先 podman) */
19917
+ /** Docker Compose 部署参数;构建配置位于项目 hai.build.json。 */
19837
19918
  container: external_exports.object({
19838
- runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto")
19919
+ /** 自建 S3 服务实现;协议仍为 storage.type=s3。 */
19920
+ s3Engine: external_exports.enum(["rustfs", "minio"]).default("rustfs")
19839
19921
  }).optional()
19840
19922
  });
19841
19923
  var DeployErrorInfo = {
19842
19924
  DEPLOY_FAILED: "001:500",
19843
19925
  PROJECT_CREATE_FAILED: "002:500",
19844
- BUILD_FAILED: "003:500",
19845
19926
  UPLOAD_FAILED: "004:500",
19846
19927
  AUTH_REQUIRED: "005:401",
19847
19928
  AUTH_FAILED: "006:401",
@@ -19853,8 +19934,6 @@ var DeployErrorInfo = {
19853
19934
  UNSUPPORTED_TYPE: "012:400",
19854
19935
  CONFIG_ERROR: "013:500",
19855
19936
  CREDENTIAL_ERROR: "014:500",
19856
- CONTAINER_RUNTIME_NOT_FOUND: "015:500",
19857
- CONTAINER_BUILD_FAILED: "016:500",
19858
19937
  REMOTE_CONNECT_FAILED: "017:500",
19859
19938
  REMOTE_COMMAND_FAILED: "018:500",
19860
19939
  IMAGE_TRANSFER_FAILED: "019:500",
@@ -19862,7 +19941,172 @@ var DeployErrorInfo = {
19862
19941
  };
19863
19942
  var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
19864
19943
 
19865
- // ../deploy/src/deploy-credentials.ts
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
+ }
19952
+ function resolveConfig(value, prefix, env) {
19953
+ if (env[prefix] !== void 0)
19954
+ return environmentValue(env[prefix]);
19955
+ if (Array.isArray(value))
19956
+ return value.map((item, index) => resolveConfig(item, `${prefix}_${index}`, env));
19957
+ if (value && typeof value === "object")
19958
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, resolveConfig(nested, `${prefix}_${key.toUpperCase()}`, env)]));
19959
+ if (typeof value !== "string")
19960
+ return value;
19961
+ const resolved = value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, name, fallback) => env[name] ?? fallback ?? "");
19962
+ return /^\$\{[^}]+\}$/.test(value) ? environmentValue(resolved) : resolved;
19963
+ }
19964
+ function loadDeploymentConfig(name, path, values) {
19965
+ try {
19966
+ const config2 = resolveConfig(parse$1(readFileSync(path, "utf8")), `HAI_${name.toUpperCase()}`, values);
19967
+ return ok(config2 && typeof config2 === "object" && !Array.isArray(config2) ? config2 : {});
19968
+ } catch {
19969
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
19970
+ }
19971
+ }
19972
+ function loadDeploymentEnvironment(options) {
19973
+ const environment = options.environment === "production" ? "prod" : options.environment === "development" ? "dev" : options.environment;
19974
+ if (!/^[a-z0-9][\w-]*$/i.test(environment) || ["local", "example"].includes(environment.toLowerCase()))
19975
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_invalidEnvironment"));
19976
+ const envDir = resolve(options.envDir);
19977
+ const target = join(envDir, `.env.${environment}`);
19978
+ if (options.requireFile !== false && !existsSync(target))
19979
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentMissing", { params: { path: target } }));
19980
+ const loaded = core.config.loadEnvFiles({ mode: environment, envDir });
19981
+ if (!loaded.success)
19982
+ return loaded;
19983
+ try {
19984
+ const values = { ...loaded.data };
19985
+ for (const [name, value] of Object.entries(process2.env)) {
19986
+ if (/^(?:HAI_|PUBLIC_|VITE_)/.test(name) && value !== void 0)
19987
+ values[name] = value;
19988
+ }
19989
+ values.HAI_DEPLOY_ENVIRONMENT = environment;
19990
+ values.HAI_CORE_ENV ??= environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
19991
+ const targetValues = {};
19992
+ if (existsSync(target)) {
19993
+ for (const [name, value] of Object.entries(parseEnv(readFileSync(target, "utf8")))) {
19994
+ if (value !== void 0)
19995
+ targetValues[name] = value;
19996
+ }
19997
+ }
19998
+ return ok({ environment, envDir, values, targetValues });
19999
+ } catch {
20000
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
20001
+ }
20002
+ }
20003
+ function isSecret(name, value) {
20004
+ if (/PASSWORD|SECRET|TOKEN|APIKEY|PRIVATEKEY|ACCESSKEY|AUTHORIZATION/i.test(name.replace(/[^a-z0-9]/gi, "")) || /(?:^|_)PASS$/i.test(name) || value.includes("PRIVATE KEY"))
20005
+ return true;
20006
+ try {
20007
+ const url2 = new URL(value);
20008
+ if (url2.username || url2.password || [...url2.searchParams.keys()].some((key) => /password|secret|token|key|authorization/i.test(key)))
20009
+ return true;
20010
+ } catch {
20011
+ }
20012
+ try {
20013
+ const parsed = JSON.parse(value);
20014
+ if (parsed && typeof parsed === "object")
20015
+ return Object.entries(parsed).some(([key, nested]) => isSecret(key, typeof nested === "string" ? nested : JSON.stringify(nested)));
20016
+ } catch {
20017
+ }
20018
+ return false;
20019
+ }
20020
+ function publicConfig(value) {
20021
+ if (Array.isArray(value))
20022
+ return value.map(publicConfig);
20023
+ if (value && typeof value === "object")
20024
+ return Object.fromEntries(Object.entries(value).filter(([name, item]) => !isSecret(name, typeof item === "string" ? item : "")).map(([name, item]) => [name, publicConfig(item)]));
20025
+ return value;
20026
+ }
20027
+ function quoteEnv(value) {
20028
+ try {
20029
+ const parsed = JSON.parse(value);
20030
+ if (parsed && typeof parsed === "object")
20031
+ return `'${JSON.stringify(parsed).replaceAll("'", "\\u0027")}'`;
20032
+ } catch {
20033
+ }
20034
+ if (!value.includes("'"))
20035
+ return `'${value}'`;
20036
+ if (!value.includes('"'))
20037
+ return `"${value}"`;
20038
+ if (!value.includes("`"))
20039
+ return `\`${value}\``;
20040
+ if (!/[\r\n#]/.test(value))
20041
+ return value;
20042
+ throw new Error("Unsupported dotenv value");
20043
+ }
20044
+ function updateFile(path, values, privateFile) {
20045
+ let content = existsSync(path) ? readFileSync(path, "utf8") : "# Local private deployment configuration; do not commit.\n";
20046
+ for (const [name, value] of Object.entries(values)) {
20047
+ if (!/^[A-Z][A-Z0-9_]*$/.test(name))
20048
+ throw new Error("Invalid environment variable name");
20049
+ const line = `${name}=${quoteEnv(value)}`;
20050
+ const pattern = new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"[^"]*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*`, "gm");
20051
+ if (pattern.test(content))
20052
+ content = content.replace(pattern, () => line);
20053
+ else
20054
+ content = `${content.trimEnd()}
20055
+ ${line}
20056
+ `;
20057
+ }
20058
+ writeFileSync(path, content, { mode: privateFile ? 384 : 420 });
20059
+ if (privateFile)
20060
+ chmodSync(path, 384);
20061
+ }
20062
+ function removeOverrides(path, names, privateFile = true) {
20063
+ if (!existsSync(path) || names.length === 0)
20064
+ return;
20065
+ let content = readFileSync(path, "utf8");
20066
+ for (const name of names)
20067
+ content = content.replace(new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"[^"]*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
20068
+ writeFileSync(path, content, { mode: privateFile ? 384 : 420 });
20069
+ chmodSync(path, privateFile ? 384 : 420);
20070
+ }
20071
+ function saveDeploymentEnvironment(environment, values, options = {}) {
20072
+ try {
20073
+ const shared = {};
20074
+ const privateValues = {};
20075
+ for (const [name, value] of Object.entries(values)) {
20076
+ if (options.private) {
20077
+ privateValues[name] = value;
20078
+ } else if (isSecret(name, value)) {
20079
+ privateValues[name] = value;
20080
+ try {
20081
+ const parsed = JSON.parse(value);
20082
+ if (parsed && typeof parsed === "object")
20083
+ shared[name] = JSON.stringify(publicConfig(parsed));
20084
+ } catch {
20085
+ }
20086
+ } else {
20087
+ shared[name] = value;
20088
+ }
20089
+ }
20090
+ if (Object.keys(shared).length)
20091
+ updateFile(join(environment.envDir, `.env.${environment.environment}`), shared, false);
20092
+ if (Object.keys(privateValues).length)
20093
+ updateFile(join(environment.envDir, ".env.local"), privateValues, true);
20094
+ removeOverrides(join(environment.envDir, `.env.${environment.environment}`), Object.keys(privateValues).filter((name) => !(name in shared)), false);
20095
+ removeOverrides(join(environment.envDir, ".env.local"), Object.keys(shared).filter((name) => !(name in privateValues)));
20096
+ if (Object.keys(privateValues).length) {
20097
+ const ignorePath = join(environment.envDir, ".gitignore");
20098
+ const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
20099
+ if (!ignore.split(/\r?\n/).includes(".env.local"))
20100
+ writeFileSync(ignorePath, `${ignore.trimEnd()}
20101
+ .env.local
20102
+ .hai/deploy/
20103
+ `);
20104
+ }
20105
+ return ok(Object.keys(values));
20106
+ } catch {
20107
+ return err(HaiDeployError.ENV_VAR_FAILED, deployM("deploy_environmentWriteFailed"));
20108
+ }
20109
+ }
19866
20110
  var logger = core.logger.child({ module: "deploy", scope: "credentials" });
19867
20111
  var DEPLOY_CREDENTIAL_KEY_PATTERN = /^HAI_DEPLOY_[A-Z0-9_]+$/;
19868
20112
  function isAllowedCredentialKey(key) {
@@ -19926,7 +20170,7 @@ function loadCredentials() {
19926
20170
  const data = readCredentialsFile();
19927
20171
  const keys = Object.keys(data);
19928
20172
  for (const key of keys) {
19929
- process.env[key] = String(data[key]);
20173
+ process2.env[key] = String(data[key]);
19930
20174
  }
19931
20175
  logger.debug("Credentials loaded", { count: keys.length });
19932
20176
  return ok(keys);
@@ -19947,7 +20191,7 @@ function saveCredential(key, value) {
19947
20191
  const data = readCredentialsFile();
19948
20192
  data[key] = value;
19949
20193
  writeCredentialsFile(data);
19950
- process.env[key] = value;
20194
+ process2.env[key] = value;
19951
20195
  logger.debug("Credential saved", { key });
19952
20196
  return ok(void 0);
19953
20197
  } catch (error62) {
@@ -19967,7 +20211,7 @@ function saveCredentials(entries) {
19967
20211
  const data = readCredentialsFile();
19968
20212
  for (const [key, value] of Object.entries(entries)) {
19969
20213
  data[key] = value;
19970
- process.env[key] = value;
20214
+ process2.env[key] = value;
19971
20215
  }
19972
20216
  writeCredentialsFile(data);
19973
20217
  logger.debug("Credentials saved", { count: Object.keys(entries).length });
@@ -19983,35 +20227,25 @@ function saveCredentials(entries) {
19983
20227
  );
19984
20228
  }
19985
20229
  }
19986
- var logger2 = core.logger.child({ module: "deploy", scope: "functions" });
19987
- function buildApp(appDir, buildCommand, envVars = {}) {
19988
- logger2.info("Building application", { appDir, buildCommand });
19989
- try {
19990
- execSync(buildCommand, {
19991
- cwd: appDir,
19992
- stdio: "inherit",
19993
- env: { ...process.env, ...envVars }
19994
- });
19995
- return ok(void 0);
19996
- } catch (error62) {
19997
- return err(
19998
- HaiDeployError.BUILD_FAILED,
19999
- deployM("deploy_buildFailed", {
20000
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
20001
- }),
20002
- error62
20003
- );
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;
20004
20239
  }
20005
20240
  }
20241
+ function usesDefaultMiddleware(scan, service, options) {
20242
+ const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
20243
+ return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
20244
+ }
20006
20245
  function resolveOutputDir(appDir, providerType) {
20007
20246
  switch (providerType) {
20008
- case "vercel": {
20009
- const vercelOutput = join(appDir, ".vercel", "output");
20010
- if (existsSync(vercelOutput)) {
20011
- return vercelOutput;
20012
- }
20013
- return join(appDir, "build");
20014
- }
20247
+ case "vercel":
20248
+ return join(appDir, ".vercel", "output");
20015
20249
  default:
20016
20250
  return join(appDir, "build");
20017
20251
  }
@@ -20042,196 +20276,300 @@ function recoveryFailure(error62, recovery) {
20042
20276
  ext: { recovery }
20043
20277
  });
20044
20278
  }
20045
- var logger3 = core.logger.child({ module: "deploy", scope: "scanner" });
20046
- function readYaml(filePath) {
20047
- if (!existsSync(filePath)) {
20048
- return null;
20049
- }
20050
- try {
20051
- const content = readFileSync(filePath, "utf-8");
20052
- const parsed = parse$1(content);
20053
- if (parsed === null || parsed === void 0 || typeof parsed !== "object") {
20054
- return null;
20279
+ var logger2 = core.logger.child({ module: "deploy", scope: "scanner" });
20280
+ function record2(value) {
20281
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20282
+ }
20283
+ function runtimeDependencies(appDir) {
20284
+ let root = appDir;
20285
+ while (!existsSync(join(root, "pnpm-lock.yaml"))) {
20286
+ const parent = dirname(root);
20287
+ if (parent === root)
20288
+ throw new Error("Missing pnpm lockfile; run pnpm install");
20289
+ root = parent;
20290
+ }
20291
+ const importerKey = relative(root, appDir).replaceAll("\\", "/") || ".";
20292
+ const documents = parseAllDocuments(readFileSync(join(root, "pnpm-lock.yaml"), "utf8"));
20293
+ if (documents.some((document) => document.errors.length))
20294
+ throw new Error("Invalid pnpm lockfile");
20295
+ const lock = documents.map((document) => record2(document.toJS())).reverse().find((document) => importerKey in record2(document.importers));
20296
+ if (!lock)
20297
+ throw new Error("Application is missing from pnpm lockfile; run pnpm install");
20298
+ const importers = record2(lock.importers);
20299
+ const packages = record2(lock.packages);
20300
+ const snapshots = record2(lock.snapshots);
20301
+ const names = /* @__PURE__ */ new Set();
20302
+ const visited = /* @__PURE__ */ new Set();
20303
+ const visit2 = (id, node2, importer) => {
20304
+ if (visited.has(id))
20305
+ return;
20306
+ visited.add(id);
20307
+ const metadata = importer === void 0 ? record2(packages[id.split("(")[0]]) : {};
20308
+ const peers = record2(metadata.peerDependencies);
20309
+ for (const [name, reference] of Object.entries({ ...record2(node2.dependencies), ...record2(node2.optionalDependencies) })) {
20310
+ if (name in peers)
20311
+ continue;
20312
+ const version2 = typeof reference === "string" ? reference : record2(reference).version;
20313
+ if (typeof version2 !== "string")
20314
+ throw new Error("Invalid pnpm dependency reference");
20315
+ if (version2.startsWith("link:")) {
20316
+ if (importer === void 0)
20317
+ throw new Error("Invalid workspace dependency");
20318
+ const target = relative(root, resolve(root, importer, version2.slice(5))).replaceAll("\\", "/") || ".";
20319
+ const linked = record2(importers[target]);
20320
+ if (!Object.keys(linked).length)
20321
+ throw new Error("Missing linked workspace importer");
20322
+ names.add(name);
20323
+ visit2(`workspace:${target}`, linked, target);
20324
+ continue;
20325
+ }
20326
+ const key = version2 in snapshots || version2 in packages ? version2 : `${name}@${version2}`;
20327
+ names.add(key.match(/^(@[^/]+\/[^@]+|[^@]+)@/)?.[1] ?? name);
20328
+ const snapshot = record2(snapshots[key] ?? packages[key]);
20329
+ if (!Object.keys(snapshot).length && !(key in snapshots) && !(key in packages))
20330
+ throw new Error("Missing pnpm dependency snapshot");
20331
+ visit2(key, snapshot);
20055
20332
  }
20056
- return parsed;
20057
- } catch {
20058
- return null;
20059
- }
20333
+ };
20334
+ visit2(`workspace:${importerKey}`, record2(importers[importerKey]), importerKey);
20335
+ return { runtime: names, build: /* @__PURE__ */ new Set([...names, ...Object.keys(record2(record2(importers[importerKey]).devDependencies))]), root };
20060
20336
  }
20061
- function readJson(filePath) {
20062
- if (!existsSync(filePath)) {
20063
- return null;
20064
- }
20337
+ async function scanApp(inputDir, options) {
20065
20338
  try {
20066
- const content = readFileSync(filePath, "utf-8");
20067
- return JSON.parse(content);
20339
+ const appDir = resolve(inputDir);
20340
+ const graph = runtimeDependencies(appDir);
20341
+ const envDir = options?.envDir ? resolve(options.envDir) : existsSync(join(appDir, ".env")) ? appDir : graph.root;
20342
+ const loaded = loadDeploymentEnvironment({ environment: options?.environment ?? "prod", envDir, requireFile: options?.environment !== void 0 });
20343
+ if (!loaded.success)
20344
+ return loaded;
20345
+ const env = loaded.data.values;
20346
+ const deps = graph.runtime;
20347
+ const configs = {};
20348
+ for (const [name, owner] of Object.entries({ db: "reldb", cache: "cache", storage: "storage", vecdb: "vecdb", iam: "iam", reach: "reach" })) {
20349
+ configs[name] = {};
20350
+ if (!deps.has(`@h-ai/${owner}`))
20351
+ continue;
20352
+ const configFile = ["yml", "yaml"].map((ext) => join(appDir, "config", `_${name}.${ext}`)).find(existsSync);
20353
+ const config2 = configFile ? loadDeploymentConfig(name, configFile, env) : void 0;
20354
+ if (config2 && !config2.success)
20355
+ return config2;
20356
+ configs[name] = config2?.data ?? {};
20357
+ }
20358
+ const middlewares = [];
20359
+ const add = (service, backend, configName = service, configPath = [], config2 = configs[configName] ?? {}, source = `_${configName}.yml`) => {
20360
+ middlewares.push({ service, backend, source, configName, configPath, envPrefix: ["HAI", configName, ...configPath].map((part) => part.toUpperCase()).join("_"), config: config2 });
20361
+ };
20362
+ const iam = deps.has("@h-ai/iam");
20363
+ if (deps.has("@h-ai/reldb"))
20364
+ add("db", String(configs.db.type ?? "sqlite"), "db", [], configs.db, "@h-ai/reldb / _db.yml");
20365
+ if (deps.has("@h-ai/cache"))
20366
+ add("cache", String(configs.cache.type ?? "memory"), "cache", [], configs.cache, "@h-ai/cache / _cache.yml");
20367
+ if (deps.has("@h-ai/storage"))
20368
+ add("storage", String(configs.storage.type ?? "local"));
20369
+ if (deps.has("@h-ai/vecdb"))
20370
+ add("vecdb", String(configs.vecdb.type ?? "lancedb"));
20371
+ if (iam) {
20372
+ const ldap = record2(configs.iam.ldap);
20373
+ add("ldap", record2(configs.iam.login).ldap === true ? "ldap" : "disabled", "iam", ["ldap"], ldap, "@h-ai/iam (LDAP)");
20374
+ }
20375
+ const providers = configs.reach.providers;
20376
+ if (deps.has("@h-ai/reach") && Array.isArray(providers)) {
20377
+ for (const [index, item] of providers.entries()) {
20378
+ const provider = record2(item);
20379
+ const type = String(provider.type ?? "");
20380
+ const service = type === "aliyun-sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
20381
+ if (service)
20382
+ add(service, type, "reach", ["providers", String(index)], provider, "@h-ai/reach");
20383
+ }
20384
+ }
20385
+ if (deps.has("@h-ai/reach") && !middlewares.some((item) => item.configName === "reach"))
20386
+ add("email", "console", "reach", ["providers", "0"], { name: "email", type: "console" }, "@h-ai/reach");
20387
+ const isSvelteKit = graph.build.has("@sveltejs/kit");
20388
+ const scan = {
20389
+ appDir,
20390
+ envDir,
20391
+ middlewares,
20392
+ haiModules: [...deps].filter((name) => name.startsWith("@h-ai/")).sort(),
20393
+ appName: basename(appDir),
20394
+ isSvelteKit,
20395
+ adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
20396
+ nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
20397
+ requiredServices: [...new Set(middlewares.map((item) => item.service))],
20398
+ serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
20399
+ };
20400
+ logger2.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
20401
+ return ok(scan);
20068
20402
  } catch {
20069
- return null;
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" } }));
20070
20404
  }
20071
20405
  }
20072
- function hasDependency(pkg, name) {
20073
- const deps = pkg.dependencies;
20074
- const devDeps = pkg.devDependencies;
20075
- return deps !== void 0 && name in deps || devDeps !== void 0 && name in devDeps;
20076
- }
20077
- function detectRequiredServices(configDir) {
20078
- const services = [];
20079
- const backends = {};
20080
- if (!existsSync(configDir)) {
20081
- return { services, backends };
20082
- }
20083
- const files = readdirSync(configDir);
20084
- const dbFile = files.find((f) => f.startsWith("_db."));
20085
- if (dbFile) {
20086
- const dbConfig = readYaml(join(configDir, dbFile));
20087
- if (dbConfig !== null) {
20088
- services.push("db");
20089
- backends.db = dbConfig.type ?? "postgresql";
20090
- }
20091
- }
20092
- const cacheFile = files.find((f) => f.startsWith("_cache."));
20093
- if (cacheFile) {
20094
- const cacheConfig = readYaml(join(configDir, cacheFile));
20095
- if (cacheConfig !== null && cacheConfig.type === "redis") {
20096
- services.push("cache");
20097
- backends.cache = "redis";
20098
- }
20099
- }
20100
- const storageFile = files.find((f) => f.startsWith("_storage."));
20101
- if (storageFile) {
20102
- const storageConfig = readYaml(join(configDir, storageFile));
20103
- if (storageConfig !== null && detectStorageIsS3(storageConfig)) {
20104
- services.push("storage");
20105
- backends.storage = "s3";
20106
- }
20107
- }
20108
- const vecdbFile = files.find((f) => f.startsWith("_vecdb."));
20109
- if (vecdbFile) {
20110
- const vecdbConfig = readYaml(join(configDir, vecdbFile));
20111
- if (vecdbConfig !== null) {
20112
- services.push("vecdb");
20113
- backends.vecdb = vecdbConfig.type ?? "qdrant";
20114
- }
20115
- }
20116
- const reachFile = files.find((f) => f.startsWith("_reach."));
20117
- if (reachFile) {
20118
- const reachConfig = readYaml(join(configDir, reachFile));
20119
- if (reachConfig !== null) {
20120
- detectReachServices(reachConfig, services);
20121
- }
20122
- }
20123
- return { services, backends };
20124
- }
20125
- function detectStorageIsS3(config2) {
20126
- if (config2.type === "s3") {
20127
- return true;
20128
- }
20129
- const providers = config2.providers;
20130
- if (providers === void 0) {
20131
- return false;
20132
- }
20133
- return Object.values(providers).some((p) => p?.type === "s3");
20134
- }
20135
- function detectReachServices(config2, services) {
20136
- const providers = config2.providers;
20137
- if (!Array.isArray(providers)) {
20138
- return;
20139
- }
20140
- let hasEmail = false;
20141
- let hasSms = false;
20142
- for (const provider of providers) {
20143
- const providerType = provider.type;
20144
- if (!hasEmail && (providerType === "smtp" || providerType === "api")) {
20145
- hasEmail = true;
20146
- services.push("email");
20147
- }
20148
- if (!hasSms && providerType === "aliyun-sms") {
20149
- hasSms = true;
20150
- services.push("sms");
20151
- }
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("***");
20152
20414
  }
20415
+ return masked;
20153
20416
  }
20154
- async function scanApp(appDir) {
20155
- logger3.debug("Scanning application", { appDir });
20156
- try {
20157
- const pkg = readJson(join(appDir, "package.json"));
20158
- if (pkg === null) {
20159
- return err(
20160
- HaiDeployError.SCAN_FAILED,
20161
- deployM("deploy_scanFailed", { params: { error: "package.json not found" } })
20162
- );
20163
- }
20164
- const appName = extractAppName(pkg);
20165
- const isSvelteKit = existsSync(join(appDir, "svelte.config.js")) || existsSync(join(appDir, "svelte.config.ts"));
20166
- const adapterInstalled = hasDependency(pkg, "@sveltejs/adapter-vercel");
20167
- const nodeAdapterInstalled = hasDependency(pkg, "@sveltejs/adapter-node");
20168
- const configDir = join(appDir, "config");
20169
- const configDetection = detectRequiredServices(configDir);
20170
- const depServices = detectServicesFromDependencies(pkg);
20171
- const requiredServices = Array.from(/* @__PURE__ */ new Set([...configDetection.services, ...depServices]));
20172
- const scripts = pkg.scripts;
20173
- const buildCommand = scripts?.build ?? "pnpm build";
20174
- const scanResult = {
20175
- appName,
20176
- isSvelteKit,
20177
- adapterInstalled,
20178
- nodeAdapterInstalled,
20179
- requiredServices,
20180
- serviceBackends: configDetection.backends,
20181
- buildCommand
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);
20182
20435
  };
20183
- logger3.info("Application scanned", {
20184
- appName,
20185
- isSvelteKit,
20186
- adapterInstalled,
20187
- nodeAdapterInstalled,
20188
- requiredServices,
20189
- serviceBackends: configDetection.backends
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");
20190
20448
  });
20191
- return ok(scanResult);
20192
- } catch (error62) {
20193
- logger3.error("Application scan failed", { appDir, error: error62 });
20194
- return err(
20195
- HaiDeployError.SCAN_FAILED,
20196
- deployM("deploy_scanFailed", {
20197
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
20198
- }),
20199
- error62
20200
- );
20201
- }
20202
- }
20203
- function detectServicesFromDependencies(pkg) {
20204
- const depMap = {
20205
- "@h-ai/reldb": "db",
20206
- "@h-ai/cache": "cache",
20207
- "@h-ai/storage": "storage",
20208
- "@h-ai/vecdb": "vecdb"
20209
- };
20210
- const services = [];
20211
- for (const [depName, serviceType] of Object.entries(depMap)) {
20212
- if (hasDependency(pkg, depName)) {
20213
- services.push(serviceType);
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();
20214
20487
  }
20215
- }
20216
- return services;
20488
+ });
20217
20489
  }
20218
- function extractAppName(pkg) {
20219
- const name = pkg.name ?? "app";
20220
- const slashIndex = name.indexOf("/");
20221
- if (slashIndex >= 0) {
20222
- return name.slice(slashIndex + 1);
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 });
20223
20560
  }
20224
- return name;
20225
20561
  }
20226
20562
  var IMAGES = {
20227
20563
  postgres: "postgres:16-alpine",
20228
20564
  mysql: "mysql:8.4",
20229
20565
  redis: "redis:7-alpine",
20230
- minio: "minio/minio:RELEASE.2025-09-07T16-13-09Z",
20231
- mc: "minio/mc:RELEASE.2025-08-13T08-35-41Z",
20566
+ rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
20567
+ minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
20232
20568
  qdrant: "qdrant/qdrant:v1.12.4",
20233
20569
  pgvector: "pgvector/pgvector:pg17",
20234
- chroma: "chromadb/chroma:0.5.23"
20570
+ chroma: "chromadb/chroma:1.5.5",
20571
+ ldap: "docker.io/osixia/openldap:1.5.0",
20572
+ smtp: "axllent/mailpit:v1.27.4"
20235
20573
  };
20236
20574
  function generatePassword() {
20237
20575
  return randomBytes(24).toString("base64url");
@@ -20246,7 +20584,8 @@ function resolveSidecarSecrets(appDir, projectName) {
20246
20584
  }
20247
20585
  const secrets = {
20248
20586
  dbPassword: generatePassword(),
20249
- storagePassword: generatePassword()
20587
+ storagePassword: generatePassword(),
20588
+ ldapPassword: generatePassword()
20250
20589
  };
20251
20590
  mkdirSync(dirname(secretFile), { recursive: true });
20252
20591
  writeFileSync(secretFile, JSON.stringify(secrets), { mode: 384 });
@@ -20307,38 +20646,57 @@ function planRedisCache(plan, projectName, net) {
20307
20646
  };
20308
20647
  plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
20309
20648
  }
20310
- function planMinioStorage(plan, projectName, password, net) {
20649
+ function planS3Storage(plan, projectName, password, net, engine) {
20311
20650
  plan.services.push("storage");
20312
20651
  plan.dependsOn.push("storage");
20313
20652
  plan.composeServices.storage = {
20314
- image: IMAGES.minio,
20653
+ image: IMAGES[engine],
20315
20654
  container_name: `hai-${projectName}-storage`,
20316
20655
  restart: "unless-stopped",
20317
- command: ["server", "/data"],
20656
+ ...engine === "minio" ? { command: ["server", "/data"] } : {},
20318
20657
  env_file: [".env.runtime"],
20319
20658
  volumes: [`${projectName}-storage-data:/data`],
20320
20659
  networks: net
20321
20660
  };
20322
20661
  plan.composeServices["storage-init"] = {
20323
- image: IMAGES.mc,
20324
20662
  depends_on: ["storage"],
20325
20663
  env_file: [".env.runtime"],
20326
20664
  entrypoint: [
20327
- "sh",
20328
- "-c",
20329
- 'until mc alias set s3 http://storage:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"; do sleep 1; done; mc mb -p s3/hai; exit 0'
20665
+ "node",
20666
+ "--input-type=module",
20667
+ "-e",
20668
+ // 复用应用 @h-ai/storage 已依赖的 SDK;不额外拉取失效的 mc 镜像,不把密钥拼进命令。
20669
+ `import { S3Client, HeadBucketCommand, CreateBucketCommand } from '@aws-sdk/client-s3';
20670
+ const config = JSON.parse(process.env.HAI_STORAGE);
20671
+ const client = new S3Client({ endpoint: config.endpoint, region: config.region, forcePathStyle: config.forcePathStyle, credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey } });
20672
+ let ready = false;
20673
+ for (let attempt = 0; attempt < 120; attempt++) {
20674
+ try {
20675
+ try { await client.send(new HeadBucketCommand({ Bucket: config.bucket })); }
20676
+ catch (error) {
20677
+ if (error.$$metadata?.httpStatusCode !== 404) throw error;
20678
+ await client.send(new CreateBucketCommand({ Bucket: config.bucket }));
20679
+ }
20680
+ ready = true; break;
20681
+ } catch { await new Promise(resolve => setTimeout(resolve, 1000)); }
20682
+ }
20683
+ client.destroy();
20684
+ if (!ready) throw new Error('S3 bucket initialization failed');`
20330
20685
  ],
20331
20686
  networks: net
20332
20687
  };
20333
20688
  plan.volumes[`${projectName}-storage-data`] = null;
20334
- plan.env.MINIO_ROOT_USER = "hai";
20335
- plan.env.MINIO_ROOT_PASSWORD = password;
20689
+ if (engine === "rustfs") {
20690
+ Object.assign(plan.env, { RUSTFS_ACCESS_KEY: "hai-storage", RUSTFS_SECRET_KEY: password, RUSTFS_ADDRESS: "0.0.0.0:9000", RUSTFS_VOLUMES: "/data", RUSTFS_CONSOLE_ENABLE: "false" });
20691
+ } else {
20692
+ Object.assign(plan.env, { MINIO_ROOT_USER: "hai-storage", MINIO_ROOT_PASSWORD: password });
20693
+ }
20336
20694
  plan.env.HAI_STORAGE = JSON.stringify({
20337
20695
  type: "s3",
20338
20696
  endpoint: "http://storage:9000",
20339
20697
  bucket: "hai",
20340
20698
  region: "us-east-1",
20341
- accessKeyId: "hai",
20699
+ accessKeyId: "hai-storage",
20342
20700
  secretAccessKey: password,
20343
20701
  forcePathStyle: true
20344
20702
  });
@@ -20388,7 +20746,7 @@ function planChromaVecdb(plan, projectName, net) {
20388
20746
  plan.volumes[`${projectName}-vecdb-data`] = null;
20389
20747
  plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
20390
20748
  }
20391
- function planSidecars(appDir, projectName, scan, provisionEnv) {
20749
+ function planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine = "rustfs") {
20392
20750
  const plan = {
20393
20751
  services: [],
20394
20752
  composeServices: {},
@@ -20398,10 +20756,14 @@ function planSidecars(appDir, projectName, scan, provisionEnv) {
20398
20756
  dependsOn: [],
20399
20757
  secrets: []
20400
20758
  };
20759
+ if (!scan.requiredServices.some((service) => usesDefaultMiddleware(scan, service, options)))
20760
+ return plan;
20401
20761
  const secrets = resolveSidecarSecrets(appDir, projectName);
20402
20762
  const net = ["hai-app"];
20403
20763
  const backends = scan.serviceBackends;
20404
20764
  for (const service of scan.requiredServices) {
20765
+ if (!usesDefaultMiddleware(scan, service, options))
20766
+ continue;
20405
20767
  if (service === "db" && !provisionEnv.HAI_DB) {
20406
20768
  const backend = backends.db ?? "postgresql";
20407
20769
  if (backend === "mysql")
@@ -20411,9 +20773,13 @@ function planSidecars(appDir, projectName, scan, provisionEnv) {
20411
20773
  else
20412
20774
  planPostgresDb(plan, projectName, secrets.dbPassword, net);
20413
20775
  } else if (service === "cache" && !provisionEnv.HAI_CACHE) {
20414
- planRedisCache(plan, projectName, net);
20776
+ if ((backends.cache ?? "redis") === "redis")
20777
+ planRedisCache(plan, projectName, net);
20415
20778
  } else if (service === "storage" && !provisionEnv.HAI_STORAGE) {
20416
- planMinioStorage(plan, projectName, secrets.storagePassword, net);
20779
+ if ((backends.storage ?? "s3") === "s3")
20780
+ planS3Storage(plan, projectName, secrets.storagePassword, net, s3Engine);
20781
+ else
20782
+ addAppDataVolume(plan, projectName);
20417
20783
  } else if (service === "vecdb" && !provisionEnv.HAI_VECDB) {
20418
20784
  const backend = backends.vecdb ?? "qdrant";
20419
20785
  if (backend === "pgvector")
@@ -20424,16 +20790,39 @@ function planSidecars(appDir, projectName, scan, provisionEnv) {
20424
20790
  addAppDataVolume(plan, projectName);
20425
20791
  else
20426
20792
  planQdrantVecdb(plan, projectName, net);
20793
+ } else if (service === "ldap" && backends.ldap === "ldap" && !provisionEnv.HAI_IAM_LDAP) {
20794
+ const password = secrets.ldapPassword ?? secrets.dbPassword;
20795
+ plan.services.push("ldap");
20796
+ plan.dependsOn.push("ldap");
20797
+ plan.composeServices.ldap = { image: IMAGES.ldap, restart: "unless-stopped", env_file: [".env.runtime"], networks: net, volumes: [`${projectName}-ldap-data:/var/lib/ldap`, `${projectName}-ldap-config:/etc/ldap/slapd.d`] };
20798
+ plan.volumes[`${projectName}-ldap-data`] = null;
20799
+ plan.volumes[`${projectName}-ldap-config`] = null;
20800
+ Object.assign(plan.env, { LDAP_DOMAIN: "example.org", LDAP_ORGANISATION: "HAI", LDAP_ADMIN_PASSWORD: password, LDAP_TLS: "false", HAI_IAM_LOGIN_LDAP: "true", HAI_IAM_LDAP: JSON.stringify({ url: "ldap://ldap:389", bindDn: "cn=admin,dc=example,dc=org", bindPassword: password, searchBase: "dc=example,dc=org" }) });
20801
+ plan.secrets.push(password);
20802
+ } else if (service === "email" && (scan.middlewares ? scan.middlewares.some((item) => item.service === "email" && item.backend === "smtp" && (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email ?? "default") === "default") : backends.email === "smtp") && !provisionEnv.HAI_REACH_PROVIDERS) {
20803
+ plan.services.push("email");
20804
+ plan.dependsOn.push("email");
20805
+ plan.composeServices.email = { image: IMAGES.smtp, restart: "unless-stopped", networks: net };
20806
+ const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
20807
+ if (requirements?.length) {
20808
+ for (const requirement of requirements)
20809
+ plan.env[requirement.envPrefix] = JSON.stringify({ name: requirement.config.name ?? "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
20810
+ } else if (!scan.middlewares) {
20811
+ plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({ name: "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
20812
+ }
20427
20813
  }
20428
20814
  }
20429
20815
  return plan;
20430
20816
  }
20431
- function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort) {
20432
- const sidecar = planSidecars(appDir, projectName, scan, provisionEnv);
20817
+ function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv = {}, s3Engine = "rustfs") {
20818
+ const sidecar = planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine);
20433
20819
  const env = {
20434
20820
  NODE_ENV: "production",
20435
20821
  HOST: "0.0.0.0",
20436
20822
  PORT: String(containerPort),
20823
+ HAI_SERV_SERVER_HOST: "0.0.0.0",
20824
+ HAI_SERV_SERVER_PORT: String(containerPort),
20825
+ ...appEnv,
20437
20826
  ...sidecar.env,
20438
20827
  ...provisionEnv
20439
20828
  };
@@ -20443,6 +20832,7 @@ function generateComposeFile(options) {
20443
20832
  const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
20444
20833
  const app = {
20445
20834
  image: imageRef,
20835
+ labels: { "org.opencontainers.image.version": options.appVersion },
20446
20836
  container_name: `hai-${projectName}-app`,
20447
20837
  restart: "unless-stopped",
20448
20838
  // 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
@@ -20455,188 +20845,110 @@ function generateComposeFile(options) {
20455
20845
  app.volumes = sidecar.appVolumes;
20456
20846
  if (sidecar.dependsOn.length > 0)
20457
20847
  app.depends_on = sidecar.dependsOn;
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
+ }
20865
+ if (services["storage-init"])
20866
+ services["storage-init"] = { ...services["storage-init"], image: imageRef };
20458
20867
  const compose = {
20459
- services: {
20460
- app,
20461
- ...sidecar.composeServices
20462
- },
20463
- networks: {
20868
+ "x-hai-version": options.appVersion,
20869
+ services,
20870
+ "networks": {
20464
20871
  "hai-app": { name: `hai-${projectName}` }
20465
20872
  }
20466
20873
  };
20467
20874
  if (Object.keys(sidecar.volumes).length > 0)
20468
20875
  compose.volumes = sidecar.volumes;
20876
+ if (options.previousCompose && options.serviceModes) {
20877
+ const previous = parse$1(options.previousCompose);
20878
+ for (const [service, mode] of Object.entries(options.serviceModes)) {
20879
+ if (mode !== "keep")
20880
+ continue;
20881
+ for (const name of [service, `${service}-init`]) {
20882
+ if (previous.services?.[name]) {
20883
+ services[name] = previous.services[name];
20884
+ app.depends_on = [.../* @__PURE__ */ new Set([...app.depends_on ?? [], service])];
20885
+ }
20886
+ }
20887
+ }
20888
+ const previousMounts = previous.services?.app?.volumes;
20889
+ if (Array.isArray(previousMounts) && Object.values(options.serviceModes).includes("keep"))
20890
+ app.volumes = [.../* @__PURE__ */ new Set([...app.volumes ?? [], ...previousMounts])];
20891
+ compose.volumes = { ...previous.volumes, ...sidecar.volumes };
20892
+ }
20893
+ if (services["storage-init"]) {
20894
+ app.depends_on = {
20895
+ ...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
20896
+ "storage-init": { condition: "service_completed_successfully" }
20897
+ };
20898
+ }
20899
+ for (const service of Object.values(services))
20900
+ service.pull_policy = "never";
20469
20901
  return stringify(compose);
20470
20902
  }
20471
- function serializeEnvFile(env) {
20472
- return `${Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n")}
20473
- `;
20474
- }
20475
- function generateDockerignore() {
20476
- return `**/node_modules
20477
- **/dist
20478
- **/build
20479
- **/.turbo
20480
- **/.svelte-kit
20481
- **/coverage
20482
- **/test-results
20483
- **/playwright-report
20484
- **/.hai/deploy
20485
- .git
20486
- `;
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
+ }
20487
20933
  }
20488
- function generateFallbackDockerfile(packageName, containerPort) {
20489
- return `# syntax=docker/dockerfile:1.7
20490
- FROM node:24-bookworm-slim AS build
20491
- ENV PNPM_HOME=/pnpm
20492
- ENV PATH=$PNPM_HOME:$PATH
20493
- ENV CI=true
20494
- WORKDIR /workspace
20495
- RUN corepack enable && corepack prepare pnpm@12.11.2 --activate
20496
- COPY . .
20497
- RUN pnpm install --frozen-lockfile --ignore-scripts
20498
- RUN pnpm --filter ${packageName} build
20499
- RUN pnpm pm deploy --filter ${packageName} --prod /prod/app
20500
-
20501
- FROM node:24-bookworm-slim AS runtime
20502
- ENV NODE_ENV=production
20503
- ENV HOST=0.0.0.0
20504
- ENV PORT=${containerPort}
20505
- WORKDIR /app
20506
- COPY --from=build --chown=node:node /prod/app ./
20507
- USER node
20508
- EXPOSE ${containerPort}
20509
- CMD ["node", "build"]
20510
20934
  `;
20511
20935
  }
20512
- var logger4 = core.logger.child({ module: "deploy", scope: "command" });
20513
- function maskSecrets(text, secrets) {
20514
- if (!secrets || secrets.length === 0)
20515
- return text;
20516
- let masked = text;
20517
- for (const secret of secrets) {
20518
- if (secret && secret.length > 0)
20519
- masked = masked.split(secret).join("***");
20520
- }
20521
- return masked;
20522
- }
20523
- function runCommand(command, args, options = {}) {
20524
- return new Promise((resolve) => {
20525
- const child = spawn(command, args, {
20526
- cwd: options.cwd,
20527
- env: options.env ? { ...process.env, ...options.env } : process.env,
20528
- shell: false
20529
- });
20530
- let stdout = "";
20531
- let stderr = "";
20532
- let settled = false;
20533
- let timer;
20534
- const finish = (result) => {
20535
- if (settled)
20536
- return;
20537
- settled = true;
20538
- if (timer)
20539
- clearTimeout(timer);
20540
- resolve(result);
20541
- };
20542
- if (options.timeoutMs && options.timeoutMs > 0) {
20543
- timer = setTimeout(() => {
20544
- child.kill("SIGKILL");
20545
- logger4.error("Command timed out", { command, timeoutMs: options.timeoutMs });
20546
- finish(err(
20547
- HaiDeployError.REMOTE_COMMAND_FAILED,
20548
- deployM("deploy_commandTimeout", { params: { command } })
20549
- ));
20550
- }, options.timeoutMs);
20551
- }
20552
- child.stdout.on("data", (chunk) => {
20553
- stdout += chunk.toString("utf-8");
20554
- });
20555
- child.stderr.on("data", (chunk) => {
20556
- stderr += chunk.toString("utf-8");
20557
- });
20558
- child.on("error", (error62) => {
20559
- logger4.error("Command failed to start", { command, error: maskSecrets(String(error62), options.secrets) });
20560
- finish(err(
20561
- HaiDeployError.REMOTE_COMMAND_FAILED,
20562
- deployM("deploy_commandFailed", {
20563
- params: { command, error: maskSecrets(error62.message, options.secrets) }
20564
- }),
20565
- error62
20566
- ));
20567
- });
20568
- child.on("close", (code) => {
20569
- const exitCode = code ?? -1;
20570
- if (exitCode === 0) {
20571
- finish(ok({ code: exitCode, stdout, stderr }));
20572
- return;
20573
- }
20574
- logger4.error("Command exited with non-zero code", {
20575
- command,
20576
- code: exitCode,
20577
- stderr: maskSecrets(stderr, options.secrets).slice(-500)
20578
- });
20579
- finish(err(
20580
- HaiDeployError.REMOTE_COMMAND_FAILED,
20581
- deployM("deploy_commandFailed", {
20582
- params: { command, error: maskSecrets(stderr || `exit ${exitCode}`, options.secrets).slice(-500) }
20583
- })
20584
- ));
20585
- });
20586
- if (options.input !== void 0) {
20587
- child.stdin.write(options.input);
20588
- child.stdin.end();
20589
- } else {
20590
- child.stdin.end();
20936
+ function serializeEnvFile(env) {
20937
+ return `${Object.entries(env).map(([key, value]) => {
20938
+ let literal2 = value;
20939
+ if (/[\s#$'"]/.test(value)) {
20940
+ if (value.startsWith("{") || value.startsWith("["))
20941
+ literal2 = `'${value.replaceAll("'", "\\u0027")}'`;
20942
+ else if (!value.includes("'"))
20943
+ literal2 = `'${value}'`;
20944
+ else
20945
+ literal2 = JSON.stringify(value).replaceAll("$", "$$");
20591
20946
  }
20592
- });
20593
- }
20594
-
20595
- // ../deploy/src/container/deploy-container-runtime.ts
20596
- var logger5 = core.logger.child({ module: "deploy", scope: "container-runtime" });
20597
- async function detectLocalRuntime(preferred = "auto") {
20598
- const candidates = preferred === "auto" ? ["podman", "docker"] : [preferred];
20599
- for (const runtime of candidates) {
20600
- const probe = await runCommand(runtime, ["version"], { timeoutMs: 15e3 });
20601
- if (probe.success) {
20602
- logger5.info("Local container runtime detected", { runtime });
20603
- return ok(runtime);
20604
- }
20605
- }
20606
- return err(
20607
- HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND,
20608
- deployM("deploy_containerRuntimeNotFound", { params: { runtime: candidates.join(" / ") } })
20609
- );
20610
- }
20611
- async function buildImage(runtime, options) {
20612
- const args = ["build", "-f", options.dockerfilePath, "-t", options.imageRef];
20613
- if (runtime === "podman")
20614
- args.push("--format", "docker", "--jobs", "1");
20615
- args.push(options.contextDir);
20616
- logger5.info("Building container image", { runtime, imageRef: options.imageRef });
20617
- const result = await runCommand(runtime, args, { cwd: options.contextDir });
20618
- if (!result.success) {
20619
- return err(
20620
- HaiDeployError.CONTAINER_BUILD_FAILED,
20621
- deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
20622
- result.error
20623
- );
20624
- }
20625
- return ok(void 0);
20626
- }
20627
- async function saveImage(runtime, imageRef, tarPath) {
20628
- logger5.info("Saving container image", { runtime, imageRef, tarPath });
20629
- const result = await runCommand(runtime, ["save", "-o", tarPath, imageRef], {});
20630
- if (!result.success) {
20631
- return err(
20632
- HaiDeployError.CONTAINER_BUILD_FAILED,
20633
- deployM("deploy_containerBuildFailed", { params: { error: result.error.message } }),
20634
- result.error
20635
- );
20636
- }
20637
- return ok(void 0);
20947
+ return `${key}=${literal2}`;
20948
+ }).join("\n")}
20949
+ `;
20638
20950
  }
20639
- var logger6 = core.logger.child({ module: "deploy", scope: "ssh" });
20951
+ var logger4 = core.logger.child({ module: "deploy", scope: "ssh" });
20640
20952
  function buildConnectOptions(config2) {
20641
20953
  const args = ["-o", "BatchMode=yes"];
20642
20954
  args.push("-o", `ConnectTimeout=${config2.connectTimeoutSeconds}`);
@@ -20687,7 +20999,7 @@ function createSshExecutor(config2) {
20687
20999
  return ok(void 0);
20688
21000
  };
20689
21001
  const ping = async () => {
20690
- logger6.debug("SSH preflight", { host: config2.host, port: config2.port });
21002
+ logger4.debug("SSH preflight", { host: config2.host, port: config2.port });
20691
21003
  const result = await exec("true", { timeoutMs: 2e4 });
20692
21004
  if (!result.success) {
20693
21005
  return err(
@@ -20702,7 +21014,7 @@ function createSshExecutor(config2) {
20702
21014
  }
20703
21015
 
20704
21016
  // ../deploy/src/providers/deploy-provider-docker-ssh.ts
20705
- var logger7 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
21017
+ var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
20706
21018
  var HEALTH_POLL_INTERVAL = 3e3;
20707
21019
  var HEALTH_MAX_ATTEMPTS = 40;
20708
21020
  function makeDeploymentId() {
@@ -20711,38 +21023,15 @@ function makeDeploymentId() {
20711
21023
  const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
20712
21024
  return `${stamp}-${randomBytes(3).toString("hex")}`;
20713
21025
  }
20714
- function parseLoadedImageRef(stdout, fallback) {
20715
- const match = stdout.match(/Loaded image(?:\(s\))?:\s*(\S+)/);
20716
- return match ? match[1] : fallback;
20717
- }
20718
21026
  function sleep(ms) {
20719
- return new Promise((resolve) => setTimeout(resolve, ms));
20720
- }
20721
- function readPackageName(appDir) {
20722
- try {
20723
- const pkg = JSON.parse(readFileSync(join(appDir, "package.json"), "utf-8"));
20724
- return pkg.name ?? "app";
20725
- } catch {
20726
- return "app";
20727
- }
21027
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
20728
21028
  }
20729
- function resolveWorkspaceRoot(appDir) {
20730
- if (existsSync(join(appDir, "pnpm-workspace.yaml")))
20731
- return appDir;
20732
- let current = appDir;
20733
- let parent = dirname(current);
20734
- while (parent !== current) {
20735
- if (existsSync(join(parent, "pnpm-workspace.yaml")))
20736
- return parent;
20737
- current = parent;
20738
- parent = dirname(parent);
20739
- }
20740
- return appDir;
20741
- }
20742
- function createDockerSshProvider(config2, localRuntimePref = "auto") {
20743
- const ssh = createSshExecutor(config2.ssh);
21029
+ function createDockerSshProvider(config2, container) {
21030
+ const ssh = config2.ssh ? createSshExecutor(config2.ssh) : void 0;
20744
21031
  const baseDir = config2.remote.baseDir;
20745
21032
  async function detectRemoteRuntime() {
21033
+ if (!ssh)
21034
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
20746
21035
  const candidates = config2.remote.runtime === "auto" ? ["podman", "docker"] : [config2.remote.runtime];
20747
21036
  for (const runtime of candidates) {
20748
21037
  const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
@@ -20750,7 +21039,7 @@ function createDockerSshProvider(config2, localRuntimePref = "auto") {
20750
21039
  continue;
20751
21040
  const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
20752
21041
  if (composeResult.success) {
20753
- logger7.info("Remote container runtime detected", { runtime });
21042
+ logger5.info("Remote container runtime detected", { runtime });
20754
21043
  return ok(runtime);
20755
21044
  }
20756
21045
  }
@@ -20772,98 +21061,103 @@ function createDockerSshProvider(config2, localRuntimePref = "auto") {
20772
21061
  return false;
20773
21062
  }
20774
21063
  const deploy2 = async (context) => {
20775
- const { appDir, projectName, scan, provisionEnv } = context;
21064
+ const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
20776
21065
  const deploymentId = makeDeploymentId();
20777
- const containerPort = config2.expose.containerPort;
20778
21066
  const hostPort = config2.expose.hostPort;
20779
- const localImageTag = `hai-${projectName}:${deploymentId}`;
20780
- const runtimeResult = await detectLocalRuntime(localRuntimePref);
20781
- if (!runtimeResult.success)
20782
- return err(runtimeResult.error);
20783
- const localRuntime = runtimeResult.data;
20784
- const artifactDir = join(appDir, ".hai", "deploy");
20785
- mkdirSync(artifactDir, { recursive: true });
20786
- const appDockerfile = join(appDir, "Dockerfile");
20787
- let dockerfilePath;
20788
- let contextDir;
20789
- if (existsSync(appDockerfile)) {
20790
- dockerfilePath = appDockerfile;
20791
- contextDir = appDir;
20792
- } else {
20793
- dockerfilePath = join(artifactDir, "Dockerfile");
20794
- writeFileSync(dockerfilePath, generateFallbackDockerfile(readPackageName(appDir), containerPort));
20795
- writeFileSync(`${dockerfilePath}.dockerignore`, generateDockerignore());
20796
- contextDir = resolveWorkspaceRoot(appDir);
20797
- logger7.info("Generated fallback Dockerfile", { dockerfilePath, contextDir });
20798
- }
20799
- const buildResult = await buildImage(localRuntime, {
20800
- contextDir,
20801
- dockerfilePath,
20802
- imageRef: localImageTag
20803
- });
20804
- if (!buildResult.success)
20805
- return err(buildResult.error);
20806
- const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort);
21067
+ const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
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}`;
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
+ }
20807
21091
  const envPath = join(artifactDir, ".env.runtime");
21092
+ const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
21093
+ Object.assign(env, { ...previousEnv, ...env });
20808
21094
  writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
20809
- const tarPath = join(artifactDir, `${projectName}.tar`);
20810
- const saveResult = await saveImage(localRuntime, localImageTag, tarPath);
20811
- if (!saveResult.success)
20812
- return err(saveResult.error);
20813
- const pingResult = await ssh.ping();
20814
- if (!pingResult.success)
20815
- return err(pingResult.error);
20816
- const remoteRuntimeResult = await detectRemoteRuntime();
20817
- if (!remoteRuntimeResult.success)
20818
- return err(remoteRuntimeResult.error);
20819
- const remoteRuntime = remoteRuntimeResult.data;
20820
- const remoteDir = `${baseDir}/${projectName}`;
20821
- const prepareResult = await ssh.exec(`mkdir -p ${remoteDir} && chmod 700 ${remoteDir}`);
20822
- if (!prepareResult.success)
20823
- return err(prepareResult.error);
20824
- const remoteTar = `${remoteDir}/image.tar`;
20825
- const uploadTarResult = await ssh.upload(tarPath, remoteTar);
20826
- if (!uploadTarResult.success)
20827
- return err(uploadTarResult.error);
20828
- const loadResult = await ssh.exec(`${remoteRuntime} load -i ${remoteTar}`, { timeoutMs: 3e5 });
20829
- if (!loadResult.success)
20830
- return err(loadResult.error);
20831
- const loadedRef = parseLoadedImageRef(loadResult.data.stdout, localImageTag);
20832
- const remoteImageRef = remoteRuntime === "podman" ? `localhost/${localImageTag}` : localImageTag;
20833
- if (loadedRef !== remoteImageRef) {
20834
- const tagResult = await ssh.exec(`${remoteRuntime} tag ${loadedRef} ${remoteImageRef}`);
20835
- if (!tagResult.success)
20836
- return err(tagResult.error);
20837
- }
21095
+ if (options?.environment) {
21096
+ const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
21097
+ if (!environment.success)
21098
+ return environment;
21099
+ const saved = saveDeploymentEnvironment(environment.data, { HAI_DEPLOY_ENVIRONMENT: environment.data.environment, HAI_CORE_ENV: environment.data.values.HAI_CORE_ENV, ...sidecar.env, ...provisionEnv });
21100
+ if (!saved.success)
21101
+ return saved;
21102
+ }
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;
20838
21107
  const composeContent = generateComposeFile({
20839
21108
  projectName,
20840
- imageRef: remoteImageRef,
21109
+ appVersion: artifact.appVersion,
21110
+ imageRef: app.imageRef,
20841
21111
  hostPort,
20842
21112
  containerPort,
20843
- sidecar
21113
+ sidecar,
21114
+ web: web ? { imageRef: web.imageRef, containerPort: web.containerPort ?? 8080 } : void 0,
21115
+ serviceModes: options?.serviceModes,
21116
+ previousCompose: previousPath && existsSync(previousPath) ? readFileSync(previousPath, "utf8") : void 0
20844
21117
  });
20845
- const composePath = join(artifactDir, "compose.yml");
21118
+ const composePath = join(artifactDir, composeName);
20846
21119
  writeFileSync(composePath, composeContent);
20847
- const uploadComposeResult = await ssh.upload(composePath, `${remoteDir}/compose.yml`);
20848
- if (!uploadComposeResult.success)
20849
- return err(uploadComposeResult.error);
20850
- const uploadEnvResult = await ssh.upload(envPath, `${remoteDir}/.env.runtime`);
20851
- if (!uploadEnvResult.success)
20852
- return err(uploadEnvResult.error);
20853
- const secretMask = sidecar.secrets;
20854
- const chmodResult = await ssh.exec(`chmod 600 ${remoteDir}/.env.runtime`, { secrets: secretMask });
20855
- if (!chmodResult.success)
20856
- return err(chmodResult.error);
20857
- const upResult = await ssh.exec(
20858
- `cd ${remoteDir} && ${remoteRuntime} compose -p hai-${projectName} up -d`,
20859
- { 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 }
20860
21155
  );
20861
- if (!upResult.success)
20862
- return err(upResult.error);
20863
- await ssh.exec(`rm -f ${remoteTar}`);
20864
- const url2 = `http://${config2.ssh.host}:${hostPort}`;
20865
- logger7.info("Running health check", { url: url2 });
20866
- const healthy = await healthCheck(`${url2}/`);
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}/`));
20867
21161
  if (!healthy) {
20868
21162
  return err(
20869
21163
  HaiDeployError.HEALTH_CHECK_FAILED,
@@ -20876,14 +21170,23 @@ function createDockerSshProvider(config2, localRuntimePref = "auto") {
20876
21170
  status: "ready",
20877
21171
  provider: "docker-ssh",
20878
21172
  healthCheck: "passed",
20879
- envVarsSet: Object.keys(env)
21173
+ envVarsSet: Object.keys(env),
21174
+ bundlePath: bundle.bundlePath
20880
21175
  };
20881
- logger7.info("docker-ssh deployment complete", { url: url2, deploymentId });
21176
+ logger5.info("docker-ssh deployment complete", { url: url2, deploymentId });
20882
21177
  return ok(result);
20883
21178
  };
20884
- 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 };
20885
21188
  }
20886
- var logger8 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
21189
+ var logger6 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
20887
21190
  var VERCEL_API = "https://api.vercel.com";
20888
21191
  var POLL_INTERVAL = 3e3;
20889
21192
  var MAX_POLL_ATTEMPTS = 60;
@@ -20936,7 +21239,7 @@ function createVercelProvider() {
20936
21239
  return {
20937
21240
  name: "vercel",
20938
21241
  async authenticate(apiToken) {
20939
- logger8.debug("Authenticating with Vercel");
21242
+ logger6.debug("Authenticating with Vercel");
20940
21243
  try {
20941
21244
  const user = await vercelFetch(
20942
21245
  apiToken,
@@ -20944,10 +21247,10 @@ function createVercelProvider() {
20944
21247
  );
20945
21248
  token = apiToken;
20946
21249
  const username = user.user.username || user.user.email;
20947
- logger8.info("Vercel authenticated", { user: username });
21250
+ logger6.info("Vercel authenticated", { user: username });
20948
21251
  return ok(username);
20949
21252
  } catch (error62) {
20950
- logger8.error("Vercel authentication failed", { error: error62 });
21253
+ logger6.error("Vercel authentication failed", { error: error62 });
20951
21254
  return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error62));
20952
21255
  }
20953
21256
  },
@@ -20958,11 +21261,11 @@ function createVercelProvider() {
20958
21261
  deployM("deploy_authRequired")
20959
21262
  );
20960
21263
  }
20961
- logger8.debug("Creating Vercel project", { projectName });
21264
+ logger6.debug("Creating Vercel project", { projectName });
20962
21265
  try {
20963
21266
  const existingId = await findExistingProject(token, projectName);
20964
21267
  if (existingId !== null) {
20965
- logger8.info("Vercel project found", { projectName, projectId: existingId });
21268
+ logger6.info("Vercel project found", { projectName, projectId: existingId });
20966
21269
  return ok(existingId);
20967
21270
  }
20968
21271
  try {
@@ -20977,13 +21280,13 @@ function createVercelProvider() {
20977
21280
  })
20978
21281
  }
20979
21282
  );
20980
- logger8.info("Vercel project created", { projectName, projectId: project.id });
21283
+ logger6.info("Vercel project created", { projectName, projectId: project.id });
20981
21284
  return ok(project.id);
20982
21285
  } catch (error62) {
20983
21286
  if (error62 instanceof VercelApiError && error62.status === 409) {
20984
21287
  const concurrentProjectId = await findExistingProject(token, projectName);
20985
21288
  if (concurrentProjectId !== null) {
20986
- logger8.info("Vercel project reused after create conflict", {
21289
+ logger6.info("Vercel project reused after create conflict", {
20987
21290
  projectName,
20988
21291
  projectId: concurrentProjectId
20989
21292
  });
@@ -20993,7 +21296,7 @@ function createVercelProvider() {
20993
21296
  throw error62;
20994
21297
  }
20995
21298
  } catch (error62) {
20996
- logger8.error("Vercel project creation failed", { projectName, error: error62 });
21299
+ logger6.error("Vercel project creation failed", { projectName, error: error62 });
20997
21300
  return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error62));
20998
21301
  }
20999
21302
  },
@@ -21004,7 +21307,7 @@ function createVercelProvider() {
21004
21307
  deployM("deploy_authRequired")
21005
21308
  );
21006
21309
  }
21007
- 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 });
21008
21311
  try {
21009
21312
  const envEntries = Object.entries(envVars).map(([key, value]) => ({
21010
21313
  key,
@@ -21020,10 +21323,10 @@ function createVercelProvider() {
21020
21323
  body: JSON.stringify(envEntries)
21021
21324
  }
21022
21325
  );
21023
- logger8.info("Vercel env vars set", { projectId, count: envEntries.length });
21326
+ logger6.info("Vercel env vars set", { projectId, count: envEntries.length });
21024
21327
  return ok(void 0);
21025
21328
  } catch (error62) {
21026
- logger8.error("Vercel env vars setup failed", { projectId, error: error62 });
21329
+ logger6.error("Vercel env vars setup failed", { projectId, error: error62 });
21027
21330
  return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error62));
21028
21331
  }
21029
21332
  },
@@ -21034,8 +21337,11 @@ function createVercelProvider() {
21034
21337
  deployM("deploy_authRequired")
21035
21338
  );
21036
21339
  }
21037
- logger8.debug("Deploying to Vercel", { projectId, outputDir });
21340
+ logger6.debug("Deploying to Vercel", { projectId, outputDir });
21038
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"));
21039
21345
  const filePaths = collectFiles(outputDir, outputDir);
21040
21346
  const files = [];
21041
21347
  for (const filePath of filePaths) {
@@ -21044,18 +21350,20 @@ function createVercelProvider() {
21044
21350
  const sha = createHash("sha1").update(content).digest("hex");
21045
21351
  const size = statSync(fullPath).size;
21046
21352
  await uploadFile(token, content, sha);
21047
- files.push({ file: filePath, sha, size });
21353
+ files.push({ file: `.vercel/output/${filePath}`, sha, size });
21048
21354
  }
21049
- logger8.debug("Files uploaded", { count: files.length });
21355
+ logger6.debug("Files uploaded", { count: files.length });
21050
21356
  const deployment = await vercelFetch(
21051
21357
  token,
21052
- "/v13/deployments",
21358
+ "/v13/deployments?prebuilt=1",
21053
21359
  {
21054
21360
  method: "POST",
21055
21361
  body: JSON.stringify({
21056
21362
  name: projectId,
21057
21363
  files,
21058
- projectSettings: { framework: "sveltekit" }
21364
+ version: 2,
21365
+ project: projectId,
21366
+ projectSettings: { framework: null, buildCommand: "", installCommand: "" }
21059
21367
  })
21060
21368
  }
21061
21369
  );
@@ -21066,14 +21374,14 @@ function createVercelProvider() {
21066
21374
  status: finalStatus,
21067
21375
  envVarsSet: []
21068
21376
  };
21069
- logger8.info("Vercel deployment complete", {
21377
+ logger6.info("Vercel deployment complete", {
21070
21378
  deploymentId: deployment.id,
21071
21379
  url: result.url,
21072
21380
  status: finalStatus
21073
21381
  });
21074
21382
  return ok(result);
21075
21383
  } catch (error62) {
21076
- logger8.error("Vercel deployment failed", { projectId, error: error62 });
21384
+ logger6.error("Vercel deployment failed", { projectId, error: error62 });
21077
21385
  return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error62));
21078
21386
  }
21079
21387
  }
@@ -21118,11 +21426,11 @@ async function pollDeploymentStatus(token, deploymentId) {
21118
21426
  if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
21119
21427
  return "error";
21120
21428
  }
21121
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
21429
+ await new Promise((resolve6) => setTimeout(resolve6, POLL_INTERVAL));
21122
21430
  }
21123
21431
  return "building";
21124
21432
  }
21125
- var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
21433
+ var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
21126
21434
  function createAliyunProvisioner() {
21127
21435
  let accessKey = null;
21128
21436
  let secretKey = null;
@@ -21131,7 +21439,7 @@ function createAliyunProvisioner() {
21131
21439
  name: "aliyun",
21132
21440
  serviceType: "sms",
21133
21441
  async authenticate(credentials) {
21134
- logger9.debug("Authenticating with Aliyun");
21442
+ logger7.debug("Authenticating with Aliyun");
21135
21443
  try {
21136
21444
  const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
21137
21445
  const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
@@ -21141,10 +21449,10 @@ function createAliyunProvisioner() {
21141
21449
  accessKey = akId;
21142
21450
  secretKey = akSecret;
21143
21451
  signName = credentials.signName;
21144
- logger9.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
21452
+ logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
21145
21453
  return ok(akId);
21146
21454
  } catch (error62) {
21147
- logger9.error("Aliyun authentication failed", { error: error62 });
21455
+ logger7.error("Aliyun authentication failed", { error: error62 });
21148
21456
  return err(
21149
21457
  HaiDeployError.AUTH_FAILED,
21150
21458
  deployM("deploy_authFailed", {
@@ -21161,7 +21469,7 @@ function createAliyunProvisioner() {
21161
21469
  deployM("deploy_authRequired")
21162
21470
  );
21163
21471
  }
21164
- logger9.debug("Provisioning Aliyun SMS service");
21472
+ logger7.debug("Provisioning Aliyun SMS service");
21165
21473
  return ok({
21166
21474
  serviceType: "sms",
21167
21475
  provisionerName: "aliyun",
@@ -21174,7 +21482,7 @@ function createAliyunProvisioner() {
21174
21482
  }
21175
21483
  };
21176
21484
  }
21177
- var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
21485
+ var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
21178
21486
  var NEON_API = "https://console.neon.tech/api/v2";
21179
21487
  var DEFAULT_NEON_DATABASE_NAME = "neondb";
21180
21488
  var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
@@ -21233,7 +21541,7 @@ function createNeonProvisioner() {
21233
21541
  name: "neon",
21234
21542
  serviceType: "db",
21235
21543
  async authenticate(credentials) {
21236
- logger10.debug("Authenticating with Neon");
21544
+ logger8.debug("Authenticating with Neon");
21237
21545
  try {
21238
21546
  const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
21239
21547
  if (!apiToken) {
@@ -21241,10 +21549,10 @@ function createNeonProvisioner() {
21241
21549
  }
21242
21550
  await listNeonProjects(apiToken);
21243
21551
  token = apiToken;
21244
- logger10.info("Neon authenticated");
21552
+ logger8.info("Neon authenticated");
21245
21553
  return ok("neon-user");
21246
21554
  } catch (error62) {
21247
- logger10.error("Neon authentication failed", { error: error62 });
21555
+ logger8.error("Neon authentication failed", { error: error62 });
21248
21556
  return err(
21249
21557
  HaiDeployError.AUTH_FAILED,
21250
21558
  deployM("deploy_authFailed", {
@@ -21261,7 +21569,7 @@ function createNeonProvisioner() {
21261
21569
  deployM("deploy_authRequired")
21262
21570
  );
21263
21571
  }
21264
- logger10.debug("Provisioning Neon database", { appName });
21572
+ logger8.debug("Provisioning Neon database", { appName });
21265
21573
  let resource;
21266
21574
  try {
21267
21575
  const projectName = `${appName}-db`;
@@ -21269,7 +21577,7 @@ function createNeonProvisioner() {
21269
21577
  if (existingProject !== null) {
21270
21578
  resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
21271
21579
  const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
21272
- logger10.info("Neon project reused", {
21580
+ logger8.info("Neon project reused", {
21273
21581
  projectId: existingProject.id,
21274
21582
  projectName
21275
21583
  });
@@ -21291,10 +21599,10 @@ function createNeonProvisioner() {
21291
21599
  if (!projectId || !connectionUri) {
21292
21600
  throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
21293
21601
  }
21294
- logger10.info("Neon database provisioned", { projectId });
21602
+ logger8.info("Neon database provisioned", { projectId });
21295
21603
  return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
21296
21604
  } catch (error62) {
21297
- logger10.error("Neon provisioning failed", { appName });
21605
+ logger8.error("Neon provisioning failed", { appName });
21298
21606
  if (resource) {
21299
21607
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
21300
21608
  projectName: appName,
@@ -21316,7 +21624,7 @@ function createNeonProvisioner() {
21316
21624
  }
21317
21625
  };
21318
21626
  }
21319
- var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
21627
+ var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
21320
21628
  var CF_API = "https://api.cloudflare.com/client/v4";
21321
21629
  function createR2Provisioner() {
21322
21630
  let token = null;
@@ -21327,7 +21635,7 @@ function createR2Provisioner() {
21327
21635
  name: "r2",
21328
21636
  serviceType: "storage",
21329
21637
  async authenticate(credentials) {
21330
- logger11.debug("Authenticating with Cloudflare");
21638
+ logger9.debug("Authenticating with Cloudflare");
21331
21639
  try {
21332
21640
  const acctId = credentials.accountId ?? credentials.account_id ?? "";
21333
21641
  const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
@@ -21344,10 +21652,10 @@ function createR2Provisioner() {
21344
21652
  token = apiTok;
21345
21653
  accessKey = credentials.accessKeyId;
21346
21654
  secretKey = credentials.secretAccessKey;
21347
- logger11.info("Cloudflare R2 authenticated", { accountId: acctId });
21655
+ logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
21348
21656
  return ok(acctId);
21349
21657
  } catch (error62) {
21350
- logger11.error("Cloudflare R2 authentication failed", { error: error62 });
21658
+ logger9.error("Cloudflare R2 authentication failed", { error: error62 });
21351
21659
  return err(
21352
21660
  HaiDeployError.AUTH_FAILED,
21353
21661
  deployM("deploy_authFailed", {
@@ -21365,7 +21673,7 @@ function createR2Provisioner() {
21365
21673
  );
21366
21674
  }
21367
21675
  const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
21368
- logger11.debug("Provisioning R2 bucket", { appName, bucketName });
21676
+ logger9.debug("Provisioning R2 bucket", { appName, bucketName });
21369
21677
  try {
21370
21678
  const createRes = await fetch(
21371
21679
  `${CF_API}/accounts/${accountId}/r2/buckets`,
@@ -21381,7 +21689,7 @@ function createR2Provisioner() {
21381
21689
  if (!createRes.ok && createRes.status !== 409) {
21382
21690
  throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
21383
21691
  }
21384
- logger11.info("R2 bucket provisioned", { bucketName });
21692
+ logger9.info("R2 bucket provisioned", { bucketName });
21385
21693
  return ok({
21386
21694
  serviceType: "storage",
21387
21695
  provisionerName: "r2",
@@ -21399,7 +21707,7 @@ function createR2Provisioner() {
21399
21707
  resourceInfo: `r2-bucket:${bucketName}`
21400
21708
  });
21401
21709
  } catch (error62) {
21402
- logger11.error("R2 provisioning failed", { appName, error: error62 });
21710
+ logger9.error("R2 provisioning failed", { appName, error: error62 });
21403
21711
  return err(
21404
21712
  HaiDeployError.PROVISION_FAILED,
21405
21713
  deployM("deploy_provisionFailed", {
@@ -21414,7 +21722,7 @@ function createR2Provisioner() {
21414
21722
  }
21415
21723
  };
21416
21724
  }
21417
- var logger12 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
21725
+ var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
21418
21726
  var RESEND_API = "https://api.resend.com";
21419
21727
  function createResendProvisioner() {
21420
21728
  let token = null;
@@ -21423,7 +21731,7 @@ function createResendProvisioner() {
21423
21731
  name: "resend",
21424
21732
  serviceType: "email",
21425
21733
  async authenticate(credentials) {
21426
- logger12.debug("Authenticating with Resend");
21734
+ logger10.debug("Authenticating with Resend");
21427
21735
  try {
21428
21736
  const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
21429
21737
  if (!apiToken || !credentials.from) {
@@ -21437,10 +21745,10 @@ function createResendProvisioner() {
21437
21745
  }
21438
21746
  token = apiToken;
21439
21747
  from = credentials.from;
21440
- logger12.info("Resend authenticated");
21748
+ logger10.info("Resend authenticated");
21441
21749
  return ok("resend-user");
21442
21750
  } catch (error62) {
21443
- logger12.error("Resend authentication failed", { error: error62 });
21751
+ logger10.error("Resend authentication failed", { error: error62 });
21444
21752
  return err(
21445
21753
  HaiDeployError.AUTH_FAILED,
21446
21754
  deployM("deploy_authFailed", {
@@ -21457,7 +21765,7 @@ function createResendProvisioner() {
21457
21765
  deployM("deploy_authRequired")
21458
21766
  );
21459
21767
  }
21460
- logger12.debug("Provisioning Resend email service");
21768
+ logger10.debug("Provisioning Resend email service");
21461
21769
  return ok({
21462
21770
  serviceType: "email",
21463
21771
  provisionerName: "resend",
@@ -21470,7 +21778,7 @@ function createResendProvisioner() {
21470
21778
  }
21471
21779
  };
21472
21780
  }
21473
- var logger13 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
21781
+ var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
21474
21782
  var UPSTASH_API = "https://api.upstash.com";
21475
21783
  function createUpstashAuthHeader(email3, apiKey) {
21476
21784
  return `Basic ${Buffer.from(`${email3}:${apiKey}`).toString("base64")}`;
@@ -21521,7 +21829,7 @@ function createUpstashProvisioner() {
21521
21829
  name: "upstash",
21522
21830
  serviceType: "cache",
21523
21831
  async authenticate(credentials) {
21524
- logger13.debug("Authenticating with Upstash");
21832
+ logger11.debug("Authenticating with Upstash");
21525
21833
  try {
21526
21834
  const userEmail = credentials.email ?? "";
21527
21835
  const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
@@ -21531,10 +21839,10 @@ function createUpstashProvisioner() {
21531
21839
  await listUpstashDatabases(userEmail, userKey);
21532
21840
  email3 = userEmail;
21533
21841
  apiKey = userKey;
21534
- logger13.info("Upstash authenticated", { email: userEmail });
21842
+ logger11.info("Upstash authenticated", { email: userEmail });
21535
21843
  return ok(userEmail);
21536
21844
  } catch (error62) {
21537
- logger13.error("Upstash authentication failed", { error: error62 });
21845
+ logger11.error("Upstash authentication failed", { error: error62 });
21538
21846
  return err(
21539
21847
  HaiDeployError.AUTH_FAILED,
21540
21848
  deployM("deploy_authFailed", {
@@ -21551,7 +21859,7 @@ function createUpstashProvisioner() {
21551
21859
  deployM("deploy_authRequired")
21552
21860
  );
21553
21861
  }
21554
- logger13.debug("Provisioning Upstash Redis", { appName });
21862
+ logger11.debug("Provisioning Upstash Redis", { appName });
21555
21863
  let resource;
21556
21864
  try {
21557
21865
  const databaseName = `${appName}-cache`;
@@ -21563,7 +21871,7 @@ function createUpstashProvisioner() {
21563
21871
  }
21564
21872
  resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
21565
21873
  const detail = await getUpstashDatabase(email3, apiKey, databaseId);
21566
- logger13.info("Upstash Redis reused", { databaseId, databaseName });
21874
+ logger11.info("Upstash Redis reused", { databaseId, databaseName });
21567
21875
  return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
21568
21876
  }
21569
21877
  const data = await upstashFetch(email3, apiKey, "/v2/redis/database", {
@@ -21576,10 +21884,10 @@ function createUpstashProvisioner() {
21576
21884
  });
21577
21885
  if (data.database_id)
21578
21886
  resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
21579
- logger13.info("Upstash Redis provisioned", { databaseId: data.database_id });
21887
+ logger11.info("Upstash Redis provisioned", { databaseId: data.database_id });
21580
21888
  return ok(buildUpstashProvisionResult(data, "created"));
21581
21889
  } catch (error62) {
21582
- logger13.error("Upstash provisioning failed", { appName });
21890
+ logger11.error("Upstash provisioning failed", { appName });
21583
21891
  if (resource) {
21584
21892
  return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
21585
21893
  projectName: appName,
@@ -21603,7 +21911,7 @@ function createUpstashProvisioner() {
21603
21911
  }
21604
21912
 
21605
21913
  // ../deploy/src/deploy-main.ts
21606
- var logger14 = core.logger.child({ module: "deploy", scope: "main" });
21914
+ var logger12 = core.logger.child({ module: "deploy", scope: "main" });
21607
21915
  var currentConfig = null;
21608
21916
  var currentProvider = null;
21609
21917
  var currentContainerProvider = null;
@@ -21656,15 +21964,31 @@ function normalizeProjectName(name) {
21656
21964
  const normalized = name.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
21657
21965
  return normalized || "app";
21658
21966
  }
21659
- function mergeProvisionEnv(results) {
21967
+ function mergeProvisionEnv(results, scan, options) {
21660
21968
  let env = {};
21661
- const reachProviders = [];
21969
+ const reachRequirements = scan?.middlewares?.filter((item) => item.configName === "reach") ?? [];
21970
+ const reachProviders = reachRequirements.map((item) => item.config);
21971
+ let hasReachOutput = false;
21662
21972
  for (const prov of results) {
21663
- if (prov.envVars.HAI_REACH_PROVIDERS)
21664
- reachProviders.push(...JSON.parse(prov.envVars.HAI_REACH_PROVIDERS));
21973
+ if (prov.envVars.HAI_REACH_PROVIDERS) {
21974
+ hasReachOutput = true;
21975
+ const supplied = JSON.parse(prov.envVars.HAI_REACH_PROVIDERS);
21976
+ for (const provider of supplied) {
21977
+ const targets = reachRequirements.filter((item) => {
21978
+ const mode = options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[item.service];
21979
+ return item.service === prov.serviceType && (!mode || mode === "default");
21980
+ });
21981
+ if (targets.length) {
21982
+ for (const target of targets)
21983
+ reachProviders[reachRequirements.indexOf(target)] = { ...provider, name: target.config.name ?? provider.name };
21984
+ } else {
21985
+ reachProviders.push(provider);
21986
+ }
21987
+ }
21988
+ }
21665
21989
  env = { ...env, ...prov.envVars };
21666
21990
  }
21667
- if (reachProviders.length > 0)
21991
+ if (hasReachOutput)
21668
21992
  env.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
21669
21993
  return env;
21670
21994
  }
@@ -21672,7 +21996,7 @@ var deploy = {
21672
21996
  credentials: deployCredentials,
21673
21997
  async init(config2) {
21674
21998
  if (initInProgress) {
21675
- logger14.warn("Deploy init already in progress, skipping concurrent call");
21999
+ logger12.warn("Deploy init already in progress, skipping concurrent call");
21676
22000
  return err(
21677
22001
  HaiDeployError.CONFIG_ERROR,
21678
22002
  deployM("deploy_configError", { params: { error: "init already in progress" } })
@@ -21681,13 +22005,13 @@ var deploy = {
21681
22005
  initInProgress = true;
21682
22006
  try {
21683
22007
  if (currentProvider || currentContainerProvider) {
21684
- logger14.warn("Deploy module is already initialized, reinitializing");
22008
+ logger12.warn("Deploy module is already initialized, reinitializing");
21685
22009
  await deploy.close();
21686
22010
  }
21687
- logger14.info("Initializing deploy module");
22011
+ logger12.info("Initializing deploy module");
21688
22012
  const parseResult = DeployConfigSchema.safeParse(config2);
21689
22013
  if (!parseResult.success) {
21690
- logger14.error("Deploy config validation failed", { error: parseResult.error.message });
22014
+ logger12.error("Deploy config validation failed", { error: parseResult.error.message });
21691
22015
  return err(
21692
22016
  HaiDeployError.CONFIG_ERROR,
21693
22017
  deployM("deploy_configError", { params: { error: parseResult.error.message } }),
@@ -21699,15 +22023,15 @@ var deploy = {
21699
22023
  let provider = null;
21700
22024
  let containerProvider = null;
21701
22025
  if (parsed.provider.type === "docker-ssh") {
21702
- containerProvider = createDockerSshProvider(parsed.provider, parsed.container?.runtime);
21703
- logger14.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh.host });
22026
+ containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
22027
+ logger12.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh?.host });
21704
22028
  } else {
21705
22029
  provider = createProviderByType(parsed.provider.type);
21706
22030
  const authResult = await provider.authenticate(parsed.provider.token);
21707
22031
  if (!authResult.success) {
21708
22032
  return err(authResult.error);
21709
22033
  }
21710
- logger14.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
22034
+ logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
21711
22035
  }
21712
22036
  currentProvisioners.clear();
21713
22037
  if (parsed.services) {
@@ -21719,14 +22043,14 @@ var deploy = {
21719
22043
  const credentials = extractCredentials(serviceConfig);
21720
22044
  const provAuthResult = await provisioner.authenticate(credentials);
21721
22045
  if (!provAuthResult.success) {
21722
- logger14.error("Provisioner authentication failed", {
22046
+ logger12.error("Provisioner authentication failed", {
21723
22047
  service: serviceType,
21724
22048
  provisioner: serviceConfig.provisioner
21725
22049
  });
21726
22050
  currentProvisioners.clear();
21727
22051
  return err(provAuthResult.error);
21728
22052
  }
21729
- logger14.info("Provisioner authenticated", {
22053
+ logger12.info("Provisioner authenticated", {
21730
22054
  service: serviceType,
21731
22055
  user: provAuthResult.data
21732
22056
  });
@@ -21736,7 +22060,7 @@ var deploy = {
21736
22060
  currentProvider = provider;
21737
22061
  currentContainerProvider = containerProvider;
21738
22062
  currentConfig = parsed;
21739
- logger14.info("Deploy module initialized", {
22063
+ logger12.info("Deploy module initialized", {
21740
22064
  provider: parsed.provider.type,
21741
22065
  provisioners: [...currentProvisioners.keys()]
21742
22066
  });
@@ -21746,7 +22070,7 @@ var deploy = {
21746
22070
  currentContainerProvider = null;
21747
22071
  currentConfig = null;
21748
22072
  currentProvisioners.clear();
21749
- logger14.error("Deploy module initialization failed", { error: error62 });
22073
+ logger12.error("Deploy module initialization failed", { error: error62 });
21750
22074
  return err(
21751
22075
  HaiDeployError.CONFIG_ERROR,
21752
22076
  deployM("deploy_configError", {
@@ -21761,15 +22085,15 @@ var deploy = {
21761
22085
  },
21762
22086
  async close() {
21763
22087
  if (!currentProvider && !currentContainerProvider) {
21764
- logger14.info("Deploy module already closed, skipping");
22088
+ logger12.info("Deploy module already closed, skipping");
21765
22089
  return;
21766
22090
  }
21767
- logger14.info("Closing deploy module");
22091
+ logger12.info("Closing deploy module");
21768
22092
  currentProvider = null;
21769
22093
  currentContainerProvider = null;
21770
22094
  currentConfig = null;
21771
22095
  currentProvisioners.clear();
21772
- logger14.info("Deploy module closed");
22096
+ logger12.info("Deploy module closed");
21773
22097
  },
21774
22098
  get config() {
21775
22099
  return currentConfig;
@@ -21777,21 +22101,23 @@ var deploy = {
21777
22101
  get isInitialized() {
21778
22102
  return currentProvider !== null || currentContainerProvider !== null;
21779
22103
  },
21780
- async scan(appDir) {
21781
- logger14.debug("Scanning application", { appDir });
21782
- return scanApp(appDir);
22104
+ async scan(appDir, options) {
22105
+ logger12.debug("Scanning application", { appDir });
22106
+ return scanApp(appDir, options);
21783
22107
  },
21784
- async provisionAll(projectName) {
22108
+ async provisionAll(projectName, services) {
21785
22109
  if (!currentProvider && !currentContainerProvider) {
21786
22110
  return notInitialized.result();
21787
22111
  }
21788
- logger14.info("Provisioning all services", { projectName, count: currentProvisioners.size });
22112
+ logger12.info("Provisioning all services", { projectName, count: currentProvisioners.size });
21789
22113
  const results = [];
21790
22114
  for (const [serviceType, provisioner] of currentProvisioners) {
21791
- logger14.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
22115
+ if (services && !services.includes(serviceType))
22116
+ continue;
22117
+ logger12.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
21792
22118
  const provResult = await provisioner.provision(projectName);
21793
22119
  if (!provResult.success) {
21794
- logger14.error("Service provisioning failed", {
22120
+ logger12.error("Service provisioning failed", {
21795
22121
  serviceType,
21796
22122
  provisioner: provisioner.name,
21797
22123
  code: provResult.error.code
@@ -21808,7 +22134,7 @@ var deploy = {
21808
22134
  });
21809
22135
  }
21810
22136
  results.push(provResult.data);
21811
- logger14.info("Service provisioned", { serviceType, provisioner: provisioner.name });
22137
+ logger12.info("Service provisioned", { serviceType, provisioner: provisioner.name });
21812
22138
  }
21813
22139
  return ok(results);
21814
22140
  },
@@ -21816,16 +22142,29 @@ var deploy = {
21816
22142
  if (!currentProvider && !currentContainerProvider) {
21817
22143
  return notInitialized.result();
21818
22144
  }
21819
- logger14.info("Starting deployment", { appDir, options });
21820
- const scanResult = await scanApp(appDir);
22145
+ logger12.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
22146
+ const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
22147
+ const scanResult = await scanApp(appDir, scanOptions);
21821
22148
  if (!scanResult.success) {
21822
22149
  return err(scanResult.error);
21823
22150
  }
21824
22151
  const scan = scanResult.data;
21825
- const projectName = options?.projectName ?? scan.appName;
22152
+ appDir = scan.appDir ?? appDir;
22153
+ const environment = loadDeploymentEnvironment({ environment: scanOptions.environment ?? "prod", envDir: scan.envDir ?? appDir, requireFile: scanOptions.environment !== void 0 });
22154
+ if (!environment.success)
22155
+ return environment;
22156
+ const appEnv = Object.fromEntries(Object.entries(environment.data.values).filter(([key]) => !key.startsWith("HAI_CLI_") && (!key.startsWith("HAI_DEPLOY_") || key === "HAI_DEPLOY_ENVIRONMENT")));
22157
+ const projectName = `${options?.projectName ?? scan.appName}${scanOptions.environment ? `-${environment.data.environment}` : ""}`;
22158
+ options = {
22159
+ ...options,
22160
+ ...scanOptions.environment ? { environment: environment.data.environment } : {},
22161
+ ...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
22162
+ };
21826
22163
  if (currentContainerProvider) {
21827
- return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options);
22164
+ return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
21828
22165
  }
22166
+ if (options?.bundleOnly)
22167
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
21829
22168
  if (!currentProvider) {
21830
22169
  return notInitialized.result();
21831
22170
  }
@@ -21837,15 +22176,22 @@ var deploy = {
21837
22176
  })
21838
22177
  );
21839
22178
  }
21840
- let allEnvVars = {};
22179
+ if (!existsSync(resolveOutputDir(appDir, "vercel")))
22180
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
22181
+ let allEnvVars = appEnv;
21841
22182
  const recovery = { projectName, stage: "provision", resources: [] };
21842
- if (!options?.skipProvision) {
21843
- const provResults = await deploy.provisionAll(projectName);
22183
+ if (!options?.skipMiddleware) {
22184
+ const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
21844
22185
  if (!provResults.success) {
21845
22186
  return err(provResults.error);
21846
22187
  }
21847
22188
  recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
21848
- allEnvVars = mergeProvisionEnv(provResults.data);
22189
+ allEnvVars = { ...allEnvVars, ...mergeProvisionEnv(provResults.data, scan, options) };
22190
+ if (options?.environment) {
22191
+ const saved = saveDeploymentEnvironment(environment.data, mergeProvisionEnv(provResults.data, scan, options));
22192
+ if (!saved.success)
22193
+ return recoveryFailure(saved.error, { ...recovery, stage: "save-env" });
22194
+ }
21849
22195
  }
21850
22196
  const projectResult = await currentProvider.createProject(projectName);
21851
22197
  if (!projectResult.success) {
@@ -21859,12 +22205,6 @@ var deploy = {
21859
22205
  return recoveryFailure(envResult.error, { ...recovery, stage: "set-env" });
21860
22206
  }
21861
22207
  }
21862
- if (!options?.skipBuild) {
21863
- const buildResult = buildApp(appDir, scan.buildCommand, allEnvVars);
21864
- if (!buildResult.success) {
21865
- return recoveryFailure(buildResult.error, { ...recovery, stage: "build" });
21866
- }
21867
- }
21868
22208
  const outputDir = resolveOutputDir(appDir, currentConfig?.provider.type ?? "vercel");
21869
22209
  const deployResult = await currentProvider.deploy(projectId, outputDir);
21870
22210
  if (!deployResult.success) {
@@ -21874,7 +22214,7 @@ var deploy = {
21874
22214
  ...deployResult.data,
21875
22215
  envVarsSet: Object.keys(allEnvVars)
21876
22216
  };
21877
- logger14.info("Deployment completed", {
22217
+ logger12.info("Deployment completed", {
21878
22218
  url: result.url,
21879
22219
  deploymentId: result.deploymentId,
21880
22220
  status: result.status
@@ -21882,25 +22222,79 @@ var deploy = {
21882
22222
  return ok(result);
21883
22223
  }
21884
22224
  };
21885
- async function deployContainerApp(provider, appDir, scan, rawProjectName, options) {
22225
+ async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
21886
22226
  const projectName = normalizeProjectName(rawProjectName);
21887
- if (scan.isSvelteKit && !scan.nodeAdapterInstalled) {
21888
- return err(
21889
- HaiDeployError.ADAPTER_MISSING,
21890
- deployM("deploy_adapterMissing", { params: { adapter: "@sveltejs/adapter-node" } })
21891
- );
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"));
21892
22239
  }
22240
+ if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
22241
+ return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
21893
22242
  let provisionEnv = {};
21894
- if (!options?.skipProvision) {
21895
- const provResults = await deploy.provisionAll(projectName);
22243
+ if (!options?.skipMiddleware) {
22244
+ const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
21896
22245
  if (!provResults.success) {
21897
22246
  return err(provResults.error);
21898
22247
  }
21899
- provisionEnv = mergeProvisionEnv(provResults.data);
22248
+ provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
21900
22249
  }
21901
- return provider.deploy({ appDir, projectName, scan, provisionEnv });
22250
+ return provider.deploy({ appDir, artifact, artifactDir, projectName, scan, provisionEnv, appEnv, options });
22251
+ }
22252
+
22253
+ // ../deploy/src/deploy-middleware.ts
22254
+ var field = (name, defaultValue, kind, secret = false) => ({ name, defaultValue, kind, secret });
22255
+ var required2 = (name, defaultValue, kind) => ({ ...field(name, defaultValue, kind), required: true });
22256
+ var DEPLOY_MIDDLEWARE = {
22257
+ db: [
22258
+ { backend: "sqlite", label: "SQLite", purpose: "deploy_purposeSqlite", fields: [required2("database", "./data/app.db")] },
22259
+ ...["postgresql", "mysql"].map((backend) => ({
22260
+ backend,
22261
+ label: backend === "mysql" ? "MySQL" : "PostgreSQL",
22262
+ purpose: "deploy_purposeDatabase",
22263
+ provisioner: backend === "postgresql" ? "neon" : void 0,
22264
+ fields: [field("url", "", "url", true), required2("host", "localhost"), required2("port", backend === "mysql" ? "3306" : "5432", "port"), required2("database", "hai"), field("user", "hai"), field("password", "", void 0, true), field("ssl", "false")]
22265
+ }))
22266
+ ],
22267
+ cache: [
22268
+ { backend: "memory", label: "Memory", purpose: "deploy_purposeMemory", fields: [] },
22269
+ { backend: "redis", label: "Redis", purpose: "deploy_purposeCache", provisioner: "upstash", fields: [field("url", "", "url", true), required2("host", "localhost"), required2("port", "6379", "port"), field("password", "", void 0, true), { ...field("db", "0", "number"), maximum: 15 }, field("tls", "false", "boolean"), field("keyPrefix"), field("connectTimeout", "10000", "number"), field("commandTimeout", "5000", "number")] }
22270
+ ],
22271
+ storage: [
22272
+ { backend: "local", label: "Local filesystem", purpose: "deploy_purposeLocal", fields: [required2("root", "./data/storage")] },
22273
+ { backend: "s3", label: "S3 / RustFS / MinIO / Cloudflare R2", purpose: "deploy_purposeStorage", provisioner: "cloudflare-r2", fields: [field("endpoint", "", "url"), required2("bucket", "hai"), required2("region", "us-east-1"), required2("accessKeyId"), { ...required2("secretAccessKey"), secret: true }, field("forcePathStyle", "true", "boolean")] }
22274
+ ],
22275
+ vecdb: [
22276
+ { backend: "lancedb", label: "LanceDB", purpose: "deploy_purposeLocal", fields: [required2("path", "./data/vectors")] },
22277
+ { backend: "qdrant", label: "Qdrant", purpose: "deploy_purposeVector", fields: [required2("url", "http://localhost:6333", "url"), field("apiKey", "", void 0, true)] },
22278
+ { backend: "pgvector", label: "PostgreSQL / pgvector", purpose: "deploy_purposeVector", fields: [field("url", "", "url", true), required2("host", "localhost"), required2("port", "5432", "port"), required2("database", "hai"), field("user", "hai"), field("password", "", void 0, true)] },
22279
+ { backend: "chroma", label: "Chroma", purpose: "deploy_purposeVector", fields: [required2("url", "http://localhost:8000", "url")] }
22280
+ ],
22281
+ ldap: [
22282
+ { backend: "disabled", label: "Disabled", purpose: "deploy_purposeDisabled", fields: [] },
22283
+ { backend: "ldap", label: "LDAP / OpenLDAP", purpose: "deploy_purposeLdap", fields: [required2("url", "ldap://localhost:389", "url"), required2("bindDn", "cn=admin,dc=example,dc=org"), { ...required2("bindPassword"), secret: true }, required2("searchBase", "dc=example,dc=org"), field("searchFilter", "(uid={{username}})"), field("usernameAttribute", "uid"), field("emailAttribute", "mail"), field("displayNameAttribute", "cn"), field("useTls", "false", "boolean"), { ...field("connectTimeout", "5000", "number"), minimum: 1e3 }] }
22284
+ ],
22285
+ email: [
22286
+ { backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
22287
+ { backend: "smtp", label: "SMTP", purpose: "deploy_purposeEmail", provisioner: "resend", fields: [required2("host", "localhost"), required2("port", "465", "port"), field("secure", "true", "boolean"), field("user"), field("pass", "", void 0, true), required2("from", "noreply@example.com")] },
22288
+ { backend: "api", label: "HTTP notification API", purpose: "deploy_purposeEmail", fields: [required2("url", "", "url"), field("method", "POST"), field("headers"), field("timeout", "10000", "number")] }
22289
+ ],
22290
+ sms: [
22291
+ { backend: "aliyun-sms", label: "Aliyun SMS", purpose: "deploy_purposeSms", provisioner: "aliyun", fields: [required2("accessKeyId"), { ...required2("accessKeySecret"), secret: true }, required2("signName"), field("endpoint", "dysmsapi.aliyuncs.com")] }
22292
+ ]
22293
+ };
22294
+ function describeMiddlewareBackend(backend) {
22295
+ return deployM(backend.purpose);
21902
22296
  }
21903
22297
 
21904
- export { DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, getDeployRecovery };
21905
- //# sourceMappingURL=src-DUGLX4YB.js.map
21906
- //# sourceMappingURL=src-DUGLX4YB.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