iniciando concha…

concha_SSH WEB
\n\n',{uid:U}); F('/var/www/app/assets/app.css',':root{--brand:#2EE6A0}body{font-family:system-ui;margin:0}\n',{uid:U}); F('/var/www/app/assets/app.js','import{render}from"./ui.js";render(document.getElementById("root"));\n',{uid:U}); F('/etc/nginx/nginx.conf','user www-data;\nworker_processes auto;\nevents { worker_connections 1024; }\nhttp {\n include mime.types;\n sendfile on;\n gzip on;\n limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;\n server {\n listen 80;\n server_name _;\n root /var/www/app;\n location /api/ {\n limit_req zone=api burst=40;\n proxy_pass http://127.0.0.1:3000;\n }\n }\n}\n',{uid:'root'}); F('/var/log/nginx/access.log',seedLog(nginxLine,32),{uid:'root',logGen:'nginx'}); F('/var/log/nginx/error.log',seedLog(()=>`${new Date().toISOString().slice(0,19).replace('T',' ')} [${pick(['error','warn','error'])}] ${ri(30,60)}#0: *${ri(100,999)} ${pick(['upstream timed out (110)','connect() failed (111: Connection refused)','limiting requests, excess: 40.120'])} while connecting to upstream`,14),{uid:'root',logGen:'nginxErr'}); F('/var/log/app/out.log',seedLog(appLine,28),{uid:U,logGen:'app'}); F('/srv/scripts/deploy.sh','#!/usr/bin/env bash\nset -euo pipefail\ncd /var/www/app\ngit pull --ff-only\ndocker compose up -d --build\ncurl -fsS http://localhost/api/health\necho "deploy ok"\n',{uid:U,mode:0o755}); F('/srv/scripts/healthcheck.sh','#!/usr/bin/env bash\ncurl -fsS http://localhost/api/health || exit 1\n',{uid:U,mode:0o755}); }else{ F(home+'/hosts.ini','[web]\nprod-web-01 ansible_host=10.20.1.15\nstg-web-01 ansible_host=10.20.2.15\n\n[db]\ndb-primary ansible_host=10.20.3.5\n',{uid:U}); } F('/var/log/syslog',seedLog(()=>`${fmtLs(Date.now()-ri(0,86400e3))} ${p.hostname} ${pick(['systemd[1]: Started Session '+ri(1,90)+' of user '+U,'sshd['+ri(800,999)+']: Accepted publickey for '+U,'CRON['+ri(500,999)+']: (root) CMD (run-parts /etc/cron.hourly)','kernel: [UFW BLOCK] IN=eth0 SRC=45.171.90.12'])}`,22),{uid:'root',logGen:'syslog'}); } F('/root/.bashrc','# root bashrc\nalias rm="rm -i"\n',{uid:'root'}); F('/root/secrets.txt','cofre-mestre: concha-demo-nada-real-aqui\n',{uid:'root',mode:0o600}); const services={ssh:{active:true,desc:'OpenBSD Secure Shell server',pid:812},cron:{active:true,desc:'Regular background program processing daemon',pid:640}}; if(host.persona==='db')services.postgresql={active:true,desc:'PostgreSQL RDBMS',pid:1201}; else if(host.persona==='pi')services.sensors={active:true,desc:'Estação meteorológica (sensor.py)',pid:1450}; else if(host.persona!=='bastion'){services.nginx={active:true,desc:'A high performance web server',pid:1105};services.docker={active:true,desc:'Docker Application Container Engine',pid:980};services.app={active:true,desc:'app-web (node server.js)',pid:1287}} const containers=(host.persona==='prod'||host.persona==='stg'||host.persona==='demo')?[ {id:'a1f9c2e40b71',name:'app-web-1',image:'app-web:2.14.0',ports:'0.0.0.0:3000->3000/tcp',up:ri(2,20)+' days',status:'up'}, {id:'9d02b1c7aa38',name:'nginx-1',image:'nginx:1.27-alpine',ports:'0.0.0.0:80->80/tcp, 443/tcp',up:ri(2,20)+' days',status:'up'}, {id:'7c55e9d1f0a2',name:'redis-1',image:'redis:7-alpine',ports:'6379/tcp',up:'3 weeks',status:'up'}]:[]; const srv={p,fs,services,containers,home,host};SERVERS.set(key,srv);return srv} const LOGGENS={nginx:nginxLine,nginxErr:()=>`${new Date().toISOString().slice(0,19).replace('T',' ')} [error] upstream timed out`,app:appLine,pg:pgLine,sensor:sensorLine,syslog:()=>`${fmtLs(Date.now())} systemd[1]: Started Session ${ri(1,99)}.`}; /* ============================== SHELL: cores & helpers ============================== */ const C={g:'\x1b[32m',bg:'\x1b[1;32m',b:'\x1b[1;34m',c:'\x1b[36m',bc:'\x1b[1;36m',y:'\x1b[33m',by:'\x1b[1;33m',r:'\x1b[31m',br:'\x1b[1;31m',m:'\x1b[35m',bm:'\x1b[1;35m',dim:'\x1b[2m',bold:'\x1b[1m',u:'\x1b[4m',x:'\x1b[0m'}; const jwait=(ms,job)=>new Promise(res=>{let done=false;const t=setTimeout(()=>{if(!done){done=true;res()}},ms);job?.timers.push(t);job?.onCancel(()=>{if(!done){done=true;clearTimeout(t);res()}})}); const makeJob=()=>({active:true,cancelled:false,timers:[],raw:null,_cbs:[],onCancel(f){this._cbs.push(f)},cancel(){if(this.cancelled)return;this.cancelled=true;this.timers.forEach(t=>{clearTimeout(t);clearInterval(t)});this._cbs.forEach(f=>{try{f()}catch{}})}}); const fileColor=n=>{if(n.type==='d')return C.b;if(n.mode&0o111)return C.bg;if(/\.(gz|zip|tar|tgz|7z)$/.test(n.name))return C.br;if(/\.(png|jpg|svg|gif|webp)$/.test(n.name))return C.bm;if(/\.(log|csv)$/.test(n.name))return C.y;return ''}; const isProtected=abs=>/^\/(etc|usr|var|root|bin|sbin|srv)(\/|$)/.test(abs); const canWrite=(abs,user)=>user==='root'||!isProtected(abs)&&(!abs.startsWith('/home/')||abs.startsWith('/home/'+user+'/')||abs==='/home/'+user)||abs.startsWith('/tmp'); const canRead=(abs,user)=>user==='root'||!(abs==='/root'||abs.startsWith('/root/')||abs==='/etc/shadow'); function columnize(items,width){const w=Math.max(...items.map(x=>ansiStrip(x).length))+2;const cols=Math.max(1,Math.floor(width/w));let out='';items.forEach((it,i)=>{out+=it+' '.repeat(Math.max(1,w-ansiStrip(it).length));if((i+1)%cols===0)out+='\n'});return out.endsWith('\n')?out:out+'\n'} async function progressBar(job,out,label,ms=1600){const W=24;const steps=28;for(let s=0;s<=steps;s++){if(job.cancelled)return false;const p=Math.round(s/steps*100);out(`\r${label} ${C.g}${'█'.repeat(Math.round(s/steps*W))}${C.dim}${'░'.repeat(W-Math.round(s/steps*W))}${C.x} ${String(p).padStart(3)}%`);await jwait(ms/steps,job)}out('\n');return!job.cancelled} const BANNER1='█▀▀ █▀█ █▄░█ █▀▀ █░█ ▄▀█',BANNER2='█▄▄ █▄█ █░▀█ █▄▄ █▀█ █▀█'; function bannerRows(){const lerp=(a,b,t)=>Math.round(a+(b-a)*t);const row=r=>{let out='';const n=r.length;for(let i=0;i1&&isNaN(Number(a))){for(const ch of a.slice(1))flags.add(ch)}else rest.push(a)}return{flags,rest,n}} function readInput(io,files){if(files.length){let out='';for(const f of files){const abs=io.sh.abs(f);const n=io.fs.get(abs);if(!n){io.err(`cat: ${f}: Arquivo ou diretório inexistente`);io.code=1;continue}if(n.type==='d'){io.err(`cat: ${f}: É um diretório`);io.code=1;continue}if(!canRead(abs,io.user)){io.err(`cat: ${f}: Permissão negada`);io.code=1;continue}if(n.b64){io.err(`cat: ${f}: arquivo binário`);io.code=1;continue}out+=n.content}return out}return io.in} def('help','Lista de comandos desta demo','Sessão',async(a,io)=>{const groups={};for(const[k,v]of Object.entries(CMDS))(groups[v.group]??=[]).push([k,v.desc]); io.out(C.bc+'🐚 Concha Demo Shell'+C.x+C.dim+' — servidor simulado, seguro para explorar'+C.x+'\n\n'); for(const[g,list]of Object.entries(groups)){io.out(C.by+g+C.x+'\n');for(const[k,d]of list.sort())io.out(' '+C.c+k.padEnd(13)+C.x+C.dim+d+C.x+'\n');io.out('\n')} io.out(C.dim+'Aliases: ll, la, cls · Suporta pipes |, redirecionamentos > >>, && ; ||, glob *, $? e $VAR'+C.x+'\n'); io.out(C.dim+'Teclas: Tab completa · Ctrl+R histórico · Ctrl+C interrompe · Ctrl+L limpa · Ctrl+D sai'+C.x+'\n');return 0}); def('man','Manual curto de um comando','Sessão',async(a,io)=>{if(!a[0]){io.err('Que página de manual você quer?');return 16}const c=CMDS[a[0]];if(!c){io.err(`Nenhuma entrada de manual para ${a[0]}`);return 16}io.out(C.bold+a[0].toUpperCase()+C.x+'\n\n'+C.bc+'NOME'+C.x+'\n '+a[0]+' — '+c.desc+'\n\n'+C.bc+'SINOPSE'+C.x+'\n '+C.g+c.usage+C.x+'\n\n'+C.dim+'Página gerada pela demo do Concha.'+C.x+'\n');return 0}); def('clear','Limpa a tela','Sessão',async(a,io)=>{io.out('\x1b[2J\x1b[3J\x1b[H');return 0}); def('history','Histórico de comandos','Sessão',async(a,io)=>{if(a[0]==='-c'){io.sh.hist.length=0;return 0}const hst=io.sh.hist;hst.slice().reverse().forEach((c,i)=>io.out(` ${String(i+1).padStart(4)} ${c}\n`));return 0}); def('exit','Encerra a sessão','Sessão',async(a,io)=>{io.sh.doExit(parseInt(a[0])||0);return 0});CMDS.logout=CMDS.exit; def('echo','Imprime texto (aceita -n, -e)','Texto',async(a,io)=>{const{flags,rest}=parseFlags(a);let s=rest.join(' ');if(flags.has('e'))s=s.replace(/\\n/g,'\n').replace(/\\t/g,'\t').replace(/\\033|\\e/g,'\x1b').replace(/\\\\/g,'\\');io.out(s+(flags.has('n')?'':'\n'));return 0},'echo [-ne] texto…'); def('cat','Concatena arquivos','Arquivos',async(a,io)=>{const{flags,rest}=parseFlags(a);io.code=0;const txt=readInput(io,rest);if(io.code)return 1;if(flags.has('n'))txt.split('\n').filter((l,i,arr)=>iio.out(String(i+1).padStart(6)+' '+l+'\n'));else io.out(txt);return 0},'cat [-n] arquivo…'); def('ls','Lista diretório','Arquivos',async(a,io)=>{const{flags,rest}=parseFlags(a);const paths=rest.length?rest:['.'];io.code=0;let multi=paths.length>1; for(const pth of paths){const abs=io.sh.abs(pth);const n=io.fs.get(abs);if(!n){io.err(`ls: não é possível acessar '${pth}': Arquivo ou diretório inexistente`);io.code=2;continue}if(!canRead(abs,io.user)){io.err(`ls: ${pth}: Permissão negada`);io.code=2;continue} let items=n.type==='d'?Object.values(n.children):[n];if(!flags.has('a')&&!flags.has('A'))items=items.filter(x=>!x.name.startsWith('.')); items.sort((x,y)=>x.name.toLowerCase().localeCompare(y.name.toLowerCase()));if(flags.has('t'))items.sort((x,y)=>y.mtime-x.mtime);if(flags.has('S'))items.sort((x,y)=>io.fs.size(y)-io.fs.size(x));if(flags.has('r'))items.reverse(); if(multi)io.out(C.bold+pth+':'+C.x+'\n'); if(flags.has('l')){io.out('total '+Math.ceil(items.reduce((s,x)=>s+io.fs.size(x),0)/1024)+'\n');const sw=Math.max(4,...items.map(x=>String(io.fs.size(x)).length));for(const x of items){const sz=flags.has('h')?fmtBytes(io.fs.size(x)):String(io.fs.size(x));io.out(`${permStr(x)} 1 ${x.uid.padEnd(8)} ${x.gid.padEnd(8)} ${sz.padStart(flags.has('h')?7:sw)} ${fmtLs(x.mtime)} ${fileColor(x)}${x.name}${C.x}${x.type==='d'?'/':''}\n`)}} else if(!io.tty)io.out(items.map(x=>x.name).join('\n')+(items.length?'\n':'')); else if(items.length)io.out(columnize(items.map(x=>fileColor(x)+x.name+C.x+(x.type==='d'?'/':'')),io.cols));} return io.code},'ls [-lahtSr] [caminho…]'); def('cd','Muda de diretório','Arquivos',async(a,io)=>{let t=a[0]||'~';if(t==='-'){t=io.sh.env.OLDPWD||io.sh.cwd;io.out(t+'\n')}const abs=io.sh.abs(t);const n=io.fs.get(abs);if(!n){io.err(`bash: cd: ${t}: Arquivo ou diretório inexistente`);return 1}if(n.type!=='d'){io.err(`bash: cd: ${t}: Não é um diretório`);return 1}if(!canRead(abs,io.user)){io.err(`bash: cd: ${t}: Permissão negada`);return 1}io.sh.env.OLDPWD=io.sh.cwd;io.sh.cwd=abs;io.sh.env.PWD=abs;return 0}); def('pwd','Diretório atual','Arquivos',async(a,io)=>{io.out(io.sh.cwd+'\n');return 0}); def('mkdir','Cria diretórios (-p)','Arquivos',async(a,io)=>{const{flags,rest}=parseFlags(a);if(!rest.length){io.err('mkdir: falta operando');return 1}let code=0;for(const p of rest){const abs=io.sh.abs(p);if(!canWrite(abs,io.user)){io.err(`mkdir: ${p}: Permissão negada`);code=1;continue}if(io.fs.get(abs)&&!flags.has('p')){io.err(`mkdir: não é possível criar '${p}': Arquivo existe`);code=1;continue}const r=io.fs.mkdir(abs,io.user,{p:flags.has('p')});if(r.err){io.err(`mkdir: ${p}: ${r.err}`);code=1}}return code}); def('touch','Cria arquivo vazio / atualiza mtime','Arquivos',async(a,io)=>{if(!a.length){io.err('touch: falta operando');return 1}let c=0;for(const p of a){const abs=io.sh.abs(p);const n=io.fs.get(abs);if(n){n.mtime=Date.now();io.fs.touchChanged();continue}if(!canWrite(abs,io.user)){io.err(`touch: ${p}: Permissão negada`);c=1;continue}const r=io.fs.write(abs,'',io.user);if(r.err){io.err(`touch: ${p}: ${r.err}`);c=1}}return c}); def('rm','Remove arquivos (-r -f)','Arquivos',async(a,io)=>{const{flags,rest}=parseFlags(a);if(!rest.length){if(flags.has('f'))return 0;io.err('rm: falta operando');return 1}let c=0;for(const p of rest){const abs=io.sh.abs(p);if(abs==='/'){io.err("rm: é perigoso operar recursivamente em '/'");io.err('rm: use --no-preserve-root para ignorar esta proteção');if(a.includes('--no-preserve-root'))io.err(C.dim+'🐚 Boa tentativa. A Concha protege este servidor.'+C.x+'\n');c=1;continue}const n=io.fs.get(abs);if(!n){if(!flags.has('f')){io.err(`rm: não é possível remover '${p}': Arquivo ou diretório inexistente`);c=1}continue}if(!canWrite(abs,io.user)){io.err(`rm: ${p}: Permissão negada`);c=1;continue}if(n.type==='d'&&!flags.has('r')){io.err(`rm: não é possível remover '${p}': É um diretório`);c=1;continue}io.fs.remove(abs)}return c}); def('rmdir','Remove diretório vazio','Arquivos',async(a,io)=>{let c=0;for(const p of a){const abs=io.sh.abs(p);const n=io.fs.get(abs);if(!n||n.type!=='d'){io.err(`rmdir: '${p}': Não é um diretório`);c=1;continue}if(Object.keys(n.children).length){io.err(`rmdir: '${p}': Diretório não vazio`);c=1;continue}io.fs.remove(abs)}return c}); def('cp','Copia arquivos (-r)','Arquivos',async(a,io)=>{const{flags,rest}=parseFlags(a);if(rest.length<2){io.err('cp: falta operando de destino');return 1}const dst=io.sh.abs(rest.pop());let c=0;const dn=io.fs.get(dst);for(const s of rest){const src=io.sh.abs(s);const n=io.fs.get(src);if(!n){io.err(`cp: '${s}': Arquivo ou diretório inexistente`);c=1;continue}if(n.type==='d'&&!flags.has('r')){io.err(`cp: -r não especificado; omitindo diretório '${s}'`);c=1;continue}if(!canRead(src,io.user)){io.err(`cp: '${s}': Permissão negada`);c=1;continue}let target=dn?.type==='d'?dst+'/'+n.name:dst;if(!canWrite(target,io.user)){io.err(`cp: '${target}': Permissão negada`);c=1;continue}const{dir,base}=io.fs.parent(target);const pd=io.fs.get(dir);if(!pd){io.err(`cp: '${target}': Arquivo ou diretório inexistente`);c=1;continue}pd.children[base]=io.fs.clone(n);pd.children[base].mtime=Date.now();io.fs.touchChanged()}return c}); def('mv','Move / renomeia','Arquivos',async(a,io)=>{if(a.length<2){io.err('mv: falta operando');return 1}const dst=io.sh.abs(a.pop());let c=0;const dn=io.fs.get(dst);for(const s of a){const src=io.sh.abs(s);const n=io.fs.get(src);if(!n){io.err(`mv: '${s}': Arquivo ou diretório inexistente`);c=1;continue}if(!canWrite(src,io.user)){io.err(`mv: '${s}': Permissão negada`);c=1;continue}const target=dn?.type==='d'?dst+'/'+n.name:dst;if(!canWrite(target,io.user)){io.err(`mv: '${target}': Permissão negada`);c=1;continue}const{dir,base}=io.fs.parent(target);const pd=io.fs.get(dir);if(!pd){io.err(`mv: '${target}': destino inexistente`);c=1;continue}pd.children[base]=n;n.name=base;n.mtime=Date.now();io.fs.remove(src)}return c}); def('chmod','Altera permissões (octal)','Arquivos',async(a,io)=>{if(a.length<2||!/^[0-7]{3,4}$/.test(a[0])){io.err('chmod: uso: chmod 755 arquivo');return 1}const mode=parseInt(a[0],8)&0o7777;let c=0;for(const p of a.slice(1)){const abs=io.sh.abs(p);const n=io.fs.get(abs);if(!n){io.err(`chmod: '${p}': inexistente`);c=1;continue}if(!canWrite(abs,io.user)&&n.uid!==io.user){io.err(`chmod: '${p}': Permissão negada`);c=1;continue}n.mode=mode;io.fs.touchChanged()}return c}); def('chown','Altera dono (root)','Arquivos',async(a,io)=>{if(io.user!=='root'){io.err('chown: apenas root pode alterar dono');return 1}if(a.length<2){io.err('chown: uso: chown usuario[:grupo] arquivo');return 1}const[u,g]=a[0].split(':');let c=0;for(const p of a.slice(1)){const n=io.fs.get(io.sh.abs(p));if(!n){io.err(`chown: '${p}': inexistente`);c=1;continue}n.uid=u;if(g)n.gid=g;io.fs.touchChanged()}return c}); def('stat','Detalhes de um arquivo','Arquivos',async(a,io)=>{if(!a[0]){io.err('stat: falta operando');return 1}const abs=io.sh.abs(a[0]);const n=io.fs.get(abs);if(!n){io.err(`stat: '${a[0]}': inexistente`);return 1}io.out(` Arquivo: ${n.name}\n Tamanho: ${io.fs.size(n)}\tTipo: ${n.type==='d'?'diretório':'arquivo comum'}\n Modo: (${(n.mode&0o7777).toString(8).padStart(4,'0')}/${permStr(n)}) Uid: ${n.uid} Gid: ${n.gid}\n Modificar: ${new Date(n.mtime).toLocaleString('pt-BR')}\n`);return 0}); def('file','Adivinha o tipo do arquivo','Arquivos',async(a,io)=>{let c=0;for(const p of a){const n=io.fs.get(io.sh.abs(p));if(!n){io.err(`file: '${p}': inexistente`);c=1;continue}let t='ASCII text';if(n.type==='d')t='directory';else if(n.b64||/\.gz$/.test(n.name))t='gzip compressed data';else if(/\.json$/.test(n.name))t='JSON data';else if(/\.py$/.test(n.name))t='Python script, ASCII text executable';else if(/\.sh$/.test(n.name)||n.content.startsWith('#!'))t='Bourne-Again shell script, ASCII text executable';else if(/\.(html)$/.test(n.name))t='HTML document, ASCII text';else if(/\.(js|css|conf|ini|csv|md)$/.test(n.name))t='ASCII text';io.out(`${p}: ${t}\n`)}return c}); def('tree','Árvore de diretórios','Arquivos',async(a,io)=>{const{flags,rest,n}=parseFlags(a);const maxD=flags.has('L')?(n||2):4;const rootP=io.sh.abs(rest[0]||'.');const rn=io.fs.get(rootP);if(!rn||rn.type!=='d'){io.err(`${rest[0]||'.'} [erro ao abrir]`);return 1}let dirs=0,files=0,count=0;io.out(C.b+(rest[0]||'.')+C.x+'\n'); const walk=(node,prefix,depth)=>{const kids=Object.values(node.children).filter(k=>!k.name.startsWith('.')).sort((x,y)=>x.name.localeCompare(y.name));kids.forEach((k,i)=>{if(++count>400)return;const last=i===kids.length-1;io.out(prefix+(last?'└── ':'├── ')+fileColor(k)+k.name+C.x+'\n');if(k.type==='d'){dirs++;if(depth{let path='.',name=null,type=null;for(let i=0;i{const rel=(base||'.')+(abs===io.sh.abs(path)?'':abs.slice(io.sh.abs(path).length));if(type&&((type==='d'&&n.type!=='d')||(type==='f'&&n.type!=='f')))return;if(rx&&!rx.test(n.name))return;io.out(rel+'\n');found++});return found?0:1}); def('du','Uso de disco (-sh)','Arquivos',async(a,io)=>{const{flags,rest}=parseFlags(a);const p=rest[0]||'.';const n=io.fs.get(io.sh.abs(p));if(!n){io.err(`du: '${p}': inexistente`);return 1}const s=io.fs.size(n);io.out((flags.has('h')?fmtBytes(s):String(Math.ceil(s/1024)))+'\t'+p+'\n');return 0}); def('grep','Filtra linhas por padrão/regex','Texto',async(a,io)=>{const{flags,rest}=parseFlags(a);if(!rest.length){io.err('uso: grep [-invc] padrão [arquivo…]');return 2}const pat=rest.shift();let rx;try{rx=new RegExp(pat,flags.has('i')?'i':'')}catch{io.err('grep: expressão regular inválida');return 2}const txt=readInput(io,rest);if(io.code)return 2;let count=0,ln=0;const lines=txt.split('\n');if(lines[lines.length-1]==='')lines.pop(); for(const line of lines){ln++;const hit=rx.test(line);if(hit!==flags.has('v')){count++;if(!flags.has('c')){let shown=line;if(io.tty&&!flags.has('v'))shown=line.replace(new RegExp(rx.source,rx.flags+'g'),m=>C.br+m+C.x);io.out((flags.has('n')?C.g+ln+C.x+':':'')+shown+'\n')}}} if(flags.has('c'))io.out(count+'\n');return count?0:1},'grep [-invc] padrão [arquivo…]'); def('head','Primeiras linhas (-n)','Texto',async(a,io)=>{const{flags,rest,n}=parseFlags(a);const N=n??10;const txt=readInput(io,rest);if(io.code)return 1;io.out(txt.split('\n').slice(0,N).join('\n')+'\n');return 0}); def('tail','Últimas linhas (-n, -f ao vivo)','Texto',async(a,io)=>{const{flags,rest,n}=parseFlags(a);const N=n??10;if(flags.has('f')){const f=rest[0];if(!f){io.err('tail: -f requer um arquivo');return 1}const abs=io.sh.abs(f);const node=io.fs.get(abs);if(!node||node.type==='d'){io.err(`tail: '${f}': inexistente`);return 1}if(!canRead(abs,io.user)){io.err(`tail: '${f}': Permissão negada`);return 1} io.out(C.dim+`==> ${f} <== (Ctrl+C para parar)`+C.x+'\n');const lines=node.content.split('\n').filter(Boolean);io.out(lines.slice(-N).join('\n')+(lines.length?'\n':'')); const gen=node.logGen?LOGGENS[node.logGen]:null;let acc=0;while(!io.job.cancelled){await jwait(ri(650,1400),io.job);if(io.job.cancelled)break;if(gen){const l=gen();node.content+=l+'\n';io.fs.touchChanged();io.out(l+'\n');acc++}} io.out('\n');return 130} const txt=readInput(io,rest);if(io.code)return 1;const ls=txt.split('\n');if(ls[ls.length-1]==='')ls.pop();io.out(ls.slice(-N).join('\n')+(ls.length?'\n':''));return 0},'tail [-n N] [-f] arquivo'); def('wc','Conta linhas/palavras/bytes','Texto',async(a,io)=>{const{flags,rest}=parseFlags(a);const txt=readInput(io,rest);if(io.code)return 1;const l=txt.split('\n').length-(txt.endsWith('\n')?1:0),w=(txt.match(/\S+/g)||[]).length,c=new Blob([txt]).size;const parts=[];if(flags.has('l'))parts.push(l);if(flags.has('w'))parts.push(w);if(flags.has('c'))parts.push(c);if(!parts.length)parts.push(l,w,c);io.out(parts.map(x=>String(x).padStart(7)).join('')+(rest[0]?' '+rest[0]:'')+'\n');return 0}); def('sort','Ordena linhas (-r -n -u)','Texto',async(a,io)=>{const{flags,rest}=parseFlags(a);const txt=readInput(io,rest);if(io.code)return 1;let ls=txt.split('\n');if(ls[ls.length-1]==='')ls.pop();ls.sort(flags.has('n')?(x,y)=>parseFloat(x)-parseFloat(y):(x,y)=>x.localeCompare(y));if(flags.has('r'))ls.reverse();if(flags.has('u'))ls=[...new Set(ls)];io.out(ls.join('\n')+(ls.length?'\n':''));return 0}); def('uniq','Remove linhas repetidas (-c)','Texto',async(a,io)=>{const{flags,rest}=parseFlags(a);const txt=readInput(io,rest);const ls=txt.split('\n');let prev=null,cnt=0,out='';for(const l of ls){if(l===prev)cnt++;else{if(prev!==null)out+=(flags.has('c')?String(cnt).padStart(7)+' ':'')+prev+'\n';prev=l;cnt=1}}if(prev!==null&&prev!=='')out+=(flags.has('c')?String(cnt).padStart(7)+' ':'')+prev+'\n';io.out(out);return 0}); def('base64','Codifica/decodifica (-d)','Texto',async(a,io)=>{const{flags,rest}=parseFlags(a);const txt=readInput(io,rest).replace(/\n$/,'');try{if(flags.has('d')){const bin=atob(txt.trim());const bytes=Uint8Array.from(bin,c=>c.charCodeAt(0));io.out(new TextDecoder().decode(bytes)+'\n')}else io.out(btoa(new TextEncoder().encode(txt).reduce((s,b)=>s+String.fromCharCode(b),''))+'\n')}catch{io.err('base64: entrada inválida');return 1}return 0}); def('sha256sum','Hash SHA-256 real do conteúdo','Texto',async(a,io)=>{const txt=readInput(io,a);if(io.code)return 1;let hex='';try{const d=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(txt));hex=[...new Uint8Array(d)].map(b=>b.toString(16).padStart(2,'0')).join('')}catch{let x=hashStr(txt);hex='';for(let i=0;i<8;i++){x=(x*1103515245+12345)>>>0;hex+=x.toString(16).padStart(8,'0')}}io.out(hex+' '+(a[0]||'-')+'\n');return 0}); def('whoami','Usuário efetivo','Sistema',async(a,io)=>{io.out(io.user+'\n');return 0}); def('id','Identidade do usuário','Sistema',async(a,io)=>{const r=io.user==='root';io.out(`uid=${r?0:1000}(${io.user}) gid=${r?0:1000}(${io.user}) grupos=${r?0:1000}(${io.user})${r?'':',27(sudo),998(docker)'}\n`);return 0}); def('hostname','Nome da máquina','Sistema',async(a,io)=>{io.out(io.srv.p.hostname+'\n');return 0}); def('uname','Info do kernel (-a -r)','Sistema',async(a,io)=>{const p=io.srv.p;if(a.includes('-a'))io.out(`Linux ${p.hostname} ${p.kernel} #45-Ubuntu SMP PREEMPT_DYNAMIC Fri Sep 18 09:32:10 UTC 2026 x86_64 GNU/Linux\n`);else if(a.includes('-r'))io.out(p.kernel+'\n');else io.out('Linux\n');return 0}); def('date','Data e hora atual','Sistema',async(a,io)=>{io.out(new Date().toLocaleString('pt-BR',{weekday:'short',year:'numeric',month:'short',day:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit',timeZoneName:'short'})+'\n');return 0}); def('uptime','Tempo ligado e carga','Sistema',async(a,io)=>{const p=io.srv.p;io.out(` ${fmtClock(Date.now())} up ${p.upDays} days, ${ri(0,23)}:${String(ri(0,59)).padStart(2,'0')}, 1 user, load average: ${p.load.toFixed(2)}, ${(p.load*.92).toFixed(2)}, ${(p.load*.85).toFixed(2)}\n`);return 0}); def('df','Uso dos sistemas de arquivos (-h)','Sistema',async(a,io)=>{const p=io.srv.p;const rows=[['/dev/sda1','40G','12.5G','27.5G','32%','/'],['tmpfs',Math.round(p.mem/2)+'M','2.1M',Math.round(p.mem/2-2)+'M','1%','/run'],['/dev/sdb1','200G','86G','114G','44%','/var/lib/docker']];io.out(C.bold+'Sist. Arq. Tam. Usado Disp. Uso% Montado em'+C.x+'\n');rows.forEach(r=>io.out(r[0].padEnd(15)+r[1].padStart(5)+r[2].padStart(7)+r[3].padStart(7)+r[4].padStart(5)+' '+r[5]+'\n'));return 0}); def('free','Memória (-h)','Sistema',async(a,io)=>{const p=io.srv.p;const f=x=>(x/1024).toFixed(1)+'Gi';io.out(C.bold+' total usado livre buff/cache'+C.x+'\n');io.out('Mem: '+f(p.mem).padStart(9)+f(p.memUsed).padStart(13)+f(p.mem-p.memUsed-820).padStart(13)+f(820).padStart(15)+'\n');io.out('Swap: 2.0Gi 0.0Gi 2.0Gi\n');return 0}); const baseProcs=io=>{const p=io.srv.p;const list=[[1,'root','systemd',.1,.4],[640,'root','cron',0,.1],[812,'root','sshd',0,.2],[901,'root','systemd-journal',.2,.5]];for(const[k,v]of Object.entries(io.srv.services))if(v.active&&v.pid>900)list.push([v.pid,k==='app'?p.shellUser:'root',{nginx:'nginx: worker',docker:'dockerd',app:'node server.js',postgresql:'postgres: writer',sensors:'python3 sensor.py'}[k]||k,ri(1,90)/10,k==='postgresql'?24.5:ri(5,40)/10]);list.push([ri(2000,2999),io.user,'bash',0,.1],[ri(3000,3999),io.user,'ps aux',0,.1]);return list}; def('ps','Processos (aux)','Sistema',async(a,io)=>{io.out(C.bold+'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND'+C.x+'\n');for(const[pid,user,cmd,cpu,mem]of baseProcs(io))io.out(user.padEnd(10)+String(pid).padStart(6)+' '+cpu.toFixed(1).padStart(4)+' '+mem.toFixed(1).padStart(4)+String(ri(20000,900000)).padStart(7)+String(ri(2000,180000)).padStart(7)+' ? Ss '+fmtLs(Date.now()-ri(1,900)*60e3).slice(0,6)+' 0:0'+ri(0,9)+' '+cmd+'\n');return 0}); def('top','Monitor ao vivo (q sai)','Sistema',async(a,io)=>{if(!io.tty){io.err('top: requer um terminal');return 1}io.out('\x1b[?25l');const p=io.srv.p;const draw=()=>{const rows=Math.max(10,io.rows);const procs=baseProcs(io).map(x=>[...x,Math.random()*14]).sort((x,y)=>y[5]-x[5]);let s='\x1b[H'; s+=C.bc+`top - ${fmtClock(Date.now())} up ${p.upDays} days, 1 user, load average: ${(p.load+Math.random()*.2).toFixed(2)}, ${p.load.toFixed(2)}, ${(p.load*.9).toFixed(2)}`+C.x+'\n'; s+=`Tasks: ${C.bold}${138+procs.length}${C.x} total, ${C.g}1 running${C.x}, 137 sleeping\n`; const cpu=clamp(p.load*10+ri(2,12),2,96);s+=`%Cpu(s): ${cpu.toFixed(1)} us ${(cpu/4).toFixed(1)} sy ${(100-cpu*1.25).toFixed(1)} id ${C.g}[${'▮'.repeat(Math.round(cpu/4)).padEnd(25,'·')}]${C.x}\n`; const mp=p.memUsed/p.mem*100;s+=`MiB Mem: ${p.mem.toFixed(0)} total, ${(p.mem-p.memUsed).toFixed(0)} free ${C.y}[${'▮'.repeat(Math.round(mp/4)).padEnd(25,'·')}]${C.x} ${mp.toFixed(0)}%\n\n`; s+=C.bold+' PID USER %CPU %MEM COMMAND'+C.x+'\n'; const maxRows=Math.max(3,rows-9);procs.slice(0,maxRows).forEach(([pid,user,cmd,c,m],i)=>{const line=String(pid).padStart(5)+' '+user.padEnd(9)+c.toFixed(1).padStart(5)+' '+m.toFixed(1).padStart(5)+' '+cmd;s+=(i===0?C.bg:'')+line.slice(0,io.cols-1)+C.x+'\n'}); s+='\n'+C.dim+'q ou Ctrl+C para sair · atualização a cada 1s'+C.x+'\n';s+='\x1b[J';io.out(s)}; draw();await new Promise(res=>{io.job.raw=d=>{if(d==='q'||d==='Q'){io.job.cancel()}};const t=setInterval(()=>{if(io.job.cancelled){clearInterval(t);res();return}draw()},1000);io.job.timers.push(t);io.job.onCancel(()=>{clearInterval(t);res()})}); io.out('\x1b[?25h\x1b[2J\x1b[H');return io.job.cancelled?0:0}); def('ping','Ping com estatísticas (Ctrl+C)','Rede',async(a,io)=>{const{flags,rest,n}=parseFlags(a);const target=rest[0];if(!target){io.err('ping: uso: ping [-c N] destino');return 1}const known=Object.values(PERSONAS).find(p=>p.hostname===target.split('.')[0]);const ip=target==='localhost'||target==='127.0.0.1'?'127.0.0.1':known?.ip||(/^\d+\.\d+\.\d+\.\d+$/.test(target)?target:null); if(!ip&&target.includes('.')){/* resolve determinístico */}const rip=ip||ipOf(target);const base=ip==='127.0.0.1'?.04:known?.lat||ri(20,90);const count=flags.has('c')?(n||4):Infinity; io.out(`PING ${target} (${rip}) 56(84) bytes of data.\n`);if(!isFinite(count))io.out(C.dim+'Ctrl+C para parar e ver as estatísticas'+C.x+'\n'); let sent=0,recv=0,min=1e9,max=0,sum=0;for(let i=1;i<=count;i++){if(io.job.cancelled)break;await jwait(i===1?300:ri(750,1050),io.job);if(io.job.cancelled)break;const t=base*(0.85+Math.random()*.4);sent++;recv++;min=Math.min(min,t);max=Math.max(max,t);sum+=t;io.out(`64 bytes de ${rip}: icmp_seq=${i} ttl=${ip==='127.0.0.1'?64:57} tempo=${t.toFixed(1)} ms\n`)} io.out(`\n--- ${target} estatísticas de ping ---\n${sent} pacotes transmitidos, ${recv} recebidos, 0% de perda\n`);if(recv)io.out(`rtt min/avg/max = ${min.toFixed(3)}/${(sum/recv).toFixed(3)}/${max.toFixed(3)} ms\n`);return io.job.cancelled?130:0},'ping [-c N] destino'); const CURLDB={'api.ipify.org':{ct:'text/plain',body:()=>ipOf('cliente')+'\n'},'ifconfig.me':{ct:'text/plain',body:()=>ipOf('cliente')+'\n'},'example.com':{ct:'text/html',body:()=>'\n\nExample Domain\n

Example Domain

This domain is for use in illustrative examples.

\n\n'},'api.github.com/zen':{ct:'text/plain',body:()=>pick(['Design for failure.','Anything added dilutes everything else.','Practicality beats purity.','Responsive is better than fast.'])+'\n'}}; def('curl','Requisita uma URL (demo) (-I -s)','Rede',async(a,io)=>{const{flags,rest}=parseFlags(a);const url=rest[0];if(!url){io.err('curl: tente curl ');return 2}const hostpart=url.replace(/^https?:\/\//,'');const bare=hostpart.split('/')[0];const path='/'+hostpart.split('/').slice(1).join('/'); if(bare==='localhost'||bare==='127.0.0.1'){if(!io.srv.services.nginx?.active){io.err('curl: (7) Failed to connect to localhost port 80: Connection refused');return 7}await jwait(220,io.job);if(flags.has('I')){io.out('HTTP/1.1 200 OK\nServer: nginx/1.27.2\nContent-Type: text/html\n\n');return 0}const idx=io.fs.get('/var/www/app/index.html');io.out(idx?idx.content:'ok\n');return 0} const key=Object.keys(CURLDB).find(k=>hostpart===k||hostpart.startsWith(k));await jwait(ri(200,600),io.job);if(io.job.cancelled)return 130; if(flags.has('I')){io.out(`HTTP/2 200\nserver: nginx\ncontent-type: ${key?CURLDB[key].ct:'application/json'}\ndate: ${new Date().toUTCString()}\nx-demo: concha\n\n`);return 0} if(key)io.out(CURLDB[key].body());else io.out(JSON.stringify({ok:true,host:bare,path,ts:new Date().toISOString(),demo:'concha'},null,2)+'\n');return 0},'curl [-Is] url'); def('wget','Baixa arquivo com progresso (demo)','Rede',async(a,io)=>{const url=a.find(x=>!x.startsWith('-'));if(!url){io.err('wget: URL ausente');return 1}const name=url.split('/').filter(Boolean).pop()||'index.html';const abs=io.sh.abs(name);if(!canWrite(abs,io.user)){io.err(`wget: ${name}: Permissão negada`);return 1}const hostpart=url.replace(/^https?:\/\//,'');const key=Object.keys(CURLDB).find(k=>hostpart.startsWith(k));const body=key?CURLDB[key].body():'conteúdo demo de '+url+'\n'; io.out(`--${new Date().toISOString().slice(0,19).replace('T',' ')}-- ${url}\nResolvendo ${hostpart.split('/')[0]}… conectado.\nRequisição HTTP enviada, aguardando resposta… 200 OK\nTamanho: ${new Blob([body]).size} [${key?CURLDB[key].ct:'text/plain'}]\nSalvando em: '${name}'\n\n`); if(!await progressBar(io.job,io.out,name.padEnd(16).slice(0,16),1400))return 130;io.fs.write(abs,body,io.user);io.out(`\n'${name}' salvo [${new Blob([body]).size}]\n`);return 0}); def('ss','Sockets em escuta (-tulpn)','Rede',async(a,io)=>{const p=io.srv.p;io.out(C.bold+'Netid State Local Address:Port Peer Address:Port Process'+C.x+'\n');const rows=[['tcp','LISTEN','0.0.0.0:22','0.0.0.0:*','sshd pid=812']];if(io.srv.services.nginx?.active)rows.push(['tcp','LISTEN','0.0.0.0:80','0.0.0.0:*','nginx pid=1105'],['tcp','LISTEN','0.0.0.0:443','0.0.0.0:*','nginx pid=1105']);if(io.srv.services.app?.active)rows.push(['tcp','LISTEN','127.0.0.1:3000','0.0.0.0:*','node pid=1287']);if(io.srv.services.postgresql?.active)rows.push(['tcp','LISTEN',p.ip+':5432','0.0.0.0:*','postgres pid=1201']);rows.push(['udp','UNCONN','0.0.0.0:68','0.0.0.0:*','dhclient']);rows.forEach(r=>io.out(r[0].padEnd(6)+r[1].padEnd(7)+(r[2]).padEnd(22)+r[3].padEnd(19)+r[4]+'\n'));return 0});CMDS.netstat=CMDS.ss; def('ssh','Abre sessão (use uma aba nova!)','Rede',async(a,io)=>{io.err('ssh: a demo não suporta sessões aninhadas — abra uma nova aba no Concha 😉');return 255}); def('sudo','Executa como root (-i vira root)','Serviços',async(a,io)=>{if(!a.length){io.err('uso: sudo comando… · sudo -i para shell root');return 1}if(a[0]==='-i'||a[0]==='su'||(a[0]==='-s')){io.sh.pushRoot();return 0}if(a[0]==='apt'||a[0]==='apt-get')return CMDS.apt.run(a.slice(1),{...io,user:'root',root:true});const name=a[0];const c=CMDS[name];if(!c){io.err(`sudo: ${name}: comando não encontrado`);return 127}return c.run(a.slice(1),{...io,user:'root',root:true})}); def('su','Vira root','Serviços',async(a,io)=>{io.sh.pushRoot();return 0}); def('systemctl','Serviços: status/start/stop/restart','Serviços',async(a,io)=>{const[action,unit]=a;if(!action||action==='list-units'){io.out(C.bold+'UNIT LOAD ACTIVE SUB DESCRIPTION'+C.x+'\n');for(const[k,v]of Object.entries(io.srv.services))io.out((k+'.service').padEnd(24)+'loaded '+(v.active?C.g+'active running'+C.x:C.r+'inactive dead'+C.x)+' '+v.desc+'\n');return 0} const u=(unit||'').replace(/\.service$/,'');const s=io.srv.services[u];if(!s){io.err(`Unit ${u}.service could not be found.`);return 4} if(action==='status'){io.out(`${C.bold}${u}.service${C.x} - ${s.desc}\n Loaded: loaded (/usr/lib/systemd/system/${u}.service; enabled)\n Active: ${s.active?C.bg+'● active (running)'+C.x:C.br+'● inactive (dead)'+C.x} since ${fmtLs(Date.now()-ri(2,50)*3600e3)}\n`);if(s.active)io.out(` Main PID: ${s.pid} (${u})\n Tasks: ${ri(2,40)}\n Memory: ${(ri(20,400)).toFixed(1)}M\n`);return s.active?0:3} if(!io.root){io.err('==== AUTHENTICATION REQUIRED ====\nFailed to '+action+' '+u+'.service: Access denied (use sudo)');return 1} if(action==='start'){s.active=true;s.pid=ri(1500,4000)}else if(action==='stop'){s.active=false}else if(action==='restart'){s.active=true;s.pid=ri(1500,4000);await jwait(500,io.job)}else{io.err(`Operação desconhecida: ${action}`);return 1}io.fs.touchChanged();return 0}); def('docker','Containers: ps / logs / restart','Serviços',async(a,io)=>{const sub=a[0]||'ps';const cs=io.srv.containers;if(!cs.length){io.err('Cannot connect to the Docker daemon (este host não roda docker)');return 1} if(sub==='ps'){io.out(C.bold+'CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES'+C.x+'\n');cs.forEach(c=>io.out(c.id+' '+c.image.padEnd(20)+'"'+(c.name.includes('nginx')?'nginx -g':'node server.js').padEnd(22)+'" '+C.g+('Up '+c.up).padEnd(12)+C.x+' '+c.ports.padEnd(30)+' '+c.name+'\n'));return 0} if(sub==='logs'){const c=cs.find(x=>x.name===a[1]);if(!c){io.err('No such container: '+(a[1]||''));return 1}for(let i=0;i<10;i++)io.out((c.name.includes('nginx')?nginxLine:appLine())+'\n');return 0} if(sub==='restart'){const c=cs.find(x=>x.name===a[1]);if(!c){io.err('No such container');return 1}await jwait(900,io.job);c.up='2 seconds';io.out(c.name+'\n');return 0} io.err('docker: subcomando demo suportado: ps, logs, restart');return 1}); def('apt','Gerencia pacotes (update/install)','Serviços',async(a,io)=>{const sub=a[0];if(!['update','upgrade','install','list'].includes(sub||'')){io.err('apt: uso: apt update | upgrade | install ');return 1} if(sub==='list'){io.out('Listing…\nnginx/noble 1.27.2-1ubuntu1 amd64 [installed]\ndocker-ce/noble 27.3.1 amd64 [installed]\nhtop/noble 3.3.0 amd64\npostgresql-16/noble 16.4 amd64\n');return 0} if(!io.root){io.err('E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)\nE: Are you root? (tente: sudo apt …)');return 100} if(sub==='update'){for(const l of['Hit:1 http://br.archive.ubuntu.com/ubuntu noble InRelease','Get:2 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]','Hit:3 https://download.docker.com/linux/ubuntu noble InRelease','Get:4 http://br.archive.ubuntu.com/ubuntu noble-updates/main amd64 Packages [844 kB]']){await jwait(ri(150,420),io.job);if(io.job.cancelled)return 130;io.out(l+'\n')}io.out('Fetched 970 kB in 1s\nReading package lists… Done\n'+C.g+'All packages are up to date.'+C.x+'\n');return 0} const pkg=sub==='install'?a[1]:'sistema';if(sub==='install'&&!pkg){io.err('E: informe o pacote');return 1} io.out(`Lendo listas de pacotes… Done\nConstruindo árvore de dependências… Done\n${sub==='install'?`Os NOVOS pacotes a seguir serão instalados:\n ${pkg}\n`:''}0 upgraded, ${sub==='install'?1:12} newly installed.\n`); if(!await progressBar(io.job,io.out,'Baixando ',1500))return 130; for(const l of['Selecting previously unselected package…','Unpacking '+pkg+'…','Setting up '+pkg+'…','Processing triggers for man-db…']){await jwait(ri(200,450),io.job);if(io.job.cancelled)return 130;io.out(l+'\n')} io.out(C.g+'✓ '+pkg+' pronto (simulado — nada foi instalado de verdade)'+C.x+'\n');return 0});CMDS['apt-get']=CMDS.apt; def('env','Variáveis de ambiente','Sistema',async(a,io)=>{Object.entries(io.sh.env).filter(([k])=>k!=='?').sort().forEach(([k,v])=>io.out(k+'='+v+'\n'));return 0});CMDS.printenv=CMDS.env; def('export','Define variável (export A=B)','Sistema',async(a,io)=>{for(const t of a){const i=t.indexOf('=');if(i>0)io.sh.env[t.slice(0,i)]=t.slice(i+1)}return 0}); def('unset','Remove variável','Sistema',async(a,io)=>{a.forEach(k=>delete io.sh.env[k]);return 0}); def('which','Localiza um comando','Sistema',async(a,io)=>{let c=0;for(const x of a){if(CMDS[x])io.out('/usr/bin/'+x+'\n');else c=1}return c}); def('sleep','Pausa N segundos','Sistema',async(a,io)=>{const s=parseFloat(a[0])||1;await jwait(Math.min(s,60)*1000,io.job);return io.job.cancelled?130:0}); def('neofetch','Cartão do sistema com logo','Diversão',async(a,io)=>{const p=io.srv.p;const art=NEO_ART[p.osId]||NEO_ART.debian;const artW=Math.max(...art.map(x=>x.length))+3; const up=`${p.upDays} days, ${ri(0,23)} hours`;const info=[[io.user+'@'+p.hostname,null],['─'.repeat((io.user+'@'+p.hostname).length),null],['OS',p.os+' x86_64'],['Kernel',p.kernel],['Uptime',up],['Packages',p.pkgs+' (dpkg)'],['Shell','bash 5.2.21'],['Terminal','Concha Web 🐚'],['CPU',p.cpu],['Memory',`${p.memUsed}MiB / ${p.mem}MiB`]]; const rows=Math.max(art.length,info.length+3);for(let i=0;i`\x1b[3${x}m███${C.x}`).join('');else if(i===info.length+2)right=[0,1,2,3,4,5,6,7].map(x=>`\x1b[9${x}m███${C.x}`).join(''); io.out(C.br+left+C.x+right+'\n')}return 0}); def('cowsay','Uma vaca falante','Diversão',async(a,io)=>{const msg=a.join(' ')||'Muuu! Bem-vindo ao Concha.';const W=Math.min(38,Math.max(...msg.split(' ').reduce((acc,w)=>{const last=acc[acc.length-1];if(last&&(last+' '+w).length<=38)acc[acc.length-1]=last+' '+w;else acc.push(w);return acc},['']).map(x=>x.length)));const lines=msg.split(' ').reduce((acc,w)=>{const last=acc[acc.length-1];if(last!==undefined&&(last+' '+w).length<=W)acc[acc.length-1]=last+' '+w;else acc.push(w);return acc},[]); const w=Math.max(...lines.map(l=>l.length));io.out(' '+'_'.repeat(w+2)+'\n');lines.forEach((l,i)=>{const L=lines.length===1?'<':i===0?'/':i===lines.length-1?'\\':'|';const R=lines.length===1?'>':i===0?'\\':i===lines.length-1?'/':'|';io.out(L+' '+l.padEnd(w)+' '+R+'\n')}); io.out(' '+'-'.repeat(w+2)+'\n \\ ^__^\n \\ (oo)\\_______\n (__)\\ )\\/\\\n ||----w |\n || ||\n');return 0}); def('fortune','Uma frase aleatória','Diversão',async(a,io)=>{io.out(C.m+'“'+pick(['Funciona na minha máquina — então vamos enviar a máquina.','Não há nuvem, apenas o computador de outra pessoa.','O melhor backup é aquele que você já restaurou uma vez.','Simplicidade é a sofisticação final.','Semana de deploy na sexta? Coragem.','Um terminal por vez, um servidor por vez.','Leia os logs. Os logs sabem.','99 pequenos bugs no código… corrige um… 127 pequenos bugs no código.'])+'”'+C.x+'\n');return 0}); def('cmatrix','Chuva de código (q/Ctrl+C sai)','Diversão',async(a,io)=>{if(!io.tty)return 1;io.out('\x1b[?25l\x1b[2J\x1b[H');const chars='アイウエオカキクケコ01<>[]{}#$';await new Promise(res=>{const t=setInterval(()=>{if(io.job.cancelled){clearInterval(t);res();return}let line='';for(let i=0;i{if(d==='q')io.job.cancel()};io.job.onCancel(()=>{clearInterval(t);res()})});io.out('\x1b[?25h\x1b[2J\x1b[H');return 0}); def('vim','Editor (indisponível na demo)','Diversão',async(a,io)=>{io.err(C.y+'Editor interativo não está disponível no servidor demo.'+C.x+'\nDica: use echo "texto" > arquivo ou o painel SFTP →');return 127});CMDS.vi=CMDS.vim;CMDS.nano=CMDS.vim; /* ============================== EDITOR DE LINHA ============================== */ const EOF=Symbol('eof'),ABORT=Symbol('abort'); class LineEditor{ constructor(sess){this.s=sess;this.active=false;this.buf=[];this.pos=0;this.prompt='';this.mask=false;this.hist=null;this.hIdx=-1;this.saved=null;this.res=null;this.opts={};this.search=null;this.lastTab=0;this.lastTabBuf=''} readLine(prompt,{mask=false,hist=null,cancelRejects=false}={}){this.prompt=prompt;this.mask=mask;this.hist=hist;this.opts={cancelRejects};this.buf=[];this.pos=0;this.hIdx=-1;this.search=null;this.active=true;this.redraw();return new Promise(r=>{this.res=r})} stop(){this.active=false;this.res=null} plainLen(){return ansiStrip(this.prompt).length} finish(v){this.active=false;const r=this.res;this.res=null;r?.(v)} redraw(){if(this.search){const q=this.search.q,m=this.search.match||'';this.s.out(`\r\x1b[K${C.dim}(reverse-i-search)\`${q}':${C.x} ${m}\x1b[${Math.max(1,1)}D`);return} const text=this.mask?'*'.repeat(this.buf.length):this.buf.join('');let out='\r\x1b[K'+this.prompt+text;const back=this.buf.length-this.pos;if(back>0)out+=`\x1b[${back}D`;this.s.out(out)} interrupt(){this.s.out('^C\r\n');this.buf=[];this.pos=0;this.search=null;if(this.opts.cancelRejects)this.finish(ABORT);else this.finish('')} data(raw){if(!this.active)return;const d=raw.replace(/\r\n/g,'\r').replace(/\n/g,'\r');const toks=d.match(/\x1b\[[0-9;]*[A-Za-z~]|\x1bO[A-Z]|[\s\S]/g)||[];let needRedraw=false; for(const t of toks){if(!this.active)break; if(this.search){this.searchKey(t);continue} if(t==='\r'){this.s.out('\r\n');SND.tick();const line=this.buf.join('');this.finish(line);continue} if(t==='\x03'){this.interrupt();continue} if(t==='\x04'){if(!this.buf.length){this.s.out('\r\n');this.finish(EOF)}else{this.buf.splice(this.pos,1);needRedraw=true}continue} if(t==='\x7f'){if(this.pos>0){this.buf.splice(this.pos-1,1);this.pos--;needRedraw=true;SND.tick()}continue} if(t==='\x1b[3~'){this.buf.splice(this.pos,1);needRedraw=true;continue} if(t==='\t'){this.complete();continue} if(t==='\x0c'){this.s.out('\x1b[2J\x1b[H');needRedraw=true;continue} if(t==='\x12'){if(this.hist?.length){this.search={q:'',match:this.hist[0],idx:0};needRedraw=true}continue} if(t==='\x1b[A'){if(this.hist?.length){if(this.hIdx===-1)this.saved=[...this.buf];this.hIdx=Math.min(this.hIdx+1,this.hist.length-1);this.buf=[...this.hist[this.hIdx]];this.pos=this.buf.length;needRedraw=true}continue} if(t==='\x1b[B'){if(this.hIdx>-1){this.hIdx--;this.buf=this.hIdx===-1?[...this.saved]:[...this.hist[this.hIdx]];this.pos=this.buf.length;needRedraw=true}continue} if(t==='\x1b[C'){this.pos=Math.min(this.pos+1,this.buf.length);needRedraw=true;continue} if(t==='\x1b[D'){this.pos=Math.max(this.pos-1,1)-1;needRedraw=true;continue} if(t==='\x1b[H'||t==='\x1b[1~'||t==='\x01'){this.pos=0;needRedraw=true;continue} if(t==='\x1b[F'||t==='\x1b[4~'||t==='\x05'){this.pos=this.buf.length;needRedraw=true;continue} if(t==='\x0b'){this.buf.splice(this.pos);needRedraw=true;continue} if(t==='\x15'){this.buf.splice(0,this.pos);this.pos=0;needRedraw=true;continue} if(t==='\x17'){let i=this.pos;while(i>0&&this.buf[i-1]===' ')i--;while(i>0&&this.buf[i-1]!==' ')i--;this.buf.splice(i,this.pos-i);this.pos=i;needRedraw=true;continue} if(t==='\x1b'||t.startsWith('\x1b'))continue; if(t>=' '){this.buf.splice(this.pos,0,...Array.from(t));this.pos+=Array.from(t).length;needRedraw=true;SND.tick()}} if(needRedraw&&this.active)this.redraw()} searchKey(t){const s=this.search;if(t==='\r'){this.buf=[...(s.match||'')];this.pos=this.buf.length;this.search=null;this.s.out('\r\n');this.finish(this.buf.join(''));return} if(t==='\x03'){this.search=null;this.interrupt();return} if(t==='\x12'){s.idx=Math.min(s.idx+1,this.hist.length-1);const hits=this.hist.filter(x=>x.includes(s.q));s.match=hits[Math.min(s.idx,hits.length-1)]||'';this.redraw();return} if(t==='\x1b'||t.startsWith('\x1b[')){this.buf=[...(s.match||'')];this.pos=this.buf.length;this.search=null;this.redraw();return} if(t==='\x7f'){s.q=s.q.slice(0,-1)}else if(t>=' ')s.q+=t;else return; const hits=this.hist.filter(x=>x.includes(s.q));s.idx=0;s.match=hits[0]||'';this.redraw()} complete(){const res=this.s.shell?.complete(this.buf.join(''),this.pos);if(!res)return;const now=Date.now(); if(res.options){if(now-this.lastTab<700&&this.lastTabBuf===this.buf.join('')){this.s.out('\r\n'+columnize(res.options.map(o=>(o.dir?C.b:C.c)+o.label+C.x),this.s.cols||80));this.redraw()}this.lastTab=now;this.lastTabBuf=this.buf.join('');return} this.buf.splice(res.start,this.pos-res.start,...Array.from(res.text));this.pos=res.start+Array.from(res.text).length;this.redraw()}} /* ============================== SHELL ============================== */ function tokenize(str,env){const out=[];let cur='',q=false,quote=null,i=0;const push=()=>{if(cur!==''||q){out.push({t:cur,q});cur='';q=false}}; while(i'&&stage.slice(i).match(/^2>&1/)){return extractRedirect(stage.slice(0,i)+stage.slice(i+4))}if(c==='>'){const append=stage[i+1]==='>';const target=stage.slice(i+(append?2:1)).trim().split(/\s+/)[0];return{stage:stage.slice(0,i).trim(),target,append}}}return{stage,target:null}} class Shell{ constructor(sess,srv){this.s=sess;this.srv=srv;this.fs=srv.fs;this.user=srv.p.shellUser;this.rootStack=[];this.cwd=srv.home;this.hist=[];this.lastCode=0;this.editor=new LineEditor(sess);sess.shell=this; this.env={USER:this.user,HOME:srv.home,HOSTNAME:srv.p.hostname,SHELL:'/bin/bash',TERM:'xterm-256color',LANG:'pt_BR.UTF-8',PWD:this.cwd,PATH:'/usr/local/bin:/usr/bin:/bin','?':'0',PS1:'\\u@\\h:\\w\\$ '}} abs(p){return this.fs.norm(p,this.cwd,this.env.HOME)} get home(){return this.env.HOME} prettyCwd(){const h=this.env.HOME;return this.cwd===h?'~':this.cwd.startsWith(h+'/')?'~'+this.cwd.slice(h.length):this.cwd} promptStr(){const err=this.lastCode!==0;const uc=this.user==='root'?C.br:C.bg;const sym=this.user==='root'?'#':'$';return `${uc}${this.user}@${this.srv.p.hostname}${C.x}:${C.b}${this.prettyCwd()}${C.x}${err?C.br:C.g}${sym}${C.x} `} pushRoot(){this.rootStack.push({user:this.user,home:this.env.HOME,cwd:this.cwd});this.user='root';this.env.USER='root';this.env.HOME='/root';this.env.OLDPWD=this.cwd;this.cwd='/root';this.env.PWD='/root';this.s.out(C.dim+'[sudo] se tornando root — digite exit para voltar'+C.x+'\n')} popRoot(){const st=this.rootStack.pop();if(!st)return false;this.user=st.user;this.env.USER=st.user;this.env.HOME=st.home;this.cwd=st.cwd;this.env.PWD=st.cwd;return true} doExit(code){if(this.popRoot()){this.s.out(C.dim+'[de volta a '+this.user+']'+C.x+'\n');return}this.s.out('logout\n');this.s.close(code,'Conexão com '+this.s.host.host+' encerrada.')} async start(){const s=this.s,hcfg=s.host,p=this.srv.p;const job=s.connectJob; const w=async ms=>{await jwait(ms,job)}; s.out(C.dim+`Conectando a ${hcfg.host}:${hcfg.port}…`+C.x+'\n');await w(ri(220,450));if(s.dead)return; s.out('Conexão estabelecida. Trocando chaves…\n'+C.dim+'SSH-2.0-OpenSSH_9.6p1 · kex curve25519-sha256 · cifra chacha20-poly1305@openssh.com'+C.x+'\n');await w(ri(150,320));if(s.dead)return; const fp=fpOf(hcfg.host); if(!DB.state.known[hcfg.host]){s.out(`\nA autenticidade do host '${hcfg.host} (${hcfg.mode==='demo'?p.ip:ipOf(hcfg.host)})' não pode ser estabelecida.\nImpressão da chave ED25519: ${C.y}SHA256:${fp}${C.x}.\n`); const ans=await this.editor.readLine('Tem certeza que deseja continuar (sim/não)? ',{cancelRejects:true});if(s.dead)return;if(ans===ABORT||ans===EOF){s.close(255,'Conexão cancelada.');return} if(!/^(s|sim|y|yes)$/i.test(ans.trim())){s.out(C.r+'Host key verification failed.'+C.x+'\n');s.close(255,'Chave do host recusada.');return} DB.state.known[hcfg.host]=fp;DB.save();s.out(C.dim+'Aviso: \''+hcfg.host+'\' adicionado permanentemente à lista de hosts conhecidos.'+C.x+'\n')} if(hcfg.auth==='key'){s.out(C.dim+'Oferecendo chave pública: id_ed25519 (ED25519 SHA256:'+fpOf(hcfg.user+hcfg.host).slice(0,20)+')'+C.x+'\n');await w(ri(250,500));if(s.dead)return;s.out('Servidor aceitou a chave. '+C.g+'Autenticado.'+C.x+'\n')} else{let ok=false;for(let t=1;t<=3;t++){const pw=await this.editor.readLine(`${hcfg.user}@${hcfg.host}'s password: `,{mask:true,cancelRejects:true});if(s.dead)return;if(pw===ABORT||pw===EOF){s.close(255,'Autenticação cancelada.');return} if(pw.length>0){ok=true;break}s.out(C.r+'Permission denied, please try again.'+C.x+'\n')} if(!ok){s.out(C.br+'Permission denied (publickey,password).'+C.x+'\n');s.close(255,'Falha de autenticação.');return} await w(ri(150,350));if(s.dead)return;s.out(C.g+'Autenticado com senha.'+C.x+'\n')} s.markConnected();await w(200);if(s.dead)return;this.motd(); if(hcfg.initial?.trim()){this.s.out(this.promptStr()+hcfg.initial+'\n');await this.exec(hcfg.initial.trim());if(s.dead)return} while(!s.dead){const line=await this.editor.readLine(this.promptStr(),{hist:this.hist});if(s.dead)return; if(line===EOF){this.doExit(0);if(s.dead)return;continue} if(line===ABORT)continue; const trimmed=line.trim();if(!trimmed)continue; await this.exec(trimmed);if(s.dead)return}} motd(){const p=this.srv.p;const[br1,br2]=bannerRows();const last=new Date(Date.now()-16*3600e3); this.s.out('\n'+br1+'\n'+br2+'\n\n'); this.s.out(` ${C.bc}${this.user}@${p.hostname}${C.x} ${C.dim}·${C.x} ${p.os} ${C.dim}· GNU/Linux ${p.kernel}${C.x}\n`); this.s.out(C.dim+' ────────────────────────────────────────────────────────────'+C.x+'\n'); this.s.out(` Sistema no ar há ${C.g}${p.upDays} dias${C.x} · carga ${p.load.toFixed(2)} · memória ${Math.round(p.memUsed/p.mem*100)}% · IP ${p.ip}\n`); this.s.out(` Último login: ${last.toLocaleString('pt-BR')} de ${pick(NGINX_IPS)}\n`); this.s.out(` ${C.by}Digite ${C.bc}help${C.by} para explorar${C.x} · Tab completa · Ctrl+R busca · ${C.dim}servidor 100% simulado${C.x}\n\n`)} async exec(line){if(line.startsWith('!!')){const last=this.hist[0];if(!last){this.s.err('bash: !!: evento não encontrado');return}line=last+line.slice(2);this.s.out(C.dim+line+C.x+'\n')} else if(/^sudo !!$/.test(line)){const last=this.hist[0];if(!last){this.s.err('bash: !!: evento não encontrado');return}line='sudo '+last;this.s.out(C.dim+line+C.x+'\n')} if(!this.hist.length||this.hist[0]!==line)this.hist.unshift(line);if(this.hist.length>500)this.hist.pop(); this.s.logCmd(line); const parts=splitTopLevel(line); for(const part of parts){if(this.s.dead)return;if(part.op==='&&'&&this.lastCode!==0)continue;if(part.op==='||'&&this.lastCode===0)continue;if(!part.text)continue; this.lastCode=await this.pipeline(part.text);this.env['?']=String(this.lastCode)}} async pipeline(text){const job=makeJob();this.s.job=job; try{const stagesRaw=splitPipes(text);const lastStage=extractRedirect(stagesRaw[stagesRaw.length-1]);stagesRaw[stagesRaw.length-1]=lastStage.stage; let input='',code=0; for(let si=0;si{if(!q&&t.startsWith('~'))t=this.env.HOME+t.slice(1);return{t,q}}); let args=[];for(const{t,q}of toks){if(!q&&/[*?]/.test(t)){const g=this.fs.glob(t,this.cwd,this.env.HOME);args.push(...(g||[t]))}else args.push(t)} if(!args.length)continue; if(ALIASES[args[0]]){const ex=ALIASES[args[0]].split(' ');args=[...ex,...args.slice(1)]} const name=args[0];const cmd=CMDS[name];let cap=''; const out=isLast&&!lastStage.target?(t=>this.s.out(t.replace(/\x07/g,'').replace(/(?{cap+=t}); const io={in:input,out,err:t=>this.s.out(t.replace(/(?lev(k,name)<=2).sort((a,b)=>lev(a,name)-lev(b,name))[0];if(sug)this.s.out(C.dim+`Você quis dizer '${sug}'? (digite help)${C.x}\n`);code=127;break} try{code=await cmd.run(args.slice(1),io)}catch(e){console.error(e);io.err(name+': erro interno na demo');code=1} if(job.cancelled&&code===0)code=130; if(lastStage.target&&isLast){const abs=this.abs(lastStage.target);if(!canWrite(abs,this.user))this.s.err(`bash: ${lastStage.target}: Permissão negada`),code=1;else{const r=this.fs.write(abs,cap,this.user,{append:lastStage.append});if(r.err)this.s.err(`bash: ${lastStage.target}: ${r.err}`),code=1}} input=cap;if(this.s.dead)break} return code}finally{job.active=false;job.raw=null;this.s.job=null}} complete(buf,pos){const upto=buf.slice(0,pos);const chunk=upto.split(/[|;]|&&|\|\|/).pop();const m=chunk.match(/(\S*)$/);const tok=m?m[1]:'';const start=pos-tok.length;const isFirst=!chunk.trimStart().includes(' '); if(isFirst&&!tok.includes('/')){const hits=Object.keys(CMDS).concat(Object.keys(ALIASES)).filter(k=>k.startsWith(tok)).sort();if(!hits.length)return null;if(hits.length===1)return{start,text:hits[0]+' '};const cp=hits.reduce((a,b)=>{let i=0;while(itok.length)return{start,text:cp};return{start,options:hits.map(x=>({label:x}))}} const slash=tok.lastIndexOf('/');const dirPart=tok.slice(0,slash+1),base=tok.slice(slash+1);const dirAbs=this.abs(dirPart||'.');const d=this.fs.get(dirAbs);if(!d||d.type!=='d')return null; const hits=Object.values(d.children).filter(n=>n.name.startsWith(base)&&(base.startsWith('.')||!n.name.startsWith('.'))).sort((a,b)=>a.name.localeCompare(b.name));if(!hits.length)return null; if(hits.length===1){const n=hits[0];return{start,text:dirPart+n.name+(n.type==='d'?'/':' ')}} const cp=hits.map(n=>n.name).reduce((a,b)=>{let i=0;while(ibase.length)return{start,text:dirPart+cp}; return{start,options:hits.map(n=>({label:n.name+(n.type==='d'?'/':''),dir:n.type==='d'}))}}} /* ============================== TRANSPORTE WS REAL ============================== */ function wsStart(sess){const hcfg=sess.host;const url=hcfg.bridge||'ws://localhost:8080'; if(!/^wss?:\/\//.test(url)){sess.out('\x1b[31mURL de ponte inválida: '+esc(url)+' — use ws:// ou wss://\x1b[0m\r\n');sess.close(1,'Configure a URL da ponte.');return} sess.out(`\x1b[2m[ponte] conectando a ${url}…\x1b[0m\r\n`); let ws;try{ws=new WebSocket(url)}catch(e){sess.out('\x1b[31m[ponte] '+e.message+'\x1b[0m\r\n');sess.close(1,'Falha ao abrir WebSocket.');return} sess.ws=ws;const t0=performance.now();let opened=false,first=false; const to=setTimeout(()=>{if(!opened){try{ws.close()}catch{}sess.out('\x1b[31m[ponte] tempo esgotado — a ponte está rodando?\x1b[0m\r\n');sess.close(1,'Ponte não respondeu. Baixe e rode concha-bridge.js (Ajuda → Servidor real).')}},12000);sess.timers.push(to); ws.onopen=()=>{opened=true;clearTimeout(to);ws.send(JSON.stringify({type:'init',host:hcfg.host,port:hcfg.port,username:hcfg.user,password:hcfg.password||undefined,privateKey:hcfg.privateKey||undefined,passphrase:hcfg.passphrase||undefined,cols:sess.cols||80,rows:sess.rows||24,term:'xterm-256color'}));sess.out('\x1b[2m[ponte] conectado — aguardando o servidor SSH…\x1b[0m\r\n'); const ping=setInterval(()=>{if(ws.readyState===1)ws.send(JSON.stringify({type:'ping',t:Date.now()}))},3000);sess.timers.push(ping)}; ws.onmessage=ev=>{const data=typeof ev.data==='string'?ev.data:new TextDecoder().decode(ev.data); if(data.startsWith('{"type"')){try{const m=JSON.parse(data);if(m.type==='pong'){sess.latency=Math.max(1,Date.now()-m.t);sess.updateStatus();return}if(m.type==='info'){sess.out('\x1b[2m[ponte] '+m.msg+'\x1b[0m\r\n');return}}catch{}} if(!first){first=true;sess.markConnected(Math.round(performance.now()-t0))}sess.out(data)}; ws.onerror=()=>{}; ws.onclose=ev=>{clearTimeout(to);if(sess.dead)return; if(!first){sess.out('\r\n\x1b[1;31m[ponte] conexão encerrada'+(ev.reason?' — '+ev.reason:'')+'.\x1b[0m\r\n\x1b[2mSem ponte? Ajuda → “Servidor real” traz o servidor Node pronto para baixar.\r\nOu use o modo Demonstração para explorar tudo offline.\x1b[0m\r\n');sess.close(1,'Ponte indisponível ou SSH recusado.')} else sess.close(ev.code===1000?0:ev.code,'Conexão encerrada pelo servidor/ponte.')}; sess.wsSend=o=>{if(ws.readyState===1)ws.send(JSON.stringify(o))}; sess.wsSend({type:'noop'})} /* ============================== SESSÃO / TERMINAL ============================== */ let XTERM_OK=false,FitCtor=null,SearchCtor=null,LinksCtor=null; const Sessions=new Set(); class Session{ constructor(tab,host){this.tab=tab;this.host=host;this.id=uid();this.dead=false;this.connected=false;this.startedAt=Date.now();this.connectedAt=null;this.latency=null;this.cols=80;this.rows=24;this.log=[];this._partial='';this.timers=[];this.job=null;this.connectJob=makeJob();this.sftpCwd=null;this.shell=null;this.ws=null;this.wsSend=null; Sessions.add(this);this.buildDom();this.makeTerm();this.bindTools()} buildDom(){const v=h('div',{class:'view',dataset:{sess:this.id}});this.view=v; this.tbTitle=h('span',{text:this.host.user+'@'+this.host.host}); const badge=h('span',{class:'badge'+(this.host.mode==='demo'?' demo':''),text:this.host.mode==='demo'?'DEMO':'PONTE WS'}); this.tb=h('div',{class:'toolbar'},h('div',{class:'ttl'},icEl('server'),this.tbTitle,badge),h('div',{class:'sp'})); const tbBtn=(ic,tip,fn,cls='')=>{const b=h('button',{class:'ibtn '+cls,'data-tip':tip,'aria-label':tip,html:I(ic),onclick:fn});this.tb.append(b);return b}; this.btnSearch=tbBtn('search','Buscar (Ctrl+Shift+F)',()=>this.toggleSearch()); tbBtn('copy','Copiar seleção (Ctrl+Shift+C)',()=>this.copySel(),'hidesm'); tbBtn('save','Colar (Ctrl+Shift+V)',()=>this.paste(),'hidesm'); this.btnSftp=tbBtn('folder','Painel SFTP',()=>Rail.open('sftp')); tbBtn('down','Exportar sessão (.log)',()=>this.exportLog(),'hidesm'); tbBtn('max','Tela cheia',()=>this.fullscreen(),'hidesm'); tbBtn('panel','Painel lateral',()=>Rail.toggleBody(),'hidesm'); this.btnPower=tbBtn('power','Desconectar',()=>{if(this.dead)this.reconnect();else this.close(0,'Desconectado pelo usuário.')},'danger'); this.termHost=h('div',{class:'term-host'}); this.scanEl=h('div',{class:'scanlines'}); this.veil=h('div',{class:'veil'}); this.searchBar=h('div',{class:'searchbar'}); const wrap=h('div',{class:'term-wrap'},h('div',{class:'term-pad'},this.termHost),this.scanEl,this.searchBar,this.veil); v.append(this.tb,wrap);$('#views').append(v)} makeTerm(){const st=SET();const theme=this.themeObj(); const opts={fontFamily:"'JetBrains Mono',ui-monospace,Menlo,Consolas,monospace",fontSize:st.fontSize,lineHeight:st.lineHeight,cursorStyle:st.cursor,cursorBlink:st.blink,scrollback:st.scrollback,allowTransparency:true,theme,convertEol:false,letterSpacing:0}; if(XTERM_OK){this.term=new Terminal(opts);this.fitAddon=FitCtor?new FitCtor():null;if(this.fitAddon)try{this.term.loadAddon(this.fitAddon)}catch{} if(SearchCtor)try{this.searchAddon=new SearchCtor();this.term.loadAddon(this.searchAddon);this.searchAddon.onDidChangeResults?.(r=>{this.searchCnt.textContent=r.resultCount?(r.resultIndex+1)+'/'+r.resultCount:'0/0'})}catch{} if(LinksCtor)try{this.term.loadAddon(new LinksCtor())}catch{} this.term.open(this.termHost);this.term.onData(d=>this.input(d));this.term.onResize(({cols,rows})=>{this.cols=cols;this.rows=rows;this.wsSend?.({type:'resize',cols,rows});this.updateStatus()}); this.term.attachCustomKeyEventHandler(e=>{if(e.type==='keydown'&&e.ctrlKey&&e.shiftKey&&(e.code==='KeyC'||e.code==='KeyV')){e.preventDefault();e.code==='KeyC'?this.copySel():this.paste();return false}return true}); this.engine='xterm.js 5.5'} else{this.term=new MiniTerm(opts);this.term.open(this.termHost);this.term.onData(d=>this.input(d));this.term.onResize(({cols,rows})=>{this.cols=cols;this.rows=rows;this.updateStatus()});this.engine='motor compacto'} this.applyAppearance()} themeObj(){const t={...THEMES[SET().theme].c};t.background=hexA(THEMES[SET().theme].c.background,SET().opacity);return t} applyAppearance(){const st=SET();const theme=this.themeObj(); if(XTERM_OK){const o=this.term.options;o.fontSize=st.fontSize;o.lineHeight=st.lineHeight;o.cursorStyle=st.cursor;o.cursorBlink=st.blink;o.scrollback=st.scrollback;o.theme=theme} else this.term.applyOpts({fontSize:st.fontSize,lineHeight:st.lineHeight,cursorStyle:st.cursor,cursorBlink:st.blink,scrollback:st.scrollback,theme}); this.view.classList.toggle('term-glow',st.glow);this.scanEl.style.display=st.scan?'block':'none';this.fit()} fit(){if(this.dead||!this.view.classList.contains('on'))return;try{if(XTERM_OK)this.fitAddon?.fit();else this.term.fit()}catch{}const d=this.term;this.cols=d.cols||this.cols;this.rows=d.rows||this.rows;this.updateStatus()} focus(){try{this.term.focus()}catch{}} input(d){if(this.dead)return;if(d.includes('\x03')){const rest=d.replaceAll('\x03','');if(this.job?.active){this.job.cancel();if(rest)this.input(rest);return}} if(this.job?.active){this.job.raw?.(d);return} if(this.shell)this.shell.editor.data(d);else if(this.wsSend)this.wsSend({type:'input',data:d})} out(t){if(this.dead&&!this._allowWrite)return;try{this.term.write(t)}catch{}this.capture(t)} err(t){this.out(t+'\n')} capture(t){const clean=ansiStrip(t);let buf=this._partial;for(const ch of clean){if(ch==='\n'){this.pushLog(buf);buf=''}else if(ch==='\r'){const i=buf.lastIndexOf('\u0000');buf=buf.slice(i+1)}else if(ch!=='\u0000')buf+=ch}this._partial=buf.slice(-4000)} pushLog(s){if(!s.trim())return;this.log.push({t:Date.now(),s,cmd:false});if(this.log.length>4000)this.log.splice(0,this.log.length-4000);if(Tabs.activeSession()===this&&Rail.tab==='log')LogPanel.append(this.log[this.log.length-1])} logCmd(s){this.log.push({t:Date.now(),s:'❯ '+s,cmd:true});if(Tabs.activeSession()===this&&Rail.tab==='log')LogPanel.append(this.log[this.log.length-1])} exportLog(){const head=`# Concha — registro de sessão\n# host: ${this.host.user}@${this.host.host}:${this.host.port} (${this.host.mode})\n# início: ${new Date(this.startedAt).toLocaleString('pt-BR')}\n# fim: ${new Date().toLocaleString('pt-BR')}\n\n`; dl(`concha-${this.host.host.replace(/[^\w.-]/g,'_')}-${Date.now()}.log`,head+this.log.map(l=>`[${fmtClock(l.t)}] ${l.s}`).join('\n'));toast('Registro exportado (.log)','ok'==='ok'?{type:'ok'}:{})} logText(){return this.log.map(l=>`[${fmtClock(l.t)}] ${l.s}`).join('\n')} start(){this.updateStatus();Tabs.renderBar(); if(this.host.mode==='demo'){const srv=getServer(this.host);this.shell=new Shell(this,srv);this.sftpCwd=srv.home;this.shell.start().catch(e=>{console.error(e);if(!this.dead)this.close(1,'Erro na sessão demo.')}); const lat=setInterval(()=>{if(this.connected&&!this.dead){this.latency=Math.max(1,Math.round(this.shell.srv.p.lat*(0.8+Math.random()*.5)));if(Tabs.activeSession()===this)this.updateStatus()}},2000);this.timers.push(lat)} else wsStart(this)} markConnected(ms){this.connected=true;this.connectedAt=Date.now();this.latency=ms??(this.host.mode==='demo'?this.shell?.srv.p.lat:null);SND.ok();this.updateStatus();Tabs.renderBar();if(this.host.mode!=='demo')toast('Sessão SSH estabelecida via ponte',{type:'ok'});this.btnPower.dataset.tip='Desconectar'} close(code=0,reason=''){if(this.dead)return;const was=this.connected;this.dead=true;this.connectJob.cancel();this.job?.cancel();this.shell?.editor.stop();try{this.ws?.close()}catch{}this.ws=null; this.timers.forEach(t=>{clearTimeout(t);clearInterval(t)});this.timers=[]; if(reason)this.out('\r\n\x1b[2m'+reason+'\x1b[0m\r\n');if(was)SND.bye(); this._allowWrite=true;this.veil.innerHTML='';this.veil.classList.add('on'); this.veil.append(h('div',{class:'closedcard'},icEl('power'),h('h4',{text:'Sessão encerrada'}),h('p',{text:`código ${code} · duração ${fmtDur(Date.now()-this.startedAt)}\n${this.host.user}@${this.host.host}`}),h('div',{class:'row'},h('button',{class:'btn btn-pri sm',html:I('refresh')+' Reconectar',onclick:()=>this.reconnect()}),h('button',{class:'btn sm',text:'Fechar aba',onclick:()=>Tabs.closeTab(this.tab.id)})),h('small',{text:'O conteúdo acima continua rolável — exporte o registro se precisar.'}))); this.btnPower.dataset.tip='Reconectar';this.btnPower.classList.remove('danger');this.btnPower.innerHTML=I('refresh');this.btnPower.onclick=()=>this.reconnect(); this.updateStatus();Tabs.renderBar();if(Rail.tab==='sftp')Rail.refresh()} reconnect(){const tab=this.tab;const fresh=DB.state.hosts.find(x=>x.id===this.host.id)||this.host;this.dispose(true);const s=new Session(tab,fresh);tab.session=s;Tabs.activate(tab.id);s.start();toast('Reconectando a '+fresh.host+'…')} dispose(keepTab=false){this.close(0);Sessions.delete(this);try{this.term.dispose()}catch{}this.view.remove();if(!keepTab){const i=Tabs.list.indexOf(this.tab);if(i>=0)Tabs.list.splice(i,1)}} copySel(){const sel=this.term.getSelection?.()||'';if(!sel){toast('Nada selecionado — selecione texto no terminal primeiro');return}copyText(sel).then(ok=>toast(ok?'Seleção copiada':'Não foi possível copiar',{type:ok?'ok':'err'}))} async paste(){let t=null;try{t=await navigator.clipboard.readText()}catch{} if(t!=null){if(!t){toast('Área de transferência vazia');return}this.input(t);this.focus();return} const ta=h('textarea',{placeholder:'Cole aqui com Ctrl+V e clique em Enviar…',style:'min-height:110px'}); const md=openModal({title:'Colar no terminal',icon:'save',body:h('div',{class:'fld'},h('label',{text:'O navegador bloqueou a leitura direta da área de transferência.'}),ta),footer:[h('button',{class:'btn btn-ghost',text:'Cancelar',onclick:()=>md.close()}),h('button',{class:'btn btn-pri',text:'Enviar',onclick:()=>{this.input(ta.value);md.close();this.focus()}})]})} fullscreen(){const el=this.view;if(document.fullscreenElement)document.exitFullscreen();else el.requestFullscreen?.().catch(()=>toast('Tela cheia bloqueada pelo navegador',{type:'warn'}));setTimeout(()=>this.fit(),300)} toggleSearch(force){if(!this.searchAddon){toast('Busca requer o xterm.js (motor compacto ativo)',{type:'warn'});return} const on=force??!this.searchBar.classList.contains('on');this.searchBar.classList.toggle('on',on); if(on){if(!this.searchInp){this.searchInp=h('input',{placeholder:'buscar…','aria-label':'Buscar no terminal'});this.searchCnt=h('span',{class:'cnt',text:''}); const doFind=prev=>{const q=this.searchInp.value;if(!q)return;const o={caseSensitive:this.caseOn,regex:this.rxOn,incremental:true};try{prev?this.searchAddon.findPrevious(q,o):this.searchAddon.findNext(q,o)}catch{}}; this.caseOn=false;this.rxOn=false; const bCase=h('button',{class:'ibtn','data-tip':'Diferenciar maiúsculas',html:'Aa',onclick:()=>{this.caseOn=!this.caseOn;bCase.classList.toggle('on',this.caseOn);doFind()}}); const bRx=h('button',{class:'ibtn','data-tip':'Regex',html:'.*',onclick:()=>{this.rxOn=!this.rxOn;bRx.classList.toggle('on',this.rxOn);doFind()}}); this.searchInp.addEventListener('input',()=>doFind());this.searchInp.addEventListener('keydown',e=>{if(e.key==='Enter'){e.preventDefault();doFind(e.shiftKey)}if(e.key==='Escape'){e.stopPropagation();this.toggleSearch(false)}}); this.searchBar.append(this.searchInp,this.searchCnt,bCase,bRx,h('button',{class:'ibtn','data-tip':'Anterior (Shift+Enter)',html:I('chevup'),onclick:()=>doFind(true)}),h('button',{class:'ibtn','data-tip':'Próximo (Enter)',html:I('chevdown'),onclick:()=>doFind()}),h('button',{class:'ibtn','data-tip':'Fechar (Esc)',html:I('x'),onclick:()=>this.toggleSearch(false)}))} this.searchInp.focus();this.searchInp.select()} else{try{this.searchAddon.clearDecorations()}catch{}this.focus()}} updateStatus(){if(Tabs.activeSession()!==this)return;const d=$('#stDot'); d.className=this.dead?'off':this.connected?'on':'conn'; $('#stState').textContent=this.dead?'desconectado':this.connected?`${this.host.user}@${this.host.host}`:'conectando…'; $('#stCipher').innerHTML=this.connected?(this.host.mode==='demo'?I('lock')+' chacha20-poly1305':I('lock')+' via ponte WS'):''; $('#stTime').innerHTML=I('clock')+' '+fmtDur(Date.now()-this.startedAt); $('#stPing').innerHTML=I('ping')+' '+(this.latency!=null&&this.connected?this.latency+' ms':'—'); $('#stSize').textContent=this.cols+'×'+this.rows; $('#stEngine').textContent=this.engine}} /* ============================== ABAS ============================== */ const Tabs={list:[],activeId:null, init(){const home={id:'home',kind:'home',session:null};this.list.push(home); home.view=h('div',{class:'view'});home.view.append(Welcome.el);$('#views').append(home.view); this.activate('home');$('#btnAddTab').innerHTML=I('plus');$('#btnAddTab').addEventListener('click',()=>this.activate('home'))}, active(){return this.list.find(t=>t.id===this.activeId)}, activeSession(){const t=this.active();return t?.kind==='ssh'?t.session:null}, openSession(host){host.last=Date.now();host.uses=(host.uses||0)+1;DB.save();Vault.render();Welcome.renderRecent(); const tab={id:uid(),kind:'ssh',host,session:null};this.list.push(tab); const s=new Session(tab,host);tab.session=s;this.activate(tab.id);s.start(); if(innerWidth<1024)Sidebar.close();return s}, activate(id){this.activeId=id;for(const t of this.list){const v=t.kind==='home'?t.view:t.session.view;v.classList.toggle('on',t.id===id)} this.renderBar();const t=this.active(); if(t?.kind==='ssh'){requestAnimationFrame(()=>{t.session.fit();t.session.focus();t.session.updateStatus()})} else{this.statusHome();Welcome.renderRecent()} Rail.refresh()}, statusHome(){$('#stDot').className='';$('#stState').textContent='cofre local · '+DB.state.hosts.length+' hosts';$('#stCipher').innerHTML='';$('#stTime').innerHTML=I('clock')+' —';$('#stPing').innerHTML=I('ping')+' —';$('#stSize').textContent='—';$('#stEngine').textContent=XTERM_OK?'xterm.js 5.5':'motor compacto'}, closeTab(id){const t=this.list.find(x=>x.id===id);if(!t||t.kind==='home')return; const wasActive=this.activeId===id;t.session.dispose();this.renderBar(); if(wasActive){const idx=Math.max(0,this.list.findIndex(x=>x.id===id));this.activate((this.list[idx]||this.list[0]).id)} toast('Sessão fechada')}, renderBar(){const box=$('#tabs');box.innerHTML=''; for(const t of this.list){const isHome=t.kind==='home';const s=t.session; const dot=isHome?h('span',{html:I('home'),style:'display:flex;color:var(--cyn)'}):h('i',{class:'dot '+(s.dead?'off':s.connected?'on':'conn')}); const el=h('button',{class:'tab'+(t.id===this.activeId?' on':''),role:'tab','aria-selected':t.id===this.activeId,onclick:()=>this.activate(t.id),title:isHome?'Início':s.host.user+'@'+s.host.host},dot,h('span',{class:'nm',text:isHome?'Início':s.host.name}),!isHome&&h('span',{class:'x',html:I('x'),'data-tip':'Fechar (Alt+W)',onclick:e=>{e.stopPropagation();this.closeTab(t.id)}})); if(!isHome)el.addEventListener('auxclick',e=>{if(e.button===1)this.closeTab(t.id)}); t.el=el;box.append(el)} const n=[...Sessions].filter(s=>!s.dead).length;document.title=(n?`(${n}) `:'')+'Concha — Cliente SSH no navegador'}}; /* ============================== SIDEBAR / VAULT ============================== */ const Sidebar={open(){$('#sidebar').classList.add('open');$('#scrim').classList.add('on')},close(){$('#sidebar').classList.remove('open');$('#scrim').classList.remove('on')}}; $('#scrim').addEventListener('click',()=>Sidebar.close()); $('#btnMenu').innerHTML=I('menu');$('#btnMenu').addEventListener('click',()=>$('#sidebar').classList.contains('open')?Sidebar.close():Sidebar.open()); const Vault={q:'',filter:DB.state.ui.filter||'all',group:DB.state.ui.group||'',sort:DB.state.ui.sort||'manual',dragId:null, init(){$('#vaultSearch').addEventListener('input',e=>{this.q=e.target.value.toLowerCase();this.render()}); $('#sortSel').value=this.sort;$('#sortSel').addEventListener('change',e=>{this.sort=e.target.value;DB.state.ui.sort=this.sort;DB.save();this.render()}); this.render()}, filtered(){let list=[...DB.state.hosts]; if(this.filter==='fav')list=list.filter(x=>x.fav);if(this.filter==='recent')list=list.filter(x=>x.last).sort((a,b)=>(b.last||0)-(a.last||0)); if(this.group)list=list.filter(x=>x.group===this.group); if(this.q)list=list.filter(x=>(x.name+' '+x.host+' '+x.user+' '+(x.tags||[]).join(' ')+' '+x.group).toLowerCase().includes(this.q)); if(this.filter!=='recent'){if(this.sort==='name')list.sort((a,b)=>a.name.localeCompare(b.name));else if(this.sort==='recent')list.sort((a,b)=>(b.last||0)-(a.last||0));else if(this.sort==='uses')list.sort((a,b)=>(b.uses||0)-(a.uses||0))} return list}, render(){const chips=$('#chips');chips.innerHTML=''; const mk=(id,label)=>h('button',{class:'chip'+(this.filter===id&&!this.group?' on':''),text:label,onclick:()=>{this.filter=id;this.group='';DB.state.ui.filter=id;DB.state.ui.group='';DB.save();this.render()}}); chips.append(mk('all','Todos'),mk('fav','★ Favoritos'),mk('recent','Recentes')); const groups=[...new Set(DB.state.hosts.map(x=>x.group).filter(Boolean))]; groups.forEach(g=>chips.append(h('button',{class:'chip'+(this.group===g?' on':''),text:g,onclick:()=>{this.group=this.group===g?'':g;this.filter='all';DB.state.ui.group=this.group;DB.state.ui.filter='all';DB.save();this.render()}}))); const list=$('#hostList');list.innerHTML='';list.classList.toggle('manual',this.sort==='manual'&&this.filter==='all'&&!this.group&&!this.q); const items=this.filtered();$('#hostCount').textContent=items.length+'/'+DB.state.hosts.length; if(!items.length){list.append(h('div',{class:'empty'},icEl('server'),h('p',{text:this.q||this.group||this.filter!=='all'?'Nenhum host encontrado com esses filtros.':'Cofre vazio — crie sua primeira conexão.'}),h('button',{class:'btn sm',text:'Limpar filtros',onclick:()=>{this.q='';this.filter='all';this.group='';$('#vaultSearch').value='';this.render()}})));return} items.forEach((host,i)=>list.append(this.card(host,i)))}, card(host,i){const card=h('div',{class:'hcard'+(host.persona==='demo'?' demo':''),role:'listitem',style:`--hc:${COLORS[host.color]||COLORS.grn};--i:${Math.min(i,10)}`,draggable:this.sort==='manual'&&this.filter==='all'&&!this.group&&!this.q,tabindex:'0','aria-label':'Conectar a '+host.name}); const connect=()=>{if(host.mode!=='demo'&&!XTERM_OK&&false){}Tabs.openSession(host)}; card.addEventListener('click',e=>{if(e.target.closest('button'))return;connect()}); card.addEventListener('keydown',e=>{if(e.key==='Enter')connect()}); card.append(h('div',{class:'top'},h('span',{class:'grip',html:I('grip')}),h('span',{class:'nm',text:host.name}),h('button',{class:'fav'+(host.fav?' on':''),html:I('star'),'data-tip':host.fav?'Remover favorito':'Favoritar',onclick:e=>{e.stopPropagation();host.fav=!host.fav;DB.save();this.render()}})), h('div',{class:'addr',text:`${host.user}@${host.host}:${host.port}`}), h('div',{class:'meta'},h('span',{class:'gchip',text:host.group||'geral'}),h('span',{class:'auth',html:I(host.auth==='key'?'key':'lock')+(host.auth==='key'?'chave':'senha')}),host.mode!=='demo'&&h('span',{class:'auth',html:I('zap')+'real'}),h('span',{class:'t',text:fmtRel(host.last)})), h('div',{class:'acts'},h('button',{class:'ibtn go','data-tip':'Conectar',html:I('play'),onclick:e=>{e.stopPropagation();connect()}}),h('button',{class:'ibtn','data-tip':'Mais ações',html:I('dots'),onclick:e=>{e.stopPropagation();this.menu(e.currentTarget,host)}}))); card.addEventListener('dragstart',e=>{this.dragId=host.id;card.classList.add('dragging');e.dataTransfer.effectAllowed='move'}); card.addEventListener('dragend',()=>{this.dragId=null;card.classList.remove('dragging');$$('.hcard.dragover').forEach(x=>x.classList.remove('dragover'))}); card.addEventListener('dragover',e=>{if(!this.dragId||this.dragId===host.id)return;e.preventDefault();card.classList.add('dragover')}); card.addEventListener('dragleave',()=>card.classList.remove('dragover')); card.addEventListener('drop',e=>{e.preventDefault();card.classList.remove('dragover');if(!this.dragId)return;const arr=DB.state.hosts;const from=arr.findIndex(x=>x.id===this.dragId),to=arr.findIndex(x=>x.id===host.id);if(from<0||to<0)return;const[m]=arr.splice(from,1);arr.splice(to,0,m);DB.save();this.render();toast('Host reordenado',{type:'ok',dur:1800})}); return card}, menu(btn,host){const idx=DB.state.hosts.findIndex(x=>x.id===host.id); openMenu(btn,[ {icon:'play',label:'Conectar',fn:()=>Tabs.openSession(host)}, {icon:'edit',label:'Editar / detalhes',fn:()=>this.drawer(host)}, {icon:'copy',label:'Duplicar',fn:()=>{const c=structuredClone(host);c.id=uid();c.name+=' (cópia)';c.created=Date.now();c.last=null;c.uses=0;DB.state.hosts.splice(idx+1,0,c);DB.save();this.render();toast('Host duplicado',{type:'ok'})}}, {icon:'down',label:'Exportar JSON (sem segredos)',fn:()=>{const c={...host};delete c.password;delete c.privateKey;delete c.passphrase;dl('concha-host-'+host.name.replace(/\s+/g,'-')+'.json',JSON.stringify(c,null,2),'application/json');toast('Exportado sem senha/chave',{type:'ok'})}}, '-', {icon:'arrup',label:'Mover para cima',disabled:this.sort!=='manual'||idx===0,fn:()=>{const a=DB.state.hosts;[a[idx-1],a[idx]]=[a[idx],a[idx-1]];DB.save();this.render()}}, {icon:'chevdown',label:'Mover para baixo',disabled:this.sort!=='manual'||idx===DB.state.hosts.length-1,fn:()=>{const a=DB.state.hosts;[a[idx+1],a[idx]]=[a[idx],a[idx+1]];DB.save();this.render()}}, '-', {icon:'trash',label:'Remover',danger:true,fn:async()=>{if(!await confirmModal(`Remover “${host.name}” do cofre?`,{danger:true,title:'Remover host',okLabel:'Remover'}))return; const[h2]=DB.state.hosts.splice(idx,1);DB.save();this.render();Welcome.renderRecent(); toast(`Host “${h2.name}” removido`,{type:'warn',dur:6500,action:{label:'Desfazer',fn:()=>{DB.state.hosts.splice(Math.min(idx,DB.state.hosts.length),0,h2);DB.save();this.render();Welcome.renderRecent();toast('Host restaurado',{type:'ok'})}}})}}])}, drawer(host){const isNew=!host;const data=host?structuredClone(host):{id:uid(),name:'',host:'',port:22,user:'root',auth:'password',password:'',savePw:false,color:'grn',group:'',tags:[],fav:false,mode:'demo',persona:'demo',initial:'',bridge:'ws://localhost:8080',created:Date.now(),last:null,uses:0}; const layer=h('div',{class:'layer',style:'justify-content:flex-end;align-items:stretch;background:#02060B77'}); const dr=h('div',{class:'drawer'});layer.append(dr);document.body.append(layer);Layers.push(layer); const close=()=>{const i=Layers.indexOf(layer);if(i>=0)Layers.splice(i,1);layer.remove()}; layer.addEventListener('pointerdown',e=>{if(e.target===layer)close()}); const inp=(id,val,ph='',type='text',mono=false)=>h('input',{id,value:val??'',placeholder:ph,type,class:mono?'mono':'',autocomplete:'off'}); const fHost=inp('x',data.host,'servidor.exemplo.br','text',true),fPort=inp('x',data.port,'22','number'),fUser=inp('x',data.user,'deploy','text',true),fName=inp('x',data.name,'Meu servidor'),fGroup=inp('x',data.group,'Produção'),fTags=inp('x',(data.tags||[]).join(', '),'nginx, docker'),fInit=inp('x',data.initial,'cd /var/www && ls','text',true),fBridge=inp('x',data.bridge,'ws://localhost:8080','text',true); const fPw=inp('x',data.password,data.persona==='demo'?'demo: qualquer senha serve':'senha','password',true); const fKey=h('textarea',{placeholder:'-----BEGIN OPENSSH PRIVATE KEY-----\n…',spellcheck:'false'},data.privateKey||''); const fPass=inp('x',data.passphrase||'','passphrase da chave (opcional)','password',true); const eyeBtn=h('button',{class:'ibtn',html:I('eye'),'data-tip':'Mostrar/ocultar',onclick:()=>{fPw.type=fPw.type==='password'?'text':'password'}}); const savePw=h('input',{type:'checkbox',...(data.savePw?{checked:''}:{})}); let auth=data.auth,color=data.color,mode=data.mode,persona=data.persona||'demo'; const segAuth=h('div',{class:'seg'},h('button',{class:auth==='password'?'on':'',text:'🔒 Senha',onclick:e=>{auth='password';upd()}}),h('button',{class:auth==='key'?'on':'',text:'🗝 Chave privada',onclick:e=>{auth='key';upd()}})); const segMode=h('div',{class:'seg'},h('button',{text:'🐚 Demonstração',onclick:()=>{mode='demo';upd()}}),h('button',{text:'⚡ Ponte real (WS)',onclick:()=>{mode='real';upd()}})); const personaSel=h('select',{},...Object.entries({demo:'Demo · Ubuntu web',prod:'Produção · nginx+docker',stg:'Staging · Ubuntu',db:'Dados · Debian+Postgres',pi:'Casa · Raspberry Pi',bastion:'Bastion · Ubuntu mínimo'}).map(([k,v])=>h('option',{value:k,...(k===persona?{selected:''}:{})},v))); personaSel.addEventListener('change',()=>persona=personaSel.value); const swatches=h('div',{class:'swatches'},...Object.entries(COLORS).map(([k,v])=>h('button',{class:'swatch'+(k===color?' on':''),style:`background:${v};color:${v}`,'data-tip':k,'aria-label':'Cor '+k,onclick:e=>{color=k;[...swatches.children].forEach(x=>x.classList.remove('on'));e.currentTarget.classList.add('on')}}))); const pwWrap=h('div',{class:'fld'},h('label',{text:'SENHA'}),h('div',{class:'pw'},fPw,eyeBtn),h('label',{class:'sw',style:'margin-top:4px'},h('span',{text:'Salvar senha no cofre local'}),savePw,h('span',{class:'tr'}))); const keyWrap=h('div',{style:'display:flex;flex-direction:column;gap:10px'},h('div',{class:'fld'},h('label',{text:'CHAVE PRIVADA (PEM)'}),fKey),h('div',{class:'fld'},h('label',{text:'PASSPHRASE'}),fPass)); const demoWrap=h('div',{class:'fld'},h('label',{text:'PERSONA DO SERVIDOR SIMULADO'}),personaSel); const realWrap=h('div',{style:'display:flex;flex-direction:column;gap:10px'},h('div',{class:'fld'},h('label',{text:'URL DA PONTE WEBSOCKET'}),fBridge),h('div',{class:'warnbox'},icEl('alert'),h('span',{html:'As credenciais vão apenas para esta ponte. Rode concha-bridge.js (Ajuda → Servidor real) e use wss:// em produção.'}))); const errHost=h('span',{class:'err',text:'Informe o endereço do host.'}); const hostFld=h('div',{class:'fld'},h('label',{text:'ENDEREÇO *'}),fHost,errHost); function upd(){[...segAuth.children].forEach((b,i)=>b.classList.toggle('on',(i===0)===(auth==='password'))); [...segMode.children].forEach((b,i)=>b.classList.toggle('on',(i===0)===(mode==='demo'))); pwWrap.style.display=auth==='password'?'flex':'none';keyWrap.style.display=auth==='key'?'flex':'none'; demoWrap.style.display=mode==='demo'?'flex':'none';realWrap.style.display=mode==='real'?'flex':'none'} const meta=!isNew&&h('div',{class:'metagrid'},h('div',{},h('b',{text:'CRIADO'}),h('span',{text:new Date(data.created).toLocaleDateString('pt-BR')})),h('div',{},h('b',{text:'ÚLTIMO ACESSO'}),h('span',{text:fmtRel(data.last)})),h('div',{},h('b',{text:'CONEXÕES'}),h('span',{text:String(data.uses||0)})),h('div',{},h('b',{text:'HOST KEY'}),h('span',{text:DB.state.known[data.host]?'conhecida ✓':'primeiro acesso'}))); const collect=()=>{const port=clamp(parseInt(fPort.value)||22,1,65535); return{...data,name:fName.value.trim()||fHost.value.trim(),host:fHost.value.trim(),port,user:fUser.value.trim()||'root',auth,color,mode,persona,group:fGroup.value.trim(),tags:fTags.value.split(',').map(x=>x.trim()).filter(Boolean),initial:fInit.value.trim(),bridge:fBridge.value.trim(),password:auth==='password'&&savePw.checked?fPw.value:'',privateKey:auth==='key'?fKey.value:'',passphrase:auth==='key'?fPass.value:'',savePw:savePw.checked,_pwLive:fPw.value}}; const validate=c=>{hostFld.classList.toggle('bad',!c.host);if(!c.host){fHost.focus();return false}return true}; const save=(thenConnect)=>{const c=collect();if(!validate(c))return; if(c.auth==='password'&&!c.savePw&&c._pwLive)c.password=c._pwLive; /* usa nesta conexão, não persiste */ const persist={...c};delete persist._pwLive;if(!c.savePw)persist.password=''; const i=DB.state.hosts.findIndex(x=>x.id===c.id);if(i>=0)DB.state.hosts[i]=persist;else DB.state.hosts.unshift(persist); DB.save();this.render();Welcome.renderRecent();close();toast(isNew?'Host adicionado ao cofre':'Host atualizado',{type:'ok'}); if(thenConnect)Tabs.openSession({...persist,password:c.savePw?persist.password:c._pwLive})}; dr.append(h('div',{class:'m-h'},h('span',{html:I(isNew?'plus':'edit'),style:'color:var(--grn);display:flex'}),h('h3',{text:isNew?'Nova conexão':'Editar conexão'}),h('button',{class:'ibtn',html:I('x'),onclick:close})), h('div',{class:'m-b'}, h('div',{class:'fld'},h('label',{text:'MODO DE CONEXÃO'}),segMode),demoWrap,realWrap, h('div',{class:'fld'},h('label',{text:'NOME DE EXIBIÇÃO'}),fName), hostFld,h('div',{class:'r2',style:'display:grid;grid-template-columns:1fr 92px;gap:10px'},h('div',{class:'fld'},h('label',{text:'USUÁRIO *'}),fUser),h('div',{class:'fld'},h('label',{text:'PORTA'}),fPort)), h('div',{class:'fld'},h('label',{text:'AUTENTICAÇÃO'}),segAuth),pwWrap,keyWrap, h('div',{class:'divider'}), h('div',{class:'r2',style:'display:grid;grid-template-columns:1fr 1fr;gap:10px'},h('div',{class:'fld'},h('label',{text:'GRUPO'}),fGroup),h('div',{class:'fld'},h('label',{text:'TAGS'}),fTags)), h('div',{class:'fld'},h('label',{text:'COR'}),swatches), h('div',{class:'fld'},h('label',{text:'COMANDO INICIAL (OPCIONAL)'}),fInit), meta), h('div',{class:'m-f'},!isNew&&h('button',{class:'btn btn-danger left',html:I('trash')+' Remover',onclick:()=>{close();this.menu(dr.querySelector('.m-h .ibtn'),DB.state.hosts.find(x=>x.id===data.id)||data)}}),h('button',{class:'btn btn-ghost',text:'Cancelar',onclick:close}),h('button',{class:'btn',html:I('save')+' Salvar',onclick:()=>save(false)}),h('button',{class:'btn btn-pri',html:I('play')+' Salvar e conectar',onclick:()=>save(true)}))); upd();setTimeout(()=>fHost.focus(),80)}}; $('#btnNew').innerHTML=I('plus')+'Nova conexão';$('#btnNew').addEventListener('click',()=>Vault.drawer()); $('#btnNew2').innerHTML=I('plus')+' Nova conexão';$('#btnNew2').addEventListener('click',()=>Vault.drawer()); /* ============================== SNIPPETS ============================== */ const Snips={q:'', init(){$('#btnSnipAdd').innerHTML=I('plus');$('#btnSnipAdd').addEventListener('click',()=>this.edit()); $('#snipSearch').addEventListener('input',e=>{this.q=e.target.value.toLowerCase();this.render()});this.render()}, render(){const box=$('#snipList');box.innerHTML='';const items=DB.state.snippets.filter(s=>!this.q||(s.name+' '+s.cmd).toLowerCase().includes(this.q)); if(!items.length){box.append(h('div',{class:'empty'},icEl('code'),h('p',{text:'Nenhum snippet.'}),h('button',{class:'btn sm',text:'Criar snippet',onclick:()=>this.edit()})));return} items.forEach(s=>{const card=h('div',{class:'snip'}, h('div',{class:'t'},h('b',{text:s.name}),h('button',{class:'ibtn run','data-tip':'Executar agora',html:I('play'),onclick:()=>this.send(s,true)}),h('button',{class:'ibtn','data-tip':'Editar',html:I('edit'),onclick:()=>this.edit(s)}),h('button',{class:'ibtn danger','data-tip':'Remover',html:I('trash'),onclick:()=>this.remove(s)})), h('code',{text:s.cmd,title:'Clique para inserir no terminal (sem executar)',onclick:()=>this.send(s,false)})); box.append(card)})}, send(s,run){const sess=Tabs.activeSession(); if(!sess||sess.dead){toast('Conecte-se a um host primeiro',{type:'warn'});return} if(sess.host.mode==='demo'&&!sess.shell?.editor.active){toast('Aguarde o comando atual terminar (Ctrl+C interrompe)',{type:'warn'});return} sess.input(run?s.cmd+'\r':s.cmd);sess.focus();if(!run)toast('Snippet inserido — Enter para executar',{dur:2200})}, remove(s){const i=DB.state.snippets.findIndex(x=>x.id===s.id);DB.state.snippets.splice(i,1);DB.save();this.render(); toast(`Snippet “${s.name}” removido`,{type:'warn',dur:6000,action:{label:'Desfazer',fn:()=>{DB.state.snippets.splice(i,0,s);DB.save();this.render()}}})}, edit(s){const isNew=!s;const fName=h('input',{value:s?.name||'',placeholder:'Ex.: Ver logs do nginx'});const fCmd=h('textarea',{placeholder:'comando…',spellcheck:'false'},s?.cmd||''); const ok=()=>{const name=fName.value.trim(),cmd=fCmd.value.trim();if(!name||!cmd){toast('Preencha nome e comando',{type:'warn'});return} if(isNew)DB.state.snippets.unshift({id:uid(),name,cmd});else Object.assign(s,{name,cmd});DB.save();this.render();md.close();toast(isNew?'Snippet criado':'Snippet atualizado',{type:'ok'})}; const md=openModal({title:isNew?'Novo snippet':'Editar snippet',icon:'code',body:h('div',{style:'display:flex;flex-direction:column;gap:12px'},h('div',{class:'fld'},h('label',{text:'NOME'}),fName),h('div',{class:'fld'},h('label',{text:'COMANDO'}),fCmd),h('div',{class:'hintbox',html:'Clique no comando insere no terminal; ▶ executa na hora. Funciona em sessões demo e reais.'})),footer:[h('button',{class:'btn btn-ghost',text:'Cancelar',onclick:()=>md.close()}),h('button',{class:'btn btn-pri',text:'Salvar',onclick:ok})]})}}; /* ============================== SFTP ============================== */ const Sftp={showHidden:false, render(){const box=$('#sftpBody');box.innerHTML='';const sess=Tabs.activeSession(); if(!sess){box.append(h('div',{class:'empty'},icEl('folder'),h('p',{text:'Abra uma sessão para navegar pelos arquivos.'})));return} if(sess.host.mode!=='demo'){box.append(h('div',{class:'empty'},icEl('folder'),h('p',{text:'SFTP está disponível nas sessões de demonstração.'}),h('div',{class:'hintbox',html:'A ponte atual transporta apenas o shell. Snippets, temas e registro funcionam normalmente na sessão real.'})));return} if(sess.dead){box.append(h('div',{class:'empty'},icEl('folder'),h('p',{text:'Sessão encerrada — reconecte para navegar.'})));return} const srv=sess.shell.srv,fs=srv.fs;sess.sftpCwd=sess.sftpCwd||srv.home; const cwd=sess.sftpCwd;const node=fs.get(cwd)||fs.get(srv.home); const crumbs=h('div',{class:'crumbs'});const segs=cwd.split('/').filter(Boolean);let acc=''; crumbs.append(h('button',{text:'/',onclick:()=>{sess.sftpCwd='/';this.render()}})); segs.forEach((sg,i)=>{acc+='/'+sg;const p=acc;crumbs.append(h('i',{text:'›'}),h('button',{text:sg,onclick:()=>{sess.sftpCwd=p;this.render()}}))}); const fileInput=h('input',{type:'file',multiple:'',style:'display:none'}); fileInput.addEventListener('change',()=>this.upload(sess,fileInput.files)); const tools=h('div',{class:'ftools'}, h('button',{class:'ibtn','data-tip':'Diretório acima',html:I('arrup'),onclick:()=>{sess.sftpCwd=fs.norm('..',cwd,srv.home);this.render()}}), h('button',{class:'ibtn','data-tip':'Atualizar',html:I('refresh'),onclick:()=>this.render()}), h('button',{class:'ibtn'+(this.showHidden?' on':''),'data-tip':'Arquivos ocultos',html:I(this.showHidden?'eye':'eyeoff'),onclick:()=>{this.showHidden=!this.showHidden;this.render()}}), h('div',{class:'sp'}), h('button',{class:'ibtn','data-tip':'Nova pasta',html:I('fplus'),onclick:()=>this.mkdir(sess)}), h('button',{class:'btn sm btn-pri',html:I('up')+' Upload',onclick:()=>fileInput.click()}),fileInput); const list=h('div',{style:'display:flex;flex-direction:column;gap:2px'}); let items=Object.values(node.children);if(!this.showHidden)items=items.filter(x=>!x.name.startsWith('.')); items.sort((a,b)=>(a.type===b.type?a.name.localeCompare(b.name):a.type==='d'?-1:1)); if(!items.length)list.append(h('div',{class:'empty',style:'padding:20px'},h('p',{text:'Diretório vazio.'}))); items.forEach(n=>{const abs=(cwd==='/'?'':cwd)+'/'+n.name;const isD=n.type==='d'; const row=h('div',{class:'frow'+(isD?' dir':'')},icEl(isD?'folder':'file'), h('button',{class:'fn',text:n.name,title:permStr(n)+' · '+n.uid,onclick:()=>{if(isD){sess.sftpCwd=abs;this.render()}else this.preview(sess,abs,n)}}), h('span',{class:'fs',text:isD?Object.keys(n.children).length+' itens':fmtBytes(fs.size(n))}), h('div',{class:'fa'}, isD&&h('button',{class:'ibtn','data-tip':'cd aqui no terminal',html:I('term'),onclick:()=>{if(sess.shell?.editor.active){sess.input('cd '+JSON.stringify(abs)+'\r');sess.focus()}else toast('Terminal ocupado',{type:'warn'})}}), !isD&&h('button',{class:'ibtn','data-tip':'Baixar',html:I('down'),onclick:()=>this.download(n)}), h('button',{class:'ibtn','data-tip':'Renomear',html:I('edit'),onclick:()=>this.rename(sess,abs,n)}), h('button',{class:'ibtn danger','data-tip':'Excluir',html:I('trash'),onclick:()=>this.remove(sess,abs,n)}))); row.addEventListener('dblclick',e=>{if(!isD&&!e.target.closest('.fa'))this.preview(sess,abs,n)}); list.append(row)}); const total=items.reduce((a,x)=>a+fs.size(x),0); box.append(crumbs,tools,list,h('div',{class:'fmeta'},h('span',{text:items.length+' itens · '+fmtBytes(total)}),h('span',{text:permStr(node)})), h('div',{class:'hintbox',html:'Uploads e downloads são reais (arquivos do seu computador ↔ servidor simulado). As mudanças aparecem no ls do terminal — e vice-versa. Sessão como '+esc(sess.shell.user)+': pastas de sistema exigem root (sudo -i).'}))}, guard(sess,abs){if(!canWrite(abs,sess.shell.user)){toast('Permissão negada — vire root no terminal (sudo -i) e tente de novo',{type:'err'});return false}return true}, async upload(sess,files){const fs=sess.shell.srv.fs;let ok=0;for(const f of files){if(f.size>2*1024*1024){toast(`“${f.name}” excede 2 MB na demo`,{type:'warn'});continue} const abs=(sess.sftpCwd==='/'?'':sess.sftpCwd)+'/'+f.name;if(!this.guard(sess,abs))continue; const textish=/^text\/|json|javascript|xml|csv|yaml|svg/.test(f.type)||/\.(txt|md|js|json|css|html|csv|log|py|sh|yml|yaml|conf|ini|svg)$/i.test(f.name); let content,b64=false;if(textish)content=await f.text();else{const buf=await f.arrayBuffer();content=btoa([...new Uint8Array(buf)].map(b=>String.fromCharCode(b)).join(''));b64=true} const r=fs.write(abs,content,sess.shell.user,{b64});if(r.err){toast(r.err,{type:'err'});continue}ok++} if(ok){toast(ok+(ok===1?' arquivo enviado':' arquivos enviados')+' para '+sess.sftpCwd,{type:'ok'});this.render()}}, download(n){let blob;if(n.b64){try{const bin=atob(n.content);blob=new Blob([Uint8Array.from(bin,c=>c.charCodeAt(0))])}catch{blob=new Blob([n.content])}}else blob=new Blob([n.content],{type:'text/plain;charset=utf-8'}); dl(n.name,blob);toast('Baixando “'+n.name+'”',{type:'ok',dur:2200})}, async mkdir(sess){const name=await promptModal('Nome da nova pasta','',{title:'Nova pasta',placeholder:'meu-diretorio'});if(!name)return; const abs=(sess.sftpCwd==='/'?'':sess.sftpCwd)+'/'+name;if(!this.guard(sess,abs))return; const r=sess.shell.srv.fs.mkdir(abs,sess.shell.user);if(r.err)toast(r.err,{type:'err'});else{toast('Pasta criada',{type:'ok'});this.render()}}, async rename(sess,abs,n){const name=await promptModal('Novo nome',n.name,{title:'Renomear'});if(!name||name===n.name)return; if(!this.guard(sess,abs))return;const fs=sess.shell.srv.fs;const{dir}=fs.parent(abs);if(fs.get(dir+'/'+name)){toast('Já existe um item com esse nome',{type:'err'});return} const r=fs.remove(abs);n.name=name;fs.restore(dir,name,n);toast('Renomeado',{type:'ok'});this.render()}, async remove(sess,abs,n){if(!this.guard(sess,abs))return; if(!await confirmModal(`Excluir “${n.name}”${n.type==='d'?' e todo o conteúdo':''}?`,{danger:true,title:'Excluir',okLabel:'Excluir'}))return; const fs=sess.shell.srv.fs;const r=fs.remove(abs);this.render(); toast(`“${n.name}” excluído`,{type:'warn',dur:6500,action:{label:'Desfazer',fn:()=>{fs.restore(r.dir,r.base,r.node);this.render();toast('Restaurado',{type:'ok'})}}})}, preview(sess,abs,n){const body=n.b64?h('div',{class:'empty'},icEl('file'),h('p',{text:'Arquivo binário ('+fmtBytes(sess.shell.srv.fs.size(n))+') — baixe para visualizar.'})):h('div',{class:'codebox'},h('div',{class:'cb-h'},h('span',{text:abs}),h('div',{class:'sp'}),h('span',{text:permStr(n)+' · '+n.uid+' · '+fmtLs(n.mtime)})),h('pre',{text:n.content||'(vazio)'})); openModal({title:n.name,icon:'file',wide:true,body,footer:[h('button',{class:'btn',html:I('down')+' Baixar',onclick:()=>this.download(n)}),h('button',{class:'btn btn-ghost',text:'Fechar',onclick:closeTop})]})}}; Bus.on('fs',hostId=>{const s=Tabs.activeSession();if(s&&s.host.id===hostId&&Rail.tab==='sftp'&&Rail.bodyOpen)Sftp.render()}); /* ============================== LOG PANEL ============================== */ const LogPanel={q:'', init(){$('#logSearch').addEventListener('input',e=>{this.q=e.target.value.toLowerCase();this.render()}); $('#btnLogClear').innerHTML=I('trash');$('#btnLogClear').addEventListener('click',()=>{const s=Tabs.activeSession();if(!s)return;s.log.length=0;this.render();toast('Registro limpo')}); $('#btnLogExport').innerHTML=I('down')+' Exportar .log';$('#btnLogExport').addEventListener('click',()=>{const s=Tabs.activeSession();if(!s){toast('Sem sessão ativa',{type:'warn'});return}s.exportLog()}); $('#btnLogCopy').innerHTML=I('copy')+' Copiar';$('#btnLogCopy').addEventListener('click',async()=>{const s=Tabs.activeSession();if(!s)return;toast(await copyText(s.logText())?'Registro copiado':'Falha ao copiar',{type:'ok'})}); this.render()}, line(l){return h('div',{class:'logline'+(l.cmd?' cmd':'')},h('span',{class:'lt',text:fmtClock(l.t)}),h('span',{class:'lx',text:l.s}))}, render(){const box=$('#logList');box.innerHTML='';const s=Tabs.activeSession(); if(!s){box.append(h('div',{class:'empty'},icEl('list'),h('p',{text:'O registro aparece quando você abre uma sessão.'})));return} const items=s.log.filter(l=>!this.q||l.s.toLowerCase().includes(this.q)).slice(-300); if(!items.length)box.append(h('div',{class:'empty',style:'padding:18px'},h('p',{text:'Sem linhas'+(this.q?' para este filtro':' ainda')+'.'}))); items.forEach(l=>box.append(this.line(l)));box.scrollTop=box.scrollHeight}, append(l){if(this.q&&!l.s.toLowerCase().includes(this.q))return;const box=$('#logList');box.querySelector('.empty')?.remove();box.append(this.line(l));while(box.children.length>300)box.firstChild.remove();box.scrollTop=box.scrollHeight}}; /* ============================== APARÊNCIA ============================== */ const Apar={ init(){const b=$('#aparBody');b.innerHTML='';const st=SET(); const apply=()=>{DB.save();Sessions.forEach(s=>s.applyAppearance());SND[st.sound?'bell':'err']&&0}; const slider=(label,min,max,step,get,set,fmt)=>{const out=h('output',{text:fmt(get())});const r=h('input',{type:'range',min,max,step,value:get()}); const paint=()=>{r.style.setProperty('--v',((get()-min)/(max-min)*100)+'%');out.textContent=fmt(get())}; r.addEventListener('input',()=>{set(parseFloat(r.value));paint();apply()});paint(); return h('div',{class:'ctl'},h('div',{class:'lb'},h('span',{text:label}),out),r)}; const toggle=(label,get,set)=>{const c=h('input',{type:'checkbox',...(get()?{checked:''}:{})});c.addEventListener('change',()=>{set(c.checked);apply()});return h('label',{class:'sw'},h('span',{text:label}),c,h('span',{class:'tr'}))}; const selCtl=(label,opts,get,set)=>{const s=h('select',{},...Object.entries(opts).map(([k,v])=>h('option',{value:k,...(k===get()?{selected:''}:{})},v)));s.addEventListener('change',()=>{set(s.value);apply()});return h('div',{class:'ctl'},h('div',{class:'lb'},h('span',{text:label})),s)}; b.append( selCtl('Tema de cores',Object.fromEntries(Object.entries(THEMES).map(([k,v])=>[k,v.label])),()=>st.theme,v=>st.theme=v), slider('Tamanho da fonte',10,22,1,()=>st.fontSize,v=>st.fontSize=v,v=>v+' px'), slider('Altura da linha',1,1.7,.05,()=>st.lineHeight,v=>st.lineHeight=v,v=>v.toFixed(2)), slider('Opacidade do fundo',.55,1,.01,()=>st.opacity,v=>st.opacity=v,v=>Math.round(v*100)+'%'), selCtl('Estilo do cursor',{block:'Bloco ▉',underline:'Sublinhado ▁',bar:'Barra ▏'},()=>st.cursor,v=>st.cursor=v), toggle('Cursor piscante',()=>st.blink,v=>st.blink=v), toggle('Brilho fosfórico (glow)',()=>st.glow,v=>st.glow=v), toggle('Scanlines CRT',()=>st.scan,v=>st.scan=v), toggle('Sons do terminal',()=>st.sound,v=>{st.sound=v;TopSound.render()}), selCtl('Scrollback',{'1000':'1.000 linhas','5000':'5.000 linhas','20000':'20.000 linhas'},()=>String(st.scrollback),v=>st.scrollback=parseInt(v)), h('div',{class:'divider'}), h('div',{class:'ctlrow'},h('button',{class:'btn sm',html:I('shuffle')+' Surpreenda-me',onclick:()=>{st.theme=pick(Object.keys(THEMES));st.cursor=pick(['block','underline','bar']);st.glow=Math.random()>.3;st.scan=Math.random()>.4;DB.save();this.init();Sessions.forEach(s=>s.applyAppearance());toast('Visual aleatório aplicado — tema '+THEMES[st.theme].label,{type:'ok'})}}),h('button',{class:'btn sm',html:I('reset')+' Padrões',onclick:()=>{Object.assign(st,DEFAULT_SETTINGS);DB.save();this.init();TopSound.render();Sessions.forEach(s=>s.applyAppearance());toast('Aparência restaurada',{type:'ok'})}})), h('div',{class:'hintbox',html:'Tudo aplica ao vivo em todas as abas abertas e fica salvo neste navegador. O tema afeta cores ANSI de ls, grep, top, neofetch…'}))}}; /* ============================== RAIL ============================== */ const Rail={tab:DB.state.ui.railTab||'apar',bodyOpen:DB.state.ui.railOpen!==false, init(){const nav=$('#railNav');const defs=[['apar','sliders','Aparência'],['snip','code','Snippets'],['sftp','folder','SFTP'],['log','list','Registro']]; this.btns={};defs.forEach(([id,ic,tip])=>{const b=h('button',{class:'ibtn','data-tip':tip,'aria-label':tip,html:I(ic),onclick:()=>{if(this.tab===id&&this.bodyOpen)this.toggleBody(false);else this.open(id)}});nav.append(b);this.btns[id]=b}); this.sync()}, open(id){this.tab=id;this.bodyOpen=true;DB.state.ui.railTab=id;DB.state.ui.railOpen=true;DB.save();this.sync();this.refresh();if(innerWidth<760)this.refresh()}, toggleBody(force){this.bodyOpen=force??!this.bodyOpen;DB.state.ui.railOpen=this.bodyOpen;DB.save();this.sync();if(this.bodyOpen)this.refresh();setTimeout(()=>Tabs.activeSession()?.fit(),80)}, sync(){$('#railBody').classList.toggle('hide',!this.bodyOpen); for(const[k,b]of Object.entries(this.btns))b.classList.toggle('on',k===this.tab&&this.bodyOpen); $$('.rpanel').forEach(p=>p.classList.remove('on'));({apar:'#pApar',snip:'#pSnip',sftp:'#pSftp',log:'#pLog'}[this.tab]&&$({apar:'#pApar',snip:'#pSnip',sftp:'#pSftp',log:'#pLog'}[this.tab])).classList.add('on')}, refresh(){if(!this.bodyOpen)return;if(this.tab==='sftp')Sftp.render();if(this.tab==='log')LogPanel.render();if(this.tab==='snip')Snips.render()}}; /* ============================== WELCOME ============================== */ const Welcome={el:null, init(){this.el=h('div',{class:'home'}); const hero=h('div',{class:'hero'}, h('h1',{html:'Seu shell, em qualquer navegador.'}), h('p',{text:'Cofre de hosts, múltiplas sessões em abas, SFTP, snippets e temas — com um servidor de demonstração completo para explorar agora e uma ponte WebSocket para seus servidores reais.'}), h('div',{class:'cta'},h('button',{class:'btn btn-pri',html:I('zap')+' Conectar ao servidor demo',onclick:()=>{const d=DB.state.hosts.find(x=>x.persona==='demo')||DB.state.hosts[0];if(d)Tabs.openSession(d);else Vault.drawer()}}),h('button',{class:'btn',html:I('plus')+' Nova conexão',onclick:()=>Vault.drawer()}),h('button',{class:'btn btn-ghost',html:I('help')+' Como conectar num servidor real',onclick:()=>Help.open('real')})), h('div',{class:'statline',id:'wstats'})); /* quick connect */ const qHost=h('input',{class:'mono',placeholder:'demo.concha.sh',autocomplete:'off'}),qUser=h('input',{class:'mono',placeholder:'deploy',value:'deploy',autocomplete:'off'}),qPort=h('input',{type:'number',value:'22'}),qPw=h('input',{type:'password',placeholder:'senha (demo: qualquer uma)',autocomplete:'off'}); let qMode='demo';const qBridge=h('input',{class:'mono',value:'ws://localhost:8080'}); const qSeg=h('div',{class:'seg'},h('button',{class:'on',text:'🐚 Demo',onclick:e=>{qMode='demo';[...qSeg.children].forEach((x,i)=>x.classList.toggle('on',i===0));qBridgeWrap.style.display='none'}}),h('button',{text:'⚡ Ponte real',onclick:e=>{qMode='real';[...qSeg.children].forEach((x,i)=>x.classList.toggle('on',i===1));qBridgeWrap.style.display='flex'}})); const qBridgeWrap=h('div',{class:'fld',style:'display:none'},h('label',{text:'URL DA PONTE'}),qBridge); const qGo=()=>{const host=qHost.value.trim()||(qMode==='demo'?'demo.concha.sh':'');if(!host){toast('Informe o host',{type:'warn'});qHost.focus();return} Tabs.openSession({id:'quick-'+uid(),name:host,host,port:clamp(parseInt(qPort.value)||22,1,65535),user:qUser.value.trim()||'root',auth:'password',password:qPw.value,color:'cyn',group:'Rápida',tags:[],fav:false,mode:qMode,persona:'demo',bridge:qBridge.value.trim(),initial:'',created:Date.now(),last:null,uses:0,_quick:true})}; [qHost,qUser,qPort,qPw].forEach(i=>i.addEventListener('keydown',e=>{if(e.key==='Enter')qGo()})); const qc=h('div',{class:'wcard',style:'--i:0'},h('h3',{},icEl('zap'),'CONEXÃO RÁPIDA'), h('div',{class:'frm'},qSeg,h('div',{class:'fld'},h('label',{text:'HOST'}),qHost),h('div',{class:'r2'},h('div',{class:'fld'},h('label',{text:'USUÁRIO'}),qUser),h('div',{class:'fld'},h('label',{text:'PORTA'}),qPort)),h('div',{class:'fld'},h('label',{text:'SENHA'}),qPw),qBridgeWrap,h('button',{class:'btn btn-pri',html:I('term')+' Conectar',onclick:qGo}),h('div',{class:'hintbox',html:'Conexões rápidas não são salvas no cofre. Use Nova conexão para guardar com cor, grupo e chave.'}))); /* preview */ const pv=h('div',{class:'wcard',style:'--i:1'},h('h3',{},icEl('term'),'UMA PRÉVIA DO QUE ESPERA VOCÊ'), h('div',{class:'preview'},h('div',{class:'bar'},h('i',{style:'background:#FF5C69'}),h('i',{style:'background:#FFB454'}),h('i',{style:'background:#2EE6A0'}),h('span',{text:'deploy@demo — concha'})), h('pre',{html:`$ ssh deploy@demo.concha.sh █▀▀ █▀█ █▄░█ █▀▀ █░█ ▄▀█ █▄▄ █▄█ █░▀█ █▄▄ █▀█ █▀█ deploy@demo:~$ neofetch OS: Ubuntu 24.04.1 LTS · Kernel: 6.8.0-45 Terminal: Concha Web 🐚 deploy@demo:~$ tail -f /var/log/app/out.log {"level":"info","msg":"request handled","latency_ms":42} {"level":"error","msg":"ECONNRESET upstream"} deploy@demo:~$ ▉`})), h('p',{style:'font-size:11.5px;color:var(--faint);margin-top:10px',text:'Servidor demo com ~50 comandos, pipes, Tab, Ctrl+R, top/ping/tail ao vivo, SFTP integrado e muito mais.'})); /* atalhos */ const keys=h('div',{class:'wcard',style:'--i:2'},h('h3',{},icEl('code'),'ATALHOS'), h('div',{class:'keys'},...[['Nova aba','Alt+T'],['Fechar aba','Alt+W'],['Trocar aba','Alt+1…9'],['Buscar no terminal','Ctrl+Shift+F'],['Copiar seleção','Ctrl+Shift+C'],['Colar','Ctrl+Shift+V'],['Interromper comando','Ctrl+C'],['Completar / histórico','Tab · Ctrl+R']].map(([a,b])=>h('div',{},h('span',{text:a}),h('kbd',{text:b}))))); /* servidor real */ const real=h('div',{class:'wcard',style:'--i:3'},h('h3',{},icEl('server'),'SERVIDOR REAL, DE VERDADE'), h('div',{class:'steps'},h('p',{html:'Navegadores não abrem TCP puro — o Concha fala com uma ponte WebSocket↔SSH que você roda em qualquer máquina (até localhost).'}),h('p',{html:'1. Baixe a ponte pronta: concha-bridge.js (Node + ssh2)'}),h('p',{html:'2. npm i ws ssh2 → node concha-bridge.js'}),h('p',{html:'3. Crie a conexão em modo Ponte real apontando para ws://localhost:8080'})), h('div',{class:'ctlrow',style:'margin-top:12px'},h('button',{class:'btn sm btn-pri',html:I('down')+' Baixar ponte',onclick:()=>Help.downloadBridge()}),h('button',{class:'btn sm',html:I('help')+' Instruções completas',onclick:()=>Help.open('real')}))); const recentCard=h('div',{class:'wcard',style:'--i:4;grid-column:1/-1'},h('h3',{},icEl('clock'),'RECENTES'),h('div',{class:'recent',id:'wrecent'})); this.el.append(hero,h('div',{class:'wgrid'},qc,pv,keys,real,recentCard));this.renderRecent()}, renderRecent(){const box=this.el?.querySelector('#wrecent');if(!box)return;box.innerHTML=''; const rec=[...DB.state.hosts].filter(x=>x.last).sort((a,b)=>b.last-a.last).slice(0,6); if(!rec.length)box.append(h('span',{style:'font-size:12px;color:var(--faint)',text:'Suas conexões recentes aparecem aqui.'})); rec.forEach(x=>box.append(h('button',{class:'rchip',style:`--hc:${COLORS[x.color]||COLORS.grn}`,onclick:()=>Tabs.openSession(x)},h('i'),x.name,h('span',{style:'color:var(--faint);font-weight:400;font-size:11px',text:fmtRel(x.last)})))); const st=this.el.querySelector('#wstats');if(st)st.innerHTML=`${DB.state.hosts.length} hosts no cofre${DB.state.snippets.length} snippets${[...Sessions].filter(s=>!s.dead).length} sessões ativasmotor: ${XTERM_OK?'xterm.js':'compacto'}`}}; /* ============================== AJUDA / PONTE ============================== */ const BRIDGE_SRC=[ '// concha-bridge.js — ponte WebSocket <-> SSH para o cliente Concha', '// Uso: npm i ws ssh2 && node concha-bridge.js', '// Depois, no Concha: Nova conexao -> modo "Ponte real" -> ws://localhost:8080', '// Em producao: coloque atras de TLS (wss://) e restrinja origens/hosts.', 'const { WebSocketServer } = require("ws");', 'const { Client } = require("ssh2");', 'const PORT = process.env.PORT || 8080;', 'const ALLOW = (process.env.ALLOW_HOSTS || "").split(",").filter(Boolean); // ex: "10.0.0.5,srv.exemplo.br"', 'const wss = new WebSocketServer({ port: PORT });', 'wss.on("connection", (ws) => {', ' let ssh = null, stream = null;', ' const send = (s) => { if (ws.readyState === 1) ws.send(s); };', ' ws.on("message", (raw) => {', ' let m; try { m = JSON.parse(raw); } catch { return; }', ' if (m.type === "ping") { send(JSON.stringify({ type: "pong", t: m.t })); return; }', ' if (m.type === "init" && !ssh) {', ' if (ALLOW.length && !ALLOW.includes(m.host)) { send("host nao permitido pela ponte"); ws.close(); return; }', ' ssh = new Client();', ' ssh.on("ready", () => {', ' send(JSON.stringify({ type: "info", msg: "SSH conectado a " + m.host }));', ' ssh.shell({ term: m.term || "xterm-256color", cols: m.cols || 80, rows: m.rows || 24 }, (err, s) => {', ' if (err) { send("erro: " + err.message); ws.close(); return; }', ' stream = s;', ' stream.on("data", (d) => send(d.toString("utf8")));', ' if (stream.stderr) stream.stderr.on("data", (d) => send(d.toString("utf8")));', ' stream.on("close", () => ws.close());', ' });', ' });', ' ssh.on("keyboard-interactive", (n, i, l, finish) => finish([m.password || ""]));', ' ssh.on("error", (e) => { send("erro SSH: " + e.message); ws.close(); });', ' ssh.on("close", () => ws.close());', ' ssh.connect({ host: m.host, port: m.port || 22, username: m.username,', ' password: m.password || undefined, privateKey: m.privateKey || undefined,', ' passphrase: m.passphrase || undefined, tryKeyboard: true,', ' readyTimeout: 15000, keepaliveInterval: 10000 });', ' } else if (m.type === "input" && stream) stream.write(m.data);', ' else if (m.type === "resize" && stream) stream.setWindow(m.rows || 24, m.cols || 80);', ' });', ' ws.on("close", () => { try { stream && stream.end(); } catch {} try { ssh && ssh.end(); } catch {} });', '});', 'console.log("Concha bridge ouvindo em ws://0.0.0.0:" + PORT);' ].join('\n'); const Help={ downloadBridge(){dl('concha-bridge.js',BRIDGE_SRC,'text/javascript');toast('concha-bridge.js baixado — rode com Node.js',{type:'ok'})}, open(section){const body=h('div',{style:'display:flex;flex-direction:column;gap:20px'}); const secReal=h('div',{class:'helpsec',id:'help-real'},h('h4',{},icEl('server'),'CONECTAR A UM SERVIDOR SSH REAL'), h('p',{text:'O navegador não abre sockets TCP; por isso o Concha usa uma ponte WebSocket↔SSH que você controla. Ela acompanha o app, pronta para rodar:'}), h('ol',{},h('li',{html:'Salve o arquivo abaixo e instale as dependências: npm i ws ssh2'}),h('li',{html:'Rode: node concha-bridge.js (escuta em ws://localhost:8080)'}),h('li',{html:'No Concha, crie a conexão em modo Ponte real com host/usuário/senha ou chave do seu servidor.'})), h('div',{class:'codebox'},h('div',{class:'cb-h'},h('span',{text:'concha-bridge.js'}),h('div',{class:'sp'}),h('button',{class:'btn sm',html:I('copy')+' Copiar',onclick:async()=>toast(await copyText(BRIDGE_SRC)?'Código copiado':'Falha ao copiar',{type:'ok'})}),h('button',{class:'btn sm btn-pri',html:I('down')+' Baixar',onclick:()=>this.downloadBridge()})),h('pre',{text:BRIDGE_SRC})), h('div',{class:'warnbox'},icEl('shield'),h('span',{html:'A ponte recebe credenciais: rode em localhost ou atrás de wss://, defina ALLOW_HOSTS para limitar destinos e nunca a exponha abertamente na internet.'})), h('p',{html:'Protocolo: cliente envia JSON {type:"init"|"input"|"resize"|"ping"}; a ponte responde com texto puro do terminal + {type:"pong"|"info"}. Qualquer ponte compatível funciona.'})); const secDemo=h('div',{class:'helpsec'},h('h4',{},icEl('zap'),'O SERVIDOR DE DEMONSTRAÇÃO'), h('p',{html:'Cada host demo tem persona própria (Ubuntu web, Debian+Postgres, Raspberry Pi…) com sistema de arquivos persistente durante a visita, serviços e logs ao vivo. ~50 comandos reais de shell: pipes, >, >>, &&, glob, $VAR, sudo -i, apt, docker, systemctl, top, ping, tail -f, neofetch, cowsay… Tudo simulado no seu navegador — nada toca a rede.'})); const secKeys=h('div',{class:'helpsec'},h('h4',{},icEl('code'),'ATALHOS'),h('div',{class:'keys'},...[['Nova aba / Início','Alt+T'],['Fechar aba','Alt+W'],['Abas 1–9','Alt+1…9'],['Buscar','Ctrl+Shift+F'],['Copiar / colar','Ctrl+Shift+C / V'],['Nova conexão','Alt+N'],['Som liga/desliga','Alt+M'],['Interromper / limpar / EOF','Ctrl+C / L / D'],['Histórico / completar','Ctrl+R / Tab']].map(([a,b])=>h('div',{},h('span',{text:a}),h('kbd',{text:b}))))); const secPriv=h('div',{class:'helpsec'},h('h4',{},icEl('shield'),'PRIVACIDADE E DADOS'), h('p',{text:'Cofre, snippets, temas e hosts conhecidos ficam no localStorage deste navegador. Senhas só são salvas se você marcar “Salvar senha” — e mesmo assim nunca saem do navegador, exceto para a ponte que você configurar. O estado dos servidores demo (arquivos criados, serviços) dura até recarregar a página.'}), h('div',{class:'ctlrow'},h('button',{class:'btn sm',html:I('down')+' Exportar cofre (sem segredos)',onclick:()=>{const hosts=DB.state.hosts.map(x=>{const c={...x};delete c.password;delete c.privateKey;delete c.passphrase;return c});dl('concha-cofre.json',JSON.stringify({hosts,snippets:DB.state.snippets},null,2),'application/json');toast('Cofre exportado sem senhas/chaves',{type:'ok'})}}), h('button',{class:'btn sm',html:I('up')+' Importar cofre',onclick:()=>{const fi=h('input',{type:'file',accept:'.json',style:'display:none'});fi.addEventListener('change',async()=>{try{const data=JSON.parse(await fi.files[0].text());let n=0;if(Array.isArray(data.hosts))data.hosts.forEach(x=>{if(x.host&&x.user){if(!DB.state.hosts.some(y=>y.id===x.id)){DB.state.hosts.push({...x,id:x.id||uid()});n++}}});if(Array.isArray(data.snippets))data.snippets.forEach(x=>{if(x.cmd&&!DB.state.snippets.some(y=>y.id===x.id)){DB.state.snippets.push(x);n++}});DB.save();Vault.render();Snips.render();Welcome.renderRecent();toast(n+' itens importados',{type:'ok'})}catch{toast('Arquivo inválido',{type:'err'})}});document.body.append(fi);fi.click();fi.remove()}}), h('button',{class:'btn sm btn-danger',html:I('reset')+' Restaurar dados de exemplo',onclick:async()=>{if(await confirmModal('Apagar cofre, snippets e preferências e restaurar os exemplos? Isso recarrega a página.',{danger:true,okLabel:'Apagar tudo'})){localStorage.removeItem('concha.v1');location.reload()}}}))); body.append(secReal,secDemo,secKeys,secPriv); openModal({title:'Ajuda do Concha',icon:'help',wide:true,body}); if(section==='real')setTimeout(()=>body.querySelector('#help-real')?.scrollIntoView({behavior:'smooth'}),80)}}; $('#btnHelp').innerHTML=I('help')+'Ajuda';$('#btnHelp').addEventListener('click',()=>Help.open()); /* ============================== SOM TOP ============================== */ const TopSound={render(){const b=$('#btnSound');const on=SET().sound!==false;b.innerHTML=I(on?'vol':'volx')+'Som';b.setAttribute('aria-pressed',on);b.dataset.tip='Som: '+(on?'ligado':'desligado')+' (Alt+M)'}, init(){this.render();$('#btnSound').addEventListener('click',()=>this.toggle())}, toggle(){SET().sound=!SET().sound;DB.save();this.render();if(Rail.tab==='apar')Apar.init();toast(SET().sound?'Som ligado':'Som desligado',{dur:1600});if(SET().sound)SND.bell()}}; /* ============================== ATALHOS GLOBAIS ============================== */ document.addEventListener('keydown',e=>{ if(e.key==='Escape'){if(menuEl){closeMenu();return}if(closeTop())return;const s=Tabs.activeSession();if(s?.searchBar.classList.contains('on')){s.toggleSearch(false);return}Sidebar.close();return} if(e.altKey&&!e.ctrlKey&&!e.metaKey){ if(e.code==='KeyT'){e.preventDefault();Tabs.activate('home');return} if(e.code==='KeyW'){e.preventDefault();const t=Tabs.active();if(t?.kind==='ssh')Tabs.closeTab(t.id);return} if(e.code==='KeyN'){e.preventDefault();Vault.drawer();return} if(e.code==='KeyM'){e.preventDefault();TopSound.toggle();return} if(/^Digit[1-9]$/.test(e.code)){e.preventDefault();const i=parseInt(e.code.slice(5))-1;if(Tabs.list[i])Tabs.activate(Tabs.list[i].id);return}} if(e.ctrlKey&&e.shiftKey&&e.code==='KeyF'){const s=Tabs.activeSession();if(s&&!s.dead){e.preventDefault();s.toggleSearch()}}}); window.addEventListener('resize',debounce(()=>{Tabs.activeSession()?.fit()},90)); document.addEventListener('fullscreenchange',()=>setTimeout(()=>Tabs.activeSession()?.fit(),120)); if(typeof ResizeObserver!=='undefined'){new ResizeObserver(debounce(()=>Tabs.activeSession()?.fit(),90)).observe($('#views'))} setInterval(()=>{const s=Tabs.activeSession();if(s)s.updateStatus()},1000); /* ============================== BOOT ============================== */ function loadScript(src){return new Promise((res,rej)=>{const s=document.createElement('script');s.src=src;s.onload=res;s.onerror=()=>{s.remove();rej(new Error(src))};document.head.appendChild(s)})} function loadCss(href){return new Promise((res,rej)=>{const l=document.createElement('link');l.rel='stylesheet';l.href=href;l.onload=res;l.onerror=()=>{l.remove();rej()};document.head.appendChild(l)})} async function loadDeps(){const msg=t=>{$('#splashMsg').textContent=t}; const bases=['https://cdn.jsdelivr.net/npm/','https://unpkg.com/']; msg('carregando motor do terminal…'); for(const b of bases){try{await loadCss(b+'@xterm/xterm@5.5.0/css/xterm.css');await loadScript(b+'@xterm/xterm@5.5.0/lib/xterm.js');if(window.Terminal){XTERM_OK=true; const addons=[['@xterm/addon-fit@0.10.0/lib/addon-fit.js',()=>window.FitAddon?.FitCtor||window.FitAddon?.FitAddon,v=>FitCtor=v],['@xterm/addon-search@0.15.0/lib/addon-search.js',()=>window.SearchAddon?.SearchAddon,v=>SearchCtor=v],['@xterm/addon-web-links@0.11.0/lib/addon-web-links.js',()=>window.WebLinksAddon?.WebLinksAddon,v=>LinksCtor=v]]; await Promise.allSettled(addons.map(async([p,get,set])=>{try{await loadScript(b+p);const c=get();if(c)set(c)}catch{}}));break}}catch{}} if(!XTERM_OK)msg('CDN indisponível — ativando motor compacto…')} (async function boot(){try{await loadDeps()}catch{} $('#splashMsg').textContent='preparando cofre…'; TopSound.init();Vault.init();Snips.init();Apar.init();LogPanel.init();Rail.init();Welcome.init();Tabs.init(); Tabs.statusHome();Tabs.renderBar(); if(document.fonts?.ready)document.fonts.ready.then(()=>setTimeout(()=>Tabs.activeSession()?.fit(),60)); setTimeout(()=>{$('#splash').classList.add('out');setTimeout(()=>$('#splash').remove(),600)},XTERM_OK?250:600); if(!XTERM_OK)toast('xterm.js não carregou — motor compacto ativo (demo 100% funcional)',{type:'warn',dur:7000}); console.log('%c🐚 concha_ v1.0','color:#2EE6A0;font-weight:bold;font-size:14px','— cliente SSH web · demo + ponte real')})();