@better-openclaw/core 1.0.7 → 1.0.9
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/dist/bare-metal-partition.d.mts.map +1 -1
- package/dist/bare-metal-partition.mjs +1 -0
- package/dist/bare-metal-partition.mjs.map +1 -1
- package/dist/composer.d.mts.map +1 -1
- package/dist/composer.mjs +11 -1
- package/dist/composer.mjs.map +1 -1
- package/dist/errors.d.mts +17 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +24 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/generate.d.mts +4 -3
- package/dist/generate.d.mts.map +1 -1
- package/dist/generate.mjs +13 -5
- package/dist/generate.mjs.map +1 -1
- package/dist/generators/traefik.d.mts +19 -0
- package/dist/generators/traefik.d.mts.map +1 -0
- package/dist/generators/traefik.mjs +86 -0
- package/dist/generators/traefik.mjs.map +1 -0
- package/dist/generators/traefik.test.d.mts +1 -0
- package/dist/generators/traefik.test.mjs +69 -0
- package/dist/generators/traefik.test.mjs.map +1 -0
- package/dist/index.d.mts +4 -2
- package/dist/index.mjs +4 -2
- package/dist/migrations.d.mts +14 -0
- package/dist/migrations.d.mts.map +1 -0
- package/dist/migrations.mjs +33 -0
- package/dist/migrations.mjs.map +1 -0
- package/dist/migrations.test.d.mts +1 -0
- package/dist/migrations.test.mjs +42 -0
- package/dist/migrations.test.mjs.map +1 -0
- package/dist/resolver.d.mts +6 -1
- package/dist/resolver.d.mts.map +1 -1
- package/dist/resolver.mjs +21 -3
- package/dist/resolver.mjs.map +1 -1
- package/dist/schema.d.mts +9 -0
- package/dist/schema.d.mts.map +1 -1
- package/dist/schema.mjs +4 -1
- package/dist/schema.mjs.map +1 -1
- package/dist/services/definitions/caddy.mjs +20 -1
- package/dist/services/definitions/caddy.mjs.map +1 -1
- package/dist/services/definitions/cal-com.d.mts +7 -0
- package/dist/services/definitions/cal-com.d.mts.map +1 -0
- package/dist/services/definitions/cal-com.mjs +88 -0
- package/dist/services/definitions/cal-com.mjs.map +1 -0
- package/dist/services/definitions/comfyui.d.mts +7 -0
- package/dist/services/definitions/comfyui.d.mts.map +1 -0
- package/dist/services/definitions/comfyui.mjs +83 -0
- package/dist/services/definitions/comfyui.mjs.map +1 -0
- package/dist/services/definitions/desktop-environment.d.mts +7 -0
- package/dist/services/definitions/desktop-environment.d.mts.map +1 -0
- package/dist/services/definitions/desktop-environment.mjs +153 -0
- package/dist/services/definitions/desktop-environment.mjs.map +1 -0
- package/dist/services/definitions/grafana.mjs +13 -1
- package/dist/services/definitions/grafana.mjs.map +1 -1
- package/dist/services/definitions/index.d.mts +7 -1
- package/dist/services/definitions/index.d.mts.map +1 -1
- package/dist/services/definitions/index.mjs +14 -2
- package/dist/services/definitions/index.mjs.map +1 -1
- package/dist/services/definitions/neo4j.d.mts +7 -0
- package/dist/services/definitions/neo4j.d.mts.map +1 -0
- package/dist/services/definitions/neo4j.mjs +91 -0
- package/dist/services/definitions/neo4j.mjs.map +1 -0
- package/dist/services/definitions/stream-gateway.d.mts +7 -0
- package/dist/services/definitions/stream-gateway.d.mts.map +1 -0
- package/dist/services/definitions/stream-gateway.mjs +133 -0
- package/dist/services/definitions/stream-gateway.mjs.map +1 -0
- package/dist/services/definitions/traefik.mjs +0 -1
- package/dist/services/definitions/traefik.mjs.map +1 -1
- package/dist/services/definitions/xyops.d.mts +7 -0
- package/dist/services/definitions/xyops.d.mts.map +1 -0
- package/dist/services/definitions/xyops.mjs +86 -0
- package/dist/services/definitions/xyops.mjs.map +1 -0
- package/dist/types.d.mts +8 -1
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs +10 -0
- package/dist/types.mjs.map +1 -1
- package/dist/validator.mjs +11 -0
- package/dist/validator.mjs.map +1 -1
- package/dist/version-manager.d.mts +1 -1
- package/dist/version-manager.d.mts.map +1 -1
- package/dist/version-manager.mjs +11 -5
- package/dist/version-manager.mjs.map +1 -1
- package/dist/version-manager.test.d.mts +1 -0
- package/dist/version-manager.test.mjs +102 -0
- package/dist/version-manager.test.mjs.map +1 -0
- package/package.json +1 -1
- package/src/__snapshots__/composer.snapshot.test.ts.snap +15 -1
- package/src/bare-metal-partition.ts +1 -0
- package/src/composer.ts +22 -1
- package/src/errors.ts +23 -0
- package/src/generate.ts +22 -4
- package/src/generators/traefik.test.ts +97 -0
- package/src/generators/traefik.ts +104 -0
- package/src/index.ts +7 -1
- package/src/migrations.test.ts +36 -0
- package/src/migrations.ts +49 -0
- package/src/resolver.ts +37 -3
- package/src/schema.ts +3 -0
- package/src/services/definitions/caddy.ts +23 -1
- package/src/services/definitions/cal-com.ts +91 -0
- package/src/services/definitions/comfyui.ts +90 -0
- package/src/services/definitions/desktop-environment.ts +163 -0
- package/src/services/definitions/grafana.ts +16 -1
- package/src/services/definitions/index.ts +18 -0
- package/src/services/definitions/neo4j.ts +96 -0
- package/src/services/definitions/stream-gateway.ts +148 -0
- package/src/services/definitions/traefik.ts +0 -2
- package/src/services/definitions/xyops.ts +94 -0
- package/src/types.ts +7 -1
- package/src/validator.ts +16 -0
- package/src/version-manager.test.ts +134 -0
- package/src/version-manager.ts +12 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bare-metal-partition.d.mts","names":[],"sources":["../src/bare-metal-partition.ts"],"mappings":";;;;;AAKA;iBAAgB,wBAAA,CAAyB,QAAA,EAAU,QAAA,GAAW,cAAA;AAAA,
|
|
1
|
+
{"version":3,"file":"bare-metal-partition.d.mts","names":[],"sources":["../src/bare-metal-partition.ts"],"mappings":";;;;;AAKA;iBAAgB,wBAAA,CAAyB,QAAA,EAAU,QAAA,GAAW,cAAA;AAAA,UAQ7C,wBAAA;EAChB,cAAA,EAAgB,eAAA;EAChB,kBAAA,EAAoB,eAAA;EACpB,SAAA,EAAW,GAAA;AAAA;;;AAHZ;;;iBAWgB,kBAAA,CACf,QAAA,EAAU,cAAA,EACV,QAAA,EAAU,QAAA,GACR,wBAAA;;;;;iBA4Ba,wBAAA,CACf,QAAA,EAAU,cAAA,EACV,QAAA,EAAU,eAAA,KACR,cAAA"}
|
|
@@ -6,6 +6,7 @@ function platformToNativePlatform(platform) {
|
|
|
6
6
|
if (platform.startsWith("linux/")) return "linux";
|
|
7
7
|
if (platform.startsWith("windows/")) return "windows";
|
|
8
8
|
if (platform.startsWith("macos/")) return "macos";
|
|
9
|
+
console.warn(`Unknown platform prefix in "${platform}", defaulting to linux`);
|
|
9
10
|
return "linux";
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bare-metal-partition.mjs","names":[],"sources":["../src/bare-metal-partition.ts"],"sourcesContent":["import type { NativePlatform, Platform, ResolvedService, ResolverOutput } from \"./types.js\";\n\n/**\n * Maps Platform (e.g. linux/amd64) to NativePlatform (linux, windows, macos).\n */\nexport function platformToNativePlatform(platform: Platform): NativePlatform {\n\tif (platform.startsWith(\"linux/\")) return \"linux\";\n\tif (platform.startsWith(\"windows/\")) return \"windows\";\n\tif (platform.startsWith(\"macos/\")) return \"macos\";\n\treturn \"linux\";\n}\n\nexport interface BareMetalPartitionResult {\n\tnativeServices: ResolvedService[];\n\tdockerOnlyServices: ResolvedService[];\n\tnativeIds: Set<string>;\n}\n\n/**\n * Partitions resolved services into native (run on host via scripts) vs Docker-only.\n * Gateway and CLI are never native; services with nativeSupported and a matching\n * nativeRecipe for the platform go to native, rest to Docker.\n */\nexport function partitionBareMetal(\n\tresolved: ResolverOutput,\n\tplatform: Platform,\n): BareMetalPartitionResult {\n\tconst nativePlatform = platformToNativePlatform(platform);\n\tconst nativeServices: ResolvedService[] = [];\n\tconst dockerOnlyServices: ResolvedService[] = [];\n\tconst nativeIds = new Set<string>();\n\n\tfor (const entry of resolved.services) {\n\t\tconst def = entry.definition;\n\t\tconst hasNativeRecipe =\n\t\t\tdef.nativeSupported === true &&\n\t\t\tdef.nativeRecipes?.length &&\n\t\t\tdef.nativeRecipes.some((r) => r.platform === nativePlatform);\n\n\t\tif (hasNativeRecipe) {\n\t\t\tnativeServices.push(entry);\n\t\t\tnativeIds.add(def.id);\n\t\t} else {\n\t\t\tdockerOnlyServices.push(entry);\n\t\t}\n\t}\n\n\treturn { nativeServices, dockerOnlyServices, nativeIds };\n}\n\n/**\n * Returns a new ResolverOutput containing only the given service list (same order).\n * Used to build compose for Docker-only subset when bare-metal has native services.\n */\nexport function resolvedWithOnlyServices(\n\tresolved: ResolverOutput,\n\tservices: ResolvedService[],\n): ResolverOutput {\n\tconst idSet = new Set(services.map((s) => s.definition.id));\n\treturn {\n\t\t...resolved,\n\t\tservices: resolved.services.filter((s) => idSet.has(s.definition.id)),\n\t};\n}\n"],"mappings":";;;;AAKA,SAAgB,yBAAyB,UAAoC;AAC5E,KAAI,SAAS,WAAW,SAAS,CAAE,QAAO;AAC1C,KAAI,SAAS,WAAW,WAAW,CAAE,QAAO;AAC5C,KAAI,SAAS,WAAW,SAAS,CAAE,QAAO;AAC1C,QAAO;;;;;;;AAcR,SAAgB,mBACf,UACA,UAC2B;CAC3B,MAAM,iBAAiB,yBAAyB,SAAS;CACzD,MAAM,iBAAoC,EAAE;CAC5C,MAAM,qBAAwC,EAAE;CAChD,MAAM,4BAAY,IAAI,KAAa;AAEnC,MAAK,MAAM,SAAS,SAAS,UAAU;EACtC,MAAM,MAAM,MAAM;AAMlB,MAJC,IAAI,oBAAoB,QACxB,IAAI,eAAe,UACnB,IAAI,cAAc,MAAM,MAAM,EAAE,aAAa,eAAe,EAExC;AACpB,kBAAe,KAAK,MAAM;AAC1B,aAAU,IAAI,IAAI,GAAG;QAErB,oBAAmB,KAAK,MAAM;;AAIhC,QAAO;EAAE;EAAgB;EAAoB;EAAW;;;;;;AAOzD,SAAgB,yBACf,UACA,UACiB;CACjB,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,WAAW,GAAG,CAAC;AAC3D,QAAO;EACN,GAAG;EACH,UAAU,SAAS,SAAS,QAAQ,MAAM,MAAM,IAAI,EAAE,WAAW,GAAG,CAAC;EACrE"}
|
|
1
|
+
{"version":3,"file":"bare-metal-partition.mjs","names":[],"sources":["../src/bare-metal-partition.ts"],"sourcesContent":["import type { NativePlatform, Platform, ResolvedService, ResolverOutput } from \"./types.js\";\n\n/**\n * Maps Platform (e.g. linux/amd64) to NativePlatform (linux, windows, macos).\n */\nexport function platformToNativePlatform(platform: Platform): NativePlatform {\n\tif (platform.startsWith(\"linux/\")) return \"linux\";\n\tif (platform.startsWith(\"windows/\")) return \"windows\";\n\tif (platform.startsWith(\"macos/\")) return \"macos\";\n\tconsole.warn(`Unknown platform prefix in \"${platform}\", defaulting to linux`);\n\treturn \"linux\";\n}\n\nexport interface BareMetalPartitionResult {\n\tnativeServices: ResolvedService[];\n\tdockerOnlyServices: ResolvedService[];\n\tnativeIds: Set<string>;\n}\n\n/**\n * Partitions resolved services into native (run on host via scripts) vs Docker-only.\n * Gateway and CLI are never native; services with nativeSupported and a matching\n * nativeRecipe for the platform go to native, rest to Docker.\n */\nexport function partitionBareMetal(\n\tresolved: ResolverOutput,\n\tplatform: Platform,\n): BareMetalPartitionResult {\n\tconst nativePlatform = platformToNativePlatform(platform);\n\tconst nativeServices: ResolvedService[] = [];\n\tconst dockerOnlyServices: ResolvedService[] = [];\n\tconst nativeIds = new Set<string>();\n\n\tfor (const entry of resolved.services) {\n\t\tconst def = entry.definition;\n\t\tconst hasNativeRecipe =\n\t\t\tdef.nativeSupported === true &&\n\t\t\tdef.nativeRecipes?.length &&\n\t\t\tdef.nativeRecipes.some((r) => r.platform === nativePlatform);\n\n\t\tif (hasNativeRecipe) {\n\t\t\tnativeServices.push(entry);\n\t\t\tnativeIds.add(def.id);\n\t\t} else {\n\t\t\tdockerOnlyServices.push(entry);\n\t\t}\n\t}\n\n\treturn { nativeServices, dockerOnlyServices, nativeIds };\n}\n\n/**\n * Returns a new ResolverOutput containing only the given service list (same order).\n * Used to build compose for Docker-only subset when bare-metal has native services.\n */\nexport function resolvedWithOnlyServices(\n\tresolved: ResolverOutput,\n\tservices: ResolvedService[],\n): ResolverOutput {\n\tconst idSet = new Set(services.map((s) => s.definition.id));\n\treturn {\n\t\t...resolved,\n\t\tservices: resolved.services.filter((s) => idSet.has(s.definition.id)),\n\t};\n}\n"],"mappings":";;;;AAKA,SAAgB,yBAAyB,UAAoC;AAC5E,KAAI,SAAS,WAAW,SAAS,CAAE,QAAO;AAC1C,KAAI,SAAS,WAAW,WAAW,CAAE,QAAO;AAC5C,KAAI,SAAS,WAAW,SAAS,CAAE,QAAO;AAC1C,SAAQ,KAAK,+BAA+B,SAAS,wBAAwB;AAC7E,QAAO;;;;;;;AAcR,SAAgB,mBACf,UACA,UAC2B;CAC3B,MAAM,iBAAiB,yBAAyB,SAAS;CACzD,MAAM,iBAAoC,EAAE;CAC5C,MAAM,qBAAwC,EAAE;CAChD,MAAM,4BAAY,IAAI,KAAa;AAEnC,MAAK,MAAM,SAAS,SAAS,UAAU;EACtC,MAAM,MAAM,MAAM;AAMlB,MAJC,IAAI,oBAAoB,QACxB,IAAI,eAAe,UACnB,IAAI,cAAc,MAAM,MAAM,EAAE,aAAa,eAAe,EAExC;AACpB,kBAAe,KAAK,MAAM;AAC1B,aAAU,IAAI,IAAI,GAAG;QAErB,oBAAmB,KAAK,MAAM;;AAIhC,QAAO;EAAE;EAAgB;EAAoB;EAAW;;;;;;AAOzD,SAAgB,yBACf,UACA,UACiB;CACjB,MAAM,QAAQ,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,WAAW,GAAG,CAAC;AAC3D,QAAO;EACN,GAAG;EACH,UAAU,SAAS,SAAS,QAAQ,MAAM,MAAM,IAAI,EAAE,WAAW,GAAG,CAAC;EACrE"}
|
package/dist/composer.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composer.d.mts","names":[],"sources":["../src/composer.ts"],"mappings":";;;UAKiB,aAAA;EAChB,KAAA,EAAO,MAAA;EACP,QAAA;EACA,QAAA;AAAA;;;;;
|
|
1
|
+
{"version":3,"file":"composer.d.mts","names":[],"sources":["../src/composer.ts"],"mappings":";;;UAKiB,aAAA;EAChB,KAAA,EAAO,MAAA;EACP,QAAA;EACA,QAAA;AAAA;;;;;iBAmQe,OAAA,CAAQ,QAAA,EAAU,cAAA,EAAgB,OAAA,EAAS,cAAA;;;AAA3D;;iBAkDgB,gBAAA,CAAiB,QAAA,EAAU,cAAA,EAAgB,OAAA,EAAS,cAAA,GAAiB,aAAA"}
|
package/dist/composer.mjs
CHANGED
|
@@ -92,6 +92,8 @@ function buildGatewayServices(resolved, options, dependsOn) {
|
|
|
92
92
|
"18789"
|
|
93
93
|
]
|
|
94
94
|
};
|
|
95
|
+
const gwTraefikLabels = options.traefikLabels?.get("openclaw-gateway");
|
|
96
|
+
if (gwTraefikLabels) gateway.labels = gwTraefikLabels;
|
|
95
97
|
if (options.bareMetalNativeHost) gateway.extra_hosts = ["host.docker.internal:host-gateway"];
|
|
96
98
|
if (dependsOn && Object.keys(dependsOn).length > 0) gateway.depends_on = dependsOn;
|
|
97
99
|
return {
|
|
@@ -156,7 +158,15 @@ function buildCompanionService(def, resolved, options, allVolumes) {
|
|
|
156
158
|
svc.networks = def.networks;
|
|
157
159
|
if (def.command) svc.command = def.command;
|
|
158
160
|
if (def.entrypoint) svc.entrypoint = def.entrypoint;
|
|
159
|
-
|
|
161
|
+
const mergedLabels = {};
|
|
162
|
+
if (def.labels) Object.assign(mergedLabels, def.labels);
|
|
163
|
+
const traefikLabels = options.traefikLabels?.get(def.id);
|
|
164
|
+
if (traefikLabels) Object.assign(mergedLabels, traefikLabels);
|
|
165
|
+
if (Object.keys(mergedLabels).length > 0) svc.labels = mergedLabels;
|
|
166
|
+
if (def.id === "traefik" && options.traefikLabels) {
|
|
167
|
+
if (!svc.volumes) svc.volumes = [];
|
|
168
|
+
svc.volumes.push("./traefik/traefik.yml:/etc/traefik/traefik.yml:ro", "/var/run/docker.sock:/var/run/docker.sock:ro");
|
|
169
|
+
}
|
|
160
170
|
let deploy;
|
|
161
171
|
if (def.deploy) deploy = JSON.parse(JSON.stringify(def.deploy));
|
|
162
172
|
if (options.gpu && def.gpuRequired) {
|
package/dist/composer.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composer.mjs","names":[],"sources":["../src/composer.ts"],"sourcesContent":["import { stringify } from \"yaml\";\nimport type { ComposeOptions, ResolverOutput, ServiceCategory } from \"./types.js\";\n\n// ── Public Types ────────────────────────────────────────────────────────────\n\nexport interface ComposeResult {\n\tfiles: Record<string, string>; // filename -> YAML content\n\tmainFile: string; // \"docker-compose.yml\"\n\tprofiles: string[]; // list of profiles used\n}\n\n// ── Category → Profile Mapping ──────────────────────────────────────────────\n\nconst CATEGORY_PROFILE_MAP: Partial<Record<ServiceCategory, { file: string; profile: string }>> = {\n\tai: { file: \"docker-compose.ai.yml\", profile: \"ai\" },\n\t\"ai-platform\": { file: \"docker-compose.ai.yml\", profile: \"ai\" },\n\tmedia: { file: \"docker-compose.media.yml\", profile: \"media\" },\n\tmonitoring: { file: \"docker-compose.monitoring.yml\", profile: \"monitoring\" },\n\tanalytics: { file: \"docker-compose.monitoring.yml\", profile: \"monitoring\" },\n\t\"dev-tools\": { file: \"docker-compose.tools.yml\", profile: \"tools\" },\n\t\"coding-agent\": { file: \"docker-compose.tools.yml\", profile: \"tools\" },\n\t\"social-media\": { file: \"docker-compose.social.yml\", profile: \"social\" },\n\tknowledge: { file: \"docker-compose.knowledge.yml\", profile: \"knowledge\" },\n\tcommunication: { file: \"docker-compose.communication.yml\", profile: \"communication\" },\n};\n\nconst YAML_OPTIONS = { lineWidth: 120, nullStr: \"\" };\n\n// ── Shared Gateway Builder ──────────────────────────────────────────────────\n\ninterface GatewayBuildResult {\n\tgatewayService: Record<string, unknown>;\n\tcliService: Record<string, unknown>;\n\tallVolumes: Set<string>;\n}\n\n/**\n * Builds the OpenClaw gateway and CLI service entries.\n * Matches the real OpenClaw docker-compose.yml structure:\n * - Bridge port 18790 for ACP/WebSocket\n * - Bind-mount volumes (not named volumes)\n * - Claude web-provider session env vars\n * - Gateway bind mode (--bind lan)\n * - CLI companion service with stdin/tty\n */\nfunction buildGatewayServices(\n\tresolved: ResolverOutput,\n\toptions: ComposeOptions,\n\tdependsOn?: Record<string, { condition: string }>,\n): GatewayBuildResult {\n\tconst allVolumes = new Set<string>();\n\n\t// Gateway environment\n\tconst gatewayEnv: Record<string, string> = {\n\t\tHOME: \"/home/node\",\n\t\tTERM: \"xterm-256color\",\n\t\tOPENCLAW_GATEWAY_TOKEN: \"${OPENCLAW_GATEWAY_TOKEN}\",\n\t\t// Claude web-provider session vars (optional, user fills in .env)\n\t\tCLAUDE_AI_SESSION_KEY: \"${CLAUDE_AI_SESSION_KEY}\",\n\t\tCLAUDE_WEB_SESSION_KEY: \"${CLAUDE_WEB_SESSION_KEY}\",\n\t\tCLAUDE_WEB_COOKIE: \"${CLAUDE_WEB_COOKIE}\",\n\t};\n\n\t// Gateway volumes (bind-mount style matching real docker-setup.sh)\n\tconst gatewayVolumes: string[] = [\n\t\t\"${OPENCLAW_CONFIG_DIR:-./openclaw/config}:/home/node/.openclaw\",\n\t\t\"${OPENCLAW_WORKSPACE_DIR:-./openclaw/workspace}:/home/node/.openclaw/workspace\",\n\t];\n\n\t// Collect env vars and volume mounts from companion services\n\tfor (const { definition: def } of resolved.services) {\n\t\tfor (const env of def.openclawEnvVars) {\n\t\t\tgatewayEnv[env.key] = env.secret ? `\\${${env.key}}` : env.defaultValue;\n\t\t}\n\t\tif (def.openclawVolumeMounts) {\n\t\t\tfor (const vol of def.openclawVolumeMounts) {\n\t\t\t\tallVolumes.add(vol.name);\n\t\t\t\tgatewayVolumes.push(`${vol.name}:${vol.containerPath}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Gateway service\n\tconst gateway: Record<string, unknown> = {\n\t\timage: `\\${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:${options.openclawVersion}}`,\n\t\tenvironment: gatewayEnv,\n\t\tvolumes: gatewayVolumes,\n\t\tports: [\"${OPENCLAW_GATEWAY_PORT:-18789}:18789\", \"${OPENCLAW_BRIDGE_PORT:-18790}:18790\"],\n\t\tnetworks: [\"openclaw-network\"],\n\t\tinit: true,\n\t\trestart: \"unless-stopped\",\n\t\tcommand: [\n\t\t\t\"node\",\n\t\t\t\"dist/index.js\",\n\t\t\t\"gateway\",\n\t\t\t\"--bind\",\n\t\t\t\"${OPENCLAW_GATEWAY_BIND:-lan}\",\n\t\t\t\"--port\",\n\t\t\t\"18789\",\n\t\t],\n\t};\n\n\tif (options.bareMetalNativeHost) {\n\t\tgateway.extra_hosts = [\"host.docker.internal:host-gateway\"];\n\t}\n\n\tif (dependsOn && Object.keys(dependsOn).length > 0) {\n\t\tgateway.depends_on = dependsOn;\n\t}\n\n\t// CLI companion service (matching real OpenClaw docker-compose.yml)\n\tconst cliService: Record<string, unknown> = {\n\t\timage: `\\${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:${options.openclawVersion}}`,\n\t\tenvironment: {\n\t\t\tHOME: \"/home/node\",\n\t\t\tTERM: \"xterm-256color\",\n\t\t\tOPENCLAW_GATEWAY_TOKEN: \"${OPENCLAW_GATEWAY_TOKEN}\",\n\t\t\tBROWSER: \"echo\",\n\t\t\tCLAUDE_AI_SESSION_KEY: \"${CLAUDE_AI_SESSION_KEY}\",\n\t\t\tCLAUDE_WEB_SESSION_KEY: \"${CLAUDE_WEB_SESSION_KEY}\",\n\t\t\tCLAUDE_WEB_COOKIE: \"${CLAUDE_WEB_COOKIE}\",\n\t\t},\n\t\tvolumes: [\n\t\t\t\"${OPENCLAW_CONFIG_DIR:-./openclaw/config}:/home/node/.openclaw\",\n\t\t\t\"${OPENCLAW_WORKSPACE_DIR:-./openclaw/workspace}:/home/node/.openclaw/workspace\",\n\t\t],\n\t\tstdin_open: true,\n\t\ttty: true,\n\t\tinit: true,\n\t\tnetworks: [\"openclaw-network\"],\n\t\tentrypoint: [\"node\", \"dist/index.js\"],\n\t};\n\n\treturn { gatewayService: gateway, cliService: cliService, allVolumes };\n}\n\n// ── Shared Companion Service Builder ────────────────────────────────────────\n\nfunction buildCompanionService(\n\tdef: ResolverOutput[\"services\"][number][\"definition\"],\n\tresolved: ResolverOutput,\n\toptions: ComposeOptions,\n\tallVolumes: Set<string>,\n): { entry: Record<string, unknown>; volumeNames: string[] } {\n\tconst svc: Record<string, unknown> = {};\n\tconst volumeNames: string[] = [];\n\n\tsvc.image = `${def.image}:${def.imageTag}`;\n\n\tif (def.environment.length > 0) {\n\t\tconst env: Record<string, string> = {};\n\t\tfor (const e of def.environment) {\n\t\t\tenv[e.key] = e.secret ? `\\${${e.key}}` : e.defaultValue;\n\t\t}\n\t\tsvc.environment = env;\n\t}\n\n\tconst exposedPorts = def.ports.filter((p) => p.exposed);\n\tif (exposedPorts.length > 0) {\n\t\tconst prefix = def.id.toUpperCase().replace(/-/g, \"_\");\n\t\tsvc.ports = exposedPorts.map((p, i) => {\n\t\t\tconst suffix = exposedPorts.length > 1 ? `_${i}` : \"\";\n\t\t\treturn `\\${${prefix}_PORT${suffix}:-${p.host}}:${p.container}`;\n\t\t});\n\t}\n\n\tif (def.volumes.length > 0) {\n\t\tsvc.volumes = def.volumes.map((v) => {\n\t\t\tallVolumes.add(v.name);\n\t\t\tvolumeNames.push(v.name);\n\t\t\treturn `${v.name}:${v.containerPath}`;\n\t\t});\n\t}\n\n\t// PostgreSQL: mount the init script for creating per-service databases\n\tif (def.id === \"postgresql\") {\n\t\tif (!svc.volumes) svc.volumes = [];\n\t\t(svc.volumes as string[]).push(\n\t\t\t\"./postgres/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro\",\n\t\t);\n\t}\n\n\tif (def.healthcheck) {\n\t\tconst hc: Record<string, unknown> = {\n\t\t\ttest: [\"CMD-SHELL\", def.healthcheck.test],\n\t\t\tinterval: def.healthcheck.interval,\n\t\t\ttimeout: def.healthcheck.timeout,\n\t\t\tretries: def.healthcheck.retries,\n\t\t};\n\t\tif (def.healthcheck.startPeriod) {\n\t\t\thc.start_period = def.healthcheck.startPeriod;\n\t\t}\n\t\tsvc.healthcheck = hc;\n\t}\n\n\tsvc.restart = def.restartPolicy;\n\tsvc.networks = def.networks;\n\n\tif (def.command) svc.command = def.command;\n\tif (def.entrypoint) svc.entrypoint = def.entrypoint;\n\tif (def.labels && Object.keys(def.labels).length > 0) svc.labels = def.labels;\n\n\tlet deploy: Record<string, unknown> | undefined;\n\tif (def.deploy) {\n\t\tdeploy = JSON.parse(JSON.stringify(def.deploy)) as Record<string, unknown>;\n\t}\n\tif (options.gpu && def.gpuRequired) {\n\t\tconst base = deploy ?? {};\n\t\tconst resources = (base.resources ?? {}) as Record<string, unknown>;\n\t\tdeploy = {\n\t\t\t...base,\n\t\t\tresources: {\n\t\t\t\t...resources,\n\t\t\t\treservations: {\n\t\t\t\t\t...((resources.reservations as Record<string, unknown>) ?? {}),\n\t\t\t\t\tdevices: [{ driver: \"nvidia\", count: \"all\", capabilities: [\"gpu\"] }],\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n\tif (deploy) svc.deploy = deploy;\n\n\t// Merge both dependsOn and requires to ensure proper Docker startup ordering\n\tconst depIds = [...new Set([...def.dependsOn, ...def.requires])].filter((id) =>\n\t\tresolved.services.some((s) => s.definition.id === id),\n\t);\n\tif (depIds.length > 0) {\n\t\tconst dependsOn: Record<string, { condition: string }> = {};\n\t\tfor (const depId of depIds) {\n\t\t\tconst dep = resolved.services.find((s) => s.definition.id === depId);\n\t\t\tdependsOn[depId] = {\n\t\t\t\tcondition: dep?.definition.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t\t};\n\t\t}\n\t\tsvc.depends_on = dependsOn;\n\t}\n\n\treturn { entry: svc, volumeNames };\n}\n\n// ── Single-File Compose ─────────────────────────────────────────────────────\n\n/**\n * Generates a single Docker Compose YAML string with ALL services.\n * Backward-compatible signature.\n */\nexport function compose(resolved: ResolverOutput, options: ComposeOptions): string {\n\t// Build depends_on for ALL companion services\n\tconst gatewayDependsOn: Record<string, { condition: string }> = {};\n\tfor (const { definition: def } of resolved.services) {\n\t\tgatewayDependsOn[def.id] = {\n\t\t\tcondition: def.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t};\n\t}\n\n\tconst { gatewayService, cliService, allVolumes } = buildGatewayServices(\n\t\tresolved,\n\t\toptions,\n\t\tgatewayDependsOn,\n\t);\n\n\tconst services: Record<string, Record<string, unknown>> = {\n\t\t\"openclaw-gateway\": gatewayService,\n\t};\n\n\tfor (const { definition: def } of resolved.services) {\n\t\tconst { entry } = buildCompanionService(def, resolved, options, allVolumes);\n\t\tservices[def.id] = entry;\n\t}\n\n\t// Add CLI service\n\tservices[\"openclaw-cli\"] = cliService;\n\n\tconst volumes: Record<string, null> = {};\n\tfor (const v of [...allVolumes].sort()) {\n\t\tvolumes[v] = null;\n\t}\n\n\tconst networks = { \"openclaw-network\": { driver: \"bridge\" } };\n\n\treturn stringify({ services, volumes, networks }, YAML_OPTIONS);\n}\n\n// ── Multi-File Compose ──────────────────────────────────────────────────────\n\ninterface ServiceInfo {\n\tid: string;\n\tcategory: ServiceCategory;\n\tentry: Record<string, unknown>;\n\tvolumeNames: string[];\n}\n\n/**\n * Generates multiple Docker Compose files, splitting services into profile-based\n * override files by category.\n */\nexport function composeMultiFile(resolved: ResolverOutput, options: ComposeOptions): ComposeResult {\n\tconst allVolumes = new Set<string>();\n\n\t// Build all companion service entries & classify by category\n\tconst serviceInfos: ServiceInfo[] = [];\n\n\tfor (const { definition: def } of resolved.services) {\n\t\tconst { entry, volumeNames } = buildCompanionService(def, resolved, options, allVolumes);\n\t\tserviceInfos.push({ id: def.id, category: def.category, entry, volumeNames });\n\t}\n\n\t// Partition services into base vs. profile files\n\tconst baseServiceIds = new Set<string>();\n\tconst profileFileMap: Record<string, { profile: string; services: ServiceInfo[] }> = {};\n\n\tfor (const info of serviceInfos) {\n\t\tconst mapping = CATEGORY_PROFILE_MAP[info.category];\n\t\tif (mapping) {\n\t\t\tif (!profileFileMap[mapping.file]) {\n\t\t\t\tprofileFileMap[mapping.file] = { profile: mapping.profile, services: [] };\n\t\t\t}\n\t\t\tprofileFileMap[mapping.file]!.services.push(info);\n\t\t} else {\n\t\t\tbaseServiceIds.add(info.id);\n\t\t}\n\t}\n\n\t// Gateway depends_on (only base services)\n\tconst gatewayDependsOn: Record<string, { condition: string }> = {};\n\tfor (const { definition: def } of resolved.services) {\n\t\tif (baseServiceIds.has(def.id)) {\n\t\t\tgatewayDependsOn[def.id] = {\n\t\t\t\tcondition: def.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t\t};\n\t\t}\n\t}\n\n\tconst {\n\t\tgatewayService,\n\t\tcliService,\n\t\tallVolumes: gwVolumes,\n\t} = buildGatewayServices(resolved, options, gatewayDependsOn);\n\n\t// Merge gateway volumes into allVolumes\n\tfor (const v of gwVolumes) allVolumes.add(v);\n\n\t// Base file: gateway + CLI + core services + ALL volumes + networks\n\tconst baseServices: Record<string, Record<string, unknown>> = {\n\t\t\"openclaw-gateway\": gatewayService,\n\t};\n\n\tfor (const info of serviceInfos) {\n\t\tif (baseServiceIds.has(info.id)) {\n\t\t\tbaseServices[info.id] = info.entry;\n\t\t}\n\t}\n\n\tbaseServices[\"openclaw-cli\"] = cliService;\n\n\tconst sortedAllVolumes: Record<string, null> = {};\n\tfor (const v of [...allVolumes].sort()) {\n\t\tsortedAllVolumes[v] = null;\n\t}\n\n\tconst networks = { \"openclaw-network\": { driver: \"bridge\" } };\n\n\tconst files: Record<string, string> = {};\n\tfiles[\"docker-compose.yml\"] = stringify(\n\t\t{ services: baseServices, volumes: sortedAllVolumes, networks },\n\t\tYAML_OPTIONS,\n\t);\n\n\t// Profile override files\n\tconst usedProfiles = new Set<string>();\n\n\tfor (const [fileName, { profile, services }] of Object.entries(profileFileMap)) {\n\t\tusedProfiles.add(profile);\n\n\t\tconst profileServices: Record<string, Record<string, unknown>> = {};\n\t\tconst profileVolumes = new Set<string>();\n\n\t\tfor (const info of services) {\n\t\t\tprofileServices[info.id] = { ...info.entry, profiles: [profile] };\n\t\t\tfor (const vName of info.volumeNames) {\n\t\t\t\tprofileVolumes.add(vName);\n\t\t\t}\n\t\t}\n\n\t\tconst fileContent: Record<string, unknown> = { services: profileServices };\n\n\t\tif (profileVolumes.size > 0) {\n\t\t\tconst sortedProfileVolumes: Record<string, null> = {};\n\t\t\tfor (const v of [...profileVolumes].sort()) {\n\t\t\t\tsortedProfileVolumes[v] = null;\n\t\t\t}\n\t\t\tfileContent.volumes = sortedProfileVolumes;\n\t\t}\n\n\t\tfiles[fileName] = stringify(fileContent, YAML_OPTIONS);\n\t}\n\n\treturn {\n\t\tfiles,\n\t\tmainFile: \"docker-compose.yml\",\n\t\tprofiles: [...usedProfiles].sort(),\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"composer.mjs","names":[],"sources":["../src/composer.ts"],"sourcesContent":["import { stringify } from \"yaml\";\nimport type { ComposeOptions, ResolverOutput, ServiceCategory } from \"./types.js\";\n\n// ── Public Types ────────────────────────────────────────────────────────────\n\nexport interface ComposeResult {\n\tfiles: Record<string, string>; // filename -> YAML content\n\tmainFile: string; // \"docker-compose.yml\"\n\tprofiles: string[]; // list of profiles used\n}\n\n// ── Category → Profile Mapping ──────────────────────────────────────────────\n\nconst CATEGORY_PROFILE_MAP: Partial<Record<ServiceCategory, { file: string; profile: string }>> = {\n\tai: { file: \"docker-compose.ai.yml\", profile: \"ai\" },\n\t\"ai-platform\": { file: \"docker-compose.ai.yml\", profile: \"ai\" },\n\tmedia: { file: \"docker-compose.media.yml\", profile: \"media\" },\n\tmonitoring: { file: \"docker-compose.monitoring.yml\", profile: \"monitoring\" },\n\tanalytics: { file: \"docker-compose.monitoring.yml\", profile: \"monitoring\" },\n\t\"dev-tools\": { file: \"docker-compose.tools.yml\", profile: \"tools\" },\n\t\"coding-agent\": { file: \"docker-compose.tools.yml\", profile: \"tools\" },\n\t\"social-media\": { file: \"docker-compose.social.yml\", profile: \"social\" },\n\tknowledge: { file: \"docker-compose.knowledge.yml\", profile: \"knowledge\" },\n\tcommunication: { file: \"docker-compose.communication.yml\", profile: \"communication\" },\n};\n\nconst YAML_OPTIONS = { lineWidth: 120, nullStr: \"\" };\n\n// ── Shared Gateway Builder ──────────────────────────────────────────────────\n\ninterface GatewayBuildResult {\n\tgatewayService: Record<string, unknown>;\n\tcliService: Record<string, unknown>;\n\tallVolumes: Set<string>;\n}\n\n/**\n * Builds the OpenClaw gateway and CLI service entries.\n * Matches the real OpenClaw docker-compose.yml structure:\n * - Bridge port 18790 for ACP/WebSocket\n * - Bind-mount volumes (not named volumes)\n * - Claude web-provider session env vars\n * - Gateway bind mode (--bind lan)\n * - CLI companion service with stdin/tty\n */\nfunction buildGatewayServices(\n\tresolved: ResolverOutput,\n\toptions: ComposeOptions,\n\tdependsOn?: Record<string, { condition: string }>,\n): GatewayBuildResult {\n\tconst allVolumes = new Set<string>();\n\n\t// Gateway environment\n\tconst gatewayEnv: Record<string, string> = {\n\t\tHOME: \"/home/node\",\n\t\tTERM: \"xterm-256color\",\n\t\tOPENCLAW_GATEWAY_TOKEN: \"${OPENCLAW_GATEWAY_TOKEN}\",\n\t\t// Claude web-provider session vars (optional, user fills in .env)\n\t\tCLAUDE_AI_SESSION_KEY: \"${CLAUDE_AI_SESSION_KEY}\",\n\t\tCLAUDE_WEB_SESSION_KEY: \"${CLAUDE_WEB_SESSION_KEY}\",\n\t\tCLAUDE_WEB_COOKIE: \"${CLAUDE_WEB_COOKIE}\",\n\t};\n\n\t// Gateway volumes (bind-mount style matching real docker-setup.sh)\n\tconst gatewayVolumes: string[] = [\n\t\t\"${OPENCLAW_CONFIG_DIR:-./openclaw/config}:/home/node/.openclaw\",\n\t\t\"${OPENCLAW_WORKSPACE_DIR:-./openclaw/workspace}:/home/node/.openclaw/workspace\",\n\t];\n\n\t// Collect env vars and volume mounts from companion services\n\tfor (const { definition: def } of resolved.services) {\n\t\tfor (const env of def.openclawEnvVars) {\n\t\t\tgatewayEnv[env.key] = env.secret ? `\\${${env.key}}` : env.defaultValue;\n\t\t}\n\t\tif (def.openclawVolumeMounts) {\n\t\t\tfor (const vol of def.openclawVolumeMounts) {\n\t\t\t\tallVolumes.add(vol.name);\n\t\t\t\tgatewayVolumes.push(`${vol.name}:${vol.containerPath}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Gateway service\n\tconst gateway: Record<string, unknown> = {\n\t\timage: `\\${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:${options.openclawVersion}}`,\n\t\tenvironment: gatewayEnv,\n\t\tvolumes: gatewayVolumes,\n\t\tports: [\"${OPENCLAW_GATEWAY_PORT:-18789}:18789\", \"${OPENCLAW_BRIDGE_PORT:-18790}:18790\"],\n\t\tnetworks: [\"openclaw-network\"],\n\t\tinit: true,\n\t\trestart: \"unless-stopped\",\n\t\tcommand: [\n\t\t\t\"node\",\n\t\t\t\"dist/index.js\",\n\t\t\t\"gateway\",\n\t\t\t\"--bind\",\n\t\t\t\"${OPENCLAW_GATEWAY_BIND:-lan}\",\n\t\t\t\"--port\",\n\t\t\t\"18789\",\n\t\t],\n\t};\n\n\t// Traefik labels for the gateway\n\tconst gwTraefikLabels = options.traefikLabels?.get(\"openclaw-gateway\");\n\tif (gwTraefikLabels) {\n\t\tgateway.labels = gwTraefikLabels;\n\t}\n\n\tif (options.bareMetalNativeHost) {\n\t\tgateway.extra_hosts = [\"host.docker.internal:host-gateway\"];\n\t}\n\n\tif (dependsOn && Object.keys(dependsOn).length > 0) {\n\t\tgateway.depends_on = dependsOn;\n\t}\n\n\t// CLI companion service (matching real OpenClaw docker-compose.yml)\n\tconst cliService: Record<string, unknown> = {\n\t\timage: `\\${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:${options.openclawVersion}}`,\n\t\tenvironment: {\n\t\t\tHOME: \"/home/node\",\n\t\t\tTERM: \"xterm-256color\",\n\t\t\tOPENCLAW_GATEWAY_TOKEN: \"${OPENCLAW_GATEWAY_TOKEN}\",\n\t\t\tBROWSER: \"echo\",\n\t\t\tCLAUDE_AI_SESSION_KEY: \"${CLAUDE_AI_SESSION_KEY}\",\n\t\t\tCLAUDE_WEB_SESSION_KEY: \"${CLAUDE_WEB_SESSION_KEY}\",\n\t\t\tCLAUDE_WEB_COOKIE: \"${CLAUDE_WEB_COOKIE}\",\n\t\t},\n\t\tvolumes: [\n\t\t\t\"${OPENCLAW_CONFIG_DIR:-./openclaw/config}:/home/node/.openclaw\",\n\t\t\t\"${OPENCLAW_WORKSPACE_DIR:-./openclaw/workspace}:/home/node/.openclaw/workspace\",\n\t\t],\n\t\tstdin_open: true,\n\t\ttty: true,\n\t\tinit: true,\n\t\tnetworks: [\"openclaw-network\"],\n\t\tentrypoint: [\"node\", \"dist/index.js\"],\n\t};\n\n\treturn { gatewayService: gateway, cliService: cliService, allVolumes };\n}\n\n// ── Shared Companion Service Builder ────────────────────────────────────────\n\nfunction buildCompanionService(\n\tdef: ResolverOutput[\"services\"][number][\"definition\"],\n\tresolved: ResolverOutput,\n\toptions: ComposeOptions,\n\tallVolumes: Set<string>,\n): { entry: Record<string, unknown>; volumeNames: string[] } {\n\tconst svc: Record<string, unknown> = {};\n\tconst volumeNames: string[] = [];\n\n\tsvc.image = `${def.image}:${def.imageTag}`;\n\n\tif (def.environment.length > 0) {\n\t\tconst env: Record<string, string> = {};\n\t\tfor (const e of def.environment) {\n\t\t\tenv[e.key] = e.secret ? `\\${${e.key}}` : e.defaultValue;\n\t\t}\n\t\tsvc.environment = env;\n\t}\n\n\tconst exposedPorts = def.ports.filter((p) => p.exposed);\n\tif (exposedPorts.length > 0) {\n\t\tconst prefix = def.id.toUpperCase().replace(/-/g, \"_\");\n\t\tsvc.ports = exposedPorts.map((p, i) => {\n\t\t\tconst suffix = exposedPorts.length > 1 ? `_${i}` : \"\";\n\t\t\treturn `\\${${prefix}_PORT${suffix}:-${p.host}}:${p.container}`;\n\t\t});\n\t}\n\n\tif (def.volumes.length > 0) {\n\t\tsvc.volumes = def.volumes.map((v) => {\n\t\t\tallVolumes.add(v.name);\n\t\t\tvolumeNames.push(v.name);\n\t\t\treturn `${v.name}:${v.containerPath}`;\n\t\t});\n\t}\n\n\t// PostgreSQL: mount the init script for creating per-service databases\n\tif (def.id === \"postgresql\") {\n\t\tif (!svc.volumes) svc.volumes = [];\n\t\t(svc.volumes as string[]).push(\n\t\t\t\"./postgres/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro\",\n\t\t);\n\t}\n\n\tif (def.healthcheck) {\n\t\tconst hc: Record<string, unknown> = {\n\t\t\ttest: [\"CMD-SHELL\", def.healthcheck.test],\n\t\t\tinterval: def.healthcheck.interval,\n\t\t\ttimeout: def.healthcheck.timeout,\n\t\t\tretries: def.healthcheck.retries,\n\t\t};\n\t\tif (def.healthcheck.startPeriod) {\n\t\t\thc.start_period = def.healthcheck.startPeriod;\n\t\t}\n\t\tsvc.healthcheck = hc;\n\t}\n\n\tsvc.restart = def.restartPolicy;\n\tsvc.networks = def.networks;\n\n\tif (def.command) svc.command = def.command;\n\tif (def.entrypoint) svc.entrypoint = def.entrypoint;\n\n\t// Labels: merge static definition labels with dynamic Traefik labels\n\tconst mergedLabels: Record<string, string> = {};\n\tif (def.labels) Object.assign(mergedLabels, def.labels);\n\tconst traefikLabels = options.traefikLabels?.get(def.id);\n\tif (traefikLabels) Object.assign(mergedLabels, traefikLabels);\n\tif (Object.keys(mergedLabels).length > 0) svc.labels = mergedLabels;\n\n\t// Traefik: bind-mount static config and Docker socket\n\tif (def.id === \"traefik\" && options.traefikLabels) {\n\t\tif (!svc.volumes) svc.volumes = [];\n\t\t(svc.volumes as string[]).push(\n\t\t\t\"./traefik/traefik.yml:/etc/traefik/traefik.yml:ro\",\n\t\t\t\"/var/run/docker.sock:/var/run/docker.sock:ro\",\n\t\t);\n\t}\n\n\tlet deploy: Record<string, unknown> | undefined;\n\tif (def.deploy) {\n\t\tdeploy = JSON.parse(JSON.stringify(def.deploy)) as Record<string, unknown>;\n\t}\n\tif (options.gpu && def.gpuRequired) {\n\t\tconst base = deploy ?? {};\n\t\tconst resources = (base.resources ?? {}) as Record<string, unknown>;\n\t\tdeploy = {\n\t\t\t...base,\n\t\t\tresources: {\n\t\t\t\t...resources,\n\t\t\t\treservations: {\n\t\t\t\t\t...((resources.reservations as Record<string, unknown>) ?? {}),\n\t\t\t\t\tdevices: [{ driver: \"nvidia\", count: \"all\", capabilities: [\"gpu\"] }],\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n\tif (deploy) svc.deploy = deploy;\n\n\t// Merge both dependsOn and requires to ensure proper Docker startup ordering\n\tconst depIds = [...new Set([...def.dependsOn, ...def.requires])].filter((id) =>\n\t\tresolved.services.some((s) => s.definition.id === id),\n\t);\n\tif (depIds.length > 0) {\n\t\tconst dependsOn: Record<string, { condition: string }> = {};\n\t\tfor (const depId of depIds) {\n\t\t\tconst dep = resolved.services.find((s) => s.definition.id === depId);\n\t\t\tdependsOn[depId] = {\n\t\t\t\tcondition: dep?.definition.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t\t};\n\t\t}\n\t\tsvc.depends_on = dependsOn;\n\t}\n\n\treturn { entry: svc, volumeNames };\n}\n\n// ── Single-File Compose ─────────────────────────────────────────────────────\n\n/**\n * Generates a single Docker Compose YAML string with ALL services.\n * Backward-compatible signature.\n */\nexport function compose(resolved: ResolverOutput, options: ComposeOptions): string {\n\t// Build depends_on for ALL companion services\n\tconst gatewayDependsOn: Record<string, { condition: string }> = {};\n\tfor (const { definition: def } of resolved.services) {\n\t\tgatewayDependsOn[def.id] = {\n\t\t\tcondition: def.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t};\n\t}\n\n\tconst { gatewayService, cliService, allVolumes } = buildGatewayServices(\n\t\tresolved,\n\t\toptions,\n\t\tgatewayDependsOn,\n\t);\n\n\tconst services: Record<string, Record<string, unknown>> = {\n\t\t\"openclaw-gateway\": gatewayService,\n\t};\n\n\tfor (const { definition: def } of resolved.services) {\n\t\tconst { entry } = buildCompanionService(def, resolved, options, allVolumes);\n\t\tservices[def.id] = entry;\n\t}\n\n\t// Add CLI service\n\tservices[\"openclaw-cli\"] = cliService;\n\n\tconst volumes: Record<string, null> = {};\n\tfor (const v of [...allVolumes].sort()) {\n\t\tvolumes[v] = null;\n\t}\n\n\tconst networks = { \"openclaw-network\": { driver: \"bridge\" } };\n\n\treturn stringify({ services, volumes, networks }, YAML_OPTIONS);\n}\n\n// ── Multi-File Compose ──────────────────────────────────────────────────────\n\ninterface ServiceInfo {\n\tid: string;\n\tcategory: ServiceCategory;\n\tentry: Record<string, unknown>;\n\tvolumeNames: string[];\n}\n\n/**\n * Generates multiple Docker Compose files, splitting services into profile-based\n * override files by category.\n */\nexport function composeMultiFile(resolved: ResolverOutput, options: ComposeOptions): ComposeResult {\n\tconst allVolumes = new Set<string>();\n\n\t// Build all companion service entries & classify by category\n\tconst serviceInfos: ServiceInfo[] = [];\n\n\tfor (const { definition: def } of resolved.services) {\n\t\tconst { entry, volumeNames } = buildCompanionService(def, resolved, options, allVolumes);\n\t\tserviceInfos.push({ id: def.id, category: def.category, entry, volumeNames });\n\t}\n\n\t// Partition services into base vs. profile files\n\tconst baseServiceIds = new Set<string>();\n\tconst profileFileMap: Record<string, { profile: string; services: ServiceInfo[] }> = {};\n\n\tfor (const info of serviceInfos) {\n\t\tconst mapping = CATEGORY_PROFILE_MAP[info.category];\n\t\tif (mapping) {\n\t\t\tif (!profileFileMap[mapping.file]) {\n\t\t\t\tprofileFileMap[mapping.file] = { profile: mapping.profile, services: [] };\n\t\t\t}\n\t\t\tprofileFileMap[mapping.file]!.services.push(info);\n\t\t} else {\n\t\t\tbaseServiceIds.add(info.id);\n\t\t}\n\t}\n\n\t// Gateway depends_on (only base services)\n\tconst gatewayDependsOn: Record<string, { condition: string }> = {};\n\tfor (const { definition: def } of resolved.services) {\n\t\tif (baseServiceIds.has(def.id)) {\n\t\t\tgatewayDependsOn[def.id] = {\n\t\t\t\tcondition: def.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t\t};\n\t\t}\n\t}\n\n\tconst {\n\t\tgatewayService,\n\t\tcliService,\n\t\tallVolumes: gwVolumes,\n\t} = buildGatewayServices(resolved, options, gatewayDependsOn);\n\n\t// Merge gateway volumes into allVolumes\n\tfor (const v of gwVolumes) allVolumes.add(v);\n\n\t// Base file: gateway + CLI + core services + ALL volumes + networks\n\tconst baseServices: Record<string, Record<string, unknown>> = {\n\t\t\"openclaw-gateway\": gatewayService,\n\t};\n\n\tfor (const info of serviceInfos) {\n\t\tif (baseServiceIds.has(info.id)) {\n\t\t\tbaseServices[info.id] = info.entry;\n\t\t}\n\t}\n\n\tbaseServices[\"openclaw-cli\"] = cliService;\n\n\tconst sortedAllVolumes: Record<string, null> = {};\n\tfor (const v of [...allVolumes].sort()) {\n\t\tsortedAllVolumes[v] = null;\n\t}\n\n\tconst networks = { \"openclaw-network\": { driver: \"bridge\" } };\n\n\tconst files: Record<string, string> = {};\n\tfiles[\"docker-compose.yml\"] = stringify(\n\t\t{ services: baseServices, volumes: sortedAllVolumes, networks },\n\t\tYAML_OPTIONS,\n\t);\n\n\t// Profile override files\n\tconst usedProfiles = new Set<string>();\n\n\tfor (const [fileName, { profile, services }] of Object.entries(profileFileMap)) {\n\t\tusedProfiles.add(profile);\n\n\t\tconst profileServices: Record<string, Record<string, unknown>> = {};\n\t\tconst profileVolumes = new Set<string>();\n\n\t\tfor (const info of services) {\n\t\t\tprofileServices[info.id] = { ...info.entry, profiles: [profile] };\n\t\t\tfor (const vName of info.volumeNames) {\n\t\t\t\tprofileVolumes.add(vName);\n\t\t\t}\n\t\t}\n\n\t\tconst fileContent: Record<string, unknown> = { services: profileServices };\n\n\t\tif (profileVolumes.size > 0) {\n\t\t\tconst sortedProfileVolumes: Record<string, null> = {};\n\t\t\tfor (const v of [...profileVolumes].sort()) {\n\t\t\t\tsortedProfileVolumes[v] = null;\n\t\t\t}\n\t\t\tfileContent.volumes = sortedProfileVolumes;\n\t\t}\n\n\t\tfiles[fileName] = stringify(fileContent, YAML_OPTIONS);\n\t}\n\n\treturn {\n\t\tfiles,\n\t\tmainFile: \"docker-compose.yml\",\n\t\tprofiles: [...usedProfiles].sort(),\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Typed error classes for stack generation.
|
|
4
|
+
*/
|
|
5
|
+
/** Thrown when the resolved stack configuration is invalid (e.g. conflicts). */
|
|
6
|
+
declare class StackConfigError extends Error {
|
|
7
|
+
readonly code: "STACK_CONFIG_ERROR";
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
/** Thrown when post-generation validation fails. */
|
|
11
|
+
declare class ValidationError extends Error {
|
|
12
|
+
readonly code: "VALIDATION_ERROR";
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { StackConfigError, ValidationError };
|
|
17
|
+
//# sourceMappingURL=errors.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";;AAKA;;;cAAa,gBAAA,SAAyB,KAAA;EAAA,SAC5B,IAAA;cAEG,OAAA;AAAA;;cAOA,eAAA,SAAwB,KAAA;EAAA,SAC3B,IAAA;cAEG,OAAA;AAAA"}
|
package/dist/errors.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
/**
|
|
3
|
+
* Typed error classes for stack generation.
|
|
4
|
+
*/
|
|
5
|
+
/** Thrown when the resolved stack configuration is invalid (e.g. conflicts). */
|
|
6
|
+
var StackConfigError = class extends Error {
|
|
7
|
+
code = "STACK_CONFIG_ERROR";
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "StackConfigError";
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/** Thrown when post-generation validation fails. */
|
|
14
|
+
var ValidationError = class extends Error {
|
|
15
|
+
code = "VALIDATION_ERROR";
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "ValidationError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
//#endregion
|
|
23
|
+
export { StackConfigError, ValidationError };
|
|
24
|
+
//# sourceMappingURL=errors.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Typed error classes for stack generation.\n */\n\n/** Thrown when the resolved stack configuration is invalid (e.g. conflicts). */\nexport class StackConfigError extends Error {\n\treadonly code = \"STACK_CONFIG_ERROR\" as const;\n\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"StackConfigError\";\n\t}\n}\n\n/** Thrown when post-generation validation fails. */\nexport class ValidationError extends Error {\n\treadonly code = \"VALIDATION_ERROR\" as const;\n\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"ValidationError\";\n\t}\n}\n"],"mappings":";;;;;AAKA,IAAa,mBAAb,cAAsC,MAAM;CAC3C,AAAS,OAAO;CAEhB,YAAY,SAAiB;AAC5B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;AAKd,IAAa,kBAAb,cAAqC,MAAM;CAC1C,AAAS,OAAO;CAEhB,YAAY,SAAiB;AAC5B,QAAM,QAAQ;AACd,OAAK,OAAO"}
|
package/dist/generate.d.mts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { GenerationInput, GenerationResult } from "./types.mjs";
|
|
1
|
+
import { GenerationInput, GenerationResult, ResolverOutput } from "./types.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/generate.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Main orchestration function: takes generation input, resolves dependencies,
|
|
6
6
|
* generates all files, validates, and returns the complete file tree.
|
|
7
7
|
*/
|
|
8
|
-
declare function generate(
|
|
8
|
+
declare function generate(rawInput: GenerationInput): GenerationResult;
|
|
9
|
+
declare function generateServicesDoc(resolved: ResolverOutput): string;
|
|
9
10
|
//#endregion
|
|
10
|
-
export { generate };
|
|
11
|
+
export { generate, generateServicesDoc };
|
|
11
12
|
//# sourceMappingURL=generate.d.mts.map
|
package/dist/generate.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.d.mts","names":[],"sources":["../src/generate.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"generate.d.mts","names":[],"sources":["../src/generate.ts"],"mappings":";;;;;AAwCA;;iBAAgB,QAAA,CAAS,QAAA,EAAU,eAAA,GAAkB,gBAAA;AAAA,iBA2MrC,mBAAA,CAAoB,QAAA,EAAD,cAAA"}
|
package/dist/generate.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { partitionBareMetal, platformToNativePlatform, resolvedWithOnlyServices } from "./bare-metal-partition.mjs";
|
|
2
2
|
import { resolve } from "./resolver.mjs";
|
|
3
3
|
import { composeMultiFile } from "./composer.mjs";
|
|
4
|
+
import { StackConfigError, ValidationError } from "./errors.mjs";
|
|
4
5
|
import { generateBareMetalInstall } from "./generators/bare-metal-install.mjs";
|
|
5
6
|
import { generateCaddyfile } from "./generators/caddy.mjs";
|
|
6
7
|
import { generatePostgresInit } from "./generators/postgres-init.mjs";
|
|
7
8
|
import { generateEnvFiles } from "./generators/env.mjs";
|
|
9
|
+
import { generateTraefikConfig } from "./generators/traefik.mjs";
|
|
8
10
|
import { generateGrafanaConfig, generateGrafanaDashboard } from "./generators/grafana.mjs";
|
|
9
11
|
import { generateN8nWorkflows } from "./generators/n8n-workflows.mjs";
|
|
10
12
|
import { generateNativeInstallScripts } from "./generators/native-services.mjs";
|
|
@@ -12,6 +14,7 @@ import { generatePrometheusConfig } from "./generators/prometheus.mjs";
|
|
|
12
14
|
import { generateReadme } from "./generators/readme.mjs";
|
|
13
15
|
import { generateScripts } from "./generators/scripts.mjs";
|
|
14
16
|
import { generateSkillFiles } from "./generators/skills.mjs";
|
|
17
|
+
import { migrateConfig } from "./migrations.mjs";
|
|
15
18
|
import { validate } from "./validator.mjs";
|
|
16
19
|
|
|
17
20
|
//#region src/generate.ts
|
|
@@ -24,7 +27,8 @@ function getComposePlatform(platform) {
|
|
|
24
27
|
* Main orchestration function: takes generation input, resolves dependencies,
|
|
25
28
|
* generates all files, validates, and returns the complete file tree.
|
|
26
29
|
*/
|
|
27
|
-
function generate(
|
|
30
|
+
function generate(rawInput) {
|
|
31
|
+
const input = migrateConfig(rawInput);
|
|
28
32
|
const composePlatform = getComposePlatform(input.platform);
|
|
29
33
|
const resolved = resolve({
|
|
30
34
|
services: input.services,
|
|
@@ -34,7 +38,7 @@ function generate(input) {
|
|
|
34
38
|
platform: composePlatform,
|
|
35
39
|
monitoring: input.monitoring
|
|
36
40
|
});
|
|
37
|
-
if (!resolved.isValid) throw new
|
|
41
|
+
if (!resolved.isValid) throw new StackConfigError(`Invalid stack configuration: ${resolved.errors.map((e) => e.message).join("; ")}`);
|
|
38
42
|
const isBareMetal = input.deploymentType === "bare-metal";
|
|
39
43
|
let resolvedForCompose = resolved;
|
|
40
44
|
let nativeIds = /* @__PURE__ */ new Set();
|
|
@@ -47,6 +51,8 @@ function generate(input) {
|
|
|
47
51
|
nativeIds = partition.nativeIds;
|
|
48
52
|
if (nativeServices.length > 0) resolvedForCompose = resolvedWithOnlyServices(resolved, dockerOnlyServices);
|
|
49
53
|
}
|
|
54
|
+
let traefikOutput;
|
|
55
|
+
if (input.proxy === "traefik" && input.domain) traefikOutput = generateTraefikConfig(resolvedForCompose, input.domain);
|
|
50
56
|
const composeOptions = {
|
|
51
57
|
projectName: input.projectName,
|
|
52
58
|
proxy: input.proxy,
|
|
@@ -55,14 +61,15 @@ function generate(input) {
|
|
|
55
61
|
platform: composePlatform,
|
|
56
62
|
deployment: input.deployment,
|
|
57
63
|
openclawVersion: input.openclawVersion,
|
|
58
|
-
bareMetalNativeHost: isBareMetal && nativeIds.size > 0
|
|
64
|
+
bareMetalNativeHost: isBareMetal && nativeIds.size > 0,
|
|
65
|
+
traefikLabels: traefikOutput?.serviceLabels
|
|
59
66
|
};
|
|
60
67
|
const composeResult = composeMultiFile(resolvedForCompose, composeOptions);
|
|
61
68
|
const validation = validate(resolvedForCompose, composeResult.files["docker-compose.yml"] ?? "", {
|
|
62
69
|
domain: input.domain,
|
|
63
70
|
generateSecrets: input.generateSecrets
|
|
64
71
|
});
|
|
65
|
-
if (!validation.valid) throw new
|
|
72
|
+
if (!validation.valid) throw new ValidationError(`Validation failed: ${validation.errors.map((e) => e.message).join("; ")}`);
|
|
66
73
|
const files = {};
|
|
67
74
|
for (const [filename, content] of Object.entries(composeResult.files)) files[filename] = content;
|
|
68
75
|
const envFiles = generateEnvFiles(resolved, {
|
|
@@ -96,6 +103,7 @@ function generate(input) {
|
|
|
96
103
|
const postgresInit = generatePostgresInit(resolved);
|
|
97
104
|
if (postgresInit) files["postgres/init-databases.sh"] = postgresInit;
|
|
98
105
|
if (input.proxy === "caddy" && input.domain) files["caddy/Caddyfile"] = generateCaddyfile(resolved, input.domain);
|
|
106
|
+
if (traefikOutput) files["traefik/traefik.yml"] = traefikOutput.staticConfig;
|
|
99
107
|
if (resolved.services.some((s) => s.definition.id === "prometheus")) files["prometheus/prometheus.yml"] = generatePrometheusConfig(resolved);
|
|
100
108
|
if (resolved.services.some((s) => s.definition.id === "grafana")) {
|
|
101
109
|
const grafanaFiles = generateGrafanaConfig();
|
|
@@ -165,5 +173,5 @@ function generateServicesDoc(resolved) {
|
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
//#endregion
|
|
168
|
-
export { generate };
|
|
176
|
+
export { generate, generateServicesDoc };
|
|
169
177
|
//# sourceMappingURL=generate.mjs.map
|
package/dist/generate.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.mjs","names":[],"sources":["../src/generate.ts"],"sourcesContent":["import {\n\tpartitionBareMetal,\n\tplatformToNativePlatform,\n\tresolvedWithOnlyServices,\n} from \"./bare-metal-partition.js\";\nimport { composeMultiFile } from \"./composer.js\";\nimport { generateBareMetalInstall } from \"./generators/bare-metal-install.js\";\nimport { generateCaddyfile } from \"./generators/caddy.js\";\nimport { generateEnvFiles } from \"./generators/env.js\";\nimport { generateGrafanaConfig, generateGrafanaDashboard } from \"./generators/grafana.js\";\nimport { generateN8nWorkflows } from \"./generators/n8n-workflows.js\";\nimport { generateNativeInstallScripts } from \"./generators/native-services.js\";\nimport { generatePostgresInit } from \"./generators/postgres-init.js\";\nimport { generatePrometheusConfig } from \"./generators/prometheus.js\";\nimport { generateReadme } from \"./generators/readme.js\";\nimport { generateScripts } from \"./generators/scripts.js\";\nimport { generateSkillFiles } from \"./generators/skills.js\";\nimport { resolve } from \"./resolver.js\";\nimport type {\n\tGeneratedFiles,\n\tGenerationInput,\n\tGenerationResult,\n\tPlatform,\n\tResolverInput,\n} from \"./types.js\";\nimport { validate } from \"./validator.js\";\n\n/** Resolver/compose only support linux image platforms; normalize for bare-metal (windows/macos). */\nfunction getComposePlatform(platform: Platform): \"linux/amd64\" | \"linux/arm64\" {\n\tif (platform === \"linux/amd64\" || platform === \"linux/arm64\") return platform;\n\treturn \"linux/amd64\";\n}\n\n/**\n * Main orchestration function: takes generation input, resolves dependencies,\n * generates all files, validates, and returns the complete file tree.\n */\nexport function generate(input: GenerationInput): GenerationResult {\n\tconst composePlatform = getComposePlatform(input.platform);\n\n\t// 1. Resolve dependencies\n\tconst resolverInput: ResolverInput = {\n\t\tservices: input.services,\n\t\tskillPacks: input.skillPacks,\n\t\tproxy: input.proxy,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tmonitoring: input.monitoring,\n\t};\n\tconst resolved = resolve(resolverInput);\n\n\tif (!resolved.isValid) {\n\t\tthrow new Error(\n\t\t\t`Invalid stack configuration: ${resolved.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\tconst isBareMetal = input.deploymentType === \"bare-metal\";\n\tlet resolvedForCompose = resolved;\n\tlet nativeIds = new Set<string>();\n\tlet nativeServices: typeof resolved.services = [];\n\tlet dockerOnlyServices: typeof resolved.services = [];\n\n\tif (isBareMetal) {\n\t\tconst partition = partitionBareMetal(resolved, input.platform);\n\t\tnativeServices = partition.nativeServices;\n\t\tdockerOnlyServices = partition.dockerOnlyServices;\n\t\tnativeIds = partition.nativeIds;\n\t\tif (nativeServices.length > 0) {\n\t\t\tresolvedForCompose = resolvedWithOnlyServices(resolved, dockerOnlyServices);\n\t\t}\n\t}\n\n\t// 2. Generate Docker Compose YAML (multi-file)\n\tconst composeOptions = {\n\t\tprojectName: input.projectName,\n\t\tproxy: input.proxy,\n\t\tdomain: input.domain,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tdeployment: input.deployment,\n\t\topenclawVersion: input.openclawVersion,\n\t\tbareMetalNativeHost: isBareMetal && nativeIds.size > 0,\n\t};\n\tconst composeResult = composeMultiFile(resolvedForCompose, composeOptions);\n\n\t// 3. Validate (using the base docker-compose.yml)\n\tconst validation = validate(resolvedForCompose, composeResult.files[\"docker-compose.yml\"] ?? \"\", {\n\t\tdomain: input.domain,\n\t\tgenerateSecrets: input.generateSecrets,\n\t});\n\tif (!validation.valid) {\n\t\tthrow new Error(`Validation failed: ${validation.errors.map((e) => e.message).join(\"; \")}`);\n\t}\n\n\t// 4. Generate all files\n\tconst files: GeneratedFiles = {};\n\n\t// Docker Compose (multi-file output)\n\tfor (const [filename, content] of Object.entries(composeResult.files)) {\n\t\tfiles[filename] = content;\n\t}\n\n\t// Environment files (when bare-metal with native services, host vars use host.docker.internal)\n\tconst envFiles = generateEnvFiles(resolved, {\n\t\tgenerateSecrets: input.generateSecrets,\n\t\tdomain: input.domain,\n\t\topenclawVersion: input.openclawVersion,\n\t\tnativeServiceIds: isBareMetal ? nativeIds : undefined,\n\t});\n\tfiles[\".env.example\"] = envFiles.envExample;\n\tfiles[\".env\"] = envFiles.env;\n\n\t// .gitignore\n\tfiles[\".gitignore\"] = [\n\t\t\".env\",\n\t\t\".env.local\",\n\t\t\".env.*.local\",\n\t\t\"*.log\",\n\t\t\"docker-compose.override.yml\",\n\t].join(\"\\n\");\n\n\t// Skills\n\tconst skillFiles = generateSkillFiles(resolved);\n\tfor (const [path, content] of Object.entries(skillFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// README\n\tfiles[\"README.md\"] = generateReadme(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdomain: input.domain,\n\t\tproxy: input.proxy,\n\t\tdeploymentType: input.deploymentType,\n\t\thasNativeServices: isBareMetal && nativeServices.length > 0,\n\t});\n\n\t// Scripts\n\tconst scripts = generateScripts();\n\tfor (const [path, content] of Object.entries(scripts)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// n8n workflows\n\tconst n8nWorkflows = generateN8nWorkflows(resolved);\n\tfor (const [path, content] of Object.entries(n8nWorkflows)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// PostgreSQL init script (creates per-service databases and users)\n\tconst postgresInit = generatePostgresInit(resolved);\n\tif (postgresInit) {\n\t\tfiles[\"postgres/init-databases.sh\"] = postgresInit;\n\t}\n\n\t// Caddy config\n\tif (input.proxy === \"caddy\" && input.domain) {\n\t\tfiles[\"caddy/Caddyfile\"] = generateCaddyfile(resolved, input.domain);\n\t}\n\n\t// Prometheus config\n\tconst hasPrometheus = resolved.services.some((s) => s.definition.id === \"prometheus\");\n\tif (hasPrometheus) {\n\t\tfiles[\"prometheus/prometheus.yml\"] = generatePrometheusConfig(resolved);\n\t}\n\n\t// Grafana config\n\tconst hasGrafana = resolved.services.some((s) => s.definition.id === \"grafana\");\n\tif (hasGrafana) {\n\t\tconst grafanaFiles = generateGrafanaConfig();\n\t\tfor (const [path, content] of Object.entries(grafanaFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t\t// Grafana dashboard\n\t\tfiles[\"config/grafana/dashboards/openclaw-stack-overview.json\"] = generateGrafanaDashboard();\n\t}\n\n\t// Docker Compose override (empty template)\n\tfiles[\"docker-compose.override.yml\"] = [\n\t\t\"# Local overrides for docker-compose.yml\",\n\t\t\"# This file is gitignored — use it for personal port changes, extra volumes, etc.\",\n\t\t\"services: {}\",\n\t\t\"\",\n\t].join(\"\\n\");\n\n\t// SERVICES.md documentation\n\tfiles[\"docs/SERVICES.md\"] = generateServicesDoc(resolved);\n\n\t// Bare-metal: native install scripts + top-level installer\n\tif (isBareMetal) {\n\t\tif (nativeServices.length > 0) {\n\t\t\tconst nativePlatform = platformToNativePlatform(input.platform);\n\t\t\tconst nativeScripts = generateNativeInstallScripts({\n\t\t\t\tnativeServices,\n\t\t\t\tplatform: nativePlatform,\n\t\t\t\tprojectName: input.projectName,\n\t\t\t});\n\t\t\tfor (const [path, content] of Object.entries(nativeScripts)) {\n\t\t\t\tfiles[path] = content;\n\t\t\t}\n\t\t}\n\t\tconst bareMetalFiles = generateBareMetalInstall({\n\t\t\tplatform: input.platform,\n\t\t\tprojectName: input.projectName,\n\t\t\thasNativeServices: nativeServices.length > 0,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(bareMetalFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// 5. Calculate metadata\n\tconst skillCount = resolved.services.reduce((sum, s) => sum + s.definition.skills.length, 0);\n\n\treturn {\n\t\tfiles,\n\t\tmetadata: {\n\t\t\tserviceCount: resolved.services.length,\n\t\t\tskillCount,\n\t\t\testimatedMemoryMB: resolved.estimatedMemoryMB,\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t},\n\t};\n}\n\nfunction generateServicesDoc(resolved: import(\"./types.js\").ResolverOutput): string {\n\tconst lines: string[] = [\n\t\t\"# Service Reference\",\n\t\t\"\",\n\t\t\"This document describes all companion services in your OpenClaw stack.\",\n\t\t\"\",\n\t];\n\n\tfor (const svc of resolved.services) {\n\t\tconst def = svc.definition;\n\t\tlines.push(`## ${def.icon} ${def.name}`);\n\t\tlines.push(\"\");\n\t\tlines.push(def.description);\n\t\tlines.push(\"\");\n\t\tlines.push(`- **Image**: \\`${def.image}:${def.imageTag}\\``);\n\t\tlines.push(`- **Category**: ${def.category}`);\n\t\tlines.push(`- **Maturity**: ${def.maturity}`);\n\t\tif (def.minMemoryMB) {\n\t\t\tlines.push(`- **Min Memory**: ${def.minMemoryMB}MB`);\n\t\t}\n\t\tif (def.ports.length > 0) {\n\t\t\tlines.push(\"- **Ports**:\");\n\t\t\tfor (const p of def.ports) {\n\t\t\t\tlines.push(\n\t\t\t\t\t` - \\`${p.container}\\` — ${p.description}${p.exposed ? \"\" : \" (internal only)\"}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tlines.push(`- **Docs**: ${def.docsUrl}`);\n\t\tlines.push(\"\");\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generate.mjs","names":[],"sources":["../src/generate.ts"],"sourcesContent":["import {\n\tpartitionBareMetal,\n\tplatformToNativePlatform,\n\tresolvedWithOnlyServices,\n} from \"./bare-metal-partition.js\";\nimport { composeMultiFile } from \"./composer.js\";\nimport { generateBareMetalInstall } from \"./generators/bare-metal-install.js\";\nimport { generateCaddyfile } from \"./generators/caddy.js\";\nimport { generateEnvFiles } from \"./generators/env.js\";\nimport { generateTraefikConfig } from \"./generators/traefik.js\";\nimport { generateGrafanaConfig, generateGrafanaDashboard } from \"./generators/grafana.js\";\nimport { generateN8nWorkflows } from \"./generators/n8n-workflows.js\";\nimport { generateNativeInstallScripts } from \"./generators/native-services.js\";\nimport { generatePostgresInit } from \"./generators/postgres-init.js\";\nimport { generatePrometheusConfig } from \"./generators/prometheus.js\";\nimport { generateReadme } from \"./generators/readme.js\";\nimport { generateScripts } from \"./generators/scripts.js\";\nimport { generateSkillFiles } from \"./generators/skills.js\";\nimport { migrateConfig } from \"./migrations.js\";\nimport { resolve } from \"./resolver.js\";\nimport type {\n\tGeneratedFiles,\n\tGenerationInput,\n\tGenerationResult,\n\tPlatform,\n\tResolverInput,\n} from \"./types.js\";\nimport { StackConfigError, ValidationError } from \"./errors.js\";\nimport { validate } from \"./validator.js\";\n\n/** Resolver/compose only support linux image platforms; normalize for bare-metal (windows/macos). */\nfunction getComposePlatform(platform: Platform): \"linux/amd64\" | \"linux/arm64\" {\n\tif (platform === \"linux/amd64\" || platform === \"linux/arm64\") return platform;\n\treturn \"linux/amd64\";\n}\n\n/**\n * Main orchestration function: takes generation input, resolves dependencies,\n * generates all files, validates, and returns the complete file tree.\n */\nexport function generate(rawInput: GenerationInput): GenerationResult {\n\t// Apply config migrations if needed\n\tconst input = migrateConfig(rawInput as Record<string, unknown>) as GenerationInput;\n\n\tconst composePlatform = getComposePlatform(input.platform);\n\n\t// 1. Resolve dependencies\n\tconst resolverInput: ResolverInput = {\n\t\tservices: input.services,\n\t\tskillPacks: input.skillPacks,\n\t\tproxy: input.proxy,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tmonitoring: input.monitoring,\n\t};\n\tconst resolved = resolve(resolverInput);\n\n\tif (!resolved.isValid) {\n\t\tthrow new StackConfigError(\n\t\t\t`Invalid stack configuration: ${resolved.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\tconst isBareMetal = input.deploymentType === \"bare-metal\";\n\tlet resolvedForCompose = resolved;\n\tlet nativeIds = new Set<string>();\n\tlet nativeServices: typeof resolved.services = [];\n\tlet dockerOnlyServices: typeof resolved.services = [];\n\n\tif (isBareMetal) {\n\t\tconst partition = partitionBareMetal(resolved, input.platform);\n\t\tnativeServices = partition.nativeServices;\n\t\tdockerOnlyServices = partition.dockerOnlyServices;\n\t\tnativeIds = partition.nativeIds;\n\t\tif (nativeServices.length > 0) {\n\t\t\tresolvedForCompose = resolvedWithOnlyServices(resolved, dockerOnlyServices);\n\t\t}\n\t}\n\n\t// 2. Generate Docker Compose YAML (multi-file)\n\t// Compute Traefik labels before composing (labels get injected into docker-compose services)\n\tlet traefikOutput: ReturnType<typeof generateTraefikConfig> | undefined;\n\tif (input.proxy === \"traefik\" && input.domain) {\n\t\ttraefikOutput = generateTraefikConfig(resolvedForCompose, input.domain);\n\t}\n\n\tconst composeOptions = {\n\t\tprojectName: input.projectName,\n\t\tproxy: input.proxy,\n\t\tdomain: input.domain,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tdeployment: input.deployment,\n\t\topenclawVersion: input.openclawVersion,\n\t\tbareMetalNativeHost: isBareMetal && nativeIds.size > 0,\n\t\ttraefikLabels: traefikOutput?.serviceLabels,\n\t};\n\tconst composeResult = composeMultiFile(resolvedForCompose, composeOptions);\n\n\t// 3. Validate (using the base docker-compose.yml)\n\tconst validation = validate(resolvedForCompose, composeResult.files[\"docker-compose.yml\"] ?? \"\", {\n\t\tdomain: input.domain,\n\t\tgenerateSecrets: input.generateSecrets,\n\t});\n\tif (!validation.valid) {\n\t\tthrow new ValidationError(`Validation failed: ${validation.errors.map((e) => e.message).join(\"; \")}`);\n\t}\n\n\t// 4. Generate all files\n\tconst files: GeneratedFiles = {};\n\n\t// Docker Compose (multi-file output)\n\tfor (const [filename, content] of Object.entries(composeResult.files)) {\n\t\tfiles[filename] = content;\n\t}\n\n\t// Environment files (when bare-metal with native services, host vars use host.docker.internal)\n\tconst envFiles = generateEnvFiles(resolved, {\n\t\tgenerateSecrets: input.generateSecrets,\n\t\tdomain: input.domain,\n\t\topenclawVersion: input.openclawVersion,\n\t\tnativeServiceIds: isBareMetal ? nativeIds : undefined,\n\t});\n\tfiles[\".env.example\"] = envFiles.envExample;\n\tfiles[\".env\"] = envFiles.env;\n\n\t// .gitignore\n\tfiles[\".gitignore\"] = [\n\t\t\".env\",\n\t\t\".env.local\",\n\t\t\".env.*.local\",\n\t\t\"*.log\",\n\t\t\"docker-compose.override.yml\",\n\t].join(\"\\n\");\n\n\t// Skills\n\tconst skillFiles = generateSkillFiles(resolved);\n\tfor (const [path, content] of Object.entries(skillFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// README\n\tfiles[\"README.md\"] = generateReadme(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdomain: input.domain,\n\t\tproxy: input.proxy,\n\t\tdeploymentType: input.deploymentType,\n\t\thasNativeServices: isBareMetal && nativeServices.length > 0,\n\t});\n\n\t// Scripts\n\tconst scripts = generateScripts();\n\tfor (const [path, content] of Object.entries(scripts)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// n8n workflows\n\tconst n8nWorkflows = generateN8nWorkflows(resolved);\n\tfor (const [path, content] of Object.entries(n8nWorkflows)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// PostgreSQL init script (creates per-service databases and users)\n\tconst postgresInit = generatePostgresInit(resolved);\n\tif (postgresInit) {\n\t\tfiles[\"postgres/init-databases.sh\"] = postgresInit;\n\t}\n\n\t// Caddy config\n\tif (input.proxy === \"caddy\" && input.domain) {\n\t\tfiles[\"caddy/Caddyfile\"] = generateCaddyfile(resolved, input.domain);\n\t}\n\n\t// Traefik config (labels are already injected via composeOptions.traefikLabels)\n\tif (traefikOutput) {\n\t\tfiles[\"traefik/traefik.yml\"] = traefikOutput.staticConfig;\n\t}\n\n\t// Prometheus config\n\tconst hasPrometheus = resolved.services.some((s) => s.definition.id === \"prometheus\");\n\tif (hasPrometheus) {\n\t\tfiles[\"prometheus/prometheus.yml\"] = generatePrometheusConfig(resolved);\n\t}\n\n\t// Grafana config\n\tconst hasGrafana = resolved.services.some((s) => s.definition.id === \"grafana\");\n\tif (hasGrafana) {\n\t\tconst grafanaFiles = generateGrafanaConfig();\n\t\tfor (const [path, content] of Object.entries(grafanaFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t\t// Grafana dashboard\n\t\tfiles[\"config/grafana/dashboards/openclaw-stack-overview.json\"] = generateGrafanaDashboard();\n\t}\n\n\t// Docker Compose override (empty template)\n\tfiles[\"docker-compose.override.yml\"] = [\n\t\t\"# Local overrides for docker-compose.yml\",\n\t\t\"# This file is gitignored — use it for personal port changes, extra volumes, etc.\",\n\t\t\"services: {}\",\n\t\t\"\",\n\t].join(\"\\n\");\n\n\t// SERVICES.md documentation\n\tfiles[\"docs/SERVICES.md\"] = generateServicesDoc(resolved);\n\n\t// Bare-metal: native install scripts + top-level installer\n\tif (isBareMetal) {\n\t\tif (nativeServices.length > 0) {\n\t\t\tconst nativePlatform = platformToNativePlatform(input.platform);\n\t\t\tconst nativeScripts = generateNativeInstallScripts({\n\t\t\t\tnativeServices,\n\t\t\t\tplatform: nativePlatform,\n\t\t\t\tprojectName: input.projectName,\n\t\t\t});\n\t\t\tfor (const [path, content] of Object.entries(nativeScripts)) {\n\t\t\t\tfiles[path] = content;\n\t\t\t}\n\t\t}\n\t\tconst bareMetalFiles = generateBareMetalInstall({\n\t\t\tplatform: input.platform,\n\t\t\tprojectName: input.projectName,\n\t\t\thasNativeServices: nativeServices.length > 0,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(bareMetalFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// 5. Calculate metadata\n\tconst skillCount = resolved.services.reduce((sum, s) => sum + s.definition.skills.length, 0);\n\n\treturn {\n\t\tfiles,\n\t\tmetadata: {\n\t\t\tserviceCount: resolved.services.length,\n\t\t\tskillCount,\n\t\t\testimatedMemoryMB: resolved.estimatedMemoryMB,\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t},\n\t};\n}\n\nexport function generateServicesDoc(resolved: import(\"./types.js\").ResolverOutput): string {\n\tconst lines: string[] = [\n\t\t\"# Service Reference\",\n\t\t\"\",\n\t\t\"This document describes all companion services in your OpenClaw stack.\",\n\t\t\"\",\n\t];\n\n\tfor (const svc of resolved.services) {\n\t\tconst def = svc.definition;\n\t\tlines.push(`## ${def.icon} ${def.name}`);\n\t\tlines.push(\"\");\n\t\tlines.push(def.description);\n\t\tlines.push(\"\");\n\t\tlines.push(`- **Image**: \\`${def.image}:${def.imageTag}\\``);\n\t\tlines.push(`- **Category**: ${def.category}`);\n\t\tlines.push(`- **Maturity**: ${def.maturity}`);\n\t\tif (def.minMemoryMB) {\n\t\t\tlines.push(`- **Min Memory**: ${def.minMemoryMB}MB`);\n\t\t}\n\t\tif (def.ports.length > 0) {\n\t\t\tlines.push(\"- **Ports**:\");\n\t\t\tfor (const p of def.ports) {\n\t\t\t\tlines.push(\n\t\t\t\t\t` - \\`${p.container}\\` — ${p.description}${p.exposed ? \"\" : \" (internal only)\"}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tlines.push(`- **Docs**: ${def.docsUrl}`);\n\t\tlines.push(\"\");\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ResolverOutput } from "../types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/generators/traefik.d.ts
|
|
4
|
+
interface TraefikGeneratorOutput {
|
|
5
|
+
staticConfig: string;
|
|
6
|
+
serviceLabels: Map<string, Record<string, string>>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Generates Traefik configuration: a static config YAML file and per-service
|
|
10
|
+
* Docker labels for automatic service discovery and reverse proxy routing.
|
|
11
|
+
*
|
|
12
|
+
* @param resolved - The resolved service configuration
|
|
13
|
+
* @param domain - The main domain for routing (e.g. "example.com")
|
|
14
|
+
* @returns Static config content and a map of serviceId → Docker labels
|
|
15
|
+
*/
|
|
16
|
+
declare function generateTraefikConfig(resolved: ResolverOutput, domain: string): TraefikGeneratorOutput;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { TraefikGeneratorOutput, generateTraefikConfig };
|
|
19
|
+
//# sourceMappingURL=traefik.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traefik.d.mts","names":[],"sources":["../../src/generators/traefik.ts"],"mappings":";;;UAEiB,sBAAA;EAChB,YAAA;EACA,aAAA,EAAe,GAAA,SAAY,MAAA;AAAA;;;;;;;;;iBAWZ,qBAAA,CACf,QAAA,EAAU,cAAA,EACV,MAAA,WACE,sBAAA"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//#region src/generators/traefik.ts
|
|
2
|
+
/**
|
|
3
|
+
* Generates Traefik configuration: a static config YAML file and per-service
|
|
4
|
+
* Docker labels for automatic service discovery and reverse proxy routing.
|
|
5
|
+
*
|
|
6
|
+
* @param resolved - The resolved service configuration
|
|
7
|
+
* @param domain - The main domain for routing (e.g. "example.com")
|
|
8
|
+
* @returns Static config content and a map of serviceId → Docker labels
|
|
9
|
+
*/
|
|
10
|
+
function generateTraefikConfig(resolved, domain) {
|
|
11
|
+
const staticConfig = generateStaticConfig(domain);
|
|
12
|
+
const serviceLabels = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const { definition } of resolved.services) {
|
|
14
|
+
if (definition.id === "traefik" || definition.id === "caddy") continue;
|
|
15
|
+
const exposedPorts = definition.ports.filter((p) => p.exposed);
|
|
16
|
+
if (exposedPorts.length === 0) continue;
|
|
17
|
+
const primaryPort = exposedPorts[0];
|
|
18
|
+
const routerName = definition.id.replace(/-/g, "");
|
|
19
|
+
const labels = {
|
|
20
|
+
"traefik.enable": "true",
|
|
21
|
+
[`traefik.http.routers.${routerName}.rule`]: `Host(\`${definition.id}.${domain}\`)`,
|
|
22
|
+
[`traefik.http.routers.${routerName}.entrypoints`]: "websecure",
|
|
23
|
+
[`traefik.http.routers.${routerName}.tls.certresolver`]: "letsencrypt",
|
|
24
|
+
[`traefik.http.services.${routerName}.loadbalancer.server.port`]: String(primaryPort.container),
|
|
25
|
+
[`traefik.http.routers.${routerName}-http.rule`]: `Host(\`${definition.id}.${domain}\`)`,
|
|
26
|
+
[`traefik.http.routers.${routerName}-http.entrypoints`]: "web",
|
|
27
|
+
[`traefik.http.routers.${routerName}-http.middlewares`]: "redirect-to-https"
|
|
28
|
+
};
|
|
29
|
+
serviceLabels.set(definition.id, labels);
|
|
30
|
+
}
|
|
31
|
+
serviceLabels.set("openclaw-gateway", {
|
|
32
|
+
"traefik.enable": "true",
|
|
33
|
+
"traefik.http.routers.gateway.rule": `Host(\`${domain}\`)`,
|
|
34
|
+
"traefik.http.routers.gateway.entrypoints": "websecure",
|
|
35
|
+
"traefik.http.routers.gateway.tls.certresolver": "letsencrypt",
|
|
36
|
+
"traefik.http.services.gateway.loadbalancer.server.port": "18789",
|
|
37
|
+
"traefik.http.routers.gateway-http.rule": `Host(\`${domain}\`)`,
|
|
38
|
+
"traefik.http.routers.gateway-http.entrypoints": "web",
|
|
39
|
+
"traefik.http.routers.gateway-http.middlewares": "redirect-to-https"
|
|
40
|
+
});
|
|
41
|
+
serviceLabels.set("traefik", {
|
|
42
|
+
"traefik.enable": "true",
|
|
43
|
+
"traefik.http.middlewares.redirect-to-https.redirectscheme.scheme": "https",
|
|
44
|
+
"traefik.http.middlewares.redirect-to-https.redirectscheme.permanent": "true"
|
|
45
|
+
});
|
|
46
|
+
return {
|
|
47
|
+
staticConfig,
|
|
48
|
+
serviceLabels
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function generateStaticConfig(domain) {
|
|
52
|
+
return `# Traefik Static Configuration — Auto-generated by OpenClaw
|
|
53
|
+
# Domain: ${domain}
|
|
54
|
+
|
|
55
|
+
api:
|
|
56
|
+
dashboard: true
|
|
57
|
+
insecure: true
|
|
58
|
+
|
|
59
|
+
entryPoints:
|
|
60
|
+
web:
|
|
61
|
+
address: ":80"
|
|
62
|
+
websecure:
|
|
63
|
+
address: ":443"
|
|
64
|
+
|
|
65
|
+
providers:
|
|
66
|
+
docker:
|
|
67
|
+
endpoint: "unix:///var/run/docker.sock"
|
|
68
|
+
exposedByDefault: false
|
|
69
|
+
network: openclaw-network
|
|
70
|
+
|
|
71
|
+
certificatesResolvers:
|
|
72
|
+
letsencrypt:
|
|
73
|
+
acme:
|
|
74
|
+
email: admin@${domain}
|
|
75
|
+
storage: /letsencrypt/acme.json
|
|
76
|
+
httpChallenge:
|
|
77
|
+
entryPoint: web
|
|
78
|
+
|
|
79
|
+
log:
|
|
80
|
+
level: INFO
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
export { generateTraefikConfig };
|
|
86
|
+
//# sourceMappingURL=traefik.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traefik.mjs","names":[],"sources":["../../src/generators/traefik.ts"],"sourcesContent":["import type { ResolverOutput } from \"../types.js\";\n\nexport interface TraefikGeneratorOutput {\n\tstaticConfig: string;\n\tserviceLabels: Map<string, Record<string, string>>;\n}\n\n/**\n * Generates Traefik configuration: a static config YAML file and per-service\n * Docker labels for automatic service discovery and reverse proxy routing.\n *\n * @param resolved - The resolved service configuration\n * @param domain - The main domain for routing (e.g. \"example.com\")\n * @returns Static config content and a map of serviceId → Docker labels\n */\nexport function generateTraefikConfig(\n\tresolved: ResolverOutput,\n\tdomain: string,\n): TraefikGeneratorOutput {\n\tconst staticConfig = generateStaticConfig(domain);\n\tconst serviceLabels = new Map<string, Record<string, string>>();\n\n\t// Per-service labels for exposed ports\n\tfor (const { definition } of resolved.services) {\n\t\tif (definition.id === \"traefik\" || definition.id === \"caddy\") continue;\n\n\t\tconst exposedPorts = definition.ports.filter((p) => p.exposed);\n\t\tif (exposedPorts.length === 0) continue;\n\n\t\tconst primaryPort = exposedPorts[0]!;\n\t\tconst routerName = definition.id.replace(/-/g, \"\");\n\n\t\tconst labels: Record<string, string> = {\n\t\t\t\"traefik.enable\": \"true\",\n\t\t\t// HTTPS router\n\t\t\t[`traefik.http.routers.${routerName}.rule`]: `Host(\\`${definition.id}.${domain}\\`)`,\n\t\t\t[`traefik.http.routers.${routerName}.entrypoints`]: \"websecure\",\n\t\t\t[`traefik.http.routers.${routerName}.tls.certresolver`]: \"letsencrypt\",\n\t\t\t[`traefik.http.services.${routerName}.loadbalancer.server.port`]: String(\n\t\t\t\tprimaryPort.container,\n\t\t\t),\n\t\t\t// HTTP → HTTPS redirect\n\t\t\t[`traefik.http.routers.${routerName}-http.rule`]: `Host(\\`${definition.id}.${domain}\\`)`,\n\t\t\t[`traefik.http.routers.${routerName}-http.entrypoints`]: \"web\",\n\t\t\t[`traefik.http.routers.${routerName}-http.middlewares`]: \"redirect-to-https\",\n\t\t};\n\n\t\tserviceLabels.set(definition.id, labels);\n\t}\n\n\t// Gateway gets the root domain\n\tserviceLabels.set(\"openclaw-gateway\", {\n\t\t\"traefik.enable\": \"true\",\n\t\t\"traefik.http.routers.gateway.rule\": `Host(\\`${domain}\\`)`,\n\t\t\"traefik.http.routers.gateway.entrypoints\": \"websecure\",\n\t\t\"traefik.http.routers.gateway.tls.certresolver\": \"letsencrypt\",\n\t\t\"traefik.http.services.gateway.loadbalancer.server.port\": \"18789\",\n\t\t\"traefik.http.routers.gateway-http.rule\": `Host(\\`${domain}\\`)`,\n\t\t\"traefik.http.routers.gateway-http.entrypoints\": \"web\",\n\t\t\"traefik.http.routers.gateway-http.middlewares\": \"redirect-to-https\",\n\t});\n\n\t// Global redirect middleware on the Traefik service itself\n\tserviceLabels.set(\"traefik\", {\n\t\t\"traefik.enable\": \"true\",\n\t\t\"traefik.http.middlewares.redirect-to-https.redirectscheme.scheme\": \"https\",\n\t\t\"traefik.http.middlewares.redirect-to-https.redirectscheme.permanent\": \"true\",\n\t});\n\n\treturn { staticConfig, serviceLabels };\n}\n\nfunction generateStaticConfig(domain: string): string {\n\treturn `# Traefik Static Configuration — Auto-generated by OpenClaw\n# Domain: ${domain}\n\napi:\n dashboard: true\n insecure: true\n\nentryPoints:\n web:\n address: \":80\"\n websecure:\n address: \":443\"\n\nproviders:\n docker:\n endpoint: \"unix:///var/run/docker.sock\"\n exposedByDefault: false\n network: openclaw-network\n\ncertificatesResolvers:\n letsencrypt:\n acme:\n email: admin@${domain}\n storage: /letsencrypt/acme.json\n httpChallenge:\n entryPoint: web\n\nlog:\n level: INFO\n`;\n}\n"],"mappings":";;;;;;;;;AAeA,SAAgB,sBACf,UACA,QACyB;CACzB,MAAM,eAAe,qBAAqB,OAAO;CACjD,MAAM,gCAAgB,IAAI,KAAqC;AAG/D,MAAK,MAAM,EAAE,gBAAgB,SAAS,UAAU;AAC/C,MAAI,WAAW,OAAO,aAAa,WAAW,OAAO,QAAS;EAE9D,MAAM,eAAe,WAAW,MAAM,QAAQ,MAAM,EAAE,QAAQ;AAC9D,MAAI,aAAa,WAAW,EAAG;EAE/B,MAAM,cAAc,aAAa;EACjC,MAAM,aAAa,WAAW,GAAG,QAAQ,MAAM,GAAG;EAElD,MAAM,SAAiC;GACtC,kBAAkB;IAEjB,wBAAwB,WAAW,SAAS,UAAU,WAAW,GAAG,GAAG,OAAO;IAC9E,wBAAwB,WAAW,gBAAgB;IACnD,wBAAwB,WAAW,qBAAqB;IACxD,yBAAyB,WAAW,6BAA6B,OACjE,YAAY,UACZ;IAEA,wBAAwB,WAAW,cAAc,UAAU,WAAW,GAAG,GAAG,OAAO;IACnF,wBAAwB,WAAW,qBAAqB;IACxD,wBAAwB,WAAW,qBAAqB;GACzD;AAED,gBAAc,IAAI,WAAW,IAAI,OAAO;;AAIzC,eAAc,IAAI,oBAAoB;EACrC,kBAAkB;EAClB,qCAAqC,UAAU,OAAO;EACtD,4CAA4C;EAC5C,iDAAiD;EACjD,0DAA0D;EAC1D,0CAA0C,UAAU,OAAO;EAC3D,iDAAiD;EACjD,iDAAiD;EACjD,CAAC;AAGF,eAAc,IAAI,WAAW;EAC5B,kBAAkB;EAClB,oEAAoE;EACpE,uEAAuE;EACvE,CAAC;AAEF,QAAO;EAAE;EAAc;EAAe;;AAGvC,SAAS,qBAAqB,QAAwB;AACrD,QAAO;YACI,OAAO;;;;;;;;;;;;;;;;;;;;;qBAqBE,OAAO"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-Qk6MgAnK.mjs";
|
|
2
|
+
import { resolve } from "../resolver.mjs";
|
|
3
|
+
import { generateTraefikConfig } from "./traefik.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/generators/traefik.test.ts
|
|
6
|
+
function resolveWith(services) {
|
|
7
|
+
return resolve({
|
|
8
|
+
services,
|
|
9
|
+
skillPacks: [],
|
|
10
|
+
proxy: "traefik",
|
|
11
|
+
gpu: false
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
describe("generateTraefikConfig", () => {
|
|
15
|
+
const domain = "example.com";
|
|
16
|
+
it("generates static config with domain email and entrypoints", () => {
|
|
17
|
+
const { staticConfig } = generateTraefikConfig(resolveWith(["redis"]), domain);
|
|
18
|
+
globalExpect(staticConfig).toContain("admin@example.com");
|
|
19
|
+
globalExpect(staticConfig).toContain("address: \":80\"");
|
|
20
|
+
globalExpect(staticConfig).toContain("address: \":443\"");
|
|
21
|
+
globalExpect(staticConfig).toContain("exposedByDefault: false");
|
|
22
|
+
globalExpect(staticConfig).toContain("openclaw-network");
|
|
23
|
+
});
|
|
24
|
+
it("generates labels for services with exposed ports", () => {
|
|
25
|
+
const { serviceLabels } = generateTraefikConfig(resolveWith(["redis"]), domain);
|
|
26
|
+
const redisLabels = serviceLabels.get("redis");
|
|
27
|
+
globalExpect(redisLabels).toBeDefined();
|
|
28
|
+
globalExpect(redisLabels["traefik.enable"]).toBe("true");
|
|
29
|
+
globalExpect(redisLabels["traefik.http.routers.redis.rule"]).toBe("Host(`redis.example.com`)");
|
|
30
|
+
globalExpect(redisLabels["traefik.http.routers.redis.entrypoints"]).toBe("websecure");
|
|
31
|
+
globalExpect(redisLabels["traefik.http.routers.redis.tls.certresolver"]).toBe("letsencrypt");
|
|
32
|
+
globalExpect(redisLabels["traefik.http.services.redis.loadbalancer.server.port"]).toBe("6379");
|
|
33
|
+
});
|
|
34
|
+
it("generates HTTP to HTTPS redirect labels", () => {
|
|
35
|
+
const { serviceLabels } = generateTraefikConfig(resolveWith(["redis"]), domain);
|
|
36
|
+
const redisLabels = serviceLabels.get("redis");
|
|
37
|
+
globalExpect(redisLabels["traefik.http.routers.redis-http.entrypoints"]).toBe("web");
|
|
38
|
+
globalExpect(redisLabels["traefik.http.routers.redis-http.middlewares"]).toBe("redirect-to-https");
|
|
39
|
+
});
|
|
40
|
+
it("assigns root domain to gateway", () => {
|
|
41
|
+
const { serviceLabels } = generateTraefikConfig(resolveWith(["redis"]), domain);
|
|
42
|
+
const gwLabels = serviceLabels.get("openclaw-gateway");
|
|
43
|
+
globalExpect(gwLabels).toBeDefined();
|
|
44
|
+
globalExpect(gwLabels["traefik.http.routers.gateway.rule"]).toBe("Host(`example.com`)");
|
|
45
|
+
globalExpect(gwLabels["traefik.http.services.gateway.loadbalancer.server.port"]).toBe("18789");
|
|
46
|
+
});
|
|
47
|
+
it("adds global redirect middleware on traefik service", () => {
|
|
48
|
+
const { serviceLabels } = generateTraefikConfig(resolveWith(["redis"]), domain);
|
|
49
|
+
const traefikLabels = serviceLabels.get("traefik");
|
|
50
|
+
globalExpect(traefikLabels).toBeDefined();
|
|
51
|
+
globalExpect(traefikLabels["traefik.http.middlewares.redirect-to-https.redirectscheme.scheme"]).toBe("https");
|
|
52
|
+
globalExpect(traefikLabels["traefik.http.middlewares.redirect-to-https.redirectscheme.permanent"]).toBe("true");
|
|
53
|
+
});
|
|
54
|
+
it("skips proxy services and services without exposed ports", () => {
|
|
55
|
+
const { serviceLabels } = generateTraefikConfig(resolveWith(["redis", "ffmpeg"]), domain);
|
|
56
|
+
globalExpect(serviceLabels.has("ffmpeg")).toBe(false);
|
|
57
|
+
globalExpect(serviceLabels.get("traefik")["traefik.http.routers.traefik.rule"]).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
it("sanitizes service names with hyphens in router names", () => {
|
|
60
|
+
const { serviceLabels } = generateTraefikConfig(resolveWith(["open-webui"]), domain);
|
|
61
|
+
const labels = serviceLabels.get("open-webui");
|
|
62
|
+
globalExpect(labels).toBeDefined();
|
|
63
|
+
globalExpect(labels["traefik.http.routers.openwebui.rule"]).toBe("Host(`open-webui.example.com`)");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
export { };
|
|
69
|
+
//# sourceMappingURL=traefik.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traefik.test.mjs","names":[],"sources":["../../src/generators/traefik.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { generateTraefikConfig } from \"./traefik.js\";\nimport type { ResolverOutput } from \"../types.js\";\nimport { resolve } from \"../resolver.js\";\n\nfunction resolveWith(services: string[]): ResolverOutput {\n\treturn resolve({ services, skillPacks: [], proxy: \"traefik\", gpu: false });\n}\n\ndescribe(\"generateTraefikConfig\", () => {\n\tconst domain = \"example.com\";\n\n\tit(\"generates static config with domain email and entrypoints\", () => {\n\t\tconst resolved = resolveWith([\"redis\"]);\n\t\tconst { staticConfig } = generateTraefikConfig(resolved, domain);\n\n\t\texpect(staticConfig).toContain(\"admin@example.com\");\n\t\texpect(staticConfig).toContain('address: \":80\"');\n\t\texpect(staticConfig).toContain('address: \":443\"');\n\t\texpect(staticConfig).toContain(\"exposedByDefault: false\");\n\t\texpect(staticConfig).toContain(\"openclaw-network\");\n\t});\n\n\tit(\"generates labels for services with exposed ports\", () => {\n\t\tconst resolved = resolveWith([\"redis\"]);\n\t\tconst { serviceLabels } = generateTraefikConfig(resolved, domain);\n\n\t\tconst redisLabels = serviceLabels.get(\"redis\");\n\t\texpect(redisLabels).toBeDefined();\n\t\texpect(redisLabels![\"traefik.enable\"]).toBe(\"true\");\n\t\texpect(redisLabels![\"traefik.http.routers.redis.rule\"]).toBe(\n\t\t\t\"Host(`redis.example.com`)\",\n\t\t);\n\t\texpect(redisLabels![\"traefik.http.routers.redis.entrypoints\"]).toBe(\"websecure\");\n\t\texpect(redisLabels![\"traefik.http.routers.redis.tls.certresolver\"]).toBe(\"letsencrypt\");\n\t\texpect(redisLabels![\"traefik.http.services.redis.loadbalancer.server.port\"]).toBe(\"6379\");\n\t});\n\n\tit(\"generates HTTP to HTTPS redirect labels\", () => {\n\t\tconst resolved = resolveWith([\"redis\"]);\n\t\tconst { serviceLabels } = generateTraefikConfig(resolved, domain);\n\n\t\tconst redisLabels = serviceLabels.get(\"redis\")!;\n\t\texpect(redisLabels[\"traefik.http.routers.redis-http.entrypoints\"]).toBe(\"web\");\n\t\texpect(redisLabels[\"traefik.http.routers.redis-http.middlewares\"]).toBe(\n\t\t\t\"redirect-to-https\",\n\t\t);\n\t});\n\n\tit(\"assigns root domain to gateway\", () => {\n\t\tconst resolved = resolveWith([\"redis\"]);\n\t\tconst { serviceLabels } = generateTraefikConfig(resolved, domain);\n\n\t\tconst gwLabels = serviceLabels.get(\"openclaw-gateway\");\n\t\texpect(gwLabels).toBeDefined();\n\t\texpect(gwLabels![\"traefik.http.routers.gateway.rule\"]).toBe(\n\t\t\t\"Host(`example.com`)\",\n\t\t);\n\t\texpect(gwLabels![\"traefik.http.services.gateway.loadbalancer.server.port\"]).toBe(\"18789\");\n\t});\n\n\tit(\"adds global redirect middleware on traefik service\", () => {\n\t\tconst resolved = resolveWith([\"redis\"]);\n\t\tconst { serviceLabels } = generateTraefikConfig(resolved, domain);\n\n\t\tconst traefikLabels = serviceLabels.get(\"traefik\");\n\t\texpect(traefikLabels).toBeDefined();\n\t\texpect(\n\t\t\ttraefikLabels![\"traefik.http.middlewares.redirect-to-https.redirectscheme.scheme\"],\n\t\t).toBe(\"https\");\n\t\texpect(\n\t\t\ttraefikLabels![\"traefik.http.middlewares.redirect-to-https.redirectscheme.permanent\"],\n\t\t).toBe(\"true\");\n\t});\n\n\tit(\"skips proxy services and services without exposed ports\", () => {\n\t\tconst resolved = resolveWith([\"redis\", \"ffmpeg\"]);\n\t\tconst { serviceLabels } = generateTraefikConfig(resolved, domain);\n\n\t\t// ffmpeg has no exposed ports\n\t\texpect(serviceLabels.has(\"ffmpeg\")).toBe(false);\n\t\t// traefik itself is handled separately (not as a regular service)\n\t\texpect(serviceLabels.get(\"traefik\")![\"traefik.http.routers.traefik.rule\"]).toBeUndefined();\n\t});\n\n\tit(\"sanitizes service names with hyphens in router names\", () => {\n\t\tconst resolved = resolveWith([\"open-webui\"]);\n\t\tconst { serviceLabels } = generateTraefikConfig(resolved, domain);\n\n\t\tconst labels = serviceLabels.get(\"open-webui\");\n\t\texpect(labels).toBeDefined();\n\t\t// Router name has hyphens removed\n\t\texpect(labels![\"traefik.http.routers.openwebui.rule\"]).toBe(\n\t\t\t\"Host(`open-webui.example.com`)\",\n\t\t);\n\t});\n});\n"],"mappings":";;;;;AAKA,SAAS,YAAY,UAAoC;AACxD,QAAO,QAAQ;EAAE;EAAU,YAAY,EAAE;EAAE,OAAO;EAAW,KAAK;EAAO,CAAC;;AAG3E,SAAS,+BAA+B;CACvC,MAAM,SAAS;AAEf,IAAG,mEAAmE;EAErE,MAAM,EAAE,iBAAiB,sBADR,YAAY,CAAC,QAAQ,CAAC,EACkB,OAAO;AAEhE,eAAO,aAAa,CAAC,UAAU,oBAAoB;AACnD,eAAO,aAAa,CAAC,UAAU,mBAAiB;AAChD,eAAO,aAAa,CAAC,UAAU,oBAAkB;AACjD,eAAO,aAAa,CAAC,UAAU,0BAA0B;AACzD,eAAO,aAAa,CAAC,UAAU,mBAAmB;GACjD;AAEF,IAAG,0DAA0D;EAE5D,MAAM,EAAE,kBAAkB,sBADT,YAAY,CAAC,QAAQ,CAAC,EACmB,OAAO;EAEjE,MAAM,cAAc,cAAc,IAAI,QAAQ;AAC9C,eAAO,YAAY,CAAC,aAAa;AACjC,eAAO,YAAa,kBAAkB,CAAC,KAAK,OAAO;AACnD,eAAO,YAAa,mCAAmC,CAAC,KACvD,4BACA;AACD,eAAO,YAAa,0CAA0C,CAAC,KAAK,YAAY;AAChF,eAAO,YAAa,+CAA+C,CAAC,KAAK,cAAc;AACvF,eAAO,YAAa,wDAAwD,CAAC,KAAK,OAAO;GACxF;AAEF,IAAG,iDAAiD;EAEnD,MAAM,EAAE,kBAAkB,sBADT,YAAY,CAAC,QAAQ,CAAC,EACmB,OAAO;EAEjE,MAAM,cAAc,cAAc,IAAI,QAAQ;AAC9C,eAAO,YAAY,+CAA+C,CAAC,KAAK,MAAM;AAC9E,eAAO,YAAY,+CAA+C,CAAC,KAClE,oBACA;GACA;AAEF,IAAG,wCAAwC;EAE1C,MAAM,EAAE,kBAAkB,sBADT,YAAY,CAAC,QAAQ,CAAC,EACmB,OAAO;EAEjE,MAAM,WAAW,cAAc,IAAI,mBAAmB;AACtD,eAAO,SAAS,CAAC,aAAa;AAC9B,eAAO,SAAU,qCAAqC,CAAC,KACtD,sBACA;AACD,eAAO,SAAU,0DAA0D,CAAC,KAAK,QAAQ;GACxF;AAEF,IAAG,4DAA4D;EAE9D,MAAM,EAAE,kBAAkB,sBADT,YAAY,CAAC,QAAQ,CAAC,EACmB,OAAO;EAEjE,MAAM,gBAAgB,cAAc,IAAI,UAAU;AAClD,eAAO,cAAc,CAAC,aAAa;AACnC,eACC,cAAe,oEACf,CAAC,KAAK,QAAQ;AACf,eACC,cAAe,uEACf,CAAC,KAAK,OAAO;GACb;AAEF,IAAG,iEAAiE;EAEnE,MAAM,EAAE,kBAAkB,sBADT,YAAY,CAAC,SAAS,SAAS,CAAC,EACS,OAAO;AAGjE,eAAO,cAAc,IAAI,SAAS,CAAC,CAAC,KAAK,MAAM;AAE/C,eAAO,cAAc,IAAI,UAAU,CAAE,qCAAqC,CAAC,eAAe;GACzF;AAEF,IAAG,8DAA8D;EAEhE,MAAM,EAAE,kBAAkB,sBADT,YAAY,CAAC,aAAa,CAAC,EACc,OAAO;EAEjE,MAAM,SAAS,cAAc,IAAI,aAAa;AAC9C,eAAO,OAAO,CAAC,aAAa;AAE5B,eAAO,OAAQ,uCAAuC,CAAC,KACtD,iCACA;GACA;EACD"}
|