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

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 (164) hide show
  1. package/README.en.md +40 -4
  2. package/README.ja.md +40 -4
  3. package/README.md +40 -4
  4. package/dist/{src-2V6VP4I5.js → chunk-FI56GONY.js} +32 -3103
  5. package/dist/chunk-FI56GONY.js.map +1 -0
  6. package/dist/chunk-MBPB37FR.js +1348 -0
  7. package/dist/chunk-MBPB37FR.js.map +1 -0
  8. package/dist/{chunk-7DSURTJ4.js → chunk-ZFNBUF77.js} +327 -199
  9. package/dist/chunk-ZFNBUF77.js.map +1 -0
  10. package/dist/cli-config-templates-W2LLPMB6.js +5 -0
  11. package/dist/{cli-config-templates-6AHNUGBN.js.map → cli-config-templates-W2LLPMB6.js.map} +1 -1
  12. package/dist/{cli-skill-templates-FC3QRKDO.js → cli-skill-templates-US5CCAJ4.js} +3 -3
  13. package/dist/{cli-skill-templates-FC3QRKDO.js.map → cli-skill-templates-US5CCAJ4.js.map} +1 -1
  14. package/dist/index.js +920 -336
  15. package/dist/index.js.map +1 -1
  16. package/dist/metafile-esm.json +1 -0
  17. package/dist/src-DSJJXM2F.js +3849 -0
  18. package/dist/src-DSJJXM2F.js.map +1 -0
  19. package/package.json +9 -8
  20. package/templates/apps/admin/messages/ja-JP.json +38 -0
  21. package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -6
  22. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +3 -3
  23. package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +2 -0
  24. package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +4 -1
  25. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +5 -1
  26. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +0 -10
  27. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +2 -1
  28. package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +24 -11
  29. package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +8 -1
  30. package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +68 -11
  31. package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +10 -2
  32. package/templates/apps/api/e2e/api-service.spec.ts.hbs +2 -2
  33. package/templates/apps/api/package.json.hbs +1 -1
  34. package/templates/apps/api/playwright.config.ts.hbs +15 -7
  35. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +1 -1
  36. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
  37. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +7 -1
  38. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +1 -1
  39. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +1 -1
  40. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
  41. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +1 -1
  42. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
  43. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +1 -1
  44. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
  45. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +1 -1
  46. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +19 -8
  47. package/templates/apps/fullstack/package.json.hbs +1 -1
  48. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
  49. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +2 -0
  50. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +3 -0
  51. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +5 -2
  52. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +0 -6
  53. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -0
  54. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +2 -1
  55. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
  56. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +9 -5
  57. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +3 -2
  58. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +16 -1
  59. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +7 -3
  60. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -0
  61. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +58 -7
  62. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +13 -3
  63. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +9 -4
  64. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +105 -0
  65. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +3 -3
  66. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +5 -5
  67. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +7 -7
  68. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +1 -1
  69. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +8 -8
  70. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +11 -12
  71. package/templates/apps/fullstack/playwright.config.ts.hbs +15 -6
  72. package/templates/apps/h5/messages/ja-JP.json +40 -0
  73. package/templates/apps/h5/src/routes/+layout.svelte.hbs +14 -3
  74. package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
  75. package/templates/apps/mobile-app/src/App.svelte.hbs +13 -3
  76. package/templates/apps/mobile-app/src/lib/api.ts.hbs +173 -14
  77. package/templates/apps/website/messages/ja-JP.json +32 -0
  78. package/templates/apps/website/src/routes/+layout.svelte.hbs +10 -2
  79. package/templates/base/e2e/app.spec.ts.hbs +13 -8
  80. package/templates/base/package.json.hbs +4 -1
  81. package/templates/base/playwright.config.ts +15 -4
  82. package/templates/base/src/hooks.server.ts.hbs +15 -6
  83. package/templates/base/src/lib/server/init.ts.hbs +139 -30
  84. package/templates/base/vite.config.ts.hbs +2 -2
  85. package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +11 -6
  86. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +18 -25
  87. package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +4 -0
  88. package/templates/i18n/project.inlang/settings.json +1 -1
  89. package/templates/shared/e2e-env.ts +98 -0
  90. package/templates/shared/run-e2e.mjs +51 -12
  91. package/templates/skills/AGENTS.md +4 -0
  92. package/templates/skills/bridges/admin/AGENTS.md +5 -1
  93. package/templates/skills/bridges/api/AGENTS.md +2 -0
  94. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +4 -0
  95. package/templates/skills/bridges/h5/AGENTS.md +5 -1
  96. package/templates/skills/bridges/mobile-app/AGENTS.md +5 -1
  97. package/templates/skills/bridges/website/AGENTS.md +5 -1
  98. package/templates/skills/hai-ai/SKILL.md +8 -2
  99. package/templates/skills/hai-ai/guide.md +134 -60
  100. package/templates/skills/hai-ai/reference.md +51 -11
  101. package/templates/skills/hai-api-client/SKILL.md +2 -0
  102. package/templates/skills/hai-api-client/reference.md +17 -5
  103. package/templates/skills/hai-api-contract/SKILL.md +1 -0
  104. package/templates/skills/hai-api-contract/reference.md +4 -2
  105. package/templates/skills/hai-app-create/SKILL.md +4 -0
  106. package/templates/skills/hai-app-create/development.md +20 -4
  107. package/templates/skills/hai-app-create/reference.md +2 -1
  108. package/templates/skills/hai-app-review/SKILL.md +6 -2
  109. package/templates/skills/hai-app-review/differential.md +34 -0
  110. package/templates/skills/hai-app-tests/SKILL.md +3 -1
  111. package/templates/skills/hai-app-tests/reference.md +4 -4
  112. package/templates/skills/hai-app-tests/validation.md +10 -3
  113. package/templates/skills/hai-audit/SKILL.md +10 -8
  114. package/templates/skills/hai-audit/reference.md +3 -0
  115. package/templates/skills/hai-build/reference.md +6 -4
  116. package/templates/skills/hai-cache/SKILL.md +9 -3
  117. package/templates/skills/hai-cache/reference.md +53 -11
  118. package/templates/skills/hai-capacitor/SKILL.md +3 -1
  119. package/templates/skills/hai-capacitor/reference.md +9 -5
  120. package/templates/skills/hai-core/SKILL.md +4 -0
  121. package/templates/skills/hai-core/reference.md +25 -5
  122. package/templates/skills/hai-crypto/SKILL.md +10 -2
  123. package/templates/skills/hai-crypto/reference.md +64 -10
  124. package/templates/skills/hai-datapipe/SKILL.md +2 -0
  125. package/templates/skills/hai-datapipe/reference.md +3 -1
  126. package/templates/skills/hai-deploy/SKILL.md +9 -4
  127. package/templates/skills/hai-deploy/middleware.md +51 -37
  128. package/templates/skills/hai-iam/SKILL.md +24 -8
  129. package/templates/skills/hai-iam/reference.md +181 -154
  130. package/templates/skills/hai-kit/SKILL.md +23 -7
  131. package/templates/skills/hai-kit/reference.md +42 -24
  132. package/templates/skills/hai-payment/SKILL.md +13 -11
  133. package/templates/skills/hai-payment/reference.md +80 -41
  134. package/templates/skills/hai-pr-review/SKILL.md +21 -88
  135. package/templates/skills/hai-reach/SKILL.md +5 -2
  136. package/templates/skills/hai-reach/reference.md +83 -22
  137. package/templates/skills/hai-reldb/SKILL.md +5 -2
  138. package/templates/skills/hai-reldb/reference.md +68 -25
  139. package/templates/skills/hai-scheduler/SKILL.md +8 -5
  140. package/templates/skills/hai-scheduler/reference.md +38 -24
  141. package/templates/skills/hai-serv/SKILL.md +23 -8
  142. package/templates/skills/hai-serv/reference.md +123 -87
  143. package/templates/skills/hai-spec-implement/SKILL.md +4 -0
  144. package/templates/skills/hai-spec-implement/evaluation.md +18 -0
  145. package/templates/skills/hai-spec-implement/handoff.md +47 -0
  146. package/templates/skills/hai-spec-implement/templates.md +5 -0
  147. package/templates/skills/hai-spec-implement/workflow.md +2 -0
  148. package/templates/skills/hai-spec-review/SKILL.md +4 -0
  149. package/templates/skills/hai-storage/SKILL.md +8 -2
  150. package/templates/skills/hai-storage/reference.md +64 -25
  151. package/templates/skills/hai-ui/SKILL.md +16 -2
  152. package/templates/skills/hai-ui/guide.md +3 -3
  153. package/templates/skills/hai-ui/reference.md +17 -6
  154. package/templates/skills/hai-ui-design/SKILL.md +31 -0
  155. package/templates/skills/hai-ui-design/patterns.md +26 -0
  156. package/templates/skills/hai-ui-review/SKILL.md +30 -0
  157. package/templates/skills/hai-ui-review/checklist.md +22 -0
  158. package/templates/skills/hai-vecdb/SKILL.md +12 -8
  159. package/templates/skills/hai-vecdb/reference.md +33 -3
  160. package/dist/chunk-7DSURTJ4.js.map +0 -1
  161. package/dist/chunk-AOQ4N657.js +0 -640
  162. package/dist/chunk-AOQ4N657.js.map +0 -1
  163. package/dist/cli-config-templates-6AHNUGBN.js +0 -5
  164. package/dist/src-2V6VP4I5.js.map +0 -1
@@ -1,15 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { __export } from './chunk-2H7UOFLK.js';
3
- import { createHash, randomBytes } from 'crypto';
4
- import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, renameSync, rmSync, statSync, readdirSync } from 'fs';
5
- import { resolve, join, basename, dirname, relative, sep } from 'path';
6
- import { core, err, ok } from '@h-ai/core';
7
- import process2 from 'process';
8
- import { parseEnv } from 'util';
9
- import { LineCounter, parseDocument, parse as parse$1, stringify, parseAllDocuments } from 'yaml';
10
- import { homedir } from 'os';
11
- import { spawn } from 'child_process';
12
- import { Buffer } from 'buffer';
13
3
 
14
4
  // ../../node_modules/zod/v4/classic/external.js
15
5
  var external_exports = {};
@@ -1398,10 +1388,10 @@ function defineLazyInternal(inst, key, compute) {
1398
1388
  Object.defineProperty(this, key, { configurable: true, writable: true, value });
1399
1389
  broke = broke || outer;
1400
1390
  return value;
1401
- } catch (err15) {
1391
+ } catch (err) {
1402
1392
  delete this[key];
1403
1393
  broke = broke || outer;
1404
- throw err15;
1394
+ throw err;
1405
1395
  }
1406
1396
  },
1407
1397
  set(value) {
@@ -1856,18 +1846,18 @@ var validateAsync = async (schema, value, _ctx) => {
1856
1846
  return result.issues.length === 0;
1857
1847
  };
1858
1848
  var _encode = (_Err) => {
1859
- const parse6 = _parse(_Err);
1849
+ const parse3 = _parse(_Err);
1860
1850
  const fn = (schema, value, _ctx, _params) => {
1861
1851
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
1862
- return parse6(schema, value, ctx, finalizeParams(fn, _params));
1852
+ return parse3(schema, value, ctx, finalizeParams(fn, _params));
1863
1853
  };
1864
1854
  return fn;
1865
1855
  };
1866
1856
  var encode = /* @__PURE__ */ _encode($ZodRealError);
1867
1857
  var _decode = (_Err) => {
1868
- const parse6 = _parse(_Err);
1858
+ const parse3 = _parse(_Err);
1869
1859
  const fn = (schema, value, _ctx, _params) => {
1870
- return parse6(schema, value, _ctx, finalizeParams(fn, _params));
1860
+ return parse3(schema, value, _ctx, finalizeParams(fn, _params));
1871
1861
  };
1872
1862
  return fn;
1873
1863
  };
@@ -3521,14 +3511,14 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
3521
3511
  const shape = zod.def.shape;
3522
3512
  const propValues = {};
3523
3513
  for (const key in shape) {
3524
- const field2 = shape[key]._zod;
3525
- if (field2.values) {
3514
+ const field = shape[key]._zod;
3515
+ if (field.values) {
3526
3516
  if (!Object.prototype.hasOwnProperty.call(propValues, key)) {
3527
3517
  assignProp(propValues, key, /* @__PURE__ */ new Set());
3528
3518
  }
3529
- for (const v of field2.values)
3519
+ for (const v of field.values)
3530
3520
  propValues[key].add(v);
3531
- if (field2.optin !== void 0)
3521
+ if (field.optin !== void 0)
3532
3522
  propValues[key].add(void 0);
3533
3523
  }
3534
3524
  }
@@ -5037,7 +5027,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5037
5027
  var NONE = 0;
5038
5028
  var ASSUMED = 1;
5039
5029
  var PROVEN = 2;
5040
- function isRecursive(inst, stack, resolve6) {
5030
+ function isRecursive(inst, stack, resolve) {
5041
5031
  const cached2 = recursive.get(inst);
5042
5032
  if (cached2 !== void 0)
5043
5033
  return cached2 ? PROVEN : NONE;
@@ -5047,7 +5037,7 @@ function isRecursive(inst, stack, resolve6) {
5047
5037
  let result = NONE;
5048
5038
  const check2 = (child) => {
5049
5039
  if (result !== PROVEN && child?._zod) {
5050
- const answer = isRecursive(child, stack, resolve6);
5040
+ const answer = isRecursive(child, stack, resolve);
5051
5041
  if (answer > result)
5052
5042
  result = answer;
5053
5043
  }
@@ -5058,7 +5048,7 @@ function isRecursive(inst, stack, resolve6) {
5058
5048
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5059
5049
  if (!desc.enumerable)
5060
5050
  continue;
5061
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve6) : NONE;
5051
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve) : NONE;
5062
5052
  if (child > answer)
5063
5053
  answer = child;
5064
5054
  }
@@ -5122,7 +5112,7 @@ function isRecursive(inst, stack, resolve6) {
5122
5112
  break;
5123
5113
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5124
5114
  case "lazy": {
5125
- const inner = def._cachedInner ?? (resolve6 ? inst._zod.innerType : void 0);
5115
+ const inner = def._cachedInner ?? (resolve ? inst._zod.innerType : void 0);
5126
5116
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5127
5117
  break;
5128
5118
  }
@@ -12940,9 +12930,9 @@ function compile(schema, options) {
12940
12930
  const clone2 = withParser(schema, parser);
12941
12931
  clone2._zod.bag.validator = compileValidator(schema, parser);
12942
12932
  return clone2;
12943
- } catch (err15) {
12933
+ } catch (err) {
12944
12934
  if (options?.strict)
12945
- throw err15;
12935
+ throw err;
12946
12936
  return schema;
12947
12937
  }
12948
12938
  }
@@ -13032,8 +13022,8 @@ ${code}
13032
13022
  try {
13033
13023
  const factory = new F(...constantNames, factoryCode);
13034
13024
  fn = factory(...constantValues);
13035
- } catch (err15) {
13036
- throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${err15.message})`);
13025
+ } catch (err) {
13026
+ throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${err.message})`);
13037
13027
  }
13038
13028
  if (options?.debug) {
13039
13029
  fn.code = fullCode;
@@ -13071,9 +13061,9 @@ function compileChild(doc, ctx, schema, accessor, needsValue = true) {
13071
13061
  const varCounter = ctx.varCounter;
13072
13062
  try {
13073
13063
  return generateCheck(doc, ctx, schema, accessor, needsValue);
13074
- } catch (err15) {
13075
- if (!(err15 instanceof ZodCompileUnsupportedError) || !err15.islandable)
13076
- throw err15;
13064
+ } catch (err) {
13065
+ if (!(err instanceof ZodCompileUnsupportedError) || !err.islandable)
13066
+ throw err;
13077
13067
  doc.content.length = contentLen;
13078
13068
  if (ctx.constants.size > constantCount) {
13079
13069
  const trailing = Array.from(ctx.constants.keys()).slice(constantCount);
@@ -15807,7 +15797,7 @@ function foldObjects(members2) {
15807
15797
  objects.push(member);
15808
15798
  }
15809
15799
  const properties = {};
15810
- const required3 = /* @__PURE__ */ new Set();
15800
+ const required2 = /* @__PURE__ */ new Set();
15811
15801
  for (const object2 of objects) {
15812
15802
  for (const key in object2.properties) {
15813
15803
  if (Object.prototype.hasOwnProperty.call(properties, key))
@@ -15824,11 +15814,11 @@ function foldObjects(members2) {
15824
15814
  assignProp(properties, key, merged);
15825
15815
  }
15826
15816
  for (const key of object2.required ?? [])
15827
- required3.add(key);
15817
+ required2.add(key);
15828
15818
  }
15829
15819
  const folded = { type: "object", properties };
15830
- if (required3.size)
15831
- folded.required = [...required3];
15820
+ if (required2.size)
15821
+ folded.required = [...required2];
15832
15822
  if (objects.every((object2) => object2.additionalProperties === false)) {
15833
15823
  folded.additionalProperties = false;
15834
15824
  } else {
@@ -16446,8 +16436,8 @@ var objectProcessor = (schema, ctx, _json, params) => {
16446
16436
  }
16447
16437
  const requiredKeys = [];
16448
16438
  for (const key of Object.keys(shape)) {
16449
- const field2 = def.shape[key];
16450
- if (ctx.io === "input" ? inputOptin(field2) === void 0 : field2._zod.optout === void 0) {
16439
+ const field = def.shape[key];
16440
+ if (ctx.io === "input" ? inputOptin(field) === void 0 : field._zod.optout === void 0) {
16451
16441
  requiredKeys.push(key);
16452
16442
  }
16453
16443
  }
@@ -16605,8 +16595,8 @@ function rewriteKeyNames(ctx) {
16605
16595
  bySchema.set(entry.schema, entry);
16606
16596
  }
16607
16597
  const rewrites = /* @__PURE__ */ new Map();
16608
- for (const record3 of pendingRecords.get(ctx) ?? []) {
16609
- const seen = ctx.seen.get(record3);
16598
+ for (const record2 of pendingRecords.get(ctx) ?? []) {
16599
+ const seen = ctx.seen.get(record2);
16610
16600
  const names = (seen?.def ?? seen?.schema)?.propertyNames;
16611
16601
  if (!names || names === true || rewrites.has(names))
16612
16602
  continue;
@@ -19668,3067 +19658,6 @@ function date4(params) {
19668
19658
  return _coercedDate(ZodDate, params);
19669
19659
  }
19670
19660
 
19671
- // ../deploy/src/deploy-artifact.ts
19672
- var DeploymentArtifactSchema = external_exports.object({
19673
- /** 清单格式版本,与应用版本和部署环境无关。 */
19674
- version: external_exports.literal(3),
19675
- /** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
19676
- appVersion: external_exports.string().max(80).regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9a-z-]+(?:\.[0-9a-z-]+)*)?(?:\+[0-9a-z-]+(?:\.[0-9a-z-]+)*)?$/i),
19677
- /** 本次构建标识;同一应用版本允许重复构建。 */
19678
- buildId: external_exports.string().regex(/^[a-z0-9][\w.+-]*$/i),
19679
- /** 只包含所选镜像的构建不能用于首次安装。 */
19680
- partial: external_exports.boolean().default(false),
19681
- /** API 单独构建时仍保留工程的 Web 拓扑。 */
19682
- hasWeb: external_exports.boolean().default(false),
19683
- /** 工程显式配置的应用端口,部分构建未包含 app 镜像时仍保留;缺省由部署配置决定。 */
19684
- appContainerPort: external_exports.number().int().min(1).max(65535).optional(),
19685
- /** 工程显式配置的 Web 端口,部分构建未包含 web 镜像时仍保留;缺省使用部署默认值。 */
19686
- webContainerPort: external_exports.number().int().min(1).max(65535).optional(),
19687
- /** 本次构建选择的镜像归档。 */
19688
- images: external_exports.array(external_exports.object({
19689
- /** app 为服务或独立应用,web 为同源 API 代理的静态前端。 */
19690
- name: external_exports.enum(["app", "web"]),
19691
- /** 相对于清单目录的 tar 文件名。 */
19692
- file: external_exports.string().regex(/^[\w.+-]+\.tar$/),
19693
- /** 归档导入后可直接使用的不可变镜像引用。 */
19694
- imageRef: external_exports.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
19695
- /** tar 内容的 SHA-256,用于部署前校验。 */
19696
- sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
19697
- /** 容器内部监听端口;服务器公开端口由部署选择。 */
19698
- containerPort: external_exports.number().int().min(1).max(65535).optional()
19699
- })).min(1).max(2).refine((images) => new Set(images.map((image) => image.name)).size === images.length)
19700
- }).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`)).refine((artifact) => artifact.partial || artifact.images.some((image) => image.name === "app")).refine((artifact) => artifact.partial || !artifact.hasWeb || artifact.images.some((image) => image.name === "web"));
19701
- async function artifactChecksum(file2) {
19702
- const hash2 = createHash("sha256");
19703
- for await (const chunk of createReadStream(file2))
19704
- hash2.update(chunk);
19705
- return hash2.digest("hex");
19706
- }
19707
- async function readDeploymentArtifact(directory) {
19708
- const artifact = DeploymentArtifactSchema.parse(JSON.parse(readFileSync(resolve(directory, "manifest.json"), "utf8")));
19709
- for (const image of artifact.images) {
19710
- const file2 = resolve(directory, image.file);
19711
- if (!existsSync(file2) || !lstatSync(file2).isFile() || lstatSync(file2).isSymbolicLink() || await artifactChecksum(file2) !== image.sha256)
19712
- throw new Error(`Invalid artifact: ${image.file}`);
19713
- }
19714
- return artifact;
19715
- }
19716
-
19717
- // ../deploy/messages/en-US.json
19718
- var en_US_default = {
19719
- deploy_notInitialized: "Deploy module not initialized, call deploy.init() first",
19720
- deploy_initFailed: "Deploy module initialization failed: {error}",
19721
- deploy_unsupportedProviderType: "Unsupported deploy provider type: {type}",
19722
- deploy_unsupportedProvisionerType: "Unsupported service provisioner type: {type}",
19723
- deploy_configTokenRequired: "Deploy provider token is required",
19724
- deploy_authRequired: "Authentication required",
19725
- deploy_authFailed: "Authentication failed: {error}",
19726
- deploy_projectCreateFailed: "Project creation failed: {error}",
19727
- deploy_uploadFailed: "Build output upload failed: {error}",
19728
- deploy_deployFailed: "Deployment failed: {error}",
19729
- deploy_provisionFailed: "Service provisioning failed: {service} - {error}",
19730
- deploy_adapterMissing: "Target platform adapter not found, please install {adapter}",
19731
- deploy_scanFailed: "Application scan failed: {error}",
19732
- deploy_envVarFailed: "Environment variable setup failed: {error}",
19733
- deploy_credentialError: "Credential read/write failed: {error}",
19734
- deploy_credentialInvalidKey: "Unsupported credential key: {key}. Only HAI_DEPLOY_* keys are allowed",
19735
- deploy_credentialSaved: "Credentials saved",
19736
- deploy_credentialLoaded: "Credentials loaded",
19737
- deploy_configError: "Deploy config error: {error}",
19738
- deploy_credentialMissing: "Missing credential fields: {fields}",
19739
- deploy_apiError: "{service} API error ({status})",
19740
- deploy_fileUploadFailed: "File upload failed ({status})",
19741
- deploy_provisionNoResult: "No connection info returned from {service}",
19742
- deploy_recoveryFailure: "Deployment stopped at {stage}; inspect the recovery details.",
19743
- deploy_recoverySuggestion: "Inspect listed resources in the provider console before retrying with the same project name. Unknown means the request may have created a resource. Reused resources are never deleted automatically; remove newly created resources manually only after checking their use.",
19744
- deploy_commandTimeout: "Command timed out: {command}",
19745
- deploy_commandFailed: "Command failed ({command}): {error}",
19746
- deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
19747
- deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
19748
- deploy_imageTransferFailed: "Image transfer failed: {error}",
19749
- deploy_healthCheckFailed: "Health check failed, app not ready at {url}",
19750
- deploy_invalidEnvironment: "Use letters, numbers, underscores or hyphens for the environment; local/example are reserved.",
19751
- deploy_environmentMissing: "Environment file missing: {path}. Create and configure it before deploying.",
19752
- deploy_environmentReadFailed: "Cannot read deployment environment files; check syntax and permissions.",
19753
- deploy_environmentWriteFailed: "Cannot save deployment configuration; check file syntax and permissions.",
19754
- deploy_multipleApplications: "Multiple service packages found; specify the application directory.",
19755
- deploy_purposeSqlite: "Local relational database persisted in a volume",
19756
- deploy_purposeDatabase: "Relational database service for business data",
19757
- deploy_purposeMemory: "Per-process memory cache; cleared on restart, no external service",
19758
- deploy_purposeLocal: "Local filesystem storage persisted in a volume",
19759
- deploy_purposeCache: "Shared cache, sessions and distributed locks",
19760
- deploy_purposeStorage: "S3-compatible object storage for uploaded files",
19761
- deploy_purposeVector: "Vector indexing and similarity search",
19762
- deploy_purposeDisabled: "LDAP login is disabled; no directory service is created",
19763
- deploy_purposeLdap: "Directory account authentication; the default directory has no business users",
19764
- deploy_purposeConsole: "Messages go to application logs without delivery",
19765
- deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
19766
- deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota",
19767
- deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
19768
- deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target docker, select valid --artifact-dir, or use pnpm run deploy --build.",
19769
- deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
19770
- deploy_bundleInstructions: "Bundle: {file}\nIncludes all images needed by selected services, including self-hosted middleware.\nWith SSH access, run deploy.ps1 (Windows) or deploy.sh (Linux/macOS) from the adjacent deployment directory.\nFor isolated servers, copy and extract the tar into a separate directory, then run sh apply.sh /opt/hai/apps/project-environment.\nLinux requires Docker/Podman, Compose, sh, tar, sha256sum, awk, curl and filesystem/container permissions.\nPartial updates require a successful full installation with matching ports/layout. Steps return nonzero on failure; result-DEPLOYMENT_ID.json records the outcome.\nThe bundle contains private runtime configuration; transfer it securely.\nIncluded images:\n{images}",
19771
- deploy_bundleMetadata: "Application version: {version}\nBuild ID: {buildId}\nCompose file: {compose}",
19772
- deploy_bundleOnlyContainer: "Compose bundle generation is supported only by docker-ssh.",
19773
- deploy_selectedContainerOnly: "Selected-service deployment requires a docker-ssh Compose destination.",
19774
- deploy_selectionInvalid: "Cannot deploy {service}: check the service name and build its image first.",
19775
- deploy_imageVersionRequired: "Specify an image version; latest is not allowed: {image}",
19776
- deploy_bundleRuntimeRequired: "Start local Podman/Docker to export middleware images, or prepare this complete offline bundle on the build machine.",
19777
- deploy_bundleFailed: "Package preparation or deployment failed: {reason}",
19778
- deploy_configFileInvalid: "Invalid YAML in {path}, line {line}. Check indentation, colons and quotes.",
19779
- deploy_configFileUnreadable: "Cannot read configuration file {path}. Check its path and permissions.",
19780
- deploy_bundleNameInvalid: "Bundle filenames may contain letters, numbers, dots, plus signs, hyphens and underscores; parent directories may contain spaces.",
19781
- deploy_secretsInvalid: "Middleware credentials are damaged or incomplete: {path}. Restore a backup; passwords are not regenerated because existing data may become inaccessible.",
19782
- deploy_storageArtifactRequired: "Storage initialization requires the application image. Select a complete artifact containing app with --artifact-dir, or run pnpm build --target docker --images app first."
19783
- };
19784
-
19785
- // ../deploy/messages/zh-CN.json
19786
- var zh_CN_default2 = {
19787
- deploy_notInitialized: "Deploy \u6A21\u5757\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 deploy.init()",
19788
- deploy_initFailed: "Deploy \u6A21\u5757\u521D\u59CB\u5316\u5931\u8D25\uFF1A{error}",
19789
- deploy_unsupportedProviderType: "\u4E0D\u652F\u6301\u7684\u90E8\u7F72\u5E73\u53F0\u7C7B\u578B\uFF1A{type}",
19790
- deploy_unsupportedProvisionerType: "\u4E0D\u652F\u6301\u7684\u670D\u52A1 Provisioner \u7C7B\u578B\uFF1A{type}",
19791
- deploy_configTokenRequired: "\u90E8\u7F72\u5E73\u53F0 Token \u4E0D\u80FD\u4E3A\u7A7A",
19792
- deploy_authRequired: "\u8BF7\u5148\u5B8C\u6210\u8BA4\u8BC1",
19793
- deploy_authFailed: "\u8BA4\u8BC1\u5931\u8D25\uFF1A{error}",
19794
- deploy_projectCreateFailed: "\u9879\u76EE\u521B\u5EFA\u5931\u8D25\uFF1A{error}",
19795
- deploy_uploadFailed: "\u6784\u5EFA\u4EA7\u7269\u4E0A\u4F20\u5931\u8D25\uFF1A{error}",
19796
- deploy_deployFailed: "\u90E8\u7F72\u5931\u8D25\uFF1A{error}",
19797
- deploy_provisionFailed: "\u57FA\u7840\u8BBE\u65BD\u5F00\u901A\u5931\u8D25\uFF1A{service} - {error}",
19798
- deploy_adapterMissing: "\u672A\u68C0\u6D4B\u5230\u76EE\u6807\u5E73\u53F0 adapter\uFF0C\u8BF7\u5B89\u88C5 {adapter}",
19799
- deploy_scanFailed: "\u5E94\u7528\u626B\u63CF\u5931\u8D25\uFF1A{error}",
19800
- deploy_envVarFailed: "\u73AF\u5883\u53D8\u91CF\u8BBE\u7F6E\u5931\u8D25\uFF1A{error}",
19801
- deploy_credentialError: "\u51ED\u8BC1\u8BFB\u5199\u5931\u8D25\uFF1A{error}",
19802
- deploy_credentialInvalidKey: "\u4E0D\u652F\u6301\u7684\u51ED\u8BC1\u952E\u540D\uFF1A{key}\u3002\u4EC5\u5141\u8BB8 HAI_DEPLOY_*",
19803
- deploy_credentialSaved: "\u51ED\u8BC1\u5DF2\u4FDD\u5B58",
19804
- deploy_credentialLoaded: "\u51ED\u8BC1\u5DF2\u52A0\u8F7D",
19805
- deploy_configError: "\u90E8\u7F72\u914D\u7F6E\u9519\u8BEF\uFF1A{error}",
19806
- deploy_credentialMissing: "\u7F3A\u5C11\u51ED\u8BC1\u5B57\u6BB5\uFF1A{fields}",
19807
- deploy_apiError: "{service} API \u8BF7\u6C42\u5931\u8D25\uFF08{status}\uFF09",
19808
- deploy_fileUploadFailed: "\u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF08{status}\uFF09",
19809
- deploy_provisionNoResult: "{service} \u672A\u8FD4\u56DE\u8FDE\u63A5\u4FE1\u606F",
19810
- deploy_recoveryFailure: "\u90E8\u7F72\u5728 {stage} \u9636\u6BB5\u505C\u6B62\uFF0C\u8BF7\u67E5\u770B\u6062\u590D\u6E05\u5355\u3002",
19811
- deploy_recoverySuggestion: "\u91CD\u8BD5\u524D\u5728\u670D\u52A1\u5546\u63A7\u5236\u53F0\u6838\u5BF9\u6E05\u5355\uFF0C\u5E76\u4F7F\u7528\u76F8\u540C\u9879\u76EE\u540D\u3002unknown \u8868\u793A\u8BF7\u6C42\u53EF\u80FD\u5DF2\u521B\u5EFA\u8D44\u6E90\u3002\u590D\u7528\u8D44\u6E90\u4E0D\u4F1A\u81EA\u52A8\u5220\u9664\uFF1B\u65B0\u5EFA\u8D44\u6E90\u4EC5\u5728\u786E\u8BA4\u672A\u88AB\u4F7F\u7528\u540E\u4EBA\u5DE5\u6E05\u7406\u3002",
19812
- deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
19813
- deploy_commandFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08{command}\uFF09\uFF1A{error}",
19814
- deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
19815
- deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
19816
- deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
19817
- deploy_healthCheckFailed: "\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF0C\u5E94\u7528\u672A\u5728 {url} \u5C31\u7EEA",
19818
- 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",
19819
- 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",
19820
- deploy_environmentReadFailed: "\u90E8\u7F72\u73AF\u5883\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u683C\u5F0F\u4E0E\u6743\u9650\u3002",
19821
- deploy_environmentWriteFailed: "\u90E8\u7F72\u914D\u7F6E\u5199\u5165\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u683C\u5F0F\u4E0E\u6743\u9650\u3002",
19822
- deploy_multipleApplications: "\u53D1\u73B0\u591A\u4E2A\u670D\u52A1\u5305\uFF0C\u8BF7\u6307\u5B9A\u8981\u90E8\u7F72\u7684\u5E94\u7528\u76EE\u5F55\u3002",
19823
- deploy_purposeSqlite: "\u672C\u5730\u6587\u4EF6\u5173\u7CFB\u6570\u636E\u5E93\uFF0C\u4F7F\u7528\u6301\u4E45\u5316\u5377",
19824
- deploy_purposeDatabase: "\u72EC\u7ACB\u5173\u7CFB\u6570\u636E\u5E93\u670D\u52A1\uFF0C\u4FDD\u5B58\u4E1A\u52A1\u6570\u636E",
19825
- deploy_purposeMemory: "\u5355\u8FDB\u7A0B\u5185\u5B58\u7F13\u5B58\uFF0C\u91CD\u542F\u540E\u6E05\u7A7A\uFF0C\u4E0D\u521B\u5EFA\u5916\u90E8\u670D\u52A1",
19826
- deploy_purposeLocal: "\u672C\u5730\u6587\u4EF6\u5B58\u50A8\uFF0C\u4F7F\u7528\u6301\u4E45\u5316\u5377",
19827
- deploy_purposeCache: "\u5171\u4EAB\u7F13\u5B58\u3001\u4F1A\u8BDD\u4E0E\u5206\u5E03\u5F0F\u9501",
19828
- deploy_purposeStorage: "S3 \u517C\u5BB9\u5BF9\u8C61\u5B58\u50A8\uFF0C\u4FDD\u5B58\u4E0A\u4F20\u6587\u4EF6",
19829
- deploy_purposeVector: "\u5411\u91CF\u7D22\u5F15\u4E0E\u76F8\u4F3C\u5EA6\u641C\u7D22",
19830
- deploy_purposeDisabled: "LDAP \u767B\u5F55\u672A\u542F\u7528\uFF0C\u4E0D\u521B\u5EFA\u76EE\u5F55\u670D\u52A1",
19831
- deploy_purposeLdap: "\u4F01\u4E1A\u76EE\u5F55\u8D26\u53F7\u8BA4\u8BC1\uFF1B\u9ED8\u8BA4\u76EE\u5F55\u4E0D\u5305\u542B\u4E1A\u52A1\u7528\u6237",
19832
- deploy_purposeConsole: "\u6D88\u606F\u8F93\u51FA\u5230\u5E94\u7528\u65E5\u5FD7\uFF0C\u4E0D\u5B9E\u9645\u6295\u9012",
19833
- deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
19834
- deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6",
19835
- deploy_webProxyInvalid: "API \u524D\u7F00\u5FC5\u987B\u662F\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26\u548C\u659C\u6760\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
19836
- deploy_artifactInvalid: "\u955C\u50CF\u4EA7\u7269\u7F3A\u5931\u6216\u6821\u9A8C\u5931\u8D25\u3002\u8BF7\u5148\u6267\u884C pnpm build --target docker\uFF0C\u6216\u7528 --artifact-dir \u6307\u5B9A\u6709\u6548\u4EA7\u7269\uFF1B\u4E5F\u53EF\u7528 pnpm run deploy --build \u4E00\u6B21\u5B8C\u6210\u3002",
19837
- deploy_sshRequired: "\u81EA\u52A8\u90E8\u7F72\u9700\u8981 SSH \u914D\u7F6E\uFF1B\u65E0\u7F51\u7EDC\u8FDE\u63A5\u65F6\u4F7F\u7528 --bundle-only \u751F\u6210\u90E8\u7F72\u5305\u5E76\u624B\u5DE5\u642C\u8FD0\u3002",
19838
- deploy_bundleInstructions: "\u90E8\u7F72\u5305\uFF1A{file}\n\u5305\u4E2D\u5DF2\u5305\u542B\u672C\u6B21\u9009\u62E9\u670D\u52A1\u9700\u8981\u7684\u955C\u50CF\uFF0C\u5305\u62EC\u81EA\u5EFA\u4E2D\u95F4\u4EF6\u3002\n\u6709 SSH \u8FDE\u63A5\u65F6\uFF0C\u5728\u65C1\u8FB9\u7684\u90E8\u7F72\u76EE\u5F55\u8FD0\u884C deploy.ps1\uFF08Windows\uFF09\u6216 deploy.sh\uFF08Linux/macOS\uFF09\u3002\n\u5B8C\u5168\u9694\u79BB\u65F6\uFF0C\u590D\u5236 tar \u5230 Linux \u670D\u52A1\u5668\uFF0C\u5728\u72EC\u7ACB\u76EE\u5F55\u89E3\u5305\uFF0C\u518D\u6267\u884C sh apply.sh /opt/hai/apps/\u9879\u76EE\u73AF\u5883\u3002\n\u670D\u52A1\u5668\u9700\u8981 Docker/Podman\u3001Compose\u3001sh\u3001tar\u3001sha256sum\u3001awk \u548C curl\uFF0C\u4EE5\u53CA\u76EE\u5F55\u3001\u5BB9\u5668\u8FD0\u884C\u6743\u9650\u3002\n\u90E8\u5206\u66F4\u65B0\u8981\u6C42\u670D\u52A1\u5668\u5DF2\u6709\u6210\u529F\u7684\u5B8C\u6574\u90E8\u7F72\uFF0C\u7AEF\u53E3\u53CA\u5E94\u7528\u5E03\u5C40\u4FDD\u6301\u4E00\u81F4\u3002\u6BCF\u6B65\u5931\u8D25\u4F1A\u8FD4\u56DE\u975E\u96F6\u9000\u51FA\u7801\uFF1B\u7ED3\u679C\u8BB0\u5F55\u5728\u90E8\u7F72\u76EE\u5F55 result-\u90E8\u7F72ID.json\u3002\n\u5305\u5305\u542B\u79C1\u6709\u8FD0\u884C\u914D\u7F6E\uFF0C\u8BF7\u5B89\u5168\u4F20\u8F93\u3002\n\u6240\u542B\u955C\u50CF\uFF1A\n{images}",
19839
- deploy_bundleMetadata: "\u5E94\u7528\u7248\u672C\uFF1A{version}\n\u6784\u5EFA\u7F16\u53F7\uFF1A{buildId}\nCompose \u6587\u4EF6\uFF1A{compose}",
19840
- deploy_bundleOnlyContainer: "\u4EC5\u751F\u6210 Compose tar \u53EA\u652F\u6301 docker-ssh\u3002",
19841
- deploy_selectedContainerOnly: "\u6309\u670D\u52A1\u5C40\u90E8\u90E8\u7F72\u4EC5\u9002\u7528\u4E8E docker-ssh Compose \u76EE\u6807\u3002",
19842
- deploy_selectionInvalid: "\u65E0\u6CD5\u90E8\u7F72\u670D\u52A1 {service}\uFF1A\u8BF7\u68C0\u67E5\u670D\u52A1\u540D\u79F0\uFF0C\u5E76\u5148\u6784\u5EFA\u5BF9\u5E94\u955C\u50CF\u3002",
19843
- deploy_storageArtifactRequired: "\u5B58\u50A8\u521D\u59CB\u5316\u9700\u8981\u5E94\u7528\u955C\u50CF\u3002\u8BF7\u7528 --artifact-dir \u9009\u62E9\u542B app \u955C\u50CF\u7684\u5B8C\u6574\u5236\u54C1\uFF0C\u6216\u5148\u6267\u884C pnpm build --target docker --images app\u3002",
19844
- deploy_imageVersionRequired: "\u955C\u50CF\u5FC5\u987B\u6307\u5B9A\u7248\u672C\uFF0C\u4E0D\u80FD\u4F7F\u7528 latest\uFF1A{image}",
19845
- deploy_bundleRuntimeRequired: "\u5B8C\u6574\u79BB\u7EBF\u5305\u9700\u8981\u5BFC\u51FA\u4E2D\u95F4\u4EF6\u955C\u50CF\u3002\u8BF7\u542F\u52A8\u672C\u5730 Podman/Docker\uFF0C\u6216\u5728\u6784\u5EFA\u673A\u6253\u5305\u3002",
19846
- deploy_bundleFailed: "\u4EA4\u4ED8\u51C6\u5907\u6216\u90E8\u7F72\u5931\u8D25\uFF1A{reason}",
19847
- deploy_configFileInvalid: "\u914D\u7F6E\u6587\u4EF6 {path} \u7B2C {line} \u884C\u683C\u5F0F\u6709\u8BEF\u3002\u8BF7\u68C0\u67E5\u7F29\u8FDB\u3001\u5192\u53F7\u548C\u5F15\u53F7\u540E\u91CD\u8BD5\u3002",
19848
- deploy_configFileUnreadable: "\u65E0\u6CD5\u8BFB\u53D6\u914D\u7F6E\u6587\u4EF6 {path}\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u8DEF\u5F84\u4E0E\u8BFB\u53D6\u6743\u9650\u3002",
19849
- deploy_bundleNameInvalid: "\u90E8\u7F72\u5305\u6587\u4EF6\u540D\u53EA\u652F\u6301\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u70B9\u3001\u52A0\u53F7\u3001\u77ED\u6A2A\u7EBF\u548C\u4E0B\u5212\u7EBF\uFF1B\u6240\u5728\u76EE\u5F55\u53EF\u4EE5\u5305\u542B\u7A7A\u683C\u3002",
19850
- deploy_secretsInvalid: "\u4E2D\u95F4\u4EF6\u51ED\u636E\u6587\u4EF6\u635F\u574F\u6216\u7F3A\u5C11\u5B57\u6BB5\uFF1A{path}\u3002\u8BF7\u4ECE\u5907\u4EFD\u6062\u590D\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u66F4\u6362\u5BC6\u7801\uFF0C\u4EE5\u514D\u5DF2\u6709\u6570\u636E\u65E0\u6CD5\u8BBF\u95EE\u3002"
19851
- };
19852
-
19853
- // ../deploy/src/deploy-i18n.ts
19854
- var deployM = core.i18n.createMessageGetter({
19855
- "zh-CN": zh_CN_default2,
19856
- "en-US": en_US_default
19857
- });
19858
-
19859
- // ../deploy/src/deploy-config.ts
19860
- var VercelProviderConfigSchema = external_exports.object({
19861
- type: external_exports.literal("vercel"),
19862
- token: external_exports.string().min(1, deployM("deploy_configTokenRequired"))
19863
- });
19864
- var SshConfigSchema = external_exports.object({
19865
- /** 远程主机地址或 ~/.ssh/config 中的 Host 别名 */
19866
- host: external_exports.string().regex(/^[a-z0-9][a-z0-9.:-]*$/i),
19867
- /** SSH 端口 */
19868
- port: external_exports.number().int().min(1).max(65535).default(22),
19869
- /** SSH 用户名 */
19870
- username: external_exports.string().regex(/^[a-z_][\w-]*$/i),
19871
- /** 私钥文件路径(省略时使用 ssh-agent 或 ~/.ssh 默认密钥) */
19872
- identityFile: external_exports.string().min(1).optional(),
19873
- /** known_hosts 文件路径(省略时使用默认;配合 CI/临时环境隔离主机指纹) */
19874
- knownHostsFile: external_exports.string().min(1).optional(),
19875
- /** 严格主机密钥校验,默认开启(true→accept-new 首次信任并拒绝变更;false→关闭校验) */
19876
- strictHostKeyChecking: external_exports.boolean().default(true),
19877
- /** 连接超时(秒) */
19878
- connectTimeoutSeconds: external_exports.number().int().positive().default(10)
19879
- });
19880
- var PortExposeSchema = external_exports.object({
19881
- type: external_exports.literal("port").default("port"),
19882
- /** 容器内监听端口(adapter-node 默认 3000) */
19883
- containerPort: external_exports.number().int().min(1).max(65535).default(3e3),
19884
- /** 宿主机映射端口 */
19885
- hostPort: external_exports.number().int().min(1).max(65535)
19886
- });
19887
- var ExposeSchema = PortExposeSchema;
19888
- var DockerSshProviderConfigSchema = external_exports.object({
19889
- type: external_exports.literal("docker-ssh"),
19890
- /** SSH 连接参数 */
19891
- ssh: SshConfigSchema.optional(),
19892
- /** 远程主机运行参数 */
19893
- remote: external_exports.object({
19894
- /** 远程应用根目录 */
19895
- baseDir: external_exports.string().regex(/^\/[\w/. -]+$/).refine((value) => !value.split("/").includes("..")).default("/opt/hai/apps"),
19896
- /** 远程容器运行时(auto 优先 podman) */
19897
- runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto")
19898
- }).default({ baseDir: "/opt/hai/apps", runtime: "auto" }),
19899
- /** 应用暴露方式 */
19900
- expose: ExposeSchema
19901
- });
19902
- var ProviderConfigSchema = external_exports.discriminatedUnion("type", [
19903
- VercelProviderConfigSchema,
19904
- DockerSshProviderConfigSchema
19905
- ]);
19906
- var NeonServiceSchema = external_exports.object({
19907
- provisioner: external_exports.literal("neon"),
19908
- apiKey: external_exports.string().min(1)
19909
- });
19910
- var UpstashServiceSchema = external_exports.object({
19911
- provisioner: external_exports.literal("upstash"),
19912
- email: external_exports.string().email(),
19913
- apiKey: external_exports.string().min(1)
19914
- });
19915
- var R2ServiceSchema = external_exports.object({
19916
- provisioner: external_exports.literal("cloudflare-r2"),
19917
- accessKeyId: external_exports.string().min(1),
19918
- secretAccessKey: external_exports.string().min(1),
19919
- accountId: external_exports.string().min(1),
19920
- apiToken: external_exports.string().min(1)
19921
- });
19922
- var ResendServiceSchema = external_exports.object({
19923
- provisioner: external_exports.literal("resend"),
19924
- from: external_exports.string().email(),
19925
- apiKey: external_exports.string().min(1)
19926
- });
19927
- var AliyunSmsServiceSchema = external_exports.object({
19928
- provisioner: external_exports.literal("aliyun"),
19929
- signName: external_exports.string().min(1),
19930
- accessKeyId: external_exports.string().min(1),
19931
- accessKeySecret: external_exports.string().min(1)
19932
- });
19933
- var ServicesConfigSchema = external_exports.object({
19934
- db: external_exports.discriminatedUnion("provisioner", [NeonServiceSchema]).optional(),
19935
- cache: external_exports.discriminatedUnion("provisioner", [UpstashServiceSchema]).optional(),
19936
- storage: external_exports.discriminatedUnion("provisioner", [R2ServiceSchema]).optional(),
19937
- email: external_exports.discriminatedUnion("provisioner", [ResendServiceSchema]).optional(),
19938
- sms: external_exports.discriminatedUnion("provisioner", [AliyunSmsServiceSchema]).optional()
19939
- }).optional();
19940
- var DeployConfigSchema = external_exports.object({
19941
- /** 部署环境文件模式;dev/test/prod 或自定义模式,禁止 local/example。 */
19942
- environment: external_exports.string().regex(/^[a-z0-9][\w-]*$/i).refine((value) => !["local", "example"].includes(value.toLowerCase())).optional(),
19943
- provider: ProviderConfigSchema,
19944
- services: ServicesConfigSchema,
19945
- /** Docker Compose 部署参数;构建配置位于项目 hai.build.json。 */
19946
- container: external_exports.object({
19947
- /** 自建 S3 服务实现;协议仍为 storage.type=s3。 */
19948
- s3Engine: external_exports.enum(["rustfs", "minio"]).default("rustfs")
19949
- }).optional()
19950
- });
19951
- var DeployErrorInfo = {
19952
- DEPLOY_FAILED: "001:500",
19953
- PROJECT_CREATE_FAILED: "002:500",
19954
- UPLOAD_FAILED: "004:500",
19955
- AUTH_REQUIRED: "005:401",
19956
- AUTH_FAILED: "006:401",
19957
- PROVISION_FAILED: "007:500",
19958
- ADAPTER_MISSING: "008:500",
19959
- SCAN_FAILED: "009:500",
19960
- NOT_INITIALIZED: "010:500",
19961
- ENV_VAR_FAILED: "011:500",
19962
- UNSUPPORTED_TYPE: "012:400",
19963
- CONFIG_ERROR: "013:500",
19964
- CREDENTIAL_ERROR: "014:500",
19965
- REMOTE_CONNECT_FAILED: "017:500",
19966
- REMOTE_COMMAND_FAILED: "018:500",
19967
- IMAGE_TRANSFER_FAILED: "019:500",
19968
- HEALTH_CHECK_FAILED: "020:500"
19969
- };
19970
- var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
19971
-
19972
- // ../deploy/src/deploy-environment.ts
19973
- function environmentValue(value) {
19974
- try {
19975
- return parse$1(value) ?? value;
19976
- } catch {
19977
- return value;
19978
- }
19979
- }
19980
- function resolveConfig(value, prefix, env) {
19981
- if (env[prefix] !== void 0)
19982
- return environmentValue(env[prefix]);
19983
- if (Array.isArray(value))
19984
- return value.map((item, index) => resolveConfig(item, `${prefix}_${index}`, env));
19985
- if (value && typeof value === "object")
19986
- return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, resolveConfig(nested, `${prefix}_${key.toUpperCase()}`, env)]));
19987
- if (typeof value !== "string")
19988
- return value;
19989
- const resolved = value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, name, fallback) => env[name] ?? fallback ?? "");
19990
- return /^\$\{[^}]+\}$/.test(value) ? environmentValue(resolved) : resolved;
19991
- }
19992
- function loadDeploymentConfig(name, path, values) {
19993
- try {
19994
- const lineCounter = new LineCounter();
19995
- const document = parseDocument(readFileSync(path, "utf8"), { lineCounter, prettyErrors: false });
19996
- if (document.errors.length)
19997
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: String(lineCounter.linePos(document.errors[0].pos[0]).line) } }));
19998
- const config2 = resolveConfig(document.toJS(), `HAI_${name.toUpperCase()}`, values);
19999
- if (config2 !== null && (typeof config2 !== "object" || Array.isArray(config2)))
20000
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
20001
- return ok(config2 && typeof config2 === "object" && !Array.isArray(config2) ? config2 : {});
20002
- } catch {
20003
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", { params: { path } }));
20004
- }
20005
- }
20006
- function loadDeploymentEnvironment(options) {
20007
- const environment = options.environment === "production" ? "prod" : options.environment === "development" ? "dev" : options.environment;
20008
- if (!/^[a-z0-9][\w-]*$/i.test(environment) || ["local", "example"].includes(environment.toLowerCase()))
20009
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_invalidEnvironment"));
20010
- const envDir = resolve(options.envDir);
20011
- const target = join(envDir, `.env.${environment}`);
20012
- if (options.requireFile !== false && !existsSync(target))
20013
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentMissing", { params: { path: target } }));
20014
- const loaded = core.config.loadEnvFiles({ mode: environment, envDir });
20015
- if (!loaded.success)
20016
- return loaded;
20017
- try {
20018
- const values = { ...loaded.data };
20019
- for (const [name, value] of Object.entries(process2.env)) {
20020
- if (/^(?:HAI_|PUBLIC_|VITE_)/.test(name) && value !== void 0)
20021
- values[name] = value;
20022
- }
20023
- values.HAI_DEPLOY_ENVIRONMENT = environment;
20024
- values.HAI_CORE_ENV ??= environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
20025
- const targetValues = {};
20026
- if (existsSync(target)) {
20027
- for (const [name, value] of Object.entries(parseEnv(readFileSync(target, "utf8")))) {
20028
- if (value !== void 0)
20029
- targetValues[name] = value;
20030
- }
20031
- }
20032
- return ok({ environment, envDir, values, targetValues });
20033
- } catch {
20034
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
20035
- }
20036
- }
20037
- function isSecret(name, value) {
20038
- if (/PASSWORD|SECRET|TOKEN|APIKEY|PRIVATEKEY|ACCESSKEY|AUTHORIZATION/i.test(name.replace(/[^a-z0-9]/gi, "")) || /(?:^|_)PASS$/i.test(name) || value.includes("PRIVATE KEY"))
20039
- return true;
20040
- try {
20041
- const url2 = new URL(value);
20042
- if (url2.username || url2.password || [...url2.searchParams.keys()].some((key) => /password|secret|token|key|authorization/i.test(key)))
20043
- return true;
20044
- } catch {
20045
- }
20046
- try {
20047
- const parsed = JSON.parse(value);
20048
- if (parsed && typeof parsed === "object")
20049
- return Object.entries(parsed).some(([key, nested]) => isSecret(key, typeof nested === "string" ? nested : JSON.stringify(nested)));
20050
- } catch {
20051
- }
20052
- return false;
20053
- }
20054
- function publicConfig(value) {
20055
- if (Array.isArray(value))
20056
- return value.map(publicConfig);
20057
- if (value && typeof value === "object")
20058
- return Object.fromEntries(Object.entries(value).filter(([name, item]) => !isSecret(name, typeof item === "string" ? item : "")).map(([name, item]) => [name, publicConfig(item)]));
20059
- return value;
20060
- }
20061
- function quoteEnv(value) {
20062
- try {
20063
- const parsed = JSON.parse(value);
20064
- if (parsed && typeof parsed === "object")
20065
- return `'${JSON.stringify(parsed).replaceAll("'", "\\u0027")}'`;
20066
- } catch {
20067
- }
20068
- if (!value.includes("'"))
20069
- return `'${value}'`;
20070
- if (!value.includes('"'))
20071
- return `"${value}"`;
20072
- if (!value.includes("`"))
20073
- return `\`${value}\``;
20074
- if (!/[\r\n#]/.test(value))
20075
- return value;
20076
- throw new Error("Unsupported dotenv value");
20077
- }
20078
- function updateFile(path, values, privateFile) {
20079
- let content = existsSync(path) ? readFileSync(path, "utf8") : "# Local private deployment configuration; do not commit.\n";
20080
- for (const [name, value] of Object.entries(values)) {
20081
- if (!/^[A-Z][A-Z0-9_]*$/.test(name))
20082
- throw new Error("Invalid environment variable name");
20083
- const line = `${name}=${quoteEnv(value)}`;
20084
- const pattern = new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"[^"]*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*`, "gm");
20085
- if (pattern.test(content))
20086
- content = content.replace(pattern, () => line);
20087
- else
20088
- content = `${content.trimEnd()}
20089
- ${line}
20090
- `;
20091
- }
20092
- writeFileSync(path, content, { mode: privateFile ? 384 : 420 });
20093
- if (privateFile)
20094
- chmodSync(path, 384);
20095
- }
20096
- function removeOverrides(path, names, privateFile = true) {
20097
- if (!existsSync(path) || names.length === 0)
20098
- return;
20099
- let content = readFileSync(path, "utf8");
20100
- for (const name of names)
20101
- content = content.replace(new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"[^"]*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
20102
- writeFileSync(path, content, { mode: privateFile ? 384 : 420 });
20103
- chmodSync(path, privateFile ? 384 : 420);
20104
- }
20105
- function saveDeploymentEnvironment(environment, values, options = {}) {
20106
- try {
20107
- const shared = {};
20108
- const privateValues = {};
20109
- for (const [name, value] of Object.entries(values)) {
20110
- if (options.private) {
20111
- privateValues[name] = value;
20112
- } else if (isSecret(name, value)) {
20113
- privateValues[name] = value;
20114
- try {
20115
- const parsed = JSON.parse(value);
20116
- if (parsed && typeof parsed === "object")
20117
- shared[name] = JSON.stringify(publicConfig(parsed));
20118
- } catch {
20119
- }
20120
- } else {
20121
- shared[name] = value;
20122
- }
20123
- }
20124
- if (Object.keys(shared).length)
20125
- updateFile(join(environment.envDir, `.env.${environment.environment}`), shared, false);
20126
- if (Object.keys(privateValues).length)
20127
- updateFile(join(environment.envDir, ".env.local"), privateValues, true);
20128
- removeOverrides(join(environment.envDir, `.env.${environment.environment}`), Object.keys(privateValues).filter((name) => !(name in shared)), false);
20129
- removeOverrides(join(environment.envDir, ".env.local"), Object.keys(shared).filter((name) => !(name in privateValues)));
20130
- if (Object.keys(privateValues).length) {
20131
- const ignorePath = join(environment.envDir, ".gitignore");
20132
- const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
20133
- if (!ignore.split(/\r?\n/).includes(".env.local"))
20134
- writeFileSync(ignorePath, `${ignore.trimEnd()}
20135
- .env.local
20136
- .hai/deploy/
20137
- `);
20138
- }
20139
- return ok(Object.keys(values));
20140
- } catch {
20141
- return err(HaiDeployError.ENV_VAR_FAILED, deployM("deploy_environmentWriteFailed"));
20142
- }
20143
- }
20144
- var logger = core.logger.child({ module: "deploy", scope: "credentials" });
20145
- var DEPLOY_CREDENTIAL_KEY_PATTERN = /^HAI_DEPLOY_[A-Z0-9_]+$/;
20146
- function isAllowedCredentialKey(key) {
20147
- return DEPLOY_CREDENTIAL_KEY_PATTERN.test(key);
20148
- }
20149
- function normalizeCredentialEntries(entries) {
20150
- const normalized = {};
20151
- for (const [key, value] of Object.entries(entries)) {
20152
- if (!isAllowedCredentialKey(key)) {
20153
- logger.warn("Ignoring unsupported deploy credential key", { key });
20154
- continue;
20155
- }
20156
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
20157
- normalized[key] = String(value);
20158
- continue;
20159
- }
20160
- logger.warn("Ignoring deploy credential with non-scalar value", { key });
20161
- }
20162
- return normalized;
20163
- }
20164
- function assertAllowedCredentialKey(key) {
20165
- if (!isAllowedCredentialKey(key)) {
20166
- throw new Error(deployM("deploy_credentialInvalidKey", { params: { key } }));
20167
- }
20168
- }
20169
- function assertAllowedCredentialEntries(entries) {
20170
- for (const key of Object.keys(entries)) {
20171
- assertAllowedCredentialKey(key);
20172
- }
20173
- }
20174
- function getCredentialsPath() {
20175
- return join(homedir(), ".hai", "credentials.yml");
20176
- }
20177
- function ensureDir(filePath) {
20178
- const dir = dirname(filePath);
20179
- if (!existsSync(dir)) {
20180
- mkdirSync(dir, { recursive: true });
20181
- }
20182
- }
20183
- function readCredentialsFile() {
20184
- const filePath = getCredentialsPath();
20185
- if (!existsSync(filePath)) {
20186
- return {};
20187
- }
20188
- const content = readFileSync(filePath, "utf-8");
20189
- const parsed = parse$1(content);
20190
- if (parsed === null || parsed === void 0 || typeof parsed !== "object" || Array.isArray(parsed)) {
20191
- return {};
20192
- }
20193
- return normalizeCredentialEntries(parsed);
20194
- }
20195
- function writeCredentialsFile(data) {
20196
- const filePath = getCredentialsPath();
20197
- ensureDir(filePath);
20198
- const header = "# Managed by hai cli - do not edit manually\n";
20199
- const content = header + stringify(data);
20200
- writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
20201
- }
20202
- function loadCredentials() {
20203
- try {
20204
- const data = readCredentialsFile();
20205
- const keys = Object.keys(data);
20206
- for (const key of keys) {
20207
- process2.env[key] = String(data[key]);
20208
- }
20209
- logger.debug("Credentials loaded", { count: keys.length });
20210
- return ok(keys);
20211
- } catch (error62) {
20212
- logger.error("Failed to load credentials", { error: error62 });
20213
- return err(
20214
- HaiDeployError.CREDENTIAL_ERROR,
20215
- deployM("deploy_credentialError", {
20216
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
20217
- }),
20218
- error62
20219
- );
20220
- }
20221
- }
20222
- function saveCredential(key, value) {
20223
- try {
20224
- assertAllowedCredentialKey(key);
20225
- const data = readCredentialsFile();
20226
- data[key] = value;
20227
- writeCredentialsFile(data);
20228
- process2.env[key] = value;
20229
- logger.debug("Credential saved", { key });
20230
- return ok(void 0);
20231
- } catch (error62) {
20232
- logger.error("Failed to save credential", { key, error: error62 });
20233
- return err(
20234
- HaiDeployError.CREDENTIAL_ERROR,
20235
- deployM("deploy_credentialError", {
20236
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
20237
- }),
20238
- error62
20239
- );
20240
- }
20241
- }
20242
- function saveCredentials(entries) {
20243
- try {
20244
- assertAllowedCredentialEntries(entries);
20245
- const data = readCredentialsFile();
20246
- for (const [key, value] of Object.entries(entries)) {
20247
- data[key] = value;
20248
- process2.env[key] = value;
20249
- }
20250
- writeCredentialsFile(data);
20251
- logger.debug("Credentials saved", { count: Object.keys(entries).length });
20252
- return ok(void 0);
20253
- } catch (error62) {
20254
- logger.error("Failed to save credentials", { error: error62 });
20255
- return err(
20256
- HaiDeployError.CREDENTIAL_ERROR,
20257
- deployM("deploy_credentialError", {
20258
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
20259
- }),
20260
- error62
20261
- );
20262
- }
20263
- }
20264
- function resolveArtifactDirectory(appDir) {
20265
- let directory = resolve(appDir);
20266
- while (true) {
20267
- if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
20268
- return join(directory, ".hai/build/docker");
20269
- const parent = dirname(directory);
20270
- if (parent === directory)
20271
- return join(appDir, ".hai/build/docker");
20272
- directory = parent;
20273
- }
20274
- }
20275
- function usesDefaultMiddleware(scan, service, options) {
20276
- const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
20277
- return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
20278
- }
20279
- function resolveOutputDir(appDir, providerType) {
20280
- switch (providerType) {
20281
- case "vercel":
20282
- return join(appDir, ".vercel", "output");
20283
- default:
20284
- return join(appDir, "build");
20285
- }
20286
- }
20287
- var RecoverySchema = external_exports.object({
20288
- projectName: external_exports.string(),
20289
- stage: external_exports.string(),
20290
- platformProjectId: external_exports.string().optional(),
20291
- resources: external_exports.array(external_exports.object({
20292
- serviceType: external_exports.enum(["db", "cache", "storage", "email", "sms"]),
20293
- provisionerName: external_exports.string(),
20294
- resourceInfo: external_exports.string(),
20295
- resourceStatus: external_exports.enum(["created", "reused", "unknown"])
20296
- }))
20297
- });
20298
- function getDeployRecovery(error62) {
20299
- const parsed = RecoverySchema.safeParse(error62.ext?.recovery);
20300
- return parsed.success ? parsed.data : void 0;
20301
- }
20302
- function recoveryFailure(error62, recovery) {
20303
- return err({
20304
- code: error62.code,
20305
- httpStatus: error62.httpStatus,
20306
- system: error62.system,
20307
- module: error62.module,
20308
- message: deployM("deploy_recoveryFailure", { params: { stage: recovery.stage } }),
20309
- suggestion: deployM("deploy_recoverySuggestion"),
20310
- ext: { recovery }
20311
- });
20312
- }
20313
- var logger2 = core.logger.child({ module: "deploy", scope: "scanner" });
20314
- function record2(value) {
20315
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20316
- }
20317
- function runtimeDependencies(appDir) {
20318
- let root = appDir;
20319
- while (!existsSync(join(root, "pnpm-lock.yaml"))) {
20320
- const parent = dirname(root);
20321
- if (parent === root)
20322
- throw new Error("Missing pnpm lockfile; run pnpm install");
20323
- root = parent;
20324
- }
20325
- const importerKey = relative(root, appDir).replaceAll("\\", "/") || ".";
20326
- const documents = parseAllDocuments(readFileSync(join(root, "pnpm-lock.yaml"), "utf8"));
20327
- if (documents.some((document) => document.errors.length))
20328
- throw new Error("Invalid pnpm lockfile");
20329
- const lock = documents.map((document) => record2(document.toJS())).reverse().find((document) => importerKey in record2(document.importers));
20330
- if (!lock)
20331
- throw new Error("Application is missing from pnpm lockfile; run pnpm install");
20332
- const importers = record2(lock.importers);
20333
- const packages = record2(lock.packages);
20334
- const snapshots = record2(lock.snapshots);
20335
- const names = /* @__PURE__ */ new Set();
20336
- const visited = /* @__PURE__ */ new Set();
20337
- const visit2 = (id, node2, importer) => {
20338
- if (visited.has(id))
20339
- return;
20340
- visited.add(id);
20341
- const metadata = importer === void 0 ? record2(packages[id.split("(")[0]]) : {};
20342
- const peers = record2(metadata.peerDependencies);
20343
- for (const [name, reference] of Object.entries({ ...record2(node2.dependencies), ...record2(node2.optionalDependencies) })) {
20344
- if (name in peers)
20345
- continue;
20346
- const version2 = typeof reference === "string" ? reference : record2(reference).version;
20347
- if (typeof version2 !== "string")
20348
- throw new Error("Invalid pnpm dependency reference");
20349
- if (version2.startsWith("link:")) {
20350
- if (importer === void 0)
20351
- throw new Error("Invalid workspace dependency");
20352
- const target = relative(root, resolve(root, importer, version2.slice(5))).replaceAll("\\", "/") || ".";
20353
- const linked = record2(importers[target]);
20354
- if (!Object.keys(linked).length)
20355
- throw new Error("Missing linked workspace importer");
20356
- names.add(name);
20357
- visit2(`workspace:${target}`, linked, target);
20358
- continue;
20359
- }
20360
- const key = version2 in snapshots || version2 in packages ? version2 : `${name}@${version2}`;
20361
- names.add(key.match(/^(@[^/]+\/[^@]+|[^@]+)@/)?.[1] ?? name);
20362
- const snapshot = record2(snapshots[key] ?? packages[key]);
20363
- if (!Object.keys(snapshot).length && !(key in snapshots) && !(key in packages))
20364
- throw new Error("Missing pnpm dependency snapshot");
20365
- visit2(key, snapshot);
20366
- }
20367
- };
20368
- visit2(`workspace:${importerKey}`, record2(importers[importerKey]), importerKey);
20369
- return { runtime: names, build: /* @__PURE__ */ new Set([...names, ...Object.keys(record2(record2(importers[importerKey]).devDependencies))]), root };
20370
- }
20371
- async function scanApp(inputDir, options) {
20372
- try {
20373
- const appDir = resolve(inputDir);
20374
- const graph = runtimeDependencies(appDir);
20375
- const envDir = options?.envDir ? resolve(options.envDir) : existsSync(join(appDir, ".env")) ? appDir : graph.root;
20376
- const loaded = loadDeploymentEnvironment({ environment: options?.environment ?? "prod", envDir, requireFile: options?.environment !== void 0 });
20377
- if (!loaded.success)
20378
- return loaded;
20379
- const env = loaded.data.values;
20380
- const deps = graph.runtime;
20381
- const configs = {};
20382
- for (const [name, owner] of Object.entries({ db: "reldb", cache: "cache", storage: "storage", vecdb: "vecdb", iam: "iam", reach: "reach" })) {
20383
- configs[name] = {};
20384
- if (!deps.has(`@h-ai/${owner}`))
20385
- continue;
20386
- const configFile = ["yml", "yaml"].map((ext) => join(appDir, "config", `_${name}.${ext}`)).find(existsSync);
20387
- const config2 = configFile ? loadDeploymentConfig(name, configFile, env) : void 0;
20388
- if (config2 && !config2.success)
20389
- return config2;
20390
- configs[name] = config2?.data ?? {};
20391
- }
20392
- const middlewares = [];
20393
- const add = (service, backend, configName = service, configPath = [], config2 = configs[configName] ?? {}, source = `_${configName}.yml`) => {
20394
- middlewares.push({ service, backend, source, configName, configPath, envPrefix: ["HAI", configName, ...configPath].map((part) => part.toUpperCase()).join("_"), config: config2 });
20395
- };
20396
- const iam = deps.has("@h-ai/iam");
20397
- if (deps.has("@h-ai/reldb"))
20398
- add("db", String(configs.db.type ?? "sqlite"), "db", [], configs.db, "@h-ai/reldb / _db.yml");
20399
- if (deps.has("@h-ai/cache"))
20400
- add("cache", String(configs.cache.type ?? "memory"), "cache", [], configs.cache, "@h-ai/cache / _cache.yml");
20401
- if (deps.has("@h-ai/storage"))
20402
- add("storage", String(configs.storage.type ?? "local"));
20403
- if (deps.has("@h-ai/vecdb"))
20404
- add("vecdb", String(configs.vecdb.type ?? "lancedb"));
20405
- if (iam) {
20406
- const ldap = record2(configs.iam.ldap);
20407
- add("ldap", record2(configs.iam.login).ldap === true ? "ldap" : "disabled", "iam", ["ldap"], ldap, "@h-ai/iam (LDAP)");
20408
- }
20409
- const providers = configs.reach.providers;
20410
- if (deps.has("@h-ai/reach") && Array.isArray(providers)) {
20411
- for (const [index, item] of providers.entries()) {
20412
- const provider = record2(item);
20413
- const type = String(provider.type ?? "");
20414
- const service = type === "aliyun-sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
20415
- if (service)
20416
- add(service, type, "reach", ["providers", String(index)], provider, "@h-ai/reach");
20417
- }
20418
- }
20419
- if (deps.has("@h-ai/reach") && !middlewares.some((item) => item.configName === "reach"))
20420
- add("email", "console", "reach", ["providers", "0"], { name: "email", type: "console" }, "@h-ai/reach");
20421
- const isSvelteKit = graph.build.has("@sveltejs/kit");
20422
- const scan = {
20423
- appDir,
20424
- envDir,
20425
- middlewares,
20426
- haiModules: [...deps].filter((name) => name.startsWith("@h-ai/")).sort(),
20427
- appName: basename(appDir),
20428
- isSvelteKit,
20429
- adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
20430
- nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
20431
- requiredServices: [...new Set(middlewares.map((item) => item.service))],
20432
- serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
20433
- };
20434
- logger2.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
20435
- return ok(scan);
20436
- } catch {
20437
- return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", { params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" } }));
20438
- }
20439
- }
20440
- var logger3 = core.logger.child({ module: "deploy", scope: "command" });
20441
- function maskSecrets(text, secrets) {
20442
- if (!secrets || secrets.length === 0)
20443
- return text;
20444
- let masked = text;
20445
- for (const secret of secrets) {
20446
- if (secret && secret.length > 0)
20447
- masked = masked.split(secret).join("***");
20448
- }
20449
- return masked;
20450
- }
20451
- function runCommand(command, args, options = {}) {
20452
- return new Promise((resolve6) => {
20453
- const child = spawn(command, args, {
20454
- windowsHide: true,
20455
- cwd: options.cwd,
20456
- env: options.env ? { ...process2.env, ...options.env } : process2.env,
20457
- shell: false
20458
- });
20459
- let stdout = "";
20460
- let stderr = "";
20461
- let settled = false;
20462
- let timer;
20463
- const finish = (result) => {
20464
- if (settled)
20465
- return;
20466
- settled = true;
20467
- if (timer)
20468
- clearTimeout(timer);
20469
- resolve6(result);
20470
- };
20471
- if (options.timeoutMs && options.timeoutMs > 0) {
20472
- timer = setTimeout(() => {
20473
- child.kill("SIGKILL");
20474
- logger3.error("Command timed out", { command, timeoutMs: options.timeoutMs });
20475
- finish(err(
20476
- HaiDeployError.REMOTE_COMMAND_FAILED,
20477
- deployM("deploy_commandTimeout", { params: { command } })
20478
- ));
20479
- }, options.timeoutMs);
20480
- }
20481
- child.stdout.on("data", (chunk) => {
20482
- stdout += chunk.toString("utf-8");
20483
- options.onOutput?.(maskSecrets(chunk.toString("utf-8"), options.secrets));
20484
- });
20485
- child.stderr.on("data", (chunk) => {
20486
- stderr += chunk.toString("utf-8");
20487
- });
20488
- child.on("error", (error62) => {
20489
- logger3.error("Command failed to start", { command, error: maskSecrets(String(error62), options.secrets) });
20490
- finish(err(
20491
- HaiDeployError.REMOTE_COMMAND_FAILED,
20492
- deployM("deploy_commandFailed", {
20493
- params: { command, error: maskSecrets(error62.message, options.secrets) }
20494
- }),
20495
- error62
20496
- ));
20497
- });
20498
- child.on("close", (code) => {
20499
- const exitCode = code ?? -1;
20500
- if (exitCode === 0) {
20501
- finish(ok({ code: exitCode, stdout, stderr }));
20502
- return;
20503
- }
20504
- logger3.error("Command exited with non-zero code", {
20505
- command,
20506
- code: exitCode,
20507
- stderr: maskSecrets(stderr, options.secrets).slice(-500),
20508
- stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
20509
- });
20510
- finish(err(
20511
- HaiDeployError.REMOTE_COMMAND_FAILED,
20512
- deployM("deploy_commandFailed", {
20513
- params: { command, error: maskSecrets(`${stdout.slice(-2500)}
20514
- ${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
20515
- })
20516
- ));
20517
- });
20518
- if (options.input !== void 0) {
20519
- child.stdin.write(options.input);
20520
- child.stdin.end();
20521
- } else {
20522
- child.stdin.end();
20523
- }
20524
- });
20525
- }
20526
-
20527
- // ../deploy/src/container/deploy-scripts.ts
20528
- function shellArgument(value) {
20529
- return `'${value.replaceAll("'", `'"'"'`)}'`;
20530
- }
20531
- function createApplyScript(options) {
20532
- const values = {
20533
- ID: shellArgument(options.deploymentId),
20534
- PROJECT: shellArgument(`hai-${options.projectName}`),
20535
- ENV: shellArgument(options.environment),
20536
- RUNTIME: shellArgument(options.runtime),
20537
- PARTIAL: options.partial ? "true" : "false",
20538
- SELECTED: shellArgument(options.selected.join(" ")),
20539
- ROOT: shellArgument(options.directory),
20540
- PORT: String(options.hostPort),
20541
- HEALTH: options.healthPaths.map(shellArgument).join(" ")
20542
- };
20543
- return String.raw`#!/bin/sh
20544
- set -eu
20545
- umask 077
20546
- source_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
20547
- destination=@@ROOT@@
20548
- if [ "$#" -gt 0 ]; then destination=$1; fi
20549
- root=$(mkdir -p -- "$destination" && CDPATH= cd -- "$destination" && pwd)
20550
- id=@@ID@@
20551
- project=@@PROJECT@@
20552
- export HAI_DEPLOY_ENVIRONMENT=@@ENV@@
20553
- runtime=@@RUNTIME@@
20554
- partial=@@PARTIAL@@
20555
- orphan_option=''
20556
- if [ "$partial" = false ]; then orphan_option=--remove-orphans; fi
20557
- recreate_option=--force-recreate
20558
- if [ "$partial" = true ]; then recreate_option=--no-recreate; fi
20559
- selected=@@SELECTED@@
20560
- release="$root/releases/$id"
20561
- case "$release/" in "$source_dir/"*) printf 'Extract the package outside the deployment directory before applying it.\n' >&2; exit 1;; esac
20562
- stage=''
20563
- previous=''
20564
- restore=''
20565
- changed=false
20566
- locked=false
20567
- started=$(date +%s)
20568
- step_started=$started
20569
- step() {
20570
- if [ -n "$stage" ]; then
20571
- duration=$(( $(date +%s) - step_started ))
20572
- printf '[PASS] %s (%ss)\n' "$stage" "$duration"
20573
- printf '{"stage":"%s","status":"passed","durationSeconds":%s}\n' "$stage" "$duration" >> "$root/steps-$id.jsonl"
20574
- fi
20575
- stage="$1"
20576
- step_started=$(date +%s)
20577
- printf '\n[%s] %s\n' "$stage" "$2"
20578
- }
20579
- compose() {
20580
- arguments_file="$release/compose-files"
20581
- if [ -n "$restore" ]; then arguments_file="$root/releases/$restore/compose-files"; fi
20582
- # -f 按顺序合并,由 Compose 保留未选服务、卷和环境文件,不自行拼装配置。
20583
- command_args="$release/.compose-command"
20584
- # 文件列表先反向读取,再向参数头部加入,保留原始合并顺序。
20585
- awk '{lines[NR]=$0} END {for(i=NR;i>0;i--) print lines[i]}' "$arguments_file" > "$command_args"
20586
- while IFS= read -r file; do set -- -f "$file" "$@"; done < "$command_args"
20587
- "$runtime" compose -p "$project" "$@"
20588
- }
20589
- remove_selected() {
20590
- test "$partial" = true || return 0
20591
- # 直接替换选中容器,避免 Compose Provider 连带重建依赖它的未选服务;不删除卷。
20592
- for service in "$@"; do
20593
- containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service") || return 1
20594
- for container in $containers; do
20595
- "$runtime" rm -f "$container" || return 1
20596
- done
20597
- done
20598
- }
20599
- finish() {
20600
- code=$?
20601
- trap - EXIT HUP INT TERM
20602
- if [ "$code" -ne 0 ]; then
20603
- printf '[FAIL] %s (exit %s)\n' "$stage" "$code" >&2
20604
- if [ "$changed" = true ]; then
20605
- diagnostics="$root/diagnostics-$id.log"
20606
- : > "$diagnostics"
20607
- for service in $selected; do
20608
- containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service" || true)
20609
- for container in $containers; do
20610
- "$runtime" inspect --format '{{.Name}} {{.State.Status}} exit={{.State.ExitCode}}' "$container" >> "$diagnostics" 2>&1 || true
20611
- "$runtime" logs --tail 80 "$container" >> "$diagnostics" 2>&1 || true
20612
- done
20613
- done
20614
- printf 'Startup diagnostics saved to %s (private server file).\n' "$diagnostics" >&2
20615
- fi
20616
- if [ -f "$release/rollback-images" ]; then
20617
- while IFS=' ' read -r image reference; do
20618
- test -n "$reference" || continue
20619
- "$runtime" tag "$image" "$reference" || printf '[FAIL] Cannot restore image %s\n' "$reference" >&2
20620
- done < "$release/rollback-images"
20621
- fi
20622
- if [ "$changed" = true ] && [ -n "$previous" ]; then
20623
- printf '[RECOVER] Restoring previous selected services\n' >&2
20624
- recover_selected=$selected
20625
- if [ "$partial" = false ]; then recover_selected=$(grep -v -- '-init$' "$root/releases/$previous/services"); fi
20626
- restore="$previous"
20627
- if remove_selected $recover_selected && compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $recover_selected; then
20628
- case " $selected " in *' app '*)
20629
- if grep -qx web "$root/releases/$previous/services"; then compose exec -T web nginx -s reload || true; fi
20630
- ;;
20631
- esac
20632
- printf '[RECOVER] Previous containers restored; verify application health\n' >&2
20633
- else
20634
- printf '[FAIL] Recovery failed. Use the previous release and inspect container logs.\n' >&2
20635
- fi
20636
- elif [ "$changed" = true ]; then
20637
- compose stop $selected || true
20638
- printf '[RECOVER] Failed first installation stopped; data volumes retained\n' >&2
20639
- fi
20640
- fi
20641
- if [ "$locked" = true ]; then
20642
- elapsed=$(( $(date +%s) - started ))
20643
- outcome=passed
20644
- if [ "$code" -ne 0 ]; then outcome=failed; fi
20645
- printf '{"stage":"%s","status":"%s","durationSeconds":%s}\n' "$stage" "$outcome" "$(( $(date +%s) - step_started ))" >> "$root/steps-$id.jsonl"
20646
- printf '{"deploymentId":"%s","stage":"%s","exitCode":%s,"durationSeconds":%s}\n' "$id" "$stage" "$code" "$elapsed" > "$root/result-$id.json"
20647
- rmdir "$root/.deploy-lock" || true
20648
- fi
20649
- exit "$code"
20650
- }
20651
- trap finish EXIT
20652
- trap 'exit 130' HUP INT TERM
20653
- step preflight 'Checking deployment tools and package'
20654
- for tool in sha256sum tar awk curl; do command -v "$tool" >/dev/null || { printf 'Required command missing: %s\n' "$tool" >&2; exit 1; }; done
20655
- if [ "$runtime" = auto ]; then
20656
- if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
20657
- fi
20658
- case "$runtime" in docker|podman) ;; *) printf 'Runtime must be docker or podman\n' >&2; exit 1;; esac
20659
- "$runtime" version >/dev/null
20660
- "$runtime" compose version >/dev/null
20661
- "$runtime" compose up --help | grep -q never || { printf 'Compose is too old. Install Docker Compose v2 or podman-compose 1.6.0 or newer (supports --pull never).\n' >&2; exit 1; }
20662
- mkdir "$root/.deploy-lock" 2>/dev/null || { printf 'Another deployment is running. Check %s/.deploy-lock before retrying.\n' "$root" >&2; exit 1; }
20663
- locked=true
20664
- cd "$source_dir"
20665
- sha256sum -c SHA256SUMS
20666
- if [ -f "$root/current" ]; then
20667
- previous=$(cat "$root/current")
20668
- case "$previous" in ''|*[!a-zA-Z0-9._-]*) printf 'Invalid deployment state\n' >&2; exit 1;; esac
20669
- test -f "$root/releases/$previous/compose-files"
20670
- fi
20671
- if [ "$partial" = true ]; then
20672
- test -n "$previous" || { printf 'Partial deployment requires a successful full installation. Deploy a complete bundle first.\n' >&2; exit 1; }
20673
- cmp -s layout "$root/releases/$previous/layout" || { printf 'Ports, API routes or application layout changed. Create a full deployment bundle.\n' >&2; exit 1; }
20674
- fi
20675
- test ! -e "$release" || { printf 'This deployment ID was already used. Create a new deployment bundle.\n' >&2; exit 1; }
20676
- mkdir -p "$release"
20677
- cp -R "$source_dir/." "$release/"
20678
- # Nginx 以非 root 用户运行;只开放不含凭据的代理配置读取权限。
20679
- if [ -f "$release/web.conf" ]; then chmod 644 "$release/web.conf"; fi
20680
- : > "$release/compose-files"
20681
- if [ "$partial" = true ]; then
20682
- cp "$root/releases/$previous/compose-files" "$release/compose-files"
20683
- cp "$root/releases/$previous/middleware.env" "$release/middleware.env"
20684
- for service in $selected; do
20685
- grep -qx "$service" "$root/releases/$previous/services" || { printf 'Service %s has no installed baseline. Use a full deployment.\n' "$service" >&2; exit 1; }
20686
- done
20687
- fi
20688
- printf '%s\n' "$release/compose-entry.json" >> "$release/compose-files"
20689
- compose config >/dev/null
20690
- compose config --services > "$release/services"
20691
- : > "$release/rollback-images"
20692
- if [ -n "$previous" ]; then
20693
- for service in $selected; do
20694
- containers=$("$runtime" ps -aq --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$service")
20695
- for container in $containers; do
20696
- "$runtime" inspect --format '{{.Image}} {{.Config.Image}}' "$container" >> "$release/rollback-images"
20697
- done
20698
- done
20699
- fi
20700
- step images 'Loading packaged images (no registry access)'
20701
- while IFS=' ' read -r archive reference; do
20702
- test -n "$archive" || continue
20703
- "$runtime" load -i "$release/images/$archive"
20704
- "$runtime" image inspect "$reference" >/dev/null
20705
- architecture=$("$runtime" image inspect --format '{{.Architecture}}' "$reference")
20706
- host_arch=$(uname -m)
20707
- case "$host_arch:$architecture" in x86_64:amd64|aarch64:arm64) ;; *) printf 'Image architecture %s does not match server %s\n' "$architecture" "$host_arch" >&2; exit 1;; esac
20708
- done < "$release/image-list"
20709
- step dependencies 'Checking dependencies of selected services'
20710
- if [ "$partial" = true ]; then cat "$release/services" >> "$release/dependencies"; fi
20711
- while IFS= read -r dependency; do
20712
- test -n "$dependency" || continue
20713
- case "$dependency" in *-init) continue;; esac
20714
- case " $selected " in *" $dependency "*) continue;; esac
20715
- running=$("$runtime" ps -q --filter "label=com.docker.compose.project=$project" --filter "label=com.docker.compose.service=$dependency")
20716
- test -n "$running" || { printf 'Dependency %s is not running. Include it in a full deployment.\n' "$dependency" >&2; exit 1; }
20717
- for container in $running; do
20718
- test "$("$runtime" inspect --format '{{.State.Running}}' "$container")" = true || { printf 'Dependency %s is stopped. Start it before retrying.\n' "$dependency" >&2; exit 1; }
20719
- done
20720
- done < "$release/dependencies"
20721
- step start 'Updating selected services'
20722
- changed=true
20723
- remove_selected $selected
20724
- if [ -f "$release/initialize-storage" ]; then
20725
- remove_selected storage-init
20726
- compose up -d --no-deps --no-build --pull never storage
20727
- compose up --no-deps --no-build --pull never "$recreate_option" --exit-code-from storage-init storage-init
20728
- fi
20729
- compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $selected
20730
- case " $selected " in
20731
- *' app '*)
20732
- case " $selected " in *' web '*) ;; *)
20733
- if grep -qx web "$release/services"; then compose exec -T web nginx -s reload; fi
20734
- esac
20735
- ;;
20736
- esac
20737
- step health 'Checking HTTP access'
20738
- for path in @@HEALTH@@; do
20739
- ready=false
20740
- attempt=0
20741
- while [ "$attempt" -lt 40 ]; do
20742
- if curl --noproxy '*' -fsS --max-time 5 "http://127.0.0.1:@@PORT@@$path" >/dev/null 2>&1; then ready=true; break; fi
20743
- attempt=$((attempt + 1))
20744
- sleep 3
20745
- done
20746
- test "$ready" = true || { printf 'HTTP check failed: %s. Inspect compose logs for selected services.\n' "$path" >&2; exit 1; }
20747
- done
20748
- step complete 'Deployment passed all checks'
20749
- printf '%s\n' "$id" > "$root/current.next"
20750
- mv "$root/current.next" "$root/current"
20751
- printf '[PASS] %s\n' "$id"
20752
- `.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]);
20753
- }
20754
- function createUploadScripts(config2, bundle, projectName) {
20755
- const ssh = config2.ssh;
20756
- const host = ssh?.host ?? "SERVER_HOST";
20757
- const user = ssh?.username ?? "deploy";
20758
- const remote = `${config2.remote.baseDir}/${projectName}`;
20759
- const port = ssh?.port ?? 22;
20760
- const sh = String.raw`#!/bin/sh
20761
- set -eu
20762
- cd "$(dirname "$0")"
20763
- host=@@HOST@@
20764
- user=@@USER@@
20765
- port=@@PORT@@
20766
- remote=@@REMOTE@@
20767
- bundle=@@BUNDLE@@
20768
- if [ "$#" -ge 1 ]; then host=$1; fi
20769
- if [ "$#" -ge 2 ]; then port=$2; fi
20770
- if [ "$#" -ge 3 ]; then user=$3; fi
20771
- identity=$(printenv HAI_DEPLOY_IDENTITY_FILE || true)
20772
- if [ -z "$identity" ]; then identity=@@IDENTITY@@; fi
20773
- test "$host" != SERVER_HOST || { printf 'Specify the server: sh deploy.sh SERVER [PORT] [USER].\n' >&2; exit 1; }
20774
- for tool in ssh scp; do command -v "$tool" >/dev/null || { printf 'Install OpenSSH (%s).\n' "$tool" >&2; exit 1; }; done
20775
- set -- -o BatchMode=yes -o StrictHostKeyChecking=@@STRICT@@ -o ConnectTimeout=10
20776
- if [ -n "$identity" ]; then set -- "$@" -i "$identity"; fi
20777
- known_hosts=@@KNOWN@@
20778
- if value=$(printenv HAI_DEPLOY_KNOWN_HOSTS_FILE); then known_hosts=$value; fi
20779
- if [ -n "$known_hosts" ]; then set -- "$@" -o "UserKnownHostsFile=$known_hosts"; fi
20780
- printf '[1/3] Connecting to %s\n' "$host"
20781
- ssh "$@" -p "$port" "$user@$host" "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"
20782
- printf '[2/3] Uploading %s\n' "$bundle"
20783
- scp_host=$host
20784
- case "$host" in *:*) scp_host="[$host]";; esac
20785
- scp "$@" -P "$port" "$bundle" "$user@$scp_host:$remote/incoming/$bundle"
20786
- printf '[3/3] Applying package and checking HTTP access\n'
20787
- ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"
20788
- `;
20789
- const ps1 = String.raw`param([string]$IdentityFile = $env:HAI_DEPLOY_IDENTITY_FILE)
20790
- $ErrorActionPreference = 'Stop'
20791
- Set-Location -LiteralPath $PSScriptRoot
20792
- $target = Get-Content -LiteralPath './deploy-target.json' -Raw | ConvertFrom-Json
20793
- if ($target.host -eq 'SERVER_HOST') { throw 'Set the server host in deploy-target.json.' }
20794
- if ($target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]+$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar$') { throw 'Invalid deployment target.' }
20795
- $options = @('-o', 'BatchMode=yes', '-o', 'StrictHostKeyChecking=accept-new', '-o', 'ConnectTimeout=10')
20796
- if ($target.strictHostKeyChecking -eq $false) { $options[3] = 'StrictHostKeyChecking=no' }
20797
- if (-not $IdentityFile) { $IdentityFile = $target.identityFile }
20798
- if ($IdentityFile) { $options += @('-i', $IdentityFile) }
20799
- if ($target.knownHostsFile) { $options += @('-o', "UserKnownHostsFile=$($target.knownHostsFile)") }
20800
- $destination = "$($target.username)@$($target.host)"
20801
- $scpDestination = if ($target.host.Contains(':')) { "$($target.username)@[$($target.host)]" } else { $destination }
20802
- $remote = $target.directory
20803
- $bundle = $target.bundle
20804
- function Invoke-Step([string]$Name, [string]$Command, [string[]]$Arguments) {
20805
- Write-Host "[$Name]"
20806
- & $Command @Arguments
20807
- if ($LASTEXITCODE -ne 0) { throw "$Name failed (exit $LASTEXITCODE)." }
20808
- }
20809
- try {
20810
- foreach ($tool in @('ssh', 'scp')) { Get-Command $tool -ErrorAction Stop | Out-Null }
20811
- Invoke-Step '1/3 Connect' 'ssh' ($options + @('-p', [string]$target.port, $destination, "mkdir -p '$remote/incoming' && chmod 700 '$remote' '$remote/incoming'"))
20812
- Invoke-Step '2/3 Upload' 'scp' ($options + @('-P', [string]$target.port, $bundle, "$($scpDestination):$remote/incoming/$bundle"))
20813
- Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$target.port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"))
20814
- } catch { Write-Error $_; exit 1 }
20815
- `;
20816
- const values = { HOST: shellArgument(host), USER: shellArgument(user), PORT: String(port), REMOTE: shellArgument(remote), BUNDLE: shellArgument(bundle), IDENTITY: shellArgument(ssh?.identityFile ?? ""), KNOWN: shellArgument(ssh?.knownHostsFile ?? ""), STRICT: ssh?.strictHostKeyChecking === false ? "no" : "accept-new" };
20817
- return { sh: sh.replace(/@@([A-Z]+)@@/g, (_match, name) => values[name]), ps1 };
20818
- }
20819
-
20820
- // ../deploy/src/container/deploy-bundle.ts
20821
- async function createDeploymentBundle(options) {
20822
- const { directory, artifactDir, artifact, projectName, environment, config: config2, selected } = options;
20823
- const stem = basename(options.output).replace(/\.tar$/i, "");
20824
- const suffix = `-${artifact.appVersion}`;
20825
- const output2 = resolve(dirname(options.output), `${stem.endsWith(suffix) ? stem : `${stem}${suffix}`}.tar`);
20826
- if (!/^[\w.+-]+\.tar$/.test(basename(output2)))
20827
- throw new Error(deployM("deploy_bundleNameInvalid"));
20828
- const staging = mkdtempSync(join(directory, ".bundle-"));
20829
- const temporaryTar = `${output2}.partial`;
20830
- mkdirSync(dirname(output2), { recursive: true, mode: 448 });
20831
- chmodSync(staging, 448);
20832
- const files = [];
20833
- const put = (file2, content) => {
20834
- mkdirSync(dirname(join(staging, file2)), { recursive: true });
20835
- writeFileSync(join(staging, file2), content, { mode: 384 });
20836
- files.push(file2);
20837
- };
20838
- try {
20839
- mkdirSync(join(staging, "images"));
20840
- const compose = parse$1(readFileSync(join(directory, `docker-compose-${artifact.appVersion}.yml`), "utf8"));
20841
- const dependencies = /* @__PURE__ */ new Set();
20842
- const references = /* @__PURE__ */ new Set();
20843
- for (const name of selected) {
20844
- const service = compose.services[name];
20845
- if (!service)
20846
- throw new Error(deployM("deploy_selectionInvalid", { params: { service: name } }));
20847
- if (typeof service.image === "string")
20848
- references.add(service.image);
20849
- if (service.depends_on) {
20850
- for (const dependency of Array.isArray(service.depends_on) ? service.depends_on : Object.keys(service.depends_on))
20851
- dependencies.add(dependency);
20852
- }
20853
- if (service.env_file)
20854
- service.env_file = [`../${options.deploymentId}/.env.runtime`, `../${options.deploymentId}/middleware.env`];
20855
- if (Array.isArray(service.volumes))
20856
- service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace("./web.conf:", `../${options.deploymentId}/web.conf:`) : mount);
20857
- }
20858
- put("compose.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, compose.services[name]])) }));
20859
- put("compose-entry.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, { extends: { file: `../${options.deploymentId}/compose.json`, service: name } }])) }));
20860
- let runtime;
20861
- const imageList = [];
20862
- for (const reference of references) {
20863
- if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference))
20864
- throw new Error(deployM("deploy_imageVersionRequired", { params: { image: reference } }));
20865
- const built = artifact.images.find((image) => image.imageRef === reference);
20866
- const file2 = built?.file ?? `middleware-${imageList.length + 1}.tar`;
20867
- if (built) {
20868
- copyFileSync(join(artifactDir, file2), join(staging, "images", file2));
20869
- } else {
20870
- if (!runtime) {
20871
- const candidates = options.runtime === "auto" ? ["podman", "docker"] : [options.runtime];
20872
- for (const candidate of candidates) {
20873
- if ((await runCommand(candidate, ["version"])).success) {
20874
- runtime = candidate;
20875
- break;
20876
- }
20877
- }
20878
- if (!runtime)
20879
- throw new Error(deployM("deploy_bundleRuntimeRequired"));
20880
- }
20881
- if (!(await runCommand(runtime, ["image", "inspect", reference])).success) {
20882
- const pulled = await runCommand(runtime, ["pull", reference], { timeoutMs: 6e5 });
20883
- if (!pulled.success)
20884
- throw new Error(pulled.error.message);
20885
- }
20886
- const saved = await runCommand(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", join(staging, "images", file2), reference], { timeoutMs: 6e5 });
20887
- if (!saved.success)
20888
- throw new Error(saved.error.message);
20889
- }
20890
- files.push(`images/${file2}`);
20891
- imageList.push(`${file2} ${reference}`);
20892
- }
20893
- put(".env.runtime", readFileSync(join(directory, ".env.runtime"), "utf8"));
20894
- put("middleware.env", options.middlewareEnv);
20895
- if (selected.includes("web"))
20896
- put("web.conf", readFileSync(join(directory, "web.conf"), "utf8"));
20897
- put("manifest.json", `${JSON.stringify({ ...artifact, images: artifact.images.filter((image) => references.has(image.imageRef)), deploymentId: options.deploymentId, selectedServices: selected, partial: options.partial }, null, 2)}
20898
- `);
20899
- put("layout", `${config2.expose.hostPort}:${options.containerPort}:${options.hasWeb}:${options.hasWeb ? await artifactChecksum(join(directory, "web.conf")) : ""}
20900
- `);
20901
- put("image-list", `${imageList.join("\n")}
20902
- `);
20903
- put("dependencies", `${[...dependencies].join("\n")}
20904
- `);
20905
- if (selected.includes("storage-init"))
20906
- put("initialize-storage", "");
20907
- put("apply.sh", createApplyScript({ deploymentId: options.deploymentId, projectName, environment, directory: `${config2.remote.baseDir}/${projectName}`, runtime: config2.remote.runtime, partial: options.partial, selected: selected.filter((name) => name !== "storage-init"), hostPort: config2.expose.hostPort, healthPaths: options.healthPaths }));
20908
- const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output2), start: "apply.sh", images: [...references].join("\n"), compose: "compose.json" } });
20909
- put("README.txt", `${deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: "compose.json" } })}
20910
- ${instructions}
20911
- `);
20912
- const checksums = [];
20913
- for (const file2 of files)
20914
- checksums.push(`${await artifactChecksum(join(staging, file2))} ${file2}`);
20915
- writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
20916
- `);
20917
- const tar = await runCommand("tar", ["-cf", temporaryTar, "-C", staging, "."]);
20918
- if (!tar.success)
20919
- throw new Error(tar.error.message);
20920
- chmodSync(temporaryTar, 384);
20921
- renameSync(temporaryTar, output2);
20922
- const upload = createUploadScripts(config2, basename(output2), projectName);
20923
- const scripts = `${output2.slice(0, -4)}-deploy`;
20924
- mkdirSync(scripts, { recursive: true, mode: 448 });
20925
- writeFileSync(join(scripts, "deploy.sh"), upload.sh.replace('cd "$(dirname "$0")"', 'cd "$(dirname "$0")/.."'), { mode: 448 });
20926
- writeFileSync(join(scripts, "deploy.ps1"), upload.ps1.replace("Set-Location -LiteralPath $PSScriptRoot", "Set-Location -LiteralPath (Split-Path -Parent $PSScriptRoot)").replace("'./deploy-target.json'", "(Join-Path $PSScriptRoot 'deploy-target.json')"));
20927
- writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...config2.ssh, host: config2.ssh?.host ?? "SERVER_HOST", username: config2.ssh?.username ?? "deploy", port: config2.ssh?.port ?? 22, directory: `${config2.remote.baseDir}/${projectName}`, bundle: basename(output2) }, null, 2)}
20928
- `);
20929
- writeFileSync(join(scripts, "README.txt"), instructions);
20930
- return { bundlePath: output2, instructions: `${instructions}
20931
- ${scripts}`, startName: "apply.sh" };
20932
- } finally {
20933
- rmSync(staging, { recursive: true, force: true });
20934
- rmSync(temporaryTar, { force: true });
20935
- }
20936
- }
20937
- var IMAGES = {
20938
- postgres: "docker.io/library/postgres:16.15-alpine",
20939
- mysql: "docker.io/library/mysql:8.4.11",
20940
- redis: "docker.io/library/redis:7.4.11-alpine",
20941
- rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
20942
- minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
20943
- qdrant: "docker.io/qdrant/qdrant:v1.19.2",
20944
- pgvector: "docker.io/pgvector/pgvector:0.8.0-pg17",
20945
- chroma: "docker.io/chromadb/chroma:1.5.5",
20946
- ldap: "docker.io/osixia/openldap:1.5.0",
20947
- smtp: "docker.io/axllent/mailpit:v1.27.4"
20948
- };
20949
- function generatePassword() {
20950
- return randomBytes(24).toString("base64url");
20951
- }
20952
- function resolveSidecarSecrets(appDir, projectName) {
20953
- const secretFile = join(appDir, ".hai", "deploy", "runtime", `${projectName}.secrets.json`);
20954
- if (existsSync(secretFile)) {
20955
- try {
20956
- const saved = JSON.parse(readFileSync(secretFile, "utf-8"));
20957
- if (!saved || !["dbPassword", "storagePassword", "ldapPassword"].every((key) => typeof saved[key] === "string" && saved[key].length > 0))
20958
- throw new Error("Invalid credentials");
20959
- return saved;
20960
- } catch {
20961
- throw new Error(deployM("deploy_secretsInvalid", { params: { path: secretFile } }));
20962
- }
20963
- }
20964
- const secrets = {
20965
- dbPassword: generatePassword(),
20966
- storagePassword: generatePassword(),
20967
- ldapPassword: generatePassword()
20968
- };
20969
- mkdirSync(dirname(secretFile), { recursive: true });
20970
- writeFileSync(secretFile, JSON.stringify(secrets), { mode: 384 });
20971
- return secrets;
20972
- }
20973
- function addAppDataVolume(plan, projectName) {
20974
- const mount = `${projectName}-data:/app/data`;
20975
- if (!plan.appVolumes.includes(mount)) {
20976
- plan.appVolumes.push(mount);
20977
- plan.volumes[`${projectName}-data`] = null;
20978
- }
20979
- }
20980
- function planPostgresDb(plan, projectName, password, net) {
20981
- plan.services.push("db");
20982
- plan.dependsOn.push("db");
20983
- plan.composeServices.db = {
20984
- image: IMAGES.postgres,
20985
- container_name: `hai-${projectName}-db`,
20986
- restart: "unless-stopped",
20987
- env_file: [".env.runtime"],
20988
- volumes: [`${projectName}-db-data:/var/lib/postgresql/data`],
20989
- networks: net
20990
- };
20991
- plan.volumes[`${projectName}-db-data`] = null;
20992
- plan.env.POSTGRES_USER = "hai";
20993
- plan.env.POSTGRES_PASSWORD = password;
20994
- plan.env.POSTGRES_DB = "hai";
20995
- plan.env.HAI_DB = JSON.stringify({ type: "postgresql", url: `postgresql://hai:${password}@db:5432/hai`, database: "hai" });
20996
- plan.secrets.push(password);
20997
- }
20998
- function planMysqlDb(plan, projectName, password, net) {
20999
- plan.services.push("db");
21000
- plan.dependsOn.push("db");
21001
- plan.composeServices.db = {
21002
- image: IMAGES.mysql,
21003
- container_name: `hai-${projectName}-db`,
21004
- restart: "unless-stopped",
21005
- env_file: [".env.runtime"],
21006
- volumes: [`${projectName}-db-data:/var/lib/mysql`],
21007
- networks: net
21008
- };
21009
- plan.volumes[`${projectName}-db-data`] = null;
21010
- plan.env.MYSQL_ROOT_PASSWORD = password;
21011
- plan.env.MYSQL_DATABASE = "hai";
21012
- plan.env.MYSQL_USER = "hai";
21013
- plan.env.MYSQL_PASSWORD = password;
21014
- plan.env.HAI_DB = JSON.stringify({ type: "mysql", url: `mysql://hai:${password}@db:3306/hai`, database: "hai" });
21015
- plan.secrets.push(password);
21016
- }
21017
- function planRedisCache(plan, projectName, net) {
21018
- plan.services.push("cache");
21019
- plan.dependsOn.push("cache");
21020
- plan.composeServices.cache = {
21021
- image: IMAGES.redis,
21022
- container_name: `hai-${projectName}-cache`,
21023
- restart: "unless-stopped",
21024
- networks: net
21025
- };
21026
- plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
21027
- }
21028
- function planS3Storage(plan, projectName, password, net, engine) {
21029
- plan.services.push("storage");
21030
- plan.dependsOn.push("storage");
21031
- plan.composeServices.storage = {
21032
- image: IMAGES[engine],
21033
- container_name: `hai-${projectName}-storage`,
21034
- restart: "unless-stopped",
21035
- ...engine === "minio" ? { command: ["server", "/data"] } : {},
21036
- env_file: [".env.runtime"],
21037
- volumes: [`${projectName}-storage-data:/data`],
21038
- networks: net
21039
- };
21040
- plan.composeServices["storage-init"] = {
21041
- depends_on: ["storage"],
21042
- env_file: [".env.runtime"],
21043
- entrypoint: [
21044
- "node",
21045
- "--input-type=module",
21046
- "-e",
21047
- // 复用应用 @h-ai/storage 已依赖的 SDK;不额外拉取失效的 mc 镜像,不把密钥拼进命令。
21048
- `import { S3Client, HeadBucketCommand, CreateBucketCommand } from '@aws-sdk/client-s3';
21049
- const config = JSON.parse(process.env.HAI_STORAGE);
21050
- const client = new S3Client({ endpoint: config.endpoint, region: config.region, forcePathStyle: config.forcePathStyle, credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey } });
21051
- let ready = false;
21052
- for (let attempt = 0; attempt < 120; attempt++) {
21053
- try {
21054
- try { await client.send(new HeadBucketCommand({ Bucket: config.bucket })); }
21055
- catch (error) {
21056
- if (error.$$metadata?.httpStatusCode !== 404) throw error;
21057
- await client.send(new CreateBucketCommand({ Bucket: config.bucket }));
21058
- }
21059
- ready = true; break;
21060
- } catch { await new Promise(resolve => setTimeout(resolve, 1000)); }
21061
- }
21062
- client.destroy();
21063
- if (!ready) throw new Error('S3 bucket initialization failed');`
21064
- ],
21065
- networks: net
21066
- };
21067
- plan.volumes[`${projectName}-storage-data`] = null;
21068
- if (engine === "rustfs") {
21069
- 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" });
21070
- } else {
21071
- Object.assign(plan.env, { MINIO_ROOT_USER: "hai-storage", MINIO_ROOT_PASSWORD: password });
21072
- }
21073
- plan.env.HAI_STORAGE = JSON.stringify({
21074
- type: "s3",
21075
- endpoint: "http://storage:9000",
21076
- bucket: "hai",
21077
- region: "us-east-1",
21078
- accessKeyId: "hai-storage",
21079
- secretAccessKey: password,
21080
- forcePathStyle: true
21081
- });
21082
- plan.secrets.push(password);
21083
- }
21084
- function planQdrantVecdb(plan, projectName, net) {
21085
- plan.services.push("vecdb");
21086
- plan.dependsOn.push("vecdb");
21087
- plan.composeServices.vecdb = {
21088
- image: IMAGES.qdrant,
21089
- container_name: `hai-${projectName}-vecdb`,
21090
- restart: "unless-stopped",
21091
- volumes: [`${projectName}-vecdb-data:/qdrant/storage`],
21092
- networks: net
21093
- };
21094
- plan.volumes[`${projectName}-vecdb-data`] = null;
21095
- plan.env.HAI_VECDB = JSON.stringify({ type: "qdrant", url: "http://vecdb:6333" });
21096
- }
21097
- function planPgvectorVecdb(plan, projectName, password, net) {
21098
- plan.services.push("vecdb");
21099
- plan.dependsOn.push("vecdb");
21100
- plan.composeServices.vecdb = {
21101
- image: IMAGES.pgvector,
21102
- container_name: `hai-${projectName}-vecdb`,
21103
- restart: "unless-stopped",
21104
- env_file: [".env.runtime"],
21105
- volumes: [`${projectName}-vecdb-data:/var/lib/postgresql/data`],
21106
- networks: net
21107
- };
21108
- plan.volumes[`${projectName}-vecdb-data`] = null;
21109
- plan.env.POSTGRES_USER = "hai";
21110
- plan.env.POSTGRES_PASSWORD = password;
21111
- plan.env.POSTGRES_DB = "hai";
21112
- plan.env.HAI_VECDB = JSON.stringify({ type: "pgvector", url: `postgresql://hai:${password}@vecdb:5432/hai`, database: "hai" });
21113
- plan.secrets.push(password);
21114
- }
21115
- function planChromaVecdb(plan, projectName, net) {
21116
- plan.services.push("vecdb");
21117
- plan.dependsOn.push("vecdb");
21118
- plan.composeServices.vecdb = {
21119
- image: IMAGES.chroma,
21120
- container_name: `hai-${projectName}-vecdb`,
21121
- restart: "unless-stopped",
21122
- volumes: [`${projectName}-vecdb-data:/data`],
21123
- networks: net
21124
- };
21125
- plan.volumes[`${projectName}-vecdb-data`] = null;
21126
- plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
21127
- }
21128
- function planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine = "rustfs") {
21129
- const serviceModes = { ...options?.serviceModes };
21130
- for (const requirement of scan.middlewares ?? []) {
21131
- if (options?.selectedServices && !options.selectedServices.includes(requirement.service))
21132
- continue;
21133
- const mode = serviceModes[requirement.envPrefix] ?? serviceModes[requirement.service];
21134
- if (mode !== "keep")
21135
- continue;
21136
- const endpoint = requirement.config.url ?? requirement.config.endpoint;
21137
- let host = requirement.config.host;
21138
- if (typeof endpoint === "string") {
21139
- try {
21140
- host = new URL(endpoint).hostname;
21141
- } catch {
21142
- continue;
21143
- }
21144
- }
21145
- if (host === requirement.service || ["sqlite", "local", "memory", "lancedb"].includes(requirement.backend)) {
21146
- serviceModes[requirement.envPrefix] = "default";
21147
- serviceModes[requirement.service] = "default";
21148
- }
21149
- }
21150
- options = { ...options, serviceModes };
21151
- const plan = {
21152
- services: [],
21153
- composeServices: {},
21154
- volumes: {},
21155
- appVolumes: [],
21156
- env: {},
21157
- dependsOn: [],
21158
- secrets: []
21159
- };
21160
- if (!scan.requiredServices.some((service) => usesDefaultMiddleware(scan, service, options)))
21161
- return plan;
21162
- const secrets = resolveSidecarSecrets(appDir, projectName);
21163
- const net = ["hai-app"];
21164
- const backends = scan.serviceBackends;
21165
- for (const service of scan.requiredServices) {
21166
- if (!usesDefaultMiddleware(scan, service, options))
21167
- continue;
21168
- if (service === "db" && !provisionEnv.HAI_DB) {
21169
- const backend = backends.db ?? "postgresql";
21170
- if (backend === "mysql")
21171
- planMysqlDb(plan, projectName, secrets.dbPassword, net);
21172
- else if (backend === "sqlite")
21173
- addAppDataVolume(plan, projectName);
21174
- else
21175
- planPostgresDb(plan, projectName, secrets.dbPassword, net);
21176
- } else if (service === "cache" && !provisionEnv.HAI_CACHE) {
21177
- if ((backends.cache ?? "redis") === "redis")
21178
- planRedisCache(plan, projectName, net);
21179
- } else if (service === "storage" && !provisionEnv.HAI_STORAGE) {
21180
- if ((backends.storage ?? "s3") === "s3")
21181
- planS3Storage(plan, projectName, secrets.storagePassword, net, s3Engine);
21182
- else
21183
- addAppDataVolume(plan, projectName);
21184
- } else if (service === "vecdb" && !provisionEnv.HAI_VECDB) {
21185
- const backend = backends.vecdb ?? "qdrant";
21186
- if (backend === "pgvector")
21187
- planPgvectorVecdb(plan, projectName, secrets.dbPassword, net);
21188
- else if (backend === "chroma")
21189
- planChromaVecdb(plan, projectName, net);
21190
- else if (backend === "lancedb")
21191
- addAppDataVolume(plan, projectName);
21192
- else
21193
- planQdrantVecdb(plan, projectName, net);
21194
- } else if (service === "ldap" && backends.ldap === "ldap" && !provisionEnv.HAI_IAM_LDAP) {
21195
- const password = secrets.ldapPassword ?? secrets.dbPassword;
21196
- plan.services.push("ldap");
21197
- plan.dependsOn.push("ldap");
21198
- 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`] };
21199
- plan.volumes[`${projectName}-ldap-data`] = null;
21200
- plan.volumes[`${projectName}-ldap-config`] = null;
21201
- 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" }) });
21202
- plan.secrets.push(password);
21203
- } 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) {
21204
- plan.services.push("email");
21205
- plan.dependsOn.push("email");
21206
- plan.composeServices.email = { image: IMAGES.smtp, restart: "unless-stopped", networks: net };
21207
- const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
21208
- if (requirements?.length) {
21209
- for (const requirement of requirements)
21210
- plan.env[requirement.envPrefix] = JSON.stringify({ name: requirement.config.name ?? "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
21211
- } else if (!scan.middlewares) {
21212
- plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({ name: "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
21213
- }
21214
- }
21215
- }
21216
- return plan;
21217
- }
21218
- function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv = {}, s3Engine = "rustfs") {
21219
- const sidecar = planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine);
21220
- const env = {
21221
- NODE_ENV: "production",
21222
- HOST: "0.0.0.0",
21223
- PORT: String(containerPort),
21224
- HAI_SERV_SERVER_HOST: "0.0.0.0",
21225
- HAI_SERV_SERVER_PORT: String(containerPort),
21226
- ...appEnv,
21227
- ...sidecar.env,
21228
- ...provisionEnv
21229
- };
21230
- return { env, sidecar };
21231
- }
21232
- function generateComposeFile(options) {
21233
- const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
21234
- const app = {
21235
- image: imageRef,
21236
- labels: { "org.opencontainers.image.version": options.appVersion },
21237
- container_name: `hai-${projectName}-app`,
21238
- restart: "unless-stopped",
21239
- // 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
21240
- pull_policy: "never",
21241
- env_file: [".env.runtime"],
21242
- ports: [`${hostPort}:${containerPort}`],
21243
- networks: ["hai-app"]
21244
- };
21245
- if (sidecar.appVolumes.length > 0)
21246
- app.volumes = sidecar.appVolumes;
21247
- if (sidecar.dependsOn.length > 0)
21248
- app.depends_on = sidecar.dependsOn;
21249
- const services = { app, ...sidecar.composeServices };
21250
- if (options.web) {
21251
- delete app.ports;
21252
- services.web = {
21253
- image: options.web.imageRef,
21254
- labels: { "org.opencontainers.image.version": options.appVersion },
21255
- container_name: `hai-${projectName}-web`,
21256
- restart: "unless-stopped",
21257
- pull_policy: "never",
21258
- ports: [`${hostPort}:${options.web.containerPort}`],
21259
- environment: { PUBLIC_API_BASE: "" },
21260
- healthcheck: { test: ["CMD", "wget", "-Y", "off", "-q", "-O", "/dev/null", `http://127.0.0.1:${options.web.containerPort}/_hai/web-health`], interval: "30s", timeout: "5s", retries: 3 },
21261
- volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
21262
- depends_on: ["app"],
21263
- networks: ["hai-app"]
21264
- };
21265
- }
21266
- if (services["storage-init"])
21267
- services["storage-init"] = { ...services["storage-init"], image: imageRef };
21268
- const compose = {
21269
- "x-hai-version": options.appVersion,
21270
- services,
21271
- "networks": {
21272
- "hai-app": { name: `hai-${projectName}` }
21273
- }
21274
- };
21275
- if (Object.keys(sidecar.volumes).length > 0)
21276
- compose.volumes = sidecar.volumes;
21277
- if (services["storage-init"]) {
21278
- app.depends_on = {
21279
- ...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
21280
- "storage-init": { condition: "service_completed_successfully" }
21281
- };
21282
- }
21283
- for (const service of Object.values(services))
21284
- service.pull_policy = "never";
21285
- return stringify(compose);
21286
- }
21287
- function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
21288
- if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535))
21289
- throw new Error("Invalid web proxy target");
21290
- return `server {
21291
- listen ${webPort};
21292
- server_name _;
21293
- root /usr/share/nginx/html;
21294
- location / {
21295
- try_files $uri $uri/ /index.html;
21296
- }
21297
- location = /runtime-config.js {
21298
- add_header Cache-Control "no-store";
21299
- default_type application/javascript;
21300
- return 200 'globalThis.__HAI_PUBLIC_CONFIG__={apiBase:globalThis.location.origin+"${apiPrefix}"};';
21301
- }
21302
- location = /_hai/web-health {
21303
- access_log off;
21304
- return 200 'ok';
21305
- }
21306
- location = /health {
21307
- proxy_pass http://app:${port};
21308
- proxy_set_header Host $http_host;
21309
- }
21310
- location ${apiPrefix.replace(/\/$/, "")}/ {
21311
- proxy_pass http://app:${port};
21312
- proxy_http_version 1.1;
21313
- proxy_set_header Host $http_host;
21314
- proxy_set_header X-Forwarded-Proto $scheme;
21315
- proxy_buffering off;
21316
- }
21317
- }
21318
- `;
21319
- }
21320
- function serializeEnvFile(env) {
21321
- return `${Object.entries(env).map(([key, value]) => {
21322
- let literal2 = value;
21323
- if (/[\s#$'"]/.test(value)) {
21324
- if (value.startsWith("{") || value.startsWith("["))
21325
- literal2 = `'${value.replaceAll("'", "\\u0027")}'`;
21326
- else if (!value.includes("'"))
21327
- literal2 = `'${value}'`;
21328
- else
21329
- literal2 = JSON.stringify(value).replaceAll("$", "$$");
21330
- }
21331
- return `${key}=${literal2}`;
21332
- }).join("\n")}
21333
- `;
21334
- }
21335
- var logger4 = core.logger.child({ module: "deploy", scope: "ssh" });
21336
- function buildConnectOptions(config2) {
21337
- const args = ["-o", "BatchMode=yes"];
21338
- args.push("-o", `ConnectTimeout=${config2.connectTimeoutSeconds}`);
21339
- args.push("-o", `StrictHostKeyChecking=${config2.strictHostKeyChecking ? "accept-new" : "no"}`);
21340
- if (config2.knownHostsFile)
21341
- args.push("-o", `UserKnownHostsFile=${config2.knownHostsFile}`);
21342
- if (config2.identityFile) {
21343
- args.push("-i", config2.identityFile);
21344
- args.push("-o", "IdentitiesOnly=yes");
21345
- }
21346
- return args;
21347
- }
21348
- function buildSshExecArgs(config2, remoteCommand) {
21349
- return [
21350
- ...buildConnectOptions(config2),
21351
- "-p",
21352
- String(config2.port),
21353
- `${config2.username}@${config2.host}`,
21354
- remoteCommand
21355
- ];
21356
- }
21357
- function buildScpUploadArgs(config2, localPath, remotePath) {
21358
- return [
21359
- ...buildConnectOptions(config2),
21360
- "-P",
21361
- String(config2.port),
21362
- localPath,
21363
- `${config2.username}@${config2.host.includes(":") ? `[${config2.host}]` : config2.host}:${remotePath}`
21364
- ];
21365
- }
21366
- function createSshExecutor(config2) {
21367
- const exec = async (remoteCommand, options) => {
21368
- return runCommand("ssh", buildSshExecArgs(config2, remoteCommand), {
21369
- timeoutMs: options?.timeoutMs,
21370
- secrets: options?.secrets,
21371
- onOutput: options?.onOutput
21372
- });
21373
- };
21374
- const upload = async (localPath, remotePath) => {
21375
- const args = buildScpUploadArgs(config2, localPath, remotePath);
21376
- const result = await runCommand("scp", args, {});
21377
- if (!result.success) {
21378
- return err(
21379
- HaiDeployError.IMAGE_TRANSFER_FAILED,
21380
- deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }),
21381
- result.error
21382
- );
21383
- }
21384
- return ok(void 0);
21385
- };
21386
- const ping = async () => {
21387
- logger4.debug("SSH preflight", { host: config2.host, port: config2.port });
21388
- const result = await exec("true", { timeoutMs: 2e4 });
21389
- if (!result.success) {
21390
- return err(
21391
- HaiDeployError.REMOTE_CONNECT_FAILED,
21392
- deployM("deploy_remoteConnectFailed", { params: { host: config2.host, error: result.error.message } }),
21393
- result.error
21394
- );
21395
- }
21396
- return ok(void 0);
21397
- };
21398
- return { exec, upload, ping };
21399
- }
21400
-
21401
- // ../deploy/src/providers/deploy-provider-docker-ssh.ts
21402
- var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
21403
- var HEALTH_POLL_INTERVAL = 3e3;
21404
- var HEALTH_MAX_ATTEMPTS = 40;
21405
- function makeDeploymentId() {
21406
- const now = /* @__PURE__ */ new Date();
21407
- const pad = (n) => String(n).padStart(2, "0");
21408
- const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
21409
- return `${stamp}-${randomBytes(3).toString("hex")}`;
21410
- }
21411
- function sleep(ms) {
21412
- return new Promise((resolve6) => setTimeout(resolve6, ms));
21413
- }
21414
- function createDockerSshProvider(config2, container) {
21415
- const ssh = config2.ssh ? createSshExecutor(config2.ssh) : void 0;
21416
- const baseDir = config2.remote.baseDir;
21417
- async function detectRemoteRuntime() {
21418
- if (!ssh)
21419
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
21420
- const candidates = config2.remote.runtime === "auto" ? ["podman", "docker"] : [config2.remote.runtime];
21421
- for (const runtime of candidates) {
21422
- const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
21423
- if (!versionResult.success)
21424
- continue;
21425
- const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
21426
- if (composeResult.success) {
21427
- logger5.info("Remote container runtime detected", { runtime });
21428
- return ok(runtime);
21429
- }
21430
- }
21431
- return err(
21432
- HaiDeployError.REMOTE_COMMAND_FAILED,
21433
- deployM("deploy_remoteRuntimeMissing", { params: { runtime: candidates.join(" / ") } })
21434
- );
21435
- }
21436
- async function healthCheck(url2) {
21437
- for (let attempt = 0; attempt < HEALTH_MAX_ATTEMPTS; attempt++) {
21438
- try {
21439
- const response = await fetch(url2, { signal: AbortSignal.timeout(5e3) });
21440
- if (response.ok)
21441
- return true;
21442
- } catch {
21443
- }
21444
- await sleep(HEALTH_POLL_INTERVAL);
21445
- }
21446
- return false;
21447
- }
21448
- const deploy2 = async (context) => {
21449
- const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
21450
- const deploymentId = makeDeploymentId();
21451
- const hostPort = config2.expose.hostPort;
21452
- const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
21453
- mkdirSync(artifactDir, { recursive: true, mode: 448 });
21454
- chmodSync(artifactDir, 448);
21455
- const artifact = context.artifact;
21456
- const resolvedBuildDir = context.artifactDir;
21457
- const images = artifact.images;
21458
- const web = images.find((image) => image.name === "web");
21459
- const app = images.find((image) => image.name === "app");
21460
- if (options?.selectedServices) {
21461
- const available = [...images.map((image) => image.name), ...scan.requiredServices];
21462
- const invalid = options.selectedServices.find((name) => !available.includes(name));
21463
- if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid)
21464
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: invalid ?? options.selectedServices.join(", ") } }));
21465
- }
21466
- const hasWeb = artifact.hasWeb || !!web;
21467
- const partial2 = artifact.partial || options?.selectedServices !== void 0;
21468
- const containerPort = app?.containerPort ?? artifact.appContainerPort ?? config2.expose.containerPort;
21469
- const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
21470
- const host = config2.ssh?.host;
21471
- const url2 = host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
21472
- const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
21473
- if (sidecar.composeServices["storage-init"] && !app && options?.selectedServices?.includes("storage"))
21474
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageArtifactRequired"));
21475
- let webProxy;
21476
- if (hasWeb) {
21477
- const server = loadDeploymentConfig("serv", join(appDir, "config", "_serv.yml"), env);
21478
- if (!server.success)
21479
- return server;
21480
- const http = server.data.http;
21481
- const cors = server.data.cors;
21482
- const apiPrefix = http?.apiPrefix ?? "/api/v1";
21483
- if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix))
21484
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
21485
- webProxy = generateWebProxyConfig(containerPort, apiPrefix, webContainerPort);
21486
- env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config2.ssh ? url2 : cors?.origin ?? "*" } });
21487
- }
21488
- const envPath = join(artifactDir, ".env.runtime");
21489
- writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
21490
- if (options?.environment) {
21491
- const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
21492
- if (!environment.success)
21493
- return environment;
21494
- const saved = saveDeploymentEnvironment(environment.data, { HAI_DEPLOY_ENVIRONMENT: environment.data.environment, HAI_CORE_ENV: environment.data.values.HAI_CORE_ENV, ...sidecar.env, ...provisionEnv });
21495
- if (!saved.success)
21496
- return saved;
21497
- }
21498
- const composeName = `docker-compose-${artifact.appVersion}.yml`;
21499
- const composeContent = generateComposeFile({
21500
- projectName,
21501
- appVersion: artifact.appVersion,
21502
- imageRef: app?.imageRef ?? "localhost/hai-unselected:0",
21503
- hostPort,
21504
- containerPort,
21505
- sidecar,
21506
- web: hasWeb ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0
21507
- });
21508
- const composePath = join(artifactDir, composeName);
21509
- writeFileSync(composePath, composeContent);
21510
- if (webProxy)
21511
- writeFileSync(join(artifactDir, "web.conf"), webProxy);
21512
- const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [...images.map((image) => image.name), ...Object.keys(sidecar.composeServices)];
21513
- if (!selected.length || selected.some((name) => !/^[a-z][a-z0-9-]*$/.test(name)) || new Set(selected).size !== selected.length)
21514
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: selected.join(", ") } }));
21515
- for (const name of selected) {
21516
- if (["app", "web"].includes(name) && !images.some((image) => image.name === name))
21517
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: name } }));
21518
- }
21519
- if (selected.includes("storage") && sidecar.composeServices["storage-init"] && !selected.includes("storage-init"))
21520
- selected.push("storage-init");
21521
- options?.onProgress?.("packaging");
21522
- const bundle = await createDeploymentBundle({
21523
- directory: artifactDir,
21524
- artifactDir: resolvedBuildDir,
21525
- artifact,
21526
- projectName,
21527
- environment: options?.environment ?? "prod",
21528
- deploymentId,
21529
- config: config2,
21530
- runtime: options?.runtime ?? "auto",
21531
- selected,
21532
- partial: partial2,
21533
- hasWeb,
21534
- containerPort,
21535
- middlewareEnv: serializeEnvFile(sidecar.env),
21536
- healthPaths: hasWeb ? ["/health", "/_hai/web-health", "/"] : [scan.isSvelteKit ? "/" : "/health"],
21537
- output: options?.output ?? join(artifactDir, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar`)
21538
- });
21539
- if (options?.bundleOnly)
21540
- return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
21541
- if (!ssh)
21542
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
21543
- const ping = await ssh.ping();
21544
- if (!ping.success)
21545
- return ping;
21546
- const remoteRuntime = await detectRemoteRuntime();
21547
- if (!remoteRuntime.success)
21548
- return remoteRuntime;
21549
- const remoteDir = `${baseDir}/${projectName}`;
21550
- const remoteTar = `${remoteDir}/incoming/${basename(bundle.bundlePath)}`;
21551
- const prepared = await ssh.exec(`mkdir -p -- ${shellArgument(`${remoteDir}/incoming`)} && chmod 700 -- ${shellArgument(remoteDir)} ${shellArgument(`${remoteDir}/incoming`)}`);
21552
- if (!prepared.success)
21553
- return prepared;
21554
- const uploaded = await ssh.upload(bundle.bundlePath, remoteTar);
21555
- if (!uploaded.success)
21556
- return uploaded;
21557
- let pendingOutput = "";
21558
- const started = await ssh.exec(
21559
- `mkdir -p ${shellArgument(`${remoteTar}.contents`)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} -C ${shellArgument(`${remoteTar}.contents`)} && sh ${shellArgument(`${remoteTar}.contents/${bundle.startName}`)} ${shellArgument(remoteDir)}`,
21560
- { timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output2) => {
21561
- pendingOutput += output2;
21562
- const lines = pendingOutput.split("\n");
21563
- pendingOutput = lines.pop() ?? "";
21564
- for (const line of lines) {
21565
- const stage = /^\[(preflight|images|dependencies|start|health|complete)\]/.exec(line)?.[1];
21566
- if (stage)
21567
- options?.onProgress?.(stage);
21568
- }
21569
- } }
21570
- );
21571
- if (!started.success)
21572
- return started;
21573
- logger5.info("Running health check", { url: url2 });
21574
- const healthy = await healthCheck(`${url2}${!hasWeb && scan.isSvelteKit ? "/" : "/health"}`) && (!hasWeb || await healthCheck(`${url2}/_hai/web-health`) && await healthCheck(`${url2}/`));
21575
- if (!healthy) {
21576
- return err(
21577
- HaiDeployError.HEALTH_CHECK_FAILED,
21578
- deployM("deploy_healthCheckFailed", { params: { url: url2 } })
21579
- );
21580
- }
21581
- const result = {
21582
- url: url2,
21583
- deploymentId,
21584
- status: "ready",
21585
- provider: "docker-ssh",
21586
- healthCheck: "passed",
21587
- envVarsSet: Object.keys(env),
21588
- bundlePath: bundle.bundlePath
21589
- };
21590
- logger5.info("docker-ssh deployment complete", { url: url2, deploymentId });
21591
- return ok(result);
21592
- };
21593
- const deploySafely = async (context) => {
21594
- try {
21595
- return await deploy2(context);
21596
- } catch (error62) {
21597
- logger5.error("Deployment bundle failed", { error: error62 });
21598
- const reason = error62 instanceof Error ? error62.message : String(error62);
21599
- return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", { params: { reason: maskSecrets(reason, Object.values(context.appEnv ?? {}).filter((value) => value.length > 6)) } }), error62);
21600
- }
21601
- };
21602
- return { name: "docker-ssh", deploy: deploySafely };
21603
- }
21604
- var logger6 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
21605
- var VERCEL_API = "https://api.vercel.com";
21606
- var POLL_INTERVAL = 3e3;
21607
- var MAX_POLL_ATTEMPTS = 60;
21608
- var VercelApiError = class extends Error {
21609
- constructor(status) {
21610
- super(deployM("deploy_apiError", { params: { service: "Vercel", status: String(status) } }));
21611
- this.status = status;
21612
- }
21613
- status;
21614
- };
21615
- function toDeployError(codeDef, messageKey, error62) {
21616
- return {
21617
- ...codeDef,
21618
- message: deployM(messageKey, {
21619
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
21620
- }),
21621
- cause: error62
21622
- };
21623
- }
21624
- async function vercelFetch(token, path, options) {
21625
- const url2 = `${VERCEL_API}${path}`;
21626
- const response = await fetch(url2, {
21627
- ...options,
21628
- headers: {
21629
- "Authorization": `Bearer ${token}`,
21630
- "Content-Type": "application/json",
21631
- ...options?.headers
21632
- }
21633
- });
21634
- if (!response.ok) {
21635
- throw new VercelApiError(response.status);
21636
- }
21637
- return response.json();
21638
- }
21639
- function collectFiles(dir, baseDir) {
21640
- const results = [];
21641
- const entries = readdirSync(dir, { withFileTypes: true });
21642
- for (const entry of entries) {
21643
- const fullPath = join(dir, entry.name);
21644
- if (entry.isDirectory()) {
21645
- results.push(...collectFiles(fullPath, baseDir));
21646
- } else {
21647
- results.push(relative(baseDir, fullPath).split(sep).join("/"));
21648
- }
21649
- }
21650
- return results;
21651
- }
21652
- function createVercelProvider() {
21653
- let token = null;
21654
- return {
21655
- name: "vercel",
21656
- async authenticate(apiToken) {
21657
- logger6.debug("Authenticating with Vercel");
21658
- try {
21659
- const user = await vercelFetch(
21660
- apiToken,
21661
- "/v2/user"
21662
- );
21663
- token = apiToken;
21664
- const username = user.user.username || user.user.email;
21665
- logger6.info("Vercel authenticated", { user: username });
21666
- return ok(username);
21667
- } catch (error62) {
21668
- logger6.error("Vercel authentication failed", { error: error62 });
21669
- return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error62));
21670
- }
21671
- },
21672
- async createProject(projectName) {
21673
- if (!token) {
21674
- return err(
21675
- HaiDeployError.AUTH_REQUIRED,
21676
- deployM("deploy_authRequired")
21677
- );
21678
- }
21679
- logger6.debug("Creating Vercel project", { projectName });
21680
- try {
21681
- const existingId = await findExistingProject(token, projectName);
21682
- if (existingId !== null) {
21683
- logger6.info("Vercel project found", { projectName, projectId: existingId });
21684
- return ok(existingId);
21685
- }
21686
- try {
21687
- const project = await vercelFetch(
21688
- token,
21689
- "/v11/projects",
21690
- {
21691
- method: "POST",
21692
- body: JSON.stringify({
21693
- name: projectName,
21694
- framework: "sveltekit"
21695
- })
21696
- }
21697
- );
21698
- logger6.info("Vercel project created", { projectName, projectId: project.id });
21699
- return ok(project.id);
21700
- } catch (error62) {
21701
- if (error62 instanceof VercelApiError && error62.status === 409) {
21702
- const concurrentProjectId = await findExistingProject(token, projectName);
21703
- if (concurrentProjectId !== null) {
21704
- logger6.info("Vercel project reused after create conflict", {
21705
- projectName,
21706
- projectId: concurrentProjectId
21707
- });
21708
- return ok(concurrentProjectId);
21709
- }
21710
- }
21711
- throw error62;
21712
- }
21713
- } catch (error62) {
21714
- logger6.error("Vercel project creation failed", { projectName, error: error62 });
21715
- return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error62));
21716
- }
21717
- },
21718
- async setEnvVars(projectId, envVars) {
21719
- if (!token) {
21720
- return err(
21721
- HaiDeployError.AUTH_REQUIRED,
21722
- deployM("deploy_authRequired")
21723
- );
21724
- }
21725
- logger6.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
21726
- try {
21727
- const envEntries = Object.entries(envVars).map(([key, value]) => ({
21728
- key,
21729
- value,
21730
- type: "encrypted",
21731
- target: ["production", "preview", "development"]
21732
- }));
21733
- await vercelFetch(
21734
- token,
21735
- `/v10/projects/${projectId}/env?upsert=true`,
21736
- {
21737
- method: "POST",
21738
- body: JSON.stringify(envEntries)
21739
- }
21740
- );
21741
- logger6.info("Vercel env vars set", { projectId, count: envEntries.length });
21742
- return ok(void 0);
21743
- } catch (error62) {
21744
- logger6.error("Vercel env vars setup failed", { projectId, error: error62 });
21745
- return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error62));
21746
- }
21747
- },
21748
- async deploy(projectId, outputDir) {
21749
- if (!token) {
21750
- return err(
21751
- HaiDeployError.AUTH_REQUIRED,
21752
- deployM("deploy_authRequired")
21753
- );
21754
- }
21755
- logger6.debug("Deploying to Vercel", { projectId, outputDir });
21756
- try {
21757
- const outputConfig = JSON.parse(readFileSync(join(outputDir, "config.json"), "utf8"));
21758
- if (outputConfig.version !== 3)
21759
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
21760
- const filePaths = collectFiles(outputDir, outputDir);
21761
- const files = [];
21762
- for (const filePath of filePaths) {
21763
- const fullPath = join(outputDir, filePath);
21764
- const content = readFileSync(fullPath);
21765
- const sha = createHash("sha1").update(content).digest("hex");
21766
- const size = statSync(fullPath).size;
21767
- await uploadFile(token, content, sha);
21768
- files.push({ file: `.vercel/output/${filePath}`, sha, size });
21769
- }
21770
- logger6.debug("Files uploaded", { count: files.length });
21771
- const deployment = await vercelFetch(
21772
- token,
21773
- "/v13/deployments?prebuilt=1",
21774
- {
21775
- method: "POST",
21776
- body: JSON.stringify({
21777
- name: projectId,
21778
- files,
21779
- version: 2,
21780
- project: projectId,
21781
- projectSettings: { framework: null, buildCommand: "", installCommand: "" }
21782
- })
21783
- }
21784
- );
21785
- const finalStatus = await pollDeploymentStatus(token, deployment.id);
21786
- const result = {
21787
- url: `https://${deployment.url}`,
21788
- deploymentId: deployment.id,
21789
- status: finalStatus,
21790
- envVarsSet: []
21791
- };
21792
- logger6.info("Vercel deployment complete", {
21793
- deploymentId: deployment.id,
21794
- url: result.url,
21795
- status: finalStatus
21796
- });
21797
- return ok(result);
21798
- } catch (error62) {
21799
- logger6.error("Vercel deployment failed", { projectId, error: error62 });
21800
- return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error62));
21801
- }
21802
- }
21803
- };
21804
- }
21805
- async function findExistingProject(token, projectName) {
21806
- try {
21807
- const project = await vercelFetch(token, `/v10/projects/${projectName}`);
21808
- return project.id;
21809
- } catch (error62) {
21810
- if (error62 instanceof VercelApiError && error62.status === 404) {
21811
- return null;
21812
- }
21813
- throw error62;
21814
- }
21815
- }
21816
- async function uploadFile(token, content, sha) {
21817
- const url2 = `${VERCEL_API}/v2/files`;
21818
- const uploadContent = new Uint8Array(content);
21819
- const response = await fetch(url2, {
21820
- method: "POST",
21821
- headers: {
21822
- "Authorization": `Bearer ${token}`,
21823
- "Content-Type": "application/octet-stream",
21824
- "x-vercel-digest": sha
21825
- },
21826
- body: new Blob([uploadContent])
21827
- });
21828
- if (!response.ok && response.status !== 409) {
21829
- throw new Error(deployM("deploy_fileUploadFailed", { params: { status: String(response.status) } }));
21830
- }
21831
- }
21832
- async function pollDeploymentStatus(token, deploymentId) {
21833
- for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
21834
- const deployment = await vercelFetch(
21835
- token,
21836
- `/v13/deployments/${deploymentId}`
21837
- );
21838
- if (deployment.readyState === "READY") {
21839
- return "ready";
21840
- }
21841
- if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
21842
- return "error";
21843
- }
21844
- await new Promise((resolve6) => setTimeout(resolve6, POLL_INTERVAL));
21845
- }
21846
- return "building";
21847
- }
21848
- var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
21849
- function createAliyunProvisioner() {
21850
- let accessKey = null;
21851
- let secretKey = null;
21852
- let signName = "";
21853
- return {
21854
- name: "aliyun",
21855
- serviceType: "sms",
21856
- async authenticate(credentials) {
21857
- logger7.debug("Authenticating with Aliyun");
21858
- try {
21859
- const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
21860
- const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
21861
- if (!akId || !akSecret || !credentials.signName) {
21862
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "accessKeyId, accessKeySecret, signName" } }));
21863
- }
21864
- accessKey = akId;
21865
- secretKey = akSecret;
21866
- signName = credentials.signName;
21867
- logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
21868
- return ok(akId);
21869
- } catch (error62) {
21870
- logger7.error("Aliyun authentication failed", { error: error62 });
21871
- return err(
21872
- HaiDeployError.AUTH_FAILED,
21873
- deployM("deploy_authFailed", {
21874
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
21875
- }),
21876
- error62
21877
- );
21878
- }
21879
- },
21880
- async provision(_appName) {
21881
- if (!accessKey || !secretKey) {
21882
- return err(
21883
- HaiDeployError.AUTH_REQUIRED,
21884
- deployM("deploy_authRequired")
21885
- );
21886
- }
21887
- logger7.debug("Provisioning Aliyun SMS service");
21888
- return ok({
21889
- serviceType: "sms",
21890
- provisionerName: "aliyun",
21891
- resourceStatus: "reused",
21892
- envVars: {
21893
- HAI_REACH_PROVIDERS: JSON.stringify([{ name: "sms", type: "aliyun-sms", accessKeyId: accessKey, accessKeySecret: secretKey, signName }])
21894
- },
21895
- resourceInfo: "aliyun:verify-only"
21896
- });
21897
- }
21898
- };
21899
- }
21900
- var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
21901
- var NEON_API = "https://console.neon.tech/api/v2";
21902
- var DEFAULT_NEON_DATABASE_NAME = "neondb";
21903
- var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
21904
- async function neonFetch(token, path, options) {
21905
- const response = await fetch(`${NEON_API}${path}`, {
21906
- ...options,
21907
- headers: {
21908
- "Accept": "application/json",
21909
- "Authorization": `Bearer ${token}`,
21910
- "Content-Type": "application/json",
21911
- ...options?.headers
21912
- }
21913
- });
21914
- if (!response.ok) {
21915
- throw new Error(deployM("deploy_apiError", { params: { service: "Neon", status: String(response.status) } }));
21916
- }
21917
- return response.json();
21918
- }
21919
- async function listNeonProjects(token) {
21920
- const data = await neonFetch(token, "/projects");
21921
- return data.projects ?? [];
21922
- }
21923
- async function findExistingNeonProject(token, projectName) {
21924
- const projects = await listNeonProjects(token);
21925
- return projects.find((project) => project.name === projectName) ?? null;
21926
- }
21927
- async function getNeonConnectionUri(token, projectId) {
21928
- const query = new URLSearchParams({
21929
- database_name: DEFAULT_NEON_DATABASE_NAME,
21930
- role_name: DEFAULT_NEON_ROLE_NAME
21931
- });
21932
- const data = await neonFetch(
21933
- token,
21934
- `/projects/${projectId}/connection_uri?${query.toString()}`,
21935
- { headers: { "Content-Type": "application/json" } }
21936
- );
21937
- if (!data.uri) {
21938
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
21939
- }
21940
- return data.uri;
21941
- }
21942
- function buildNeonProvisionResult(projectId, connectionUri, resourceStatus) {
21943
- return {
21944
- serviceType: "db",
21945
- provisionerName: "neon",
21946
- resourceStatus,
21947
- envVars: {
21948
- HAI_DB: JSON.stringify({ type: "postgresql", url: connectionUri, database: decodeURIComponent(new URL(connectionUri).pathname.slice(1)) })
21949
- },
21950
- resourceInfo: `neon-project:${projectId}`
21951
- };
21952
- }
21953
- function createNeonProvisioner() {
21954
- let token = null;
21955
- return {
21956
- name: "neon",
21957
- serviceType: "db",
21958
- async authenticate(credentials) {
21959
- logger8.debug("Authenticating with Neon");
21960
- try {
21961
- const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
21962
- if (!apiToken) {
21963
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "token" } }));
21964
- }
21965
- await listNeonProjects(apiToken);
21966
- token = apiToken;
21967
- logger8.info("Neon authenticated");
21968
- return ok("neon-user");
21969
- } catch (error62) {
21970
- logger8.error("Neon authentication failed", { error: error62 });
21971
- return err(
21972
- HaiDeployError.AUTH_FAILED,
21973
- deployM("deploy_authFailed", {
21974
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
21975
- }),
21976
- error62
21977
- );
21978
- }
21979
- },
21980
- async provision(appName) {
21981
- if (!token) {
21982
- return err(
21983
- HaiDeployError.AUTH_REQUIRED,
21984
- deployM("deploy_authRequired")
21985
- );
21986
- }
21987
- logger8.debug("Provisioning Neon database", { appName });
21988
- let resource;
21989
- try {
21990
- const projectName = `${appName}-db`;
21991
- const existingProject = await findExistingNeonProject(token, projectName);
21992
- if (existingProject !== null) {
21993
- resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
21994
- const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
21995
- logger8.info("Neon project reused", {
21996
- projectId: existingProject.id,
21997
- projectName
21998
- });
21999
- return ok(buildNeonProvisionResult(existingProject.id, connectionUri2, "reused"));
22000
- }
22001
- const data = await neonFetch(token, "/projects", {
22002
- method: "POST",
22003
- body: JSON.stringify({
22004
- project: {
22005
- name: projectName,
22006
- pg_version: 16
22007
- }
22008
- })
22009
- });
22010
- const projectId = data.project?.id ?? "";
22011
- if (projectId)
22012
- resource = { resourceInfo: `neon-project:${projectId}`, resourceStatus: "created" };
22013
- const connectionUri = data.connection_uris?.[0]?.connection_uri ?? "";
22014
- if (!projectId || !connectionUri) {
22015
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
22016
- }
22017
- logger8.info("Neon database provisioned", { projectId });
22018
- return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
22019
- } catch (error62) {
22020
- logger8.error("Neon provisioning failed", { appName });
22021
- if (resource) {
22022
- return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
22023
- projectName: appName,
22024
- stage: "provision:db",
22025
- resources: [{ serviceType: "db", provisionerName: "neon", ...resource }]
22026
- });
22027
- }
22028
- return err(
22029
- HaiDeployError.PROVISION_FAILED,
22030
- deployM("deploy_provisionFailed", {
22031
- params: {
22032
- service: "neon",
22033
- error: error62 instanceof Error ? error62.message : String(error62)
22034
- }
22035
- }),
22036
- error62
22037
- );
22038
- }
22039
- }
22040
- };
22041
- }
22042
- var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
22043
- var CF_API = "https://api.cloudflare.com/client/v4";
22044
- function createR2Provisioner() {
22045
- let token = null;
22046
- let accountId = null;
22047
- let accessKey = "";
22048
- let secretKey = "";
22049
- return {
22050
- name: "r2",
22051
- serviceType: "storage",
22052
- async authenticate(credentials) {
22053
- logger9.debug("Authenticating with Cloudflare");
22054
- try {
22055
- const acctId = credentials.accountId ?? credentials.account_id ?? "";
22056
- const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
22057
- if (!acctId || !apiTok || !credentials.accessKeyId || !credentials.secretAccessKey) {
22058
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "accountId, apiToken, accessKeyId, secretAccessKey" } }));
22059
- }
22060
- const res = await fetch(`${CF_API}/accounts/${acctId}/r2/buckets`, {
22061
- headers: { Authorization: `Bearer ${apiTok}` }
22062
- });
22063
- if (!res.ok) {
22064
- throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(res.status) } }));
22065
- }
22066
- accountId = acctId;
22067
- token = apiTok;
22068
- accessKey = credentials.accessKeyId;
22069
- secretKey = credentials.secretAccessKey;
22070
- logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
22071
- return ok(acctId);
22072
- } catch (error62) {
22073
- logger9.error("Cloudflare R2 authentication failed", { error: error62 });
22074
- return err(
22075
- HaiDeployError.AUTH_FAILED,
22076
- deployM("deploy_authFailed", {
22077
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
22078
- }),
22079
- error62
22080
- );
22081
- }
22082
- },
22083
- async provision(appName) {
22084
- if (!token || !accountId) {
22085
- return err(
22086
- HaiDeployError.AUTH_REQUIRED,
22087
- deployM("deploy_authRequired")
22088
- );
22089
- }
22090
- const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
22091
- logger9.debug("Provisioning R2 bucket", { appName, bucketName });
22092
- try {
22093
- const createRes = await fetch(
22094
- `${CF_API}/accounts/${accountId}/r2/buckets`,
22095
- {
22096
- method: "POST",
22097
- headers: {
22098
- "Authorization": `Bearer ${token}`,
22099
- "Content-Type": "application/json"
22100
- },
22101
- body: JSON.stringify({ name: bucketName })
22102
- }
22103
- );
22104
- if (!createRes.ok && createRes.status !== 409) {
22105
- throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
22106
- }
22107
- logger9.info("R2 bucket provisioned", { bucketName });
22108
- return ok({
22109
- serviceType: "storage",
22110
- provisionerName: "r2",
22111
- resourceStatus: createRes.status === 409 ? "reused" : "created",
22112
- envVars: {
22113
- HAI_STORAGE: JSON.stringify({
22114
- type: "s3",
22115
- endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
22116
- bucket: bucketName,
22117
- region: "auto",
22118
- accessKeyId: accessKey,
22119
- secretAccessKey: secretKey
22120
- })
22121
- },
22122
- resourceInfo: `r2-bucket:${bucketName}`
22123
- });
22124
- } catch (error62) {
22125
- logger9.error("R2 provisioning failed", { appName, error: error62 });
22126
- return err(
22127
- HaiDeployError.PROVISION_FAILED,
22128
- deployM("deploy_provisionFailed", {
22129
- params: {
22130
- service: "r2",
22131
- error: error62 instanceof Error ? error62.message : String(error62)
22132
- }
22133
- }),
22134
- error62
22135
- );
22136
- }
22137
- }
22138
- };
22139
- }
22140
- var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
22141
- var RESEND_API = "https://api.resend.com";
22142
- function createResendProvisioner() {
22143
- let token = null;
22144
- let from = "";
22145
- return {
22146
- name: "resend",
22147
- serviceType: "email",
22148
- async authenticate(credentials) {
22149
- logger10.debug("Authenticating with Resend");
22150
- try {
22151
- const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
22152
- if (!apiToken || !credentials.from) {
22153
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "apiKey, from" } }));
22154
- }
22155
- const res = await fetch(`${RESEND_API}/domains`, {
22156
- headers: { Authorization: `Bearer ${apiToken}` }
22157
- });
22158
- if (!res.ok) {
22159
- throw new Error(deployM("deploy_apiError", { params: { service: "Resend", status: String(res.status) } }));
22160
- }
22161
- token = apiToken;
22162
- from = credentials.from;
22163
- logger10.info("Resend authenticated");
22164
- return ok("resend-user");
22165
- } catch (error62) {
22166
- logger10.error("Resend authentication failed", { error: error62 });
22167
- return err(
22168
- HaiDeployError.AUTH_FAILED,
22169
- deployM("deploy_authFailed", {
22170
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
22171
- }),
22172
- error62
22173
- );
22174
- }
22175
- },
22176
- async provision(_appName) {
22177
- if (!token) {
22178
- return err(
22179
- HaiDeployError.AUTH_REQUIRED,
22180
- deployM("deploy_authRequired")
22181
- );
22182
- }
22183
- logger10.debug("Provisioning Resend email service");
22184
- return ok({
22185
- serviceType: "email",
22186
- provisionerName: "resend",
22187
- resourceStatus: "reused",
22188
- envVars: {
22189
- HAI_REACH_PROVIDERS: JSON.stringify([{ name: "email", type: "smtp", host: "smtp.resend.com", port: 465, secure: true, user: "resend", pass: token, from }])
22190
- },
22191
- resourceInfo: "resend:verify-only"
22192
- });
22193
- }
22194
- };
22195
- }
22196
- var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
22197
- var UPSTASH_API = "https://api.upstash.com";
22198
- function createUpstashAuthHeader(email3, apiKey) {
22199
- return `Basic ${Buffer.from(`${email3}:${apiKey}`).toString("base64")}`;
22200
- }
22201
- async function upstashFetch(email3, apiKey, path, options) {
22202
- const response = await fetch(`${UPSTASH_API}${path}`, {
22203
- ...options,
22204
- headers: {
22205
- "Authorization": createUpstashAuthHeader(email3, apiKey),
22206
- "Content-Type": "application/json",
22207
- ...options?.headers
22208
- }
22209
- });
22210
- if (!response.ok) {
22211
- throw new Error(deployM("deploy_apiError", { params: { service: "Upstash", status: String(response.status) } }));
22212
- }
22213
- return response.json();
22214
- }
22215
- async function listUpstashDatabases(email3, apiKey) {
22216
- return upstashFetch(email3, apiKey, "/v2/redis/databases");
22217
- }
22218
- async function findExistingUpstashDatabase(email3, apiKey, databaseName) {
22219
- const databases = await listUpstashDatabases(email3, apiKey);
22220
- return databases.find((database) => database.database_name === databaseName) ?? null;
22221
- }
22222
- async function getUpstashDatabase(email3, apiKey, databaseId) {
22223
- return upstashFetch(email3, apiKey, `/v2/redis/database/${databaseId}`);
22224
- }
22225
- function buildUpstashProvisionResult(database, resourceStatus) {
22226
- const databaseId = database.database_id ?? "";
22227
- if (!databaseId || !database.endpoint || !database.password || !database.port) {
22228
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
22229
- }
22230
- return {
22231
- serviceType: "cache",
22232
- provisionerName: "upstash",
22233
- resourceStatus,
22234
- envVars: {
22235
- HAI_CACHE: JSON.stringify({ type: "redis", url: `rediss://default:${encodeURIComponent(database.password)}@${database.endpoint}:${database.port}` })
22236
- },
22237
- resourceInfo: `upstash-db:${databaseId}`
22238
- };
22239
- }
22240
- function createUpstashProvisioner() {
22241
- let apiKey = null;
22242
- let email3 = null;
22243
- return {
22244
- name: "upstash",
22245
- serviceType: "cache",
22246
- async authenticate(credentials) {
22247
- logger11.debug("Authenticating with Upstash");
22248
- try {
22249
- const userEmail = credentials.email ?? "";
22250
- const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
22251
- if (!userEmail || !userKey) {
22252
- throw new Error(deployM("deploy_credentialMissing", { params: { fields: "email, api_key" } }));
22253
- }
22254
- await listUpstashDatabases(userEmail, userKey);
22255
- email3 = userEmail;
22256
- apiKey = userKey;
22257
- logger11.info("Upstash authenticated", { email: userEmail });
22258
- return ok(userEmail);
22259
- } catch (error62) {
22260
- logger11.error("Upstash authentication failed", { error: error62 });
22261
- return err(
22262
- HaiDeployError.AUTH_FAILED,
22263
- deployM("deploy_authFailed", {
22264
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
22265
- }),
22266
- error62
22267
- );
22268
- }
22269
- },
22270
- async provision(appName) {
22271
- if (!apiKey || !email3) {
22272
- return err(
22273
- HaiDeployError.AUTH_REQUIRED,
22274
- deployM("deploy_authRequired")
22275
- );
22276
- }
22277
- logger11.debug("Provisioning Upstash Redis", { appName });
22278
- let resource;
22279
- try {
22280
- const databaseName = `${appName}-cache`;
22281
- const existingDatabase = await findExistingUpstashDatabase(email3, apiKey, databaseName);
22282
- if (existingDatabase !== null) {
22283
- const databaseId = existingDatabase.database_id ?? "";
22284
- if (!databaseId) {
22285
- throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
22286
- }
22287
- resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
22288
- const detail = await getUpstashDatabase(email3, apiKey, databaseId);
22289
- logger11.info("Upstash Redis reused", { databaseId, databaseName });
22290
- return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
22291
- }
22292
- const data = await upstashFetch(email3, apiKey, "/v2/redis/database", {
22293
- method: "POST",
22294
- body: JSON.stringify({
22295
- name: databaseName,
22296
- region: "global",
22297
- tls: true
22298
- })
22299
- });
22300
- if (data.database_id)
22301
- resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
22302
- logger11.info("Upstash Redis provisioned", { databaseId: data.database_id });
22303
- return ok(buildUpstashProvisionResult(data, "created"));
22304
- } catch (error62) {
22305
- logger11.error("Upstash provisioning failed", { appName });
22306
- if (resource) {
22307
- return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
22308
- projectName: appName,
22309
- stage: "provision:cache",
22310
- resources: [{ serviceType: "cache", provisionerName: "upstash", ...resource }]
22311
- });
22312
- }
22313
- return err(
22314
- HaiDeployError.PROVISION_FAILED,
22315
- deployM("deploy_provisionFailed", {
22316
- params: {
22317
- service: "upstash",
22318
- error: error62 instanceof Error ? error62.message : String(error62)
22319
- }
22320
- }),
22321
- error62
22322
- );
22323
- }
22324
- }
22325
- };
22326
- }
22327
-
22328
- // ../deploy/src/deploy-main.ts
22329
- var logger12 = core.logger.child({ module: "deploy", scope: "main" });
22330
- var currentConfig = null;
22331
- var currentProvider = null;
22332
- var currentContainerProvider = null;
22333
- var currentProvisioners = /* @__PURE__ */ new Map();
22334
- var initInProgress = false;
22335
- var notInitialized = core.module.createNotInitializedKit(
22336
- HaiDeployError.NOT_INITIALIZED,
22337
- () => deployM("deploy_notInitialized")
22338
- );
22339
- function createProviderByType(type) {
22340
- switch (type) {
22341
- case "vercel":
22342
- return createVercelProvider();
22343
- default:
22344
- throw new Error(deployM("deploy_unsupportedProviderType", { params: { type } }));
22345
- }
22346
- }
22347
- function createProvisionerByName(provisioner) {
22348
- switch (provisioner) {
22349
- case "neon":
22350
- return createNeonProvisioner();
22351
- case "upstash":
22352
- return createUpstashProvisioner();
22353
- case "cloudflare-r2":
22354
- return createR2Provisioner();
22355
- case "resend":
22356
- return createResendProvisioner();
22357
- case "aliyun":
22358
- return createAliyunProvisioner();
22359
- default:
22360
- throw new Error(deployM("deploy_unsupportedProvisionerType", { params: { type: provisioner } }));
22361
- }
22362
- }
22363
- function extractCredentials(serviceConfig) {
22364
- const result = {};
22365
- for (const [key, value] of Object.entries(serviceConfig)) {
22366
- if (key !== "provisioner" && typeof value === "string") {
22367
- result[key] = value;
22368
- }
22369
- }
22370
- return result;
22371
- }
22372
- var deployCredentials = {
22373
- getPath: () => getCredentialsPath(),
22374
- load: () => loadCredentials(),
22375
- save: (key, value) => saveCredential(key, value),
22376
- saveAll: (entries) => saveCredentials(entries)
22377
- };
22378
- function normalizeProjectName(name) {
22379
- const normalized = name.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
22380
- return normalized || "app";
22381
- }
22382
- function mergeProvisionEnv(results, scan, options) {
22383
- let env = {};
22384
- const reachRequirements = scan?.middlewares?.filter((item) => item.configName === "reach") ?? [];
22385
- const reachProviders = reachRequirements.map((item) => item.config);
22386
- let hasReachOutput = false;
22387
- for (const prov of results) {
22388
- if (prov.envVars.HAI_REACH_PROVIDERS) {
22389
- hasReachOutput = true;
22390
- const supplied = JSON.parse(prov.envVars.HAI_REACH_PROVIDERS);
22391
- for (const provider of supplied) {
22392
- const targets = reachRequirements.filter((item) => {
22393
- const mode = options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[item.service];
22394
- return item.service === prov.serviceType && (!mode || mode === "default");
22395
- });
22396
- if (targets.length) {
22397
- for (const target of targets)
22398
- reachProviders[reachRequirements.indexOf(target)] = { ...provider, name: target.config.name ?? provider.name };
22399
- } else {
22400
- reachProviders.push(provider);
22401
- }
22402
- }
22403
- }
22404
- env = { ...env, ...prov.envVars };
22405
- }
22406
- if (hasReachOutput)
22407
- env.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
22408
- return env;
22409
- }
22410
- var deploy = {
22411
- credentials: deployCredentials,
22412
- async init(config2) {
22413
- if (initInProgress) {
22414
- logger12.warn("Deploy init already in progress, skipping concurrent call");
22415
- return err(
22416
- HaiDeployError.CONFIG_ERROR,
22417
- deployM("deploy_configError", { params: { error: "init already in progress" } })
22418
- );
22419
- }
22420
- initInProgress = true;
22421
- try {
22422
- if (currentProvider || currentContainerProvider) {
22423
- logger12.warn("Deploy module is already initialized, reinitializing");
22424
- await deploy.close();
22425
- }
22426
- logger12.info("Initializing deploy module");
22427
- const parseResult = DeployConfigSchema.safeParse(config2);
22428
- if (!parseResult.success) {
22429
- logger12.error("Deploy config validation failed", { error: parseResult.error.message });
22430
- return err(
22431
- HaiDeployError.CONFIG_ERROR,
22432
- deployM("deploy_configError", { params: { error: parseResult.error.message } }),
22433
- parseResult.error
22434
- );
22435
- }
22436
- const parsed = parseResult.data;
22437
- try {
22438
- let provider = null;
22439
- let containerProvider = null;
22440
- if (parsed.provider.type === "docker-ssh") {
22441
- containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
22442
- logger12.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh?.host });
22443
- } else {
22444
- provider = createProviderByType(parsed.provider.type);
22445
- const authResult = await provider.authenticate(parsed.provider.token);
22446
- if (!authResult.success) {
22447
- return err(authResult.error);
22448
- }
22449
- logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
22450
- }
22451
- currentProvisioners.clear();
22452
- if (parsed.services) {
22453
- const serviceEntries = Object.entries(parsed.services);
22454
- for (const [serviceType, serviceConfig] of serviceEntries) {
22455
- if (!serviceConfig)
22456
- continue;
22457
- const provisioner = createProvisionerByName(serviceConfig.provisioner);
22458
- const credentials = extractCredentials(serviceConfig);
22459
- const provAuthResult = await provisioner.authenticate(credentials);
22460
- if (!provAuthResult.success) {
22461
- logger12.error("Provisioner authentication failed", {
22462
- service: serviceType,
22463
- provisioner: serviceConfig.provisioner
22464
- });
22465
- currentProvisioners.clear();
22466
- return err(provAuthResult.error);
22467
- }
22468
- logger12.info("Provisioner authenticated", {
22469
- service: serviceType,
22470
- user: provAuthResult.data
22471
- });
22472
- currentProvisioners.set(serviceType, provisioner);
22473
- }
22474
- }
22475
- currentProvider = provider;
22476
- currentContainerProvider = containerProvider;
22477
- currentConfig = parsed;
22478
- logger12.info("Deploy module initialized", {
22479
- provider: parsed.provider.type,
22480
- provisioners: [...currentProvisioners.keys()]
22481
- });
22482
- return ok(void 0);
22483
- } catch (error62) {
22484
- currentProvider = null;
22485
- currentContainerProvider = null;
22486
- currentConfig = null;
22487
- currentProvisioners.clear();
22488
- logger12.error("Deploy module initialization failed", { error: error62 });
22489
- return err(
22490
- HaiDeployError.CONFIG_ERROR,
22491
- deployM("deploy_configError", {
22492
- params: { error: error62 instanceof Error ? error62.message : String(error62) }
22493
- }),
22494
- error62
22495
- );
22496
- }
22497
- } finally {
22498
- initInProgress = false;
22499
- }
22500
- },
22501
- async close() {
22502
- if (!currentProvider && !currentContainerProvider) {
22503
- logger12.info("Deploy module already closed, skipping");
22504
- return;
22505
- }
22506
- logger12.info("Closing deploy module");
22507
- currentProvider = null;
22508
- currentContainerProvider = null;
22509
- currentConfig = null;
22510
- currentProvisioners.clear();
22511
- logger12.info("Deploy module closed");
22512
- },
22513
- get config() {
22514
- return currentConfig;
22515
- },
22516
- get isInitialized() {
22517
- return currentProvider !== null || currentContainerProvider !== null;
22518
- },
22519
- async scan(appDir, options) {
22520
- logger12.debug("Scanning application", { appDir });
22521
- return scanApp(appDir, options);
22522
- },
22523
- async provisionAll(projectName, services) {
22524
- if (!currentProvider && !currentContainerProvider) {
22525
- return notInitialized.result();
22526
- }
22527
- logger12.info("Provisioning all services", { projectName, count: currentProvisioners.size });
22528
- const results = [];
22529
- for (const [serviceType, provisioner] of currentProvisioners) {
22530
- if (services && !services.includes(serviceType))
22531
- continue;
22532
- logger12.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
22533
- const provResult = await provisioner.provision(projectName);
22534
- if (!provResult.success) {
22535
- logger12.error("Service provisioning failed", {
22536
- serviceType,
22537
- provisioner: provisioner.name,
22538
- code: provResult.error.code
22539
- });
22540
- return recoveryFailure(provResult.error, {
22541
- projectName,
22542
- stage: `provision:${serviceType}`,
22543
- resources: [
22544
- ...results.map(({ envVars: _envVars, ...resource }) => resource),
22545
- ...getDeployRecovery(provResult.error)?.resources ?? [
22546
- { serviceType, provisionerName: provisioner.name, resourceInfo: projectName, resourceStatus: "unknown" }
22547
- ]
22548
- ]
22549
- });
22550
- }
22551
- results.push(provResult.data);
22552
- logger12.info("Service provisioned", { serviceType, provisioner: provisioner.name });
22553
- }
22554
- return ok(results);
22555
- },
22556
- async deployApp(appDir, options) {
22557
- if (!currentProvider && !currentContainerProvider) {
22558
- return notInitialized.result();
22559
- }
22560
- logger12.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
22561
- const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
22562
- const scanResult = await scanApp(appDir, scanOptions);
22563
- if (!scanResult.success) {
22564
- return err(scanResult.error);
22565
- }
22566
- const scan = scanResult.data;
22567
- appDir = scan.appDir ?? appDir;
22568
- const environment = loadDeploymentEnvironment({ environment: scanOptions.environment ?? "prod", envDir: scan.envDir ?? appDir, requireFile: scanOptions.environment !== void 0 });
22569
- if (!environment.success)
22570
- return environment;
22571
- const appEnv = Object.fromEntries(Object.entries(environment.data.values).filter(([key]) => !key.startsWith("HAI_CLI_") && (!key.startsWith("HAI_DEPLOY_") || key === "HAI_DEPLOY_ENVIRONMENT")));
22572
- const projectName = `${options?.projectName ?? scan.appName}${scanOptions.environment ? `-${environment.data.environment}` : ""}`;
22573
- options = {
22574
- ...options,
22575
- ...scanOptions.environment ? { environment: environment.data.environment } : {},
22576
- ...options?.skipMiddleware ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
22577
- };
22578
- if (options.selectedServices) {
22579
- options.serviceModes = {
22580
- ...options.serviceModes,
22581
- ...Object.fromEntries(scan.requiredServices.filter((service) => !options.selectedServices.includes(service)).map((service) => [service, "keep"]))
22582
- };
22583
- }
22584
- if (currentContainerProvider) {
22585
- return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
22586
- }
22587
- if (options?.bundleOnly)
22588
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
22589
- if (options.selectedServices)
22590
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectedContainerOnly"));
22591
- if (!currentProvider) {
22592
- return notInitialized.result();
22593
- }
22594
- if (scan.isSvelteKit && !scan.adapterInstalled) {
22595
- return err(
22596
- HaiDeployError.ADAPTER_MISSING,
22597
- deployM("deploy_adapterMissing", {
22598
- params: { adapter: "@sveltejs/adapter-vercel" }
22599
- })
22600
- );
22601
- }
22602
- if (!existsSync(resolveOutputDir(appDir, "vercel")))
22603
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
22604
- let allEnvVars = appEnv;
22605
- const recovery = { projectName, stage: "provision", resources: [] };
22606
- if (!options?.skipMiddleware) {
22607
- const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
22608
- if (!provResults.success) {
22609
- return err(provResults.error);
22610
- }
22611
- recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
22612
- allEnvVars = { ...allEnvVars, ...mergeProvisionEnv(provResults.data, scan, options) };
22613
- if (options?.environment) {
22614
- const saved = saveDeploymentEnvironment(environment.data, mergeProvisionEnv(provResults.data, scan, options));
22615
- if (!saved.success)
22616
- return recoveryFailure(saved.error, { ...recovery, stage: "save-env" });
22617
- }
22618
- }
22619
- const projectResult = await currentProvider.createProject(projectName);
22620
- if (!projectResult.success) {
22621
- return recoveryFailure(projectResult.error, { ...recovery, stage: "create-project" });
22622
- }
22623
- const projectId = projectResult.data;
22624
- recovery.platformProjectId = projectId;
22625
- if (Object.keys(allEnvVars).length > 0) {
22626
- const envResult = await currentProvider.setEnvVars(projectId, allEnvVars);
22627
- if (!envResult.success) {
22628
- return recoveryFailure(envResult.error, { ...recovery, stage: "set-env" });
22629
- }
22630
- }
22631
- const outputDir = resolveOutputDir(appDir, currentConfig?.provider.type ?? "vercel");
22632
- const deployResult = await currentProvider.deploy(projectId, outputDir);
22633
- if (!deployResult.success) {
22634
- return recoveryFailure(deployResult.error, { ...recovery, stage: "deploy" });
22635
- }
22636
- const result = {
22637
- ...deployResult.data,
22638
- envVarsSet: Object.keys(allEnvVars)
22639
- };
22640
- logger12.info("Deployment completed", {
22641
- url: result.url,
22642
- deploymentId: result.deploymentId,
22643
- status: result.status
22644
- });
22645
- return ok(result);
22646
- }
22647
- };
22648
- async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
22649
- const projectName = normalizeProjectName(rawProjectName);
22650
- let artifact;
22651
- let artifactDir = options?.artifactDir ?? resolveArtifactDirectory(appDir);
22652
- try {
22653
- if (existsSync(join(artifactDir, "latest"))) {
22654
- const latest = readFileSync(join(artifactDir, "latest"), "utf8").trim();
22655
- if (!/^[a-z0-9][\w.+-]*$/i.test(latest))
22656
- throw new Error("Invalid artifact pointer");
22657
- artifactDir = join(artifactDir, latest);
22658
- }
22659
- artifact = await readDeploymentArtifact(artifactDir);
22660
- } catch {
22661
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
22662
- }
22663
- if (artifact.partial && !options?.selectedServices) {
22664
- const selectedServices = artifact.images.map((image) => image.name);
22665
- options = {
22666
- ...options,
22667
- selectedServices,
22668
- serviceModes: {
22669
- ...options?.serviceModes,
22670
- ...Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"]))
22671
- }
22672
- };
22673
- }
22674
- if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
22675
- return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
22676
- let provisionEnv = {};
22677
- if (!options?.skipMiddleware) {
22678
- const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
22679
- if (!provResults.success) {
22680
- return err(provResults.error);
22681
- }
22682
- provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
22683
- }
22684
- return provider.deploy({ appDir, artifact, artifactDir, projectName, scan, provisionEnv, appEnv, options });
22685
- }
22686
-
22687
- // ../deploy/src/deploy-middleware.ts
22688
- var field = (name, defaultValue, kind, secret = false) => ({ name, defaultValue, kind, secret });
22689
- var required2 = (name, defaultValue, kind) => ({ ...field(name, defaultValue, kind), required: true });
22690
- var DEPLOY_MIDDLEWARE = {
22691
- db: [
22692
- { backend: "sqlite", label: "SQLite", purpose: "deploy_purposeSqlite", fields: [required2("database", "./data/app.db")] },
22693
- ...["postgresql", "mysql"].map((backend) => ({
22694
- backend,
22695
- label: backend === "mysql" ? "MySQL" : "PostgreSQL",
22696
- purpose: "deploy_purposeDatabase",
22697
- provisioner: backend === "postgresql" ? "neon" : void 0,
22698
- 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")]
22699
- }))
22700
- ],
22701
- cache: [
22702
- { backend: "memory", label: "Memory", purpose: "deploy_purposeMemory", fields: [] },
22703
- { 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")] }
22704
- ],
22705
- storage: [
22706
- { backend: "local", label: "Local filesystem", purpose: "deploy_purposeLocal", fields: [required2("root", "./data/storage")] },
22707
- { 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")] }
22708
- ],
22709
- vecdb: [
22710
- { backend: "lancedb", label: "LanceDB", purpose: "deploy_purposeLocal", fields: [required2("path", "./data/vectors")] },
22711
- { backend: "qdrant", label: "Qdrant", purpose: "deploy_purposeVector", fields: [required2("url", "http://localhost:6333", "url"), field("apiKey", "", void 0, true)] },
22712
- { 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)] },
22713
- { backend: "chroma", label: "Chroma", purpose: "deploy_purposeVector", fields: [required2("url", "http://localhost:8000", "url")] }
22714
- ],
22715
- ldap: [
22716
- { backend: "disabled", label: "Disabled", purpose: "deploy_purposeDisabled", fields: [] },
22717
- { 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 }] }
22718
- ],
22719
- email: [
22720
- { backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
22721
- { 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")] },
22722
- { backend: "api", label: "HTTP notification API", purpose: "deploy_purposeEmail", fields: [required2("url", "", "url"), field("method", "POST"), field("headers"), field("timeout", "10000", "number")] }
22723
- ],
22724
- sms: [
22725
- { 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")] }
22726
- ]
22727
- };
22728
- function describeMiddlewareBackend(backend) {
22729
- return deployM(backend.purpose);
22730
- }
22731
-
22732
- export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, readDeploymentArtifact, saveDeploymentEnvironment };
22733
- //# sourceMappingURL=src-2V6VP4I5.js.map
22734
- //# sourceMappingURL=src-2V6VP4I5.js.map
19661
+ export { external_exports };
19662
+ //# sourceMappingURL=chunk-FI56GONY.js.map
19663
+ //# sourceMappingURL=chunk-FI56GONY.js.map