@swarmclawai/swarmclaw 0.7.4 → 0.7.6

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.
@@ -0,0 +1,724 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { existsSync } from 'node:fs'
3
+ import path from 'node:path'
4
+ import {
5
+ getManagedProcess,
6
+ killManagedProcess,
7
+ removeManagedProcess,
8
+ startManagedProcess,
9
+ } from './process-manager'
10
+ import { normalizeOpenClawEndpoint, deriveOpenClawWsUrl } from '@/lib/openclaw-endpoint'
11
+
12
+ export type OpenClawRemoteDeployTemplate = 'docker' | 'render' | 'fly' | 'railway'
13
+ export type OpenClawRemoteDeployProvider =
14
+ | 'hetzner'
15
+ | 'digitalocean'
16
+ | 'vultr'
17
+ | 'linode'
18
+ | 'lightsail'
19
+ | 'gcp'
20
+ | 'azure'
21
+ | 'oci'
22
+ | 'generic'
23
+
24
+ export interface OpenClawLocalDeployStatus {
25
+ running: boolean
26
+ processId: string | null
27
+ pid: number | null
28
+ port: number
29
+ endpoint: string
30
+ wsUrl: string
31
+ token: string | null
32
+ startedAt: number | null
33
+ tail: string
34
+ lastError: string | null
35
+ launchCommand: string
36
+ installCommand: string
37
+ }
38
+
39
+ export interface OpenClawDeployBundleFile {
40
+ name: string
41
+ language: 'bash' | 'yaml' | 'env' | 'toml' | 'text'
42
+ content: string
43
+ }
44
+
45
+ export interface OpenClawDeployBundle {
46
+ template: OpenClawRemoteDeployTemplate
47
+ provider: OpenClawRemoteDeployProvider
48
+ providerLabel: string
49
+ title: string
50
+ summary: string
51
+ endpoint: string
52
+ wsUrl: string
53
+ token: string
54
+ runbook: string[]
55
+ files: OpenClawDeployBundleFile[]
56
+ }
57
+
58
+ interface LocalRuntimeState {
59
+ processId: string | null
60
+ port: number
61
+ endpoint: string
62
+ wsUrl: string
63
+ token: string | null
64
+ startedAt: number | null
65
+ lastError: string | null
66
+ }
67
+
68
+ interface DeployRuntimeState {
69
+ local: LocalRuntimeState
70
+ }
71
+
72
+ interface RemoteProviderMeta {
73
+ id: OpenClawRemoteDeployProvider
74
+ label: string
75
+ shortLabel: string
76
+ bootstrapHint: string
77
+ summary: string
78
+ }
79
+
80
+ const DEFAULT_LOCAL_PORT = 18789
81
+ const DEFAULT_REMOTE_PORT = 18789
82
+ const GLOBAL_KEY = '__swarmclaw_openclaw_deploy__' as const
83
+
84
+ const REMOTE_PROVIDER_META: Record<OpenClawRemoteDeployProvider, RemoteProviderMeta> = {
85
+ hetzner: {
86
+ id: 'hetzner',
87
+ label: 'Hetzner Cloud',
88
+ shortLabel: 'Hetzner',
89
+ bootstrapHint: 'Paste cloud-init.yaml into the Cloud Config field when you create the server.',
90
+ summary: 'Cheap Ubuntu VPS with excellent fit for always-on OpenClaw control planes.',
91
+ },
92
+ digitalocean: {
93
+ id: 'digitalocean',
94
+ label: 'DigitalOcean Droplet',
95
+ shortLabel: 'DigitalOcean',
96
+ bootstrapHint: 'Paste cloud-init.yaml into the User Data field when you create the Droplet.',
97
+ summary: 'Simple Ubuntu VPS path with predictable pricing and easy DNS + volume add-ons.',
98
+ },
99
+ vultr: {
100
+ id: 'vultr',
101
+ label: 'Vultr Cloud Compute',
102
+ shortLabel: 'Vultr',
103
+ bootstrapHint: 'Paste cloud-init.yaml into User Data / Startup Script on the instance create screen.',
104
+ summary: 'Straightforward VPS deployment with broad region coverage.',
105
+ },
106
+ linode: {
107
+ id: 'linode',
108
+ label: 'Linode',
109
+ shortLabel: 'Linode',
110
+ bootstrapHint: 'Paste cloud-init.yaml into your instance User Data during provisioning.',
111
+ summary: 'Good fit for users who want an uncomplicated Linux VM with persistent disks.',
112
+ },
113
+ lightsail: {
114
+ id: 'lightsail',
115
+ label: 'AWS Lightsail',
116
+ shortLabel: 'Lightsail',
117
+ bootstrapHint: 'Paste cloud-init.yaml into the Launch script / user data area on instance creation.',
118
+ summary: 'AWS-backed VPS option for users who want a simpler path than full EC2.',
119
+ },
120
+ gcp: {
121
+ id: 'gcp',
122
+ label: 'Google Cloud',
123
+ shortLabel: 'GCP',
124
+ bootstrapHint: 'Use an Ubuntu or Debian VM and provide cloud-init.yaml as startup metadata or cloud-init user data.',
125
+ summary: 'Good option when you already use Google Cloud networking or IAM.',
126
+ },
127
+ azure: {
128
+ id: 'azure',
129
+ label: 'Azure',
130
+ shortLabel: 'Azure',
131
+ bootstrapHint: 'Paste cloud-init.yaml into Custom data / cloud-init when creating the VM.',
132
+ summary: 'Useful for teams already standardized on Azure subscriptions and networking.',
133
+ },
134
+ oci: {
135
+ id: 'oci',
136
+ label: 'Oracle Cloud',
137
+ shortLabel: 'OCI',
138
+ bootstrapHint: 'Paste cloud-init.yaml into cloud-init user data when creating the instance.',
139
+ summary: 'A practical low-cost VPS path if you already operate in Oracle Cloud.',
140
+ },
141
+ generic: {
142
+ id: 'generic',
143
+ label: 'Any Ubuntu VPS',
144
+ shortLabel: 'Generic VPS',
145
+ bootstrapHint: 'Use cloud-init.yaml on any Ubuntu 24.04 host with cloud-init, or copy bootstrap.sh after SSHing in.',
146
+ summary: 'Generic fallback for bare metal, homelab servers, and providers not listed above.',
147
+ },
148
+ }
149
+
150
+ function getRuntimeState(): DeployRuntimeState {
151
+ const fallback: DeployRuntimeState = {
152
+ local: {
153
+ processId: null,
154
+ port: DEFAULT_LOCAL_PORT,
155
+ endpoint: normalizeOpenClawEndpoint(`http://127.0.0.1:${DEFAULT_LOCAL_PORT}`),
156
+ wsUrl: deriveOpenClawWsUrl(`http://127.0.0.1:${DEFAULT_LOCAL_PORT}`),
157
+ token: null,
158
+ startedAt: null,
159
+ lastError: null,
160
+ },
161
+ }
162
+ const globalState = globalThis as typeof globalThis & { [GLOBAL_KEY]?: DeployRuntimeState }
163
+ if (!globalState[GLOBAL_KEY]) {
164
+ globalState[GLOBAL_KEY] = fallback
165
+ }
166
+ return globalState[GLOBAL_KEY] || fallback
167
+ }
168
+
169
+ function shellEscape(value: string): string {
170
+ return `'${value.replace(/'/g, `'\\''`)}'`
171
+ }
172
+
173
+ function resolveBundledOpenClawBinary(): string {
174
+ const binName = process.platform === 'win32' ? 'openclaw.cmd' : 'openclaw'
175
+ const candidates = [
176
+ path.join(process.cwd(), 'node_modules', '.bin', binName),
177
+ path.join(process.cwd(), '.next', 'standalone', 'node_modules', '.bin', binName),
178
+ ]
179
+ for (const candidate of candidates) {
180
+ if (existsSync(candidate)) return candidate
181
+ }
182
+ return 'openclaw'
183
+ }
184
+
185
+ function buildLocalRunCommand(port: number, token?: string | null): string {
186
+ const parts = [
187
+ 'npx',
188
+ 'openclaw',
189
+ 'gateway',
190
+ 'run',
191
+ '--allow-unconfigured',
192
+ '--force',
193
+ '--bind',
194
+ 'loopback',
195
+ '--port',
196
+ String(port),
197
+ ]
198
+ if (token) {
199
+ parts.push('--auth', 'token', '--token', token)
200
+ }
201
+ return parts.join(' ')
202
+ }
203
+
204
+ function buildLocalInstallCommand(port: number, token?: string | null): string {
205
+ const parts = [
206
+ 'npx',
207
+ 'openclaw',
208
+ 'gateway',
209
+ 'install',
210
+ '--port',
211
+ String(port),
212
+ ]
213
+ if (token) parts.push('--token', token)
214
+ return `${parts.join(' ')} && npx openclaw gateway start`
215
+ }
216
+
217
+ function sanitizePort(value: unknown, fallback = DEFAULT_LOCAL_PORT): number {
218
+ const parsed = typeof value === 'number'
219
+ ? value
220
+ : typeof value === 'string'
221
+ ? Number.parseInt(value, 10)
222
+ : Number.NaN
223
+ if (!Number.isFinite(parsed)) return fallback
224
+ return Math.max(1024, Math.min(65535, Math.trunc(parsed)))
225
+ }
226
+
227
+ function normalizeToken(value: unknown): string | null {
228
+ return typeof value === 'string' && value.trim() ? value.trim() : null
229
+ }
230
+
231
+ function normalizeRemoteProvider(value: unknown): OpenClawRemoteDeployProvider {
232
+ if (
233
+ value === 'hetzner'
234
+ || value === 'digitalocean'
235
+ || value === 'vultr'
236
+ || value === 'linode'
237
+ || value === 'lightsail'
238
+ || value === 'gcp'
239
+ || value === 'azure'
240
+ || value === 'oci'
241
+ || value === 'generic'
242
+ ) {
243
+ return value
244
+ }
245
+ return 'hetzner'
246
+ }
247
+
248
+ function wait(ms: number): Promise<void> {
249
+ return new Promise((resolve) => setTimeout(resolve, ms))
250
+ }
251
+
252
+ async function waitForLocalRuntime(processId: string, attempts = 12): Promise<void> {
253
+ for (let i = 0; i < attempts; i += 1) {
254
+ const process = getManagedProcess(processId)
255
+ if (!process || process.status !== 'running') break
256
+ if ((process.log || '').toLowerCase().includes('listening')) return
257
+ await wait(500)
258
+ }
259
+ }
260
+
261
+ function readTail(text: string, size = 1200): string {
262
+ if (!text) return ''
263
+ return text.length <= size ? text : text.slice(text.length - size)
264
+ }
265
+
266
+ function currentLocalStatus(): OpenClawLocalDeployStatus {
267
+ const state = getRuntimeState()
268
+ const processId = state.local.processId
269
+ const process = processId ? getManagedProcess(processId) : null
270
+ const running = !!process && process.status === 'running'
271
+
272
+ if (!running && processId && process && process.status !== 'running') {
273
+ state.local.lastError = readTail(process.log || '') || state.local.lastError
274
+ state.local.processId = null
275
+ state.local.startedAt = null
276
+ }
277
+
278
+ const endpoint = normalizeOpenClawEndpoint(`http://127.0.0.1:${state.local.port}`)
279
+ return {
280
+ running,
281
+ processId: running ? processId : null,
282
+ pid: running ? (process?.pid ?? null) : null,
283
+ port: state.local.port,
284
+ endpoint,
285
+ wsUrl: deriveOpenClawWsUrl(endpoint),
286
+ token: state.local.token || null,
287
+ startedAt: running ? state.local.startedAt : null,
288
+ tail: process ? readTail(process.log || '') : '',
289
+ lastError: running ? null : (state.local.lastError || null),
290
+ launchCommand: buildLocalRunCommand(state.local.port, state.local.token),
291
+ installCommand: buildLocalInstallCommand(state.local.port, state.local.token),
292
+ }
293
+ }
294
+
295
+ export function getOpenClawLocalDeployStatus(): OpenClawLocalDeployStatus {
296
+ return currentLocalStatus()
297
+ }
298
+
299
+ export function generateOpenClawGatewayToken(): string {
300
+ return randomBytes(24).toString('base64url')
301
+ }
302
+
303
+ export async function startOpenClawLocalDeploy(input?: {
304
+ port?: number
305
+ token?: string | null
306
+ }): Promise<{ local: OpenClawLocalDeployStatus; token: string }> {
307
+ const state = getRuntimeState()
308
+ const current = currentLocalStatus()
309
+ if (current.running && current.processId) {
310
+ killManagedProcess(current.processId)
311
+ removeManagedProcess(current.processId)
312
+ }
313
+
314
+ const port = sanitizePort(input?.port, DEFAULT_LOCAL_PORT)
315
+ const token = normalizeToken(input?.token) || generateOpenClawGatewayToken()
316
+ const endpoint = normalizeOpenClawEndpoint(`http://127.0.0.1:${port}`)
317
+ const wsUrl = deriveOpenClawWsUrl(endpoint)
318
+ const binary = resolveBundledOpenClawBinary()
319
+ const args = [
320
+ binary,
321
+ 'gateway',
322
+ 'run',
323
+ '--allow-unconfigured',
324
+ '--force',
325
+ '--bind',
326
+ 'loopback',
327
+ '--port',
328
+ String(port),
329
+ '--auth',
330
+ 'token',
331
+ '--token',
332
+ token,
333
+ '--verbose',
334
+ ]
335
+
336
+ const result = await startManagedProcess({
337
+ command: args.map(shellEscape).join(' '),
338
+ cwd: process.cwd(),
339
+ background: true,
340
+ timeoutMs: 24 * 60 * 60_000,
341
+ })
342
+
343
+ if (result.status !== 'running') {
344
+ const message = result.output || result.tail || 'OpenClaw failed to start.'
345
+ state.local = {
346
+ processId: null,
347
+ port,
348
+ endpoint,
349
+ wsUrl,
350
+ token,
351
+ startedAt: null,
352
+ lastError: message,
353
+ }
354
+ throw new Error(message)
355
+ }
356
+
357
+ state.local = {
358
+ processId: result.processId,
359
+ port,
360
+ endpoint,
361
+ wsUrl,
362
+ token,
363
+ startedAt: Date.now(),
364
+ lastError: null,
365
+ }
366
+
367
+ await waitForLocalRuntime(result.processId)
368
+
369
+ return {
370
+ local: currentLocalStatus(),
371
+ token,
372
+ }
373
+ }
374
+
375
+ export function stopOpenClawLocalDeploy(): OpenClawLocalDeployStatus {
376
+ const state = getRuntimeState()
377
+ const processId = state.local.processId
378
+ if (processId) {
379
+ const process = getManagedProcess(processId)
380
+ if (process?.status === 'running') {
381
+ killManagedProcess(processId)
382
+ }
383
+ removeManagedProcess(processId)
384
+ }
385
+ state.local.processId = null
386
+ state.local.startedAt = null
387
+ return currentLocalStatus()
388
+ }
389
+
390
+ function ensureSchemeAndPort(raw: string, scheme: 'http' | 'https', port: number): string {
391
+ const trimmed = raw.trim()
392
+ if (!trimmed) {
393
+ return `${scheme}://127.0.0.1:${port}`
394
+ }
395
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return trimmed
396
+ const defaultPort = scheme === 'https' ? 443 : port
397
+ const hasPort = /:\d+$/.test(trimmed)
398
+ const portSuffix = hasPort || defaultPort === 443 ? '' : `:${defaultPort}`
399
+ return `${scheme}://${trimmed}${portSuffix}`
400
+ }
401
+
402
+ function deriveRemoteDeploymentName(target: string): string {
403
+ const cleaned = target
404
+ .replace(/^https?:\/\//i, '')
405
+ .replace(/\/.*$/, '')
406
+ .replace(/:\d+$/, '')
407
+ .trim()
408
+ return cleaned || 'Remote OpenClaw'
409
+ }
410
+
411
+ function indentBlock(value: string, spaces: number): string {
412
+ const padding = ' '.repeat(spaces)
413
+ return value
414
+ .replace(/\r\n/g, '\n')
415
+ .split('\n')
416
+ .map((line) => `${padding}${line}`)
417
+ .join('\n')
418
+ }
419
+
420
+ function buildDockerComposeFile(): string {
421
+ return `services:
422
+ openclaw-gateway:
423
+ image: \${OPENCLAW_IMAGE:-openclaw:latest}
424
+ environment:
425
+ HOME: /home/node
426
+ TERM: xterm-256color
427
+ OPENCLAW_GATEWAY_TOKEN: \${OPENCLAW_GATEWAY_TOKEN}
428
+ OPENCLAW_GATEWAY_BIND: \${OPENCLAW_GATEWAY_BIND:-lan}
429
+ volumes:
430
+ - \${OPENCLAW_CONFIG_DIR:-./.openclaw}:/home/node/.openclaw
431
+ - \${OPENCLAW_WORKSPACE_DIR:-./workspace}:/home/node/.openclaw/workspace
432
+ ports:
433
+ - "\${OPENCLAW_GATEWAY_PORT:-18789}:18789"
434
+ - "\${OPENCLAW_BRIDGE_PORT:-18790}:18790"
435
+ init: true
436
+ restart: unless-stopped
437
+ command:
438
+ [
439
+ "node",
440
+ "dist/index.js",
441
+ "gateway",
442
+ "--allow-unconfigured",
443
+ "--bind",
444
+ "\${OPENCLAW_GATEWAY_BIND:-lan}",
445
+ "--port",
446
+ "18789",
447
+ "--auth",
448
+ "token",
449
+ "--token",
450
+ "\${OPENCLAW_GATEWAY_TOKEN}",
451
+ ]
452
+ healthcheck:
453
+ test:
454
+ [
455
+ "CMD",
456
+ "node",
457
+ "-e",
458
+ "fetch('http://127.0.0.1:18789/healthz').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
459
+ ]
460
+ interval: 30s
461
+ timeout: 5s
462
+ retries: 5
463
+ start_period: 20s
464
+ `
465
+ }
466
+
467
+ function buildDockerEnvFile(token: string): string {
468
+ return `OPENCLAW_IMAGE=openclaw:latest
469
+ OPENCLAW_GATEWAY_TOKEN=${token}
470
+ OPENCLAW_GATEWAY_BIND=lan
471
+ OPENCLAW_GATEWAY_PORT=18789
472
+ OPENCLAW_BRIDGE_PORT=18790
473
+ OPENCLAW_CONFIG_DIR=./.openclaw
474
+ OPENCLAW_WORKSPACE_DIR=./workspace
475
+ `
476
+ }
477
+
478
+ function buildDockerBootstrapScript(): string {
479
+ return `#!/usr/bin/env bash
480
+ set -euo pipefail
481
+
482
+ APP_DIR="\${OPENCLAW_APP_DIR:-$HOME/openclaw}"
483
+
484
+ mkdir -p "$APP_DIR"
485
+ cd "$APP_DIR"
486
+ mkdir -p .openclaw workspace
487
+
488
+ if ! command -v docker >/dev/null 2>&1; then
489
+ echo "Docker is required. On Ubuntu 24.04 you can install it with:"
490
+ echo " sudo apt-get update && sudo apt-get install -y docker.io docker-compose-plugin"
491
+ exit 1
492
+ fi
493
+
494
+ docker pull "\${OPENCLAW_IMAGE:-openclaw:latest}"
495
+ docker compose up -d
496
+ docker compose ps
497
+ `
498
+ }
499
+
500
+ function buildCloudInitFile(token: string): string {
501
+ const envFile = buildDockerEnvFile(token)
502
+ const composeFile = buildDockerComposeFile()
503
+ return `#cloud-config
504
+ package_update: true
505
+ package_upgrade: true
506
+ packages:
507
+ - ca-certificates
508
+ - curl
509
+ - docker.io
510
+ - docker-compose-plugin
511
+ write_files:
512
+ - path: /opt/openclaw/.env
513
+ owner: root:root
514
+ permissions: "0600"
515
+ content: |
516
+ ${indentBlock(envFile, 6)}
517
+ - path: /opt/openclaw/docker-compose.yml
518
+ owner: root:root
519
+ permissions: "0644"
520
+ content: |
521
+ ${indentBlock(composeFile, 6)}
522
+ runcmd:
523
+ - mkdir -p /opt/openclaw/.openclaw /opt/openclaw/workspace
524
+ - systemctl enable --now docker
525
+ - bash -lc 'cd /opt/openclaw && docker pull "\${OPENCLAW_IMAGE:-openclaw:latest}"'
526
+ - bash -lc 'cd /opt/openclaw && docker compose up -d'
527
+ final_message: "OpenClaw gateway bootstrap complete. Run: sudo docker compose -f /opt/openclaw/docker-compose.yml ps"
528
+ `
529
+ }
530
+
531
+ function buildRenderManifest(): string {
532
+ return `services:
533
+ - type: web
534
+ name: openclaw
535
+ runtime: docker
536
+ plan: starter
537
+ healthCheckPath: /health
538
+ envVars:
539
+ - key: PORT
540
+ value: "8080"
541
+ - key: SETUP_PASSWORD
542
+ sync: false
543
+ - key: OPENCLAW_STATE_DIR
544
+ value: /data/.openclaw
545
+ - key: OPENCLAW_WORKSPACE_DIR
546
+ value: /data/workspace
547
+ - key: OPENCLAW_GATEWAY_TOKEN
548
+ sync: false
549
+ disk:
550
+ name: openclaw-data
551
+ mountPath: /data
552
+ sizeGB: 1
553
+ `
554
+ }
555
+
556
+ function buildFlyToml(): string {
557
+ return `app = "openclaw"
558
+ primary_region = "iad"
559
+
560
+ [build]
561
+ dockerfile = "Dockerfile"
562
+
563
+ [env]
564
+ NODE_ENV = "production"
565
+ OPENCLAW_PREFER_PNPM = "1"
566
+ OPENCLAW_STATE_DIR = "/data"
567
+ NODE_OPTIONS = "--max-old-space-size=1536"
568
+
569
+ [processes]
570
+ app = "node dist/index.js gateway --allow-unconfigured --port 3000 --bind lan"
571
+
572
+ [http_service]
573
+ internal_port = 3000
574
+ force_https = true
575
+ auto_stop_machines = false
576
+ auto_start_machines = true
577
+ min_machines_running = 1
578
+ processes = ["app"]
579
+
580
+ [[vm]]
581
+ size = "shared-cpu-2x"
582
+ memory = "2048mb"
583
+
584
+ [mounts]
585
+ source = "openclaw_data"
586
+ destination = "/data"
587
+ `
588
+ }
589
+
590
+ function buildRailwayEnvTemplate(token: string): string {
591
+ return `OPENCLAW_GATEWAY_TOKEN=${token}
592
+ OPENCLAW_STATE_DIR=/data/.openclaw
593
+ OPENCLAW_WORKSPACE_DIR=/data/workspace
594
+ PORT=8080
595
+ `
596
+ }
597
+
598
+ function buildRailwayConfig(): string {
599
+ return `{
600
+ "$schema": "https://railway.com/railway.schema.json",
601
+ "deploy": {
602
+ "healthcheckPath": "/healthz",
603
+ "restartPolicyType": "ON_FAILURE",
604
+ "restartPolicyMaxRetries": 10
605
+ }
606
+ }
607
+ `
608
+ }
609
+
610
+ function buildDockerRunbook(
611
+ providerMeta: RemoteProviderMeta,
612
+ endpoint: string,
613
+ ): string[] {
614
+ const endpointHost = deriveRemoteDeploymentName(endpoint)
615
+ return [
616
+ `Provision a small Ubuntu 24.04 server on ${providerMeta.label}. ${providerMeta.bootstrapHint}`,
617
+ 'Let first boot finish, then confirm the service with: sudo docker compose -f /opt/openclaw/docker-compose.yml ps',
618
+ `Point a DNS name, reverse proxy, or Tailscale hostname at ${endpointHost} and keep the generated token private.`,
619
+ 'Use the generated endpoint and token in SwarmClaw to save the gateway profile.',
620
+ ]
621
+ }
622
+
623
+ export function buildOpenClawDeployBundle(input?: {
624
+ template?: OpenClawRemoteDeployTemplate
625
+ target?: string | null
626
+ token?: string | null
627
+ scheme?: 'http' | 'https'
628
+ port?: number
629
+ provider?: OpenClawRemoteDeployProvider
630
+ }): OpenClawDeployBundle {
631
+ const template = input?.template || 'docker'
632
+ const token = normalizeToken(input?.token) || generateOpenClawGatewayToken()
633
+ const scheme = input?.scheme === 'http' ? 'http' : 'https'
634
+ const port = sanitizePort(input?.port, DEFAULT_REMOTE_PORT)
635
+ const rawTarget = typeof input?.target === 'string' ? input.target.trim() : ''
636
+ const endpoint = normalizeOpenClawEndpoint(ensureSchemeAndPort(rawTarget || 'openclaw.example.com', scheme, port))
637
+ const wsUrl = deriveOpenClawWsUrl(endpoint)
638
+ const provider = normalizeRemoteProvider(input?.provider)
639
+ const providerMeta = REMOTE_PROVIDER_META[provider]
640
+
641
+ if (template === 'render') {
642
+ return {
643
+ template,
644
+ provider: 'generic',
645
+ providerLabel: 'Render',
646
+ title: 'Render OpenClaw Service',
647
+ summary: 'Deploy the official OpenClaw repo as a Docker web service on Render, then point SwarmClaw at the public HTTPS URL.',
648
+ endpoint,
649
+ wsUrl,
650
+ token,
651
+ runbook: [
652
+ 'Create a new Render Web Service from the official OpenClaw GitHub repo.',
653
+ 'Add OPENCLAW_GATEWAY_TOKEN as a secret environment variable using the generated token below.',
654
+ 'After the service is live, paste the HTTPS URL back into SwarmClaw and save this gateway.',
655
+ ],
656
+ files: [
657
+ { name: 'render.yaml', language: 'yaml', content: buildRenderManifest() },
658
+ { name: 'OPENCLAW_GATEWAY_TOKEN.txt', language: 'text', content: token },
659
+ ],
660
+ }
661
+ }
662
+
663
+ if (template === 'fly') {
664
+ return {
665
+ template,
666
+ provider: 'generic',
667
+ providerLabel: 'Fly.io',
668
+ title: 'Fly.io OpenClaw App',
669
+ summary: 'Deploy the official OpenClaw repo on Fly.io for an always-on remote gateway with a persistent volume and HTTPS out of the box.',
670
+ endpoint,
671
+ wsUrl,
672
+ token,
673
+ runbook: [
674
+ 'Deploy the official OpenClaw repo with this fly.toml.',
675
+ 'Set OPENCLAW_GATEWAY_TOKEN as a Fly secret before first deploy.',
676
+ 'Use the resulting HTTPS app URL as your SwarmClaw OpenClaw endpoint.',
677
+ ],
678
+ files: [
679
+ { name: 'fly.toml', language: 'toml', content: buildFlyToml() },
680
+ { name: 'OPENCLAW_GATEWAY_TOKEN.txt', language: 'text', content: token },
681
+ ],
682
+ }
683
+ }
684
+
685
+ if (template === 'railway') {
686
+ return {
687
+ template,
688
+ provider: 'generic',
689
+ providerLabel: 'Railway',
690
+ title: 'Railway OpenClaw Service',
691
+ summary: 'Deploy the official OpenClaw repo on Railway using its Dockerfile, then attach a volume and set the generated gateway token.',
692
+ endpoint,
693
+ wsUrl,
694
+ token,
695
+ runbook: [
696
+ 'Create a Railway project from the official OpenClaw GitHub repo so Railway builds the root Dockerfile automatically.',
697
+ 'Attach a persistent volume at /data, then paste the generated variables below into the service variables editor.',
698
+ 'After Railway deploys, use the public HTTPS URL as your SwarmClaw OpenClaw endpoint.',
699
+ ],
700
+ files: [
701
+ { name: 'railway.json', language: 'text', content: buildRailwayConfig() },
702
+ { name: 'railway.env', language: 'env', content: buildRailwayEnvTemplate(token) },
703
+ ],
704
+ }
705
+ }
706
+
707
+ return {
708
+ template: 'docker',
709
+ provider,
710
+ providerLabel: providerMeta.shortLabel,
711
+ title: `${providerMeta.shortLabel} OpenClaw Smart Deploy`,
712
+ summary: `${providerMeta.summary} This bundle only uses the official OpenClaw Docker image and gives you both manual Docker files and a cloud-init quickstart.`,
713
+ endpoint,
714
+ wsUrl,
715
+ token,
716
+ runbook: buildDockerRunbook(providerMeta, endpoint),
717
+ files: [
718
+ { name: 'cloud-init.yaml', language: 'yaml', content: buildCloudInitFile(token) },
719
+ { name: '.env', language: 'env', content: buildDockerEnvFile(token) },
720
+ { name: 'docker-compose.yml', language: 'yaml', content: buildDockerComposeFile() },
721
+ { name: 'bootstrap.sh', language: 'bash', content: buildDockerBootstrapScript() },
722
+ ],
723
+ }
724
+ }