Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
Zig
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470const std = @import("std");const builtin = @import("builtin");const io_mod = @import("../shared/io.zig");const app_lifecycle = @import("../app/app_lifecycle.zig");const background_record_liveness = @import("../background/background_record_liveness.zig");const background_store = @import("../background/background_store.zig");const chatgpt_oauth = @import("../auth/chatgpt_oauth.zig");const grok_oauth = @import("../auth/grok_oauth.zig");const acp_runner = @import("acp_runner.zig");const cli_ask = @import("cli_ask.zig");const cli_replay = @import("cli_replay.zig");const command_specs = @import("../slash_commands/command_specs.zig");const collections = @import("../shared/collections.zig");const config_runtime = @import("../config/config_runtime.zig");const credentials = @import("../auth/credentials.zig");const model_provider = @import("../config/model_provider.zig");const debug_trace = @import("../shared/debug_trace.zig");const doctor_runtime = @import("doctor_runtime.zig");const gateway_provider = @import("../gateway/gateway_provider.zig");const model_catalog = @import("../gateway/model_catalog.zig");const agent_stream_provider = @import("../agent/stream_provider.zig");const background_process_provider = @import( "../execution/background_process_provider.zig",);const github_publish = @import("../github/github_publish.zig");const github_workflows = @import("../github/github_workflows.zig");const host = @import("../hosts/host.zig");const login_flow = @import("../auth/login_flow.zig");const oauth_transport = @import("../auth/oauth_transport.zig");const provider_catalog = @import("../auth/provider_catalog.zig");const secret = @import("../auth/secret.zig");const output_contracts = @import("../output/output_contracts.zig");const permission_auto_classifier = @import("../permissions/auto_classifier.zig");const prompt_policy = @import("../config/prompt_policy.zig");const session_store = @import("../session/session_store.zig");const usage_report = @import("../session/usage_report.zig");const skill_contract = @import("../skills/skill_contract.zig");const types = @import("../shared/types.zig");const update_target = @import("../upgrade/update_target.zig");const test_builtin_gateway = if (builtin.is_test) @import("../../builtins/gateway.zig")else struct {};const context_contract = @import("../workspace/context_contract.zig");const mode_registry = @import("../modes/mode_registry.zig");const mcp_contract = @import("../mcp/mcp_contract.zig");const mcp_runtime = @import("../mcp/mcp_runtime.zig");const tool_set_contract = @import("../tooling/tool_set.zig");const workspace_access = @import("../workspace/workspace_access.zig");const workspace_commands = @import("../workspace/workspace_commands.zig");const usage_cli_runtime = @import("usage_cli_runtime.zig");
const Allocator = std.mem.Allocator;const CommandCatalog = command_specs.TopLevelRegistry;const TopLevelKind = command_specs.TopLevelKind;
pub const Command = union(enum) { interactive, help, ask: []const [:0]const u8, acp: []const [:0]const u8, pr: []const [:0]const u8, issue: []const [:0]const u8, login: []const [:0]const u8, logout: []const [:0]const u8, setup: []const [:0]const u8, status: []const [:0]const u8, permissions: []const [:0]const u8, models: []const [:0]const u8, provider: []const [:0]const u8, doctor: []const [:0]const u8, background: []const [:0]const u8, teams: []const [:0]const u8, session: []const [:0]const u8, sessions: []const [:0]const u8, resume_session: ResumeInvocation, credits: []const [:0]const u8, usage: []const [:0]const u8, upgrade: []const [:0]const u8, replay: []const [:0]const u8, workspace: []const [:0]const u8, unknown: []const u8,};
const ResumeInvocation = struct { args: []const [:0]const u8, top_level_alias: bool = false,};
const resume_id_alias_prefix = "--resume-";pub const upgrade_relaunch_arg = "--upgrade-relaunch";
// The one resume alias that asks which session to open. Every other spelling// names its target, so it resumes without a prompt.const resume_picker_alias = "-r";
pub const ResumeTarget = union(enum) { pick, last, id: []u8,
pub fn deinit(self: *ResumeTarget, alloc: Allocator) void { switch (self.*) { .pick, .last => {}, .id => |value| alloc.free(value), } self.* = undefined; }};
pub const LaunchModifiers = struct { context_limit_overrides: []config_runtime.context_limits.Override = &.{}, additional_directories: [][]u8 = &.{}, saved_directories_suppressed: bool = false,
pub fn deinit(self: *LaunchModifiers, alloc: Allocator) void { if (self.context_limit_overrides.len > 0) alloc.free(self.context_limit_overrides); for (self.additional_directories) |path| alloc.free(path); if (self.additional_directories.len > 0) alloc.free(self.additional_directories); self.* = .{}; }
pub fn hasWorkspaceModifiers(self: LaunchModifiers) bool { return self.additional_directories.len > 0 or self.saved_directories_suppressed; }};
pub const InteractiveLaunch = struct { requested_resume: ?ResumeTarget = null, upgrade_relaunch: bool = false, record_requested: bool = false, modifiers: LaunchModifiers = .{},
pub fn deinit(self: *InteractiveLaunch, alloc: Allocator) void { if (self.requested_resume) |*target| target.deinit(alloc); self.modifiers.deinit(alloc); self.* = undefined; }};
pub const RunResult = union(enum) { interactive: InteractiveLaunch, handled_success, handled_failure, handled_exit: u8,};
pub const record_modifier_usage = "usage: fx --record is only supported for interactive startup\n";const version_usage = "usage: fx --version\n";
pub fn recordRequested(args: []const [:0]const u8) error{RecordModifierRequiresInteractive}!bool { var count: usize = 0; for (args) |arg| { if (std.mem.eql(u8, arg, "--record")) count += 1; } if (count == 0) return false; if (count != 1) return error.RecordModifierRequiresInteractive; if (args.len == 1 and std.mem.eql(u8, args[0], "--record")) return true; if (args.len >= 2 and (std.mem.eql(u8, args[0], "resume") or std.mem.eql(u8, args[0], "--resume")) and std.mem.eql(u8, args[args.len - 1], "--record")) return true; if (args.len >= 3 and std.mem.eql(u8, args[0], "session") and std.mem.eql(u8, args[1], "resume") and std.mem.eql(u8, args[args.len - 1], "--record")) return true; return error.RecordModifierRequiresInteractive;}
pub const Config = struct { version: []const u8 = "", revision: []const u8 = "", build_channel: update_target.Channel = .stable, command_catalog: CommandCatalog, default_model: []const u8, default_agent_step_limit: usize, models_path: []const u8, gateway_retry_count: usize, gateway_chat_url: []const u8, gateway_provider: gateway_provider.Provider, codex_agent_stream: ?agent_stream_provider.Provider = null, codex_cli_model_catalog: ?gateway_provider.CliModelCatalogProvider = null, codex_model_catalog: ?model_catalog.Provider = null, grok_agent_stream: ?agent_stream_provider.Provider = null, grok_cli_model_catalog: ?gateway_provider.CliModelCatalogProvider = null, grok_model_catalog: ?model_catalog.Provider = null, background_process_provider: background_process_provider.Provider = background_process_provider.unavailable_provider, url_opener: host.UrlOpener, secret_store: host.SecretStore, prompt_policy: prompt_policy.Policy, skill_root_policy: skill_contract.RootPolicy, ignored_list_entries: []const []const u8, max_list_entries: usize, max_read_file_bytes: usize, max_read_file_lines: usize, max_read_file_line_len: usize, max_command_output_bytes: usize, max_tool_result_bytes: usize, max_history_turns: usize, context_registry: context_contract.Registry, mode_registry: mode_registry.Registry, tool_set: tool_set_contract.ToolSet, inspect_mcp_profile_config: mcp_contract.InspectProfileConfigFn, load_mcp_runtime: mcp_runtime.LoadRuntimeFn, acp_runner: acp_runner.Runner, permission_reviewer_provider: ?permission_auto_classifier.Provider = null, codex_permission_reviewer_provider: ?permission_auto_classifier.Provider = null, grok_permission_reviewer_provider: ?permission_auto_classifier.Provider = null,};
const LocalSurfaceOptions = struct { format: output_contracts.OutputFormat = .text,};
fn parseLoginProvider(rest: []const [:0]const u8) !?provider_catalog.Id { if (rest.len == 0) return null; if (rest.len != 1) return error.InvalidLoginProviderArgs; return provider_catalog.parse(rest[0]) orelse error.InvalidLoginProviderArgs;}
fn selectCatalogModel( entries: []const model_catalog.ModelCatalogEntry, saved: ?[]const u8,) ?[]const u8 { if (saved) |candidate| { for (entries) |entry| { if (std.mem.eql(u8, entry.id, candidate)) return entry.id; } } return if (entries.len > 0) entries[0].id else null;}
const UpgradeOptions = struct { format: output_contracts.OutputFormat = .text, channel: ?update_target.Channel = null,};
const SessionListOptions = struct { format: output_contracts.OutputFormat = .text, scope: session_store.SessionListScope = .current_workspace, limit: usize = session_store.session_list_default_limit, continuation: ?session_store.ResumableSessionContinuation = null,};
const UsageOptions = struct { format: output_contracts.OutputFormat = .text, scope: usage_report.Scope = .days_30,};
const WorkspaceOptions = struct { format: output_contracts.OutputFormat = .text, action: ?workspace_commands.Action = null,};
const PersistedRecordTarget = union(enum) { last, id: u64,};
const PersistedRecordOptions = struct { format: output_contracts.OutputFormat = .text, target: ?PersistedRecordTarget = null,};
// `fx session` reads one saved session, so it names its target and never// reaches for the picker that `ResumeTarget` carries.const SessionDetailTarget = union(enum) { last, id: []u8,
fn deinit(self: *SessionDetailTarget, alloc: Allocator) void { switch (self.*) { .last => {}, .id => |value| alloc.free(value), } self.* = undefined; }};
const SessionDetailOptions = struct { format: output_contracts.OutputFormat = .text, target: ?SessionDetailTarget = null,
fn deinit(self: *SessionDetailOptions, alloc: Allocator) void { if (self.target) |*target| target.deinit(alloc); self.* = undefined; }};
const SessionMigrationOptions = struct { format: output_contracts.OutputFormat = .text, session_id: []u8, allow_large: bool = false,
fn deinit(self: *SessionMigrationOptions, alloc: Allocator) void { alloc.free(self.session_id); self.* = undefined; }};
const SessionRecoveryOptions = struct { format: output_contracts.OutputFormat = .text, session_id: []u8,
fn deinit(self: *SessionRecoveryOptions, alloc: Allocator) void { alloc.free(self.session_id); self.* = undefined; }};
const AcpOptions = struct { model: ?[]const u8 = null, log_file: ?[]const u8 = null,};
const WorkflowOptions = struct { auto_permission: bool, create: bool, context: []u8,
fn deinit(self: WorkflowOptions, alloc: Allocator) void { alloc.free(self.context); }};
const WriteFn = *const fn (?*anyopaque, []const u8) anyerror!void;const LoadStartupStateFn = *const fn (Allocator, oauth_transport.Provider, host.SecretStore, []const u8, usize) anyerror!app_lifecycle.StartupState;const LoadCatalogStartupStateFn = *const fn (Allocator, host.SecretStore, []const u8, usize) anyerror!app_lifecycle.StartupState;const LoadStartupStateWithoutCredentialsFn = *const fn (Allocator, []const u8, usize) anyerror!app_lifecycle.StartupState;const LoadStartupStatusFn = *const fn (Allocator, host.SecretStore, []const u8, usize) anyerror!app_lifecycle.StartupStatus;const GetenvFn = *const fn (?*anyopaque, []const u8) ?[]const u8;const EnvironMapFn = *const fn (?*anyopaque) ?*const std.process.Environ.Map;const SelfExePathFn = *const fn (?*anyopaque, Allocator) anyerror![]u8;const ReadMaskedKeyFn = *const fn (?*anyopaque, Allocator, WriteFn, ?*anyopaque) anyerror![]u8;const SetupTerminalAvailableFn = *const fn (?*anyopaque) bool;const RunDeps = struct { stdout_ctx: ?*anyopaque = null, stderr_ctx: ?*anyopaque = null, env_ctx: ?*anyopaque = null, self_exe_ctx: ?*anyopaque = null, setup_ctx: ?*anyopaque = null, write_stdout: WriteFn = writeRealStdout, write_stderr: WriteFn = writeRealStderr, load_startup_state: LoadStartupStateFn = app_lifecycle.loadStartupState, load_catalog_startup_state: LoadCatalogStartupStateFn = app_lifecycle.loadCatalogStartupState, load_startup_state_without_credentials: LoadStartupStateWithoutCredentialsFn = app_lifecycle.loadStartupStateWithoutCredentials, load_startup_status: LoadStartupStatusFn = app_lifecycle.loadStartupStatus, getenv: GetenvFn = getenvDefault, environ_map: EnvironMapFn = environMapDefault, self_exe_path: SelfExePathFn = selfExePathDefault, read_masked_key: ReadMaskedKeyFn = readMaskedKeyDefault, setup_terminal_available: SetupTerminalAvailableFn = setupTerminalAvailableDefault,};
const GlobalLaunchArgs = struct { remaining: []const [:0]const u8, modifiers: LaunchModifiers = .{},
fn deinit(self: *GlobalLaunchArgs, alloc: Allocator) void { self.modifiers.deinit(alloc); self.* = undefined; }
fn takeModifiers(self: *GlobalLaunchArgs) LaunchModifiers { const result = self.modifiers; self.modifiers = .{}; return result; }};
fn parseGlobalLaunchArgs( alloc: Allocator, args: []const [:0]const u8,) !GlobalLaunchArgs { var overrides: std.ArrayList(config_runtime.context_limits.Override) = .empty; errdefer overrides.deinit(alloc); var directories: std.ArrayList([]u8) = .empty; errdefer { for (directories.items) |path| alloc.free(path); directories.deinit(alloc); } var suppress_saved = false;
var index: usize = 0; while (index < args.len) { const arg = args[index]; if (std.mem.eql(u8, arg, "--context-limit")) { index += 1; if (index >= args.len) return error.MissingContextLimitValue; try overrides.append(alloc, try config_runtime.context_limits.parseOverride(args[index])); } else if (std.mem.startsWith(u8, arg, "--context-limit=")) { try overrides.append(alloc, try config_runtime.context_limits.parseOverride(arg["--context-limit=".len..])); } else if (std.mem.eql(u8, arg, "--add-dir")) { index += 1; if (index >= args.len or args[index].len == 0) return error.MissingAddDirectoryValue; try directories.append(alloc, try alloc.dupe(u8, args[index])); } else if (std.mem.startsWith(u8, arg, "--add-dir=")) { const value = arg["--add-dir=".len..]; if (value.len == 0) return error.MissingAddDirectoryValue; try directories.append(alloc, try alloc.dupe(u8, value)); } else if (std.mem.eql(u8, arg, "--no-additional-dirs")) { if (suppress_saved) return error.DuplicateAdditionalDirectorySuppression; suppress_saved = true; } else { break; } index += 1; }
const override_slice = try overrides.toOwnedSlice(alloc); errdefer if (override_slice.len > 0) alloc.free(override_slice); const directory_slice = try directories.toOwnedSlice(alloc); return .{ .remaining = args[index..], .modifiers = .{ .context_limit_overrides = override_slice, .additional_directories = directory_slice, .saved_directories_suppressed = suppress_saved, }, };}
/// Returns the command that follows the supported global launch modifiers./// Startup uses this same surface to select the full runtime configuration/// before the allocating parser runs.pub fn commandAfterGlobalLaunchArgs(args: []const [:0]const u8) ?[]const u8 { var index: usize = 0; while (index < args.len) { const arg = args[index]; if (std.mem.eql(u8, arg, "--context-limit") or std.mem.eql(u8, arg, "--add-dir")) { index += 1; if (index >= args.len) return null; } else if (!std.mem.startsWith(u8, arg, "--context-limit=") and !std.mem.startsWith(u8, arg, "--add-dir=") and !std.mem.eql(u8, arg, "--no-additional-dirs")) { return arg; } index += 1; } return null;}
pub fn parse(command_catalog: CommandCatalog, args: []const [:0]const u8) Command { @setRuntimeSafety(false); if (args.len == 0) return .interactive;
const command = args[0]; if (command.len == 0) return .{ .unknown = command }; switch (command[0]) { '-', 'h' => { if (command_specs.matchesTopLevel(command_catalog, command, .help)) return .help; if (command_specs.matchesTopLevel(command_catalog, command, .@"resume") or std.mem.startsWith(u8, command, resume_id_alias_prefix)) { return .{ .resume_session = .{ .args = args, .top_level_alias = true, } }; } }, 'a' => { if (command_specs.matchesTopLevel(command_catalog, command, .ask)) return .{ .ask = args[1..] }; if (command_specs.matchesTopLevel(command_catalog, command, .acp)) return .{ .acp = args[1..] }; }, 'b' => { if (command_specs.matchesTopLevel(command_catalog, command, .background)) return .{ .background = args[1..] }; if (command_specs.matchesTopLevel(command_catalog, command, .credits)) return .{ .credits = args[1..] }; }, 'c' => { if (command_specs.matchesTopLevel(command_catalog, command, .credits)) return .{ .credits = args[1..] }; }, 'd' => { if (command_specs.matchesTopLevel(command_catalog, command, .doctor)) return .{ .doctor = args[1..] }; }, 'i' => { if (command_specs.matchesTopLevel(command_catalog, command, .issue)) return .{ .issue = args[1..] }; }, 'l' => { if (command_specs.matchesTopLevel(command_catalog, command, .login)) return .{ .login = args[1..] }; if (command_specs.matchesTopLevel(command_catalog, command, .logout)) return .{ .logout = args[1..] }; }, 'm' => { if (command_specs.matchesTopLevel(command_catalog, command, .models)) return .{ .models = args[1..] }; }, 'p' => { if (command_specs.matchesTopLevel(command_catalog, command, .pr)) return .{ .pr = args[1..] }; if (command_specs.matchesTopLevel(command_catalog, command, .permissions)) return .{ .permissions = args[1..] }; if (command_specs.matchesTopLevel(command_catalog, command, .provider)) return .{ .provider = args[1..] }; }, 'r' => { if (command_specs.matchesTopLevel(command_catalog, command, .@"resume")) return .{ .resume_session = .{ .args = args[1..] } }; if (command_specs.matchesTopLevel(command_catalog, command, .replay)) return .{ .replay = args[1..] }; }, 's' => { if (command_specs.matchesTopLevel(command_catalog, command, .setup)) return .{ .setup = args[1..] }; if (command_specs.matchesTopLevel(command_catalog, command, .status)) return .{ .status = args[1..] }; if (command_specs.matchesTopLevel(command_catalog, command, .sessions)) return .{ .sessions = args[1..] }; if (command_specs.matchesTopLevel(command_catalog, command, .session)) { if (args.len > 1 and std.mem.eql(u8, args[1], "resume")) { return .{ .resume_session = .{ .args = args[2..] } }; } return .{ .session = args[1..] }; } }, 't' => { if (command_specs.matchesTopLevel(command_catalog, command, .teams)) return .{ .teams = args[1..] }; }, 'u' => { if (command_specs.matchesTopLevel(command_catalog, command, .usage)) return .{ .usage = args[1..] }; if (command_specs.matchesTopLevel(command_catalog, command, .upgrade)) return .{ .upgrade = args[1..] }; }, 'w' => { if (command_specs.matchesTopLevel(command_catalog, command, .workspace)) return .{ .workspace = args[1..] }; }, else => {}, } return .{ .unknown = command };}
pub const NonInteractiveLaunch = struct { global_args: GlobalLaunchArgs, effective_args: []const [:0]const u8, command: Command,
pub fn deinit(self: *NonInteractiveLaunch, alloc: Allocator) void { self.global_args.deinit(alloc); self.* = undefined; }};
pub const InteractiveLaunchParseResult = union(enum) { interactive: InteractiveLaunch, noninteractive: NonInteractiveLaunch,};
/// Parses the shared interactive launch language without dispatching commands./// Returned launch values own their allocations and must be deinitialized.pub fn parseInteractiveLaunch( alloc: Allocator, args: []const [:0]const u8, command_catalog: CommandCatalog,) !InteractiveLaunchParseResult { var global_args = try parseGlobalLaunchArgs(alloc, args); errdefer global_args.deinit(alloc); const effective_args = global_args.remaining;
if (effective_args.len == 0) { return .{ .interactive = .{ .modifiers = global_args.takeModifiers() } }; }
const record_requested = try recordRequested(effective_args); if (record_requested and effective_args.len == 1) { return .{ .interactive = .{ .record_requested = true, .modifiers = global_args.takeModifiers(), } }; }
const command = parse(command_catalog, effective_args); if (topLevelHelpRequest(command_catalog, effective_args) != null) { return .{ .noninteractive = .{ .global_args = global_args, .effective_args = effective_args, .command = command, } }; } switch (command) { .interactive => return .{ .interactive = .{ .record_requested = record_requested, .modifiers = global_args.takeModifiers(), } }, .resume_session => |invocation| { const resume_args = if (record_requested) invocation.args[0 .. invocation.args.len - 1] else invocation.args; const upgrade_relaunch = !invocation.top_level_alias and resume_args.len == 2 and std.mem.eql(u8, resume_args[1], upgrade_relaunch_arg); const target_args = if (upgrade_relaunch) resume_args[0..1] else resume_args; const target = try parseResumeArgs( alloc, command_catalog, target_args, invocation.top_level_alias, ); return .{ .interactive = .{ .requested_resume = target, .upgrade_relaunch = upgrade_relaunch, .record_requested = record_requested, .modifiers = global_args.takeModifiers(), } }; }, else => return .{ .noninteractive = .{ .global_args = global_args, .effective_args = effective_args, .command = command, } }, }}
/// Detects `fx <subcommand> --help` / `-h` and returns the subcommand kind so the/// caller can render command-specific help. Top-level `fx --help`/`fx help` are/// handled separately and intentionally excluded here.fn topLevelHelpRequest(command_catalog: CommandCatalog, args: []const [:0]const u8) ?TopLevelKind { if (args.len < 2) return null; const kind = command_specs.topLevelKindFromToken(command_catalog, args[0]) orelse return null; if (kind == .help) return null; for (args[1..]) |arg| { if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) return kind; } return null;}
pub fn runIfRequested(alloc: Allocator, args: []const [:0]const u8, cfg: Config) !RunResult { return runIfRequestedWithDeps(alloc, args, cfg, .{});}
pub fn runNoConfigIfRequested(alloc: Allocator, args: []const [:0]const u8, version: []const u8, command_catalog: CommandCatalog) !bool { return runNoConfigIfRequestedWithDeps(alloc, args, version, command_catalog, .{});}
fn runNoConfigIfRequestedWithDeps( alloc: Allocator, args: []const [:0]const u8, version: []const u8, command_catalog: CommandCatalog, deps: RunDeps,) !bool { _ = recordRequested(args) catch { try writeStderr(deps, record_modifier_usage); return true; }; if (args.len != 1 or !command_specs.matchesTopLevel(command_catalog, args[0], .help)) { return false; } try writeTopLevelHelp(alloc, command_catalog, deps, version, .stdout); return true;}
const ProviderActivationCaller = enum { provider_command, provider_login,};
fn writeProviderActivationError( alloc: Allocator, deps: RunDeps, caller: ProviderActivationCaller, detail: []const u8,) !void { const message = try std.fmt.allocPrint( alloc, "{s}: {s}\n", .{ if (caller == .provider_login) "fx login" else "fx provider", detail }, ); defer alloc.free(message); try writeStderr(deps, message);}
fn activateProviderSelection( alloc: Allocator, cfg: Config, deps: RunDeps, target: model_provider.ProviderId, caller: ProviderActivationCaller,) !bool { const workspace_root = try io_mod.realpathAlloc(alloc, "."); defer alloc.free(workspace_root); var settings = config_runtime.loadMergedSettings(alloc, workspace_root) catch |err| { try writeProviderActivationError(alloc, deps, caller, "could not load settings"); debug_trace.logf("config", "provider selection settings load failed err={s}", .{@errorName(err)}); return false; }; defer settings.deinit(alloc);
var resolution = try credentials.resolveForProvider( alloc, cfg.gateway_provider.oauth_transport, cfg.secret_store, .refresh_if_needed, target, settings.credential_source, ); defer if (resolution.credential) |*credential| credential.deinit(alloc);
const already_selected = (settings.provider orelse .gateway) == target; if (caller == .provider_command and already_selected and resolution.credential != null) { try writeStdout(deps, switch (target) { .gateway => "Gateway is already selected.\n", .codex => "Codex is already selected.\n", .grok => "Grok is already selected.\n", }); return true; }
var performed_login: ?model_provider.ProviderId = null; if (resolution.credential == null and target == .codex and caller == .provider_command) { chatgpt_oauth.runLogin(alloc, cfg.gateway_provider.oauth_transport, cfg.url_opener) catch |err| { debug_trace.logf("auth", "provider selection Codex login failed err={s}", .{@errorName(err)}); try writeProviderActivationError(alloc, deps, caller, "Codex login failed"); return false; }; performed_login = .codex; resolution = try credentials.resolveForProvider( alloc, cfg.gateway_provider.oauth_transport, cfg.secret_store, .refresh_if_needed, target, settings.credential_source, ); } if (resolution.credential == null and target == .grok and caller == .provider_command) { grok_oauth.runLogin(alloc, cfg.gateway_provider.oauth_transport, cfg.url_opener) catch |err| { debug_trace.logf("auth", "provider selection Grok login failed err={s}", .{@errorName(err)}); try writeProviderActivationError(alloc, deps, caller, "Grok login failed"); return false; }; performed_login = .grok; resolution = try credentials.resolveForProvider( alloc, cfg.gateway_provider.oauth_transport, cfg.secret_store, .refresh_if_needed, target, settings.credential_source, ); }
const credential = if (resolution.credential) |*value| value else { try writeProviderActivationError( alloc, deps, caller, switch (target) { .codex => "Codex credential is unavailable", .grok => "Grok credential is unavailable", .gateway => "configure a Gateway credential first", }, ); return false; }; const catalog_provider = switch (target) { .codex => cfg.codex_model_catalog orelse { try writeProviderActivationError(alloc, deps, caller, "Codex model catalog is unavailable"); return false; }, .grok => cfg.grok_model_catalog orelse { try writeProviderActivationError(alloc, deps, caller, "Grok model catalog is unavailable"); return false; }, .gateway => cfg.gateway_provider.model_catalog, }; const fetch_result = model_catalog.fetchWithPublicFallback(catalog_provider, alloc, .{ .access = credentials.catalogAccessAt(credential.*, io_mod.milliTimestamp()), .endpoint = cfg.models_path, .view = .picker, }); var loaded = switch (fetch_result) { .loaded => |loaded| loaded, .failed => |failure| { debug_trace.logf("catalog", "provider selection catalog failed provider={s} category={s}", .{ @tagName(target), @tagName(failure.failure.category) }); const detail = try std.fmt.allocPrint( alloc, "could not load the target model catalog ({s})", .{@tagName(failure.failure.category)}, ); defer alloc.free(detail); try writeProviderActivationError(alloc, deps, caller, detail); return false; }, }; defer model_catalog.freeModelCatalog(alloc, &loaded.catalog); const saved_model = switch (target) { .gateway => settings.model, .codex => settings.codex_model, .grok => settings.grok_model, }; const selected_model = selectCatalogModel(loaded.catalog.items, saved_model) orelse { try writeProviderActivationError(alloc, deps, caller, "target model catalog is empty"); return false; }; var attempt = config_runtime.attemptUserPreferences(alloc, switch (target) { .gateway => .{ .provider = target, .model = selected_model }, .codex => .{ .provider = target, .codex_model = selected_model }, .grok => .{ .provider = target, .grok_model = selected_model }, }); defer attempt.deinit(alloc); switch (attempt) { .failure => |failure| { debug_trace.logf("config", "provider selection persistence failed err={s}", .{@errorName(failure.err)}); try writeProviderActivationError(alloc, deps, caller, "failed to save provider selection"); return false; }, .outcome => {}, } if (performed_login) |provider| switch (provider) { .codex => try writeStdout(deps, "Signed in with Codex.\n"), .grok => try writeStdout(deps, "Signed in with Grok.\n"), .gateway => unreachable, }; if (caller == .provider_command) { try writeStdout(deps, switch (target) { .gateway => "Provider set to Gateway.\n", .codex => "Provider set to Codex.\n", .grok => "Provider set to Grok.\n", }); } return true;}
fn runIfRequestedWithDeps(alloc: Allocator, args: []const [:0]const u8, cfg: Config, deps: RunDeps) !RunResult { const parsed_launch = parseInteractiveLaunch(alloc, args, cfg.command_catalog) catch |err| { if (err == error.RecordModifierRequiresInteractive) { try writeStderr(deps, record_modifier_usage); return .handled_failure; } if (err == error.InvalidResumeArgs) { try writeTopLevelUsage(cfg.command_catalog, deps, .@"resume"); return .handled_failure; } var writer: std.Io.Writer.Allocating = .init(alloc); defer writer.deinit(); if (globalLaunchErrorMessage(err)) |message| { try writer.writer.print("fx: {s}\n", .{message}); } else { try writer.writer.print("fx: invalid global launch option: {s}\n", .{@errorName(err)}); } try writer.writer.writeAll("usage: fx [--context-limit NAME=BYTES|off] [--add-dir PATH]... [--no-additional-dirs] <command>\n"); try writeStderr(deps, writer.written()); return .handled_failure; }; switch (parsed_launch) { .interactive => |launch| return .{ .interactive = launch }, .noninteractive => |value| { var noninteractive = value; defer noninteractive.deinit(alloc); return runNonInteractiveWithDeps(alloc, &noninteractive, cfg, deps); }, }}
fn runNonInteractiveWithDeps( alloc: Allocator, parsed_launch: *NonInteractiveLaunch, cfg: Config, deps: RunDeps,) !RunResult { const global_args = &parsed_launch.global_args; const effective_args = parsed_launch.effective_args; const parsed_command = parsed_launch.command;
if (global_args.modifiers.hasWorkspaceModifiers() and !commandSupportsWorkspaceModifiers(parsed_command)) { try writeWorkspaceModifierUsage(deps); return .handled_failure; }
if (isVersionFlag(effective_args[0])) { if (effective_args.len != 1) { try writeStderr(deps, version_usage); return .handled_failure; } try writeStdout(deps, cfg.version); try writeStdout(deps, "\n"); return .handled_success; }
if (topLevelHelpRequest(cfg.command_catalog, effective_args)) |kind| { const text = try command_specs.renderTopLevelCommandHelp(alloc, cfg.command_catalog, kind); defer alloc.free(text); try writeStdout(deps, text); return .handled_success; }
switch (parsed_command) { .interactive, .resume_session => unreachable, .help => { try writeTopLevelHelp(alloc, cfg.command_catalog, deps, cfg.version, .stdout); return .handled_success; }, .ask => |rest| { const exit_code = try cli_ask.run(alloc, rest, workflowConfigWithLaunchModifiers(cfg, global_args.modifiers), cfg.context_registry, cfg.tool_set); return if (exit_code == 0) .handled_success else .handled_failure; }, .acp => |rest| { const acp_opts = parseAcpArgs(rest) catch { try writeStderr(deps, "usage: fx acp [--model <id>] [--log-file <path>]\n"); return .handled_failure; }; try cfg.acp_runner.run(alloc, .{ .default_model = cfg.default_model, .default_agent_step_limit = cfg.default_agent_step_limit, .gateway_retry_count = cfg.gateway_retry_count, .gateway_chat_url = cfg.gateway_provider.chat_url.resolve(cfg.gateway_chat_url), .gateway_models_path = cfg.models_path, .gateway_provider = cfg.gateway_provider, .codex_agent_stream = cfg.codex_agent_stream, .codex_model_catalog = cfg.codex_model_catalog, .grok_agent_stream = cfg.grok_agent_stream, .grok_model_catalog = cfg.grok_model_catalog, .background_process_provider = cfg.background_process_provider, .secret_store = cfg.secret_store, .prompt_policy = cfg.prompt_policy, .ignored_list_entries = cfg.ignored_list_entries, .max_list_entries = cfg.max_list_entries, .max_read_file_bytes = cfg.max_read_file_bytes, .max_read_file_lines = cfg.max_read_file_lines, .max_read_file_line_len = cfg.max_read_file_line_len, .max_command_output_bytes = cfg.max_command_output_bytes, .max_tool_result_bytes = cfg.max_tool_result_bytes, .max_history_turns = cfg.max_history_turns, .context_registry = cfg.context_registry, .mode_registry = cfg.mode_registry, .permission_reviewer_provider = cfg.permission_reviewer_provider, .codex_permission_reviewer_provider = cfg.codex_permission_reviewer_provider, .grok_permission_reviewer_provider = cfg.grok_permission_reviewer_provider, .context_limit_overrides = global_args.modifiers.context_limit_overrides, .additional_directories = global_args.modifiers.additional_directories, .saved_directories_suppressed = global_args.modifiers.saved_directories_suppressed, .model_override = acp_opts.model, .log_file = acp_opts.log_file, }); return .handled_success; }, .pr => |rest| return runGithubWorkflow(alloc, rest, cfg, global_args.modifiers, deps, .pull_request), .issue => |rest| return runGithubWorkflow(alloc, rest, cfg, global_args.modifiers, deps, .issue), .login => |rest| { const maybe_login_provider = parseLoginProvider(rest) catch { try writeStderr(deps, "usage: fx login [vercel|codex|grok]\n"); return .handled_failure; }; // Preserve the original `fx login` behavior for scripts and users. const login_provider = maybe_login_provider orelse .vercel; switch (login_provider) { .vercel => login_flow.runLogin( alloc, cfg.gateway_provider.oauth_transport, cfg.url_opener, ) catch |err| { const message = switch (err) { error.ClientIdMissing => "fx login: missing FX_OAUTH_CLIENT_ID; configure the fx Vercel App client id first\n", error.AccessDenied => "fx login: authorization denied\n", error.ExpiredToken, error.LoginTimedOut => "fx login: authorization expired; run fx login again\n", else => "fx login: failed to sign in\n", }; try writeStderr(deps, message); return .handled_failure; }, .codex => { chatgpt_oauth.runLogin( alloc, cfg.gateway_provider.oauth_transport, cfg.url_opener, ) catch |err| { const message = switch (err) { error.ChatGptLoginTimedOut => "fx login: Codex authorization expired; run fx login codex again\n", error.ChatGptAuthorizationFailed => "fx login: Codex authorization denied\n", else => "fx login: failed to sign in with Codex\n", }; try writeStderr(deps, message); return .handled_failure; }; if (!try activateProviderSelection(alloc, cfg, deps, .codex, .provider_login)) { return .handled_failure; } try writeStdout(deps, "Signed in with Codex.\n"); }, .grok => { grok_oauth.runLogin( alloc, cfg.gateway_provider.oauth_transport, cfg.url_opener, ) catch |err| { debug_trace.logf("auth", "Grok login failed err={s}", .{@errorName(err)}); try writeStderr(deps, "fx login: failed to sign in with Grok\n"); return .handled_failure; }; if (!try activateProviderSelection(alloc, cfg, deps, .grok, .provider_login)) { return .handled_failure; } try writeStdout(deps, "Signed in with Grok.\n"); }, } return .handled_success; }, .logout => |rest| { const maybe_login_provider = parseLoginProvider(rest) catch { try writeStderr(deps, "usage: fx logout [vercel|codex|grok]\n"); return .handled_failure; }; // Preserve the original `fx logout` behavior for scripts and users. const login_provider = maybe_login_provider orelse .vercel; if (login_provider == .codex) { const outcome = chatgpt_oauth.logout() catch { try writeStderr(deps, "fx logout: failed to durably remove saved Codex login\n"); return .handled_failure; }; return switch (outcome) { .deleted => result: { try writeStdout(deps, "Signed out of Codex.\n"); break :result .handled_success; }, .missing => result: { try writeStdout(deps, "No Codex login session found.\n"); break :result .handled_success; }, .deleted_not_durable => result: { try writeStderr(deps, "fx logout: failed to durably remove saved Codex login\n"); break :result .handled_failure; }, }; } if (login_provider == .grok) { const outcome = grok_oauth.logout(alloc, cfg.gateway_provider.oauth_transport) catch { try writeStderr(deps, "fx logout: failed to durably remove saved Grok login\n"); return .handled_failure; }; if (outcome.revocation_failed) { try writeStderr(deps, "fx logout: local Grok session removed, but remote revocation could not be confirmed\n"); } return switch (outcome.deletion) { .deleted => result: { try writeStdout(deps, "Signed out of Grok.\n"); break :result .handled_success; }, .missing => result: { try writeStdout(deps, "No Grok login session found.\n"); break :result .handled_success; }, .deleted_not_durable => result: { try writeStderr(deps, "fx logout: failed to durably remove saved Grok login\n"); break :result .handled_failure; }, }; } const result = login_flow.logout(alloc, cfg.gateway_provider.oauth_transport) catch |err| switch (err) { error.SessionDeleteFailed => { try writeStderr(deps, "fx logout: failed to durably remove saved Fx login\n"); return .handled_failure; }, }; if (result.local_durability_failed) { try writeStderr(deps, "fx logout: failed to durably remove saved Fx login\n"); } else { try writeStdout( deps, if (result.session_deleted) "Signed out of fx.\n" else "No fx login session found.\n", ); } if (result.remote_revocation_failed) { try writeStderr(deps, login_flow.remote_revocation_warning); try writeStderr(deps, "\n"); } return if (result.local_durability_failed) .handled_failure else .handled_success; }, .teams => |rest| { if (rest.len != 0) { try writeStderr(deps, "usage: fx teams\n"); return .handled_failure; } login_flow.runTeams(alloc, cfg.gateway_provider.oauth_transport) catch |err| { const message = switch (err) { error.NoSession => "fx teams: run fx login first\n", error.SessionChanged => "fx teams: authentication changed; try again\n", error.TeamRequestFailed => "fx teams: failed to list Vercel teams\n", error.InvalidTeamSelection => "fx teams: no team selected\n", error.AccessDenied => "fx teams: authorization denied\n", else => "fx teams: failed to switch team\n", }; try writeStderr(deps, message); return .handled_failure; }; return .handled_success; }, .provider => |rest| { if (rest.len != 1) { try writeStderr(deps, "usage: fx provider <gateway|codex|grok>\n"); return .handled_failure; } const target = model_provider.parse(rest[0]) orelse { try writeStderr(deps, "fx provider: expected gateway, codex, or grok\n"); return .handled_failure; }; return if (try activateProviderSelection(alloc, cfg, deps, target, .provider_command)) .handled_success else .handled_failure; }, .setup => |rest| { if (rest.len != 0) { try writeTopLevelUsage(cfg.command_catalog, deps, .setup); return .handled_failure; } return if (try runPasteSetup(alloc, cfg.secret_store, deps)) .handled_success else .handled_failure; }, .status => |rest| { const opts = parseLocalSurfaceArgs(rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .status, "status", err, rest); return .handled_failure; }; var startup = try deps.load_startup_status( alloc, cfg.secret_store, cfg.default_model, cfg.default_agent_step_limit, ); defer startup.deinit(alloc); try writeConfigDiagnostics(alloc, deps, startup.config_diagnostics); const mcp_config_diagnostic = try cfg.inspect_mcp_profile_config(alloc);
const snapshot = statusSnapshotFromStartupWithBuild(startup, .{ .channel = cfg.build_channel, .version = cfg.version, .revision = cfg.revision, }, mcp_config_diagnostic); if (opts.format == .json) { try writeStatusJsonLine(alloc, deps, snapshot); return .handled_success; }
const text = try snapshot.render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .permissions => |rest| { const opts = parseLocalSurfaceArgs(rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .permissions, "permissions", err, rest); return .handled_failure; }; var startup = try deps.load_startup_state_without_credentials(alloc, cfg.default_model, cfg.default_agent_step_limit); defer startup.deinit(alloc); try writeConfigDiagnostics(alloc, deps, startup.config_diagnostics); const rules = try permissionRulesForSnapshot(alloc, startup.permission_rules); defer if (rules.rules.len > 0) alloc.free(rules.rules);
const text = try (output_contracts.PermissionsSnapshot{ .workspace_root = startup.workspace_root, .mode = permissionModeForSnapshot(startup.permission_mode), .grants = &.{}, .rules = rules, .runtime_grants_available = false, }).render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .models => |rest| { const opts = parseLocalSurfaceArgs(rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .models, "models", err, rest); return .handled_failure; };
var startup = try deps.load_catalog_startup_state( alloc, cfg.secret_store, cfg.default_model, cfg.default_agent_step_limit, ); defer startup.deinit(alloc); try writeConfigDiagnostics(alloc, deps, startup.config_diagnostics);
const catalog_access = startup.modelCatalogAccess(); const catalog_provider = switch (startup.provider) { .codex => cfg.codex_cli_model_catalog orelse { try writeStderr(deps, "fx models: Codex model catalog is unavailable\n"); return .handled_failure; }, .grok => cfg.grok_cli_model_catalog orelse { try writeStderr(deps, "fx models: Grok model catalog is unavailable\n"); return .handled_failure; }, .gateway => cfg.gateway_provider.cli_model_catalog, }; const loaded = switch (catalog_provider.fetch(alloc, .{ .access = catalog_access, .endpoint = cfg.models_path, })) { .loaded => |loaded| loaded, .failure => |failure| { const error_name = @errorName(failure.failure.asError()); const message = try std.fmt.allocPrint( alloc, "could not list models: {s}", .{catalogFailureDetail(failure.failure)}, ); defer alloc.free(message); if (opts.format == .json) { try writeJsonCommandFailureCode( alloc, deps, "models", error_name, message, ); } else { try writeStderr(deps, "fx models: "); try writeStderr(deps, message); try writeStderr(deps, "\n"); } return .handled_failure; }, }; var ids = loaded.ids; defer collections.freeStringList(alloc, &ids);
const text = try (output_contracts.ModelListSnapshot{ .ids = ids.items, .provider = startup.provider, .private_models_hidden = loaded.provenance.access.private_models_may_be_hidden, .public_only_reason = loaded.provenance.access.public_only_reason, }).render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .doctor => |rest| { const opts = parseLocalSurfaceArgs(rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .doctor, "doctor", err, rest); return .handled_failure; };
const mcp_config_diagnostic = try cfg.inspect_mcp_profile_config(alloc); var snapshot = try doctor_runtime.collect( alloc, cfg.secret_store, cfg.default_model, cfg.default_agent_step_limit, mcp_config_diagnostic, ); defer snapshot.deinit(alloc);
const output_snapshot = doctorSnapshotFromRuntime(snapshot); if (opts.format == .json) { try writeDoctorJsonLine(alloc, deps, output_snapshot); return .handled_success; }
const text = try output_snapshot.render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .background => |rest| { const opts = parsePersistedRecordArgs(rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .background, "background", err, rest); return .handled_failure; };
const workspace_root = try io_mod.realpathAlloc(alloc, "."); defer alloc.free(workspace_root);
if (opts.target) |target| { switch (target) { .id => |id| { var record = loadWorkspaceBackgroundRecord( alloc, cfg.background_process_provider, workspace_root, id, ) catch |err| { try writeLookupFailure(alloc, deps, "background", err, opts.format); return .handled_failure; }; defer record.deinit(alloc); const text = try (output_contracts.BackgroundDetailSnapshot{ .record = record }).render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .last => {}, } }
var records = loadWorkspaceBackgroundRecords( alloc, cfg.background_process_provider, workspace_root, ) catch |err| { try writeLookupFailure(alloc, deps, "background", err, opts.format); return .handled_failure; }; defer { for (records.items) |*entry| entry.deinit(alloc); records.deinit(alloc); }
if (opts.target) |target| { switch (target) { .last => { const record = findBackgroundRecord(records.items, target) orelse { const err = if (records.items.len == 0) error.NoBackgroundRecords else error.BackgroundRecordNotFound; try writeLookupFailure(alloc, deps, "background", err, opts.format); return .handled_failure; }; const text = try (output_contracts.BackgroundDetailSnapshot{ .record = record }).render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .id => unreachable, } }
const text = try (output_contracts.BackgroundListSnapshot{ .records = records.items }).render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .session => |rest| { if (rest.len > 0 and std.mem.eql(u8, rest[0], "recover")) { var recovery = parseSessionRecoveryArgs( alloc, rest[1..], ) catch |err| { try writeUsageOrJsonError( alloc, cfg.command_catalog, deps, .session, "session", err, rest[1..], ); return .handled_failure; }; defer recovery.deinit(alloc);
const workspace_root = try io_mod.realpathAlloc(alloc, "."); defer alloc.free(workspace_root); var store = session_store.Store.init( alloc, workspace_root, ) catch |err| { try writeLookupFailure( alloc, deps, "session", err, recovery.format, ); return .handled_failure; }; defer store.deinit(alloc); var result = store.recoverSessionCopy( alloc, recovery.session_id, .{}, ) catch |err| { try writeLookupFailure( alloc, deps, "session", err, recovery.format, ); return .handled_failure; }; defer result.deinit(alloc);
const text = try (output_contracts.SessionRecoverySnapshot{ .result = result, }).render(alloc, recovery.format); defer alloc.free(text); try writeFormattedOutput(deps, text, recovery.format); return if (result.status == .recovered) .handled_success else .handled_failure; }
if (rest.len > 0 and std.mem.eql(u8, rest[0], "migrate")) { var migration = parseSessionMigrationArgs(alloc, rest[1..]) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .session, "session", err, rest[1..]); return .handled_failure; }; defer migration.deinit(alloc);
const workspace_root = try io_mod.realpathAlloc(alloc, "."); defer alloc.free(workspace_root);
var store = session_store.Store.init(alloc, workspace_root) catch |err| { try writeLookupFailure(alloc, deps, "session", err, migration.format); return .handled_failure; }; defer store.deinit(alloc); var result = store.migrateLegacyStorageOnly( alloc, migration.session_id, .{ .allow_large = migration.allow_large }, ) catch |err| { try writeLookupFailure(alloc, deps, "session", err, migration.format); return .handled_failure; }; defer result.deinit(alloc);
const text = try (output_contracts.SessionMigrationSnapshot{ .result = result, }).render(alloc, migration.format); defer alloc.free(text); try writeFormattedOutput(deps, text, migration.format); return .handled_success; }
var opts = parseSessionDetailArgs(alloc, rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .session, "session", err, rest); return .handled_failure; }; defer opts.deinit(alloc);
const target = opts.target orelse { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .session, "session", error.InvalidSessionDetailArgs, rest); return .handled_failure; };
const workspace_root = try io_mod.realpathAlloc(alloc, "."); defer alloc.free(workspace_root);
var store = session_store.Store.initReadOnly(alloc, workspace_root) catch |err| { try writeLookupFailure(alloc, deps, "session", err, opts.format); return .handled_failure; }; defer store.deinit(alloc);
switch (target) { .last => { var summary = store.latestReadOnlyWorkspaceSummary(alloc) catch |err| { try writeLookupFailure(alloc, deps, "session", err, opts.format); return .handled_failure; }; defer summary.deinit(alloc);
const text = try (output_contracts.SessionSummarySnapshot{ .summary = summary, }).render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .id => |id| { var detail = store.loadReadOnlyDetail( alloc, id, .{}, ) catch |err| { try writeSessionDetailFailure( alloc, deps, id, err, opts.format, ); return .handled_failure; }; defer detail.deinit(alloc);
const text = try (output_contracts.SessionDetailSnapshot{ .detail = detail, }).render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, } }, .sessions => |rest| { const opts = parseSessionListArgs(rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .sessions, "sessions", err, rest); return .handled_failure; };
const workspace_root = try io_mod.realpathAlloc(alloc, "."); defer alloc.free(workspace_root);
var store = session_store.Store.initReadOnly(alloc, workspace_root) catch |err| { try writeLookupFailure(alloc, deps, "sessions", err, opts.format); return .handled_failure; }; defer store.deinit(alloc);
var page = store.listSessionPage( alloc, opts.scope, opts.continuation, opts.limit, ) catch |err| return err; defer page.deinit(alloc); const next_cursor = if (page.has_more and page.summaries.items.len > 0) try formatSessionListCursor( alloc, page.summaries.items[page.summaries.items.len - 1], ) else null; defer if (next_cursor) |cursor| alloc.free(cursor);
const text = try (output_contracts.SessionListSnapshot{ .sessions = page.summaries.items, .has_more = page.has_more, .next_cursor = next_cursor, .skipped_invalid = page.skipped_invalid, .all_workspaces = opts.scope == .all_workspaces, }).render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .workspace => |rest| { const opts = parseWorkspaceArgs(rest) catch |err| { try writeWorkspaceCommandError(alloc, cfg.command_catalog, deps, rest, err); return .handled_failure; }; var startup = deps.load_startup_state_without_credentials( alloc, cfg.default_model, cfg.default_agent_step_limit, ) catch |err| { try writeWorkspaceCommandError(alloc, cfg.command_catalog, deps, rest, err); return .handled_failure; }; defer startup.deinit(alloc); try writeConfigDiagnostics(alloc, deps, startup.config_diagnostics);
if (opts.action == null) { const snapshot = output_contracts.WorkspaceSnapshot.fromAccess( startup.workspace_root, &startup.workspace_access, ); const text = try snapshot.render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }
var failure_phase: workspace_commands.FailurePhase = .stage; var result = workspace_commands.execute( alloc, startup.workspace_root, &startup.workspace_access, opts.action.?, &failure_phase, ) catch |err| { try writeWorkspaceCommandError(alloc, cfg.command_catalog, deps, rest, err); return .handled_failure; }; defer result.deinit(alloc);
switch (result) { .updated => |updated| { var snapshot = output_contracts.WorkspaceSnapshot.fromAccess(startup.workspace_root, &updated.access); snapshot.mutation = updated.mutation; const text = try snapshot.render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .indeterminate => |reconciliation| { try writeWorkspaceIndeterminateError(alloc, deps, rest, reconciliation); return .handled_failure; }, } }, .credits => |rest| { const opts = parseLocalSurfaceArgs(rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .credits, "credits", err, rest); return .handled_failure; }; var startup = try deps.load_startup_state( alloc, cfg.gateway_provider.oauth_transport, cfg.secret_store, cfg.default_model, cfg.default_agent_step_limit, ); defer startup.deinit(alloc); try writeConfigDiagnostics(alloc, deps, startup.config_diagnostics);
var snapshot = cfg.gateway_provider.credits.fetch(alloc, .{ .credential = startup.apiKey(), .credential_source = if (startup.credential) |credential| credential.source else null, .tenant = startup.gatewayTeam(), }); defer snapshot.deinit(alloc); const text = try snapshot.render(alloc, opts.format); defer alloc.free(text); if (snapshot.err_message != null) { if (opts.format == .json) { try writeFormattedOutput(deps, text, opts.format); } else { try writeStderr(deps, text); } return .handled_failure; } try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .usage => |rest| { const opts = parseUsageArgs(rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .usage, "usage", err, rest); return .handled_failure; }; const home = deps.getenv(deps.env_ctx, "HOME") orelse { try writeUsageCommandFailure( alloc, deps, error.HomeNotSet, opts.format, ); return .handled_failure; }; var report = usage_cli_runtime.collect( alloc, home, opts.scope, @max(io_mod.milliTimestamp(), 0), ) catch |err| { try writeUsageCommandFailure(alloc, deps, err, opts.format); return .handled_failure; }; defer report.deinit(alloc); const text = try (output_contracts.UsageSnapshot{ .report = &report, }).render(alloc, opts.format); defer alloc.free(text); try writeFormattedOutput(deps, text, opts.format); return .handled_success; }, .upgrade => |rest| { const upgrade_runtime = @import("../upgrade/upgrade_runtime.zig"); const opts = parseUpgradeArgs(rest) catch |err| { try writeUsageOrJsonError(alloc, cfg.command_catalog, deps, .upgrade, "upgrade", err, rest); return .handled_failure; };
var startup = deps.load_startup_state_without_credentials( alloc, cfg.default_model, cfg.default_agent_step_limit, ) catch |err| { if (opts.format == .json) { try writeJsonCommandFailure(alloc, deps, "upgrade", err, "failed to load update settings"); } else { try writeStderr(deps, "fx upgrade: failed to load update settings\n"); } return .handled_failure; }; defer startup.deinit(alloc); try writeConfigDiagnostics(alloc, deps, startup.config_diagnostics);
const channel = opts.channel orelse startup.update_channel; if (opts.channel) |selected| { var outcome = config_runtime.setUserPreferences(alloc, .{ .update_channel = selected }) catch |err| { if (opts.format == .json) { try writeJsonCommandFailure(alloc, deps, "upgrade", err, "failed to save update channel"); } else { try writeStderr(deps, "fx upgrade: failed to save update channel\n"); } return .handled_failure; }; defer outcome.deinit(alloc); }
var result = upgrade_runtime.run(alloc, .{ .channel = cfg.build_channel, .version = cfg.version, .revision = cfg.revision, }, channel, switch (opts.format) { .text => .text, .json => .json, }); defer result.deinit(alloc); const text = result.snapshot.render(alloc, switch (opts.format) { .text => .text, .json => .json, }) catch { try writeStderr(deps, "fx upgrade: render failed\n"); return .handled_failure; }; defer alloc.free(text); try writeStdout(deps, text); if (opts.format == .json) try writeStdout(deps, "\n"); return if (result.snapshot.status == .failed) .handled_failure else .handled_success; }, .replay => |rest| { const exit_code = try cli_replay.run(alloc, rest); return if (exit_code == 0) .handled_success else .handled_failure; }, .unknown => |command| { try writeStderr(deps, "fx: unknown subcommand: "); try writeStderr(deps, command); try writeStderr(deps, "\n\n"); try writeTopLevelHelp(alloc, cfg.command_catalog, deps, cfg.version, .stderr); return error.UnknownCliCommand; }, }}
const TopLevelHelpDestination = enum { stdout, stderr };
fn writeTopLevelHelp( alloc: Allocator, command_catalog: CommandCatalog, deps: RunDeps, version: []const u8, destination: TopLevelHelpDestination,) !void { const text = try command_specs.renderTopLevelHelp( alloc, command_catalog, command_specs.top_level_help_default_width, version, ); defer alloc.free(text); switch (destination) { .stdout => try writeStdout(deps, text), .stderr => try writeStderr(deps, text), }}
fn runGithubWorkflow( alloc: Allocator, args: []const [:0]const u8, cfg: Config, launch_modifiers: LaunchModifiers, deps: RunDeps, workflow: github_workflows.Workflow,) !RunResult { const opts = try parseWorkflowArgs(alloc, args); defer opts.deinit(alloc);
const prompt = switch (workflow) { .pull_request => github_workflows.buildPrompt(alloc, workflow, workflowLanguagePlaceholder(), opts.context) catch |err| switch (err) { error.NotGitRepository => { try writeStderr(deps, "fx pr: requires running inside a git repository\n"); return .handled_failure; }, else => return err, }, .issue => try github_workflows.buildPrompt(alloc, workflow, workflowLanguagePlaceholder(), opts.context), }; defer alloc.free(prompt);
const workflow_cfg = workflowConfigWithLaunchModifiers(cfg, launch_modifiers); if (!opts.create) { const exit_code = try cli_ask.runPrompt(alloc, prompt, opts.auto_permission, workflow_cfg, cfg.context_registry, cfg.tool_set); return if (exit_code == 0) .handled_success else .handled_failure; }
const run_result = try cli_ask.runPromptCapture(alloc, prompt, opts.auto_permission, workflow_cfg, cfg.context_registry, cfg.tool_set); defer run_result.deinit(alloc); if (run_result.exit_code != 0) return .handled_failure;
const draft = github_publish.parseDraft(alloc, run_result.assistant_output) catch { try writeStderr(deps, switch (workflow) { .pull_request => "fx pr: failed to parse drafted PR title/body\n", .issue => "fx issue: failed to parse drafted issue title/body\n", }); return .handled_failure; }; defer draft.deinit(alloc);
const published = try github_publish.publish(alloc, switch (workflow) { .pull_request => .pull_request, .issue => .issue, }, draft); defer published.deinit(alloc); if (!published.ok) { try writeStderr(deps, switch (workflow) { .pull_request => "fx pr: ", .issue => "fx issue: ", }); try writeStderr(deps, published.text); try writeStderr(deps, "\n"); return .handled_failure; } try writeStdout(deps, published.text); try writeStdout(deps, "\n"); return .handled_success;}
fn writeStdout(deps: RunDeps, text: []const u8) !void { try deps.write_stdout(deps.stdout_ctx, text);}
fn writeStderr(deps: RunDeps, text: []const u8) !void { try deps.write_stderr(deps.stderr_ctx, text);}
fn runPasteSetup( alloc: Allocator, secret_store: host.SecretStore, deps: RunDeps,) !bool { if (secret_store.isDisabled()) { try writeStderr(deps, "fx setup: stored API keys are disabled by FX_DISABLE_KEYCHAIN\n"); return false; } if (!deps.setup_terminal_available(deps.setup_ctx)) { try writeStderr(deps, "fx setup: an interactive terminal is required to paste an API key\n"); return false; }
try writeStderr(deps, "Paste AI Gateway API key (input hidden): "); const stored_interactively = secret_store.storeInteractive() catch { try writeStderr(deps, "\nfx setup: API key was not saved\n"); return false; }; if (!stored_interactively) { const key = deps.read_masked_key( deps.setup_ctx, alloc, deps.write_stderr, deps.stderr_ctx, ) catch { try writeStderr(deps, "\nfx setup: API key was not saved\n"); return false; }; defer secret.zeroAndFree(alloc, key); try writeStderr(deps, "\n"); secret_store.store(alloc, key) catch { try writeStderr(deps, "fx setup: API key was not saved\n"); return false; }; }
const message = try std.fmt.allocPrint( alloc, "Saved API key to {s}.\n", .{secret_store.backend_label}, ); defer alloc.free(message); try writeStdout(deps, message); return true;}
fn setupTerminalAvailableDefault(_: ?*anyopaque) bool { return std.c.isatty(std.posix.STDIN_FILENO) != 0 and std.c.isatty(std.posix.STDERR_FILENO) != 0;}
fn readMaskedKeyDefault( _: ?*anyopaque, alloc: Allocator, write_mask: WriteFn, write_ctx: ?*anyopaque,) ![]u8 { var raw = try MaskedKeyRawMode.enable(); defer raw.disable();
var input: std.ArrayList(u8) = .empty; errdefer { if (input.capacity > 0) secret.zeroAndFree(alloc, input.allocatedSlice()); }
while (input.items.len < 8 * 1024) { var byte: [1]u8 = undefined; if (try std.posix.read(std.posix.STDIN_FILENO, &byte) == 0) return error.SetupCancelled; switch (byte[0]) { '\r', '\n' => { if (input.items.len == 0) continue; // toOwnedSlice shrinks through realloc, which may move the buffer // and free the original without zeroing it. Copy out and wipe the // source so no unzeroed key is left behind in freed memory. const owned = try alloc.dupe(u8, input.items); secret.zeroAndFree(alloc, input.allocatedSlice()); input = .empty; return owned; }, 3, 4, 0x1b => return error.SetupCancelled, 8, 127 => if (input.items.len > 0) { _ = input.pop(); try write_mask(write_ctx, "\x08 \x08"); }, 0x20...0x7e => { try input.append(alloc, byte[0]); try write_mask(write_ctx, "•"); }, else => {}, } } return error.SetupKeyTooLong;}
const MaskedKeyRawMode = struct { original: std.posix.termios = undefined, active: bool = false,
fn enable() !MaskedKeyRawMode { if (std.c.isatty(std.posix.STDIN_FILENO) == 0 or std.c.isatty(std.posix.STDERR_FILENO) == 0) { return error.NotATerminal; }
var self: MaskedKeyRawMode = .{}; self.original = try std.posix.tcgetattr(std.posix.STDIN_FILENO); var raw = self.original; raw.iflag.BRKINT = false; raw.iflag.ICRNL = false; raw.iflag.INPCK = false; raw.iflag.ISTRIP = false; raw.iflag.IXON = false; raw.iflag.IXOFF = false; raw.cflag.CSIZE = .CS8; raw.lflag.ECHO = false; raw.lflag.ICANON = false; raw.lflag.IEXTEN = false; raw.lflag.ISIG = false; const vmin_idx = switch (builtin.os.tag) { .linux => 6, else => 16, }; const vtime_idx = switch (builtin.os.tag) { .linux => 5, else => 17, }; if (vmin_idx < raw.cc.len and vtime_idx < raw.cc.len) { raw.cc[vmin_idx] = 1; raw.cc[vtime_idx] = 0; } try std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, raw); self.active = true; return self; }
fn disable(self: *MaskedKeyRawMode) void { if (!self.active) return; std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, self.original) catch {}; self.active = false; }};
fn writeConfigDiagnostics( alloc: Allocator, deps: RunDeps, diagnostics: []const config_runtime.ConfigDiagnostic,) !void { for (diagnostics) |diagnostic| { if (!diagnostic.reportAtStartup()) continue; var notice_writer: std.Io.Writer.Allocating = .init(alloc); defer notice_writer.deinit(); try notice_writer.writer.print( "fx: config {s}: {s}", .{ @tagName(diagnostic.layer), @tagName(diagnostic.cause) }, ); try config_runtime.writeDiagnosticMetadata(¬ice_writer.writer, diagnostic); try notice_writer.writer.writeByte('\n'); const notice = try notice_writer.toOwnedSlice(); defer alloc.free(notice); try writeStderr(deps, notice); }}
fn writeFormattedOutput(deps: RunDeps, text: []const u8, format: output_contracts.OutputFormat) !void { switch (format) { .text => try writeStdout(deps, text), .json => try writeJsonLine(deps, text), }}
fn writeJsonLine(deps: RunDeps, text: []const u8) !void { @setRuntimeSafety(false); var buf: [4096]u8 = undefined; if (text.len < buf.len) { @memcpy(buf[0..text.len], text); buf[text.len] = '\n'; return writeStdout(deps, buf[0 .. text.len + 1]); }
try writeStdout(deps, text); try writeStdout(deps, "\n");}
const JsonLinePayload = union(enum) { status: output_contracts.StatusSnapshot, doctor: output_contracts.DoctorSnapshot,};
fn writeRenderedJsonLine(alloc: Allocator, deps: RunDeps, fixed_buffer: []u8, payload: JsonLinePayload) !void { var writer: std.Io.Writer = .fixed(fixed_buffer); renderJsonLinePayload(&writer, payload) catch |err| switch (err) { error.WriteFailed => { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try renderJsonLinePayload(&out.writer, payload); return writeJsonLine(deps, out.writer.buffered()); }, }; try writeJsonLine(deps, writer.buffered());}
fn renderJsonLinePayload(writer: *std.Io.Writer, payload: JsonLinePayload) std.Io.Writer.Error!void { switch (payload) { .status => |snapshot| try snapshot.writeJson(writer), .doctor => |snapshot| try snapshot.writeJson(writer), }}
fn writeStatusJsonLine(alloc: Allocator, deps: RunDeps, snapshot: output_contracts.StatusSnapshot) !void { var buf: [1024]u8 = undefined; try writeRenderedJsonLine(alloc, deps, buf[0..], .{ .status = snapshot });}
fn statusSnapshotFromStartup(startup: app_lifecycle.StartupStatus) output_contracts.StatusSnapshot { return statusSnapshotFromStartupWithBuild(startup, .{ .channel = .stable, .version = "", .revision = "", }, .clear);}
fn statusSnapshotFromStartupWithBuild( startup: app_lifecycle.StartupStatus, build: update_target.CurrentBuild, mcp_config_diagnostic: mcp_contract.ProfileConfigDiagnostic,) output_contracts.StatusSnapshot { return .{ .model = startup.selected_model, .provider = startup.provider, .auth = startup.auth, .auth_help = startup.auth.missingHelp(.cli), .permission_mode = permissionModeForSnapshot(startup.permission_mode), .workspace_root = startup.workspace_root, .history_turns = 0, .session_permission_grants = 0, .agent_step_limit = startup.agent_step_limit, .update_channel = startup.update_channel.label(), .build_channel = build.channel.label(), .build_revision = build.revision, .mcp_config_error = switch (mcp_config_diagnostic) { .clear => null, .failed => |err| @errorName(err), }, };}
fn writeDoctorJsonLine(alloc: Allocator, deps: RunDeps, snapshot: output_contracts.DoctorSnapshot) !void { var buf: [4096]u8 = undefined; try writeRenderedJsonLine(alloc, deps, buf[0..], .{ .doctor = snapshot });}
fn doctorSnapshotFromRuntime(snapshot: doctor_runtime.Snapshot) output_contracts.DoctorSnapshot { return .{ .workspace_root = snapshot.workspace_root, .model = snapshot.model, .provider = snapshot.provider, .auth = snapshot.auth, .permission_mode = permissionModeForSnapshot(snapshot.permission_mode), .agent_step_limit = snapshot.agent_step_limit, .checks = snapshot.checks, };}
fn writeRealStdout(_: ?*anyopaque, text: []const u8) !void { if (comptime builtin.os.tag != .windows) { return writeFdAll(std.posix.STDOUT_FILENO, text); } try std.Io.File.stdout().writeStreamingAll(io_mod.getIo(), text);}
fn writeRealStderr(_: ?*anyopaque, text: []const u8) !void { if (comptime builtin.os.tag != .windows) { return writeFdAll(std.posix.STDERR_FILENO, text); } try std.Io.File.stderr().writeStreamingAll(io_mod.getIo(), text);}
fn writeFdAll(fd: std.posix.fd_t, text: []const u8) !void { @setRuntimeSafety(false); var remaining = text; while (remaining.len > 0) { const written = std.c.write(fd, remaining.ptr, remaining.len); if (written <= 0) return error.WriteFailed; remaining = remaining[@intCast(written)..]; }}
fn getenvDefault(_: ?*anyopaque, key: []const u8) ?[]const u8 { return io_mod.getenv(key);}
fn environMapDefault(_: ?*anyopaque) ?*const std.process.Environ.Map { return io_mod.environMap();}
fn selfExePathDefault(_: ?*anyopaque, alloc: Allocator) ![]u8 { const path_z = try std.process.executablePathAlloc(io_mod.getIo(), alloc); defer alloc.free(path_z); return alloc.dupe(u8, path_z);}
fn writeTopLevelUsage(command_catalog: CommandCatalog, deps: RunDeps, kind: TopLevelKind) !void { try writeStderr(deps, "usage: fx "); try writeStderr(deps, command_specs.topLevelUsage(command_catalog, kind)); try writeStderr(deps, "\n");}
fn writeUsageOrJsonError( alloc: Allocator, command_catalog: CommandCatalog, deps: RunDeps, usage_kind: TopLevelKind, output_kind: []const u8, err: anyerror, args: anytype,) !void { if (argsContainJson(args)) { try writeCommandFailure(alloc, deps, output_kind, err, .json); } else { try writeTopLevelUsage(command_catalog, deps, usage_kind); }}
fn writeUsageCommandFailure( alloc: Allocator, deps: RunDeps, err: anyerror, format: output_contracts.OutputFormat,) !void { const message = usageFailureMessage(err); if (format == .json) { return writeJsonCommandFailure( alloc, deps, "usage", err, message, ); } try writeStderr(deps, "fx usage: "); try writeStderr(deps, message); try writeStderr(deps, "\n");}
fn usageFailureMessage(err: anyerror) []const u8 { return switch (err) { error.HomeNotSet => "HOME is not set", error.DurablePathUnsafe, error.PrivateStatePermissionsUnsupported, => "local usage storage is unsafe", else => "local usage data is unavailable", };}
fn writeWorkspaceCommandError( alloc: Allocator, command_catalog: CommandCatalog, deps: RunDeps, args: []const [:0]const u8, err: anyerror,) !void { if (!argsContainJson(args)) { if (output_contracts.workspaceErrorMessage(err)) |message| { try writeStderr(deps, "fx workspace: "); try writeStderr(deps, message); try writeStderr(deps, "\n"); return; } try writeTopLevelUsage(command_catalog, deps, .workspace); return; }
const message = output_contracts.workspaceErrorMessage(err) orelse "invalid arguments"; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"kind\":\"workspace\",\"error\":"); try std.json.Stringify.value(message, .{}, &out.writer); try out.writer.writeAll(",\"code\":"); try std.json.Stringify.value(@errorName(err), .{}, &out.writer); try out.writer.writeByte('}'); try writeJsonLine(deps, out.writer.buffered());}
fn writeWorkspaceIndeterminateError( alloc: Allocator, deps: RunDeps, args: []const [:0]const u8, reconciliation: workspace_commands.Reconciliation,) !void { const message = switch (reconciliation) { .intended => "settings durability is uncertain; reloaded settings match the requested update", .previous => "settings durability is uncertain; reloaded settings match the previous state, so the update was not applied", .unconfirmed => "settings durability is uncertain; reloaded settings match neither the requested nor previous state", }; if (!argsContainJson(args)) { try writeStderr(deps, "fx workspace: "); try writeStderr(deps, message); try writeStderr(deps, "\n"); return; }
var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"kind\":\"workspace\",\"error\":"); try std.json.Stringify.value(message, .{}, &out.writer); try out.writer.writeAll(",\"code\":\"SettingsCommitIndeterminate\"}"); try writeJsonLine(deps, out.writer.buffered());}
fn argsContainJson(args: anytype) bool { for (args) |arg| { if (std.mem.eql(u8, arg, "--json")) return true; } return false;}
fn workflowLanguagePlaceholder() types.ConversationLanguage { return types.ConversationLanguage.default();}
fn permissionModeForSnapshot(mode: anytype) types.PermissionMode { return switch (mode) { .ask => .ask, .auto => .auto, .yolo => .yolo, };}
fn permissionModeLabel(mode: anytype) []const u8 { return switch (mode) { .ask => "ask", .auto => "auto", .yolo => "yolo", };}
fn permissionRulesForSnapshot(alloc: Allocator, active_rules: anytype) !types.PermissionRuleSet { if (active_rules.rules.len == 0) return .{}; const rules = try alloc.alloc(types.PermissionRule, active_rules.rules.len); for (active_rules.rules, 0..) |rule, i| { rules[i] = .{ .permission = rule.permission, .pattern = rule.pattern, .action = switch (rule.action) { .allow => .allow, .ask => .ask, .deny => .deny, }, }; } return .{ .rules = rules };}
fn loadLatestWorkspaceSessionDetail( alloc: Allocator, store: session_store.Store,) !session_store.ReadOnlyDetail { var summary = try store.latestReadOnlyWorkspaceSummary(alloc); defer summary.deinit(alloc); return store.loadReadOnlyDetail(alloc, summary.id, .{});}
fn loadLatestWorkspaceSessionSummary( alloc: Allocator, store: session_store.Store,) !session_store.SessionSummary { return store.latestReadOnlyWorkspaceSummary(alloc);}
fn loadWorkspaceBackgroundRecords( alloc: Allocator, process_provider: background_process_provider.Provider, workspace_root: []const u8,) !std.ArrayList(background_store.Record) { var session_store_value = session_store.Store.initReadOnly(alloc, workspace_root) catch |err| switch (err) { error.HomeNotSet => return .empty, else => return err, }; defer session_store_value.deinit(alloc);
var sessions = try session_store_value.listManagedChildCandidatesForWorkspace(alloc); defer { for (sessions.items) |*summary| summary.deinit(alloc); sessions.deinit(alloc); }
var sourced: std.ArrayList(SourcedBackgroundRecord) = .empty; errdefer { for (sourced.items) |*record| record.deinit(alloc); sourced.deinit(alloc); }
for (sessions.items) |summary| { var capability = try session_store_value.openListedChildCapabilityReadOnly( alloc, summary.id, ); defer capability.deinit(); var store = background_store.Store.initManaged(&capability); defer store.deinit(alloc);
var session_records = try store.list(alloc); defer { for (session_records.items) |*record| record.deinit(alloc); session_records.deinit(alloc); }
for (session_records.items) |*record| { if (!background_store.recordBelongsToWorkspace(record.*, workspace_root)) continue; try background_record_liveness.refreshPersistedRecordLiveness( alloc, process_provider, record, ); try appendSourcedBackgroundRecord( alloc, &sourced, summary.id, record.*, ); } }
sortSourcedBackgroundRecords(sourced.items);
var records: std.ArrayList(background_store.Record) = .empty; errdefer { for (records.items) |*record| record.deinit(alloc); records.deinit(alloc); } try records.ensureTotalCapacity(alloc, sourced.items.len); for (sourced.items) |record| { records.appendAssumeCapacity(try cloneBackgroundRecord( alloc, record.record, )); } for (sourced.items) |*record| record.deinit(alloc); sourced.deinit(alloc); return records;}
fn loadWorkspaceBackgroundRecord( alloc: Allocator, process_provider: background_process_provider.Provider, workspace_root: []const u8, id: u64,) !background_store.Record { var session_store_value = session_store.Store.initReadOnly(alloc, workspace_root) catch |err| switch (err) { error.HomeNotSet => return error.NoBackgroundRecords, else => return err, }; defer session_store_value.deinit(alloc);
var sessions = try session_store_value.listManagedChildCandidatesForWorkspace(alloc); defer { for (sessions.items) |*summary| summary.deinit(alloc); sessions.deinit(alloc); }
var matched_workspace = false; for (sessions.items) |summary| { matched_workspace = true;
var capability = try session_store_value.openListedChildCapabilityReadOnly( alloc, summary.id, ); defer capability.deinit(); var store = background_store.Store.initManaged(&capability); defer store.deinit(alloc);
var record = store.load(alloc, id) catch |err| switch (err) { error.BackgroundRecordNotFound => continue, else => return err, }; errdefer record.deinit(alloc); if (!background_store.recordBelongsToWorkspace(record, workspace_root)) { record.deinit(alloc); continue; } try background_record_liveness.refreshPersistedRecordLiveness( alloc, process_provider, &record, ); return record; } return if (matched_workspace) error.BackgroundRecordNotFound else error.NoBackgroundRecords;}
const SourcedBackgroundRecord = struct { source_session_id: []u8, record: background_store.Record,
fn deinit(self: *SourcedBackgroundRecord, alloc: Allocator) void { alloc.free(self.source_session_id); self.record.deinit(alloc); self.* = undefined; }};
fn appendSourcedBackgroundRecord( alloc: Allocator, records: *std.ArrayList(SourcedBackgroundRecord), source_session_id: []const u8, record: background_store.Record,) !void { const owned_source_session_id = try alloc.dupe(u8, source_session_id); errdefer alloc.free(owned_source_session_id); var owned_record = try cloneBackgroundRecord(alloc, record); errdefer owned_record.deinit(alloc); try records.append(alloc, .{ .source_session_id = owned_source_session_id, .record = owned_record, });}
fn sortSourcedBackgroundRecords(records: []SourcedBackgroundRecord) void { var i: usize = 1; while (i < records.len) : (i += 1) { var j = i; while (j > 0 and sourcedBackgroundRanksBefore( records[j], records[j - 1], )) : (j -= 1) { std.mem.swap( SourcedBackgroundRecord, &records[j - 1], &records[j], ); } }}
fn sourcedBackgroundRanksBefore( left: SourcedBackgroundRecord, right: SourcedBackgroundRecord,) bool { if (left.record.updated_at_ms != right.record.updated_at_ms) { return left.record.updated_at_ms > right.record.updated_at_ms; } const source_order = std.mem.order( u8, left.source_session_id, right.source_session_id, ); if (source_order != .eq) return source_order == .gt; return if (left.record.background_record_id) |left_id| blk: { if (right.record.background_record_id) |right_id| { break :blk std.mem.order(u8, &left_id, &right_id) == .gt; } break :blk true; } else false;}
fn cloneBackgroundRecord(alloc: Allocator, record: background_store.Record) !background_store.Record { const pid = try alloc.dupe(u8, record.pid); errdefer alloc.free(pid); const command = try alloc.dupe(u8, record.command); errdefer alloc.free(command); const cwd = try alloc.dupe(u8, record.cwd); errdefer alloc.free(cwd); const log_path = try alloc.dupe(u8, record.log_path); errdefer alloc.free(log_path);
var process_token: ?[]u8 = null; errdefer if (process_token) |token| alloc.free(token); if (record.process_token) |token| { process_token = try alloc.dupe(u8, token); }
var log_storage: ?background_store.LogStorage = null; errdefer if (log_storage) |*storage| storage.deinit(alloc); if (record.log_storage) |storage| { log_storage = switch (storage) { .managed_session => |managed| .{ .managed_session = .{ .managed_log_name = try alloc.dupe( u8, managed.managed_log_name, ), } }, .external => |external| .{ .external = .{ .path = try alloc.dupe(u8, external.path), } }, }; }
var server_url: ?[]u8 = null; errdefer if (server_url) |url| alloc.free(url); if (record.server_url) |url| { server_url = try alloc.dupe(u8, url); }
var diagnostic: ?[]u8 = null; errdefer if (diagnostic) |value| alloc.free(value); if (record.diagnostic) |value| { diagnostic = try alloc.dupe(u8, value); }
return .{ .id = record.id, .background_record_id = record.background_record_id, .process_token = process_token, .pid = pid, .command = command, .cwd = cwd, .log_path = log_path, .log_storage = log_storage, .expect_url = record.expect_url, .server_url = server_url, .started_at_ms = record.started_at_ms, .updated_at_ms = record.updated_at_ms, .exit_code = record.exit_code, .state = record.state, .diagnostic = diagnostic, };}
test "direct background ranking uses updated time source session and stable id" { const low_stable = background_store.StableBackgroundRecordId{ 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, }; const high_stable = background_store.StableBackgroundRecordId{ 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, }; const base = background_store.Record{ .id = 7, .pid = @constCast("1"), .command = @constCast("cmd"), .cwd = @constCast("/tmp"), .log_path = @constCast("/tmp/log"), .expect_url = false, .started_at_ms = 1, .updated_at_ms = 10, .state = .running, };
var older = base; older.updated_at_ms = 9; try std.testing.expect(sourcedBackgroundRanksBefore( .{ .source_session_id = @constCast("a"), .record = base }, .{ .source_session_id = @constCast("z"), .record = older }, ));
try std.testing.expect(sourcedBackgroundRanksBefore( .{ .source_session_id = @constCast("z"), .record = base }, .{ .source_session_id = @constCast("a"), .record = base }, ));
var low = base; low.background_record_id = low_stable; var high = base; high.background_record_id = high_stable; try std.testing.expect(sourcedBackgroundRanksBefore( .{ .source_session_id = @constCast("same"), .record = high }, .{ .source_session_id = @constCast("same"), .record = low }, )); try std.testing.expect(sourcedBackgroundRanksBefore( .{ .source_session_id = @constCast("same"), .record = low }, .{ .source_session_id = @constCast("same"), .record = base }, ));}
fn findBackgroundRecord(records: []const background_store.Record, target: PersistedRecordTarget) ?background_store.Record { return switch (target) { .last => if (records.len == 0) null else records[0], .id => |id| blk: { for (records) |record| { if (record.id == id) break :blk record; } break :blk null; }, };}
fn loadBackgroundRecord(alloc: Allocator, store: background_store.Store, target: PersistedRecordTarget) !background_store.Record { return switch (target) { .last => store.loadLatest(alloc), .id => |id| store.load(alloc, id), };}
fn catalogFailureDetail(failure: model_catalog.Failure) []const u8 { return switch (failure.category) { .authentication => "AuthenticationRejected", .cancellation => "the request was cancelled", .malformed_response => "MalformedResponse", .resource_exhausted => "OutOfMemory", .rate_limited, .gateway_unavailable, .transport, .http_status, .runtime => "Unavailable", };}
fn writeCommandFailure( alloc: Allocator, deps: RunDeps, kind: []const u8, err: anyerror, format: output_contracts.OutputFormat,) !void { if (format != .json) return err; const message = commandFailureMessage(err) orelse return err; return writeJsonCommandFailure(alloc, deps, kind, err, message);}
fn writeJsonCommandFailure( alloc: Allocator, deps: RunDeps, kind: []const u8, err: anyerror, message: []const u8,) !void { return writeJsonCommandFailureCode( alloc, deps, kind, @errorName(err), message, );}
fn writeJsonCommandFailureCode( alloc: Allocator, deps: RunDeps, kind: []const u8, code: []const u8, message: []const u8,) !void { const json = try (output_contracts.CommandFailureSnapshot{ .kind = kind, .message = message, .code = code, }).renderJson(alloc); defer alloc.free(json); try writeJsonLine(deps, json);}
fn writeLookupFailure( alloc: Allocator, deps: RunDeps, kind: []const u8, err: anyerror, format: output_contracts.OutputFormat,) !void { if (format == .json) { return writeCommandFailure(alloc, deps, kind, err, format); }
switch (err) { error.NoBackgroundRecords => { try writeStderr(deps, "fx "); try writeStderr(deps, kind); try writeStderr(deps, ": no persisted records for this workspace\n"); }, error.BackgroundRecordNotFound => { try writeStderr(deps, "fx "); try writeStderr(deps, kind); try writeStderr(deps, ": record not found\n"); }, error.InvalidBackgroundRecord, error.UnsupportedBackgroundSchema => { try writeStderr(deps, "fx "); try writeStderr(deps, kind); try writeStderr(deps, ": record is unreadable or from an unsupported version\n"); }, error.NoSavedSessions => { try writeStderr(deps, "fx session: no saved sessions for this workspace\n"); }, error.NoReadableSessions => { try writeStderr(deps, "fx session: saved sessions are unreadable; run `fx doctor` for recovery guidance\n"); }, error.SessionNotFound => { try writeStderr(deps, "fx session: record not found\n"); }, error.InvalidSessionFormat => { try writeStderr( deps, "fx session: record is corrupt; run `fx doctor` for recovery guidance\n", ); }, error.UnsupportedSessionSchema => { try writeStderr( deps, "fx session: record uses an unsupported session version\n", ); }, error.InvalidSessionId => { try writeStderr(deps, "fx session: invalid session id\n"); }, error.LegacySessionTooLarge => { try writeStderr( deps, "fx session: legacy session is too large for automatic loading; run `fx session migrate <id> --allow-large`\n", ); }, error.LegacySessionReadResourceExhausted => { try writeStderr( deps, "fx session: legacy session could not be loaded with available resources\n", ); }, error.LegacySessionMigrationResourceExhausted => { try writeStderr( deps, "fx session: migration did not complete because resources were exhausted; the original session remains authoritative\n", ); }, error.LegacySessionMigrationFailed, error.LegacySessionChanged => { try writeStderr( deps, "fx session: migration did not complete; the original session remains authoritative\n", ); }, error.LegacySessionMigrationIndeterminate => { try writeStderr( deps, "fx session: migration outcome is indeterminate and will be resolved by the next exact writable load\n", ); }, error.SessionRecoveryNotNeeded => { try writeStderr( deps, "fx session: recovery was refused because the session has a valid commit boundary; resume it normally\n", ); }, error.SessionRecoveryRequiresCurrentSchema => { try writeStderr( deps, "fx session: recovery only applies to current schema-v3 sessions; migrate legacy sessions first\n", ); }, error.SessionRecoveryUnsupportedSchema => { try writeStderr( deps, "fx session: recovery is unavailable for this unsupported session version\n", ); }, error.SessionRecoveryBoundaryInvalid => { try writeStderr( deps, "fx session: no exact trustworthy recovery boundary was found; the source was left unchanged\n", ); }, error.SessionRecoveryIndeterminate => { try writeStderr( deps, "fx session: the recovery copy could not be confirmed; the source was left unchanged\n", ); }, error.SessionAuthorityBoundaryUnavailable, error.SessionCommitBoundaryUnavailable, => { try writeStderr( deps, "fx session: session authority is temporarily unavailable while an incomplete commit is resolved\n", ); }, error.SessionAuthorityIntentCleanupPending => { try writeStderr( deps, "fx session: session authority is confirmed but transition cleanup is still pending\n", ); }, error.SessionBusy, error.SessionLockUnsupported => { try writeStderr( deps, "fx session: session is busy or the filesystem cannot provide the required lock\n", ); }, error.SessionPathUnsafe, error.DurablePathUnsafe, error.PrivateStatePermissionsUnsupported, => { try writeStderr( deps, "fx session: durable session storage is unsafe or does not support required private permissions\n", ); }, error.DurableLayoutFailed, error.SessionStoreUnavailable => { try writeStderr(deps, "fx session: durable session store is unavailable\n"); }, error.HomeNotSet => { try writeStderr(deps, "fx "); try writeStderr(deps, kind); try writeStderr(deps, ": HOME is not set\n"); }, else => return err, }}
fn writeSessionDetailFailure( alloc: Allocator, deps: RunDeps, session_id: []const u8, err: anyerror, format: output_contracts.OutputFormat,) !void { const message = switch (err) { error.InvalidSessionFormat => try std.fmt.allocPrint( alloc, "session {s} is corrupt; run `fx session recover {s}`", .{ session_id, session_id }, ), error.UnsupportedSessionSchema => try std.fmt.allocPrint( alloc, "session {s} uses an unsupported session version", .{session_id}, ), else => return writeLookupFailure( alloc, deps, "session", err, format, ), }; defer alloc.free(message); if (format == .json) { return writeJsonCommandFailure( alloc, deps, "session", err, message, ); } try writeStderr(deps, "fx session: "); try writeStderr(deps, message); try writeStderr(deps, "\n");}
fn commandFailureMessage(err: anyerror) ?[]const u8 { if (lookupFailureMessage(err)) |message| return message; return switch (err) { error.InvalidLocalSurfaceArgs, error.InvalidUsageArgs, error.InvalidPersistedRecordArgs, error.InvalidSessionDetailArgs, error.InvalidSessionMigrationArgs, error.InvalidSessionRecoveryArgs, error.InvalidResumeArgs, => "invalid arguments", else => null, };}
fn lookupFailureMessage(err: anyerror) ?[]const u8 { return switch (err) { error.NoBackgroundRecords => "no persisted records for this workspace", error.BackgroundRecordNotFound => "record not found", error.InvalidBackgroundRecord, error.UnsupportedBackgroundSchema => "record is unreadable or from an unsupported version", error.NoSavedSessions => "no saved sessions for this workspace", error.NoReadableSessions => "saved sessions are unreadable; run `fx doctor` for recovery guidance", error.SessionNotFound => "record not found", error.InvalidSessionFormat => "record is corrupt; run `fx doctor` for recovery guidance", error.UnsupportedSessionSchema => "record uses an unsupported session version", error.InvalidSessionId => "invalid session id", error.LegacySessionTooLarge => "legacy session is too large for automatic loading; run `fx session migrate <id> --allow-large`", error.LegacySessionReadResourceExhausted => "legacy session could not be loaded with available resources", error.LegacySessionMigrationResourceExhausted => "migration did not complete because resources were exhausted; the original session remains authoritative", error.LegacySessionMigrationFailed, error.LegacySessionChanged => "migration did not complete; the original session remains authoritative", error.LegacySessionMigrationIndeterminate => "migration outcome is indeterminate and will be resolved by the next exact writable load", error.SessionRecoveryNotNeeded => "recovery was refused because the session has a valid commit boundary; resume it normally", error.SessionRecoveryRequiresCurrentSchema => "recovery only applies to current schema-v3 sessions; migrate legacy sessions first", error.SessionRecoveryUnsupportedSchema => "recovery is unavailable for this unsupported session version", error.SessionRecoveryBoundaryInvalid => "no exact trustworthy recovery boundary was found; the source was left unchanged", error.SessionRecoveryIndeterminate => "the recovery copy could not be confirmed; the source was left unchanged", error.SessionAuthorityBoundaryUnavailable, error.SessionCommitBoundaryUnavailable, => "session authority is temporarily unavailable while an incomplete commit is resolved", error.SessionAuthorityIntentCleanupPending => "session authority is confirmed but transition cleanup is still pending", error.SessionBusy, error.SessionLockUnsupported => "session is busy or the filesystem cannot provide the required lock", error.SessionPathUnsafe, error.DurablePathUnsafe, error.PrivateStatePermissionsUnsupported, => "durable session storage is unsafe or does not support required private permissions", error.DurableLayoutFailed, error.SessionStoreUnavailable => "durable session store is unavailable", error.HomeNotSet => "HOME is not set", else => null, };}
test "session detail failures separate corruption from unsupported schema" { var corrupt_text = CaptureOutput.init(std.testing.allocator); defer corrupt_text.deinit(); try writeSessionDetailFailure( std.testing.allocator, corrupt_text.deps(), "broken-session", error.InvalidSessionFormat, .text, ); try std.testing.expectEqualStrings("", corrupt_text.stdout.written()); try std.testing.expectEqualStrings( "fx session: session broken-session is corrupt; run `fx session recover broken-session`\n", corrupt_text.stderr.written(), );
var corrupt_json = CaptureOutput.init(std.testing.allocator); defer corrupt_json.deinit(); try writeSessionDetailFailure( std.testing.allocator, corrupt_json.deps(), "broken-session", error.InvalidSessionFormat, .json, ); try std.testing.expectEqualStrings("", corrupt_json.stderr.written()); try std.testing.expect( std.mem.find( u8, corrupt_json.stdout.written(), "\"error\":\"session broken-session is corrupt; run `fx session recover broken-session`\"", ) != null, ); try std.testing.expect( std.mem.find( u8, corrupt_json.stdout.written(), "\"code\":\"InvalidSessionFormat\"", ) != null, );
var unsupported_text = CaptureOutput.init(std.testing.allocator); defer unsupported_text.deinit(); try writeSessionDetailFailure( std.testing.allocator, unsupported_text.deps(), "future-session", error.UnsupportedSessionSchema, .text, ); try std.testing.expectEqualStrings("", unsupported_text.stdout.written()); try std.testing.expectEqualStrings( "fx session: session future-session uses an unsupported session version\n", unsupported_text.stderr.written(), );}
test "session recovery boundary failures keep stable text and json guidance" { var text_output = CaptureOutput.init(std.testing.allocator); defer text_output.deinit(); try writeLookupFailure( std.testing.allocator, text_output.deps(), "session", error.SessionRecoveryBoundaryInvalid, .text, ); try std.testing.expectEqualStrings("", text_output.stdout.written()); try std.testing.expectEqualStrings( "fx session: no exact trustworthy recovery boundary was found; the source was left unchanged\n", text_output.stderr.written(), );
var json_output = CaptureOutput.init(std.testing.allocator); defer json_output.deinit(); try writeLookupFailure( std.testing.allocator, json_output.deps(), "session", error.SessionRecoveryBoundaryInvalid, .json, ); try std.testing.expectEqualStrings("", json_output.stderr.written()); try std.testing.expect( std.mem.find( u8, json_output.stdout.written(), "\"code\":\"SessionRecoveryBoundaryInvalid\"", ) != null, ); try std.testing.expect( std.mem.find( u8, json_output.stdout.written(), "\"error\":\"no exact trustworthy recovery boundary was found; the source was left unchanged\"", ) != null, );}
fn workflowConfig(cfg: Config) @import("cli_ask.zig").Config { return .{ .command_usage = command_specs.topLevelUsage(cfg.command_catalog, .ask), .default_model = cfg.default_model, .default_agent_step_limit = cfg.default_agent_step_limit, .gateway_retry_count = cfg.gateway_retry_count, .gateway_chat_url = cfg.gateway_provider.chat_url.resolve(cfg.gateway_chat_url), .gateway_models_path = cfg.models_path, .gateway_provider = cfg.gateway_provider, .codex_agent_stream = cfg.codex_agent_stream, .codex_model_catalog = cfg.codex_model_catalog, .grok_agent_stream = cfg.grok_agent_stream, .grok_model_catalog = cfg.grok_model_catalog, .background_process_provider = cfg.background_process_provider, .secret_store = cfg.secret_store, .prompt_policy = cfg.prompt_policy, .skill_root_policy = cfg.skill_root_policy, .ignored_list_entries = cfg.ignored_list_entries, .max_list_entries = cfg.max_list_entries, .max_read_file_bytes = cfg.max_read_file_bytes, .max_read_file_lines = cfg.max_read_file_lines, .max_read_file_line_len = cfg.max_read_file_line_len, .max_command_output_bytes = cfg.max_command_output_bytes, .max_tool_result_bytes = cfg.max_tool_result_bytes, .max_history_turns = cfg.max_history_turns, .mode_registry = cfg.mode_registry, .load_mcp_runtime = cfg.load_mcp_runtime, .permission_reviewer_provider = cfg.permission_reviewer_provider, .codex_permission_reviewer_provider = cfg.codex_permission_reviewer_provider, .grok_permission_reviewer_provider = cfg.grok_permission_reviewer_provider, };}
fn workflowConfigWithLaunchModifiers( cfg: Config, modifiers: LaunchModifiers,) @import("cli_ask.zig").Config { var result = workflowConfig(cfg); result.context_limit_overrides = modifiers.context_limit_overrides; result.additional_directories = modifiers.additional_directories; result.saved_directories_suppressed = modifiers.saved_directories_suppressed; return result;}
fn commandSupportsWorkspaceModifiers(command: Command) bool { return switch (command) { .interactive, .ask, .acp, .pr, .issue, .resume_session => true, else => false, };}
fn writeWorkspaceModifierUsage(deps: RunDeps) !void { try writeStderr( deps, "fx: --add-dir and --no-additional-dirs are only supported for interactive, resume, ask, ACP, PR, and issue launches\n", );}
fn globalLaunchErrorMessage(err: anyerror) ?[]const u8 { return switch (err) { error.MissingAddDirectoryValue => "--add-dir requires a directory path", error.DuplicateAdditionalDirectorySuppression => "--no-additional-dirs may only be specified once", else => null, };}
fn parseAcpArgs(args: []const [:0]const u8) !AcpOptions { var opts = AcpOptions{}; var i: usize = 0; while (i < args.len) : (i += 1) { if (std.mem.eql(u8, args[i], "--model")) { if (opts.model != null or i + 1 >= args.len) return error.InvalidAcpArgs; i += 1; opts.model = args[i]; } else if (std.mem.eql(u8, args[i], "--log-file")) { if (opts.log_file != null or i + 1 >= args.len) return error.InvalidAcpArgs; i += 1; opts.log_file = args[i]; } else { return error.InvalidAcpArgs; } } return opts;}
fn parseLocalSurfaceArgs(args: []const [:0]const u8) !LocalSurfaceOptions { var options = LocalSurfaceOptions{}; for (args) |arg| { if (std.mem.eql(u8, arg, "--json")) { options.format = .json; continue; } return error.InvalidLocalSurfaceArgs; } return options;}
fn parseUpgradeArgs(args: []const [:0]const u8) !UpgradeOptions { var options = UpgradeOptions{}; var format_seen = false; var channel_seen = false; var index: usize = 0; while (index < args.len) : (index += 1) { const arg = args[index]; if (std.mem.eql(u8, arg, "--json")) { if (format_seen) return error.InvalidUpgradeArgs; format_seen = true; options.format = .json; continue; } if (std.mem.eql(u8, arg, "--channel")) { if (channel_seen or index + 1 >= args.len) return error.InvalidUpgradeArgs; channel_seen = true; index += 1; options.channel = update_target.Channel.parse(args[index]) orelse return error.InvalidUpgradeArgs; continue; } if (std.mem.startsWith(u8, arg, "--channel=")) { if (channel_seen) return error.InvalidUpgradeArgs; channel_seen = true; options.channel = update_target.Channel.parse(arg["--channel=".len..]) orelse return error.InvalidUpgradeArgs; continue; } return error.InvalidUpgradeArgs; } return options;}
fn parseSessionListArgs(args: []const [:0]const u8) !SessionListOptions { var options = SessionListOptions{}; var format_seen = false; var limit_seen = false; var cursor_seen = false; var scope_seen = false; var index: usize = 0; while (index < args.len) : (index += 1) { const arg = args[index]; if (std.mem.eql(u8, arg, "--json")) { if (format_seen) return error.InvalidLocalSurfaceArgs; format_seen = true; options.format = .json; continue; } if (std.mem.eql(u8, arg, "--all")) { if (scope_seen) return error.InvalidLocalSurfaceArgs; scope_seen = true; options.scope = .all_workspaces; continue; } if (std.mem.eql(u8, arg, "--limit")) { if (limit_seen or index + 1 >= args.len) return error.InvalidLocalSurfaceArgs; limit_seen = true; index += 1; options.limit = std.fmt.parseUnsigned(usize, args[index], 10) catch return error.InvalidLocalSurfaceArgs; if (options.limit == 0 or options.limit > session_store.session_list_max_limit) { return error.InvalidLocalSurfaceArgs; } continue; } if (std.mem.eql(u8, arg, "--cursor")) { if (cursor_seen or index + 1 >= args.len) return error.InvalidLocalSurfaceArgs; cursor_seen = true; index += 1; options.continuation = try parseSessionListCursor(args[index]); continue; } return error.InvalidLocalSurfaceArgs; } return options;}
fn parseUsageArgs(args: []const [:0]const u8) !UsageOptions { var options = UsageOptions{}; var period_seen = false; var json_seen = false; var index: usize = 0; while (index < args.len) : (index += 1) { const arg = args[index]; if (std.mem.eql(u8, arg, "--json")) { if (json_seen) return error.InvalidUsageArgs; json_seen = true; options.format = .json; continue; } if (std.mem.eql(u8, arg, "--period")) { if (period_seen or index + 1 >= args.len) return error.InvalidUsageArgs; period_seen = true; index += 1; options.scope = if (std.mem.eql(u8, args[index], "24h")) .hours_24 else if (std.mem.eql(u8, args[index], "7d")) .days_7 else if (std.mem.eql(u8, args[index], "30d")) .days_30 else return error.InvalidUsageArgs; continue; } return error.InvalidUsageArgs; } return options;}
fn parseSessionListCursor(raw: []const u8) !session_store.ResumableSessionContinuation { if (raw.len == 0 or raw.len > 320) return error.InvalidLocalSurfaceArgs; var fields = std.mem.splitScalar(u8, raw, ':'); if (!std.mem.eql(u8, fields.next() orelse return error.InvalidLocalSurfaceArgs, "v1")) { return error.InvalidLocalSurfaceArgs; } const updated_text = fields.next() orelse return error.InvalidLocalSurfaceArgs; const id = fields.next() orelse return error.InvalidLocalSurfaceArgs; if (fields.next() != null) return error.InvalidLocalSurfaceArgs; session_store.validateSessionId(id) catch return error.InvalidLocalSurfaceArgs; const updated_at_ms = std.fmt.parseInt(i64, updated_text, 10) catch return error.InvalidLocalSurfaceArgs; var canonical: [320]u8 = undefined; const encoded = std.fmt.bufPrint( &canonical, "v1:{d}:{s}", .{ updated_at_ms, id }, ) catch return error.InvalidLocalSurfaceArgs; if (!std.mem.eql(u8, encoded, raw)) return error.InvalidLocalSurfaceArgs; return .{ .updated_at_ms = updated_at_ms, .id = id };}
fn formatSessionListCursor( alloc: Allocator, summary: session_store.SessionSummary,) ![]u8 { return std.fmt.allocPrint( alloc, "v1:{d}:{s}", .{ summary.updated_at_ms, summary.id }, );}
fn parseWorkspaceArgs(args: []const [:0]const u8) !WorkspaceOptions { var positional: [2][]const u8 = undefined; var positional_len: usize = 0; var options = WorkspaceOptions{}; for (args) |arg| { if (std.mem.eql(u8, arg, "--json")) { if (options.format == .json) return error.InvalidWorkspaceArgs; options.format = .json; continue; } if (positional_len >= positional.len) return error.InvalidWorkspaceArgs; positional[positional_len] = arg; positional_len += 1; }
if (positional_len == 0) return options; if (std.mem.eql(u8, positional[0], "list")) { if (positional_len != 1) return error.InvalidWorkspaceArgs; return options; } if (std.mem.eql(u8, positional[0], "clear")) { if (positional_len != 1) return error.InvalidWorkspaceArgs; options.action = .clear; return options; } if (std.mem.eql(u8, positional[0], "add")) { if (positional_len != 2 or positional[1].len == 0) return error.InvalidWorkspaceArgs; options.action = .{ .add = positional[1] }; return options; } if (std.mem.eql(u8, positional[0], "remove")) { if (positional_len != 2 or positional[1].len == 0) return error.InvalidWorkspaceArgs; options.action = .{ .remove = positional[1] }; return options; } return error.InvalidWorkspaceArgs;}
fn parsePersistedRecordArgs(args: []const [:0]const u8) !PersistedRecordOptions { var options = PersistedRecordOptions{}; for (args) |arg| { if (std.mem.eql(u8, arg, "--json")) { options.format = .json; continue; }
if (options.target != null) return error.InvalidPersistedRecordArgs;
const trimmed = std.mem.trim(u8, arg, " \t\r\n"); if (trimmed.len == 0) return error.InvalidPersistedRecordArgs; if (std.mem.eql(u8, trimmed, "last")) { options.target = .last; continue; }
options.target = .{ .id = std.fmt.parseUnsigned(u64, trimmed, 10) catch return error.InvalidPersistedRecordArgs, }; } return options;}
fn parseSessionDetailArgs(alloc: Allocator, args: []const [:0]const u8) !SessionDetailOptions { var options = SessionDetailOptions{}; errdefer options.deinit(alloc);
var i: usize = 0; while (i < args.len) : (i += 1) { const arg = args[i]; if (std.mem.eql(u8, arg, "--json")) { options.format = .json; continue; }
if (options.target != null) return error.InvalidSessionDetailArgs;
const exact_id = std.mem.eql(u8, arg, "--id"); if (exact_id) { i += 1; if (i >= args.len) return error.InvalidSessionDetailArgs; }
const trimmed = std.mem.trim(u8, args[i], " \t\r\n"); if (trimmed.len == 0) return error.InvalidSessionDetailArgs; if (!exact_id and std.mem.eql(u8, trimmed, "last")) { options.target = .last; continue; }
options.target = .{ .id = try alloc.dupe(u8, trimmed) }; }
return options;}
fn parseSessionMigrationArgs(alloc: Allocator, args: []const [:0]const u8) !SessionMigrationOptions { var format: output_contracts.OutputFormat = .text; var allow_large = false; var session_id: ?[]u8 = null; errdefer if (session_id) |id| alloc.free(id);
var i: usize = 0; while (i < args.len) : (i += 1) { const arg = args[i]; if (std.mem.eql(u8, arg, "--json")) { format = .json; continue; } if (std.mem.eql(u8, arg, "--allow-large")) { allow_large = true; continue; } if (session_id != null) return error.InvalidSessionMigrationArgs;
const exact_id = std.mem.eql(u8, arg, "--id"); if (exact_id) { i += 1; if (i >= args.len) return error.InvalidSessionMigrationArgs; }
const trimmed = std.mem.trim(u8, args[i], " \t\r\n"); if (trimmed.len == 0) return error.InvalidSessionMigrationArgs; session_id = try alloc.dupe(u8, trimmed); }
return .{ .format = format, .session_id = session_id orelse return error.InvalidSessionMigrationArgs, .allow_large = allow_large, };}
fn parseSessionRecoveryArgs( alloc: Allocator, args: []const [:0]const u8,) !SessionRecoveryOptions { var format: output_contracts.OutputFormat = .text; var session_id: ?[]u8 = null; errdefer if (session_id) |id| alloc.free(id);
var i: usize = 0; while (i < args.len) : (i += 1) { const arg = args[i]; if (std.mem.eql(u8, arg, "--json")) { format = .json; continue; } if (session_id != null) return error.InvalidSessionRecoveryArgs; const exact_id = std.mem.eql(u8, arg, "--id"); if (exact_id) { i += 1; if (i >= args.len) return error.InvalidSessionRecoveryArgs; } const trimmed = std.mem.trim(u8, args[i], " \t\r\n"); if (trimmed.len == 0) return error.InvalidSessionRecoveryArgs; session_id = try alloc.dupe(u8, trimmed); } return .{ .format = format, .session_id = session_id orelse return error.InvalidSessionRecoveryArgs, };}
fn parseResumeArgs( alloc: Allocator, command_catalog: CommandCatalog, args: []const [:0]const u8, top_level_alias: bool,) !ResumeTarget { if (args.len == 0) return .last;
if (top_level_alias) { if (std.mem.eql(u8, args[0], "--resume")) { if (args.len == 1) return .last; if (args.len != 2) return error.InvalidResumeArgs; const id = std.mem.trim(u8, args[1], " \t\r\n"); if (id.len == 0) return error.InvalidResumeArgs; if (std.mem.eql(u8, id, "last")) return .last; return .{ .id = try alloc.dupe(u8, id) }; } if (args.len != 1) return error.InvalidResumeArgs; if (std.mem.eql(u8, args[0], resume_picker_alias)) return .pick; if (command_specs.matchesTopLevel(command_catalog, args[0], .@"resume")) return .last; if (!std.mem.startsWith(u8, args[0], resume_id_alias_prefix)) return error.InvalidResumeArgs; const id = args[0][resume_id_alias_prefix.len..]; if (id.len == 0) return error.InvalidResumeArgs; return .{ .id = try alloc.dupe(u8, id) }; }
if (std.mem.eql(u8, args[0], "--resume")) { if (args.len == 2 and std.mem.eql(u8, args[1], "--last")) return .last; return error.InvalidResumeArgs; }
const exact_id = std.mem.eql(u8, args[0], "--id"); const operand_index: usize = if (exact_id) 1 else 0; if (args.len != operand_index + 1) return error.InvalidResumeArgs;
const trimmed = std.mem.trim(u8, args[operand_index], " \t\r\n"); if (trimmed.len == 0) return error.InvalidResumeArgs; if (!exact_id and std.mem.eql(u8, trimmed, "last")) return .last; return .{ .id = try alloc.dupe(u8, trimmed) };}
fn parseWorkflowArgs(alloc: Allocator, args: []const [:0]const u8) !WorkflowOptions { var auto_permission = false; var create = false; var start_index: usize = 0; while (start_index < args.len) : (start_index += 1) { if (std.mem.eql(u8, args[start_index], "--auto")) { auto_permission = true; continue; } if (std.mem.eql(u8, args[start_index], "--create")) { create = true; continue; } break; }
return .{ .auto_permission = auto_permission, .create = create, .context = try joinArgs(alloc, args[start_index..]), };}
fn joinArgs(alloc: Allocator, args: []const [:0]const u8) ![]u8 { if (args.len == 0) return alloc.dupe(u8, "");
var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); for (args, 0..) |arg, i| { if (i > 0) try out.writer.writeByte(' '); try out.writer.writeAll(arg); } return try out.toOwnedSlice();}
fn isVersionFlag(arg: []const u8) bool { return std.mem.eql(u8, arg, "--version") or std.mem.eql(u8, arg, "-v");}
fn testCommandCatalog() CommandCatalog { const builtin_commands = @import("../../builtins/commands.zig"); return builtin_commands.top_level_registry;}
test "parse recognizes every top-level command and preserves unknown commands" { const command_catalog = testCommandCatalog(); try std.testing.expectEqual(Command.interactive, parse(command_catalog, &.{})); try std.testing.expectEqual(Command.help, parse(command_catalog, &.{@constCast("help")}));
switch (parse(command_catalog, &.{ @constCast("ask"), @constCast("hello") })) { .ask => |rest| try std.testing.expectEqual(@as(usize, 1), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("acp"), @constCast("--model"), @constCast("m") })) { .acp => |rest| try std.testing.expectEqual(@as(usize, 2), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("pr"), @constCast("ready") })) { .pr => |rest| try std.testing.expectEqual(@as(usize, 1), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("issue"), @constCast("flaky") })) { .issue => |rest| try std.testing.expectEqual(@as(usize, 1), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{@constCast("setup")})) { .setup => |rest| try std.testing.expectEqual(@as(usize, 0), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("status"), @constCast("--json") })) { .status => |rest| try std.testing.expectEqual(@as(usize, 1), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{@constCast("permissions")})) { .permissions => |rest| try std.testing.expectEqual(@as(usize, 0), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("models"), @constCast("--json") })) { .models => |rest| try std.testing.expectEqual(@as(usize, 1), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{@constCast("doctor")})) { .doctor => |rest| try std.testing.expectEqual(@as(usize, 0), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{@constCast("background")})) { .background => |rest| try std.testing.expectEqual(@as(usize, 0), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("session"), @constCast("last") })) { .session => |rest| try std.testing.expectEqual(@as(usize, 1), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("session"), @constCast("resume"), @constCast("last") })) { .resume_session => |invocation| try std.testing.expectEqual(@as(usize, 1), invocation.args.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("sessions"), @constCast("--json") })) { .sessions => |rest| try std.testing.expectEqual(@as(usize, 1), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("resume"), @constCast("last") })) { .resume_session => |invocation| try std.testing.expectEqual(@as(usize, 1), invocation.args.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("credits"), @constCast("--json") })) { .credits => |rest| try std.testing.expectEqual(@as(usize, 1), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("usage"), @constCast("--period"), @constCast("24h") })) { .usage => |rest| try std.testing.expectEqual(@as(usize, 2), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{@constCast("upgrade")})) { .upgrade => |rest| try std.testing.expectEqual(@as(usize, 0), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{ @constCast("replay"), @constCast("tape") })) { .replay => |rest| try std.testing.expectEqual(@as(usize, 1), rest.len), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{@constCast("wat")})) { .unknown => |value| try std.testing.expectEqualStrings("wat", value), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{@constCast("task")})) { .unknown => |value| try std.testing.expectEqualStrings("task", value), else => return error.TestExpectedEqual, } switch (parse(command_catalog, &.{@constCast("tasks")})) { .unknown => |value| try std.testing.expectEqualStrings("tasks", value), else => return error.TestExpectedEqual, }}
test "help aliases route to help" { const command_catalog = testCommandCatalog(); try std.testing.expectEqual(Command.help, parse(command_catalog, &.{@constCast("--help")})); try std.testing.expectEqual(Command.help, parse(command_catalog, &.{@constCast("-h")}));}
test "usage arguments accept only rolling periods and one JSON flag" { const defaults = try parseUsageArgs(&.{}); try std.testing.expectEqual(usage_report.Scope.days_30, defaults.scope); try std.testing.expectEqual(output_contracts.OutputFormat.text, defaults.format);
const selected = try parseUsageArgs(&.{ @constCast("--json"), @constCast("--period"), @constCast("7d"), }); try std.testing.expectEqual(usage_report.Scope.days_7, selected.scope); try std.testing.expectEqual(output_contracts.OutputFormat.json, selected.format);
for ([_][]const [:0]const u8{ &.{@constCast("--period")}, &.{ @constCast("--period"), @constCast("session") }, &.{ @constCast("--period"), @constCast("24h"), @constCast("--period"), @constCast("7d") }, &.{ @constCast("--json"), @constCast("--json") }, &.{@constCast("30d")}, }) |invalid| { try std.testing.expectError(error.InvalidUsageArgs, parseUsageArgs(invalid)); }}
test "global launch modifiers preserve repeatable context limits before the command" { var parsed = try parseGlobalLaunchArgs(std.testing.allocator, &.{ @constCast("--context-limit"), @constCast("skill_chunk_bytes=4096"), @constCast("--context-limit=mcp_description_bytes=off"), @constCast("ask"), @constCast("hello"), }); defer parsed.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 2), parsed.modifiers.context_limit_overrides.len); try std.testing.expectEqual(config_runtime.context_limits.Name.skill_chunk_bytes, parsed.modifiers.context_limit_overrides[0].name); try std.testing.expectEqual(@as(usize, 4096), parsed.modifiers.context_limit_overrides[0].value.bytes); try std.testing.expectEqual(config_runtime.context_limits.Name.mcp_description_bytes, parsed.modifiers.context_limit_overrides[1].name); try std.testing.expect(parsed.modifiers.context_limit_overrides[1].value == .off); try std.testing.expectEqualStrings("ask", parsed.remaining[0]); try std.testing.expectEqualStrings("hello", parsed.remaining[1]);}
test "global context limits reject missing values and stop at the command" { try std.testing.expectError( error.MissingContextLimitValue, parseGlobalLaunchArgs(std.testing.allocator, &.{@constCast("--context-limit")}), ); var parsed = try parseGlobalLaunchArgs(std.testing.allocator, &.{ @constCast("ask"), @constCast("--context-limit"), @constCast("skill_chunk_bytes=1"), }); defer parsed.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 0), parsed.modifiers.context_limit_overrides.len); try std.testing.expectEqual(@as(usize, 3), parsed.remaining.len);}
test "global launch modifiers own repeatable additional directories and suppression" { var parsed = try parseGlobalLaunchArgs(std.testing.allocator, &.{ @constCast("--add-dir"), @constCast("/tmp/shared one"), @constCast("--context-limit=skill_chunk_bytes=2048"), @constCast("--add-dir=/tmp/shared-two"), @constCast("--no-additional-dirs"), @constCast("ask"), @constCast("inspect"), }); defer parsed.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 2), parsed.modifiers.additional_directories.len); try std.testing.expectEqualStrings("/tmp/shared one", parsed.modifiers.additional_directories[0]); try std.testing.expectEqualStrings("/tmp/shared-two", parsed.modifiers.additional_directories[1]); try std.testing.expect(parsed.modifiers.saved_directories_suppressed); try std.testing.expectEqualStrings("ask", parsed.remaining[0]);}
test "additional directory flags fail closed when malformed" { try std.testing.expectError( error.MissingAddDirectoryValue, parseGlobalLaunchArgs(std.testing.allocator, &.{@constCast("--add-dir")}), ); try std.testing.expectError( error.MissingAddDirectoryValue, parseGlobalLaunchArgs(std.testing.allocator, &.{@constCast("--add-dir=")}), ); try std.testing.expectError( error.DuplicateAdditionalDirectorySuppression, parseGlobalLaunchArgs(std.testing.allocator, &.{ @constCast("--no-additional-dirs"), @constCast("--no-additional-dirs") }), );}
test "parse acp args extracts known flags and rejects invalid arguments" { const opts = try parseAcpArgs(&.{ @constCast("--model"), @constCast("openai/gpt-4o"), @constCast("--log-file"), @constCast("/tmp/fx.log"), }); try std.testing.expectEqualStrings("openai/gpt-4o", opts.model.?); try std.testing.expectEqualStrings("/tmp/fx.log", opts.log_file.?);
try std.testing.expectError(error.InvalidAcpArgs, parseAcpArgs(&.{@constCast("--unknown")})); try std.testing.expectError(error.InvalidAcpArgs, parseAcpArgs(&.{@constCast("--model")})); try std.testing.expectError(error.InvalidAcpArgs, parseAcpArgs(&.{@constCast("--log-file")})); try std.testing.expectError( error.InvalidAcpArgs, parseAcpArgs(&.{ @constCast("--model"), @constCast("first"), @constCast("--model"), @constCast("second") }), );}
test "ACP command routes parsed options and launch config through the injected runner" { const Capture = struct { expected: Config, calls: usize = 0, config_matches: bool = false, launch_matches: bool = false,
fn run(raw: ?*anyopaque, _: Allocator, cfg: acp_runner.Config) anyerror!void { const self: *@This() = @ptrCast(@alignCast(raw.?)); self.calls += 1; const expected = self.expected; self.config_matches = std.mem.eql(u8, cfg.default_model, expected.default_model) and cfg.default_agent_step_limit == expected.default_agent_step_limit and cfg.gateway_retry_count == expected.gateway_retry_count and std.mem.eql( u8, cfg.gateway_chat_url, expected.gateway_provider.chat_url.resolve(expected.gateway_chat_url), ) and std.mem.eql(u8, cfg.gateway_models_path, expected.models_path) and cfg.gateway_provider.chat_url.resolve_fn == expected.gateway_provider.chat_url.resolve_fn and std.mem.eql(u8, cfg.prompt_policy.system_prompt, expected.prompt_policy.system_prompt) and cfg.ignored_list_entries.len == expected.ignored_list_entries.len and cfg.max_list_entries == expected.max_list_entries and cfg.max_read_file_bytes == expected.max_read_file_bytes and cfg.max_read_file_lines == expected.max_read_file_lines and cfg.max_read_file_line_len == expected.max_read_file_line_len and cfg.max_command_output_bytes == expected.max_command_output_bytes and cfg.max_tool_result_bytes == expected.max_tool_result_bytes and cfg.max_history_turns == expected.max_history_turns and std.mem.eql( u8, cfg.context_registry.defaultProvider().id, expected.context_registry.defaultProvider().id, ) and std.mem.eql(u8, cfg.mode_registry.default_mode_id, expected.mode_registry.default_mode_id) and cfg.permission_reviewer_provider.?.review_fn == expected.permission_reviewer_provider.?.review_fn;
const limit_matches = cfg.context_limit_overrides.len == 1 and cfg.context_limit_overrides[0].name == .project_instructions_total_bytes and switch (cfg.context_limit_overrides[0].value) { .bytes => |bytes| bytes == 1234, .off => false, }; self.launch_matches = limit_matches and cfg.additional_directories.len == 1 and std.mem.eql(u8, cfg.additional_directories[0], "/tmp/acp-extra") and cfg.saved_directories_suppressed and std.mem.eql(u8, cfg.model_override.?, "model-override") and std.mem.eql(u8, cfg.log_file.?, "/tmp/acp.log"); } };
var cfg = testConfig(); cfg.permission_reviewer_provider = test_builtin_gateway.permission_reviewer.provider; var capture = Capture{ .expected = cfg }; cfg.acp_runner = .{ .context = &capture, .run_fn = Capture.run }; const result = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("--context-limit"), @constCast("project_instructions_total_bytes=1234"), @constCast("--add-dir"), @constCast("/tmp/acp-extra"), @constCast("--no-additional-dirs"), @constCast("acp"), @constCast("--model"), @constCast("model-override"), @constCast("--log-file"), @constCast("/tmp/acp.log"), }, cfg, .{}, );
try std.testing.expectEqual(RunResult.handled_success, result); try std.testing.expectEqual(@as(usize, 1), capture.calls); try std.testing.expect(capture.config_matches); try std.testing.expect(capture.launch_matches);}
test "ACP runner errors preserve their identity" { const Fixture = struct { fn run(_: ?*anyopaque, _: Allocator, _: acp_runner.Config) anyerror!void { return error.TestAcpRunnerFailed; } };
var cfg = testConfig(); cfg.acp_runner = .{ .run_fn = Fixture.run }; try std.testing.expectError( error.TestAcpRunnerFailed, runIfRequested(std.testing.allocator, &.{@constCast("acp")}, cfg), );}
test "parse local surface args accepts only json" { const empty = try parseLocalSurfaceArgs(&.{}); try std.testing.expectEqual(output_contracts.OutputFormat.text, empty.format);
const opts = try parseLocalSurfaceArgs(&.{@constCast("--json")}); try std.testing.expectEqual(output_contracts.OutputFormat.json, opts.format);
try std.testing.expectError(error.InvalidLocalSurfaceArgs, parseLocalSurfaceArgs(&.{@constCast("--wat")}));}
test "parse upgrade args accepts a remembered release channel" { const defaults = try parseUpgradeArgs(&.{}); try std.testing.expectEqual(output_contracts.OutputFormat.text, defaults.format); try std.testing.expect(defaults.channel == null);
const selected = try parseUpgradeArgs(&.{ @constCast("--channel"), @constCast("dev"), @constCast("--json"), }); try std.testing.expectEqual(output_contracts.OutputFormat.json, selected.format); try std.testing.expectEqual(update_target.Channel.dev, selected.channel.?);
const stable = try parseUpgradeArgs(&.{@constCast("--channel=stable")}); try std.testing.expectEqual(update_target.Channel.stable, stable.channel.?);
try std.testing.expectError( error.InvalidUpgradeArgs, parseUpgradeArgs(&.{ @constCast("--channel"), @constCast("nightly") }), ); try std.testing.expectError( error.InvalidUpgradeArgs, parseUpgradeArgs(&.{ @constCast("--channel=dev"), @constCast("--channel=stable") }), );}
test "parse session list args supports bounded canonical pagination" { const empty = try parseSessionListArgs(&.{}); try std.testing.expectEqual(output_contracts.OutputFormat.text, empty.format); try std.testing.expectEqual(session_store.session_list_default_limit, empty.limit); try std.testing.expect(empty.continuation == null);
const paged = try parseSessionListArgs(&.{ @constCast("--json"), @constCast("--all"), @constCast("--limit"), @constCast("2"), @constCast("--cursor"), @constCast("v1:20:session-a"), }); try std.testing.expectEqual(output_contracts.OutputFormat.json, paged.format); try std.testing.expectEqual(session_store.SessionListScope.all_workspaces, paged.scope); try std.testing.expectEqual(@as(usize, 2), paged.limit); try std.testing.expectEqual(@as(i64, 20), paged.continuation.?.updated_at_ms); try std.testing.expectEqualStrings("session-a", paged.continuation.?.id);
try std.testing.expectError( error.InvalidLocalSurfaceArgs, parseSessionListArgs(&.{ @constCast("--all"), @constCast("--all") }), ); try std.testing.expectError( error.InvalidLocalSurfaceArgs, parseSessionListArgs(&.{ @constCast("--limit"), @constCast("0") }), ); try std.testing.expectError( error.InvalidLocalSurfaceArgs, parseSessionListArgs(&.{ @constCast("--limit"), @constCast("101") }), ); try std.testing.expectError( error.InvalidLocalSurfaceArgs, parseSessionListArgs(&.{ @constCast("--limit"), @constCast("2"), @constCast("--limit"), @constCast("3") }), ); try std.testing.expectError( error.InvalidLocalSurfaceArgs, parseSessionListArgs(&.{@constCast("--cursor")}), ); try std.testing.expectError( error.InvalidLocalSurfaceArgs, parseSessionListArgs(&.{ @constCast("--cursor"), @constCast("v1:020:session-a") }), ); try std.testing.expectError( error.InvalidLocalSurfaceArgs, parseSessionListArgs(&.{ @constCast("--cursor"), @constCast("v2:20:session-a") }), ); try std.testing.expectError( error.InvalidLocalSurfaceArgs, parseSessionListArgs(&.{ @constCast("--cursor"), @constCast("v1:20:../unsafe") }), );}
test "parse persisted record args supports empty last numeric id and json" { const empty = try parsePersistedRecordArgs(&.{}); try std.testing.expectEqual(output_contracts.OutputFormat.text, empty.format); try std.testing.expect(empty.target == null);
const latest = try parsePersistedRecordArgs(&.{ @constCast("last"), @constCast("--json") }); try std.testing.expectEqual(output_contracts.OutputFormat.json, latest.format); try std.testing.expectEqual(PersistedRecordTarget.last, latest.target.?);
const specific = try parsePersistedRecordArgs(&.{@constCast(" 7 ")}); switch (specific.target.?) { .id => |value| try std.testing.expectEqual(@as(u64, 7), value), else => return error.TestExpectedEqual, }
try std.testing.expectError(error.InvalidPersistedRecordArgs, parsePersistedRecordArgs(&.{ @constCast("7"), @constCast("8") })); try std.testing.expectError(error.InvalidPersistedRecordArgs, parsePersistedRecordArgs(&.{@constCast("abc")})); try std.testing.expectError(error.InvalidPersistedRecordArgs, parsePersistedRecordArgs(&.{@constCast(" ")}));}
test "parse session detail args owns string ids and frees through deinit" { var latest = try parseSessionDetailArgs(std.testing.allocator, &.{ @constCast("last"), @constCast("--json") }); defer latest.deinit(std.testing.allocator); try std.testing.expectEqual(output_contracts.OutputFormat.json, latest.format); try std.testing.expectEqual(SessionDetailTarget.last, latest.target.?);
var specific = try parseSessionDetailArgs(std.testing.allocator, &.{@constCast(" sess-1 ")}); defer specific.deinit(std.testing.allocator); switch (specific.target.?) { .id => |value| try std.testing.expectEqualStrings("sess-1", value), else => return error.TestExpectedEqual, }
try std.testing.expectError(error.InvalidSessionDetailArgs, parseSessionDetailArgs(std.testing.allocator, &.{ @constCast("a"), @constCast("b") })); try std.testing.expectError(error.InvalidSessionDetailArgs, parseSessionDetailArgs(std.testing.allocator, &.{@constCast("")}));}
test "parse session detail args accepts explicit id flag" { var specific = try parseSessionDetailArgs(std.testing.allocator, &.{ @constCast("--id"), @constCast("release.2026.06"), @constCast("--json"), }); defer specific.deinit(std.testing.allocator);
try std.testing.expectEqual(output_contracts.OutputFormat.json, specific.format); switch (specific.target.?) { .last => return error.TestExpectedExactResumeId, .id => |id| try std.testing.expectEqualStrings("release.2026.06", id), }}
test "parse session detail args treats last after id flag as exact id" { var specific = try parseSessionDetailArgs(std.testing.allocator, &.{ @constCast("--id"), @constCast("last"), }); defer specific.deinit(std.testing.allocator);
switch (specific.target.?) { .last => return error.TestExpectedExactResumeId, .id => |id| try std.testing.expectEqualStrings("last", id), }}
test "parse session migration args accepts positional and exact ids" { var positional = try parseSessionMigrationArgs(std.testing.allocator, &.{ @constCast("session.v2"), @constCast("--allow-large"), @constCast("--json"), }); defer positional.deinit(std.testing.allocator); try std.testing.expectEqualStrings("session.v2", positional.session_id); try std.testing.expect(positional.allow_large); try std.testing.expectEqual(output_contracts.OutputFormat.json, positional.format);
var exact = try parseSessionMigrationArgs(std.testing.allocator, &.{ @constCast("--id"), @constCast("--allow-large"), @constCast("--json"), }); defer exact.deinit(std.testing.allocator); try std.testing.expectEqualStrings("--allow-large", exact.session_id); try std.testing.expect(!exact.allow_large); try std.testing.expectEqual(output_contracts.OutputFormat.json, exact.format);}
test "parse session migration args rejects missing repeated and mixed targets" { try std.testing.expectError( error.InvalidSessionMigrationArgs, parseSessionMigrationArgs(std.testing.allocator, &.{@constCast("--id")}), ); try std.testing.expectError( error.InvalidSessionMigrationArgs, parseSessionMigrationArgs(std.testing.allocator, &.{ @constCast("session.v2"), @constCast("--id"), @constCast("session.v3"), }), ); try std.testing.expectError( error.InvalidSessionMigrationArgs, parseSessionMigrationArgs(std.testing.allocator, &.{ @constCast("session.v2"), @constCast("session.v3"), }), );}
test "parse session recovery args accepts exact ids and rejects ambiguity" { var positional = try parseSessionRecoveryArgs( std.testing.allocator, &.{ @constCast("session.v3"), @constCast("--json"), }, ); defer positional.deinit(std.testing.allocator); try std.testing.expectEqualStrings("session.v3", positional.session_id); try std.testing.expectEqual( output_contracts.OutputFormat.json, positional.format, );
var exact = try parseSessionRecoveryArgs( std.testing.allocator, &.{ @constCast("--id"), @constCast("last"), }, ); defer exact.deinit(std.testing.allocator); try std.testing.expectEqualStrings("last", exact.session_id);
try std.testing.expectError( error.InvalidSessionRecoveryArgs, parseSessionRecoveryArgs( std.testing.allocator, &.{@constCast("--id")}, ), ); try std.testing.expectError( error.InvalidSessionRecoveryArgs, parseSessionRecoveryArgs( std.testing.allocator, &.{ @constCast("first"), @constCast("second"), }, ), );}
test "parse resume args defaults to last owns ids and rejects invalid input" { const command_catalog = testCommandCatalog(); const implicit = try parseResumeArgs(std.testing.allocator, command_catalog, &.{}, false); try std.testing.expectEqual(ResumeTarget.last, implicit);
const explicit = try parseResumeArgs(std.testing.allocator, command_catalog, &.{@constCast("last")}, false); try std.testing.expectEqual(ResumeTarget.last, explicit);
var target = try parseResumeArgs(std.testing.allocator, command_catalog, &.{@constCast(" session-123 ")}, false); defer target.deinit(std.testing.allocator); switch (target) { .id => |value| try std.testing.expectEqualStrings("session-123", value), else => return error.TestExpectedEqual, }
try std.testing.expectError(error.InvalidResumeArgs, parseResumeArgs(std.testing.allocator, command_catalog, &.{ @constCast("a"), @constCast("b") }, false)); try std.testing.expectError(error.InvalidResumeArgs, parseResumeArgs(std.testing.allocator, command_catalog, &.{@constCast(" ")}, false));}
test "parse resume args accepts explicit id flag" { const command_catalog = testCommandCatalog(); var target = try parseResumeArgs(std.testing.allocator, command_catalog, &.{ @constCast("--id"), @constCast("release.2026.06"), }, false); defer target.deinit(std.testing.allocator);
switch (target) { .pick, .last => return error.TestExpectedExactResumeId, .id => |id| try std.testing.expectEqualStrings("release.2026.06", id), }}
test "parse resume args accepts an operand on the top-level resume flag" { const command_catalog = testCommandCatalog(); var target = try parseResumeArgs(std.testing.allocator, command_catalog, &.{ @constCast("--resume"), @constCast("session-123"), }, true); defer target.deinit(std.testing.allocator);
switch (target) { .pick, .last => return error.TestExpectedExactResumeId, .id => |id| try std.testing.expectEqualStrings("session-123", id), }
const latest = try parseResumeArgs(std.testing.allocator, command_catalog, &.{ @constCast("--resume"), @constCast("last"), }, true); try std.testing.expectEqual(ResumeTarget.last, latest);}
test "parse resume args treats last after id flag as exact id" { const command_catalog = testCommandCatalog(); var target = try parseResumeArgs(std.testing.allocator, command_catalog, &.{ @constCast("--id"), @constCast("last"), }, false); defer target.deinit(std.testing.allocator);
switch (target) { .pick, .last => return error.TestExpectedExactResumeId, .id => |id| try std.testing.expectEqualStrings("last", id), }}
test "parseInteractiveLaunch shares native resume grammar" { const alloc = std.testing.allocator; const command_catalog = testCommandCatalog(); const cases = [_]struct { args: []const [:0]const u8, expected_id: ?[]const u8, }{ .{ .args = &.{@constCast("--resume")}, .expected_id = null }, .{ .args = &.{ @constCast("--resume"), @constCast("last") }, .expected_id = null }, .{ .args = &.{ @constCast("--resume"), @constCast("session-123") }, .expected_id = "session-123" }, .{ .args = &.{ @constCast("session"), @constCast("resume"), @constCast("last") }, .expected_id = null }, .{ .args = &.{ @constCast("session"), @constCast("resume"), @constCast("--id"), @constCast("session.v3") }, .expected_id = "session.v3" }, }; for (cases) |case| { const parsed = try parseInteractiveLaunch(alloc, case.args, command_catalog); switch (parsed) { .interactive => |value| { var launch = value; defer launch.deinit(alloc); const target = launch.requested_resume orelse return error.TestExpectedResumeTarget; if (case.expected_id) |expected_id| switch (target) { .id => |id| try std.testing.expectEqualStrings(expected_id, id), .pick, .last => return error.TestExpectedExactResumeId, } else try std.testing.expectEqual(ResumeTarget.last, target); }, .noninteractive => |value| { var noninteractive = value; defer noninteractive.deinit(alloc); return error.TestExpectedInteractiveLaunch; }, } }
try std.testing.expectError( error.InvalidResumeArgs, parseInteractiveLaunch( alloc, &.{ @constCast("--resume"), @constCast("one"), @constCast("two") }, command_catalog, ), ); try std.testing.expectError( error.MissingAddDirectoryValue, parseInteractiveLaunch(alloc, &.{@constCast("--add-dir")}, command_catalog), );}
test "parse workflow args consumes leading flags and joins remaining context exactly" { var opts = try parseWorkflowArgs(std.testing.allocator, &.{ @constCast("--auto"), @constCast("--create"), @constCast("ready"), @constCast("for"), @constCast("review"), }); defer opts.deinit(std.testing.allocator); try std.testing.expect(opts.auto_permission); try std.testing.expect(opts.create); try std.testing.expectEqualStrings("ready for review", opts.context);
var later_flag = try parseWorkflowArgs(std.testing.allocator, &.{ @constCast("context"), @constCast("--auto"), }); defer later_flag.deinit(std.testing.allocator); try std.testing.expect(!later_flag.auto_permission); try std.testing.expect(!later_flag.create); try std.testing.expectEqualStrings("context --auto", later_flag.context);
var empty = try parseWorkflowArgs(std.testing.allocator, &.{}); defer empty.deinit(std.testing.allocator); try std.testing.expectEqualStrings("", empty.context);}
test "runIfRequested help writes top-level help" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{@constCast("help")}, testConfig(), capture.deps()); try std.testing.expectEqual(RunResult.handled_success, result); try std.testing.expect(std.mem.startsWith(u8, capture.stdout.written(), "𝒇x v0.0.0\nFast, native coding agent for the terminal.")); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), testConfig().version) != null); try std.testing.expectEqualStrings("", capture.stderr.written());}
test "workspace launch modifiers preserve supported command help" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); const deps = capture.deps();
const result = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("--add-dir"), @constCast("/tmp/shared"), @constCast("ask"), @constCast("--help") }, testConfig(), deps, ); try std.testing.expectEqual(RunResult.handled_success, result); try std.testing.expect(std.mem.startsWith(u8, capture.stdout.written(), "fx ask\n\n")); try std.testing.expectEqualStrings("", capture.stderr.written());}
test "workspace launch modifiers still reject unsupported local command help" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); const deps = capture.deps();
const result = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("--add-dir"), @constCast("/tmp/shared"), @constCast("status"), @constCast("--help") }, testConfig(), deps, ); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings("", capture.stdout.written()); try std.testing.expect(std.mem.find(u8, capture.stderr.written(), "only supported for interactive, resume, ask, ACP, PR, and issue launches") != null);}
test "global workspace launch option errors use user-facing copy" { const cases = [_]struct { args: []const [:0]const u8, expected: []const u8, }{ .{ .args = &.{@constCast("--add-dir")}, .expected = "fx: --add-dir requires a directory path\n", }, .{ .args = &.{ @constCast("--no-additional-dirs"), @constCast("--no-additional-dirs") }, .expected = "fx: --no-additional-dirs may only be specified once\n", }, };
for (cases) |case| { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); const deps = capture.deps();
const result = try runIfRequestedWithDeps(std.testing.allocator, case.args, testConfig(), deps); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings("", capture.stdout.written()); try std.testing.expect(std.mem.startsWith(u8, capture.stderr.written(), case.expected)); try std.testing.expect(std.mem.endsWith(u8, capture.stderr.written(), "<command>\n")); }}
test "runIfRequested version flags write configured version" { const cases = [_][]const [:0]const u8{ &.{@constCast("--version")}, &.{@constCast("-v")}, };
for (cases) |args| { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps(std.testing.allocator, args, testConfig(), capture.deps()); try std.testing.expectEqual(RunResult.handled_success, result); try std.testing.expectEqualStrings("0.0.0\n", capture.stdout.written()); try std.testing.expectEqualStrings("", capture.stderr.written()); }}
test "runIfRequested version flags reject extra args" { const cases = [_][]const [:0]const u8{ &.{ @constCast("--version"), @constCast("extra") }, &.{ @constCast("-v"), @constCast("extra") }, };
for (cases) |args| { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps(std.testing.allocator, args, testConfig(), capture.deps()); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings("", capture.stdout.written()); try std.testing.expectEqualStrings("usage: fx --version\n", capture.stderr.written()); }}
test "setup is a paste-only stored-key adapter" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); var cfg = testConfig(); cfg.secret_store = capture.secretStore();
const result = try runIfRequestedWithDeps( std.testing.allocator, &.{@constCast("setup")}, cfg, capture.deps(), );
try std.testing.expectEqual(RunResult.handled_success, result); try std.testing.expectEqual(@as(usize, 1), capture.setup_store_calls); try std.testing.expectEqual(@as(usize, 1), capture.setup_read_calls); try std.testing.expect(capture.setup_value_matched); try std.testing.expect(std.mem.find(u8, capture.stderr.written(), "Paste AI Gateway API key") != null); try std.testing.expect(std.mem.find(u8, capture.stderr.written(), "Vercel CLI") == null); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), cfg.secret_store.backend_label) != null);}
test "setup delegates secure input to an interactive host store" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); capture.setup_interactive_store = true; var cfg = testConfig(); cfg.secret_store = capture.secretStore();
const result = try runIfRequestedWithDeps( std.testing.allocator, &.{@constCast("setup")}, cfg, capture.deps(), );
try std.testing.expectEqual(RunResult.handled_success, result); try std.testing.expectEqual(@as(usize, 1), capture.setup_store_calls); try std.testing.expectEqual(@as(usize, 0), capture.setup_read_calls); try std.testing.expect(!capture.setup_value_matched);}
test "setup preserves the disabled secret-store failure" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); capture.setup_store_disabled = true; var cfg = testConfig(); cfg.secret_store = capture.secretStore();
const result = try runIfRequestedWithDeps( std.testing.allocator, &.{@constCast("setup")}, cfg, capture.deps(), );
try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqual(@as(usize, 0), capture.setup_store_calls); try std.testing.expectEqual(@as(usize, 0), capture.setup_read_calls); try std.testing.expectEqualStrings( "fx setup: stored API keys are disabled by FX_DISABLE_KEYCHAIN\n", capture.stderr.written(), );}
test "workspace indeterminate errors report the reconciled durable state" { const cases = [_]struct { reconciliation: workspace_commands.Reconciliation, expected: []const u8, }{ .{ .reconciliation = .{ .intended = .{} }, .expected = "reloaded settings match the requested update", }, .{ .reconciliation = .{ .previous = .{} }, .expected = "reloaded settings match the previous state", }, .{ .reconciliation = .unconfirmed, .expected = "reloaded settings match neither the requested nor previous state", }, };
for (cases) |case| { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); try writeWorkspaceIndeterminateError( std.testing.allocator, capture.deps(), &.{@constCast("--json")}, case.reconciliation, ); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), case.expected) != null); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), "\"code\":\"SettingsCommitIndeterminate\"") != null); try std.testing.expectEqualStrings("", capture.stderr.written()); }}
test "workspace json errors keep stable codes with shared user-facing copy" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
try writeWorkspaceCommandError( std.testing.allocator, testCommandCatalog(), capture.deps(), &.{@constCast("--json")}, error.PrimaryDirectory, ); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), "\"error\":\"the primary workspace cannot be added or removed\"") != null); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), "\"code\":\"PrimaryDirectory\"") != null); try std.testing.expectEqualStrings("", capture.stderr.written());}
test "workspace unknown directory errors keep stable json codes" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
try writeWorkspaceCommandError( std.testing.allocator, testCommandCatalog(), capture.deps(), &.{@constCast("--json")}, error.UnknownAdditionalDirectory, ); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), "\"error\":\"directory is not configured as an additional workspace\"") != null); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), "\"code\":\"UnknownAdditionalDirectory\"") != null); try std.testing.expectEqualStrings("", capture.stderr.written());}
test "runIfRequested rejects record modifier outside interactive startup" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("help"), @constCast("--record") }, testConfig(), capture.deps(), );
try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings("usage: fx --record is only supported for interactive startup\n", capture.stderr.written());}
test "runIfRequested carries record intent through supported interactive launches" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const plain = try runIfRequestedWithDeps( std.testing.allocator, &.{@constCast("--record")}, testConfig(), capture.deps(), ); switch (plain) { .interactive => |launch| { try std.testing.expect(launch.record_requested); try std.testing.expect(launch.requested_resume == null); }, else => return error.TestExpectedInteractiveLaunch, }
const resumed = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("--resume"), @constCast("session-123"), @constCast("--record") }, testConfig(), capture.deps(), ); switch (resumed) { .interactive => |launch_value| { var launch = launch_value; defer launch.deinit(std.testing.allocator); try std.testing.expect(launch.record_requested); switch (launch.requested_resume.?) { .id => |id| try std.testing.expectEqualStrings("session-123", id), .pick, .last => return error.TestExpectedResumeId, } }, else => return error.TestExpectedInteractiveLaunch, }
const grouped = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("session"), @constCast("resume"), @constCast("session-123"), @constCast("--record") }, testConfig(), capture.deps(), ); switch (grouped) { .interactive => |launch_value| { var launch = launch_value; defer launch.deinit(std.testing.allocator); try std.testing.expect(launch.record_requested); switch (launch.requested_resume.?) { .id => |id| try std.testing.expectEqualStrings("session-123", id), .pick, .last => return error.TestExpectedResumeId, } }, else => return error.TestExpectedInteractiveLaunch, }}
test "runIfRequested rejects record modifier before noninteractive handlers" { const cases = [_][]const [:0]const u8{ &.{ @constCast("ask"), @constCast("--record"), @constCast("hello") }, &.{ @constCast("acp"), @constCast("--record") }, &.{ @constCast("pr"), @constCast("--record") }, &.{ @constCast("issue"), @constCast("--record") }, &.{ @constCast("--version"), @constCast("--record") }, &.{ @constCast("-v"), @constCast("--record") }, }; for (cases) |args| { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps(std.testing.allocator, args, testConfig(), capture.deps()); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings(record_modifier_usage, capture.stderr.written()); }}
test "runNoConfigIfRequested handles help without config" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
try std.testing.expect(try runNoConfigIfRequestedWithDeps( std.testing.allocator, &.{@constCast("help")}, "0.0.0", testCommandCatalog(), capture.deps(), )); try std.testing.expect(std.mem.startsWith(u8, capture.stdout.written(), "𝒇x v0.0.0\nFast, native coding agent for the terminal.")); try std.testing.expectEqualStrings("", capture.stderr.written());
try std.testing.expect(!try runNoConfigIfRequestedWithDeps( std.testing.allocator, &.{@constCast("status")}, "0.0.0", testCommandCatalog(), capture.deps(), ));}
test "CLI surface uses the supplied command catalog for parsing usage and help" { const specs = [_]command_specs.TopLevelSpec{ .{ .kind = .help, .token = "guide", .aliases = &.{"-?"}, .usage = "guide", .summary = "Show injected help", }, .{ .kind = .setup, .token = "start", .usage = "start", .summary = "Run injected setup", }, }; const help_groups = [_]command_specs.TopLevelHelpGroup{ .{ .entries = &.{ .{ .kind = .setup, .usage = "start" }, .{ .kind = .help, .usage = "guide" }, } }, }; const command_catalog = CommandCatalog{ .specs = &specs, .description = "Injected command catalog.", .interactive_hint = "Injected interactive hint.", .help_groups = &help_groups, };
try std.testing.expectEqual(Command.help, parse(command_catalog, &.{@constCast("-?")})); switch (parse(command_catalog, &.{@constCast("start")})) { .setup => {}, else => return error.TestExpectedEqual, }
var help_capture = CaptureOutput.init(std.testing.allocator); defer help_capture.deinit(); try std.testing.expect(try runNoConfigIfRequestedWithDeps( std.testing.allocator, &.{@constCast("guide")}, "1.2.3", command_catalog, help_capture.deps(), )); try std.testing.expect(std.mem.find(u8, help_capture.stdout.written(), "Injected command catalog.") != null);
var usage_capture = CaptureOutput.init(std.testing.allocator); defer usage_capture.deinit(); var cfg = testConfig(); cfg.command_catalog = command_catalog; const result = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("start"), @constCast("unexpected") }, cfg, usage_capture.deps(), ); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings("usage: fx start\n", usage_capture.stderr.written());}
test "workflow config does not carry placeholder gateway tools" { const skill_roots = [_]skill_contract.RootSpec{ .{ .source = .workspace_shared, .path = "skills" }, }; var chat_url_probe = ChatUrlProbe{}; var surface_cfg = testConfig(); surface_cfg.skill_root_policy.workspace_roots = &skill_roots; surface_cfg.gateway_provider.chat_url = chat_url_probe.provider(); const cfg = workflowConfig(surface_cfg); try std.testing.expect(!@hasField(@TypeOf(cfg), "gateway_tools_json")); try std.testing.expect(!@hasField(@TypeOf(cfg), "context_registry")); try std.testing.expectEqualStrings("test-model", cfg.default_model); try std.testing.expectEqualStrings("http://127.0.0.1:43123/chat", cfg.gateway_chat_url); try std.testing.expect(chat_url_probe.called); try std.testing.expectEqualStrings("surface", cfg.mode_registry.default_mode_id); try std.testing.expectEqualStrings("skills", cfg.skill_root_policy.workspace_roots[0].path); try std.testing.expect(cfg.load_mcp_runtime == noMcpRuntimeForTest);}test "runIfRequested invalid local flags write usage" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{ @constCast("status"), @constCast("--wat") }, testConfig(), capture.deps()); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings("", capture.stdout.written()); try std.testing.expectEqualStrings("usage: fx status [--json]\n", capture.stderr.written());}
test "runIfRequested invalid json local flags write json error" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{ @constCast("status"), @constCast("--json"), @constCast("--wat") }, testConfig(), capture.deps()); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings("", capture.stderr.written()); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), "\"kind\":\"status\"") != null); try std.testing.expect(std.mem.find(u8, capture.stdout.written(), "\"code\":\"InvalidLocalSurfaceArgs\"") != null);}
test "runIfRequested resume no args returns last target" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{@constCast("resume")}, testConfig(), capture.deps()); switch (result) { .interactive => |launch| try std.testing.expectEqual(ResumeTarget.last, launch.requested_resume.?), else => return error.TestExpectedEqual, } try std.testing.expectEqualStrings("", capture.stderr.written());}
test "runIfRequested -r asks which session to resume" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps( std.testing.allocator, &.{@constCast("-r")}, testConfig(), capture.deps(), ); switch (result) { .interactive => |launch| try std.testing.expectEqual(ResumeTarget.pick, launch.requested_resume.?), else => return error.TestExpectedEqual, } try std.testing.expectEqualStrings("", capture.stdout.written()); try std.testing.expectEqualStrings("", capture.stderr.written());
var extra_capture = CaptureOutput.init(std.testing.allocator); defer extra_capture.deinit(); const extra = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("-r"), @constCast("session.123") }, testConfig(), extra_capture.deps(), ); try std.testing.expectEqual(RunResult.handled_failure, extra);}
test "runIfRequested top-level resume aliases return the existing target" { const aliases = [_][]const [:0]const u8{ &.{@constCast("--resume")}, &.{@constCast("--resume-last")}, &.{@constCast("--continue")}, &.{@constCast("-c")}, }; for (aliases) |args| { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps( std.testing.allocator, args, testConfig(), capture.deps(), ); switch (result) { .interactive => |launch| try std.testing.expectEqual(ResumeTarget.last, launch.requested_resume.?), else => return error.TestExpectedEqual, } try std.testing.expectEqualStrings("", capture.stdout.written()); try std.testing.expectEqualStrings("", capture.stderr.written()); }
var operand_capture = CaptureOutput.init(std.testing.allocator); defer operand_capture.deinit();
const operand = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("--resume"), @constCast("session.123") }, testConfig(), operand_capture.deps(), ); switch (operand) { .interactive => |launch_value| { var launch = launch_value; defer launch.deinit(std.testing.allocator); switch (launch.requested_resume.?) { .id => |id| try std.testing.expectEqualStrings("session.123", id), .pick, .last => return error.TestExpectedExactResumeId, } }, else => return error.TestExpectedEqual, }
var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const exact = try runIfRequestedWithDeps( std.testing.allocator, &.{@constCast("--resume-session.123")}, testConfig(), capture.deps(), ); switch (exact) { .interactive => |launch_value| { var launch = launch_value; defer launch.deinit(std.testing.allocator); switch (launch.requested_resume.?) { .id => |id| try std.testing.expectEqualStrings("session.123", id), .pick, .last => return error.TestExpectedExactResumeId, } }, else => return error.TestExpectedEqual, }
const nested = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("resume"), @constCast("--resume"), @constCast("--last") }, testConfig(), capture.deps(), ); switch (nested) { .interactive => |launch| try std.testing.expectEqual(ResumeTarget.last, launch.requested_resume.?), else => return error.TestExpectedEqual, }}
test "runIfRequested rejects malformed resume aliases with canonical usage" { const cases = [_][]const [:0]const u8{ &.{@constCast("--resume-")}, &.{ @constCast("--resume-last"), @constCast("unexpected") }, &.{ @constCast("--continue"), @constCast("unexpected") }, &.{ @constCast("--resume"), @constCast(" ") }, &.{ @constCast("resume"), @constCast("--resume") }, }; for (cases) |args| { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps( std.testing.allocator, args, testConfig(), capture.deps(), ); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings( "usage: fx session resume [last|<id>] [--record] | session resume --id <id> [--record] | --resume [last|<id>] [--record] | resume [last|<id>] [--record] | resume --id <id> [--record] | --resume-last | --continue | -c | -r | --resume-<id>\n", capture.stderr.written(), ); }}
test "runIfRequested resume id returns owned id" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{ @constCast("resume"), @constCast("abc123") }, testConfig(), capture.deps()); switch (result) { .interactive => |launch_value| { var launch = launch_value; defer launch.deinit(std.testing.allocator); switch (launch.requested_resume.?) { .id => |value| try std.testing.expectEqualStrings("abc123", value), else => return error.TestExpectedEqual, } }, else => return error.TestExpectedEqual, }}
test "runIfRequested invalid resume writes usage" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{ @constCast("resume"), @constCast("a"), @constCast("b") }, testConfig(), capture.deps()); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings( "usage: fx session resume [last|<id>] [--record] | session resume --id <id> [--record] | --resume [last|<id>] [--record] | resume [last|<id>] [--record] | resume --id <id> [--record] | --resume-last | --continue | -c | -r | --resume-<id>\n", capture.stderr.written(), );}
test "runIfRequested unknown command writes header and help" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
try std.testing.expectError( error.UnknownCliCommand, runIfRequestedWithDeps(std.testing.allocator, &.{@constCast("wat")}, testConfig(), capture.deps()), ); try std.testing.expect(std.mem.startsWith(u8, capture.stderr.written(), "fx: unknown subcommand: wat\n\n𝒇x v0.0.0\nFast, native coding agent for the terminal.\n"));}
test "runIfRequested bare version subcommand remains unknown" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
try std.testing.expectError( error.UnknownCliCommand, runIfRequestedWithDeps(std.testing.allocator, &.{@constCast("version")}, testConfig(), capture.deps()), ); try std.testing.expect(std.mem.startsWith(u8, capture.stderr.written(), "fx: unknown subcommand: version\n\n𝒇x v0.0.0\nFast, native coding agent for the terminal.\n"));}
test "runIfRequested model fetch failure is handled" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); var probe = ModelFetchProbe{ .outcome = .failure }; var cfg = testConfig(); cfg.gateway_provider.cli_model_catalog = probe.provider();
var deps = capture.deps(); deps.load_startup_state = failingStartupState; deps.load_catalog_startup_state = stubLoadCatalogStartupState;
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{@constCast("models")}, cfg, deps); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings( "fx models: could not list models: Unavailable\n", capture.stderr.written(), );}
test "runIfRequested model fetch failure preserves json output" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); var probe = ModelFetchProbe{ .outcome = .failure }; var cfg = testConfig(); cfg.gateway_provider.cli_model_catalog = probe.provider();
var deps = capture.deps(); deps.load_catalog_startup_state = stubLoadCatalogStartupState;
const result = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("models"), @constCast("--json") }, cfg, deps, ); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings( "{\"kind\":\"models\",\"error\":\"could not list models: Unavailable\",\"code\":\"Unavailable\"}\n", capture.stdout.written(), ); try std.testing.expectEqualStrings("", capture.stderr.written());}
test "runIfRequested model provider cancellation is handled" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); var probe = ModelFetchProbe{ .outcome = .cancelled }; var cfg = testConfig(); cfg.gateway_provider.cli_model_catalog = probe.provider();
var deps = capture.deps(); deps.load_catalog_startup_state = stubLoadCatalogStartupState;
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{@constCast("models")}, cfg, deps); try std.testing.expectEqual(RunResult.handled_failure, result); try std.testing.expectEqualStrings( "fx models: could not list models: the request was cancelled\n", capture.stderr.written(), );}
test "runIfRequested models passes startup team to fetch seam" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); var probe = ModelFetchProbe{}; var cfg = testConfig(); cfg.gateway_provider.cli_model_catalog = probe.provider();
var deps = capture.deps(); deps.load_catalog_startup_state = stubLoadCatalogStartupState;
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{ @constCast("models"), @constCast("--json") }, cfg, deps); try std.testing.expectEqual(RunResult.handled_success, result); try std.testing.expect(probe.called); try std.testing.expectEqualStrings( "{\"kind\":\"models\",\"count\":1,\"shown_count\":1,\"more_count\":0,\"private_models_hidden\":false,\"ids\":[\"private/blue-hornbill\"]}\n", capture.stdout.written(), );}
test "runIfRequested credits renders through the configured provider" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit(); var probe = CreditsProviderProbe{ .outcome = .success }; var cfg = testConfig(); cfg.gateway_provider.credits = probe.provider();
var deps = capture.deps(); deps.load_startup_state = stubLoadStartupState;
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{ @constCast("credits"), @constCast("--json") }, cfg, deps); try std.testing.expectEqual(RunResult.handled_success, result); try std.testing.expectEqual(@as(usize, 1), probe.calls); try std.testing.expect(probe.saw_expected_input); try std.testing.expectEqualStrings( "{\"kind\":\"credits\",\"balance\":\"10\",\"used\":\"2\",\"plan\":\"pro\"}\n", capture.stdout.written(), );}
test "runIfRequested credits failures use nonzero text and json contracts" { var text_capture = CaptureOutput.init(std.testing.allocator); defer text_capture.deinit(); var text_probe = CreditsProviderProbe{ .outcome = .failure }; var text_cfg = testConfig(); text_cfg.gateway_provider.credits = text_probe.provider(); var text_deps = text_capture.deps(); text_deps.load_startup_state = stubLoadStartupState;
const text_result = try runIfRequestedWithDeps( std.testing.allocator, &.{@constCast("credits")}, text_cfg, text_deps, ); try std.testing.expectEqual(RunResult.handled_failure, text_result); try std.testing.expectEqualStrings("", text_capture.stdout.written()); try std.testing.expectEqualStrings( "[credits] error: gateway unavailable\n", text_capture.stderr.written(), );
var json_capture = CaptureOutput.init(std.testing.allocator); defer json_capture.deinit(); var json_probe = CreditsProviderProbe{ .outcome = .failure }; var json_cfg = testConfig(); json_cfg.gateway_provider.credits = json_probe.provider(); var json_deps = json_capture.deps(); json_deps.load_startup_state = stubLoadStartupState;
const json_result = try runIfRequestedWithDeps( std.testing.allocator, &.{ @constCast("credits"), @constCast("--json") }, json_cfg, json_deps, ); try std.testing.expectEqual(RunResult.handled_failure, json_result); try std.testing.expectEqualStrings( "{\"kind\":\"credits\",\"error\":\"gateway unavailable\"}\n", json_capture.stdout.written(), ); try std.testing.expectEqualStrings("", json_capture.stderr.written());}
test "runIfRequested local json success appends exactly one newline" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
var deps = capture.deps(); deps.load_startup_status = stubLoadStartupStatus;
const result = try runIfRequestedWithDeps(std.testing.allocator, &.{ @constCast("status"), @constCast("--json") }, testConfig(), deps); try std.testing.expectEqual(RunResult.handled_success, result); try std.testing.expectEqualStrings( "{\"kind\":\"status\",\"model\":\"test-model\",\"update_channel\":\"stable\",\"build_channel\":\"stable\",\"build_revision\":\"\",\"auth\":\"missing\",\"auth_refreshable\":false,\"auth_help\":\"Fx needs access to Vercel AI Gateway. Run fx login to sign in, fx setup to use an API key, or set AI_GATEWAY_API_KEY.\",\"permission_mode\":\"auto\",\"workspace\":\"/tmp/fx\",\"history_turns\":0,\"session_permission_grants\":0,\"agent_step_limit\":42}\n", capture.stdout.written(), ); try std.testing.expect(!std.mem.endsWith(u8, capture.stdout.written(), "\n\n"));}
test "status and doctor inspect the supplied MCP profile diagnostic once" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); const home = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(home);
var environ = std.process.Environ.Map.init(alloc); defer environ.deinit(); try environ.put("HOME", home); try environ.put("PATH", ""); const stable_environ = try stableCliTestEnviron(); io_mod.setEnvironMap(&environ); defer io_mod.setEnvironMap(stable_environ);
mcp_config_inspection_calls_for_test = 0; mcp_runtime_load_calls_for_test = 0; var cfg = testConfig(); cfg.inspect_mcp_profile_config = failingMcpConfigInspectionForTest; cfg.load_mcp_runtime = countingMcpRuntimeForTest;
var status_capture = CaptureOutput.init(alloc); defer status_capture.deinit(); var status_deps = status_capture.deps(); status_deps.load_startup_status = stubLoadStartupStatus; const status_result = try runIfRequestedWithDeps( alloc, &.{ @constCast("status"), @constCast("--json") }, cfg, status_deps, ); try std.testing.expectEqual(RunResult.handled_success, status_result); try std.testing.expectEqual(@as(usize, 1), mcp_config_inspection_calls_for_test); try std.testing.expectEqual(@as(usize, 0), mcp_runtime_load_calls_for_test); try std.testing.expect(std.mem.find( u8, status_capture.stdout.written(), "\"mcp_config_error\":\"McpConfigInvalidJson\"", ) != null);
mcp_config_inspection_calls_for_test = 0; var doctor_capture = CaptureOutput.init(alloc); defer doctor_capture.deinit(); const doctor_result = try runIfRequestedWithDeps( alloc, &.{ @constCast("doctor"), @constCast("--json") }, cfg, doctor_capture.deps(), ); try std.testing.expectEqual(RunResult.handled_success, doctor_result); try std.testing.expectEqual(@as(usize, 1), mcp_config_inspection_calls_for_test); try std.testing.expectEqual(@as(usize, 0), mcp_runtime_load_calls_for_test); try std.testing.expectEqual( @as(usize, 1), std.mem.count( u8, doctor_capture.stdout.written(), "\"name\":\"mcp_config\"", ), ); try std.testing.expect(std.mem.find( u8, doctor_capture.stdout.written(), "\"detail\":\"failed to load ~/.fx/mcp.json: McpConfigInvalidJson\"", ) != null);}
test "writeRenderedJsonLine falls back to heap and appends exactly one newline" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
var tiny_buf: [8]u8 = undefined; const startup = app_lifecycle.StartupStatus{ .workspace_root = @constCast("/tmp/fx"), .selected_model = "test-model", .permission_mode = .ask, .agent_step_limit = 42, };
try writeRenderedJsonLine( std.testing.allocator, capture.deps(), tiny_buf[0..], .{ .status = statusSnapshotFromStartup(startup) }, );
try std.testing.expectEqualStrings( "{\"kind\":\"status\",\"model\":\"test-model\",\"update_channel\":\"stable\",\"build_channel\":\"stable\",\"build_revision\":\"\",\"auth\":\"missing\",\"auth_refreshable\":false,\"auth_help\":\"Fx needs access to Vercel AI Gateway. Run fx login to sign in, fx setup to use an API key, or set AI_GATEWAY_API_KEY.\",\"permission_mode\":\"ask\",\"workspace\":\"/tmp/fx\",\"history_turns\":0,\"session_permission_grants\":0,\"agent_step_limit\":42}\n", capture.stdout.written(), );}
test "writeRenderedJsonLine renders doctor json through output contract" { var capture = CaptureOutput.init(std.testing.allocator); defer capture.deinit();
var checks = [_]doctor_runtime.Check{ .{ .name = "auth", .status = .ok, .detail = "AI_GATEWAY_API_KEY is configured" }, .{ .name = "gh", .status = .warn, .detail = "GitHub CLI not found in PATH" }, }; const snapshot = doctor_runtime.Snapshot{ .workspace_root = @constCast("/tmp/fx"), .model = "test-model", .auth = .{ .active_source = .ai_gateway_api_key }, .permission_mode = .auto, .agent_step_limit = 42, .checks = checks[0..], };
var tiny_buf: [8]u8 = undefined; try writeRenderedJsonLine( std.testing.allocator, capture.deps(), tiny_buf[0..], .{ .doctor = doctorSnapshotFromRuntime(snapshot) }, );
try std.testing.expectEqualStrings( "{\"kind\":\"doctor\",\"ok_count\":1,\"warn_count\":1,\"fail_count\":0,\"workspace\":\"/tmp/fx\",\"model\":\"test-model\",\"auth\":\"AI_GATEWAY_API_KEY\",\"auth_refreshable\":false,\"permission_mode\":\"auto\",\"agent_step_limit\":42,\"checks\":[{\"name\":\"auth\",\"status\":\"ok\",\"detail\":\"AI_GATEWAY_API_KEY is configured\"},{\"name\":\"gh\",\"status\":\"warn\",\"detail\":\"GitHub CLI not found in PATH\"}]}\n", capture.stdout.written(), );}
const CaptureOutput = struct { stdout: std.Io.Writer.Allocating, stderr: std.Io.Writer.Allocating, setup_store_calls: usize = 0, setup_read_calls: usize = 0, setup_value_matched: bool = false, setup_interactive_store: bool = false, setup_store_disabled: bool = false,
fn init(alloc: Allocator) CaptureOutput { return .{ .stdout = .init(alloc), .stderr = .init(alloc), }; }
fn deinit(self: *@This()) void { self.stdout.deinit(); self.stderr.deinit(); }
fn deps(self: *@This()) RunDeps { return .{ .stdout_ctx = self, .stderr_ctx = self, .setup_ctx = self, .write_stdout = captureStdout, .write_stderr = captureStderr, .read_masked_key = captureReadMaskedKey, .setup_terminal_available = captureSetupTerminalAvailable, }; }
fn secretStore(self: *@This()) host.SecretStore { return .{ .context = self, .backend_label = "test credential store", .is_disabled_fn = captureSecretStoreIsDisabled, .load_fn = captureSecretStoreLoad, .store_fn = captureSecretStoreWrite, .store_interactive_fn = captureSecretStoreInteractiveWrite, }; }};
fn captureStdout(ctx: ?*anyopaque, text: []const u8) !void { const capture: *CaptureOutput = @ptrCast(@alignCast(ctx.?)); try capture.stdout.writer.writeAll(text);}
fn captureStderr(ctx: ?*anyopaque, text: []const u8) !void { const capture: *CaptureOutput = @ptrCast(@alignCast(ctx.?)); try capture.stderr.writer.writeAll(text);}
fn captureSetupTerminalAvailable(_: ?*anyopaque) bool { return true;}
fn captureReadMaskedKey( ctx: ?*anyopaque, alloc: Allocator, _: WriteFn, _: ?*anyopaque,) ![]u8 { const capture: *CaptureOutput = @ptrCast(@alignCast(ctx.?)); capture.setup_read_calls += 1; return alloc.dupe(u8, "paste-only-test-key");}
fn captureSecretStoreIsDisabled(ctx: ?*anyopaque) bool { const capture: *CaptureOutput = @ptrCast(@alignCast(ctx.?)); return capture.setup_store_disabled;}
fn captureSecretStoreLoad( _: ?*anyopaque, _: Allocator,) host.SecretStoreLoadError!?[]u8 { return null;}
fn captureSecretStoreWrite( ctx: ?*anyopaque, _: Allocator, value: []const u8,) host.SecretStoreWriteError!void { const capture: *CaptureOutput = @ptrCast(@alignCast(ctx.?)); capture.setup_store_calls += 1; capture.setup_value_matched = std.mem.eql(u8, value, "paste-only-test-key");}
fn captureSecretStoreInteractiveWrite( ctx: ?*anyopaque,) host.SecretStoreWriteError!bool { const capture: *CaptureOutput = @ptrCast(@alignCast(ctx.?)); if (!capture.setup_interactive_store) return false; capture.setup_store_calls += 1; return true;}
fn gatherNoopContextForTest(_: Allocator, _: context_contract.InitialContextInput) context_contract.ProviderError!context_contract.ProviderContext { return .{};}
fn appendNoopStaticContextForTest(_: context_contract.StaticContextInput, _: Allocator, _: *std.ArrayList(types.ChatMessage)) context_contract.ProviderError!void {}
fn appendNoopTransientContextForTest(_: context_contract.TransientContextInput, _: Allocator, _: *std.ArrayList(types.ChatMessage)) context_contract.ProviderError!void {}
const test_surface_context_registry = context_contract.Registry{ .default_provider = .{ .id = "test.surface_context", .gather_project_context_fn = gatherNoopContextForTest, .select_applicable_project_context_fn = context_contract.selectNoApplicableProjectContext, .append_static_fn = appendNoopStaticContextForTest, .append_transient_fn = appendNoopTransientContextForTest,} };
fn noMcpRuntimeForTest(_: Allocator, _: @import("../mcp/elicitation.zig").Capabilities) !?*mcp_runtime.McpRuntime { return null;}
fn clearMcpConfigInspectionForTest( _: Allocator,) error{OutOfMemory}!mcp_contract.ProfileConfigDiagnostic { return .clear;}
var mcp_config_inspection_calls_for_test: usize = 0;var mcp_runtime_load_calls_for_test: usize = 0;
fn failingMcpConfigInspectionForTest( _: Allocator,) error{OutOfMemory}!mcp_contract.ProfileConfigDiagnostic { mcp_config_inspection_calls_for_test += 1; return .{ .failed = error.McpConfigInvalidJson };}
fn countingMcpRuntimeForTest( _: Allocator, _: @import("../mcp/elicitation.zig").Capabilities,) !?*mcp_runtime.McpRuntime { mcp_runtime_load_calls_for_test += 1; return null;}
var stable_cli_test_environ: ?*std.process.Environ.Map = null;
fn stableCliTestEnviron() !*const std.process.Environ.Map { if (stable_cli_test_environ) |map| return map;
const alloc = std.heap.page_allocator; const map = try alloc.create(std.process.Environ.Map); map.* = std.process.Environ.Map.init(alloc); stable_cli_test_environ = map; return map;}
fn unexpectedAcpRunForTest(_: ?*anyopaque, _: Allocator, _: acp_runner.Config) anyerror!void { return error.TestUnexpectedAcpRun;}
fn testConfig() Config { return .{ .version = "0.0.0", .command_catalog = testCommandCatalog(), .default_model = "test-model", .default_agent_step_limit = 42, .models_path = "/v1/models", .gateway_retry_count = 1, .gateway_chat_url = "https://example.test/chat", .gateway_provider = test_builtin_gateway.provider, .url_opener = host.unavailable_url_opener, .secret_store = host.unavailable_secret_store, .prompt_policy = .{ .system_prompt = "system" }, .skill_root_policy = .{ .managed_root_source = .global_fx }, .ignored_list_entries = &.{}, .max_list_entries = 10, .max_read_file_bytes = 1024, .max_read_file_lines = 100, .max_read_file_line_len = 200, .max_command_output_bytes = 4096, .max_tool_result_bytes = 4096, .max_history_turns = 8, .context_registry = test_surface_context_registry, .mode_registry = .{ .default_mode_id = "surface" }, .inspect_mcp_profile_config = clearMcpConfigInspectionForTest, .load_mcp_runtime = noMcpRuntimeForTest, .acp_runner = .{ .run_fn = unexpectedAcpRunForTest }, .tool_set = .{ .registry = .{ .tools = &.{} }, .order = &.{}, .read_only_tool_names = &.{}, }, };}
fn stubLoadStartupState( alloc: Allocator, _: oauth_transport.Provider, _: host.SecretStore, default_model: []const u8, default_agent_step_limit: usize,) !app_lifecycle.StartupState { var state = app_lifecycle.StartupState{ .agent_step_limit = default_agent_step_limit }; errdefer state.deinit(alloc); state.workspace_root = try alloc.dupe(u8, "/tmp/fx"); state.selected_model = try alloc.dupe(u8, default_model); state.credential = .{ .token = try alloc.dupe(u8, "test-key"), .source = .ai_gateway_api_key, }; state.credential.?.team_id = try alloc.dupe(u8, "team_123"); return state;}
fn stubLoadCatalogStartupState( alloc: Allocator, secret_store: host.SecretStore, default_model: []const u8, default_agent_step_limit: usize,) !app_lifecycle.StartupState { return stubLoadStartupState(alloc, oauth_transport.unavailable_provider, secret_store, default_model, default_agent_step_limit);}
fn stubLoadStartupStatus( alloc: Allocator, _: host.SecretStore, default_model: []const u8, default_agent_step_limit: usize,) !app_lifecycle.StartupStatus { const workspace_root = try alloc.dupe(u8, "/tmp/fx"); errdefer alloc.free(workspace_root); const selected_model = try alloc.dupe(u8, default_model); errdefer alloc.free(selected_model); return .{ .workspace_root = workspace_root, .selected_model = selected_model, .owned_selected_model = selected_model, .permission_mode = config_runtime.default_permission_mode, .agent_step_limit = default_agent_step_limit, };}
fn failingStartupState( _: Allocator, _: oauth_transport.Provider, _: host.SecretStore, _: []const u8, _: usize,) !app_lifecycle.StartupState { return error.StartupShouldNotRun;}
const ModelFetchProbe = struct { const Outcome = enum { success, failure, cancelled, };
called: bool = false, outcome: Outcome = .success,
fn provider(self: *ModelFetchProbe) gateway_provider.CliModelCatalogProvider { return .{ .context = self, .fetch_fn = fetch, }; }
fn failure( input: gateway_provider.CliModelCatalogInput, category: model_catalog.FailureCategory, ) gateway_provider.CliModelCatalogResult { return .{ .failure = .{ .access = .init(input.access), .anonymous_fallback_used = false, .failure = .{ .category = category }, } }; }
fn fetch( raw: ?*anyopaque, alloc: Allocator, input: gateway_provider.CliModelCatalogInput, ) gateway_provider.CliModelCatalogResult { const self: *ModelFetchProbe = @ptrCast(@alignCast(raw.?)); self.called = true; if (!std.mem.eql(u8, input.access.authorizationCredential() orelse "", "test-key") or !std.mem.eql(u8, input.access.teamContext() orelse "", "team_123") or input.access.credentialSource() != .ai_gateway_api_key or !std.mem.eql(u8, input.endpoint, "/v1/models") or input.cancel_flag != null) { return failure(input, .runtime); }
switch (self.outcome) { .failure => return failure(input, .runtime), .cancelled => return failure(input, .cancellation), .success => {}, }
var ids: std.ArrayList([]u8) = .empty; const id = alloc.dupe(u8, "private/blue-hornbill") catch { return failure(input, .resource_exhausted); }; ids.append(alloc, id) catch { alloc.free(id); return failure(input, .resource_exhausted); }; return .{ .loaded = .{ .ids = ids, .provenance = .{ .access = .init(input.access) }, } }; }};
const ChatUrlProbe = struct { called: bool = false,
fn provider(self: *ChatUrlProbe) gateway_provider.ChatUrlProvider { return .{ .context = self, .resolve_fn = resolve, }; }
fn resolve(raw: ?*anyopaque, fallback: []const u8) []const u8 { const self: *ChatUrlProbe = @ptrCast(@alignCast(raw.?)); self.called = std.mem.eql(u8, fallback, "https://example.test/chat"); return "http://127.0.0.1:43123/chat"; }};
const CreditsProviderProbe = struct { outcome: enum { success, failure }, calls: usize = 0, saw_expected_input: bool = false,
fn provider(self: *CreditsProviderProbe) gateway_provider.CreditsProvider { return .{ .context = self, .fetch_fn = fetch, }; }
fn fetch( raw: ?*anyopaque, alloc: Allocator, input: gateway_provider.CreditsLookupInput, ) output_contracts.CreditsSnapshot { const self: *CreditsProviderProbe = @ptrCast(@alignCast(raw.?)); self.calls += 1; self.saw_expected_input = std.mem.eql(u8, input.credential orelse "", "test-key") and std.mem.eql(u8, input.tenant orelse "", "team_123"); if (self.outcome == .failure) { return ownedCreditsErrorSnapshot(alloc, "gateway unavailable"); }
var snapshot = output_contracts.CreditsSnapshot{}; snapshot.balance = alloc.dupe(u8, "10") catch return ownedCreditsErrorSnapshot(alloc, "invalid JSON response from gateway"); snapshot.used = alloc.dupe(u8, "2") catch { snapshot.deinit(alloc); return ownedCreditsErrorSnapshot(alloc, "invalid JSON response from gateway"); }; snapshot.plan = alloc.dupe(u8, "pro") catch { snapshot.deinit(alloc); return ownedCreditsErrorSnapshot(alloc, "invalid JSON response from gateway"); }; return snapshot; }};
fn ownedCreditsErrorSnapshot(alloc: Allocator, message: []const u8) output_contracts.CreditsSnapshot { return .{ .err_message = alloc.dupe(u8, message) catch null };}