<?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>DEV Community: Gabriel Wu</title>
    <description>The latest articles on DEV Community by Gabriel Wu (@lucifer1004).</description>
    <link>https://dev.to/lucifer1004</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%2F124572%2F3ff4b5b1-3eee-4589-9e8b-d01ffeed163f.jpeg</url>
      <title>DEV Community: Gabriel Wu</title>
      <link>https://dev.to/lucifer1004</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucifer1004"/>
    <language>en</language>
    <item>
      <title>Custom Tailscale DERP Server</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Mon, 02 Mar 2026 03:19:02 +0000</pubDate>
      <link>https://dev.to/lucifer1004/custom-tailscale-derp-server-n73</link>
      <guid>https://dev.to/lucifer1004/custom-tailscale-derp-server-n73</guid>
      <description>&lt;h2&gt;
  
  
  Deploying a Custom Tailscale DERP Server with DNS-01 SSL (When Ports 80/443 are Blocked)
&lt;/h2&gt;

&lt;p&gt;Setting up a self-hosted Tailscale DERP (Designated Encrypted Relay for Packets) node is the best way to ensure low-latency, private connections. But if your VPS provider or local ISP blocks ports 80 and 443, the built-in Let’s Encrypt automation will fail, leaving you in a “no viable challenge type found” loop.&lt;/p&gt;

&lt;p&gt;Here is the battle-tested, three-tier configuration to get your DERP node running on a custom port (e.g., &lt;strong&gt;3443&lt;/strong&gt;) with a valid SSL certificate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Cloudflare Side (The DNS Validator)
&lt;/h3&gt;

&lt;p&gt;Since we can’t use HTTP-01 challenges (because port 80 is blocked), we use &lt;strong&gt;DNS-01&lt;/strong&gt;. This proves you own the domain by adding a temporary TXT record via API.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create an API Token&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;My Profile &amp;gt; API Tokens &amp;gt; Create Token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Use the &lt;strong&gt;Edit zone DNS&lt;/strong&gt; template.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Zone Resources&lt;/strong&gt; to your specific domain (e.g., &lt;code&gt;example.com&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Copy the Token&lt;/strong&gt;: You will only see this once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS Record&lt;/strong&gt;: Ensure you have an &lt;code&gt;A&lt;/code&gt; record pointing your subdomain (e.g., &lt;code&gt;derp.example.com&lt;/code&gt;) to your VPS IP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 2: VPS Side (The Engine Room)
&lt;/h3&gt;

&lt;p&gt;We will use &lt;strong&gt;Docker&lt;/strong&gt; for the DERP server and a &lt;strong&gt;Python virtual environment&lt;/strong&gt; for Certbot and its Cloudflare DNS plugin, avoiding conflicts with system packages.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Prepare the Virtual Environment
&lt;/h4&gt;

&lt;p&gt;Install the necessary Python headers and create a dedicated space for Certbot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;python3-venv python3-pip &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="c"&gt;# Create the venv in /opt/certbot&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv /opt/certbot/
&lt;span class="nb"&gt;sudo&lt;/span&gt; /opt/certbot/bin/pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--upgrade&lt;/span&gt; pip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Install Certbot &amp;amp; Cloudflare Plugin
&lt;/h4&gt;

&lt;p&gt;Install the packages into the virtual environment and symlink the binary so you can run &lt;code&gt;certbot&lt;/code&gt; from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; /opt/certbot/bin/pip &lt;span class="nb"&gt;install &lt;/span&gt;certbot certbot-dns-cloudflare

&lt;span class="c"&gt;# Create a symlink for ease of use (use -f to overwrite if exists)&lt;/span&gt;
&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; /opt/certbot/bin/certbot /usr/bin/certbot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Configure Credentials
&lt;/h4&gt;

&lt;p&gt;Create a secure directory for your Cloudflare API token (in root’s home, since certbot runs as root):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /root/.secrets/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /root/.secrets/cloudflare.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;cloudflare.ini&lt;/code&gt;, add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;dns_cloudflare_api_token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;YOUR_CLOUDFLARE_TOKEN_HERE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security check: Run &lt;code&gt;sudo chmod 400 /root/.secrets/cloudflare.ini&lt;/code&gt; to restrict access.&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Request the Certificate (DNS-01 Challenge)
&lt;/h4&gt;

&lt;p&gt;Trigger the challenge. Certbot will create a temporary TXT record via Cloudflare, verify it, and pull your certs—no port 80 required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot certonly &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-cloudflare&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-cloudflare-credentials&lt;/span&gt; /root/.secrets/cloudflare.ini &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dns-cloudflare-propagation-seconds&lt;/span&gt; 60 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; derp.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--agree-tos&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--register-unsafely-without-email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Optional:&lt;/strong&gt; Add &lt;code&gt;--email your@example.com&lt;/code&gt; if you want Let’s Encrypt expiry notices.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Copy Certs and Deploy the Container
&lt;/h4&gt;

&lt;p&gt;The container reads certs from &lt;code&gt;/app/certs&lt;/code&gt;. Certbot stores them under &lt;code&gt;/etc/letsencrypt/live/&lt;/code&gt;. Copy (or symlink) them into &lt;code&gt;/root/derper-data&lt;/code&gt; with the names the image expects, then start the container. One-time setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SOURCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/letsencrypt/live/derp.example.com"&lt;/span&gt;
&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/root/derper-data"&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOURCE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/fullchain.pem &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/derp.example.com.crt
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOURCE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/privkey.pem &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/derp.example.com.key
&lt;span class="nb"&gt;chmod &lt;/span&gt;644 &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/derp.example.com.crt
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/derp.example.com.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  6. Start the Container
&lt;/h4&gt;

&lt;p&gt;Since we use &lt;strong&gt;manual&lt;/strong&gt; mode, mount the certificate directory to the path &lt;code&gt;fredliang/derper&lt;/code&gt; expects: &lt;code&gt;/app/certs&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; derper &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt; always &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--net&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /root/derper-data:/app/certs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /root/derper-data:/var/lib/derper &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DERP_DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;derp.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DERP_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;:3443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DERP_CERT_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;manual &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DERP_HTTP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DERP_VERIFY_CLIENTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  fredliang/derper:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;DERP_VERIFY_CLIENTS=false&lt;/code&gt; lets anyone use your DERP server. To restrict access to your Tailnet members, set to &lt;code&gt;true&lt;/code&gt; and add the tailscaled socket mount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/tailscale:/var/run/tailscale:ro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DERP_VERIFY_CLIENTS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This requires &lt;code&gt;tailscaled&lt;/code&gt; running on the host AND the Docker image’s derper version must match the host’s tailscaled version. Version mismatches cause verification failures. For simplicity, most self-hosted setups use &lt;code&gt;false&lt;/code&gt; since TLS still encrypts all traffic and DERP only relays already-encrypted WireGuard packets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Tailscale Side (The Controller)
&lt;/h3&gt;

&lt;p&gt;Now that the server is humming on port 3443, we need to tell the Tailscale network how to find it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into your &lt;a href="https://login.tailscale.com/admin/acls" rel="noopener noreferrer"&gt;Tailscale Admin Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Edit your &lt;strong&gt;ACL JSON&lt;/strong&gt; to include the &lt;code&gt;derpMap&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Crucial:&lt;/strong&gt; You must specify the &lt;code&gt;DERPPort&lt;/code&gt; as &lt;code&gt;3443&lt;/code&gt;. With &lt;code&gt;OmitDefaultRegions: true&lt;/code&gt;, your custom DERP is the only option—if it fails, clients have no relay fallback. With &lt;code&gt;OmitDefaultRegions: false&lt;/code&gt;, Tailscale may prefer default DERP regions over your custom node (due to latency heuristics or reachability issues), potentially causing unexpected routing. Test with &lt;code&gt;tailscale netcheck&lt;/code&gt; after setup to confirm your node is selected.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"derpMap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"OmitDefaultRegions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Regions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"901"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"RegionID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;901&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"RegionCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My-Custom-Node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Nodes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"RegionID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;901&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"HostName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"derp.example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"DERPPort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3443&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"IPv4"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_VPS_IP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"InsecureForTests"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Phase 4: Verification – The “200 OK” Moment
&lt;/h3&gt;

&lt;p&gt;Before you celebrate, run this from your &lt;strong&gt;local machine&lt;/strong&gt; (Mac/Linux/Windows). Replace &lt;code&gt;derp.example.com&lt;/code&gt; with your hostname if different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-Iv&lt;/span&gt; https://derp.example.com:3443
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Success&lt;/strong&gt;: You see &lt;code&gt;HTTP/1.1 200 OK&lt;/code&gt; and &lt;code&gt;SSL certificate verify ok&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure&lt;/strong&gt;: If it hangs on &lt;code&gt;Client Hello&lt;/code&gt;, your VPS firewall (iptables) is blocking port 3443, or the Docker container cannot read the certificate files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally, run &lt;code&gt;tailscale netcheck&lt;/code&gt; on your local device. When you see your custom Region ID with a low latency number, &lt;strong&gt;you are officially off the grid!&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 5: Automation – Renewal and Deploy Hook
&lt;/h3&gt;

&lt;p&gt;With the venv-based Certbot, use an explicit deploy hook for renewal. Create &lt;code&gt;/root/update_derp_cert.sh&lt;/code&gt;, make it executable (&lt;code&gt;chmod +x /root/update_derp_cert.sh&lt;/code&gt;), then use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Path to your Let's Encrypt live directory&lt;/span&gt;
&lt;span class="nv"&gt;SOURCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/etc/letsencrypt/live/derp.example.com"&lt;/span&gt;
&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/root/derper-data"&lt;/span&gt;

&lt;span class="c"&gt;# Copy and rename for the container&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOURCE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/fullchain.pem &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/derp.example.com.crt
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SOURCE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/privkey.pem &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/derp.example.com.key

&lt;span class="c"&gt;# Ensure permissions are correct&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;644 &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/derp.example.com.crt
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEST&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/derp.example.com.key

&lt;span class="c"&gt;# Restart container to pick up new certs&lt;/span&gt;
docker restart derper
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a crontab entry via &lt;code&gt;crontab -e&lt;/code&gt; to check for renewal daily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check for renewal daily at 3 AM&lt;/span&gt;
0 3 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/bin/certbot renew &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="nt"&gt;--deploy-hook&lt;/span&gt; /root/update_derp_cert.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your DERP node is now a set-it-and-forget-it powerhouse.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Using p7zip: Why You Should Switch to 7zz on Linux</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Sat, 28 Feb 2026 03:16:56 +0000</pubDate>
      <link>https://dev.to/lucifer1004/stop-using-p7zip-why-you-should-switch-to-7zz-on-linux-518j</link>
      <guid>https://dev.to/lucifer1004/stop-using-p7zip-why-you-should-switch-to-7zz-on-linux-518j</guid>
      <description>&lt;h2&gt;
  
  
  Stop Using p7zip: Why You Should Switch to 7zz on Linux
&lt;/h2&gt;

&lt;p&gt;Still using &lt;code&gt;p7zip&lt;/code&gt; on Linux? You’re working with software that hasn’t seen a significant update since &lt;strong&gt;2016&lt;/strong&gt;. While this community port served us well for years, the official &lt;strong&gt;7-Zip for Linux&lt;/strong&gt; has arrived—and it’s a game-changer.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: Stuck in 2016
&lt;/h3&gt;

&lt;p&gt;Many package managers still default to &lt;code&gt;p7zip&lt;/code&gt; version 16.02. This creates real problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Modern Instruction Sets:&lt;/strong&gt; The aging codebase can’t leverage &lt;strong&gt;AES-NI&lt;/strong&gt;, &lt;strong&gt;AVX2&lt;/strong&gt;, or &lt;strong&gt;AVX-512&lt;/strong&gt;—features your CPU has had for years.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compatibility Gaps:&lt;/strong&gt; Archives created with newer compression methods? Good luck opening them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Solution: Meet 7zz
&lt;/h3&gt;

&lt;p&gt;The official 7-Zip releases now include a &lt;strong&gt;standalone binary&lt;/strong&gt; called &lt;code&gt;7zz&lt;/code&gt;. Everything is baked into one executable—no external dependencies, no path headaches.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why It’s Faster
&lt;/h4&gt;

&lt;p&gt;Switching from &lt;code&gt;p7zip&lt;/code&gt; to &lt;code&gt;7zz&lt;/code&gt; delivers substantial performance gains on modern hardware:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hardware Acceleration&lt;/strong&gt; — Encryption and data operations now run at the silicon level using AES-NI and SIMD instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Superior Multithreading&lt;/strong&gt; — High-core-count CPUs finally get fully utilized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refined Algorithms&lt;/strong&gt; — Years of micro-optimizations to LZMA/LZMA2 dictionary searches add up to real-world speed improvements.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How to Switch
&lt;/h3&gt;

&lt;p&gt;If you’re using &lt;strong&gt;Pixi&lt;/strong&gt; or &lt;strong&gt;Conda&lt;/strong&gt;, install the modern &lt;code&gt;7zip&lt;/code&gt; package instead of &lt;code&gt;p7zip&lt;/code&gt;. Then, use the standalone command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Avoid the legacy version&lt;/span&gt;
7z x archive.zip

&lt;span class="c"&gt;# Use the modern standalone binary&lt;/span&gt;
7zz x archive.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Pro Tip: Keep Your Muscle Memory
&lt;/h4&gt;

&lt;p&gt;Add an alias to your &lt;code&gt;.bashrc&lt;/code&gt; or &lt;code&gt;.zshrc&lt;/code&gt;, and keep typing &lt;code&gt;7z&lt;/code&gt; like you always have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;7z&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'7zz'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  See the Difference Yourself
&lt;/h3&gt;

&lt;p&gt;Run the built-in benchmark to measure compression and decompression speeds on your hardware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;7zz b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare the results with your old &lt;code&gt;p7zip&lt;/code&gt; installation. The numbers speak for themselves.&lt;/p&gt;

</description>
      <category>cli</category>
      <category>linux</category>
      <category>performance</category>
      <category>tooling</category>
    </item>
    <item>
      <title>CLI is ALL you need</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Wed, 25 Feb 2026 23:56:41 +0000</pubDate>
      <link>https://dev.to/lucifer1004/cli-is-all-you-need-4n2o</link>
      <guid>https://dev.to/lucifer1004/cli-is-all-you-need-4n2o</guid>
      <description>&lt;h2&gt;
  
  
  CLI is ALL you need
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Starting from Obsidian CLI
&lt;/h3&gt;

&lt;p&gt;Not long ago, the veteran knowledge management software Obsidian officially announced its command-line tool &lt;a href="https://x.com/obsdmd/status/2021241384057930224" rel="noopener noreferrer"&gt;Obsidian CLI&lt;/a&gt;, which is now available in the &lt;code&gt;v1.12&lt;/code&gt; preview release. Obsidian CLI provides functionality completely equivalent to the Obsidian GUI version: from basic note retrieval, editing, and task management, to advanced developer features such as plugin management, screenshots, JS code execution, Chrome DevTools Protocol (CDP), console message handling, and DOM element retrieval, all are supported in Obsidian CLI.&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%2Fozweeijljck69fg65er0.png" 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%2Fozweeijljck69fg65er0.png" alt="Obsidian CLI -- Terminal Interface" width="800" height="922"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Some common Obsidian CLI commands are as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Open daily note&lt;/span&gt;
obsidian daily

&lt;span class="c"&gt;# Add task to daily note&lt;/span&gt;
obsidian daily:append &lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"- [ ] Buy groceries"&lt;/span&gt;

&lt;span class="c"&gt;# Search notes&lt;/span&gt;
obsidian search &lt;span class="nv"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"meeting notes"&lt;/span&gt;

&lt;span class="c"&gt;# List daily tasks&lt;/span&gt;
obsidian tasks daily

&lt;span class="c"&gt;# Create new note from template&lt;/span&gt;
obsidian create &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Trip to Paris"&lt;/span&gt; &lt;span class="nv"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Travel

&lt;span class="c"&gt;# List all tags with counts&lt;/span&gt;
obsidian tags counts

&lt;span class="c"&gt;# Take screenshot&lt;/span&gt;
obsidian dev:screenshot &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;screenshot.png

&lt;span class="c"&gt;# Execute JavaScript code&lt;/span&gt;
obsidian &lt;span class="nb"&gt;eval &lt;/span&gt;&lt;span class="nv"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app.vault.getFiles().length"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Now?
&lt;/h3&gt;

&lt;p&gt;Obsidian has a massive plugin and theme ecosystem, but for a long time did not release an official CLI tool. Three years ago, the community created &lt;a href="https://github.com/Yakitrak/notesmd-cli" rel="noopener noreferrer"&gt;&lt;code&gt;notesmd-cli&lt;/code&gt;&lt;/a&gt; (originally named &lt;code&gt;obsidian-cli&lt;/code&gt;, renamed after the official CLI was released), a command-line tool that can operate on the Obsidian database without opening the Obsidian application.&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%2Fyprz57dczem94aj5app0.png" 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%2Fyprz57dczem94aj5app0.png" alt="notesmd-cli Project Star Trend Chart" width="800" height="578"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;From the &lt;code&gt;notesmd-cli&lt;/code&gt; Star trend chart, we can clearly see that the project’s Star growth had been relatively stable since its inception, but after entering 2026, the project’s Star count experienced explosive growth.&lt;/p&gt;

&lt;p&gt;The driving force behind this growth is obvious: the viral popularity of OpenClaw🦞. This lobster sparked the biggest wave in the AI community since 2026. As one of the default configured tools for OpenClaw, &lt;code&gt;notesmd-cli&lt;/code&gt; naturally rode the wave of OpenClaw’s success.&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%2Fumj4qlttthogkgjzicwh.png" 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%2Fumj4qlttthogkgjzicwh.png" alt="OpenClaw Evolution History (Clawdbot -\&amp;gt; Moltbot -\&amp;gt; OpenClaw)" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Obsidian’s official release of CLI at this time undoubtedly indicates they saw the immense potential behind the &lt;code&gt;notesmd-cli&lt;/code&gt; project. Obsidian’s knowledge management and task tracking system perfectly fits the needs of AI assistants like OpenClaw. Compared to simple &lt;code&gt;MEMORY.md&lt;/code&gt; and &lt;code&gt;memory/&lt;/code&gt; folders, using Obsidian as an AI assistant’s memory bank is definitely a leap forward in productivity tools. The extensibility provided by over 2,000 plugins in the Obsidian community offers even more possibilities on top of this foundation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why CLI?
&lt;/h3&gt;

&lt;p&gt;So, why CLI?&lt;/p&gt;

&lt;p&gt;Because &lt;strong&gt;CLI is the natural language of AI Agents&lt;/strong&gt;. Current mainstream LLMs all have complete tool-calling capabilities, and command-line invocation is one of the most fundamental and important tools for LLMs. When an AI Agent has command-line calling capabilities, it has the power to do anything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;cat&lt;/code&gt; to read files&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;grep&lt;/code&gt; to search for keywords&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;sed&lt;/code&gt; to modify files&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;curl&lt;/code&gt; to access web pages or call APIs&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;git&lt;/code&gt; to manage code&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;ssh&lt;/code&gt; for remote access&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In fact, this is how humans used computers in the early days. No complex graphical interfaces needed, no fancy interaction methods required - a terminal and a keyboard could make a programmer feel like the world was in their hands.&lt;/p&gt;

&lt;p&gt;Similarly, when an OpenClaw bot gains access to hundreds of CLI tools, it truly transforms from a chatbot into an omnipotent AI assistant.&lt;/p&gt;

&lt;p&gt;Admittedly, current multimodal LLMs already possess powerful image and video understanding capabilities, and can use these abilities to control browsers and other desktop programs. However, this Computer Use approach consumes more tokens and also has greater uncertainty. The correctness of operations cannot be guaranteed; and correctly obtaining operation results is equally difficult (requiring another screenshot and parsing). In contrast, command-line invocation doesn’t rely on the LLM’s multimodal capabilities at all - text input, text output, everything seems incredibly natural.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Not MCP?
&lt;/h3&gt;

&lt;p&gt;The Model Context Protocol (MCP) is a protocol proposed by Anthropic in November 2024, aimed at providing a standardized way for LLMs to call external tools. Through MCP, LLMs can call external tools without needing to know the specific implementation details of the tools.&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%2Fqlwewcgddmnhsbm8nz0n.png" 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%2Fqlwewcgddmnhsbm8nz0n.png" alt="MCP Architecture Diagram (Source: modelcontextprotocol.io)" width="800" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MCP is good, but MCP also has quite a few problems, compared with CLI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MCP is a feature specifically for Agents, with no direct use value for human users. A large number of existing software need specialized packaging to enter the MCP ecosystem. CLI, on the other hand, was born with computer systems and is the most fundamental way humans interact with computers.&lt;/li&gt;
&lt;li&gt;MCP descriptions directly occupy Context. If hundreds of MCPs are enabled simultaneously, it means consuming tens of thousands of tokens during the system prompt phase, which directly increases API call costs and slows down response times. Moreover, these MCPs are not always useful for the current task, and conflicts may exist between MCPs, all of which can affect Agent execution precision. Some advanced users perform fine-grained control over MCPs used for each project, but this also brings additional workload and mental burden. In contrast, CLI provides self-explanatory documentation through the &lt;code&gt;--help&lt;/code&gt; option, requiring far fewer fixed tokens than MCP.&lt;/li&gt;
&lt;li&gt;There is currently no convenient mechanism for re-inserting MCP descriptions (writing as skills is a possible solution), which leads to imprecise Agent calls to MCP after Context grows. CLI-related information, on the other hand, can be inserted at the end of the current conversation at any time (for example, simply saying “please use the &lt;code&gt;xxx&lt;/code&gt; command”), ensuring the Agent always has relevant information.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s think again, what can MCP do that CLI cannot?&lt;/p&gt;

&lt;p&gt;The answer is clear: everything that can be done through MCP can also be done through CLI. Because MCP and CLI are essentially just two different user interfaces.&lt;/p&gt;

&lt;p&gt;CLI naturally possesses an important feature that MCP lacks: CLI invocations are essentially running Shell scripts, so &lt;strong&gt;they can easily form chained or decision-tree processes to achieve complex processing&lt;/strong&gt;; whereas MCP currently can only make single calls to get single results. To achieve condition-based flow control, it must rely on the LLM’s autonomous design, and the process requires multiple MCP calls, increasing complexity and uncertainty.&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI Design in the Agent Era
&lt;/h3&gt;

&lt;p&gt;But why has MCP received more attention than CLI in the AI era? On one hand, it’s human instinct to chase the new: MCP is a new product of the AI era, while CLI is an “antique” that has existed for decades; on the other hand, it’s because CLI has long lacked a rigorous specification system. What we have are just scattered conventions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single-character short commands like &lt;code&gt;-x&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Long commands like &lt;code&gt;--execute&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Help options like &lt;code&gt;-h|--help&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Space-separated arguments&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compared to MCP’s strict specifications, CLI’s freedom and looseness has become an obstacle preventing its widespread use by Agents.&lt;/p&gt;

&lt;p&gt;The key issue is not that CLI itself is inferior to MCP, but that traditional CLI design must change in the Agent era. Here I propose three core principles for CLI design in the Agent era:&lt;/p&gt;

&lt;h4&gt;
  
  
  Clear and Reasonable Hierarchical Structure
&lt;/h4&gt;

&lt;p&gt;A good CLI tool’s help information should be a clearly structured, well-organized document. We know that the core of current Agent applications lies in context engineering. Reasonable hierarchical design can help Agents load the tool context needed for the current task on-demand after a few &lt;code&gt;--help&lt;/code&gt; calls, thereby achieving better Context utilization efficiency.&lt;/p&gt;

&lt;h4&gt;
  
  
  Intuitive Command Style
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Use the same verbs for the same type of resources;&lt;/li&gt;
&lt;li&gt;Follow the same parameter order for the same type of verbs;&lt;/li&gt;
&lt;li&gt;Maintain consistent semantics for global options (such as &lt;code&gt;--verbose&lt;/code&gt;, &lt;code&gt;--dry-run&lt;/code&gt;, &lt;code&gt;--output&lt;/code&gt;, etc.);&lt;/li&gt;
&lt;li&gt;Option naming should follow conventions as much as possible, don’t invent new vocabulary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By following these principles, Agents can execute command calls with high accuracy without viewing help documentation (thus saving Context).&lt;/p&gt;

&lt;p&gt;A simple design approach: &lt;strong&gt;think about how you would describe this command in natural language (English)?&lt;/strong&gt; A command system that closely aligns with natural language can naturally enable Agents to achieve “what they think is what they see, what they see is what they get.”&lt;/p&gt;

&lt;h4&gt;
  
  
  Stable and Readable Structured Output
&lt;/h4&gt;

&lt;p&gt;In the Agent era, CLI tools are oriented toward AI Agents instead of humans, so their output must also follow Agent-oriented design principles and must have a stable structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can be formats like &lt;code&gt;--json&lt;/code&gt;, &lt;code&gt;--yaml&lt;/code&gt;, or in some cases, a stable text structure format (such as compilers, etc.);&lt;/li&gt;
&lt;li&gt;Have clear exit codes and parsable error fields.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Structured output allows Agents to easily perform information extraction (with tools like &lt;code&gt;jq&lt;/code&gt;) and process orchestration, thus fully leveraging the powerful capabilities of CLI.&lt;/p&gt;

&lt;p&gt;Besides these three core principles, there are some details worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write operations should support idempotency or &lt;code&gt;--dry-run&lt;/code&gt; options as much as possible, so Agents can preview and debug;&lt;/li&gt;
&lt;li&gt;For multi-agent and subagent architectures, CLI concurrency control needs to be considered. A simple file lock can avoid many write conflict issues.&lt;/li&gt;
&lt;li&gt;Complex features should ideally have observability, so Agents can better troubleshoot when problems occur during calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;The release of Obsidian’s official CLI tool is an important signal in the wave of AI assistants sparked by OpenClaw. When the MCP hype gradually settles down and we look back, the CLI that has accompanied us for decades has been there all along.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>agents</category>
    </item>
    <item>
      <title>Do not know how to optimize your agent skills? `skillc` helps you with detailed tracing and stats.</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Fri, 30 Jan 2026 13:46:09 +0000</pubDate>
      <link>https://dev.to/lucifer1004/do-not-know-how-to-optimize-your-agent-skills-skillc-helps-you-with-detailed-tracing-and-stats-4852</link>
      <guid>https://dev.to/lucifer1004/do-not-know-how-to-optimize-your-agent-skills-skillc-helps-you-with-detailed-tracing-and-stats-4852</guid>
      <description>&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/lucifer1004/introducing-skillc-the-development-kit-for-agent-skills-40hl" class="crayons-story__hidden-navigation-link"&gt;Introducing skillc: The Development Kit for Agent Skills&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/lucifer1004" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F124572%2F3ff4b5b1-3eee-4589-9e8b-d01ffeed163f.jpeg" alt="lucifer1004 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/lucifer1004" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Gabriel Wu
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Gabriel Wu
                
              
              &lt;div id="story-author-preview-content-3210624" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/lucifer1004" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F124572%2F3ff4b5b1-3eee-4589-9e8b-d01ffeed163f.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Gabriel Wu&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/lucifer1004/introducing-skillc-the-development-kit-for-agent-skills-40hl" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jan 30&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/lucifer1004/introducing-skillc-the-development-kit-for-agent-skills-40hl" id="article-link-3210624"&gt;
          Introducing skillc: The Development Kit for Agent Skills
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/skill"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;skill&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agents&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/cli"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;cli&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/rust"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;rust&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/lucifer1004/introducing-skillc-the-development-kit-for-agent-skills-40hl" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt; reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/lucifer1004/introducing-skillc-the-development-kit-for-agent-skills-40hl#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




</description>
      <category>skill</category>
      <category>agents</category>
      <category>compiler</category>
      <category>tracing</category>
    </item>
    <item>
      <title>Introducing skillc: The Development Kit for Agent Skills</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Fri, 30 Jan 2026 12:47:42 +0000</pubDate>
      <link>https://dev.to/lucifer1004/introducing-skillc-the-development-kit-for-agent-skills-40hl</link>
      <guid>https://dev.to/lucifer1004/introducing-skillc-the-development-kit-for-agent-skills-40hl</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; &lt;a href="https://github.com/govctl-org/skillc" rel="noopener noreferrer"&gt;skillc&lt;/a&gt; helps you create, validate, and optimize reusable knowledge packages that make AI coding agents smarter. Whether you're building skills or consuming them, skillc gives you the tools to work confidently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: AI Agents Forget Everything
&lt;/h2&gt;

&lt;p&gt;You've probably noticed this pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You explain a complex workflow to your AI coding agent&lt;/li&gt;
&lt;li&gt;It helps you perfectly... this one time&lt;/li&gt;
&lt;li&gt;Next session, you explain it all over again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;AI agents are stateless. They don't remember your team's conventions, your favorite libraries, or that obscure API you've mastered. Every conversation starts from zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agent Skills: Persistent Knowledge for AI Agents
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://agentskills.io/" rel="noopener noreferrer"&gt;Agent Skills&lt;/a&gt; solve this by giving agents &lt;strong&gt;persistent, structured knowledge&lt;/strong&gt; they can reference across sessions. A skill is just a directory with a &lt;code&gt;SKILL.md&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rust-skill/
├── SKILL.md          &lt;span class="c"&gt;# Metadata + instructions&lt;/span&gt;
└── docs/
    ├── patterns.md   &lt;span class="c"&gt;# Your coding patterns&lt;/span&gt;
    └── gotchas.md    &lt;span class="c"&gt;# Common pitfalls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent reads your skill and applies that knowledge contextually. No more repeating yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter skillc: The Development Kit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;skillc&lt;/strong&gt; is the CLI toolkit for working with Agent Skills. It handles two distinct workflows:&lt;/p&gt;

&lt;h3&gt;
  
  
  For Skill Authors: Build with Confidence
&lt;/h3&gt;

&lt;p&gt;Creating a skill that &lt;em&gt;actually helps&lt;/em&gt; agents requires validation. skillc provides the full authoring pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skc init  →  skc lint  →  skc build  →  skc stats  →  git push
    ↓            ↓            ↓             ↓             ↓
scaffold    validate    test locally   trace usage    publish
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a new skill&lt;/span&gt;
skc init rust-patterns

&lt;span class="c"&gt;# Validate structure, links, and quality&lt;/span&gt;
skc lint rust-patterns

&lt;span class="c"&gt;# Build locally and test with your agent&lt;/span&gt;
skc build rust-patterns

&lt;span class="c"&gt;# See how agents actually use your skill&lt;/span&gt;
skc stats rust-patterns &lt;span class="nt"&gt;--group-by&lt;/span&gt; sections

&lt;span class="c"&gt;# Publish when ready&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;skc stats&lt;/code&gt; command is particularly powerful — it shows you which sections agents access most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;skc stats rust-patterns &lt;span class="nt"&gt;--group-by&lt;/span&gt; sections

Skill: rust-patterns
Path: /Users/dev/.skillc/skills/rust-patterns
Query: Sections
Filters: &lt;span class="nv"&gt;since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;none&amp;gt;, &lt;span class="k"&gt;until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;none&amp;gt;, &lt;span class="nv"&gt;projects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;none&amp;gt;
Period: &lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-01-15T09:12:33+00:00, &lt;span class="nv"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-01-30T16:45:21+00:00
23      SKILL.md        Error Handling
18      SKILL.md        Iteration
12      SKILL.md        Lifetimes
9       references/async.md     Tokio Patterns
7       references/async.md     Channel Selection
5       references/testing.md   Mocking Strategies
3       references/testing.md   Property Testing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you know: agents need more content on error handling and iteration. This data helps you optimize for real usage patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  For Power Users: Unlock Search and Analytics
&lt;/h3&gt;

&lt;p&gt;Even if you're just &lt;em&gt;using&lt;/em&gt; skills, skillc adds superpowers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skc build  →  skc search  →  skc stats
     ↓             ↓              ↓
  indexing    find content   track usage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install any skill&lt;/span&gt;
npx skills add username/awesome-skill

&lt;span class="c"&gt;# Compile it to enable indexing&lt;/span&gt;
skc build awesome-skill

&lt;span class="c"&gt;# Full-text search across all content&lt;/span&gt;
skc search awesome-skill &lt;span class="s2"&gt;"error handling"&lt;/span&gt;

&lt;span class="c"&gt;# Track what your agents are actually reading&lt;/span&gt;
skc stats awesome-skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building a skill creates a search index, so you can quickly find relevant content without reading everything manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Example: A Rust Skill
&lt;/h2&gt;

&lt;p&gt;Here's a minimal but useful skill for Rust development. First, the &lt;code&gt;SKILL.md&lt;/code&gt; frontmatter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;rust-patterns&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idiomatic&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Rust&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;patterns&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;common&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;gotchas"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the content with practical patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error Handling section:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Always use &lt;code&gt;thiserror&lt;/code&gt; for library errors and &lt;code&gt;anyhow&lt;/code&gt; for applications:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Library code&lt;/span&gt;
&lt;span class="nd"&gt;#[derive(thiserror::Error,&lt;/span&gt; &lt;span class="nd"&gt;Debug)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;MyError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[error(&lt;/span&gt;&lt;span class="s"&gt;"invalid input: {0}"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="nf"&gt;InvalidInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Application code&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;do_thing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to do thing"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Iteration section:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prefer iterators over manual loops:&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Avoid&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Prefer&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After publishing, any agent with this skill installed will apply these patterns automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Integration: Direct Agent Access
&lt;/h2&gt;

&lt;p&gt;skillc includes an MCP (Model Context Protocol) server, so agents can query skills directly. Add it to your agent's MCP configuration:&lt;/p&gt;

&lt;p&gt;For example, in &lt;strong&gt;Cursor&lt;/strong&gt; (&lt;code&gt;.cursor/mcp.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"skillc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"skc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This exposes tools like &lt;code&gt;skc_search&lt;/code&gt;, &lt;code&gt;skc_show&lt;/code&gt;, &lt;code&gt;skc_outline&lt;/code&gt;, and &lt;code&gt;skc_stats&lt;/code&gt; that agents can call programmatically — no CLI needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;skillc

&lt;span class="c"&gt;# Create your first skill&lt;/span&gt;
skc init my-first-skill &lt;span class="nt"&gt;--global&lt;/span&gt;

&lt;span class="c"&gt;# Edit SKILL.md with your knowledge&lt;/span&gt;
&lt;span class="c"&gt;# Then validate and test&lt;/span&gt;
skc lint my-first-skill
skc build my-first-skill
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Will You Teach Your Agent?
&lt;/h2&gt;

&lt;p&gt;The best skills come from real expertise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework patterns&lt;/strong&gt; you've learned the hard way&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API quirks&lt;/strong&gt; that aren't in the docs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team conventions&lt;/strong&gt; that keep code consistent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging techniques&lt;/strong&gt; for specific tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your knowledge, captured once, applied forever.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/govctl-org/skillc" rel="noopener noreferrer"&gt;GitHub: skillc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://agentskills.io/" rel="noopener noreferrer"&gt;Agent Skills Spec&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://crates.io/crates/skillc" rel="noopener noreferrer"&gt;Crates.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;What skill would you create first? Drop a comment below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>skill</category>
      <category>agents</category>
      <category>cli</category>
      <category>rust</category>
    </item>
    <item>
      <title>I Built a CLI Because Cursor Keeps Losing My Chat History</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Mon, 19 Jan 2026 09:49:10 +0000</pubDate>
      <link>https://dev.to/lucifer1004/i-built-a-cli-because-cursor-keeps-losing-my-chat-history-30kc</link>
      <guid>https://dev.to/lucifer1004/i-built-a-cli-because-cursor-keeps-losing-my-chat-history-30kc</guid>
      <description>&lt;p&gt;You know that feeling when you rename a project folder and suddenly weeks of AI conversations just... vanish?&lt;/p&gt;

&lt;p&gt;I do. It happened to me three times before I decided to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Cursor IDE is incredible. But it has a dirty secret: &lt;strong&gt;your chat history is tied to your folder path&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Rename &lt;code&gt;my-project&lt;/code&gt; to &lt;code&gt;my-awesome-project&lt;/code&gt;? Gone.&lt;br&gt;
Move it from &lt;code&gt;~/dev/&lt;/code&gt; to &lt;code&gt;~/projects/&lt;/code&gt;? Gone.&lt;br&gt;
Clone it to a new location? Gone.&lt;/p&gt;

&lt;p&gt;The data isn't deleted — it's still sitting in &lt;code&gt;~/Library/Application Support/Cursor/User/workspaceStorage/&lt;/code&gt;(for MacOS). But Cursor can't find it anymore because the path changed.&lt;/p&gt;

&lt;p&gt;I searched for solutions. Found forum posts with manual workarounds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the old workspace folder by hash&lt;/li&gt;
&lt;li&gt;Copy contents to the new folder&lt;/li&gt;
&lt;li&gt;Edit &lt;code&gt;workspace.json&lt;/code&gt; to update the path&lt;/li&gt;
&lt;li&gt;Pray it works&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's insane. So I built a tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Fix: One Command
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;cursor-helper
cursor-helper rename /old/path /new/path

&lt;span class="c"&gt;# Or copy if you want to keep the old folder&lt;/span&gt;
cursor-helper rename &lt;span class="nt"&gt;-c&lt;/span&gt; /old/path /new/path
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That's it. Your chat history, workspace settings, MCP cache — everything stays intact.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Else It Does
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Export Chats (With the Good Stuff)
&lt;/h3&gt;

&lt;p&gt;Cursor's built-in export strips out thinking blocks and tool calls. Mine doesn't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Full export with AI reasoning, tool calls, and token counts&lt;/span&gt;
cursor-helper export-chat /path/to/project &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; full-chat-history.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### 💭 **Thinking** _12.3s_&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;Click to expand thinking...&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;

The user wants me to refactor the authentication module...
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;### 🔧 **Tool: edit_file** [completed]&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;Parameters&lt;span class="nt"&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;

{"path": "src/auth.rs", "old_string": "...", "new_string": "..."}
&lt;span class="nt"&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documenting how you solved complex problems&lt;/li&gt;
&lt;li&gt;Training yourself (or others) on AI-assisted workflows&lt;/li&gt;
&lt;li&gt;Keeping receipts when the AI does something clever (or dumb)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  List All Your Projects
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cursor-helper list &lt;span class="nt"&gt;--sort&lt;/span&gt; chats &lt;span class="nt"&gt;--limit&lt;/span&gt; 10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────┬────────────────────────────────┬───────┬──────────────────┐
│ Remote   │ Path                           │ Chats │ Modified         │
├──────────┼────────────────────────────────┼───────┼──────────────────┤
│ -        │ /Users/me/projects/cursor-help │ 47    │ 2026-01-19 16:04 │
│ tunnel   │ /home/user/big-project         │ 23    │ 2026-01-18 09:30 │
│ ssh      │ /workspace/client-work         │ 12    │ 2026-01-15 14:22 │
└──────────┴────────────────────────────────┴───────┴──────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clean Up Orphaned Data
&lt;/h3&gt;

&lt;p&gt;Deleted some projects? Cursor keeps the workspace data forever.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cursor-helper clean &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Found 8 orphaned workspace(s):

  /Users/me/.../abc123def456 (234.5 MB)
    Original: file:///Users/me/deleted-project

Total: 1.2 GB in 8 item(s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reclaim that disk space.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;Cursor stores workspace data in two places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;workspaceStorage/&amp;lt;hash&amp;gt;/&lt;/code&gt;&lt;/strong&gt; — Chat history, settings, state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;~/.cursor/projects/&amp;lt;folder-id&amp;gt;/&lt;/code&gt;&lt;/strong&gt; — MCP cache, terminal info&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The hash is computed as &lt;code&gt;MD5(absolutePath + birthtimeMs)&lt;/code&gt;. When you rename a folder, the path changes, the hash changes, and Cursor creates a new empty workspace.&lt;/p&gt;

&lt;p&gt;cursor-helper:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Finds the old workspace by scanning for matching folder URIs&lt;/li&gt;
&lt;li&gt;Moves/copies the project folder&lt;/li&gt;
&lt;li&gt;Moves/copies the workspace data to the new hash location&lt;/li&gt;
&lt;li&gt;Updates &lt;code&gt;workspace.json&lt;/code&gt; with the new path&lt;/li&gt;
&lt;li&gt;Updates &lt;code&gt;globalStorage/storage.json&lt;/code&gt; with the new workspace reference&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All in one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From crates.io&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;cursor-helper

&lt;span class="c"&gt;# Or grab a binary from GitHub Releases&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works on macOS, Linux (experimental), and Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Catch
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cursor must be closed&lt;/strong&gt; when running write operations (rename, clone, backup, restore, clean).&lt;/p&gt;

&lt;p&gt;Why? Cursor uses SQLite with WAL mode and keeps databases locked while running. If you modify files while it's open, you risk corruption or Cursor overwriting your changes on exit.&lt;/p&gt;

&lt;p&gt;Read-only commands (list, stats, export-chat) work fine while Cursor is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compared to Alternatives
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Rename support?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;cursor-helper&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Workspace management + export&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://specstory.com" rel="noopener noreferrer"&gt;SpecStory&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Auto-save chats to Markdown&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/ibrahim317/cursor-chat-transfer" rel="noopener noreferrer"&gt;cursor-chat-transfer&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Copy chats between workspaces&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/thomas-pedersen/cursor-chat-browser" rel="noopener noreferrer"&gt;cursor-chat-browser&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Browse/export chat history&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;cursor-helper is the only tool that solves the rename/move problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/lucifer1004/cursor-helper" rel="noopener noreferrer"&gt;lucifer1004/cursor-helper&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;crates.io&lt;/strong&gt;: &lt;a href="https://crates.io/crates/cursor-helper" rel="noopener noreferrer"&gt;cursor-helper&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT licensed. Contributions welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This tool is not affiliated with Anysphere/Cursor. It reads your local data files for backup and portability — no servers, no APIs, no telemetry.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Have you lost chat history to a rename? Let me know in the comments.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>ai</category>
      <category>vibecoding</category>
      <category>rust</category>
    </item>
    <item>
      <title>govctl: Opinionated CLI tool to enforce RFC-driven AI coding</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Sat, 17 Jan 2026 22:22:44 +0000</pubDate>
      <link>https://dev.to/lucifer1004/govctl-opinionated-cli-tool-to-enforce-rfc-driven-ai-coding-2ngi</link>
      <guid>https://dev.to/lucifer1004/govctl-opinionated-cli-tool-to-enforce-rfc-driven-ai-coding-2ngi</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;AI coding assistants are fast. They're also completely undisciplined.&lt;/p&gt;

&lt;p&gt;Here's a pattern I've seen (and lived) too many times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Day 1:  "Let's add caching!"
Day 2:  AI generates 500 lines of Redis integration
Day 7:  "Wait, did we agree on Redis or Memcached?"
Day 14: Half the team implements one, half the other
Day 30: Two incompatible caching layers, no spec, nobody knows why
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI didn't cause this. &lt;strong&gt;We did&lt;/strong&gt; — by skipping the specification phase.&lt;/p&gt;

&lt;p&gt;AI amplifies whatever workflow you feed it. Feed it chaos, get chaos faster. Feed it discipline, get discipline faster.&lt;/p&gt;

&lt;p&gt;I chose discipline.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Insight
&lt;/h2&gt;

&lt;p&gt;Every sustainable software project I've worked on had one thing in common: &lt;strong&gt;specification preceded implementation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not because of bureaucracy. Because clarity compounds.&lt;/p&gt;

&lt;p&gt;When you write down &lt;em&gt;what&lt;/em&gt; you're building before &lt;em&gt;how&lt;/em&gt;, you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Force yourself to think through edge cases&lt;/li&gt;
&lt;li&gt;Give reviewers something to review before 5,000 lines of code exist&lt;/li&gt;
&lt;li&gt;Create an audit trail for future maintainers (including future you)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem? Nothing enforces this. "Best practices" are suggestions. Suggestions get skipped when deadlines hit.&lt;/p&gt;

&lt;p&gt;So I built a tool that doesn't suggest. It enforces.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: govctl
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/govctl-org/govctl" rel="noopener noreferrer"&gt;govctl&lt;/a&gt; is a governance CLI that enforces phase discipline on software development.&lt;/p&gt;

&lt;p&gt;The core model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│  SPEC   │ ──► │   IMPL   │ ──► │   TEST   │ ──► │  STABLE  │
└─────────┘     └──────────┘     └──────────┘     └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Phases cannot be skipped. Gates are checked, not suggested.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start with a specification (RFC)&lt;/span&gt;
govctl new rfc &lt;span class="s2"&gt;"Caching Strategy"&lt;/span&gt;

&lt;span class="c"&gt;# Add clauses — atomic units of specification&lt;/span&gt;
govctl new clause RFC-0001:C-SCOPE &lt;span class="s2"&gt;"Scope"&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"Specification"&lt;/span&gt; &lt;span class="nt"&gt;-k&lt;/span&gt; normative

&lt;span class="c"&gt;# Edit the clause&lt;/span&gt;
govctl edit RFC-0001:C-SCOPE &lt;span class="nt"&gt;--stdin&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
The system MUST use Redis for session caching.
The TTL MUST be configurable via environment variable.
Cache invalidation MUST occur on user logout.
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Lock the spec&lt;/span&gt;
govctl finalize RFC-0001 normative

&lt;span class="c"&gt;# NOW you can implement&lt;/span&gt;
govctl advance RFC-0001 impl

&lt;span class="c"&gt;# Validate everything&lt;/span&gt;
govctl check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you try to implement against a draft RFC, &lt;code&gt;govctl check&lt;/code&gt; fails. That's the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Manages
&lt;/h2&gt;

&lt;p&gt;govctl handles three artifact types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;RFCs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Normative specifications — what will be built&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ADRs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Architecture Decision Records — why decisions were made&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Work Items&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Task tracking with acceptance criteria&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All stored as structured data (JSON/TOML), rendered to human-readable Markdown.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Proof: Dogfooding
&lt;/h2&gt;

&lt;p&gt;govctl governs itself.&lt;/p&gt;

&lt;p&gt;Every feature in the CLI was specified in an RFC before I wrote a line of code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/govctl-org/govctl/blob/main/docs/rfc/RFC-0000.md" rel="noopener noreferrer"&gt;RFC-0000: Governance Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/govctl-org/govctl/blob/main/docs/rfc/RFC-0001.md" rel="noopener noreferrer"&gt;RFC-0001: Lifecycle State Machines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can trace any line of code back to its specification. This isn't documentation — it's &lt;strong&gt;proof that the model works&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Teams frustrated by AI "code now, think later" patterns
&lt;/li&gt;
&lt;li&gt;✅ Projects where specs drift from implementations
&lt;/li&gt;
&lt;li&gt;✅ Organizations needing audit trails for AI-generated code
&lt;/li&gt;
&lt;li&gt;✅ Developers who believe &lt;strong&gt;discipline enables velocity&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;❌ Not for "move fast and break things" workflows
&lt;/li&gt;
&lt;li&gt;❌ Not for projects without review processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't for everyone, and that's okay.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;govctl

&lt;span class="c"&gt;# With TUI dashboard&lt;/span&gt;
cargo &lt;span class="nb"&gt;install &lt;/span&gt;govctl &lt;span class="nt"&gt;--features&lt;/span&gt; tui

&lt;span class="c"&gt;# Initialize&lt;/span&gt;
govctl init

&lt;span class="c"&gt;# Create your first RFC&lt;/span&gt;
govctl new rfc &lt;span class="s2"&gt;"My Feature"&lt;/span&gt;

&lt;span class="c"&gt;# Validate&lt;/span&gt;
govctl check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full documentation: &lt;a href="https://govctl-org.github.io/govctl/" rel="noopener noreferrer"&gt;govctl User Guide&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a CLI?
&lt;/h2&gt;

&lt;p&gt;I considered building an MCP integration, a VS Code extension, a web app.&lt;/p&gt;

&lt;p&gt;Then I realized: &lt;strong&gt;every AI coding agent already speaks shell&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Claude, Cursor, Codex, Copilot — they can all run commands. The CLI &lt;em&gt;is&lt;/em&gt; the universal interface. No JSON-RPC negotiation. No custom protocols. Just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;govctl new rfc &lt;span class="s2"&gt;"Feature Title"&lt;/span&gt;
govctl check
govctl advance RFC-0010 impl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Complexity avoided. Capability preserved.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI-Native: Embedded Claude Commands
&lt;/h2&gt;

&lt;p&gt;govctl ships with a built-in &lt;a href="https://github.com/govctl-org/govctl/blob/main/.claude/commands/gov.md" rel="noopener noreferrer"&gt;Claude command&lt;/a&gt; that teaches AI agents the complete governed workflow.&lt;/p&gt;

&lt;p&gt;Just type &lt;code&gt;/gov add user authentication&lt;/code&gt; and Claude will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create or activate a work item&lt;/li&gt;
&lt;li&gt;Check if an RFC is needed&lt;/li&gt;
&lt;li&gt;Create the RFC with proper clauses&lt;/li&gt;
&lt;li&gt;Ask permission before advancing phases&lt;/li&gt;
&lt;li&gt;Implement the feature&lt;/li&gt;
&lt;li&gt;Run tests&lt;/li&gt;
&lt;li&gt;Tick acceptance criteria&lt;/li&gt;
&lt;li&gt;Mark the work item done&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The command encodes the entire workflow — phase discipline, RFC supremacy, commit conventions — so the AI follows governance rules automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In Cursor or Claude Code&lt;/span&gt;
/gov implement caching with Redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI becomes a &lt;strong&gt;constrained autonomous agent&lt;/strong&gt; that respects your governance model.&lt;/p&gt;




&lt;h2&gt;
  
  
  Built With
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; (edition 2024) — for correctness and speed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clap&lt;/strong&gt; — CLI framework&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serde&lt;/strong&gt; — serialization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ratatui&lt;/strong&gt; — optional TUI dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MIT licensed. Contributions welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;AI coding tools are here to stay. The question isn't whether to use them — it's how to use them without losing control.&lt;/p&gt;

&lt;p&gt;govctl is my answer: &lt;strong&gt;specification-first, phase-gated, governance as code&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If that resonates, give it a try:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/govctl-org/govctl" rel="noopener noreferrer"&gt;github.com/govctl-org/govctl&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;"Discipline is not the opposite of creativity. It is the foundation."&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vibecoding</category>
      <category>cli</category>
      <category>rust</category>
    </item>
    <item>
      <title>Compile OpenCV4</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Mon, 06 May 2019 11:53:18 +0000</pubDate>
      <link>https://dev.to/lucifer1004/compile-opencv4-36oe</link>
      <guid>https://dev.to/lucifer1004/compile-opencv4-36oe</guid>
      <description>&lt;p&gt;Since OpenCV4 does not generata .pc file by default, we need to compile it from source and turn on the &lt;code&gt;OPENCV_GENERATE_PKGCONFIG&lt;/code&gt; option if other programs need the pkg-config file.&lt;/p&gt;

&lt;p&gt;Another problem I encountered is a number of errors similar to &lt;code&gt;undefined reference to TIFFReadRGBAStrip@LIBTIFF_4.0&lt;/code&gt;. To solve that, the &lt;code&gt;BUILD_TIFF&lt;/code&gt; option needs to be turned on.&lt;/p&gt;

&lt;p&gt;And following is the script for compilation and installation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://github.com/opencv/opencv/archive/4.1.0.zip
unzip opencv-4.1.0.zip
&lt;span class="nb"&gt;cd &lt;/span&gt;opencv-4.1.0
&lt;span class="nb"&gt;mkdir &lt;/span&gt;build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;build
cmake &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nv"&gt;CMAKE_BUILD_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Release &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nv"&gt;OPENCV_GENERATE_PKGCONFIG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;YES &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nv"&gt;CMAKE_INSTALL_PREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/local &lt;span class="nt"&gt;-D&lt;/span&gt; &lt;span class="nv"&gt;BUILD_TIFF&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON
&lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nt"&gt;-j&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;nproc&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;make &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>opencv</category>
    </item>
    <item>
      <title>My GDAL CLI snippets</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Thu, 07 Mar 2019 15:05:17 +0000</pubDate>
      <link>https://dev.to/lucifer1004/my-gdal-cli-snippets-12ci</link>
      <guid>https://dev.to/lucifer1004/my-gdal-cli-snippets-12ci</guid>
      <description>&lt;h2&gt;
  
  
  To crop a raster file according to a shapefile
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdalwarp &lt;span class="nt"&gt;-overwrite&lt;/span&gt; &lt;span class="nt"&gt;-s_srs&lt;/span&gt; EPSG:32649 &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-cutline&lt;/span&gt; shapefile.shp &lt;span class="nt"&gt;-of&lt;/span&gt; GTiff raw.tiff crop.tiff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  To merge several bands into a raster file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdal_merge.py &lt;span class="nt"&gt;-o&lt;/span&gt; target.tiff &lt;span class="nt"&gt;-of&lt;/span&gt; GTiff &lt;span class="nt"&gt;-ps&lt;/span&gt; 10 10 &lt;span class="nt"&gt;-separate&lt;/span&gt; B1.tiff B2.tiff B3.tiff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;10 10&lt;/code&gt; means 10 meters in the x-axis and 10 meters in the y-axis. If &lt;code&gt;-ps&lt;/code&gt; is not specified, the spatial resolution of the first raster file will be considered to be the target resolution.&lt;/p&gt;

&lt;h2&gt;
  
  
  To merge several files spatially
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdal_merge.py &lt;span class="nt"&gt;-o&lt;/span&gt; target.tiff &lt;span class="nt"&gt;-of&lt;/span&gt; GTiff TL.tiff BL.tiff TR.tiff BR.tiff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  To resample a raster file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdal_translate &lt;span class="nt"&gt;-outsize&lt;/span&gt; 50 50 raw.jp2 target.jp2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here &lt;code&gt;50 50&lt;/code&gt; means 50% in x-axis, and 50% in y-axis.&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix: Install GDAL for Python on Ubuntu
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install gdal library&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; gdal-bin libgdal-dev
&lt;span class="c"&gt;# gdal-bin is not necessary, but you will definitely need it to run the CLI commands above&lt;/span&gt;

&lt;span class="c"&gt;# Export include path&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;CPLUS_INCLUDE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/include/gdal
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;C_INCLUDE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/include/gdal

&lt;span class="c"&gt;# Install the exact version of Python package&lt;/span&gt;
gdal-config &lt;span class="nt"&gt;--version&lt;/span&gt; | xargs &lt;span class="nt"&gt;-0&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;&lt;span class="nv"&gt;gdal&lt;/span&gt;&lt;span class="o"&gt;=={}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>gdal</category>
      <category>gis</category>
    </item>
    <item>
      <title>How I wrote my own React wrapper for Google Map</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Wed, 27 Feb 2019 13:27:00 +0000</pubDate>
      <link>https://dev.to/lucifer1004/how-i-wrote-my-own-react-wrapper-for-google-map-15ei</link>
      <guid>https://dev.to/lucifer1004/how-i-wrote-my-own-react-wrapper-for-google-map-15ei</guid>
      <description>

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ljF7H-4v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://images.unsplash.com/photo-1451988336904-b1a6e8846746%3Fixlib%3Drb-1.2.1%26ixid%3DeyJhcHBfaWQiOjEyMDd9%26auto%3Dformat%26fit%3Dcrop%26w%3D1000%26q%3D60" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ljF7H-4v--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://images.unsplash.com/photo-1451988336904-b1a6e8846746%3Fixlib%3Drb-1.2.1%26ixid%3DeyJhcHBfaWQiOjEyMDd9%26auto%3Dformat%26fit%3Dcrop%26w%3D1000%26q%3D60" alt="A map"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few months ago, when I started the Neighborhood Map project of Udacity, I first checked the libraries available. There were quite a few choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/tomchentw/react-google-maps"&gt;tomchentw/react-google-maps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/google-map-react/google-map-react"&gt;google-map-react/google-map-react&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fullstackreact/google-maps-react"&gt;fullstackreact/google-maps-react&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, none of them could meet my requirements (it was also possible that I did not figure out the proper way to deal with the problems). I want the components to be flexible, e.g., the &lt;code&gt;Marker&lt;/code&gt; component does not necessarily to be placed within a &lt;code&gt;Map&lt;/code&gt; component. This flexibility is essential when designing the layouts, and also when structuring components so as not to trigger unnecessary rerender.&lt;/p&gt;

&lt;p&gt;What they provide (generally):&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Marker&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InfoWindow&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Map&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;What I want:&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Map&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ComponentA&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Marker&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ComponentB&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InfoWindow&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ComponentB&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ComponentA&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The idea came into my mind that I could write my own React wrapper for Google Map. This sounded a bit audacious because I had never written a React component library before. As the deadline of the Udacity project came closer and closer, I finally made up my mind to write my own Google Map library, with React hooks and TypeScript, and TDD.&lt;/p&gt;

&lt;p&gt;Although I had not written a React component library, I had written a very simple Vue component library (following instructions of a blog). I had been writing TypeScript for several months, and had written a React app with context and hooks. And I had used TDD in several projects. These experiences gave me confidence.&lt;/p&gt;

&lt;p&gt;Yet challenges did come, one after another. I had written some tests, but I had not written library mocks. I managed to mock &lt;code&gt;loadjs&lt;/code&gt;, which I used to load Google Map scripts.&lt;/p&gt;

&lt;p&gt;Another problem was that hooks live with functional components, and functional components do not have instances. Other Google Map libraries all use class components, and implement methods for class instances to surrogate Google Map objects' methods. But I could not do so. In the end, I chose to maintain an id-object Map to store references to all Google Map objects. It worked fluently, and could be used without using React &lt;code&gt;ref&lt;/code&gt; (class instances rely on &lt;code&gt;ref&lt;/code&gt;). The only price was that things like &lt;code&gt;Marker&lt;/code&gt;, &lt;code&gt;Polygon&lt;/code&gt; would require a unique &lt;code&gt;id&lt;/code&gt; when using my library.&lt;/p&gt;

&lt;p&gt;At first, I just thought about my own needs, and the API design was way too casual (you can check &lt;a href="https://github.com/lucifer1004/react-google-map"&gt;my original repo&lt;/a&gt; and time-travel to earlier versions). Later, my personal project was finished, but I knew a lot of things were still up in the air.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qF2jUiUG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/github-logo-6a5bca60a4ebf959a6df7f08217acd07ac2bc285164fae041eacb8a148b1bab9.svg"&gt;&lt;a href="https://github.com/lucifer1004"&gt;lucifer1004&lt;/a&gt; / &lt;a href="https://github.com/lucifer1004/boycott"&gt;boycott&lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;A map app.&lt;/h3&gt;
  &lt;/div&gt;
&lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="instapaper_body md"&gt;
&lt;h1&gt;
Boycott&lt;/h1&gt;
&lt;p&gt;This is a Udacity project. It is statically deployed
&lt;a href="https://boycott.gabriel-wu.com" rel="nofollow"&gt;here&lt;/a&gt; via Now.&lt;/p&gt;
&lt;h2&gt;
To run it locally&lt;/h2&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;git clone https://github.com/lucifer1004/boycott
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; boycott
yarn install
yarn start&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can then visit it at &lt;code&gt;localhost:3000&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;
Features&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Search for places using Yelp Fusion API (&lt;code&gt;cors-anywhere&lt;/code&gt; is used to address
the CORS issue)&lt;/li&gt;
&lt;li&gt;Filter options: All/Open/High Rating/Low Price&lt;/li&gt;
&lt;li&gt;Use of Google Map API is via
&lt;a href="https://github.com/lucifer1004/react-google-map"&gt;&lt;code&gt;@lucifer1004/react-google-map&lt;/code&gt;&lt;/a&gt;,
which is a React wrapper for Google Map written by myself.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lucifer1004/boycott"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;It is a simple React app, using Google Map and Yelp to implement basic place search.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XapbWhl_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/googlemap-react/googlemap-react/raw/master/images/boycott.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XapbWhl_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://github.com/googlemap-react/googlemap-react/raw/master/images/boycott.png" alt="Screenshot from Boycott"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After submitting the project at Udacity, I went on with my library. For my personal project's needs, I only implemented &lt;code&gt;MapBox&lt;/code&gt;, &lt;code&gt;Marker&lt;/code&gt;, &lt;code&gt;InfoWindow&lt;/code&gt;, &lt;code&gt;HeatMap&lt;/code&gt; and &lt;code&gt;Polygon&lt;/code&gt;. There were around 20 more Google Map components.&lt;/p&gt;

&lt;p&gt;It happened several times that I had to refactor the whole library when trying to implement a new component. Luckily, I wrote unit tests for each component, and those tests helped a lot during refactors.&lt;/p&gt;

&lt;p&gt;I spent about two weeks' spare time implementing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;other shapes: Circle, Polyline, Rectangle&lt;/li&gt;
&lt;li&gt;layers: BicycleLayer, TrafficLayer, TransitLayer&lt;/li&gt;
&lt;li&gt;search: SearchBox, StandaloneSearchBox&lt;/li&gt;
&lt;li&gt;streetview: StreetView, StandaloneStreetView&lt;/li&gt;
&lt;li&gt;overlays: CustomControl, GroundOverlay, KmlLayer, OverlayView&lt;/li&gt;
&lt;li&gt;drawing: DrawingManager&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The library started from &lt;code&gt;create-react-app&lt;/code&gt;, I used a separate &lt;code&gt;package.json&lt;/code&gt; in &lt;code&gt;src/lib&lt;/code&gt; to configure the library, while the app was configured by the root level &lt;code&gt;package.json&lt;/code&gt;. As the library grew, I felt I should set up a monorepo properly.&lt;/p&gt;

&lt;p&gt;The week of refactoring project structure was rather tough. I read many blogs and posts on monorepos, but still could not get everything work as I hoped. I gave up once, and nearly gave up again the second time, and I made it.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;lerna&lt;/code&gt; and &lt;code&gt;yarn workspaces&lt;/code&gt;, and a custom symlink，I was finally pleased with the new structure. By running &lt;code&gt;yarn dev:lib&lt;/code&gt; and &lt;code&gt;yarn dev:CRA&lt;/code&gt; at the same time, the example CRA app would reload each time I changed the code of the library.&lt;/p&gt;

&lt;p&gt;I really appreciate that I decided to write my own wrapper library a month ago, otherwise I would not have learnt so much (I am going to write more posts in the series to talk about things I have learnt in detail). And I will try to further improve my library. It has not been tested in real projects. Compared to existing libraries, some functions are missing. Also there are some known issues, or limitations.&lt;/p&gt;

&lt;p&gt;I am prepared.&lt;/p&gt;




&lt;p&gt;Recently, I moved this project to a separate organization. Below is the repo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XtIqs6Mo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://avatars1.githubusercontent.com/u/47935106%3Fs%3D200%26v%3D4" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XtIqs6Mo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://avatars1.githubusercontent.com/u/47935106%3Fs%3D200%26v%3D4" alt="logo of @googlemap-react"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qF2jUiUG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://practicaldev-herokuapp-com.freetls.fastly.net/assets/github-logo-6a5bca60a4ebf959a6df7f08217acd07ac2bc285164fae041eacb8a148b1bab9.svg"&gt;&lt;a href="https://github.com/googlemap-react"&gt;googlemap-react&lt;/a&gt; / &lt;a href="https://github.com/googlemap-react/googlemap-react"&gt;googlemap-react&lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;Easier Google Map Integration for React projects.&lt;/h3&gt;
  &lt;/div&gt;
&lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="instapaper_body md"&gt;
&lt;h1&gt;
googlemap-react&lt;/h1&gt;
&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@googlemap-react/core" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/972d6577cf4c80fe3f53e7a95d89ec794129d442/68747470733a2f2f696d672e736869656c64732e696f2f6e706d2f762f40676f6f676c656d61702d72656163742f636f72652f6c61746573742e737667" alt="npm package"&gt;&lt;/a&gt;
&lt;a href="https://opensource.org/licenses/MIT" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/3ccf4c50a1576b0dd30b286717451fa56b783512/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/googlemap-react/googlemap-react" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/94107e959e84f4e66d601aca1761591dcc1a99f8/68747470733a2f2f636f6465636f762e696f2f67682f676f6f676c656d61702d72656163742f676f6f676c656d61702d72656163742f6272616e63682f6d61737465722f67726170682f62616467652e737667" alt="codecov"&gt;&lt;/a&gt;
&lt;a href="https://codebeat.co/projects/github-com-googlemap-react-googlemap-react-master" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/9bccc3a5063800f3639eb58db3c009af3cd57bae/68747470733a2f2f636f6465626561742e636f2f6261646765732f35663264333766642d306537622d343333352d613034332d343737353036633434313863" alt="codebeat badge"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://spectrum.chat/googlemap-react" rel="nofollow"&gt;&lt;img src="https://camo.githubusercontent.com/3cc3d27f23a2c3948de24fc02c58bc576655d621/68747470733a2f2f77697468737065637472756d2e6769746875622e696f2f62616467652f62616467652e737667" alt="Join the community on Spectrum"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Easier Google Map Integration for React projects.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://doc.googlemap-react.com" rel="nofollow"&gt;READ THE DOC&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
Why a new package&lt;/h2&gt;
&lt;p&gt;There has been similar packages such as
&lt;a href="https://github.com/tomchentw/react-google-maps"&gt;tomchentw/react-google-maps&lt;/a&gt;
&lt;a href="https://github.com/google-map-react/google-map-react"&gt;google-map-react/google-map-react&lt;/a&gt;
&lt;a href="https://github.com/fullstackreact/google-maps-react"&gt;fullstackreact/google-maps-react&lt;/a&gt;
so why bother writing a new library?&lt;/p&gt;
&lt;p&gt;The aim is to make an easier-to-use Google Map library for React users,
empowered by &lt;code&gt;React&lt;/code&gt;'s latest features (&lt;code&gt;React &amp;gt;= 16.8.0&lt;/code&gt; is required) and
&lt;code&gt;TypeScript&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
What is different&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Component position is free (generally)&lt;/li&gt;
&lt;li&gt;Direct access to Google Map objects&lt;/li&gt;
&lt;li&gt;More uniform API&lt;/li&gt;
&lt;li&gt;Type safe&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
Example usage&lt;/h2&gt;
&lt;h3&gt;
Prerequisites&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;npm or yarn&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;yarn add @googlemap-react/core
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Or you can use&lt;/span&gt;
npm install --save @googlemap-react/core&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;a valid Google Map API key (to replace the place holder in the code snippet
below)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight highlight-source-js"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; {
  &lt;span class="pl-smi"&gt;GoogleMapProvider&lt;/span&gt;
  &lt;span class="pl-smi"&gt;HeatMap&lt;/span&gt;,
  &lt;span class="pl-smi"&gt;InfoWindow&lt;/span&gt;,
  &lt;span class="pl-smi"&gt;MapBox&lt;/span&gt;,
  &lt;span class="pl-smi"&gt;Marker&lt;/span&gt;,
  &lt;span class="pl-smi"&gt;Polygon&lt;/span&gt;,
} &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;@lucifer1004/react-google-map&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;//&lt;/span&gt; In your component&lt;/span&gt;
&lt;span class="pl-k"&gt;return&lt;/span&gt; (
  &lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;GoogleMapProvider&lt;span class="pl-k"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt;MapBox
      apiKey&lt;span class="pl-k"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;YOUR_GOOGLE_MAP_API_KEY&lt;/span&gt;&lt;/pre&gt;…&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
&lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/googlemap-react/googlemap-react"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Any advice or suggestions are welcome! If you want to use my library and run into any problem, just ask me!&lt;/p&gt;

&lt;p&gt;If you want to join, that would be great!&lt;/p&gt;


</description>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
      <category>googlemap</category>
    </item>
    <item>
      <title>Simple way to diff server/browser rendering results</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Sun, 24 Feb 2019 11:00:52 +0000</pubDate>
      <link>https://dev.to/lucifer1004/simple-way-to-diff-serverbrowser-rendering-results-4d84</link>
      <guid>https://dev.to/lucifer1004/simple-way-to-diff-serverbrowser-rendering-results-4d84</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I found this &lt;a href="https://stackoverflow.com/questions/53571192/react-nextjs-how-to-debug-different-nodes-of-ssr-react-application" rel="noopener noreferrer"&gt;answer&lt;/a&gt; at StackOverflow. I think it is useful, so I share with you here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Browser side
&lt;/h2&gt;

&lt;p&gt;First of all, you should ensure the site is served. Then you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open your site in any browser&lt;/li&gt;
&lt;li&gt;Open the browser console, and type:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Copy the output, and save it as &lt;code&gt;client.html&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Server side
&lt;/h2&gt;

&lt;p&gt;Supposing your site is running on &lt;code&gt;localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open a shell, and type:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl localhost:3000 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; server.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Compare the differences
&lt;/h2&gt;

&lt;p&gt;You can simply run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;diff client.html server.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note that you may need to format both files before diff.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>javascript</category>
      <category>frontend</category>
      <category>ssr</category>
    </item>
    <item>
      <title>It is NOT always right to pin your dependencies</title>
      <dc:creator>Gabriel Wu</dc:creator>
      <pubDate>Tue, 19 Feb 2019 01:21:30 +0000</pubDate>
      <link>https://dev.to/lucifer1004/it-is-not-always-right-to-pin-your-dependencies-36jg</link>
      <guid>https://dev.to/lucifer1004/it-is-not-always-right-to-pin-your-dependencies-36jg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR;&lt;br&gt;
If you are writing a library/package, make sure you will update your dependencies in time before pinning all dependencies. Otherwise, issues may be caused.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You might have used tools like &lt;a href="https://github.com/renovatebot/renovate" rel="noopener noreferrer"&gt;&lt;code&gt;renovate&lt;/code&gt;&lt;/a&gt; to manage dependencies of your GitHub repository. The first thing it will do is to pin your dependencies.&lt;/p&gt;

&lt;p&gt;Pinning dependencies can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Avoid potential bugs caused by upstream package/library updates. (&lt;em&gt;Such bugs are still possible because upstream packages may not have pinned their dependencies.&lt;/em&gt;) &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Help your collaborators or people who are interested in your project to reproduce your dev environment. (&lt;em&gt;It will be even sweeter if combined with docker.&lt;/em&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, it can also cause issues sometimes. And I am going to share with you a recent experience.&lt;/p&gt;

&lt;p&gt;I am doing a Google Map project, and I have two repositories for this. (I tried &lt;code&gt;lerna&lt;/code&gt;, but failed to get everything to work well and had to give up.)&lt;/p&gt;

&lt;p&gt;One is a React wrapper around Google Map API:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lucifer1004" rel="noopener noreferrer"&gt;
        lucifer1004
      &lt;/a&gt; / &lt;a href="https://github.com/lucifer1004/react-google-map" rel="noopener noreferrer"&gt;
        react-google-map
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Easier Google Map Integration for React projects.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;React Google Map&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@lucifer1004/react-google-map" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6d4c83dd8545d3876043a96c4a2092d65362581d3650f21918ed430f609ee862/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f2534306c7563696665723130303425324672656163742d2d676f6f676c652d2d6d61702d332e302e302d626c75652e737667" alt="version"&gt;&lt;/a&gt;
&lt;a href="https://opensource.org/licenses/MIT" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/6cd0120cc4c5ac11d28b2c60f76033b52db98dac641de3b2644bb054b449d60c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d79656c6c6f772e737667" alt="License: MIT"&gt;&lt;/a&gt;
&lt;a href="https://codecov.io/gh/lucifer1004/react-google-map" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/7d740b5c805fe20d9137464aa62a5953b20517bb311919369ea584c2862a052b/68747470733a2f2f636f6465636f762e696f2f67682f6c756369666572313030342f72656163742d676f6f676c652d6d61702f6272616e63682f6d61737465722f67726170682f62616467652e737667" alt="codecov"&gt;&lt;/a&gt;
&lt;a href="https://codebeat.co/projects/github-com-lucifer1004-react-google-map-master" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/54fb67b0482d65c8db41ba9fb475e01a4ac5ddc6db74d7d1138a479342b41cfd/68747470733a2f2f636f6465626561742e636f2f6261646765732f65376135623036342d323737622d343936642d393532382d366662383335656236616434" alt="codebeat badge"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Easier Google Map Integration for React projects.&lt;/p&gt;
&lt;blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;NOTE&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;This project has been moved to
&lt;a href="https://github.com/googlemap-react/googlemap-react" rel="noopener noreferrer"&gt;@googlemap-react&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://react-google-map.gabriel-wu.com" rel="nofollow noopener noreferrer"&gt;READ THE DOC&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why a new package&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;There has been similar packages such as
&lt;a href="https://github.com/tomchentw/react-google-maps" rel="noopener noreferrer"&gt;tomchentw/react-google-maps&lt;/a&gt;,
&lt;a href="https://github.com/google-map-react/google-map-react" rel="noopener noreferrer"&gt;google-map-react/google-map-react&lt;/a&gt;,
&lt;a href="https://github.com/fullstackreact/google-maps-react" rel="noopener noreferrer"&gt;fullstackreact/google-maps-react&lt;/a&gt;,
so why bother writing a new library?&lt;/p&gt;
&lt;p&gt;The aim is to make an easier-to-use Google Map library for React users,
empowered by &lt;code&gt;React&lt;/code&gt;'s latest features (&lt;code&gt;React &amp;gt;= 16.8.0&lt;/code&gt; is required) and
&lt;code&gt;TypeScript&lt;/code&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What is different&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Component position is free (generally)&lt;/li&gt;
&lt;li&gt;Direct access to Google Map objects&lt;/li&gt;
&lt;li&gt;More uniform API&lt;/li&gt;
&lt;li&gt;Type safe&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Example usage&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Prerequisites&lt;/h3&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;npm or yarn&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;yarn add &lt;a class="mentioned-user" href="https://dev.to/lucifer1004"&gt;@lucifer1004&lt;/a&gt;/react-google-map
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Or you can use&lt;/span&gt;
npm install --save &lt;a class="mentioned-user" href="https://dev.to/lucifer1004"&gt;@lucifer1004&lt;/a&gt;/react-google-map&lt;/pre&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;a valid Google Map API key (to replace the place holder in the code snippet
below)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight highlight-source-js notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-v"&gt;GoogleMapProvider&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-v"&gt;HeatMap&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-v"&gt;InfoWindow&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-v"&gt;MapBox&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-v"&gt;Marker&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-v"&gt;Polygon&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;'&lt;a class="mentioned-user" href="https://dev.to/lucifer1004"&gt;@lucifer1004&lt;/a&gt;/react-google-map'&lt;/span&gt;
&lt;span class="pl-c"&gt;// In your component&lt;/span&gt;
&lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lucifer1004/react-google-map" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The other is the application:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lucifer1004" rel="noopener noreferrer"&gt;
        lucifer1004
      &lt;/a&gt; / &lt;a href="https://github.com/lucifer1004/boycott" rel="noopener noreferrer"&gt;
        boycott
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A map app.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Boycott&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;This is a Udacity project. It is statically deployed
&lt;a href="https://boycott.gabriel-wu.com" rel="nofollow noopener noreferrer"&gt;here&lt;/a&gt; via Now.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;To run it locally&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;git clone https://github.com/lucifer1004/boycott
&lt;span class="pl-c1"&gt;cd&lt;/span&gt; boycott
yarn install
yarn start&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;You can then visit it at &lt;code&gt;localhost:3000&lt;/code&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;

&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Search for places using Yelp Fusion API (&lt;code&gt;cors-anywhere&lt;/code&gt; is used to address
the CORS issue)&lt;/li&gt;
&lt;li&gt;Filter options: All/Open/High Rating/Low Price&lt;/li&gt;
&lt;li&gt;Use of Google Map API is via
&lt;a href="https://github.com/lucifer1004/react-google-map" rel="noopener noreferrer"&gt;&lt;code&gt;@googlemap-react/core&lt;/code&gt;&lt;/a&gt;,
which is a React wrapper for Google Map written by myself.&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lucifer1004/boycott" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;For both repositories, the dependencies are pinned. One day, to my surprise, all my React hooks failed to work. Such error messages kept occurring:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;React Hooks Error: Hooks can only be called inside the body of a function component...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;They provided no useful information at all! I WAS calling hooks inside the body of functional components.&lt;/p&gt;

&lt;p&gt;After a tough search, I finally found something inspiring. &lt;strong&gt;This error can occur when there is React version inconsistency.&lt;/strong&gt; I then checked &lt;code&gt;package.json&lt;/code&gt; of both the library repo and the application repo, and found that the library was using &lt;code&gt;React 16.8.1&lt;/code&gt; while the application was using &lt;code&gt;React 16.8.2&lt;/code&gt;. I quickly updated my library to use &lt;code&gt;React 16.8.2&lt;/code&gt;, and then the errors went away.&lt;/p&gt;

&lt;p&gt;Now, I have moved &lt;code&gt;react&lt;/code&gt; and &lt;code&gt;react-dom&lt;/code&gt; to &lt;code&gt;peerDependencies&lt;/code&gt; and relaxed the version restrictions to &lt;code&gt;^16.8.2&lt;/code&gt;. I think this is enough to prevent similar issues.&lt;/p&gt;

&lt;p&gt;What I have learned from this experience is that something GOOD (pinning dependencies) is not always RIGHT. It DEPENDS! Dependency pinning is GREAT for an application. However, you should be more CAREFUL when writing a library/package.&lt;/p&gt;

</description>
      <category>javascript</category>
    </item>
  </channel>
</rss>
