'use strict'; const fs = require('fs'); const fsp = fs.promises; const os = require('os'); const path = require('path'); const http = require('http'); const crypto = require('crypto'); const {spawn, execFile} = require('child_process'); const {createWorldBackup,verifyWorldBackup,restoreWorldBackup}=require('../server/world-backup'); const {checkPublicDeployment}=require('../server/public-health'); const {renderApacheBootstrap,replaceManagedBlock}=require('../server/apache-bridge'); const ROOT = path.resolve(__dirname, '..'); const SERVER_FILE = path.join(ROOT, 'server.js'); const RUNNER_FILE = path.join(ROOT, 'scripts', 'service-runner.js'); const SERVICE_ROOT = path.resolve(process.env.LINK_FIELD_SERVICE_DIR || path.join(os.homedir(), '.local', 'share', 'LinkField', 'service')); const PID_FILE = path.join(SERVICE_ROOT, 'server.pid'); const IDENTITY_FILE = path.join(SERVICE_ROOT, 'service.identity.json'); const LOG_FILE = path.resolve(process.env.LINK_FIELD_LOG_FILE || path.join(SERVICE_ROOT, 'server.log')); const TEST_DATA_ROOT=String(process.env.LINK_FIELD_TEST_DATA_ROOT||'').trim(); const WORLD_DIR=TEST_DATA_ROOT?path.join(path.resolve(TEST_DATA_ROOT),'world'):path.resolve(String(process.env.LINK_FIELD_WORLD_DIR||'/link-field/world')); const BACKUP_DIR=path.resolve(String(process.env.LINK_FIELD_BACKUP_DIR||path.join(path.dirname(WORLD_DIR),'backups'))); const STARTUP_TIMEOUT_MS = 12_000; const POLL_INTERVAL_MS = 100; const MAX_LOG_BYTES = 10 * 1024 * 1024; const LOG_GENERATIONS = 5; const PUBLIC_ENTRIES = Object.freeze([ 'index.html', 'style.css', 'favicon.svg', 'favicon.ico', 'build-meta.js', 'runtime-config.js', 'shared-contracts.js', 'store-catalog.generated.js', 'store-catalog.json', 'puzzle-patterns.js', 'puzzle-core.js', 'app-logic.js', 'archive-codec.js', 'field-persistence.js', 'field-persistence-worker.js', 'puzzle-worker.js', 'app.js', 'api-bridge.php', 'assets', 'client', ]); function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function execFileText(command, args) { return new Promise((resolve, reject) => execFile(command, args, {encoding:'utf8'}, (error, stdout) => error ? reject(error) : resolve(stdout))); } function rootMatches(value){ if(typeof value!=='string'||!value.trim())return false; const actual=path.resolve(value),expected=ROOT; return process.platform==='win32'?actual.toLowerCase()===expected.toLowerCase():actual===expected; } async function readWorldInstanceLock(worldDir=WORLD_DIR){ try{ const raw=(await fsp.readFile(path.join(worldDir,'server.pid'),'utf8')).trim(); if(/^\d+$/.test(raw))return{pid:Number(raw),legacy:true}; const value=JSON.parse(raw);return value&&typeof value==='object'?value:null; }catch(_){return null} } function isFreshRuntimeRecord(record,maxAgeMs){ return Boolean(record&&rootMatches(record.root)&&Number.isSafeInteger(Number(record.pid))&&Number(record.pid)>0&&isProcessRunning(Number(record.pid))&&Date.now()-Number(record.heartbeatAt||0)0&&pid!==process.pid&&isProcessRunning(pid)&&age>=-5000&&agepid!==process.pid))]; } let output; try { output = await execFileText('ps', ['-ax', '-o', 'pid=', '-o', 'comm=', '-o', 'command=']); } catch (_) { return [...new Set(matches.filter(pid=>pid!==process.pid))]; } for (const line of output.split(/\r?\n/)) { const match=line.match(/^\s*(\d+)\s+(\S+)\s+(.+)$/);if(!match)continue; const pid=Number(match[1]),executable=path.basename(match[2]).toLowerCase(),command=match[3]; if(pid===process.pid||!Number.isSafeInteger(pid)||!['node','nodejs'].includes(executable)||!/(?:^|[\s/])(?:server\.js|service-runner\.js)(?:\s|$)/.test(command))continue; let cwd='';try{cwd=await fsp.readlink(`/proc/${pid}/cwd`)}catch(_){} const candidates=[cwd];const absolute=command.match(/(?:^|\s)(\/[^\s]*\/server\.js)(?:\s|$)/);if(absolute)candidates.push(path.dirname(absolute[1])); let linkField=false; for(const directory of candidates.filter(Boolean)){ try{const pkg=JSON.parse(await fsp.readFile(path.join(directory,'package.json'),'utf8'));if(['link-field-v47-shared-world','link-field-shared-world'].includes(pkg?.name)){linkField=true;break}}catch(_){} } if(linkField)matches.push(pid); } return [...new Set(matches.filter(pid=>pid!==process.pid))]; } async function stopLegacyLinkFieldServers() { const stopped=new Set(),deadline=Date.now()+5_000;let quietSince=0; while(Date.now()!stopped.has(pid)); for(const pid of pids){ stopped.add(pid); if(!await terminateProcess(pid,2_000))throw new Error(`Older LinkField process PID ${pid} did not stop.`); } if(active.length||pids.length)quietSince=0;else if(!quietSince)quietSince=Date.now(); if(quietSince&&Date.now()-quietSince>=1_500)break; await sleep(pids.length?300:150); } const remaining=await legacyLinkFieldPids(); if(remaining.length)throw new Error(`Older LinkField process PID ${remaining.join(', ')} kept restarting and could not be stopped.`); return[...stopped]; } function isProcessRunning(pid) { if (!Number.isSafeInteger(pid) || pid <= 0) return false; try { process.kill(pid, 0); return true; } catch (error) { return error?.code === 'EPERM'; } } async function readServiceRecord() { try { const raw=(await fsp.readFile(PID_FILE, 'utf8')).trim(); if(/^\d+$/.test(raw)){const pid=Number(raw);return Number.isSafeInteger(pid)&&pid>0?{pid,nonce:null,legacy:true}:null} const value=JSON.parse(raw),pid=Number(value?.pid); return Number.isSafeInteger(pid)&&pid>0?{pid,nonce:typeof value.nonce==='string'?value.nonce:null,root:value.root}:null; } catch (error) { if (error?.code === 'ENOENT') return null; return null; } } async function readPid(){return(await readServiceRecord())?.pid||null} async function readIdentity(){try{return JSON.parse(await fsp.readFile(IDENTITY_FILE,'utf8'))}catch(error){if(error?.code==='ENOENT')return null;return null}} async function isManagedService(pid){ if(!isProcessRunning(pid))return false;const [identity,record]=await Promise.all([readIdentity(),readServiceRecord()]); return Boolean(identity&&record&&record.pid===pid&&identity.pid===pid&&path.resolve(identity.root||'')===ROOT&&path.resolve(record.root||ROOT)===ROOT&&(!record.nonce||identity.nonce===record.nonce)&&Date.now()-Number(identity.heartbeatAt||0)<5000); } async function terminateProcess(pid,timeoutMs=5000){ if(!isProcessRunning(pid))return true; try{process.kill(pid,'SIGTERM')}catch(error){if(error?.code!=='ESRCH')throw error} const started=Date.now();while(isProcessRunning(pid)&&Date.now()-started { if (error?.code !== 'ENOENT') throw error; }); await fsp.unlink(IDENTITY_FILE).catch(error=>{if(error?.code!=='ENOENT')throw error}); return null; } async function recoverWorldLock(){ await fsp.mkdir(SERVICE_ROOT,{recursive:true,mode:0o700}); const stopped=new Set(),deadline=Date.now()+15_000;let quietSince=0; while(Date.now()0&&isProcessRunning(lockPid)){ if(!await processExecutableIsNode(lockPid))throw new Error(`Refusing recovery: world-lock PID ${lockPid} is not a verified Node process.`); candidates.add(lockPid); }else if(lock){await fsp.unlink(path.join(WORLD_DIR,'server.pid')).catch(error=>{if(error?.code!=='ENOENT')throw error})} const active=[...candidates].filter(pid=>pid!==process.pid&&isProcessRunning(pid)); if(active.length){quietSince=0;for(const pid of active){if(!await terminateProcess(pid,3_000))throw new Error(`LinkField recovery could not stop PID ${pid}.`);stopped.add(pid)}} else if(!quietSince)quietSince=Date.now(); if(quietSince&&Date.now()-quietSince>=2_500)break; await sleep(active.length?300:150); } const remaining=await readWorldInstanceLock(),remainingPid=Number(remaining?.pid); if(Number.isSafeInteger(remainingPid)&&remainingPid>0&&isProcessRunning(remainingPid))throw new Error(`World-lock PID ${remainingPid} is still active after recovery.`); await fsp.unlink(path.join(WORLD_DIR,'server.pid')).catch(error=>{if(error?.code!=='ENOENT')throw error}); await fsp.unlink(PID_FILE).catch(error=>{if(error?.code!=='ENOENT')throw error}); await fsp.unlink(IDENTITY_FILE).catch(error=>{if(error?.code!=='ENOENT')throw error}); await fsp.unlink(path.join(resolvePublicDir(),'.linkfield-port')).catch(error=>{if(error?.code!=='ENOENT')throw error}); console.log(stopped.size?`Recovered the LinkField world lock and stopped ${stopped.size} old process${stopped.size===1?'':'es'}.`:'Removed a stale LinkField world lock.'); console.log('Run npm start now.'); return[...stopped]; } async function rotateLogs(){ let stat;try{stat=await fsp.stat(LOG_FILE)}catch(error){if(error?.code==='ENOENT')return;if(error)throw error} if(!stat||stat.size{if(error?.code!=='ENOENT')throw error}); for(let index=LOG_GENERATIONS-1;index>=1;index--){try{await fsp.rename(`${LOG_FILE}.${index}`,`${LOG_FILE}.${index+1}`)}catch(error){if(error?.code!=='ENOENT')throw error}} await fsp.rename(LOG_FILE,`${LOG_FILE}.1`); } function isWithin(parent, child) { const relative = path.relative(parent, child); return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative)); } function resolvePublicDir() { const explicit = String(process.env.LINK_FIELD_PUBLIC_DIR || '').trim(); if (explicit) return path.resolve(explicit); const home = os.homedir(); const defaultDir = path.join(home, 'public_html', 'link-field'); const defaultParent = path.dirname(defaultDir); const rootParent = path.dirname(ROOT); const rootName = path.basename(ROOT).toLowerCase(); const parentName = path.basename(rootParent).toLowerCase(); if (path.resolve(ROOT) === path.resolve(defaultDir)) return ROOT; if (isWithin(defaultDir, ROOT)) return defaultDir; if (parentName === 'link-field' && /^link-field-v\d/i.test(rootName)) return rootParent; if (isWithin(defaultParent, ROOT) && /^link-field-v\d/i.test(rootName)) return defaultDir; return defaultDir; } async function copyEntry(source, destination) { const stat = await fsp.lstat(source); if(stat.isSymbolicLink())return; if (stat.isDirectory()) { await fsp.mkdir(destination, {recursive:true, mode:0o755}); const entries = await fsp.readdir(source, {withFileTypes:true}); for (const entry of entries) { await copyEntry(path.join(source, entry.name), path.join(destination, entry.name)); } return; } if (!stat.isFile()) return; await fsp.mkdir(path.dirname(destination), {recursive:true, mode:0o755}); await fsp.copyFile(source, destination); await fsp.chmod(destination, 0o644).catch(() => {}); } function assertSafePublicDir(publicDir){ const resolved=path.resolve(publicDir),root=path.parse(resolved).root,home=path.resolve(os.homedir()); if(resolved===root||resolved===home||path.dirname(resolved)===resolved)throw new Error(`Unsafe LinkField public directory: ${resolved}`); return resolved; } async function writeApacheBootstrap(publicDir){ const apacheFile=path.join(publicDir,'.htaccess');let existing=''; try{existing=await fsp.readFile(apacheFile,'utf8')}catch(error){if(error?.code!=='ENOENT')throw error} await fsp.writeFile(apacheFile,replaceManagedBlock(existing,renderApacheBootstrap()),{encoding:'utf8',mode:0o644}); } async function deployPublicFiles(publicDir = resolvePublicDir()) { publicDir=assertSafePublicDir(publicDir); const manifest = { app: 'LinkField', version: require('../build-meta').APP_VERSION, source: ROOT, deployedAt: new Date().toISOString(), entries:[...PUBLIC_ENTRIES], }; if(path.resolve(publicDir)===ROOT){await writeApacheBootstrap(publicDir);await fsp.writeFile(path.join(publicDir,'.linkfield-deployment.json'),`${JSON.stringify(manifest,null,2)}\n`,{encoding:'utf8',mode:0o644});return publicDir} const parent=path.dirname(publicDir),name=path.basename(publicDir),stamp=`${process.pid}-${Date.now()}`,stage=path.join(parent,`.${name}.linkfield-stage-${stamp}`),backup=path.join(parent,`.${name}.linkfield-old-${stamp}`); await fsp.mkdir(parent,{recursive:true,mode:0o755}); const artifacts=(await fsp.readdir(parent,{withFileTypes:true})).filter(entry=>entry.isDirectory()&&(entry.name.startsWith(`.${name}.linkfield-stage-`)||entry.name.startsWith(`.${name}.linkfield-old-`))).map(entry=>path.join(parent,entry.name)).sort(); if(!fs.existsSync(publicDir)){const recoverable=[...artifacts].reverse().find(item=>path.basename(item).startsWith(`.${name}.linkfield-old-`));if(recoverable){await fsp.rename(recoverable,publicDir);artifacts.splice(artifacts.indexOf(recoverable),1)}} for(const artifact of artifacts)await fsp.rm(artifact,{recursive:true,force:true}); try{ if(fs.existsSync(publicDir))await copyEntry(publicDir,stage);else await fsp.mkdir(stage,{recursive:true,mode:0o755}); let previous=null;try{previous=JSON.parse(await fsp.readFile(path.join(stage,'.linkfield-deployment.json'),'utf8'))}catch(_){} for(const retired of Array.isArray(previous?.entries)?previous.entries.filter(entry=>!PUBLIC_ENTRIES.includes(entry)):[]){const target=path.resolve(stage,retired);if(target.startsWith(`${stage}${path.sep}`))await fsp.rm(target,{recursive:true,force:true})} for(const entry of PUBLIC_ENTRIES){const source=path.join(ROOT,entry),destination=path.join(stage,entry);await fsp.rm(destination,{recursive:true,force:true});try{await copyEntry(source,destination)}catch(error){if(error?.code!=='ENOENT')throw error}} await writeApacheBootstrap(stage); await fsp.writeFile(path.join(stage,'.linkfield-deployment.json'),`${JSON.stringify(manifest,null,2)}\n`,{encoding:'utf8',mode:0o644}); if(fs.existsSync(publicDir))await fsp.rename(publicDir,backup); try{await fsp.rename(stage,publicDir)}catch(error){if(fs.existsSync(backup)&&!fs.existsSync(publicDir))await fsp.rename(backup,publicDir);throw error} await fsp.rm(backup,{recursive:true,force:true}); }catch(error){await fsp.rm(stage,{recursive:true,force:true}).catch(()=>{});throw error} return publicDir; } async function tailLog(lines = 20) { try { const text = await fsp.readFile(LOG_FILE, 'utf8'); return text.trimEnd().split(/\r?\n/).slice(-lines).join('\n'); } catch (error) { return error?.code === 'ENOENT' ? '' : `Could not read log: ${error.message}`; } } function localStatus(port){ return new Promise((resolve,reject)=>{const request=http.get({hostname:'127.0.0.1',port,path:'/api/cloud/status',timeout:1500,headers:{connection:'close'}},response=>{let body='';response.setEncoding('utf8');response.on('data',chunk=>{if(body.length<65536)body+=chunk});response.on('end',()=>{try{const value=JSON.parse(body);if(response.statusCode===200&&value?.available===true&&value?.appVersion===require('../build-meta').APP_VERSION)resolve(value);else reject(new Error(`Local health returned HTTP ${response.statusCode}`))}catch(error){reject(error)}})});request.on('timeout',()=>request.destroy(new Error('Local health timed out')));request.on('error',reject)}); } async function publicStatus(publicUrl){ const result=await checkPublicDeployment(publicUrl,{appVersion:require('../build-meta').APP_VERSION,timeoutMs:10_000});return result?.api||null; } function configuredPublicUrl(){return String(process.env.LINK_FIELD_PUBLIC_URL||'').trim()} async function waitForStartup(pid, publicDir) { const portFile = path.join(publicDir, '.linkfield-port'); const started = Date.now(); while (Date.now() - started < STARTUP_TIMEOUT_MS) { if (!isProcessRunning(pid)) { const log = await tailLog(); throw new Error(`LinkField exited during startup.${log ? `\n\n${log}` : ''}`); } try { const port = Number((await fsp.readFile(portFile, 'utf8')).trim()); if (Number.isSafeInteger(port) && port > 0 && port <= 65535&&await isManagedService(pid)){try{await localStatus(port);return port}catch(_){}} } catch (error) { if (error?.code !== 'ENOENT') throw error; } await sleep(POLL_INTERVAL_MS); } const log = await tailLog(); throw new Error(`LinkField did not finish startup within ${STARTUP_TIMEOUT_MS / 1000} seconds.${log ? `\n\n${log}` : ''}`); } async function start() { await fsp.mkdir(SERVICE_ROOT, {recursive:true, mode:0o700}); const existingPid = await removeStalePid(); let publicDir=null; if (existingPid) { publicDir = await deployPublicFiles(); console.log(`Replacing the running LinkField server (PID ${existingPid}) with v${require('../build-meta').APP_VERSION}.`); await stop({quiet:true}); } const stoppedLegacy=await stopLegacyLinkFieldServers(); if(stoppedLegacy.length)console.log(`Stopped ${stoppedLegacy.length} older LinkField server process${stoppedLegacy.length===1?'':'es'}.`); const unresolvedLock=await readWorldInstanceLock(),unresolvedPid=Number(unresolvedLock?.pid); if(Number.isSafeInteger(unresolvedPid)&&unresolvedPid>0&&isProcessRunning(unresolvedPid))throw new Error(`A legacy LinkField world lock is still held by PID ${unresolvedPid}. Run npm run recover once, then run npm start again.`); if(!publicDir)publicDir = await deployPublicFiles(); await fsp.unlink(path.join(publicDir,'.linkfield-port')).catch(error=>{if(error?.code!=='ENOENT')throw error}); await fsp.mkdir(path.dirname(LOG_FILE), {recursive:true, mode:0o755}); await rotateLogs(); const logFd = fs.openSync(LOG_FILE, 'a'); let child; const nonce=crypto.randomBytes(16).toString('hex'); try { child = spawn(process.execPath, [RUNNER_FILE], { cwd: ROOT, detached: true, stdio: ['ignore', logFd, logFd], env: {...process.env, LINK_FIELD_PUBLIC_DIR: publicDir, LINK_FIELD_SERVICE_DIR: SERVICE_ROOT,LINK_FIELD_SERVICE_IDENTITY_FILE:IDENTITY_FILE,LINK_FIELD_SERVICE_NONCE:nonce,LINK_FIELD_LOG_FILE:LOG_FILE}, }); } finally { fs.closeSync(logFd); } if (!child.pid) throw new Error('Could not start the LinkField background process.'); try { await fsp.writeFile(PID_FILE, `${JSON.stringify({pid:child.pid,nonce,root:ROOT})}\n`, {encoding:'utf8', mode:0o600}); child.unref(); const port = await waitForStartup(child.pid, publicDir); const publicEndpoint=await publicStatus(configuredPublicUrl()); console.log('LinkField started in the background. The command prompt is available again.'); console.log(`PID: ${child.pid}`); console.log(`Local port: ${port}`); console.log(`Public directory: ${publicDir}`); console.log(`Log: ${LOG_FILE}`); if(publicEndpoint)console.log(`Public health: ${publicEndpoint}`); console.log(`Check: ${process.execPath} scripts/public-smoke-test.js https://YOUR-HOST/~333/link-field/`); } catch (error) { const stopped=!child?.pid||await terminateProcess(child.pid).catch(()=>false); if(!stopped)throw new Error(`${error.message}\nLinkField supervisor PID ${child.pid} could not be stopped; its PID record was preserved for a safe manual stop.`); await fsp.unlink(PID_FILE).catch(() => {}); await fsp.unlink(IDENTITY_FILE).catch(()=>{}); await fsp.unlink(path.join(publicDir,'.linkfield-port')).catch(()=>{}); throw error; } } async function stop({quiet = false} = {}) { const pid = await readPid(); if (!pid || !isProcessRunning(pid)) { await fsp.unlink(PID_FILE).catch(() => {}); await fsp.unlink(IDENTITY_FILE).catch(()=>{}); await fsp.unlink(path.join(resolvePublicDir(),'.linkfield-port')).catch(()=>{}); if (!quiet) console.log('LinkField is not running.'); return false; } if(!await isManagedService(pid))throw new Error(`Refusing to stop PID ${pid}: it is not a verified LinkField supervisor.`); if(!await terminateProcess(pid))throw new Error(`LinkField supervisor PID ${pid} did not stop.`); await fsp.unlink(PID_FILE).catch(() => {}); await fsp.unlink(IDENTITY_FILE).catch(()=>{}); await fsp.unlink(path.join(resolvePublicDir(),'.linkfield-port')).catch(()=>{}); if (!quiet) console.log(`LinkField stopped (PID ${pid}).`); return true; } async function status() { const pid = await readPid(); const publicDir = resolvePublicDir(); if (!pid || !await isManagedService(pid)) { console.log('LinkField is stopped.'); process.exitCode = 1; return; } let port = ''; try { port = (await fsp.readFile(path.join(publicDir, '.linkfield-port'), 'utf8')).trim(); } catch {} let healthy=false,publicEndpoint=null;if(port)try{await localStatus(Number(port));publicEndpoint=await publicStatus(configuredPublicUrl());healthy=true}catch(_){} console.log(`LinkField is ${healthy?'healthy':'unhealthy'} (supervisor PID ${pid}${port ? `, port ${port}` : ''}).`); console.log(`Public directory: ${publicDir}`); console.log(`Log: ${LOG_FILE}`);if(publicEndpoint)console.log(`Public health: ${publicEndpoint}`);if(!healthy)process.exitCode=1; } async function foreground() { const publicDir = await deployPublicFiles(); process.env.LINK_FIELD_PUBLIC_DIR = publicDir; const {main} = require('../server'); await main(); console.log(`LinkField is running in the foreground. Public directory: ${publicDir}`); } async function assertServiceStopped(){const pid=await readPid();if(pid&&await isManagedService(pid))throw new Error('Stop LinkField before running an offline backup or restore.');try{const raw=(await fsp.readFile(path.join(WORLD_DIR,'server.pid'),'utf8')).trim(),lock=/^\d+$/.test(raw)?{pid:Number(raw)}:JSON.parse(raw);if(isProcessRunning(Number(lock?.pid)))throw new Error('A LinkField world process is still active. Stop it before backup or restore.')}catch(error){if(error?.code!=='ENOENT'&&!/Unexpected token|Unexpected end/.test(error?.message||''))throw error}} async function backup(){await assertServiceStopped();const result=await createWorldBackup(WORLD_DIR,BACKUP_DIR,{retain:7,appVersion:require('../build-meta').APP_VERSION});console.log(`LinkField backup created: ${result.destination}`);return result} async function listBackups(){await fsp.mkdir(BACKUP_DIR,{recursive:true,mode:0o700});const names=(await fsp.readdir(BACKUP_DIR,{withFileTypes:true})).filter(entry=>entry.isDirectory()&&/^\d{4}-/.test(entry.name)).map(entry=>entry.name).sort().reverse();for(const name of names){try{const result=await verifyWorldBackup(BACKUP_DIR,name);console.log(`${name} revision=${result.manifest.worldRevision} files=${result.manifest.fileCount}`)}catch(error){console.log(`${name} INVALID ${error.message}`)}}return names} async function restore(name){await assertServiceStopped();if(!name)throw new Error('Specify a backup name from npm run backups.');const result=await restoreWorldBackup(WORLD_DIR,BACKUP_DIR,name);console.log(`LinkField restored backup ${result.restored}. Previous data remains at ${result.previous}`);return result} async function main() { const command = String(process.argv[2] || 'start').toLowerCase(); if (command === 'start') return start(); if (command === 'stop') return stop(); if (command === 'restart') { await stop({quiet:true}); return start(); } if (command === 'status') return status(); if (command === 'foreground') return foreground(); if (command === 'backup') return backup(); if (command === 'backups') return listBackups(); if (command === 'restore') return restore(process.argv[3]); if (command === 'recover') return recoverWorldLock(); if (command === 'deploy') { const publicDir = await deployPublicFiles(); console.log(`LinkField public files deployed to ${publicDir}`); return; } throw new Error(`Unknown service command: ${command}`); } module.exports = Object.freeze({ROOT, SERVICE_ROOT, PID_FILE, IDENTITY_FILE, LOG_FILE, WORLD_DIR, BACKUP_DIR, PUBLIC_ENTRIES, isProcessRunning,legacyLinkFieldPids,recoverWorldLock,resolvePublicDir,deployPublicFiles,start,stop,status,backup,listBackups,restore}); if (require.main === module) main().catch(error => { console.error(`LinkField service command failed: ${error.message}`); process.exitCode = 1; });