/ bin / swarmclaw.js
swarmclaw.js
  1  #!/usr/bin/env node
  2  'use strict'
  3  /* eslint-disable @typescript-eslint/no-require-imports */
  4  
  5  const path = require('node:path')
  6  const { spawnSync } = require('node:child_process')
  7  
  8  // Legacy TS CLI groups/actions that provide richer, command-specific options.
  9  const TS_CLI_ACTIONS = Object.freeze({
 10    agents: new Set(['list', 'get']),
 11    tasks: new Set(['list', 'get', 'create', 'update', 'delete', 'archive']),
 12    schedules: new Set(['list', 'get', 'create']),
 13    runs: new Set(['list', 'get']),
 14    sessions: new Set(['list', 'get', 'create', 'update', 'delete', 'history', 'mailbox', 'stop']),
 15    memory: new Set(['get', 'search', 'store', 'maintenance']),
 16    'memory-images': new Set(['get']),
 17    setup: new Set(['init', 'check-provider', 'doctor', 'openclaw-device']),
 18    connectors: new Set(['list', 'get', 'create', 'update', 'delete', 'start', 'stop', 'repair']),
 19    webhooks: new Set(['list', 'get', 'create', 'update', 'delete', 'trigger']),
 20  })
 21  
 22  const LEGACY_TS_CLI_ALIAS_MAP = Object.freeze({
 23    '--base-url': '--url',
 24    '--access-key': '--key',
 25  })
 26  
 27  function shouldUseLegacyTsCli(argv) {
 28    const group = argv[0]
 29    const action = argv[1]
 30  
 31    // Default to mapped CLI for top-level help/version and unknown groups.
 32    if (!group || group.startsWith('-')) return false
 33  
 34    const actions = TS_CLI_ACTIONS[group]
 35    if (!actions) return false
 36  
 37    // Prefer mapped CLI for group help so all API-backed actions are discoverable.
 38    if (!action || action === 'help' || action.startsWith('-')) return false
 39  
 40    return actions.has(action)
 41  }
 42  
 43  function supportsStripTypes() {
 44    return process.allowedNodeEnvironmentFlags.has('--experimental-strip-types')
 45  }
 46  
 47  function hasTsxRuntime() {
 48    try {
 49      require.resolve('tsx/package.json')
 50      return true
 51    } catch {
 52      return false
 53    }
 54  }
 55  
 56  function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
 57    const stripTypesSupported = options.supportsStripTypes ?? supportsStripTypes()
 58    if (stripTypesSupported) {
 59      return ['--no-warnings', '--experimental-strip-types', cliPath, ...argv]
 60    }
 61  
 62    const tsxAvailable = options.hasTsxRuntime ?? hasTsxRuntime()
 63    if (tsxAvailable) {
 64      return ['--no-warnings', '--import', 'tsx', cliPath, ...argv]
 65    }
 66  
 67    return null
 68  }
 69  
 70  function normalizeLegacyTsCliArgv(argv) {
 71    const normalized = []
 72  
 73    for (const token of argv) {
 74      if (!token.startsWith('--')) {
 75        normalized.push(token)
 76        continue
 77      }
 78  
 79      const eqIndex = token.indexOf('=')
 80      const flag = eqIndex > -1 ? token.slice(0, eqIndex) : token
 81      const mappedFlag = LEGACY_TS_CLI_ALIAS_MAP[flag]
 82  
 83      if (!mappedFlag) {
 84        normalized.push(token)
 85        continue
 86      }
 87  
 88      if (eqIndex > -1) {
 89        normalized.push(`${mappedFlag}=${token.slice(eqIndex + 1)}`)
 90      } else {
 91        normalized.push(mappedFlag)
 92      }
 93    }
 94  
 95    return normalized
 96  }
 97  
 98  function runLegacyTsCli(argv) {
 99    const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.ts')
100    const args = buildLegacyTsCliArgs(cliPath, normalizeLegacyTsCliArgv(argv))
101    const env = normalizeLegacyCliEnv(process.env)
102    if (!args) {
103      process.stderr.write('Legacy CLI commands require Node 22.6+ or an available local tsx runtime.\n')
104      return 1
105    }
106    const child = spawnSync(
107      process.execPath,
108      args,
109      { stdio: 'inherit', env },
110    )
111  
112    if (child.error) {
113      process.stderr.write(`${child.error.message}\n`)
114      return 1
115    }
116    if (typeof child.status === 'number') return child.status
117    return 1
118  }
119  
120  function normalizeLegacyCliEnv(env) {
121    const nextEnv = { ...env }
122    if (!nextEnv.SWARMCLAW_URL && nextEnv.SWARMCLAW_BASE_URL) {
123      nextEnv.SWARMCLAW_URL = nextEnv.SWARMCLAW_BASE_URL
124    }
125    if (!nextEnv.SWARMCLAW_ACCESS_KEY) {
126      const key = nextEnv.SWARMCLAW_API_KEY || nextEnv.SC_ACCESS_KEY || ''
127      if (key) nextEnv.SWARMCLAW_ACCESS_KEY = key
128    }
129    return nextEnv
130  }
131  
132  function printPackageVersion() {
133    const pkg = require('../package.json')
134    process.stdout.write(`${pkg.name || 'swarmclaw'} ${pkg.version || '0.0.0'}\n`)
135  }
136  
137  function printVersionHelp() {
138    process.stdout.write(`
139  Usage: swarmclaw version
140  
141  Show the installed SwarmClaw package version.
142  `.trim() + '\n')
143  }
144  
145  async function runMappedCli(argv) {
146    const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.js')
147    const cliModule = await import(cliPath)
148    const runCli = cliModule.runCli || (cliModule.default && cliModule.default.runCli)
149    if (typeof runCli !== 'function') {
150      throw new Error('Unable to load API-mapped CLI runtime')
151    }
152    return runCli(argv)
153  }
154  
155  async function runHelp(argv) {
156    const [target, ...rest] = argv
157    if (!target) {
158      const code = await runMappedCli(['--help'])
159      process.exitCode = typeof code === 'number' ? code : 1
160      return
161    }
162  
163    if (target === 'run' || target === 'start' || target === 'stop' || target === 'status' || target === 'server') {
164      await require('./server-cmd.js').main(['--help'])
165      return
166    }
167    if (target === 'daemon') {
168      await require('./daemon-cmd.js').main(['--help'])
169      return
170    }
171    if (target === 'worker') {
172      require('./worker-cmd.js').main(['--help'])
173      return
174    }
175    if (target === 'doctor') {
176      require('./doctor-cmd.js').main(['--help'])
177      return
178    }
179    if (target === 'update') {
180      require('./update-cmd.js').main(['--help'])
181      return
182    }
183    if (target === 'version') {
184      printVersionHelp()
185      return
186    }
187  
188    const forwarded = rest.includes('--help') || rest.includes('-h')
189      ? [target, ...rest]
190      : [target, ...rest, '--help']
191    const code = shouldUseLegacyTsCli(forwarded)
192      ? runLegacyTsCli(forwarded)
193      : await runMappedCli(forwarded)
194  
195    process.exitCode = typeof code === 'number' ? code : 1
196  }
197  
198  async function main() {
199    const argv = process.argv.slice(2)
200    const top = argv[0]
201  
202    // Default to 'server' when invoked with no arguments.
203    if (!top) {
204      await require('./server-cmd.js').main([])
205      return
206    }
207  
208    if (top === '-v') {
209      printPackageVersion()
210      return
211    }
212  
213    if (top === 'version' && argv.length === 1) {
214      printPackageVersion()
215      return
216    }
217  
218    if (top === 'help') {
219      await runHelp(argv.slice(1))
220      return
221    }
222  
223    // Route local lifecycle/maintenance commands to CJS scripts (no TS dependency).
224    if (top === 'server') {
225      await require('./server-cmd.js').main(argv.slice(1))
226      return
227    }
228    if (top === 'daemon') {
229      const subcommand = argv[1]
230      if (!subcommand || subcommand === 'run' || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
231        await require('./daemon-cmd.js').main(argv.slice(1))
232        return
233      }
234    }
235    if (top === 'run' || top === 'start') {
236      await require('./server-cmd.js').main(argv.slice(1))
237      return
238    }
239    if (top === 'status' || top === 'stop') {
240      await require('./server-cmd.js').main([top, ...argv.slice(1)])
241      return
242    }
243    if (top === 'worker') {
244      require('./worker-cmd.js').main()
245      return
246    }
247    if (top === 'doctor') {
248      require('./doctor-cmd.js').main(argv.slice(1))
249      return
250    }
251    if (top === 'update') {
252      require('./update-cmd.js').main()
253      return
254    }
255  
256    const code = shouldUseLegacyTsCli(argv)
257      ? runLegacyTsCli(argv)
258      : await runMappedCli(argv)
259  
260    process.exitCode = typeof code === 'number' ? code : 1
261  }
262  
263  if (require.main === module) {
264    void main().catch((err) => {
265      process.stderr.write(`${err?.message || String(err)}\n`)
266      process.exit(1)
267    })
268  }
269  
270  module.exports = {
271    buildLegacyTsCliArgs,
272    hasTsxRuntime,
273    normalizeLegacyTsCliArgv,
274    TS_CLI_ACTIONS,
275    normalizeLegacyCliEnv,
276    printPackageVersion,
277    supportsStripTypes,
278    shouldUseLegacyTsCli,
279  }