#!/usr/bin/env node /* eslint-disable no-console */ /** * SPLEXA Universe CLI — single-file, zero-dependency. * * Built on Node 20+ built-in fetch. Distributable as a plain JS file that any * customer can `curl | node` or `npm i -g @splexa/cli`. * * Auth model: stores `{ apiBase, apiKey }` in $XDG_CONFIG_HOME/splexa/config.json * (or %APPDATA%\splexa\config.json on Windows). Permissions are tightened to * 0600 on POSIX so a stray API key on a shared host doesn't leak. */ 'use strict'; const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); const readline = require('node:readline'); // ----------------------------- config storage ------------------------------ function configDir() { if (process.platform === 'win32') { return path.join(process.env.APPDATA || os.homedir(), 'splexa'); } return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'splexa'); } function configPath() { return path.join(configDir(), 'config.json'); } function loadConfig() { try { const raw = fs.readFileSync(configPath(), 'utf8'); return JSON.parse(raw); } catch { return { apiBase: 'https://universe.splexa.com', apiKey: null }; } } function saveConfig(cfg) { const dir = configDir(); fs.mkdirSync(dir, { recursive: true }); const file = configPath(); fs.writeFileSync(file, JSON.stringify(cfg, null, 2), { encoding: 'utf8' }); // Best-effort permission tightening — ignore on Windows. try { if (process.platform !== 'win32') fs.chmodSync(file, 0o600); } catch { /* ignore */ } } function effectiveConfig() { const cfg = loadConfig(); // Env vars override the stored config, e.g. for CI. if (process.env.SPLEXA_API_KEY) cfg.apiKey = process.env.SPLEXA_API_KEY; if (process.env.SPLEXA_API_BASE) cfg.apiBase = process.env.SPLEXA_API_BASE; return cfg; } // ------------------------------ HTTP helpers ------------------------------- async function api(method, urlPath, { body, requireAuth = true, accept = 'application/json' } = {}) { const cfg = effectiveConfig(); if (requireAuth && !cfg.apiKey) { fail('Not logged in. Run: splexa login'); } const headers = { 'User-Agent': `splexa-cli/${pkgVersion()}`, Accept: accept, }; if (requireAuth) headers.Authorization = `Bearer ${cfg.apiKey}`; if (body !== undefined) headers['Content-Type'] = 'application/json'; const url = `${cfg.apiBase.replace(/\/+$/, '')}${urlPath}`; let res; try { res = await fetch(url, { method, headers, body: body !== undefined ? JSON.stringify(body) : undefined, }); } catch (e) { fail(`Network error talking to ${url}: ${e.message}`); } // Stream-friendly: callers may want the raw response (e.g. SSE). if (accept !== 'application/json') return res; let payload = null; const text = await res.text(); if (text) { try { payload = JSON.parse(text); } catch { payload = { success: false, error: text }; } } if (!res.ok) { const msg = (payload && payload.error) || `HTTP ${res.status}`; const scope = payload && payload.requiredScope ? ` (required scope: ${payload.requiredScope})` : ''; fail(`${msg}${scope}`); } return payload; } function pkgVersion() { try { return require('../package.json').version; } catch { return '0.0.0'; } } // ------------------------------ tiny UI utils ------------------------------ function fail(msg) { process.stderr.write(`splexa: ${msg}\n`); process.exit(1); } function ok(msg) { process.stdout.write(`${msg}\n`); } function isTTY() { return process.stdout.isTTY; } function color(code, s) { return isTTY() ? `\x1b[${code}m${s}\x1b[0m` : s; } const dim = (s) => color('2', s); const bold = (s) => color('1', s); const green = (s) => color('32', s); const red = (s) => color('31', s); const yellow = (s) => color('33', s); function table(rows, columns) { if (rows.length === 0) { ok(dim('(no results)')); return; } const widths = columns.map((c) => Math.max(c.header.length, ...rows.map((r) => String(c.value(r) ?? '').length)) ); const headerLine = columns.map((c, i) => bold(c.header.padEnd(widths[i]))).join(' '); ok(headerLine); ok(columns.map((_, i) => '-'.repeat(widths[i])).join(' ')); for (const r of rows) { ok(columns.map((c, i) => String(c.value(r) ?? '').padEnd(widths[i])).join(' ')); } } function prompt(question, { silent = false } = {}) { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal: true, }); if (silent) { // Mute echo while typing the secret. const stdin = process.stdin; process.stdout.write(question); let input = ''; const onData = (ch) => { const s = ch.toString('utf8'); for (const c of s) { if (c === '\n' || c === '\r') { stdin.removeListener('data', onData); stdin.setRawMode && stdin.setRawMode(false); stdin.pause(); process.stdout.write('\n'); rl.close(); resolve(input); return; } else if (c === '\u0003') { // Ctrl-C process.exit(130); } else if (c === '\u007f' || c === '\b') { input = input.slice(0, -1); } else { input += c; } } }; stdin.setRawMode && stdin.setRawMode(true); stdin.resume(); stdin.on('data', onData); } else { rl.question(question, (answer) => { rl.close(); resolve(answer); }); } }); } // ------------------------------- commands ---------------------------------- async function cmdLogin(args) { const flags = parseFlags(args, { '--api-base': 'string', '--api-key': 'string' }); const cfg = loadConfig(); if (flags['--api-base']) cfg.apiBase = flags['--api-base']; if (!flags['--api-base'] && isTTY()) { const def = cfg.apiBase || 'https://universe.splexa.com'; const inp = (await prompt(`API base [${def}]: `)).trim(); if (inp) cfg.apiBase = inp; } cfg.apiBase = cfg.apiBase || 'https://universe.splexa.com'; if (flags['--api-key']) { cfg.apiKey = flags['--api-key']; } else if (isTTY()) { cfg.apiKey = (await prompt('API key (sk_live_...): ', { silent: true })).trim(); } else { fail('Provide --api-key when stdin is not a TTY'); } if (!cfg.apiKey || !cfg.apiKey.startsWith('sk_live_')) { fail('API key must start with sk_live_'); } saveConfig(cfg); // Verify the key by calling the discovery endpoint. const meta = await api('GET', '/api/v1'); ok(green('✓ ') + `Logged in to ${cfg.apiBase} (company ${meta.company.id})`); ok(dim(`Config saved to ${configPath()}`)); } async function cmdLogout() { const cfg = loadConfig(); cfg.apiKey = null; saveConfig(cfg); ok(green('✓ ') + 'Logged out'); } async function cmdWhoami() { const cfg = effectiveConfig(); const meta = await api('GET', '/api/v1'); ok(`API base: ${cfg.apiBase}`); ok(`Company: ${meta.company.id}`); ok(`Version: ${meta.version}`); ok(`Endpoints: ${meta.endpoints.length}`); } async function cmdNetworksList(args) { const flags = parseFlags(args, { '--json': 'bool' }); const r = await api('GET', '/api/v1/networks'); if (flags['--json']) return ok(JSON.stringify(r.data, null, 2)); table(r.data, [ { header: 'ID', value: (n) => n.id.slice(0, 8) }, { header: 'NAME', value: (n) => n.name }, { header: 'ZT-NETWORK', value: (n) => n.ztNetworkId }, { header: 'SUBNET', value: (n) => n.ipv4Subnet }, { header: 'NODES', value: (n) => n.nodeCount }, { header: 'ACTIVE', value: (n) => (n.active ? green('yes') : red('no')) }, ]); } async function cmdNetworksGet(args) { const id = args[0]; if (!id) fail('Usage: splexa networks get '); const r = await api('GET', `/api/v1/networks/${encodeURIComponent(id)}`); ok(JSON.stringify(r.data, null, 2)); } async function cmdNodesList(args) { const flags = parseFlags(args, { '--network': 'string', '--json': 'bool' }); const networkId = flags['--network'] || args[0]; if (!networkId) fail('Usage: splexa nodes list --network '); const r = await api('GET', `/api/v1/networks/${encodeURIComponent(networkId)}/nodes`); if (flags['--json']) return ok(JSON.stringify(r.data, null, 2)); table(r.data, [ { header: 'ID', value: (n) => n.id.slice(0, 8) }, { header: 'ZT-ADDR', value: (n) => n.ztAddress }, { header: 'NAME', value: (n) => n.name }, { header: 'IP', value: (n) => n.ip || dim('-') }, { header: 'AUTH', value: (n) => (n.authorized ? green('yes') : yellow('no')) }, { header: 'ONLINE', value: (n) => (n.online ? green('up') : red('down')) }, { header: 'AGENT', value: (n) => (n.agentActive ? green('active') : dim('idle')) }, { header: 'PLATFORM', value: (n) => n.platform || dim('-') }, ]); } async function cmdNodesGet(args) { const id = args[0]; if (!id) fail('Usage: splexa nodes get '); const r = await api('GET', `/api/v1/nodes/${encodeURIComponent(id)}`); ok(JSON.stringify(r.data, null, 2)); } async function cmdNodesAuthorize(args) { const id = args[0]; if (!id) fail('Usage: splexa nodes authorize '); const r = await api('POST', `/api/v1/nodes/${encodeURIComponent(id)}/authorize`, { body: {} }); ok(green('✓ ') + `Authorized ${r.data.id}`); } async function cmdNodesRevoke(args) { const id = args[0]; if (!id) fail('Usage: splexa nodes revoke '); const r = await api('POST', `/api/v1/nodes/${encodeURIComponent(id)}/revoke`, { body: {} }); ok(yellow('✓ ') + `Revoked ${r.data.id}`); } async function cmdOpenapi(args) { const flags = parseFlags(args, { '--out': 'string' }); // Spec endpoint is public — call without auth. const cfg = effectiveConfig(); const url = `${cfg.apiBase.replace(/\/+$/, '')}/api/v1/openapi.json`; const res = await fetch(url); if (!res.ok) fail(`Failed to fetch spec: HTTP ${res.status}`); const text = await res.text(); if (flags['--out']) { fs.writeFileSync(flags['--out'], text); ok(green('✓ ') + `Wrote spec to ${flags['--out']} (${text.length} bytes)`); } else { process.stdout.write(text); } } function cmdHelp() { ok(`${bold('splexa')} — SPLEXA Universe command-line interface (v${pkgVersion()}) ${bold('USAGE')} splexa [args] ${bold('AUTH')} splexa login Save API base + key to local config splexa logout Forget the saved API key splexa whoami Show current company + endpoint catalog ${bold('NETWORKS')} splexa networks list [--json] splexa networks get ${bold('NODES (devices)')} splexa nodes list --network [--json] splexa nodes get splexa nodes authorize splexa nodes revoke ${bold('TOOLING')} splexa openapi [--out spec.json] Download the OpenAPI 3.1 spec ${bold('ENVIRONMENT')} SPLEXA_API_KEY Override the stored API key (CI-friendly) SPLEXA_API_BASE Override the stored API base URL ${bold('CONFIG')} ${configPath()} `); } // ------------------------------ flag parser -------------------------------- function parseFlags(argv, schema) { const out = {}; const positional = []; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (a in schema) { if (schema[a] === 'bool') { out[a] = true; } else { out[a] = argv[++i]; } } else { positional.push(a); } } // Replace argv items with the positionals so commands can use args[0] etc. argv.length = 0; argv.push(...positional); return out; } // --------------------------------- main ------------------------------------ async function main() { const argv = process.argv.slice(2); const cmd = argv.shift(); const sub = argv[0]; try { switch (cmd) { case 'login': return await cmdLogin(argv); case 'logout': return await cmdLogout(); case 'whoami': return await cmdWhoami(); case 'openapi': return await cmdOpenapi(argv); case 'networks': argv.shift(); if (sub === 'list' || sub === 'ls') return await cmdNetworksList(argv); if (sub === 'get' || sub === 'show') return await cmdNetworksGet(argv); fail('Usage: splexa networks '); break; case 'nodes': case 'devices': argv.shift(); if (sub === 'list' || sub === 'ls') return await cmdNodesList(argv); if (sub === 'get' || sub === 'show') return await cmdNodesGet(argv); if (sub === 'authorize' || sub === 'approve') return await cmdNodesAuthorize(argv); if (sub === 'revoke' || sub === 'deauthorize') return await cmdNodesRevoke(argv); fail('Usage: splexa nodes '); break; case '-h': case '--help': case 'help': case undefined: return cmdHelp(); case '--version': case '-v': return ok(pkgVersion()); default: fail(`Unknown command: ${cmd}. Run 'splexa help'.`); } } catch (e) { if (e && e.message) fail(e.message); throw e; } } main();