Team Ai
Apppublic

Mega-Evolutions/Shellular

sourceHugging Faceupdated 1mo agoView on Hugging Face
1likes
app.js307 linesDownload Raw Back to root
1import express from 'express';2import { spawn } from 'child_process';3import crypto from 'crypto';4import fs from 'fs';5import os from 'os';6import path from 'path';7import { fileURLToPath } from 'url';8 9const __dirname = path.dirname(fileURLToPath(import.meta.url));10 11const app = express();12const PORT = process.env.PORT || 3000;13const SECRET_KEY = process.env.SECRET_KEY;14 15if (!SECRET_KEY) {16  console.error('[ERROR] SECRET_KEY environment variable is not set. Exiting.');17  process.exit(1);18}19 20app.use(express.json());21app.use(express.static(path.join(__dirname, 'public')));22 23const sessions = new Set();24 25function stripAnsi(str) {26  return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><~]/g, '');27}28 29function seedShellularConfig() {30  const hostId    = process.env.SHELLULAR_HOST_ID;31  const keyB64    = process.env.SHELLULAR_KEY;32  const machineId = process.env.SHELLULAR_MACHINE_ID;33 34  if (!hostId || !keyB64 || !machineId) return;35 36  // Only seed if the saved SHELLULAR_MACHINE_ID matches the actual machine ID.37  // If they differ (e.g. new build generated a different machine-id), skip seeding38  // so shellular registers fresh rather than failing with "Machine ID mismatch".39  const actualMachineId = getHashedMachineId();40  if (actualMachineId && actualMachineId !== machineId) {41    console.warn(`[shellular] machine-id mismatch — skipping seed (set MACHINE_ID_RAW to fix)`);42    return;43  }44 45  const shellularDir = path.join(os.homedir(), '.shellular');46  const configFile   = path.join(shellularDir, 'config.json');47  const keyFile      = path.join(shellularDir, `shellular-${machineId}.e2ee`);48 49  try {50    fs.mkdirSync(shellularDir, { recursive: true });51    if (!fs.existsSync(configFile)) {52      fs.writeFileSync(configFile, JSON.stringify({ hostId, machineId }), 'utf-8');53      console.log(`[shellular] seeded config: hostId=${hostId}`);54    }55    if (!fs.existsSync(keyFile)) {56      fs.writeFileSync(keyFile, Buffer.from(keyB64, 'base64'), { mode: 0o600 });57      console.log(`[shellular] seeded key: ${keyFile}`);58    }59  } catch (err) {60    console.error('[shellular] failed to seed config:', err.message);61  }62}63 64function getHashedMachineId() {65  try {66    const raw = process.env.MACHINE_ID_RAW ||67                fs.readFileSync('/etc/machine-id', 'utf-8').trim();68    if (!raw) return null;69    return crypto.createHash('sha256').update(raw).digest('hex');70  } catch {71    return null;72  }73}74 75seedShellularConfig();76 77app.get('/api/shellular/machine-id', (_req, res) => {78  const id = getHashedMachineId();79  id ? res.json({ machineId: id }) : res.status(500).json({ error: 'Cannot read machine-id' });80});81 82app.get('/api/shellular/raw-machine-id', (_req, res) => {83  try {84    const raw = process.env.MACHINE_ID_RAW ||85                fs.readFileSync('/etc/machine-id', 'utf-8').trim();86    raw ? res.json({ raw }) : res.status(500).json({ error: 'Not available' });87  } catch {88    res.status(500).json({ error: 'Not available' });89  }90});91 92app.post('/api/shellular/seed-host', requireAuth, (req, res) => {93  const { hostId } = req.body || {};94  if (!hostId || typeof hostId !== 'string' || !hostId.trim()) {95    return res.status(400).json({ error: 'hostId is required' });96  }97  const machineId = getHashedMachineId();98  if (!machineId) return res.status(500).json({ error: 'Cannot read machine-id' });99 100  try {101    const shellularDir = path.join(os.homedir(), '.shellular');102    fs.mkdirSync(shellularDir, { recursive: true });103    fs.writeFileSync(104      path.join(shellularDir, 'config.json'),105      JSON.stringify({ hostId: hostId.trim(), machineId }, null, 2),106      'utf-8'107    );108    stopShellular();109    outputBuffer = '';110    broadcast({ type: 'clear' });111    setTimeout(startShellular, 600);112    res.json({ ok: true });113  } catch (err) {114    res.status(500).json({ error: err.message });115  }116});117 118app.post('/api/login', (req, res) => {119  const { key } = req.body;120  if (!key || key !== SECRET_KEY) {121    return res.status(401).json({ error: 'Invalid key' });122  }123  const token = crypto.randomUUID();124  sessions.add(token);125  res.json({ token });126});127 128app.post('/api/logout', (req, res) => {129  const token = req.headers.authorization?.split(' ')[1];130  sessions.delete(token);131  res.json({ ok: true });132});133 134function requireAuth(req, res, next) {135  const token = req.headers.authorization?.split(' ')[1];136  if (!token || !sessions.has(token)) {137    return res.status(401).json({ error: 'Unauthorized' });138  }139  next();140}141 142let shellularProc = null;143let outputBuffer  = '';144let retryTimer    = null;145const sseClients  = new Set();146 147function send(res, payload) {148  res.write(`data: ${JSON.stringify(payload)}\n\n`);149}150 151function broadcast(payload) {152  const frame = `data: ${JSON.stringify(payload)}\n\n`;153  for (const client of sseClients) {154    client.write(frame);155  }156}157 158function startShellular() {159  if (shellularProc || retryTimer) return;160 161  broadcast({ type: 'status', status: 'starting' });162 163  shellularProc = spawn('npx', ['shellular', '--unknown-clients', 'always-allow'], {164    env: { ...process.env, FORCE_COLOR: '0' },165    stdio: ['ignore', 'pipe', 'pipe'],166  });167 168  let procOutput = '';169 170  const handleData = (chunk) => {171    const text = stripAnsi(chunk.toString());172    procOutput   += text;173    outputBuffer += text;174    broadcast({ type: 'output', text });175  };176 177  shellularProc.stdout.on('data', handleData);178  shellularProc.stderr.on('data', handleData);179 180  shellularProc.on('error', (err) => {181    const text = `\n[spawn error] ${err.message}\n`;182    outputBuffer += text;183    broadcast({ type: 'output', text });184    shellularProc = null;185    broadcast({ type: 'status', status: 'error' });186  });187 188  shellularProc.on('exit', (code, signal) => {189    shellularProc = null;190 191    const isMachineIdMismatch = procOutput.includes('Machine ID mismatch');192    const isRegError = code === 1 && !signal && !isMachineIdMismatch &&193      (procOutput.includes('invalid_union') || procOutput.includes('Too many requests') ||194       procOutput.includes('host registration'));195 196    if (isRegError) {197      const WAIT = 30;198      const msg = `\n⚠ Registration rate-limited by shellular API.\n  Retrying automatically in ${WAIT}s — please wait…\n`;199      outputBuffer += msg;200      broadcast({ type: 'output', text: msg });201      broadcast({ type: 'status', status: 'retrying' });202 203      retryTimer = setTimeout(() => {204        retryTimer = null;205        const msg2 = '\n[Retrying registration…]\n';206        outputBuffer += msg2;207        broadcast({ type: 'output', text: msg2 });208        startShellular();209      }, WAIT * 1000);210    } else {211      const text = code !== 0212        ? `\n[shellular exited — code=${code ?? '?'}, signal=${signal ?? 'none'}]\n`213        : '\n[shellular disconnected]\n';214      outputBuffer += text;215      broadcast({ type: 'output', text });216      broadcast({ type: 'status', status: 'stopped' });217    }218  });219 220  broadcast({ type: 'status', status: 'running' });221}222 223function stopShellular() {224  if (retryTimer) { clearTimeout(retryTimer); retryTimer = null; }225  if (!shellularProc) return;226  shellularProc.kill('SIGTERM');227  shellularProc = null;228}229 230app.get('/api/stream', requireAuth, (req, res) => {231  res.setHeader('Content-Type', 'text/event-stream');232  res.setHeader('Cache-Control', 'no-cache');233  res.setHeader('Connection', 'keep-alive');234  res.setHeader('X-Accel-Buffering', 'no');235  res.flushHeaders();236 237  send(res, { type: 'status', status: shellularProc ? 'running' : 'stopped' });238  if (outputBuffer) send(res, { type: 'output', text: outputBuffer });239 240  sseClients.add(res);241  req.on('close', () => sseClients.delete(res));242});243 244app.post('/api/shellular/start', requireAuth, (_req, res) => {245  startShellular();246  res.json({ ok: true, running: !!shellularProc });247});248 249app.post('/api/shellular/stop', requireAuth, (_req, res) => {250  stopShellular();251  outputBuffer = '';252  broadcast({ type: 'output', text: '' });253  res.json({ ok: true });254});255 256app.post('/api/shellular/restart', requireAuth, (_req, res) => {257  stopShellular();258  outputBuffer = '';259  broadcast({ type: 'clear' });260  setTimeout(startShellular, 600);261  res.json({ ok: true });262});263 264app.get('/api/status', requireAuth, (_req, res) => {265  res.json({ running: !!shellularProc });266});267 268app.get('/api/setup-status', requireAuth, (_req, res) => {269  const seeded = !!(270    process.env.SHELLULAR_HOST_ID &&271    process.env.SHELLULAR_KEY &&272    process.env.SHELLULAR_MACHINE_ID &&273    process.env.MACHINE_ID_RAW274  );275  res.json({ seeded });276});277 278app.get('/api/shellular/credentials', requireAuth, (_req, res) => {279  try {280    const shellularDir = path.join(os.homedir(), '.shellular');281    const configRaw    = fs.readFileSync(path.join(shellularDir, 'config.json'), 'utf-8');282    const { hostId, machineId } = JSON.parse(configRaw);283    const keyFile = path.join(shellularDir, `shellular-${machineId}.e2ee`);284    const keyB64  = fs.readFileSync(keyFile).toString('base64');285    res.json({ hostId, machineId, keyB64 });286  } catch {287    res.status(404).json({ error: 'Not registered yet.' });288  }289});290 291app.get('/api/shellular/qr-data', requireAuth, (_req, res) => {292  try {293    const shellularDir = path.join(os.homedir(), '.shellular');294    const configRaw    = fs.readFileSync(path.join(shellularDir, 'config.json'), 'utf-8');295    const { hostId, machineId } = JSON.parse(configRaw);296    const keyFile = path.join(shellularDir, `shellular-${machineId}.e2ee`);297    const keyB64  = fs.readFileSync(keyFile).toString('base64');298    res.json({ qrData: `${hostId}:${keyB64}` });299  } catch {300    res.status(404).json({ error: 'Config not seeded yet.' });301  }302});303 304app.listen(PORT, '0.0.0.0', () => {305  console.log(`Shellular Web UI → http://0.0.0.0:${PORT}`);306});307