<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Sujal rana</title>
    <description>The latest articles on Forem by Sujal rana (@sujalrana).</description>
    <link>https://forem.com/sujalrana</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1236854%2Fab07b7fb-685f-4da9-91ff-85bdb6746e0e.jpg</url>
      <title>Forem: Sujal rana</title>
      <link>https://forem.com/sujalrana</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/sujalrana"/>
    <language>en</language>
    <item>
      <title>Building CLI tool (v1 of zap-search)</title>
      <dc:creator>Sujal rana</dc:creator>
      <pubDate>Tue, 14 Apr 2026 08:59:07 +0000</pubDate>
      <link>https://forem.com/sujalrana/building-cli-tool-v1-of-zap-search-1kkb</link>
      <guid>https://forem.com/sujalrana/building-cli-tool-v1-of-zap-search-1kkb</guid>
      <description>&lt;p&gt;Zap started from a very simple need : &lt;br&gt;
I wanted a fast terminal tool to search files, folders, and old shell commands without breaking flow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17loaecv4bro2h12qf0i.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F17loaecv4bro2h12qf0i.jpg" alt="zap-search by sujal"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The idea was straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;search files and folders from the current directory&lt;/li&gt;
&lt;li&gt;search zsh history too&lt;/li&gt;
&lt;li&gt;open files directly&lt;/li&gt;
&lt;li&gt;jump into folders quickly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Simple idea, but while building v1, I ran into two problems that taught me a lot more than I expected:&lt;/p&gt;

&lt;p&gt;search felt slower than it should&lt;br&gt;
selecting a folder sometimes printed a path instead of actually changing my shell directory&lt;br&gt;
This post is about those two problems and how I understood them while building zap.&lt;/p&gt;

&lt;p&gt;Why I built zap&lt;br&gt;
When I’m inside a repo, many times I only remember part of a filename or folder name. Maybe I remember pac, not the full path. I don’t want to manually cd around or type long nested paths just to reach package.json or packages/.&lt;/p&gt;

&lt;p&gt;So the goal of zap was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;type a query&lt;/li&gt;
&lt;li&gt;get useful matches&lt;/li&gt;
&lt;li&gt;select one&lt;/li&gt;
&lt;li&gt;act on it immediately
That was the whole point. Fast navigation from terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first version worked, but something felt off&lt;br&gt;
I got the basic flow running:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;scan the current directory&lt;/li&gt;
&lt;li&gt;fuzzy-match files and folders&lt;/li&gt;
&lt;li&gt;show them in a terminal picker&lt;/li&gt;
&lt;li&gt;open a file or move to a folder
At first it looked fine. But once I started actually using it, one thing became obvious very quickly:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Search felt slower than it should&lt;/strong&gt;&lt;br&gt;
For a search tool, speed is the main thing. Even a small delay feels bad because the whole point is instant recall.&lt;/p&gt;

&lt;p&gt;The repo I was testing on was not even huge, but the search still felt heavier than expected. That told me the issue was probably not the fuzzy matching idea itself. It was likely in how I was walking the filesystem.&lt;/p&gt;

&lt;p&gt;Understanding the slowdown: readdirSync and statSync&lt;br&gt;
The original traversal logic worked roughly like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;use readdirSync(dir) to list entries inside a folder&lt;/li&gt;
&lt;li&gt;for each entry, call statSync(fullPath)&lt;/li&gt;
&lt;li&gt;check whether it is a file or a folder&lt;/li&gt;
&lt;li&gt;recurse if it is a folder
That works, but it does more filesystem work than needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;What readdirSync does&lt;br&gt;
readdirSync(path) reads a directory and gives you the names inside it.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;src&lt;br&gt;
package.json&lt;br&gt;
README.md&lt;br&gt;
But it does not tell you what each item is.&lt;/p&gt;

&lt;p&gt;What statSync does&lt;br&gt;
statSync(path) asks the OS for metadata about a path. That tells you things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is it a file&lt;/li&gt;
&lt;li&gt;is it a directory&lt;/li&gt;
&lt;li&gt;size&lt;/li&gt;
&lt;li&gt;timestamps&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the old flow was:&lt;/p&gt;

&lt;p&gt;list names with readdirSync&lt;br&gt;
then make another OS call with statSync for every single item&lt;br&gt;
That is where the extra cost was coming from.&lt;/p&gt;

&lt;p&gt;And because these were synchronous calls, Node was waiting on each one in sequence. For a terminal search tool, that is exactly the kind of thing that starts making the tool feel sluggish.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The fix: cheaper directory walking&lt;/em&gt;&lt;br&gt;
The cleaner approach was to switch to:&lt;/p&gt;

&lt;p&gt;fs.readdirSync(dir, { withFileTypes: true })&lt;/p&gt;

&lt;p&gt;This returns Dirent objects instead of plain string names.&lt;/p&gt;

&lt;p&gt;That means each item already knows whether it is a file or directory through methods like:&lt;/p&gt;

&lt;p&gt;item.isDirectory()&lt;br&gt;
item.isFile()&lt;br&gt;
So I no longer needed statSync just to classify every entry.&lt;/p&gt;

&lt;p&gt;This was a very practical improvement because it made traversal cheaper without changing the search experience itself. Same behavior, less unnecessary filesystem overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shell problem: why folder selection printed CD: instead of changing directory&lt;/strong&gt;&lt;br&gt;
This was the most interesting part of the build for me.&lt;/p&gt;

&lt;p&gt;At one point I ran the CLI directly like this:&lt;/p&gt;

&lt;p&gt;node apps/cli/dist/index.js pac&lt;/p&gt;

&lt;p&gt;Then I selected a folder and saw:&lt;/p&gt;

&lt;p&gt;CD:/home/sujal/zap/packages&lt;/p&gt;

&lt;p&gt;At first, it looked broken. Why print the path instead of just moving into it?&lt;/p&gt;

&lt;p&gt;The answer is simple once you understand what is happening:&lt;/p&gt;

&lt;p&gt;a child process cannot change the parent shell’s current directory.&lt;/p&gt;

&lt;p&gt;That is the key point.&lt;/p&gt;

&lt;p&gt;When you run a Node CLI from terminal, that Node process is a child of your shell. Even if the CLI knows which folder you selected, it cannot make your current shell session run cd.&lt;/p&gt;

&lt;p&gt;So if I execute the CLI directly, the best it can do is report the selected directory somehow. It cannot itself move my shell.&lt;/p&gt;

&lt;p&gt;Once I understood that, the behavior made complete sense.&lt;/p&gt;

&lt;p&gt;The actual fix: a zsh wrapper and a temp file&lt;br&gt;
To make cd work, the shell itself needs to perform the cd.&lt;/p&gt;

&lt;p&gt;So the solution was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;create a shell wrapper function called zap&lt;/li&gt;
&lt;li&gt;create a temp file&lt;/li&gt;
&lt;li&gt;pass the temp file path to the CLI through an environment variable&lt;/li&gt;
&lt;li&gt;let the CLI write the selected directory into that temp file&lt;/li&gt;
&lt;li&gt;after the CLI exits, let the shell wrapper read the path and run cd
The wrapper looked like this:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;zap&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;cd_file
  &lt;span class="nb"&gt;local &lt;/span&gt;exit_code
  &lt;span class="nb"&gt;local &lt;/span&gt;target

  &lt;span class="nv"&gt;cd_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;ZAP_CD_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cd_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;command &lt;/span&gt;zap &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;exit_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$exit_code&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cd_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&amp;lt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cd_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$target&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi
  fi

  &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cd_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$exit_code&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And on the CLI side, the logic was simple:&lt;/p&gt;

&lt;p&gt;if ZAP_CD_FILE exists, write the chosen folder path there&lt;br&gt;
otherwise print CD:...&lt;br&gt;
That was the missing bridge between the CLI and the shell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What v1 really taught me&lt;/strong&gt;&lt;br&gt;
The biggest lesson from building zap v1 is that many frustrating bugs are not logic bugs in the usual sense. They are boundary problems.&lt;/p&gt;

&lt;p&gt;In my case:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;slow search was about how I was talking to the filesystem&lt;/li&gt;
&lt;li&gt;missing cd was about the boundary between a child process and the parent shell&lt;/li&gt;
&lt;li&gt;confusing command behavior was about understanding what executable was actually being run
Once I understood the boundary properly, the behavior stopped feeling random.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Final thoughts&lt;/strong&gt;&lt;br&gt;
I started zap as a small utility to speed up navigation in terminal. But building v1 ended up teaching me much more than I expected.&lt;/p&gt;

&lt;p&gt;It taught me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how expensive repeated filesystem calls can feel in a terminal UX&lt;/li&gt;
&lt;li&gt;why shell behavior can seem broken when it is actually just process isolation&lt;/li&gt;
&lt;li&gt;why tiny tools still force you to understand systems properly
And honestly, that is one of the best parts of building small tools. They look simple, but they make you learn real things.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can use zap by installing it through npm registry :&lt;br&gt;
&lt;code&gt;npm i zap-search&lt;/code&gt;&lt;br&gt;
&lt;code&gt;zap init zsh &amp;gt;&amp;gt; ~/.zshrc&lt;/code&gt;&lt;br&gt;
&lt;code&gt;source ~/.zshrc&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;if wanna contribute to zap then you're welcome : &lt;br&gt;
&lt;a href="https://github.com/Sran012/zap" rel="noopener noreferrer"&gt;https://github.com/Sran012/zap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That was my zap version 1.0.1 story.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>performance</category>
      <category>productivity</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
