2026-01-24 22:59:20 +00:00
#!/usr/bin/env node
/**
* Archipelago Mock Backend Server
* Pure Archipelago implementation - NO StartOS dependencies
* Supports dev modes: setup, onboarding, existing
*/
import express from 'express'
import cors from 'cors'
import cookieParser from 'cookie-parser'
import { WebSocketServer } from 'ws'
import http from 'http'
import { exec } from 'child_process'
import { promisify } from 'util'
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
2026-01-27 23:06:18 +00:00
import Docker from 'dockerode'
2026-01-24 22:59:20 +00:00
const __filename = fileURLToPath ( import . meta . url )
const __dirname = path . dirname ( __filename )
const execPromise = promisify ( exec )
2026-01-27 23:06:18 +00:00
const docker = new Docker ()
2026-01-24 22:59:20 +00:00
const app = express ()
const PORT = 5959
// Dev mode from environment (setup, onboarding, existing, or default)
const DEV_MODE = process . env . VITE_DEV_MODE || 'default'
// CORS configuration
const corsOptions = {
credentials : true ,
origin : ( origin , callback ) => {
if ( ! origin ) return callback ( null , true )
callback ( null , true )
}
}
app . use ( cors ( corsOptions ))
app . use ( express . json ())
app . use ( cookieParser ())
// Mock session storage
const sessions = new Map ()
const MOCK_PASSWORD = 'password123'
// User state (simulated file-based storage)
let userState = {
setupComplete : false ,
onboardingComplete : false ,
passwordHash : null , // In real app, this would be bcrypt hash
}
// Initialize user state based on dev mode
function initializeUserState () {
switch ( DEV_MODE ) {
case 'setup' :
// Setup mode: Original StartOS node setup - user needs to set password
// This is the simple password setup, NOT the experimental onboarding
userState = {
setupComplete : false , // User hasn't set password yet
onboardingComplete : false , // Onboarding not relevant for setup mode
passwordHash : null ,
}
break
case 'onboarding' :
// Onboarding mode: Experimental onboarding flow
// User has set password (via setup) but needs to go through experimental onboarding
userState = {
setupComplete : true , // Password already set
onboardingComplete : false , // Needs experimental onboarding
passwordHash : MOCK_PASSWORD ,
}
break
case 'existing' :
// Existing user: Fully set up, just needs to login
userState = {
setupComplete : true ,
onboardingComplete : true ,
passwordHash : MOCK_PASSWORD ,
}
break
default :
// Default: Fully set up (for UI development)
userState = {
setupComplete : true ,
onboardingComplete : true ,
passwordHash : MOCK_PASSWORD ,
}
}
console . log ( `[Auth] Dev mode: ${ DEV_MODE } ` )
console . log ( `[Auth] Setup: ${ userState . setupComplete } , Onboarding: ${ userState . onboardingComplete } ` )
}
initializeUserState ()
// WebSocket clients for broadcasting updates
const wsClients = new Set ()
// Helper: Broadcast data update to all WebSocket clients
function broadcastUpdate ( patch ) {
const message = JSON . stringify ({
rev : Date . now (),
patch : patch
})
wsClients . forEach ( client => {
if ( client . readyState === 1 ) { // OPEN
client . send ( message )
}
})
}
// Track used ports and running containers
const usedPorts = new Set ([ 5959 , 8100 ])
const runningContainers = new Map ()
// Predefined port mappings for known apps
const portMappings = {
'atob' : 8102 ,
'k484' : 8103 ,
'amin' : 8104
}
2026-01-27 23:06:18 +00:00
// Helper: Query real Docker containers
async function getDockerContainers () {
try {
const containers = await docker . listContainers ({ all : true })
// Map of container names to app IDs
const containerMapping = {
'archy-bitcoin' : 'bitcoin' ,
'archy-btcpay' : 'btcpay-server' ,
'archy-homeassistant' : 'homeassistant' ,
'archy-grafana' : 'grafana' ,
'archy-endurain' : 'endurain' ,
'archy-fedimint' : 'fedimint' ,
'archy-morphos' : 'morphos-server' ,
'archy-lnd' : 'lightning-stack' ,
'archy-mempool-web' : 'mempool' ,
2026-02-17 15:03:34 +00:00
'mempool-electrs' : 'mempool-electrs' ,
2026-01-27 23:06:18 +00:00
'archy-ollama' : 'ollama' ,
'archy-searxng' : 'searxng' ,
'archy-onlyoffice' : 'onlyoffice' ,
'archy-penpot-frontend' : 'penpot'
}
const apps = {}
for ( const container of containers ) {
const name = container . Names [ 0 ]. replace ( /^\// , '' )
const appId = containerMapping [ name ]
if ( ! appId ) continue
const isRunning = container . State === 'running'
const ports = container . Ports || []
const hostPort = ports . find ( p => p . PublicPort ) ? . PublicPort || null
// Get app metadata
const appMetadata = {
'bitcoin' : {
title : 'Bitcoin Core' ,
icon : '/assets/img/app-icons/bitcoin.svg' ,
description : 'Full Bitcoin node implementation'
},
'btcpay-server' : {
title : 'BTCPay Server' ,
icon : '/assets/img/app-icons/btcpay-server.png' ,
description : 'Self-hosted Bitcoin payment processor'
},
'homeassistant' : {
title : 'Home Assistant' ,
icon : '/assets/img/app-icons/homeassistant.png' ,
description : 'Open source home automation platform'
},
'grafana' : {
title : 'Grafana' ,
icon : '/assets/img/grafana.png' ,
description : 'Analytics and monitoring platform'
},
'endurain' : {
title : 'Endurain' ,
icon : '/assets/img/endurain.png' ,
description : 'Application platform'
},
'fedimint' : {
title : 'Fedimint' ,
2026-02-17 15:03:34 +00:00
icon : '/assets/img/app-icons/fedimint.png' ,
2026-01-27 23:06:18 +00:00
description : 'Federated Bitcoin mint'
},
'morphos-server' : {
title : 'MorphOS Server' ,
icon : '/assets/img/morphos.png' ,
description : 'Server platform'
},
'lightning-stack' : {
title : 'Lightning Stack' ,
icon : '/assets/img/app-icons/lightning-stack.png' ,
description : 'Lightning Network (LND)'
},
'mempool' : {
title : 'Mempool' ,
icon : '/assets/img/app-icons/mempool.png' ,
description : 'Bitcoin blockchain explorer'
},
2026-02-17 15:03:34 +00:00
'mempool-electrs' : {
title : 'Electrs' ,
icon : '/assets/img/app-icons/electrs.svg' ,
description : 'Electrum protocol indexer for Bitcoin'
},
2026-01-27 23:06:18 +00:00
'ollama' : {
title : 'Ollama' ,
2026-02-01 18:46:35 +00:00
icon : '/assets/img/app-icons/ollama.png' ,
2026-01-27 23:06:18 +00:00
description : 'Run large language models locally'
},
'searxng' : {
title : 'SearXNG' ,
icon : '/assets/img/app-icons/searxng.png' ,
description : 'Privacy-respecting metasearch engine'
},
'onlyoffice' : {
title : 'OnlyOffice' ,
icon : '/assets/img/onlyoffice.webp' ,
description : 'Office suite and document collaboration'
},
'penpot' : {
title : 'Penpot' ,
icon : '/assets/img/penpot.webp' ,
description : 'Open-source design and prototyping'
}
}
const metadata = appMetadata [ appId ] || {
title : appId ,
2026-03-06 01:11:00 +00:00
icon : '/assets/icon/pwa-192x192-v2.png' ,
2026-01-27 23:06:18 +00:00
description : ` ${ appId } application`
}
apps [ appId ] = {
title : metadata . title ,
version : '1.0.0' ,
status : isRunning ? 'running' : 'stopped' ,
state : isRunning ? 'running' : 'stopped' ,
'static-files' : {
license : 'MIT' ,
instructions : metadata . description ,
icon : metadata . icon
},
manifest : {
id : appId ,
title : metadata . title ,
version : '1.0.0' ,
description : {
short : metadata . description ,
long : metadata . description
},
'release-notes' : 'Initial release' ,
license : 'MIT' ,
'wrapper-repo' : '#' ,
'upstream-repo' : '#' ,
'support-site' : '#' ,
'marketing-site' : '#' ,
'donation-url' : null ,
interfaces : hostPort ? {
main : {
name : 'Web Interface' ,
description : ` ${ metadata . title } web interface` ,
ui : true
}
} : {}
},
installed : {
'current-dependents' : {},
'current-dependencies' : {},
'last-backup' : null ,
'interface-addresses' : hostPort ? {
main : {
'tor-address' : ` ${ appId } .onion` ,
'lan-address' : `http://localhost: ${ hostPort } `
}
} : {},
status : isRunning ? 'running' : 'stopped'
}
}
}
return apps
} catch ( error ) {
console . error ( '[Docker] Error querying containers:' , error . message )
return {}
}
}
2026-01-24 22:59:20 +00:00
// Helper: Check if Docker/Podman is available
async function isContainerRuntimeAvailable () {
try {
// Try Podman first (Archipelago's choice)
await execPromise ( 'podman ps' )
return { available : true , runtime : 'podman' }
} catch {
try {
// Fallback to Docker
await execPromise ( 'docker ps' )
return { available : true , runtime : 'docker' }
} catch {
return { available : false , runtime : null }
}
}
}
// Helper: Install package with container runtime (if available) or simulate
async function installPackage ( id , manifestUrl ) {
console . log ( `[Package] 📦 Installing ${ id } ...` )
try {
// Check if already installed
if ( mockData [ 'package-data' ][ id ]) {
throw new Error ( `Package ${ id } is already installed` )
}
const version = '0.1.0'
const runtime = await isContainerRuntimeAvailable ()
// Get package metadata
const packageMetadata = {
'atob' : {
title : 'A to B Bitcoin' ,
shortDesc : 'Bitcoin tools and services for seamless transactions' ,
longDesc : 'A to B Bitcoin provides tools and services for Bitcoin transactions.' ,
icon : '/assets/img/atob.png'
},
'k484' : {
title : 'K484' ,
shortDesc : 'Point of Sale and Admin system' ,
longDesc : 'K484 provides a complete POS and administration system.' ,
icon : '/assets/img/k484.png'
},
'amin' : {
title : 'Amin' ,
shortDesc : 'Administrative interface for Archipelago' ,
longDesc : 'Amin provides administrative tools and monitoring.' ,
2026-03-06 01:11:00 +00:00
icon : '/assets/icon/pwa-192x192-v2.png'
2026-01-24 22:59:20 +00:00
}
}
2026-03-06 01:11:00 +00:00
2026-01-24 22:59:20 +00:00
const metadata = packageMetadata [ id ] || {
title : id . charAt ( 0 ). toUpperCase () + id . slice ( 1 ),
shortDesc : ` ${ id } application` ,
longDesc : ` ${ id } application for Archipelago` ,
2026-03-06 01:11:00 +00:00
icon : '/assets/icon/pwa-192x192-v2.png'
2026-01-24 22:59:20 +00:00
}
// Determine port
const assignedPort = portMappings [ id ] || 8105
usedPorts . add ( assignedPort )
let containerMode = false
let actuallyRunning = false
// Try to run with container runtime if available
if ( runtime . available ) {
try {
console . log ( `[Package] 🐳 ${ runtime . runtime } available, attempting to run container...` )
const containerName = ` ${ id } -archipelago`
const stopCmd = runtime . runtime === 'podman'
? `podman stop ${ containerName } 2>/dev/null || true`
: `docker stop ${ containerName } 2>/dev/null || true`
const rmCmd = runtime . runtime === 'podman'
? `podman rm ${ containerName } 2>/dev/null || true`
: `docker rm ${ containerName } 2>/dev/null || true`
// Stop and remove existing container if it exists
await execPromise ( stopCmd )
await execPromise ( rmCmd )
// Check if image exists
const imageCheckCmd = runtime . runtime === 'podman'
? `podman images -q ${ id } : ${ version } `
: `docker images -q ${ id } : ${ version } `
let { stdout } = await execPromise ( imageCheckCmd )
if ( stdout . trim ()) {
// Image exists, start container
const runCmd = runtime . runtime === 'podman'
? `podman run -d --name ${ containerName } -p ${ assignedPort } :80 ${ id } : ${ version } `
: `docker run -d --name ${ containerName } -p ${ assignedPort } :80 ${ id } : ${ version } `
await execPromise ( runCmd )
// Wait for container to be ready
await new Promise ( resolve => setTimeout ( resolve , 2000 ))
// Verify container is running
const statusCmd = runtime . runtime === 'podman'
? `podman ps --filter name= ${ containerName } --format "{{.Status}}"`
: `docker ps --filter name= ${ containerName } --format "{{.Status}}"`
const { stdout : containerStatus } = await execPromise ( statusCmd )
if ( containerStatus . includes ( 'Up' )) {
containerMode = true
actuallyRunning = true
runningContainers . set ( id , {
port : assignedPort ,
containerId : containerName ,
runtime : runtime . runtime
})
console . log ( `[Package] 🐳 ${ runtime . runtime } container running on port ${ assignedPort } ` )
}
} else {
console . log ( `[Package] ℹ ️ Container image ${ id } : ${ version } not found, using simulation mode` )
}
} catch ( containerError ) {
console . log ( `[Package] ⚠️ Container error ( ${ containerError . message } ), falling back to simulation` )
}
} else {
console . log ( `[Package] ℹ ️ Container runtime not available, using simulation mode` )
}
// If container didn't work, simulate installation
if ( ! containerMode ) {
await new Promise ( resolve => setTimeout ( resolve , 1500 ))
runningContainers . set ( id , { port : assignedPort , containerId : null , runtime : null })
}
// Add to mock data
mockData [ 'package-data' ][ id ] = {
title : metadata . title ,
version : version ,
status : 'running' ,
state : 'running' ,
port : assignedPort ,
containerMode : containerMode ,
actuallyRunning : actuallyRunning ,
manifest : {
id : id ,
title : metadata . title ,
version : version ,
description : {
short : metadata . shortDesc ,
long : metadata . longDesc
},
icon : metadata . icon ,
interfaces : {
main : {
name : 'Web Interface' ,
description : ` ${ metadata . title } web interface` ,
ui : true ,
},
},
},
}
// Broadcast update
broadcastUpdate ([
{
op : 'add' ,
path : `/package-data/ ${ id } ` ,
value : mockData [ 'package-data' ][ id ]
}
])
if ( containerMode ) {
console . log ( `[Package] ✅ ${ id } installed and RUNNING at http://localhost: ${ assignedPort } ` )
} else {
console . log ( `[Package] ✅ ${ id } installed (simulated)` )
}
return { success : true , port : assignedPort , containerMode }
} catch ( error ) {
console . error ( `[Package] ❌ Installation failed:` , error . message )
throw error
}
}
// Helper: Uninstall package
async function uninstallPackage ( id ) {
console . log ( `[Package] 🗑️ Uninstalling ${ id } ...` )
try {
2026-02-18 08:30:12 +00:00
if ( staticDevApps [ id ]) {
throw new Error ( ` ${ id } is a demo app and cannot be uninstalled` )
}
2026-01-24 22:59:20 +00:00
if ( ! mockData [ 'package-data' ][ id ]) {
throw new Error ( `Package ${ id } is not installed` )
}
// Stop container if it's running
const containerInfo = runningContainers . get ( id )
if ( containerInfo && containerInfo . containerId ) {
try {
const runtime = containerInfo . runtime || 'docker'
const stopCmd = runtime === 'podman'
? `podman stop ${ containerInfo . containerId } 2>/dev/null || true`
: `docker stop ${ containerInfo . containerId } 2>/dev/null || true`
const rmCmd = runtime === 'podman'
? `podman rm ${ containerInfo . containerId } 2>/dev/null || true`
: `docker rm ${ containerInfo . containerId } 2>/dev/null || true`
console . log ( `[Package] 🐳 Stopping container ${ containerInfo . containerId } ...` )
await execPromise ( stopCmd )
await execPromise ( rmCmd )
console . log ( `[Package] 🐳 Container stopped` )
} catch ( error ) {
console . log ( `[Package] ⚠️ Error stopping container: ${ error . message } ` )
}
}
await new Promise ( resolve => setTimeout ( resolve , 1000 ))
const port = mockData [ 'package-data' ][ id ]. port
if ( port ) {
usedPorts . delete ( port )
}
runningContainers . delete ( id )
delete mockData [ 'package-data' ][ id ]
broadcastUpdate ([
{
op : 'remove' ,
path : `/package-data/ ${ id } `
}
])
console . log ( `[Package] ✅ ${ id } uninstalled successfully` )
return { success : true }
} catch ( error ) {
console . error ( `[Package] ❌ Uninstall failed:` , error . message )
throw error
}
}
// Mock data
const mockData = {
'server-info' : {
id : 'archipelago-dev' ,
version : '0.1.0' ,
name : 'Archipelago Dev Server' ,
pubkey : 'mock-pubkey' ,
'status-info' : {
restarting : false ,
'shutting-down' : false ,
updated : false ,
'backup-progress' : null ,
'update-progress' : null ,
},
'lan-address' : '192.168.1.100' ,
unread : 0 ,
'wifi-ssids' : [],
'zram-enabled' : false ,
},
2026-01-27 23:06:18 +00:00
'package-data' : {}, // Will be populated from Docker
2026-01-24 22:59:20 +00:00
ui : {
name : 'Archipelago' ,
'ack-welcome' : '0.1.0' ,
marketplace : {
'selected-hosts' : [],
'known-hosts' : {},
},
theme : 'dark' ,
},
}
2026-02-18 08:30:12 +00:00
// Static dev apps (always shown in My Apps when using mock backend)
const staticDevApps = {
'lorabell' : {
title : 'LoraBell' ,
version : '1.0.0' ,
status : 'running' ,
state : 'running' ,
'static-files' : {
license : 'MIT' ,
instructions : 'A LoRa based doorbell' ,
icon : '/assets/img/app-icons/lorabell.png'
},
manifest : {
id : 'lorabell' ,
title : 'LoraBell' ,
version : '1.0.0' ,
description : {
short : 'A LoRa based doorbell' ,
long : 'A LoRa based doorbell - receive doorbell notifications over LoRa radio.'
},
'release-notes' : 'Initial release' ,
license : 'MIT' ,
'wrapper-repo' : '#' ,
'upstream-repo' : '#' ,
'support-site' : '#' ,
'marketing-site' : '#' ,
'donation-url' : null ,
interfaces : {
main : {
name : 'Web Interface' ,
description : 'LoraBell web interface' ,
ui : true
}
}
},
installed : {
'current-dependents' : {},
'current-dependencies' : {},
'last-backup' : null ,
'interface-addresses' : {
main : {
'tor-address' : 'lorabell.onion' ,
2026-02-18 08:42:24 +00:00
'lan-address' : 'http://192.168.1.166'
2026-02-18 08:30:12 +00:00
}
},
status : 'running'
}
}
}
function mergePackageData ( dockerApps ) {
return { ... dockerApps , ... staticDevApps }
}
2026-01-27 23:06:18 +00:00
// Initialize package data from Docker on startup
async function initializePackageData () {
console . log ( '[Docker] Querying running containers...' )
const dockerApps = await getDockerContainers ()
2026-02-18 08:30:12 +00:00
mockData [ 'package-data' ] = mergePackageData ( dockerApps )
2026-01-27 23:06:18 +00:00
2026-02-18 08:30:12 +00:00
const appCount = Object . keys ( mockData [ 'package-data' ]). length
const runningCount = Object . values ( mockData [ 'package-data' ]). filter ( app => app . state === 'running' ). length
2026-01-27 23:06:18 +00:00
console . log ( `[Docker] Found ${ appCount } containers ( ${ runningCount } running)` )
if ( appCount > 0 ) {
console . log ( '[Docker] Apps detected:' )
2026-02-18 08:30:12 +00:00
Object . entries ( mockData [ 'package-data' ]). forEach (([ id , app ]) => {
2026-01-27 23:06:18 +00:00
const port = app . installed ? .[ 'interface-addresses' ] ? . main ? .[ 'lan-address' ]
console . log ( ` - ${ app . title } ( ${ app . state } ) ${ port ? ` → ${ port } ` : '' } ` )
})
} else {
console . log ( '[Docker] No containers found. Start docker-compose to see apps.' )
}
}
2026-01-24 22:59:20 +00:00
// Handle CORS preflight
app . options ( '/rpc/v1' , ( req , res ) => {
res . status ( 200 ). end ()
})
// RPC endpoint
app . post ( '/rpc/v1' , ( req , res ) => {
const { method , params } = req . body
console . log ( `[RPC] ${ method } ` )
try {
switch ( method ) {
// Authentication endpoints
case 'auth.setup' : {
const { password } = params
if ( ! password || password . length < 8 ) {
return res . json ({
error : {
code : - 32602 ,
message : 'Password must be at least 8 characters' ,
},
})
}
// Set up user
userState . setupComplete = true
userState . passwordHash = password // In real app, bcrypt hash
console . log ( `[Auth] User setup completed` )
return res . json ({ result : { success : true } })
}
case 'auth.isSetup' : {
return res . json ({ result : userState . setupComplete })
}
case 'auth.onboardingComplete' : {
userState . onboardingComplete = true
console . log ( `[Auth] Onboarding completed` )
2026-02-17 15:03:34 +00:00
return res . json ({ result : true })
2026-01-24 22:59:20 +00:00
}
case 'auth.isOnboardingComplete' : {
return res . json ({ result : userState . onboardingComplete })
}
2026-03-02 08:34:13 +00:00
case 'auth.resetOnboarding' : {
userState . onboardingComplete = false
console . log ( '[Auth] Onboarding reset' )
return res . json ({ result : true })
}
2026-02-17 15:03:34 +00:00
case 'node.did' : {
const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'
const mockPubkey = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456'
return res . json ({ result : { did : mockDid , pubkey : mockPubkey } })
}
case 'node.nostr-publish' : {
return res . json ({ result : { event_id : 'mock-event-id' , success : 2 , failed : 0 } })
}
case 'node.nostr-pubkey' : {
return res . json ({ result : { nostr_pubkey : 'mock-nostr-pubkey-hex' } })
}
2026-03-02 08:34:13 +00:00
case 'node.signChallenge' : {
const { challenge } = params || {}
const mockSig = Buffer . from ( `mock-sig- ${ challenge || 'challenge' } ` ). toString ( 'hex' )
return res . json ({ result : { signature : mockSig } })
}
case 'node.createBackup' : {
const { passphrase } = params || {}
if ( ! passphrase ) {
return res . json ({ error : { code : - 32602 , message : 'Missing passphrase' } })
}
const mockDid = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'
const mockPubkey = 'a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456'
return res . json ({
result : {
version : 1 ,
did : mockDid ,
pubkey : mockPubkey ,
kid : ` ${ mockDid } #key-1` ,
encrypted : true ,
blob : Buffer . from ( `mock-encrypted-backup- ${ passphrase } ` ). toString ( 'base64' ),
timestamp : new Date (). toISOString (),
},
})
}
2026-01-24 22:59:20 +00:00
case 'auth.login' : {
const { password } = params
if ( ! userState . setupComplete ) {
return res . json ({
error : {
code : - 32603 ,
message : 'User not set up. Please complete setup first.' ,
},
})
}
// Simple password check (in real app, use bcrypt)
if ( password !== userState . passwordHash && password !== MOCK_PASSWORD ) {
return res . json ({
error : {
code : - 32603 ,
message : 'Password Incorrect' ,
},
})
}
const sessionId = `session- ${ Date . now () } `
sessions . set ( sessionId , {
createdAt : new Date (),
})
res . cookie ( 'session' , sessionId , {
httpOnly : true ,
maxAge : 24 * 60 * 60 * 1000 ,
})
return res . json ({ result : null })
}
case 'auth.logout' : {
const sessionId = req . cookies ? . session
if ( sessionId ) {
sessions . delete ( sessionId )
}
res . clearCookie ( 'session' )
return res . json ({ result : null })
}
case 'server.echo' : {
return res . json ({ result : { message : params ? . message || 'Hello from Archipelago!' } })
}
case 'server.time' : {
return res . json ({
result : {
now : new Date (). toISOString (),
uptime : process . uptime (),
},
})
}
case 'server.metrics' : {
return res . json ({
result : {
cpu : 45.2 ,
memory : 62.8 ,
disk : 38.1 ,
},
})
}
case 'marketplace.get' : {
const mockApps = [
{
id : 'bitcoin' ,
title : 'Bitcoin Core' ,
description : 'A full Bitcoin node.' ,
version : '25.0.0' ,
icon : '/assets/img/bitcoin.svg' ,
author : 'Bitcoin Core Team' ,
license : 'MIT' ,
},
{
id : 'lightning' ,
title : 'Core Lightning' ,
description : 'Lightning Network implementation.' ,
version : '23.08' ,
icon : '/assets/img/c-lightning.png' ,
author : 'Blockstream' ,
license : 'MIT' ,
},
]
return res . json ({ result : mockApps })
}
case 'server.update' :
case 'server.restart' :
case 'server.shutdown' : {
return res . json ({ result : 'ok' })
}
case 'package.install' : {
const { id , url } = params
installPackage ( id , url ). catch ( err => {
console . error ( `[RPC] Installation failed:` , err . message )
})
return res . json ({ result : `job- ${ Date . now () } ` })
}
case 'package.uninstall' : {
const { id } = params
uninstallPackage ( id ). catch ( err => {
console . error ( `[RPC] Uninstall failed:` , err . message )
})
return res . json ({ result : 'ok' })
}
case 'package.start' :
case 'package.stop' :
case 'package.restart' : {
return res . json ({ result : 'ok' })
}
2026-03-06 12:23:57 +00:00
case 'auth.totp.status' : {
return res . json ({ result : { enabled : false } })
}
case 'auth.totp.setup.begin' : {
return res . json ({
result : {
qr_svg : '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><rect width="200" height="200" fill="#fff"/><text x="100" y="100" text-anchor="middle" font-size="12" fill="#333">Mock QR Code</text></svg>' ,
secret_base32 : 'JBSWY3DPEHPK3PXP' ,
pending_token : 'mock-pending-token' ,
},
})
}
case 'auth.totp.setup.confirm' : {
return res . json ({
result : {
enabled : true ,
backup_codes : [ 'ABCD-EFGH' , 'JKLM-NPQR' , 'STUV-WXYZ' , '2345-6789' , 'ABCD-2345' , 'EFGH-6789' , 'JKLM-STUV' , 'NPQR-WXYZ' ],
},
})
}
case 'auth.totp.disable' : {
return res . json ({ result : { disabled : true } })
}
case 'auth.login.totp' :
case 'auth.login.backup' : {
return res . json ({ result : { success : true } })
}
2026-03-07 22:36:45 +00:00
case 'node-messages-received' :
case 'node.messages' :
2026-03-08 00:27:34 +00:00
case 'node.notifications' :
case 'node-list-peers' : {
2026-03-07 22:36:45 +00:00
return res . json ({ result : [] })
}
2026-01-24 22:59:20 +00:00
default : {
2026-03-07 22:36:45 +00:00
console . log ( `[RPC] Unknown method: ${ method } ` )
2026-01-24 22:59:20 +00:00
return res . json ({
error : {
code : - 32601 ,
message : `Method not found: ${ method } ` ,
},
})
}
}
} catch ( error ) {
console . error ( '[RPC Error]' , error )
return res . json ({
error : {
code : - 32603 ,
message : error . message ,
},
})
}
})
2026-03-07 22:50:05 +00:00
// =============================================================================
// Mock FileBrowser API (for Cloud page in demo/Docker deployments)
// =============================================================================
const MOCK_FILES = {
'/' : [
{ name : 'Music' , path : '/Music' , size : 0 , modified : '2025-03-01T10:00:00Z' , isDir : true , type : '' },
{ name : 'Documents' , path : '/Documents' , size : 0 , modified : '2025-02-28T14:30:00Z' , isDir : true , type : '' },
{ name : 'Photos' , path : '/Photos' , size : 0 , modified : '2025-02-20T09:15:00Z' , isDir : true , type : '' },
{ name : 'Videos' , path : '/Videos' , size : 0 , modified : '2025-01-15T18:00:00Z' , isDir : true , type : '' },
],
'/Music' : [
{ name : 'Bad Actors Reveal.mp3' , path : '/Music/Bad Actors Reveal.mp3' , size : 8_400_000 , modified : '2025-01-10T12:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Architects of Tomorrow.wav' , path : '/Music/Architects of Tomorrow.wav' , size : 42_000_000 , modified : '2025-01-08T15:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Sats or Shackles.mp3' , path : '/Music/Sats or Shackles.mp3' , size : 6_200_000 , modified : '2024-12-20T10:00:00Z' , isDir : false , type : 'audio' },
{ name : 'The Four Horseman of Technocracy.mp3' , path : '/Music/The Four Horseman of Technocracy.mp3' , size : 7_800_000 , modified : '2024-12-15T11:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Inverse Dylan (Remix).mp3' , path : '/Music/Inverse Dylan (Remix).mp3' , size : 5_600_000 , modified : '2024-12-10T16:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Hootcoiner.mp3' , path : '/Music/Hootcoiner.mp3' , size : 4_200_000 , modified : '2024-11-28T09:00:00Z' , isDir : false , type : 'audio' },
{ name : 'decentrealisation.mp3' , path : '/Music/decentrealisation.mp3' , size : 5_100_000 , modified : '2024-11-20T14:00:00Z' , isDir : false , type : 'audio' },
{ name : 'neo-morality.mp3' , path : '/Music/neo-morality.mp3' , size : 6_800_000 , modified : '2024-11-15T11:00:00Z' , isDir : false , type : 'audio' },
{ name : 'death is a gift.mp3' , path : '/Music/death is a gift.mp3' , size : 4_500_000 , modified : '2024-11-10T08:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Wash the fucking dishes.mp3' , path : '/Music/Wash the fucking dishes.mp3' , size : 3_900_000 , modified : '2024-11-05T13:00:00Z' , isDir : false , type : 'audio' },
{ name : 'All the leaves are brown.mp3' , path : '/Music/All the leaves are brown.mp3' , size : 5_300_000 , modified : '2024-10-28T10:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Builders not talkers.mp3' , path : '/Music/Builders not talkers.mp3' , size : 4_700_000 , modified : '2024-10-20T15:00:00Z' , isDir : false , type : 'audio' },
{ name : 'SMRI.mp3' , path : '/Music/SMRI.mp3' , size : 5_900_000 , modified : '2024-10-15T12:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Shadrap.mp3' , path : '/Music/Shadrap.mp3' , size : 3_400_000 , modified : '2024-10-10T09:00:00Z' , isDir : false , type : 'audio' },
{ name : 'The Wehrman.mp3' , path : '/Music/The Wehrman.mp3' , size : 6_100_000 , modified : '2024-10-05T14:00:00Z' , isDir : false , type : 'audio' },
{ name : 'An Exploited Substrate.mp3' , path : '/Music/An Exploited Substrate.mp3' , size : 4_800_000 , modified : '2024-09-28T11:00:00Z' , isDir : false , type : 'audio' },
{ name : 'Govcucks.wav' , path : '/Music/Govcucks.wav' , size : 38_000_000 , modified : '2024-09-20T16:00:00Z' , isDir : false , type : 'audio' },
],
'/Documents' : [
{ name : 'bitcoin-whitepaper-notes.md' , path : '/Documents/bitcoin-whitepaper-notes.md' , size : 820 , modified : '2025-02-28T14:30:00Z' , isDir : false , type : 'text' },
{ name : 'node-setup-checklist.md' , path : '/Documents/node-setup-checklist.md' , size : 950 , modified : '2025-02-25T10:00:00Z' , isDir : false , type : 'text' },
{ name : 'lightning-channels.csv' , path : '/Documents/lightning-channels.csv' , size : 680 , modified : '2025-02-20T16:00:00Z' , isDir : false , type : 'text' },
{ name : 'sovereignty-manifesto.txt' , path : '/Documents/sovereignty-manifesto.txt' , size : 1100 , modified : '2025-02-15T12:00:00Z' , isDir : false , type : 'text' },
{ name : 'backup-log.json' , path : '/Documents/backup-log.json' , size : 1450 , modified : '2025-03-01T02:00:00Z' , isDir : false , type : 'text' },
],
'/Photos' : [
{ name : 'node-rack-setup.jpg' , path : '/Photos/node-rack-setup.jpg' , size : 2_400_000 , modified : '2025-02-20T09:15:00Z' , isDir : false , type : 'image' },
{ name : 'bitcoin-conference-2024.jpg' , path : '/Photos/bitcoin-conference-2024.jpg' , size : 3_100_000 , modified : '2024-12-15T14:30:00Z' , isDir : false , type : 'image' },
{ name : 'lightning-network-visualization.png' , path : '/Photos/lightning-network-visualization.png' , size : 1_800_000 , modified : '2025-01-10T11:00:00Z' , isDir : false , type : 'image' },
{ name : 'home-server-build.jpg' , path : '/Photos/home-server-build.jpg' , size : 2_900_000 , modified : '2024-11-20T16:45:00Z' , isDir : false , type : 'image' },
{ name : 'sunset-from-balcony.jpg' , path : '/Photos/sunset-from-balcony.jpg' , size : 4_200_000 , modified : '2025-02-14T18:30:00Z' , isDir : false , type : 'image' },
],
'/Videos' : [
{ name : 'node-unboxing-timelapse.mp4' , path : '/Videos/node-unboxing-timelapse.mp4' , size : 85_000_000 , modified : '2024-11-01T10:00:00Z' , isDir : false , type : 'video' },
{ name : 'bitcoin-explained-5min.mp4' , path : '/Videos/bitcoin-explained-5min.mp4' , size : 42_000_000 , modified : '2024-10-15T14:00:00Z' , isDir : false , type : 'video' },
{ name : 'lightning-payment-demo.mp4' , path : '/Videos/lightning-payment-demo.mp4' , size : 28_000_000 , modified : '2025-01-20T12:00:00Z' , isDir : false , type : 'video' },
],
}
const MOCK_FILE_CONTENTS = {
'/Documents/bitcoin-whitepaper-notes.md' : `# Bitcoin Whitepaper Notes\n\n## Key Concepts\n\n### Peer-to-Peer Electronic Cash\n- No trusted third party needed\n- Double-spending solved via proof-of-work\n- Longest chain = truth\n\n### Proof of Work\n- SHA-256 based hashing\n- Difficulty adjusts every 2016 blocks (~2 weeks)\n- Incentive: block reward + transaction fees\n\n## My Thoughts\n- The 21M supply cap is genius - digital scarcity\n- Lightning Network solves the scaling concern\n- Self-custody is the whole point` ,
'/Documents/node-setup-checklist.md' : `# Archipelago Node Setup Checklist\n\n## Hardware\n- [x] Intel NUC / Mini PC (16GB RAM minimum)\n- [x] 2TB NVMe SSD\n- [x] USB drive for installer\n- [x] Ethernet cable\n\n## Core Apps\n- [x] Bitcoin Knots\n- [x] LND\n- [x] Mempool Explorer\n- [ ] BTCPay Server\n- [ ] Fedimint` ,
'/Documents/lightning-channels.csv' : `channel_id,peer_alias,capacity_sats,local_balance,remote_balance,status\nch_001,ACINQ,5000000,2450000,2550000,active\nch_002,WalletOfSatoshi,2000000,1200000,800000,active\nch_003,Voltage,10000000,4500000,5500000,active\nch_004,Kraken,3000000,1800000,1200000,active` ,
'/Documents/sovereignty-manifesto.txt' : `THE SOVEREIGNTY MANIFESTO\n=========================\n\nWe hold these truths to be self-evident:\n\n1. Your data belongs to you.\n2. Your money should be uncensorable.\n3. Your communications should be private.\n4. Your compute should be sovereign.\n5. Your identity should be self-issued.\n\nRun your own node. Hold your own keys. Own your own data. Be sovereign.` ,
'/Documents/backup-log.json' : JSON . stringify ({ backups : [{ id : 'bkp-2025-03-01' , timestamp : '2025-03-01T02:00:00Z' , type : 'full' , apps : [ 'bitcoin-knots' , 'lnd' , 'mempool' ], size_mb : 2340 , status : 'success' }] }, null , 2 ),
}
// FileBrowser login - return mock JWT
app . post ( '/app/filebrowser/api/login' , ( req , res ) => {
res . send ( '"mock-filebrowser-token-demo"' )
})
// FileBrowser list resources
app . get ( '/app/filebrowser/api/resources/*' , ( req , res ) => {
const reqPath = decodeURIComponent ( req . params [ 0 ] || '/' ). replace ( /\/+$/ , '' ) || '/'
const items = MOCK_FILES [ reqPath ] || []
res . json ({
items ,
numDirs : items . filter ( i => i . isDir ). length ,
numFiles : items . filter ( i => ! i . isDir ). length ,
sorting : { by : 'name' , asc : true },
})
})
app . get ( '/app/filebrowser/api/resources' , ( req , res ) => {
const items = MOCK_FILES [ '/' ] || []
res . json ({
items ,
numDirs : items . filter ( i => i . isDir ). length ,
numFiles : items . filter ( i => ! i . isDir ). length ,
sorting : { by : 'name' , asc : true },
})
})
// FileBrowser raw file content (for text file reading)
app . get ( '/app/filebrowser/api/raw/*' , ( req , res ) => {
const reqPath = '/' + decodeURIComponent ( req . params [ 0 ] || '' )
const content = MOCK_FILE_CONTENTS [ reqPath ]
if ( content ) {
res . type ( 'text/plain' ). send ( content )
} else {
res . status ( 404 ). send ( 'File not found' )
}
})
2026-03-07 23:07:38 +00:00
// Claude API Proxy (reads ANTHROPIC_API_KEY from environment)
// =============================================================================
import https from 'https'
app . post ( '/aiui/api/claude/*' , ( req , res ) => {
const apiKey = process . env . ANTHROPIC_API_KEY
if ( ! apiKey ) {
return res . status ( 500 ). json ({
type : 'error' ,
error : { type : 'configuration_error' , message : 'ANTHROPIC_API_KEY not configured on server' }
})
}
const apiPath = '/' + req . params [ 0 ]
2026-03-07 23:22:30 +00:00
2026-03-08 00:21:38 +00:00
// Clean request body for Anthropic API
2026-03-07 23:22:30 +00:00
const body = req . body
2026-03-08 00:21:38 +00:00
if ( body ) {
if ( ! body . max_tokens ) body . max_tokens = 4096
2026-03-08 00:27:34 +00:00
// Fix model IDs — AIUI may send short names
if ( body . model && ! body . model . includes ( '-2' )) {
const modelMap = {
'claude-haiku-4.5' : 'claude-haiku-4-5-20251001' ,
'claude-haiku-4-5' : 'claude-haiku-4-5-20251001' ,
'claude-sonnet-4-5' : 'claude-sonnet-4-5-20250514' ,
'claude-sonnet-4.5' : 'claude-sonnet-4-5-20250514' ,
}
if ( modelMap [ body . model ]) body . model = modelMap [ body . model ]
}
2026-03-08 00:21:38 +00:00
// Strip AIUI-specific fields that Anthropic API rejects
delete body . webSearch
delete body . webResults
delete body . context
2026-03-07 23:22:30 +00:00
}
const bodyStr = JSON . stringify ( body )
2026-03-07 23:07:38 +00:00
const options = {
hostname : 'api.anthropic.com' ,
port : 443 ,
path : apiPath ,
method : 'POST' ,
headers : {
'Content-Type' : 'application/json' ,
'x-api-key' : apiKey ,
'anthropic-version' : '2023-06-01' ,
'Content-Length' : Buffer . byteLength ( bodyStr ),
},
}
const proxyReq = https . request ( options , ( proxyRes ) => {
res . writeHead ( proxyRes . statusCode , proxyRes . headers )
proxyRes . pipe ( res )
})
proxyReq . on ( 'error' , ( err ) => {
2026-03-07 23:58:08 +00:00
const msg = err . message || err . code || JSON . stringify ( err ) || 'Unknown proxy error'
console . error ( '[Claude Proxy] Error:' , msg )
2026-03-07 23:07:38 +00:00
if ( ! res . headersSent ) {
res . status ( 502 ). json ({
type : 'error' ,
2026-03-07 23:58:08 +00:00
error : { type : 'proxy_error' , message : msg }
2026-03-07 23:07:38 +00:00
})
}
})
proxyReq . write ( bodyStr )
proxyReq . end ()
})
2026-03-08 01:48:23 +00:00
// Ollama (local AI) proxy — forwards to localhost:11434
app . all ( '/aiui/api/ollama/*' , ( req , res ) => {
const ollamaPath = '/' + req . params [ 0 ]
const bodyStr = JSON . stringify ( req . body )
const options = {
hostname : '127.0.0.1' ,
port : 11434 ,
path : ollamaPath ,
method : req . method ,
headers : {
'Content-Type' : 'application/json' ,
'Content-Length' : Buffer . byteLength ( bodyStr ),
},
}
const proxyReq = http . request ( options , ( proxyRes ) => {
res . writeHead ( proxyRes . statusCode , proxyRes . headers )
proxyRes . pipe ( res )
})
proxyReq . on ( 'error' , ( err ) => {
const msg = err . message || err . code || 'Ollama not available'
console . error ( '[Ollama Proxy] Error:' , msg )
if ( ! res . headersSent ) {
res . status ( 502 ). json ({ error : msg })
}
})
if ( req . method !== 'GET' && req . method !== 'HEAD' ) {
proxyReq . write ( bodyStr )
}
proxyReq . end ()
})
// =============================================================================
// Ollama Local AI Proxy (forwards to Ollama on localhost:11434)
// =============================================================================
app . all ( '/api/ollama/*' , ( req , res ) => {
const ollamaPath = '/' + req . params [ 0 ]
const isPost = req . method === 'POST'
const bodyStr = isPost ? JSON . stringify ( req . body ) : null
const options = {
hostname : '127.0.0.1' ,
port : 11434 ,
path : ollamaPath ,
method : req . method ,
headers : { 'Content-Type' : 'application/json' },
}
const proxyReq = http . request ( options , ( proxyRes ) => {
res . writeHead ( proxyRes . statusCode , proxyRes . headers )
proxyRes . pipe ( res )
})
proxyReq . on ( 'error' , ( err ) => {
const msg = err . message || err . code || 'Ollama not available'
console . error ( '[Ollama Proxy] Error:' , msg )
if ( ! res . headersSent ) {
res . status ( 502 ). json ({ error : msg })
}
})
if ( bodyStr ) proxyReq . write ( bodyStr )
proxyReq . end ()
})
2026-03-07 23:24:27 +00:00
// Web search stub (no search engine configured in demo)
app . get ( '/api/web-search' , ( req , res ) => {
2026-03-07 23:58:08 +00:00
res . json ({ results : [] })
2026-03-07 23:24:27 +00:00
})
2026-03-07 23:58:08 +00:00
// TMDB API stub (no TMDB key in demo)
app . get ( '/api/tmdb/*' , ( req , res ) => {
res . json ({ results : [] })
})
// Catch-all for unimplemented API endpoints (return JSON, not HTML)
app . all ( '/api/*' , ( req , res ) => {
res . status ( 404 ). json ({ error : 'Not available in demo mode' })
})
2026-03-07 23:24:27 +00:00
app . all ( '/aiui/api/*' , ( req , res ) => {
2026-03-07 23:58:08 +00:00
res . status ( 404 ). json ({ error : 'Not available in demo mode' })
})
2026-01-24 22:59:20 +00:00
// Health check
app . get ( '/health' , ( req , res ) => {
res . status ( 200 ). send ( 'healthy' )
})
// WebSocket endpoint
const server = http . createServer ( app )
const wss = new WebSocketServer ({ server , path : '/ws/db' })
wss . on ( 'connection' , ( ws , req ) => {
console . log ( '[WebSocket] Client connected from' , req . socket . remoteAddress )
wsClients . add ( ws )
// Set up ping/pong to keep connection alive
const pingInterval = setInterval (() => {
if ( ws . readyState === 1 ) { // OPEN
try {
ws . ping ()
} catch ( err ) {
console . error ( '[WebSocket] Ping error:' , err )
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
}
} else {
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
}
}, 30000 ) // Ping every 30 seconds
2026-03-07 22:36:45 +00:00
// Send periodic heartbeat data so clients don't think the connection is dead
const heartbeatInterval = setInterval (() => {
if ( ws . readyState === 1 ) {
try {
2026-03-07 23:22:30 +00:00
ws . send ( JSON . stringify ({ type : 'heartbeat' , rev : Date . now () }))
2026-03-07 22:36:45 +00:00
} catch { /* ignore */ }
}
}, 45000 ) // Every 45s (client expects data within 60s)
2026-01-24 22:59:20 +00:00
// Send initial data immediately
try {
ws . send ( JSON . stringify ({
type : 'initial' ,
data : mockData ,
}))
console . log ( '[WebSocket] Initial data sent' )
} catch ( err ) {
console . error ( '[WebSocket] Error sending initial data:' , err )
}
ws . on ( 'pong' , () => {
// Client responded to ping, connection is alive
})
ws . on ( 'message' , ( message ) => {
// Handle incoming messages if needed
try {
const data = JSON . parse ( message . toString ())
console . log ( '[WebSocket] Received message:' , data )
} catch ( err ) {
console . error ( '[WebSocket] Error parsing message:' , err )
}
})
ws . on ( 'close' , ( code , reason ) => {
console . log ( '[WebSocket] Client disconnected' , { code , reason : reason . toString () })
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
wsClients . delete ( ws )
})
ws . on ( 'error' , ( error ) => {
console . error ( '[WebSocket Error]' , error )
clearInterval ( pingInterval )
2026-03-07 22:36:45 +00:00
clearInterval ( heartbeatInterval )
2026-01-24 22:59:20 +00:00
wsClients . delete ( ws )
})
})
server . listen ( PORT , '0.0.0.0' , async () => {
const runtime = await isContainerRuntimeAvailable ()
2026-01-27 23:06:18 +00:00
// Initialize package data from Docker
await initializePackageData ()
2026-01-24 22:59:20 +00:00
console . log ( `
╔════════════════════════════════════════════════════════════╗
║ ║
║ 🚀 Archipelago Mock Backend Server ║
║ ║
║ RPC: http://localhost: ${ PORT } /rpc/v1 ║
║ WebSocket: ws://localhost: ${ PORT } /ws/db ║
║ ║
║ Dev Mode: ${ DEV_MODE . padEnd ( 47 ) } ║
║ Setup: ${ userState . setupComplete ? '✅ Complete' : '❌ Not done' . padEnd ( 47 ) } ║
║ Onboarding: ${ userState . onboardingComplete ? '✅ Complete' : '❌ Not done' . padEnd ( 46 ) } ║
║ ║
║ Mock Password: ${ MOCK_PASSWORD . padEnd ( 40 ) } ║
║ ║
║ Container Runtime: ${ runtime . available ? `✅ ${ runtime . runtime } ` . padEnd ( 40 ) : '❌ Not available' . padEnd ( 40 ) } ║
2026-01-27 23:06:18 +00:00
║ Docker API: ✅ Connected ║
2026-01-24 22:59:20 +00:00
║ ║
╚════════════════════════════════════════════════════════════╝
` )
console . log ( 'Mock backend is running. Press Ctrl+C to stop.\n' )
2026-01-27 23:06:18 +00:00
2026-02-18 08:30:12 +00:00
// Periodically update package data from Docker (merge with static dev apps)
2026-03-07 20:53:02 +00:00
// Only poll if container runtime is available (avoids log spam in demo/Docker deployments)
if ( runtime . available ) {
setInterval ( async () => {
const dockerApps = await getDockerContainers ()
mockData [ 'package-data' ] = mergePackageData ( dockerApps )
// Broadcast update to connected clients
broadcastUpdate ([
{
op : 'replace' ,
path : '/package-data' ,
value : mockData [ 'package-data' ]
}
])
}, 5000 ) // Update every 5 seconds
}
2026-01-24 22:59:20 +00:00
})
process . on ( 'SIGINT' , () => {
console . log ( '\n\nShutting down mock backend...' )
server . close (() => {
console . log ( 'Server stopped.' )
process . exit ( 0 )
})
})