@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.
Files changed (112) hide show
  1. package/dist/bare-metal-partition.d.mts.map +1 -1
  2. package/dist/bare-metal-partition.mjs +1 -0
  3. package/dist/bare-metal-partition.mjs.map +1 -1
  4. package/dist/composer.d.mts.map +1 -1
  5. package/dist/composer.mjs +11 -1
  6. package/dist/composer.mjs.map +1 -1
  7. package/dist/errors.d.mts +17 -0
  8. package/dist/errors.d.mts.map +1 -0
  9. package/dist/errors.mjs +24 -0
  10. package/dist/errors.mjs.map +1 -0
  11. package/dist/generate.d.mts +4 -3
  12. package/dist/generate.d.mts.map +1 -1
  13. package/dist/generate.mjs +13 -5
  14. package/dist/generate.mjs.map +1 -1
  15. package/dist/generators/traefik.d.mts +19 -0
  16. package/dist/generators/traefik.d.mts.map +1 -0
  17. package/dist/generators/traefik.mjs +86 -0
  18. package/dist/generators/traefik.mjs.map +1 -0
  19. package/dist/generators/traefik.test.d.mts +1 -0
  20. package/dist/generators/traefik.test.mjs +69 -0
  21. package/dist/generators/traefik.test.mjs.map +1 -0
  22. package/dist/index.d.mts +4 -2
  23. package/dist/index.mjs +4 -2
  24. package/dist/migrations.d.mts +14 -0
  25. package/dist/migrations.d.mts.map +1 -0
  26. package/dist/migrations.mjs +33 -0
  27. package/dist/migrations.mjs.map +1 -0
  28. package/dist/migrations.test.d.mts +1 -0
  29. package/dist/migrations.test.mjs +42 -0
  30. package/dist/migrations.test.mjs.map +1 -0
  31. package/dist/resolver.d.mts +6 -1
  32. package/dist/resolver.d.mts.map +1 -1
  33. package/dist/resolver.mjs +21 -3
  34. package/dist/resolver.mjs.map +1 -1
  35. package/dist/schema.d.mts +9 -0
  36. package/dist/schema.d.mts.map +1 -1
  37. package/dist/schema.mjs +4 -1
  38. package/dist/schema.mjs.map +1 -1
  39. package/dist/services/definitions/caddy.mjs +20 -1
  40. package/dist/services/definitions/caddy.mjs.map +1 -1
  41. package/dist/services/definitions/cal-com.d.mts +7 -0
  42. package/dist/services/definitions/cal-com.d.mts.map +1 -0
  43. package/dist/services/definitions/cal-com.mjs +88 -0
  44. package/dist/services/definitions/cal-com.mjs.map +1 -0
  45. package/dist/services/definitions/comfyui.d.mts +7 -0
  46. package/dist/services/definitions/comfyui.d.mts.map +1 -0
  47. package/dist/services/definitions/comfyui.mjs +83 -0
  48. package/dist/services/definitions/comfyui.mjs.map +1 -0
  49. package/dist/services/definitions/desktop-environment.d.mts +7 -0
  50. package/dist/services/definitions/desktop-environment.d.mts.map +1 -0
  51. package/dist/services/definitions/desktop-environment.mjs +153 -0
  52. package/dist/services/definitions/desktop-environment.mjs.map +1 -0
  53. package/dist/services/definitions/grafana.mjs +13 -1
  54. package/dist/services/definitions/grafana.mjs.map +1 -1
  55. package/dist/services/definitions/index.d.mts +7 -1
  56. package/dist/services/definitions/index.d.mts.map +1 -1
  57. package/dist/services/definitions/index.mjs +14 -2
  58. package/dist/services/definitions/index.mjs.map +1 -1
  59. package/dist/services/definitions/neo4j.d.mts +7 -0
  60. package/dist/services/definitions/neo4j.d.mts.map +1 -0
  61. package/dist/services/definitions/neo4j.mjs +91 -0
  62. package/dist/services/definitions/neo4j.mjs.map +1 -0
  63. package/dist/services/definitions/stream-gateway.d.mts +7 -0
  64. package/dist/services/definitions/stream-gateway.d.mts.map +1 -0
  65. package/dist/services/definitions/stream-gateway.mjs +133 -0
  66. package/dist/services/definitions/stream-gateway.mjs.map +1 -0
  67. package/dist/services/definitions/traefik.mjs +0 -1
  68. package/dist/services/definitions/traefik.mjs.map +1 -1
  69. package/dist/services/definitions/xyops.d.mts +7 -0
  70. package/dist/services/definitions/xyops.d.mts.map +1 -0
  71. package/dist/services/definitions/xyops.mjs +86 -0
  72. package/dist/services/definitions/xyops.mjs.map +1 -0
  73. package/dist/types.d.mts +8 -1
  74. package/dist/types.d.mts.map +1 -1
  75. package/dist/types.mjs +10 -0
  76. package/dist/types.mjs.map +1 -1
  77. package/dist/validator.mjs +11 -0
  78. package/dist/validator.mjs.map +1 -1
  79. package/dist/version-manager.d.mts +1 -1
  80. package/dist/version-manager.d.mts.map +1 -1
  81. package/dist/version-manager.mjs +11 -5
  82. package/dist/version-manager.mjs.map +1 -1
  83. package/dist/version-manager.test.d.mts +1 -0
  84. package/dist/version-manager.test.mjs +102 -0
  85. package/dist/version-manager.test.mjs.map +1 -0
  86. package/package.json +1 -1
  87. package/src/__snapshots__/composer.snapshot.test.ts.snap +15 -1
  88. package/src/bare-metal-partition.ts +1 -0
  89. package/src/composer.ts +22 -1
  90. package/src/errors.ts +23 -0
  91. package/src/generate.ts +22 -4
  92. package/src/generators/traefik.test.ts +97 -0
  93. package/src/generators/traefik.ts +104 -0
  94. package/src/index.ts +7 -1
  95. package/src/migrations.test.ts +36 -0
  96. package/src/migrations.ts +49 -0
  97. package/src/resolver.ts +37 -3
  98. package/src/schema.ts +3 -0
  99. package/src/services/definitions/caddy.ts +23 -1
  100. package/src/services/definitions/cal-com.ts +91 -0
  101. package/src/services/definitions/comfyui.ts +90 -0
  102. package/src/services/definitions/desktop-environment.ts +163 -0
  103. package/src/services/definitions/grafana.ts +16 -1
  104. package/src/services/definitions/index.ts +18 -0
  105. package/src/services/definitions/neo4j.ts +96 -0
  106. package/src/services/definitions/stream-gateway.ts +148 -0
  107. package/src/services/definitions/traefik.ts +0 -2
  108. package/src/services/definitions/xyops.ts +94 -0
  109. package/src/types.ts +7 -1
  110. package/src/validator.ts +16 -0
  111. package/src/version-manager.test.ts +134 -0
  112. package/src/version-manager.ts +12 -5
@@ -0,0 +1,148 @@
1
+ import type { ServiceDefinition } from "../../types.js";
2
+
3
+ export const streamGatewayDefinition: ServiceDefinition = {
4
+ id: "stream-gateway",
5
+ name: "Stream Gateway",
6
+ description:
7
+ "NGINX-RTMP relay server that receives a local RTMP stream (e.g. from OBS in the desktop-environment) and fans it out to YouTube, Twitch, TikTok, and Telegram simultaneously. Also serves an HLS preview on HTTP for local viewing.",
8
+ category: "streaming",
9
+ icon: "📺",
10
+
11
+ image: "tiangolo/nginx-rtmp",
12
+ imageTag: "latest",
13
+ ports: [
14
+ {
15
+ host: 1935,
16
+ container: 1935,
17
+ description: "RTMP ingest (receives stream from OBS or ffmpeg)",
18
+ exposed: true,
19
+ },
20
+ {
21
+ host: 8080,
22
+ container: 8080,
23
+ description: "HTTP server for HLS preview and stats",
24
+ exposed: true,
25
+ },
26
+ ],
27
+ volumes: [
28
+ {
29
+ name: "stream-gateway-hls",
30
+ containerPath: "/tmp/hls",
31
+ description: "HLS segment storage for live preview playback",
32
+ },
33
+ ],
34
+ environment: [
35
+ {
36
+ key: "YOUTUBE_STREAM_KEY",
37
+ defaultValue: "",
38
+ secret: true,
39
+ description: "YouTube Live stream key (leave empty to skip YouTube relay)",
40
+ required: false,
41
+ },
42
+ {
43
+ key: "TWITCH_STREAM_KEY",
44
+ defaultValue: "",
45
+ secret: true,
46
+ description: "Twitch stream key (leave empty to skip Twitch relay)",
47
+ required: false,
48
+ },
49
+ {
50
+ key: "TIKTOK_STREAM_URL",
51
+ defaultValue: "",
52
+ secret: true,
53
+ description:
54
+ "Full TikTok RTMP URL from TikTok Studio (leave empty to skip TikTok relay)",
55
+ required: false,
56
+ },
57
+ {
58
+ key: "TELEGRAM_STREAM_URL",
59
+ defaultValue: "",
60
+ secret: true,
61
+ description:
62
+ "Full Telegram RTMPS URL including stream key (leave empty to skip Telegram relay)",
63
+ required: false,
64
+ },
65
+ ],
66
+ healthcheck: {
67
+ test: "curl -sf http://localhost:8080/health || exit 1",
68
+ interval: "15s",
69
+ timeout: "5s",
70
+ retries: 3,
71
+ },
72
+ dependsOn: [],
73
+ restartPolicy: "unless-stopped",
74
+ networks: ["openclaw-network"],
75
+
76
+ deploy: {
77
+ resources: {
78
+ limits: { cpus: "2.0", memory: "2G" },
79
+ reservations: { cpus: "0.5", memory: "512M" },
80
+ },
81
+ },
82
+
83
+ skills: [],
84
+ openclawEnvVars: [
85
+ {
86
+ key: "STREAM_GATEWAY_HOST",
87
+ defaultValue: "stream-gateway",
88
+ secret: false,
89
+ description: "Hostname of the stream-gateway container",
90
+ required: true,
91
+ },
92
+ {
93
+ key: "STREAM_GATEWAY_RTMP_PORT",
94
+ defaultValue: "1935",
95
+ secret: false,
96
+ description: "RTMP ingest port on the stream-gateway",
97
+ required: true,
98
+ },
99
+ {
100
+ key: "STREAM_GATEWAY_HLS_PORT",
101
+ defaultValue: "8080",
102
+ secret: false,
103
+ description: "HTTP port for HLS preview on the stream-gateway",
104
+ required: true,
105
+ },
106
+ ],
107
+
108
+ docsUrl: "https://github.com/tiangolo/nginx-rtmp-docker",
109
+ tags: [
110
+ "streaming",
111
+ "rtmp",
112
+ "hls",
113
+ "relay",
114
+ "youtube",
115
+ "twitch",
116
+ "tiktok",
117
+ "telegram",
118
+ "obs",
119
+ "nginx",
120
+ ],
121
+ maturity: "experimental",
122
+
123
+ requires: [],
124
+ recommends: ["desktop-environment"],
125
+ conflictsWith: [],
126
+
127
+ removalWarning:
128
+ "⚠️ STREAMING KEYS REQUIRED: To relay to platforms you must provide at least one stream key. Without any keys configured the gateway will still accept RTMP input and serve HLS locally but nothing will be forwarded.",
129
+ minMemoryMB: 512,
130
+ gpuRequired: false,
131
+
132
+ nativeSupported: true,
133
+ nativeRecipes: [
134
+ {
135
+ platform: "linux",
136
+ installSteps: [
137
+ "command -v nginx >/dev/null 2>&1 || (command -v apt-get >/dev/null 2>&1 && sudo apt-get update -qq && sudo apt-get install -y -qq nginx libnginx-mod-rtmp ffmpeg)",
138
+ "command -v nginx >/dev/null 2>&1 || (command -v dnf >/dev/null 2>&1 && sudo dnf install -y nginx nginx-mod-rtmp ffmpeg)",
139
+ ],
140
+ startCommand: "sudo systemctl start nginx",
141
+ stopCommand: "sudo systemctl stop nginx",
142
+ configPath: "/etc/nginx/nginx.conf",
143
+ configTemplate:
144
+ '# Generated for OpenClaw bare-metal\nworker_processes auto;\nrtmp_auto_push on;\n\nevents { worker_connections 1024; }\n\nrtmp {\n server {\n listen 1935;\n chunk_size 4096;\n application live {\n live on;\n record off;\n hls on;\n hls_path /tmp/hls;\n hls_fragment 3;\n hls_playlist_length 60;\n }\n }\n}\n\nhttp {\n server {\n listen 8080;\n location /hls { types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } root /tmp; add_header Cache-Control no-cache; }\n location /health { return 200 "OK"; }\n }\n}\n',
145
+ systemdUnit: "nginx",
146
+ },
147
+ ],
148
+ };
@@ -38,8 +38,6 @@ export const traefikDefinition: ServiceDefinition = {
38
38
  },
39
39
  ],
40
40
  environment: [],
41
- command:
42
- "--api.dashboard=true --providers.docker=true --entrypoints.web.address=:80 --entrypoints.websecure.address=:443",
43
41
  dependsOn: [],
44
42
  restartPolicy: "unless-stopped",
45
43
  networks: ["openclaw-network"],
@@ -0,0 +1,94 @@
1
+ import type { ServiceDefinition } from "../../types.js";
2
+
3
+ export const xyopsDefinition: ServiceDefinition = {
4
+ id: "xyops",
5
+ name: "xyOps",
6
+ description:
7
+ "Job scheduling, workflow automation, server monitoring, alerting, and incident response platform with a visual workflow editor and fleet management.",
8
+ category: "automation",
9
+ icon: "⚙️",
10
+
11
+ image: "ghcr.io/pixlcore/xyops",
12
+ imageTag: "latest",
13
+ ports: [
14
+ {
15
+ host: 5522,
16
+ container: 5522,
17
+ description: "xyOps web interface",
18
+ exposed: true,
19
+ },
20
+ {
21
+ host: 5523,
22
+ container: 5523,
23
+ description: "xyOps secondary service",
24
+ exposed: false,
25
+ },
26
+ ],
27
+ volumes: [
28
+ {
29
+ name: "xyops-data",
30
+ containerPath: "/opt/xyops/data",
31
+ description: "Persistent xyOps data and configuration",
32
+ },
33
+ {
34
+ name: "/var/run/docker.sock",
35
+ containerPath: "/var/run/docker.sock",
36
+ description: "Docker socket for container management",
37
+ },
38
+ ],
39
+ environment: [
40
+ {
41
+ key: "TZ",
42
+ defaultValue: "UTC",
43
+ secret: false,
44
+ description: "Timezone for xyOps",
45
+ required: false,
46
+ },
47
+ {
48
+ key: "XYOPS_xysat_local",
49
+ defaultValue: "1",
50
+ secret: false,
51
+ description: "Enable local satellite mode for monitoring the host",
52
+ required: false,
53
+ },
54
+ ],
55
+ healthcheck: {
56
+ test: "wget -q --spider http://localhost:5522 || exit 1",
57
+ interval: "30s",
58
+ timeout: "10s",
59
+ retries: 3,
60
+ startPeriod: "30s",
61
+ },
62
+ dependsOn: [],
63
+ restartPolicy: "unless-stopped",
64
+ networks: ["openclaw-network"],
65
+
66
+ skills: [],
67
+ openclawEnvVars: [
68
+ {
69
+ key: "XYOPS_HOST",
70
+ defaultValue: "xyops",
71
+ secret: false,
72
+ description: "xyOps hostname for OpenClaw",
73
+ required: true,
74
+ },
75
+ {
76
+ key: "XYOPS_PORT",
77
+ defaultValue: "5522",
78
+ secret: false,
79
+ description: "xyOps port for OpenClaw",
80
+ required: true,
81
+ },
82
+ ],
83
+
84
+ docsUrl: "https://github.com/pixlcore/xyops",
85
+ tags: ["scheduling", "automation", "monitoring", "alerting", "incident-response", "workflow"],
86
+ maturity: "stable",
87
+
88
+ requires: [],
89
+ recommends: [],
90
+ conflictsWith: [],
91
+
92
+ minMemoryMB: 256,
93
+ gpuRequired: false,
94
+ };
package/src/types.ts CHANGED
@@ -58,7 +58,10 @@ export type SkillPack = z.infer<typeof SkillPackSchema>;
58
58
  export type Preset = z.infer<typeof PresetSchema>;
59
59
 
60
60
  export type GenerationInput = z.infer<typeof GenerationInputSchema>;
61
- export type ComposeOptions = z.infer<typeof ComposeOptionsSchema>;
61
+ export type ComposeOptions = z.infer<typeof ComposeOptionsSchema> & {
62
+ /** Dynamic Traefik labels per service, computed by the Traefik generator. */
63
+ traefikLabels?: Map<string, Record<string, string>>;
64
+ };
62
65
  export type ResolvedService = z.infer<typeof ResolvedServiceSchema>;
63
66
  export type AddedDependency = z.infer<typeof AddedDependencySchema>;
64
67
  export type Warning = z.infer<typeof WarningSchema>;
@@ -78,6 +81,7 @@ export interface ResolverInput {
78
81
  gpu?: boolean;
79
82
  platform?: Platform;
80
83
  monitoring?: boolean;
84
+ memoryThresholds?: { info: number; warning: number; critical: number };
81
85
  }
82
86
 
83
87
  export interface GeneratedFiles {
@@ -120,4 +124,6 @@ export const SERVICE_CATEGORIES: CategoryInfo[] = [
120
124
  { id: "browser", name: "Browser Automation", icon: "🌐" },
121
125
  { id: "search", name: "Search", icon: "🔍" },
122
126
  { id: "communication", name: "Notifications", icon: "🔔" },
127
+ { id: "desktop", name: "Desktop Environment", icon: "🖥️" },
128
+ { id: "streaming", name: "Streaming & Relay", icon: "📺" },
123
129
  ];
package/src/validator.ts CHANGED
@@ -93,6 +93,22 @@ function checkEnvCompleteness(
93
93
  message: `Secret "${envVar.key}" for "${svc.definition.name}" needs to be configured manually`,
94
94
  });
95
95
  }
96
+ if (envVar.validation && envVar.defaultValue) {
97
+ try {
98
+ const regex = new RegExp(envVar.validation);
99
+ if (!regex.test(envVar.defaultValue)) {
100
+ warnings.push({
101
+ type: "env_validation",
102
+ message: `Environment variable "${envVar.key}" for "${svc.definition.name}" default value does not match validation pattern: ${envVar.validation}`,
103
+ });
104
+ }
105
+ } catch {
106
+ warnings.push({
107
+ type: "env_validation",
108
+ message: `Environment variable "${envVar.key}" for "${svc.definition.name}" has invalid validation regex: ${envVar.validation}`,
109
+ });
110
+ }
111
+ }
96
112
  }
97
113
  }
98
114
  }
@@ -0,0 +1,134 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolve } from "./resolver.js";
3
+ import { getAllServices, getServiceById } from "./services/registry.js";
4
+ import { checkCompatibility, getImageReference, getImageTag, pinImageTags } from "./version-manager.js";
5
+
6
+ describe("getImageTag", () => {
7
+ it("returns the tag for a known service", () => {
8
+ const tag = getImageTag("redis");
9
+ expect(tag).toBeDefined();
10
+ expect(typeof tag).toBe("string");
11
+ });
12
+
13
+ it("returns undefined for an unknown service", () => {
14
+ expect(getImageTag("nonexistent-service-xyz")).toBeUndefined();
15
+ });
16
+ });
17
+
18
+ describe("getImageReference", () => {
19
+ it("returns image:tag for a known service", () => {
20
+ const ref = getImageReference("redis");
21
+ expect(ref).toBeDefined();
22
+ expect(ref).toContain(":");
23
+ expect(ref).toMatch(/^.+:.+$/);
24
+ });
25
+
26
+ it("returns undefined for an unknown service", () => {
27
+ expect(getImageReference("nonexistent-service-xyz")).toBeUndefined();
28
+ });
29
+ });
30
+
31
+ describe("pinImageTags", () => {
32
+ it("pins image tags from the registry for all services", () => {
33
+ const resolved = resolve({
34
+ services: ["redis", "postgresql"],
35
+ skillPacks: [],
36
+ proxy: "none",
37
+ gpu: false,
38
+ platform: "linux/amd64",
39
+ });
40
+
41
+ const pinned = pinImageTags(resolved);
42
+
43
+ expect(pinned.services).toHaveLength(resolved.services.length);
44
+
45
+ for (const svc of pinned.services) {
46
+ expect(svc.definition.imageTag).toBeDefined();
47
+ expect(typeof svc.definition.imageTag).toBe("string");
48
+ expect(svc.definition.imageTag.length).toBeGreaterThan(0);
49
+ }
50
+ });
51
+
52
+ it("does not mutate the original resolved output", () => {
53
+ const resolved = resolve({
54
+ services: ["redis"],
55
+ skillPacks: [],
56
+ proxy: "none",
57
+ gpu: false,
58
+ platform: "linux/amd64",
59
+ });
60
+
61
+ const originalTag = resolved.services[0]?.definition.imageTag;
62
+ pinImageTags(resolved);
63
+
64
+ expect(resolved.services[0]?.definition.imageTag).toBe(originalTag);
65
+ });
66
+
67
+ it("preserves non-tag properties", () => {
68
+ const resolved = resolve({
69
+ services: ["redis"],
70
+ skillPacks: [],
71
+ proxy: "none",
72
+ gpu: false,
73
+ platform: "linux/amd64",
74
+ });
75
+
76
+ const pinned = pinImageTags(resolved);
77
+ expect(pinned.services[0]?.definition.id).toBe("redis");
78
+ expect(pinned.services[0]?.definition.name).toBe("Redis");
79
+ expect(pinned.estimatedMemoryMB).toBe(resolved.estimatedMemoryMB);
80
+ });
81
+ });
82
+
83
+ describe("checkCompatibility", () => {
84
+ it("warns when Redis and Valkey are both selected", () => {
85
+ const all = getAllServices();
86
+ const redis = all.find((s) => s.id === "redis");
87
+ const valkey = all.find((s) => s.id === "valkey");
88
+
89
+ if (redis && valkey) {
90
+ const warnings = checkCompatibility([redis, valkey]);
91
+ expect(warnings.some((w) => w.message.includes("Redis") && w.message.includes("Valkey"))).toBe(true);
92
+ }
93
+ });
94
+
95
+ it("warns when Caddy and Traefik are both selected", () => {
96
+ const all = getAllServices();
97
+ const caddy = all.find((s) => s.id === "caddy");
98
+ const traefik = all.find((s) => s.id === "traefik");
99
+
100
+ if (caddy && traefik) {
101
+ const warnings = checkCompatibility([caddy, traefik]);
102
+ expect(warnings.some((w) => w.message.includes("Caddy") && w.message.includes("Traefik"))).toBe(true);
103
+ }
104
+ });
105
+
106
+ it("warns about multiple vector databases", () => {
107
+ const all = getAllServices();
108
+ const qdrant = all.find((s) => s.id === "qdrant");
109
+ const chromadb = all.find((s) => s.id === "chromadb");
110
+
111
+ if (qdrant && chromadb) {
112
+ const warnings = checkCompatibility([qdrant, chromadb]);
113
+ expect(warnings.some((w) => w.message.includes("vector database"))).toBe(true);
114
+ }
115
+ });
116
+
117
+ it("warns about GPU services", () => {
118
+ const all = getAllServices();
119
+ const gpuService = all.find((s) => s.gpuRequired);
120
+
121
+ if (gpuService) {
122
+ const warnings = checkCompatibility([gpuService]);
123
+ expect(warnings.some((w) => w.message.includes("GPU"))).toBe(true);
124
+ }
125
+ });
126
+
127
+ it("returns no warnings for a single non-conflicting service", () => {
128
+ const redis = getServiceById("redis");
129
+ if (redis) {
130
+ const warnings = checkCompatibility([redis]);
131
+ expect(warnings.filter((w) => w.type === "compatibility" && !w.message.includes("GPU"))).toHaveLength(0);
132
+ }
133
+ });
134
+ });
@@ -14,14 +14,21 @@ export function getImageReference(serviceId: string): string | undefined {
14
14
  return `${svc.image}:${svc.imageTag}`;
15
15
  }
16
16
 
17
- /** Pin all service image tags in a resolved output (returns a copy) */
17
+ /** Pin all service image tags to the registry-defined versions (returns a copy) */
18
18
  export function pinImageTags(resolved: ResolverOutput): ResolverOutput {
19
19
  return {
20
20
  ...resolved,
21
- services: resolved.services.map((s) => ({
22
- ...s,
23
- definition: { ...s.definition },
24
- })),
21
+ services: resolved.services.map((s) => {
22
+ const registryDef = getServiceById(s.definition.id);
23
+ const pinnedTag = registryDef?.imageTag ?? s.definition.imageTag;
24
+ return {
25
+ ...s,
26
+ definition: {
27
+ ...s.definition,
28
+ imageTag: pinnedTag,
29
+ },
30
+ };
31
+ }),
25
32
  };
26
33
  }
27
34