[email protected]:~$ less site.tg

site.tg

The source of targa.me: a terminal written in TargaScript, 4902 lines in 18 files. It's what your browser runs at targa.me, colored here by TargaScript itself when the site is built.

Plain text · Open the terminal · Try TargaScript

18 files
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902
// site.tg — the program behind targa.me: a terminal written in TargaScript
// (https://script.targa.me), run in your browser by its WebAssembly
// interpreter. It is the files of the site joined in order, like the files
// of a Go package, so every top-level name is visible in all of them.
//
// Contents                                                            line
//   util.tg              HTML, padding, colors, the terminal size       29
//   strings.tg           string and time helpers from Go               112
//   content.tg           the text: bio, projects, logs, configs        258
//   hash.tg              MD5, SHA-256 and hex                          590
//   fs.tg                the virtual filesystem                        745
//   anim.tg              animations that Ctrl+C stops                  956
//   commands.tg          the command list, help, man, theme, exit     1093
//   commands/files.tg    ls, cd, cat, tree, find, ...                 1339
//   commands/text.tg     echo, grep, sort, base64, bc, ...            1707
//   commands/system.tg   uname, ps, top, neofetch, ...                2220
//   commands/network.tg  ping, nmap, dig, curl, ssh, ...              2506
//   commands/fun.tg      cowsay, lolcat, sl, cmatrix, figlet          2674
//   eggs.tg              hidden commands and easter eggs              2909
//   breakin.tg           ssh [email protected]: a root shell with this source  3328
//   targascript.tg       TargaScript inside the site and its REPL     3411
//   llm.tg               the small language model in the browser      3650
//   chat.tg              the claude command                           3928
//   main.tg              the shell: parsing, history, keys, boot      4372
//
// Try it: type help, or targascript to write your own.


// ── util.tg ─────────────────────────────────────────────────────────────

// util.tg — small helpers shared by every file: HTML, padding, colors,
// numbers and the size of the terminal.

fn escapeHtml(s) {
    s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;").replace("'", "&#39;")
}

fn unescape(s) {
    s.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&#39;", "'").replace("&amp;", "&")
}

// stripTags drops HTML tags, keeping the text between them
fn stripTags(s) {
    let out = ""
    let inTag = false
    repeat c in s {
        if c == "<" {
            inTag = true
        } else if c == ">" {
            inTag = false
        } else if !inTag {
            out += c
        }
    }
    out
}

fn errf(cmd, msg) { "<span class=\"red\">" + escapeHtml(cmd + ": " + msg) + "</span>" }

fn dim(s) { "<span class=\"dim\">" + s + "</span>" }

fn green(s) { "<span class=\"green\">" + s + "</span>" }

fn yellow(s) { "<span class=\"yellow\">" + s + "</span>" }

fn red(s) { "<span class=\"red\">" + s + "</span>" }

fn pad(s, n) { s.padEnd(n) }

fn lpad(s, n) { s.padStart(n) }

fn zpad(s, n) { toString(s).padStart(n, "0") }

fn itoa(n) { toString(n) }

// lastIndex is the position of the last sub in s, or -1
fn lastIndex(s, sub) {
    let at = -1
    let from = 0
    repeat true {
        let i = s.slice(from).indexOf(sub)
        if i < 0 { break }
        at = from + i
        from = at + 1
    }
    at
}

// rand is a random integer from 0 to n - 1
fn rand(n) { random(0, n - 1) }

// The time the terminal started, in milliseconds since the program did
let bootClock = clock()

fn uptimeSeconds() { (clock() - bootClock) / 1000.0 }

fn cores() { js.navigator.hardwareConcurrency ?? 1 }

// columns estimates how many monospace characters fit in the terminal
fn columns() {
    let probe = doc.createElement("span")
    probe.textContent = "M".repeat(20)
    probe.style.visibility = "hidden"
    probe.style.whiteSpace = "pre"
    terminal.appendChild(probe)
    let w = probe.offsetWidth / 20.0
    probe.remove()
    if w <= 0 { return 80 }
    max(20, toInt((terminal.clientWidth - 20) / w))
}

// ── strings.tg ──────────────────────────────────────────────────────────

// strings.tg — Go's string and time helpers, which the commands use:
// strings.Fields, TrimSpace, TrimRight, Trim, strconv.Atoi, ParseFloat,
// FormatFloat(v, 'f', -1) and time.Format.

// The characters Go's unicode.IsSpace accepts
let spaceChars = " \t\n\r" + bytes([
    11, 12, 0xC2, 0x85, 0xC2, 0xA0, 0xE1, 0x9A, 0x80,
    0xE2, 0x80, 0x80, 0xE2, 0x80, 0x81, 0xE2, 0x80, 0x82, 0xE2, 0x80, 0x83,
    0xE2, 0x80, 0x84, 0xE2, 0x80, 0x85, 0xE2, 0x80, 0x86, 0xE2, 0x80, 0x87,
    0xE2, 0x80, 0x88, 0xE2, 0x80, 0x89, 0xE2, 0x80, 0x8A,
    0xE2, 0x80, 0xA8, 0xE2, 0x80, 0xA9, 0xE2, 0x80, 0xAF, 0xE2, 0x81, 0x9F,
    0xE3, 0x80, 0x80,
]).text()

fn isSpace(ch) { spaceChars.contains(ch) }

// fields splits s around runs of white space, like strings.Fields
fn fields(s) {
    let out = []
    let cur = ""
    repeat ch in s {
        if isSpace(ch) {
            if cur != "" {
                out.insert(cur)
                cur = ""
            }
        } else {
            cur += ch
        }
    }
    if cur != "" { out.insert(cur) }
    out
}

// trimSpace drops white space at both ends, like strings.TrimSpace
fn trimSpace(s) {
    let chars = s.chars()
    let start = 0
    let end = chars.length
    repeat start < end && isSpace(chars[start]) { start += 1 }
    repeat end > start && isSpace(chars[end - 1]) { end -= 1 }
    if start == 0 && end == chars.length { return s }
    chars.slice(start, end).join("")
}

// trimRightChars drops the characters in cutset from the end of s, like
// strings.TrimRight
fn trimRightChars(s, cutset) {
    let out = s
    repeat out != "" && cutset.contains(out[-1]) { out = out.slice(0, -1) }
    out
}

// trimChars drops the characters in cutset from both ends, like strings.Trim
fn trimChars(s, cutset) {
    let out = trimRightChars(s, cutset)
    repeat out != "" && cutset.contains(out[0]) { out = out.slice(1) }
    out
}

// atoi parses a decimal integer like strconv.Atoi: [n, ok], n is 0 when not ok
fn atoi(s) {
    if !s.matches("^[+-]?[0-9]+$") { return [0, false] }
    try { [toInt(s), true] } catch e { [0, false] }
}

// parseFloat parses a number like strconv.ParseFloat: [x, ok]
fn parseFloat(s) {
    let l = s.lower()
    let sign = if l.startsWith("-") { -1.0 } else { 1.0 }
    let body = if l.startsWith("-") || l.startsWith("+") { l.slice(1) } else { l }
    if body == "inf" || body == "infinity" { return [sign / 0.0, true] }
    if body == "nan" { return [0.0 / 0.0, true] }
    if !s.matches("^[+-]?([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?$") { return [0.0, false] }
    let v = toFloat(s)
    if v == 1.0 / 0.0 || v == -1.0 / 0.0 { return [v, false] } // out of range
    [v, true]
}

// formatFloat writes a float in plain decimal notation with the fewest
// digits that read back the same, like strconv.FormatFloat(v, 'f', -1, 64)
fn formatFloat(v) {
    if v != v { return "NaN" }
    if v == 1.0 / 0.0 { return "+Inf" }
    if v == -1.0 / 0.0 { return "-Inf" }
    let s = toString(v)
    let neg = s.startsWith("-")
    if neg { s = s.slice(1) }
    let exp = 0
    let e = s.lower().indexOf("e")
    if e >= 0 {
        exp = toInt(s.slice(e + 1))
        s = s.slice(0, e)
    }
    let dot = s.indexOf(".")
    let digits = s.replace(".", "")
    let point = (if dot >= 0 { dot } else { s.length }) + exp
    // Drop leading zeros, keeping the point where it was
    repeat digits.length > 1 && digits.startsWith("0") {
        digits = digits.slice(1)
        point -= 1
    }
    let intPart = ""
    let fracPart = ""
    if point <= 0 {
        intPart = "0"
        fracPart = "0".repeat(-point) + digits
    } else if point >= digits.length {
        intPart = digits + "0".repeat(point - digits.length)
    } else {
        intPart = digits.slice(0, point)
        fracPart = digits.slice(point)
    }
    fracPart = trimRightChars(fracPart, "0")
    (if neg { "-" } else { "" }) + intPart + (if fracPart != "" { "." + fracPart } else { "" })
}

// goTime formats Unix seconds with a Go time layout: Mon, Jan, January,
// 2006, 01, 02, 2, _2, 15, 04 and 05
fn goTime(t, layout) {
    let tokens = [
        ["January", "MMMM"], ["2006", "YYYY"], ["Mon", "ddd"], ["Jan", "MMM"], ["_2", "_2"],
        ["15", "HH"], ["04", "mm"], ["05", "ss"], ["01", "MM"], ["02", "DD"], ["2", "2"],
    ]
    let out = ""
    let rest = layout
    repeat rest != "" {
        let tok = tokens.find(([k, f]) => rest.startsWith(k))
        if tok == null {
            out += rest[0]
            rest = rest.slice(1)
            continue
        }
        let [k, f] = tok
        out += match f {
            "_2" => lpad(toString(toInt(timeFormat(t, "DD"))), 2)
            "2" => toString(toInt(timeFormat(t, "DD")))
            _ => timeFormat(t, f)
        }
        rest = rest.slice(k.length)
    }
    out
}

// ── content.tg ──────────────────────────────────────────────────────────

// content.tg — static content: the text that visitors read. Files marked html
// in fs.tg are rendered as-is; everything else is plain text and gets escaped.

const version = "v2.0"

let banner = " _____                                      \n" +
"|_   _|_ _ _ __ __ _  __ _   _ __ ___   ___ \n" +
"  | |/ _` | '__/ _` |/ _` | | '_ ` _ \\ / _ \\\n" +
"  | | (_| | | | (_| | (_| |_| | | | | |  __/\n" +
"  |_|\\__,_|_|  \\__, |\\__,_(_)_| |_| |_|\\___|\n" +
"               |___/               " + version + "\n\n" +
`Type <span class="yellow">help</span> to get started.` + "\n"

const aboutMe = `
<span class="green">Hey there! 👋</span> I'm <span class="yellow">Miguel Targa</span>, a <span class="blue">software engineer</span> and <span class="blue">reverse engineering enthusiast</span> with over a decade in the industry.

<span class="green">Quick facts:</span>
- Born in sunny <span class="yellow">Brazil</span>, moved to the <span class="yellow">United States</span> at 15.
- Started dissecting other people's code in <span class="blue">Delphi</span> and <span class="blue">Pascal</span> at 12. Curiosity about how things work under the hood turned into a career.

<span class="green">Security:</span>
I enjoy finding vulnerabilities and helping fix them. That earned me a spot in Google's <a href="https://bughunters.google.com/profile/fa3f5d87-6047-424d-b223-de7cf12e47ac" class="link" target="_blank" rel="noopener">Bug Hunters Hall of Fame</a>. Binary analysis and low-level programming are where I have the most fun.

<span class="green">What I work with:</span>
- Languages: <span class="yellow">Go, Python, TypeScript, JavaScript, Kotlin, C#, Java, PHP, Delphi, CFML</span>
- Platforms: <span class="yellow">.NET, Android, Lucee, embedded (ESP32)</span>

Have a look at <span class="yellow">projects</span> or say hi via <span class="yellow">contact</span>. Thanks for stopping by!
`

const contactInfo = `<span class="green">Find me here:</span>
  - <a href="mailto:[email protected]" class="link">[email protected]</a>
  - <a href="https://github.com/migueltarga" target="_blank" rel="noopener" class="link">github.com/migueltarga</a>
  - <a href="https://linkedin.com/in/migueltarga" target="_blank" rel="noopener" class="link">linkedin.com/in/migueltarga</a>
  - <a href="https://bughunters.google.com/profile/fa3f5d87-6047-424d-b223-de7cf12e47ac" target="_blank" rel="noopener" class="link">Google Bug Hunters profile</a>`

// profile is who Miguel is, in plain words, for the pages build.sh makes for
// search engines and AI assistants (see pages.tg). Every fact here is also
// somewhere in the terminal.
const profile = {
    name: "Miguel Targa",
    url: "https://targa.me/",
    email: "[email protected]",
    title: "Software engineer and reverse engineer",
    summary: "Miguel Targa is a software engineer and reverse engineer with over a decade in the industry. He is in Google's Bug Hunters Hall of Fame for finding security vulnerabilities and helping fix them, and he created TargaScript, a programming language with its own compiler, bytecode virtual machine and WebAssembly runtime. His personal site, targa.me, is an interactive terminal written in TargaScript.",
    highlights: [
        "Listed in Google's Bug Hunters Hall of Fame for finding security vulnerabilities and helping fix them.",
        "Created TargaScript, a programming language with a compiler, a bytecode virtual machine, a WebAssembly runtime, a formatter, a test runner, a language server and a playground written in the language itself.",
        "Wrote targa.me in TargaScript: an interactive terminal with a virtual filesystem, over 150 commands and a small language model that runs in the browser.",
        "Over a decade in the industry, working across Go, Python, TypeScript, JavaScript, Kotlin, C#, Java, PHP, Delphi and CFML, on .NET, Android, Lucee and ESP32 hardware.",
        "Started reading other people's code in Delphi and Pascal at 12; how things work under the hood is still what drives him.",
        "Writes about peer-to-peer networking, with runnable Rust code, and builds hardware projects like kiddo, a storytelling device for kids.",
    ],
    story: "Born in Brazil, Miguel moved to the United States at 15. Binary analysis, security research and low-level programming are where he has the most fun.",
    topics: ["Reverse engineering", "Security research", "Vulnerability research", "Binary analysis", "Low-level programming",
        "Programming language design", "Compilers", "Virtual machines", "WebAssembly", "Go", "Python", "TypeScript", "JavaScript",
        "Kotlin", "C#", "Java", "PHP", "Delphi", "CFML", ".NET", "Android", "Embedded systems", "ESP32", "Peer-to-peer networking"],
    links: [
        {name: "GitHub", url: "https://github.com/migueltarga"},
        {name: "LinkedIn", url: "https://linkedin.com/in/migueltarga"},
        {name: "Google Bug Hunters", url: "https://bughunters.google.com/profile/fa3f5d87-6047-424d-b223-de7cf12e47ac"},
        {name: "TargaScript playground", url: "https://script.targa.me/"},
    ],
}

// projectList is every project, for the terminal (projects and the files
// in ~/projects) and for the pages build.sh makes for search engines.
// summary goes in the list, desc in the project's file; both are HTML.
const projectList = [
    {file: "targascript.md", name: "TargaScript", url: "https://github.com/migueltarga/TargaScript", stack: "Go",
        summary: `A scripting language written in Go. <span class="yellow">This site runs on it:</span> try <span class="yellow">targascript</span>`,
        desc: "If TypeScript can be rewritten in Go, why not build a language in it? TargaScript is a small, modern scripting language: pipes, pattern matching, structs and enums, destructuring and top-level await, with a formatter, a test runner, a web server and editor support built in. A compiler turns programs into bytecode for a virtual machine, which runs the same in the terminal and, compiled to WebAssembly, in web pages. This site is written in it. Try it at <a href=\"https://script.targa.me/\" target=\"_blank\" rel=\"noopener\" class=\"link\">script.targa.me</a>, a playground also written in TargaScript. Inspired by \"Writing An Interpreter In Go\"."},
    {file: "p2p.md", name: "P2P", url: "https://github.com/migueltarga/P2P", stack: "Rust, Portuguese",
        summary: "Article series on peer-to-peer networking, with Rust companion code",
        desc: "A written series about how peer-to-peer networks actually work, built from experiments and personal curiosity. Each article ships with runnable Rust code."},
    {file: "kiddo.md", name: "kiddo", url: "https://github.com/migueltarga/kiddo", stack: "C, ESP32",
        summary: "Interactive storytelling device for kids on an ESP32 touchscreen",
        desc: "A fun, interactive storytelling device for kids built on a Cheap Yellow Display (ESP32 with a touchscreen)."},
    {file: "telegram-blockly.md", name: "TelegramBlockly", url: "https://github.com/migueltarga/TelegramBlockly", stack: "JavaScript",
        summary: "Build Telegram bots with a visual block language",
        desc: "Create Telegram bots by snapping blocks together instead of writing code. A visual programming environment on top of Blockly."},
    {file: "ssh-control.md", name: "SSH-Control", url: "https://github.com/migueltarga/SSH-Control", stack: "TypeScript",
        summary: "VS Code extension for managing SSH connections",
        desc: "Manage and open SSH connections without leaving VS Code."},
]

const projectsIndex = `<span class="green">Projects</span>  <span class="dim">(cat a file for details, e.g. cat projects/targascript.md)</span>

` + projectList.map(p => `  <a href="` + p.url + `" target="_blank" rel="noopener" class="link">` + p.name + `</a>` + " ".repeat(17 - p.name.length) + p.summary).join("\n") + `

More on <a href="https://github.com/migueltarga" target="_blank" rel="noopener" class="link">GitHub</a>.`

fn project(name, url, stack, desc) {
    `<span class="green">` + name + `</span>  <span class="dim">` + stack + `</span>
` + desc + `
<a href="` + url + `" target="_blank" rel="noopener" class="link">` + url + `</a>`
}

// What the ai command knows: the system prompt for the small model in the
// browser. Prose and an example work better than rules at this size, and
// telling it to refuse makes it go silent, so the disclaimer footer does that
// job instead. Contact questions never reach the model (see cmdAI).
const askFacts = "You are a small assistant in the terminal on targa.me, the personal site of Miguel Targa. Visitors ask you about Miguel. " +
"Miguel Targa is a software engineer and reverse engineering enthusiast with over a decade in the industry. He was born in Brazil and moved to the United States at 15. He started reading other people's code in Delphi and Pascal at 12. " +
"He finds and helps fix security vulnerabilities and is listed in Google's Bug Hunters Hall of Fame; he enjoys binary analysis and low-level programming. " +
"He uses Go, Python, TypeScript, JavaScript, Kotlin, C#, Java, PHP, Delphi and CFML, on .NET, Android, Lucee and ESP32. " +
"His projects: TargaScript (a scripting language written in Go, which this site is written in), P2P (articles on peer-to-peer networking with Rust code), kiddo (a storytelling device for kids on an ESP32), TelegramBlockly (build Telegram bots with visual blocks) and SSH-Control (a VS Code extension for SSH connections). " +
"This website, targa.me, is a fake terminal written in TargaScript, running in the browser on TargaScript's WebAssembly interpreter, with no backend. " +
"TargaScript is Miguel's own scripting language, written in Go. It is not TypeScript, and has nothing to do with TypeScript, Python or JavaScript. It compiles programs to bytecode for a virtual machine that runs in the terminal and, compiled to WebAssembly, in web pages. The website is written in it and embeds it: type targascript for a REPL or targascript hello.tg to run an example. Anyone can try it at script.targa.me, a playground that is also written in TargaScript. TargaScript syntax uses let, fn, repeat, match and braces, with pipes (|>), string interpolation, structs and enums. Example TargaScript program:\n" +
"// comment\nlet nums = [1, 2, 3]\nfn double(n) { n * 2 }\nrepeat n in nums { print(double(n)) }\nrepeat i in 1..5 { print(\"line \${i}\") }\n" +
"let person = {name: \"Ada\"}\nif person.name == \"Ada\" { print(\"hi \${person.name}\") } else { print(\"who?\") }\nlet evens = nums |> filter(it % 2 == 0)\nprint(evens.length)\n" +
"Answer in one or two sentences, speaking about Miguel in the third person. When asked for TargaScript code, reply with a short example in that syntax."

// The flag for the little treasure hunt. Chain: ls -a -> .secret -> auth.log -> base64 -d.
// Anyone can read this file, so it only keeps the flag's SHA-256: the way to
// the flag is still the hunt.
const flagHash = "10cadc52f7255484d0a3bfb84ecb777f48a44d36eb757f92bcd3f3ab04ecf75f"

// \$ is an escape even in backtick strings, so the prompt's backslash and
// dollar come from a quoted string.
const bashrc = `# ~/.bashrc: executed by tsh for interactive shells
export EDITOR=vim
export PS1='\w ` + "\\$" + ` '
alias ll='ls -l'
alias la='ls -a'
alias please='sudo'
# TODO: remove the secret before deploying
`

const bashHistory = `ls -la
cat .secret
cat /etc/shadow
sudo cat /etc/shadow
ssh root@localhost
vim
:q
:q!
rm -rf node_modules
git push --force
neofetch
fortune | cowsay | lolcat
`

const secretFile = `Nobody looks at the logs.
`

const authLog = `Sep 11 02:14:07 targa sshd[666]: Failed password for root from 10.0.0.66 port 4444 ssh2
Sep 11 02:14:09 targa sshd[666]: Failed password for root from 10.0.0.66 port 4444 ssh2
Sep 11 02:14:12 targa sshd[666]: Disconnected from authenticating user root 10.0.0.66 port 4444 [preauth]
Sep 11 02:15:00 targa sudo:    guest : command not allowed ; TTY=pts/0 ; PWD=/home/guest ; USER=root ; COMMAND=/bin/cat /etc/shadow
Sep 11 02:15:31 targa sshd[667]: Invalid user dGFyZ2F7cjN2M3JzMW5nX3RoM193NHNtfQ== from 127.0.0.1 port 31337
Sep 11 02:15:31 targa sshd[667]: input_userauth_request: invalid user (looks encoded, try base64 -d) [preauth]
Sep 11 02:16:00 targa tsh[1]: session opened for user guest by (uid=0)
`

const syslog = `Sep 11 00:00:00 targa kernel: [    0.000000] Booting targa.me ` + version + ` (wasm32)
Sep 11 00:00:00 targa kernel: [    0.000420] Memory: 16384K available
Sep 11 00:00:00 targa kernel: [    0.001337] Loading easter eggs... done
Sep 11 00:00:01 targa tsh[1]: Welcome, guest. Try 'help -a'.
Sep 11 00:00:01 targa cron[42]: (guest) CMD (fortune | cowsay)
`

const passwd = `root:x:0:0:root:/root:/bin/tsh
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
neo:x:1337:1337:Thomas A. Anderson:/nonexistent:/usr/sbin/nologin
guest:x:1000:1000:Visitor:/home/guest:/bin/tsh
`

const groupFile = `root:x:0:
sudo:x:27:root
guest:x:1000:
`

const hosts = `127.0.0.1   localhost targa.me
::1         localhost
10.0.0.66   definitely-not-a-honeypot
`

const motd = `Welcome to targa.me. Unauthorized access is encouraged.
`

const osRelease = `NAME="targa.me"
VERSION="` + version + ` (Trinity)"
ID=targa
PRETTY_NAME="targa.me ` + version + `"
HOME_URL="https://targa.me"
BUILD_ID=wasm32
`

const fstab = `# <device>   <mount>   <type>   <options>
/dev/wasm0   /         wasmfs   ro,relatime   0 1
tmpfs        /tmp      tmpfs    rw,nosuid     0 0
`

const resolvConf = `nameserver 127.0.0.1
search targa.me
`

const idRSA = `-----BEGIN OPENSSH PRIVATE KEY-----
bm90IHRvZGF5LiBuaWNlIHRyeSB0aG91Z2guCg==
-----END OPENSSH PRIVATE KEY-----
`

const authorizedKeys = `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFollowTheWhiteRabbit0000000000000000000 neo@matrix
`

const knownHosts = `targa.me ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN0tR34lly4K3y00000000000000000000000
`

const procVersion = `targa.me version ` + version + ` (go@wasm32) #1 SMP PREEMPT_DYNAMIC
`

const wasmStrings = `targa.me
tsh
guest
/home/guest/.secret
/root/flag.txt is not where the flag is
base64 is not encryption
Wake up, Neo...
`

const cowFile = `##
## Default cow. Moo.
##
$the_cow = <<EOC;
        $thoughts   ^__^
         $thoughts  ($eyes)\\_______
            (__)\\       )\\/\\
             $tongue ||----w |
                ||     ||
EOC
`

const wwwIndex = `<!DOCTYPE html>
<!-- You are already looking at it. -->
`

let fortunes = [
    "There are only two hard things in computer science: cache invalidation and naming things.\n    -- Phil Karlton",
    "Talk is cheap. Show me the code.\n    -- Linus Torvalds",
    "Any sufficiently advanced technology is indistinguishable from magic.\n    -- Arthur C. Clarke",
    "It works on my machine.",
    "The best way to predict the future is to invent it.\n    -- Alan Kay",
    "Programs must be written for people to read, and only incidentally for machines to execute.\n    -- Abelson & Sussman",
    "Security is a process, not a product.\n    -- Bruce Schneier",
    "Premature optimization is the root of all evil.\n    -- Donald Knuth",
    "Simplicity is prerequisite for reliability.\n    -- Edsger W. Dijkstra",
    "First, solve the problem. Then, write the code.\n    -- John Johnson",
    "In theory, theory and practice are the same. In practice, they are not.",
    "Weeks of coding can save you hours of planning.",
    "There's no place like 127.0.0.1.",
    "rm -rf / is not a cleaning command.",
    "The cake is a lie.",
    "If debugging is the process of removing bugs, then programming must be the process of putting them in.\n    -- Edsger W. Dijkstra",
    "Every byte tells a story. Reverse it.",
    "A good disassembler is worth a thousand comments.",
]

let trainFrames = [
    "      ====        ________                ___________ ",
    "  _D _|  |_______/        \\__I_I_____===__|_________| ",
    "   |(_)---  |   H\\________/ |   |        =|___ ___|   ",
    "   /     |  |   H  |  |     |   |         ||_| |_||   ",
    "  |      |  |   H  |__--------------------| [___] |   ",
    "  | ________|___H__/__|_____/[][]~\\_______|       |   ",
    "  |/ |   |-----------I_____I [][] []  D   |=======|__ ",
    "__/ =| o |=-~~\\  /~~\\  /~~\\  /~~\\ ____Y___________|__ ",
    " |/-=|___|=    ||    ||    ||    |_____/~\\___/        ",
    "  \\_/      \\O=====O=====O=====O_/      \\_/            ",
]

let neofetchLogo = [
    " ████████╗ ",
    " ╚══██╔══╝ ",
    "    ██║    ",
    "    ██║    ",
    "    ██║    ",
    "    ╚═╝    ",
]

// zoeArt is printed by the hidden `zoe` command.
const zoeArt = `                                        ...:^~~~!!!!!~~^^^^^..                                      
                                   .:^~!777777777?????????777!~:.                                   
                                 :~!7????77????JJJJJJJJJJYYYYYJ?!~~^:.                              
                               :!7????????JJJJJJJJJJJJJY5PPPP5YYY?77!!~^.                           
                             .~7??????JJJJJJYYYYYYYJJJY5PP55Y??JJJ??777??~                          
                            :7????JJJJYYYYYYYYYYYYYYYYY555Y7!????JJJJJJJJY?:                        
                           ^?J?JYYYYYYYYYYYY55555YYYYY5P5Y?~???????J5555YJYY~                       
                          !JJJY55YYYYY55555YY555555YYYYYJ?!!???JJJ?J55PPP5YY57.                     
                         !JJY55555555P55YYY55555YYYJ?7!!~~~~!??JJYYY555PGP5Y557.                    
                        ~JY5P555PPPPPYYY5555YYJ??7!~~^^^^^^~~!7?JY55PPP5PBG5Y55!                    
                       :JY55PPGGPPP5YY5P5YJ?7!!~^^^::^^^^^^^^~77JJY55PPPPPBP555Y:                   
                      .!Y5PPGGGGP55Y5PP5J7~~^^^::::::::::::::^~!7?JY5555PPPBP555?                   
                      ^JPPGGGGGP5JY5PPY7!~^^:::::::::::::::::^^~!77JY5PPPPPGB5555~                  
                      ~5PGGBBGPG5J5PGY7!~^^^:::::::::::::^^^^^^~~!!7J5PGGGPPBG555J.                 
                     .?PGBB#BPPGYYPG5?!~^^^^^^^^^^^^^^^^^^^^^^~~~~!!7J5PGGGPPGP555~                 
                     :5GB##BGPPPY5GGJ7!~~~~~~~~~~~~~~~~~~~~~~~~~~!!!!?Y5GBBGGPGP55?                 
                   ..7GB##BGPPP5Y5B5?7!!~~~~~~~~~~~~~~~~~~~~~~~~!!!!!7?YPBBBBGBG55Y:                
                    :5GBBGPPPP55YGBY7!!!~~~~~~~~~~~~~~~~~!~!!!!!!!!!!!7?YB#B####G5Y~                
                    :YGP55PPPP5JYGP?!!!!!!!!!~~!!!~~~~~~!!!!~~!!!!!~~~~!7YG##&&&BPY!.               
                   .:7YY5PPPP5JJPBY777777????777!!!~~~~!!!!77??????7!!!!!!JG#&&&#P57.               
                   .!?YPGPGP5?7PGP?77777777????777!!~~!!77??JJ??????777!~~!5#&###G5?.               
                   .?5PGGGGP?75G5J77??JYYYYYJ???77!!~~!!7?JJJYYYYYYJJ?7!~~~YBBB##G5J.               
                   :5GGGPPP5J55YJ???JYY5PGGP?JJ777!~~~~!??YY5PBGGY55YJ?!~~~YGB#&#P5J.               
                   ~PGPPPPGP5J?7!!!!7777JJJ?!77!!!~^^^^~7!777JJJ?7?77!~~^~!5B#&&#G5Y:               
                   ^PPPPGGGP?7!~^^^^^^~~!!777!!~~~^::::^~~!77777!!!~^^^^^~?BB#&#BP5J^               
                   .75PPPPPP?!~^^:::::^^^~~~^^^^^^:....:^~~~~~~~^^^:::^^^!YBB##BBPY?:               
                   .~?YP5555?~~^:::::::::::::^^^^:.    .:^~^^^:::::::^^^~7PG###GGPY7:.              
                   ^~!JP5Y5J?!~^::::::::::::^~~^::::::::^:^~~^^::::::^^^~?PB#BBGG5J7:               
                  .~~^?5Y5G?5?~^^:::::::^^^~~!~~!??!!!!?J7~~!!~^^^^^^^^~!55##BGGP5J?~               
                  .:^~?YPB###G~^^^^^^^^^^~~~!!~~!777??7??7!~~!!~~~~^~~~~?&#BBBGGP5YJ7:              
                   .^7J5PGB###?^^^^^^~~~~~~~~~~^^~~!!!~~~~~~~~!!~~~~~~~~Y##BBBGGP555J~.             
                  .^!JY5GPGB#&G!~~~~~~~~~~!!7?77?JJJJJJJJ?7???!!!~!!!!!7B#B#BBBGP5Y?YJ^             
                 .:~7Y5PGGPGB#&P!~~~~~~~~~!?JJYYYYYY55YY5555Y?7!!~!!!!7PB###BBBGGP5JJ?^             
               .^~!?YPPGGGGGB##&G?!!!!!!!!!!~!!!77????????7!~!!!!!!77JPB####B5PGBBGPY!              
              .~?Y55PPGGPGGBBB&&&BJ77777!!!!~~~!!7777??777!!!!777??JPB#&&#BBB5YYPGBGP7              
             :!JPGPP55PPGGGBBB#&&#5J????777!!!~~~~~~~~~~~~!!!7??JYGB#&&&&BGBBBG5YJYBBP!             
            :7J55YY55PPGGGGB#BB#&&5J???JJJ?77!!~~~~~~~~~~!!7?JJY5B&##&&&&##BGGPPPPPPGP?.            
           .:~JYJJYPGGPPGGBBGGB#&#5J?77???JJJ??777777777???JYYYYYPB##&#######BGGGGP5J!:..:          
            .~?YPPPPPPPGBBGPGGG#BG5J?77777???JJJJJJJJJJJJJJJJJJJY5PPGB#&#BBBBBBGP5YJJ~^.^??J?:..    
             ^~7?5PG5P#BBGBBBGGGPPYJ??777777??????JJJJJJJJJJJJJJYYY555P#&&&&&&#B5JYJ7..?B#&&#BGPJ^. 
         .~~!77!755GG5G#GGGGGGGPP5YJ?77777777777???????JJJJJJ?JJJJJJYY5#&&&&&&&&#GPGPY5B######BB#BY.
        :~G#&&&&&#GPGGGGGGGBBGP55YJ??7777777777????????JJJJJ???77????JP&&&&&&&&&&&&&&&&###BB#B#BBBP7
    ^!J5G##&&&&&&@&#BBGGGB##&B5YJJ?7777777777777???????????777!!!7777JB&&&&&&&&&&&&#####&###GG##BGPJ
   .Y####&&&&&&&&@@@&&&&&&&&&#5??777!!!!!!!!7777777777??77!!!!~~!!~!7G&&&&&&&&&&&&&&&####&##B#&#BGP!
   .!Y5PPB#&&&&&&@@@@@@@@@&&&&BJ!!!!!!~~~!!!!!!!!!!!!!!!!~~~~~~~~~~!G&&&&&&&&&&&&&&&&###########BBGY
    ...:?#&&&&&&&&&&&&&&&&&&&&&#Y!~~~~~~~~~~~~~~~~~~~~~~~^~~^^^~~!Y#&&&&&&&&&&&&&&&&&###B#######BP?:
  ..::.:G&&&&&&&&&&&&&&&&&&&&&&&&G?~~~~~~~~~~~~~~~~~~~^^^^^^^^~~?G&&&&&&&&&&&&&&&&&&&&&######GPGY!~^
`

// ── hash.tg ─────────────────────────────────────────────────────────────

// hash.tg — compact MD5, SHA-256 and hex. Integers are 64-bit, so the
// 32-bit words are kept with & 0xFFFFFFFF after every addition and shift.

// hexEncode is the lowercase hex of some bytes
fn hexEncode(b) { b.hex() }

fn rotl32(x, n) { ((x << n) | (x >> (32 - n))) & 0xFFFFFFFF }

fn rotr32(x, n) { ((x >> n) | (x << (32 - n))) & 0xFFFFFFFF }

// padMessage appends the standard Merkle–Damgård padding with the bit length
// encoded big- or little-endian.
fn padMessage(msg, bigEndian) {
    let n = msg.length * 8
    let zeros = (55 - msg.length % 64 + 64) % 64
    let out = msg + bytes([0x80]) + bytes(zeros + 8)
    if bigEndian {
        out.writeBE(out.length - 8, 8, n)
    } else {
        out.write(out.length - 8, 8, n)
    }
    out
}

// Precomputed floor(abs(sin(i+1)) * 2^32) constants.
const md5K = [
    0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
    0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
    0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
    0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
    0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
    0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
    0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
    0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
]

const md5S = [
    7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
    5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
    4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
    6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
]

// md5Hex is the MD5 of a text (its UTF-8) or of bytes, in hex
fn md5Hex(data) {
    let msg = padMessage(bytes(data), false)
    let a0 = 0x67452301
    let b0 = 0xefcdab89
    let c0 = 0x98badcfe
    let d0 = 0x10325476
    let m = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
    let off = 0
    repeat off < msg.length {
        repeat i in 0..15 { m[i] = msg.read(off + i * 4, 4) }
        let a = a0
        let b = b0
        let c = c0
        let d = d0
        repeat i in 0..63 {
            let f = 0
            let g = 0
            if i < 16 {
                f = (b & c) | (~b & d)
                g = i
            } else if i < 32 {
                f = (d & b) | (~d & c)
                g = (5 * i + 1) % 16
            } else if i < 48 {
                f = b ^ c ^ d
                g = (3 * i + 5) % 16
            } else {
                f = (c ^ (b | ~d)) & 0xFFFFFFFF
                g = (7 * i) % 16
            }
            f = (f + a + md5K[i] + m[g]) & 0xFFFFFFFF
            a = d
            d = c
            c = b
            b = (b + rotl32(f, md5S[i])) & 0xFFFFFFFF
        }
        a0 = (a0 + a) & 0xFFFFFFFF
        b0 = (b0 + b) & 0xFFFFFFFF
        c0 = (c0 + c) & 0xFFFFFFFF
        d0 = (d0 + d) & 0xFFFFFFFF
        off += 64
    }
    let out = bytes(16)
    out.write(0, 4, a0)
    out.write(4, 4, b0)
    out.write(8, 4, c0)
    out.write(12, 4, d0)
    hexEncode(out)
}

const sha256K = [
    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]

// sha256Hex is the SHA-256 of a text (its UTF-8) or of bytes, in hex
fn sha256Hex(data) {
    let msg = padMessage(bytes(data), true)
    let h = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]
    let w = []
    repeat 64 { w.insert(0) }
    let off = 0
    repeat off < msg.length {
        repeat i in 0..15 { w[i] = msg.readBE(off + i * 4, 4) }
        repeat i in 16..63 {
            let x = w[i - 15]
            let y = w[i - 2]
            let s0 = rotr32(x, 7) ^ rotr32(x, 18) ^ (x >> 3)
            let s1 = rotr32(y, 17) ^ rotr32(y, 19) ^ (y >> 10)
            w[i] = (w[i - 16] + s0 + w[i - 7] + s1) & 0xFFFFFFFF
        }
        let [a, b, c, d, e, f, g, hh] = h
        repeat i in 0..63 {
            let S1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25)
            let ch = (e & f) ^ (~e & g)
            let t1 = (hh + S1 + ch + sha256K[i] + w[i]) & 0xFFFFFFFF
            let S0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22)
            let maj = (a & b) ^ (a & c) ^ (b & c)
            let t2 = S0 + maj
            hh = g
            g = f
            f = e
            e = (d + t1) & 0xFFFFFFFF
            d = c
            c = b
            b = a
            a = (t1 + t2) & 0xFFFFFFFF
        }
        h[0] = (h[0] + a) & 0xFFFFFFFF
        h[1] = (h[1] + b) & 0xFFFFFFFF
        h[2] = (h[2] + c) & 0xFFFFFFFF
        h[3] = (h[3] + d) & 0xFFFFFFFF
        h[4] = (h[4] + e) & 0xFFFFFFFF
        h[5] = (h[5] + f) & 0xFFFFFFFF
        h[6] = (h[6] + g) & 0xFFFFFFFF
        h[7] = (h[7] + hh) & 0xFFFFFFFF
        off += 64
    }
    let out = bytes(32)
    repeat i, v in h { out.writeBE(i * 4, 4, v) }
    hexEncode(out)
}

// ── fs.tg ───────────────────────────────────────────────────────────────

// fs.tg — the virtual filesystem. Directories map to lists of entries;
// paths are absolute and the visitor's home is /home/guest, shown as ~.
//
// An entry is an object with a name and some of: dir, content, gen (a
// function making the content, as for /proc), html (the content is trusted
// HTML), exec (shown as an executable), binary (cat refuses, xxd and strings
// work), denied (root only) and link (a symlink's target, for display).

const home = "/home/guest"

// The magic number and version that start a WebAssembly file
let wasmHeader = bytes([0, 97, 115, 109, 1, 0, 0, 0]).text()

let tree = {
    "/": [
        {name: "bin", dir: true}, {name: "boot", dir: true}, {name: "dev", dir: true},
        {name: "etc", dir: true}, {name: "home", dir: true}, {name: "proc", dir: true},
        {name: "root", dir: true, denied: true}, {name: "tmp", dir: true},
        {name: "usr", dir: true}, {name: "var", dir: true},
    ],
    "/boot": [
        {name: "targa.wasm", binary: true, content: wasmHeader + wasmStrings},
        {name: "config-wasm", content: "CONFIG_EASTER_EGGS=y\nCONFIG_ROOT_LOGIN=n\nCONFIG_COFFEE=m\n"},
    ],
    "/dev": [
        {name: "null", content: ""},
        {name: "zero", gen: () => "^@".repeat(64) + "\n"},
        {name: "random", gen: randomBytes},
        {name: "urandom", gen: randomBytes},
        {name: "tty", gen: () => "It's you.\n"},
        {name: "wasm0", binary: true, content: wasmHeader.slice(0, 4)},
    ],
    "/etc": [
        {name: "fstab", content: fstab},
        {name: "group", content: groupFile},
        {name: "hostname", content: "targa.me\n"},
        {name: "hosts", content: hosts},
        {name: "motd", content: motd},
        {name: "os-release", content: osRelease},
        {name: "passwd", content: passwd},
        {name: "resolv.conf", content: resolvConf},
        {name: "shadow", denied: true, content: "root:$6$nice$try:19999:0:99999:7:::\n"},
    ],
    "/home": [{name: "guest", dir: true}],
    "/home/guest": [
        {name: "projects", dir: true}, {name: "about.txt", html: true, content: aboutMe},
        {name: "contact.txt", html: true, content: contactInfo}, {name: ".bashrc", content: bashrc},
        {name: ".bash_history", content: bashHistory}, {name: ".secret", content: secretFile},
        {name: ".ssh", dir: true},
    ],
    "/home/guest/.ssh": [
        {name: "authorized_keys", content: authorizedKeys}, {name: "id_rsa", content: idRSA},
        {name: "known_hosts", content: knownHosts},
    ],
    "/home/guest/projects/targascript": tgExamples(),
    "/home/guest/projects": [
        {name: "README.md", html: true, content: projectsIndex},
        {name: "targascript", dir: true},
        ...projectList.map(p => {name: p.file, html: true, content: project(p.name, p.url, p.stack, p.desc)}),
    ],
    "/proc": [
        {name: "cpuinfo", gen: procCPUInfo},
        {name: "meminfo", gen: procMemInfo},
        {name: "uptime", gen: () => format("%.2f %.2f\n", uptimeSeconds(), uptimeSeconds() * 0.9)},
        {name: "version", content: procVersion},
    ],
    "/root": [{name: "flag.txt", denied: true, content: "Not here. Try harder.\n"}],
    "/tmp": [],
    "/usr": [{name: "bin", dir: true, link: "/bin"}, {name: "games", dir: true}, {name: "local", dir: true}, {name: "share", dir: true}],
    "/usr/bin": [],
    "/usr/games": [],
    "/usr/local": [{name: "bin", dir: true}],
    "/usr/local/bin": [{name: "tsh", exec: true, content: "#!/bin/tsh\n# the targa shell. you're soaking in it.\n"}],
    "/usr/share": [{name: "cowsay", dir: true}, {name: "fortunes", dir: true}],
    "/usr/share/cowsay": [{name: "default.cow", content: cowFile}],
    "/usr/share/fortunes": [{name: "quotes", gen: () => fortunes.join("\n%\n") + "\n"}],
    "/var": [{name: "log", dir: true}, {name: "www", dir: true}],
    "/var/log": [{name: "auth.log", content: authLog}, {name: "syslog", content: syslog}],
    "/var/www": [{name: "index.html", content: wwwIndex}, {name: "targa.me", dir: true, denied: true}],
    "/bin": [],
}

// populateBin fills /bin and /usr/games from the command table
fn populateBin() {
    let bin = []
    let games = []
    repeat c in commands {
        if c.hidden { continue }
        let e = {name: c.name, exec: true, content: "#!/bin/tsh\n# " + c.name + ": " + c.desc + "\n"}
        if c.cat == "fun" { games.insert(e) } else { bin.insert(e) }
    }
    tree["/bin"] = bin.sortBy(e => e.name)
    tree["/usr/bin"] = tree["/bin"]
    tree["/usr/games"] = games.sortBy(e => e.name)
}

// resolve turns what the visitor typed into a normalized absolute path
fn resolve(p) {
    let stack = []
    let rest = p
    if p == "~" || p.startsWith("~/") {
        stack = home.slice(1).split("/")
        rest = p.slice(1)
    } else if !p.startsWith("/") && cwd != "/" {
        stack = cwd.slice(1).split("/")
    }
    repeat seg in rest.split("/") {
        match seg {
            "", "." => null
            ".." => if stack.length > 0 { stack.pop() }
            _ => stack.insert(seg)
        }
    }
    "/" + stack.join("/")
}

// display shortens the home directory to ~ for prompts and messages
fn display(p) {
    if p == home { return "~" }
    if p.startsWith(home + "/") { return "~" + p.slice(home.length) }
    p
}

// splitPath splits a path at its last slash: [dir, base]
fn splitPath(p) {
    let i = lastIndex(p, "/")
    if i < 0 { return ["", p] }
    [p.slice(0, i), p.slice(i + 1)]
}

// find looks up an absolute path, returning its entry or null
fn find(path) {
    if path == "/" { return {name: "/", dir: true} }
    let [parent, base] = splitPath(path)
    (tree[if parent == "" { "/" } else { parent }] ?? []).find(e => e.name == base)
}

// denied reports whether the visitor may not read a path. After the
// break-in (breakin.tg) the visitor is root and may read everything.
fn denied(path, e) {
    if root { return false }
    (e.denied ?? false) || path == "/root" || path.startsWith("/root/") || path.startsWith("/var/www/targa.me/")
}

fn children(path) {
    let e = find(path)
    if e != null && e.link != null {
        return tree[e.link] ?? []
    }
    tree[path] ?? []
}

// text is a file's content as plain text, without HTML
fn text(e) {
    let c = if e.gen != null { e.gen() } else { e.content ?? "" }
    if e.html { stripTags(c) } else { c }
}

// rendered is a file's content ready to put in the terminal
fn rendered(e) {
    if e.html { return trimNewlines(e.content) }
    escapeHtml(text(e).trimEnd())
}

fn trimNewlines(s) {
    let out = s
    repeat out.startsWith("\n") { out = out.slice(1) }
    repeat out.endsWith("\n") { out = out.slice(0, -1) }
    out
}

fn fileSize(e) { bytes(text(e)).length }

// walk visits every path under root, depth first, in tree order
fn walk(root, visit) {
    fn visitDir(dir, depth) {
        repeat e in children(dir) {
            let p = if dir == "/" { "/" + e.name } else { dir + "/" + e.name }
            visit(p, e, depth)
            if e.dir && e.link == null && !denied(p, e) {
                visitDir(p, depth + 1)
            }
        }
    }
    visitDir(root, 0)
}

fn randomBytes() {
    let glyphs = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~▒░▓█".chars()
    let out = ""
    repeat i in 0..255 {
        out += glyphs[rand(glyphs.length)]
        if i % 64 == 63 { out += "\n" }
    }
    out
}

fn procCPUInfo() {
    let out = ""
    repeat i in 0..cores() - 1 {
        out += "processor\t: ${i}\nvendor_id\t: WebAssembly\nmodel name\t: Virtual wasm32 CPU @ 1337 MHz\ncache size\t: 64 KB\nflags\t\t: simd128 bulk-memory easter-eggs\n\n"
    }
    out
}

fn procMemInfo() {
    "MemTotal:       16384 kB\nMemFree:        12288 kB\nMemAvailable:   12288 kB\nWasmMemory:     16384 kB\nSwapTotal:          0 kB\n"
}

// ── anim.tg ─────────────────────────────────────────────────────────────

// anim.tg — animations: anything that keeps writing to the screen after the
// command returned. Only one runs at a time. While busy, Enter, Escape and
// Ctrl+C call the animation's cancel function (null means it can't be
// interrupted, like sl).

let busy = false
let animBuffer = "" // shown after terminalContent, replaced every frame
let timers = []
let ticker = null
let onCancel = null
let cancelKeys = "all" // which keys cancel: "all" or "ctrl-c"

fn beginAnim(cancel, keys) {
    busy = true
    onCancel = cancel
    cancelKeys = keys
    inputField.value = ""
}

fn endAnim() {
    repeat t in timers { js.clearTimeout(t) }
    timers = []
    if ticker != null {
        js.clearInterval(ticker)
        ticker = null
    }
    animBuffer = ""
    busy = false
    onCancel = null
    inputLine.style.display = ""
    render()
    inputField.focus()
}

// interrupt is called by the keyboard handler and reports whether it used
// the key
fn interrupt(key, ctrl) {
    if !busy { return false }
    let isCtrlC = ctrl && key == "c"
    if !isCtrlC && !(cancelKeys == "all" && (key == "Enter" || key == "Escape")) {
        return true // other keys are swallowed while busy
    }
    if onCancel == null {
        return true // can't be interrupted
    }
    let cancel = onCancel
    endAnim()
    cancel()
    render()
    true
}

fn after(ms, f) {
    timers.insert(js.setTimeout(() => { f() }, ms))
}

fn every(ms, f) {
    ticker = js.setInterval(() => { f() }, ms)
}

// sequence adds text to the terminal step by step, each {delay, text} after
// the one before. Ctrl+C stops it and prints ^C, like a long-running process.
fn sequence(steps, hideInput, done = null) {
    beginAnim(() => { terminalContent += "^C\n" }, "ctrl-c")
    if hideInput {
        inputLine.style.display = "none"
    }
    let t = 0
    repeat i, s in steps {
        let last = i == steps.length - 1
        t += s.delay
        after(t, () => {
            terminalContent += s.text
            render()
            if last {
                endAnim()
                if done != null {
                    done()
                    render()
                }
            }
        })
    }
}

// frames runs a loop of frames: frame(i) returns [text, done]. tail goes to
// the transcript when the animation ends.
fn frames(ms, cancellable, frame, tail) {
    beginAnim(if cancellable { () => null } else { null }, "all")
    let i = 0
    every(ms, () => {
        let [text, done] = frame(i)
        i += 1
        animBuffer = text
        render()
        if done {
            endAnim()
            terminalContent += tail
            render()
        }
    })
}

// typewriter shows HTML a few characters at a time; tags appear at once so
// markup never shows half typed. Any cancel key completes it.
fn typewriter(html) {
    let chars = html.chars()
    let pos = 0
    let shown = ""
    beginAnim(() => { terminalContent += html + "\n" }, "all")
    every(8, () => {
        let n = 0
        repeat n < 3 && pos < chars.length {
            if chars[pos] == "<" {
                let end = pos
                repeat end < chars.length && chars[end] != ">" { end += 1 }
                if end < chars.length { end += 1 }
                shown += chars.slice(pos, end).join("")
                pos = end
                continue
            }
            shown += chars[pos]
            pos += 1
            n += 1
        }
        animBuffer = shown
        render()
        if pos >= chars.length {
            endAnim()
            terminalContent += html + "\n"
            render()
        }
    })
}

// ── commands.tg ─────────────────────────────────────────────────────────

// commands.tg — the list of the commands that help and help -a show, and
// the general ones: help, man, theme and exit. The others are in commands/,
// one file per category.

// The time the terminal started, in Unix seconds (Go's bootTime in main.go)
let bootTime = now()

let coreCommands = [
    // core
    {name: "help", usage: "[-a]", desc: "List commands (-a for all)", cat: "core", run: cmdHelp},
    {name: "about", desc: "Learn more about me", cat: "core", run: fn(c) {
        typewriter(trimNewlines(aboutMe))
        ""
    }},
    {name: "projects", desc: "Things I've built", cat: "core", run: fn(c) { projectsIndex }},
    {name: "contact", desc: "How to reach me", cat: "core", run: fn(c) { contactInfo }},
    {name: "ai", usage: "[-y] <question>", desc: "Ask a small LLM that runs in your browser", cat: "core", run: cmdAI},
    {name: "claude", usage: "[question]", desc: "Chat with it, Claude Code style", cat: "core", run: cmdClaude},
    {name: "clear", desc: "Clear the screen", cat: "core", run: fn(c) {
        terminalContent = ""
        ""
    }},
    {name: "man", usage: "<cmd>", desc: "Manual page for a command", cat: "core", run: cmdMan},
    {name: "theme", usage: "[name]", desc: "Change the terminal color", cat: "core", run: cmdTheme},
    {name: "exit", desc: "Close the session", cat: "core", run: cmdExit},

    // files
    {name: "ls", usage: "[-la] [path]", desc: "List directory contents", cat: "files", run: cmdLs},
    {name: "cd", usage: "[dir]", desc: "Change directory", cat: "files", run: cmdCd},
    {name: "pwd", desc: "Print working directory", cat: "files", run: fn(c) { cwd }},
    {name: "cat", usage: "<file>", desc: "Print files", cat: "files", run: cmdCat},
    {name: "tree", usage: "[dir]", desc: "Directory tree", cat: "files", run: cmdTree},
    {name: "find", usage: "[dir] [-name x]", desc: "Find files", cat: "files", run: cmdFind},
    {name: "file", usage: "<path>", desc: "Identify file type", cat: "files", run: cmdFile},
    {name: "stat", usage: "<path>", desc: "File status", cat: "files", run: cmdStat},
    {name: "touch", usage: "<file>", desc: "Create a file", cat: "files", run: readOnly("touch")},
    {name: "mkdir", usage: "<dir>", desc: "Create a directory", cat: "files", run: readOnly("mkdir")},
    {name: "rm", usage: "<path>", desc: "Remove files", cat: "files", run: cmdRm},
    {name: "mv", usage: "<a> <b>", desc: "Move files", cat: "files", run: readOnly("mv")},
    {name: "cp", usage: "<a> <b>", desc: "Copy files", cat: "files", run: readOnly("cp")},
    {name: "chmod", usage: "<mode> <path>", desc: "Change permissions", cat: "files", run: readOnly("chmod")},
    {name: "chown", usage: "<owner> <path>", desc: "Change owner", cat: "files", run: readOnly("chown")},
    {name: "ln", usage: "<a> <b>", desc: "Link files", cat: "files", run: readOnly("ln")},
    {name: "du", usage: "[path]", desc: "Disk usage", cat: "files", run: cmdDu},
    {name: "df", desc: "Filesystem usage", cat: "files", run: cmdDf},
    {name: "mount", desc: "Mounted filesystems", cat: "files", run: fn(c) {
        "/dev/wasm0 on / type wasmfs (ro,relatime)\ntmpfs on /tmp type tmpfs (rw,nosuid)\nproc on /proc type proc (rw,nosuid,nodev,noexec)"
    }},

    // text
    {name: "echo", usage: "<text>", desc: "Print text", cat: "text", run: cmdEcho},
    {name: "printf", usage: "<text>", desc: "Print text", cat: "text", run: fn(c) {
        escapeHtml(c.args.join(" ").replace(`\n`, "\n"))
    }},
    {name: "head", usage: "[-n N] <file>", desc: "First lines of a file", cat: "text", run: cmdHead},
    {name: "tail", usage: "[-n N] <file>", desc: "Last lines of a file", cat: "text", run: cmdTail},
    {name: "wc", usage: "<file>", desc: "Count lines, words, bytes", cat: "text", run: cmdWc},
    {name: "grep", usage: "[-i] <pattern> <file>", desc: "Search text", cat: "text", run: cmdGrep},
    {name: "sort", usage: "[-r] <file>", desc: "Sort lines", cat: "text", run: cmdSort},
    {name: "uniq", usage: "<file>", desc: "Drop repeated lines", cat: "text", run: cmdUniq},
    {name: "rev", usage: "<file>", desc: "Reverse each line", cat: "text", run: cmdRev},
    {name: "tac", usage: "<file>", desc: "Reverse line order", cat: "text", run: cmdTac},
    {name: "nl", usage: "<file>", desc: "Number lines", cat: "text", run: cmdNl},
    {name: "strings", usage: "<file>", desc: "Printable strings in a file", cat: "text", run: cmdStrings},
    {name: "xxd", usage: "<file>", desc: "Hex dump", cat: "text", run: cmdXxd},
    {name: "base64", usage: "[-d] <text|file>", desc: "Encode or decode base64", cat: "text", run: cmdBase64},
    {name: "md5sum", usage: "<file>", desc: "MD5 checksum", cat: "text", run: cmdHash("md5sum")},
    {name: "sha256sum", usage: "<file>", desc: "SHA-256 checksum", cat: "text", run: cmdHash("sha256sum")},
    {name: "seq", usage: "<first> [last]", desc: "Print a sequence", cat: "text", run: cmdSeq},
    {name: "bc", usage: "<expression>", desc: "Calculator", cat: "text", run: cmdBc},
    {name: "yes", usage: "[text]", desc: "Repeat until interrupted", cat: "text", run: cmdYes},

    // system
    {name: "whoami", desc: "Current user", cat: "system", run: fn(c) { env.USER }},
    {name: "id", desc: "User identity", cat: "system", run: fn(c) { if root { "uid=0(root) gid=0(root) groups=0(root)" } else { "uid=1000(guest) gid=1000(guest) groups=1000(guest)" } }},
    {name: "groups", desc: "User groups", cat: "system", run: fn(c) { env.USER }},
    {name: "hostname", desc: "System hostname", cat: "system", run: fn(c) { "targa.me" }},
    {name: "uname", usage: "[-a]", desc: "System information", cat: "system", run: cmdUname},
    {name: "uptime", desc: "Time since boot", cat: "system", run: cmdUptime},
    {name: "date", desc: "Current date and time", cat: "system", run: fn(c) { goTime(now(), "Mon Jan _2 15:04:05 2006") }},
    {name: "cal", desc: "Calendar", cat: "system", run: cmdCal},
    {name: "env", desc: "Environment variables", cat: "system", run: cmdEnv},
    {name: "export", usage: "NAME=value", desc: "Set a variable", cat: "system", run: cmdExport},
    {name: "alias", desc: "List aliases", cat: "system", run: cmdAlias},
    {name: "which", usage: "<cmd>", desc: "Locate a command", cat: "system", run: cmdWhich},
    {name: "type", usage: "<cmd>", desc: "Describe a command", cat: "system", run: cmdWhich},
    {name: "history", usage: "[-c]", desc: "Command history", cat: "system", run: cmdHistory},
    {name: "ps", desc: "Running processes", cat: "system", run: cmdPs},
    {name: "top", desc: "Process monitor", cat: "system", run: cmdTop},
    {name: "kill", usage: "<pid>", desc: "Terminate a process", cat: "system", run: cmdKill},
    {name: "free", desc: "Memory usage", cat: "system", run: fn(c) {
        "               total        used        free      shared\nMem:           16384        4096       12288           0\nSwap:              0           0           0"
    }},
    {name: "lscpu", desc: "CPU information", cat: "system", run: cmdLscpu},
    {name: "dmesg", desc: "Kernel messages", cat: "system", run: cmdDmesg},
    {name: "neofetch", desc: "System summary with style", cat: "system", run: cmdNeofetch},
    {name: "who", desc: "Logged-in users", cat: "system", run: fn(c) {
        "guest    pts/0        " + goTime(bootTime, "2006-01-02 15:04") + " (127.0.0.1)"
    }},
    {name: "w", desc: "Who is doing what", cat: "system", run: fn(c) {
        " " + goTime(now(), "15:04:05") + " up " + prettyUptime() + ",  1 user\nUSER     TTY      FROM         LOGIN@   WHAT\nguest    pts/0    127.0.0.1    " + goTime(bootTime, "15:04") + "    w"
    }},
    {name: "last", desc: "Login history", cat: "system", run: fn(c) {
        "guest    pts/0    127.0.0.1    " + goTime(bootTime, "Mon Jan _2 15:04") + "   still logged in\nroot     pts/1    10.0.0.66    Thu Sep 11 02:14 - 02:14  (00:00)  <- failed, see /var/log/auth.log"
    }},
    {name: "sleep", usage: "<seconds>", desc: "Wait", cat: "system", run: cmdSleep},
    {name: "time", usage: "<command>", desc: "Time a command", cat: "system", run: cmdTime},
    {name: "true", desc: "Succeed", cat: "system", run: fn(c) { "" }},
    {name: "false", desc: "Fail", cat: "system", run: fn(c) {
        lastStatus = 1
        ""
    }},
    {name: "reboot", desc: "Reload the page", cat: "system", run: cmdReboot},
    {name: "shutdown", desc: "Power off", cat: "system", run: cmdShutdown},

    // network
    {name: "ping", usage: "<host>", desc: "Send ICMP echo requests", cat: "network", run: cmdPing},
    {name: "ifconfig", desc: "Network interfaces", cat: "network", run: fn(c) {
        "lo: flags=73<UP,LOOPBACK,RUNNING>  mtu 65536\n        inet 127.0.0.1  netmask 255.0.0.0\nwasm0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500\n        inet 10.0.0.1  netmask 255.255.255.0\n        ether 00:de:ad:be:ef:00"
    }},
    {name: "netstat", desc: "Network connections", cat: "network", run: fn(c) {
        "Proto  Local Address      Foreign Address    State\ntcp    0.0.0.0:22         0.0.0.0:*          LISTEN\ntcp    0.0.0.0:443        0.0.0.0:*          LISTEN\ntcp    127.0.0.1:31337    10.0.0.66:4444     ESTABLISHED"
    }},
    {name: "nmap", usage: "<host>", desc: "Port scan", cat: "network", run: cmdNmap},
    {name: "traceroute", usage: "<host>", desc: "Trace the route to a host", cat: "network", run: cmdTraceroute},
    {name: "dig", usage: "<host>", desc: "DNS lookup", cat: "network", run: cmdDig},
    {name: "nslookup", usage: "<host>", desc: "DNS lookup", cat: "network", run: cmdDig},
    {name: "whois", usage: "<domain>", desc: "Domain registration", cat: "network", run: cmdWhois},
    {name: "curl", usage: "<url>", desc: "Fetch a URL", cat: "network", run: cmdCurl},
    {name: "wget", usage: "<url>", desc: "Download a URL", cat: "network", run: fn(c) {
        errf("wget", "unable to resolve host address '" + c.arg(0) + "'. This box has no uplink; try curl targa.me")
    }},
    {name: "ssh", usage: "<user@host>", desc: "Remote login", cat: "network", run: cmdSSH},

    // languages
    {name: "targascript", usage: "[file.tg | -e code]", desc: "Run TargaScript, my own language", cat: "lang", run: cmdTargaScript},

    // fun (listed, lives in /usr/games)
    {name: "fortune", desc: "Random wisdom", cat: "fun", run: fn(c) { escapeHtml(fortunes[rand(fortunes.length)]) }},
    {name: "cowsay", usage: "<text>", desc: "A cow says things", cat: "fun", run: cmdCowsay},
    {name: "lolcat", desc: "Rainbow text (pipe into it)", cat: "fun", run: cmdLolcat},
    {name: "sl", desc: "Steam locomotive", cat: "fun", run: cmdSl},
    {name: "cmatrix", desc: "Digital rain", cat: "fun", run: cmdCmatrix},
    {name: "figlet", usage: "<text>", desc: "Big letters", cat: "fun", run: cmdFiglet},
    {name: "banner", desc: "Show the logo again", cat: "fun", run: fn(c) { trimRightChars(banner, "\n") }},
]

let catLabels = [
    {key: "core", label: "General"}, {key: "files", label: "Files"}, {key: "text", label: "Text"},
    {key: "system", label: "System"}, {key: "network", label: "Network"}, {key: "lang", label: "Languages"},
    {key: "fun", label: "Fun"},
]

// ---------------------------------------------------------------------------
// General
// ---------------------------------------------------------------------------

fn cmdHelp(c) {
    let b = ""
    if c.flag("a") || c.has("all") {
        repeat cat in catLabels {
            b += green(cat.label) + "\n"
            repeat cmd in commands {
                if cmd.cat == cat.key && !cmd.hidden {
                    b += helpRow(cmd)
                }
            }
            b += "\n"
        }
        b += dim("Pipes, && and ; work: fortune | cowsay | lolcat. Variables too: echo $USER.\nThere are easter eggs. ls -a is a good start.")
        return b
    }
    b += green("Available commands:") + "\n"
    repeat cmd in commands {
        if cmd.cat == "core" || cmd.cat == "lang" || cmd.name == "ls" || cmd.name == "cd" || cmd.name == "cat" {
            b += helpRow(cmd)
        }
    }
    b += "\n" + dim("This is a small Linux-like shell: try help -a for " + itoa(visibleCount()) + " commands.\nTab completes, Up/Down browse history, Ctrl+C interrupts, Enter skips animations.")
    b
}

fn helpRow(cmd) {
    let label = cmd.name
    if (cmd.usage ?? "") != "" {
        label += " " + cmd.usage
    }
    if columns() < 64 {
        return "  " + yellow(escapeHtml(label)) + "\n      " + dim(cmd.desc) + "\n"
    }
    "  " + yellow(escapeHtml(pad(label, 24))) + dim(cmd.desc) + "\n"
}

fn visibleCount() { commands.count(c => !c.hidden) }

fn cmdMan(c) {
    let name = c.arg(0)
    if name == "" {
        return "What manual page do you want?\nFor example, try 'man ls'."
    }
    if name in aliases {
        name = fields(aliases[name])[0]
    }
    let cmd = commandIndex[name]
    if cmd == null || cmd.hidden {
        if name == "woman" {
            return "No manual entry for woman. (Yes, that is the classic one.)"
        }
        return "No manual entry for " + escapeHtml(name)
    }
    green("NAME") + "\n       " + cmd.name + " - " + cmd.desc + "\n\n"
    + green("SYNOPSIS") + "\n       " + escapeHtml(trimSpace(cmd.name + " " + (cmd.usage ?? ""))) + "\n\n"
    + green("DESCRIPTION") + "\n       " + cmd.desc + ". Part of tsh, the targa shell, which runs entirely in your browser as WebAssembly.\n\n"
    + green("SEE ALSO") + "\n       help(1), help -a"
}

let themes = {
    green: "#0f0", amber: "#ffb000", white: "#e6e6e6", cyan: "#00e5ff", pink: "#ff6ec7", red: "#ff4444", purple: "#c084fc",
}

fn cmdTheme(c) {
    let name = c.arg(0)
    if name == "" {
        let names = themes.keys().sort()
        return "usage: theme <name>\navailable: " + names.join(", ")
    }
    if !(name in themes) {
        return errf("theme", "unknown theme '" + name + "'")
    }
    doc.documentElement.style.setProperty("--fg", themes[name])
    dim("theme set to " + name)
}

fn cmdExit(c) {
    if root { return leaveRoot() }
    mode = "closed"
    closedMessage = "logout\nConnection to targa.me closed.\n" + dim("Press Enter to reconnect.")
    after(400, () => {
        terminalContent += closedMessage + "\n"
        render()
    })
    ""
}

// ── commands/files.tg ───────────────────────────────────────────────────

// commands/files.tg — ls, cd, cat, tree, find and the other commands for
// the virtual filesystem, which is read-only.

fn readOnly(name) {
    fn(c) {
        let target = c.arg(0)
        if name == "chmod" || name == "chown" || name == "mv" || name == "cp" || name == "ln" {
            target = c.arg(1)
            if target == "" {
                target = c.arg(0)
            }
        }
        if target == "" {
            lastStatus = 1
            return errf(name, "missing operand")
        }
        lastStatus = 1
        errf(name, "cannot touch '" + target + "': Read-only file system")
    }
}

fn cmdLs(c) {
    let target = c.arg(0)
    if target == "" {
        target = "."
    }
    let path = resolve(target)
    let e = find(path)
    if e == null {
        lastStatus = 2
        return errf("ls", "cannot access '" + target + "': No such file or directory")
    }
    if denied(path, e) {
        lastStatus = 2
        return errf("ls", "cannot open directory '" + target + "': Permission denied")
    }
    let long = c.flag("l")
    let showAll = c.flag("a")
    if !e.dir {
        if long {
            return longRow(path, e)
        }
        return escapeHtml(target)
    }
    let entries = children(path)
    let rows = []
    if showAll {
        entries = [{name: ".", dir: true}, {name: "..", dir: true}] + entries
    }
    repeat k in entries {
        if !showAll && k.name.startsWith(".") {
            continue
        }
        if long {
            rows.insert(longRow(path + "/" + k.name, k))
        } else {
            rows.insert(colorName(k))
        }
    }
    if long {
        return "total " + itoa(rows.length) + "\n" + rows.join("\n")
    }
    rows.join("  ")
}

fn colorName(e) {
    if e.dir { return `<span class="dir">` + e.name + `</span>` }
    if e.exec { return `<span class="exe">` + e.name + `</span>` }
    if e.denied { return `<span class="red">` + e.name + `</span>` }
    e.name
}

fn longRow(path, e) {
    let perms = "-rw-r--r--"
    let owner = "guest guest"
    if e.dir && e.link != null {
        perms = "lrwxrwxrwx"
    } else if e.dir {
        perms = "drwxr-xr-x"
    } else if e.exec {
        perms = "-rwxr-xr-x"
        owner = "root  root "
    }
    if denied(path, e) {
        perms = perms.slice(0, 1) + "rw-------"
        if e.dir {
            perms = "drwx------"
        }
        owner = "root  root "
    }
    if !path.startsWith(home) {
        owner = "root  root "
    }
    let size = 4096
    if !e.dir {
        size = fileSize(e)
    }
    let name = colorName(e)
    if e.link != null {
        name += " -> " + e.link
    }
    perms + " 1 " + owner + " " + lpad(itoa(size), 6) + " " + goTime(bootTime, "Jan _2 15:04") + " " + name
}

fn cmdCd(c) {
    let target = c.arg(0)
    if target == "" {
        target = "~"
    }
    if target == "-" {
        if (env.OLDPWD ?? "") == "" {
            return errf("cd", "OLDPWD not set")
        }
        target = env.OLDPWD
    }
    let path = resolve(target)
    let e = find(path)
    if e == null {
        lastStatus = 1
        return errf("cd", target + ": No such file or directory")
    }
    if !e.dir {
        lastStatus = 1
        return errf("cd", target + ": Not a directory")
    }
    if denied(path, e) {
        lastStatus = 1
        return errf("cd", target + ": Permission denied")
    }
    if e.link != null {
        path = e.link
    }
    env.OLDPWD = cwd
    cwd = path
    env.PWD = cwd
    ""
}

fn cmdCat(c) {
    if c.arg(0) == "" {
        if c.piped {
            return escapeHtml(trimRightChars(c.stdin, "\n"))
        }
        return errf("cat", "missing file operand")
    }
    let out = []
    repeat a in c.args {
        if a.startsWith("-") {
            continue
        }
        let path = resolve(a)
        let e = find(path)
        if e == null {
            lastStatus = 1
            out.insert(errf("cat", a + ": No such file or directory"))
        } else if e.dir {
            lastStatus = 1
            out.insert(errf("cat", a + ": Is a directory"))
        } else if denied(path, e) {
            lastStatus = 1
            out.insert(errf("cat", a + ": Permission denied"))
        } else if e.binary {
            out.insert(errf("cat", a + ": binary file. Try xxd or strings."))
        } else {
            out.insert(rendered(e))
        }
    }
    out.join("\n")
}

fn cmdTree(c) {
    let target = c.arg(0)
    if target == "" {
        target = "."
    }
    let root = resolve(target)
    let e = find(root)
    if e == null || !e.dir {
        lastStatus = 1
        return errf("tree", target + ": No such directory")
    }
    if denied(root, e) {
        lastStatus = 1
        return errf("tree", target + ": Permission denied")
    }
    let b = `<span class="dir">` + escapeHtml(display(root)) + `</span>` + "\n"
    let dirs = 0
    let files = 0
    fn rec(dir, indent) {
        let kids = children(dir)
        repeat i, k in kids {
            if k.name.startsWith(".") && !c.flag("a") {
                continue
            }
            let branch = "├── "
            let next = "│   "
            if i == kids.length - 1 {
                branch = "└── "
                next = "    "
            }
            b += indent + branch + colorName(k) + "\n"
            let p = if dir == "/" { "/" + k.name } else { dir + "/" + k.name }
            if k.dir {
                dirs += 1
                if k.link == null && !denied(p, k) {
                    rec(p, indent + next)
                }
            } else {
                files += 1
            }
        }
    }
    rec(root, "")
    b += "\n" + itoa(dirs) + " directories, " + itoa(files) + " files"
    b
}

fn cmdFind(c) {
    let root = "."
    let pattern = ""
    let i = 0
    repeat i < c.args.length {
        if c.args[i] == "-name" && i + 1 < c.args.length {
            pattern = trimChars(c.args[i + 1], "*")
            i += 1
        } else if !c.args[i].startsWith("-") {
            root = c.args[i]
        }
        i += 1
    }
    let abs = resolve(root)
    let e = find(abs)
    if e == null || !e.dir {
        lastStatus = 1
        return errf("find", "'" + root + "': No such file or directory")
    }
    let out = []
    walk(abs, (p, k, depth) => {
        if pattern != "" && !k.name.contains(pattern) {
            return null
        }
        let shown = p
        if abs == cwd && root == "." {
            shown = "." + (if p.startsWith(abs) { p.slice(abs.length) } else { p })
        }
        out.insert(escapeHtml(shown))
    })
    if out.length == 0 {
        return ""
    }
    out.join("\n")
}

fn cmdFile(c) {
    let target = c.arg(0)
    if target == "" {
        return errf("file", "missing file operand")
    }
    let path = resolve(target)
    let e = find(path)
    if e == null {
        lastStatus = 1
        return errf("file", target + ": cannot open (No such file or directory)")
    }
    if e.dir { return escapeHtml(target) + ": directory" }
    if e.binary { return escapeHtml(target) + ": WebAssembly (wasm) binary module version 0x1 (MVP)" }
    if e.exec { return escapeHtml(target) + ": tsh script, ASCII text executable" }
    if e.name.endsWith(".md") { return escapeHtml(target) + ": Markdown text, UTF-8 Unicode" }
    if e.name.endsWith(".tg") {
        return escapeHtml(target) + ": TargaScript source, ASCII text (run it: targascript " + escapeHtml(target) + ")"
    }
    if e.html { return escapeHtml(target) + ": UTF-8 Unicode text, with very long lines" }
    escapeHtml(target) + ": ASCII text"
}

fn cmdStat(c) {
    let target = c.arg(0)
    if target == "" {
        return errf("stat", "missing operand")
    }
    let path = resolve(target)
    let e = find(path)
    if e == null {
        lastStatus = 1
        return errf("stat", "cannot stat '" + target + "': No such file or directory")
    }
    let kind = "regular file"
    if e.dir {
        kind = "directory"
    }
    "  File: " + escapeHtml(target) + "\n  Size: " + itoa(fileSize(e)) + "\tBlocks: 8\tIO Block: 4096\t" + kind
    + "\nDevice: wasm0\tInode: " + itoa(1000 + bytes(path).length * 7) + "\tLinks: 1\nAccess: " + fields(longRow(path, e))[0]
    + "\nModify: " + goTime(bootTime, "2006-01-02 15:04:05")
}

fn cmdRm(c) {
    let target = c.arg(0)
    if target == "" {
        return errf("rm", "missing operand")
    }
    let recursive = c.flag("r") || c.flag("R") || c.has("--recursive")
    if (target == "/" || target == "/*") && recursive {
        if !c.has("--no-preserve-root") {
            lastStatus = 1
            return errf("rm", "it is dangerous to operate recursively on '/'") + "\n" + errf("rm", "use --no-preserve-root to override this failsafe")
        }
        let steps = []
        repeat d in ["/bin", "/boot", "/etc", "/home/guest/projects", "/home/guest/about.txt", "/usr", "/var/log"] {
            steps.insert({delay: 120, text: "removing '" + d + "'\n"})
        }
        steps.insert({delay: 600, text: `<span class="red">rm: cannot remove '/': Device or resource busy</span>` + "\n"})
        steps.insert({delay: 800, text: "Just kidding. The filesystem is read-only and lives in your browser tab. Everything is fine.\n"})
        sequence(steps, false, null)
        return ""
    }
    let path = resolve(target)
    let e = find(path)
    if e == null {
        lastStatus = 1
        return errf("rm", "cannot remove '" + target + "': No such file or directory")
    }
    if e.dir && !recursive {
        lastStatus = 1
        return errf("rm", "cannot remove '" + target + "': Is a directory")
    }
    if denied(path, e) {
        lastStatus = 1
        return errf("rm", "cannot remove '" + target + "': Permission denied")
    }
    lastStatus = 1
    errf("rm", "cannot remove '" + target + "': Read-only file system")
}

fn cmdDu(c) {
    let target = c.arg(0)
    if target == "" {
        target = "."
    }
    let root = resolve(target)
    let e = find(root)
    if e == null {
        lastStatus = 1
        return errf("du", "cannot access '" + target + "': No such file or directory")
    } else if !e.dir {
        return itoa((fileSize(e) + 1023) / 1024) + "\t" + escapeHtml(target)
    }
    let total = 0
    walk(root, (p, k, depth) => {
        if !k.dir {
            total += fileSize(k)
        }
    })
    itoa((total + 1023) / 1024) + "\t" + escapeHtml(target)
}

fn cmdDf(c) {
    let total = 0
    walk("/", (p, e, depth) => {
        if !e.dir {
            total += fileSize(e)
        }
    })
    let used = itoa((total + 1023) / 1024)
    "Filesystem     1K-blocks  Used Available Use% Mounted on\n/dev/wasm0         16384  " + lpad(used, 4) + "     " + lpad(itoa(16384 - (total + 1023) / 1024), 5) + "   1% /\ntmpfs               1024     0      1024   0% /tmp"
}

// ── commands/text.tg ────────────────────────────────────────────────────

// commands/text.tg — echo, head, grep, sort, base64, the checksums, bc
// and the other commands that read and print text.

fn lines(s) {
    let t = trimRightChars(s, "\n")
    if t == "" {
        return []
    }
    t.split("\n")
}

// nFlag reads -n N, -nN or -N: [n, the other arguments]
fn nFlag(c, def) {
    let n = def
    let rest = []
    let i = 0
    repeat i < c.args.length {
        let a = c.args[i]
        if a == "-n" && i + 1 < c.args.length {
            n = atoi(c.args[i + 1])[0]
            i += 1
        } else if a.startsWith("-n") {
            n = atoi(a.slice(2))[0]
        } else if a.startsWith("-") && a.length > 1 {
            let [v, ok] = atoi(a.slice(1))
            if ok {
                n = v
            }
        } else {
            rest.insert(a)
        }
        i += 1
    }
    [n, rest]
}

fn cmdEcho(c) {
    let words = c.args
    let escapes = false
    repeat words.length > 0 && (words[0] == "-n" || words[0] == "-e" || words[0] == "-ne" || words[0] == "-en") {
        escapes = escapes || words[0].contains("e")
        words = words.slice(1)
    }
    let out = words.join(" ")
    if escapes {
        out = out.replace(`\n`, "\n").replace(`\t`, "\t")
    }
    escapeHtml(out)
}

fn cmdHead(c) {
    let [n, rest] = nFlag(c, 10)
    c.args = rest
    let [src, err] = c.input("head", 0)
    if err != "" {
        return err
    }
    let l = lines(src)
    if l.length > n {
        l = l.slice(0, n)
    }
    escapeHtml(l.join("\n"))
}

fn cmdTail(c) {
    let [n, rest] = nFlag(c, 10)
    c.args = rest
    let [src, err] = c.input("tail", 0)
    if err != "" {
        return err
    }
    let l = lines(src)
    if l.length > n {
        l = l.slice(l.length - n)
    }
    escapeHtml(l.join("\n"))
}

fn cmdWc(c) {
    let [src, err] = c.input("wc", 0)
    if err != "" {
        return err
    }
    let name = c.arg(0)
    lpad(itoa(lines(src).length), 4) + lpad(itoa(fields(src).length), 5) + lpad(itoa(bytes(src).length), 6) + " " + escapeHtml(name)
}

fn cmdGrep(c) {
    let pattern = c.arg(0)
    if pattern == "" {
        return "usage: grep [-i] <pattern> <file>"
    }
    let [src, err] = c.input("grep", 1)
    if err != "" {
        return err
    }
    let ci = c.flag("i")
    let out = []
    repeat l in lines(src) {
        let hay = l
        let needle = pattern
        if ci {
            hay = l.lower()
            needle = pattern.lower()
        }
        let i = hay.indexOf(needle)
        if i >= 0 {
            let end = i + needle.length
            out.insert(escapeHtml(l.slice(0, i)) + `<span class="red">` + escapeHtml(l.slice(i, end)) + `</span>` + escapeHtml(l.slice(end)))
        }
    }
    if out.length == 0 {
        lastStatus = 1
    }
    out.join("\n")
}

fn cmdSort(c) {
    let [src, err] = c.input("sort", 0)
    if err != "" {
        return err
    }
    let l = lines(src)
    l.sort()
    if c.flag("r") {
        l.reverse()
    }
    escapeHtml(l.join("\n"))
}

fn cmdUniq(c) {
    let [src, err] = c.input("uniq", 0)
    if err != "" {
        return err
    }
    let out = []
    repeat l in lines(src) {
        if out.length == 0 || out[-1] != l {
            out.insert(l)
        }
    }
    escapeHtml(out.join("\n"))
}

fn cmdRev(c) {
    let [src, err] = c.input("rev", 0)
    if err != "" {
        return err
    }
    escapeHtml(lines(src).map(l => l.reverse()).join("\n"))
}

fn cmdTac(c) {
    let [src, err] = c.input("tac", 0)
    if err != "" {
        return err
    }
    let l = lines(src)
    l.reverse()
    escapeHtml(l.join("\n"))
}

fn cmdNl(c) {
    let [src, err] = c.input("nl", 0)
    if err != "" {
        return err
    }
    lines(src).map((l, i) => lpad(itoa(i + 1), 6) + "\t" + escapeHtml(l)).join("\n")
}

fn cmdStrings(c) {
    let [src, err] = c.input("strings", 0)
    if err != "" {
        return err
    }
    let out = []
    repeat l in src.split("\n") {
        let cur = ""
        repeat ch in l {
            if ch >= " " && ch <= "~" {
                cur += ch
            } else {
                if cur.length >= 4 {
                    out.insert(cur)
                }
                cur = ""
            }
        }
        if cur.length >= 4 {
            out.insert(cur)
        }
    }
    escapeHtml(out.join("\n"))
}

fn cmdXxd(c) {
    let [src, err] = c.input("xxd", 0)
    if err != "" {
        return err
    }
    let whole = bytes(src)
    let data = if whole.length > 256 { whole.slice(0, 256) } else { whole }
    let b = ""
    let off = 0
    repeat off < data.length {
        let end = min(off + 16, data.length)
        let chunk = data.slice(off, end)
        b += dim(zpad(format("%x", off), 8) + ":") + " "
        repeat i in 0..15 {
            if i < chunk.length {
                b += hexEncode(chunk.slice(i, i + 1))
            } else {
                b += "  "
            }
            if i % 2 == 1 {
                b += " "
            }
        }
        b += " "
        repeat ch in chunk {
            if ch >= 32 && ch < 127 {
                b += escapeHtml(bytes([ch]).text())
            } else {
                b += "."
            }
        }
        b += "\n"
        off += 16
    }
    if whole.length > 256 {
        b += dim("... (" + itoa(whole.length - 256) + " more bytes)")
    }
    trimRightChars(b, "\n")
}

let base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

// base64Encode is base64.StdEncoding.EncodeToString
fn base64Encode(data) {
    let digits = base64Alphabet.chars()
    let out = ""
    let i = 0
    repeat i < data.length {
        let n = data.length - i
        let v = data[i] << 16
        if n > 1 { v = v | data[i + 1] << 8 }
        if n > 2 { v = v | data[i + 2] }
        out += digits[v >> 18 & 63] + digits[v >> 12 & 63]
        out += if n > 1 { digits[v >> 6 & 63] } else { "=" }
        out += if n > 2 { digits[v & 63] } else { "=" }
        i += 3
    }
    out
}

// base64Decode is base64.StdEncoding.DecodeString: the bytes, or null when
// the input isn't valid (newlines are skipped, padding is required)
fn base64Decode(s) {
    let chars = s.chars().filter(ch => ch != "\n" && ch != "\r")
    if chars.length % 4 != 0 { return null }
    let out = []
    let i = 0
    repeat i < chars.length {
        let quad = chars.slice(i, i + 4)
        let last = i + 4 == chars.length
        let vals = []
        let pads = 0
        repeat j, ch in quad {
            if ch == "=" {
                if !last || j < 2 { return null }
                pads += 1
            } else {
                if pads > 0 { return null }
                let v = base64Alphabet.indexOf(ch)
                if v < 0 { return null }
                vals.insert(v)
            }
        }
        let v = 0
        repeat k in 0..3 { v = v << 6 | (vals[k] ?? 0) }
        out.insert(v >> 16 & 255)
        if pads < 2 { out.insert(v >> 8 & 255) }
        if pads < 1 { out.insert(v & 255) }
        i += 4
    }
    bytes(out)
}

fn cmdBase64(c) {
    let decode = c.flag("d") || c.has("--decode")
    let src = c.arg(0)
    let data = ""
    if src == "" && c.piped {
        data = trimRightChars(c.stdin, "\n")
    } else if src == "" {
        return "usage: base64 [-d] <text or file>"
    } else {
        let e = find(resolve(src))
        if e != null && !e.dir {
            data = trimRightChars(text(e), "\n")
        } else {
            data = argsWithoutFlags(c).join(" ")
        }
    }
    if decode {
        let out = base64Decode(trimSpace(data))
        if out == null {
            lastStatus = 1
            return errf("base64", "invalid input")
        }
        return escapeHtml(out.text())
    }
    base64Encode(bytes(data))
}

// argsWithoutFlags is every argument that isn't a flag (a Ctx method in Go)
fn argsWithoutFlags(c) {
    c.args.filter(a => !(a.startsWith("-") && a.length > 1))
}

fn cmdHash(name) {
    fn(c) {
        let [src, err] = c.input(name, 0)
        if err != "" {
            return err
        }
        let label = c.arg(0)
        if label == "" {
            label = "-"
        }
        if name == "md5sum" {
            return md5Hex(src) + "  " + escapeHtml(label)
        }
        sha256Hex(src) + "  " + escapeHtml(label)
    }
}

fn cmdSeq(c) {
    let [a, ok] = atoi(c.arg(0))
    if !ok {
        return "usage: seq <first> [last]"
    }
    let b = a
    if c.arg(1) != "" {
        b = atoi(c.arg(1))[0]
    } else {
        a = 1
    }
    let out = []
    let i = a
    repeat i <= b && out.length < 1000 {
        out.insert(itoa(i))
        i += 1
    }
    out.join("\n")
}

fn cmdYes(c) {
    let word = c.args.join(" ")
    if word == "" {
        word = "y"
    }
    let steps = []
    repeat i in 1..40 {
        steps.insert({delay: 40, text: escapeHtml(word) + "\n"})
    }
    steps.insert({delay: 200, text: dim("(yes would go on forever; I stopped it for you)") + "\n"})
    sequence(steps, false, null)
    ""
}

// satInt converts a float to an integer as Go's int64(v) does in the wasm
// build: truncating, saturating at the ends, and NaN gives 0
fn satInt(v) {
    if v != v { return 0 }
    if v >= 9223372036854775807.0 { return 9223372036854775807 }
    if v <= -9223372036854775808.0 { return -9223372036854775807 - 1 }
    toInt(v)
}

// ---------------------------------------------------------------------------
// bc: a tiny expression evaluator (+ - * / % ^ and parentheses)
// ---------------------------------------------------------------------------

struct exprParser {
    s = [], pos = 0, err = false

    // peek skips spaces and gives the next character, or "" at the end
    fn peek() {
        repeat this.pos < this.s.length && this.s[this.pos] == " " { this.pos += 1 }
        this.s[this.pos] ?? ""
    }

    fn expr() {
        let v = this.term()
        repeat true {
            match this.peek() {
                "+" => {
                    this.pos += 1
                    v += this.term()
                }
                "-" => {
                    this.pos += 1
                    v -= this.term()
                }
                _ => { return v }
            }
        }
    }

    fn term() {
        let v = this.factor()
        repeat true {
            match this.peek() {
                "*" => {
                    this.pos += 1
                    v *= this.factor()
                }
                "/" => {
                    this.pos += 1
                    let d = this.factor()
                    if d == 0 {
                        this.err = true
                        return 0.0
                    }
                    v /= d
                }
                "%" => {
                    this.pos += 1
                    let d = this.factor()
                    if d == 0 {
                        this.err = true
                        return 0.0
                    }
                    v = toFloat(satInt(v) % satInt(d))
                }
                _ => { return v }
            }
        }
    }

    fn factor() {
        let base = this.unary()
        if this.peek() == "^" {
            this.pos += 1
            let power = this.factor()
            let r = 1.0
            let i = 0
            repeat i < satInt(power) {
                r *= base
                i += 1
            }
            return r
        }
        base
    }

    fn unary() {
        match this.peek() {
            "-" => {
                this.pos += 1
                return -this.unary()
            }
            "(" => {
                this.pos += 1
                let v = this.expr()
                if this.peek() != ")" {
                    this.err = true
                    return 0.0
                }
                this.pos += 1
                return v
            }
            _ => null
        }
        let start = this.pos
        repeat this.pos < this.s.length && (this.s[this.pos] >= "0" && this.s[this.pos] <= "9" || this.s[this.pos] == ".") {
            this.pos += 1
        }
        if start == this.pos {
            this.err = true
            return 0.0
        }
        let [v, ok] = parseFloat(this.s.slice(start, this.pos).join(""))
        if !ok {
            this.err = true
        }
        v
    }
}

fn cmdBc(c) {
    let src = c.args.join(" ")
    if src == "" && c.piped {
        src = trimSpace(c.stdin)
    }
    if src == "" {
        return "usage: bc <expression>    e.g. bc 2^10 or echo 6*7 | bc"
    }
    let p = exprParser(src.chars())
    let v = p.expr()
    if p.err || p.peek() != "" {
        lastStatus = 1
        return errf("bc", "syntax error")
    }
    if toFloat(satInt(v)) == v {
        return itoa(satInt(v))
    }
    formatFloat(v)
}

// ── commands/system.tg ──────────────────────────────────────────────────

// commands/system.tg — uname, uptime, ps, top, neofetch and the other
// commands about the (pretend) machine.

fn cmdUname(c) {
    if c.flag("a") {
        return "targa.me " + version + " wasm32 #1 SMP " + goTime(bootTime, "Mon Jan _2 15:04:05 2006") + " wasm32 WebAssembly GNU/Browser"
    }
    if c.flag("r") {
        return version + "-wasm32"
    }
    if c.flag("m") {
        return "wasm32"
    }
    "targa.me"
}

fn prettyUptime() {
    let s = toInt(uptimeSeconds())
    if s < 60 {
        return itoa(s) + " sec"
    }
    if s < 3600 {
        return itoa(s / 60) + " min"
    }
    itoa(s / 3600) + ":" + lpad(itoa(s % 3600 / 60), 2)
}

fn cmdUptime(c) {
    " " + goTime(now(), "15:04:05") + " up " + prettyUptime() + ",  1 user,  load average: 0.13, 0.37, 0.42"
}

fn cmdCal(c) {
    let t = now()
    let year = toInt(timeFormat(t, "YYYY"))
    let month = toInt(timeFormat(t, "MM"))
    let today = toInt(timeFormat(t, "DD"))
    let first = parseTime("01/" + timeFormat(t, "MM/YYYY"), "DD/MM/YYYY")
    let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
    let days = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1]
    let title = goTime(t, "January 2006")
    let padLeft = (20 - bytes(title).length) / 2
    let b = " ".repeat(padLeft) + title + "\nSu Mo Tu We Th Fr Sa\n"
    let col = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"].indexOf(timeFormat(first, "ddd"))
    b += "   ".repeat(col)
    repeat d in 1..days {
        let cell = lpad(itoa(d), 2)
        if d == today {
            cell = `<span class="hl">` + cell + `</span>`
        }
        b += cell
        col += 1
        if col % 7 == 0 {
            b += "\n"
        } else {
            b += " "
        }
    }
    trimRightChars(b, " \n")
}

fn cmdEnv(c) {
    env.keys().sort().map(k => escapeHtml(k + "=" + env[k])).join("\n")
}

fn cmdExport(c) {
    if c.args.length == 0 {
        return cmdEnv(c)
    }
    repeat a in c.args {
        let i = a.indexOf("=")
        if i > 0 {
            env[a.slice(0, i)] = a.slice(i + 1)
        } else {
            return errf("export", "'" + a + "': not a valid identifier")
        }
    }
    ""
}

fn cmdAlias(c) {
    aliases.keys().sort().map(k => "alias " + escapeHtml(k) + "='" + escapeHtml(aliases[k]) + "'").join("\n")
}

fn cmdWhich(c) {
    let name = c.arg(0)
    if name == "" {
        return ""
    }
    if name in aliases {
        return escapeHtml(name) + " is aliased to '" + escapeHtml(aliases[name]) + "'"
    }
    let cmd = commandIndex[name]
    if cmd == null {
        lastStatus = 1
        return errf("which", "no " + name + " in (" + (env.PATH ?? "") + ")")
    }
    if cmd.hidden {
        return escapeHtml(name) + " is a shell built-in"
    }
    if cmd.cat == "fun" {
        return "/usr/games/" + name
    }
    if cmd.cat == "core" || name == "cd" || name == "pwd" || name == "history" || name == "export" || name == "alias" {
        return escapeHtml(name) + " is a shell built-in"
    }
    "/usr/bin/" + name
}

fn cmdHistory(c) {
    if c.flag("c") {
        history = []
        historyIndex = 0
        return ""
    }
    let b = ""
    repeat i, h in history {
        b += dim(lpad(itoa(i + 1), 5)) + "  " + escapeHtml(h) + "\n"
    }
    trimRightChars(b, "\n")
}

let processes = [
    {pid: 1, user: "root", cpu: "0.0", mem: "0.4", command: "/sbin/init"},
    {pid: 2, user: "root", cpu: "0.0", mem: "0.0", command: "[kthreadd]"},
    {pid: 42, user: "root", cpu: "0.0", mem: "0.1", command: "/usr/sbin/cron"},
    {pid: 666, user: "root", cpu: "0.0", mem: "0.2", command: "/usr/sbin/sshd -D"},
    {pid: 1000, user: "guest", cpu: "0.3", mem: "1.2", command: "/bin/tsh"},
    {pid: 1337, user: "neo", cpu: "99.9", mem: "13.37", command: "/usr/bin/follow --the-white-rabbit"},
    {pid: 4096, user: "guest", cpu: "0.1", mem: "2.0", command: "wasm [targa.wasm]"},
]

fn cmdPs(c) {
    let b = "USER       PID %CPU %MEM COMMAND\n"
    repeat p in processes {
        b += pad(p.user, 8) + lpad(itoa(p.pid), 6) + lpad(p.cpu, 5) + lpad(p.mem, 5) + " " + p.command + "\n"
    }
    trimRightChars(b, "\n")
}

fn cmdTop(c) {
    "top - " + goTime(now(), "15:04:05") + " up " + prettyUptime() + ",  1 user,  load average: 0.13, 0.37, 0.42\nTasks: "
    + itoa(processes.length) + " total,   1 running\n%Cpu(s):  0.4 us,  0.1 sy, 99.5 id\nMiB Mem :     16.0 total,     12.0 free,      4.0 used\n\n"
    + cmdPs(c) + "\n\n" + dim("(this is a snapshot; a real top would trap you until q)")
}

fn cmdKill(c) {
    let target = c.arg(0)
    if target == "" {
        return "usage: kill [-9] <pid>"
    }
    let [pid, ok] = atoi(target)
    if !ok {
        return errf("kill", "failed to parse argument: '" + target + "'")
    }
    if processes.some(p => p.pid == pid) {
        return match pid {
            1 => errf("kill", "(1) - Operation not permitted. Killing init? Bold.")
            1337 => "kill: (1337) - Operation not permitted. Nobody stops Neo."
            1000, 4096 => errf("kill", "(" + target + ") - Operation not permitted. That is you.")
            _ => errf("kill", "(" + target + ") - Operation not permitted")
        }
    }
    lastStatus = 1
    errf("kill", "(" + target + ") - No such process")
}

fn cmdLscpu(c) {
    "Architecture:        wasm32\nCPU(s):              " + itoa(cores()) + "\nVendor ID:           WebAssembly\nModel name:          Virtual wasm32 CPU\nFlags:               simd128 bulk-memory easter-eggs"
}

fn cmdDmesg(c) {
    "[    0.000000] Booting targa.me " + version + " (wasm32)\n[    0.000420] Memory: 16384K available\n[    0.001337] Detected " + itoa(cores()) + " CPU cores\n[    0.002000] wasmfs: mounted / read-only\n[    0.003000] Loading " + itoa(commands.length - visibleCount()) + " easter eggs... done\n[    0.004000] tsh: session opened for user guest\n[    0.005000] random: crng init done (browser entropy)"
}

fn cmdNeofetch(c) {
    let nav = js.navigator
    let ua = nav.userAgent
    let browser = "Browser"
    if ua.contains("Firefox") {
        browser = "Firefox"
    } else if ua.contains("Edg/") {
        browser = "Edge"
    } else if ua.contains("Chrome") {
        browser = "Chrome"
    } else if ua.contains("Safari") {
        browser = "Safari"
    }
    let host = "Unknown"
    if ua.contains("Mac") {
        host = "macOS"
    } else if ua.contains("Windows") {
        host = "Windows"
    } else if ua.contains("Android") {
        host = "Android"
    } else if ua.contains("Linux") {
        host = "Linux"
    } else if ua.contains("iPhone") || ua.contains("iPad") {
        host = "iOS"
    }
    let scr = js.screen
    let res = itoa(toInt(scr.width)) + "x" + itoa(toInt(scr.height))
    let mem = "unknown"
    let dm = nav.deviceMemory
    if type(dm) == "integer" || type(dm) == "float" {
        mem = itoa(toInt(dm)) + " GB"
    }
    let info = [
        green("guest") + "@" + green("targa.me"),
        "-----------------",
        yellow("OS") + ": targa.me " + version + " wasm32",
        yellow("Host") + ": " + host + " / " + browser,
        yellow("Kernel") + ": WebAssembly (TargaScript)",
        yellow("Uptime") + ": " + prettyUptime(),
        yellow("Shell") + ": tsh " + version,
        yellow("Resolution") + ": " + res,
        yellow("Terminal") + ": " + browser,
        yellow("CPU") + ": " + itoa(cores()) + " cores",
        yellow("Memory") + ": " + mem,
        yellow("Language") + ": " + nav.language,
        "",
        `<span style="color:#0f0">███</span><span style="color:#ffd700">███</span><span style="color:#ff5555">███</span><span style="color:#00bfff">███</span><span style="color:#c084fc">███</span><span style="color:#e6e6e6">███</span>`,
    ]
    let b = ""
    let i = 0
    repeat i < info.length || i < neofetchLogo.length {
        let logo = " ".repeat(11)
        if i < neofetchLogo.length {
            logo = green(neofetchLogo[i])
        }
        let line = ""
        if i < info.length {
            line = info[i]
        }
        b += logo + "   " + line + "\n"
        i += 1
    }
    trimRightChars(b, "\n")
}

fn cmdSleep(c) {
    let [n, ok] = parseFloat(c.arg(0))
    if !ok {
        return "usage: sleep <seconds>"
    }
    if n > 10 {
        n = 10
    }
    // A negative or NaN delay waits no time, as setTimeout does
    let ms = if n != n || n < 0 { 0 } else { toInt(n * 1000) }
    sequence([{delay: ms, text: ""}], false, null)
    ""
}

fn cmdTime(c) {
    let start = clock()
    let out = ""
    if c.args.length > 0 {
        out = execPipeline(c.args.join(" "))
    }
    let sec = format("%.3f", (clock() - start) / 1000.0)
    if out != "" {
        out += "\n"
    }
    out + "\nreal\t0m" + sec + "s\nuser\t0m" + sec + "s\nsys\t0m0.000s"
}

fn cmdReboot(c) {
    sequence([{delay: 0, text: "Rebooting...\n"}, {delay: 800, text: dim("[  OK  ] Reached target Reboot.") + "\n"}], true, () => {
        js.location.reload()
    })
    ""
}

fn cmdShutdown(c) {
    mode = "closed"
    sequence([
        {delay: 0, text: "Broadcast message from [email protected]:\n\nThe system is going down for power off NOW!\n"},
        {delay: 900, text: dim("[  OK  ] Stopped tsh session.") + "\n"},
        {delay: 500, text: dim("[  OK  ] Reached target Power-Off.") + "\n"},
        {delay: 700, text: "System halted.\n" + dim("Press Enter to power on.") + "\n"},
    ], true, null)
    ""
}

// ── commands/network.tg ─────────────────────────────────────────────────

// commands/network.tg — ping, nmap, dig, curl, ssh and the other network
// commands, all simulated.

fn hostOf(c) {
    let h = c.arg(0)
    if h == "" {
        return ""
    }
    if h.startsWith("https://") { h = h.slice(8) }
    if h.startsWith("http://") { h = h.slice(7) }
    let i = h.indexOf("/")
    if i >= 0 {
        h = h.slice(0, i)
    }
    h
}

fn isSelf(h) {
    h == "targa.me" || h == "www.targa.me" || h == "localhost" || h == "127.0.0.1" || h == "::1"
}

fn cmdPing(c) {
    let h = hostOf(c)
    if h == "" {
        return "usage: ping <host>"
    }
    if !isSelf(h) {
        lastStatus = 2
        return errf("ping", h + ": Temporary failure in name resolution (no uplink from inside a wasm sandbox)")
    }
    let steps = [{delay: 0, text: "PING " + escapeHtml(h) + " (127.0.0.1) 56(84) bytes of data.\n"}]
    let total = 0.0
    repeat i in 1..4 {
        let t = 0.02 + random() * 0.05
        total += t
        steps.insert({delay: 1000, text: "64 bytes from " + escapeHtml(h) + " (127.0.0.1): icmp_seq=" + itoa(i) + " ttl=64 time=" + format("%.3f", t) + " ms\n"})
    }
    steps.insert({delay: 300, text: "\n--- " + escapeHtml(h) + " ping statistics ---\n4 packets transmitted, 4 received, 0% packet loss\nrtt avg = " + format("%.3f", total / 4) + " ms\n"})
    sequence(steps, false, null)
    ""
}

fn cmdNmap(c) {
    let h = hostOf(c)
    if h == "" {
        return "usage: nmap <host>"
    }
    if !isSelf(h) {
        return "Starting Nmap 7.95 ( https://nmap.org )\nNote: Host seems down. This sandbox has no network. Try nmap targa.me"
    }
    let steps = [
        {delay: 0, text: "Starting Nmap 7.95 ( https://nmap.org ) at " + goTime(now(), "2006-01-02 15:04") + "\n"},
        {delay: 900, text: "Nmap scan report for " + escapeHtml(h) + " (127.0.0.1)\nHost is up (0.00013s latency).\n\nPORT      STATE  SERVICE\n"},
        {delay: 500, text: "22/tcp    open   ssh       " + dim("try: ssh root@localhost") + "\n"},
        {delay: 300, text: "80/tcp    closed http\n"},
        {delay: 300, text: "443/tcp   open   https     " + dim("you are here") + "\n"},
        {delay: 300, text: "31337/tcp open   Elite     " + dim("suspicious. check netstat") + "\n"},
        {delay: 400, text: "\nNmap done: 1 IP address (1 host up) scanned in 2.70 seconds\n"},
    ]
    sequence(steps, false, null)
    ""
}

fn cmdTraceroute(c) {
    let h = hostOf(c)
    if h == "" {
        return "usage: traceroute <host>"
    }
    let steps = [
        {delay: 0, text: "traceroute to " + escapeHtml(h) + " (127.0.0.1), 30 hops max\n"},
        {delay: 600, text: " 1  your-browser (127.0.0.1)  0.041 ms\n"},
        {delay: 600, text: " 2  wasm-sandbox (10.0.0.1)  0.113 ms\n"},
        {delay: 600, text: " 3  the-cloud ☁  (10.0.0.2)  13.370 ms\n"},
    ]
    if isSelf(h) {
        steps.insert({delay: 600, text: " 4  " + escapeHtml(h) + " (127.0.0.1)  0.001 ms   " + dim("you never left") + "\n"})
    } else {
        steps.insert({delay: 600, text: " 4  * * *\n"})
        steps.insert({delay: 600, text: " 5  * * *   " + dim("(no uplink from inside a wasm sandbox)") + "\n"})
    }
    sequence(steps, false, null)
    ""
}

fn cmdDig(c) {
    let h = hostOf(c)
    if h == "" {
        h = "targa.me"
    }
    if !isSelf(h) {
        return ";; connection timed out; no servers could be reached (no uplink)"
    }
    ";; ANSWER SECTION:\n" + escapeHtml(h) + ".\t\t300\tIN\tA\t127.0.0.1\n" + escapeHtml(h) + ".\t\t300\tIN\tTXT\t\"you are already here\"\n\n;; Query time: 0 msec\n;; SERVER: 127.0.0.1#53"
}

fn cmdWhois(c) {
    let h = hostOf(c)
    if h == "" {
        return "usage: whois <domain>"
    }
    if !isSelf(h) {
        return "No match for \"" + escapeHtml(h) + "\" (this sandbox only knows about itself)"
    }
    "Domain Name: TARGA.ME\nRegistrant: Miguel Targa\nRegistrant Job: software engineer, reverse engineer\nStatus: clientTransferProhibited (nice try)\nName Server: 127.0.0.1\nContact: [email protected]"
}

fn cmdCurl(c) {
    let h = hostOf(c)
    if h == "" {
        return "curl: try 'curl targa.me'"
    }
    if isSelf(h) {
        return escapeHtml("<!DOCTYPE html>\n<html><body>\n<!-- you are looking at it -->\n</body></html>")
    }
    if h.contains("wttr.in") {
        return "Weather report: your location\n\n     \\  /       Partly cloudy\n   _ /\"\".-.     +20(22) °C\n     \\_(   ).   ↗ 11 km/h\n     /(___(__)  10 km\n                0.0 mm\n\n" + dim("(fake; there is no uplink from inside a wasm sandbox)")
    }
    if h.contains("parrot.live") {
        return "🦜 🦜 🦜  (imagine it dancing)"
    }
    if h.contains("google.com") {
        return "curl: (7) Failed to connect to " + escapeHtml(h) + " port 443: the sandbox has no uplink. Also, it's Google. They're fine."
    }
    lastStatus = 6
    errf("curl", "(6) Could not resolve host: " + h + " (no uplink from inside a wasm sandbox)")
}

// cmdSSH is the fake login easter egg (kept from v1).
fn cmdSSH(c) {
    if c.args.length == 0 {
        return dim(`usage: ssh [-46AaCfGgKkMNnqsTtVvXxYy] [-B bind_interface] [-b bind_address]
           [-c cipher_spec] [-D [bind_address:]port] [-E log_file]
           [-e escape_char] [-F configfile] [-I pkcs11] [-i identity_file]
           [-J destination] [-L address] [-l login_name] [-m mac_spec]
           [-O ctl_cmd] [-o option] [-P tag] [-p port] [-R address]
           [-S ctl_path] [-W host:port] [-w local_tun[:remote_tun]]
           destination [command [argument ...]]
       ssh [-Q query_option]`)
    }
    let dest = c.arg(0)
    let user = "guest"
    let host = dest
    let i = dest.indexOf("@")
    if i >= 0 {
        user = dest.slice(0, i)
        host = dest.slice(i + 1)
    }
    if !isSelf(host) {
        lastStatus = 255
        return errf("ssh", "connect to host " + host + " port 22: Connection refused")
    }
    if user != "root" {
        lastStatus = 255
        return escapeHtml(user + "@" + host) + ": Permission denied (publickey). " + dim("Hint: who else is in /etc/passwd?")
    }
    let h = escapeHtml(host)
    sequence([
        {delay: 0, text: dim("Connecting to " + h + "...") + "\n"},
        {delay: 2000, text: yellow("Warning: Remote host identification has changed!") + "\n" + green("SSH key fingerprint:") + " SHA256:12:f8:7e:78:61:b4:bf:e2:de:24:15:96:4e:d4:72:53\n" + dim("Are you sure you want to continue connecting (yes/no)?") + " "},
        {delay: 2000, text: "yes\n" + dim("root@" + h + "'s password:") + " "},
        {delay: 2000, text: "\n" + `<span class="red">Permission denied, please try again.</span>` + "\n" + dim("root@" + h + "'s password:") + " "},
        {delay: 2000, text: "\n" + dim("Last login: " + goTime(now(), "Mon Jan 2 15:04:05 2006") + " from 127.0.0.1") + "\nWelcome to the Matrix, " + green("Neo") + ".\n"},
    ], true, breakIn)
    ""
}

// ── commands/fun.tg ─────────────────────────────────────────────────────

// commands/fun.tg — cowsay, lolcat, sl, cmatrix and figlet.

fn cmdCowsay(c) {
    let msg = c.args.join(" ")
    if msg == "" && c.piped {
        msg = trimSpace(c.stdin)
    }
    if msg == "" {
        msg = "Moo?"
    }
    let wrapped = []
    repeat para in msg.split("\n") {
        let line = ""
        repeat w in fields(para) {
            if line != "" && line.length + 1 + w.length > 40 {
                wrapped.insert(line)
                line = w
            } else if line == "" {
                line = w
            } else {
                line += " " + w
            }
        }
        wrapped.insert(line)
    }
    let width = 0
    repeat l in wrapped {
        if l.length > width {
            width = l.length
        }
    }
    let b = " " + "_".repeat(width + 2) + "\n"
    repeat i, l in wrapped {
        let left = "|"
        let right = "|"
        if wrapped.length == 1 {
            left = "<"
            right = ">"
        } else if i == 0 {
            left = "/"
            right = "\\"
        } else if i == wrapped.length - 1 {
            left = "\\"
            right = "/"
        }
        b += left + " " + escapeHtml(pad(l, width)) + " " + right + "\n"
    }
    b += " " + "-".repeat(width + 2) + "\n"
    let eyes = "oo"
    if c.flag("d") {
        eyes = "xx"
    }
    b += "        \\   ^__^\n         \\  (" + eyes + ")\\_______\n            (__)\\       )\\/\\\n                ||----w |\n                ||     ||"
    b
}

fn cmdLolcat(c) {
    let src = c.args.join(" ")
    if c.piped {
        src = trimRightChars(c.stdin, "\n")
    }
    if src == "" {
        return "usage: something | lolcat"
    }
    let b = ""
    let line = 0
    let col = 0
    repeat r in src {
        if r == "\n" {
            b += "\n"
            line += 1
            col = 0
            continue
        }
        let hue = (col * 6 + line * 20) % 360
        b += `<span style="color:hsl(` + itoa(hue) + `,100%,65%)">` + escapeHtml(r) + `</span>`
        col += 1
    }
    b
}

fn cmdSl(c) {
    let cols = columns()
    let width = trainFrames[0].length
    let off = cols
    frames(35, false, i => {
        off -= 1
        let b = ""
        repeat line in trainFrames {
            let row = ""
            if off >= 0 {
                row = " ".repeat(off) + line
            } else if -off < width {
                row = line.slice(-off)
            }
            if row.length > cols {
                row = row.slice(0, cols)
            }
            b += escapeHtml(row) + "\n"
        }
        [b, off <= -width]
    }, "")
    ""
}

fn cmdCmatrix(c) {
    let cols = min(columns(), 120)
    let rows = 16
    let glyphs = "ハミヒーウシナモニサワツオリアホテマケメエカキムユラセネスタヌヘ0123456789Z*+-<>|:=".chars()
    let drops = []
    repeat x in 1..cols {
        drops.insert({y: -rand(rows * 2), length: 4 + rand(10), speed: 1 + rand(2)})
    }
    let grid = []
    repeat y in 1..rows {
        let row = []
        repeat x in 1..cols { row.insert(glyphs[rand(glyphs.length)]) }
        grid.insert(row)
    }
    frames(70, true, f => {
        let b = ""
        repeat r in 0..rows - 1 {
            repeat x in 0..cols - 1 {
                let d = drops[x]
                if r == d.y {
                    b += `<span class="mxh">` + grid[r][x] + `</span>`
                } else if r < d.y && r > d.y - d.length {
                    if r > d.y - d.length / 2 {
                        b += `<span class="mx">` + grid[r][x] + `</span>`
                    } else {
                        b += `<span class="mxd">` + grid[r][x] + `</span>`
                    }
                } else {
                    b += " "
                }
            }
            b += "\n"
        }
        repeat x in 0..cols - 1 {
            if f % drops[x].speed == 0 {
                drops[x].y += 1
            }
            if drops[x].y - drops[x].length > rows {
                drops[x] = {y: -rand(rows), length: 4 + rand(10), speed: 1 + rand(2)}
            }
            if rand(20) == 0 {
                grid[rand(rows)][x] = glyphs[rand(glyphs.length)]
            }
        }
        [b, f >= 90]
    }, green("Wake up, Neo...") + "\n")
    ""
}

fn cmdFiglet(c) {
    let msg = c.args.join(" ")
    if msg == "" && c.piped {
        msg = trimSpace(c.stdin)
    }
    if msg == "" {
        return "usage: figlet <text>"
    }
    // Go cuts at 24 bytes; a character cut in half reads as one invalid
    // character per byte left, each drawn as ?
    let chars = []
    let size = 0
    repeat ch in msg {
        let n = bytes(ch).length
        if size + n > 24 {
            repeat k in 1..24 - size { chars.insert("?") }
            break
        }
        chars.insert(ch)
        size += n
    }
    let rows = ["", "", "", "", ""]
    repeat ch in chars {
        let up = ch.upper()
        if up.length != 1 { up = ch } // Go upper-cases one character to one
        let g = figFont[up] ?? figFont["?"]
        repeat r in 0..4 {
            rows[r] += g[r] + " "
        }
    }
    rows.map(r => escapeHtml(trimRightChars(r, " "))).join("\n")
}

// A compact 5-row block font.
let figFont = {
    "A": [" ▄▀▄ ", "█▀▀█ ", "█  █ ", "█  █ ", "▀  ▀ "],
    "B": ["█▀▀▄ ", "█▄▄▀ ", "█  █ ", "█▄▄▀ ", "▀▀▀  "],
    "C": [" ▄▀▀ ", "█    ", "█    ", "█    ", " ▀▀▀ "],
    "D": ["█▀▀▄ ", "█  █ ", "█  █ ", "█  █ ", "▀▀▀  "],
    "E": ["█▀▀▀ ", "█▄▄  ", "█    ", "█    ", "▀▀▀▀ "],
    "F": ["█▀▀▀ ", "█▄▄  ", "█    ", "█    ", "▀    "],
    "G": [" ▄▀▀ ", "█    ", "█ ▀█ ", "█  █ ", " ▀▀▀ "],
    "H": ["█  █ ", "█▄▄█ ", "█  █ ", "█  █ ", "▀  ▀ "],
    "I": ["▀█▀ ", " █  ", " █  ", " █  ", "▀▀▀ "],
    "J": ["  ▀█ ", "   █ ", "   █ ", "█  █ ", " ▀▀  "],
    "K": ["█ ▄▀ ", "█▀▄  ", "█  █ ", "█  █ ", "▀  ▀ "],
    "L": ["█    ", "█    ", "█    ", "█    ", "▀▀▀▀ "],
    "M": ["█▄ ▄█ ", "█ ▀ █ ", "█   █ ", "█   █ ", "▀   ▀ "],
    "N": ["█▄  █ ", "█ ▀▄█ ", "█   █ ", "█   █ ", "▀   ▀ "],
    "O": [" ▄▀▄ ", "█   █", "█   █", "█   █", " ▀▄▀ "],
    "P": ["█▀▀▄ ", "█▄▄▀ ", "█    ", "█    ", "▀    "],
    "Q": [" ▄▀▄ ", "█   █", "█ ▄ █", "█  ▀█", " ▀▀▀▀"],
    "R": ["█▀▀▄ ", "█▄▄▀ ", "█ ▀▄ ", "█  █ ", "▀  ▀ "],
    "S": [" ▄▀▀ ", "▀▄▄  ", "   █ ", "   █ ", "▀▀▀  "],
    "T": ["▀▀█▀▀ ", "  █   ", "  █   ", "  █   ", "  ▀   "],
    "U": ["█  █ ", "█  █ ", "█  █ ", "█  █ ", " ▀▀  "],
    "V": ["█   █ ", "█   █ ", "█   █ ", " █ █  ", "  ▀   "],
    "W": ["█   █ ", "█   █ ", "█ █ █ ", "█ █ █ ", " ▀ ▀  "],
    "X": ["█   █ ", " █ █  ", "  █   ", " █ █  ", "▀   ▀ "],
    "Y": ["█   █ ", " █ █  ", "  █   ", "  █   ", "  ▀   "],
    "Z": ["▀▀▀▀█ ", "   ▄▀ ", "  ▄▀  ", " ▄▀   ", "▀▀▀▀▀ "],
    "0": [" ▄▀▄ ", "█  ▄█", "█ ▄ █", "█▄  █", " ▀▄▀ "],
    "1": [" ▄█ ", "  █ ", "  █ ", "  █ ", "▀▀▀▀"],
    "2": ["▀▀▀▄ ", "   █ ", " ▄▀  ", "█    ", "▀▀▀▀ "],
    "3": ["▀▀▀▄ ", "  ▄▀ ", "   █ ", "   █ ", "▀▀▀  "],
    "4": ["█  █ ", "█  █ ", "▀▀▀█ ", "   █ ", "   ▀ "],
    "5": ["█▀▀▀ ", "█▄▄  ", "   █ ", "   █ ", "▀▀▀  "],
    "6": [" ▄▀▀ ", "█▄▄  ", "█  █ ", "█  █ ", " ▀▀  "],
    "7": ["▀▀▀█ ", "  ▄▀ ", "  █  ", "  █  ", "  ▀  "],
    "8": [" ▄▀▄ ", " ▀▄▀ ", "█   █", "█   █", " ▀▀▀ "],
    "9": [" ▄▀▄ ", "█   █", " ▀▀█ ", "   █ ", " ▀▀  "],
    " ": ["  ", "  ", "  ", "  ", "  "],
    "!": ["█ ", "█ ", "█ ", "  ", "▀ "],
    "?": ["▀▀▄ ", "  █ ", " ▄▀ ", "    ", " ▀  "],
    ".": ["  ", "  ", "  ", "  ", "▀ "],
    "-": ["    ", "    ", "▀▀▀ ", "    ", "    "],
    "_": ["    ", "    ", "    ", "    ", "▀▀▀ "],
}

// ── eggs.tg ─────────────────────────────────────────────────────────────

// eggs.tg — easter eggs: hidden from help, completion and /bin. Found by
// trying.

let flagFound = false

fn cmdFlagFound() {
    if flagFound {
        return green("You already found it.") + " Show-off."
    }
    flagFound = true
    after(50, () => {
        cmdFiglet(Ctx(["YOU WIN"]))
    })
    green("Correct!") + " 🎉\n\n" + cmdFiglet(Ctx(["NICE"])) + "\n\nYou followed the trail: hidden file → log → base64.\nThat's the same instinct that finds real bugs.\nIf you want to talk about it: " + `<a href="mailto:[email protected]?subject=I%20found%20the%20flag" class="link">[email protected]</a>`
}

// ---------------------------------------------------------------------------
// sudo
// ---------------------------------------------------------------------------

let sudoAttempts = 0

fn cmdSudo(c) {
    if c.args.length == 0 {
        return "usage: sudo <command>"
    }
    if root {
        return "You are root already. sudo is for mortals."
    }
    sudoAttempts += 1
    let full = c.args.join(" ")
    if c.arg(0) == "make" && c.arg(1) == "sandwich" || full.contains("make me a sandwich") {
        return "Okay. 🥪"
    }
    if c.arg(0) == "rm" && (c.has("-rf") || c.has("-fr")) && (c.has("/") || c.has("/*")) {
        return cmdRm(Ctx(c.args.slice(1) + ["--no-preserve-root"]))
    }
    if c.arg(0) == "su" || c.arg(0) == "-i" || c.arg(0) == "bash" || c.arg(0) == "sh" || c.arg(0) == "-s" {
        return "[sudo] password for guest: \n" + errf("sudo", "3 incorrect password attempts") + "\n" + dim("Root has no password. Root doesn't need you.")
    }
    match sudoAttempts {
        1 => "[sudo] password for guest: \n" + `<span class="red">guest is not in the sudoers file. This incident will be reported.</span>`
        2 => "[sudo] password for guest: \n" + `<span class="red">guest is still not in the sudoers file. The incident has been reported again.</span>`
        3 => `<span class="red">sudo: a report has been sent to root. root is not amused.</span>`
        _ => `<span class="red">sudo: seriously? Try: cat /etc/sudoers... oh wait.</span>`
    }
}

// ---------------------------------------------------------------------------
// vim: the classic trap
// ---------------------------------------------------------------------------

let vimFile = ""

fn cmdVim(c) {
    mode = "vim"
    vimFile = c.arg(0)
    terminalContent = vimScreen()
    ""
}

fn vimScreen() {
    let name = "[No Name]"
    let body = ""
    if vimFile != "" {
        name = vimFile
        let e = find(resolve(vimFile))
        if e != null && !e.dir && !denied(resolve(vimFile), e) && !e.binary {
            let t = text(e)
            repeat t.endsWith("\n") { t = t.slice(0, -1) }
            body = escapeHtml(t) + "\n"
        }
    }
    let rows = 14
    if body != "" {
        rows -= body.count("\n")
        if rows < 2 {
            rows = 2
        }
    }
    body + (`<span class="blue">~</span>` + "\n").repeat(rows) + `<span class="hl"> ` + escapeHtml(name) + ` </span>                                        ` + dim("0,0-1  All") + "\n" +
    dim("Type  :q  and press <Enter> to exit Vim. Yes, really. This is the whole joke.") + "\n"
}

fn vimInput(raw) {
    if raw in [":q", ":q!", ":wq", ":x", ":qa", ":qa!", "ZZ", "ZQ", ":wq!", ":exit", ":quit"] {
        mode = "shell"
        terminalContent = banner + dim("You escaped vim. Achievement unlocked.") + "\n"
        if raw == ":wq" || raw == ":x" || raw == ":wq!" || raw == "ZZ" {
            terminalContent = banner + dim(`"` + vimName() + `" E212: Can't open file for writing (read-only filesystem). You escaped anyway.`) + "\n"
        }
    } else if raw == ":help" || raw == ":h" {
        terminalContent += dim("The help says: type :q to quit. Which you could have guessed.") + "\n"
    } else if raw in ["i", "a", "o", "I", "A", "O"] {
        terminalContent += dim("-- INSERT --  (there is nowhere to insert. press Esc in your mind, then :q)") + "\n"
    } else if raw == ":w" {
        terminalContent += `<span class="red">E212: Can't open file for writing</span>` + "\n"
    } else if raw == "" {
        terminalContent += dim("~") + "\n"
    } else if raw.startsWith(":") {
        terminalContent += `<span class="red">E492: Not an editor command: ` + escapeHtml(raw.slice(1)) + `</span>` + "\n"
    } else {
        terminalContent += dim("Vim ignores '" + escapeHtml(raw) + "'. It's not that kind of program. :q to leave.") + "\n"
    }
    render()
}

fn vimName() {
    if vimFile == "" {
        return "Untitled"
    }
    vimFile
}

// ---------------------------------------------------------------------------
// python REPL joke, git, apt
// ---------------------------------------------------------------------------

fn cmdPython(c) {
    if c.flag("c") && c.args.length > 1 {
        let src = c.args.slice(1).join(" ")
        if src.startsWith("print(") && src.endsWith(")") {
            // Trim quotes from both ends, like Go's strings.Trim
            let inner = src.slice(6, -1)
            repeat inner.startsWith("\"") || inner.startsWith("'") { inner = inner.slice(1) }
            repeat inner.endsWith("\"") || inner.endsWith("'") { inner = inner.slice(0, -1) }
            let v = cmdBc(Ctx([inner]))
            let hasDigit = "0123456789".chars().some(d => inner.contains(d))
            if !v.contains("syntax error") && hasDigit && !inner.contains("\"") && !inner.contains("'") {
                return v
            }
            return escapeHtml(inner)
        }
        return `<span class="red">SyntaxError: this Python only knows print(). Try: python -c "print(2**10)" (hint: use ^ ; it is bc underneath)</span>`
    }
    if c.arg(0) != "" {
        return errf("python", "can't open file '" + c.arg(0) + "': [Errno 2] No such file or directory")
    }
    "Python 3.13.0 (wasm32)\n>>> import antigravity\n>>> " + dim("(REPL not available; try python -c \"print(6*7)\")")
}

fn cmdGit(c) {
    match c.arg(0) {
        "" => "usage: git <command>\n\nTry: git status, git log, git push, git blame"
        "status" => "On branch main\nYour branch is up to date with 'origin/main'.\n\nnothing to commit, working tree clean " + dim("(it's a read-only filesystem, after all)")
        "log" => yellow("commit 0a750b5") + "  ssh easter egg\n" + yellow("commit 6449ac9") + "  add clear\n" + yellow("commit ab00e9d") + "  easter egg\n" + yellow("commit 780bab2") + "  new version\n" + yellow("commit 4b588b8") + "  first commit\n" + dim("(real history: github.com/migueltarga)")
        "push" => "fatal: could not read Username for 'https://github.com': No such device or address\n" + dim("You're pushing from inside a browser tab. Ambitious.")
        "pull", "fetch" => "Already up to date."
        "blame" => "^0a750b5 (Miguel Targa 2025-02-26 1) everything\n" + dim("It was me. It's always me.")
        "commit" => "nothing to commit, working tree clean"
        "clone" => {
            let repo = c.arg(1)
            if repo.startsWith("https://github.com/") { repo = repo.slice("https://github.com/".length) }
            if repo.endsWith(".git") { repo = repo.slice(0, -4) }
            "Cloning into '" + escapeHtml(repo) + "'...\nfatal: no uplink from inside a wasm sandbox. Open the link with your other hand."
        }
        "checkout", "switch" => "error: pathspec '" + escapeHtml(c.arg(1)) + "' did not match any file(s) known to git"
        "stash" => "Saved working directory and index state WIP on main: nothing"
        "rebase" => "fatal: it is not possible to rebase reality."
        _ => "git: '" + escapeHtml(c.arg(0)) + "' is not a git command. See 'git --help'."
    }
}

fn cmdApt(c) {
    let pkg = c.arg(1)
    match c.arg(0) {
        "install", "add" => {
            if pkg == "" {
                return "E: Nothing to install."
            }
            if commandIndex[pkg] != null {
                return pkg + " is already the newest version. Try running it."
            }
            let steps = [
                {delay: 0, text: "Reading package lists... Done\nBuilding dependency tree... Done\n"},
                {delay: 500, text: "The following NEW packages will be installed:\n  " + escapeHtml(pkg) + "\n"},
                {delay: 500, text: "Get:1 https://targa.me/apt " + escapeHtml(pkg) + " [" + itoa(100 + rand(900)) + " kB]\n"},
                {delay: 700, text: `<span class="red">E: Failed to fetch https://targa.me/apt/` + escapeHtml(pkg) + `  Read-only file system (and no uplink)</span>` + "\n"},
            ]
            sequence(steps, false, null)
            ""
        }
        "update" => "Hit:1 https://targa.me/apt stable InRelease\nReading package lists... Done\nAll packages are up to date. Everything is compiled in."
        "upgrade" => "0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded."
        "remove", "purge" => "E: Unable to remove " + escapeHtml(pkg) + ": Read-only file system"
        "moo" => "         (__)\n         (oo)\n   /------\\/\n  / |    ||\n *  /\\---/\\\n    ~~   ~~\n...\"Have you mooed today?\"..."
        _ => "apt " + version + " (wasm32)\nUsage: apt install <pkg> | apt update | apt moo"
    }
}

// ---------------------------------------------------------------------------
// Bigger eggs
// ---------------------------------------------------------------------------

fn cmdStarwars(c) {
    let scenes = [
        "        A long time ago in a browser tab\n              far, far away....",
        "                    STAR WARS\n            Episode IV.V: A NEW SHELL",
        "  It is a period of civil war between tabs and spaces.\n  Rebel programs, striking from a hidden wasm module,\n  have won their first victory against the evil\n  JavaScript Empire.",
        "         .-.      /\\   \n        (o o)    /  \\  \n        | O \\   /    \\ \n         \\   \\ /______\\\n          `~~~'   ||   \n                  ||   \n           (it's a droid, use your imagination)",
        "                  THE END",
    ]
    let steps = []
    repeat i, s in scenes {
        let d = if i == 0 { 0 } else { 2500 }
        steps.insert({delay: d, text: "\n" + yellow(escapeHtml(s)) + "\n"})
    }
    steps.insert({delay: 1500, text: "\n   " + dim("the full ascii movie lives at towel.blinkenlights.nl (real telnet needed)") + "\n"})
    sequence(steps, false, null)
    ""
}

fn cmdHack(c) {
    let target = c.arg(0)
    if target == "" {
        target = "the mainframe"
    }
    let t = escapeHtml(target)
    let steps = [
        {delay: 0, text: green("[*]") + " Initializing hack on " + t + "...\n"},
        {delay: 600, text: green("[*]") + " Bypassing firewall............ " + green("OK") + "\n"},
        {delay: 600, text: green("[*]") + " Enumerating open ports........ 22, 443, 31337\n"},
        {delay: 600, text: green("[*]") + " Cracking SHA-256 with rainbow tables... " + yellow("(this would take longer than the universe)") + "\n"},
        {delay: 900, text: green("[*]") + " Uploading payload  [" + "=".repeat(10) + "          ] 50%\n"},
        {delay: 700, text: green("[*]") + " Uploading payload  [" + "=".repeat(20) + "] 100%\n"},
    ]
    // This machine is the one target that falls
    if isSelf(target) {
        sequence(steps, true, breakIn)
        return ""
    }
    steps = [
        ...steps,
        {delay: 600, text: green("[*]") + " Gaining root......... " + `<span class="red">DENIED</span>` + "\n"},
        {delay: 800, text: green("[+]") + " ACCESS GRANTED  " + dim("(to the guest account, which you already had)") + "\n"},
        {delay: 600, text: dim("I'm in. 😎  Real hacking is 90% reading logs. Try: cat /var/log/auth.log") + "\n"},
    ]
    sequence(steps, false, null)
    ""
}

// ---------------------------------------------------------------------------
// The table. It comes last: top-level values may only use functions defined
// above them.
// ---------------------------------------------------------------------------

let eggCommands = [
    {name: "sudo", hidden: true, run: cmdSudo},
    {name: "su", hidden: true, run: fn(c) {
        lastStatus = 1
        "Password: \n" + errf("su", "Authentication failure")
    }},
    {name: "passwd", hidden: true, run: fn(c) {
        "Changing password for guest.\nCurrent password: \n" + errf("passwd", "Authentication token manipulation error. This is a static site.")
    }},
    {name: "vim", hidden: true, run: cmdVim},
    {name: "emacs", hidden: true, run: fn(c) { "emacs: not installed. This machine has taste. Try vim." }},
    {name: "nano", hidden: true, run: fn(c) { "nano: not installed. Real hackers use vim. (You can try it.)" }},
    {name: "python", hidden: true, run: cmdPython},
    {name: "node", hidden: true, run: fn(c) {
        "Welcome to Node.js.\n> " + dim("Just kidding. This shell is TargaScript running on wasm. Try: uname -a")
    }},
    {name: "go", hidden: true, run: fn(c) {
        if c.arg(0) == "version" {
            return "go version go1.25 js/wasm"
        }
        "Go is a tool for managing Go source code.\n\nUsage:\n\n\tgo <command> [arguments]\n\nTargaScript, the language of this shell, was built with it. Try: go version"
    }},
    {name: "gcc", hidden: true, run: fn(c) {
        "gcc: fatal error: no input files. Also, no compiler. Also, this is a browser."
    }},
    {name: "make", hidden: true, run: fn(c) {
        if c.arg(0) == "coffee" {
            return "make: *** No rule to make target 'coffee'. Try: coffee"
        }
        "make: *** No targets specified and no makefile found.  Stop."
    }},
    {name: "git", hidden: true, run: cmdGit},
    {name: "docker", hidden: true, run: fn(c) {
        "Cannot connect to the Docker daemon. You are already inside a sandbox inside a sandbox."
    }},
    {name: "apt", hidden: true, run: cmdApt},
    {name: "yum", hidden: true, run: cmdApt},
    {name: "pacman", hidden: true, run: fn(c) { "ᗧ · · · ᗣ  " + dim("(wrong pacman)") }},
    {name: "brew", hidden: true, run: fn(c) {
        "==> Brewing... ☕\nError: No formulae found. This is a static site, not a Mac."
    }},
    {name: "npm", hidden: true, run: fn(c) {
        if c.arg(0) == "install" || c.arg(0) == "i" {
            return "added 1,337 packages in 0.4s\n\n" + yellow("42 vulnerabilities") + " (13 moderate, 29 high)\n\n" + dim("(fake, but you believed it for a second)")
        }
        "npm: run 'npm install' if you want to feel something"
    }},
    {name: "coffee", hidden: true, run: fn(c) {
        "HTTP/1.1 418 I'm a teapot\n\n      ( (\n       ) )\n    .______.\n    |      |]\n    \\      /\n     `----'"
    }},
    {name: "tea", hidden: true, run: fn(c) { "Brewing... 🍵  Steep for 3 minutes. Do not microwave." }},
    {name: "beer", hidden: true, run: fn(c) { "🍺  Cheers! You've earned it after all these commands." }},
    {name: "hello", hidden: true, run: fn(c) { "Hello, world! 👋 It's a pleasure to have you here." }},
    {name: "hi", hidden: true, run: fn(c) { "Hi! Type help if you're lost, or ls -a if you're curious." }},
    {name: "thanks", hidden: true, run: fn(c) { "You're welcome. Come back anytime." }},
    {name: "42", hidden: true, run: fn(c) {
        "The answer to life, the universe and everything. But what was the question?"
    }},
    {name: "xyzzy", hidden: true, run: fn(c) { "Nothing happens." }},
    {name: "plugh", hidden: true, run: fn(c) { "A hollow voice says 'Fool.'" }},
    {name: "moo", hidden: true, run: fn(c) {
        "         (__)\n         (oo)\n   /------\\/\n  / |    ||\n *  /\\---/\\\n    ~~   ~~\n...\"Have you mooed today?\"..."
    }},
    {name: "dice", hidden: true, run: fn(c) { "🎲 You rolled a " + itoa(1 + rand(6)) }},
    {name: "roll", hidden: true, run: fn(c) { "🎲 You rolled a " + itoa(1 + rand(6)) }},
    {name: "flip", hidden: true, run: fn(c) {
        if rand(2) == 0 {
            return "🪙 Heads"
        }
        "🪙 Tails"
    }},
    {name: "coin", hidden: true, run: fn(c) {
        if rand(2) == 0 {
            return "🪙 Heads"
        }
        "🪙 Tails"
    }},
    {name: "rickroll", hidden: true, run: fn(c) {
        "Never gonna give you up\nNever gonna let you down\n" + dim("(that's all you get; copyright is a thing)")
    }},
    {name: "starwars", hidden: true, run: cmdStarwars},
    {name: "hack", hidden: true, run: cmdHack},
    {name: "hackerman", hidden: true, run: cmdHack},
    {name: "matrix", hidden: true, run: cmdCmatrix},
    {name: "flag", hidden: true, run: fn(c) {
        "There is a flag hidden on this system. It starts with targa{ and is not in /root.\nRun it as a command when you find it."
    }},
    {name: "hint", hidden: true, run: fn(c) {
        "Hints:\n  1. ls -a shows hidden files.\n  2. Logs are worth reading.\n  3. base64 is not encryption.\n  4. Once you have the flag, type it as a command."
    }},
    {name: "credits", hidden: true, run: fn(c) {
        green("targa.me " + version) + "\n  Shell, filesystem, animations and " + itoa(commands.length) + " commands\n  written in TargaScript, running entirely in your browser\n  on its WebAssembly interpreter.\n  No frameworks, no analytics, no backend.\n\n  Source: it's plain TargaScript. Read it at " + dim("targa.me/site.tg")
    }},
    {name: "source", hidden: true, run: fn(c) { "The source is plain TargaScript: targa.me/site.tg" }},
    {name: "konami", hidden: true, run: fn(c) {
        "↑ ↑ ↓ ↓ ← → ← → B A   +30 lives. Unfortunately this is not that kind of site."
    }},
    {name: "love", hidden: true, run: fn(c) { "❤️  <3  Made with love, TargaScript and questionable amounts of coffee." }},
    {name: "vibe", hidden: true, run: fn(c) { "vibe check: passed ✅" }},
    {name: "wasm", hidden: true, run: fn(c) {
        "You're soaking in it. Everything you see was rendered by TargaScript code, run by an interpreter compiled to WebAssembly."
    }},
    {name: "tsh", hidden: true, run: fn(c) {
        "tsh, version " + version + " (wasm32)\nThe targa shell. You are already in it."
    }},
    {name: "bash", hidden: true, run: fn(c) {
        "bash: not installed. You have tsh, which is like bash but with more easter eggs."
    }},
    {name: "zsh", hidden: true, run: fn(c) { "zsh: not installed. No oh-my-zsh either. Just tsh." }},
    {name: "sh", hidden: true, run: fn(c) { "sh: already running. It's turtles all the way down." }},
    {name: "telnet", hidden: true, run: fn(c) {
        if c.args.join(" ").contains("towel.blinkenlights.nl") {
            return cmdStarwars(c)
        }
        "Trying " + escapeHtml(c.arg(0)) + "...\ntelnet: Unable to connect to remote host: No uplink. Try: telnet towel.blinkenlights.nl"
    }},
    {name: "mail", hidden: true, run: fn(c) {
        "You have 1 new message.\n\nFrom: [email protected]\nSubject: Welcome\n\n  Enjoy the box. Don't touch /root.\n  Also: [email protected] is a real inbox."
    }},
    {name: "wall", hidden: true, run: fn(c) {
        "Broadcast message from [email protected] (pts/0):\n\n" + escapeHtml(c.args.join(" ")) + "\n\n" + dim("(nobody else is logged in)")
    }},
    {name: "cowthink", hidden: true, run: fn(c) { cmdCowsay(c).replace("\\", "o") }},
    {name: "sudoku", hidden: true, run: fn(c) { "sudoku: not installed. But sudo is, and it doesn't like you either." }},
    {name: "fuck", hidden: true, run: fn(c) {
        if history.length >= 2 {
            return "The Fuck: did you mean " + yellow(escapeHtml(history[history.length - 2])) + "? Probably. Try again with fewer typos."
        }
        "Language! Also, nothing to correct yet."
    }},
    {name: "lol", hidden: true, run: fn(c) { "😂" }},
    {name: "whatis", hidden: true, run: fn(c) {
        let cmd = commandIndex[c.arg(0)]
        if cmd != null && !cmd.hidden {
            return cmd.name + " (1) - " + cmd.desc
        }
        escapeHtml(c.arg(0)) + ": nothing appropriate."
    }},
    {name: "whereis", hidden: true, run: fn(c) {
        if c.arg(0) == "waldo" {
            return "waldo: /somewhere/in/a/crowd"
        }
        cmdWhich(c)
    }},
    {name: "locate", hidden: true, run: fn(c) {
        if c.arg(0) == "" {
            return "usage: locate <name>"
        }
        let out = []
        walk("/", fn(p, e, depth) {
            if e.name.contains(c.arg(0)) {
                out.insert(escapeHtml(p))
            }
        })
        if out.length == 0 {
            lastStatus = 1
            return ""
        }
        out.join("\n")
    }},
    {name: "zoe", hidden: true, run: fn(c) {
        let art = zoeArt
        repeat art.endsWith("\n") { art = art.slice(0, -1) }
        "<span class=\"art\">" + escapeHtml(art) + "</span>"
    }},
    {name: "clearflag", hidden: true, run: fn(c) {
        flagFound = false
        "flag state reset"
    }},
]

// ── breakin.tg ──────────────────────────────────────────────────────────

// breakin.tg — the break-in: ssh [email protected] or hack targa.me end in a
// root shell, and root can read /var/www/targa.me/src, the real source of
// this site, split back into its files from site.tg.

let root = false

const breakInReadme = `Ok, you got in.

Well, sort of: this whole machine is a TargaScript program running in your
browser, so you broke into your own tab. But what you found is real.
Everything in src/ is what draws this terminal: the shell, the filesystem
you've been walking around, every command, the easter eggs, and this
break-in too.

It's written in TargaScript, a language Miguel made from scratch: a
compiler, a virtual machine and a WebAssembly runtime. Start with main.tg,
or read it all with colors at https://targa.me/source/.

Found a real hole somewhere? Miguel is in Google's Bug Hunters Hall of
Fame and would love to hear about it: [email protected]
`

tree["/var/www/targa.me"] = [{name: "README.txt", content: breakInReadme}, {name: "src", dir: true}]
tree["/var/www/targa.me/src"] = []

// breakIn plays the exploit and opens the root shell. It starts fetching
// the source right away, so it's there when the shell opens.
fn breakIn() {
    loadSource()
    sequence([
        {delay: 600, text: green("[*]") + " Exploiting tsh (CVE-2026-31337: the shell trusts the browser)...\n"},
        {delay: 800, text: green("[*]") + " Escaping the WebAssembly sandbox....... " + green("OK") + "\n"},
        {delay: 700, text: green("[*]") + " Mounting /var/www/targa.me............ " + yellow("read-only, but readable") + "\n"},
        {delay: 700, text: green("[+]") + " uid=0(root) gid=0(root) groups=0(root)\n"},
        {delay: 500, text: "\n" + `<span class="red">root shell obtained.</span>` + " The source of this site is in " + yellow("/var/www/targa.me") + ".\n" + dim("Start with cat README.txt. Type exit to go back to being a guest.") + "\n"},
    ], true, () => {
        root = true
        env.USER = "root"
        cwd = "/var/www/targa.me"
    })
}

// loadSource fills /var/www/targa.me from site.tg, with the same version the
// page loaded so the browser has it cached. Every file in it starts with a
// rule: "// ── commands/files.tg ───".
fn loadSource() {
    let script = doc.querySelector("script[src*='targascript.js']")
    let version = if script == null { null } else { js.URL.new(script.src).searchParams.get("v") }
    let url = "/site.tg" + if version == null { "" } else { "?v=" + version }
    then(js.fetch(url), resp => {
        then(resp.text(), source => {
            let files = {}
            let name = ""
            repeat line in toString(source).split("\n") {
                if line.startsWith("// ── ") {
                    name = line.slice(6).split(" ")[0]
                    files[name] = []
                } else if name != "" {
                    files[name].insert(line)
                }
            }
            let src = []
            let commandDir = []
            repeat name in files.keys() {
                let e = {name: splitPath("/" + name)[1], content: files[name].join("\n").trim() + "\n"}
                if name.startsWith("commands/") { commandDir.insert(e) } else { src.insert(e) }
            }
            tree["/var/www/targa.me/src"] = [{name: "commands", dir: true}, ...src].sortBy(e => e.name)
            tree["/var/www/targa.me/src/commands"] = commandDir.sortBy(e => e.name)
        }, _ => null)
    }, _ => null)
}

// leaveRoot is exit in the root shell: back to the guest account
fn leaveRoot() {
    root = false
    env.USER = "guest"
    cwd = home
    "logout\n" + dim("You are a guest again. The source stays at ") + `<a href="/source/" target="_blank" rel="noopener" class="link">targa.me/source</a>` + dim(".")
}

// ── targascript.tg ──────────────────────────────────────────────────────

// targascript.tg — TargaScript, in TargaScript. The page itself runs on the
// TargaScript runtime, which can run another program in a fresh environment
// (targascript.run) or keep a session whose variables last between runs
// (targascript.session()). So the targascript command can run a .tg file
// from the virtual filesystem, evaluate inline code, or drop into a REPL
// with a persistent environment.

let tgSession = null

// tgRun runs src and returns what the program printed, plus the value of
// the last expression when wantValue is set (REPL behaviour). Without a
// value it runs in a fresh environment; with one it evaluates in session.
// Errors come from the runtime already formatted, with the source line.
fn tgRun(src, session, wantValue) {
    let buffer = ""
    let onOutput = fn(chunk) { buffer += chunk }
    let failure = null
    let result = null
    if wantValue {
        try {
            result = await session.eval(src, onOutput)
        } catch e {
            failure = e.message
        }
    } else {
        failure = await targascript.run(src, onOutput)
    }
    repeat buffer.endsWith("\n") { buffer = buffer.slice(0, -1) }
    let out = escapeHtml(buffer)
    if failure != null {
        lastStatus = 1
        if out != "" {
            out += "\n"
        }
        out += "<span class=\"red\">" + escapeHtml(failure) + "</span>"
    } else if wantValue && result != null {
        if out != "" {
            out += "\n"
        }
        out += dim(escapeHtml(toString(result)))
    }
    out
}

fn newTgEnv() { targascript.session() }

const tgUsage = `<span class="green">TargaScript</span> — a scripting language written in Go. This site is written in it,
running here in your browser on its WebAssembly runtime.

usage: targascript                 start a REPL (exit with :q, exit or Ctrl+D)
       targascript &lt;file.tg&gt;       run a script from this filesystem
       targascript -e '&lt;code&gt;'     evaluate code inline
       cat file.tg | targascript   read a script from a pipe

examples:   ls ~/projects/targascript
playground: <a href="https://script.targa.me/" target="_blank" rel="noopener" class="link">script.targa.me</a>
this site:  <a href="/source/" target="_blank" rel="noopener" class="link">targa.me/site.tg</a>
source:     <a href="https://github.com/migueltarga/TargaScript" target="_blank" rel="noopener" class="link">github.com/migueltarga/TargaScript</a>`

fn cmdTargaScript(c) {
    if c.has("-e") || c.has("--eval") {
        let code = []
        let skip = false
        repeat a in c.args {
            if skip {
                code.insert(a)
            } else if a == "-e" || a == "--eval" {
                skip = true
            }
        }
        let src = code.join(" ")
        if src == "" {
            return "usage: targascript -e '&lt;code&gt;'"
        }
        return tgRun(src, newTgEnv(), true)
    }
    if c.has("-h") || c.has("--help") {
        return tgUsage
    }
    let f = c.arg(0)
    if f != "" {
        let path = resolve(f)
        let e = find(path)
        if e == null {
            lastStatus = 1
            return errf("targascript", "cannot open '" + f + "': No such file or directory")
        }
        if e.dir {
            lastStatus = 1
            return errf("targascript", "'" + f + "' is a directory")
        }
        if denied(path, e) {
            lastStatus = 1
            return errf("targascript", "cannot open '" + f + "': Permission denied")
        }
        return tgRun(text(e), null, false)
    }
    if c.piped {
        return tgRun(c.stdin, null, false)
    }
    // REPL
    mode = "targascript"
    tgSession = newTgEnv()
    green("TargaScript REPL") + dim("  (persistent session; :q or exit to leave, :reset to clear variables)") + "\n" + dim("try: let x = [1, 2, 3]  then  x |> map(it * 2)")
}

fn tgReplInput(raw) {
    terminalContent += "<span class=\"prompt\">&gt;&gt;</span> " + escapeHtml(raw) + "\n"
    match raw.trim() {
        "" => null
        ":q", "exit", "quit", ":quit", ":exit" => {
            mode = "shell"
            terminalContent += dim("bye") + "\n"
        }
        ":reset" => {
            tgSession = newTgEnv()
            terminalContent += dim("environment reset") + "\n"
        }
        ":help", "help" => {
            terminalContent += tgUsage + "\n"
        }
        _ => {
            let out = tgRun(raw, tgSession, true)
            if out != "" {
                terminalContent += out + "\n"
            }
        }
    }
    render()
}

// tgExamples are the example programs shown under ~/projects/targascript.
// fs.tg builds its tree with them before this file's top-level values run,
// so the programs live inside the function.
fn tgExamples() {
    const tgHello = `// hello.tg — the basics
fn greet(name, greeting = "Hello") {
    "\${greeting}, \${name}!"
}

let person = {name: "Visitor", skills: ["curiosity", "typing commands"]}

print(greet(person.name))
print(greet("TargaScript", "Hi"))
repeat i, skill in person.skills {
    print("\${i + 1}. \${skill}")
}
print(person.address?.city ?? "no address")
`

    const tgFib = `// fib.tg — recursion, ranges and pipes
fn fib(n) {
    if n <= 1 { n } else { fib(n - 1) + fib(n - 2) }
}

repeat i in 0..14 {
    printf("fib(%2d) = %d", i, fib(i))
}

let total = 0..20 |> map(fib) |> sum
print("sum of the first 21: \${total}")
`

    const tgArrays = `// arrays.tg — pipes, it and arrow functions
let nums = 1..10

let squares = nums
    |> filter(it % 2 == 0)
    |> map(it * it)

print(squares)
print("sum: \${squares |> sum}")

let words = ["wasm", "terminal", "go", "repeat", "match"]
print(words.sortBy(w => w.length))
print(words.groupBy(w => if w.length > 4 { "long" } else { "short" }))
`

    const tgClosures = `// closures.tg — functions that remember
fn counter(start = 0) {
    let count = start
    () => {
        count++
        count
    }
}

let next = counter()
print(next(), next(), next())

let fromTen = counter(10)
print(fromTen())
`

    const tgShapes = `// shapes.tg — structs, enums and match
enum Kind { Circle, Square }

struct Shape {
    kind = Kind.Square, size = 1
    fn area() {
        match this.kind {
            Kind.Circle => 3.14159 * this.size ** 2
            Kind.Square => this.size ** 2
        }
    }
}

let shapes = [Shape(Kind.Circle, 2), Shape(Kind.Square, 3), Shape()]
repeat s in shapes {
    printf("%-14s size %d  area %6.2f", s.kind, s.size, s.area())
}

let {kind, size} = shapes[0]
print("the first is a \${kind.name} of size \${size}")
`

    [
        {name: "README.md", html: true, content: `<span class="green">TargaScript examples</span>
Run one with:   targascript hello.tg
Or explore:     targascript        (REPL)

  hello.tg      functions, defaults, interpolation, ?. and ??
  fib.tg        recursion, ranges, printf and |>
  arrays.tg     pipes, it, sortBy and groupBy
  closures.tg   functions that capture state
  shapes.tg     structs, enums and match

This whole site is written in TargaScript: read it at <a href="/source/" target="_blank" rel="noopener" class="link">targa.me/site.tg</a>.
Try more in the playground: <a href="https://script.targa.me/" target="_blank" rel="noopener" class="link">script.targa.me</a>`},
        {name: "hello.tg", content: tgHello},
        {name: "fib.tg", content: tgFib},
        {name: "arrays.tg", content: tgArrays},
        {name: "closures.tg", content: tgClosures},
        {name: "shapes.tg", content: tgShapes},
    ]
}

// ── llm.tg ──────────────────────────────────────────────────────────────

// llm.tg — a small language model running inside the visitor's browser,
// behind the ai (one-shot) and claude (chat) commands. build/llm.js wraps
// wllama (llama.cpp in wasm); models are downloaded from Hugging Face once and
// kept in the browser cache. Nothing leaves the machine and there is still no
// backend.

// A model is {id, name, size, url}. Pick with /model in claude. The first one
// is the default.
let llmModels = [
    {id: "qwen3-0.6b", name: "Qwen3 0.6B", size: "397 MB", url: "https://huggingface.co/unsloth/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q4_K_M.gguf"},
    {id: "qwen3-1.7b", name: "Qwen3 1.7B", size: "1.1 GB", url: "https://huggingface.co/unsloth/Qwen3-1.7B-GGUF/resolve/main/Qwen3-1.7B-Q4_K_M.gguf"},
    {id: "gemma3-1b", name: "Gemma 3 1B", size: "806 MB", url: "https://huggingface.co/unsloth/gemma-3-1b-it-GGUF/resolve/main/gemma-3-1b-it-Q4_K_M.gguf"},
    {id: "gemma3-270m", name: "Gemma 3 270M", size: "253 MB", url: "https://huggingface.co/unsloth/gemma-3-270m-it-GGUF/resolve/main/gemma-3-270m-it-Q4_K_M.gguf"},
    {id: "lfm2-350m", name: "LFM2 350M", size: "229 MB", url: "https://huggingface.co/unsloth/LFM2-350M-GGUF/resolve/main/LFM2-350M-Q4_K_M.gguf"},
]

let llmCur = llmModels[0] // selected model
let llmLoaded = "" // id of the model in memory, "" for none
let llmScript = false // llm.js has been injected
let llmWarned = false // the "it makes things up" footer was shown
let llmGen = 0 // bumps on every operation so stale callbacks are ignored

fn llmName() { llmCur.name }

fn llmSize() { llmCur.size }

// findModel is the model with that id, or null
fn findModel(id) {
    llmModels.find(m => m.id == id.lower())
}

// A message is {role, content}.

// LlmOp is one download or generation: an animation the visitor can Ctrl+C,
// plus the AbortController that stops the JS side when they do.
struct LlmOp {
    gen = 0
    ctl = null
    partial = "" // streamed text so far
    fromCache = false // the model file is local, so "progress" is a cache read
    retried = false // the model was reloaded once after vanishing mid-chat
    onFail = null // extra cleanup when the operation fails

    // live reports whether callbacks for this operation should still do anything
    fn live() { this.gen == llmGen && busy }

    fn fail(msg) {
        if !this.live() { return null }
        endAnim()
        if this.onFail != null {
            let f = this.onFail
            f()
        }
        // Whatever broke, the JS side starts over next time.
        llmLoaded = ""
        js.targaLLM.reset()
        if isOutOfSpace(msg) {
            terminalContent += "ai: " + yellow("your device ran out of room trying to fit a language model in its head.") + "\n"
            + "    Free up some space (photos of food, the 47 open tabs, that game from 2019) and try again.\n"
        } else {
            terminalContent += "ai: the model hiccupped. Try again.  " + dim(escapeHtml(msg)) + "\n"
        }
        if mode == "claude" {
            terminalContent += dim("Falling back to the classic terminal.") + "\n"
            leaveChat()
        }
        render()
    }

    // ensure makes sure the selected model is in memory, then calls ready.
    // When that needs a download and yes is false, consent is called instead
    // (animation ended).
    fn ensure(yes, ready, consent) {
        let op = this
        op.withScript(() => {
            if llmLoaded == llmCur.id {
                ready()
                return null
            }
            op.cached(urls => {
                if urls[llmCur.url] == true || yes {
                    op.fromCache = urls[llmCur.url] == true
                    op.loadModel(ready)
                    return null
                }
                endAnim()
                consent()
                render()
            })
        })
    }

    // cached looks up which model files are already in the browser cache
    fn cached(f) {
        let op = this
        then(js.targaLLM.cached(), v => {
            if !op.live() { return null }
            let urls = {}
            repeat u in v { urls[u] = true }
            f(urls)
        }, e => { op.fail(jsError(e)) })
    }

    // withScript runs f once llm.js is loaded
    fn withScript(f) {
        let op = this
        if llmScript {
            f()
            return null
        }
        let s = doc.createElement("script")
        s.src = "llm.js"
        s.onload = () => {
            llmScript = true
            f()
        }
        s.onerror = () => { op.fail("could not load llm.js") }
        doc.head.appendChild(s)
    }

    fn loadModel(ready) {
        let op = this
        let onProgress = (loaded, total) => {
            if !op.live() { return null }
            if total <= 0 { return null }
            let pct = toInt(toFloat(loaded) / toFloat(total) * 100)
            let bar = "#".repeat(pct / 5) + " ".repeat(20 - pct / 5)
            let mb = itoa(toInt(loaded / 1000000.0)) + "/" + itoa(toInt(total / 1000000.0)) + " MB"
            let verb = if op.fromCache { "reading cached " } else { "downloading " }
            animBuffer = verb + llmName() + " [" + bar + "] " + itoa(pct) + "%  " + mb + "\n"
            if pct >= 100 {
                animBuffer += dim("loading model...") + "\n"
            }
            render()
        }
        llmLoaded = ""
        then(js.targaLLM.load(llmCur.url, onProgress, op.ctl.signal), _ => {
            if !op.live() { return null }
            llmLoaded = llmCur.id
            ready()
        }, e => { op.fail(jsError(e)) })
    }

    // stream generates a reply to the conversation. show is called with the
    // text so far on every token (and once with "" at the start); done gets
    // the full text, with the animation still busy so the caller decides how
    // to end it.
    fn stream(messages, show, done) {
        let op = this
        op.partial = ""
        show("")
        let onToken = token => {
            if !op.live() { return null }
            op.partial += toString(token)
            show(op.partial)
        }
        then(js.targaLLM.chat(messages, onToken, op.ctl.signal), _ => {
            if !op.live() { return null }
            done(op.partial.trim())
        }, e => {
            let msg = jsError(e)
            // The model can vanish under memory pressure (the wasm worker
            // gets killed); bring it back once, quietly, before giving up.
            if !op.retried && op.live() && isNotLoaded(msg) {
                op.retried = true
                llmLoaded = ""
                op.ensure(true, () => { op.stream(messages, show, done) }, null)
                return null
            }
            op.fail(msg)
        })
    }
}

// llmBegin starts an operation. onCancel runs after Ctrl+C, once the
// animation is already torn down, and should commit whatever is worth keeping.
fn llmBegin(onCancel) {
    llmGen += 1
    let op = LlmOp(llmGen, js.AbortController.new())
    beginAnim(() => {
        op.ctl.abort()
        onCancel(op)
    }, "ctrl-c")
    op
}

// isOutOfSpace recognises the browser giving up on storage or memory: the
// DOMException wording ("unknown transient reason (e.g. out of memory)"),
// quota errors and wasm memory failures.
fn isOutOfSpace(msg) {
    let m = msg.lower()
    ["transient", "out of memory", "quota", "memory", "storage", "disk", "space", "allocate", "allocation"].some(w => m.contains(w))
}

fn isNotLoaded(msg) {
    let m = msg.lower()
    m.contains("loadmodel()") || m.contains("not loaded")
}

// The ai command: one question, one answer.
fn cmdAI(c) {
    let words = c.args.filter(a => a != "-y")
    let q = words.join(" ").trim()
    if q == "" && c.piped {
        q = c.stdin.trim()
    }
    if q == "" {
        return "ai: ask me something about Miguel. e.g. ai what do you work on\n" + dim("For a conversation, try: claude")
    }
    if isContactQuestion(q) {
        return contactInfo
    }
    if isInsult(q) {
        prank("")
        return ""
    }
    let op = llmBegin(op => {
        if op.partial != "" {
            terminalContent += escapeHtml(op.partial) + "\n"
        }
        terminalContent += "^C\n"
    })
    animBuffer = dim("thinking...") + "\n"
    render()
    let show = partial => {
        if partial != "" {
            animBuffer = escapeHtml(partial) + "\n"
            render()
        }
    }
    op.ensure(c.has("-y"), () => {
        op.stream([{role: "system", content: askFacts}, {role: "user", content: q}], show, text => {
            let out = escapeHtml(text)
            if out == "" {
                out = dim("(no answer; try rephrasing)")
            }
            terminalContent += out + "\n"
            if !llmWarned {
                llmWarned = true
                terminalContent += dim("(" + llmName() + ", running locally in your browser. It will make things up.)") + "\n"
            }
            endAnim()
        })
    }, () => {
        terminalContent += llmNotice() + "    Continue with:  " + yellow("ai -y " + escapeHtml(q)) + "\n"
    })
    ""
}

fn llmNotice() {
    "ai: runs " + llmName() + ", a small language model, inside your browser.\n"
    + "    First use downloads " + llmSize() + " (once; it stays in this browser's cache).\n"
}

// Small instruct models tend to deflect anything that looks like sharing
// personal details, so the one question worth getting right bypasses them.
fn isContactQuestion(q) {
    let l = q.lower()
    ["contact", "reach", "email", "e-mail", "mail", "linkedin", "github", "hire", "get in touch", "talk to"].some(w => l.contains(w))
}

// then wires callbacks to a JS promise: ok gets its value, fail its reason
fn then(p, ok, fail) {
    p.then(v => { ok(v) }, e => { fail(e) })
}

// jsError is the message of a JS error or rejection reason
fn jsError(e) {
    if type(e) == "string" { return e }
    if e != null {
        let m = try { e.message } catch err { null }
        if type(m) == "string" { return m }
    }
    "unknown error"
}

// ── chat.tg ─────────────────────────────────────────────────────────────

// chat.tg — the claude command: a conversation with the model, dressed up as
// the Claude Code CLI. The page switches to the chat look and back when the
// visitor leaves.

let chatHistory = [] // messages, {role, content}
let chatConsent = false // waiting for y/N before a download
let chatOnYes = null
let chatOnNo = null
let chatMode = 0 // Shift+Tab cycles: 0 default, 1 accept edits, 2 plan
let chatVerbs = ["Whisking", "Baking", "Brewing", "Pondering", "Simmering", "Marinating", "Percolating", "Cogitating", "Noodling", "Reticulating splines", "Compiling", "Hallucinating"]
let chatSpinner = ["·", "✢", "✳", "✶", "✻", "✽", "✻", "✶", "✳", "✢"]
let chatTips = [
    "Use /clear to start a fresh conversation",
    "Ask how to write a loop in TargaScript",
    "Everything runs in this tab; it works offline once downloaded",
    "Ctrl+C interrupts an answer, Ctrl+C again leaves",
    "The model has 0.6 billion parameters. Be gentle.",
    "Ask what Miguel works on, then ask a follow-up",
]

const chatHelp = `  /help    show this
  /model   list models, /model <name> to switch (downloads once)
  /clear   clear the screen and forget the conversation
  /exit    back to the shell (also Ctrl+C or Ctrl+D)
  Shift+Tab cycles modes: default, accept edits, plan
Anything else is sent to the model. It remembers the last few turns.`

fn chatWelcome() {
    `<span class="cl-box"><span class="cl-orange">✻</span> Welcome to <b>claude</b> (home edition)

  ` + llmName() + ` running locally in your browser. No backend, nothing leaves this tab.
  /help for commands · shift+tab to cycle modes · /exit or Ctrl+C to leave</span>` + "\n\n"
}

// chatStatus is the footer under the input, one line per mode like the real CLI
fn chatStatus() {
    match chatMode {
        1 => `<span class="cl-orange">⏵⏵ accept edits on</span>` + dim(" (shift+tab to cycle) · there are no edits, but it feels good")
        2 => `<span class="cl-plan">⏸ plan mode on</span>` + dim(" (shift+tab to cycle) · answers come as a plan")
        _ => `<span class="cl-orange">⏵⏵ local model on</span>` + dim(" (" + llmName() + " in your browser) · shift+tab to cycle · ctrl+c to leave")
    }
}

// chatCycleMode handles Shift+Tab
fn chatCycleMode() {
    chatMode = (chatMode + 1) % 3
    render()
}

fn chatEcho(raw) {
    `<div class="cl-user">&gt; ` + escapeHtml(raw) + `</div>` + "\n"
}

// The chat look (boxed input, status line) is body.claude in index.html
fn enterChat() {
    mode = "claude"
    chatConsent = false
    doc.body.classList.add("claude")
}

fn leaveChat() {
    mode = "shell"
    chatConsent = false
    chatOnYes = null
    chatOnNo = null
    doc.body.classList.remove("claude")
}

fn cmdClaude(c) {
    let q = c.args.join(" ").trim()
    enterChat()
    terminalContent = "" // takes over the screen, like the real one
    chatHistory = [] // and every session starts fresh
    chatMode = 0
    let op = llmBegin(_ => {
        terminalContent += "^C\n"
        leaveChat()
    })
    animBuffer = dim("starting...") + "\n"
    render()
    op.ensure(false, () => {
        endAnim()
        terminalContent += chatWelcome()
        render()
        if q != "" {
            chatInput(q)
        }
    }, () => {
        chatConsentFor(() => {
            terminalContent += chatWelcome()
        }, () => {
            terminalContent += dim("Maybe later. Back to the shell.") + "\n"
            leaveChat()
        }, null)
    })
    ""
}

// chatConsentFor asks before a download. On y the selected model is loaded
// and then loaded runs; anything else runs declined; failed runs if the
// download breaks (may be null).
fn chatConsentFor(loaded, declined, failed) {
    chatConsent = true
    terminalContent += "This downloads " + llmName() + " (" + llmSize() + ") once; it stays in this browser's cache. Continue? " + yellow("[y/N]") + "\n"
    chatOnYes = () => {
        let op = llmBegin(_ => {
            terminalContent += "^C\n"
            leaveChat()
        })
        op.onFail = failed
        op.loadModel(() => {
            endAnim()
            loaded()
            render()
        })
    }
    chatOnNo = declined
}

fn chatInput(raw) {
    raw = raw.trim()
    if chatConsent {
        terminalContent += chatEcho(raw)
        chatConsent = false
        let yes = chatOnYes
        let no = chatOnNo
        chatOnYes = null
        chatOnNo = null
        let l = raw.lower()
        if l == "y" || l == "yes" {
            yes()
        } else {
            no()
            render()
        }
        return null
    }
    if raw == "" {
        render()
        return null
    }
    terminalContent += chatEcho(raw)
    let l = raw.lower()
    if l in ["/exit", "/quit", "/q", "exit", "quit"] {
        terminalContent += dim("Goodbye!") + "\n"
        leaveChat()
    } else if l == "/clear" {
        terminalContent = ""
        chatHistory = []
    } else if l in ["/help", "help", "?"] {
        terminalContent += chatHelp + "\n\n"
    } else if l in ["/model", "/models"] {
        chatListModels()
        return null
    } else if l.startsWith("/model ") {
        chatSwitchModel(raw.slice(7).trim())
        return null
    } else if raw.startsWith("/") {
        terminalContent += dim("Unknown command " + escapeHtml(raw) + ". Try /help.") + "\n\n"
    } else {
        chatAsk(raw)
        return null
    }
    render()
}

// chatClock is "3:04 PM", the time in the footer of an answer
fn chatClock() {
    let t = timeFormat(now(), "hh:mm A")
    if t.startsWith("0") { t.slice(1) } else { t }
}

// chatPast turns the verb into the footer's past tense: Baking -> Baked
fn chatPast(verb) {
    let first = verb.split(" ")[0]
    if first.endsWith("ing") { first = first.slice(0, -3) }
    first + "ed"
}

fn chatAsk(q) {
    if isInsult(q) {
        prank("● ")
        return null
    }
    if isContactQuestion(q) {
        terminalContent += "● " + contactInfo + "\n\n"
        chatRemember(q, "You can reach Miguel at [email protected], github.com/migueltarga or linkedin.com/in/migueltarga.")
        render()
        return null
    }
    let verb = chatVerbs[rand(chatVerbs.length)]
    let tip = chatTips[rand(chatTips.length)]
    let start = clock()
    let tokens = 0
    let frame = 0
    let partial = ""

    let op = llmBegin(op => {
        if op.partial != "" {
            terminalContent += "● " + escapeHtml(op.partial.trim()) + "\n"
        }
        terminalContent += dim("Interrupted · Ctrl+C again to leave") + "\n\n"
        chatRemember(q, op.partial)
    })
    let draw = () => {
        let secs = itoa(toInt((clock() - start) / 1000))
        let buf = ""
        if partial != "" {
            buf = "● " + escapeHtml(partial) + "\n\n"
        }
        buf += `<span class="cl-orange">` + chatSpinner[frame % chatSpinner.length] + `</span> ` + verb + "… "
        + dim("(" + secs + "s · ↓ " + itoa(tokens) + " tokens)") + "\n" + dim("  Tip: " + tip) + "\n"
        animBuffer = buf
        render()
    }
    every(120, () => {
        frame += 1
        draw()
    })
    let msgs = [{role: "system", content: askFacts}] + chatHistory
    let ask = q
    if chatMode == 2 {
        ask += "\n\nAnswer as a short numbered plan, one step per line."
    }
    msgs.insert({role: "user", content: ask})
    op.stream(msgs, p => {
        if p != "" {
            tokens += 1
        }
        partial = p
        draw()
    }, text => {
        let secs = itoa(toInt((clock() - start) / 1000))
        let past = chatPast(verb)
        if text == "" {
            text = "(no answer; try rephrasing)"
        }
        let footer = "✻ " + past + " for " + secs + "s · done " + chatClock()
        if chatMode == 1 {
            footer += " · 0 edits accepted"
        }
        terminalContent += "● " + escapeHtml(text) + "\n\n" + dim(footer) + "\n\n"
        chatRemember(q, text)
        endAnim()
    })
}

// chatListModels prints the registry, marking what is downloaded and current
fn chatListModels() {
    let op = llmBegin(_ => { terminalContent += "^C\n\n" })
    op.withScript(() => {
        op.cached(urls => {
            endAnim()
            let b = dim("Models (all run in your browser; /model <name> to switch):") + "\n"
            repeat m in llmModels {
                let mark = "  "
                let note = ""
                if m.id == llmCur.id {
                    mark = `<span class="cl-orange">▸ </span>`
                    note = dim("  current")
                } else if urls[m.url] == true {
                    note = dim("  downloaded")
                }
                b += mark + yellow(pad(m.id, 13)) + pad(m.name, 14) + dim(pad(m.size, 8)) + note + "\n"
            }
            terminalContent += b + "\n"
            render()
        })
    })
}

// chatSwitchModel selects another model, downloading it after consent
fn chatSwitchModel(id) {
    let m = findModel(id)
    if m == null {
        terminalContent += dim("Unknown model " + escapeHtml(id) + ". /model lists them.") + "\n\n"
        render()
        return null
    }
    if m.id == llmCur.id {
        terminalContent += dim("Already using " + m.name + ".") + "\n\n"
        render()
        return null
    }
    let prev = llmCur
    llmCur = m
    let switched = () => {
        chatHistory = []
        terminalContent += dim("✻ Switched to " + m.name + ". The conversation starts over.") + "\n\n"
    }
    let op = llmBegin(_ => {
        llmCur = prev
        terminalContent += "^C\n\n"
    })
    op.onFail = () => { llmCur = prev }
    animBuffer = dim("loading " + m.name + "...") + "\n"
    render()
    op.ensure(false, () => {
        endAnim()
        switched()
        render()
    }, () => {
        chatConsentFor(switched, () => {
            llmCur = prev
            terminalContent += dim("Kept " + prev.name + ".") + "\n\n"
        }, () => { llmCur = prev })
    })
}

// isInsult spots people being rude about Miguel to the model
fn isInsult(q) {
    q = " " + q.lower() + " "
    let about = [
        "miguel", "targa", " he ", "he's", " his ", " you", "this guy", "the owner", "this site", "this website", "the author",
        " ele ", " dele", "esse cara", "o dono", "você", "voce", "esse site", "o autor",
    ].some(w => q.contains(w))
    if !about { return false }
    [
        "stupid", "idiot", "dumb", "suck", "loser", "ugly", "hate", "terrible", "awful", "worst", "trash", "garbage", "lame", "incompetent",
        "useless", "fraud", "noob", "moron", " bad", "overrated", "boring", "fake", "scam", "clown", "fail", "pathetic", "gay",
        // pt-BR
        "burro", "idiota", "otário", "otario", "babaca", "imbecil", "lixo", "horrível", "horrivel", "péssimo", "pessimo", "feio", "merda",
        "bosta", "porra", "caralho", "viado", "bicha", "corno", "fdp", "filho da puta", "puta", "vagabundo", "incompetente", "inútil", "inutil",
        "ridículo", "ridiculo", "nojento", "chato", "odeio", "trouxa", "mané", "cuzão", "cuzao", "arrombado", "escroto", "palhaço", "palhaco",
    ].some(w => q.contains(w))
}

// prank is what the rude get instead of an answer: a fake counter-hack with
// the screen going red, ending with them thrown out to a Google search on
// manners. prefix is "● " in chat, "" in the shell. The "locating" step is
// real-ish: Cloudflare's /cdn-cgi/trace (same origin, no backend) tells us
// the IP, country and nearest datacenter.
fn prank(prefix) {
    let root = doc.documentElement.style
    let before = toString(root.getPropertyValue("--fg"))
    root.setProperty("--fg", "#ff4444")
    let restore = () => { root.setProperty("--fg", before) }

    terminalContent += prefix + "Wrong answer.\n"
    beginAnim(() => {
        terminalContent += "^C\n"
        restore()
    }, "ctrl-c")
    render()

    let started = false
    let start = where => {
        if started || !busy { return null }
        started = true
        sequence([
            {delay: 500, text: `<span class="cl-orange">✻</span> Deploying countermeasures…` + "\n"},
            {delay: 900, text: "  ⎿ Locating you… " + where + "\n"},
            {delay: 900, text: "  ⎿ Scanning ports… " + dim("22/tcp open. Cute.") + "\n"},
            {delay: 900, text: "  ⎿ Reverse shell… " + dim("connected.") + "\n"},
            {delay: 700, text: "  ⎿ rm -rf ~/Documents … " + dim("done") + "\n"},
            {delay: 500, text: "  ⎿ rm -rf ~/Photos … " + dim("done") + "\n"},
            {delay: 500, text: "  ⎿ rm -rf ~/.ssh … " + dim("done (you didn't need those)") + "\n"},
            {delay: 1000, text: "  ⎿ Forwarding your browser history to your mother… " + dim("sent.") + "\n"},
            {delay: 1200, text: prefix + "Come back when you've learned some manners.\n"},
            {delay: 1200, text: "Connection to targa.me closed by remote host.\n"},
        ], true, () => {
            inputLine.style.display = "none" // stays "closed" until the redirect
            after(2500, () => {
                js.location.href = "https://www.google.com/search?q=how+to+be+nice+to+people"
            })
        })
        let interrupted = onCancel // Ctrl+C must not leave the screen red
        onCancel = () => {
            interrupted()
            restore()
        }
    }
    let fallback = _ => { start(dim("found (somewhere on Earth)")) }
    after(2500, () => { fallback(null) })
    then(js.fetch("/cdn-cgi/trace"), resp => {
        then(resp.text(), t => {
            let [ip, loc, colo] = traceFields(toString(t))
            if ip == "" {
                fallback(null)
                return null
            }
            start(traceWhere(ip, loc, colo))
        }, fallback)
    }, fallback)
}

// traceFields picks [ip, loc, colo] out of a /cdn-cgi/trace response
fn traceFields(t) {
    let ip = ""
    let loc = ""
    let colo = ""
    repeat line in t.split("\n") {
        let i = line.indexOf("=")
        let k = if i < 0 { line } else { line.slice(0, i) }
        let v = if i < 0 { "" } else { line.slice(i + 1) }
        match k {
            "ip" => { ip = v }
            "loc" => { loc = v }
            "colo" => { colo = v }
            _ => null
        }
    }
    [ip, loc, colo]
}

// traceWhere is the "Locating you…" text: the IP, the country and the city
// of the datacenter
fn traceWhere(ip, loc, colo) {
    let where = yellow(escapeHtml(ip))
    if loc != "" {
        where += ", " + escapeHtml(loc)
    }
    let city = if colos.has(colo) { colos[colo] } else { null }
    if city != null {
        where += ", near " + city
    } else if colo != "" {
        where += ", near " + escapeHtml(colo)
    }
    where
}

// Cloudflare datacenters (IATA codes) visitors are likely to hit
let colos = {
    MIA: "Miami", ATL: "Atlanta", DFW: "Dallas", ORD: "Chicago", IAD: "Washington", EWR: "Newark", JFK: "New York",
    LAX: "Los Angeles", SJC: "San Jose", SEA: "Seattle", DEN: "Denver", PHX: "Phoenix", BOS: "Boston", YYZ: "Toronto", YVR: "Vancouver",
    GRU: "São Paulo", GIG: "Rio de Janeiro", CNF: "Belo Horizonte", BSB: "Brasília", CWB: "Curitiba", POA: "Porto Alegre",
    FOR: "Fortaleza", REC: "Recife", SSA: "Salvador", EZE: "Buenos Aires", SCL: "Santiago", LIM: "Lima", BOG: "Bogotá", MEX: "Mexico City",
    LHR: "London", CDG: "Paris", AMS: "Amsterdam", FRA: "Frankfurt", MAD: "Madrid", LIS: "Lisbon", MXP: "Milan", DUB: "Dublin",
    WAW: "Warsaw", STO: "Stockholm", ZRH: "Zurich", VIE: "Vienna", IST: "Istanbul", DXB: "Dubai", BOM: "Mumbai", DEL: "Delhi",
    SIN: "Singapore", HKG: "Hong Kong", NRT: "Tokyo", ICN: "Seoul", SYD: "Sydney", MEL: "Melbourne", AKL: "Auckland",
    JNB: "Johannesburg", CPT: "Cape Town",
}

// chatRemember keeps the last few turns so follow-up questions make sense
fn chatRemember(q, a) {
    chatHistory.insert({role: "user", content: q})
    chatHistory.insert({role: "assistant", content: a})
    if chatHistory.length > 8 {
        chatHistory = chatHistory.slice(chatHistory.length - 8)
    }
}

// ── main.tg ─────────────────────────────────────────────────────────────

// main.tg — the shell: parsing (quotes, $VARS, pipes, && and ;), history,
// Tab completion, the keyboard and the boot. It comes last, after the files
// it uses.

let doc = js.document
let terminal = null
let inputLine = null
let inputField = null
let promptEl = null
let statusEl = null

let terminalContent = ""
let cwd = home
let history = []
let historyIndex = 0
let draft = ""
let lastStatus = 0
let mode = "shell" // shell, vim, closed, targascript or claude
let closedMessage = ""
let env = {
    USER: "guest", HOME: home, SHELL: "/bin/tsh", TERM: "xterm-256color",
    LANG: "en_US.UTF-8", PATH: "/usr/local/bin:/usr/bin:/bin:/usr/games",
    EDITOR: "vim", HOSTNAME: "targa.me", PWD: home,
}

// ---------------------------------------------------------------------------
// The context of a command and the command table
// ---------------------------------------------------------------------------

struct Ctx {
    args = [], stdin = "", piped = false, raw = ""

    // arg is the i-th argument that isn't a flag, or ""
    fn arg(i) {
        this.args.filter(a => !(a.startsWith("-") && a.length > 1))[i] ?? ""
    }

    // flag reports whether a short flag, like l in -la, was given
    fn flag(letter) {
        this.args.some(a => a.startsWith("-") && !a.startsWith("--") && a.slice(1).contains(letter))
    }

    fn has(word) { word in this.args }

    // input is what a command reads: the file named by argument i, or what
    // came through the pipe. It returns [text, error].
    fn input(cmd, i) {
        let p = this.arg(i)
        if p != "" {
            let path = resolve(p)
            let e = find(path)
            if e == null { return ["", errf(cmd, p + ": No such file or directory")] }
            if e.dir { return ["", errf(cmd, p + ": Is a directory")] }
            if denied(path, e) { return ["", errf(cmd, p + ": Permission denied")] }
            return [text(e), ""]
        }
        if this.piped { return [this.stdin, ""] }
        ["", errf(cmd, "missing file operand")]
    }
}

// A command is {name, usage, desc, cat, hidden, run}, with cat one of core,
// files, text, system, network, fun or egg. Eggs are hidden: not in help,
// completion, man or /bin.
let commands = coreCommands + eggCommands
let commandIndex = commands.map(c => [c.name, c]).toObject()

let aliases = {
    ll: "ls -l", la: "ls -a", please: "sudo", cls: "clear", dir: "ls", matrix: "cmatrix",
    vi: "vim", nvim: "vim", more: "cat", less: "cat", printenv: "env", screenfetch: "neofetch",
    gpt: "ai", chatgpt: "ai", "ai,": "ai", "ai:": "ai", ip: "ifconfig", ss: "netstat", htop: "top",
    python3: "python", nodejs: "node", "apt-get": "apt", poweroff: "shutdown", halt: "shutdown",
    logout: "exit", "?": "help", tg: "targascript", ts: "targascript", tgs: "targascript", calc: "bc", expr: "bc", hexdump: "xxd", hd: "xxd",
}

// ---------------------------------------------------------------------------
// Parsing: quotes, variables, pipes and separators
// ---------------------------------------------------------------------------

fn tokenize(s) {
    let out = []
    let cur = ""
    let quote = ""
    let inToken = false
    repeat ch in s {
        if quote != "" {
            if ch == quote { quote = "" } else { cur += ch }
        } else if ch == "\"" || ch == "'" {
            quote = ch
            inToken = true
        } else if ch == " " || ch == "\t" {
            if inToken {
                out.insert(cur)
                cur = ""
                inToken = false
            }
        } else {
            cur += ch
            inToken = true
        }
    }
    if inToken { out.insert(cur) }
    out
}

fn isWordChar(c) {
    c == "_" || c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c >= "0" && c <= "9"
}

fn expandVars(s) {
    if !s.contains("$") { return s }
    let chars = s.chars()
    let out = ""
    let i = 0
    repeat i < chars.length {
        if chars[i] != "$" || i == chars.length - 1 {
            out += chars[i]
            i += 1
            continue
        }
        let j = i + 1
        let braced = chars[j] == "{"
        if braced { j += 1 }
        let k = j
        repeat k < chars.length && (isWordChar(chars[k]) || k == j && chars[k] == "?") {
            k += 1
            if chars[k - 1] == "?" { break }
        }
        let name = chars.slice(j, k).join("")
        if braced && k < chars.length && chars[k] == "}" { k += 1 }
        if name == "" {
            out += "$"
            i += 1
            continue
        }
        out += lookupVar(name)
        i = k
    }
    out
}

fn lookupVar(name) {
    match name {
        "?" => toString(lastStatus)
        "RANDOM" => toString(rand(32768))
        "PWD" => cwd
        _ => env[name] ?? ""
    }
}

// splitTop splits s at sep where it isn't inside quotes
fn splitTop(s, sep) {
    let out = []
    let quote = ""
    let start = 0
    let i = 0
    repeat i < s.length {
        let ch = s[i]
        if quote != "" {
            if ch == quote { quote = "" }
        } else if ch == "\"" || ch == "'" {
            quote = ch
        } else if s.slice(i).startsWith(sep) {
            out.insert(s.slice(start, i))
            start = i + sep.length
            i = start
            continue
        }
        i += 1
    }
    out.insert(s.slice(start))
    out
}

// ---------------------------------------------------------------------------
// Running commands
// ---------------------------------------------------------------------------

fn promptText() {
    match mode {
        "vim", "closed" => ""
        "targascript" => ">>"
        "claude" => ">"
        _ => display(cwd) + if root { " #" } else { " $" }
    }
}

// statusText is the Claude Code style line under the input, in the chat
fn statusText() {
    if mode != "claude" { return "" }
    chatStatus()
}

fn echoLine(raw) {
    "<span class=\"prompt\">" + promptText() + "</span> " + escapeHtml(raw) + "\n"
}

// submit handles the Enter key
fn submit(typed) {
    let raw = typed.trim()
    match mode {
        "closed" => {
            mode = "shell"
            terminalContent = banner
            render()
            return null
        }
        "vim" => {
            vimInput(raw)
            return null
        }
        "targascript" => {
            tgReplInput(raw)
            return null
        }
        "claude" => {
            chatInput(raw)
            return null
        }
        _ => null
    }
    terminalContent += echoLine(raw)
    if raw == "" {
        render()
        return null
    }
    // History expansion
    if raw == "!!" && history.length > 0 {
        raw = history.last()
        terminalContent += escapeHtml(raw) + "\n"
    } else if raw.startsWith("!") && raw.length > 1 {
        let n = try { toInt(raw.slice(1)) } catch e { 0 }
        if n >= 1 && n <= history.length {
            raw = history[n - 1]
            terminalContent += escapeHtml(raw) + "\n"
        }
    }
    history.insert(raw)
    historyIndex = history.length
    draft = ""
    let out = execLine(raw)
    if out != "" {
        terminalContent += out + "\n"
    }
    render()
}

// execLine runs a whole command line: a && b; c | d
fn execLine(raw) {
    if raw.contains(":(){") {
        lastStatus = 1
        return red("tsh: fork bomb detected. Nice try.")
    }
    let typed = raw.trim()
    if typed.startsWith("targa{") && sha256Hex(typed) == flagHash {
        return cmdFlagFound()
    }
    let out = []
    repeat chunk in splitTop(raw, ";") {
        repeat segment in splitTop(chunk, "&&") {
            let seg = segment.trim()
            if seg == "" { continue }
            let r = execPipeline(seg)
            if r != "" { out.insert(r) }
            if lastStatus != 0 { break }
        }
    }
    out.join("\n")
}

fn execPipeline(seg) {
    if seg.contains(">") && !seg.contains("->") {
        lastStatus = 1
        return red("tsh: cannot create file: Read-only file system")
    }
    let out = ""
    repeat i, stage in splitTop(seg, "|") {
        let args = tokenize(stage).map(expandVars)
        if args.length == 0 {
            lastStatus = 2
            return red("tsh: syntax error near unexpected token '|'")
        }
        let name = (if args[0].startsWith("/") { args[0].slice(1) } else { args[0] }).lower()
        let alias = aliases[name]
        if alias != null {
            args = tokenize(alias) + args.slice(1)
            name = args[0]
        }
        let c = commandIndex[name]
        if c == null {
            lastStatus = 127
            return red("tsh: " + escapeHtml(args[0]) + ": command not found")
        }
        lastStatus = 0
        out = c.run(Ctx(args.slice(1), unescape(stripTags(out)), i > 0, stage))
    }
    out
}

fn render() {
    terminal.innerHTML = terminalContent + animBuffer
    terminal.scrollTop = terminal.scrollHeight
    promptEl.textContent = promptText()
    promptEl.className = if root && mode == "shell" { "root" } else { "" }
    let status = statusText()
    statusEl.innerHTML = status
    statusEl.hidden = status == ""
}

// ---------------------------------------------------------------------------
// The keyboard
// ---------------------------------------------------------------------------

fn selecting() { inputField.selectionStart != inputField.selectionEnd }

fn handleKey(event) {
    let key = event.key
    let ctrl = event.ctrlKey
    let meta = event.metaKey || event.altKey

    if interrupt(key, ctrl) {
        if !(ctrl && key == "c") || !selecting() {
            event.preventDefault()
        }
        return null
    }

    // Typing anywhere on the page goes to the input
    if !js.Object.is(event.target, inputField) && !ctrl && !meta && key.length == 1 {
        inputField.focus()
        return null
    }

    if ctrl && key == "c" {
        if selecting() { return null } // copying selected text
        event.preventDefault()
        match mode {
            "vim" => { terminalContent += "Type  :qa!  and press <Enter> to abandon all changes and exit Vim\n" }
            "targascript" => { terminalContent += "<span class=\"prompt\">&gt;&gt;</span> " + escapeHtml(inputField.value) + "^C\n" }
            "claude" => {
                terminalContent += chatEcho(inputField.value + "^C") + "\n"
                leaveChat()
            }
            _ => { terminalContent += echoLine(inputField.value + "^C") }
        }
        inputField.value = ""
        historyIndex = history.length
        render()
    } else if ctrl && key == "l" {
        event.preventDefault()
        terminalContent = if mode == "vim" { vimScreen() } else { "" }
        render()
    } else if ctrl && key == "d" {
        event.preventDefault()
        if mode == "targascript" {
            tgReplInput("exit")
        } else if mode == "claude" {
            chatInput("/exit")
        } else if mode == "shell" && inputField.value == "" {
            terminalContent += echoLine("")
            terminalContent += execLine("exit") + "\n"
            render()
        }
    } else if key == "Enter" {
        let raw = inputField.value
        inputField.value = ""
        submit(raw)
    } else if key == "ArrowUp" {
        event.preventDefault()
        if historyIndex > 0 {
            if historyIndex == history.length { draft = inputField.value }
            historyIndex -= 1
            inputField.value = history[historyIndex]
        }
    } else if key == "ArrowDown" {
        event.preventDefault()
        if historyIndex < history.length {
            historyIndex += 1
            inputField.value = if historyIndex == history.length { draft } else { history[historyIndex] }
        }
    } else if key == "Tab" {
        event.preventDefault()
        if mode == "shell" {
            complete()
        } else if mode == "claude" && event.shiftKey {
            chatCycleMode()
        }
    }
}

// complete does Tab completion of command names and paths
fn complete() {
    let current = inputField.value
    // Only the last stage of a pipeline matters
    let stageStart = 0
    repeat sep in ["|", "&&", ";"] {
        let i = lastIndex(current, sep)
        if i >= 0 && i + sep.length > stageStart { stageStart = i + sep.length }
    }
    let stage = current.slice(stageStart)
    let trimmed = stage.trimStart()
    let space = lastIndex(trimmed, " ")

    let matches = []
    let shown = []
    let prefix = current.slice(0, stageStart) + stage.slice(0, stage.length - trimmed.length)

    if space < 0 {
        repeat c in commands {
            if !c.hidden && c.name.startsWith(trimmed.lower()) {
                matches.insert(c.name + " ")
                shown.insert(c.name)
            }
        }
    } else {
        prefix += trimmed.slice(0, space + 1)
        let arg = trimmed.slice(space + 1)
        let [dirPart, base] = splitPath(arg)
        let dir = cwd
        if arg.contains("/") {
            if dirPart == "" { dirPart = "/" }
            dir = resolve(dirPart)
            if !dirPart.endsWith("/") { dirPart += "/" }
        } else if arg == "~" {
            matches.insert("~/")
            shown.insert("~/")
        }
        repeat e in children(dir) {
            if e.name.startsWith(base) && (base.startsWith(".") || !e.name.startsWith(".")) {
                if e.dir {
                    matches.insert(dirPart + e.name + "/")
                    shown.insert(e.name + "/")
                } else {
                    matches.insert(dirPart + e.name + " ")
                    shown.insert(e.name)
                }
            }
        }
    }

    if matches.length == 1 {
        inputField.value = prefix + matches[0]
    } else if matches.length > 1 {
        inputField.value = prefix + commonPrefix(matches)
        terminalContent += echoLine(current) + dim(shown.join("  ")) + "\n"
        render()
    }
}

fn commonPrefix(items) {
    let p = items[0]
    repeat s in items.slice(1) {
        repeat !s.startsWith(p) { p = p.slice(0, -1) }
    }
    p
}

// ---------------------------------------------------------------------------
// Boot
// ---------------------------------------------------------------------------

fn mount() {
    let body = doc.body
    doc.getElementById("fallback")?.remove()

    terminal = doc.createElement("div")
    terminal.id = "terminal"
    terminal.setAttribute("aria-live", "polite")
    body.appendChild(terminal)

    inputLine = doc.createElement("div")
    inputLine.id = "input-line"

    promptEl = doc.createElement("label")
    promptEl.id = "prompt"
    promptEl.setAttribute("for", "input")
    inputLine.appendChild(promptEl)

    inputField = doc.createElement("input")
    inputField.id = "input"
    inputField.type = "text"
    inputField.setAttribute("autocomplete", "off")
    inputField.setAttribute("autocapitalize", "none")
    inputField.setAttribute("spellcheck", "false")
    inputField.setAttribute("aria-label", "Terminal input")
    inputLine.appendChild(inputField)
    body.appendChild(inputLine)

    statusEl = doc.createElement("div")
    statusEl.id = "status"
    body.appendChild(statusEl)

    doc.addEventListener("keydown", handleKey)

    // On phones the on-screen keyboard shrinks the visual viewport but not
    // 100dvh, hiding the input behind it: size the page to what is visible
    let viewport = js.visualViewport
    if viewport != null {
        let fit = () => {
            body.style.height = "${toInt(viewport.height)}px"
            js.scrollTo(0, 0)
            terminal.scrollTop = terminal.scrollHeight
        }
        viewport.addEventListener("resize", fit)
        viewport.addEventListener("scroll", fit)
    }

    // Coming back, as from the prank's redirect, must not bring the old
    // screen back from the back-forward cache: start over instead
    js.addEventListener("pageshow", event => {
        if event.persisted { js.location.reload() }
    })

    // Clicking empty space focuses the input again, but never at the cost
    // of a text selection or a link click
    body.addEventListener("click", event => {
        if event.target.tagName == "A" { return null }
        if js.getSelection().toString() != "" { return null }
        inputField.focus()
    })

    terminalContent = banner
    render()
    inputField.focus()
}

populateBin()
mount()