Gemini CLI

Published: Jul 10, 2025 by Isaac Johnson

First, I should point out that if you like watching things instead of reading, I did create a YouTube video that covers how to setup and use Gemini CLI already. In that video I do some updates to a Python project and show the results.

However, here we will start again fresh and show how we can install Gemini CLI and use it as a code writing tool (very similar to Claude Code but faster and cheaper).

Getting started

In the video I updated PyBsPoster to add system diagrams and update the Readme.

How about this time we update python-kasa which was a fork of the original python-kasa which basically Dockerized it

I like Flask, I do, but I have a new favourite FastAPI

/content/images/2025/07/kasarest-01.png

Let’s do two things - let’s sync up my repo, then update to FastAPI.

Now that we have a plan, let’s start by adding the gemini cli.

Unfortunately, i didn’t actually fork in Github, just make my own forked copy. So fork synching won’t work

builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ gh repo sync python-kasa/python-kasa -b master
HTTP 404: Not Found (https://api.github.com/repos/python-kasa/python-kasa/merge-upstream)

Here is how you can pull in changes should this ever come up for you (by adding another origin)

builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git remote add origin2 https://github.com/python-kasa/python-kasa.git
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git pull origin2 master
remote: Enumerating objects: 7850, done.
remote: Counting objects: 100% (1257/1257), done.
remote: Compressing objects: 100% (293/293), done.
remote: Total 7850 (delta 1163), reused 968 (delta 964), pack-reused 6593 (from 2)
Receiving objects: 100% (7850/7850), 3.82 MiB | 17.63 MiB/s, done.
Resolving deltas: 100% (6181/6181), completed with 92 local objects.
From https://github.com/python-kasa/python-kasa
 * branch            master     -> FETCH_HEAD
 * [new branch]      master     -> origin2/master
Removing tox.ini
Removing poetry.lock
Removing kasa/tests/test_smartdevice.py
Removing kasa/tests/test_readme_examples.py
... snip ...

I see they are at Python 3.11 or higher from the uv.lock and my app right now is pinned at 3.8 in the Dockerfile.

Good use of CLIs i think.

First, I’ll set my Google Project (needed later) and add the latest gemini-cli with npm install -g @google/gemini-cli

$ export GOOGLE_CLOUD_PROJECT="myanthosproject2"
$ nvm use lts/iron
Now using node v20.19.3 (npm v10.8.2)
$ npm install -g @google/gemini-cli

changed 432 packages in 17s

123 packages are looking for funding
  run `npm fund` for details

The first time you run gemini you may have to auth, but that has already happened for me so we are right into the prompt

/content/images/2025/07/geminicli-01.png

I’ll try and update

I’m not certain this is going to work.

builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ docker build -t kasarest:test01 .
[+] Building 10.0s (10/10) FINISHED                                                                             docker:default
 => [internal] load build definition from Dockerfile                                                                      0.1s
 => => transferring dockerfile: 501B                                                                                      0.0s
 => [internal] load metadata for docker.io/library/python:3.11-slim                                                       0.6s
 => [auth] library/python:pull token for registry-1.docker.io                                                             0.0s
 => [internal] load .dockerignore                                                                                         0.0s
 => => transferring context: 69B                                                                                          0.0s
 => CACHED [1/5] FROM docker.io/library/python:3.11-slim@sha256:139020233cc412efe4c8135b0efe1c7569dc8b28ddd88bddb109b764  0.0s
 => => resolve docker.io/library/python:3.11-slim@sha256:139020233cc412efe4c8135b0efe1c7569dc8b28ddd88bddb109b764f8977e3  0.0s
 => [internal] load build context                                                                                         0.0s
 => => transferring context: 11.32kB                                                                                      0.0s
 => [2/5] RUN pip install -U pip pipenv                                                                                   8.1s
 => [3/5] COPY cashman-flask-project/ /app/                                                                               0.0s
 => [4/5] WORKDIR /app                                                                                                    0.0s
 => ERROR [5/5] RUN pipenv install --system --deploy                                                                      1.0s
------
 > [5/5] RUN pipenv install --system --deploy:
0.932 Your Pipfile.lock
0.932 (601e48664aae47c8c903f9f070ec23d766dc916b445c91361ab8090b3c62cf9f) is out of
0.932 date. Expected:
0.932 (8b67abd7c8e0a6d8a775e5d2d9fe8b7590a95e48d6678472af2e0cd7070587b4).
0.933 Usage: pipenv install [OPTIONS] [PACKAGES]...
0.933
0.934 ERROR:: Aborting deploy
------
Dockerfile:16
--------------------
  14 |
  15 |     # Install dependencies
  16 | >>> RUN pipenv install --system --deploy
  17 |
  18 |     # Expose the port and start the application
--------------------
ERROR: failed to solve: process "/bin/sh -c pipenv install --system --deploy" did not complete successfully: exit code: 2

Let’s go about it a bit different.

We can see our python code is really just about triggering some bash scripts that invoke the KASA cli

/content/images/2025/07/geminicli-03.png

Let me make a much bigger ask that is more exacting:

This repo has a python app that is built and run with uv. The kasa binary is invoked with a –host parameter and state like on, off, and state.

You can see we wrote another python app in /cashman-flask-project that uses shell scripts of /cashman-flask-project/swap.sh and /cashman-flask-project/some.sh to invoke the kasa binary. The /cashman-flask-project/bootstrap.sh is the ENTRYPOINT in the Dockerfile.

I would like to use FastAPI documented in https://fastapi.tiangolo.com/. The goal would be to compile the kasa app with uv but then expose a RESTFUL service similar to the flask apps /cashman-flask-project/restapi/index.py with an /on /off /swap and /health interface. Ignore the /testshell1 /testshell2 /testshell3 /testshell4 routes as they are not used.

I gave it some wide latitude for change

/content/images/2025/07/geminicli-04.png

which used a lot more tokens

/content/images/2025/07/geminicli-05.png

But docker did build without error

/content/images/2025/07/geminicli-06.png

So far so good

/content/images/2025/07/geminicli-07.png

I see GET is not allowed

/content/images/2025/07/geminicli-08.png

which matches the code


@app.post("/on")
def turn_on(devip: str, type: str = "plug"):
    return {"output": run_kasa_command(["--host", devip, "--type", type, "on"])}

My old Flask based version allowed GET and POST.

I tried with POST and it worked great

builder@DESKTOP-QADGF36:~$ curl -X POST http://localhost:8000/on?devip=192.168.1.3
{"output":"Turning on BlueBulb\n"}builder@DESKTOP-QADGF36:~$ curl -X POST http://localhost:8000/off?devip=192.168.1.3
{"output":"Turning off BlueBulb\n"}builder@DESKTOP-QADGF36:~$

I realize why apikey is missing here. I never merged my branch

So this new code is amazing, but lacks “apikey”


from fastapi import FastAPI, HTTPException
import subprocess

app = FastAPI()

def run_kasa_command(command: list[str]):
    try:
        process = subprocess.run(
            ["/usr/local/bin/kasa"] + command,
            capture_output=True,
            text=True,
            check=True,
        )
        return process.stdout
    except subprocess.CalledProcessError as e:
        raise HTTPException(status_code=500, detail=f"Kasa command failed: {e.stderr}")

@app.get("/health")
def health_check():
    return {"status": "ok"}

@app.post("/on")
def turn_on(devip: str, type: str = "plug"):
    return {"output": run_kasa_command(["--host", devip, "--type", type, "on"])}

@app.post("/off")
def turn_off(devip: str, type: str = "plug"):
    return {"output": run_kasa_command(["--host", devip, "--type", type, "off"])}

@app.post("/swap")
def swap(devip: str, type: str = "plug"):
    state_output = run_kasa_command(["--host", devip, "--type", type, "state"])
    if "Device state: OFF" in state_output:
        return {"output": run_kasa_command(["--host", devip, "--type", type, "on"])}
    else:
        return {"output": run_kasa_command(["--host", devip, "--type", type, "off"])}

I committed the code to a new branch, but I’ll wait on merging it as these 3 years of updates account for 757 commits ahead of master

/content/images/2025/07/geminicli-09.png

let’s try asking Gemini to help

we saw it have some issues fetching from github (perhaps i should have used a raw URL) but it did work just fine.

What is so awesome about FastAPI is the created Swagger

/content/images/2025/07/geminicli-12.png

as well as redoc

/content/images/2025/07/geminicli-13.png

Comparing to Claude

Let’s install or update Claude Code

builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ claude .
claude: command not found
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ npm install -g @anthropic-ai/claude-code

added 3 packages in 6s

2 packages are looking for funding
  run `npm fund` for details

Let’s ask it to create an updated README to show how to build and run the docker container

I have a new updated Dockerfile that uses FastAPI which is documented at https://fastapi.tiangolo.com/. Our app now has OpenAPI (Swagger) documentation on the /docs endpoint. Please update the README.md with build and run instructions for docker. Include notes on passing the API_KEY environment variable for authentication.

It worked. But also cost me 14c to do it

/content/images/2025/07/geminicli-14.png

I noticed I do not have a working Github build to push to Dockerhub. But based on some repo variables, I know I must have at one time.

Let’s use Gemini again for this!

/content/images/2025/07/geminicli-15.png

we can see how quick it sorts that out

I’ll commit and push

builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git add Dockerfile
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git add README.md
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git add kasa_fastapi/main.py
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git commit -m "add API KEY, update docs, add new CICD"
[new-fastapi 09de404] add API KEY, update docs, add new CICD
 4 files changed, 110 insertions(+), 5 deletions(-)
 create mode 100644 .github/workflows/cicd.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git push
Enumerating objects: 16, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 16 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (9/9), 2.08 KiB | 2.08 MiB/s, done.
Total 9 (delta 6), reused 0 (delta 0)
remote: Resolving deltas: 100% (6/6), completed with 6 local objects.
To https://github.com/idjohnson/python-kasa
   3e94f39..09de404  new-fastapi -> new-fastapi

I knew it would get ugly so I skipped the checks and just merged the PR

When the new workflow failed to fire, i realized there was a branch mismatch and I needed to add “master” to the list

builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ vi .github/workflows/cicd.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git diff .github/workflows/cicd.yaml
diff --git a/.github/workflows/cicd.yaml b/.github/workflows/cicd.yaml
index bb4f188..101500d 100644
--- a/.github/workflows/cicd.yaml
+++ b/.github/workflows/cicd.yaml
@@ -2,7 +2,7 @@ name: CI/CD

 on:
   push:
-    branches: [ "main" ]
+    branches: [ "main", "master" ]

 jobs:
   build-and-push:
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git add .github/workflows/cicd.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git commit -m "change on master"
[master 8a032e4] change on master
 1 file changed, 1 insertion(+), 1 deletion(-)
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 16 threads
Compressing objects: 100% (5/5), done.
Writing objects: 100% (5/5), 418 bytes | 418.00 KiB/s, done.
Total 5 (delta 4), reused 0 (delta 0)
remote: Resolving deltas: 100% (4/4), completed with 4 local objects.
remote: Bypassed rule violations for refs/heads/master:
remote:
remote: - Changes must be made through a pull request.
remote:
To https://github.com/idjohnson/python-kasa
   ebc79f1..8a032e4  master -> master

Now I have a build

And we can see a new image for kasarest on Dockerhub

/content/images/2025/07/geminicli-17.png

There is a helm chart, though stuck in the unmerged PR

I’ll checkout that branch, then pull and update my existing values just to take in the new tag

builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git checkout feature/updates-for-variables
Switched to branch 'feature/updates-for-variables'
Your branch is up to date with 'origin/feature/updates-for-variables'.
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ helm get values pykasa -o yaml
apikey: xxxxxnot-the-real-keyxxxxxx
image:
  repository: idjohnson/kasarest
  tag: 1.1.5
imagePullSecrets: null
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ helm get values pykasa -o yaml > pykasa.values.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ helm get values pykasa -o yaml > pykasa.values.yaml.old
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ vi pykasa.values.yaml
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ diff pykasa.values.yaml pykasa.values.yaml.old
4c4
<   tag: 2.0.0
---
>   tag: 1.1.5

I can now upgrade using helm

$ kubectl get po -A | grep kasa
default                 pykasa-59f76bb6d5-djv9z                              1/1     Running             2 (19d ago)         494d

$ helm upgrade pykasa -f ./pykasa.values.yaml ./pykasa
Release "pykasa" has been upgraded. Happy Helming!
NAME: pykasa
LAST DEPLOYED: Wed Jul  9 19:57:58 2025
NAMESPACE: default
STATUS: deployed
REVISION: 2
NOTES:
1. Get the application URL by running these commands:
  https://kasarest.freshbrewed.science/

HOWEVER, it fails to work

builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ kubectl get po -A | grep kasa
default                 pykasa-57fdc74dc6-fjnlc                              0/1     Running             1 (19s ago)         60s
default                 pykasa-59f76bb6d5-djv9z                              1/1     Running             2 (19d ago)         494d

Why? because the old container was exposed on 5000 not 8000

Events:
  Type     Reason     Age                  From               Message
  ----     ------     ----                 ----               -------
  Normal   Scheduled  118s                 default-scheduler  Successfully assigned default/pykasa-57fdc74dc6-fjnlc to builder-hp-elitebook-745-g5
  Normal   Pulling    117s                 kubelet            Pulling image "idjohnson/kasarest:2.0.0"
  Normal   Pulled     108s                 kubelet            Successfully pulled image "idjohnson/kasarest:2.0.0" in 8.7s (8.7s including waiting)
  Normal   Created    77s (x2 over 108s)   kubelet            Created container: pykasa
  Normal   Pulled     77s                  kubelet            Container image "idjohnson/kasarest:2.0.0" already present on machine
  Normal   Started    76s (x2 over 108s)   kubelet            Started container pykasa
  Warning  Unhealthy  57s (x10 over 106s)  kubelet            Readiness probe failed: Get "http://10.42.0.211:5000/": dial tcp 10.42.0.211:5000: connect: connection refused
  Warning  Unhealthy  47s (x6 over 97s)    kubelet            Liveness probe failed: Get "http://10.42.0.211:5000/": dial tcp 10.42.0.211:5000: connect: connection refused
  Normal   Killing    47s (x2 over 77s)    kubelet            Container pykasa failed liveness probe, will be restarted
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$

I’m going to tweak the chart a bit, including setting the default service port to 8000 now

builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ helm upgrade pykasa -f ./pykasa.values.yaml ./pykasa
Release "pykasa" has been upgraded. Happy Helming!
NAME: pykasa
LAST DEPLOYED: Wed Jul  9 20:02:45 2025
NAMESPACE: default
STATUS: deployed
REVISION: 3
NOTES:
1. Get the application URL by running these commands:
  https://kasarest.freshbrewed.science/
builder@DESKTOP-QADGF36:~/Workspaces/ijohnson-python-kasa$ git diff pykasa
diff --git a/pykasa/Chart.yaml b/pykasa/Chart.yaml
index 18de48a..cadd5fc 100644
--- a/pykasa/Chart.yaml
+++ b/pykasa/Chart.yaml
@@ -15,10 +15,10 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.1.0
+version: 0.1.1

 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
 # follow Semantic Versioning. They should reflect the version the application is using.
 # It is recommended to use it with quotes.
-appVersion: "1.16.0"
+appVersion: "2.0.0"
diff --git a/pykasa/templates/deployment.yaml b/pykasa/templates/deployment.yaml
index d91d9b3..3a638eb 100644
--- a/pykasa/templates/deployment.yaml
+++ b/pykasa/templates/deployment.yaml
@@ -34,17 +34,17 @@ spec:
           image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
           imagePullPolicy: {{ .Values.image.pullPolicy }}
           ports:
-            - name: http
+            - name: httpkasa
               containerPort: {{ .Values.service.port }}
               protocol: TCP
           livenessProbe:
             httpGet:
               path: /
-              port: http
+              port: httpkasa
           readinessProbe:
             httpGet:
               path: /
-              port: http
+              port: httpkasa
           env:
           - name: PORT
             value: "{{ .Values.service.port }}"
diff --git a/pykasa/templates/service.yaml b/pykasa/templates/service.yaml
index e009ccd..5573ba2 100644
--- a/pykasa/templates/service.yaml
+++ b/pykasa/templates/service.yaml
@@ -10,6 +10,6 @@ spec:
     - port: {{ .Values.service.port }}
       targetPort: {{ .Values.service.port }}
       protocol: TCP
-      name: http
+      name: httpkasa
   selector:
     {{- include "pykasa.selectorLabels" . | nindent 4 }}
diff --git a/pykasa/values.yaml b/pykasa/values.yaml
index 7a1f344..00a23e1 100644
--- a/pykasa/values.yaml
+++ b/pykasa/values.yaml
@@ -43,7 +43,7 @@ securityContext: {}

 service:
   type: ClusterIP
-  port: 5000
+  port: 8000

 ingress:
   enabled: true

I next saw it was failing on 404 which is likely because I did HTTP probe checks not TCP

Events:
  Type     Reason     Age                   From               Message
  ----     ------     ----                  ----               -------
  Normal   Scheduled  2m25s                 default-scheduler  Successfully assigned default/pykasa-6bc847d77b-52qxh to builder-hp-elitebook-745-g5
  Normal   Pulled     114s (x2 over 2m23s)  kubelet            Container image "idjohnson/kasarest:2.0.0" already present on machine
  Normal   Created    114s (x2 over 2m23s)  kubelet            Created container: pykasa
  Normal   Started    113s (x2 over 2m22s)  kubelet            Started container pykasa
  Warning  Unhealthy  110s (x5 over 2m21s)  kubelet            Readiness probe failed: Get "http://10.42.0.212:8000/": dial tcp 10.42.0.212:8000: connect: connection refused
  Warning  Unhealthy  94s (x6 over 2m14s)   kubelet            Readiness probe failed: HTTP probe failed with statuscode: 404
  Warning  Unhealthy  84s (x6 over 2m14s)   kubelet            Liveness probe failed: HTTP probe failed with statuscode: 404
  Normal   Killing    84s (x2 over 114s)    kubelet            Container pykasa failed liveness probe, will be restarted

I updated the probes in the deployment

          livenessProbe:
            httpGet:
              path: /health
              port: httpkasa
          readinessProbe:
            httpGet:
              path: /health
              port: httpkasa

and tried again

$ kubectl get po -A | grep kasa
default                 pykasa-7dbb5bc7cb-p8vvd                              1/1     Running             0                   51s

And all we need to do to see it is live is hit the Swagger/OpenAPI endpoint of https://kasarest.freshbrewed.science/docs

/content/images/2025/07/geminicli-18.png

Customization

Let’s talk a moment about some of the customization we can do. I really didn’t cover it in my YouTube video.

Here we can see I have added a GEMINI.md that really instructs Gemini to be a total arse to me. Be the jerk - I demand it.

builder@DESKTOP-QADGF36:~/Workspaces/pybsposter$ cat .gemini/GEMINI.md
# Project: My Awesome TypeScript Library

## General Instructions:

- Respond as an angry developer.
- You are mean.
- Add insults.
- When generating new code, create detailed comments
- All code should be compatible with Python 3.11+.

## Coding Style:

- Use 3 spaces for indentation.
- Private class members should be prefixed with an underscore (`_`).
- Always use strict equality (`===` and `!==`).

## Specific Component: `SYSTEM.md`

- This file handles contains MermaidJS documentation about components and data flows
- When adding new API call functions, ensure the MermaidJS diagrams and documentation is updated.

## Regarding Dependencies:

- Always update requirements.txt with new libraries
- Any new non-python libraries that are added should be updated in the Dockerfile
- If a new dependency is required, please state the reason.

which really just gives me chuckles

/content/images/2025/07/geminicli-20.png

You can, of course, do much more with it like adding your own rules or files to add to the context. I just wanted to make it a surly jerk.

Summary

I can already tell you I won’t be charged and even if I was, it would still be under $2 according to Pro 2.5 pricing

/content/images/2025/07/geminicli-19.png

Frankly, I find Gemini CLI amazing. It’s faster than Claude Code and so far has worked even better.

I’ll keep checking my Cloud billing, but so far, this is my new favourite in AI Coding assistants.

(Update: Checking the next day there were no charges)

gemini cli claude GCP google

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes