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