[{"data":1,"prerenderedAt":7429},["ShallowReactive",2],{"footer-articles":3,"topic-vue-sri-lanka":7394,"talk-for-topic-vue-sri-lanka":7410,"topic-articles-vue-sri-lanka":7428},[4,57,7326,7360],{"id":5,"title":6,"body":7,"category":45,"date":46,"description":47,"draft":48,"extension":49,"image":50,"link":37,"meta":51,"navigation":52,"path":53,"seo":54,"stem":55,"tighten":52,"__hash__":56},"blog/blog/build-an-agent-friendly-cli-tool-in-php-and-run-it-anywhere-with-cpx.md","Build an Agent-Friendly CLI Tool in PHP and Run It Anywhere With CPX",{"type":8,"value":9,"toc":41},"minimark",[10,19,30,33],[11,12,13,14,18],"p",{},"CPX is to Composer what ",[15,16,17],"code",{},"npx"," is to npm: it runs any Composer package's commands without adding it to your project or your global dependencies.",[11,20,21,22,29],{},"In this article for ",[23,24,28],"a",{"href":25,"rel":26},"https://tighten.com",[27],"nofollow","Tighten",", I show how CPX works, then build a small PHP CLI tool that returns JSON, publish it to Packagist, and hand it to an AI agent.",[11,31,32],{},"Read the full article here:",[34,35,38],"article-preview",{"image":36,"link":37,"title":6},"https://tighten.com/assets/images/insights/build-an-agent-friendly-cli-tool-in-php-and-run-it-anywhere-with-cpx-preview.jpg","https://tighten.com/insights/build-an-agent-friendly-cli-tool-in-php-and-run-it-anywhere-with-cpx/",[11,39,40],{},"Command-line tools are more popular than ever. They're useful not only for humans, but also for agents: having a command-line tool that returns JSON data is chef's kiss for any clanker.",{"title":42,"searchDepth":43,"depth":43,"links":44},"",2,[],"PHP","2026-11-04","CPX is to Composer what npx is to npm: it runs any Composer package's commands without adding it to your project or your global dependencies.",false,"md","/img/tighten/build-an-agent-friendly-cli-tool-in-php-and-run-it-anywhere-with-cpx.jpg",{},true,"/blog/build-an-agent-friendly-cli-tool-in-php-and-run-it-anywhere-with-cpx",{"title":6,"description":47},"blog/build-an-agent-friendly-cli-tool-in-php-and-run-it-anywhere-with-cpx","ce04Akg7b0Aln-huPKsEQl0Vi13IUcpjrxQnP0NrAGM",{"id":58,"title":59,"body":60,"category":94,"date":7317,"description":7318,"draft":48,"extension":49,"image":7319,"link":7320,"meta":7321,"navigation":52,"path":7322,"seo":7323,"stem":7324,"tighten":48,"__hash__":7325},"blog/blog/build-command-line-tools-in-typescript-a-step-by-step-guide.md","Build Command Line Tools in TypeScript: A Step-by-Step Guide",{"type":8,"value":61,"toc":7302},[62,65,88,104,112,115,118,123,126,153,162,197,204,210,230,247,251,254,268,271,279,282,288,291,303,428,431,445,649,659,667,671,680,736,751,760,764,770,793,816,896,908,918,925,1277,1287,1290,1330,1333,1506,1525,1540,1543,1748,1768,1774,1780,1789,1793,1796,1802,1820,1824,1834,2299,2302,2366,2373,2379,2391,2407,2411,2424,2621,2668,2673,3633,3655,3660,3663,3670,3673,3703,3793,3796,3810,3816,3822,3826,3839,3842,4307,4310,4365,4370,4376,4379,4817,4830,4842,5044,5056,5062,5130,5135,5164,5254,5260,5264,5271,5461,5478,5483,5490,5502,5669,5672,5675,5928,5931,5944,6118,6121,6125,6131,6134,6362,6368,6647,6654,7071,7074,7086,7092,7098,7102,7105,7120,7128,7153,7172,7194,7200,7203,7225,7229,7248,7251,7274,7278,7284,7287,7295,7298],[11,63,64],{},"How far is Tokyo from Buenos Aires? You could open a map, drop two pins, and squint at the scale bar. Or you could type a single command:",[66,67,71],"pre",{"className":68,"code":69,"language":70,"meta":42,"style":42},"language-bash shiki shiki-themes monokai","npx @nicodevs/howfar Tokyo\n","bash",[15,72,73],{"__ignoreMap":42},[74,75,78,81,85],"span",{"class":76,"line":77},"line",1,[74,79,17],{"class":80},"sHkqI",[74,82,84],{"class":83},"s_Ekj"," @nicodevs/howfar",[74,86,87],{"class":83}," Tokyo\n",[11,89,90,91,95,96,99,100,103],{},"That's what we're building today: a command-line tool in ",[92,93,94],"strong",{},"TypeScript"," that tells you how far any city is from yours. We'll publish it to ",[92,97,98],{},"npm",", so anyone with ",[92,101,102],{},"Node.js"," can run it from any folder without installing anything.",[11,105,106,107,111],{},"It follows the same structure I use in ",[23,108,110],{"href":109},"/blog/climail-the-email-client-for-your-agent","climail",", my command-line email client, and it prints JSON, so both people and AI agents can use its output.",[11,113,114],{},"I know, I know, measuring distances is a simple example. But you can swap the distance math for your own code. What carries over is how to structure the project, how to publish it, and which libraries make a CLI beautiful and useful without reinventing the wheel.",[11,116,117],{},"Let's dive in!",[119,120,122],"h2",{"id":121},"how-npx-works","How npx Works",[11,124,125],{},"If you've never published a package before, that command has a few moving parts, so let's go through them one by one.",[11,127,128,129,131,132,134,135,138,139,144,145,148,149,152],{},"When you install ",[92,130,102],{},", you also get ",[92,133,98],{},", its package manager. npm comes with a public ",[92,136,137],{},"registry",", ",[23,140,143],{"href":141,"rel":142},"https://www.npmjs.com/",[27],"npmjs.com",", which is where packages live. When you run ",[15,146,147],{},"npm install something",", npm downloads ",[15,150,151],{},"something"," from that registry and adds it to your project.",[11,154,155,157,158,161],{},[92,156,17],{}," comes with npm too, and it works a little differently. Instead of adding a package to your project, it downloads it to a cache, runs the command inside it, and leaves your project untouched. That's why ",[15,159,160],{},"npx @nicodevs/howfar Tokyo"," works from any folder, without installing anything first.",[11,163,164,165,168,169,172,173,176,177,180,181,184,185,188,189,192,193,196],{},"Now, what about the ",[15,166,167],{},"@nicodevs/"," part? That's called a ",[92,170,171],{},"scope",". Every package on the registry needs a unique name, and short names like ",[15,174,175],{},"howfar"," get claimed fast (this one already is). Scopes solve that problem. When you create an account on npmjs.com, you get a scope that matches your username, and only you can publish packages under it. My npm username is ",[15,178,179],{},"nicodevs",", so my package is called ",[15,182,183],{},"@nicodevs/howfar",". If your npm username is ",[15,186,187],{},"jane",", your package will be called ",[15,190,191],{},"@jane/howfar",", and people will run it with ",[15,194,195],{},"npx @jane/howfar",".",[11,198,199,200,203],{},"When we build our own version, I'll use ",[15,201,202],{},"@username"," as a placeholder. Whenever you see it, replace it with your own npm username.",[205,206,207],"blockquote",{},[11,208,209],{},"Can npx run a GitHub repository too?",[11,211,212,213,138,216,219,220,223,224,226,227,229],{},"It can, but with a different syntax. Without the ",[15,214,215],{},"@",[15,217,218],{},"npx jane/howfar"," (or the more explicit ",[15,221,222],{},"npx github:jane/howfar",") means \"the ",[15,225,175],{}," repository of the GitHub user ",[15,228,187],{},".\" In that case, npx clones the repository, installs it, and runs its command, with no npm account or registry involved.",[11,231,232,233,236,237,239,240,243,244,246],{},"There's one catch, though. A GitHub repository usually holds the source code, not the built file that npx needs to run. In our project, the built file will live in a ",[15,234,235],{},"dist"," folder that we keep out of Git. So if you want to go the GitHub route, you'll need to either commit ",[15,238,235],{}," or make the ",[15,241,242],{},"prepare"," script build the project, since npm runs ",[15,245,242],{}," when it installs a package from Git. Publishing to npm avoids all of that, so that's what we'll do in this article.",[119,248,250],{"id":249},"the-final-product","The Final Product",[11,252,253],{},"Okay, let's take a look at the final product. The first time, we run the setup wizard:",[66,255,257],{"className":68,"code":256,"language":70,"meta":42,"style":42},"npx @nicodevs/howfar init\n",[15,258,259],{"__ignoreMap":42},[74,260,261,263,265],{"class":76,"line":77},[74,262,17],{"class":80},[74,264,84],{"class":83},[74,266,267],{"class":83}," init\n",[11,269,270],{},"It asks for our city, and then for the unit we want our distances in:",[66,272,277],{"className":273,"code":275,"language":276,"meta":42},[274],"language-text","┌  howfar setup\n│\n◇  Your city\n│  Buenos Aires\n│\n◆  Distance unit\n│  ● Kilometers\n│  ○ Miles\n│  ↑/↓ to navigate • Enter: confirm\n└\n","text",[15,278,275],{"__ignoreMap":42},[11,280,281],{},"Once we pick a unit and press Enter, it saves our answers and tells us what to try next:",[66,283,286],{"className":284,"code":285,"language":276,"meta":42},[274],"◇  Distance unit\n│  Kilometers\n│\n◆  Saved config to ~/.config/howfar.conf\n│\n└  Done. Try: npx @nicodevs/howfar Tokyo\n",[15,287,285],{"__ignoreMap":42},[11,289,290],{},"That's it. From now on, we can ask how far any city is from home:",[66,292,293],{"className":68,"code":69,"language":70,"meta":42,"style":42},[15,294,295],{"__ignoreMap":42},[74,296,297,299,301],{"class":76,"line":77},[74,298,17],{"class":80},[74,300,84],{"class":83},[74,302,87],{"class":83},[66,304,308],{"className":305,"code":306,"language":307,"meta":42,"style":42},"language-json shiki shiki-themes monokai","{\n  \"ok\": true,\n  \"from\": \"Buenos Aires, Argentina\",\n  \"city\": \"Tokyo, Japan\",\n  \"latitude\": 35.6895,\n  \"longitude\": 139.69171,\n  \"distance\": 18370,\n  \"unit\": \"km\",\n  \"summary\": \"Tokyo is 18,370 km from Buenos Aires\"\n}\n","json",[15,309,310,316,332,346,359,372,385,398,411,422],{"__ignoreMap":42},[74,311,312],{"class":76,"line":77},[74,313,315],{"class":314},"sCdxs","{\n",[74,317,318,322,325,329],{"class":76,"line":43},[74,319,321],{"class":320},"sOx1s","  \"ok\"",[74,323,324],{"class":314},": ",[74,326,328],{"class":327},"s7s5_","true",[74,330,331],{"class":314},",\n",[74,333,335,338,340,344],{"class":76,"line":334},3,[74,336,337],{"class":320},"  \"from\"",[74,339,324],{"class":314},[74,341,343],{"class":342},"susgL","\"Buenos Aires, Argentina\"",[74,345,331],{"class":314},[74,347,349,352,354,357],{"class":76,"line":348},4,[74,350,351],{"class":320},"  \"city\"",[74,353,324],{"class":314},[74,355,356],{"class":342},"\"Tokyo, Japan\"",[74,358,331],{"class":314},[74,360,362,365,367,370],{"class":76,"line":361},5,[74,363,364],{"class":320},"  \"latitude\"",[74,366,324],{"class":314},[74,368,369],{"class":327},"35.6895",[74,371,331],{"class":314},[74,373,375,378,380,383],{"class":76,"line":374},6,[74,376,377],{"class":320},"  \"longitude\"",[74,379,324],{"class":314},[74,381,382],{"class":327},"139.69171",[74,384,331],{"class":314},[74,386,388,391,393,396],{"class":76,"line":387},7,[74,389,390],{"class":320},"  \"distance\"",[74,392,324],{"class":314},[74,394,395],{"class":327},"18370",[74,397,331],{"class":314},[74,399,401,404,406,409],{"class":76,"line":400},8,[74,402,403],{"class":320},"  \"unit\"",[74,405,324],{"class":314},[74,407,408],{"class":342},"\"km\"",[74,410,331],{"class":314},[74,412,414,417,419],{"class":76,"line":413},9,[74,415,416],{"class":320},"  \"summary\"",[74,418,324],{"class":314},[74,420,421],{"class":342},"\"Tokyo is 18,370 km from Buenos Aires\"\n",[74,423,425],{"class":76,"line":424},10,[74,426,427],{"class":314},"}\n",[11,429,430],{},"We can also measure several cities at once:",[66,432,434],{"className":68,"code":433,"language":70,"meta":42,"style":42},"npx @nicodevs/howfar \"Tokyo, San Francisco\"\n",[15,435,436],{"__ignoreMap":42},[74,437,438,440,442],{"class":76,"line":77},[74,439,17],{"class":80},[74,441,84],{"class":83},[74,443,444],{"class":83}," \"Tokyo, San Francisco\"\n",[66,446,448],{"className":305,"code":447,"language":307,"meta":42,"style":42},"{\n  \"ok\": true,\n  \"from\": \"Buenos Aires, Argentina\",\n  \"cities\": [\n    {\n      \"city\": \"Tokyo, Japan\",\n      \"latitude\": 35.6895,\n      \"longitude\": 139.69171,\n      \"distance\": 18370,\n      \"unit\": \"km\",\n      \"summary\": \"Tokyo is 18,370 km from Buenos Aires\"\n    },\n    {\n      \"city\": \"San Francisco, United States\",\n      \"latitude\": 37.77493,\n      \"longitude\": -122.41942,\n      \"distance\": 10411,\n      \"unit\": \"km\",\n      \"summary\": \"San Francisco is 10,411 km from Buenos Aires\"\n    }\n  ]\n}\n",[15,449,450,454,464,474,482,487,498,509,520,531,542,552,558,563,575,587,599,611,622,632,638,644],{"__ignoreMap":42},[74,451,452],{"class":76,"line":77},[74,453,315],{"class":314},[74,455,456,458,460,462],{"class":76,"line":43},[74,457,321],{"class":320},[74,459,324],{"class":314},[74,461,328],{"class":327},[74,463,331],{"class":314},[74,465,466,468,470,472],{"class":76,"line":334},[74,467,337],{"class":320},[74,469,324],{"class":314},[74,471,343],{"class":342},[74,473,331],{"class":314},[74,475,476,479],{"class":76,"line":348},[74,477,478],{"class":320},"  \"cities\"",[74,480,481],{"class":314},": [\n",[74,483,484],{"class":76,"line":361},[74,485,486],{"class":314},"    {\n",[74,488,489,492,494,496],{"class":76,"line":374},[74,490,491],{"class":320},"      \"city\"",[74,493,324],{"class":314},[74,495,356],{"class":342},[74,497,331],{"class":314},[74,499,500,503,505,507],{"class":76,"line":387},[74,501,502],{"class":320},"      \"latitude\"",[74,504,324],{"class":314},[74,506,369],{"class":327},[74,508,331],{"class":314},[74,510,511,514,516,518],{"class":76,"line":400},[74,512,513],{"class":320},"      \"longitude\"",[74,515,324],{"class":314},[74,517,382],{"class":327},[74,519,331],{"class":314},[74,521,522,525,527,529],{"class":76,"line":413},[74,523,524],{"class":320},"      \"distance\"",[74,526,324],{"class":314},[74,528,395],{"class":327},[74,530,331],{"class":314},[74,532,533,536,538,540],{"class":76,"line":424},[74,534,535],{"class":320},"      \"unit\"",[74,537,324],{"class":314},[74,539,408],{"class":342},[74,541,331],{"class":314},[74,543,545,548,550],{"class":76,"line":544},11,[74,546,547],{"class":320},"      \"summary\"",[74,549,324],{"class":314},[74,551,421],{"class":342},[74,553,555],{"class":76,"line":554},12,[74,556,557],{"class":314},"    },\n",[74,559,561],{"class":76,"line":560},13,[74,562,486],{"class":314},[74,564,566,568,570,573],{"class":76,"line":565},14,[74,567,491],{"class":320},[74,569,324],{"class":314},[74,571,572],{"class":342},"\"San Francisco, United States\"",[74,574,331],{"class":314},[74,576,578,580,582,585],{"class":76,"line":577},15,[74,579,502],{"class":320},[74,581,324],{"class":314},[74,583,584],{"class":327},"37.77493",[74,586,331],{"class":314},[74,588,590,592,594,597],{"class":76,"line":589},16,[74,591,513],{"class":320},[74,593,324],{"class":314},[74,595,596],{"class":327},"-122.41942",[74,598,331],{"class":314},[74,600,602,604,606,609],{"class":76,"line":601},17,[74,603,524],{"class":320},[74,605,324],{"class":314},[74,607,608],{"class":327},"10411",[74,610,331],{"class":314},[74,612,614,616,618,620],{"class":76,"line":613},18,[74,615,535],{"class":320},[74,617,324],{"class":314},[74,619,408],{"class":342},[74,621,331],{"class":314},[74,623,625,627,629],{"class":76,"line":624},19,[74,626,547],{"class":320},[74,628,324],{"class":314},[74,630,631],{"class":342},"\"San Francisco is 10,411 km from Buenos Aires\"\n",[74,633,635],{"class":76,"line":634},20,[74,636,637],{"class":314},"    }\n",[74,639,641],{"class":76,"line":640},21,[74,642,643],{"class":314},"  ]\n",[74,645,647],{"class":76,"line":646},22,[74,648,427],{"class":314},[11,650,651,652,655,656,196],{},"And when we want to measure from somewhere else, ",[15,653,654],{},"--from"," overrides our home city for a single run, as in ",[15,657,658],{},"npx @nicodevs/howfar Tokyo --from Lima",[11,660,661,662,196],{},"Pretty cool, right? It's already published, so you can try any of these commands right now. And the full source code is on ",[23,663,666],{"href":664,"rel":665},"https://github.com/nicodevs/howfar",[27],"GitHub",[119,668,670],{"id":669},"the-building-blocks","The Building Blocks",[11,672,673,674,679],{},"A CLI has to parse arguments, ask the user questions, remember settings between runs, and check those settings before trusting them. Rather than one big framework, we'll use a small package for each of those jobs. Three of them come from the ",[23,675,678],{"href":676,"rel":677},"https://unjs.io/",[27],"UnJS"," collection, which is a set of small, well-maintained packages that work anywhere JavaScript runs.",[681,682,683,702,716,726],"ul",{},[684,685,686,693,694,697,698,701],"li",{},[92,687,688],{},[23,689,692],{"href":690,"rel":691},"https://github.com/unjs/citty",[27],"citty"," defines commands and parses arguments. You describe a command as an object (name, description, args, run function) and citty handles ",[15,695,696],{},"--flags",", positional arguments, and the ",[15,699,700],{},"--help"," screen.",[684,703,704,711,712,715],{},[92,705,706],{},[23,707,710],{"href":708,"rel":709},"https://github.com/bombshell-dev/clack",[27],"@clack/prompts"," draws interactive prompts, such as text inputs, password fields, and select menus. It's what draws the ",[15,713,714],{},"init"," wizard you just saw.",[684,717,718,725],{},[92,719,720],{},[23,721,724],{"href":722,"rel":723},"https://github.com/unjs/rc9",[27],"rc9"," reads and writes config files in the user's home directory, so the tool remembers its settings between runs.",[684,727,728,735],{},[92,729,730],{},[23,731,734],{"href":732,"rel":733},"https://zod.dev/",[27],"zod"," validates that config before we trust it.",[11,737,738,739,746,747,750],{},"Then, for tooling, we'll use ",[92,740,741],{},[23,742,745],{"href":743,"rel":744},"https://viteplus.dev/",[27],"Vite+",", a single binary called ",[15,748,749],{},"vp"," that bundles a formatter, a linter, a test runner, a bundler, and Git hooks. Before Vite+, I had five dev dependencies and five config files for that. Now I have one of each.",[11,752,753,754,759],{},"And finally, to find each city, we'll call the ",[23,755,758],{"href":756,"rel":757},"https://open-meteo.com/",[27],"Open-Meteo"," geocoding API, which is free, needs no key, and returns the city's coordinates. The distance between two sets of coordinates is a formula we'll write ourselves, so that's the only API we need.",[119,761,763],{"id":762},"creating-the-project","Creating the Project",[11,765,766,767,769],{},"Alright, let's get started. First, let's install the global ",[15,768,749],{}," binary:",[66,771,773],{"className":68,"code":772,"language":70,"meta":42,"style":42},"curl -fsSL https://vite.plus | bash\n",[15,774,775],{"__ignoreMap":42},[74,776,777,780,783,786,790],{"class":76,"line":77},[74,778,779],{"class":80},"curl",[74,781,782],{"class":327}," -fsSL",[74,784,785],{"class":83}," https://vite.plus",[74,787,789],{"class":788},"s8I7P"," |",[74,791,792],{"class":80}," bash\n",[11,794,795,796,799,800,803,804,807,808,811,812,815],{},"Now, let's create the folder and initialize the package. We'll also add ",[15,797,798],{},"vite-plus"," as a dev dependency, so the project pins its own version and our config file can import from it. The odd-looking ",[15,801,802],{},"vite@npm:..."," entry installs the Vite+ core under the name ",[15,805,806],{},"vite",", which is the name the test runner looks for. Without it, ",[15,809,810],{},"npm test"," fails with ",[15,813,814],{},"Cannot find package 'vite'",":",[66,817,819],{"className":68,"code":818,"language":70,"meta":42,"style":42},"mkdir howfar && cd howfar\ngit init\nnpm init -y\nnpm install citty @clack/prompts rc9 zod\nnpm install -D typescript vite-plus @types/node vite@npm:@voidzero-dev/vite-plus-core\n",[15,820,821,839,846,856,875],{"__ignoreMap":42},[74,822,823,826,829,832,836],{"class":76,"line":77},[74,824,825],{"class":80},"mkdir",[74,827,828],{"class":83}," howfar",[74,830,831],{"class":314}," && ",[74,833,835],{"class":834},"sYf5A","cd",[74,837,838],{"class":83}," howfar\n",[74,840,841,844],{"class":76,"line":43},[74,842,843],{"class":80},"git",[74,845,267],{"class":83},[74,847,848,850,853],{"class":76,"line":334},[74,849,98],{"class":80},[74,851,852],{"class":83}," init",[74,854,855],{"class":327}," -y\n",[74,857,858,860,863,866,869,872],{"class":76,"line":348},[74,859,98],{"class":80},[74,861,862],{"class":83}," install",[74,864,865],{"class":83}," citty",[74,867,868],{"class":83}," @clack/prompts",[74,870,871],{"class":83}," rc9",[74,873,874],{"class":83}," zod\n",[74,876,877,879,881,884,887,890,893],{"class":76,"line":361},[74,878,98],{"class":80},[74,880,862],{"class":83},[74,882,883],{"class":327}," -D",[74,885,886],{"class":83}," typescript",[74,888,889],{"class":83}," vite-plus",[74,891,892],{"class":83}," @types/node",[74,894,895],{"class":83}," vite@npm:@voidzero-dev/vite-plus-core\n",[205,897,898],{},[11,899,900,903,904,907],{},[15,901,902],{},"npm install"," crashes with ",[15,905,906],{},"Cannot read properties of null (reading 'edgesOut')","?",[11,909,910,911,913,914,917],{},"That's a bug in npm 10, the version bundled with Node 22, when it resolves the peer dependencies of ",[15,912,798],{},". You can fix it by upgrading npm with ",[15,915,916],{},"npm install -g npm@11",", or by switching to Node 24, which ships with it. Then, run the install again.",[11,919,920,921,924],{},"Then, let's open ",[15,922,923],{},"package.json"," and make it look like this:",[66,926,928],{"className":305,"code":927,"filename":923,"language":307,"meta":42,"style":42},"{\n  \"name\": \"@username/howfar\",\n  \"version\": \"0.1.0\",\n  \"description\": \"How far any city is from yours, as JSON.\",\n  \"license\": \"MIT\",\n  \"type\": \"module\",\n  \"bin\": {\n    \"howfar\": \"dist/howfar.mjs\"\n  },\n  \"files\": [\"dist\"],\n  \"engines\": {\n    \"node\": \">=22\"\n  },\n  \"scripts\": {\n    \"lint\": \"vp lint\",\n    \"format\": \"vp fmt\",\n    \"test\": \"vp test run\",\n    \"type-check\": \"tsc --noEmit\",\n    \"build\": \"vp run type-check && vp pack\",\n    \"prepare\": \"vp config --no-agent\",\n    \"prepublishOnly\": \"npm run build\"\n  },\n  \"dependencies\": {\n    \"@clack/prompts\": \"^1.8.1\",\n    \"citty\": \"^0.2.2\",\n    \"rc9\": \"^3.1.0\",\n    \"zod\": \"^4.6.7\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^26.6.6\",\n    \"typescript\": \"^7.0.2\",\n    \"vite\": \"npm:@voidzero-dev/vite-plus-core@^1.4.0\",\n    \"vite-plus\": \"^1.4.0\"\n  }\n}\n",[15,929,930,934,946,958,970,982,994,1002,1012,1017,1031,1038,1048,1052,1059,1071,1083,1095,1107,1119,1131,1141,1145,1153,1166,1179,1192,1203,1208,1216,1229,1242,1255,1266,1272],{"__ignoreMap":42},[74,931,932],{"class":76,"line":77},[74,933,315],{"class":314},[74,935,936,939,941,944],{"class":76,"line":43},[74,937,938],{"class":320},"  \"name\"",[74,940,324],{"class":314},[74,942,943],{"class":342},"\"@username/howfar\"",[74,945,331],{"class":314},[74,947,948,951,953,956],{"class":76,"line":334},[74,949,950],{"class":320},"  \"version\"",[74,952,324],{"class":314},[74,954,955],{"class":342},"\"0.1.0\"",[74,957,331],{"class":314},[74,959,960,963,965,968],{"class":76,"line":348},[74,961,962],{"class":320},"  \"description\"",[74,964,324],{"class":314},[74,966,967],{"class":342},"\"How far any city is from yours, as JSON.\"",[74,969,331],{"class":314},[74,971,972,975,977,980],{"class":76,"line":361},[74,973,974],{"class":320},"  \"license\"",[74,976,324],{"class":314},[74,978,979],{"class":342},"\"MIT\"",[74,981,331],{"class":314},[74,983,984,987,989,992],{"class":76,"line":374},[74,985,986],{"class":320},"  \"type\"",[74,988,324],{"class":314},[74,990,991],{"class":342},"\"module\"",[74,993,331],{"class":314},[74,995,996,999],{"class":76,"line":387},[74,997,998],{"class":320},"  \"bin\"",[74,1000,1001],{"class":314},": {\n",[74,1003,1004,1007,1009],{"class":76,"line":400},[74,1005,1006],{"class":320},"    \"howfar\"",[74,1008,324],{"class":314},[74,1010,1011],{"class":342},"\"dist/howfar.mjs\"\n",[74,1013,1014],{"class":76,"line":413},[74,1015,1016],{"class":314},"  },\n",[74,1018,1019,1022,1025,1028],{"class":76,"line":424},[74,1020,1021],{"class":320},"  \"files\"",[74,1023,1024],{"class":314},": [",[74,1026,1027],{"class":342},"\"dist\"",[74,1029,1030],{"class":314},"],\n",[74,1032,1033,1036],{"class":76,"line":544},[74,1034,1035],{"class":320},"  \"engines\"",[74,1037,1001],{"class":314},[74,1039,1040,1043,1045],{"class":76,"line":554},[74,1041,1042],{"class":320},"    \"node\"",[74,1044,324],{"class":314},[74,1046,1047],{"class":342},"\">=22\"\n",[74,1049,1050],{"class":76,"line":560},[74,1051,1016],{"class":314},[74,1053,1054,1057],{"class":76,"line":565},[74,1055,1056],{"class":320},"  \"scripts\"",[74,1058,1001],{"class":314},[74,1060,1061,1064,1066,1069],{"class":76,"line":577},[74,1062,1063],{"class":320},"    \"lint\"",[74,1065,324],{"class":314},[74,1067,1068],{"class":342},"\"vp lint\"",[74,1070,331],{"class":314},[74,1072,1073,1076,1078,1081],{"class":76,"line":589},[74,1074,1075],{"class":320},"    \"format\"",[74,1077,324],{"class":314},[74,1079,1080],{"class":342},"\"vp fmt\"",[74,1082,331],{"class":314},[74,1084,1085,1088,1090,1093],{"class":76,"line":601},[74,1086,1087],{"class":320},"    \"test\"",[74,1089,324],{"class":314},[74,1091,1092],{"class":342},"\"vp test run\"",[74,1094,331],{"class":314},[74,1096,1097,1100,1102,1105],{"class":76,"line":613},[74,1098,1099],{"class":320},"    \"type-check\"",[74,1101,324],{"class":314},[74,1103,1104],{"class":342},"\"tsc --noEmit\"",[74,1106,331],{"class":314},[74,1108,1109,1112,1114,1117],{"class":76,"line":624},[74,1110,1111],{"class":320},"    \"build\"",[74,1113,324],{"class":314},[74,1115,1116],{"class":342},"\"vp run type-check && vp pack\"",[74,1118,331],{"class":314},[74,1120,1121,1124,1126,1129],{"class":76,"line":634},[74,1122,1123],{"class":320},"    \"prepare\"",[74,1125,324],{"class":314},[74,1127,1128],{"class":342},"\"vp config --no-agent\"",[74,1130,331],{"class":314},[74,1132,1133,1136,1138],{"class":76,"line":640},[74,1134,1135],{"class":320},"    \"prepublishOnly\"",[74,1137,324],{"class":314},[74,1139,1140],{"class":342},"\"npm run build\"\n",[74,1142,1143],{"class":76,"line":646},[74,1144,1016],{"class":314},[74,1146,1148,1151],{"class":76,"line":1147},23,[74,1149,1150],{"class":320},"  \"dependencies\"",[74,1152,1001],{"class":314},[74,1154,1156,1159,1161,1164],{"class":76,"line":1155},24,[74,1157,1158],{"class":320},"    \"@clack/prompts\"",[74,1160,324],{"class":314},[74,1162,1163],{"class":342},"\"^1.8.1\"",[74,1165,331],{"class":314},[74,1167,1169,1172,1174,1177],{"class":76,"line":1168},25,[74,1170,1171],{"class":320},"    \"citty\"",[74,1173,324],{"class":314},[74,1175,1176],{"class":342},"\"^0.2.2\"",[74,1178,331],{"class":314},[74,1180,1182,1185,1187,1190],{"class":76,"line":1181},26,[74,1183,1184],{"class":320},"    \"rc9\"",[74,1186,324],{"class":314},[74,1188,1189],{"class":342},"\"^3.1.0\"",[74,1191,331],{"class":314},[74,1193,1195,1198,1200],{"class":76,"line":1194},27,[74,1196,1197],{"class":320},"    \"zod\"",[74,1199,324],{"class":314},[74,1201,1202],{"class":342},"\"^4.6.7\"\n",[74,1204,1206],{"class":76,"line":1205},28,[74,1207,1016],{"class":314},[74,1209,1211,1214],{"class":76,"line":1210},29,[74,1212,1213],{"class":320},"  \"devDependencies\"",[74,1215,1001],{"class":314},[74,1217,1219,1222,1224,1227],{"class":76,"line":1218},30,[74,1220,1221],{"class":320},"    \"@types/node\"",[74,1223,324],{"class":314},[74,1225,1226],{"class":342},"\"^26.6.6\"",[74,1228,331],{"class":314},[74,1230,1232,1235,1237,1240],{"class":76,"line":1231},31,[74,1233,1234],{"class":320},"    \"typescript\"",[74,1236,324],{"class":314},[74,1238,1239],{"class":342},"\"^7.0.2\"",[74,1241,331],{"class":314},[74,1243,1245,1248,1250,1253],{"class":76,"line":1244},32,[74,1246,1247],{"class":320},"    \"vite\"",[74,1249,324],{"class":314},[74,1251,1252],{"class":342},"\"npm:@voidzero-dev/vite-plus-core@^1.4.0\"",[74,1254,331],{"class":314},[74,1256,1258,1261,1263],{"class":76,"line":1257},33,[74,1259,1260],{"class":320},"    \"vite-plus\"",[74,1262,324],{"class":314},[74,1264,1265],{"class":342},"\"^1.4.0\"\n",[74,1267,1269],{"class":76,"line":1268},34,[74,1270,1271],{"class":314},"  }\n",[74,1273,1275],{"class":76,"line":1274},35,[74,1276,427],{"class":314},[11,1278,1279,1280,1283,1284,1286],{},"First, the ",[15,1281,1282],{},"name"," uses the scope we talked about at the beginning, so remember to replace ",[15,1285,202],{}," with your npm username.",[11,1288,1289],{},"Then, there are three keys that do the heavy lifting for a CLI:",[681,1291,1292,1308,1320],{},[684,1293,1294,1297,1298,1301,1302,1305,1306,196],{},[15,1295,1296],{},"bin"," maps a command name to a file. When someone runs ",[15,1299,1300],{},"npx @username/howfar",", npm executes ",[15,1303,1304],{},"dist/howfar.mjs",". If they install the package globally, the command they type is still just ",[15,1307,175],{},[684,1309,1310,1313,1314,1316,1317,196],{},[15,1311,1312],{},"files"," limits what gets published to the ",[15,1315,235],{}," folder. Nobody needs our TypeScript sources or tests in their ",[15,1318,1319],{},"node_modules",[684,1321,1322,1325,1326,1329],{},[15,1323,1324],{},"prepublishOnly"," runs the build right before ",[15,1327,1328],{},"npm publish",", so you can never publish stale output.",[11,1331,1332],{},"Next, let's add the TypeScript config:",[66,1334,1337],{"className":305,"code":1335,"filename":1336,"language":307,"meta":42,"style":42},"{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"NodeNext\",\n    \"moduleResolution\": \"NodeNext\",\n    \"outDir\": \"dist\",\n    \"rootDir\": \".\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"verbatimModuleSyntax\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"types\": [\"node\"]\n  },\n  \"include\": [\"bin\", \"commands\", \"lib\"]\n}\n","tsconfig.json",[15,1338,1339,1343,1350,1362,1374,1385,1396,1408,1419,1430,1441,1452,1463,1476,1480,1502],{"__ignoreMap":42},[74,1340,1341],{"class":76,"line":77},[74,1342,315],{"class":314},[74,1344,1345,1348],{"class":76,"line":43},[74,1346,1347],{"class":320},"  \"compilerOptions\"",[74,1349,1001],{"class":314},[74,1351,1352,1355,1357,1360],{"class":76,"line":334},[74,1353,1354],{"class":320},"    \"target\"",[74,1356,324],{"class":314},[74,1358,1359],{"class":342},"\"ES2022\"",[74,1361,331],{"class":314},[74,1363,1364,1367,1369,1372],{"class":76,"line":348},[74,1365,1366],{"class":320},"    \"module\"",[74,1368,324],{"class":314},[74,1370,1371],{"class":342},"\"NodeNext\"",[74,1373,331],{"class":314},[74,1375,1376,1379,1381,1383],{"class":76,"line":361},[74,1377,1378],{"class":320},"    \"moduleResolution\"",[74,1380,324],{"class":314},[74,1382,1371],{"class":342},[74,1384,331],{"class":314},[74,1386,1387,1390,1392,1394],{"class":76,"line":374},[74,1388,1389],{"class":320},"    \"outDir\"",[74,1391,324],{"class":314},[74,1393,1027],{"class":342},[74,1395,331],{"class":314},[74,1397,1398,1401,1403,1406],{"class":76,"line":387},[74,1399,1400],{"class":320},"    \"rootDir\"",[74,1402,324],{"class":314},[74,1404,1405],{"class":342},"\".\"",[74,1407,331],{"class":314},[74,1409,1410,1413,1415,1417],{"class":76,"line":400},[74,1411,1412],{"class":320},"    \"strict\"",[74,1414,324],{"class":314},[74,1416,328],{"class":327},[74,1418,331],{"class":314},[74,1420,1421,1424,1426,1428],{"class":76,"line":413},[74,1422,1423],{"class":320},"    \"esModuleInterop\"",[74,1425,324],{"class":314},[74,1427,328],{"class":327},[74,1429,331],{"class":314},[74,1431,1432,1435,1437,1439],{"class":76,"line":424},[74,1433,1434],{"class":320},"    \"skipLibCheck\"",[74,1436,324],{"class":314},[74,1438,328],{"class":327},[74,1440,331],{"class":314},[74,1442,1443,1446,1448,1450],{"class":76,"line":544},[74,1444,1445],{"class":320},"    \"verbatimModuleSyntax\"",[74,1447,324],{"class":314},[74,1449,328],{"class":327},[74,1451,331],{"class":314},[74,1453,1454,1457,1459,1461],{"class":76,"line":554},[74,1455,1456],{"class":320},"    \"noUncheckedIndexedAccess\"",[74,1458,324],{"class":314},[74,1460,328],{"class":327},[74,1462,331],{"class":314},[74,1464,1465,1468,1470,1473],{"class":76,"line":560},[74,1466,1467],{"class":320},"    \"types\"",[74,1469,1024],{"class":314},[74,1471,1472],{"class":342},"\"node\"",[74,1474,1475],{"class":314},"]\n",[74,1477,1478],{"class":76,"line":565},[74,1479,1016],{"class":314},[74,1481,1482,1485,1487,1490,1492,1495,1497,1500],{"class":76,"line":577},[74,1483,1484],{"class":320},"  \"include\"",[74,1486,1024],{"class":314},[74,1488,1489],{"class":342},"\"bin\"",[74,1491,138],{"class":314},[74,1493,1494],{"class":342},"\"commands\"",[74,1496,138],{"class":314},[74,1498,1499],{"class":342},"\"lib\"",[74,1501,1475],{"class":314},[74,1503,1504],{"class":76,"line":589},[74,1505,427],{"class":314},[11,1507,1508,1509,1512,1513,1516,1517,1520,1521,1524],{},"I'd never turn off ",[15,1510,1511],{},"strict"," or ",[15,1514,1515],{},"noUncheckedIndexedAccess",". The second one types ",[15,1518,1519],{},"array[0]"," as ",[15,1522,1523],{},"T | undefined",", which is the truth, and it will catch a whole category of bugs before your users do.",[11,1526,1527,1528,1531,1532,1535,1536,1539],{},"And the ",[15,1529,1530],{},"types"," line matters more than it looks. Since TypeScript 6, the compiler no longer loads every ",[15,1533,1534],{},"@types"," package automatically, so without it, ",[15,1537,1538],{},"process"," is an unknown name and the entry point won't compile.",[11,1541,1542],{},"Now, let's add the Vite+ config:",[66,1544,1549],{"className":1545,"code":1546,"filename":1547,"language":1548,"meta":42,"style":42},"language-ts shiki shiki-themes monokai","import { defineConfig } from 'vite-plus'\n\nexport default defineConfig({\n  fmt: {\n    semi: false,\n    singleQuote: true,\n    ignorePatterns: ['dist/**', '**/*.md'],\n  },\n  lint: {\n    ignorePatterns: ['dist/**'],\n    rules: {\n      curly: ['error', 'all'],\n    },\n  },\n  pack: {\n    entry: ['bin/howfar.ts'],\n    format: ['es'],\n    platform: 'node',\n    target: 'node22',\n  },\n  staged: {\n    '*.{ts,js}': ['vp check --fix', 'vp test related --run'],\n  },\n})\n","vite.config.ts","ts",[15,1550,1551,1565,1570,1584,1589,1599,1608,1623,1627,1632,1640,1645,1660,1664,1668,1673,1683,1693,1703,1713,1717,1722,1739,1743],{"__ignoreMap":42},[74,1552,1553,1556,1559,1562],{"class":76,"line":77},[74,1554,1555],{"class":788},"import",[74,1557,1558],{"class":314}," { defineConfig } ",[74,1560,1561],{"class":788},"from",[74,1563,1564],{"class":83}," 'vite-plus'\n",[74,1566,1567],{"class":76,"line":43},[74,1568,1569],{"emptyLinePlaceholder":52},"\n",[74,1571,1572,1575,1578,1581],{"class":76,"line":334},[74,1573,1574],{"class":788},"export",[74,1576,1577],{"class":788}," default",[74,1579,1580],{"class":80}," defineConfig",[74,1582,1583],{"class":314},"({\n",[74,1585,1586],{"class":76,"line":348},[74,1587,1588],{"class":314},"  fmt: {\n",[74,1590,1591,1594,1597],{"class":76,"line":361},[74,1592,1593],{"class":314},"    semi: ",[74,1595,1596],{"class":327},"false",[74,1598,331],{"class":314},[74,1600,1601,1604,1606],{"class":76,"line":374},[74,1602,1603],{"class":314},"    singleQuote: ",[74,1605,328],{"class":327},[74,1607,331],{"class":314},[74,1609,1610,1613,1616,1618,1621],{"class":76,"line":387},[74,1611,1612],{"class":314},"    ignorePatterns: [",[74,1614,1615],{"class":83},"'dist/**'",[74,1617,138],{"class":314},[74,1619,1620],{"class":83},"'**/*.md'",[74,1622,1030],{"class":314},[74,1624,1625],{"class":76,"line":400},[74,1626,1016],{"class":314},[74,1628,1629],{"class":76,"line":413},[74,1630,1631],{"class":314},"  lint: {\n",[74,1633,1634,1636,1638],{"class":76,"line":424},[74,1635,1612],{"class":314},[74,1637,1615],{"class":83},[74,1639,1030],{"class":314},[74,1641,1642],{"class":76,"line":544},[74,1643,1644],{"class":314},"    rules: {\n",[74,1646,1647,1650,1653,1655,1658],{"class":76,"line":554},[74,1648,1649],{"class":314},"      curly: [",[74,1651,1652],{"class":83},"'error'",[74,1654,138],{"class":314},[74,1656,1657],{"class":83},"'all'",[74,1659,1030],{"class":314},[74,1661,1662],{"class":76,"line":560},[74,1663,557],{"class":314},[74,1665,1666],{"class":76,"line":565},[74,1667,1016],{"class":314},[74,1669,1670],{"class":76,"line":577},[74,1671,1672],{"class":314},"  pack: {\n",[74,1674,1675,1678,1681],{"class":76,"line":589},[74,1676,1677],{"class":314},"    entry: [",[74,1679,1680],{"class":83},"'bin/howfar.ts'",[74,1682,1030],{"class":314},[74,1684,1685,1688,1691],{"class":76,"line":601},[74,1686,1687],{"class":314},"    format: [",[74,1689,1690],{"class":83},"'es'",[74,1692,1030],{"class":314},[74,1694,1695,1698,1701],{"class":76,"line":613},[74,1696,1697],{"class":314},"    platform: ",[74,1699,1700],{"class":83},"'node'",[74,1702,331],{"class":314},[74,1704,1705,1708,1711],{"class":76,"line":624},[74,1706,1707],{"class":314},"    target: ",[74,1709,1710],{"class":83},"'node22'",[74,1712,331],{"class":314},[74,1714,1715],{"class":76,"line":634},[74,1716,1016],{"class":314},[74,1718,1719],{"class":76,"line":640},[74,1720,1721],{"class":314},"  staged: {\n",[74,1723,1724,1727,1729,1732,1734,1737],{"class":76,"line":646},[74,1725,1726],{"class":83},"    '*.{ts,js}'",[74,1728,1024],{"class":314},[74,1730,1731],{"class":83},"'vp check --fix'",[74,1733,138],{"class":314},[74,1735,1736],{"class":83},"'vp test related --run'",[74,1738,1030],{"class":314},[74,1740,1741],{"class":76,"line":1147},[74,1742,1016],{"class":314},[74,1744,1745],{"class":76,"line":1155},[74,1746,1747],{"class":314},"})\n",[11,1749,1750,1751,1754,1755,1758,1759,1762,1763,1765,1766,196],{},"Here, the ",[15,1752,1753],{},"pack"," block is the important one. It tells Vite+ to take ",[15,1756,1757],{},"bin/howfar.ts",", follow every import, and bundle the result into a single ES module for Node 22. The ",[15,1760,1761],{},"staged"," block sets up a pre-commit hook that formats and lints the files you're about to commit, then runs the tests that import them. The ",[15,1764,242],{}," script we added earlier installs that hook the first time you run ",[15,1767,902],{},[11,1769,1770,1771,815],{},"Finally, let's add a ",[15,1772,1773],{},".gitignore",[66,1775,1778],{"className":1776,"code":1777,"filename":1773,"language":276,"meta":42},[274],"node_modules/\ndist/\n",[15,1779,1777],{"__ignoreMap":42},[11,1781,1782,1783,1785,1786,1788],{},"Then, let's run ",[15,1784,902],{}," once more so the ",[15,1787,242],{}," script installs the Git hook. And with that, the setup is done.",[119,1790,1792],{"id":1791},"the-folder-structure","The Folder Structure",[11,1794,1795],{},"Before we write the first command, here's the layout we're building toward. I settled on it after trying a few, and it's the one climail uses too:",[66,1797,1800],{"className":1798,"code":1799,"language":276,"meta":42},[274],"howfar/\n├── bin/\n│   └── howfar.ts        # entry point: wires commands together\n├── commands/\n│   ├── init.ts          # one file per command\n│   └── lookup.ts\n├── lib/\n│   ├── cli.ts           # shared argument parsers\n│   ├── config.ts        # read, validate and write the config file\n│   └── distance.ts      # finds cities and measures between them\n├── tests/\n│   ├── cli.test.ts\n│   ├── distance.test.ts\n│   └── lookup.test.ts\n├── package.json\n├── tsconfig.json\n└── vite.config.ts\n",[15,1801,1799],{"__ignoreMap":42},[11,1803,1804,1805,1808,1809,1812,1813,1816,1817,1819],{},"The layout stays tidy as long as ",[92,1806,1807],{},"commands stay thin and libraries stay thick."," A file in ",[15,1810,1811],{},"commands/"," reads its arguments, calls into ",[15,1814,1815],{},"lib/",", and returns a result. Everything that does actual work lives in ",[15,1818,1815],{},", where it can be imported, reused, and tested on its own, with no terminal involved.",[119,1821,1823],{"id":1822},"the-entry-point","The Entry Point",[11,1825,1826,1827,1830,1831,1833],{},"Okay, let's start writing code. The file in ",[15,1828,1829],{},"bin/"," is what runs when someone types ",[15,1832,175],{},". Its only job is to collect the commands and dispatch to the right one.",[66,1835,1837],{"className":1545,"code":1836,"filename":1757,"language":1548,"meta":42,"style":42},"#!/usr/bin/env node\n\nimport { defineCommand, runCommand, showUsage, type CommandDef } from 'citty'\nimport { lookup } from '../commands/lookup.js'\n\nconst subCommands: Record\u003Cstring, CommandDef\u003Cany>> = {}\n\nconst main = defineCommand({ ...lookup, subCommands })\n\nasync function run() {\n  const argv = process.argv.slice(2)\n  const [name, ...rest] = argv\n  const subCommand = name ? subCommands[name] : undefined\n  const wantsHelp = argv.includes('--help') || argv.includes('-h')\n\n  if (wantsHelp) {\n    await (subCommand ? showUsage(subCommand, main) : showUsage(main))\n\n    return\n  }\n\n  const command = subCommand ?? lookup\n  const { result } = await runCommand(command, { rawArgs: subCommand ? rest : argv })\n\n  if (result !== undefined) {\n    console.log(JSON.stringify(result, null, 2))\n  }\n}\n\nrun().catch((error: unknown) => {\n  console.error(error instanceof Error ? error.message : error)\n  process.exit(1)\n})\n",[15,1838,1839,1845,1849,1867,1879,1883,1922,1926,1947,1951,1965,1990,2007,2029,2066,2070,2078,2101,2105,2110,2114,2118,2135,2163,2167,2183,2210,2214,2218,2222,2253,2280,2295],{"__ignoreMap":42},[74,1840,1841],{"class":76,"line":77},[74,1842,1844],{"class":1843},"snpHw","#!/usr/bin/env node\n",[74,1846,1847],{"class":76,"line":43},[74,1848,1569],{"emptyLinePlaceholder":52},[74,1850,1851,1853,1856,1859,1862,1864],{"class":76,"line":334},[74,1852,1555],{"class":788},[74,1854,1855],{"class":314}," { defineCommand, runCommand, showUsage, ",[74,1857,1858],{"class":788},"type",[74,1860,1861],{"class":314}," CommandDef } ",[74,1863,1561],{"class":788},[74,1865,1866],{"class":83}," 'citty'\n",[74,1868,1869,1871,1874,1876],{"class":76,"line":348},[74,1870,1555],{"class":788},[74,1872,1873],{"class":314}," { lookup } ",[74,1875,1561],{"class":788},[74,1877,1878],{"class":83}," '../commands/lookup.js'\n",[74,1880,1881],{"class":76,"line":361},[74,1882,1569],{"emptyLinePlaceholder":52},[74,1884,1885,1888,1891,1893,1897,1900,1903,1905,1908,1910,1913,1916,1919],{"class":76,"line":374},[74,1886,1887],{"class":320},"const",[74,1889,1890],{"class":314}," subCommands",[74,1892,815],{"class":788},[74,1894,1896],{"class":1895},"soZtE"," Record",[74,1898,1899],{"class":314},"\u003C",[74,1901,1902],{"class":320},"string",[74,1904,138],{"class":314},[74,1906,1907],{"class":1895},"CommandDef",[74,1909,1899],{"class":314},[74,1911,1912],{"class":320},"any",[74,1914,1915],{"class":314},">> ",[74,1917,1918],{"class":788},"=",[74,1920,1921],{"class":314}," {}\n",[74,1923,1924],{"class":76,"line":387},[74,1925,1569],{"emptyLinePlaceholder":52},[74,1927,1928,1930,1933,1935,1938,1941,1944],{"class":76,"line":400},[74,1929,1887],{"class":320},[74,1931,1932],{"class":314}," main ",[74,1934,1918],{"class":788},[74,1936,1937],{"class":80}," defineCommand",[74,1939,1940],{"class":314},"({ ",[74,1942,1943],{"class":788},"...",[74,1945,1946],{"class":314},"lookup, subCommands })\n",[74,1948,1949],{"class":76,"line":413},[74,1950,1569],{"emptyLinePlaceholder":52},[74,1952,1953,1956,1959,1962],{"class":76,"line":424},[74,1954,1955],{"class":788},"async",[74,1957,1958],{"class":320}," function",[74,1960,1961],{"class":80}," run",[74,1963,1964],{"class":314},"() {\n",[74,1966,1967,1970,1973,1975,1978,1981,1984,1987],{"class":76,"line":544},[74,1968,1969],{"class":320},"  const",[74,1971,1972],{"class":314}," argv ",[74,1974,1918],{"class":788},[74,1976,1977],{"class":314}," process.argv.",[74,1979,1980],{"class":80},"slice",[74,1982,1983],{"class":314},"(",[74,1985,1986],{"class":327},"2",[74,1988,1989],{"class":314},")\n",[74,1991,1992,1994,1997,1999,2002,2004],{"class":76,"line":554},[74,1993,1969],{"class":320},[74,1995,1996],{"class":314}," [name, ",[74,1998,1943],{"class":788},[74,2000,2001],{"class":314},"rest] ",[74,2003,1918],{"class":788},[74,2005,2006],{"class":314}," argv\n",[74,2008,2009,2011,2014,2016,2019,2021,2024,2026],{"class":76,"line":560},[74,2010,1969],{"class":320},[74,2012,2013],{"class":314}," subCommand ",[74,2015,1918],{"class":788},[74,2017,2018],{"class":314}," name ",[74,2020,907],{"class":788},[74,2022,2023],{"class":314}," subCommands[name] ",[74,2025,815],{"class":788},[74,2027,2028],{"class":327}," undefined\n",[74,2030,2031,2033,2036,2038,2041,2044,2046,2049,2052,2055,2057,2059,2061,2064],{"class":76,"line":565},[74,2032,1969],{"class":320},[74,2034,2035],{"class":314}," wantsHelp ",[74,2037,1918],{"class":788},[74,2039,2040],{"class":314}," argv.",[74,2042,2043],{"class":80},"includes",[74,2045,1983],{"class":314},[74,2047,2048],{"class":83},"'--help'",[74,2050,2051],{"class":314},") ",[74,2053,2054],{"class":788},"||",[74,2056,2040],{"class":314},[74,2058,2043],{"class":80},[74,2060,1983],{"class":314},[74,2062,2063],{"class":83},"'-h'",[74,2065,1989],{"class":314},[74,2067,2068],{"class":76,"line":577},[74,2069,1569],{"emptyLinePlaceholder":52},[74,2071,2072,2075],{"class":76,"line":589},[74,2073,2074],{"class":788},"  if",[74,2076,2077],{"class":314}," (wantsHelp) {\n",[74,2079,2080,2083,2086,2088,2091,2094,2096,2098],{"class":76,"line":601},[74,2081,2082],{"class":788},"    await",[74,2084,2085],{"class":314}," (subCommand ",[74,2087,907],{"class":788},[74,2089,2090],{"class":80}," showUsage",[74,2092,2093],{"class":314},"(subCommand, main) ",[74,2095,815],{"class":788},[74,2097,2090],{"class":80},[74,2099,2100],{"class":314},"(main))\n",[74,2102,2103],{"class":76,"line":613},[74,2104,1569],{"emptyLinePlaceholder":52},[74,2106,2107],{"class":76,"line":624},[74,2108,2109],{"class":788},"    return\n",[74,2111,2112],{"class":76,"line":634},[74,2113,1271],{"class":314},[74,2115,2116],{"class":76,"line":640},[74,2117,1569],{"emptyLinePlaceholder":52},[74,2119,2120,2122,2125,2127,2129,2132],{"class":76,"line":646},[74,2121,1969],{"class":320},[74,2123,2124],{"class":314}," command ",[74,2126,1918],{"class":788},[74,2128,2013],{"class":314},[74,2130,2131],{"class":788},"??",[74,2133,2134],{"class":314}," lookup\n",[74,2136,2137,2139,2142,2144,2147,2150,2153,2155,2158,2160],{"class":76,"line":1147},[74,2138,1969],{"class":320},[74,2140,2141],{"class":314}," { result } ",[74,2143,1918],{"class":788},[74,2145,2146],{"class":788}," await",[74,2148,2149],{"class":80}," runCommand",[74,2151,2152],{"class":314},"(command, { rawArgs: subCommand ",[74,2154,907],{"class":788},[74,2156,2157],{"class":314}," rest ",[74,2159,815],{"class":788},[74,2161,2162],{"class":314}," argv })\n",[74,2164,2165],{"class":76,"line":1155},[74,2166,1569],{"emptyLinePlaceholder":52},[74,2168,2169,2171,2174,2177,2180],{"class":76,"line":1168},[74,2170,2074],{"class":788},[74,2172,2173],{"class":314}," (result ",[74,2175,2176],{"class":788},"!==",[74,2178,2179],{"class":327}," undefined",[74,2181,2182],{"class":314},") {\n",[74,2184,2185,2188,2191,2194,2197,2200,2203,2205,2207],{"class":76,"line":1181},[74,2186,2187],{"class":314},"    console.",[74,2189,2190],{"class":80},"log",[74,2192,2193],{"class":314},"(JSON.",[74,2195,2196],{"class":80},"stringify",[74,2198,2199],{"class":314},"(result, ",[74,2201,2202],{"class":327},"null",[74,2204,138],{"class":314},[74,2206,1986],{"class":327},[74,2208,2209],{"class":314},"))\n",[74,2211,2212],{"class":76,"line":1194},[74,2213,1271],{"class":314},[74,2215,2216],{"class":76,"line":1205},[74,2217,427],{"class":314},[74,2219,2220],{"class":76,"line":1210},[74,2221,1569],{"emptyLinePlaceholder":52},[74,2223,2224,2227,2230,2233,2236,2240,2242,2245,2247,2250],{"class":76,"line":1218},[74,2225,2226],{"class":80},"run",[74,2228,2229],{"class":314},"().",[74,2231,2232],{"class":80},"catch",[74,2234,2235],{"class":314},"((",[74,2237,2239],{"class":2238},"sW0Xf","error",[74,2241,815],{"class":788},[74,2243,2244],{"class":320}," unknown",[74,2246,2051],{"class":314},[74,2248,2249],{"class":320},"=>",[74,2251,2252],{"class":314}," {\n",[74,2254,2255,2258,2260,2263,2266,2269,2272,2275,2277],{"class":76,"line":1231},[74,2256,2257],{"class":314},"  console.",[74,2259,2239],{"class":80},[74,2261,2262],{"class":314},"(error ",[74,2264,2265],{"class":788},"instanceof",[74,2267,2268],{"class":1895}," Error",[74,2270,2271],{"class":788}," ?",[74,2273,2274],{"class":314}," error.message ",[74,2276,815],{"class":788},[74,2278,2279],{"class":314}," error)\n",[74,2281,2282,2285,2288,2290,2293],{"class":76,"line":1244},[74,2283,2284],{"class":314},"  process.",[74,2286,2287],{"class":80},"exit",[74,2289,1983],{"class":314},[74,2291,2292],{"class":327},"1",[74,2294,1989],{"class":314},[74,2296,2297],{"class":76,"line":1257},[74,2298,1747],{"class":314},[11,2300,2301],{},"Let's break this down:",[681,2303,2304,2319,2325,2343,2355],{},[684,2305,2306,2307,2310,2311,2314,2315,2318],{},"The first line, ",[15,2308,2309],{},"#!/usr/bin/env node",", is the shebang. It means \"execute me with whatever ",[15,2312,2313],{},"node"," is on the user's ",[15,2316,2317],{},"PATH","\". Vite+ keeps it in the bundled output, so the published file is directly executable.",[684,2320,2321,2324],{},[15,2322,2323],{},"process.argv.slice(2)"," drops the first two entries (the Node binary and the script path) and leaves us with what the user actually typed. The first word is either a subcommand name or a city.",[684,2326,2327,2328,2331,2332,2335,2336,2339,2340,2342],{},"If the first word matches a subcommand, we run it with the rest of the arguments. Anything else is a city, so the whole list goes to the default ",[15,2329,2330],{},"lookup"," command. That's what lets users type ",[15,2333,2334],{},"howfar Tokyo"," instead of ",[15,2337,2338],{},"howfar lookup Tokyo",". The subcommand list is empty for now, but ",[15,2341,714],{}," will land there soon.",[684,2344,2345,2348,2349,2351,2352,2354],{},[15,2346,2347],{},"main"," is the ",[15,2350,2330],{}," command with the subcommands attached. We only use it for ",[15,2353,700],{},", so the top-level help screen lists both the city argument and every subcommand.",[684,2356,2357,2358,2361,2362,2365],{},"Every command ",[92,2359,2360],{},"returns"," its result instead of printing it. The entry point is the only place that calls ",[15,2363,2364],{},"console.log",", and it always prints JSON.",[11,2367,2368,2369,2372],{},"That last point is deliberate. When output is JSON, your tool works in a pipeline: ",[15,2370,2371],{},"npx @username/howfar Tokyo | jq .distance",", a shell script, a cron job, or an AI agent that runs your command and reads the result. Plain text is nicer to look at, but much harder to build on. climail prints JSON for exactly this reason, and it's what makes it useful to agents.",[11,2374,2375,2376,2378],{},"And then the ",[15,2377,2232],{}," at the bottom turns any thrown error into a single line on stderr and a non-zero exit code. Without it, a misspelled city would dump a stack trace at the user, which is fine for us and terrible for them.",[205,2380,2381],{},[11,2382,2383,2384,2387,2388,907],{},"Why the ",[15,2385,2386],{},".js"," extension when the file is called ",[15,2389,2390],{},"lookup.ts",[11,2392,2393,2394,2397,2398,2402,2403,2406],{},"That's how ES modules with ",[15,2395,2396],{},"NodeNext"," resolution work: imports point at the file that will exist ",[2399,2400,2401],"em",{},"after"," compilation. TypeScript knows to look for the ",[15,2404,2405],{},".ts"," source, and the bundler does the same. It looks wrong the first time, but then you stop noticing it.",[119,2408,2410],{"id":2409},"the-first-command","The First Command",[11,2412,2413,2414,2417,2418,2420,2421,2423],{},"Each command lives in its own file and exports a ",[15,2415,2416],{},"defineCommand"," object. Here's ",[15,2419,2330],{},", the default command. For now, it takes the city to measure the distance to, and a ",[15,2422,654],{}," flag with the city to measure from:",[66,2425,2428],{"className":1545,"code":2426,"filename":2427,"language":1548,"meta":42,"style":42},"import { defineCommand } from 'citty'\nimport { fetchDistance } from '../lib/distance.js'\n\nconst lookup = defineCommand({\n  meta: { name: 'howfar', description: 'Distance between two cities, as JSON' },\n  args: {\n    city: {\n      type: 'positional',\n      required: true,\n      description: 'City to measure the distance to',\n    },\n    from: {\n      type: 'string',\n      required: true,\n      description: 'City to measure the distance from',\n    },\n  },\n  run: async ({ args }) => {\n    return fetchDistance(args.city, args.from)\n  },\n})\n\nexport { lookup }\n","commands/lookup.ts",[15,2429,2430,2441,2453,2457,2470,2487,2492,2497,2507,2516,2526,2530,2535,2544,2552,2561,2565,2569,2591,2602,2606,2610,2614],{"__ignoreMap":42},[74,2431,2432,2434,2437,2439],{"class":76,"line":77},[74,2433,1555],{"class":788},[74,2435,2436],{"class":314}," { defineCommand } ",[74,2438,1561],{"class":788},[74,2440,1866],{"class":83},[74,2442,2443,2445,2448,2450],{"class":76,"line":43},[74,2444,1555],{"class":788},[74,2446,2447],{"class":314}," { fetchDistance } ",[74,2449,1561],{"class":788},[74,2451,2452],{"class":83}," '../lib/distance.js'\n",[74,2454,2455],{"class":76,"line":334},[74,2456,1569],{"emptyLinePlaceholder":52},[74,2458,2459,2461,2464,2466,2468],{"class":76,"line":348},[74,2460,1887],{"class":320},[74,2462,2463],{"class":314}," lookup ",[74,2465,1918],{"class":788},[74,2467,1937],{"class":80},[74,2469,1583],{"class":314},[74,2471,2472,2475,2478,2481,2484],{"class":76,"line":361},[74,2473,2474],{"class":314},"  meta: { name: ",[74,2476,2477],{"class":83},"'howfar'",[74,2479,2480],{"class":314},", description: ",[74,2482,2483],{"class":83},"'Distance between two cities, as JSON'",[74,2485,2486],{"class":314}," },\n",[74,2488,2489],{"class":76,"line":374},[74,2490,2491],{"class":314},"  args: {\n",[74,2493,2494],{"class":76,"line":387},[74,2495,2496],{"class":314},"    city: {\n",[74,2498,2499,2502,2505],{"class":76,"line":400},[74,2500,2501],{"class":314},"      type: ",[74,2503,2504],{"class":83},"'positional'",[74,2506,331],{"class":314},[74,2508,2509,2512,2514],{"class":76,"line":413},[74,2510,2511],{"class":314},"      required: ",[74,2513,328],{"class":327},[74,2515,331],{"class":314},[74,2517,2518,2521,2524],{"class":76,"line":424},[74,2519,2520],{"class":314},"      description: ",[74,2522,2523],{"class":83},"'City to measure the distance to'",[74,2525,331],{"class":314},[74,2527,2528],{"class":76,"line":544},[74,2529,557],{"class":314},[74,2531,2532],{"class":76,"line":554},[74,2533,2534],{"class":314},"    from: {\n",[74,2536,2537,2539,2542],{"class":76,"line":560},[74,2538,2501],{"class":314},[74,2540,2541],{"class":83},"'string'",[74,2543,331],{"class":314},[74,2545,2546,2548,2550],{"class":76,"line":565},[74,2547,2511],{"class":314},[74,2549,328],{"class":327},[74,2551,331],{"class":314},[74,2553,2554,2556,2559],{"class":76,"line":577},[74,2555,2520],{"class":314},[74,2557,2558],{"class":83},"'City to measure the distance from'",[74,2560,331],{"class":314},[74,2562,2563],{"class":76,"line":589},[74,2564,557],{"class":314},[74,2566,2567],{"class":76,"line":601},[74,2568,1016],{"class":314},[74,2570,2571,2574,2576,2578,2581,2584,2587,2589],{"class":76,"line":613},[74,2572,2573],{"class":80},"  run",[74,2575,324],{"class":314},[74,2577,1955],{"class":788},[74,2579,2580],{"class":314}," ({ ",[74,2582,2583],{"class":2238},"args",[74,2585,2586],{"class":314}," }) ",[74,2588,2249],{"class":320},[74,2590,2252],{"class":314},[74,2592,2593,2596,2599],{"class":76,"line":624},[74,2594,2595],{"class":788},"    return",[74,2597,2598],{"class":80}," fetchDistance",[74,2600,2601],{"class":314},"(args.city, args.from)\n",[74,2603,2604],{"class":76,"line":634},[74,2605,1016],{"class":314},[74,2607,2608],{"class":76,"line":640},[74,2609,1747],{"class":314},[74,2611,2612],{"class":76,"line":646},[74,2613,1569],{"emptyLinePlaceholder":52},[74,2615,2616,2618],{"class":76,"line":1147},[74,2617,1574],{"class":788},[74,2619,2620],{"class":314}," { lookup }\n",[11,2622,2623,2624,2626,2627,2629,2630,2632,2633,2636,2637,2640,2641,2643,2644,1512,2647,2650,2651,2654,2655,2658,2659,2661,2662,2664,2665,2667],{},"The ",[15,2625,2583],{}," object is the whole argument parser. citty reads it to validate input, to generate the help screen, and to type ",[15,2628,2583],{}," inside ",[15,2631,2226],{},". A ",[15,2634,2635],{},"positional"," argument is a bare word, like ",[15,2638,2639],{},"Tokyo",", and a ",[15,2642,1902],{}," argument is a flag, like ",[15,2645,2646],{},"--from Lima",[15,2648,2649],{},"--from=Lima",". Since we marked both as required, ",[15,2652,2653],{},"args.city"," and ",[15,2656,2657],{},"args.from"," are typed as ",[15,2660,1902],{}," in the ",[15,2663,2226],{}," function, with no casting needed. A few lines of declaration, and ",[15,2666,700],{}," already works.",[11,2669,2670,2671,815],{},"Next, let's write the distance logic, which goes in ",[15,2672,1815],{},[66,2674,2677],{"className":1545,"code":2675,"filename":2676,"language":1548,"meta":42,"style":42},"type Place = { name: string; country: string; latitude: number; longitude: number }\n\ntype Unit = 'km' | 'mi'\n\nconst EARTH_RADIUS: Record\u003CUnit, number> = { km: 6371, mi: 3959 }\n\nasync function geocode(city: string): Promise\u003CPlace> {\n  const url = new URL('https://geocoding-api.open-meteo.com/v1/search')\n  url.searchParams.set('name', city)\n  url.searchParams.set('count', '1')\n\n  const response = await fetch(url)\n  const data = (await response.json()) as { results?: Place[] }\n  const place = data.results?.[0]\n\n  if (!place) {\n    throw new Error(`Could not find a city called \"${city}\"`)\n  }\n\n  return place\n}\n\nfunction toRadians(degrees: number): number {\n  return (degrees * Math.PI) / 180\n}\n\n/** Shortest distance over the Earth's surface between two places, using the haversine formula. */\nfunction distanceBetween(a: Place, b: Place, unit: Unit): number {\n  const latitude = toRadians(b.latitude - a.latitude)\n  const longitude = toRadians(b.longitude - a.longitude)\n  const h =\n    Math.sin(latitude / 2) ** 2 +\n    Math.cos(toRadians(a.latitude)) * Math.cos(toRadians(b.latitude)) * Math.sin(longitude / 2) ** 2\n\n  return Math.round(2 * EARTH_RADIUS[unit] * Math.asin(Math.sqrt(h)))\n}\n\nfunction describe(place: Place, from: Place, unit: Unit) {\n  const distance = distanceBetween(from, place, unit)\n\n  return {\n    city: `${place.name}, ${place.country}`,\n    latitude: place.latitude,\n    longitude: place.longitude,\n    distance,\n    unit,\n    summary: `${place.name} is ${distance.toLocaleString('en-US')} ${unit} from ${from.name}`,\n  }\n}\n\nasync function fetchDistance(city: string, from: string, unit: Unit = 'km') {\n  const [place, origin] = await Promise.all([geocode(city), geocode(from)])\n\n  return { ok: true, from: `${origin.name}, ${origin.country}`, ...describe(place, origin, unit) }\n}\n\nexport { geocode, distanceBetween, fetchDistance }\n\nexport type { Place, Unit }\n","lib/distance.ts",[15,2678,2679,2722,2726,2743,2747,2787,2791,2825,2847,2863,2881,2885,2902,2939,2956,2960,2972,2999,3003,3007,3015,3019,3023,3048,3067,3071,3075,3080,3121,3141,3160,3170,3196,3245,3249,3284,3289,3294,3329,3344,3349,3356,3385,3391,3397,3403,3409,3464,3469,3474,3479,3518,3551,3556,3599,3604,3609,3617,3622],{"__ignoreMap":42},[74,2680,2681,2683,2686,2689,2692,2694,2697,2700,2702,2704,2707,2709,2712,2715,2717,2719],{"class":76,"line":77},[74,2682,1858],{"class":320},[74,2684,2685],{"class":1895}," Place",[74,2687,2688],{"class":788}," =",[74,2690,2691],{"class":314}," { name",[74,2693,815],{"class":788},[74,2695,2696],{"class":320}," string",[74,2698,2699],{"class":314},"; country",[74,2701,815],{"class":788},[74,2703,2696],{"class":320},[74,2705,2706],{"class":314},"; latitude",[74,2708,815],{"class":788},[74,2710,2711],{"class":320}," number",[74,2713,2714],{"class":314},"; longitude",[74,2716,815],{"class":788},[74,2718,2711],{"class":320},[74,2720,2721],{"class":314}," }\n",[74,2723,2724],{"class":76,"line":43},[74,2725,1569],{"emptyLinePlaceholder":52},[74,2727,2728,2730,2733,2735,2738,2740],{"class":76,"line":334},[74,2729,1858],{"class":320},[74,2731,2732],{"class":1895}," Unit",[74,2734,2688],{"class":788},[74,2736,2737],{"class":83}," 'km'",[74,2739,789],{"class":788},[74,2741,2742],{"class":83}," 'mi'\n",[74,2744,2745],{"class":76,"line":348},[74,2746,1569],{"emptyLinePlaceholder":52},[74,2748,2749,2751,2754,2756,2758,2760,2763,2765,2768,2771,2773,2776,2779,2782,2785],{"class":76,"line":361},[74,2750,1887],{"class":320},[74,2752,2753],{"class":314}," EARTH_RADIUS",[74,2755,815],{"class":788},[74,2757,1896],{"class":1895},[74,2759,1899],{"class":314},[74,2761,2762],{"class":1895},"Unit",[74,2764,138],{"class":314},[74,2766,2767],{"class":320},"number",[74,2769,2770],{"class":314},"> ",[74,2772,1918],{"class":788},[74,2774,2775],{"class":314}," { km: ",[74,2777,2778],{"class":327},"6371",[74,2780,2781],{"class":314},", mi: ",[74,2783,2784],{"class":327},"3959",[74,2786,2721],{"class":314},[74,2788,2789],{"class":76,"line":374},[74,2790,1569],{"emptyLinePlaceholder":52},[74,2792,2793,2795,2797,2800,2802,2805,2807,2809,2812,2814,2817,2819,2822],{"class":76,"line":387},[74,2794,1955],{"class":788},[74,2796,1958],{"class":320},[74,2798,2799],{"class":80}," geocode",[74,2801,1983],{"class":314},[74,2803,2804],{"class":2238},"city",[74,2806,815],{"class":788},[74,2808,2696],{"class":320},[74,2810,2811],{"class":314},")",[74,2813,815],{"class":788},[74,2815,2816],{"class":1895}," Promise",[74,2818,1899],{"class":314},[74,2820,2821],{"class":1895},"Place",[74,2823,2824],{"class":314},"> {\n",[74,2826,2827,2829,2832,2834,2837,2840,2842,2845],{"class":76,"line":400},[74,2828,1969],{"class":320},[74,2830,2831],{"class":314}," url ",[74,2833,1918],{"class":788},[74,2835,2836],{"class":788}," new",[74,2838,2839],{"class":80}," URL",[74,2841,1983],{"class":314},[74,2843,2844],{"class":83},"'https://geocoding-api.open-meteo.com/v1/search'",[74,2846,1989],{"class":314},[74,2848,2849,2852,2855,2857,2860],{"class":76,"line":413},[74,2850,2851],{"class":314},"  url.searchParams.",[74,2853,2854],{"class":80},"set",[74,2856,1983],{"class":314},[74,2858,2859],{"class":83},"'name'",[74,2861,2862],{"class":314},", city)\n",[74,2864,2865,2867,2869,2871,2874,2876,2879],{"class":76,"line":424},[74,2866,2851],{"class":314},[74,2868,2854],{"class":80},[74,2870,1983],{"class":314},[74,2872,2873],{"class":83},"'count'",[74,2875,138],{"class":314},[74,2877,2878],{"class":83},"'1'",[74,2880,1989],{"class":314},[74,2882,2883],{"class":76,"line":544},[74,2884,1569],{"emptyLinePlaceholder":52},[74,2886,2887,2889,2892,2894,2896,2899],{"class":76,"line":554},[74,2888,1969],{"class":320},[74,2890,2891],{"class":314}," response ",[74,2893,1918],{"class":788},[74,2895,2146],{"class":788},[74,2897,2898],{"class":80}," fetch",[74,2900,2901],{"class":314},"(url)\n",[74,2903,2904,2906,2909,2911,2914,2917,2920,2922,2925,2928,2931,2934,2936],{"class":76,"line":560},[74,2905,1969],{"class":320},[74,2907,2908],{"class":314}," data ",[74,2910,1918],{"class":788},[74,2912,2913],{"class":314}," (",[74,2915,2916],{"class":788},"await",[74,2918,2919],{"class":314}," response.",[74,2921,307],{"class":80},[74,2923,2924],{"class":314},"()) ",[74,2926,2927],{"class":788},"as",[74,2929,2930],{"class":314}," { results",[74,2932,2933],{"class":788},"?:",[74,2935,2685],{"class":1895},[74,2937,2938],{"class":314},"[] }\n",[74,2940,2941,2943,2946,2948,2951,2954],{"class":76,"line":565},[74,2942,1969],{"class":320},[74,2944,2945],{"class":314}," place ",[74,2947,1918],{"class":788},[74,2949,2950],{"class":314}," data.results?.[",[74,2952,2953],{"class":327},"0",[74,2955,1475],{"class":314},[74,2957,2958],{"class":76,"line":577},[74,2959,1569],{"emptyLinePlaceholder":52},[74,2961,2962,2964,2966,2969],{"class":76,"line":589},[74,2963,2074],{"class":788},[74,2965,2913],{"class":314},[74,2967,2968],{"class":788},"!",[74,2970,2971],{"class":314},"place) {\n",[74,2973,2974,2977,2979,2981,2983,2986,2989,2991,2994,2997],{"class":76,"line":601},[74,2975,2976],{"class":788},"    throw",[74,2978,2836],{"class":788},[74,2980,2268],{"class":80},[74,2982,1983],{"class":314},[74,2984,2985],{"class":83},"`Could not find a city called \"",[74,2987,2988],{"class":788},"${",[74,2990,2804],{"class":314},[74,2992,2993],{"class":788},"}",[74,2995,2996],{"class":83},"\"`",[74,2998,1989],{"class":314},[74,3000,3001],{"class":76,"line":613},[74,3002,1271],{"class":314},[74,3004,3005],{"class":76,"line":624},[74,3006,1569],{"emptyLinePlaceholder":52},[74,3008,3009,3012],{"class":76,"line":634},[74,3010,3011],{"class":788},"  return",[74,3013,3014],{"class":314}," place\n",[74,3016,3017],{"class":76,"line":640},[74,3018,427],{"class":314},[74,3020,3021],{"class":76,"line":646},[74,3022,1569],{"emptyLinePlaceholder":52},[74,3024,3025,3028,3031,3033,3036,3038,3040,3042,3044,3046],{"class":76,"line":1147},[74,3026,3027],{"class":320},"function",[74,3029,3030],{"class":80}," toRadians",[74,3032,1983],{"class":314},[74,3034,3035],{"class":2238},"degrees",[74,3037,815],{"class":788},[74,3039,2711],{"class":320},[74,3041,2811],{"class":314},[74,3043,815],{"class":788},[74,3045,2711],{"class":320},[74,3047,2252],{"class":314},[74,3049,3050,3052,3055,3058,3061,3064],{"class":76,"line":1155},[74,3051,3011],{"class":788},[74,3053,3054],{"class":314}," (degrees ",[74,3056,3057],{"class":788},"*",[74,3059,3060],{"class":314}," Math.PI) ",[74,3062,3063],{"class":788},"/",[74,3065,3066],{"class":327}," 180\n",[74,3068,3069],{"class":76,"line":1168},[74,3070,427],{"class":314},[74,3072,3073],{"class":76,"line":1181},[74,3074,1569],{"emptyLinePlaceholder":52},[74,3076,3077],{"class":76,"line":1194},[74,3078,3079],{"class":1843},"/** Shortest distance over the Earth's surface between two places, using the haversine formula. */\n",[74,3081,3082,3084,3087,3089,3091,3093,3095,3097,3100,3102,3104,3106,3109,3111,3113,3115,3117,3119],{"class":76,"line":1205},[74,3083,3027],{"class":320},[74,3085,3086],{"class":80}," distanceBetween",[74,3088,1983],{"class":314},[74,3090,23],{"class":2238},[74,3092,815],{"class":788},[74,3094,2685],{"class":1895},[74,3096,138],{"class":314},[74,3098,3099],{"class":2238},"b",[74,3101,815],{"class":788},[74,3103,2685],{"class":1895},[74,3105,138],{"class":314},[74,3107,3108],{"class":2238},"unit",[74,3110,815],{"class":788},[74,3112,2732],{"class":1895},[74,3114,2811],{"class":314},[74,3116,815],{"class":788},[74,3118,2711],{"class":320},[74,3120,2252],{"class":314},[74,3122,3123,3125,3128,3130,3132,3135,3138],{"class":76,"line":1210},[74,3124,1969],{"class":320},[74,3126,3127],{"class":314}," latitude ",[74,3129,1918],{"class":788},[74,3131,3030],{"class":80},[74,3133,3134],{"class":314},"(b.latitude ",[74,3136,3137],{"class":788},"-",[74,3139,3140],{"class":314}," a.latitude)\n",[74,3142,3143,3145,3148,3150,3152,3155,3157],{"class":76,"line":1218},[74,3144,1969],{"class":320},[74,3146,3147],{"class":314}," longitude ",[74,3149,1918],{"class":788},[74,3151,3030],{"class":80},[74,3153,3154],{"class":314},"(b.longitude ",[74,3156,3137],{"class":788},[74,3158,3159],{"class":314}," a.longitude)\n",[74,3161,3162,3164,3167],{"class":76,"line":1231},[74,3163,1969],{"class":320},[74,3165,3166],{"class":314}," h ",[74,3168,3169],{"class":788},"=\n",[74,3171,3172,3175,3178,3181,3183,3186,3188,3191,3193],{"class":76,"line":1244},[74,3173,3174],{"class":314},"    Math.",[74,3176,3177],{"class":80},"sin",[74,3179,3180],{"class":314},"(latitude ",[74,3182,3063],{"class":788},[74,3184,3185],{"class":327}," 2",[74,3187,2051],{"class":314},[74,3189,3190],{"class":788},"**",[74,3192,3185],{"class":327},[74,3194,3195],{"class":788}," +\n",[74,3197,3198,3200,3203,3205,3208,3211,3213,3216,3218,3220,3222,3225,3227,3229,3231,3234,3236,3238,3240,3242],{"class":76,"line":1257},[74,3199,3174],{"class":314},[74,3201,3202],{"class":80},"cos",[74,3204,1983],{"class":314},[74,3206,3207],{"class":80},"toRadians",[74,3209,3210],{"class":314},"(a.latitude)) ",[74,3212,3057],{"class":788},[74,3214,3215],{"class":314}," Math.",[74,3217,3202],{"class":80},[74,3219,1983],{"class":314},[74,3221,3207],{"class":80},[74,3223,3224],{"class":314},"(b.latitude)) ",[74,3226,3057],{"class":788},[74,3228,3215],{"class":314},[74,3230,3177],{"class":80},[74,3232,3233],{"class":314},"(longitude ",[74,3235,3063],{"class":788},[74,3237,3185],{"class":327},[74,3239,2051],{"class":314},[74,3241,3190],{"class":788},[74,3243,3244],{"class":327}," 2\n",[74,3246,3247],{"class":76,"line":1268},[74,3248,1569],{"emptyLinePlaceholder":52},[74,3250,3251,3253,3255,3258,3260,3262,3265,3268,3270,3272,3275,3278,3281],{"class":76,"line":1274},[74,3252,3011],{"class":788},[74,3254,3215],{"class":314},[74,3256,3257],{"class":80},"round",[74,3259,1983],{"class":314},[74,3261,1986],{"class":327},[74,3263,3264],{"class":788}," *",[74,3266,3267],{"class":314}," EARTH_RADIUS[unit] ",[74,3269,3057],{"class":788},[74,3271,3215],{"class":314},[74,3273,3274],{"class":80},"asin",[74,3276,3277],{"class":314},"(Math.",[74,3279,3280],{"class":80},"sqrt",[74,3282,3283],{"class":314},"(h)))\n",[74,3285,3287],{"class":76,"line":3286},36,[74,3288,427],{"class":314},[74,3290,3292],{"class":76,"line":3291},37,[74,3293,1569],{"emptyLinePlaceholder":52},[74,3295,3297,3299,3302,3304,3307,3309,3311,3313,3315,3317,3319,3321,3323,3325,3327],{"class":76,"line":3296},38,[74,3298,3027],{"class":320},[74,3300,3301],{"class":80}," describe",[74,3303,1983],{"class":314},[74,3305,3306],{"class":2238},"place",[74,3308,815],{"class":788},[74,3310,2685],{"class":1895},[74,3312,138],{"class":314},[74,3314,1561],{"class":2238},[74,3316,815],{"class":788},[74,3318,2685],{"class":1895},[74,3320,138],{"class":314},[74,3322,3108],{"class":2238},[74,3324,815],{"class":788},[74,3326,2732],{"class":1895},[74,3328,2182],{"class":314},[74,3330,3332,3334,3337,3339,3341],{"class":76,"line":3331},39,[74,3333,1969],{"class":320},[74,3335,3336],{"class":314}," distance ",[74,3338,1918],{"class":788},[74,3340,3086],{"class":80},[74,3342,3343],{"class":314},"(from, place, unit)\n",[74,3345,3347],{"class":76,"line":3346},40,[74,3348,1569],{"emptyLinePlaceholder":52},[74,3350,3352,3354],{"class":76,"line":3351},41,[74,3353,3011],{"class":788},[74,3355,2252],{"class":314},[74,3357,3359,3362,3365,3367,3370,3372,3374,3376,3379,3381,3383],{"class":76,"line":3358},42,[74,3360,3361],{"class":314},"    city: ",[74,3363,3364],{"class":83},"`",[74,3366,2988],{"class":788},[74,3368,3369],{"class":314},"place.name",[74,3371,2993],{"class":788},[74,3373,138],{"class":83},[74,3375,2988],{"class":788},[74,3377,3378],{"class":314},"place.country",[74,3380,2993],{"class":788},[74,3382,3364],{"class":83},[74,3384,331],{"class":314},[74,3386,3388],{"class":76,"line":3387},43,[74,3389,3390],{"class":314},"    latitude: place.latitude,\n",[74,3392,3394],{"class":76,"line":3393},44,[74,3395,3396],{"class":314},"    longitude: place.longitude,\n",[74,3398,3400],{"class":76,"line":3399},45,[74,3401,3402],{"class":314},"    distance,\n",[74,3404,3406],{"class":76,"line":3405},46,[74,3407,3408],{"class":314},"    unit,\n",[74,3410,3412,3415,3417,3419,3421,3423,3426,3428,3431,3434,3436,3439,3441,3443,3446,3448,3450,3453,3455,3458,3460,3462],{"class":76,"line":3411},47,[74,3413,3414],{"class":314},"    summary: ",[74,3416,3364],{"class":83},[74,3418,2988],{"class":788},[74,3420,3369],{"class":314},[74,3422,2993],{"class":788},[74,3424,3425],{"class":83}," is ",[74,3427,2988],{"class":788},[74,3429,3430],{"class":314},"distance.",[74,3432,3433],{"class":80},"toLocaleString",[74,3435,1983],{"class":314},[74,3437,3438],{"class":83},"'en-US'",[74,3440,2811],{"class":314},[74,3442,2993],{"class":788},[74,3444,3445],{"class":788}," ${",[74,3447,3108],{"class":314},[74,3449,2993],{"class":788},[74,3451,3452],{"class":83}," from ",[74,3454,2988],{"class":788},[74,3456,3457],{"class":314},"from.name",[74,3459,2993],{"class":788},[74,3461,3364],{"class":83},[74,3463,331],{"class":314},[74,3465,3467],{"class":76,"line":3466},48,[74,3468,1271],{"class":314},[74,3470,3472],{"class":76,"line":3471},49,[74,3473,427],{"class":314},[74,3475,3477],{"class":76,"line":3476},50,[74,3478,1569],{"emptyLinePlaceholder":52},[74,3480,3482,3484,3486,3488,3490,3492,3494,3496,3498,3500,3502,3504,3506,3508,3510,3512,3514,3516],{"class":76,"line":3481},51,[74,3483,1955],{"class":788},[74,3485,1958],{"class":320},[74,3487,2598],{"class":80},[74,3489,1983],{"class":314},[74,3491,2804],{"class":2238},[74,3493,815],{"class":788},[74,3495,2696],{"class":320},[74,3497,138],{"class":314},[74,3499,1561],{"class":2238},[74,3501,815],{"class":788},[74,3503,2696],{"class":320},[74,3505,138],{"class":314},[74,3507,3108],{"class":2238},[74,3509,815],{"class":788},[74,3511,2732],{"class":1895},[74,3513,2688],{"class":788},[74,3515,2737],{"class":83},[74,3517,2182],{"class":314},[74,3519,3521,3523,3526,3528,3530,3532,3534,3537,3540,3543,3546,3548],{"class":76,"line":3520},52,[74,3522,1969],{"class":320},[74,3524,3525],{"class":314}," [place, origin] ",[74,3527,1918],{"class":788},[74,3529,2146],{"class":788},[74,3531,2816],{"class":320},[74,3533,196],{"class":314},[74,3535,3536],{"class":80},"all",[74,3538,3539],{"class":314},"([",[74,3541,3542],{"class":80},"geocode",[74,3544,3545],{"class":314},"(city), ",[74,3547,3542],{"class":80},[74,3549,3550],{"class":314},"(from)])\n",[74,3552,3554],{"class":76,"line":3553},53,[74,3555,1569],{"emptyLinePlaceholder":52},[74,3557,3559,3561,3564,3566,3569,3571,3573,3576,3578,3580,3582,3585,3587,3589,3591,3593,3596],{"class":76,"line":3558},54,[74,3560,3011],{"class":788},[74,3562,3563],{"class":314}," { ok: ",[74,3565,328],{"class":327},[74,3567,3568],{"class":314},", from: ",[74,3570,3364],{"class":83},[74,3572,2988],{"class":788},[74,3574,3575],{"class":314},"origin.name",[74,3577,2993],{"class":788},[74,3579,138],{"class":83},[74,3581,2988],{"class":788},[74,3583,3584],{"class":314},"origin.country",[74,3586,2993],{"class":788},[74,3588,3364],{"class":83},[74,3590,138],{"class":314},[74,3592,1943],{"class":788},[74,3594,3595],{"class":80},"describe",[74,3597,3598],{"class":314},"(place, origin, unit) }\n",[74,3600,3602],{"class":76,"line":3601},55,[74,3603,427],{"class":314},[74,3605,3607],{"class":76,"line":3606},56,[74,3608,1569],{"emptyLinePlaceholder":52},[74,3610,3612,3614],{"class":76,"line":3611},57,[74,3613,1574],{"class":788},[74,3615,3616],{"class":314}," { geocode, distanceBetween, fetchDistance }\n",[74,3618,3620],{"class":76,"line":3619},58,[74,3621,1569],{"emptyLinePlaceholder":52},[74,3623,3625,3627,3630],{"class":76,"line":3624},59,[74,3626,1574],{"class":788},[74,3628,3629],{"class":788}," type",[74,3631,3632],{"class":314}," { Place, Unit }\n",[11,3634,3635,3636,3639,3640,3643,3644,3646,3647,3650,3651,3654],{},"The geocoding API gives us each city's latitude and longitude, and ",[15,3637,3638],{},"distanceBetween"," does the rest. It uses the ",[92,3641,3642],{},"haversine formula",", which takes two points on a sphere and returns the length of the shortest path between them along its surface. The formula works with angles in radians, so ",[15,3645,3207],{}," converts the degrees we get from the API first. Then, multiplying by the radius of the Earth in kilometers or miles gives us the distance in that unit. And since ",[15,3648,3649],{},"fetchDistance"," geocodes both cities with ",[15,3652,3653],{},"Promise.all",", the two requests run in parallel.",[205,3656,3657],{},[11,3658,3659],{},"Why not just subtract the coordinates?",[11,3661,3662],{},"Because a degree of longitude doesn't have a fixed length. At the equator it's about 111 km, and it shrinks to zero at the poles, where every meridian meets. The haversine formula accounts for that. It does treat the Earth as a perfect sphere, which it isn't, so the result can be off by up to about 0.5%. For \"how far is Tokyo?\", that's close enough.",[11,3664,3665,3666,3669],{},"Nothing in this file knows it's part of a CLI. It takes strings, returns objects, and throws a plain ",[15,3667,3668],{},"Error"," when something goes wrong. That's what makes it easy to test later, and it's why the \"thin commands, thick libraries\" rule matters.",[11,3671,3672],{},"Alright, it's time to try it. Let's build and run:",[66,3674,3676],{"className":68,"code":3675,"language":70,"meta":42,"style":42},"npm run build\nnode dist/howfar.mjs Tokyo --from \"Buenos Aires\"\n",[15,3677,3678,3687],{"__ignoreMap":42},[74,3679,3680,3682,3684],{"class":76,"line":77},[74,3681,98],{"class":80},[74,3683,1961],{"class":83},[74,3685,3686],{"class":83}," build\n",[74,3688,3689,3691,3694,3697,3700],{"class":76,"line":43},[74,3690,2313],{"class":80},[74,3692,3693],{"class":83}," dist/howfar.mjs",[74,3695,3696],{"class":83}," Tokyo",[74,3698,3699],{"class":327}," --from",[74,3701,3702],{"class":83}," \"Buenos Aires\"\n",[66,3704,3705],{"className":305,"code":306,"language":307,"meta":42,"style":42},[15,3706,3707,3711,3721,3731,3741,3751,3761,3771,3781,3789],{"__ignoreMap":42},[74,3708,3709],{"class":76,"line":77},[74,3710,315],{"class":314},[74,3712,3713,3715,3717,3719],{"class":76,"line":43},[74,3714,321],{"class":320},[74,3716,324],{"class":314},[74,3718,328],{"class":327},[74,3720,331],{"class":314},[74,3722,3723,3725,3727,3729],{"class":76,"line":334},[74,3724,337],{"class":320},[74,3726,324],{"class":314},[74,3728,343],{"class":342},[74,3730,331],{"class":314},[74,3732,3733,3735,3737,3739],{"class":76,"line":348},[74,3734,351],{"class":320},[74,3736,324],{"class":314},[74,3738,356],{"class":342},[74,3740,331],{"class":314},[74,3742,3743,3745,3747,3749],{"class":76,"line":361},[74,3744,364],{"class":320},[74,3746,324],{"class":314},[74,3748,369],{"class":327},[74,3750,331],{"class":314},[74,3752,3753,3755,3757,3759],{"class":76,"line":374},[74,3754,377],{"class":320},[74,3756,324],{"class":314},[74,3758,382],{"class":327},[74,3760,331],{"class":314},[74,3762,3763,3765,3767,3769],{"class":76,"line":387},[74,3764,390],{"class":320},[74,3766,324],{"class":314},[74,3768,395],{"class":327},[74,3770,331],{"class":314},[74,3772,3773,3775,3777,3779],{"class":76,"line":400},[74,3774,403],{"class":320},[74,3776,324],{"class":314},[74,3778,408],{"class":342},[74,3780,331],{"class":314},[74,3782,3783,3785,3787],{"class":76,"line":413},[74,3784,416],{"class":320},[74,3786,324],{"class":314},[74,3788,421],{"class":342},[74,3790,3791],{"class":76,"line":424},[74,3792,427],{"class":314},[11,3794,3795],{},"We also get the help screen for free:",[66,3797,3799],{"className":68,"code":3798,"language":70,"meta":42,"style":42},"node dist/howfar.mjs --help\n",[15,3800,3801],{"__ignoreMap":42},[74,3802,3803,3805,3807],{"class":76,"line":77},[74,3804,2313],{"class":80},[74,3806,3693],{"class":83},[74,3808,3809],{"class":327}," --help\n",[66,3811,3814],{"className":3812,"code":3813,"language":276,"meta":42},[274],"Distance between two cities, as JSON (howfar)\n\nUSAGE howfar [OPTIONS] \u003CCITY> --from=\u003Cfrom>\n\nARGUMENTS\n\n  CITY    City to measure the distance to (Required)\n\nOPTIONS\n\n  --from=\u003Cfrom>    City to measure the distance from (Required)\n",[15,3815,3813],{"__ignoreMap":42},[11,3817,3818,3819,3821],{},"Every command we add from here follows the same shape: one file, one ",[15,3820,2416],{},", one returned result.",[119,3823,3825],{"id":3824},"a-config-file-and-an-init-wizard","A Config File and an Init Wizard",[11,3827,3828,3829,3832,3833,3835,3836,3838],{},"Typing ",[15,3830,3831],{},"--from \"Buenos Aires\""," on every run gets old fast. Real tools remember your settings, so we'll store your city and preferred unit in a config file. After that, ",[15,3834,654],{}," becomes optional, and every lookup measures from home unless you say otherwise. Then, we'll add the ",[15,3837,714],{}," command that asks for them.",[11,3840,3841],{},"Let's start with the config module. rc9 handles the file, and zod handles the shape:",[66,3843,3846],{"className":1545,"code":3844,"filename":3845,"language":1548,"meta":42,"style":42},"import { z } from 'zod'\nimport { readUserConfig, writeUserConfig } from 'rc9'\n\n// Lives at ~/.config/howfar.conf so the tool works from any directory.\nconst RC_NAME = 'howfar.conf'\n\nconst configSchema = z.object({\n  home: z.string(),\n  unit: z.enum(['km', 'mi']).default('km'),\n})\n\ntype Config = z.infer\u003Ctypeof configSchema>\n\nfunction loadConfig(): Config | null {\n  const raw = readUserConfig(RC_NAME)\n\n  if (!raw.home) {\n    return null\n  }\n\n  const result = configSchema.safeParse(raw)\n\n  if (!result.success) {\n    throw new Error(`Configuration validation failed:\\n${z.prettifyError(result.error)}`)\n  }\n\n  return result.data\n}\n\nfunction resolveConfig(from?: string) {\n  const config = loadConfig()\n  const origin = from ?? config?.home\n\n  if (!origin) {\n    throw new Error('No starting city. Run \"npx @username/howfar init\" or pass --from.')\n  }\n\n  return { from: origin, unit: config?.unit ?? 'km' }\n}\n\nfunction saveConfig(config: Config) {\n  writeUserConfig(config, RC_NAME)\n}\n\nexport { loadConfig, resolveConfig, saveConfig }\n\nexport type { Config }\n","lib/config.ts",[15,3847,3848,3860,3872,3876,3881,3893,3897,3914,3924,3955,3959,3963,3988,3992,4013,4028,4032,4043,4050,4054,4058,4076,4080,4091,4124,4128,4132,4139,4143,4147,4164,4178,4194,4198,4209,4224,4228,4232,4245,4249,4253,4271,4279,4283,4287,4294,4298],{"__ignoreMap":42},[74,3849,3850,3852,3855,3857],{"class":76,"line":77},[74,3851,1555],{"class":788},[74,3853,3854],{"class":314}," { z } ",[74,3856,1561],{"class":788},[74,3858,3859],{"class":83}," 'zod'\n",[74,3861,3862,3864,3867,3869],{"class":76,"line":43},[74,3863,1555],{"class":788},[74,3865,3866],{"class":314}," { readUserConfig, writeUserConfig } ",[74,3868,1561],{"class":788},[74,3870,3871],{"class":83}," 'rc9'\n",[74,3873,3874],{"class":76,"line":334},[74,3875,1569],{"emptyLinePlaceholder":52},[74,3877,3878],{"class":76,"line":348},[74,3879,3880],{"class":1843},"// Lives at ~/.config/howfar.conf so the tool works from any directory.\n",[74,3882,3883,3885,3888,3890],{"class":76,"line":361},[74,3884,1887],{"class":320},[74,3886,3887],{"class":314}," RC_NAME ",[74,3889,1918],{"class":788},[74,3891,3892],{"class":83}," 'howfar.conf'\n",[74,3894,3895],{"class":76,"line":374},[74,3896,1569],{"emptyLinePlaceholder":52},[74,3898,3899,3901,3904,3906,3909,3912],{"class":76,"line":387},[74,3900,1887],{"class":320},[74,3902,3903],{"class":314}," configSchema ",[74,3905,1918],{"class":788},[74,3907,3908],{"class":314}," z.",[74,3910,3911],{"class":80},"object",[74,3913,1583],{"class":314},[74,3915,3916,3919,3921],{"class":76,"line":400},[74,3917,3918],{"class":314},"  home: z.",[74,3920,1902],{"class":80},[74,3922,3923],{"class":314},"(),\n",[74,3925,3926,3929,3932,3934,3937,3939,3942,3945,3948,3950,3952],{"class":76,"line":413},[74,3927,3928],{"class":314},"  unit: z.",[74,3930,3931],{"class":80},"enum",[74,3933,3539],{"class":314},[74,3935,3936],{"class":83},"'km'",[74,3938,138],{"class":314},[74,3940,3941],{"class":83},"'mi'",[74,3943,3944],{"class":314},"]).",[74,3946,3947],{"class":80},"default",[74,3949,1983],{"class":314},[74,3951,3936],{"class":83},[74,3953,3954],{"class":314},"),\n",[74,3956,3957],{"class":76,"line":424},[74,3958,1747],{"class":314},[74,3960,3961],{"class":76,"line":544},[74,3962,1569],{"emptyLinePlaceholder":52},[74,3964,3965,3967,3970,3972,3975,3977,3980,3982,3985],{"class":76,"line":554},[74,3966,1858],{"class":320},[74,3968,3969],{"class":1895}," Config",[74,3971,2688],{"class":788},[74,3973,3974],{"class":1895}," z",[74,3976,196],{"class":314},[74,3978,3979],{"class":1895},"infer",[74,3981,1899],{"class":314},[74,3983,3984],{"class":788},"typeof",[74,3986,3987],{"class":314}," configSchema>\n",[74,3989,3990],{"class":76,"line":560},[74,3991,1569],{"emptyLinePlaceholder":52},[74,3993,3994,3996,3999,4002,4004,4006,4008,4011],{"class":76,"line":565},[74,3995,3027],{"class":320},[74,3997,3998],{"class":80}," loadConfig",[74,4000,4001],{"class":314},"()",[74,4003,815],{"class":788},[74,4005,3969],{"class":1895},[74,4007,789],{"class":788},[74,4009,4010],{"class":320}," null",[74,4012,2252],{"class":314},[74,4014,4015,4017,4020,4022,4025],{"class":76,"line":577},[74,4016,1969],{"class":320},[74,4018,4019],{"class":314}," raw ",[74,4021,1918],{"class":788},[74,4023,4024],{"class":80}," readUserConfig",[74,4026,4027],{"class":314},"(RC_NAME)\n",[74,4029,4030],{"class":76,"line":589},[74,4031,1569],{"emptyLinePlaceholder":52},[74,4033,4034,4036,4038,4040],{"class":76,"line":601},[74,4035,2074],{"class":788},[74,4037,2913],{"class":314},[74,4039,2968],{"class":788},[74,4041,4042],{"class":314},"raw.home) {\n",[74,4044,4045,4047],{"class":76,"line":613},[74,4046,2595],{"class":788},[74,4048,4049],{"class":327}," null\n",[74,4051,4052],{"class":76,"line":624},[74,4053,1271],{"class":314},[74,4055,4056],{"class":76,"line":634},[74,4057,1569],{"emptyLinePlaceholder":52},[74,4059,4060,4062,4065,4067,4070,4073],{"class":76,"line":640},[74,4061,1969],{"class":320},[74,4063,4064],{"class":314}," result ",[74,4066,1918],{"class":788},[74,4068,4069],{"class":314}," configSchema.",[74,4071,4072],{"class":80},"safeParse",[74,4074,4075],{"class":314},"(raw)\n",[74,4077,4078],{"class":76,"line":646},[74,4079,1569],{"emptyLinePlaceholder":52},[74,4081,4082,4084,4086,4088],{"class":76,"line":1147},[74,4083,2074],{"class":788},[74,4085,2913],{"class":314},[74,4087,2968],{"class":788},[74,4089,4090],{"class":314},"result.success) {\n",[74,4092,4093,4095,4097,4099,4101,4104,4107,4109,4112,4115,4118,4120,4122],{"class":76,"line":1155},[74,4094,2976],{"class":788},[74,4096,2836],{"class":788},[74,4098,2268],{"class":80},[74,4100,1983],{"class":314},[74,4102,4103],{"class":83},"`Configuration validation failed:",[74,4105,4106],{"class":327},"\\n",[74,4108,2988],{"class":788},[74,4110,4111],{"class":314},"z.",[74,4113,4114],{"class":80},"prettifyError",[74,4116,4117],{"class":314},"(result.error)",[74,4119,2993],{"class":788},[74,4121,3364],{"class":83},[74,4123,1989],{"class":314},[74,4125,4126],{"class":76,"line":1168},[74,4127,1271],{"class":314},[74,4129,4130],{"class":76,"line":1181},[74,4131,1569],{"emptyLinePlaceholder":52},[74,4133,4134,4136],{"class":76,"line":1194},[74,4135,3011],{"class":788},[74,4137,4138],{"class":314}," result.data\n",[74,4140,4141],{"class":76,"line":1205},[74,4142,427],{"class":314},[74,4144,4145],{"class":76,"line":1210},[74,4146,1569],{"emptyLinePlaceholder":52},[74,4148,4149,4151,4154,4156,4158,4160,4162],{"class":76,"line":1218},[74,4150,3027],{"class":320},[74,4152,4153],{"class":80}," resolveConfig",[74,4155,1983],{"class":314},[74,4157,1561],{"class":2238},[74,4159,2933],{"class":788},[74,4161,2696],{"class":320},[74,4163,2182],{"class":314},[74,4165,4166,4168,4171,4173,4175],{"class":76,"line":1231},[74,4167,1969],{"class":320},[74,4169,4170],{"class":314}," config ",[74,4172,1918],{"class":788},[74,4174,3998],{"class":80},[74,4176,4177],{"class":314},"()\n",[74,4179,4180,4182,4185,4187,4189,4191],{"class":76,"line":1244},[74,4181,1969],{"class":320},[74,4183,4184],{"class":314}," origin ",[74,4186,1918],{"class":788},[74,4188,3452],{"class":314},[74,4190,2131],{"class":788},[74,4192,4193],{"class":314}," config?.home\n",[74,4195,4196],{"class":76,"line":1257},[74,4197,1569],{"emptyLinePlaceholder":52},[74,4199,4200,4202,4204,4206],{"class":76,"line":1268},[74,4201,2074],{"class":788},[74,4203,2913],{"class":314},[74,4205,2968],{"class":788},[74,4207,4208],{"class":314},"origin) {\n",[74,4210,4211,4213,4215,4217,4219,4222],{"class":76,"line":1274},[74,4212,2976],{"class":788},[74,4214,2836],{"class":788},[74,4216,2268],{"class":80},[74,4218,1983],{"class":314},[74,4220,4221],{"class":83},"'No starting city. Run \"npx @username/howfar init\" or pass --from.'",[74,4223,1989],{"class":314},[74,4225,4226],{"class":76,"line":3286},[74,4227,1271],{"class":314},[74,4229,4230],{"class":76,"line":3291},[74,4231,1569],{"emptyLinePlaceholder":52},[74,4233,4234,4236,4239,4241,4243],{"class":76,"line":3296},[74,4235,3011],{"class":788},[74,4237,4238],{"class":314}," { from: origin, unit: config?.unit ",[74,4240,2131],{"class":788},[74,4242,2737],{"class":83},[74,4244,2721],{"class":314},[74,4246,4247],{"class":76,"line":3331},[74,4248,427],{"class":314},[74,4250,4251],{"class":76,"line":3346},[74,4252,1569],{"emptyLinePlaceholder":52},[74,4254,4255,4257,4260,4262,4265,4267,4269],{"class":76,"line":3351},[74,4256,3027],{"class":320},[74,4258,4259],{"class":80}," saveConfig",[74,4261,1983],{"class":314},[74,4263,4264],{"class":2238},"config",[74,4266,815],{"class":788},[74,4268,3969],{"class":1895},[74,4270,2182],{"class":314},[74,4272,4273,4276],{"class":76,"line":3358},[74,4274,4275],{"class":80},"  writeUserConfig",[74,4277,4278],{"class":314},"(config, RC_NAME)\n",[74,4280,4281],{"class":76,"line":3387},[74,4282,427],{"class":314},[74,4284,4285],{"class":76,"line":3393},[74,4286,1569],{"emptyLinePlaceholder":52},[74,4288,4289,4291],{"class":76,"line":3399},[74,4290,1574],{"class":788},[74,4292,4293],{"class":314}," { loadConfig, resolveConfig, saveConfig }\n",[74,4295,4296],{"class":76,"line":3405},[74,4297,1569],{"emptyLinePlaceholder":52},[74,4299,4300,4302,4304],{"class":76,"line":3411},[74,4301,1574],{"class":788},[74,4303,3629],{"class":788},[74,4305,4306],{"class":314}," { Config }\n",[11,4308,4309],{},"Here's what's happening:",[681,4311,4312,4330,4344,4356],{},[684,4313,4314,4317,4318,4321,4322,4325,4326,4329],{},[15,4315,4316],{},"readUserConfig"," looks for ",[15,4319,4320],{},"howfar.conf"," in ",[15,4323,4324],{},"~/.config"," (or wherever ",[15,4327,4328],{},"$XDG_CONFIG_HOME"," points). We never build that path ourselves, so it's correct on macOS, Linux, and Windows.",[684,4331,4332,4333,4335,4336,4339,4340,4343],{},"The file on disk is just text. zod's ",[15,4334,4072],{}," is where we turn it into a ",[15,4337,4338],{},"Config"," we can trust. If someone edits the file by hand and types ",[15,4341,4342],{},"unit=miles",", the tool stops right there, with an error that names the field and the values it accepts.",[684,4345,4346,4349,4350,4352,4353,4355],{},[15,4347,4348],{},"loadConfig"," returns ",[15,4351,2202],{}," when nothing is configured. The ",[15,4354,714],{}," wizard uses it to prefill the prompts with existing values when you run it again.",[684,4357,4358,4361,4362,4364],{},[15,4359,4360],{},"resolveConfig"," is what commands use. When you pass ",[15,4363,654],{},", that city wins. Without it, the tool measures from your home city. With neither, it throws a helpful error instead of guessing.",[11,4366,4367,4368,815],{},"Here's the error for that hand-edited ",[15,4369,4342],{},[66,4371,4374],{"className":4372,"code":4373,"language":276,"meta":42},[274],"Configuration validation failed:\n✖ Invalid option: expected one of \"km\"|\"mi\"\n  → at unit\n",[15,4375,4373],{"__ignoreMap":42},[11,4377,4378],{},"With the config in place, we can build the wizard:",[66,4380,4383],{"className":1545,"code":4381,"filename":4382,"language":1548,"meta":42,"style":42},"import { defineCommand } from 'citty'\nimport { intro, outro, text, select, isCancel, cancel, log } from '@clack/prompts'\nimport { loadConfig, saveConfig } from '../lib/config.js'\n\nfunction required(value: string | undefined) {\n  return value ? undefined : 'Required'\n}\n\nasync function runInit() {\n  intro('howfar setup')\n\n  const existing = loadConfig()\n\n  const home = await text({\n    message: 'Your city',\n    placeholder: 'Buenos Aires',\n    initialValue: existing?.home ?? '',\n    validate: required,\n  })\n  if (isCancel(home)) {\n    return cancel('Setup cancelled')\n  }\n\n  const unit = await select({\n    message: 'Distance unit',\n    options: [\n      { value: 'km', label: 'Kilometers' },\n      { value: 'mi', label: 'Miles' },\n    ],\n    initialValue: existing?.unit ?? 'km',\n  })\n  if (isCancel(unit)) {\n    return cancel('Setup cancelled')\n  }\n\n  saveConfig({ home, unit })\n\n  log.success('Saved config to ~/.config/howfar.conf')\n  outro('Done. Try: npx @username/howfar Tokyo')\n}\n\nconst init = defineCommand({\n  meta: { name: 'init', description: 'Setup wizard for your city' },\n  run: () => runInit(),\n})\n\nexport { init }\n","commands/init.ts",[15,4384,4385,4395,4407,4419,4423,4445,4462,4466,4470,4481,4493,4497,4510,4514,4530,4540,4550,4562,4567,4572,4584,4598,4602,4606,4622,4631,4636,4651,4664,4669,4680,4684,4695,4707,4711,4715,4723,4727,4742,4754,4758,4762,4775,4789,4802,4806,4810],{"__ignoreMap":42},[74,4386,4387,4389,4391,4393],{"class":76,"line":77},[74,4388,1555],{"class":788},[74,4390,2436],{"class":314},[74,4392,1561],{"class":788},[74,4394,1866],{"class":83},[74,4396,4397,4399,4402,4404],{"class":76,"line":43},[74,4398,1555],{"class":788},[74,4400,4401],{"class":314}," { intro, outro, text, select, isCancel, cancel, log } ",[74,4403,1561],{"class":788},[74,4405,4406],{"class":83}," '@clack/prompts'\n",[74,4408,4409,4411,4414,4416],{"class":76,"line":334},[74,4410,1555],{"class":788},[74,4412,4413],{"class":314}," { loadConfig, saveConfig } ",[74,4415,1561],{"class":788},[74,4417,4418],{"class":83}," '../lib/config.js'\n",[74,4420,4421],{"class":76,"line":348},[74,4422,1569],{"emptyLinePlaceholder":52},[74,4424,4425,4427,4430,4432,4435,4437,4439,4441,4443],{"class":76,"line":361},[74,4426,3027],{"class":320},[74,4428,4429],{"class":80}," required",[74,4431,1983],{"class":314},[74,4433,4434],{"class":2238},"value",[74,4436,815],{"class":788},[74,4438,2696],{"class":320},[74,4440,789],{"class":788},[74,4442,2179],{"class":320},[74,4444,2182],{"class":314},[74,4446,4447,4449,4452,4454,4456,4459],{"class":76,"line":374},[74,4448,3011],{"class":788},[74,4450,4451],{"class":314}," value ",[74,4453,907],{"class":788},[74,4455,2179],{"class":327},[74,4457,4458],{"class":788}," :",[74,4460,4461],{"class":83}," 'Required'\n",[74,4463,4464],{"class":76,"line":387},[74,4465,427],{"class":314},[74,4467,4468],{"class":76,"line":400},[74,4469,1569],{"emptyLinePlaceholder":52},[74,4471,4472,4474,4476,4479],{"class":76,"line":413},[74,4473,1955],{"class":788},[74,4475,1958],{"class":320},[74,4477,4478],{"class":80}," runInit",[74,4480,1964],{"class":314},[74,4482,4483,4486,4488,4491],{"class":76,"line":424},[74,4484,4485],{"class":80},"  intro",[74,4487,1983],{"class":314},[74,4489,4490],{"class":83},"'howfar setup'",[74,4492,1989],{"class":314},[74,4494,4495],{"class":76,"line":544},[74,4496,1569],{"emptyLinePlaceholder":52},[74,4498,4499,4501,4504,4506,4508],{"class":76,"line":554},[74,4500,1969],{"class":320},[74,4502,4503],{"class":314}," existing ",[74,4505,1918],{"class":788},[74,4507,3998],{"class":80},[74,4509,4177],{"class":314},[74,4511,4512],{"class":76,"line":560},[74,4513,1569],{"emptyLinePlaceholder":52},[74,4515,4516,4518,4521,4523,4525,4528],{"class":76,"line":565},[74,4517,1969],{"class":320},[74,4519,4520],{"class":314}," home ",[74,4522,1918],{"class":788},[74,4524,2146],{"class":788},[74,4526,4527],{"class":80}," text",[74,4529,1583],{"class":314},[74,4531,4532,4535,4538],{"class":76,"line":577},[74,4533,4534],{"class":314},"    message: ",[74,4536,4537],{"class":83},"'Your city'",[74,4539,331],{"class":314},[74,4541,4542,4545,4548],{"class":76,"line":589},[74,4543,4544],{"class":314},"    placeholder: ",[74,4546,4547],{"class":83},"'Buenos Aires'",[74,4549,331],{"class":314},[74,4551,4552,4555,4557,4560],{"class":76,"line":601},[74,4553,4554],{"class":314},"    initialValue: existing?.home ",[74,4556,2131],{"class":788},[74,4558,4559],{"class":83}," ''",[74,4561,331],{"class":314},[74,4563,4564],{"class":76,"line":613},[74,4565,4566],{"class":314},"    validate: required,\n",[74,4568,4569],{"class":76,"line":624},[74,4570,4571],{"class":314},"  })\n",[74,4573,4574,4576,4578,4581],{"class":76,"line":634},[74,4575,2074],{"class":788},[74,4577,2913],{"class":314},[74,4579,4580],{"class":80},"isCancel",[74,4582,4583],{"class":314},"(home)) {\n",[74,4585,4586,4588,4591,4593,4596],{"class":76,"line":640},[74,4587,2595],{"class":788},[74,4589,4590],{"class":80}," cancel",[74,4592,1983],{"class":314},[74,4594,4595],{"class":83},"'Setup cancelled'",[74,4597,1989],{"class":314},[74,4599,4600],{"class":76,"line":646},[74,4601,1271],{"class":314},[74,4603,4604],{"class":76,"line":1147},[74,4605,1569],{"emptyLinePlaceholder":52},[74,4607,4608,4610,4613,4615,4617,4620],{"class":76,"line":1155},[74,4609,1969],{"class":320},[74,4611,4612],{"class":314}," unit ",[74,4614,1918],{"class":788},[74,4616,2146],{"class":788},[74,4618,4619],{"class":80}," select",[74,4621,1583],{"class":314},[74,4623,4624,4626,4629],{"class":76,"line":1168},[74,4625,4534],{"class":314},[74,4627,4628],{"class":83},"'Distance unit'",[74,4630,331],{"class":314},[74,4632,4633],{"class":76,"line":1181},[74,4634,4635],{"class":314},"    options: [\n",[74,4637,4638,4641,4643,4646,4649],{"class":76,"line":1194},[74,4639,4640],{"class":314},"      { value: ",[74,4642,3936],{"class":83},[74,4644,4645],{"class":314},", label: ",[74,4647,4648],{"class":83},"'Kilometers'",[74,4650,2486],{"class":314},[74,4652,4653,4655,4657,4659,4662],{"class":76,"line":1205},[74,4654,4640],{"class":314},[74,4656,3941],{"class":83},[74,4658,4645],{"class":314},[74,4660,4661],{"class":83},"'Miles'",[74,4663,2486],{"class":314},[74,4665,4666],{"class":76,"line":1210},[74,4667,4668],{"class":314},"    ],\n",[74,4670,4671,4674,4676,4678],{"class":76,"line":1218},[74,4672,4673],{"class":314},"    initialValue: existing?.unit ",[74,4675,2131],{"class":788},[74,4677,2737],{"class":83},[74,4679,331],{"class":314},[74,4681,4682],{"class":76,"line":1231},[74,4683,4571],{"class":314},[74,4685,4686,4688,4690,4692],{"class":76,"line":1244},[74,4687,2074],{"class":788},[74,4689,2913],{"class":314},[74,4691,4580],{"class":80},[74,4693,4694],{"class":314},"(unit)) {\n",[74,4696,4697,4699,4701,4703,4705],{"class":76,"line":1257},[74,4698,2595],{"class":788},[74,4700,4590],{"class":80},[74,4702,1983],{"class":314},[74,4704,4595],{"class":83},[74,4706,1989],{"class":314},[74,4708,4709],{"class":76,"line":1268},[74,4710,1271],{"class":314},[74,4712,4713],{"class":76,"line":1274},[74,4714,1569],{"emptyLinePlaceholder":52},[74,4716,4717,4720],{"class":76,"line":3286},[74,4718,4719],{"class":80},"  saveConfig",[74,4721,4722],{"class":314},"({ home, unit })\n",[74,4724,4725],{"class":76,"line":3291},[74,4726,1569],{"emptyLinePlaceholder":52},[74,4728,4729,4732,4735,4737,4740],{"class":76,"line":3296},[74,4730,4731],{"class":314},"  log.",[74,4733,4734],{"class":80},"success",[74,4736,1983],{"class":314},[74,4738,4739],{"class":83},"'Saved config to ~/.config/howfar.conf'",[74,4741,1989],{"class":314},[74,4743,4744,4747,4749,4752],{"class":76,"line":3331},[74,4745,4746],{"class":80},"  outro",[74,4748,1983],{"class":314},[74,4750,4751],{"class":83},"'Done. Try: npx @username/howfar Tokyo'",[74,4753,1989],{"class":314},[74,4755,4756],{"class":76,"line":3346},[74,4757,427],{"class":314},[74,4759,4760],{"class":76,"line":3351},[74,4761,1569],{"emptyLinePlaceholder":52},[74,4763,4764,4766,4769,4771,4773],{"class":76,"line":3358},[74,4765,1887],{"class":320},[74,4767,4768],{"class":314}," init ",[74,4770,1918],{"class":788},[74,4772,1937],{"class":80},[74,4774,1583],{"class":314},[74,4776,4777,4779,4782,4784,4787],{"class":76,"line":3387},[74,4778,2474],{"class":314},[74,4780,4781],{"class":83},"'init'",[74,4783,2480],{"class":314},[74,4785,4786],{"class":83},"'Setup wizard for your city'",[74,4788,2486],{"class":314},[74,4790,4791,4793,4796,4798,4800],{"class":76,"line":3393},[74,4792,2573],{"class":80},[74,4794,4795],{"class":314},": () ",[74,4797,2249],{"class":320},[74,4799,4478],{"class":80},[74,4801,3923],{"class":314},[74,4803,4804],{"class":76,"line":3399},[74,4805,1747],{"class":314},[74,4807,4808],{"class":76,"line":3405},[74,4809,1569],{"emptyLinePlaceholder":52},[74,4811,4812,4814],{"class":76,"line":3411},[74,4813,1574],{"class":788},[74,4815,4816],{"class":314}," { init }\n",[11,4818,4819,4820,4822,4823,4825,4826,4829],{},"There are two details worth pointing out here. First, every prompt is followed by an ",[15,4821,4580],{}," check, because the user can press Ctrl+C at any point and clack returns a cancel symbol instead of a value. Without that check, you'd save a cancel symbol as someone's city. Second, ",[15,4824,714],{}," returns nothing, so the entry point prints nothing. The wizard draws its own UI, so a JSON blob after the ",[15,4827,4828],{},"outro"," line would look broken.",[11,4831,4832,4833,4835,4836,4838,4839,4841],{},"Next, let's update ",[15,4834,2330],{}," to use the config. ",[15,4837,654],{}," is no longer required, and ",[15,4840,4360],{}," decides where to measure from:",[66,4843,4845],{"className":1545,"code":4844,"filename":2427,"language":1548,"meta":42,"style":42},"import { defineCommand } from 'citty'\nimport { resolveConfig } from '../lib/config.js'\nimport { fetchDistance } from '../lib/distance.js'\n\nconst lookup = defineCommand({\n  meta: { name: 'howfar', description: 'Distance from your city to another, as JSON' },\n  args: {\n    city: {\n      type: 'positional',\n      required: true,\n      description: 'City to measure the distance to',\n    },\n    from: {\n      type: 'string',\n      required: false,\n      description: 'City to measure the distance from (defaults to yours)',\n    },\n  },\n  run: async ({ args }) => {\n    const { from, unit } = resolveConfig(args.from)\n\n    return fetchDistance(args.city, from, unit)\n  },\n})\n\nexport { lookup }\n",[15,4846,4847,4857,4868,4878,4882,4894,4907,4911,4915,4923,4931,4939,4943,4947,4955,4963,4972,4976,4980,4998,5013,5017,5026,5030,5034,5038],{"__ignoreMap":42},[74,4848,4849,4851,4853,4855],{"class":76,"line":77},[74,4850,1555],{"class":788},[74,4852,2436],{"class":314},[74,4854,1561],{"class":788},[74,4856,1866],{"class":83},[74,4858,4859,4861,4864,4866],{"class":76,"line":43},[74,4860,1555],{"class":788},[74,4862,4863],{"class":314}," { resolveConfig } ",[74,4865,1561],{"class":788},[74,4867,4418],{"class":83},[74,4869,4870,4872,4874,4876],{"class":76,"line":334},[74,4871,1555],{"class":788},[74,4873,2447],{"class":314},[74,4875,1561],{"class":788},[74,4877,2452],{"class":83},[74,4879,4880],{"class":76,"line":348},[74,4881,1569],{"emptyLinePlaceholder":52},[74,4883,4884,4886,4888,4890,4892],{"class":76,"line":361},[74,4885,1887],{"class":320},[74,4887,2463],{"class":314},[74,4889,1918],{"class":788},[74,4891,1937],{"class":80},[74,4893,1583],{"class":314},[74,4895,4896,4898,4900,4902,4905],{"class":76,"line":374},[74,4897,2474],{"class":314},[74,4899,2477],{"class":83},[74,4901,2480],{"class":314},[74,4903,4904],{"class":83},"'Distance from your city to another, as JSON'",[74,4906,2486],{"class":314},[74,4908,4909],{"class":76,"line":387},[74,4910,2491],{"class":314},[74,4912,4913],{"class":76,"line":400},[74,4914,2496],{"class":314},[74,4916,4917,4919,4921],{"class":76,"line":413},[74,4918,2501],{"class":314},[74,4920,2504],{"class":83},[74,4922,331],{"class":314},[74,4924,4925,4927,4929],{"class":76,"line":424},[74,4926,2511],{"class":314},[74,4928,328],{"class":327},[74,4930,331],{"class":314},[74,4932,4933,4935,4937],{"class":76,"line":544},[74,4934,2520],{"class":314},[74,4936,2523],{"class":83},[74,4938,331],{"class":314},[74,4940,4941],{"class":76,"line":554},[74,4942,557],{"class":314},[74,4944,4945],{"class":76,"line":560},[74,4946,2534],{"class":314},[74,4948,4949,4951,4953],{"class":76,"line":565},[74,4950,2501],{"class":314},[74,4952,2541],{"class":83},[74,4954,331],{"class":314},[74,4956,4957,4959,4961],{"class":76,"line":577},[74,4958,2511],{"class":314},[74,4960,1596],{"class":327},[74,4962,331],{"class":314},[74,4964,4965,4967,4970],{"class":76,"line":589},[74,4966,2520],{"class":314},[74,4968,4969],{"class":83},"'City to measure the distance from (defaults to yours)'",[74,4971,331],{"class":314},[74,4973,4974],{"class":76,"line":601},[74,4975,557],{"class":314},[74,4977,4978],{"class":76,"line":613},[74,4979,1016],{"class":314},[74,4981,4982,4984,4986,4988,4990,4992,4994,4996],{"class":76,"line":624},[74,4983,2573],{"class":80},[74,4985,324],{"class":314},[74,4987,1955],{"class":788},[74,4989,2580],{"class":314},[74,4991,2583],{"class":2238},[74,4993,2586],{"class":314},[74,4995,2249],{"class":320},[74,4997,2252],{"class":314},[74,4999,5000,5003,5006,5008,5010],{"class":76,"line":634},[74,5001,5002],{"class":320},"    const",[74,5004,5005],{"class":314}," { from, unit } ",[74,5007,1918],{"class":788},[74,5009,4153],{"class":80},[74,5011,5012],{"class":314},"(args.from)\n",[74,5014,5015],{"class":76,"line":640},[74,5016,1569],{"emptyLinePlaceholder":52},[74,5018,5019,5021,5023],{"class":76,"line":646},[74,5020,2595],{"class":788},[74,5022,2598],{"class":80},[74,5024,5025],{"class":314},"(args.city, from, unit)\n",[74,5027,5028],{"class":76,"line":1147},[74,5029,1016],{"class":314},[74,5031,5032],{"class":76,"line":1155},[74,5033,1747],{"class":314},[74,5035,5036],{"class":76,"line":1168},[74,5037,1569],{"emptyLinePlaceholder":52},[74,5039,5040,5042],{"class":76,"line":1181},[74,5041,1574],{"class":788},[74,5043,2620],{"class":314},[11,5045,5046,5047,5049,5050,5052,5053],{},"Notice that ",[15,5048,2676],{}," didn't change at all. ",[15,5051,3649],{}," already took a starting city and a unit, so the config only had to decide which values to pass. ",[92,5054,5055],{},"That's the payoff of thin commands.",[11,5057,5058,5059,5061],{},"Then, let's register ",[15,5060,714],{}," in the entry point:",[66,5063,5065],{"className":1545,"code":5064,"filename":1757,"language":1548,"meta":42,"style":42},"import { init } from '../commands/init.js'\nimport { lookup } from '../commands/lookup.js'\n\nconst subCommands: Record\u003Cstring, CommandDef\u003Cany>> = {\n  init,\n}\n",[15,5066,5067,5079,5089,5093,5121,5126],{"__ignoreMap":42},[74,5068,5069,5071,5074,5076],{"class":76,"line":77},[74,5070,1555],{"class":788},[74,5072,5073],{"class":314}," { init } ",[74,5075,1561],{"class":788},[74,5077,5078],{"class":83}," '../commands/init.js'\n",[74,5080,5081,5083,5085,5087],{"class":76,"line":43},[74,5082,1555],{"class":788},[74,5084,1873],{"class":314},[74,5086,1561],{"class":788},[74,5088,1878],{"class":83},[74,5090,5091],{"class":76,"line":334},[74,5092,1569],{"emptyLinePlaceholder":52},[74,5094,5095,5097,5099,5101,5103,5105,5107,5109,5111,5113,5115,5117,5119],{"class":76,"line":348},[74,5096,1887],{"class":320},[74,5098,1890],{"class":314},[74,5100,815],{"class":788},[74,5102,1896],{"class":1895},[74,5104,1899],{"class":314},[74,5106,1902],{"class":320},[74,5108,138],{"class":314},[74,5110,1907],{"class":1895},[74,5112,1899],{"class":314},[74,5114,1912],{"class":320},[74,5116,1915],{"class":314},[74,5118,1918],{"class":788},[74,5120,2252],{"class":314},[74,5122,5123],{"class":76,"line":361},[74,5124,5125],{"class":314},"  init,\n",[74,5127,5128],{"class":76,"line":374},[74,5129,427],{"class":314},[11,5131,5132,5133,815],{},"That's all it takes. Let's build, run the wizard once, and measure Tokyo again, this time without ",[15,5134,654],{},[66,5136,5138],{"className":68,"code":5137,"language":70,"meta":42,"style":42},"npm run build\nnode dist/howfar.mjs init\nnode dist/howfar.mjs Tokyo\n",[15,5139,5140,5148,5156],{"__ignoreMap":42},[74,5141,5142,5144,5146],{"class":76,"line":77},[74,5143,98],{"class":80},[74,5145,1961],{"class":83},[74,5147,3686],{"class":83},[74,5149,5150,5152,5154],{"class":76,"line":43},[74,5151,2313],{"class":80},[74,5153,3693],{"class":83},[74,5155,267],{"class":83},[74,5157,5158,5160,5162],{"class":76,"line":334},[74,5159,2313],{"class":80},[74,5161,3693],{"class":83},[74,5163,87],{"class":83},[66,5165,5166],{"className":305,"code":306,"language":307,"meta":42,"style":42},[15,5167,5168,5172,5182,5192,5202,5212,5222,5232,5242,5250],{"__ignoreMap":42},[74,5169,5170],{"class":76,"line":77},[74,5171,315],{"class":314},[74,5173,5174,5176,5178,5180],{"class":76,"line":43},[74,5175,321],{"class":320},[74,5177,324],{"class":314},[74,5179,328],{"class":327},[74,5181,331],{"class":314},[74,5183,5184,5186,5188,5190],{"class":76,"line":334},[74,5185,337],{"class":320},[74,5187,324],{"class":314},[74,5189,343],{"class":342},[74,5191,331],{"class":314},[74,5193,5194,5196,5198,5200],{"class":76,"line":348},[74,5195,351],{"class":320},[74,5197,324],{"class":314},[74,5199,356],{"class":342},[74,5201,331],{"class":314},[74,5203,5204,5206,5208,5210],{"class":76,"line":361},[74,5205,364],{"class":320},[74,5207,324],{"class":314},[74,5209,369],{"class":327},[74,5211,331],{"class":314},[74,5213,5214,5216,5218,5220],{"class":76,"line":374},[74,5215,377],{"class":320},[74,5217,324],{"class":314},[74,5219,382],{"class":327},[74,5221,331],{"class":314},[74,5223,5224,5226,5228,5230],{"class":76,"line":387},[74,5225,390],{"class":320},[74,5227,324],{"class":314},[74,5229,395],{"class":327},[74,5231,331],{"class":314},[74,5233,5234,5236,5238,5240],{"class":76,"line":400},[74,5235,403],{"class":320},[74,5237,324],{"class":314},[74,5239,408],{"class":342},[74,5241,331],{"class":314},[74,5243,5244,5246,5248],{"class":76,"line":413},[74,5245,416],{"class":320},[74,5247,324],{"class":314},[74,5249,421],{"class":342},[74,5251,5252],{"class":76,"line":424},[74,5253,427],{"class":314},[11,5255,5256,5257,5259],{},"We get the same result, and from now on, ",[15,5258,654],{}," is only for the times we want to measure from somewhere else.",[119,5261,5263],{"id":5262},"multiple-cities-and-validation","Multiple Cities and Validation",[11,5265,5266,5267,5270],{},"Alright, now we're going to add the second feature: several cities at once, each with its distance from yours. This is where beginners usually pile the parsing into ",[15,5268,5269],{},"run()",", next to everything else, and a month later, nobody wants to touch it. The fix is a tiny module for parsing arguments:",[66,5272,5275],{"className":1545,"code":5273,"filename":5274,"language":1548,"meta":42,"style":42},"/** Split a comma-separated list into one or more city names. */\nfunction parseCities(value: string): [string, ...string[]] {\n  const [first, ...rest] = value.split(',').map((city) => city.trim())\n\n  if (!first || rest.some((city) => city === '')) {\n    throw new Error(`Expected a comma-separated list of cities, got \"${value}\"`)\n  }\n\n  return [first, ...rest]\n}\n\nexport { parseCities }\n","lib/cli.ts",[15,5276,5277,5282,5315,5362,5366,5404,5427,5431,5435,5446,5450,5454],{"__ignoreMap":42},[74,5278,5279],{"class":76,"line":77},[74,5280,5281],{"class":1843},"/** Split a comma-separated list into one or more city names. */\n",[74,5283,5284,5286,5289,5291,5293,5295,5297,5299,5301,5304,5306,5308,5310,5312],{"class":76,"line":43},[74,5285,3027],{"class":320},[74,5287,5288],{"class":80}," parseCities",[74,5290,1983],{"class":314},[74,5292,4434],{"class":2238},[74,5294,815],{"class":788},[74,5296,2696],{"class":320},[74,5298,2811],{"class":314},[74,5300,815],{"class":788},[74,5302,5303],{"class":314}," [",[74,5305,1902],{"class":320},[74,5307,138],{"class":314},[74,5309,1943],{"class":788},[74,5311,1902],{"class":320},[74,5313,5314],{"class":314},"[]] {\n",[74,5316,5317,5319,5322,5324,5326,5328,5331,5334,5336,5339,5342,5345,5347,5349,5351,5353,5356,5359],{"class":76,"line":334},[74,5318,1969],{"class":320},[74,5320,5321],{"class":314}," [first, ",[74,5323,1943],{"class":788},[74,5325,2001],{"class":314},[74,5327,1918],{"class":788},[74,5329,5330],{"class":314}," value.",[74,5332,5333],{"class":80},"split",[74,5335,1983],{"class":314},[74,5337,5338],{"class":83},"','",[74,5340,5341],{"class":314},").",[74,5343,5344],{"class":80},"map",[74,5346,2235],{"class":314},[74,5348,2804],{"class":2238},[74,5350,2051],{"class":314},[74,5352,2249],{"class":320},[74,5354,5355],{"class":314}," city.",[74,5357,5358],{"class":80},"trim",[74,5360,5361],{"class":314},"())\n",[74,5363,5364],{"class":76,"line":348},[74,5365,1569],{"emptyLinePlaceholder":52},[74,5367,5368,5370,5372,5374,5377,5379,5382,5385,5387,5389,5391,5393,5396,5399,5401],{"class":76,"line":361},[74,5369,2074],{"class":788},[74,5371,2913],{"class":314},[74,5373,2968],{"class":788},[74,5375,5376],{"class":314},"first ",[74,5378,2054],{"class":788},[74,5380,5381],{"class":314}," rest.",[74,5383,5384],{"class":80},"some",[74,5386,2235],{"class":314},[74,5388,2804],{"class":2238},[74,5390,2051],{"class":314},[74,5392,2249],{"class":320},[74,5394,5395],{"class":314}," city ",[74,5397,5398],{"class":788},"===",[74,5400,4559],{"class":83},[74,5402,5403],{"class":314},")) {\n",[74,5405,5406,5408,5410,5412,5414,5417,5419,5421,5423,5425],{"class":76,"line":374},[74,5407,2976],{"class":788},[74,5409,2836],{"class":788},[74,5411,2268],{"class":80},[74,5413,1983],{"class":314},[74,5415,5416],{"class":83},"`Expected a comma-separated list of cities, got \"",[74,5418,2988],{"class":788},[74,5420,4434],{"class":314},[74,5422,2993],{"class":788},[74,5424,2996],{"class":83},[74,5426,1989],{"class":314},[74,5428,5429],{"class":76,"line":387},[74,5430,1271],{"class":314},[74,5432,5433],{"class":76,"line":400},[74,5434,1569],{"emptyLinePlaceholder":52},[74,5436,5437,5439,5441,5443],{"class":76,"line":413},[74,5438,3011],{"class":788},[74,5440,5321],{"class":314},[74,5442,1943],{"class":788},[74,5444,5445],{"class":314},"rest]\n",[74,5447,5448],{"class":76,"line":424},[74,5449,427],{"class":314},[74,5451,5452],{"class":76,"line":544},[74,5453,1569],{"emptyLinePlaceholder":52},[74,5455,5456,5458],{"class":76,"line":554},[74,5457,1574],{"class":788},[74,5459,5460],{"class":314}," { parseCities }\n",[11,5462,5463,5464,5467,5468,5471,5472,5474,5475,5477],{},"The return type, ",[15,5465,5466],{},"[string, ...string[]]",", says there's always at least one city. That's what lets the command read ",[15,5469,5470],{},"cities[0]"," as a ",[15,5473,1902],{}," without ",[15,5476,1515],{}," complaining.",[205,5479,5480],{},[11,5481,5482],{},"Why throw on an empty city instead of skipping it?",[11,5484,5485,5486,5489],{},"Because ",[15,5487,5488],{},"Tokyo,,Lima"," is almost always a typo. Silently dropping the empty entry hides the mistake, while a clear error tells the user (or the agent) exactly what went wrong. Parsing the raw string myself costs a few lines and gives me full control over that message.",[11,5491,5492,5493,5496,5497,5499,5500,815],{},"Next, let's add a ",[15,5494,5495],{},"fetchDistances"," function to ",[15,5498,2676],{},", right below ",[15,5501,3649],{},[66,5503,5505],{"className":1545,"code":5504,"filename":2676,"language":1548,"meta":42,"style":42},"async function fetchDistances(cities: string[], from: string, unit: Unit = 'km') {\n  const [places, origin] = await Promise.all([Promise.all(cities.map(geocode)), geocode(from)])\n\n  return {\n    ok: true,\n    from: `${origin.name}, ${origin.country}`,\n    cities: places.map((place) => describe(place, origin, unit)),\n  }\n}\n\nexport { geocode, distanceBetween, fetchDistance, fetchDistances }\n",[15,5506,5507,5548,5586,5590,5596,5605,5630,5650,5654,5658,5662],{"__ignoreMap":42},[74,5508,5509,5511,5513,5516,5518,5521,5523,5525,5528,5530,5532,5534,5536,5538,5540,5542,5544,5546],{"class":76,"line":77},[74,5510,1955],{"class":788},[74,5512,1958],{"class":320},[74,5514,5515],{"class":80}," fetchDistances",[74,5517,1983],{"class":314},[74,5519,5520],{"class":2238},"cities",[74,5522,815],{"class":788},[74,5524,2696],{"class":320},[74,5526,5527],{"class":314},"[], ",[74,5529,1561],{"class":2238},[74,5531,815],{"class":788},[74,5533,2696],{"class":320},[74,5535,138],{"class":314},[74,5537,3108],{"class":2238},[74,5539,815],{"class":788},[74,5541,2732],{"class":1895},[74,5543,2688],{"class":788},[74,5545,2737],{"class":83},[74,5547,2182],{"class":314},[74,5549,5550,5552,5555,5557,5559,5561,5563,5565,5567,5570,5572,5574,5577,5579,5582,5584],{"class":76,"line":43},[74,5551,1969],{"class":320},[74,5553,5554],{"class":314}," [places, origin] ",[74,5556,1918],{"class":788},[74,5558,2146],{"class":788},[74,5560,2816],{"class":320},[74,5562,196],{"class":314},[74,5564,3536],{"class":80},[74,5566,3539],{"class":314},[74,5568,5569],{"class":320},"Promise",[74,5571,196],{"class":314},[74,5573,3536],{"class":80},[74,5575,5576],{"class":314},"(cities.",[74,5578,5344],{"class":80},[74,5580,5581],{"class":314},"(geocode)), ",[74,5583,3542],{"class":80},[74,5585,3550],{"class":314},[74,5587,5588],{"class":76,"line":334},[74,5589,1569],{"emptyLinePlaceholder":52},[74,5591,5592,5594],{"class":76,"line":348},[74,5593,3011],{"class":788},[74,5595,2252],{"class":314},[74,5597,5598,5601,5603],{"class":76,"line":361},[74,5599,5600],{"class":314},"    ok: ",[74,5602,328],{"class":327},[74,5604,331],{"class":314},[74,5606,5607,5610,5612,5614,5616,5618,5620,5622,5624,5626,5628],{"class":76,"line":374},[74,5608,5609],{"class":314},"    from: ",[74,5611,3364],{"class":83},[74,5613,2988],{"class":788},[74,5615,3575],{"class":314},[74,5617,2993],{"class":788},[74,5619,138],{"class":83},[74,5621,2988],{"class":788},[74,5623,3584],{"class":314},[74,5625,2993],{"class":788},[74,5627,3364],{"class":83},[74,5629,331],{"class":314},[74,5631,5632,5635,5637,5639,5641,5643,5645,5647],{"class":76,"line":387},[74,5633,5634],{"class":314},"    cities: places.",[74,5636,5344],{"class":80},[74,5638,2235],{"class":314},[74,5640,3306],{"class":2238},[74,5642,2051],{"class":314},[74,5644,2249],{"class":320},[74,5646,3301],{"class":80},[74,5648,5649],{"class":314},"(place, origin, unit)),\n",[74,5651,5652],{"class":76,"line":400},[74,5653,1271],{"class":314},[74,5655,5656],{"class":76,"line":413},[74,5657,427],{"class":314},[74,5659,5660],{"class":76,"line":424},[74,5661,1569],{"emptyLinePlaceholder":52},[74,5663,5664,5666],{"class":76,"line":544},[74,5665,1574],{"class":788},[74,5667,5668],{"class":314}," { geocode, distanceBetween, fetchDistance, fetchDistances }\n",[11,5670,5671],{},"The starting city and the whole list are geocoded in parallel, so measuring five cities takes about as long as measuring one. The starting city is also geocoded only once, no matter how many cities we measure.",[11,5673,5674],{},"And then the command itself, which is now almost entirely wiring:",[66,5676,5678],{"className":1545,"code":5677,"filename":2427,"language":1548,"meta":42,"style":42},"import { defineCommand } from 'citty'\nimport { resolveConfig } from '../lib/config.js'\nimport { parseCities } from '../lib/cli.js'\nimport { fetchDistance, fetchDistances } from '../lib/distance.js'\n\nconst lookup = defineCommand({\n  meta: { name: 'howfar', description: 'Distance from your city to one or more cities, as JSON' },\n  args: {\n    city: {\n      type: 'positional',\n      required: true,\n      description: 'City, or several separated by commas',\n    },\n    from: {\n      type: 'string',\n      required: false,\n      description: 'City to measure the distance from (defaults to yours)',\n    },\n  },\n  run: async ({ args }) => {\n    const { from, unit } = resolveConfig(args.from)\n    const cities = parseCities(args.city)\n\n    return cities.length === 1\n      ? fetchDistance(cities[0], from, unit)\n      : fetchDistances(cities, from, unit)\n  },\n})\n\nexport { lookup }\n",[15,5679,5680,5690,5700,5712,5723,5727,5739,5752,5756,5760,5768,5776,5785,5789,5793,5801,5809,5817,5821,5825,5843,5855,5869,5873,5885,5900,5910,5914,5918,5922],{"__ignoreMap":42},[74,5681,5682,5684,5686,5688],{"class":76,"line":77},[74,5683,1555],{"class":788},[74,5685,2436],{"class":314},[74,5687,1561],{"class":788},[74,5689,1866],{"class":83},[74,5691,5692,5694,5696,5698],{"class":76,"line":43},[74,5693,1555],{"class":788},[74,5695,4863],{"class":314},[74,5697,1561],{"class":788},[74,5699,4418],{"class":83},[74,5701,5702,5704,5707,5709],{"class":76,"line":334},[74,5703,1555],{"class":788},[74,5705,5706],{"class":314}," { parseCities } ",[74,5708,1561],{"class":788},[74,5710,5711],{"class":83}," '../lib/cli.js'\n",[74,5713,5714,5716,5719,5721],{"class":76,"line":348},[74,5715,1555],{"class":788},[74,5717,5718],{"class":314}," { fetchDistance, fetchDistances } ",[74,5720,1561],{"class":788},[74,5722,2452],{"class":83},[74,5724,5725],{"class":76,"line":361},[74,5726,1569],{"emptyLinePlaceholder":52},[74,5728,5729,5731,5733,5735,5737],{"class":76,"line":374},[74,5730,1887],{"class":320},[74,5732,2463],{"class":314},[74,5734,1918],{"class":788},[74,5736,1937],{"class":80},[74,5738,1583],{"class":314},[74,5740,5741,5743,5745,5747,5750],{"class":76,"line":387},[74,5742,2474],{"class":314},[74,5744,2477],{"class":83},[74,5746,2480],{"class":314},[74,5748,5749],{"class":83},"'Distance from your city to one or more cities, as JSON'",[74,5751,2486],{"class":314},[74,5753,5754],{"class":76,"line":400},[74,5755,2491],{"class":314},[74,5757,5758],{"class":76,"line":413},[74,5759,2496],{"class":314},[74,5761,5762,5764,5766],{"class":76,"line":424},[74,5763,2501],{"class":314},[74,5765,2504],{"class":83},[74,5767,331],{"class":314},[74,5769,5770,5772,5774],{"class":76,"line":544},[74,5771,2511],{"class":314},[74,5773,328],{"class":327},[74,5775,331],{"class":314},[74,5777,5778,5780,5783],{"class":76,"line":554},[74,5779,2520],{"class":314},[74,5781,5782],{"class":83},"'City, or several separated by commas'",[74,5784,331],{"class":314},[74,5786,5787],{"class":76,"line":560},[74,5788,557],{"class":314},[74,5790,5791],{"class":76,"line":565},[74,5792,2534],{"class":314},[74,5794,5795,5797,5799],{"class":76,"line":577},[74,5796,2501],{"class":314},[74,5798,2541],{"class":83},[74,5800,331],{"class":314},[74,5802,5803,5805,5807],{"class":76,"line":589},[74,5804,2511],{"class":314},[74,5806,1596],{"class":327},[74,5808,331],{"class":314},[74,5810,5811,5813,5815],{"class":76,"line":601},[74,5812,2520],{"class":314},[74,5814,4969],{"class":83},[74,5816,331],{"class":314},[74,5818,5819],{"class":76,"line":613},[74,5820,557],{"class":314},[74,5822,5823],{"class":76,"line":624},[74,5824,1016],{"class":314},[74,5826,5827,5829,5831,5833,5835,5837,5839,5841],{"class":76,"line":634},[74,5828,2573],{"class":80},[74,5830,324],{"class":314},[74,5832,1955],{"class":788},[74,5834,2580],{"class":314},[74,5836,2583],{"class":2238},[74,5838,2586],{"class":314},[74,5840,2249],{"class":320},[74,5842,2252],{"class":314},[74,5844,5845,5847,5849,5851,5853],{"class":76,"line":640},[74,5846,5002],{"class":320},[74,5848,5005],{"class":314},[74,5850,1918],{"class":788},[74,5852,4153],{"class":80},[74,5854,5012],{"class":314},[74,5856,5857,5859,5862,5864,5866],{"class":76,"line":646},[74,5858,5002],{"class":320},[74,5860,5861],{"class":314}," cities ",[74,5863,1918],{"class":788},[74,5865,5288],{"class":80},[74,5867,5868],{"class":314},"(args.city)\n",[74,5870,5871],{"class":76,"line":1147},[74,5872,1569],{"emptyLinePlaceholder":52},[74,5874,5875,5877,5880,5882],{"class":76,"line":1155},[74,5876,2595],{"class":788},[74,5878,5879],{"class":314}," cities.length ",[74,5881,5398],{"class":788},[74,5883,5884],{"class":327}," 1\n",[74,5886,5887,5890,5892,5895,5897],{"class":76,"line":1168},[74,5888,5889],{"class":788},"      ?",[74,5891,2598],{"class":80},[74,5893,5894],{"class":314},"(cities[",[74,5896,2953],{"class":327},[74,5898,5899],{"class":314},"], from, unit)\n",[74,5901,5902,5905,5907],{"class":76,"line":1181},[74,5903,5904],{"class":788},"      :",[74,5906,5515],{"class":80},[74,5908,5909],{"class":314},"(cities, from, unit)\n",[74,5911,5912],{"class":76,"line":1194},[74,5913,1016],{"class":314},[74,5915,5916],{"class":76,"line":1205},[74,5917,1747],{"class":314},[74,5919,5920],{"class":76,"line":1210},[74,5921,1569],{"emptyLinePlaceholder":52},[74,5923,5924,5926],{"class":76,"line":1218},[74,5925,1574],{"class":788},[74,5927,2620],{"class":314},[11,5929,5930],{},"Let's rebuild and try it:",[66,5932,5934],{"className":68,"code":5933,"language":70,"meta":42,"style":42},"node dist/howfar.mjs \"Tokyo, San Francisco\"\n",[15,5935,5936],{"__ignoreMap":42},[74,5937,5938,5940,5942],{"class":76,"line":77},[74,5939,2313],{"class":80},[74,5941,3693],{"class":83},[74,5943,444],{"class":83},[66,5945,5946],{"className":305,"code":447,"language":307,"meta":42,"style":42},[15,5947,5948,5952,5962,5972,5978,5982,5992,6002,6012,6022,6032,6040,6044,6048,6058,6068,6078,6088,6098,6106,6110,6114],{"__ignoreMap":42},[74,5949,5950],{"class":76,"line":77},[74,5951,315],{"class":314},[74,5953,5954,5956,5958,5960],{"class":76,"line":43},[74,5955,321],{"class":320},[74,5957,324],{"class":314},[74,5959,328],{"class":327},[74,5961,331],{"class":314},[74,5963,5964,5966,5968,5970],{"class":76,"line":334},[74,5965,337],{"class":320},[74,5967,324],{"class":314},[74,5969,343],{"class":342},[74,5971,331],{"class":314},[74,5973,5974,5976],{"class":76,"line":348},[74,5975,478],{"class":320},[74,5977,481],{"class":314},[74,5979,5980],{"class":76,"line":361},[74,5981,486],{"class":314},[74,5983,5984,5986,5988,5990],{"class":76,"line":374},[74,5985,491],{"class":320},[74,5987,324],{"class":314},[74,5989,356],{"class":342},[74,5991,331],{"class":314},[74,5993,5994,5996,5998,6000],{"class":76,"line":387},[74,5995,502],{"class":320},[74,5997,324],{"class":314},[74,5999,369],{"class":327},[74,6001,331],{"class":314},[74,6003,6004,6006,6008,6010],{"class":76,"line":400},[74,6005,513],{"class":320},[74,6007,324],{"class":314},[74,6009,382],{"class":327},[74,6011,331],{"class":314},[74,6013,6014,6016,6018,6020],{"class":76,"line":413},[74,6015,524],{"class":320},[74,6017,324],{"class":314},[74,6019,395],{"class":327},[74,6021,331],{"class":314},[74,6023,6024,6026,6028,6030],{"class":76,"line":424},[74,6025,535],{"class":320},[74,6027,324],{"class":314},[74,6029,408],{"class":342},[74,6031,331],{"class":314},[74,6033,6034,6036,6038],{"class":76,"line":544},[74,6035,547],{"class":320},[74,6037,324],{"class":314},[74,6039,421],{"class":342},[74,6041,6042],{"class":76,"line":554},[74,6043,557],{"class":314},[74,6045,6046],{"class":76,"line":560},[74,6047,486],{"class":314},[74,6049,6050,6052,6054,6056],{"class":76,"line":565},[74,6051,491],{"class":320},[74,6053,324],{"class":314},[74,6055,572],{"class":342},[74,6057,331],{"class":314},[74,6059,6060,6062,6064,6066],{"class":76,"line":577},[74,6061,502],{"class":320},[74,6063,324],{"class":314},[74,6065,584],{"class":327},[74,6067,331],{"class":314},[74,6069,6070,6072,6074,6076],{"class":76,"line":589},[74,6071,513],{"class":320},[74,6073,324],{"class":314},[74,6075,596],{"class":327},[74,6077,331],{"class":314},[74,6079,6080,6082,6084,6086],{"class":76,"line":601},[74,6081,524],{"class":320},[74,6083,324],{"class":314},[74,6085,608],{"class":327},[74,6087,331],{"class":314},[74,6089,6090,6092,6094,6096],{"class":76,"line":613},[74,6091,535],{"class":320},[74,6093,324],{"class":314},[74,6095,408],{"class":342},[74,6097,331],{"class":314},[74,6099,6100,6102,6104],{"class":76,"line":624},[74,6101,547],{"class":320},[74,6103,324],{"class":314},[74,6105,631],{"class":342},[74,6107,6108],{"class":76,"line":634},[74,6109,637],{"class":314},[74,6111,6112],{"class":76,"line":640},[74,6113,643],{"class":314},[74,6115,6116],{"class":76,"line":646},[74,6117,427],{"class":314},[11,6119,6120],{},"That's a second feature, and it took five minutes because the structure was already there.",[119,6122,6124],{"id":6123},"adding-tests","Adding Tests",[11,6126,6127,6128,6130],{},"Now let's make sure this keeps working. Because the logic lives in ",[15,6129,1815],{}," and the commands are thin, the tests are short. Vite+ ships Vitest, so there's nothing to install.",[11,6132,6133],{},"Let's start with the pure functions. First, the city parser:",[66,6135,6138],{"className":1545,"code":6136,"filename":6137,"language":1548,"meta":42,"style":42},"import { describe, expect, it } from 'vitest'\nimport { parseCities } from '../lib/cli.js'\n\ndescribe('parseCities', () => {\n  it('returns a single city', () => {\n    expect(parseCities('Tokyo')).toEqual(['Tokyo'])\n  })\n\n  it('splits and trims several cities', () => {\n    expect(parseCities(' Tokyo ,San Francisco, Lima')).toEqual(['Tokyo', 'San Francisco', 'Lima'])\n  })\n\n  it.each(['', ' , ', 'Tokyo,,Lima'])('rejects %j', (value) => {\n    expect(() => parseCities(value)).toThrow('Expected a comma-separated list of cities')\n  })\n})\n","tests/cli.test.ts",[15,6139,6140,6152,6162,6166,6182,6198,6226,6230,6234,6249,6282,6286,6290,6330,6354,6358],{"__ignoreMap":42},[74,6141,6142,6144,6147,6149],{"class":76,"line":77},[74,6143,1555],{"class":788},[74,6145,6146],{"class":314}," { describe, expect, it } ",[74,6148,1561],{"class":788},[74,6150,6151],{"class":83}," 'vitest'\n",[74,6153,6154,6156,6158,6160],{"class":76,"line":43},[74,6155,1555],{"class":788},[74,6157,5706],{"class":314},[74,6159,1561],{"class":788},[74,6161,5711],{"class":83},[74,6163,6164],{"class":76,"line":334},[74,6165,1569],{"emptyLinePlaceholder":52},[74,6167,6168,6170,6172,6175,6178,6180],{"class":76,"line":348},[74,6169,3595],{"class":80},[74,6171,1983],{"class":314},[74,6173,6174],{"class":83},"'parseCities'",[74,6176,6177],{"class":314},", () ",[74,6179,2249],{"class":320},[74,6181,2252],{"class":314},[74,6183,6184,6187,6189,6192,6194,6196],{"class":76,"line":361},[74,6185,6186],{"class":80},"  it",[74,6188,1983],{"class":314},[74,6190,6191],{"class":83},"'returns a single city'",[74,6193,6177],{"class":314},[74,6195,2249],{"class":320},[74,6197,2252],{"class":314},[74,6199,6200,6203,6205,6208,6210,6213,6216,6219,6221,6223],{"class":76,"line":374},[74,6201,6202],{"class":80},"    expect",[74,6204,1983],{"class":314},[74,6206,6207],{"class":80},"parseCities",[74,6209,1983],{"class":314},[74,6211,6212],{"class":83},"'Tokyo'",[74,6214,6215],{"class":314},")).",[74,6217,6218],{"class":80},"toEqual",[74,6220,3539],{"class":314},[74,6222,6212],{"class":83},[74,6224,6225],{"class":314},"])\n",[74,6227,6228],{"class":76,"line":387},[74,6229,4571],{"class":314},[74,6231,6232],{"class":76,"line":400},[74,6233,1569],{"emptyLinePlaceholder":52},[74,6235,6236,6238,6240,6243,6245,6247],{"class":76,"line":413},[74,6237,6186],{"class":80},[74,6239,1983],{"class":314},[74,6241,6242],{"class":83},"'splits and trims several cities'",[74,6244,6177],{"class":314},[74,6246,2249],{"class":320},[74,6248,2252],{"class":314},[74,6250,6251,6253,6255,6257,6259,6262,6264,6266,6268,6270,6272,6275,6277,6280],{"class":76,"line":424},[74,6252,6202],{"class":80},[74,6254,1983],{"class":314},[74,6256,6207],{"class":80},[74,6258,1983],{"class":314},[74,6260,6261],{"class":83},"' Tokyo ,San Francisco, Lima'",[74,6263,6215],{"class":314},[74,6265,6218],{"class":80},[74,6267,3539],{"class":314},[74,6269,6212],{"class":83},[74,6271,138],{"class":314},[74,6273,6274],{"class":83},"'San Francisco'",[74,6276,138],{"class":314},[74,6278,6279],{"class":83},"'Lima'",[74,6281,6225],{"class":314},[74,6283,6284],{"class":76,"line":544},[74,6285,4571],{"class":314},[74,6287,6288],{"class":76,"line":554},[74,6289,1569],{"emptyLinePlaceholder":52},[74,6291,6292,6295,6298,6300,6303,6305,6308,6310,6313,6316,6319,6322,6324,6326,6328],{"class":76,"line":560},[74,6293,6294],{"class":314},"  it.",[74,6296,6297],{"class":80},"each",[74,6299,3539],{"class":314},[74,6301,6302],{"class":83},"''",[74,6304,138],{"class":314},[74,6306,6307],{"class":83},"' , '",[74,6309,138],{"class":314},[74,6311,6312],{"class":83},"'Tokyo,,Lima'",[74,6314,6315],{"class":314},"])(",[74,6317,6318],{"class":83},"'rejects %j'",[74,6320,6321],{"class":314},", (",[74,6323,4434],{"class":2238},[74,6325,2051],{"class":314},[74,6327,2249],{"class":320},[74,6329,2252],{"class":314},[74,6331,6332,6334,6337,6339,6341,6344,6347,6349,6352],{"class":76,"line":565},[74,6333,6202],{"class":80},[74,6335,6336],{"class":314},"(() ",[74,6338,2249],{"class":320},[74,6340,5288],{"class":80},[74,6342,6343],{"class":314},"(value)).",[74,6345,6346],{"class":80},"toThrow",[74,6348,1983],{"class":314},[74,6350,6351],{"class":83},"'Expected a comma-separated list of cities'",[74,6353,1989],{"class":314},[74,6355,6356],{"class":76,"line":577},[74,6357,4571],{"class":314},[74,6359,6360],{"class":76,"line":589},[74,6361,1747],{"class":314},[11,6363,6364,6365,6367],{},"Then, the distance math. ",[15,6366,3638],{}," takes two places and returns a number, so we can pass it coordinates directly, with no network request and nothing to mock:",[66,6369,6372],{"className":1545,"code":6370,"filename":6371,"language":1548,"meta":42,"style":42},"import { describe, expect, it } from 'vitest'\nimport { distanceBetween } from '../lib/distance.js'\n\nconst buenosAires = {\n  name: 'Buenos Aires',\n  country: 'Argentina',\n  latitude: -34.61315,\n  longitude: -58.37723,\n}\nconst tokyo = { name: 'Tokyo', country: 'Japan', latitude: 35.6895, longitude: 139.69171 }\n\ndescribe('distanceBetween', () => {\n  it('measures the distance in kilometers', () => {\n    expect(distanceBetween(buenosAires, tokyo, 'km')).toBe(18370)\n  })\n\n  it('measures the distance in miles', () => {\n    expect(distanceBetween(buenosAires, tokyo, 'mi')).toBe(11415)\n  })\n\n  it('returns zero for the same place', () => {\n    expect(distanceBetween(tokyo, tokyo, 'km')).toBe(0)\n  })\n})\n","tests/distance.test.ts",[15,6373,6374,6384,6395,6399,6410,6419,6429,6441,6453,6457,6489,6493,6508,6523,6547,6551,6555,6570,6593,6597,6601,6616,6639,6643],{"__ignoreMap":42},[74,6375,6376,6378,6380,6382],{"class":76,"line":77},[74,6377,1555],{"class":788},[74,6379,6146],{"class":314},[74,6381,1561],{"class":788},[74,6383,6151],{"class":83},[74,6385,6386,6388,6391,6393],{"class":76,"line":43},[74,6387,1555],{"class":788},[74,6389,6390],{"class":314}," { distanceBetween } ",[74,6392,1561],{"class":788},[74,6394,2452],{"class":83},[74,6396,6397],{"class":76,"line":334},[74,6398,1569],{"emptyLinePlaceholder":52},[74,6400,6401,6403,6406,6408],{"class":76,"line":348},[74,6402,1887],{"class":320},[74,6404,6405],{"class":314}," buenosAires ",[74,6407,1918],{"class":788},[74,6409,2252],{"class":314},[74,6411,6412,6415,6417],{"class":76,"line":361},[74,6413,6414],{"class":314},"  name: ",[74,6416,4547],{"class":83},[74,6418,331],{"class":314},[74,6420,6421,6424,6427],{"class":76,"line":374},[74,6422,6423],{"class":314},"  country: ",[74,6425,6426],{"class":83},"'Argentina'",[74,6428,331],{"class":314},[74,6430,6431,6434,6436,6439],{"class":76,"line":387},[74,6432,6433],{"class":314},"  latitude: ",[74,6435,3137],{"class":788},[74,6437,6438],{"class":327},"34.61315",[74,6440,331],{"class":314},[74,6442,6443,6446,6448,6451],{"class":76,"line":400},[74,6444,6445],{"class":314},"  longitude: ",[74,6447,3137],{"class":788},[74,6449,6450],{"class":327},"58.37723",[74,6452,331],{"class":314},[74,6454,6455],{"class":76,"line":413},[74,6456,427],{"class":314},[74,6458,6459,6461,6464,6466,6469,6471,6474,6477,6480,6482,6485,6487],{"class":76,"line":424},[74,6460,1887],{"class":320},[74,6462,6463],{"class":314}," tokyo ",[74,6465,1918],{"class":788},[74,6467,6468],{"class":314}," { name: ",[74,6470,6212],{"class":83},[74,6472,6473],{"class":314},", country: ",[74,6475,6476],{"class":83},"'Japan'",[74,6478,6479],{"class":314},", latitude: ",[74,6481,369],{"class":327},[74,6483,6484],{"class":314},", longitude: ",[74,6486,382],{"class":327},[74,6488,2721],{"class":314},[74,6490,6491],{"class":76,"line":544},[74,6492,1569],{"emptyLinePlaceholder":52},[74,6494,6495,6497,6499,6502,6504,6506],{"class":76,"line":554},[74,6496,3595],{"class":80},[74,6498,1983],{"class":314},[74,6500,6501],{"class":83},"'distanceBetween'",[74,6503,6177],{"class":314},[74,6505,2249],{"class":320},[74,6507,2252],{"class":314},[74,6509,6510,6512,6514,6517,6519,6521],{"class":76,"line":560},[74,6511,6186],{"class":80},[74,6513,1983],{"class":314},[74,6515,6516],{"class":83},"'measures the distance in kilometers'",[74,6518,6177],{"class":314},[74,6520,2249],{"class":320},[74,6522,2252],{"class":314},[74,6524,6525,6527,6529,6531,6534,6536,6538,6541,6543,6545],{"class":76,"line":565},[74,6526,6202],{"class":80},[74,6528,1983],{"class":314},[74,6530,3638],{"class":80},[74,6532,6533],{"class":314},"(buenosAires, tokyo, ",[74,6535,3936],{"class":83},[74,6537,6215],{"class":314},[74,6539,6540],{"class":80},"toBe",[74,6542,1983],{"class":314},[74,6544,395],{"class":327},[74,6546,1989],{"class":314},[74,6548,6549],{"class":76,"line":577},[74,6550,4571],{"class":314},[74,6552,6553],{"class":76,"line":589},[74,6554,1569],{"emptyLinePlaceholder":52},[74,6556,6557,6559,6561,6564,6566,6568],{"class":76,"line":601},[74,6558,6186],{"class":80},[74,6560,1983],{"class":314},[74,6562,6563],{"class":83},"'measures the distance in miles'",[74,6565,6177],{"class":314},[74,6567,2249],{"class":320},[74,6569,2252],{"class":314},[74,6571,6572,6574,6576,6578,6580,6582,6584,6586,6588,6591],{"class":76,"line":613},[74,6573,6202],{"class":80},[74,6575,1983],{"class":314},[74,6577,3638],{"class":80},[74,6579,6533],{"class":314},[74,6581,3941],{"class":83},[74,6583,6215],{"class":314},[74,6585,6540],{"class":80},[74,6587,1983],{"class":314},[74,6589,6590],{"class":327},"11415",[74,6592,1989],{"class":314},[74,6594,6595],{"class":76,"line":624},[74,6596,4571],{"class":314},[74,6598,6599],{"class":76,"line":634},[74,6600,1569],{"emptyLinePlaceholder":52},[74,6602,6603,6605,6607,6610,6612,6614],{"class":76,"line":640},[74,6604,6186],{"class":80},[74,6606,1983],{"class":314},[74,6608,6609],{"class":83},"'returns zero for the same place'",[74,6611,6177],{"class":314},[74,6613,2249],{"class":320},[74,6615,2252],{"class":314},[74,6617,6618,6620,6622,6624,6627,6629,6631,6633,6635,6637],{"class":76,"line":646},[74,6619,6202],{"class":80},[74,6621,1983],{"class":314},[74,6623,3638],{"class":80},[74,6625,6626],{"class":314},"(tokyo, tokyo, ",[74,6628,3936],{"class":83},[74,6630,6215],{"class":314},[74,6632,6540],{"class":80},[74,6634,1983],{"class":314},[74,6636,2953],{"class":327},[74,6638,1989],{"class":314},[74,6640,6641],{"class":76,"line":1147},[74,6642,4571],{"class":314},[74,6644,6645],{"class":76,"line":1155},[74,6646,1747],{"class":314},[11,6648,6649,6650,6653],{},"Then, let's write a command test. Here, the trick is that citty's ",[15,6651,6652],{},"runCommand"," accepts a raw argument array, so we can drive a command exactly the way the terminal would, without spawning a process. We mock the distance module so no network request happens, and the config module so the test never reads the real config file on your machine:",[66,6655,6658],{"className":1545,"code":6656,"filename":6657,"language":1548,"meta":42,"style":42},"import { describe, expect, it, vi } from 'vitest'\nimport { runCommand } from 'citty'\nimport { lookup } from '../commands/lookup.js'\nimport { fetchDistance, fetchDistances } from '../lib/distance.js'\n\nvi.mock('../lib/config.js', () => ({\n  resolveConfig: (from?: string) => ({ from: from ?? 'Lisbon', unit: 'km' }),\n}))\n\nvi.mock('../lib/distance.js', () => ({\n  fetchDistance: vi.fn(async () => ({ ok: true })),\n  fetchDistances: vi.fn(async () => ({ ok: true })),\n}))\n\ndescribe('lookup', () => {\n  it('measures from your city by default', async () => {\n    await runCommand(lookup, { rawArgs: ['Tokyo'] })\n\n    expect(fetchDistance).toHaveBeenCalledWith('Tokyo', 'Lisbon', 'km')\n  })\n\n  it('measures from another city with --from', async () => {\n    await runCommand(lookup, { rawArgs: ['Tokyo', '--from', 'Lima'] })\n\n    expect(fetchDistance).toHaveBeenCalledWith('Tokyo', 'Lima', 'km')\n  })\n\n  it('measures several cities', async () => {\n    await runCommand(lookup, { rawArgs: ['Tokyo, San Francisco'] })\n\n    expect(fetchDistances).toHaveBeenCalledWith(['Tokyo', 'San Francisco'], 'Lisbon', 'km')\n  })\n})\n","tests/lookup.test.ts",[15,6659,6660,6671,6682,6692,6702,6706,6726,6760,6765,6769,6786,6811,6832,6836,6840,6855,6874,6888,6892,6917,6921,6925,6944,6965,6969,6991,6995,6999,7018,7031,7035,7063,7067],{"__ignoreMap":42},[74,6661,6662,6664,6667,6669],{"class":76,"line":77},[74,6663,1555],{"class":788},[74,6665,6666],{"class":314}," { describe, expect, it, vi } ",[74,6668,1561],{"class":788},[74,6670,6151],{"class":83},[74,6672,6673,6675,6678,6680],{"class":76,"line":43},[74,6674,1555],{"class":788},[74,6676,6677],{"class":314}," { runCommand } ",[74,6679,1561],{"class":788},[74,6681,1866],{"class":83},[74,6683,6684,6686,6688,6690],{"class":76,"line":334},[74,6685,1555],{"class":788},[74,6687,1873],{"class":314},[74,6689,1561],{"class":788},[74,6691,1878],{"class":83},[74,6693,6694,6696,6698,6700],{"class":76,"line":348},[74,6695,1555],{"class":788},[74,6697,5718],{"class":314},[74,6699,1561],{"class":788},[74,6701,2452],{"class":83},[74,6703,6704],{"class":76,"line":361},[74,6705,1569],{"emptyLinePlaceholder":52},[74,6707,6708,6711,6714,6716,6719,6721,6723],{"class":76,"line":374},[74,6709,6710],{"class":314},"vi.",[74,6712,6713],{"class":80},"mock",[74,6715,1983],{"class":314},[74,6717,6718],{"class":83},"'../lib/config.js'",[74,6720,6177],{"class":314},[74,6722,2249],{"class":320},[74,6724,6725],{"class":314}," ({\n",[74,6727,6728,6731,6734,6736,6738,6740,6742,6744,6747,6749,6752,6755,6757],{"class":76,"line":387},[74,6729,6730],{"class":80},"  resolveConfig",[74,6732,6733],{"class":314},": (",[74,6735,1561],{"class":2238},[74,6737,2933],{"class":788},[74,6739,2696],{"class":320},[74,6741,2051],{"class":314},[74,6743,2249],{"class":320},[74,6745,6746],{"class":314}," ({ from: from ",[74,6748,2131],{"class":788},[74,6750,6751],{"class":83}," 'Lisbon'",[74,6753,6754],{"class":314},", unit: ",[74,6756,3936],{"class":83},[74,6758,6759],{"class":314}," }),\n",[74,6761,6762],{"class":76,"line":400},[74,6763,6764],{"class":314},"}))\n",[74,6766,6767],{"class":76,"line":413},[74,6768,1569],{"emptyLinePlaceholder":52},[74,6770,6771,6773,6775,6777,6780,6782,6784],{"class":76,"line":424},[74,6772,6710],{"class":314},[74,6774,6713],{"class":80},[74,6776,1983],{"class":314},[74,6778,6779],{"class":83},"'../lib/distance.js'",[74,6781,6177],{"class":314},[74,6783,2249],{"class":320},[74,6785,6725],{"class":314},[74,6787,6788,6791,6794,6796,6798,6801,6803,6806,6808],{"class":76,"line":544},[74,6789,6790],{"class":314},"  fetchDistance: vi.",[74,6792,6793],{"class":80},"fn",[74,6795,1983],{"class":314},[74,6797,1955],{"class":788},[74,6799,6800],{"class":314}," () ",[74,6802,2249],{"class":320},[74,6804,6805],{"class":314}," ({ ok: ",[74,6807,328],{"class":327},[74,6809,6810],{"class":314}," })),\n",[74,6812,6813,6816,6818,6820,6822,6824,6826,6828,6830],{"class":76,"line":554},[74,6814,6815],{"class":314},"  fetchDistances: vi.",[74,6817,6793],{"class":80},[74,6819,1983],{"class":314},[74,6821,1955],{"class":788},[74,6823,6800],{"class":314},[74,6825,2249],{"class":320},[74,6827,6805],{"class":314},[74,6829,328],{"class":327},[74,6831,6810],{"class":314},[74,6833,6834],{"class":76,"line":560},[74,6835,6764],{"class":314},[74,6837,6838],{"class":76,"line":565},[74,6839,1569],{"emptyLinePlaceholder":52},[74,6841,6842,6844,6846,6849,6851,6853],{"class":76,"line":577},[74,6843,3595],{"class":80},[74,6845,1983],{"class":314},[74,6847,6848],{"class":83},"'lookup'",[74,6850,6177],{"class":314},[74,6852,2249],{"class":320},[74,6854,2252],{"class":314},[74,6856,6857,6859,6861,6864,6866,6868,6870,6872],{"class":76,"line":589},[74,6858,6186],{"class":80},[74,6860,1983],{"class":314},[74,6862,6863],{"class":83},"'measures from your city by default'",[74,6865,138],{"class":314},[74,6867,1955],{"class":788},[74,6869,6800],{"class":314},[74,6871,2249],{"class":320},[74,6873,2252],{"class":314},[74,6875,6876,6878,6880,6883,6885],{"class":76,"line":601},[74,6877,2082],{"class":788},[74,6879,2149],{"class":80},[74,6881,6882],{"class":314},"(lookup, { rawArgs: [",[74,6884,6212],{"class":83},[74,6886,6887],{"class":314},"] })\n",[74,6889,6890],{"class":76,"line":613},[74,6891,1569],{"emptyLinePlaceholder":52},[74,6893,6894,6896,6899,6902,6904,6906,6908,6911,6913,6915],{"class":76,"line":624},[74,6895,6202],{"class":80},[74,6897,6898],{"class":314},"(fetchDistance).",[74,6900,6901],{"class":80},"toHaveBeenCalledWith",[74,6903,1983],{"class":314},[74,6905,6212],{"class":83},[74,6907,138],{"class":314},[74,6909,6910],{"class":83},"'Lisbon'",[74,6912,138],{"class":314},[74,6914,3936],{"class":83},[74,6916,1989],{"class":314},[74,6918,6919],{"class":76,"line":634},[74,6920,4571],{"class":314},[74,6922,6923],{"class":76,"line":640},[74,6924,1569],{"emptyLinePlaceholder":52},[74,6926,6927,6929,6931,6934,6936,6938,6940,6942],{"class":76,"line":646},[74,6928,6186],{"class":80},[74,6930,1983],{"class":314},[74,6932,6933],{"class":83},"'measures from another city with --from'",[74,6935,138],{"class":314},[74,6937,1955],{"class":788},[74,6939,6800],{"class":314},[74,6941,2249],{"class":320},[74,6943,2252],{"class":314},[74,6945,6946,6948,6950,6952,6954,6956,6959,6961,6963],{"class":76,"line":1147},[74,6947,2082],{"class":788},[74,6949,2149],{"class":80},[74,6951,6882],{"class":314},[74,6953,6212],{"class":83},[74,6955,138],{"class":314},[74,6957,6958],{"class":83},"'--from'",[74,6960,138],{"class":314},[74,6962,6279],{"class":83},[74,6964,6887],{"class":314},[74,6966,6967],{"class":76,"line":1155},[74,6968,1569],{"emptyLinePlaceholder":52},[74,6970,6971,6973,6975,6977,6979,6981,6983,6985,6987,6989],{"class":76,"line":1168},[74,6972,6202],{"class":80},[74,6974,6898],{"class":314},[74,6976,6901],{"class":80},[74,6978,1983],{"class":314},[74,6980,6212],{"class":83},[74,6982,138],{"class":314},[74,6984,6279],{"class":83},[74,6986,138],{"class":314},[74,6988,3936],{"class":83},[74,6990,1989],{"class":314},[74,6992,6993],{"class":76,"line":1181},[74,6994,4571],{"class":314},[74,6996,6997],{"class":76,"line":1194},[74,6998,1569],{"emptyLinePlaceholder":52},[74,7000,7001,7003,7005,7008,7010,7012,7014,7016],{"class":76,"line":1205},[74,7002,6186],{"class":80},[74,7004,1983],{"class":314},[74,7006,7007],{"class":83},"'measures several cities'",[74,7009,138],{"class":314},[74,7011,1955],{"class":788},[74,7013,6800],{"class":314},[74,7015,2249],{"class":320},[74,7017,2252],{"class":314},[74,7019,7020,7022,7024,7026,7029],{"class":76,"line":1210},[74,7021,2082],{"class":788},[74,7023,2149],{"class":80},[74,7025,6882],{"class":314},[74,7027,7028],{"class":83},"'Tokyo, San Francisco'",[74,7030,6887],{"class":314},[74,7032,7033],{"class":76,"line":1218},[74,7034,1569],{"emptyLinePlaceholder":52},[74,7036,7037,7039,7042,7044,7046,7048,7050,7052,7055,7057,7059,7061],{"class":76,"line":1231},[74,7038,6202],{"class":80},[74,7040,7041],{"class":314},"(fetchDistances).",[74,7043,6901],{"class":80},[74,7045,3539],{"class":314},[74,7047,6212],{"class":83},[74,7049,138],{"class":314},[74,7051,6274],{"class":83},[74,7053,7054],{"class":314},"], ",[74,7056,6910],{"class":83},[74,7058,138],{"class":314},[74,7060,3936],{"class":83},[74,7062,1989],{"class":314},[74,7064,7065],{"class":76,"line":1244},[74,7066,4571],{"class":314},[74,7068,7069],{"class":76,"line":1257},[74,7070,1747],{"class":314},[11,7072,7073],{},"Now, let's run them:",[66,7075,7077],{"className":68,"code":7076,"language":70,"meta":42,"style":42},"npm test\n",[15,7078,7079],{"__ignoreMap":42},[74,7080,7081,7083],{"class":76,"line":77},[74,7082,98],{"class":80},[74,7084,7085],{"class":83}," test\n",[66,7087,7090],{"className":7088,"code":7089,"language":276,"meta":42},[274]," ✓ tests/cli.test.ts (5 tests)\n ✓ tests/lookup.test.ts (3 tests)\n ✓ tests/distance.test.ts (3 tests)\n\n Test Files  3 passed (3)\n      Tests  11 passed (11)\n",[15,7091,7089],{"__ignoreMap":42},[11,7093,7094,7095,7097],{},"And remember the ",[15,7096,1761],{}," block from the Vite+ config we set up earlier? From now on, every commit formats and lints the files you touched and runs the tests that import them. If a test fails, the commit is rejected. I've had that hook catch more bugs than I'd like to admit.",[119,7099,7101],{"id":7100},"publishing-the-package","Publishing the Package",[11,7103,7104],{},"Alright, the last step. Before publishing, it's a good idea to check that the name is free:",[66,7106,7108],{"className":68,"code":7107,"language":70,"meta":42,"style":42},"npm view @username/howfar\n",[15,7109,7110],{"__ignoreMap":42},[74,7111,7112,7114,7117],{"class":76,"line":77},[74,7113,98],{"class":80},[74,7115,7116],{"class":83}," view",[74,7118,7119],{"class":83}," @username/howfar\n",[11,7121,7122,7123,7127],{},"If that returns a package, you'll need to pick another name. Then, let's log in and publish. If you don't have an npm account yet, you can create one at ",[23,7124,143],{"href":7125,"rel":7126},"https://www.npmjs.com/signup",[27]," first:",[66,7129,7131],{"className":68,"code":7130,"language":70,"meta":42,"style":42},"npm login\nnpm publish --access public\n",[15,7132,7133,7140],{"__ignoreMap":42},[74,7134,7135,7137],{"class":76,"line":77},[74,7136,98],{"class":80},[74,7138,7139],{"class":83}," login\n",[74,7141,7142,7144,7147,7150],{"class":76,"line":43},[74,7143,98],{"class":80},[74,7145,7146],{"class":83}," publish",[74,7148,7149],{"class":327}," --access",[74,7151,7152],{"class":83}," public\n",[11,7154,2623,7155,7158,7159,7161,7162,7165,7166,7168,7169,7171],{},[15,7156,7157],{},"--access public"," flag is only needed the first time, because scoped packages default to private. And that's it. The ",[15,7160,1324],{}," script runs the type check and ",[15,7163,7164],{},"vp pack",", and the ",[15,7167,1312],{}," field makes sure only ",[15,7170,235],{}," goes up. A minute later, anyone with Node installed can run:",[66,7173,7175],{"className":68,"code":7174,"language":70,"meta":42,"style":42},"npx @username/howfar init\nnpx @username/howfar Tokyo\n",[15,7176,7177,7186],{"__ignoreMap":42},[74,7178,7179,7181,7184],{"class":76,"line":77},[74,7180,17],{"class":80},[74,7182,7183],{"class":83}," @username/howfar",[74,7185,267],{"class":83},[74,7187,7188,7190,7192],{"class":76,"line":43},[74,7189,17],{"class":80},[74,7191,7183],{"class":83},[74,7193,87],{"class":83},[11,7195,7196,7197,7199],{},"npm downloads the package into a cache, finds the ",[15,7198,1296],{}," entry, and runs it. The user never installs anything globally or builds anything.",[11,7201,7202],{},"Later, to ship a new version, you can bump the number and publish again:",[66,7204,7206],{"className":68,"code":7205,"language":70,"meta":42,"style":42},"npm version patch\nnpm publish\n",[15,7207,7208,7218],{"__ignoreMap":42},[74,7209,7210,7212,7215],{"class":76,"line":77},[74,7211,98],{"class":80},[74,7213,7214],{"class":83}," version",[74,7216,7217],{"class":83}," patch\n",[74,7219,7220,7222],{"class":76,"line":43},[74,7221,98],{"class":80},[74,7223,7224],{"class":83}," publish\n",[119,7226,7228],{"id":7227},"where-to-go-from-here","Where to Go From Here",[11,7230,7231,7232,7234,7235,7237,7238,7241,7242,7244,7245,7247],{},"Everything in howfar maps one-to-one onto climail, which is a much bigger tool. climail has thirteen commands instead of two, an IMAP client instead of a geocoding API, and a config with two nested blocks instead of two fields. The ",[15,7233,1829],{}," file is almost identical. The ",[15,7236,5274],{}," file has a ",[15,7239,7240],{},"parseCount"," where we have ",[15,7243,6207],{},". The ",[15,7246,714],{}," wizard asks for a host and a password instead of a city. Same skeleton, more code.",[11,7249,7250],{},"So when your tool needs to grow, you already know where things go:",[681,7252,7253,7258,7263,7268],{},[684,7254,7255,7256,196],{},"A new feature is a new file in ",[15,7257,1811],{},[684,7259,7260,7261,196],{},"Logic that two commands share moves to ",[15,7262,1815],{},[684,7264,7265,7266,196],{},"A new setting is a new field in the zod schema and a new prompt in ",[15,7267,714],{},[684,7269,7270,7271,7273],{},"A new argument that needs validation gets a parse function in ",[15,7272,5274],{},", with a test next to it.",[119,7275,7277],{"id":7276},"in-closing","In Closing",[11,7279,7280,7281,7283],{},"We started from an empty folder and ended with a published ",[15,7282,17],{}," command that has a default command, a flag, a subcommand, a help screen, a setup wizard, a validated config file, tests, and a pre-commit hook. The distance part was about seventy lines. The rest is a structure you can copy into your next tool tomorrow.",[11,7285,7286],{},"And the next time someone asks how far Tokyo is, you won't need to drop any pins.",[11,7288,7289,7290,196],{},"If you turn one of your own scripts into a tool, I'd love to hear what you built! I'm ",[23,7291,7294],{"href":7292,"rel":7293},"https://x.com/nicodevs",[27],"@nicodevs",[11,7296,7297],{},"Until next time!",[7299,7300,7301],"style",{},"html pre.shiki code .sHkqI, html code.shiki .sHkqI{--shiki-default:#A6E22E}html pre.shiki code .s_Ekj, html code.shiki .s_Ekj{--shiki-default:#E6DB74}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sCdxs, html code.shiki .sCdxs{--shiki-default:#F8F8F2}html pre.shiki code .sOx1s, html code.shiki .sOx1s{--shiki-default:#66D9EF;--shiki-default-font-style:italic}html pre.shiki code .s7s5_, html code.shiki .s7s5_{--shiki-default:#AE81FF}html pre.shiki code .susgL, html code.shiki .susgL{--shiki-default:#CFCFC2}html pre.shiki code .s8I7P, html code.shiki .s8I7P{--shiki-default:#F92672}html pre.shiki code .sYf5A, html code.shiki .sYf5A{--shiki-default:#66D9EF}html pre.shiki code .snpHw, html code.shiki .snpHw{--shiki-default:#88846F}html pre.shiki code .soZtE, html code.shiki .soZtE{--shiki-default:#A6E22E;--shiki-default-text-decoration:underline}html pre.shiki code .sW0Xf, html code.shiki .sW0Xf{--shiki-default:#FD971F;--shiki-default-font-style:italic}",{"title":42,"searchDepth":43,"depth":43,"links":7303},[7304,7305,7306,7307,7308,7309,7310,7311,7312,7313,7314,7315,7316],{"id":121,"depth":43,"text":122},{"id":249,"depth":43,"text":250},{"id":669,"depth":43,"text":670},{"id":762,"depth":43,"text":763},{"id":1791,"depth":43,"text":1792},{"id":1822,"depth":43,"text":1823},{"id":2409,"depth":43,"text":2410},{"id":3824,"depth":43,"text":3825},{"id":5262,"depth":43,"text":5263},{"id":6123,"depth":43,"text":6124},{"id":7100,"depth":43,"text":7101},{"id":7227,"depth":43,"text":7228},{"id":7276,"depth":43,"text":7277},"2026-11-01","From an empty folder to a published npx command. Learn the structure I use for my own CLIs: citty for commands, clack for prompts, rc9 and zod for config, and Vite+ for linting, testing and bundling.","/img/blog/build-command-line-tools-in-typescript-a-step-by-step-guide.png",null,{},"/blog/build-command-line-tools-in-typescript-a-step-by-step-guide",{"title":59,"description":7318},"blog/build-command-line-tools-in-typescript-a-step-by-step-guide","hbuzE4bvNSnOZuiaMxB-YPgWaRpt0fClTzkeTuO2Oo4",{"id":7327,"title":7328,"body":7329,"category":7352,"date":7353,"description":7333,"draft":48,"extension":49,"image":7354,"link":7346,"meta":7355,"navigation":52,"path":7356,"seo":7357,"stem":7358,"tighten":52,"__hash__":7359},"blog/blog/web-workers-a-second-thread-for-your-javascript.md","Web Workers: A Second Thread for Your JavaScript",{"type":8,"value":7330,"toc":7350},[7331,7334,7341,7343],[11,7332,7333],{},"JavaScript runs on a single thread. Give it a heavy task, like resizing a large photo before uploading it, and the page freezes until it finishes.",[11,7335,7336,7337,7340],{},"Web Workers solve this by running that work on a second thread. In this article for ",[23,7338,28],{"href":25,"rel":7339},[27],", I cover how they work, their two main limitations, how they differ from Service Workers, and how to use one to resize and compress images in the browser before they reach your server.",[11,7342,32],{},[34,7344,7347],{"image":7345,"link":7346,"title":7328},"https://tighten.com/assets/images/insights/web-workers-a-second-thread-for-your-javascript-preview.jpg","https://tighten.com/insights/web-workers-a-second-thread-for-your-javascript/",[11,7348,7349],{},"Suppose you have a web app that lets users upload photos. Cool! But there's a catch: photos straight from a phone camera can easily be around 8 MB each. On a weak mobile connection, uploading the full-size file can be painfully slow for them, and expensive for you.",{"title":42,"searchDepth":43,"depth":43,"links":7351},[],"JavaScript","2026-09-21","/img/tighten/web-workers-a-second-thread-for-your-javascript.jpg",{},"/blog/web-workers-a-second-thread-for-your-javascript",{"title":7328,"description":7333},"blog/web-workers-a-second-thread-for-your-javascript","nSK9ik-PR0A8yoYeHiQccCemfL3OnRcNEsvU_b9jhGI",{"id":7361,"title":7362,"body":7363,"category":7386,"date":7387,"description":7367,"draft":48,"extension":49,"image":7388,"link":7380,"meta":7389,"navigation":52,"path":7390,"seo":7391,"stem":7392,"tighten":52,"__hash__":7393},"blog/blog/run-ai-in-the-browser-a-practical-guide-to-transformers-js.md","Run AI in the Browser: A Practical Guide to Transformers.js",{"type":8,"value":7364,"toc":7384},[7365,7368,7375,7377],[11,7366,7367],{},"Transformers.js lets you run open source AI models directly in the browser. No API keys, no server round trips, and it keeps working offline once the model is cached.",[11,7369,7370,7371,7374],{},"I wrote a guide for ",[23,7372,28],{"href":25,"rel":7373},[27]," on how it works, which browsers support it, what to expect from small models, and how to build a grammar checker that runs entirely on the user's device.",[11,7376,32],{},[34,7378,7381],{"image":7379,"link":7380,"title":7362},"https://tighten.com/assets/images/insights/run-ai-in-the-browser-a-practical-guide-to-transformers-js-preview.jpg","https://tighten.com/insights/run-ai-in-the-browser-a-practical-guide-to-transformers-js/",[11,7382,7383],{},"Imagine you need to integrate AI into your web app. Let's say you want to quickly translate text. Preferably, you'd like to use something free and open source (you don't want any surprise bills at the end of the month). One more thing: people use your app while traveling, so it must continue working when they have poor connections or no connection at all.",{"title":42,"searchDepth":43,"depth":43,"links":7385},[],"AI","2026-09-03","/img/tighten/run-ai-in-the-browser-a-practical-guide-to-transformers-js.jpg",{},"/blog/run-ai-in-the-browser-a-practical-guide-to-transformers-js",{"title":7362,"description":7367},"blog/run-ai-in-the-browser-a-practical-guide-to-transformers-js","6dqonwM6_yLcHslSIEH9MquPDCtbeaDHdh0FcOEBifQ",{"id":7395,"title":7396,"articles":7397,"body":7398,"description":42,"extension":49,"image":7402,"meta":7403,"navigation":52,"path":7404,"resources":7320,"seo":7405,"slides":7406,"stem":7407,"subtitle":7408,"video":7320,"__hash__":7409},"topics/topics/vue-sri-lanka.md","Why Vue.js? The Evolution of SPAs",[],{"type":8,"value":7399,"toc":7400},[],{"title":42,"searchDepth":43,"depth":43,"links":7401},[],"/img/talks/sri-lanka.png",{},"/topics/vue-sri-lanka",{"title":7396,"description":42},"https://www.canva.com/design/DAG5oF_kQEM/5tC4B6yjBIgQV7mmycDcRQ/view?embed","topics/vue-sri-lanka","Hey! Here you'll find the slides from my talk at the Vue.js Sri Lanka Community Meetup, about how web apps moved from server-rendered pages to Single Page Applications, and where Vue fits in that shift.","xGv8fVyIdywonna1arFNoEpaS2soHU0jU9SpnZRg3gQ",{"id":7411,"title":7396,"body":7412,"coming_soon":48,"date":7416,"date_label":7417,"description":7418,"display":52,"event":7419,"extension":49,"highlighted":48,"image":7402,"meta":7420,"navigation":52,"order":77,"path":7421,"platform":7422,"seo":7423,"slides":7424,"stem":7425,"thumbnail":7320,"time":7320,"topic":7426,"video":7320,"website":7320,"__hash__":7427},"talks/talks/vue-sri-lanka-2025.md",{"type":8,"value":7413,"toc":7414},[],{"title":42,"searchDepth":43,"depth":43,"links":7415},[],"2025-11-28","November 28","How web apps moved from server-rendered pages to Single Page Applications, and where Vue fits in that shift.","Vue.js Sri Lanka Community Meetup",{},"/talks/vue-sri-lanka-2025","🌐 Online",{"title":7396,"description":7418},"https://www.canva.com/design/DAG5oF_kQEM/5tC4B6yjBIgQV7mmycDcRQ/view?utm_content=DAG5oF_kQEM","talks/vue-sri-lanka-2025","vue-sri-lanka","ekNqnkP1940sV8S8TQi9b4ftcX2-ixDZV06WGTo87ls",[],1793905345099]