@h-ai/cli 0.1.0-alpha.64 → 0.1.0-alpha.65
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 +20 -4
- package/README.ja.md +20 -4
- package/README.md +28 -12
- package/dist/chunk-47V5OT22.js +5639 -0
- package/dist/chunk-47V5OT22.js.map +1 -0
- package/dist/{chunk-X3RSYNBG.js → chunk-IPG3QNYK.js} +62 -4
- package/dist/chunk-IPG3QNYK.js.map +1 -0
- package/dist/cli-config-templates-SJ2MBIR7.js +5 -0
- package/dist/{cli-config-templates-QRF6B5T4.js.map → cli-config-templates-SJ2MBIR7.js.map} +1 -1
- package/dist/{cli-skill-templates-EZFW7D43.js → cli-skill-templates-A5VIR5J6.js} +3 -3
- package/dist/{cli-skill-templates-EZFW7D43.js.map → cli-skill-templates-A5VIR5J6.js.map} +1 -1
- package/dist/index.js +524 -66
- package/dist/index.js.map +1 -1
- package/dist/{src-DUGLX4YB.js → src-6PPBKQ6Q.js} +666 -290
- package/dist/src-6PPBKQ6Q.js.map +1 -0
- package/package.json +9 -7
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +4 -2
- package/templates/apps/api/.dockerignore.hbs +5 -0
- package/templates/apps/api/.gitignore.hbs +4 -4
- package/templates/apps/api/Dockerfile.hbs +11 -1
- package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +2 -0
- package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +2 -2
- package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +4 -4
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +9 -2
- package/templates/apps/api/package.json.hbs +9 -0
- package/templates/apps/fullstack/.dockerignore.hbs +5 -3
- package/templates/apps/fullstack/.gitignore.hbs +22 -9
- package/templates/apps/fullstack/Dockerfile.hbs +11 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +6 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +7 -3
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +5 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +7 -3
- package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +6 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +7 -3
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +23 -1
- package/templates/apps/fullstack/package.json.hbs +12 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +2 -2
- package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +8 -14
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +6 -4
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -8
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +9 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +4 -4
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +14 -12
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +17 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +15 -5
- package/templates/apps/fullstack/playwright.config.ts.hbs +6 -1
- package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +0 -1
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +13 -13
- package/templates/apps/mobile-app/src/App.svelte.hbs +46 -117
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +58 -0
- package/templates/apps/mobile-app/src/lib/pages/profile.svelte.hbs +13 -0
- package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +16 -0
- package/templates/base/.dockerignore.hbs +5 -0
- package/templates/base/.gitignore.hbs +18 -4
- package/templates/base/Dockerfile.hbs +17 -1
- package/templates/base/e2e/app.spec.ts.hbs +14 -0
- package/templates/base/package.json.hbs +14 -2
- package/templates/base/tests/scaffold.test.ts.hbs +9 -3
- package/templates/base/vite.config.ts.hbs +6 -4
- package/templates/shared/hash-router.svelte.ts +83 -0
- package/templates/skills/AGENTS.md +15 -1
- package/templates/skills/bridges/admin/AGENTS.md +17 -1
- package/templates/skills/bridges/api/AGENTS.md +17 -3
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +35 -11
- package/templates/skills/bridges/h5/AGENTS.md +17 -1
- package/templates/skills/bridges/mobile-app/AGENTS.md +19 -3
- package/templates/skills/bridges/website/AGENTS.md +17 -1
- package/templates/skills/hai-ai/reference.md +2 -2
- package/templates/skills/hai-app-create/SKILL.md +5 -3
- package/templates/skills/hai-app-create/development.md +126 -2
- package/templates/skills/hai-app-create/reference.md +2 -2
- package/templates/skills/hai-app-review/SKILL.md +2 -2
- package/templates/skills/hai-app-tests/SKILL.md +2 -1
- package/templates/skills/hai-app-tests/reference.md +1 -1
- package/templates/skills/hai-core/SKILL.md +2 -1
- package/templates/skills/hai-deploy/SKILL.md +42 -34
- package/templates/skills/hai-deploy/middleware.md +94 -0
- package/templates/skills/hai-framework-sync/SKILL.md +1 -1
- package/templates/skills/hai-framework-sync/sync.md +1 -1
- package/templates/skills/hai-reach/reference.md +8 -8
- package/templates/skills/hai-spec-implement/SKILL.md +12 -9
- package/templates/skills/hai-spec-implement/changes.md +67 -0
- package/templates/skills/hai-spec-implement/templates.md +25 -3
- package/templates/skills/hai-spec-implement/workflow.md +7 -6
- package/templates/skills/hai-spec-review/SKILL.md +14 -5
- package/templates/skills/hai-spec-review/checklist.md +4 -3
- package/templates/skills/hai-storage/SKILL.md +2 -0
- package/dist/chunk-WW2MIO6T.js +0 -992
- package/dist/chunk-WW2MIO6T.js.map +0 -1
- package/dist/chunk-X3RSYNBG.js.map +0 -1
- package/dist/cli-config-templates-QRF6B5T4.js +0 -5
- package/dist/src-DUGLX4YB.js.map +0 -1
- /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/AuditManagement.svelte.hbs → pages/audit.svelte.hbs} +0 -0
- /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/RoleManagement.svelte.hbs → pages/roles.svelte.hbs} +0 -0
- /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/ProfileSettings.svelte.hbs → pages/settings.svelte.hbs} +0 -0
- /package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/{components/UserManagement.svelte.hbs → pages/users.svelte.hbs} +0 -0
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { __export } from './chunk-2H7UOFLK.js';
|
|
3
|
-
import { core,
|
|
4
|
-
import {
|
|
3
|
+
import { core, ok, err } from '@h-ai/core';
|
|
4
|
+
import { readFileSync, existsSync, writeFileSync, chmodSync, mkdirSync, statSync, readdirSync } from 'fs';
|
|
5
|
+
import { resolve, join, basename, dirname, relative, sep } from 'path';
|
|
6
|
+
import process2 from 'process';
|
|
7
|
+
import { parseEnv } from 'util';
|
|
8
|
+
import { parse as parse$1, stringify, parseAllDocuments } from 'yaml';
|
|
5
9
|
import { homedir } from 'os';
|
|
6
|
-
import { join, dirname, relative, sep } from 'path';
|
|
7
|
-
import process from 'process';
|
|
8
|
-
import { parse as parse$1, stringify } from 'yaml';
|
|
9
10
|
import { execSync, spawn } from 'child_process';
|
|
10
11
|
import { randomBytes, createHash } from 'crypto';
|
|
11
12
|
import { Buffer } from 'buffer';
|
|
@@ -1397,10 +1398,10 @@ function defineLazyInternal(inst, key, compute) {
|
|
|
1397
1398
|
Object.defineProperty(this, key, { configurable: true, writable: true, value });
|
|
1398
1399
|
broke = broke || outer;
|
|
1399
1400
|
return value;
|
|
1400
|
-
} catch (
|
|
1401
|
+
} catch (err17) {
|
|
1401
1402
|
delete this[key];
|
|
1402
1403
|
broke = broke || outer;
|
|
1403
|
-
throw
|
|
1404
|
+
throw err17;
|
|
1404
1405
|
}
|
|
1405
1406
|
},
|
|
1406
1407
|
set(value) {
|
|
@@ -1855,18 +1856,18 @@ var validateAsync = async (schema, value, _ctx) => {
|
|
|
1855
1856
|
return result.issues.length === 0;
|
|
1856
1857
|
};
|
|
1857
1858
|
var _encode = (_Err) => {
|
|
1858
|
-
const
|
|
1859
|
+
const parse6 = _parse(_Err);
|
|
1859
1860
|
const fn = (schema, value, _ctx, _params) => {
|
|
1860
1861
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
1861
|
-
return
|
|
1862
|
+
return parse6(schema, value, ctx, finalizeParams(fn, _params));
|
|
1862
1863
|
};
|
|
1863
1864
|
return fn;
|
|
1864
1865
|
};
|
|
1865
1866
|
var encode = /* @__PURE__ */ _encode($ZodRealError);
|
|
1866
1867
|
var _decode = (_Err) => {
|
|
1867
|
-
const
|
|
1868
|
+
const parse6 = _parse(_Err);
|
|
1868
1869
|
const fn = (schema, value, _ctx, _params) => {
|
|
1869
|
-
return
|
|
1870
|
+
return parse6(schema, value, _ctx, finalizeParams(fn, _params));
|
|
1870
1871
|
};
|
|
1871
1872
|
return fn;
|
|
1872
1873
|
};
|
|
@@ -3520,14 +3521,14 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
3520
3521
|
const shape = zod.def.shape;
|
|
3521
3522
|
const propValues = {};
|
|
3522
3523
|
for (const key in shape) {
|
|
3523
|
-
const
|
|
3524
|
-
if (
|
|
3524
|
+
const field2 = shape[key]._zod;
|
|
3525
|
+
if (field2.values) {
|
|
3525
3526
|
if (!Object.prototype.hasOwnProperty.call(propValues, key)) {
|
|
3526
3527
|
assignProp(propValues, key, /* @__PURE__ */ new Set());
|
|
3527
3528
|
}
|
|
3528
|
-
for (const v of
|
|
3529
|
+
for (const v of field2.values)
|
|
3529
3530
|
propValues[key].add(v);
|
|
3530
|
-
if (
|
|
3531
|
+
if (field2.optin !== void 0)
|
|
3531
3532
|
propValues[key].add(void 0);
|
|
3532
3533
|
}
|
|
3533
3534
|
}
|
|
@@ -5036,7 +5037,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
|
|
|
5036
5037
|
var NONE = 0;
|
|
5037
5038
|
var ASSUMED = 1;
|
|
5038
5039
|
var PROVEN = 2;
|
|
5039
|
-
function isRecursive(inst, stack,
|
|
5040
|
+
function isRecursive(inst, stack, resolve3) {
|
|
5040
5041
|
const cached2 = recursive.get(inst);
|
|
5041
5042
|
if (cached2 !== void 0)
|
|
5042
5043
|
return cached2 ? PROVEN : NONE;
|
|
@@ -5046,7 +5047,7 @@ function isRecursive(inst, stack, resolve) {
|
|
|
5046
5047
|
let result = NONE;
|
|
5047
5048
|
const check2 = (child) => {
|
|
5048
5049
|
if (result !== PROVEN && child?._zod) {
|
|
5049
|
-
const answer = isRecursive(child, stack,
|
|
5050
|
+
const answer = isRecursive(child, stack, resolve3);
|
|
5050
5051
|
if (answer > result)
|
|
5051
5052
|
result = answer;
|
|
5052
5053
|
}
|
|
@@ -5057,7 +5058,7 @@ function isRecursive(inst, stack, resolve) {
|
|
|
5057
5058
|
const desc = Object.getOwnPropertyDescriptor(sh, key);
|
|
5058
5059
|
if (!desc.enumerable)
|
|
5059
5060
|
continue;
|
|
5060
|
-
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack,
|
|
5061
|
+
const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
|
|
5061
5062
|
if (child > answer)
|
|
5062
5063
|
answer = child;
|
|
5063
5064
|
}
|
|
@@ -5121,7 +5122,7 @@ function isRecursive(inst, stack, resolve) {
|
|
|
5121
5122
|
break;
|
|
5122
5123
|
// `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
|
|
5123
5124
|
case "lazy": {
|
|
5124
|
-
const inner = def._cachedInner ?? (
|
|
5125
|
+
const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
|
|
5125
5126
|
merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
|
|
5126
5127
|
break;
|
|
5127
5128
|
}
|
|
@@ -12939,9 +12940,9 @@ function compile(schema, options) {
|
|
|
12939
12940
|
const clone2 = withParser(schema, parser);
|
|
12940
12941
|
clone2._zod.bag.validator = compileValidator(schema, parser);
|
|
12941
12942
|
return clone2;
|
|
12942
|
-
} catch (
|
|
12943
|
+
} catch (err17) {
|
|
12943
12944
|
if (options?.strict)
|
|
12944
|
-
throw
|
|
12945
|
+
throw err17;
|
|
12945
12946
|
return schema;
|
|
12946
12947
|
}
|
|
12947
12948
|
}
|
|
@@ -13031,8 +13032,8 @@ ${code}
|
|
|
13031
13032
|
try {
|
|
13032
13033
|
const factory = new F(...constantNames, factoryCode);
|
|
13033
13034
|
fn = factory(...constantValues);
|
|
13034
|
-
} catch (
|
|
13035
|
-
throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${
|
|
13035
|
+
} catch (err17) {
|
|
13036
|
+
throw new ZodCompileUnsupportedError(`this schema (generated code failed to evaluate: ${err17.message})`);
|
|
13036
13037
|
}
|
|
13037
13038
|
if (options?.debug) {
|
|
13038
13039
|
fn.code = fullCode;
|
|
@@ -13070,9 +13071,9 @@ function compileChild(doc, ctx, schema, accessor, needsValue = true) {
|
|
|
13070
13071
|
const varCounter = ctx.varCounter;
|
|
13071
13072
|
try {
|
|
13072
13073
|
return generateCheck(doc, ctx, schema, accessor, needsValue);
|
|
13073
|
-
} catch (
|
|
13074
|
-
if (!(
|
|
13075
|
-
throw
|
|
13074
|
+
} catch (err17) {
|
|
13075
|
+
if (!(err17 instanceof ZodCompileUnsupportedError) || !err17.islandable)
|
|
13076
|
+
throw err17;
|
|
13076
13077
|
doc.content.length = contentLen;
|
|
13077
13078
|
if (ctx.constants.size > constantCount) {
|
|
13078
13079
|
const trailing = Array.from(ctx.constants.keys()).slice(constantCount);
|
|
@@ -15806,7 +15807,7 @@ function foldObjects(members2) {
|
|
|
15806
15807
|
objects.push(member);
|
|
15807
15808
|
}
|
|
15808
15809
|
const properties = {};
|
|
15809
|
-
const
|
|
15810
|
+
const required3 = /* @__PURE__ */ new Set();
|
|
15810
15811
|
for (const object2 of objects) {
|
|
15811
15812
|
for (const key in object2.properties) {
|
|
15812
15813
|
if (Object.prototype.hasOwnProperty.call(properties, key))
|
|
@@ -15823,11 +15824,11 @@ function foldObjects(members2) {
|
|
|
15823
15824
|
assignProp(properties, key, merged);
|
|
15824
15825
|
}
|
|
15825
15826
|
for (const key of object2.required ?? [])
|
|
15826
|
-
|
|
15827
|
+
required3.add(key);
|
|
15827
15828
|
}
|
|
15828
15829
|
const folded = { type: "object", properties };
|
|
15829
|
-
if (
|
|
15830
|
-
folded.required = [...
|
|
15830
|
+
if (required3.size)
|
|
15831
|
+
folded.required = [...required3];
|
|
15831
15832
|
if (objects.every((object2) => object2.additionalProperties === false)) {
|
|
15832
15833
|
folded.additionalProperties = false;
|
|
15833
15834
|
} else {
|
|
@@ -16445,8 +16446,8 @@ var objectProcessor = (schema, ctx, _json, params) => {
|
|
|
16445
16446
|
}
|
|
16446
16447
|
const requiredKeys = [];
|
|
16447
16448
|
for (const key of Object.keys(shape)) {
|
|
16448
|
-
const
|
|
16449
|
-
if (ctx.io === "input" ? inputOptin(
|
|
16449
|
+
const field2 = def.shape[key];
|
|
16450
|
+
if (ctx.io === "input" ? inputOptin(field2) === void 0 : field2._zod.optout === void 0) {
|
|
16450
16451
|
requiredKeys.push(key);
|
|
16451
16452
|
}
|
|
16452
16453
|
}
|
|
@@ -16604,8 +16605,8 @@ function rewriteKeyNames(ctx) {
|
|
|
16604
16605
|
bySchema.set(entry.schema, entry);
|
|
16605
16606
|
}
|
|
16606
16607
|
const rewrites = /* @__PURE__ */ new Map();
|
|
16607
|
-
for (const
|
|
16608
|
-
const seen = ctx.seen.get(
|
|
16608
|
+
for (const record3 of pendingRecords.get(ctx) ?? []) {
|
|
16609
|
+
const seen = ctx.seen.get(record3);
|
|
16609
16610
|
const names = (seen?.def ?? seen?.schema)?.propertyNames;
|
|
16610
16611
|
if (!names || names === true || rewrites.has(names))
|
|
16611
16612
|
continue;
|
|
@@ -19702,7 +19703,24 @@ var en_US_default = {
|
|
|
19702
19703
|
deploy_remoteConnectFailed: "Cannot connect to remote host {host}: {error}",
|
|
19703
19704
|
deploy_remoteRuntimeMissing: "Remote host missing container runtime or compose: {runtime}",
|
|
19704
19705
|
deploy_imageTransferFailed: "Image transfer failed: {error}",
|
|
19705
|
-
deploy_healthCheckFailed: "Health check failed, app not ready at {url}"
|
|
19706
|
+
deploy_healthCheckFailed: "Health check failed, app not ready at {url}",
|
|
19707
|
+
deploy_invalidEnvironment: "Use letters, numbers, underscores or hyphens for the environment; local/example are reserved.",
|
|
19708
|
+
deploy_environmentMissing: "Environment file missing: {path}. Create and configure it before deploying.",
|
|
19709
|
+
deploy_environmentReadFailed: "Cannot read deployment environment files; check syntax and permissions.",
|
|
19710
|
+
deploy_environmentWriteFailed: "Cannot save deployment configuration; check file syntax and permissions.",
|
|
19711
|
+
deploy_multipleApplications: "Multiple service packages found; specify the application directory.",
|
|
19712
|
+
deploy_purposeSqlite: "Local relational database persisted in a volume",
|
|
19713
|
+
deploy_purposeDatabase: "Relational database service for business data",
|
|
19714
|
+
deploy_purposeMemory: "Per-process memory cache; cleared on restart, no external service",
|
|
19715
|
+
deploy_purposeLocal: "Local filesystem storage persisted in a volume",
|
|
19716
|
+
deploy_purposeCache: "Shared cache, sessions and distributed locks",
|
|
19717
|
+
deploy_purposeStorage: "S3-compatible object storage for uploaded files",
|
|
19718
|
+
deploy_purposeVector: "Vector indexing and similarity search",
|
|
19719
|
+
deploy_purposeDisabled: "LDAP login is disabled; no directory service is created",
|
|
19720
|
+
deploy_purposeLdap: "Directory account authentication; the default directory has no business users",
|
|
19721
|
+
deploy_purposeConsole: "Messages go to application logs without delivery",
|
|
19722
|
+
deploy_purposeEmail: "Email/HTTP delivery; the default SMTP container only captures test messages",
|
|
19723
|
+
deploy_purposeSms: "SMS through Aliyun credentials; requires an existing signature and quota"
|
|
19706
19724
|
};
|
|
19707
19725
|
|
|
19708
19726
|
// ../deploy/messages/zh-CN.json
|
|
@@ -19740,7 +19758,24 @@ var zh_CN_default2 = {
|
|
|
19740
19758
|
deploy_remoteConnectFailed: "\u65E0\u6CD5\u8FDE\u63A5\u8FDC\u7A0B\u4E3B\u673A {host}\uFF1A{error}",
|
|
19741
19759
|
deploy_remoteRuntimeMissing: "\u8FDC\u7A0B\u4E3B\u673A\u7F3A\u5C11\u5BB9\u5668\u8FD0\u884C\u65F6\u6216 compose\uFF1A{runtime}",
|
|
19742
19760
|
deploy_imageTransferFailed: "\u955C\u50CF\u4F20\u8F93\u5931\u8D25\uFF1A{error}",
|
|
19743
|
-
deploy_healthCheckFailed: "\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF0C\u5E94\u7528\u672A\u5728 {url} \u5C31\u7EEA"
|
|
19761
|
+
deploy_healthCheckFailed: "\u5065\u5EB7\u68C0\u67E5\u5931\u8D25\uFF0C\u5E94\u7528\u672A\u5728 {url} \u5C31\u7EEA",
|
|
19762
|
+
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",
|
|
19763
|
+
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",
|
|
19764
|
+
deploy_environmentReadFailed: "\u90E8\u7F72\u73AF\u5883\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u683C\u5F0F\u4E0E\u6743\u9650\u3002",
|
|
19765
|
+
deploy_environmentWriteFailed: "\u90E8\u7F72\u914D\u7F6E\u5199\u5165\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u6587\u4EF6\u683C\u5F0F\u4E0E\u6743\u9650\u3002",
|
|
19766
|
+
deploy_multipleApplications: "\u53D1\u73B0\u591A\u4E2A\u670D\u52A1\u5305\uFF0C\u8BF7\u6307\u5B9A\u8981\u90E8\u7F72\u7684\u5E94\u7528\u76EE\u5F55\u3002",
|
|
19767
|
+
deploy_purposeSqlite: "\u672C\u5730\u6587\u4EF6\u5173\u7CFB\u6570\u636E\u5E93\uFF0C\u4F7F\u7528\u6301\u4E45\u5316\u5377",
|
|
19768
|
+
deploy_purposeDatabase: "\u72EC\u7ACB\u5173\u7CFB\u6570\u636E\u5E93\u670D\u52A1\uFF0C\u4FDD\u5B58\u4E1A\u52A1\u6570\u636E",
|
|
19769
|
+
deploy_purposeMemory: "\u5355\u8FDB\u7A0B\u5185\u5B58\u7F13\u5B58\uFF0C\u91CD\u542F\u540E\u6E05\u7A7A\uFF0C\u4E0D\u521B\u5EFA\u5916\u90E8\u670D\u52A1",
|
|
19770
|
+
deploy_purposeLocal: "\u672C\u5730\u6587\u4EF6\u5B58\u50A8\uFF0C\u4F7F\u7528\u6301\u4E45\u5316\u5377",
|
|
19771
|
+
deploy_purposeCache: "\u5171\u4EAB\u7F13\u5B58\u3001\u4F1A\u8BDD\u4E0E\u5206\u5E03\u5F0F\u9501",
|
|
19772
|
+
deploy_purposeStorage: "S3 \u517C\u5BB9\u5BF9\u8C61\u5B58\u50A8\uFF0C\u4FDD\u5B58\u4E0A\u4F20\u6587\u4EF6",
|
|
19773
|
+
deploy_purposeVector: "\u5411\u91CF\u7D22\u5F15\u4E0E\u76F8\u4F3C\u5EA6\u641C\u7D22",
|
|
19774
|
+
deploy_purposeDisabled: "LDAP \u767B\u5F55\u672A\u542F\u7528\uFF0C\u4E0D\u521B\u5EFA\u76EE\u5F55\u670D\u52A1",
|
|
19775
|
+
deploy_purposeLdap: "\u4F01\u4E1A\u76EE\u5F55\u8D26\u53F7\u8BA4\u8BC1\uFF1B\u9ED8\u8BA4\u76EE\u5F55\u4E0D\u5305\u542B\u4E1A\u52A1\u7528\u6237",
|
|
19776
|
+
deploy_purposeConsole: "\u6D88\u606F\u8F93\u51FA\u5230\u5E94\u7528\u65E5\u5FD7\uFF0C\u4E0D\u5B9E\u9645\u6295\u9012",
|
|
19777
|
+
deploy_purposeEmail: "\u90AE\u4EF6\u6216 HTTP \u901A\u77E5\u6295\u9012\uFF1B\u9ED8\u8BA4 SMTP \u5BB9\u5668\u4EC5\u7528\u4E8E\u6D4B\u8BD5\u63A5\u6536",
|
|
19778
|
+
deploy_purposeSms: "\u901A\u8FC7\u963F\u91CC\u4E91\u51ED\u8BC1\u63A5\u5165\u77ED\u4FE1\u670D\u52A1\uFF0C\u9700\u5DF2\u6709\u7B7E\u540D\u4E0E\u989D\u5EA6"
|
|
19744
19779
|
};
|
|
19745
19780
|
|
|
19746
19781
|
// ../deploy/src/deploy-i18n.ts
|
|
@@ -19773,9 +19808,9 @@ var SshConfigSchema = external_exports.object({
|
|
|
19773
19808
|
var PortExposeSchema = external_exports.object({
|
|
19774
19809
|
type: external_exports.literal("port").default("port"),
|
|
19775
19810
|
/** 容器内监听端口(adapter-node 默认 3000) */
|
|
19776
|
-
containerPort: external_exports.number().int().
|
|
19811
|
+
containerPort: external_exports.number().int().min(1).max(65535).default(3e3),
|
|
19777
19812
|
/** 宿主机映射端口 */
|
|
19778
|
-
hostPort: external_exports.number().int().
|
|
19813
|
+
hostPort: external_exports.number().int().min(1).max(65535)
|
|
19779
19814
|
});
|
|
19780
19815
|
var ExposeSchema = PortExposeSchema;
|
|
19781
19816
|
var DockerSshProviderConfigSchema = external_exports.object({
|
|
@@ -19831,11 +19866,22 @@ var ServicesConfigSchema = external_exports.object({
|
|
|
19831
19866
|
sms: external_exports.discriminatedUnion("provisioner", [AliyunSmsServiceSchema]).optional()
|
|
19832
19867
|
}).optional();
|
|
19833
19868
|
var DeployConfigSchema = external_exports.object({
|
|
19869
|
+
/** 部署环境文件模式;dev/test/prod 或自定义模式,禁止 local/example。 */
|
|
19870
|
+
environment: external_exports.string().regex(/^[a-z0-9][\w-]*$/i).refine((value) => !["local", "example"].includes(value.toLowerCase())).optional(),
|
|
19834
19871
|
provider: ProviderConfigSchema,
|
|
19835
19872
|
services: ServicesConfigSchema,
|
|
19836
19873
|
/** 本地容器构建参数(docker-ssh 使用;auto 优先 podman) */
|
|
19837
19874
|
container: external_exports.object({
|
|
19838
|
-
runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto")
|
|
19875
|
+
runtime: external_exports.enum(["auto", "docker", "podman"]).default("auto"),
|
|
19876
|
+
/** Podman 构建是否继承代理;容器不能访问宿主机 localhost 代理时设为 false。 */
|
|
19877
|
+
httpProxy: external_exports.boolean().default(true),
|
|
19878
|
+
/** 自建 S3 服务实现;协议仍为 storage.type=s3。 */
|
|
19879
|
+
s3Engine: external_exports.enum(["rustfs", "minio"]).default("rustfs"),
|
|
19880
|
+
/** SQLite 预编译包镜像根地址;省略时从官方 GitHub Release 下载,不允许嵌入凭据。 */
|
|
19881
|
+
sqlitePrebuildMirror: external_exports.url().refine((value) => {
|
|
19882
|
+
const url2 = new URL(value);
|
|
19883
|
+
return ["http:", "https:"].includes(url2.protocol) && !url2.username && !url2.password && !url2.search && !url2.hash;
|
|
19884
|
+
}).optional()
|
|
19839
19885
|
}).optional()
|
|
19840
19886
|
});
|
|
19841
19887
|
var DeployErrorInfo = {
|
|
@@ -19862,7 +19908,165 @@ var DeployErrorInfo = {
|
|
|
19862
19908
|
};
|
|
19863
19909
|
var HaiDeployError = core.error.buildHaiErrorsDef("deploy", DeployErrorInfo);
|
|
19864
19910
|
|
|
19865
|
-
// ../deploy/src/deploy-
|
|
19911
|
+
// ../deploy/src/deploy-environment.ts
|
|
19912
|
+
function resolveConfig(value, prefix, env) {
|
|
19913
|
+
if (env[prefix] !== void 0)
|
|
19914
|
+
return parse$1(env[prefix]) ?? env[prefix];
|
|
19915
|
+
if (Array.isArray(value))
|
|
19916
|
+
return value.map((item, index) => resolveConfig(item, `${prefix}_${index}`, env));
|
|
19917
|
+
if (value && typeof value === "object")
|
|
19918
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, resolveConfig(nested, `${prefix}_${key.toUpperCase()}`, env)]));
|
|
19919
|
+
if (typeof value !== "string")
|
|
19920
|
+
return value;
|
|
19921
|
+
const resolved = value.replace(/\$\{([^:}]+)(?::([^}]*))?\}/g, (_match, name, fallback) => env[name] ?? fallback ?? "");
|
|
19922
|
+
return /^\$\{[^}]+\}$/.test(value) ? parse$1(resolved) ?? resolved : resolved;
|
|
19923
|
+
}
|
|
19924
|
+
function loadDeploymentConfig(name, path, values) {
|
|
19925
|
+
try {
|
|
19926
|
+
const config2 = resolveConfig(parse$1(readFileSync(path, "utf8")), `HAI_${name.toUpperCase()}`, values);
|
|
19927
|
+
return ok(config2 && typeof config2 === "object" && !Array.isArray(config2) ? config2 : {});
|
|
19928
|
+
} catch {
|
|
19929
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
|
|
19930
|
+
}
|
|
19931
|
+
}
|
|
19932
|
+
function loadDeploymentEnvironment(options) {
|
|
19933
|
+
const environment = options.environment === "production" ? "prod" : options.environment === "development" ? "dev" : options.environment;
|
|
19934
|
+
if (!/^[a-z0-9][\w-]*$/i.test(environment) || ["local", "example"].includes(environment.toLowerCase()))
|
|
19935
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_invalidEnvironment"));
|
|
19936
|
+
const envDir = resolve(options.envDir);
|
|
19937
|
+
const target = join(envDir, `.env.${environment}`);
|
|
19938
|
+
if (options.requireFile !== false && !existsSync(target))
|
|
19939
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentMissing", { params: { path: target } }));
|
|
19940
|
+
const loaded = core.config.loadEnvFiles({ mode: environment, envDir });
|
|
19941
|
+
if (!loaded.success)
|
|
19942
|
+
return loaded;
|
|
19943
|
+
try {
|
|
19944
|
+
const values = { ...loaded.data };
|
|
19945
|
+
for (const [name, value] of Object.entries(process2.env)) {
|
|
19946
|
+
if (/^(?:HAI_|PUBLIC_|VITE_)/.test(name) && value !== void 0)
|
|
19947
|
+
values[name] = value;
|
|
19948
|
+
}
|
|
19949
|
+
values.HAI_DEPLOY_ENVIRONMENT = environment;
|
|
19950
|
+
values.HAI_CORE_ENV ??= environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
|
|
19951
|
+
const targetValues = {};
|
|
19952
|
+
if (existsSync(target)) {
|
|
19953
|
+
for (const [name, value] of Object.entries(parseEnv(readFileSync(target, "utf8")))) {
|
|
19954
|
+
if (value !== void 0)
|
|
19955
|
+
targetValues[name] = value;
|
|
19956
|
+
}
|
|
19957
|
+
}
|
|
19958
|
+
return ok({ environment, envDir, values, targetValues });
|
|
19959
|
+
} catch {
|
|
19960
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
|
|
19961
|
+
}
|
|
19962
|
+
}
|
|
19963
|
+
function isSecret(name, value) {
|
|
19964
|
+
if (/PASSWORD|SECRET|TOKEN|APIKEY|PRIVATEKEY|ACCESSKEY|AUTHORIZATION/i.test(name.replace(/[^a-z0-9]/gi, "")) || /(?:^|_)PASS$/i.test(name) || value.includes("PRIVATE KEY"))
|
|
19965
|
+
return true;
|
|
19966
|
+
try {
|
|
19967
|
+
const url2 = new URL(value);
|
|
19968
|
+
if (url2.username || url2.password || [...url2.searchParams.keys()].some((key) => /password|secret|token|key|authorization/i.test(key)))
|
|
19969
|
+
return true;
|
|
19970
|
+
} catch {
|
|
19971
|
+
}
|
|
19972
|
+
try {
|
|
19973
|
+
const parsed = JSON.parse(value);
|
|
19974
|
+
if (parsed && typeof parsed === "object")
|
|
19975
|
+
return Object.entries(parsed).some(([key, nested]) => isSecret(key, typeof nested === "string" ? nested : JSON.stringify(nested)));
|
|
19976
|
+
} catch {
|
|
19977
|
+
}
|
|
19978
|
+
return false;
|
|
19979
|
+
}
|
|
19980
|
+
function publicConfig(value) {
|
|
19981
|
+
if (Array.isArray(value))
|
|
19982
|
+
return value.map(publicConfig);
|
|
19983
|
+
if (value && typeof value === "object")
|
|
19984
|
+
return Object.fromEntries(Object.entries(value).filter(([name, item]) => !isSecret(name, typeof item === "string" ? item : "")).map(([name, item]) => [name, publicConfig(item)]));
|
|
19985
|
+
return value;
|
|
19986
|
+
}
|
|
19987
|
+
function quoteEnv(value) {
|
|
19988
|
+
try {
|
|
19989
|
+
const parsed = JSON.parse(value);
|
|
19990
|
+
if (parsed && typeof parsed === "object")
|
|
19991
|
+
return `'${JSON.stringify(parsed).replaceAll("'", "\\u0027")}'`;
|
|
19992
|
+
} catch {
|
|
19993
|
+
}
|
|
19994
|
+
if (!value.includes("'"))
|
|
19995
|
+
return `'${value}'`;
|
|
19996
|
+
if (!value.includes('"'))
|
|
19997
|
+
return `"${value}"`;
|
|
19998
|
+
if (!value.includes("`"))
|
|
19999
|
+
return `\`${value}\``;
|
|
20000
|
+
if (!/[\r\n#]/.test(value))
|
|
20001
|
+
return value;
|
|
20002
|
+
throw new Error("Unsupported dotenv value");
|
|
20003
|
+
}
|
|
20004
|
+
function updateFile(path, values, privateFile) {
|
|
20005
|
+
let content = existsSync(path) ? readFileSync(path, "utf8") : "# Local private deployment configuration; do not commit.\n";
|
|
20006
|
+
for (const [name, value] of Object.entries(values)) {
|
|
20007
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name))
|
|
20008
|
+
throw new Error("Invalid environment variable name");
|
|
20009
|
+
const line = `${name}=${quoteEnv(value)}`;
|
|
20010
|
+
const pattern = new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"[^"]*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*`, "gm");
|
|
20011
|
+
if (pattern.test(content))
|
|
20012
|
+
content = content.replace(pattern, () => line);
|
|
20013
|
+
else
|
|
20014
|
+
content = `${content.trimEnd()}
|
|
20015
|
+
${line}
|
|
20016
|
+
`;
|
|
20017
|
+
}
|
|
20018
|
+
writeFileSync(path, content, { mode: privateFile ? 384 : 420 });
|
|
20019
|
+
if (privateFile)
|
|
20020
|
+
chmodSync(path, 384);
|
|
20021
|
+
}
|
|
20022
|
+
function removeOverrides(path, names, privateFile = true) {
|
|
20023
|
+
if (!existsSync(path) || names.length === 0)
|
|
20024
|
+
return;
|
|
20025
|
+
let content = readFileSync(path, "utf8");
|
|
20026
|
+
for (const name of names)
|
|
20027
|
+
content = content.replace(new RegExp(`^(?:export\\s+)?${name}\\s*=\\s*(?:'[^']*'|"[^"]*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
|
|
20028
|
+
writeFileSync(path, content, { mode: privateFile ? 384 : 420 });
|
|
20029
|
+
chmodSync(path, privateFile ? 384 : 420);
|
|
20030
|
+
}
|
|
20031
|
+
function saveDeploymentEnvironment(environment, values, options = {}) {
|
|
20032
|
+
try {
|
|
20033
|
+
const shared = {};
|
|
20034
|
+
const privateValues = {};
|
|
20035
|
+
for (const [name, value] of Object.entries(values)) {
|
|
20036
|
+
if (options.private) {
|
|
20037
|
+
privateValues[name] = value;
|
|
20038
|
+
} else if (isSecret(name, value)) {
|
|
20039
|
+
privateValues[name] = value;
|
|
20040
|
+
try {
|
|
20041
|
+
const parsed = JSON.parse(value);
|
|
20042
|
+
if (parsed && typeof parsed === "object")
|
|
20043
|
+
shared[name] = JSON.stringify(publicConfig(parsed));
|
|
20044
|
+
} catch {
|
|
20045
|
+
}
|
|
20046
|
+
} else {
|
|
20047
|
+
shared[name] = value;
|
|
20048
|
+
}
|
|
20049
|
+
}
|
|
20050
|
+
if (Object.keys(shared).length)
|
|
20051
|
+
updateFile(join(environment.envDir, `.env.${environment.environment}`), shared, false);
|
|
20052
|
+
if (Object.keys(privateValues).length)
|
|
20053
|
+
updateFile(join(environment.envDir, ".env.local"), privateValues, true);
|
|
20054
|
+
removeOverrides(join(environment.envDir, `.env.${environment.environment}`), Object.keys(privateValues).filter((name) => !(name in shared)), false);
|
|
20055
|
+
removeOverrides(join(environment.envDir, ".env.local"), Object.keys(shared).filter((name) => !(name in privateValues)));
|
|
20056
|
+
if (Object.keys(privateValues).length) {
|
|
20057
|
+
const ignorePath = join(environment.envDir, ".gitignore");
|
|
20058
|
+
const ignore = existsSync(ignorePath) ? readFileSync(ignorePath, "utf8") : "";
|
|
20059
|
+
if (!ignore.split(/\r?\n/).includes(".env.local"))
|
|
20060
|
+
writeFileSync(ignorePath, `${ignore.trimEnd()}
|
|
20061
|
+
.env.local
|
|
20062
|
+
.hai/deploy/
|
|
20063
|
+
`);
|
|
20064
|
+
}
|
|
20065
|
+
return ok(Object.keys(values));
|
|
20066
|
+
} catch {
|
|
20067
|
+
return err(HaiDeployError.ENV_VAR_FAILED, deployM("deploy_environmentWriteFailed"));
|
|
20068
|
+
}
|
|
20069
|
+
}
|
|
19866
20070
|
var logger = core.logger.child({ module: "deploy", scope: "credentials" });
|
|
19867
20071
|
var DEPLOY_CREDENTIAL_KEY_PATTERN = /^HAI_DEPLOY_[A-Z0-9_]+$/;
|
|
19868
20072
|
function isAllowedCredentialKey(key) {
|
|
@@ -19926,7 +20130,7 @@ function loadCredentials() {
|
|
|
19926
20130
|
const data = readCredentialsFile();
|
|
19927
20131
|
const keys = Object.keys(data);
|
|
19928
20132
|
for (const key of keys) {
|
|
19929
|
-
|
|
20133
|
+
process2.env[key] = String(data[key]);
|
|
19930
20134
|
}
|
|
19931
20135
|
logger.debug("Credentials loaded", { count: keys.length });
|
|
19932
20136
|
return ok(keys);
|
|
@@ -19947,7 +20151,7 @@ function saveCredential(key, value) {
|
|
|
19947
20151
|
const data = readCredentialsFile();
|
|
19948
20152
|
data[key] = value;
|
|
19949
20153
|
writeCredentialsFile(data);
|
|
19950
|
-
|
|
20154
|
+
process2.env[key] = value;
|
|
19951
20155
|
logger.debug("Credential saved", { key });
|
|
19952
20156
|
return ok(void 0);
|
|
19953
20157
|
} catch (error62) {
|
|
@@ -19967,7 +20171,7 @@ function saveCredentials(entries) {
|
|
|
19967
20171
|
const data = readCredentialsFile();
|
|
19968
20172
|
for (const [key, value] of Object.entries(entries)) {
|
|
19969
20173
|
data[key] = value;
|
|
19970
|
-
|
|
20174
|
+
process2.env[key] = value;
|
|
19971
20175
|
}
|
|
19972
20176
|
writeCredentialsFile(data);
|
|
19973
20177
|
logger.debug("Credentials saved", { count: Object.keys(entries).length });
|
|
@@ -19984,13 +20188,17 @@ function saveCredentials(entries) {
|
|
|
19984
20188
|
}
|
|
19985
20189
|
}
|
|
19986
20190
|
var logger2 = core.logger.child({ module: "deploy", scope: "functions" });
|
|
20191
|
+
function usesDefaultMiddleware(scan, service, options) {
|
|
20192
|
+
const requirements = scan.middlewares?.filter((item) => item.service === service) ?? [];
|
|
20193
|
+
return requirements.length ? requirements.some((item) => (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[service] ?? "default") === "default") : (options?.serviceModes?.[service] ?? "default") === "default";
|
|
20194
|
+
}
|
|
19987
20195
|
function buildApp(appDir, buildCommand, envVars = {}) {
|
|
19988
20196
|
logger2.info("Building application", { appDir, buildCommand });
|
|
19989
20197
|
try {
|
|
19990
20198
|
execSync(buildCommand, {
|
|
19991
20199
|
cwd: appDir,
|
|
19992
20200
|
stdio: "inherit",
|
|
19993
|
-
env: { ...
|
|
20201
|
+
env: { ...process2.env, ...envVars }
|
|
19994
20202
|
});
|
|
19995
20203
|
return ok(void 0);
|
|
19996
20204
|
} catch (error62) {
|
|
@@ -20043,195 +20251,144 @@ function recoveryFailure(error62, recovery) {
|
|
|
20043
20251
|
});
|
|
20044
20252
|
}
|
|
20045
20253
|
var logger3 = core.logger.child({ module: "deploy", scope: "scanner" });
|
|
20046
|
-
function
|
|
20047
|
-
|
|
20048
|
-
|
|
20049
|
-
|
|
20050
|
-
|
|
20051
|
-
|
|
20052
|
-
const
|
|
20053
|
-
if (
|
|
20054
|
-
|
|
20055
|
-
|
|
20056
|
-
|
|
20057
|
-
|
|
20058
|
-
|
|
20059
|
-
|
|
20060
|
-
|
|
20061
|
-
|
|
20062
|
-
if (!
|
|
20063
|
-
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
|
|
20073
|
-
|
|
20074
|
-
|
|
20075
|
-
|
|
20076
|
-
|
|
20077
|
-
|
|
20078
|
-
|
|
20079
|
-
|
|
20080
|
-
|
|
20081
|
-
|
|
20082
|
-
|
|
20083
|
-
|
|
20084
|
-
|
|
20085
|
-
|
|
20086
|
-
|
|
20087
|
-
|
|
20088
|
-
|
|
20089
|
-
|
|
20090
|
-
|
|
20091
|
-
|
|
20092
|
-
|
|
20093
|
-
|
|
20094
|
-
|
|
20095
|
-
|
|
20096
|
-
|
|
20097
|
-
|
|
20098
|
-
}
|
|
20099
|
-
}
|
|
20100
|
-
const storageFile = files.find((f) => f.startsWith("_storage."));
|
|
20101
|
-
if (storageFile) {
|
|
20102
|
-
const storageConfig = readYaml(join(configDir, storageFile));
|
|
20103
|
-
if (storageConfig !== null && detectStorageIsS3(storageConfig)) {
|
|
20104
|
-
services.push("storage");
|
|
20105
|
-
backends.storage = "s3";
|
|
20106
|
-
}
|
|
20107
|
-
}
|
|
20108
|
-
const vecdbFile = files.find((f) => f.startsWith("_vecdb."));
|
|
20109
|
-
if (vecdbFile) {
|
|
20110
|
-
const vecdbConfig = readYaml(join(configDir, vecdbFile));
|
|
20111
|
-
if (vecdbConfig !== null) {
|
|
20112
|
-
services.push("vecdb");
|
|
20113
|
-
backends.vecdb = vecdbConfig.type ?? "qdrant";
|
|
20114
|
-
}
|
|
20115
|
-
}
|
|
20116
|
-
const reachFile = files.find((f) => f.startsWith("_reach."));
|
|
20117
|
-
if (reachFile) {
|
|
20118
|
-
const reachConfig = readYaml(join(configDir, reachFile));
|
|
20119
|
-
if (reachConfig !== null) {
|
|
20120
|
-
detectReachServices(reachConfig, services);
|
|
20121
|
-
}
|
|
20122
|
-
}
|
|
20123
|
-
return { services, backends };
|
|
20124
|
-
}
|
|
20125
|
-
function detectStorageIsS3(config2) {
|
|
20126
|
-
if (config2.type === "s3") {
|
|
20127
|
-
return true;
|
|
20128
|
-
}
|
|
20129
|
-
const providers = config2.providers;
|
|
20130
|
-
if (providers === void 0) {
|
|
20131
|
-
return false;
|
|
20132
|
-
}
|
|
20133
|
-
return Object.values(providers).some((p) => p?.type === "s3");
|
|
20134
|
-
}
|
|
20135
|
-
function detectReachServices(config2, services) {
|
|
20136
|
-
const providers = config2.providers;
|
|
20137
|
-
if (!Array.isArray(providers)) {
|
|
20138
|
-
return;
|
|
20139
|
-
}
|
|
20140
|
-
let hasEmail = false;
|
|
20141
|
-
let hasSms = false;
|
|
20142
|
-
for (const provider of providers) {
|
|
20143
|
-
const providerType = provider.type;
|
|
20144
|
-
if (!hasEmail && (providerType === "smtp" || providerType === "api")) {
|
|
20145
|
-
hasEmail = true;
|
|
20146
|
-
services.push("email");
|
|
20147
|
-
}
|
|
20148
|
-
if (!hasSms && providerType === "aliyun-sms") {
|
|
20149
|
-
hasSms = true;
|
|
20150
|
-
services.push("sms");
|
|
20254
|
+
function record2(value) {
|
|
20255
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
20256
|
+
}
|
|
20257
|
+
function runtimeDependencies(appDir) {
|
|
20258
|
+
let root = appDir;
|
|
20259
|
+
while (!existsSync(join(root, "pnpm-lock.yaml"))) {
|
|
20260
|
+
const parent = dirname(root);
|
|
20261
|
+
if (parent === root)
|
|
20262
|
+
throw new Error("Missing pnpm lockfile; run pnpm install");
|
|
20263
|
+
root = parent;
|
|
20264
|
+
}
|
|
20265
|
+
const importerKey = relative(root, appDir).replaceAll("\\", "/") || ".";
|
|
20266
|
+
const documents = parseAllDocuments(readFileSync(join(root, "pnpm-lock.yaml"), "utf8"));
|
|
20267
|
+
if (documents.some((document) => document.errors.length))
|
|
20268
|
+
throw new Error("Invalid pnpm lockfile");
|
|
20269
|
+
const lock = documents.map((document) => record2(document.toJS())).reverse().find((document) => importerKey in record2(document.importers));
|
|
20270
|
+
if (!lock)
|
|
20271
|
+
throw new Error("Application is missing from pnpm lockfile; run pnpm install");
|
|
20272
|
+
const importers = record2(lock.importers);
|
|
20273
|
+
const packages = record2(lock.packages);
|
|
20274
|
+
const snapshots = record2(lock.snapshots);
|
|
20275
|
+
const names = /* @__PURE__ */ new Set();
|
|
20276
|
+
const visited = /* @__PURE__ */ new Set();
|
|
20277
|
+
const visit2 = (id, node2, importer) => {
|
|
20278
|
+
if (visited.has(id))
|
|
20279
|
+
return;
|
|
20280
|
+
visited.add(id);
|
|
20281
|
+
const metadata = importer === void 0 ? record2(packages[id.split("(")[0]]) : {};
|
|
20282
|
+
const peers = record2(metadata.peerDependencies);
|
|
20283
|
+
for (const [name, reference] of Object.entries({ ...record2(node2.dependencies), ...record2(node2.optionalDependencies) })) {
|
|
20284
|
+
if (name in peers)
|
|
20285
|
+
continue;
|
|
20286
|
+
const version2 = typeof reference === "string" ? reference : record2(reference).version;
|
|
20287
|
+
if (typeof version2 !== "string")
|
|
20288
|
+
throw new Error("Invalid pnpm dependency reference");
|
|
20289
|
+
if (version2.startsWith("link:")) {
|
|
20290
|
+
if (importer === void 0)
|
|
20291
|
+
throw new Error("Invalid workspace dependency");
|
|
20292
|
+
const target = relative(root, resolve(root, importer, version2.slice(5))).replaceAll("\\", "/") || ".";
|
|
20293
|
+
const linked = record2(importers[target]);
|
|
20294
|
+
if (!Object.keys(linked).length)
|
|
20295
|
+
throw new Error("Missing linked workspace importer");
|
|
20296
|
+
names.add(name);
|
|
20297
|
+
visit2(`workspace:${target}`, linked, target);
|
|
20298
|
+
continue;
|
|
20299
|
+
}
|
|
20300
|
+
const key = version2 in snapshots || version2 in packages ? version2 : `${name}@${version2}`;
|
|
20301
|
+
names.add(key.match(/^(@[^/]+\/[^@]+|[^@]+)@/)?.[1] ?? name);
|
|
20302
|
+
const snapshot = record2(snapshots[key] ?? packages[key]);
|
|
20303
|
+
if (!Object.keys(snapshot).length && !(key in snapshots) && !(key in packages))
|
|
20304
|
+
throw new Error("Missing pnpm dependency snapshot");
|
|
20305
|
+
visit2(key, snapshot);
|
|
20151
20306
|
}
|
|
20152
|
-
}
|
|
20307
|
+
};
|
|
20308
|
+
visit2(`workspace:${importerKey}`, record2(importers[importerKey]), importerKey);
|
|
20309
|
+
return { runtime: names, build: /* @__PURE__ */ new Set([...names, ...Object.keys(record2(record2(importers[importerKey]).devDependencies))]), root };
|
|
20153
20310
|
}
|
|
20154
|
-
async function scanApp(
|
|
20155
|
-
logger3.debug("Scanning application", { appDir });
|
|
20311
|
+
async function scanApp(inputDir, options) {
|
|
20156
20312
|
try {
|
|
20157
|
-
const
|
|
20158
|
-
|
|
20159
|
-
|
|
20160
|
-
|
|
20161
|
-
|
|
20162
|
-
|
|
20163
|
-
|
|
20164
|
-
const
|
|
20165
|
-
const
|
|
20166
|
-
const
|
|
20167
|
-
|
|
20168
|
-
|
|
20169
|
-
|
|
20170
|
-
|
|
20171
|
-
|
|
20172
|
-
|
|
20173
|
-
|
|
20174
|
-
|
|
20175
|
-
|
|
20176
|
-
|
|
20177
|
-
|
|
20178
|
-
|
|
20179
|
-
requiredServices,
|
|
20180
|
-
serviceBackends: configDetection.backends,
|
|
20181
|
-
buildCommand
|
|
20313
|
+
const appDir = resolve(inputDir);
|
|
20314
|
+
const graph = runtimeDependencies(appDir);
|
|
20315
|
+
const envDir = options?.envDir ? resolve(options.envDir) : existsSync(join(appDir, ".env")) ? appDir : graph.root;
|
|
20316
|
+
const loaded = loadDeploymentEnvironment({ environment: options?.environment ?? "prod", envDir, requireFile: options?.environment !== void 0 });
|
|
20317
|
+
if (!loaded.success)
|
|
20318
|
+
return loaded;
|
|
20319
|
+
const env = loaded.data.values;
|
|
20320
|
+
const deps = graph.runtime;
|
|
20321
|
+
const configs = {};
|
|
20322
|
+
for (const [name, owner] of Object.entries({ db: "reldb", cache: "cache", storage: "storage", vecdb: "vecdb", iam: "iam", reach: "reach" })) {
|
|
20323
|
+
configs[name] = {};
|
|
20324
|
+
if (!deps.has(`@h-ai/${owner}`))
|
|
20325
|
+
continue;
|
|
20326
|
+
const configFile = ["yml", "yaml"].map((ext) => join(appDir, "config", `_${name}.${ext}`)).find(existsSync);
|
|
20327
|
+
const config2 = configFile ? loadDeploymentConfig(name, configFile, env) : void 0;
|
|
20328
|
+
if (config2 && !config2.success)
|
|
20329
|
+
return config2;
|
|
20330
|
+
configs[name] = config2?.data ?? {};
|
|
20331
|
+
}
|
|
20332
|
+
const middlewares = [];
|
|
20333
|
+
const add = (service, backend, configName = service, configPath = [], config2 = configs[configName] ?? {}, source = `_${configName}.yml`) => {
|
|
20334
|
+
middlewares.push({ service, backend, source, configName, configPath, envPrefix: ["HAI", configName, ...configPath].map((part) => part.toUpperCase()).join("_"), config: config2 });
|
|
20182
20335
|
};
|
|
20183
|
-
|
|
20184
|
-
|
|
20336
|
+
const iam = deps.has("@h-ai/iam");
|
|
20337
|
+
if (deps.has("@h-ai/reldb"))
|
|
20338
|
+
add("db", String(configs.db.type ?? "sqlite"), "db", [], configs.db, "@h-ai/reldb / _db.yml");
|
|
20339
|
+
if (deps.has("@h-ai/cache"))
|
|
20340
|
+
add("cache", String(configs.cache.type ?? "memory"), "cache", [], configs.cache, "@h-ai/cache / _cache.yml");
|
|
20341
|
+
if (deps.has("@h-ai/storage"))
|
|
20342
|
+
add("storage", String(configs.storage.type ?? "local"));
|
|
20343
|
+
if (deps.has("@h-ai/vecdb"))
|
|
20344
|
+
add("vecdb", String(configs.vecdb.type ?? "lancedb"));
|
|
20345
|
+
if (iam) {
|
|
20346
|
+
const ldap = record2(configs.iam.ldap);
|
|
20347
|
+
add("ldap", record2(configs.iam.login).ldap === true ? "ldap" : "disabled", "iam", ["ldap"], ldap, "@h-ai/iam (LDAP)");
|
|
20348
|
+
}
|
|
20349
|
+
const providers = configs.reach.providers;
|
|
20350
|
+
if (deps.has("@h-ai/reach") && Array.isArray(providers)) {
|
|
20351
|
+
for (const [index, item] of providers.entries()) {
|
|
20352
|
+
const provider = record2(item);
|
|
20353
|
+
const type = String(provider.type ?? "");
|
|
20354
|
+
const service = type === "aliyun-sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
|
|
20355
|
+
if (service)
|
|
20356
|
+
add(service, type, "reach", ["providers", String(index)], provider, "@h-ai/reach");
|
|
20357
|
+
}
|
|
20358
|
+
}
|
|
20359
|
+
if (deps.has("@h-ai/reach") && !middlewares.some((item) => item.configName === "reach"))
|
|
20360
|
+
add("email", "console", "reach", ["providers", "0"], { name: "email", type: "console" }, "@h-ai/reach");
|
|
20361
|
+
const isSvelteKit = graph.build.has("@sveltejs/kit");
|
|
20362
|
+
const scan = {
|
|
20363
|
+
appDir,
|
|
20364
|
+
envDir,
|
|
20365
|
+
middlewares,
|
|
20366
|
+
haiModules: [...deps].filter((name) => name.startsWith("@h-ai/")).sort(),
|
|
20367
|
+
appName: basename(appDir),
|
|
20185
20368
|
isSvelteKit,
|
|
20186
|
-
adapterInstalled,
|
|
20187
|
-
nodeAdapterInstalled,
|
|
20188
|
-
requiredServices,
|
|
20189
|
-
serviceBackends:
|
|
20190
|
-
|
|
20191
|
-
|
|
20192
|
-
|
|
20193
|
-
|
|
20194
|
-
|
|
20195
|
-
|
|
20196
|
-
deployM("deploy_scanFailed", {
|
|
20197
|
-
params: { error: error62 instanceof Error ? error62.message : String(error62) }
|
|
20198
|
-
}),
|
|
20199
|
-
error62
|
|
20200
|
-
);
|
|
20201
|
-
}
|
|
20202
|
-
}
|
|
20203
|
-
function detectServicesFromDependencies(pkg) {
|
|
20204
|
-
const depMap = {
|
|
20205
|
-
"@h-ai/reldb": "db",
|
|
20206
|
-
"@h-ai/cache": "cache",
|
|
20207
|
-
"@h-ai/storage": "storage",
|
|
20208
|
-
"@h-ai/vecdb": "vecdb"
|
|
20209
|
-
};
|
|
20210
|
-
const services = [];
|
|
20211
|
-
for (const [depName, serviceType] of Object.entries(depMap)) {
|
|
20212
|
-
if (hasDependency(pkg, depName)) {
|
|
20213
|
-
services.push(serviceType);
|
|
20214
|
-
}
|
|
20215
|
-
}
|
|
20216
|
-
return services;
|
|
20217
|
-
}
|
|
20218
|
-
function extractAppName(pkg) {
|
|
20219
|
-
const name = pkg.name ?? "app";
|
|
20220
|
-
const slashIndex = name.indexOf("/");
|
|
20221
|
-
if (slashIndex >= 0) {
|
|
20222
|
-
return name.slice(slashIndex + 1);
|
|
20369
|
+
adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
|
|
20370
|
+
nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
|
|
20371
|
+
requiredServices: [...new Set(middlewares.map((item) => item.service))],
|
|
20372
|
+
serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend])),
|
|
20373
|
+
buildCommand: "pnpm build"
|
|
20374
|
+
};
|
|
20375
|
+
logger3.info("Application scanned", { appName: scan.appName, haiModules: scan.haiModules, requiredServices: scan.requiredServices, serviceBackends: scan.serviceBackends });
|
|
20376
|
+
return ok(scan);
|
|
20377
|
+
} catch {
|
|
20378
|
+
return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", { params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" } }));
|
|
20223
20379
|
}
|
|
20224
|
-
return name;
|
|
20225
20380
|
}
|
|
20226
20381
|
var IMAGES = {
|
|
20227
20382
|
postgres: "postgres:16-alpine",
|
|
20228
20383
|
mysql: "mysql:8.4",
|
|
20229
20384
|
redis: "redis:7-alpine",
|
|
20230
|
-
|
|
20231
|
-
|
|
20385
|
+
rustfs: "ghcr.io/rustfs/rustfs:1.0.1",
|
|
20386
|
+
minio: "ghcr.io/coollabsio/minio:RELEASE.2025-10-15T17-29-55Z",
|
|
20232
20387
|
qdrant: "qdrant/qdrant:v1.12.4",
|
|
20233
20388
|
pgvector: "pgvector/pgvector:pg17",
|
|
20234
|
-
chroma: "chromadb/chroma:
|
|
20389
|
+
chroma: "chromadb/chroma:1.5.5",
|
|
20390
|
+
ldap: "docker.io/osixia/openldap:1.5.0",
|
|
20391
|
+
smtp: "axllent/mailpit:v1.27.4"
|
|
20235
20392
|
};
|
|
20236
20393
|
function generatePassword() {
|
|
20237
20394
|
return randomBytes(24).toString("base64url");
|
|
@@ -20246,7 +20403,8 @@ function resolveSidecarSecrets(appDir, projectName) {
|
|
|
20246
20403
|
}
|
|
20247
20404
|
const secrets = {
|
|
20248
20405
|
dbPassword: generatePassword(),
|
|
20249
|
-
storagePassword: generatePassword()
|
|
20406
|
+
storagePassword: generatePassword(),
|
|
20407
|
+
ldapPassword: generatePassword()
|
|
20250
20408
|
};
|
|
20251
20409
|
mkdirSync(dirname(secretFile), { recursive: true });
|
|
20252
20410
|
writeFileSync(secretFile, JSON.stringify(secrets), { mode: 384 });
|
|
@@ -20307,38 +20465,57 @@ function planRedisCache(plan, projectName, net) {
|
|
|
20307
20465
|
};
|
|
20308
20466
|
plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
|
|
20309
20467
|
}
|
|
20310
|
-
function
|
|
20468
|
+
function planS3Storage(plan, projectName, password, net, engine) {
|
|
20311
20469
|
plan.services.push("storage");
|
|
20312
20470
|
plan.dependsOn.push("storage");
|
|
20313
20471
|
plan.composeServices.storage = {
|
|
20314
|
-
image: IMAGES
|
|
20472
|
+
image: IMAGES[engine],
|
|
20315
20473
|
container_name: `hai-${projectName}-storage`,
|
|
20316
20474
|
restart: "unless-stopped",
|
|
20317
|
-
command: ["server", "/data"],
|
|
20475
|
+
...engine === "minio" ? { command: ["server", "/data"] } : {},
|
|
20318
20476
|
env_file: [".env.runtime"],
|
|
20319
20477
|
volumes: [`${projectName}-storage-data:/data`],
|
|
20320
20478
|
networks: net
|
|
20321
20479
|
};
|
|
20322
20480
|
plan.composeServices["storage-init"] = {
|
|
20323
|
-
image: IMAGES.mc,
|
|
20324
20481
|
depends_on: ["storage"],
|
|
20325
20482
|
env_file: [".env.runtime"],
|
|
20326
20483
|
entrypoint: [
|
|
20327
|
-
"
|
|
20328
|
-
"-
|
|
20329
|
-
|
|
20484
|
+
"node",
|
|
20485
|
+
"--input-type=module",
|
|
20486
|
+
"-e",
|
|
20487
|
+
// 复用应用 @h-ai/storage 已依赖的 SDK;不额外拉取失效的 mc 镜像,不把密钥拼进命令。
|
|
20488
|
+
`import { S3Client, HeadBucketCommand, CreateBucketCommand } from '@aws-sdk/client-s3';
|
|
20489
|
+
const config = JSON.parse(process.env.HAI_STORAGE);
|
|
20490
|
+
const client = new S3Client({ endpoint: config.endpoint, region: config.region, forcePathStyle: config.forcePathStyle, credentials: { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey } });
|
|
20491
|
+
let ready = false;
|
|
20492
|
+
for (let attempt = 0; attempt < 120; attempt++) {
|
|
20493
|
+
try {
|
|
20494
|
+
try { await client.send(new HeadBucketCommand({ Bucket: config.bucket })); }
|
|
20495
|
+
catch (error) {
|
|
20496
|
+
if (error.$$metadata?.httpStatusCode !== 404) throw error;
|
|
20497
|
+
await client.send(new CreateBucketCommand({ Bucket: config.bucket }));
|
|
20498
|
+
}
|
|
20499
|
+
ready = true; break;
|
|
20500
|
+
} catch { await new Promise(resolve => setTimeout(resolve, 1000)); }
|
|
20501
|
+
}
|
|
20502
|
+
client.destroy();
|
|
20503
|
+
if (!ready) throw new Error('S3 bucket initialization failed');`
|
|
20330
20504
|
],
|
|
20331
20505
|
networks: net
|
|
20332
20506
|
};
|
|
20333
20507
|
plan.volumes[`${projectName}-storage-data`] = null;
|
|
20334
|
-
|
|
20335
|
-
|
|
20508
|
+
if (engine === "rustfs") {
|
|
20509
|
+
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" });
|
|
20510
|
+
} else {
|
|
20511
|
+
Object.assign(plan.env, { MINIO_ROOT_USER: "hai-storage", MINIO_ROOT_PASSWORD: password });
|
|
20512
|
+
}
|
|
20336
20513
|
plan.env.HAI_STORAGE = JSON.stringify({
|
|
20337
20514
|
type: "s3",
|
|
20338
20515
|
endpoint: "http://storage:9000",
|
|
20339
20516
|
bucket: "hai",
|
|
20340
20517
|
region: "us-east-1",
|
|
20341
|
-
accessKeyId: "hai",
|
|
20518
|
+
accessKeyId: "hai-storage",
|
|
20342
20519
|
secretAccessKey: password,
|
|
20343
20520
|
forcePathStyle: true
|
|
20344
20521
|
});
|
|
@@ -20388,7 +20565,7 @@ function planChromaVecdb(plan, projectName, net) {
|
|
|
20388
20565
|
plan.volumes[`${projectName}-vecdb-data`] = null;
|
|
20389
20566
|
plan.env.HAI_VECDB = JSON.stringify({ type: "chroma", url: "http://vecdb:8000" });
|
|
20390
20567
|
}
|
|
20391
|
-
function planSidecars(appDir, projectName, scan, provisionEnv) {
|
|
20568
|
+
function planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine = "rustfs") {
|
|
20392
20569
|
const plan = {
|
|
20393
20570
|
services: [],
|
|
20394
20571
|
composeServices: {},
|
|
@@ -20398,10 +20575,14 @@ function planSidecars(appDir, projectName, scan, provisionEnv) {
|
|
|
20398
20575
|
dependsOn: [],
|
|
20399
20576
|
secrets: []
|
|
20400
20577
|
};
|
|
20578
|
+
if (!scan.requiredServices.some((service) => usesDefaultMiddleware(scan, service, options)))
|
|
20579
|
+
return plan;
|
|
20401
20580
|
const secrets = resolveSidecarSecrets(appDir, projectName);
|
|
20402
20581
|
const net = ["hai-app"];
|
|
20403
20582
|
const backends = scan.serviceBackends;
|
|
20404
20583
|
for (const service of scan.requiredServices) {
|
|
20584
|
+
if (!usesDefaultMiddleware(scan, service, options))
|
|
20585
|
+
continue;
|
|
20405
20586
|
if (service === "db" && !provisionEnv.HAI_DB) {
|
|
20406
20587
|
const backend = backends.db ?? "postgresql";
|
|
20407
20588
|
if (backend === "mysql")
|
|
@@ -20411,9 +20592,13 @@ function planSidecars(appDir, projectName, scan, provisionEnv) {
|
|
|
20411
20592
|
else
|
|
20412
20593
|
planPostgresDb(plan, projectName, secrets.dbPassword, net);
|
|
20413
20594
|
} else if (service === "cache" && !provisionEnv.HAI_CACHE) {
|
|
20414
|
-
|
|
20595
|
+
if ((backends.cache ?? "redis") === "redis")
|
|
20596
|
+
planRedisCache(plan, projectName, net);
|
|
20415
20597
|
} else if (service === "storage" && !provisionEnv.HAI_STORAGE) {
|
|
20416
|
-
|
|
20598
|
+
if ((backends.storage ?? "s3") === "s3")
|
|
20599
|
+
planS3Storage(plan, projectName, secrets.storagePassword, net, s3Engine);
|
|
20600
|
+
else
|
|
20601
|
+
addAppDataVolume(plan, projectName);
|
|
20417
20602
|
} else if (service === "vecdb" && !provisionEnv.HAI_VECDB) {
|
|
20418
20603
|
const backend = backends.vecdb ?? "qdrant";
|
|
20419
20604
|
if (backend === "pgvector")
|
|
@@ -20424,16 +20609,39 @@ function planSidecars(appDir, projectName, scan, provisionEnv) {
|
|
|
20424
20609
|
addAppDataVolume(plan, projectName);
|
|
20425
20610
|
else
|
|
20426
20611
|
planQdrantVecdb(plan, projectName, net);
|
|
20612
|
+
} else if (service === "ldap" && backends.ldap === "ldap" && !provisionEnv.HAI_IAM_LDAP) {
|
|
20613
|
+
const password = secrets.ldapPassword ?? secrets.dbPassword;
|
|
20614
|
+
plan.services.push("ldap");
|
|
20615
|
+
plan.dependsOn.push("ldap");
|
|
20616
|
+
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`] };
|
|
20617
|
+
plan.volumes[`${projectName}-ldap-data`] = null;
|
|
20618
|
+
plan.volumes[`${projectName}-ldap-config`] = null;
|
|
20619
|
+
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" }) });
|
|
20620
|
+
plan.secrets.push(password);
|
|
20621
|
+
} 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) {
|
|
20622
|
+
plan.services.push("email");
|
|
20623
|
+
plan.dependsOn.push("email");
|
|
20624
|
+
plan.composeServices.email = { image: IMAGES.smtp, restart: "unless-stopped", networks: net };
|
|
20625
|
+
const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
|
|
20626
|
+
if (requirements?.length) {
|
|
20627
|
+
for (const requirement of requirements)
|
|
20628
|
+
plan.env[requirement.envPrefix] = JSON.stringify({ name: requirement.config.name ?? "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
|
|
20629
|
+
} else if (!scan.middlewares) {
|
|
20630
|
+
plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({ name: "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
|
|
20631
|
+
}
|
|
20427
20632
|
}
|
|
20428
20633
|
}
|
|
20429
20634
|
return plan;
|
|
20430
20635
|
}
|
|
20431
|
-
function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort) {
|
|
20432
|
-
const sidecar = planSidecars(appDir, projectName, scan, provisionEnv);
|
|
20636
|
+
function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv = {}, s3Engine = "rustfs") {
|
|
20637
|
+
const sidecar = planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine);
|
|
20433
20638
|
const env = {
|
|
20434
20639
|
NODE_ENV: "production",
|
|
20435
20640
|
HOST: "0.0.0.0",
|
|
20436
20641
|
PORT: String(containerPort),
|
|
20642
|
+
HAI_SERV_SERVER_HOST: "0.0.0.0",
|
|
20643
|
+
HAI_SERV_SERVER_PORT: String(containerPort),
|
|
20644
|
+
...appEnv,
|
|
20437
20645
|
...sidecar.env,
|
|
20438
20646
|
...provisionEnv
|
|
20439
20647
|
};
|
|
@@ -20455,21 +20663,55 @@ function generateComposeFile(options) {
|
|
|
20455
20663
|
app.volumes = sidecar.appVolumes;
|
|
20456
20664
|
if (sidecar.dependsOn.length > 0)
|
|
20457
20665
|
app.depends_on = sidecar.dependsOn;
|
|
20666
|
+
const services = { app, ...sidecar.composeServices };
|
|
20667
|
+
if (services["storage-init"])
|
|
20668
|
+
services["storage-init"] = { ...services["storage-init"], image: imageRef };
|
|
20458
20669
|
const compose = {
|
|
20459
|
-
services
|
|
20460
|
-
app,
|
|
20461
|
-
...sidecar.composeServices
|
|
20462
|
-
},
|
|
20670
|
+
services,
|
|
20463
20671
|
networks: {
|
|
20464
20672
|
"hai-app": { name: `hai-${projectName}` }
|
|
20465
20673
|
}
|
|
20466
20674
|
};
|
|
20467
20675
|
if (Object.keys(sidecar.volumes).length > 0)
|
|
20468
20676
|
compose.volumes = sidecar.volumes;
|
|
20677
|
+
if (options.previousCompose && options.serviceModes) {
|
|
20678
|
+
const previous = parse$1(options.previousCompose);
|
|
20679
|
+
for (const [service, mode] of Object.entries(options.serviceModes)) {
|
|
20680
|
+
if (mode !== "keep")
|
|
20681
|
+
continue;
|
|
20682
|
+
for (const name of [service, `${service}-init`]) {
|
|
20683
|
+
if (previous.services?.[name]) {
|
|
20684
|
+
services[name] = previous.services[name];
|
|
20685
|
+
app.depends_on = [.../* @__PURE__ */ new Set([...app.depends_on ?? [], service])];
|
|
20686
|
+
}
|
|
20687
|
+
}
|
|
20688
|
+
}
|
|
20689
|
+
const previousMounts = previous.services?.app?.volumes;
|
|
20690
|
+
if (Array.isArray(previousMounts) && Object.values(options.serviceModes).includes("keep"))
|
|
20691
|
+
app.volumes = [.../* @__PURE__ */ new Set([...app.volumes ?? [], ...previousMounts])];
|
|
20692
|
+
compose.volumes = { ...previous.volumes, ...sidecar.volumes };
|
|
20693
|
+
}
|
|
20694
|
+
if (services["storage-init"]) {
|
|
20695
|
+
app.depends_on = {
|
|
20696
|
+
...Object.fromEntries((app.depends_on ?? []).map((name) => [name, { condition: "service_started" }])),
|
|
20697
|
+
"storage-init": { condition: "service_completed_successfully" }
|
|
20698
|
+
};
|
|
20699
|
+
}
|
|
20469
20700
|
return stringify(compose);
|
|
20470
20701
|
}
|
|
20471
20702
|
function serializeEnvFile(env) {
|
|
20472
|
-
return `${Object.entries(env).map(([key, value]) =>
|
|
20703
|
+
return `${Object.entries(env).map(([key, value]) => {
|
|
20704
|
+
let literal2 = value;
|
|
20705
|
+
if (/[\s#$'"]/.test(value)) {
|
|
20706
|
+
if (value.startsWith("{") || value.startsWith("["))
|
|
20707
|
+
literal2 = `'${value.replaceAll("'", "\\u0027")}'`;
|
|
20708
|
+
else if (!value.includes("'"))
|
|
20709
|
+
literal2 = `'${value}'`;
|
|
20710
|
+
else
|
|
20711
|
+
literal2 = JSON.stringify(value).replaceAll("$", "$$");
|
|
20712
|
+
}
|
|
20713
|
+
return `${key}=${literal2}`;
|
|
20714
|
+
}).join("\n")}
|
|
20473
20715
|
`;
|
|
20474
20716
|
}
|
|
20475
20717
|
function generateDockerignore() {
|
|
@@ -20482,31 +20724,46 @@ function generateDockerignore() {
|
|
|
20482
20724
|
**/test-results
|
|
20483
20725
|
**/playwright-report
|
|
20484
20726
|
**/.hai/deploy
|
|
20727
|
+
**/.env.local
|
|
20728
|
+
**/.env.*.local
|
|
20729
|
+
**/.env.runtime
|
|
20485
20730
|
.git
|
|
20486
20731
|
`;
|
|
20487
20732
|
}
|
|
20488
|
-
function generateFallbackDockerfile(packageName, containerPort) {
|
|
20733
|
+
function generateFallbackDockerfile(packageName, containerPort, options = {}) {
|
|
20734
|
+
if (!/^@?[a-z0-9][\w./-]*$/i.test(packageName) || !/^[a-z0-9][\w-]*$/i.test(options.environment ?? "prod"))
|
|
20735
|
+
throw new Error("Invalid container build target");
|
|
20736
|
+
const environment = options.environment ?? "prod";
|
|
20737
|
+
const coreEnv = environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
|
|
20489
20738
|
return `# syntax=docker/dockerfile:1.7
|
|
20490
20739
|
FROM node:24-bookworm-slim AS build
|
|
20740
|
+
ARG SQLITE_PREBUILD_MIRROR
|
|
20741
|
+
ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
|
|
20491
20742
|
ENV PNPM_HOME=/pnpm
|
|
20492
20743
|
ENV PATH=$PNPM_HOME:$PATH
|
|
20493
20744
|
ENV CI=true
|
|
20745
|
+
ENV HAI_DEPLOY_ENVIRONMENT=${environment}
|
|
20746
|
+
ENV HAI_CORE_ENV=${coreEnv}
|
|
20494
20747
|
WORKDIR /workspace
|
|
20495
20748
|
RUN corepack enable && corepack prepare pnpm@12.11.2 --activate
|
|
20496
20749
|
COPY . .
|
|
20497
20750
|
RUN pnpm install --frozen-lockfile --ignore-scripts
|
|
20498
|
-
RUN pnpm --filter ${packageName} build
|
|
20751
|
+
RUN pnpm --filter ${packageName}... build
|
|
20752
|
+
RUN if [ -d node_modules/better-sqlite3 ]; then cd node_modules/better-sqlite3; for attempt in 1 2 3; do node -e "require('node:child_process').execFileSync(process.execPath,[require.resolve('prebuild-install/bin.js'),'--verbose'],{stdio:'inherit'})" && break; [ "$attempt" = 3 ] && exit 1; sleep 3; done; fi
|
|
20499
20753
|
RUN pnpm pm deploy --filter ${packageName} --prod /prod/app
|
|
20500
20754
|
|
|
20501
20755
|
FROM node:24-bookworm-slim AS runtime
|
|
20502
20756
|
ENV NODE_ENV=production
|
|
20757
|
+
ENV HAI_DEPLOY_ENVIRONMENT=${environment}
|
|
20758
|
+
ENV HAI_CORE_ENV=${coreEnv}
|
|
20503
20759
|
ENV HOST=0.0.0.0
|
|
20504
20760
|
ENV PORT=${containerPort}
|
|
20505
20761
|
WORKDIR /app
|
|
20506
20762
|
COPY --from=build --chown=node:node /prod/app ./
|
|
20763
|
+
RUN mkdir -p data && chown node:node data
|
|
20507
20764
|
USER node
|
|
20508
20765
|
EXPOSE ${containerPort}
|
|
20509
|
-
CMD ["node", "build"]
|
|
20766
|
+
CMD ["node", ${JSON.stringify(options.entrypoint ?? "build")}]
|
|
20510
20767
|
`;
|
|
20511
20768
|
}
|
|
20512
20769
|
var logger4 = core.logger.child({ module: "deploy", scope: "command" });
|
|
@@ -20521,10 +20778,10 @@ function maskSecrets(text, secrets) {
|
|
|
20521
20778
|
return masked;
|
|
20522
20779
|
}
|
|
20523
20780
|
function runCommand(command, args, options = {}) {
|
|
20524
|
-
return new Promise((
|
|
20781
|
+
return new Promise((resolve3) => {
|
|
20525
20782
|
const child = spawn(command, args, {
|
|
20526
20783
|
cwd: options.cwd,
|
|
20527
|
-
env: options.env ? { ...
|
|
20784
|
+
env: options.env ? { ...process2.env, ...options.env } : process2.env,
|
|
20528
20785
|
shell: false
|
|
20529
20786
|
});
|
|
20530
20787
|
let stdout = "";
|
|
@@ -20537,7 +20794,7 @@ function runCommand(command, args, options = {}) {
|
|
|
20537
20794
|
settled = true;
|
|
20538
20795
|
if (timer)
|
|
20539
20796
|
clearTimeout(timer);
|
|
20540
|
-
|
|
20797
|
+
resolve3(result);
|
|
20541
20798
|
};
|
|
20542
20799
|
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
20543
20800
|
timer = setTimeout(() => {
|
|
@@ -20574,12 +20831,14 @@ function runCommand(command, args, options = {}) {
|
|
|
20574
20831
|
logger4.error("Command exited with non-zero code", {
|
|
20575
20832
|
command,
|
|
20576
20833
|
code: exitCode,
|
|
20577
|
-
stderr: maskSecrets(stderr, options.secrets).slice(-500)
|
|
20834
|
+
stderr: maskSecrets(stderr, options.secrets).slice(-500),
|
|
20835
|
+
stdout: maskSecrets(stdout, options.secrets).slice(-3e3)
|
|
20578
20836
|
});
|
|
20579
20837
|
finish(err(
|
|
20580
20838
|
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
20581
20839
|
deployM("deploy_commandFailed", {
|
|
20582
|
-
params: { command, error: maskSecrets(
|
|
20840
|
+
params: { command, error: maskSecrets(`${stdout.slice(-2500)}
|
|
20841
|
+
${stderr || `exit ${exitCode}`}`, options.secrets).slice(-3e3) }
|
|
20583
20842
|
})
|
|
20584
20843
|
));
|
|
20585
20844
|
});
|
|
@@ -20612,6 +20871,12 @@ async function buildImage(runtime, options) {
|
|
|
20612
20871
|
const args = ["build", "-f", options.dockerfilePath, "-t", options.imageRef];
|
|
20613
20872
|
if (runtime === "podman")
|
|
20614
20873
|
args.push("--format", "docker", "--jobs", "1");
|
|
20874
|
+
if (runtime === "podman" && existsSync(`${options.dockerfilePath}.dockerignore`))
|
|
20875
|
+
args.push("--ignorefile", `${options.dockerfilePath}.dockerignore`);
|
|
20876
|
+
if (runtime === "podman" && options.httpProxy === false)
|
|
20877
|
+
args.push("--http-proxy=false");
|
|
20878
|
+
for (const [key, value] of Object.entries(options.buildArgs ?? {}))
|
|
20879
|
+
args.push("--build-arg", `${key}=${value}`);
|
|
20615
20880
|
args.push(options.contextDir);
|
|
20616
20881
|
logger5.info("Building container image", { runtime, imageRef: options.imageRef });
|
|
20617
20882
|
const result = await runCommand(runtime, args, { cwd: options.contextDir });
|
|
@@ -20705,6 +20970,9 @@ function createSshExecutor(config2) {
|
|
|
20705
20970
|
var logger7 = core.logger.child({ module: "deploy", scope: "provider-docker-ssh" });
|
|
20706
20971
|
var HEALTH_POLL_INTERVAL = 3e3;
|
|
20707
20972
|
var HEALTH_MAX_ATTEMPTS = 40;
|
|
20973
|
+
function shellArgument(value) {
|
|
20974
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
20975
|
+
}
|
|
20708
20976
|
function makeDeploymentId() {
|
|
20709
20977
|
const now = /* @__PURE__ */ new Date();
|
|
20710
20978
|
const pad = (n) => String(n).padStart(2, "0");
|
|
@@ -20716,7 +20984,7 @@ function parseLoadedImageRef(stdout, fallback) {
|
|
|
20716
20984
|
return match ? match[1] : fallback;
|
|
20717
20985
|
}
|
|
20718
20986
|
function sleep(ms) {
|
|
20719
|
-
return new Promise((
|
|
20987
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
20720
20988
|
}
|
|
20721
20989
|
function readPackageName(appDir) {
|
|
20722
20990
|
try {
|
|
@@ -20739,7 +21007,9 @@ function resolveWorkspaceRoot(appDir) {
|
|
|
20739
21007
|
}
|
|
20740
21008
|
return appDir;
|
|
20741
21009
|
}
|
|
20742
|
-
function createDockerSshProvider(config2,
|
|
21010
|
+
function createDockerSshProvider(config2, container) {
|
|
21011
|
+
const localRuntimePref = container?.runtime ?? "auto";
|
|
21012
|
+
const httpProxy = container?.httpProxy ?? true;
|
|
20743
21013
|
const ssh = createSshExecutor(config2.ssh);
|
|
20744
21014
|
const baseDir = config2.remote.baseDir;
|
|
20745
21015
|
async function detectRemoteRuntime() {
|
|
@@ -20772,7 +21042,7 @@ function createDockerSshProvider(config2, localRuntimePref = "auto") {
|
|
|
20772
21042
|
return false;
|
|
20773
21043
|
}
|
|
20774
21044
|
const deploy2 = async (context) => {
|
|
20775
|
-
const { appDir, projectName, scan, provisionEnv } = context;
|
|
21045
|
+
const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
|
|
20776
21046
|
const deploymentId = makeDeploymentId();
|
|
20777
21047
|
const containerPort = config2.expose.containerPort;
|
|
20778
21048
|
const hostPort = config2.expose.hostPort;
|
|
@@ -20781,7 +21051,7 @@ function createDockerSshProvider(config2, localRuntimePref = "auto") {
|
|
|
20781
21051
|
if (!runtimeResult.success)
|
|
20782
21052
|
return err(runtimeResult.error);
|
|
20783
21053
|
const localRuntime = runtimeResult.data;
|
|
20784
|
-
const artifactDir = join(appDir, ".hai", "deploy");
|
|
21054
|
+
const artifactDir = join(appDir, ".hai", "deploy", options?.environment ?? "prod");
|
|
20785
21055
|
mkdirSync(artifactDir, { recursive: true });
|
|
20786
21056
|
const appDockerfile = join(appDir, "Dockerfile");
|
|
20787
21057
|
let dockerfilePath;
|
|
@@ -20791,21 +21061,33 @@ function createDockerSshProvider(config2, localRuntimePref = "auto") {
|
|
|
20791
21061
|
contextDir = appDir;
|
|
20792
21062
|
} else {
|
|
20793
21063
|
dockerfilePath = join(artifactDir, "Dockerfile");
|
|
20794
|
-
writeFileSync(dockerfilePath, generateFallbackDockerfile(readPackageName(appDir), containerPort));
|
|
21064
|
+
writeFileSync(dockerfilePath, generateFallbackDockerfile(readPackageName(appDir), containerPort, { environment: options?.environment, entrypoint: scan.isSvelteKit ? "build" : "dist/index.js" }));
|
|
20795
21065
|
writeFileSync(`${dockerfilePath}.dockerignore`, generateDockerignore());
|
|
20796
21066
|
contextDir = resolveWorkspaceRoot(appDir);
|
|
20797
21067
|
logger7.info("Generated fallback Dockerfile", { dockerfilePath, contextDir });
|
|
20798
21068
|
}
|
|
21069
|
+
const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
|
|
21070
|
+
const envPath = join(artifactDir, ".env.runtime");
|
|
21071
|
+
const previousEnv = existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {};
|
|
21072
|
+
Object.assign(env, { ...previousEnv, ...env });
|
|
21073
|
+
writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
|
|
21074
|
+
if (options?.environment) {
|
|
21075
|
+
const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
|
|
21076
|
+
if (!environment.success)
|
|
21077
|
+
return environment;
|
|
21078
|
+
const saved = saveDeploymentEnvironment(environment.data, { HAI_DEPLOY_ENVIRONMENT: environment.data.environment, HAI_CORE_ENV: environment.data.values.HAI_CORE_ENV, ...sidecar.env, ...provisionEnv });
|
|
21079
|
+
if (!saved.success)
|
|
21080
|
+
return saved;
|
|
21081
|
+
}
|
|
20799
21082
|
const buildResult = await buildImage(localRuntime, {
|
|
20800
21083
|
contextDir,
|
|
20801
21084
|
dockerfilePath,
|
|
20802
|
-
imageRef: localImageTag
|
|
21085
|
+
imageRef: localImageTag,
|
|
21086
|
+
buildArgs: { HAI_DEPLOY_ENVIRONMENT: options?.environment ?? "prod", HAI_CORE_ENV: env.HAI_CORE_ENV ?? "production", SQLITE_PREBUILD_MIRROR: container?.sqlitePrebuildMirror ?? "" },
|
|
21087
|
+
httpProxy
|
|
20803
21088
|
});
|
|
20804
21089
|
if (!buildResult.success)
|
|
20805
21090
|
return err(buildResult.error);
|
|
20806
|
-
const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort);
|
|
20807
|
-
const envPath = join(artifactDir, ".env.runtime");
|
|
20808
|
-
writeFileSync(envPath, serializeEnvFile(env), { mode: 384 });
|
|
20809
21091
|
const tarPath = join(artifactDir, `${projectName}.tar`);
|
|
20810
21092
|
const saveResult = await saveImage(localRuntime, localImageTag, tarPath);
|
|
20811
21093
|
if (!saveResult.success)
|
|
@@ -20818,20 +21100,20 @@ function createDockerSshProvider(config2, localRuntimePref = "auto") {
|
|
|
20818
21100
|
return err(remoteRuntimeResult.error);
|
|
20819
21101
|
const remoteRuntime = remoteRuntimeResult.data;
|
|
20820
21102
|
const remoteDir = `${baseDir}/${projectName}`;
|
|
20821
|
-
const prepareResult = await ssh.exec(`mkdir -p ${remoteDir} && chmod 700 ${remoteDir}`);
|
|
21103
|
+
const prepareResult = await ssh.exec(`mkdir -p -- ${shellArgument(remoteDir)} && chmod 700 -- ${shellArgument(remoteDir)}`);
|
|
20822
21104
|
if (!prepareResult.success)
|
|
20823
21105
|
return err(prepareResult.error);
|
|
20824
21106
|
const remoteTar = `${remoteDir}/image.tar`;
|
|
20825
21107
|
const uploadTarResult = await ssh.upload(tarPath, remoteTar);
|
|
20826
21108
|
if (!uploadTarResult.success)
|
|
20827
21109
|
return err(uploadTarResult.error);
|
|
20828
|
-
const loadResult = await ssh.exec(`${remoteRuntime} load -i ${remoteTar}`, { timeoutMs: 3e5 });
|
|
21110
|
+
const loadResult = await ssh.exec(`${remoteRuntime} load -i ${shellArgument(remoteTar)}`, { timeoutMs: 3e5 });
|
|
20829
21111
|
if (!loadResult.success)
|
|
20830
21112
|
return err(loadResult.error);
|
|
20831
21113
|
const loadedRef = parseLoadedImageRef(loadResult.data.stdout, localImageTag);
|
|
20832
21114
|
const remoteImageRef = remoteRuntime === "podman" ? `localhost/${localImageTag}` : localImageTag;
|
|
20833
21115
|
if (loadedRef !== remoteImageRef) {
|
|
20834
|
-
const tagResult = await ssh.exec(`${remoteRuntime} tag ${loadedRef} ${remoteImageRef}`);
|
|
21116
|
+
const tagResult = await ssh.exec(`${remoteRuntime} tag ${shellArgument(loadedRef)} ${shellArgument(remoteImageRef)}`);
|
|
20835
21117
|
if (!tagResult.success)
|
|
20836
21118
|
return err(tagResult.error);
|
|
20837
21119
|
}
|
|
@@ -20840,7 +21122,9 @@ function createDockerSshProvider(config2, localRuntimePref = "auto") {
|
|
|
20840
21122
|
imageRef: remoteImageRef,
|
|
20841
21123
|
hostPort,
|
|
20842
21124
|
containerPort,
|
|
20843
|
-
sidecar
|
|
21125
|
+
sidecar,
|
|
21126
|
+
serviceModes: options?.serviceModes,
|
|
21127
|
+
previousCompose: existsSync(join(artifactDir, "compose.yml")) ? readFileSync(join(artifactDir, "compose.yml"), "utf8") : void 0
|
|
20844
21128
|
});
|
|
20845
21129
|
const composePath = join(artifactDir, "compose.yml");
|
|
20846
21130
|
writeFileSync(composePath, composeContent);
|
|
@@ -20850,20 +21134,33 @@ function createDockerSshProvider(config2, localRuntimePref = "auto") {
|
|
|
20850
21134
|
const uploadEnvResult = await ssh.upload(envPath, `${remoteDir}/.env.runtime`);
|
|
20851
21135
|
if (!uploadEnvResult.success)
|
|
20852
21136
|
return err(uploadEnvResult.error);
|
|
21137
|
+
const startScript = `#!/bin/sh
|
|
21138
|
+
set -eu
|
|
21139
|
+
cd "$(dirname "$0")"
|
|
21140
|
+
export HAI_DEPLOY_ENVIRONMENT=${options?.environment ?? "prod"}
|
|
21141
|
+
export HAI_CORE_ENV=${shellArgument(env.HAI_CORE_ENV ?? "production")}
|
|
21142
|
+
exec ${remoteRuntime} compose --env-file .env.runtime -p hai-${projectName} up -d
|
|
21143
|
+
`;
|
|
21144
|
+
const startName = `start-${options?.environment ?? "prod"}.sh`;
|
|
21145
|
+
const startPath = join(artifactDir, startName);
|
|
21146
|
+
writeFileSync(startPath, startScript, { mode: 448 });
|
|
21147
|
+
const uploadStart = await ssh.upload(startPath, `${remoteDir}/${startName}`);
|
|
21148
|
+
if (!uploadStart.success)
|
|
21149
|
+
return uploadStart;
|
|
20853
21150
|
const secretMask = sidecar.secrets;
|
|
20854
|
-
const chmodResult = await ssh.exec(`chmod 600 ${remoteDir}/.env.runtime`, { secrets: secretMask });
|
|
21151
|
+
const chmodResult = await ssh.exec(`chmod 600 -- ${shellArgument(`${remoteDir}/.env.runtime`)}`, { secrets: secretMask });
|
|
20855
21152
|
if (!chmodResult.success)
|
|
20856
21153
|
return err(chmodResult.error);
|
|
20857
21154
|
const upResult = await ssh.exec(
|
|
20858
|
-
`cd ${remoteDir} && ${
|
|
21155
|
+
`cd ${shellArgument(remoteDir)} && sh ${shellArgument(startName)}`,
|
|
20859
21156
|
{ timeoutMs: 3e5, secrets: secretMask }
|
|
20860
21157
|
);
|
|
20861
21158
|
if (!upResult.success)
|
|
20862
21159
|
return err(upResult.error);
|
|
20863
|
-
await ssh.exec(`rm -f ${remoteTar}`);
|
|
21160
|
+
await ssh.exec(`rm -f -- ${shellArgument(remoteTar)}`);
|
|
20864
21161
|
const url2 = `http://${config2.ssh.host}:${hostPort}`;
|
|
20865
21162
|
logger7.info("Running health check", { url: url2 });
|
|
20866
|
-
const healthy = await healthCheck(`${url2}
|
|
21163
|
+
const healthy = await healthCheck(`${url2}${scan.isSvelteKit ? "/" : "/health"}`);
|
|
20867
21164
|
if (!healthy) {
|
|
20868
21165
|
return err(
|
|
20869
21166
|
HaiDeployError.HEALTH_CHECK_FAILED,
|
|
@@ -21118,7 +21415,7 @@ async function pollDeploymentStatus(token, deploymentId) {
|
|
|
21118
21415
|
if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
|
|
21119
21416
|
return "error";
|
|
21120
21417
|
}
|
|
21121
|
-
await new Promise((
|
|
21418
|
+
await new Promise((resolve3) => setTimeout(resolve3, POLL_INTERVAL));
|
|
21122
21419
|
}
|
|
21123
21420
|
return "building";
|
|
21124
21421
|
}
|
|
@@ -21656,15 +21953,31 @@ function normalizeProjectName(name) {
|
|
|
21656
21953
|
const normalized = name.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
21657
21954
|
return normalized || "app";
|
|
21658
21955
|
}
|
|
21659
|
-
function mergeProvisionEnv(results) {
|
|
21956
|
+
function mergeProvisionEnv(results, scan, options) {
|
|
21660
21957
|
let env = {};
|
|
21661
|
-
const
|
|
21958
|
+
const reachRequirements = scan?.middlewares?.filter((item) => item.configName === "reach") ?? [];
|
|
21959
|
+
const reachProviders = reachRequirements.map((item) => item.config);
|
|
21960
|
+
let hasReachOutput = false;
|
|
21662
21961
|
for (const prov of results) {
|
|
21663
|
-
if (prov.envVars.HAI_REACH_PROVIDERS)
|
|
21664
|
-
|
|
21962
|
+
if (prov.envVars.HAI_REACH_PROVIDERS) {
|
|
21963
|
+
hasReachOutput = true;
|
|
21964
|
+
const supplied = JSON.parse(prov.envVars.HAI_REACH_PROVIDERS);
|
|
21965
|
+
for (const provider of supplied) {
|
|
21966
|
+
const targets = reachRequirements.filter((item) => {
|
|
21967
|
+
const mode = options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.[item.service];
|
|
21968
|
+
return item.service === prov.serviceType && (!mode || mode === "default");
|
|
21969
|
+
});
|
|
21970
|
+
if (targets.length) {
|
|
21971
|
+
for (const target of targets)
|
|
21972
|
+
reachProviders[reachRequirements.indexOf(target)] = { ...provider, name: target.config.name ?? provider.name };
|
|
21973
|
+
} else {
|
|
21974
|
+
reachProviders.push(provider);
|
|
21975
|
+
}
|
|
21976
|
+
}
|
|
21977
|
+
}
|
|
21665
21978
|
env = { ...env, ...prov.envVars };
|
|
21666
21979
|
}
|
|
21667
|
-
if (
|
|
21980
|
+
if (hasReachOutput)
|
|
21668
21981
|
env.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
|
|
21669
21982
|
return env;
|
|
21670
21983
|
}
|
|
@@ -21699,7 +22012,7 @@ var deploy = {
|
|
|
21699
22012
|
let provider = null;
|
|
21700
22013
|
let containerProvider = null;
|
|
21701
22014
|
if (parsed.provider.type === "docker-ssh") {
|
|
21702
|
-
containerProvider = createDockerSshProvider(parsed.provider, parsed.container
|
|
22015
|
+
containerProvider = createDockerSshProvider(parsed.provider, parsed.container);
|
|
21703
22016
|
logger14.info("Container deploy provider ready", { provider: "docker-ssh", host: parsed.provider.ssh.host });
|
|
21704
22017
|
} else {
|
|
21705
22018
|
provider = createProviderByType(parsed.provider.type);
|
|
@@ -21777,17 +22090,19 @@ var deploy = {
|
|
|
21777
22090
|
get isInitialized() {
|
|
21778
22091
|
return currentProvider !== null || currentContainerProvider !== null;
|
|
21779
22092
|
},
|
|
21780
|
-
async scan(appDir) {
|
|
22093
|
+
async scan(appDir, options) {
|
|
21781
22094
|
logger14.debug("Scanning application", { appDir });
|
|
21782
|
-
return scanApp(appDir);
|
|
22095
|
+
return scanApp(appDir, options);
|
|
21783
22096
|
},
|
|
21784
|
-
async provisionAll(projectName) {
|
|
22097
|
+
async provisionAll(projectName, services) {
|
|
21785
22098
|
if (!currentProvider && !currentContainerProvider) {
|
|
21786
22099
|
return notInitialized.result();
|
|
21787
22100
|
}
|
|
21788
22101
|
logger14.info("Provisioning all services", { projectName, count: currentProvisioners.size });
|
|
21789
22102
|
const results = [];
|
|
21790
22103
|
for (const [serviceType, provisioner] of currentProvisioners) {
|
|
22104
|
+
if (services && !services.includes(serviceType))
|
|
22105
|
+
continue;
|
|
21791
22106
|
logger14.debug("Provisioning service", { serviceType, provisioner: provisioner.name });
|
|
21792
22107
|
const provResult = await provisioner.provision(projectName);
|
|
21793
22108
|
if (!provResult.success) {
|
|
@@ -21816,15 +22131,26 @@ var deploy = {
|
|
|
21816
22131
|
if (!currentProvider && !currentContainerProvider) {
|
|
21817
22132
|
return notInitialized.result();
|
|
21818
22133
|
}
|
|
21819
|
-
logger14.info("Starting deployment", { appDir, options });
|
|
21820
|
-
const
|
|
22134
|
+
logger14.info("Starting deployment", { appDir, environment: options?.environment, projectName: options?.projectName });
|
|
22135
|
+
const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
|
|
22136
|
+
const scanResult = await scanApp(appDir, scanOptions);
|
|
21821
22137
|
if (!scanResult.success) {
|
|
21822
22138
|
return err(scanResult.error);
|
|
21823
22139
|
}
|
|
21824
22140
|
const scan = scanResult.data;
|
|
21825
|
-
|
|
22141
|
+
appDir = scan.appDir ?? appDir;
|
|
22142
|
+
const environment = loadDeploymentEnvironment({ environment: scanOptions.environment ?? "prod", envDir: scan.envDir ?? appDir, requireFile: scanOptions.environment !== void 0 });
|
|
22143
|
+
if (!environment.success)
|
|
22144
|
+
return environment;
|
|
22145
|
+
const appEnv = Object.fromEntries(Object.entries(environment.data.values).filter(([key]) => !key.startsWith("HAI_CLI_") && (!key.startsWith("HAI_DEPLOY_") || key === "HAI_DEPLOY_ENVIRONMENT")));
|
|
22146
|
+
const projectName = `${options?.projectName ?? scan.appName}${scanOptions.environment ? `-${environment.data.environment}` : ""}`;
|
|
22147
|
+
options = {
|
|
22148
|
+
...options,
|
|
22149
|
+
...scanOptions.environment ? { environment: environment.data.environment } : {},
|
|
22150
|
+
...options?.skipProvision ? { serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"])) } : {}
|
|
22151
|
+
};
|
|
21826
22152
|
if (currentContainerProvider) {
|
|
21827
|
-
return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options);
|
|
22153
|
+
return deployContainerApp(currentContainerProvider, appDir, scan, projectName, options, appEnv);
|
|
21828
22154
|
}
|
|
21829
22155
|
if (!currentProvider) {
|
|
21830
22156
|
return notInitialized.result();
|
|
@@ -21837,15 +22163,20 @@ var deploy = {
|
|
|
21837
22163
|
})
|
|
21838
22164
|
);
|
|
21839
22165
|
}
|
|
21840
|
-
let allEnvVars =
|
|
22166
|
+
let allEnvVars = appEnv;
|
|
21841
22167
|
const recovery = { projectName, stage: "provision", resources: [] };
|
|
21842
22168
|
if (!options?.skipProvision) {
|
|
21843
|
-
const provResults = await deploy.provisionAll(projectName);
|
|
22169
|
+
const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
|
|
21844
22170
|
if (!provResults.success) {
|
|
21845
22171
|
return err(provResults.error);
|
|
21846
22172
|
}
|
|
21847
22173
|
recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
|
|
21848
|
-
allEnvVars = mergeProvisionEnv(provResults.data);
|
|
22174
|
+
allEnvVars = { ...allEnvVars, ...mergeProvisionEnv(provResults.data, scan, options) };
|
|
22175
|
+
if (options?.environment) {
|
|
22176
|
+
const saved = saveDeploymentEnvironment(environment.data, mergeProvisionEnv(provResults.data, scan, options));
|
|
22177
|
+
if (!saved.success)
|
|
22178
|
+
return recoveryFailure(saved.error, { ...recovery, stage: "save-env" });
|
|
22179
|
+
}
|
|
21849
22180
|
}
|
|
21850
22181
|
const projectResult = await currentProvider.createProject(projectName);
|
|
21851
22182
|
if (!projectResult.success) {
|
|
@@ -21882,7 +22213,7 @@ var deploy = {
|
|
|
21882
22213
|
return ok(result);
|
|
21883
22214
|
}
|
|
21884
22215
|
};
|
|
21885
|
-
async function deployContainerApp(provider, appDir, scan, rawProjectName, options) {
|
|
22216
|
+
async function deployContainerApp(provider, appDir, scan, rawProjectName, options, appEnv = {}) {
|
|
21886
22217
|
const projectName = normalizeProjectName(rawProjectName);
|
|
21887
22218
|
if (scan.isSvelteKit && !scan.nodeAdapterInstalled) {
|
|
21888
22219
|
return err(
|
|
@@ -21892,15 +22223,60 @@ async function deployContainerApp(provider, appDir, scan, rawProjectName, option
|
|
|
21892
22223
|
}
|
|
21893
22224
|
let provisionEnv = {};
|
|
21894
22225
|
if (!options?.skipProvision) {
|
|
21895
|
-
const provResults = await deploy.provisionAll(projectName);
|
|
22226
|
+
const provResults = await deploy.provisionAll(projectName, options?.serviceModes ? scan.requiredServices.filter((service) => usesDefaultMiddleware(scan, service, options)) : void 0);
|
|
21896
22227
|
if (!provResults.success) {
|
|
21897
22228
|
return err(provResults.error);
|
|
21898
22229
|
}
|
|
21899
|
-
provisionEnv = mergeProvisionEnv(provResults.data);
|
|
22230
|
+
provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
|
|
21900
22231
|
}
|
|
21901
|
-
return provider.deploy({ appDir, projectName, scan, provisionEnv });
|
|
22232
|
+
return provider.deploy({ appDir, projectName, scan, provisionEnv, appEnv, options });
|
|
22233
|
+
}
|
|
22234
|
+
|
|
22235
|
+
// ../deploy/src/deploy-middleware.ts
|
|
22236
|
+
var field = (name, defaultValue, kind, secret = false) => ({ name, defaultValue, kind, secret });
|
|
22237
|
+
var required2 = (name, defaultValue, kind) => ({ ...field(name, defaultValue, kind), required: true });
|
|
22238
|
+
var DEPLOY_MIDDLEWARE = {
|
|
22239
|
+
db: [
|
|
22240
|
+
{ backend: "sqlite", label: "SQLite", purpose: "deploy_purposeSqlite", fields: [required2("database", "./data/app.db")] },
|
|
22241
|
+
...["postgresql", "mysql"].map((backend) => ({
|
|
22242
|
+
backend,
|
|
22243
|
+
label: backend === "mysql" ? "MySQL" : "PostgreSQL",
|
|
22244
|
+
purpose: "deploy_purposeDatabase",
|
|
22245
|
+
provisioner: backend === "postgresql" ? "neon" : void 0,
|
|
22246
|
+
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")]
|
|
22247
|
+
}))
|
|
22248
|
+
],
|
|
22249
|
+
cache: [
|
|
22250
|
+
{ backend: "memory", label: "Memory", purpose: "deploy_purposeMemory", fields: [] },
|
|
22251
|
+
{ 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")] }
|
|
22252
|
+
],
|
|
22253
|
+
storage: [
|
|
22254
|
+
{ backend: "local", label: "Local filesystem", purpose: "deploy_purposeLocal", fields: [required2("root", "./data/storage")] },
|
|
22255
|
+
{ 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")] }
|
|
22256
|
+
],
|
|
22257
|
+
vecdb: [
|
|
22258
|
+
{ backend: "lancedb", label: "LanceDB", purpose: "deploy_purposeLocal", fields: [required2("path", "./data/vectors")] },
|
|
22259
|
+
{ backend: "qdrant", label: "Qdrant", purpose: "deploy_purposeVector", fields: [required2("url", "http://localhost:6333", "url"), field("apiKey", "", void 0, true)] },
|
|
22260
|
+
{ 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)] },
|
|
22261
|
+
{ backend: "chroma", label: "Chroma", purpose: "deploy_purposeVector", fields: [required2("url", "http://localhost:8000", "url")] }
|
|
22262
|
+
],
|
|
22263
|
+
ldap: [
|
|
22264
|
+
{ backend: "disabled", label: "Disabled", purpose: "deploy_purposeDisabled", fields: [] },
|
|
22265
|
+
{ 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 }] }
|
|
22266
|
+
],
|
|
22267
|
+
email: [
|
|
22268
|
+
{ backend: "console", label: "Console", purpose: "deploy_purposeConsole", fields: [] },
|
|
22269
|
+
{ 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")] },
|
|
22270
|
+
{ backend: "api", label: "HTTP notification API", purpose: "deploy_purposeEmail", fields: [required2("url", "", "url"), field("method", "POST"), field("headers"), field("timeout", "10000", "number")] }
|
|
22271
|
+
],
|
|
22272
|
+
sms: [
|
|
22273
|
+
{ 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")] }
|
|
22274
|
+
]
|
|
22275
|
+
};
|
|
22276
|
+
function describeMiddlewareBackend(backend) {
|
|
22277
|
+
return deployM(backend.purpose);
|
|
21902
22278
|
}
|
|
21903
22279
|
|
|
21904
|
-
export { DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, getDeployRecovery };
|
|
21905
|
-
//# sourceMappingURL=src-
|
|
21906
|
-
//# sourceMappingURL=src-
|
|
22280
|
+
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, HaiDeployError, ProviderConfigSchema, deploy, describeMiddlewareBackend, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, saveDeploymentEnvironment };
|
|
22281
|
+
//# sourceMappingURL=src-6PPBKQ6Q.js.map
|
|
22282
|
+
//# sourceMappingURL=src-6PPBKQ6Q.js.map
|