@h-ai/cli 0.1.0-alpha.66 → 0.1.0-alpha.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +86 -241
- package/README.ja.md +86 -241
- package/README.md +86 -241
- package/dist/{src-RDQ2EIBT.js → chunk-FI56GONY.js} +32 -2669
- package/dist/chunk-FI56GONY.js.map +1 -0
- package/dist/chunk-MBPB37FR.js +1348 -0
- package/dist/chunk-MBPB37FR.js.map +1 -0
- package/dist/{chunk-QYX6V7H2.js → chunk-ZFNBUF77.js} +347 -276
- package/dist/chunk-ZFNBUF77.js.map +1 -0
- package/dist/cli-config-templates-W2LLPMB6.js +5 -0
- package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-W2LLPMB6.js.map} +1 -1
- package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-US5CCAJ4.js} +3 -3
- package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-US5CCAJ4.js.map} +1 -1
- package/dist/index.js +1304 -532
- package/dist/index.js.map +1 -1
- package/dist/metafile-esm.json +1 -0
- package/dist/src-DSJJXM2F.js +3849 -0
- package/dist/src-DSJJXM2F.js.map +1 -0
- package/package.json +9 -8
- package/templates/README.md +48 -145
- package/templates/apps/admin/messages/ja-JP.json +38 -0
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -6
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +3 -3
- package/templates/apps/api/.dockerignore.hbs +14 -7
- package/templates/apps/api/.gitignore.hbs +7 -0
- package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +4 -1
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +33 -8
- package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +0 -10
- package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +2 -1
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
- package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +24 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +8 -1
- package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +68 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +10 -2
- package/templates/apps/api/e2e/api-service.spec.ts.hbs +2 -2
- package/templates/apps/api/hai.build.json.hbs +1 -1
- package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
- package/templates/apps/api/package.json.hbs +6 -1
- package/templates/apps/api/playwright.config.ts.hbs +15 -7
- package/templates/apps/fullstack/.dockerignore.hbs +14 -9
- package/templates/apps/fullstack/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +7 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +23 -5
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +30 -9
- package/templates/apps/fullstack/hai.build.json.hbs +2 -2
- package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
- package/templates/apps/fullstack/package.json.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +32 -8
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +0 -6
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +2 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +9 -5
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +3 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +16 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +7 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +58 -7
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +13 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +40 -14
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +105 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +6 -6
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +7 -7
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +4 -4
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +8 -8
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +13 -14
- package/templates/apps/fullstack/playwright.config.ts.hbs +15 -6
- package/templates/apps/h5/messages/ja-JP.json +40 -0
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +16 -5
- package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
- package/templates/apps/mobile-app/src/App.svelte.hbs +15 -5
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +173 -14
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
- package/templates/apps/website/messages/ja-JP.json +32 -0
- package/templates/apps/website/src/routes/+layout.svelte.hbs +13 -5
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
- package/templates/base/.dockerignore.hbs +14 -8
- package/templates/base/.gitignore.hbs +7 -0
- package/templates/base/e2e/app.spec.ts.hbs +23 -8
- package/templates/base/hai.build.json.hbs +1 -1
- package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
- package/templates/base/package.json.hbs +11 -3
- package/templates/base/playwright.config.ts +15 -4
- package/templates/base/src/hooks.server.ts.hbs +15 -6
- package/templates/base/src/lib/server/init.ts.hbs +139 -30
- package/templates/base/vite.config.ts.hbs +5 -3
- package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +11 -6
- package/templates/features/payment/routes/api/v1/payment/create/+server.ts +18 -25
- package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +4 -0
- package/templates/i18n/project.inlang/settings.json +1 -1
- package/templates/shared/.gitattributes +6 -0
- package/templates/shared/e2e-env.ts +98 -0
- package/templates/shared/run-e2e.mjs +51 -12
- package/templates/skills/AGENTS.md +7 -3
- package/templates/skills/bridges/admin/AGENTS.md +6 -2
- package/templates/skills/bridges/api/AGENTS.md +3 -1
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +5 -1
- package/templates/skills/bridges/h5/AGENTS.md +6 -2
- package/templates/skills/bridges/mobile-app/AGENTS.md +6 -2
- package/templates/skills/bridges/website/AGENTS.md +6 -2
- package/templates/skills/hai-ai/SKILL.md +8 -2
- package/templates/skills/hai-ai/guide.md +134 -60
- package/templates/skills/hai-ai/reference.md +51 -11
- package/templates/skills/hai-api-client/SKILL.md +15 -160
- package/templates/skills/hai-api-client/reference.md +178 -0
- package/templates/skills/hai-api-contract/SKILL.md +16 -273
- package/templates/skills/hai-api-contract/reference.md +280 -0
- package/templates/skills/hai-app-create/SKILL.md +6 -2
- package/templates/skills/hai-app-create/development.md +24 -8
- package/templates/skills/hai-app-create/reference.md +7 -5
- package/templates/skills/hai-app-review/SKILL.md +7 -3
- package/templates/skills/hai-app-review/differential.md +34 -0
- package/templates/skills/hai-app-tests/SKILL.md +6 -3
- package/templates/skills/hai-app-tests/reference.md +5 -5
- package/templates/skills/hai-app-tests/validation.md +38 -0
- package/templates/skills/hai-audit/SKILL.md +24 -147
- package/templates/skills/hai-audit/reference.md +148 -0
- package/templates/skills/hai-build/SKILL.md +14 -8
- package/templates/skills/hai-build/reference.md +16 -13
- package/templates/skills/hai-cache/SKILL.md +20 -181
- package/templates/skills/hai-cache/reference.md +229 -0
- package/templates/skills/hai-capacitor/SKILL.md +7 -4
- package/templates/skills/hai-capacitor/reference.md +15 -9
- package/templates/skills/hai-ci/SKILL.md +2 -0
- package/templates/skills/hai-core/SKILL.md +7 -3
- package/templates/skills/hai-core/reference.md +25 -5
- package/templates/skills/hai-crypto/SKILL.md +22 -250
- package/templates/skills/hai-crypto/reference.md +310 -0
- package/templates/skills/hai-datapipe/SKILL.md +16 -195
- package/templates/skills/hai-datapipe/reference.md +204 -0
- package/templates/skills/hai-deploy/SKILL.md +24 -54
- package/templates/skills/hai-deploy/middleware.md +51 -43
- package/templates/skills/hai-framework-sync/SKILL.md +2 -2
- package/templates/skills/hai-framework-sync/sync.md +1 -1
- package/templates/skills/hai-iam/SKILL.md +25 -11
- package/templates/skills/hai-iam/reference.md +181 -154
- package/templates/skills/hai-kit/SKILL.md +35 -174
- package/templates/skills/hai-kit/reference.md +190 -0
- package/templates/skills/hai-payment/SKILL.md +33 -263
- package/templates/skills/hai-payment/reference.md +295 -0
- package/templates/skills/hai-pr-review/SKILL.md +21 -88
- package/templates/skills/hai-reach/SKILL.md +6 -3
- package/templates/skills/hai-reach/reference.md +83 -22
- package/templates/skills/hai-reldb/SKILL.md +6 -3
- package/templates/skills/hai-reldb/reference.md +68 -25
- package/templates/skills/hai-scheduler/SKILL.md +18 -195
- package/templates/skills/hai-scheduler/reference.md +216 -0
- package/templates/skills/hai-serv/SKILL.md +25 -10
- package/templates/skills/hai-serv/reference.md +123 -87
- package/templates/skills/hai-spec-implement/SKILL.md +5 -1
- package/templates/skills/hai-spec-implement/evaluation.md +18 -0
- package/templates/skills/hai-spec-implement/handoff.md +47 -0
- package/templates/skills/hai-spec-implement/templates.md +5 -0
- package/templates/skills/hai-spec-implement/workflow.md +5 -3
- package/templates/skills/hai-spec-review/SKILL.md +4 -0
- package/templates/skills/hai-storage/SKILL.md +23 -238
- package/templates/skills/hai-storage/reference.md +282 -0
- package/templates/skills/hai-ui/SKILL.md +17 -3
- package/templates/skills/hai-ui/guide.md +3 -3
- package/templates/skills/hai-ui/reference.md +26 -11
- package/templates/skills/hai-ui-design/SKILL.md +31 -0
- package/templates/skills/hai-ui-design/patterns.md +26 -0
- package/templates/skills/hai-ui-review/SKILL.md +30 -0
- package/templates/skills/hai-ui-review/checklist.md +22 -0
- package/templates/skills/hai-vecdb/SKILL.md +14 -10
- package/templates/skills/hai-vecdb/reference.md +33 -3
- package/dist/chunk-FWN5B54L.js +0 -468
- package/dist/chunk-FWN5B54L.js.map +0 -1
- package/dist/chunk-QYX6V7H2.js.map +0 -1
- package/dist/cli-config-templates-3XSFIXBP.js +0 -5
- package/dist/src-RDQ2EIBT.js.map +0 -1
- package/templates/apps/api/Dockerfile.hbs +0 -35
- package/templates/apps/fullstack/Dockerfile.hbs +0 -57
- /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
- /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { __export } from './chunk-2H7UOFLK.js';
|
|
3
|
-
import { createHash, randomBytes } from 'crypto';
|
|
4
|
-
import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, 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';
|
|
10
|
-
import { homedir } from 'os';
|
|
11
|
-
import { spawn } from 'child_process';
|
|
12
|
-
import { Buffer } from 'buffer';
|
|
13
3
|
|
|
14
4
|
// ../../node_modules/zod/v4/classic/external.js
|
|
15
5
|
var external_exports = {};
|
|
@@ -1398,10 +1388,10 @@ function defineLazyInternal(inst, key, compute) {
|
|
|
1398
1388
|
Object.defineProperty(this, key, { configurable: true, writable: true, value });
|
|
1399
1389
|
broke = broke || outer;
|
|
1400
1390
|
return value;
|
|
1401
|
-
} catch (
|
|
1391
|
+
} catch (err) {
|
|
1402
1392
|
delete this[key];
|
|
1403
1393
|
broke = broke || outer;
|
|
1404
|
-
throw
|
|
1394
|
+
throw err;
|
|
1405
1395
|
}
|
|
1406
1396
|
},
|
|
1407
1397
|
set(value) {
|
|
@@ -1856,18 +1846,18 @@ var validateAsync = async (schema, value, _ctx) => {
|
|
|
1856
1846
|
return result.issues.length === 0;
|
|
1857
1847
|
};
|
|
1858
1848
|
var _encode = (_Err) => {
|
|
1859
|
-
const
|
|
1849
|
+
const parse3 = _parse(_Err);
|
|
1860
1850
|
const fn = (schema, value, _ctx, _params) => {
|
|
1861
1851
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
1862
|
-
return
|
|
1852
|
+
return parse3(schema, value, ctx, finalizeParams(fn, _params));
|
|
1863
1853
|
};
|
|
1864
1854
|
return fn;
|
|
1865
1855
|
};
|
|
1866
1856
|
var encode = /* @__PURE__ */ _encode($ZodRealError);
|
|
1867
1857
|
var _decode = (_Err) => {
|
|
1868
|
-
const
|
|
1858
|
+
const parse3 = _parse(_Err);
|
|
1869
1859
|
const fn = (schema, value, _ctx, _params) => {
|
|
1870
|
-
return
|
|
1860
|
+
return parse3(schema, value, _ctx, finalizeParams(fn, _params));
|
|
1871
1861
|
};
|
|
1872
1862
|
return fn;
|
|
1873
1863
|
};
|
|
@@ -3521,14 +3511,14 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
3521
3511
|
const shape = zod.def.shape;
|
|
3522
3512
|
const propValues = {};
|
|
3523
3513
|
for (const key in shape) {
|
|
3524
|
-
const
|
|
3525
|
-
if (
|
|
3514
|
+
const field = shape[key]._zod;
|
|
3515
|
+
if (field.values) {
|
|
3526
3516
|
if (!Object.prototype.hasOwnProperty.call(propValues, key)) {
|
|
3527
3517
|
assignProp(propValues, key, /* @__PURE__ */ new Set());
|
|
3528
3518
|
}
|
|
3529
|
-
for (const v of
|
|
3519
|
+
for (const v of field.values)
|
|
3530
3520
|
propValues[key].add(v);
|
|
3531
|
-
if (
|
|
3521
|
+
if (field.optin !== void 0)
|
|
3532
3522
|
propValues[key].add(void 0);
|
|
3533
3523
|
}
|
|
3534
3524
|
}
|
|
@@ -5037,7 +5027,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
5037
5027
|
var NONE = 0;
|
|
5038
5028
|
var ASSUMED = 1;
|
|
5039
5029
|
var PROVEN = 2;
|
|
5040
|
-
function isRecursive(inst, stack,
|
|
5030
|
+
function isRecursive(inst, stack, resolve) {
|
|
5041
5031
|
const cached2 = recursive.get(inst);
|
|
5042
5032
|
if (cached2 !== void 0)
|
|
5043
5033
|
return cached2 ? PROVEN : NONE;
|
|
@@ -5047,7 +5037,7 @@ function isRecursive(inst, stack, resolve6) {
|
|
|
5047
5037
|
let result = NONE;
|
|
5048
5038
|
const check2 = (child) => {
|
|
5049
5039
|
if (result !== PROVEN && child?._zod) {
|
|
5050
|
-
const answer = isRecursive(child, stack,
|
|
5040
|
+
const answer = isRecursive(child, stack, resolve);
|
|
5051
5041
|
if (answer > result)
|
|
5052
5042
|
result = answer;
|
|
5053
5043
|
}
|
|
@@ -5058,7 +5048,7 @@ function isRecursive(inst, stack, resolve6) {
|
|
|
5058
5048
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
5059
5049
|
if (!desc.enumerable)
|
|
5060
5050
|
continue;
|
|
5061
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
5051
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve) : NONE;
|
|
5062
5052
|
if (child > answer)
|
|
5063
5053
|
answer = child;
|
|
5064
5054
|
}
|
|
@@ -5122,7 +5112,7 @@ function isRecursive(inst, stack, resolve6) {
|
|
|
5122
5112
|
break;
|
|
5123
5113
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
5124
5114
|
case "lazy": {
|
|
5125
|
-
const inner = def._cachedInner ?? (
|
|
5115
|
+
const inner = def._cachedInner ?? (resolve ? inst._zod.innerType : void 0);
|
|
5126
5116
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
5127
5117
|
break;
|
|
5128
5118
|
}
|
|
@@ -12940,9 +12930,9 @@ function compile(schema, options) {
|
|
|
12940
12930
|
const clone2 = withParser(schema, parser);
|
|
12941
12931
|
clone2._zod.bag.validator = compileValidator(schema, parser);
|
|
12942
12932
|
return clone2;
|
|
12943
|
-
} catch (
|
|
12933
|
+
} catch (err) {
|
|
12944
12934
|
if (options?.strict)
|
|
12945
|
-
throw
|
|
12935
|
+
throw err;
|
|
12946
12936
|
return schema;
|
|
12947
12937
|
}
|
|
12948
12938
|
}
|
|
@@ -13032,8 +13022,8 @@ ${code}
|
|
|
13032
13022
|
try {
|
|
13033
13023
|
const factory = new F(...constantNames, factoryCode);
|
|
13034
13024
|
fn = factory(...constantValues);
|
|
13035
|
-
} catch (
|
|
13036
|
-
throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${
|
|
13025
|
+
} catch (err) {
|
|
13026
|
+
throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${err.message})`);
|
|
13037
13027
|
}
|
|
13038
13028
|
if (options?.debug) {
|
|
13039
13029
|
fn.code = fullCode;
|
|
@@ -13071,9 +13061,9 @@ function compileChild(doc, ctx, schema, accessor, needsValue = true) {
|
|
|
13071
13061
|
const varCounter = ctx.varCounter;
|
|
13072
13062
|
try {
|
|
13073
13063
|
return generateCheck(doc, ctx, schema, accessor, needsValue);
|
|
13074
|
-
} catch (
|
|
13075
|
-
if (!(
|
|
13076
|
-
throw
|
|
13064
|
+
} catch (err) {
|
|
13065
|
+
if (!(err instanceof ZodCompileUnsupportedError) || !err.islandable)
|
|
13066
|
+
throw err;
|
|
13077
13067
|
doc.content.length = contentLen;
|
|
13078
13068
|
if (ctx.constants.size > constantCount) {
|
|
13079
13069
|
const trailing = Array.from(ctx.constants.keys()).slice(constantCount);
|
|
@@ -15807,7 +15797,7 @@ function foldObjects(members2) {
|
|
|
15807
15797
|
objects.push(member);
|
|
15808
15798
|
}
|
|
15809
15799
|
const properties = {};
|
|
15810
|
-
const
|
|
15800
|
+
const required2 = /* @__PURE__ */ new Set();
|
|
15811
15801
|
for (const object2 of objects) {
|
|
15812
15802
|
for (const key in object2.properties) {
|
|
15813
15803
|
if (Object.prototype.hasOwnProperty.call(properties, key))
|
|
@@ -15824,11 +15814,11 @@ function foldObjects(members2) {
|
|
|
15824
15814
|
assignProp(properties, key, merged);
|
|
15825
15815
|
}
|
|
15826
15816
|
for (const key of object2.required ?? [])
|
|
15827
|
-
|
|
15817
|
+
required2.add(key);
|
|
15828
15818
|
}
|
|
15829
15819
|
const folded = { type: "object", properties };
|
|
15830
|
-
if (
|
|
15831
|
-
folded.required = [...
|
|
15820
|
+
if (required2.size)
|
|
15821
|
+
folded.required = [...required2];
|
|
15832
15822
|
if (objects.every((object2) => object2.additionalProperties === false)) {
|
|
15833
15823
|
folded.additionalProperties = false;
|
|
15834
15824
|
} else {
|
|
@@ -16446,8 +16436,8 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
16446
16436
|
}
|
|
16447
16437
|
const requiredKeys = [];
|
|
16448
16438
|
for (const key of Object.keys(shape)) {
|
|
16449
|
-
const
|
|
16450
|
-
if (ctx.io === "input" ? inputOptin(
|
|
16439
|
+
const field = def.shape[key];
|
|
16440
|
+
if (ctx.io === "input" ? inputOptin(field) === void 0 : field._zod.optout === void 0) {
|
|
16451
16441
|
requiredKeys.push(key);
|
|
16452
16442
|
}
|
|
16453
16443
|
}
|
|
@@ -16605,8 +16595,8 @@ function rewriteKeyNames(ctx) {
|
|
|
16605
16595
|
bySchema.set(entry.schema, entry);
|
|
16606
16596
|
}
|
|
16607
16597
|
const rewrites = /* @__PURE__ */ new Map();
|
|
16608
|
-
for (const
|
|
16609
|
-
const seen = ctx.seen.get(
|
|
16598
|
+
for (const record2 of pendingRecords.get(ctx) ?? []) {
|
|
16599
|
+
const seen = ctx.seen.get(record2);
|
|
16610
16600
|
const names = (seen?.def ?? seen?.schema)?.propertyNames;
|
|
16611
16601
|
if (!names || names === true || rewrites.has(names))
|
|
16612
16602
|
continue;
|
|
@@ -19668,2633 +19658,6 @@ function date4(params) {
|
|
|
19668
19658
|
return _coercedDate(ZodDate, params);
|
|
19669
19659
|
}
|
|
19670
19660
|
|
|
19671
|
-
|
|
19672
|
-
|
|
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
|
-
|
|
19709
|
-
// ../deploy/messages/en-US.json
|
|
19710
|
-
var en_US_default = {
|
|
19711
|
-
deploy_notInitialized: "Deploy module not initialized, call deploy.init() first",
|
|
19712
|
-
deploy_initFailed: "Deploy module initialization failed: {error}",
|
|
19713
|
-
deploy_unsupportedProviderType: "Unsupported deploy provider type: {type}",
|
|
19714
|
-
deploy_unsupportedProvisionerType: "Unsupported service provisioner type: {type}",
|
|
19715
|
-
deploy_configTokenRequired: "Deploy provider token is required",
|
|
19716
|
-
deploy_authRequired: "Authentication required",
|
|
19717
|
-
deploy_authFailed: "Authentication failed: {error}",
|
|
19718
|
-
deploy_projectCreateFailed: "Project creation failed: {error}",
|
|
19719
|
-
deploy_uploadFailed: "Build output upload failed: {error}",
|
|
19720
|
-
deploy_deployFailed: "Deployment failed: {error}",
|
|
19721
|
-
deploy_provisionFailed: "Service provisioning failed: {service} - {error}",
|
|
19722
|
-
deploy_adapterMissing: "Target platform adapter not found, please install {adapter}",
|
|
19723
|
-
deploy_scanFailed: "Application scan failed: {error}",
|
|
19724
|
-
deploy_envVarFailed: "Environment variable setup failed: {error}",
|
|
19725
|
-
deploy_credentialError: "Credential read/write failed: {error}",
|
|
19726
|
-
deploy_credentialInvalidKey: "Unsupported credential key: {key}. Only HAI_DEPLOY_* keys are allowed",
|
|
19727
|
-
deploy_credentialSaved: "Credentials saved",
|
|
19728
|
-
deploy_credentialLoaded: "Credentials loaded",
|
|
19729
|
-
deploy_configError: "Deploy config error: {error}",
|
|
19730
|
-
deploy_credentialMissing: "Missing credential fields: {fields}",
|
|
19731
|
-
deploy_apiError: "{service} API error ({status})",
|
|
19732
|
-
deploy_fileUploadFailed: "File upload failed ({status})",
|
|
19733
|
-
deploy_provisionNoResult: "No connection info returned from {service}",
|
|
19734
|
-
deploy_recoveryFailure: "Deployment stopped at {stage}; inspect the recovery details.",
|
|
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.",
|
|
19736
|
-
deploy_commandTimeout: "Command timed out: {command}",
|
|
19737
|
-
deploy_commandFailed: "Command failed ({command}): {error}",
|
|
19738
|
-
deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
|
|
19739
|
-
deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
|
|
19740
|
-
deploy_imageTransferFailed: "Image transfer failed: {error}",
|
|
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."
|
|
19765
|
-
};
|
|
19766
|
-
|
|
19767
|
-
// ../deploy/messages/zh-CN.json
|
|
19768
|
-
var zh_CN_default2 = {
|
|
19769
|
-
deploy_notInitialized: "Deploy \u6A21\u5757\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 deploy.init()",
|
|
19770
|
-
deploy_initFailed: "Deploy \u6A21\u5757\u521D\u59CB\u5316\u5931\u8D25\uFF1A{error}",
|
|
19771
|
-
deploy_unsupportedProviderType: "\u4E0D\u652F\u6301\u7684\u90E8\u7F72\u5E73\u53F0\u7C7B\u578B\uFF1A{type}",
|
|
19772
|
-
deploy_unsupportedProvisionerType: "\u4E0D\u652F\u6301\u7684\u670D\u52A1 Provisioner \u7C7B\u578B\uFF1A{type}",
|
|
19773
|
-
deploy_configTokenRequired: "\u90E8\u7F72\u5E73\u53F0 Token \u4E0D\u80FD\u4E3A\u7A7A",
|
|
19774
|
-
deploy_authRequired: "\u8BF7\u5148\u5B8C\u6210\u8BA4\u8BC1",
|
|
19775
|
-
deploy_authFailed: "\u8BA4\u8BC1\u5931\u8D25\uFF1A{error}",
|
|
19776
|
-
deploy_projectCreateFailed: "\u9879\u76EE\u521B\u5EFA\u5931\u8D25\uFF1A{error}",
|
|
19777
|
-
deploy_uploadFailed: "\u6784\u5EFA\u4EA7\u7269\u4E0A\u4F20\u5931\u8D25\uFF1A{error}",
|
|
19778
|
-
deploy_deployFailed: "\u90E8\u7F72\u5931\u8D25\uFF1A{error}",
|
|
19779
|
-
deploy_provisionFailed: "\u57FA\u7840\u8BBE\u65BD\u5F00\u901A\u5931\u8D25\uFF1A{service} - {error}",
|
|
19780
|
-
deploy_adapterMissing: "\u672A\u68C0\u6D4B\u5230\u76EE\u6807\u5E73\u53F0 adapter\uFF0C\u8BF7\u5B89\u88C5 {adapter}",
|
|
19781
|
-
deploy_scanFailed: "\u5E94\u7528\u626B\u63CF\u5931\u8D25\uFF1A{error}",
|
|
19782
|
-
deploy_envVarFailed: "\u73AF\u5883\u53D8\u91CF\u8BBE\u7F6E\u5931\u8D25\uFF1A{error}",
|
|
19783
|
-
deploy_credentialError: "\u51ED\u8BC1\u8BFB\u5199\u5931\u8D25\uFF1A{error}",
|
|
19784
|
-
deploy_credentialInvalidKey: "\u4E0D\u652F\u6301\u7684\u51ED\u8BC1\u952E\u540D\uFF1A{key}\u3002\u4EC5\u5141\u8BB8 HAI_DEPLOY_*",
|
|
19785
|
-
deploy_credentialSaved: "\u51ED\u8BC1\u5DF2\u4FDD\u5B58",
|
|
19786
|
-
deploy_credentialLoaded: "\u51ED\u8BC1\u5DF2\u52A0\u8F7D",
|
|
19787
|
-
deploy_configError: "\u90E8\u7F72\u914D\u7F6E\u9519\u8BEF\uFF1A{error}",
|
|
19788
|
-
deploy_credentialMissing: "\u7F3A\u5C11\u51ED\u8BC1\u5B57\u6BB5\uFF1A{fields}",
|
|
19789
|
-
deploy_apiError: "{service} API \u8BF7\u6C42\u5931\u8D25\uFF08{status}\uFF09",
|
|
19790
|
-
deploy_fileUploadFailed: "\u6587\u4EF6\u4E0A\u4F20\u5931\u8D25\uFF08{status}\uFF09",
|
|
19791
|
-
deploy_provisionNoResult: "{service} \u672A\u8FD4\u56DE\u8FDE\u63A5\u4FE1\u606F",
|
|
19792
|
-
deploy_recoveryFailure: "\u90E8\u7F72\u5728 {stage} \u9636\u6BB5\u505C\u6B62\uFF0C\u8BF7\u67E5\u770B\u6062\u590D\u6E05\u5355\u3002",
|
|
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",
|
|
19794
|
-
deploy_commandTimeout: "\u547D\u4EE4\u6267\u884C\u8D85\u65F6\uFF1A{command}",
|
|
19795
|
-
deploy_commandFailed: "\u547D\u4EE4\u6267\u884C\u5931\u8D25\uFF08{command}\uFF09\uFF1A{error}",
|
|
19796
|
-
deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
|
|
19797
|
-
deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
|
|
19798
|
-
deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
|
|
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"
|
|
19823
|
-
};
|
|
19824
|
-
|
|
19825
|
-
// ../deploy/src/deploy-i18n.ts
|
|
19826
|
-
var deployM = core.i18n.createMessageGetter({
|
|
19827
|
-
"zh-CN": zh_CN_default2,
|
|
19828
|
-
"en-US": en_US_default
|
|
19829
|
-
});
|
|
19830
|
-
|
|
19831
|
-
// ../deploy/src/deploy-config.ts
|
|
19832
|
-
var VercelProviderConfigSchema = external_exports.object({
|
|
19833
|
-
type: external_exports.literal("vercel"),
|
|
19834
|
-
token: external_exports.string().min(1, deployM("deploy_configTokenRequired"))
|
|
19835
|
-
});
|
|
19836
|
-
var SshConfigSchema = external_exports.object({
|
|
19837
|
-
/** 远程主机地址或 ~/.ssh/config 中的 Host 别名 */
|
|
19838
|
-
host: external_exports.string().min(1),
|
|
19839
|
-
/** SSH 端口 */
|
|
19840
|
-
port: external_exports.number().int().min(1).max(65535).default(22),
|
|
19841
|
-
/** SSH 用户名 */
|
|
19842
|
-
username: external_exports.string().min(1),
|
|
19843
|
-
/** 私钥文件路径(省略时使用 ssh-agent 或 ~/.ssh 默认密钥) */
|
|
19844
|
-
identityFile: external_exports.string().min(1).optional(),
|
|
19845
|
-
/** known_hosts 文件路径(省略时使用默认;配合 CI/临时环境隔离主机指纹) */
|
|
19846
|
-
knownHostsFile: external_exports.string().min(1).optional(),
|
|
19847
|
-
/** 严格主机密钥校验,默认开启(true→accept-new 首次信任并拒绝变更;false→关闭校验) */
|
|
19848
|
-
strictHostKeyChecking: external_exports.boolean().default(true),
|
|
19849
|
-
/** 连接超时(秒) */
|
|
19850
|
-
connectTimeoutSeconds: external_exports.number().int().positive().default(10)
|
|
19851
|
-
});
|
|
19852
|
-
var PortExposeSchema = external_exports.object({
|
|
19853
|
-
type: external_exports.literal("port").default("port"),
|
|
19854
|
-
/** 容器内监听端口(adapter-node 默认 3000) */
|
|
19855
|
-
containerPort: external_exports.number().int().min(1).max(65535).default(3e3),
|
|
19856
|
-
/** 宿主机映射端口 */
|
|
19857
|
-
hostPort: external_exports.number().int().min(1).max(65535)
|
|
19858
|
-
});
|
|
19859
|
-
var ExposeSchema = PortExposeSchema;
|
|
19860
|
-
var DockerSshProviderConfigSchema = external_exports.object({
|
|
19861
|
-
type: external_exports.literal("docker-ssh"),
|
|
19862
|
-
/** SSH 连接参数 */
|
|
19863
|
-
ssh: SshConfigSchema.optional(),
|
|
19864
|
-
/** 远程主机运行参数 */
|
|
19865
|
-
remote: external_exports.object({
|
|
19866
|
-
/** 远程应用根目录 */
|
|
19867
|
-
baseDir: external_exports.string().min(1).default("/opt/hai/apps"),
|
|
19868
|
-
/** 远程容器运行时(auto 优先 podman) */
|
|
19869
|
-
runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto")
|
|
19870
|
-
}).default({ baseDir: "/opt/hai/apps", runtime: "auto" }),
|
|
19871
|
-
/** 应用暴露方式 */
|
|
19872
|
-
expose: ExposeSchema
|
|
19873
|
-
});
|
|
19874
|
-
var ProviderConfigSchema = external_exports.discriminatedUnion("type", [
|
|
19875
|
-
VercelProviderConfigSchema,
|
|
19876
|
-
DockerSshProviderConfigSchema
|
|
19877
|
-
]);
|
|
19878
|
-
var NeonServiceSchema = external_exports.object({
|
|
19879
|
-
provisioner: external_exports.literal("neon"),
|
|
19880
|
-
apiKey: external_exports.string().min(1)
|
|
19881
|
-
});
|
|
19882
|
-
var UpstashServiceSchema = external_exports.object({
|
|
19883
|
-
provisioner: external_exports.literal("upstash"),
|
|
19884
|
-
email: external_exports.string().email(),
|
|
19885
|
-
apiKey: external_exports.string().min(1)
|
|
19886
|
-
});
|
|
19887
|
-
var R2ServiceSchema = external_exports.object({
|
|
19888
|
-
provisioner: external_exports.literal("cloudflare-r2"),
|
|
19889
|
-
accessKeyId: external_exports.string().min(1),
|
|
19890
|
-
secretAccessKey: external_exports.string().min(1),
|
|
19891
|
-
accountId: external_exports.string().min(1),
|
|
19892
|
-
apiToken: external_exports.string().min(1)
|
|
19893
|
-
});
|
|
19894
|
-
var ResendServiceSchema = external_exports.object({
|
|
19895
|
-
provisioner: external_exports.literal("resend"),
|
|
19896
|
-
from: external_exports.string().email(),
|
|
19897
|
-
apiKey: external_exports.string().min(1)
|
|
19898
|
-
});
|
|
19899
|
-
var AliyunSmsServiceSchema = external_exports.object({
|
|
19900
|
-
provisioner: external_exports.literal("aliyun"),
|
|
19901
|
-
signName: external_exports.string().min(1),
|
|
19902
|
-
accessKeyId: external_exports.string().min(1),
|
|
19903
|
-
accessKeySecret: external_exports.string().min(1)
|
|
19904
|
-
});
|
|
19905
|
-
var ServicesConfigSchema = external_exports.object({
|
|
19906
|
-
db: external_exports.discriminatedUnion("provisioner", [NeonServiceSchema]).optional(),
|
|
19907
|
-
cache: external_exports.discriminatedUnion("provisioner", [UpstashServiceSchema]).optional(),
|
|
19908
|
-
storage: external_exports.discriminatedUnion("provisioner", [R2ServiceSchema]).optional(),
|
|
19909
|
-
email: external_exports.discriminatedUnion("provisioner", [ResendServiceSchema]).optional(),
|
|
19910
|
-
sms: external_exports.discriminatedUnion("provisioner", [AliyunSmsServiceSchema]).optional()
|
|
19911
|
-
}).optional();
|
|
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(),
|
|
19915
|
-
provider: ProviderConfigSchema,
|
|
19916
|
-
services: ServicesConfigSchema,
|
|
19917
|
-
/** Docker Compose 部署参数;构建配置位于项目 hai.build.json。 */
|
|
19918
|
-
container: external_exports.object({
|
|
19919
|
-
/** 自建 S3 服务实现;协议仍为 storage.type=s3。 */
|
|
19920
|
-
s3Engine: external_exports.enum(["rustfs", "minio"]).default("rustfs")
|
|
19921
|
-
}).optional()
|
|
19922
|
-
});
|
|
19923
|
-
var DeployErrorInfo = {
|
|
19924
|
-
DEPLOY_FAILED: "001:500",
|
|
19925
|
-
PROJECT_CREATE_FAILED: "002:500",
|
|
19926
|
-
UPLOAD_FAILED: "004:500",
|
|
19927
|
-
AUTH_REQUIRED: "005:401",
|
|
19928
|
-
AUTH_FAILED: "006:401",
|
|
19929
|
-
PROVISION_FAILED: "007:500",
|
|
19930
|
-
ADAPTER_MISSING: "008:500",
|
|
19931
|
-
SCAN_FAILED: "009:500",
|
|
19932
|
-
NOT_INITIALIZED: "010:500",
|
|
19933
|
-
ENV_VAR_FAILED: "011:500",
|
|
19934
|
-
UNSUPPORTED_TYPE: "012:400",
|
|
19935
|
-
CONFIG_ERROR: "013:500",
|
|
19936
|
-
CREDENTIAL_ERROR: "014:500",
|
|
19937
|
-
REMOTE_CONNECT_FAILED: "017:500",
|
|
19938
|
-
REMOTE_COMMAND_FAILED: "018:500",
|
|
19939
|
-
IMAGE_TRANSFER_FAILED: "019:500",
|
|
19940
|
-
HEALTH_CHECK_FAILED: "020:500"
|
|
19941
|
-
};
|
|
19942
|
-
var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
|
|
19943
|
-
|
|
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
|
-
}
|
|
20110
|
-
var logger = core.logger.child({ module: "deploy", scope: "credentials" });
|
|
20111
|
-
var DEPLOY_CREDENTIAL_KEY_PATTERN = /^HAI_DEPLOY_[A-Z0-9_]+$/;
|
|
20112
|
-
function isAllowedCredentialKey(key) {
|
|
20113
|
-
return DEPLOY_CREDENTIAL_KEY_PATTERN.test(key);
|
|
20114
|
-
}
|
|
20115
|
-
function normalizeCredentialEntries(entries) {
|
|
20116
|
-
const normalized = {};
|
|
20117
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
20118
|
-
if (!isAllowedCredentialKey(key)) {
|
|
20119
|
-
logger.warn("Ignoring unsupported deploy credential key", { key });
|
|
20120
|
-
continue;
|
|
20121
|
-
}
|
|
20122
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
20123
|
-
normalized[key] = String(value);
|
|
20124
|
-
continue;
|
|
20125
|
-
}
|
|
20126
|
-
logger.warn("Ignoring deploy credential with non-scalar value", { key });
|
|
20127
|
-
}
|
|
20128
|
-
return normalized;
|
|
20129
|
-
}
|
|
20130
|
-
function assertAllowedCredentialKey(key) {
|
|
20131
|
-
if (!isAllowedCredentialKey(key)) {
|
|
20132
|
-
throw new Error(deployM("deploy_credentialInvalidKey", { params: { key } }));
|
|
20133
|
-
}
|
|
20134
|
-
}
|
|
20135
|
-
function assertAllowedCredentialEntries(entries) {
|
|
20136
|
-
for (const key of Object.keys(entries)) {
|
|
20137
|
-
assertAllowedCredentialKey(key);
|
|
20138
|
-
}
|
|
20139
|
-
}
|
|
20140
|
-
function getCredentialsPath() {
|
|
20141
|
-
return join(homedir(), ".hai", "credentials.yml");
|
|
20142
|
-
}
|
|
20143
|
-
function ensureDir(filePath) {
|
|
20144
|
-
const dir = dirname(filePath);
|
|
20145
|
-
if (!existsSync(dir)) {
|
|
20146
|
-
mkdirSync(dir, { recursive: true });
|
|
20147
|
-
}
|
|
20148
|
-
}
|
|
20149
|
-
function readCredentialsFile() {
|
|
20150
|
-
const filePath = getCredentialsPath();
|
|
20151
|
-
if (!existsSync(filePath)) {
|
|
20152
|
-
return {};
|
|
20153
|
-
}
|
|
20154
|
-
const content = readFileSync(filePath, "utf-8");
|
|
20155
|
-
const parsed = parse$1(content);
|
|
20156
|
-
if (parsed === null || parsed === void 0 || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
20157
|
-
return {};
|
|
20158
|
-
}
|
|
20159
|
-
return normalizeCredentialEntries(parsed);
|
|
20160
|
-
}
|
|
20161
|
-
function writeCredentialsFile(data) {
|
|
20162
|
-
const filePath = getCredentialsPath();
|
|
20163
|
-
ensureDir(filePath);
|
|
20164
|
-
const header = "# Managed by hai cli - do not edit manually\n";
|
|
20165
|
-
const content = header + stringify(data);
|
|
20166
|
-
writeFileSync(filePath, content, { encoding: "utf-8", mode: 384 });
|
|
20167
|
-
}
|
|
20168
|
-
function loadCredentials() {
|
|
20169
|
-
try {
|
|
20170
|
-
const data = readCredentialsFile();
|
|
20171
|
-
const keys = Object.keys(data);
|
|
20172
|
-
for (const key of keys) {
|
|
20173
|
-
process2.env[key] = String(data[key]);
|
|
20174
|
-
}
|
|
20175
|
-
logger.debug("Credentials loaded", { count: keys.length });
|
|
20176
|
-
return ok(keys);
|
|
20177
|
-
} catch (error62) {
|
|
20178
|
-
logger.error("Failed to load credentials", { error: error62 });
|
|
20179
|
-
return err(
|
|
20180
|
-
HaiDeployError.CREDENTIAL_ERROR,
|
|
20181
|
-
deployM("deploy_credentialError", {
|
|
20182
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
20183
|
-
}),
|
|
20184
|
-
error62
|
|
20185
|
-
);
|
|
20186
|
-
}
|
|
20187
|
-
}
|
|
20188
|
-
function saveCredential(key, value) {
|
|
20189
|
-
try {
|
|
20190
|
-
assertAllowedCredentialKey(key);
|
|
20191
|
-
const data = readCredentialsFile();
|
|
20192
|
-
data[key] = value;
|
|
20193
|
-
writeCredentialsFile(data);
|
|
20194
|
-
process2.env[key] = value;
|
|
20195
|
-
logger.debug("Credential saved", { key });
|
|
20196
|
-
return ok(void 0);
|
|
20197
|
-
} catch (error62) {
|
|
20198
|
-
logger.error("Failed to save credential", { key, error: error62 });
|
|
20199
|
-
return err(
|
|
20200
|
-
HaiDeployError.CREDENTIAL_ERROR,
|
|
20201
|
-
deployM("deploy_credentialError", {
|
|
20202
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
20203
|
-
}),
|
|
20204
|
-
error62
|
|
20205
|
-
);
|
|
20206
|
-
}
|
|
20207
|
-
}
|
|
20208
|
-
function saveCredentials(entries) {
|
|
20209
|
-
try {
|
|
20210
|
-
assertAllowedCredentialEntries(entries);
|
|
20211
|
-
const data = readCredentialsFile();
|
|
20212
|
-
for (const [key, value] of Object.entries(entries)) {
|
|
20213
|
-
data[key] = value;
|
|
20214
|
-
process2.env[key] = value;
|
|
20215
|
-
}
|
|
20216
|
-
writeCredentialsFile(data);
|
|
20217
|
-
logger.debug("Credentials saved", { count: Object.keys(entries).length });
|
|
20218
|
-
return ok(void 0);
|
|
20219
|
-
} catch (error62) {
|
|
20220
|
-
logger.error("Failed to save credentials", { error: error62 });
|
|
20221
|
-
return err(
|
|
20222
|
-
HaiDeployError.CREDENTIAL_ERROR,
|
|
20223
|
-
deployM("deploy_credentialError", {
|
|
20224
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
20225
|
-
}),
|
|
20226
|
-
error62
|
|
20227
|
-
);
|
|
20228
|
-
}
|
|
20229
|
-
}
|
|
20230
|
-
function resolveArtifactDirectory(appDir) {
|
|
20231
|
-
let directory = resolve(appDir);
|
|
20232
|
-
while (true) {
|
|
20233
|
-
if (existsSync(join(directory, "hai.build.json")) || existsSync(join(directory, "pnpm-lock.yaml")))
|
|
20234
|
-
return join(directory, ".hai/build/docker");
|
|
20235
|
-
const parent = dirname(directory);
|
|
20236
|
-
if (parent === directory)
|
|
20237
|
-
return join(appDir, ".hai/build/docker");
|
|
20238
|
-
directory = parent;
|
|
20239
|
-
}
|
|
20240
|
-
}
|
|
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
|
-
}
|
|
20245
|
-
function resolveOutputDir(appDir, providerType) {
|
|
20246
|
-
switch (providerType) {
|
|
20247
|
-
case "vercel":
|
|
20248
|
-
return join(appDir, ".vercel", "output");
|
|
20249
|
-
default:
|
|
20250
|
-
return join(appDir, "build");
|
|
20251
|
-
}
|
|
20252
|
-
}
|
|
20253
|
-
var RecoverySchema = external_exports.object({
|
|
20254
|
-
projectName: external_exports.string(),
|
|
20255
|
-
stage: external_exports.string(),
|
|
20256
|
-
platformProjectId: external_exports.string().optional(),
|
|
20257
|
-
resources: external_exports.array(external_exports.object({
|
|
20258
|
-
serviceType: external_exports.enum(["db", "cache", "storage", "email", "sms"]),
|
|
20259
|
-
provisionerName: external_exports.string(),
|
|
20260
|
-
resourceInfo: external_exports.string(),
|
|
20261
|
-
resourceStatus: external_exports.enum(["created", "reused", "unknown"])
|
|
20262
|
-
}))
|
|
20263
|
-
});
|
|
20264
|
-
function getDeployRecovery(error62) {
|
|
20265
|
-
const parsed = RecoverySchema.safeParse(error62.ext?.recovery);
|
|
20266
|
-
return parsed.success ? parsed.data : void 0;
|
|
20267
|
-
}
|
|
20268
|
-
function recoveryFailure(error62, recovery) {
|
|
20269
|
-
return err({
|
|
20270
|
-
code: error62.code,
|
|
20271
|
-
httpStatus: error62.httpStatus,
|
|
20272
|
-
system: error62.system,
|
|
20273
|
-
module: error62.module,
|
|
20274
|
-
message: deployM("deploy_recoveryFailure", { params: { stage: recovery.stage } }),
|
|
20275
|
-
suggestion: deployM("deploy_recoverySuggestion"),
|
|
20276
|
-
ext: { recovery }
|
|
20277
|
-
});
|
|
20278
|
-
}
|
|
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);
|
|
20332
|
-
}
|
|
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 };
|
|
20336
|
-
}
|
|
20337
|
-
async function scanApp(inputDir, options) {
|
|
20338
|
-
try {
|
|
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);
|
|
20402
|
-
} catch {
|
|
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" } }));
|
|
20404
|
-
}
|
|
20405
|
-
}
|
|
20406
|
-
var logger3 = core.logger.child({ module: "deploy", scope: "command" });
|
|
20407
|
-
function maskSecrets(text, secrets) {
|
|
20408
|
-
if (!secrets || secrets.length === 0)
|
|
20409
|
-
return text;
|
|
20410
|
-
let masked = text;
|
|
20411
|
-
for (const secret of secrets) {
|
|
20412
|
-
if (secret && secret.length > 0)
|
|
20413
|
-
masked = masked.split(secret).join("***");
|
|
20414
|
-
}
|
|
20415
|
-
return masked;
|
|
20416
|
-
}
|
|
20417
|
-
function runCommand(command, args, options = {}) {
|
|
20418
|
-
return new Promise((resolve6) => {
|
|
20419
|
-
const child = spawn(command, args, {
|
|
20420
|
-
cwd: options.cwd,
|
|
20421
|
-
env: options.env ? { ...process2.env, ...options.env } : process2.env,
|
|
20422
|
-
shell: false
|
|
20423
|
-
});
|
|
20424
|
-
let stdout = "";
|
|
20425
|
-
let stderr = "";
|
|
20426
|
-
let settled = false;
|
|
20427
|
-
let timer;
|
|
20428
|
-
const finish = (result) => {
|
|
20429
|
-
if (settled)
|
|
20430
|
-
return;
|
|
20431
|
-
settled = true;
|
|
20432
|
-
if (timer)
|
|
20433
|
-
clearTimeout(timer);
|
|
20434
|
-
resolve6(result);
|
|
20435
|
-
};
|
|
20436
|
-
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
20437
|
-
timer = setTimeout(() => {
|
|
20438
|
-
child.kill("SIGKILL");
|
|
20439
|
-
logger3.error("Command timed out", { command, timeoutMs: options.timeoutMs });
|
|
20440
|
-
finish(err(
|
|
20441
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20442
|
-
deployM("deploy_commandTimeout", { params: { command } })
|
|
20443
|
-
));
|
|
20444
|
-
}, options.timeoutMs);
|
|
20445
|
-
}
|
|
20446
|
-
child.stdout.on("data", (chunk) => {
|
|
20447
|
-
stdout += chunk.toString("utf-8");
|
|
20448
|
-
});
|
|
20449
|
-
child.stderr.on("data", (chunk) => {
|
|
20450
|
-
stderr += chunk.toString("utf-8");
|
|
20451
|
-
});
|
|
20452
|
-
child.on("error", (error62) => {
|
|
20453
|
-
logger3.error("Command failed to start", { command, error: maskSecrets(String(error62), options.secrets) });
|
|
20454
|
-
finish(err(
|
|
20455
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20456
|
-
deployM("deploy_commandFailed", {
|
|
20457
|
-
params: { command, error: maskSecrets(error62.message, options.secrets) }
|
|
20458
|
-
}),
|
|
20459
|
-
error62
|
|
20460
|
-
));
|
|
20461
|
-
});
|
|
20462
|
-
child.on("close", (code) => {
|
|
20463
|
-
const exitCode = code ?? -1;
|
|
20464
|
-
if (exitCode === 0) {
|
|
20465
|
-
finish(ok({ code: exitCode, stdout, stderr }));
|
|
20466
|
-
return;
|
|
20467
|
-
}
|
|
20468
|
-
logger3.error("Command exited with non-zero code", {
|
|
20469
|
-
command,
|
|
20470
|
-
code: exitCode,
|
|
20471
|
-
stderr: maskSecrets(stderr, options.secrets).slice(-500),
|
|
20472
|
-
stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
|
|
20473
|
-
});
|
|
20474
|
-
finish(err(
|
|
20475
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20476
|
-
deployM("deploy_commandFailed", {
|
|
20477
|
-
params: { command, error: maskSecrets(`${stdout.slice(-2500)}
|
|
20478
|
-
${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
|
|
20479
|
-
})
|
|
20480
|
-
));
|
|
20481
|
-
});
|
|
20482
|
-
if (options.input !== void 0) {
|
|
20483
|
-
child.stdin.write(options.input);
|
|
20484
|
-
child.stdin.end();
|
|
20485
|
-
} else {
|
|
20486
|
-
child.stdin.end();
|
|
20487
|
-
}
|
|
20488
|
-
});
|
|
20489
|
-
}
|
|
20490
|
-
|
|
20491
|
-
// ../deploy/src/container/deploy-bundle.ts
|
|
20492
|
-
function shellArgument(value) {
|
|
20493
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
20494
|
-
}
|
|
20495
|
-
async function createDeploymentBundle(options) {
|
|
20496
|
-
const { directory, artifactDir, artifact, projectName, environment, runtime, coreEnv } = options;
|
|
20497
|
-
const stem = basename(options.output).replace(/\.tar$/i, "");
|
|
20498
|
-
const versionSuffix = `-${artifact.appVersion}`;
|
|
20499
|
-
const output2 = resolve(dirname(options.output), `${stem.endsWith(versionSuffix) ? stem : `${stem}${versionSuffix}`}.tar`);
|
|
20500
|
-
const composeName = `docker-compose-${artifact.appVersion}.yml`;
|
|
20501
|
-
mkdirSync(dirname(output2), { recursive: true, mode: 448 });
|
|
20502
|
-
const staging = mkdtempSync(join(directory, ".bundle-"));
|
|
20503
|
-
chmodSync(staging, 448);
|
|
20504
|
-
const temporaryTar = `${output2}.partial`;
|
|
20505
|
-
const startName = `start-${environment}.sh`;
|
|
20506
|
-
try {
|
|
20507
|
-
mkdirSync(join(staging, "images"));
|
|
20508
|
-
for (const image of artifact.images)
|
|
20509
|
-
copyFileSync(join(artifactDir, image.file), join(staging, "images", image.file));
|
|
20510
|
-
for (const file2 of [composeName, ".env.runtime", ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : []])
|
|
20511
|
-
copyFileSync(join(directory, file2), join(staging, file2));
|
|
20512
|
-
const compose = parse$1(readFileSync(join(directory, composeName), "utf8"));
|
|
20513
|
-
const images = [...new Set(Object.values(compose.services).flatMap((service) => service.image ? [service.image] : []))];
|
|
20514
|
-
const start = `#!/bin/sh
|
|
20515
|
-
set -eu
|
|
20516
|
-
cd "$(dirname "$0")"
|
|
20517
|
-
umask 077
|
|
20518
|
-
export HAI_DEPLOY_ENVIRONMENT=${shellArgument(environment)}
|
|
20519
|
-
export HAI_CORE_ENV=${shellArgument(coreEnv)}
|
|
20520
|
-
runtime=${shellArgument(runtime)}
|
|
20521
|
-
if [ -n "\${HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME:-}" ]; then runtime="$HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME"; fi
|
|
20522
|
-
if [ "$runtime" = auto ]; then
|
|
20523
|
-
if command -v podman >/dev/null 2>&1 && podman version >/dev/null 2>&1; then runtime=podman; else runtime=docker; fi
|
|
20524
|
-
fi
|
|
20525
|
-
case "$runtime" in docker|podman) ;; *) echo 'HAI_DEPLOY_PROVIDER_REMOTE_RUNTIME must be docker or podman' >&2; exit 1;; esac
|
|
20526
|
-
"$runtime" version >/dev/null
|
|
20527
|
-
"$runtime" compose version >/dev/null
|
|
20528
|
-
sha256sum -c SHA256SUMS
|
|
20529
|
-
chmod 600 .env.runtime
|
|
20530
|
-
${artifact.images.map((image) => `"$runtime" load -i ${shellArgument(`images/${image.file}`)}`).join("\n")}
|
|
20531
|
-
# \u79BB\u7EBF\u670D\u52A1\u5668\u5E94\u5148\u5BFC\u5165\u5168\u90E8\u4E2D\u95F4\u4EF6\u955C\u50CF\uFF1B\u7981\u6B62\u9759\u9ED8\u5C1D\u8BD5\u8054\u7F51\u3002
|
|
20532
|
-
for image in ${images.map(shellArgument).join(" ")}; do
|
|
20533
|
-
"$runtime" image inspect "$image" >/dev/null 2>&1 || { echo "Missing image: $image. Load the matching image archive before retrying." >&2; exit 1; }
|
|
20534
|
-
done
|
|
20535
|
-
exec "$runtime" compose -f ${shellArgument(composeName)} --env-file .env.runtime -p ${shellArgument(`hai-${projectName}`)} up -d
|
|
20536
|
-
`;
|
|
20537
|
-
writeFileSync(join(directory, startName), start, { mode: 448 });
|
|
20538
|
-
writeFileSync(join(staging, startName), start, { mode: 448 });
|
|
20539
|
-
const instructions = deployM("deploy_bundleInstructions", { params: { file: basename(output2), start: startName, images: images.join("\n"), compose: composeName } });
|
|
20540
|
-
const metadata = deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, compose: composeName } });
|
|
20541
|
-
writeFileSync(join(staging, "README.txt"), `${metadata}
|
|
20542
|
-
${instructions}
|
|
20543
|
-
`);
|
|
20544
|
-
copyFileSync(join(artifactDir, "manifest.json"), join(staging, "manifest.json"));
|
|
20545
|
-
const checksums = [];
|
|
20546
|
-
for (const file2 of [composeName, "manifest.json", ".env.runtime", startName, ...artifact.images.some((image) => image.name === "web") ? ["web.conf"] : [], ...artifact.images.map((image) => `images/${image.file}`)])
|
|
20547
|
-
checksums.push(`${await artifactChecksum(join(staging, file2))} ${file2}`);
|
|
20548
|
-
writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
|
|
20549
|
-
`);
|
|
20550
|
-
const tar = await runCommand("tar", ["-cf", temporaryTar, "-C", staging, "."]);
|
|
20551
|
-
if (!tar.success)
|
|
20552
|
-
throw new Error(tar.error.message);
|
|
20553
|
-
chmodSync(temporaryTar, 384);
|
|
20554
|
-
const { renameSync } = await import('fs');
|
|
20555
|
-
renameSync(temporaryTar, output2);
|
|
20556
|
-
return { bundlePath: output2, instructions, startName };
|
|
20557
|
-
} finally {
|
|
20558
|
-
rmSync(staging, { recursive: true, force: true });
|
|
20559
|
-
rmSync(temporaryTar, { force: true });
|
|
20560
|
-
}
|
|
20561
|
-
}
|
|
20562
|
-
var IMAGES = {
|
|
20563
|
-
postgres: "postgres:16-alpine",
|
|
20564
|
-
mysql: "mysql:8.4",
|
|
20565
|
-
redis: "redis:7-alpine",
|
|
20566
|
-
rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
|
|
20567
|
-
minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
|
|
20568
|
-
qdrant: "qdrant/qdrant:v1.12.4",
|
|
20569
|
-
pgvector: "pgvector/pgvector:pg17",
|
|
20570
|
-
chroma: "chromadb/chroma:1.5.5",
|
|
20571
|
-
ldap: "docker.io/osixia/openldap:1.5.0",
|
|
20572
|
-
smtp: "axllent/mailpit:v1.27.4"
|
|
20573
|
-
};
|
|
20574
|
-
function generatePassword() {
|
|
20575
|
-
return randomBytes(24).toString("base64url");
|
|
20576
|
-
}
|
|
20577
|
-
function resolveSidecarSecrets(appDir, projectName) {
|
|
20578
|
-
const secretFile = join(appDir, ".hai", "deploy", "runtime", `${projectName}.secrets.json`);
|
|
20579
|
-
if (existsSync(secretFile)) {
|
|
20580
|
-
try {
|
|
20581
|
-
return JSON.parse(readFileSync(secretFile, "utf-8"));
|
|
20582
|
-
} catch {
|
|
20583
|
-
}
|
|
20584
|
-
}
|
|
20585
|
-
const secrets = {
|
|
20586
|
-
dbPassword: generatePassword(),
|
|
20587
|
-
storagePassword: generatePassword(),
|
|
20588
|
-
ldapPassword: generatePassword()
|
|
20589
|
-
};
|
|
20590
|
-
mkdirSync(dirname(secretFile), { recursive: true });
|
|
20591
|
-
writeFileSync(secretFile, JSON.stringify(secrets), { mode: 384 });
|
|
20592
|
-
return secrets;
|
|
20593
|
-
}
|
|
20594
|
-
function addAppDataVolume(plan, projectName) {
|
|
20595
|
-
const mount = `${projectName}-data:/app/data`;
|
|
20596
|
-
if (!plan.appVolumes.includes(mount)) {
|
|
20597
|
-
plan.appVolumes.push(mount);
|
|
20598
|
-
plan.volumes[`${projectName}-data`] = null;
|
|
20599
|
-
}
|
|
20600
|
-
}
|
|
20601
|
-
function planPostgresDb(plan, projectName, password, net) {
|
|
20602
|
-
plan.services.push("db");
|
|
20603
|
-
plan.dependsOn.push("db");
|
|
20604
|
-
plan.composeServices.db = {
|
|
20605
|
-
image: IMAGES.postgres,
|
|
20606
|
-
container_name: `hai-${projectName}-db`,
|
|
20607
|
-
restart: "unless-stopped",
|
|
20608
|
-
env_file: [".env.runtime"],
|
|
20609
|
-
volumes: [`${projectName}-db-data:/var/lib/postgresql/data`],
|
|
20610
|
-
networks: net
|
|
20611
|
-
};
|
|
20612
|
-
plan.volumes[`${projectName}-db-data`] = null;
|
|
20613
|
-
plan.env.POSTGRES_USER = "hai";
|
|
20614
|
-
plan.env.POSTGRES_PASSWORD = password;
|
|
20615
|
-
plan.env.POSTGRES_DB = "hai";
|
|
20616
|
-
plan.env.HAI_DB = JSON.stringify({ type: "postgresql", url: `postgresql://hai:${password}@db:5432/hai`, database: "hai" });
|
|
20617
|
-
plan.secrets.push(password);
|
|
20618
|
-
}
|
|
20619
|
-
function planMysqlDb(plan, projectName, password, net) {
|
|
20620
|
-
plan.services.push("db");
|
|
20621
|
-
plan.dependsOn.push("db");
|
|
20622
|
-
plan.composeServices.db = {
|
|
20623
|
-
image: IMAGES.mysql,
|
|
20624
|
-
container_name: `hai-${projectName}-db`,
|
|
20625
|
-
restart: "unless-stopped",
|
|
20626
|
-
env_file: [".env.runtime"],
|
|
20627
|
-
volumes: [`${projectName}-db-data:/var/lib/mysql`],
|
|
20628
|
-
networks: net
|
|
20629
|
-
};
|
|
20630
|
-
plan.volumes[`${projectName}-db-data`] = null;
|
|
20631
|
-
plan.env.MYSQL_ROOT_PASSWORD = password;
|
|
20632
|
-
plan.env.MYSQL_DATABASE = "hai";
|
|
20633
|
-
plan.env.MYSQL_USER = "hai";
|
|
20634
|
-
plan.env.MYSQL_PASSWORD = password;
|
|
20635
|
-
plan.env.HAI_DB = JSON.stringify({ type: "mysql", url: `mysql://hai:${password}@db:3306/hai`, database: "hai" });
|
|
20636
|
-
plan.secrets.push(password);
|
|
20637
|
-
}
|
|
20638
|
-
function planRedisCache(plan, projectName, net) {
|
|
20639
|
-
plan.services.push("cache");
|
|
20640
|
-
plan.dependsOn.push("cache");
|
|
20641
|
-
plan.composeServices.cache = {
|
|
20642
|
-
image: IMAGES.redis,
|
|
20643
|
-
container_name: `hai-${projectName}-cache`,
|
|
20644
|
-
restart: "unless-stopped",
|
|
20645
|
-
networks: net
|
|
20646
|
-
};
|
|
20647
|
-
plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
|
|
20648
|
-
}
|
|
20649
|
-
function planS3Storage(plan, projectName, password, net, engine) {
|
|
20650
|
-
plan.services.push("storage");
|
|
20651
|
-
plan.dependsOn.push("storage");
|
|
20652
|
-
plan.composeServices.storage = {
|
|
20653
|
-
image: IMAGES[engine],
|
|
20654
|
-
container_name: `hai-${projectName}-storage`,
|
|
20655
|
-
restart: "unless-stopped",
|
|
20656
|
-
...engine === "minio" ? { command: ["server", "/data"] } : {},
|
|
20657
|
-
env_file: [".env.runtime"],
|
|
20658
|
-
volumes: [`${projectName}-storage-data:/data`],
|
|
20659
|
-
networks: net
|
|
20660
|
-
};
|
|
20661
|
-
plan.composeServices["storage-init"] = {
|
|
20662
|
-
depends_on: ["storage"],
|
|
20663
|
-
env_file: [".env.runtime"],
|
|
20664
|
-
entrypoint: [
|
|
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');`
|
|
20685
|
-
],
|
|
20686
|
-
networks: net
|
|
20687
|
-
};
|
|
20688
|
-
plan.volumes[`${projectName}-storage-data`] = null;
|
|
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
|
-
}
|
|
20694
|
-
plan.env.HAI_STORAGE = JSON.stringify({
|
|
20695
|
-
type: "s3",
|
|
20696
|
-
endpoint: "http://storage:9000",
|
|
20697
|
-
bucket: "hai",
|
|
20698
|
-
region: "us-east-1",
|
|
20699
|
-
accessKeyId: "hai-storage",
|
|
20700
|
-
secretAccessKey: password,
|
|
20701
|
-
forcePathStyle: true
|
|
20702
|
-
});
|
|
20703
|
-
plan.secrets.push(password);
|
|
20704
|
-
}
|
|
20705
|
-
function planQdrantVecdb(plan, projectName, net) {
|
|
20706
|
-
plan.services.push("vecdb");
|
|
20707
|
-
plan.dependsOn.push("vecdb");
|
|
20708
|
-
plan.composeServices.vecdb = {
|
|
20709
|
-
image: IMAGES.qdrant,
|
|
20710
|
-
container_name: `hai-${projectName}-vecdb`,
|
|
20711
|
-
restart: "unless-stopped",
|
|
20712
|
-
volumes: [`${projectName}-vecdb-data:/qdrant/storage`],
|
|
20713
|
-
networks: net
|
|
20714
|
-
};
|
|
20715
|
-
plan.volumes[`${projectName}-vecdb-data`] = null;
|
|
20716
|
-
plan.env.HAI_VECDB = JSON.stringify({ type: "qdrant", url: "http://vecdb:6333" });
|
|
20717
|
-
}
|
|
20718
|
-
function planPgvectorVecdb(plan, projectName, password, net) {
|
|
20719
|
-
plan.services.push("vecdb");
|
|
20720
|
-
plan.dependsOn.push("vecdb");
|
|
20721
|
-
plan.composeServices.vecdb = {
|
|
20722
|
-
image: IMAGES.pgvector,
|
|
20723
|
-
container_name: `hai-${projectName}-vecdb`,
|
|
20724
|
-
restart: "unless-stopped",
|
|
20725
|
-
env_file: [".env.runtime"],
|
|
20726
|
-
volumes: [`${projectName}-vecdb-data:/var/lib/postgresql/data`],
|
|
20727
|
-
networks: net
|
|
20728
|
-
};
|
|
20729
|
-
plan.volumes[`${projectName}-vecdb-data`] = null;
|
|
20730
|
-
plan.env.POSTGRES_USER = "hai";
|
|
20731
|
-
plan.env.POSTGRES_PASSWORD = password;
|
|
20732
|
-
plan.env.POSTGRES_DB = "hai";
|
|
20733
|
-
plan.env.HAI_VECDB = JSON.stringify({ type: "pgvector", url: `postgresql://hai:${password}@vecdb:5432/hai`, database: "hai" });
|
|
20734
|
-
plan.secrets.push(password);
|
|
20735
|
-
}
|
|
20736
|
-
function planChromaVecdb(plan, projectName, net) {
|
|
20737
|
-
plan.services.push("vecdb");
|
|
20738
|
-
plan.dependsOn.push("vecdb");
|
|
20739
|
-
plan.composeServices.vecdb = {
|
|
20740
|
-
image: IMAGES.chroma,
|
|
20741
|
-
container_name: `hai-${projectName}-vecdb`,
|
|
20742
|
-
restart: "unless-stopped",
|
|
20743
|
-
volumes: [`${projectName}-vecdb-data:/data`],
|
|
20744
|
-
networks: net
|
|
20745
|
-
};
|
|
20746
|
-
plan.volumes[`${projectName}-vecdb-data`] = null;
|
|
20747
|
-
plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
|
|
20748
|
-
}
|
|
20749
|
-
function planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine = "rustfs") {
|
|
20750
|
-
const plan = {
|
|
20751
|
-
services: [],
|
|
20752
|
-
composeServices: {},
|
|
20753
|
-
volumes: {},
|
|
20754
|
-
appVolumes: [],
|
|
20755
|
-
env: {},
|
|
20756
|
-
dependsOn: [],
|
|
20757
|
-
secrets: []
|
|
20758
|
-
};
|
|
20759
|
-
if (!scan.requiredServices.some((service) => usesDefaultMiddleware(scan, service, options)))
|
|
20760
|
-
return plan;
|
|
20761
|
-
const secrets = resolveSidecarSecrets(appDir, projectName);
|
|
20762
|
-
const net = ["hai-app"];
|
|
20763
|
-
const backends = scan.serviceBackends;
|
|
20764
|
-
for (const service of scan.requiredServices) {
|
|
20765
|
-
if (!usesDefaultMiddleware(scan, service, options))
|
|
20766
|
-
continue;
|
|
20767
|
-
if (service === "db" && !provisionEnv.HAI_DB) {
|
|
20768
|
-
const backend = backends.db ?? "postgresql";
|
|
20769
|
-
if (backend === "mysql")
|
|
20770
|
-
planMysqlDb(plan, projectName, secrets.dbPassword, net);
|
|
20771
|
-
else if (backend === "sqlite")
|
|
20772
|
-
addAppDataVolume(plan, projectName);
|
|
20773
|
-
else
|
|
20774
|
-
planPostgresDb(plan, projectName, secrets.dbPassword, net);
|
|
20775
|
-
} else if (service === "cache" && !provisionEnv.HAI_CACHE) {
|
|
20776
|
-
if ((backends.cache ?? "redis") === "redis")
|
|
20777
|
-
planRedisCache(plan, projectName, net);
|
|
20778
|
-
} else if (service === "storage" && !provisionEnv.HAI_STORAGE) {
|
|
20779
|
-
if ((backends.storage ?? "s3") === "s3")
|
|
20780
|
-
planS3Storage(plan, projectName, secrets.storagePassword, net, s3Engine);
|
|
20781
|
-
else
|
|
20782
|
-
addAppDataVolume(plan, projectName);
|
|
20783
|
-
} else if (service === "vecdb" && !provisionEnv.HAI_VECDB) {
|
|
20784
|
-
const backend = backends.vecdb ?? "qdrant";
|
|
20785
|
-
if (backend === "pgvector")
|
|
20786
|
-
planPgvectorVecdb(plan, projectName, secrets.dbPassword, net);
|
|
20787
|
-
else if (backend === "chroma")
|
|
20788
|
-
planChromaVecdb(plan, projectName, net);
|
|
20789
|
-
else if (backend === "lancedb")
|
|
20790
|
-
addAppDataVolume(plan, projectName);
|
|
20791
|
-
else
|
|
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
|
-
}
|
|
20813
|
-
}
|
|
20814
|
-
}
|
|
20815
|
-
return plan;
|
|
20816
|
-
}
|
|
20817
|
-
function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv = {}, s3Engine = "rustfs") {
|
|
20818
|
-
const sidecar = planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine);
|
|
20819
|
-
const env = {
|
|
20820
|
-
NODE_ENV: "production",
|
|
20821
|
-
HOST: "0.0.0.0",
|
|
20822
|
-
PORT: String(containerPort),
|
|
20823
|
-
HAI_SERV_SERVER_HOST: "0.0.0.0",
|
|
20824
|
-
HAI_SERV_SERVER_PORT: String(containerPort),
|
|
20825
|
-
...appEnv,
|
|
20826
|
-
...sidecar.env,
|
|
20827
|
-
...provisionEnv
|
|
20828
|
-
};
|
|
20829
|
-
return { env, sidecar };
|
|
20830
|
-
}
|
|
20831
|
-
function generateComposeFile(options) {
|
|
20832
|
-
const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
|
|
20833
|
-
const app = {
|
|
20834
|
-
image: imageRef,
|
|
20835
|
-
labels: { "org.opencontainers.image.version": options.appVersion },
|
|
20836
|
-
container_name: `hai-${projectName}-app`,
|
|
20837
|
-
restart: "unless-stopped",
|
|
20838
|
-
// 镜像已由 load 注入本地,禁止 compose 再从 registry 拉取
|
|
20839
|
-
pull_policy: "never",
|
|
20840
|
-
env_file: [".env.runtime"],
|
|
20841
|
-
ports: [`${hostPort}:${containerPort}`],
|
|
20842
|
-
networks: ["hai-app"]
|
|
20843
|
-
};
|
|
20844
|
-
if (sidecar.appVolumes.length > 0)
|
|
20845
|
-
app.volumes = sidecar.appVolumes;
|
|
20846
|
-
if (sidecar.dependsOn.length > 0)
|
|
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 };
|
|
20867
|
-
const compose = {
|
|
20868
|
-
"x-hai-version": options.appVersion,
|
|
20869
|
-
services,
|
|
20870
|
-
"networks": {
|
|
20871
|
-
"hai-app": { name: `hai-${projectName}` }
|
|
20872
|
-
}
|
|
20873
|
-
};
|
|
20874
|
-
if (Object.keys(sidecar.volumes).length > 0)
|
|
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";
|
|
20901
|
-
return stringify(compose);
|
|
20902
|
-
}
|
|
20903
|
-
function generateWebProxyConfig(port, apiPrefix, webPort = 8080) {
|
|
20904
|
-
if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535))
|
|
20905
|
-
throw new Error("Invalid web proxy target");
|
|
20906
|
-
return `server {
|
|
20907
|
-
listen ${webPort};
|
|
20908
|
-
server_name _;
|
|
20909
|
-
root /usr/share/nginx/html;
|
|
20910
|
-
location / {
|
|
20911
|
-
try_files $uri $uri/ /index.html;
|
|
20912
|
-
}
|
|
20913
|
-
location = /runtime-config.js {
|
|
20914
|
-
add_header Cache-Control "no-store";
|
|
20915
|
-
default_type application/javascript;
|
|
20916
|
-
return 200 'globalThis.__HAI_PUBLIC_CONFIG__={apiBase:globalThis.location.origin+"${apiPrefix}"};';
|
|
20917
|
-
}
|
|
20918
|
-
location = /_hai/web-health {
|
|
20919
|
-
access_log off;
|
|
20920
|
-
return 200 'ok';
|
|
20921
|
-
}
|
|
20922
|
-
location = /health {
|
|
20923
|
-
proxy_pass http://app:${port};
|
|
20924
|
-
proxy_set_header Host $http_host;
|
|
20925
|
-
}
|
|
20926
|
-
location ${apiPrefix.replace(/\/$/, "")}/ {
|
|
20927
|
-
proxy_pass http://app:${port};
|
|
20928
|
-
proxy_http_version 1.1;
|
|
20929
|
-
proxy_set_header Host $http_host;
|
|
20930
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
20931
|
-
proxy_buffering off;
|
|
20932
|
-
}
|
|
20933
|
-
}
|
|
20934
|
-
`;
|
|
20935
|
-
}
|
|
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("$", "$$");
|
|
20946
|
-
}
|
|
20947
|
-
return `${key}=${literal2}`;
|
|
20948
|
-
}).join("\n")}
|
|
20949
|
-
`;
|
|
20950
|
-
}
|
|
20951
|
-
var logger4 = core.logger.child({ module: "deploy", scope: "ssh" });
|
|
20952
|
-
function buildConnectOptions(config2) {
|
|
20953
|
-
const args = ["-o", "BatchMode=yes"];
|
|
20954
|
-
args.push("-o", `ConnectTimeout=${config2.connectTimeoutSeconds}`);
|
|
20955
|
-
args.push("-o", `StrictHostKeyChecking=${config2.strictHostKeyChecking ? "accept-new" : "no"}`);
|
|
20956
|
-
if (config2.knownHostsFile)
|
|
20957
|
-
args.push("-o", `UserKnownHostsFile=${config2.knownHostsFile}`);
|
|
20958
|
-
if (config2.identityFile) {
|
|
20959
|
-
args.push("-i", config2.identityFile);
|
|
20960
|
-
args.push("-o", "IdentitiesOnly=yes");
|
|
20961
|
-
}
|
|
20962
|
-
return args;
|
|
20963
|
-
}
|
|
20964
|
-
function buildSshExecArgs(config2, remoteCommand) {
|
|
20965
|
-
return [
|
|
20966
|
-
...buildConnectOptions(config2),
|
|
20967
|
-
"-p",
|
|
20968
|
-
String(config2.port),
|
|
20969
|
-
`${config2.username}@${config2.host}`,
|
|
20970
|
-
remoteCommand
|
|
20971
|
-
];
|
|
20972
|
-
}
|
|
20973
|
-
function buildScpUploadArgs(config2, localPath, remotePath) {
|
|
20974
|
-
return [
|
|
20975
|
-
...buildConnectOptions(config2),
|
|
20976
|
-
"-P",
|
|
20977
|
-
String(config2.port),
|
|
20978
|
-
localPath,
|
|
20979
|
-
`${config2.username}@${config2.host}:${remotePath}`
|
|
20980
|
-
];
|
|
20981
|
-
}
|
|
20982
|
-
function createSshExecutor(config2) {
|
|
20983
|
-
const exec = async (remoteCommand, options) => {
|
|
20984
|
-
return runCommand("ssh", buildSshExecArgs(config2, remoteCommand), {
|
|
20985
|
-
timeoutMs: options?.timeoutMs,
|
|
20986
|
-
secrets: options?.secrets
|
|
20987
|
-
});
|
|
20988
|
-
};
|
|
20989
|
-
const upload = async (localPath, remotePath) => {
|
|
20990
|
-
const args = buildScpUploadArgs(config2, localPath, remotePath);
|
|
20991
|
-
const result = await runCommand("scp", args, {});
|
|
20992
|
-
if (!result.success) {
|
|
20993
|
-
return err(
|
|
20994
|
-
HaiDeployError.IMAGE_TRANSFER_FAILED,
|
|
20995
|
-
deployM("deploy_imageTransferFailed", { params: { error: result.error.message } }),
|
|
20996
|
-
result.error
|
|
20997
|
-
);
|
|
20998
|
-
}
|
|
20999
|
-
return ok(void 0);
|
|
21000
|
-
};
|
|
21001
|
-
const ping = async () => {
|
|
21002
|
-
logger4.debug("SSH preflight", { host: config2.host, port: config2.port });
|
|
21003
|
-
const result = await exec("true", { timeoutMs: 2e4 });
|
|
21004
|
-
if (!result.success) {
|
|
21005
|
-
return err(
|
|
21006
|
-
HaiDeployError.REMOTE_CONNECT_FAILED,
|
|
21007
|
-
deployM("deploy_remoteConnectFailed", { params: { host: config2.host, error: result.error.message } }),
|
|
21008
|
-
result.error
|
|
21009
|
-
);
|
|
21010
|
-
}
|
|
21011
|
-
return ok(void 0);
|
|
21012
|
-
};
|
|
21013
|
-
return { exec, upload, ping };
|
|
21014
|
-
}
|
|
21015
|
-
|
|
21016
|
-
// ../deploy/src/providers/deploy-provider-docker-ssh.ts
|
|
21017
|
-
var logger5 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
|
|
21018
|
-
var HEALTH_POLL_INTERVAL = 3e3;
|
|
21019
|
-
var HEALTH_MAX_ATTEMPTS = 40;
|
|
21020
|
-
function makeDeploymentId() {
|
|
21021
|
-
const now = /* @__PURE__ */ new Date();
|
|
21022
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
21023
|
-
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
21024
|
-
return `${stamp}-${randomBytes(3).toString("hex")}`;
|
|
21025
|
-
}
|
|
21026
|
-
function sleep(ms) {
|
|
21027
|
-
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
21028
|
-
}
|
|
21029
|
-
function createDockerSshProvider(config2, container) {
|
|
21030
|
-
const ssh = config2.ssh ? createSshExecutor(config2.ssh) : void 0;
|
|
21031
|
-
const baseDir = config2.remote.baseDir;
|
|
21032
|
-
async function detectRemoteRuntime() {
|
|
21033
|
-
if (!ssh)
|
|
21034
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
21035
|
-
const candidates = config2.remote.runtime === "auto" ? ["podman", "docker"] : [config2.remote.runtime];
|
|
21036
|
-
for (const runtime of candidates) {
|
|
21037
|
-
const versionResult = await ssh.exec(`${runtime} version`, { timeoutMs: 2e4 });
|
|
21038
|
-
if (!versionResult.success)
|
|
21039
|
-
continue;
|
|
21040
|
-
const composeResult = await ssh.exec(`${runtime} compose version`, { timeoutMs: 2e4 });
|
|
21041
|
-
if (composeResult.success) {
|
|
21042
|
-
logger5.info("Remote container runtime detected", { runtime });
|
|
21043
|
-
return ok(runtime);
|
|
21044
|
-
}
|
|
21045
|
-
}
|
|
21046
|
-
return err(
|
|
21047
|
-
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
21048
|
-
deployM("deploy_remoteRuntimeMissing", { params: { runtime: candidates.join(" / ") } })
|
|
21049
|
-
);
|
|
21050
|
-
}
|
|
21051
|
-
async function healthCheck(url2) {
|
|
21052
|
-
for (let attempt = 0; attempt < HEALTH_MAX_ATTEMPTS; attempt++) {
|
|
21053
|
-
try {
|
|
21054
|
-
const response = await fetch(url2, { signal: AbortSignal.timeout(5e3) });
|
|
21055
|
-
if (response.ok)
|
|
21056
|
-
return true;
|
|
21057
|
-
} catch {
|
|
21058
|
-
}
|
|
21059
|
-
await sleep(HEALTH_POLL_INTERVAL);
|
|
21060
|
-
}
|
|
21061
|
-
return false;
|
|
21062
|
-
}
|
|
21063
|
-
const deploy2 = async (context) => {
|
|
21064
|
-
const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
|
|
21065
|
-
const deploymentId = makeDeploymentId();
|
|
21066
|
-
const hostPort = config2.expose.hostPort;
|
|
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
|
-
}
|
|
21091
|
-
const envPath = join(artifactDir, ".env.runtime");
|
|
21092
|
-
const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
|
|
21093
|
-
Object.assign(env, { ...previousEnv, ...env });
|
|
21094
|
-
writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
|
|
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;
|
|
21107
|
-
const composeContent = generateComposeFile({
|
|
21108
|
-
projectName,
|
|
21109
|
-
appVersion: artifact.appVersion,
|
|
21110
|
-
imageRef: app.imageRef,
|
|
21111
|
-
hostPort,
|
|
21112
|
-
containerPort,
|
|
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
|
|
21117
|
-
});
|
|
21118
|
-
const composePath = join(artifactDir, composeName);
|
|
21119
|
-
writeFileSync(composePath, composeContent);
|
|
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 }
|
|
21155
|
-
);
|
|
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}/`));
|
|
21161
|
-
if (!healthy) {
|
|
21162
|
-
return err(
|
|
21163
|
-
HaiDeployError.HEALTH_CHECK_FAILED,
|
|
21164
|
-
deployM("deploy_healthCheckFailed", { params: { url: url2 } })
|
|
21165
|
-
);
|
|
21166
|
-
}
|
|
21167
|
-
const result = {
|
|
21168
|
-
url: url2,
|
|
21169
|
-
deploymentId,
|
|
21170
|
-
status: "ready",
|
|
21171
|
-
provider: "docker-ssh",
|
|
21172
|
-
healthCheck: "passed",
|
|
21173
|
-
envVarsSet: Object.keys(env),
|
|
21174
|
-
bundlePath: bundle.bundlePath
|
|
21175
|
-
};
|
|
21176
|
-
logger5.info("docker-ssh deployment complete", { url: url2, deploymentId });
|
|
21177
|
-
return ok(result);
|
|
21178
|
-
};
|
|
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 };
|
|
21188
|
-
}
|
|
21189
|
-
var logger6 = core.logger.child({ module: "deploy", scope: "provider-vercel" });
|
|
21190
|
-
var VERCEL_API = "https://api.vercel.com";
|
|
21191
|
-
var POLL_INTERVAL = 3e3;
|
|
21192
|
-
var MAX_POLL_ATTEMPTS = 60;
|
|
21193
|
-
var VercelApiError = class extends Error {
|
|
21194
|
-
constructor(status) {
|
|
21195
|
-
super(deployM("deploy_apiError", { params: { service: "Vercel", status: String(status) } }));
|
|
21196
|
-
this.status = status;
|
|
21197
|
-
}
|
|
21198
|
-
status;
|
|
21199
|
-
};
|
|
21200
|
-
function toDeployError(codeDef, messageKey, error62) {
|
|
21201
|
-
return {
|
|
21202
|
-
...codeDef,
|
|
21203
|
-
message: deployM(messageKey, {
|
|
21204
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
21205
|
-
}),
|
|
21206
|
-
cause: error62
|
|
21207
|
-
};
|
|
21208
|
-
}
|
|
21209
|
-
async function vercelFetch(token, path, options) {
|
|
21210
|
-
const url2 = `${VERCEL_API}${path}`;
|
|
21211
|
-
const response = await fetch(url2, {
|
|
21212
|
-
...options,
|
|
21213
|
-
headers: {
|
|
21214
|
-
"Authorization": `Bearer ${token}`,
|
|
21215
|
-
"Content-Type": "application/json",
|
|
21216
|
-
...options?.headers
|
|
21217
|
-
}
|
|
21218
|
-
});
|
|
21219
|
-
if (!response.ok) {
|
|
21220
|
-
throw new VercelApiError(response.status);
|
|
21221
|
-
}
|
|
21222
|
-
return response.json();
|
|
21223
|
-
}
|
|
21224
|
-
function collectFiles(dir, baseDir) {
|
|
21225
|
-
const results = [];
|
|
21226
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
21227
|
-
for (const entry of entries) {
|
|
21228
|
-
const fullPath = join(dir, entry.name);
|
|
21229
|
-
if (entry.isDirectory()) {
|
|
21230
|
-
results.push(...collectFiles(fullPath, baseDir));
|
|
21231
|
-
} else {
|
|
21232
|
-
results.push(relative(baseDir, fullPath).split(sep).join("/"));
|
|
21233
|
-
}
|
|
21234
|
-
}
|
|
21235
|
-
return results;
|
|
21236
|
-
}
|
|
21237
|
-
function createVercelProvider() {
|
|
21238
|
-
let token = null;
|
|
21239
|
-
return {
|
|
21240
|
-
name: "vercel",
|
|
21241
|
-
async authenticate(apiToken) {
|
|
21242
|
-
logger6.debug("Authenticating with Vercel");
|
|
21243
|
-
try {
|
|
21244
|
-
const user = await vercelFetch(
|
|
21245
|
-
apiToken,
|
|
21246
|
-
"/v2/user"
|
|
21247
|
-
);
|
|
21248
|
-
token = apiToken;
|
|
21249
|
-
const username = user.user.username || user.user.email;
|
|
21250
|
-
logger6.info("Vercel authenticated", { user: username });
|
|
21251
|
-
return ok(username);
|
|
21252
|
-
} catch (error62) {
|
|
21253
|
-
logger6.error("Vercel authentication failed", { error: error62 });
|
|
21254
|
-
return err(toDeployError(HaiDeployError.AUTH_FAILED, "deploy_authFailed", error62));
|
|
21255
|
-
}
|
|
21256
|
-
},
|
|
21257
|
-
async createProject(projectName) {
|
|
21258
|
-
if (!token) {
|
|
21259
|
-
return err(
|
|
21260
|
-
HaiDeployError.AUTH_REQUIRED,
|
|
21261
|
-
deployM("deploy_authRequired")
|
|
21262
|
-
);
|
|
21263
|
-
}
|
|
21264
|
-
logger6.debug("Creating Vercel project", { projectName });
|
|
21265
|
-
try {
|
|
21266
|
-
const existingId = await findExistingProject(token, projectName);
|
|
21267
|
-
if (existingId !== null) {
|
|
21268
|
-
logger6.info("Vercel project found", { projectName, projectId: existingId });
|
|
21269
|
-
return ok(existingId);
|
|
21270
|
-
}
|
|
21271
|
-
try {
|
|
21272
|
-
const project = await vercelFetch(
|
|
21273
|
-
token,
|
|
21274
|
-
"/v11/projects",
|
|
21275
|
-
{
|
|
21276
|
-
method: "POST",
|
|
21277
|
-
body: JSON.stringify({
|
|
21278
|
-
name: projectName,
|
|
21279
|
-
framework: "sveltekit"
|
|
21280
|
-
})
|
|
21281
|
-
}
|
|
21282
|
-
);
|
|
21283
|
-
logger6.info("Vercel project created", { projectName, projectId: project.id });
|
|
21284
|
-
return ok(project.id);
|
|
21285
|
-
} catch (error62) {
|
|
21286
|
-
if (error62 instanceof VercelApiError && error62.status === 409) {
|
|
21287
|
-
const concurrentProjectId = await findExistingProject(token, projectName);
|
|
21288
|
-
if (concurrentProjectId !== null) {
|
|
21289
|
-
logger6.info("Vercel project reused after create conflict", {
|
|
21290
|
-
projectName,
|
|
21291
|
-
projectId: concurrentProjectId
|
|
21292
|
-
});
|
|
21293
|
-
return ok(concurrentProjectId);
|
|
21294
|
-
}
|
|
21295
|
-
}
|
|
21296
|
-
throw error62;
|
|
21297
|
-
}
|
|
21298
|
-
} catch (error62) {
|
|
21299
|
-
logger6.error("Vercel project creation failed", { projectName, error: error62 });
|
|
21300
|
-
return err(toDeployError(HaiDeployError.PROJECT_CREATE_FAILED, "deploy_projectCreateFailed", error62));
|
|
21301
|
-
}
|
|
21302
|
-
},
|
|
21303
|
-
async setEnvVars(projectId, envVars) {
|
|
21304
|
-
if (!token) {
|
|
21305
|
-
return err(
|
|
21306
|
-
HaiDeployError.AUTH_REQUIRED,
|
|
21307
|
-
deployM("deploy_authRequired")
|
|
21308
|
-
);
|
|
21309
|
-
}
|
|
21310
|
-
logger6.debug("Setting Vercel env vars", { projectId, count: Object.keys(envVars).length });
|
|
21311
|
-
try {
|
|
21312
|
-
const envEntries = Object.entries(envVars).map(([key, value]) => ({
|
|
21313
|
-
key,
|
|
21314
|
-
value,
|
|
21315
|
-
type: "encrypted",
|
|
21316
|
-
target: ["production", "preview", "development"]
|
|
21317
|
-
}));
|
|
21318
|
-
await vercelFetch(
|
|
21319
|
-
token,
|
|
21320
|
-
`/v10/projects/${projectId}/env?upsert=true`,
|
|
21321
|
-
{
|
|
21322
|
-
method: "POST",
|
|
21323
|
-
body: JSON.stringify(envEntries)
|
|
21324
|
-
}
|
|
21325
|
-
);
|
|
21326
|
-
logger6.info("Vercel env vars set", { projectId, count: envEntries.length });
|
|
21327
|
-
return ok(void 0);
|
|
21328
|
-
} catch (error62) {
|
|
21329
|
-
logger6.error("Vercel env vars setup failed", { projectId, error: error62 });
|
|
21330
|
-
return err(toDeployError(HaiDeployError.ENV_VAR_FAILED, "deploy_envVarFailed", error62));
|
|
21331
|
-
}
|
|
21332
|
-
},
|
|
21333
|
-
async deploy(projectId, outputDir) {
|
|
21334
|
-
if (!token) {
|
|
21335
|
-
return err(
|
|
21336
|
-
HaiDeployError.AUTH_REQUIRED,
|
|
21337
|
-
deployM("deploy_authRequired")
|
|
21338
|
-
);
|
|
21339
|
-
}
|
|
21340
|
-
logger6.debug("Deploying to Vercel", { projectId, outputDir });
|
|
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"));
|
|
21345
|
-
const filePaths = collectFiles(outputDir, outputDir);
|
|
21346
|
-
const files = [];
|
|
21347
|
-
for (const filePath of filePaths) {
|
|
21348
|
-
const fullPath = join(outputDir, filePath);
|
|
21349
|
-
const content = readFileSync(fullPath);
|
|
21350
|
-
const sha = createHash("sha1").update(content).digest("hex");
|
|
21351
|
-
const size = statSync(fullPath).size;
|
|
21352
|
-
await uploadFile(token, content, sha);
|
|
21353
|
-
files.push({ file: `.vercel/output/${filePath}`, sha, size });
|
|
21354
|
-
}
|
|
21355
|
-
logger6.debug("Files uploaded", { count: files.length });
|
|
21356
|
-
const deployment = await vercelFetch(
|
|
21357
|
-
token,
|
|
21358
|
-
"/v13/deployments?prebuilt=1",
|
|
21359
|
-
{
|
|
21360
|
-
method: "POST",
|
|
21361
|
-
body: JSON.stringify({
|
|
21362
|
-
name: projectId,
|
|
21363
|
-
files,
|
|
21364
|
-
version: 2,
|
|
21365
|
-
project: projectId,
|
|
21366
|
-
projectSettings: { framework: null, buildCommand: "", installCommand: "" }
|
|
21367
|
-
})
|
|
21368
|
-
}
|
|
21369
|
-
);
|
|
21370
|
-
const finalStatus = await pollDeploymentStatus(token, deployment.id);
|
|
21371
|
-
const result = {
|
|
21372
|
-
url: `https://${deployment.url}`,
|
|
21373
|
-
deploymentId: deployment.id,
|
|
21374
|
-
status: finalStatus,
|
|
21375
|
-
envVarsSet: []
|
|
21376
|
-
};
|
|
21377
|
-
logger6.info("Vercel deployment complete", {
|
|
21378
|
-
deploymentId: deployment.id,
|
|
21379
|
-
url: result.url,
|
|
21380
|
-
status: finalStatus
|
|
21381
|
-
});
|
|
21382
|
-
return ok(result);
|
|
21383
|
-
} catch (error62) {
|
|
21384
|
-
logger6.error("Vercel deployment failed", { projectId, error: error62 });
|
|
21385
|
-
return err(toDeployError(HaiDeployError.UPLOAD_FAILED, "deploy_uploadFailed", error62));
|
|
21386
|
-
}
|
|
21387
|
-
}
|
|
21388
|
-
};
|
|
21389
|
-
}
|
|
21390
|
-
async function findExistingProject(token, projectName) {
|
|
21391
|
-
try {
|
|
21392
|
-
const project = await vercelFetch(token, `/v10/projects/${projectName}`);
|
|
21393
|
-
return project.id;
|
|
21394
|
-
} catch (error62) {
|
|
21395
|
-
if (error62 instanceof VercelApiError && error62.status === 404) {
|
|
21396
|
-
return null;
|
|
21397
|
-
}
|
|
21398
|
-
throw error62;
|
|
21399
|
-
}
|
|
21400
|
-
}
|
|
21401
|
-
async function uploadFile(token, content, sha) {
|
|
21402
|
-
const url2 = `${VERCEL_API}/v2/files`;
|
|
21403
|
-
const uploadContent = new Uint8Array(content);
|
|
21404
|
-
const response = await fetch(url2, {
|
|
21405
|
-
method: "POST",
|
|
21406
|
-
headers: {
|
|
21407
|
-
"Authorization": `Bearer ${token}`,
|
|
21408
|
-
"Content-Type": "application/octet-stream",
|
|
21409
|
-
"x-vercel-digest": sha
|
|
21410
|
-
},
|
|
21411
|
-
body: new Blob([uploadContent])
|
|
21412
|
-
});
|
|
21413
|
-
if (!response.ok && response.status !== 409) {
|
|
21414
|
-
throw new Error(deployM("deploy_fileUploadFailed", { params: { status: String(response.status) } }));
|
|
21415
|
-
}
|
|
21416
|
-
}
|
|
21417
|
-
async function pollDeploymentStatus(token, deploymentId) {
|
|
21418
|
-
for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
|
|
21419
|
-
const deployment = await vercelFetch(
|
|
21420
|
-
token,
|
|
21421
|
-
`/v13/deployments/${deploymentId}`
|
|
21422
|
-
);
|
|
21423
|
-
if (deployment.readyState === "READY") {
|
|
21424
|
-
return "ready";
|
|
21425
|
-
}
|
|
21426
|
-
if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
|
|
21427
|
-
return "error";
|
|
21428
|
-
}
|
|
21429
|
-
await new Promise((resolve6) => setTimeout(resolve6, POLL_INTERVAL));
|
|
21430
|
-
}
|
|
21431
|
-
return "building";
|
|
21432
|
-
}
|
|
21433
|
-
var logger7 = core.logger.child({ module: "deploy", scope: "provisioner-aliyun" });
|
|
21434
|
-
function createAliyunProvisioner() {
|
|
21435
|
-
let accessKey = null;
|
|
21436
|
-
let secretKey = null;
|
|
21437
|
-
let signName = "";
|
|
21438
|
-
return {
|
|
21439
|
-
name: "aliyun",
|
|
21440
|
-
serviceType: "sms",
|
|
21441
|
-
async authenticate(credentials) {
|
|
21442
|
-
logger7.debug("Authenticating with Aliyun");
|
|
21443
|
-
try {
|
|
21444
|
-
const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
|
|
21445
|
-
const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
|
|
21446
|
-
if (!akId || !akSecret || !credentials.signName) {
|
|
21447
|
-
throw new Error(deployM("deploy_credentialMissing", { params: { fields: "accessKeyId, accessKeySecret, signName" } }));
|
|
21448
|
-
}
|
|
21449
|
-
accessKey = akId;
|
|
21450
|
-
secretKey = akSecret;
|
|
21451
|
-
signName = credentials.signName;
|
|
21452
|
-
logger7.info("Aliyun authenticated", { accessKeyId: `${akId.slice(0, 6)}***` });
|
|
21453
|
-
return ok(akId);
|
|
21454
|
-
} catch (error62) {
|
|
21455
|
-
logger7.error("Aliyun authentication failed", { error: error62 });
|
|
21456
|
-
return err(
|
|
21457
|
-
HaiDeployError.AUTH_FAILED,
|
|
21458
|
-
deployM("deploy_authFailed", {
|
|
21459
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
21460
|
-
}),
|
|
21461
|
-
error62
|
|
21462
|
-
);
|
|
21463
|
-
}
|
|
21464
|
-
},
|
|
21465
|
-
async provision(_appName) {
|
|
21466
|
-
if (!accessKey || !secretKey) {
|
|
21467
|
-
return err(
|
|
21468
|
-
HaiDeployError.AUTH_REQUIRED,
|
|
21469
|
-
deployM("deploy_authRequired")
|
|
21470
|
-
);
|
|
21471
|
-
}
|
|
21472
|
-
logger7.debug("Provisioning Aliyun SMS service");
|
|
21473
|
-
return ok({
|
|
21474
|
-
serviceType: "sms",
|
|
21475
|
-
provisionerName: "aliyun",
|
|
21476
|
-
resourceStatus: "reused",
|
|
21477
|
-
envVars: {
|
|
21478
|
-
HAI_REACH_PROVIDERS: JSON.stringify([{ name: "sms", type: "aliyun-sms", accessKeyId: accessKey, accessKeySecret: secretKey, signName }])
|
|
21479
|
-
},
|
|
21480
|
-
resourceInfo: "aliyun:verify-only"
|
|
21481
|
-
});
|
|
21482
|
-
}
|
|
21483
|
-
};
|
|
21484
|
-
}
|
|
21485
|
-
var logger8 = core.logger.child({ module: "deploy", scope: "provisioner-neon" });
|
|
21486
|
-
var NEON_API = "https://console.neon.tech/api/v2";
|
|
21487
|
-
var DEFAULT_NEON_DATABASE_NAME = "neondb";
|
|
21488
|
-
var DEFAULT_NEON_ROLE_NAME = "neondb_owner";
|
|
21489
|
-
async function neonFetch(token, path, options) {
|
|
21490
|
-
const response = await fetch(`${NEON_API}${path}`, {
|
|
21491
|
-
...options,
|
|
21492
|
-
headers: {
|
|
21493
|
-
"Accept": "application/json",
|
|
21494
|
-
"Authorization": `Bearer ${token}`,
|
|
21495
|
-
"Content-Type": "application/json",
|
|
21496
|
-
...options?.headers
|
|
21497
|
-
}
|
|
21498
|
-
});
|
|
21499
|
-
if (!response.ok) {
|
|
21500
|
-
throw new Error(deployM("deploy_apiError", { params: { service: "Neon", status: String(response.status) } }));
|
|
21501
|
-
}
|
|
21502
|
-
return response.json();
|
|
21503
|
-
}
|
|
21504
|
-
async function listNeonProjects(token) {
|
|
21505
|
-
const data = await neonFetch(token, "/projects");
|
|
21506
|
-
return data.projects ?? [];
|
|
21507
|
-
}
|
|
21508
|
-
async function findExistingNeonProject(token, projectName) {
|
|
21509
|
-
const projects = await listNeonProjects(token);
|
|
21510
|
-
return projects.find((project) => project.name === projectName) ?? null;
|
|
21511
|
-
}
|
|
21512
|
-
async function getNeonConnectionUri(token, projectId) {
|
|
21513
|
-
const query = new URLSearchParams({
|
|
21514
|
-
database_name: DEFAULT_NEON_DATABASE_NAME,
|
|
21515
|
-
role_name: DEFAULT_NEON_ROLE_NAME
|
|
21516
|
-
});
|
|
21517
|
-
const data = await neonFetch(
|
|
21518
|
-
token,
|
|
21519
|
-
`/projects/${projectId}/connection_uri?${query.toString()}`,
|
|
21520
|
-
{ headers: { "Content-Type": "application/json" } }
|
|
21521
|
-
);
|
|
21522
|
-
if (!data.uri) {
|
|
21523
|
-
throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
|
|
21524
|
-
}
|
|
21525
|
-
return data.uri;
|
|
21526
|
-
}
|
|
21527
|
-
function buildNeonProvisionResult(projectId, connectionUri, resourceStatus) {
|
|
21528
|
-
return {
|
|
21529
|
-
serviceType: "db",
|
|
21530
|
-
provisionerName: "neon",
|
|
21531
|
-
resourceStatus,
|
|
21532
|
-
envVars: {
|
|
21533
|
-
HAI_DB: JSON.stringify({ type: "postgresql", url: connectionUri, database: decodeURIComponent(new URL(connectionUri).pathname.slice(1)) })
|
|
21534
|
-
},
|
|
21535
|
-
resourceInfo: `neon-project:${projectId}`
|
|
21536
|
-
};
|
|
21537
|
-
}
|
|
21538
|
-
function createNeonProvisioner() {
|
|
21539
|
-
let token = null;
|
|
21540
|
-
return {
|
|
21541
|
-
name: "neon",
|
|
21542
|
-
serviceType: "db",
|
|
21543
|
-
async authenticate(credentials) {
|
|
21544
|
-
logger8.debug("Authenticating with Neon");
|
|
21545
|
-
try {
|
|
21546
|
-
const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
|
|
21547
|
-
if (!apiToken) {
|
|
21548
|
-
throw new Error(deployM("deploy_credentialMissing", { params: { fields: "token" } }));
|
|
21549
|
-
}
|
|
21550
|
-
await listNeonProjects(apiToken);
|
|
21551
|
-
token = apiToken;
|
|
21552
|
-
logger8.info("Neon authenticated");
|
|
21553
|
-
return ok("neon-user");
|
|
21554
|
-
} catch (error62) {
|
|
21555
|
-
logger8.error("Neon authentication failed", { error: error62 });
|
|
21556
|
-
return err(
|
|
21557
|
-
HaiDeployError.AUTH_FAILED,
|
|
21558
|
-
deployM("deploy_authFailed", {
|
|
21559
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
21560
|
-
}),
|
|
21561
|
-
error62
|
|
21562
|
-
);
|
|
21563
|
-
}
|
|
21564
|
-
},
|
|
21565
|
-
async provision(appName) {
|
|
21566
|
-
if (!token) {
|
|
21567
|
-
return err(
|
|
21568
|
-
HaiDeployError.AUTH_REQUIRED,
|
|
21569
|
-
deployM("deploy_authRequired")
|
|
21570
|
-
);
|
|
21571
|
-
}
|
|
21572
|
-
logger8.debug("Provisioning Neon database", { appName });
|
|
21573
|
-
let resource;
|
|
21574
|
-
try {
|
|
21575
|
-
const projectName = `${appName}-db`;
|
|
21576
|
-
const existingProject = await findExistingNeonProject(token, projectName);
|
|
21577
|
-
if (existingProject !== null) {
|
|
21578
|
-
resource = { resourceInfo: `neon-project:${existingProject.id}`, resourceStatus: "reused" };
|
|
21579
|
-
const connectionUri2 = await getNeonConnectionUri(token, existingProject.id);
|
|
21580
|
-
logger8.info("Neon project reused", {
|
|
21581
|
-
projectId: existingProject.id,
|
|
21582
|
-
projectName
|
|
21583
|
-
});
|
|
21584
|
-
return ok(buildNeonProvisionResult(existingProject.id, connectionUri2, "reused"));
|
|
21585
|
-
}
|
|
21586
|
-
const data = await neonFetch(token, "/projects", {
|
|
21587
|
-
method: "POST",
|
|
21588
|
-
body: JSON.stringify({
|
|
21589
|
-
project: {
|
|
21590
|
-
name: projectName,
|
|
21591
|
-
pg_version: 16
|
|
21592
|
-
}
|
|
21593
|
-
})
|
|
21594
|
-
});
|
|
21595
|
-
const projectId = data.project?.id ?? "";
|
|
21596
|
-
if (projectId)
|
|
21597
|
-
resource = { resourceInfo: `neon-project:${projectId}`, resourceStatus: "created" };
|
|
21598
|
-
const connectionUri = data.connection_uris?.[0]?.connection_uri ?? "";
|
|
21599
|
-
if (!projectId || !connectionUri) {
|
|
21600
|
-
throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Neon" } }));
|
|
21601
|
-
}
|
|
21602
|
-
logger8.info("Neon database provisioned", { projectId });
|
|
21603
|
-
return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
|
|
21604
|
-
} catch (error62) {
|
|
21605
|
-
logger8.error("Neon provisioning failed", { appName });
|
|
21606
|
-
if (resource) {
|
|
21607
|
-
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
21608
|
-
projectName: appName,
|
|
21609
|
-
stage: "provision:db",
|
|
21610
|
-
resources: [{ serviceType: "db", provisionerName: "neon", ...resource }]
|
|
21611
|
-
});
|
|
21612
|
-
}
|
|
21613
|
-
return err(
|
|
21614
|
-
HaiDeployError.PROVISION_FAILED,
|
|
21615
|
-
deployM("deploy_provisionFailed", {
|
|
21616
|
-
params: {
|
|
21617
|
-
service: "neon",
|
|
21618
|
-
error: error62 instanceof Error ? error62.message : String(error62)
|
|
21619
|
-
}
|
|
21620
|
-
}),
|
|
21621
|
-
error62
|
|
21622
|
-
);
|
|
21623
|
-
}
|
|
21624
|
-
}
|
|
21625
|
-
};
|
|
21626
|
-
}
|
|
21627
|
-
var logger9 = core.logger.child({ module: "deploy", scope: "provisioner-r2" });
|
|
21628
|
-
var CF_API = "https://api.cloudflare.com/client/v4";
|
|
21629
|
-
function createR2Provisioner() {
|
|
21630
|
-
let token = null;
|
|
21631
|
-
let accountId = null;
|
|
21632
|
-
let accessKey = "";
|
|
21633
|
-
let secretKey = "";
|
|
21634
|
-
return {
|
|
21635
|
-
name: "r2",
|
|
21636
|
-
serviceType: "storage",
|
|
21637
|
-
async authenticate(credentials) {
|
|
21638
|
-
logger9.debug("Authenticating with Cloudflare");
|
|
21639
|
-
try {
|
|
21640
|
-
const acctId = credentials.accountId ?? credentials.account_id ?? "";
|
|
21641
|
-
const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
|
|
21642
|
-
if (!acctId || !apiTok || !credentials.accessKeyId || !credentials.secretAccessKey) {
|
|
21643
|
-
throw new Error(deployM("deploy_credentialMissing", { params: { fields: "accountId, apiToken, accessKeyId, secretAccessKey" } }));
|
|
21644
|
-
}
|
|
21645
|
-
const res = await fetch(`${CF_API}/accounts/${acctId}/r2/buckets`, {
|
|
21646
|
-
headers: { Authorization: `Bearer ${apiTok}` }
|
|
21647
|
-
});
|
|
21648
|
-
if (!res.ok) {
|
|
21649
|
-
throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(res.status) } }));
|
|
21650
|
-
}
|
|
21651
|
-
accountId = acctId;
|
|
21652
|
-
token = apiTok;
|
|
21653
|
-
accessKey = credentials.accessKeyId;
|
|
21654
|
-
secretKey = credentials.secretAccessKey;
|
|
21655
|
-
logger9.info("Cloudflare R2 authenticated", { accountId: acctId });
|
|
21656
|
-
return ok(acctId);
|
|
21657
|
-
} catch (error62) {
|
|
21658
|
-
logger9.error("Cloudflare R2 authentication failed", { error: error62 });
|
|
21659
|
-
return err(
|
|
21660
|
-
HaiDeployError.AUTH_FAILED,
|
|
21661
|
-
deployM("deploy_authFailed", {
|
|
21662
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
21663
|
-
}),
|
|
21664
|
-
error62
|
|
21665
|
-
);
|
|
21666
|
-
}
|
|
21667
|
-
},
|
|
21668
|
-
async provision(appName) {
|
|
21669
|
-
if (!token || !accountId) {
|
|
21670
|
-
return err(
|
|
21671
|
-
HaiDeployError.AUTH_REQUIRED,
|
|
21672
|
-
deployM("deploy_authRequired")
|
|
21673
|
-
);
|
|
21674
|
-
}
|
|
21675
|
-
const bucketName = `${appName}-storage`.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
21676
|
-
logger9.debug("Provisioning R2 bucket", { appName, bucketName });
|
|
21677
|
-
try {
|
|
21678
|
-
const createRes = await fetch(
|
|
21679
|
-
`${CF_API}/accounts/${accountId}/r2/buckets`,
|
|
21680
|
-
{
|
|
21681
|
-
method: "POST",
|
|
21682
|
-
headers: {
|
|
21683
|
-
"Authorization": `Bearer ${token}`,
|
|
21684
|
-
"Content-Type": "application/json"
|
|
21685
|
-
},
|
|
21686
|
-
body: JSON.stringify({ name: bucketName })
|
|
21687
|
-
}
|
|
21688
|
-
);
|
|
21689
|
-
if (!createRes.ok && createRes.status !== 409) {
|
|
21690
|
-
throw new Error(deployM("deploy_apiError", { params: { service: "Cloudflare", status: String(createRes.status) } }));
|
|
21691
|
-
}
|
|
21692
|
-
logger9.info("R2 bucket provisioned", { bucketName });
|
|
21693
|
-
return ok({
|
|
21694
|
-
serviceType: "storage",
|
|
21695
|
-
provisionerName: "r2",
|
|
21696
|
-
resourceStatus: createRes.status === 409 ? "reused" : "created",
|
|
21697
|
-
envVars: {
|
|
21698
|
-
HAI_STORAGE: JSON.stringify({
|
|
21699
|
-
type: "s3",
|
|
21700
|
-
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
|
|
21701
|
-
bucket: bucketName,
|
|
21702
|
-
region: "auto",
|
|
21703
|
-
accessKeyId: accessKey,
|
|
21704
|
-
secretAccessKey: secretKey
|
|
21705
|
-
})
|
|
21706
|
-
},
|
|
21707
|
-
resourceInfo: `r2-bucket:${bucketName}`
|
|
21708
|
-
});
|
|
21709
|
-
} catch (error62) {
|
|
21710
|
-
logger9.error("R2 provisioning failed", { appName, error: error62 });
|
|
21711
|
-
return err(
|
|
21712
|
-
HaiDeployError.PROVISION_FAILED,
|
|
21713
|
-
deployM("deploy_provisionFailed", {
|
|
21714
|
-
params: {
|
|
21715
|
-
service: "r2",
|
|
21716
|
-
error: error62 instanceof Error ? error62.message : String(error62)
|
|
21717
|
-
}
|
|
21718
|
-
}),
|
|
21719
|
-
error62
|
|
21720
|
-
);
|
|
21721
|
-
}
|
|
21722
|
-
}
|
|
21723
|
-
};
|
|
21724
|
-
}
|
|
21725
|
-
var logger10 = core.logger.child({ module: "deploy", scope: "provisioner-resend" });
|
|
21726
|
-
var RESEND_API = "https://api.resend.com";
|
|
21727
|
-
function createResendProvisioner() {
|
|
21728
|
-
let token = null;
|
|
21729
|
-
let from = "";
|
|
21730
|
-
return {
|
|
21731
|
-
name: "resend",
|
|
21732
|
-
serviceType: "email",
|
|
21733
|
-
async authenticate(credentials) {
|
|
21734
|
-
logger10.debug("Authenticating with Resend");
|
|
21735
|
-
try {
|
|
21736
|
-
const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
21737
|
-
if (!apiToken || !credentials.from) {
|
|
21738
|
-
throw new Error(deployM("deploy_credentialMissing", { params: { fields: "apiKey, from" } }));
|
|
21739
|
-
}
|
|
21740
|
-
const res = await fetch(`${RESEND_API}/domains`, {
|
|
21741
|
-
headers: { Authorization: `Bearer ${apiToken}` }
|
|
21742
|
-
});
|
|
21743
|
-
if (!res.ok) {
|
|
21744
|
-
throw new Error(deployM("deploy_apiError", { params: { service: "Resend", status: String(res.status) } }));
|
|
21745
|
-
}
|
|
21746
|
-
token = apiToken;
|
|
21747
|
-
from = credentials.from;
|
|
21748
|
-
logger10.info("Resend authenticated");
|
|
21749
|
-
return ok("resend-user");
|
|
21750
|
-
} catch (error62) {
|
|
21751
|
-
logger10.error("Resend authentication failed", { error: error62 });
|
|
21752
|
-
return err(
|
|
21753
|
-
HaiDeployError.AUTH_FAILED,
|
|
21754
|
-
deployM("deploy_authFailed", {
|
|
21755
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
21756
|
-
}),
|
|
21757
|
-
error62
|
|
21758
|
-
);
|
|
21759
|
-
}
|
|
21760
|
-
},
|
|
21761
|
-
async provision(_appName) {
|
|
21762
|
-
if (!token) {
|
|
21763
|
-
return err(
|
|
21764
|
-
HaiDeployError.AUTH_REQUIRED,
|
|
21765
|
-
deployM("deploy_authRequired")
|
|
21766
|
-
);
|
|
21767
|
-
}
|
|
21768
|
-
logger10.debug("Provisioning Resend email service");
|
|
21769
|
-
return ok({
|
|
21770
|
-
serviceType: "email",
|
|
21771
|
-
provisionerName: "resend",
|
|
21772
|
-
resourceStatus: "reused",
|
|
21773
|
-
envVars: {
|
|
21774
|
-
HAI_REACH_PROVIDERS: JSON.stringify([{ name: "email", type: "smtp", host: "smtp.resend.com", port: 465, secure: true, user: "resend", pass: token, from }])
|
|
21775
|
-
},
|
|
21776
|
-
resourceInfo: "resend:verify-only"
|
|
21777
|
-
});
|
|
21778
|
-
}
|
|
21779
|
-
};
|
|
21780
|
-
}
|
|
21781
|
-
var logger11 = core.logger.child({ module: "deploy", scope: "provisioner-upstash" });
|
|
21782
|
-
var UPSTASH_API = "https://api.upstash.com";
|
|
21783
|
-
function createUpstashAuthHeader(email3, apiKey) {
|
|
21784
|
-
return `Basic ${Buffer.from(`${email3}:${apiKey}`).toString("base64")}`;
|
|
21785
|
-
}
|
|
21786
|
-
async function upstashFetch(email3, apiKey, path, options) {
|
|
21787
|
-
const response = await fetch(`${UPSTASH_API}${path}`, {
|
|
21788
|
-
...options,
|
|
21789
|
-
headers: {
|
|
21790
|
-
"Authorization": createUpstashAuthHeader(email3, apiKey),
|
|
21791
|
-
"Content-Type": "application/json",
|
|
21792
|
-
...options?.headers
|
|
21793
|
-
}
|
|
21794
|
-
});
|
|
21795
|
-
if (!response.ok) {
|
|
21796
|
-
throw new Error(deployM("deploy_apiError", { params: { service: "Upstash", status: String(response.status) } }));
|
|
21797
|
-
}
|
|
21798
|
-
return response.json();
|
|
21799
|
-
}
|
|
21800
|
-
async function listUpstashDatabases(email3, apiKey) {
|
|
21801
|
-
return upstashFetch(email3, apiKey, "/v2/redis/databases");
|
|
21802
|
-
}
|
|
21803
|
-
async function findExistingUpstashDatabase(email3, apiKey, databaseName) {
|
|
21804
|
-
const databases = await listUpstashDatabases(email3, apiKey);
|
|
21805
|
-
return databases.find((database) => database.database_name === databaseName) ?? null;
|
|
21806
|
-
}
|
|
21807
|
-
async function getUpstashDatabase(email3, apiKey, databaseId) {
|
|
21808
|
-
return upstashFetch(email3, apiKey, `/v2/redis/database/${databaseId}`);
|
|
21809
|
-
}
|
|
21810
|
-
function buildUpstashProvisionResult(database, resourceStatus) {
|
|
21811
|
-
const databaseId = database.database_id ?? "";
|
|
21812
|
-
if (!databaseId || !database.endpoint || !database.password || !database.port) {
|
|
21813
|
-
throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
|
|
21814
|
-
}
|
|
21815
|
-
return {
|
|
21816
|
-
serviceType: "cache",
|
|
21817
|
-
provisionerName: "upstash",
|
|
21818
|
-
resourceStatus,
|
|
21819
|
-
envVars: {
|
|
21820
|
-
HAI_CACHE: JSON.stringify({ type: "redis", url: `rediss://default:${encodeURIComponent(database.password)}@${database.endpoint}:${database.port}` })
|
|
21821
|
-
},
|
|
21822
|
-
resourceInfo: `upstash-db:${databaseId}`
|
|
21823
|
-
};
|
|
21824
|
-
}
|
|
21825
|
-
function createUpstashProvisioner() {
|
|
21826
|
-
let apiKey = null;
|
|
21827
|
-
let email3 = null;
|
|
21828
|
-
return {
|
|
21829
|
-
name: "upstash",
|
|
21830
|
-
serviceType: "cache",
|
|
21831
|
-
async authenticate(credentials) {
|
|
21832
|
-
logger11.debug("Authenticating with Upstash");
|
|
21833
|
-
try {
|
|
21834
|
-
const userEmail = credentials.email ?? "";
|
|
21835
|
-
const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
21836
|
-
if (!userEmail || !userKey) {
|
|
21837
|
-
throw new Error(deployM("deploy_credentialMissing", { params: { fields: "email, api_key" } }));
|
|
21838
|
-
}
|
|
21839
|
-
await listUpstashDatabases(userEmail, userKey);
|
|
21840
|
-
email3 = userEmail;
|
|
21841
|
-
apiKey = userKey;
|
|
21842
|
-
logger11.info("Upstash authenticated", { email: userEmail });
|
|
21843
|
-
return ok(userEmail);
|
|
21844
|
-
} catch (error62) {
|
|
21845
|
-
logger11.error("Upstash authentication failed", { error: error62 });
|
|
21846
|
-
return err(
|
|
21847
|
-
HaiDeployError.AUTH_FAILED,
|
|
21848
|
-
deployM("deploy_authFailed", {
|
|
21849
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
21850
|
-
}),
|
|
21851
|
-
error62
|
|
21852
|
-
);
|
|
21853
|
-
}
|
|
21854
|
-
},
|
|
21855
|
-
async provision(appName) {
|
|
21856
|
-
if (!apiKey || !email3) {
|
|
21857
|
-
return err(
|
|
21858
|
-
HaiDeployError.AUTH_REQUIRED,
|
|
21859
|
-
deployM("deploy_authRequired")
|
|
21860
|
-
);
|
|
21861
|
-
}
|
|
21862
|
-
logger11.debug("Provisioning Upstash Redis", { appName });
|
|
21863
|
-
let resource;
|
|
21864
|
-
try {
|
|
21865
|
-
const databaseName = `${appName}-cache`;
|
|
21866
|
-
const existingDatabase = await findExistingUpstashDatabase(email3, apiKey, databaseName);
|
|
21867
|
-
if (existingDatabase !== null) {
|
|
21868
|
-
const databaseId = existingDatabase.database_id ?? "";
|
|
21869
|
-
if (!databaseId) {
|
|
21870
|
-
throw new Error(deployM("deploy_provisionNoResult", { params: { service: "Upstash" } }));
|
|
21871
|
-
}
|
|
21872
|
-
resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
|
|
21873
|
-
const detail = await getUpstashDatabase(email3, apiKey, databaseId);
|
|
21874
|
-
logger11.info("Upstash Redis reused", { databaseId, databaseName });
|
|
21875
|
-
return ok(buildUpstashProvisionResult({ ...existingDatabase, ...detail }, "reused"));
|
|
21876
|
-
}
|
|
21877
|
-
const data = await upstashFetch(email3, apiKey, "/v2/redis/database", {
|
|
21878
|
-
method: "POST",
|
|
21879
|
-
body: JSON.stringify({
|
|
21880
|
-
name: databaseName,
|
|
21881
|
-
region: "global",
|
|
21882
|
-
tls: true
|
|
21883
|
-
})
|
|
21884
|
-
});
|
|
21885
|
-
if (data.database_id)
|
|
21886
|
-
resource = { resourceInfo: `upstash-db:${data.database_id}`, resourceStatus: "created" };
|
|
21887
|
-
logger11.info("Upstash Redis provisioned", { databaseId: data.database_id });
|
|
21888
|
-
return ok(buildUpstashProvisionResult(data, "created"));
|
|
21889
|
-
} catch (error62) {
|
|
21890
|
-
logger11.error("Upstash provisioning failed", { appName });
|
|
21891
|
-
if (resource) {
|
|
21892
|
-
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
21893
|
-
projectName: appName,
|
|
21894
|
-
stage: "provision:cache",
|
|
21895
|
-
resources: [{ serviceType: "cache", provisionerName: "upstash", ...resource }]
|
|
21896
|
-
});
|
|
21897
|
-
}
|
|
21898
|
-
return err(
|
|
21899
|
-
HaiDeployError.PROVISION_FAILED,
|
|
21900
|
-
deployM("deploy_provisionFailed", {
|
|
21901
|
-
params: {
|
|
21902
|
-
service: "upstash",
|
|
21903
|
-
error: error62 instanceof Error ? error62.message : String(error62)
|
|
21904
|
-
}
|
|
21905
|
-
}),
|
|
21906
|
-
error62
|
|
21907
|
-
);
|
|
21908
|
-
}
|
|
21909
|
-
}
|
|
21910
|
-
};
|
|
21911
|
-
}
|
|
21912
|
-
|
|
21913
|
-
// ../deploy/src/deploy-main.ts
|
|
21914
|
-
var logger12 = core.logger.child({ module: "deploy", scope: "main" });
|
|
21915
|
-
var currentConfig = null;
|
|
21916
|
-
var currentProvider = null;
|
|
21917
|
-
var currentContainerProvider = null;
|
|
21918
|
-
var currentProvisioners = /* @__PURE__ */ new Map();
|
|
21919
|
-
var initInProgress = false;
|
|
21920
|
-
var notInitialized = core.module.createNotInitializedKit(
|
|
21921
|
-
HaiDeployError.NOT_INITIALIZED,
|
|
21922
|
-
() => deployM("deploy_notInitialized")
|
|
21923
|
-
);
|
|
21924
|
-
function createProviderByType(type) {
|
|
21925
|
-
switch (type) {
|
|
21926
|
-
case "vercel":
|
|
21927
|
-
return createVercelProvider();
|
|
21928
|
-
default:
|
|
21929
|
-
throw new Error(deployM("deploy_unsupportedProviderType", { params: { type } }));
|
|
21930
|
-
}
|
|
21931
|
-
}
|
|
21932
|
-
function createProvisionerByName(provisioner) {
|
|
21933
|
-
switch (provisioner) {
|
|
21934
|
-
case "neon":
|
|
21935
|
-
return createNeonProvisioner();
|
|
21936
|
-
case "upstash":
|
|
21937
|
-
return createUpstashProvisioner();
|
|
21938
|
-
case "cloudflare-r2":
|
|
21939
|
-
return createR2Provisioner();
|
|
21940
|
-
case "resend":
|
|
21941
|
-
return createResendProvisioner();
|
|
21942
|
-
case "aliyun":
|
|
21943
|
-
return createAliyunProvisioner();
|
|
21944
|
-
default:
|
|
21945
|
-
throw new Error(deployM("deploy_unsupportedProvisionerType", { params: { type: provisioner } }));
|
|
21946
|
-
}
|
|
21947
|
-
}
|
|
21948
|
-
function extractCredentials(serviceConfig) {
|
|
21949
|
-
const result = {};
|
|
21950
|
-
for (const [key, value] of Object.entries(serviceConfig)) {
|
|
21951
|
-
if (key !== "provisioner" && typeof value === "string") {
|
|
21952
|
-
result[key] = value;
|
|
21953
|
-
}
|
|
21954
|
-
}
|
|
21955
|
-
return result;
|
|
21956
|
-
}
|
|
21957
|
-
var deployCredentials = {
|
|
21958
|
-
getPath: () => getCredentialsPath(),
|
|
21959
|
-
load: () => loadCredentials(),
|
|
21960
|
-
save: (key, value) => saveCredential(key, value),
|
|
21961
|
-
saveAll: (entries) => saveCredentials(entries)
|
|
21962
|
-
};
|
|
21963
|
-
function normalizeProjectName(name) {
|
|
21964
|
-
const normalized = name.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
21965
|
-
return normalized || "app";
|
|
21966
|
-
}
|
|
21967
|
-
function mergeProvisionEnv(results, scan, options) {
|
|
21968
|
-
let env = {};
|
|
21969
|
-
const reachRequirements = scan?.middlewares?.filter((item) => item.configName === "reach") ?? [];
|
|
21970
|
-
const reachProviders = reachRequirements.map((item) => item.config);
|
|
21971
|
-
let hasReachOutput = false;
|
|
21972
|
-
for (const prov of results) {
|
|
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
|
-
}
|
|
21989
|
-
env = { ...env, ...prov.envVars };
|
|
21990
|
-
}
|
|
21991
|
-
if (hasReachOutput)
|
|
21992
|
-
env.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
|
|
21993
|
-
return env;
|
|
21994
|
-
}
|
|
21995
|
-
var deploy = {
|
|
21996
|
-
credentials: deployCredentials,
|
|
21997
|
-
async init(config2) {
|
|
21998
|
-
if (initInProgress) {
|
|
21999
|
-
logger12.warn("Deploy init already in progress, skipping concurrent call");
|
|
22000
|
-
return err(
|
|
22001
|
-
HaiDeployError.CONFIG_ERROR,
|
|
22002
|
-
deployM("deploy_configError", { params: { error: "init already in progress" } })
|
|
22003
|
-
);
|
|
22004
|
-
}
|
|
22005
|
-
initInProgress = true;
|
|
22006
|
-
try {
|
|
22007
|
-
if (currentProvider || currentContainerProvider) {
|
|
22008
|
-
logger12.warn("Deploy module is already initialized, reinitializing");
|
|
22009
|
-
await deploy.close();
|
|
22010
|
-
}
|
|
22011
|
-
logger12.info("Initializing deploy module");
|
|
22012
|
-
const parseResult = DeployConfigSchema.safeParse(config2);
|
|
22013
|
-
if (!parseResult.success) {
|
|
22014
|
-
logger12.error("Deploy config validation failed", { error: parseResult.error.message });
|
|
22015
|
-
return err(
|
|
22016
|
-
HaiDeployError.CONFIG_ERROR,
|
|
22017
|
-
deployM("deploy_configError", { params: { error: parseResult.error.message } }),
|
|
22018
|
-
parseResult.error
|
|
22019
|
-
);
|
|
22020
|
-
}
|
|
22021
|
-
const parsed = parseResult.data;
|
|
22022
|
-
try {
|
|
22023
|
-
let provider = null;
|
|
22024
|
-
let containerProvider = null;
|
|
22025
|
-
if (parsed.provider.type === "docker-ssh") {
|
|
22026
|
-
containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
|
|
22027
|
-
logger12.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh?.host });
|
|
22028
|
-
} else {
|
|
22029
|
-
provider = createProviderByType(parsed.provider.type);
|
|
22030
|
-
const authResult = await provider.authenticate(parsed.provider.token);
|
|
22031
|
-
if (!authResult.success) {
|
|
22032
|
-
return err(authResult.error);
|
|
22033
|
-
}
|
|
22034
|
-
logger12.info("Provider authenticated", { provider: parsed.provider.type, user: authResult.data });
|
|
22035
|
-
}
|
|
22036
|
-
currentProvisioners.clear();
|
|
22037
|
-
if (parsed.services) {
|
|
22038
|
-
const serviceEntries = Object.entries(parsed.services);
|
|
22039
|
-
for (const [serviceType, serviceConfig] of serviceEntries) {
|
|
22040
|
-
if (!serviceConfig)
|
|
22041
|
-
continue;
|
|
22042
|
-
const provisioner = createProvisionerByName(serviceConfig.provisioner);
|
|
22043
|
-
const credentials = extractCredentials(serviceConfig);
|
|
22044
|
-
const provAuthResult = await provisioner.authenticate(credentials);
|
|
22045
|
-
if (!provAuthResult.success) {
|
|
22046
|
-
logger12.error("Provisioner authentication failed", {
|
|
22047
|
-
service: serviceType,
|
|
22048
|
-
provisioner: serviceConfig.provisioner
|
|
22049
|
-
});
|
|
22050
|
-
currentProvisioners.clear();
|
|
22051
|
-
return err(provAuthResult.error);
|
|
22052
|
-
}
|
|
22053
|
-
logger12.info("Provisioner authenticated", {
|
|
22054
|
-
service: serviceType,
|
|
22055
|
-
user: provAuthResult.data
|
|
22056
|
-
});
|
|
22057
|
-
currentProvisioners.set(serviceType, provisioner);
|
|
22058
|
-
}
|
|
22059
|
-
}
|
|
22060
|
-
currentProvider = provider;
|
|
22061
|
-
currentContainerProvider = containerProvider;
|
|
22062
|
-
currentConfig = parsed;
|
|
22063
|
-
logger12.info("Deploy module initialized", {
|
|
22064
|
-
provider: parsed.provider.type,
|
|
22065
|
-
provisioners: [...currentProvisioners.keys()]
|
|
22066
|
-
});
|
|
22067
|
-
return ok(void 0);
|
|
22068
|
-
} catch (error62) {
|
|
22069
|
-
currentProvider = null;
|
|
22070
|
-
currentContainerProvider = null;
|
|
22071
|
-
currentConfig = null;
|
|
22072
|
-
currentProvisioners.clear();
|
|
22073
|
-
logger12.error("Deploy module initialization failed", { error: error62 });
|
|
22074
|
-
return err(
|
|
22075
|
-
HaiDeployError.CONFIG_ERROR,
|
|
22076
|
-
deployM("deploy_configError", {
|
|
22077
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
22078
|
-
}),
|
|
22079
|
-
error62
|
|
22080
|
-
);
|
|
22081
|
-
}
|
|
22082
|
-
} finally {
|
|
22083
|
-
initInProgress = false;
|
|
22084
|
-
}
|
|
22085
|
-
},
|
|
22086
|
-
async close() {
|
|
22087
|
-
if (!currentProvider && !currentContainerProvider) {
|
|
22088
|
-
logger12.info("Deploy module already closed, skipping");
|
|
22089
|
-
return;
|
|
22090
|
-
}
|
|
22091
|
-
logger12.info("Closing deploy module");
|
|
22092
|
-
currentProvider = null;
|
|
22093
|
-
currentContainerProvider = null;
|
|
22094
|
-
currentConfig = null;
|
|
22095
|
-
currentProvisioners.clear();
|
|
22096
|
-
logger12.info("Deploy module closed");
|
|
22097
|
-
},
|
|
22098
|
-
get config() {
|
|
22099
|
-
return currentConfig;
|
|
22100
|
-
},
|
|
22101
|
-
get isInitialized() {
|
|
22102
|
-
return currentProvider !== null || currentContainerProvider !== null;
|
|
22103
|
-
},
|
|
22104
|
-
async scan(appDir, options) {
|
|
22105
|
-
logger12.debug("Scanning application", { appDir });
|
|
22106
|
-
return scanApp(appDir, options);
|
|
22107
|
-
},
|
|
22108
|
-
async provisionAll(projectName, services) {
|
|
22109
|
-
if (!currentProvider && !currentContainerProvider) {
|
|
22110
|
-
return notInitialized.result();
|
|
22111
|
-
}
|
|
22112
|
-
logger12.info("Provisioning all services", { projectName, count: currentProvisioners.size });
|
|
22113
|
-
const results = [];
|
|
22114
|
-
for (const [serviceType, provisioner] of currentProvisioners) {
|
|
22115
|
-
if (services && !services.includes(serviceType))
|
|
22116
|
-
continue;
|
|
22117
|
-
logger12.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
|
|
22118
|
-
const provResult = await provisioner.provision(projectName);
|
|
22119
|
-
if (!provResult.success) {
|
|
22120
|
-
logger12.error("Service provisioning failed", {
|
|
22121
|
-
serviceType,
|
|
22122
|
-
provisioner: provisioner.name,
|
|
22123
|
-
code: provResult.error.code
|
|
22124
|
-
});
|
|
22125
|
-
return recoveryFailure(provResult.error, {
|
|
22126
|
-
projectName,
|
|
22127
|
-
stage: `provision:${serviceType}`,
|
|
22128
|
-
resources: [
|
|
22129
|
-
...results.map(({ envVars: _envVars, ...resource }) => resource),
|
|
22130
|
-
...getDeployRecovery(provResult.error)?.resources ?? [
|
|
22131
|
-
{ serviceType, provisionerName: provisioner.name, resourceInfo: projectName, resourceStatus: "unknown" }
|
|
22132
|
-
]
|
|
22133
|
-
]
|
|
22134
|
-
});
|
|
22135
|
-
}
|
|
22136
|
-
results.push(provResult.data);
|
|
22137
|
-
logger12.info("Service provisioned", { serviceType, provisioner: provisioner.name });
|
|
22138
|
-
}
|
|
22139
|
-
return ok(results);
|
|
22140
|
-
},
|
|
22141
|
-
async deployApp(appDir, options) {
|
|
22142
|
-
if (!currentProvider && !currentContainerProvider) {
|
|
22143
|
-
return notInitialized.result();
|
|
22144
|
-
}
|
|
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);
|
|
22148
|
-
if (!scanResult.success) {
|
|
22149
|
-
return err(scanResult.error);
|
|
22150
|
-
}
|
|
22151
|
-
const scan = scanResult.data;
|
|
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
|
-
};
|
|
22163
|
-
if (currentContainerProvider) {
|
|
22164
|
-
return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
|
|
22165
|
-
}
|
|
22166
|
-
if (options?.bundleOnly)
|
|
22167
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleOnlyContainer"));
|
|
22168
|
-
if (!currentProvider) {
|
|
22169
|
-
return notInitialized.result();
|
|
22170
|
-
}
|
|
22171
|
-
if (scan.isSvelteKit && !scan.adapterInstalled) {
|
|
22172
|
-
return err(
|
|
22173
|
-
HaiDeployError.ADAPTER_MISSING,
|
|
22174
|
-
deployM("deploy_adapterMissing", {
|
|
22175
|
-
params: { adapter: "@sveltejs/adapter-vercel" }
|
|
22176
|
-
})
|
|
22177
|
-
);
|
|
22178
|
-
}
|
|
22179
|
-
if (!existsSync(resolveOutputDir(appDir, "vercel")))
|
|
22180
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
22181
|
-
let allEnvVars = appEnv;
|
|
22182
|
-
const recovery = { projectName, stage: "provision", resources: [] };
|
|
22183
|
-
if (!options?.skipMiddleware) {
|
|
22184
|
-
const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
|
|
22185
|
-
if (!provResults.success) {
|
|
22186
|
-
return err(provResults.error);
|
|
22187
|
-
}
|
|
22188
|
-
recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
|
|
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
|
-
}
|
|
22195
|
-
}
|
|
22196
|
-
const projectResult = await currentProvider.createProject(projectName);
|
|
22197
|
-
if (!projectResult.success) {
|
|
22198
|
-
return recoveryFailure(projectResult.error, { ...recovery, stage: "create-project" });
|
|
22199
|
-
}
|
|
22200
|
-
const projectId = projectResult.data;
|
|
22201
|
-
recovery.platformProjectId = projectId;
|
|
22202
|
-
if (Object.keys(allEnvVars).length > 0) {
|
|
22203
|
-
const envResult = await currentProvider.setEnvVars(projectId, allEnvVars);
|
|
22204
|
-
if (!envResult.success) {
|
|
22205
|
-
return recoveryFailure(envResult.error, { ...recovery, stage: "set-env" });
|
|
22206
|
-
}
|
|
22207
|
-
}
|
|
22208
|
-
const outputDir = resolveOutputDir(appDir, currentConfig?.provider.type ?? "vercel");
|
|
22209
|
-
const deployResult = await currentProvider.deploy(projectId, outputDir);
|
|
22210
|
-
if (!deployResult.success) {
|
|
22211
|
-
return recoveryFailure(deployResult.error, { ...recovery, stage: "deploy" });
|
|
22212
|
-
}
|
|
22213
|
-
const result = {
|
|
22214
|
-
...deployResult.data,
|
|
22215
|
-
envVarsSet: Object.keys(allEnvVars)
|
|
22216
|
-
};
|
|
22217
|
-
logger12.info("Deployment completed", {
|
|
22218
|
-
url: result.url,
|
|
22219
|
-
deploymentId: result.deploymentId,
|
|
22220
|
-
status: result.status
|
|
22221
|
-
});
|
|
22222
|
-
return ok(result);
|
|
22223
|
-
}
|
|
22224
|
-
};
|
|
22225
|
-
async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
|
|
22226
|
-
const projectName = normalizeProjectName(rawProjectName);
|
|
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"));
|
|
22239
|
-
}
|
|
22240
|
-
if (!options?.bundleOnly && currentConfig?.provider.type === "docker-ssh" && !currentConfig.provider.ssh)
|
|
22241
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
22242
|
-
let provisionEnv = {};
|
|
22243
|
-
if (!options?.skipMiddleware) {
|
|
22244
|
-
const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
|
|
22245
|
-
if (!provResults.success) {
|
|
22246
|
-
return err(provResults.error);
|
|
22247
|
-
}
|
|
22248
|
-
provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
|
|
22249
|
-
}
|
|
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);
|
|
22296
|
-
}
|
|
22297
|
-
|
|
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
|
|
19661
|
+
export { external_exports };
|
|
19662
|
+
//# sourceMappingURL=chunk-FI56GONY.js.map
|
|
19663
|
+
//# sourceMappingURL=chunk-FI56GONY.js.map
|