@vellumai/cli 0.7.0 → 0.7.2

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 (54) hide show
  1. package/AGENTS.md +3 -11
  2. package/README.md +49 -0
  3. package/bun.lock +0 -15
  4. package/package.json +1 -6
  5. package/src/__tests__/backup.test.ts +591 -0
  6. package/src/__tests__/config-utils.test.ts +35 -48
  7. package/src/__tests__/teleport.test.ts +597 -37
  8. package/src/commands/backup.ts +149 -70
  9. package/src/commands/client.ts +56 -14
  10. package/src/commands/events.ts +3 -0
  11. package/src/commands/exec.ts +34 -12
  12. package/src/commands/hatch.ts +3 -7
  13. package/src/commands/login.ts +15 -33
  14. package/src/commands/logs.ts +2 -7
  15. package/src/commands/ps.ts +41 -6
  16. package/src/commands/restore.ts +32 -47
  17. package/src/commands/setup.ts +38 -73
  18. package/src/commands/ssh.ts +2 -5
  19. package/src/commands/teleport.ts +148 -34
  20. package/src/commands/tunnel.ts +2 -7
  21. package/src/commands/upgrade.ts +114 -7
  22. package/src/commands/wake.ts +5 -16
  23. package/src/components/DefaultMainScreen.tsx +65 -129
  24. package/src/index.ts +2 -13
  25. package/src/lib/__tests__/docker.test.ts +50 -32
  26. package/src/lib/__tests__/local-runtime-client.test.ts +308 -25
  27. package/src/lib/__tests__/platform-client-signed-url.test.ts +237 -2
  28. package/src/lib/__tests__/runtime-url.test.ts +125 -0
  29. package/src/lib/__tests__/terminal-session.test.ts +202 -0
  30. package/src/lib/assistant-client.ts +18 -26
  31. package/src/lib/assistant-config.ts +34 -41
  32. package/src/lib/backup-ops.ts +43 -17
  33. package/src/lib/cli-error.ts +1 -0
  34. package/src/lib/client-identity.ts +1 -1
  35. package/src/lib/config-utils.ts +1 -97
  36. package/src/lib/docker-statefulset.ts +381 -0
  37. package/src/lib/docker.ts +8 -247
  38. package/src/lib/guardian-token.ts +56 -6
  39. package/src/lib/hatch-local.ts +3 -26
  40. package/src/lib/job-polling.ts +1 -1
  41. package/src/lib/local-runtime-client.ts +162 -28
  42. package/src/lib/local.ts +35 -64
  43. package/src/lib/ngrok.ts +36 -26
  44. package/src/lib/platform-client.ts +97 -221
  45. package/src/lib/platform-releases.ts +23 -0
  46. package/src/lib/retire-local.ts +2 -2
  47. package/src/lib/runtime-url.ts +52 -0
  48. package/src/lib/sync-cloud-assistants.ts +126 -0
  49. package/src/lib/terminal-client.ts +6 -1
  50. package/src/lib/terminal-session.ts +127 -48
  51. package/src/lib/tui-log.ts +60 -0
  52. package/src/lib/upgrade-lifecycle.ts +65 -0
  53. package/src/lib/xdg-log.ts +10 -4
  54. package/src/commands/pair.ts +0 -212
@@ -2,11 +2,6 @@ import { writeFileSync } from "fs";
2
2
  import { tmpdir } from "os";
3
3
  import { join } from "path";
4
4
 
5
- const ANTHROPIC_PROVIDER = "anthropic";
6
- const ANTHROPIC_DEFAULT_MODEL = "claude-opus-4-7";
7
- const MAIN_AGENT_OPUS_MODEL = "claude-opus-4-7";
8
- const MAIN_AGENT_OPUS_MAX_TOKENS = 32000;
9
-
10
5
  /**
11
6
  * Convert flat dot-notation key=value pairs into a nested config object.
12
7
  *
@@ -37,20 +32,6 @@ export function buildNestedConfig(
37
32
  return config;
38
33
  }
39
34
 
40
- /**
41
- * Build the first-boot workspace config overlay passed to the assistant during
42
- * hatch. Anthropic onboarding sets `llm.default.model` to Sonnet so background
43
- * fallback work stays cheaper, while the main conversation thread should remain
44
- * on Opus via the same call-site override seeded by workspace migration 050.
45
- */
46
- export function buildInitialConfig(
47
- configValues: Record<string, string>,
48
- ): Record<string, unknown> {
49
- const config = buildNestedConfig(configValues);
50
- seedAnthropicMainAgentCallSite(config);
51
- return config;
52
- }
53
-
54
35
  /**
55
36
  * Write arbitrary key-value pairs to a temporary JSON file and return its
56
37
  * path. The caller passes this path to the daemon via the
@@ -68,7 +49,7 @@ export function writeInitialConfig(
68
49
  ): string | undefined {
69
50
  if (Object.keys(configValues).length === 0) return undefined;
70
51
 
71
- const config = buildInitialConfig(configValues);
52
+ const config = buildNestedConfig(configValues);
72
53
  const tempPath = join(
73
54
  tmpdir(),
74
55
  `vellum-default-workspace-config-${process.pid}-${Date.now()}.json`,
@@ -76,80 +57,3 @@ export function writeInitialConfig(
76
57
  writeFileSync(tempPath, JSON.stringify(config, null, 2) + "\n");
77
58
  return tempPath;
78
59
  }
79
-
80
- function seedAnthropicMainAgentCallSite(config: Record<string, unknown>): void {
81
- const llm = ensureObject(config, "llm");
82
-
83
- const existingCallSites = readObject(llm.callSites);
84
- if (existingCallSites !== null && "mainAgent" in existingCallSites) return;
85
-
86
- const { provider, model } = resolveInitialMainAgentBaseSelection(llm);
87
- if (provider !== ANTHROPIC_PROVIDER) return;
88
-
89
- if (
90
- model !== undefined &&
91
- model !== ANTHROPIC_DEFAULT_MODEL &&
92
- model !== MAIN_AGENT_OPUS_MODEL
93
- ) {
94
- return;
95
- }
96
-
97
- const callSites = ensureObject(llm, "callSites");
98
-
99
- callSites.mainAgent = {
100
- model: MAIN_AGENT_OPUS_MODEL,
101
- maxTokens: MAIN_AGENT_OPUS_MAX_TOKENS,
102
- };
103
- }
104
-
105
- function resolveInitialMainAgentBaseSelection(llm: Record<string, unknown>): {
106
- provider: string;
107
- model?: string;
108
- } {
109
- const defaultBlock = readObject(llm.default);
110
- let provider = readString(defaultBlock?.provider) ?? ANTHROPIC_PROVIDER;
111
- let model = readString(defaultBlock?.model);
112
-
113
- const profiles = readObject(llm.profiles);
114
- const activeProfileName = readString(llm.activeProfile);
115
- const activeProfile =
116
- profiles !== null && activeProfileName !== undefined
117
- ? readObject(profiles[activeProfileName])
118
- : null;
119
-
120
- if (activeProfile !== null) {
121
- provider = readString(activeProfile.provider) ?? provider;
122
- model = readString(activeProfile.model) ?? model;
123
- }
124
-
125
- return model === undefined ? { provider } : { provider, model };
126
- }
127
-
128
- function ensureObject(
129
- parent: Record<string, unknown>,
130
- key: string,
131
- ): Record<string, unknown> {
132
- const existing = parent[key];
133
- if (
134
- existing != null &&
135
- typeof existing === "object" &&
136
- !Array.isArray(existing)
137
- ) {
138
- return existing as Record<string, unknown>;
139
- }
140
-
141
- const next: Record<string, unknown> = {};
142
- parent[key] = next;
143
- return next;
144
- }
145
-
146
- function readObject(value: unknown): Record<string, unknown> | null {
147
- if (value == null || typeof value !== "object" || Array.isArray(value)) {
148
- return null;
149
- }
150
- return value as Record<string, unknown>;
151
- }
152
-
153
- function readString(value: unknown): string | undefined {
154
- return typeof value === "string" && value.length > 0 ? value : undefined;
155
- }
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Declarative StatefulSet spec for the Docker service group.
3
+ *
4
+ * Mirrors the schema of the platform's `stateful_template.yaml` (vembda).
5
+ * Container names, volume names, and env var names are kept in sync with
6
+ * the K8s template so the two topologies are auditable side-by-side.
7
+ *
8
+ * This file is self-contained — it does not import from `docker.ts` to
9
+ * avoid a circular dependency. Constants are inlined from their definitions
10
+ * in `docker.ts` and must be kept in sync if those change.
11
+ */
12
+
13
+ import { existsSync } from "fs";
14
+
15
+ import { PROVIDER_ENV_VAR_NAMES } from "../shared/provider-env-vars.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Constants (mirrored from docker.ts — keep in sync)
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const GATEWAY_INTERNAL_PORT = 7830;
22
+ const ASSISTANT_INTERNAL_PORT = 7821;
23
+ const AVATAR_DEVICE_ENV_VAR = "VELLUM_AVATAR_DEVICE";
24
+
25
+ /** Logical service name used throughout the CLI. */
26
+ export type ServiceName = "assistant" | "gateway" | "credential-executor";
27
+
28
+ /**
29
+ /**
30
+ * The four fields from `dockerResourceNames()` that the builder actually uses.
31
+ * Container/network names come from here; volume names are generated by the
32
+ * `volumeClaimTemplates` in the spec. `ReturnType<typeof dockerResourceNames>`
33
+ * structurally satisfies this interface (it has all these fields plus more).
34
+ */
35
+ export interface DockerResourceNames {
36
+ assistantContainer: string;
37
+ cesContainer: string;
38
+ gatewayContainer: string;
39
+ network: string;
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Types
44
+ // ---------------------------------------------------------------------------
45
+
46
+ /** A static env var whose value is known at spec-definition time. */
47
+ interface StaticEnv {
48
+ kind: "static";
49
+ name: string;
50
+ value: string;
51
+ }
52
+
53
+ /**
54
+ * An env var whose value comes from the opts object passed to
55
+ * `buildServiceRunArgs` at container-start time.
56
+ */
57
+ interface SecretEnv {
58
+ kind: "secret";
59
+ name: string;
60
+ /** Key in `DockerRunSecrets`. */
61
+ secret: keyof DockerRunSecrets;
62
+ }
63
+
64
+ /**
65
+ * An env var conditionally forwarded from the host process environment.
66
+ * Only emitted when `process.env[hostVar]` is set.
67
+ * Defaults to `name` for `hostVar` when omitted.
68
+ */
69
+ interface HostEnv {
70
+ kind: "host";
71
+ name: string;
72
+ hostVar?: string;
73
+ }
74
+
75
+ type EnvEntry = StaticEnv | SecretEnv | HostEnv;
76
+
77
+ interface VolumeMount {
78
+ /** Logical volume name — maps to a Docker volume via `DockerVolumeClaimTemplate`. */
79
+ volumeName: string;
80
+ mountPath: string;
81
+ readOnly?: boolean;
82
+ }
83
+
84
+ interface PortSpec {
85
+ containerPort: number;
86
+ /**
87
+ * Host-side port. Literal string = use as-is. `"{{ gatewayPort }}"` is
88
+ * a sentinel replaced with the instance-specific gateway port at build time.
89
+ */
90
+ hostPort?: string;
91
+ description?: string;
92
+ }
93
+
94
+ export interface DockerContainerSpec {
95
+ /** K8s-style container name (matches `stateful_template.yaml`). */
96
+ name: string;
97
+ /** Internal CLI `ServiceName` key used by `SERVICE_START_ORDER`. */
98
+ internalName: ServiceName;
99
+ /**
100
+ * Network mode:
101
+ * - "bridge": owns the network (assistant only — gateway port published here).
102
+ * - "container": share the assistant container's network namespace.
103
+ */
104
+ network: "bridge" | "container";
105
+ ports?: PortSpec[];
106
+ env: EnvEntry[];
107
+ volumeMounts: VolumeMount[];
108
+ }
109
+
110
+ export interface DockerVolumeClaimTemplate {
111
+ name: string;
112
+ dockerVolume: (instanceName: string) => string;
113
+ }
114
+
115
+ export interface DockerStatefulSetSpec {
116
+ containers: DockerContainerSpec[];
117
+ startOrder: ServiceName[];
118
+ volumeClaimTemplates: DockerVolumeClaimTemplate[];
119
+ readiness: {
120
+ endpoint: string;
121
+ timeoutMs: number;
122
+ intervalMs: number;
123
+ };
124
+ }
125
+
126
+ /** Secrets injected at container-start time (from hatch / upgrade opts). */
127
+ export interface DockerRunSecrets {
128
+ signingKey?: string;
129
+ bootstrapSecret?: string;
130
+ cesServiceToken?: string;
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Spec
135
+ // ---------------------------------------------------------------------------
136
+
137
+ export const DOCKER_STATEFUL_SET_SPEC: DockerStatefulSetSpec = {
138
+ startOrder: ["assistant", "gateway", "credential-executor"],
139
+
140
+ readiness: {
141
+ endpoint: "/readyz",
142
+ timeoutMs: 180_000,
143
+ intervalMs: 1_000,
144
+ },
145
+
146
+ volumeClaimTemplates: [
147
+ { name: "assistant-workspace", dockerVolume: (n) => `${n}-workspace` },
148
+ { name: "ces-bootstrap-socket", dockerVolume: (n) => `${n}-socket` },
149
+ { name: "assistant-ipc-socket", dockerVolume: (n) => `${n}-assistant-ipc` },
150
+ { name: "gateway-ipc-socket", dockerVolume: (n) => `${n}-gateway-ipc` },
151
+ { name: "gateway-security", dockerVolume: (n) => `${n}-gateway-sec` },
152
+ { name: "ces-security", dockerVolume: (n) => `${n}-ces-sec` },
153
+ ],
154
+
155
+ containers: [
156
+ {
157
+ name: "assistant-container",
158
+ internalName: "assistant",
159
+ network: "bridge",
160
+ ports: [
161
+ {
162
+ containerPort: GATEWAY_INTERNAL_PORT,
163
+ hostPort: "{{ gatewayPort }}",
164
+ description: "Gateway reverse-proxy (published to host)",
165
+ },
166
+ {
167
+ containerPort: ASSISTANT_INTERNAL_PORT,
168
+ hostPort: `${ASSISTANT_INTERNAL_PORT}`,
169
+ description: "Assistant HTTP API",
170
+ },
171
+ ],
172
+ env: [
173
+ { kind: "static", name: "IS_CONTAINERIZED", value: "true" },
174
+ { kind: "static", name: "DEBUG_STDOUT_LOGS", value: "1" },
175
+ { kind: "static", name: "VELLUM_CLOUD", value: "docker" },
176
+ { kind: "static", name: "RUNTIME_HTTP_HOST", value: "0.0.0.0" },
177
+ { kind: "static", name: "VELLUM_WORKSPACE_DIR", value: "/workspace" },
178
+ { kind: "static", name: "VELLUM_BACKUP_DIR", value: "/workspace/.backups" },
179
+ { kind: "static", name: "VELLUM_BACKUP_KEY_PATH", value: "/workspace/.backup.key" },
180
+ { kind: "static", name: "CES_CREDENTIAL_URL", value: "http://localhost:8090" },
181
+ { kind: "static", name: "GATEWAY_IPC_SOCKET_DIR", value: "/run/gateway-ipc" },
182
+ { kind: "static", name: "ASSISTANT_IPC_SOCKET_DIR", value: "/run/assistant-ipc" },
183
+ { kind: "secret", name: "CES_SERVICE_TOKEN", secret: "cesServiceToken" },
184
+ { kind: "secret", name: "ACTOR_TOKEN_SIGNING_KEY", secret: "signingKey" },
185
+ { kind: "secret", name: "GUARDIAN_BOOTSTRAP_SECRET", secret: "bootstrapSecret" },
186
+ // Provider API keys — forwarded from host if set
187
+ ...Object.values(PROVIDER_ENV_VAR_NAMES).map(
188
+ (v): HostEnv => ({ kind: "host", name: v }),
189
+ ),
190
+ { kind: "host", name: "VELLUM_ENVIRONMENT" },
191
+ { kind: "host", name: "VELLUM_PLATFORM_URL" },
192
+ ],
193
+ volumeMounts: [
194
+ { volumeName: "assistant-workspace", mountPath: "/workspace" },
195
+ { volumeName: "ces-bootstrap-socket", mountPath: "/run/ces-bootstrap" },
196
+ { volumeName: "assistant-ipc-socket", mountPath: "/run/assistant-ipc" },
197
+ { volumeName: "gateway-ipc-socket", mountPath: "/run/gateway-ipc" },
198
+ ],
199
+ },
200
+
201
+ {
202
+ name: "gateway-sidecar",
203
+ internalName: "gateway",
204
+ network: "container",
205
+ env: [
206
+ { kind: "static", name: "VELLUM_WORKSPACE_DIR", value: "/workspace" },
207
+ { kind: "static", name: "GATEWAY_SECURITY_DIR", value: "/gateway-security" },
208
+ { kind: "static", name: "ASSISTANT_HOST", value: "localhost" },
209
+ { kind: "static", name: "CES_CREDENTIAL_URL", value: "http://localhost:8090" },
210
+ { kind: "static", name: "GATEWAY_IPC_SOCKET_DIR", value: "/run/gateway-ipc" },
211
+ { kind: "static", name: "ASSISTANT_IPC_SOCKET_DIR", value: "/run/assistant-ipc" },
212
+ { kind: "static", name: "GATEWAY_PORT", value: `${GATEWAY_INTERNAL_PORT}` },
213
+ { kind: "static", name: "RUNTIME_HTTP_PORT", value: `${ASSISTANT_INTERNAL_PORT}` },
214
+ { kind: "secret", name: "CES_SERVICE_TOKEN", secret: "cesServiceToken" },
215
+ { kind: "secret", name: "ACTOR_TOKEN_SIGNING_KEY", secret: "signingKey" },
216
+ { kind: "secret", name: "GUARDIAN_BOOTSTRAP_SECRET", secret: "bootstrapSecret" },
217
+ { kind: "host", name: "VELLUM_ENVIRONMENT" },
218
+ { kind: "host", name: "VELLUM_PLATFORM_URL" },
219
+ { kind: "host", name: "VELAY_BASE_URL" },
220
+ ],
221
+ volumeMounts: [
222
+ { volumeName: "assistant-workspace", mountPath: "/workspace" },
223
+ { volumeName: "gateway-security", mountPath: "/gateway-security" },
224
+ { volumeName: "assistant-ipc-socket", mountPath: "/run/assistant-ipc" },
225
+ { volumeName: "gateway-ipc-socket", mountPath: "/run/gateway-ipc" },
226
+ ],
227
+ },
228
+
229
+ {
230
+ name: "credential-executor-sidecar",
231
+ internalName: "credential-executor",
232
+ network: "container",
233
+ env: [
234
+ { kind: "static", name: "CES_MODE", value: "managed" },
235
+ { kind: "static", name: "VELLUM_WORKSPACE_DIR", value: "/workspace" },
236
+ { kind: "static", name: "CES_BOOTSTRAP_SOCKET_DIR", value: "/run/ces-bootstrap" },
237
+ { kind: "static", name: "CREDENTIAL_SECURITY_DIR", value: "/ces-security" },
238
+ { kind: "secret", name: "CES_SERVICE_TOKEN", secret: "cesServiceToken" },
239
+ ],
240
+ volumeMounts: [
241
+ { volumeName: "assistant-workspace", mountPath: "/workspace", readOnly: true },
242
+ { volumeName: "ces-bootstrap-socket", mountPath: "/run/ces-bootstrap" },
243
+ { volumeName: "ces-security", mountPath: "/ces-security" },
244
+ ],
245
+ },
246
+ ],
247
+ };
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Builder
251
+ // ---------------------------------------------------------------------------
252
+
253
+ export interface BuildServiceRunArgsOpts extends DockerRunSecrets {
254
+ gatewayPort: number;
255
+ imageTags: Record<ServiceName, string>;
256
+ instanceName: string;
257
+ res: DockerResourceNames;
258
+ extraAssistantEnv?: Record<string, string>;
259
+ defaultWorkspaceConfigPath?: string;
260
+ /** Avatar device path, if available. Injected by `docker.ts` after resolving. */
261
+ avatarDevicePath?: string;
262
+ }
263
+
264
+ function resolveVolume(
265
+ spec: DockerStatefulSetSpec,
266
+ instanceName: string,
267
+ volumeName: string,
268
+ ): string {
269
+ const claim = spec.volumeClaimTemplates.find((v) => v.name === volumeName);
270
+ if (!claim) throw new Error(`docker-statefulset: unknown volume "${volumeName}"`);
271
+ return claim.dockerVolume(instanceName);
272
+ }
273
+
274
+ /**
275
+ * Build `docker run` argument arrays for each container in the StatefulSet.
276
+ * Returns `Record<ServiceName, () => string[]>` — callers evaluate lazily.
277
+ */
278
+ export function buildServiceRunArgs(
279
+ opts: BuildServiceRunArgsOpts,
280
+ spec = DOCKER_STATEFUL_SET_SPEC,
281
+ ): Record<ServiceName, () => string[]> {
282
+ const {
283
+ gatewayPort,
284
+ imageTags,
285
+ instanceName,
286
+ res,
287
+ extraAssistantEnv,
288
+ defaultWorkspaceConfigPath,
289
+ avatarDevicePath,
290
+ } = opts;
291
+
292
+ const result = {} as Record<ServiceName, () => string[]>;
293
+
294
+ for (const container of spec.containers) {
295
+ const svc = container.internalName;
296
+
297
+ result[svc] = () => {
298
+ const args: string[] = ["run", "--init", "-d"];
299
+
300
+ // Container name
301
+ const containerName =
302
+ svc === "assistant"
303
+ ? res.assistantContainer
304
+ : svc === "gateway"
305
+ ? res.gatewayContainer
306
+ : res.cesContainer;
307
+ args.push("--name", containerName);
308
+
309
+ // Network
310
+ if (container.network === "bridge") {
311
+ args.push(`--network=${res.network}`);
312
+ for (const port of container.ports ?? []) {
313
+ const hostSide =
314
+ port.hostPort === "{{ gatewayPort }}"
315
+ ? `${gatewayPort}`
316
+ : port.hostPort;
317
+ if (hostSide !== undefined) {
318
+ args.push("-p", `${hostSide}:${port.containerPort}`);
319
+ }
320
+ }
321
+ } else {
322
+ args.push(`--network=container:${res.assistantContainer}`);
323
+ }
324
+
325
+ // Volume mounts
326
+ for (const mount of container.volumeMounts) {
327
+ const vol = resolveVolume(spec, instanceName, mount.volumeName);
328
+ args.push("-v", mount.readOnly ? `${vol}:${mount.mountPath}:ro` : `${vol}:${mount.mountPath}`);
329
+ }
330
+
331
+ // Env vars from spec
332
+ for (const entry of container.env) {
333
+ if (entry.kind === "static") {
334
+ args.push("-e", `${entry.name}=${entry.value}`);
335
+ } else if (entry.kind === "secret") {
336
+ const val = opts[entry.secret];
337
+ if (val) args.push("-e", `${entry.name}=${val}`);
338
+ } else {
339
+ const hostVar = entry.hostVar ?? entry.name;
340
+ const val = process.env[hostVar];
341
+ if (val) args.push("-e", `${entry.name}=${val}`);
342
+ }
343
+ }
344
+
345
+ // Assistant-only computed / optional additions
346
+ if (svc === "assistant") {
347
+ args.push(
348
+ "-e", `VELLUM_ASSISTANT_NAME=${instanceName}`,
349
+ "-e", `GATEWAY_INTERNAL_URL=http://localhost:${GATEWAY_INTERNAL_PORT}`,
350
+ );
351
+
352
+ if (defaultWorkspaceConfigPath) {
353
+ const cPath = `/tmp/vellum-default-workspace-config-${Date.now()}.json`;
354
+ args.push(
355
+ "-v", `${defaultWorkspaceConfigPath}:${cPath}:ro`,
356
+ "-e", `VELLUM_DEFAULT_WORKSPACE_CONFIG_PATH=${cPath}`,
357
+ );
358
+ }
359
+
360
+ if (extraAssistantEnv) {
361
+ for (const [k, v] of Object.entries(extraAssistantEnv)) {
362
+ args.push("-e", `${k}=${v}`);
363
+ }
364
+ }
365
+
366
+ if (avatarDevicePath && existsSync(avatarDevicePath)) {
367
+ args.push(
368
+ "--device", `${avatarDevicePath}:${avatarDevicePath}`,
369
+ "-e", `${AVATAR_DEVICE_ENV_VAR}=${avatarDevicePath}`,
370
+ );
371
+ }
372
+ }
373
+
374
+ // Image is always last
375
+ args.push(imageTags[svc]);
376
+ return args;
377
+ };
378
+ }
379
+
380
+ return result;
381
+ }