2026年6月30日 星期二

Google Antigravity 學習筆記 : CLI 待辦清單 (to-do list) 專案

安裝好 Antigravity CLI 後, 接下來要透過實戰演練來熟悉如何使用此工具來開發專案, 本篇的目標是要用它來開發一個 CLI 介面的 to-do list. 

本系列之前的文章參考 :


以前開發專案的日常通常是打開程式編輯器 (Thonny, VS Code), 建立專案, 寫程式, 抓臭蟲, 修改程式 ... 直到滿意為止. 但使用 Antigravity, 只需要將需求寫清楚, 叫 AI 規劃要怎麼做, 檢視無誤後叫它開始按規劃完成專案. 


1. 建立專案目錄並啟動 agy : 

開啟 PS 視窗, 建立一個專案資料夾 (例如 todo-cli-app), 切換到專案資料夾, 下 agy 啟動 Google  Antigravity 工具 : 

PS D:\agy> mkdir todo-cli-app 
PS D:\agy> cd todo-cli-app  
PS D:\agy\todo-cli-app> agy   

然後輸入專案需求 :

請幫我建立一個 Todo CLI App
需求:
- Python CLI
- 支援 add / list / done / delete
- 使用 JSON 儲存資料
請先不要寫程式碼
先產出 Implementation Plan
以及檔案架構




agy 回應了下列規劃摘要 : 

### 本次工作摘要:

  1. 規劃了模組化的專案目錄結構。
  2. 設計了 JSON 資料儲存的 Schema。
  3. 定義了  add 、 list 、 done 、 delete  等 CLI 指令與對應的參數。
  4. 規劃了開發階段(儲存層 -> 邏輯層 -> CLI 解析層)。

請確認此計畫是否符合您的需求。若同意此規劃,請點擊「Proceed」或告知我,我將開始為您撰寫程式碼。

看起來此規劃沒問題, 輸入如下提示 : 

請依照剛剛的 Implementation Plan 逐步實作專案
每一步請先說明要做什麼再執行

按 Enter 後 agy 便開始執行它所規劃之專案 :

> 請依照剛剛的 Implementation Plan 逐步實作專案
  每一步請先說明要做什麼再執行

  ### 步驟 1:實作儲存層(Storage Layer)
  在這一步中,我們將建立專案的基礎結構與儲存模組:

  1. 建立  todo  套件目錄及空的  __init__.py 。
  2. 建立  todo/storage.py ,其中包含一個  Storage  類別。該類別負責讀取與寫入  todo_data.
  json ,並包含對應的錯誤處理(例如檔案不存在或 JSON 格式錯誤)。

  首先,我們來建立  todo/__init__.py  與  todo/storage.py 。

● Create(D:/agy/todo-cli-app/todo/__init__.py)
● Create(D:/agy/todo-cli-app/todo/storage.py) (ctrl+o to expand)

  ### 步驟 2:實作邏輯層(Logic Layer / Business Logic)

  接下來,我們將實作  todo/manager.py 。
  這個模組會包含  TodoManager  類別,它會調用剛剛建立的  Storage
  模組來載入與儲存資料,並實作核心業務邏輯:

  • 新增(Add):取得現有的 todo 列表,自動生成唯一的 ID(現有最大 ID +
  1),並記錄建立時間(ISO 8601 格式格式化字串),接著寫回檔案。
  • 列表(List):根據過濾條件(全部、已完成、未完成)傳回對應的 todo 項目。
  • 完成(Done):根據指定的 ID 將 todo 狀態改為  done = True 。
  • 刪除(Delete):根據指定的 ID 從列表中移除該項目。

  現在,我們來建立  todo/manager.py 。
● Create(D:/agy/todo-cli-app/todo/manager.py) (ctrl+o to expand)
▸ Thought for 7s, 527 tokens
  Defining CLI Interface
  ### 步驟 3:實作 CLI 解析層與進入點(CLI Parsing & Entry point)
  現在,我們將實作 CLI 解析邏輯。
  1. 建立  todo/cli.py :使用 Python 內建的  argparse  模組來定義 subcommands( add 、 list
  、 done 、 delete )。
      • 當使用者輸入對應指令時,它會調用  TodoManager
      ,並以友善的可讀格式輸出結果。例如,已完成事項標記為  [x] ,未完成標記為  [ ] 。
  2. 建立專案根目錄的  main.py :做為應用程式的啟動點,直接調用  todo/cli.py  中的主函式。

  首先,我們來建立  todo/cli.py 。

● Create(D:/agy/todo-cli-app/todo/cli.py) (ctrl+o to expand)

  接下來,我們在根目錄建立進入點檔案  main.py ,它會直接執行  todo.cli  模組中的  main()
  函式。

● Create(D:/agy/todo-cli-app/main.py) (ctrl+o to expand)

  ### 步驟 4:測試與驗證(Verification & Testing)

  現在我們來驗證實作是否正確運作。我們將執行以下測試步驟:

  1. 執行  python main.py --help  檢查說明選單是否正確。
  2. 新增兩個 todo 項目。
  3. 列出所有待辦事項。
  4. 標記其中一個待辦事項為已完成。
  5. 再次列出,確認狀態更新。
  6. 刪除一個事項,確認已被移除。

  首先,我們執行指令來新增事項並列出它們。

● Bash(python main.py add "Buy fresh milk"; python main.py add "Practice Py...) (ctrl+o to
expand)

Command
───────────────────────────────────────────────────────────────────────────────────────────────

  Requesting permission for:
     python main.py add "Buy fresh milk"; python main.py add "Practice Python coding";
  python main.py list

Do you want to proceed?
> 1. Yes
  2. Yes, and always allow in this conversation for commands that start with 'python main.py
add "Buy fresh milk"; python main.py add "Practice Python coding"...'
  3. Yes, and always allow for commands that start with 'python main.py add "Buy fresh milk";
python main.py add "Practice Python coding"...' (Persist to settings.json)
  4. No

按 1.Yes 讓 agy 繼續執行專案, 完成工作 ID 1 後會再次詢問是否允許執行下一步 :

接下來,我們測試將 ID 1 的待辦事項標記為「已完成」,然後刪除 ID 2,最後列出清單以驗證。

● Bash(python main.py done 1; python main.py delete 2; python main.py list) (ctrl+o to expand)

Command
───────────────────────────────────────────────────────────────────────────────────────────────

  Requesting permission for:
     python main.py done 1; python main.py delete 2; python main.py list

Do you want to proceed?
> 1. Yes
  2. Yes, and always allow in this conversation for commands that start with 'python main.py
done 1; python main.py delete 2; python main.py list'
  3. Yes, and always allow for commands that start with 'python main.py done 1; python main.py
delete 2; python main.py list' (Persist to settings.json)
  4. No

  ↑/↓ Navigate · tab Amend · ctrl+g edit/expand command · ctrl+r Review
esc to cancel                                                         Gemini 3.5 Flash (Medium)

完成所有規劃的工作 :

  ### 步驟 5:撰寫 README.md(Documentation)

  為了讓專案結構更完整,並方便您或其他人日後使用,我們將在專案根目錄下建立一個 README.md
  說明文件,內容會說明如何安裝以及如何執行這些指令。

● Create(D:/agy/todo-cli-app/README.md) (ctrl+o to expand)

  我們已經按照 Implementation Plan 逐步實作完成整個 Todo CLI App!

  ### 實作檔案與程式結構連結:

  1. __init__.py:初始化 todo 套件。
  2. storage.py:定義了 storage.py 類別,負責讀寫 JSON 檔案。
  3. manager.py:定義了 manager.py 類別,實作新增、刪除、完成、列出等核心邏輯。
  4. cli.py:處理指令列參數解析 ( argparse ) 與文字輸出格式化。
  5. main.py:專案啟動入口。
  6. README.md:專案的使用說明文檔。

  ### 測試結果:

  我們已在終端機中成功測試了下列功能:

  • 新增事項: python main.py add "Buy fresh milk"
  • 列出事項: python main.py list
  • 完成事項: python main.py done 1
  • 刪除事項: python main.py delete 2

  所有程式碼與功能皆已測試完畢並可正常運行!如果您有任何新功能需求或需要微調,請隨時告訴我。

以上就是 agy 執行此專案的過程, 至此 AI 已完成全部它所規劃的工作. 


2. 驗收專案成果 : 

開啟一個新的 PS 視窗, 切換至專案目錄下, 先檢視專案內容 :

PS D:\agy\todo-cli-app> dir  
    目錄: D:\agy\todo-cli-app

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----       2026/6/30  下午 02:57                todo
-a----       2026/6/30  下午 12:39             65 main.py
-a----       2026/6/30  下午 03:01           1452 README.md
-a----       2026/6/30  下午 03:01            127 todo_data.json

其中 todo 是放專案程式的資料夾; todo_data.json 是用來存放待辦事項的 JSON 格式資料檔; README.md 是整個專案的說明檔, 裡面有此軟體的使用方法; main.py 為程式進入點, 它的內容很簡單, 先從 todo 資料夾下的 cli.py 匯入 main() 含是後是呼叫它而已 :

from todo.cli import main

if __name__ == "__main__":
    main()

真正的邏輯寫在 todo 子目錄下的程式裡面 :

PS D:\agy\todo-cli-app\todo> dir
    目錄: D:\agy\todo-cli-app\todo
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----       2026/6/30  下午 02:57                __pycache__
-a----       2026/6/30  下午 12:39           3600 cli.py
-a----       2026/6/30  下午 12:39           2197 manager.py
-a----       2026/6/30  下午 12:39           1074 storage.py
-a----       2026/6/30  下午 12:39             26 __init__.py

其中 __init__.py 是一個用來符合 Python 結構的空檔案, cli.py 才是主程式 :

# cli.py
import argparse
import sys
from datetime import datetime
from todo.storage import Storage
from todo.manager import TodoManager

def parse_iso_datetime(iso_str: str) -> str:
    """Parses an ISO format datetime string and returns a user-friendly format."""
    try:
        dt = datetime.fromisoformat(iso_str)
        return dt.strftime("%Y-%m-%d %H:%M")
    except ValueError:
        return iso_str

def handle_add(manager: TodoManager, args):
    if not args.title or not args.title.strip():
        print("Error: Todo title cannot be empty.")
        sys.exit(1)
    
    new_todo = manager.add(args.title)
    print(f"Added task: [ID: {new_todo['id']}] \"{new_todo['title']}\"")

def handle_list(manager: TodoManager, args):
    # Determine filter status
    if args.done:
        status = "done"
    elif args.pending:
        status = "pending"
    else:
        status = "all"
        
    todos = manager.list_todos(status)
    if not todos:
        print(f"No {status if status != 'all' else ''} tasks found.")
        return

    print(f"{'ID':<4} {'Status':<8} {'Title':<40} {'Created At':<16}")
    print("-" * 72)
    for todo in todos:
        status_str = "[x]" if todo["done"] else "[ ]"
        created_time = parse_iso_datetime(todo["created_at"])
        print(f"{todo['id']:<4} {status_str:<8} {todo['title']:<40} {created_time:<16}")

def handle_done(manager: TodoManager, args):
    success = manager.mark_done(args.id)
    if success:
        print(f"Success: Marked task {args.id} as completed.")
    else:
        print(f"Error: Task with ID {args.id} not found.")
        sys.exit(1)

def handle_delete(manager: TodoManager, args):
    success = manager.delete(args.id)
    if success:
        print(f"Success: Deleted task {args.id}.")
    else:
        print(f"Error: Task with ID {args.id} not found.")
        sys.exit(1)

def main():
    parser = argparse.ArgumentParser(
        description="Todo CLI Application - Manage your tasks from the terminal."
    )
    subparsers = parser.add_subparsers(dest="command", help="Available subcommands")

    # 'add' subcommand
    add_parser = subparsers.add_parser("add", help="Add a new task")
    add_parser.add_argument("title", type=str, help="The title of the task")

    # 'list' subcommand
    list_parser = subparsers.add_parser("list", help="List tasks")
    group = list_parser.add_mutually_exclusive_group()
    group.add_argument("--done", action="store_true", help="List only completed tasks")
    group.add_argument("--pending", action="store_true", help="List only pending tasks")
    group.add_argument("--all", action="store_true", default=True, help="List all tasks (default)")

    # 'done' subcommand
    done_parser = subparsers.add_parser("done", help="Mark a task as completed")
    done_parser.add_argument("id", type=int, help="ID of the task to mark as done")

    # 'delete' subcommand
    delete_parser = subparsers.add_parser("delete", help="Delete a task")
    delete_parser.add_argument("id", type=int, help="ID of the task to delete")

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(0)

    # Initialize storage and manager
    storage = Storage("todo_data.json")
    manager = TodoManager(storage)

    # Dispatch to handlers
    if args.command == "add":
        handle_add(manager, args)
    elif args.command == "list":
        handle_list(manager, args)
    elif args.command == "done":
        handle_done(manager, args)
    elif args.command == "delete":
        handle_delete(manager, args)

if __name__ == "__main__":
    main()

manager.py 負責增刪待辦事項等操作 :

# manager.py 
from datetime import datetime
from typing import List, Dict, Optional
from todo.storage import Storage

class TodoManager:
    """Manages todo actions: add, list, done, delete, delegating persistence to Storage."""
    
    def __init__(self, storage: Storage):
        self.storage = storage

    def _load_todos(self) -> List[Dict]:
        return self.storage.load()

    def _save_todos(self, todos: List[Dict]) -> None:
        self.storage.save(todos)

    def add(self, title: str) -> Dict:
        """Adds a new todo item and returns it."""
        todos = self._load_todos()
        
        # Auto-increment ID
        new_id = max([todo["id"] for todo in todos], default=0) + 1
        
        new_todo = {
            "id": new_id,
            "title": title.strip(),
            "done": False,
            "created_at": datetime.now().isoformat()
        }
        
        todos.append(new_todo)
        self._save_todos(todos)
        return new_todo

    def list_todos(self, status: str = "all") -> List[Dict]:
        """Lists todos filtered by status: 'all', 'done', or 'pending'."""
        todos = self._load_todos()
        if status == "done":
            return [todo for todo in todos if todo["done"]]
        elif status == "pending":
            return [todo for todo in todos if not todo["done"]]
        return todos

    def mark_done(self, todo_id: int) -> bool:
        """Marks a todo item as completed. Returns True if found and updated, False otherwise."""
        todos = self._load_todos()
        for todo in todos:
            if todo["id"] == todo_id:
                if todo["done"]:
                    return True # Already done
                todo["done"] = True
                self._save_todos(todos)
                return True
        return False

    def delete(self, todo_id: int) -> bool:
        """Deletes a todo item. Returns True if found and deleted, False otherwise."""
        todos = self._load_todos()
        initial_length = len(todos)
        todos = [todo for todo in todos if todo["id"] != todo_id]
        
        if len(todos) < initial_length:
            self._save_todos(todos)
            return True
        return False

storage.py 為上面的 manager.py 操作 todo_data.json 提供基礎類別 :

import json
import os

class Storage:
    """Handles loading and saving todo data from/to a JSON file."""
    
    def __init__(self, filepath: str = "todo_data.json"):
        self.filepath = filepath

    def load(self) -> list:
        """Loads todo list from JSON file. Returns empty list if file doesn't exist or is invalid."""
        if not os.path.exists(self.filepath):
            return []
        try:
            with open(self.filepath, "r", encoding="utf-8") as f:
                data = json.load(f)
                if not isinstance(data, list):
                    return []
                return data
        except (json.JSONDecodeError, IOError):
            # If the file is corrupted or unreadable, return an empty list
            return []

    def save(self, data: list) -> None:
        """Saves todo list to JSON file."""
        try:
            with open(self.filepath, "w", encoding="utf-8") as f:
                json.dump(data, f, ensure_ascii=False, indent=2)
        except IOError as e:
            print(f"Error saving todo data: {e}")

可見 agy 規劃的專案架構非常清晰, 可維護性很高. 


3. 驗證程式功能 : 

最後手動測試此軟體 :

首先用 python main.py list 檢視代辦清單內容 : 

PS D:\agy\todo-cli-app> python main.py list   
ID   Status   Title                                    Created At
------------------------------------------------------------------------
1    [x]      Buy fresh milk                

目前只有一筆資料, Status 欄為 x 表示該備忘事項已完成. 

用 python main.py add 新增一筆備忘後再次檢視就會顯示兩筆 : 

PS D:\agy\todo-cli-app> python main.py add "買 A4 影印紙"   
Added task: [ID: 2] "買 A4 影印紙"
PS D:\agy\todo-cli-app> python main.py list   
ID   Status   Title                                    Created At
------------------------------------------------------------------------
1    [x]      Buy fresh milk                           2026-06-30 14:57
2    [ ]      買 A4 影印紙                                 2026-06-30 15:15

Status 欄空白表示尚未完成. 接下來 python main.py done 2 指令將 ID=2 的備忘標示為已完成, 再次檢視清單就會看見兩筆都被標示為 x :

PS D:\agy\todo-cli-app> python main.py done 2     
Success: Marked task 2 as completed.
PS D:\agy\todo-cli-app> python main.py list   
ID   Status   Title                                    Created At
------------------------------------------------------------------------
1    [x]      Buy fresh milk                           2026-06-30 14:57
2    [x]      買 A4 影印紙                                 2026-06-30 15:15

開啟 todo_data.json 可以看到所儲存這兩筆待辦事項內容 :

[
  {
    "id": 1,
    "title": "Buy fresh milk",
    "done": true,
    "created_at": "2026-07-01T08:15:30.657339"
  },
  {
    "id": 2,
    "title": "買 A4 影印紙",
    "done": true,
    "created_at": "2026-07-01T08:15:36.159076"
  }
]

最後來檢查刪除功能, 連續刪除兩筆後再次檢視清單應為空 :

PS D:\agy\todo-cli-app> python main.py delete 1   
Success: Deleted task 1.
PS D:\agy\todo-cli-app> python main.py delete 2   
Success: Deleted task 2.
PS D:\agy\todo-cli-app> python main.py list   
No  tasks found.

可見 agy 實作的專案成果符合預期之功能. 

完成後回 agy 命令列, 輸入 /usage 斜線指令檢視目前 token 用量餘額 :




此用量統計分為兩部分, 上層是 Gemini Flash 與 Gemini Pro 模型的用量餘額, 下層是第三方模型 CLAUDE (Claude Opus/Claude Sonnet) 與 GPT 用量餘額合計, 各自有分 Weekly Limit (每週上限) 與 Five Hour Limit (五小時滾動限制), 以及多久後重置, 98% 以上的周累計餘額表示過去一周都沒用到多少. 


4. 上傳 GitHub : 

先在 GitHub 建立一個名為 antigravity 的 repo, 並在此 repo 下新增一個 projects 資料夾, 作法參考 : 





跳到專案目錄上一層資料夾 :

PS D:\agy\todo-cli-app> cd .. 
PS D:\agy>  

用下列指令將上面建立的 GitHub 儲存庫複製到本機 :

git clone https://github.com/你的帳號/antigravity_cli.git

git clone https://github.com/tony1966/antigravity_cli.git

PS D:\agy> git clone https://github.com/tony1966/antigravity_cli.git   
Cloning into 'antigravity_cli'...
remote: Enumerating objects: 8, done.
remote: Counting objects: 100% (8/8), done.
remote: Compressing objects: 100% (6/6), done.
remote: Total 8 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (8/8), done.
Resolving deltas: 100% (1/1), done.

檢視目錄結構, 確認 antigravity_cli 底下有 projects 資料夾 : 

PS D:\agy> tree antigravity_cli   
列出磁碟區 新增磁碟區 的資料夾 PATH
磁碟區序號為 00000239 1258:16B8
D:\AGY\ANTIGRAVITY_CLI
└─projects

將專案目錄 todo-cli-app 移動或複製到 projects 下 : 

PS D:\agy\> cd antigravity_cli  
PS D:\agy\antigravity_cli> tree projects      
列出磁碟區 新增磁碟區 的資料夾 PATH
磁碟區序號為 0000018F 1258:16B8
D:\AGY\ANTIGRAVITY_CLI\PROJECTS
└─todo-cli-app
    └─todo

用記事本編輯一個 .gitignore 檔, 貼上下列萬用內容 :

# ==========================================
# Python 相關快取與編譯檔案
# ==========================================
__pycache__/
*.py[cod]
*$py.class
.pytest_cache/
.poetry/
.venv/
venv/
ENV/
env/

# ==========================================
# 專案實務資料 (避免將測試的待辦清單資料上傳)
# ==========================================
# 如果你希望每個人 clone 下來都是乾淨的,就排除 json 檔案
# 如果你希望上傳一個範本,可以改為排除特定名稱(例如:todo_db.json)
*.json

# ==========================================
# 開發工具與編輯器暫存檔
# ==========================================
.vscode/
.idea/
*.swp
*.bak
.DS_Store
Thumbs.db

# ==========================================
# 敏感資料與環境變數 (絕對不能上傳)
# ==========================================
.env
.env.local
.env.*.local
*.env

存檔後用 git add. 指令讓 Git 開始追蹤新加入的專案 : 

PS D:\agy\antigravity_cli> git add .  
warning: in the working copy of 'projects/todo-cli-app/README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'projects/todo-cli-app/main.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'projects/todo-cli-app/todo/__init__.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'projects/todo-cli-app/todo/cli.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'projects/todo-cli-app/todo/manager.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'projects/todo-cli-app/todo/storage.py', LF will be replaced by CRLF the next time Git touches it

這些 warning 無關緊要, 這樣就可以提交變更了 :

PS D:\agy\antigravity_cli> git commit -m "feat: add todo-cli-app to projects folder"   
[main 239b617] feat: add todo-cli-app to projects folder
 7 files changed, 300 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 projects/todo-cli-app/README.md
 create mode 100644 projects/todo-cli-app/main.py
 create mode 100644 projects/todo-cli-app/todo/__init__.py
 create mode 100644 projects/todo-cli-app/todo/cli.py
 create mode 100644 projects/todo-cli-app/todo/manager.py
 create mode 100644 projects/todo-cli-app/todo/storage.py

用 git push 指令推送到 GitHub : 

git push origin main

這時會彈出一個網頁詢問頁面, 點選 manager 與 Always use this from now on 後按 Select 即開始推送 : 




PS D:\agy\antigravity_cli> git push origin main  
Enumerating objects: 14, done.
Counting objects: 100% (14/14), done.
Delta compression using up to 16 threads
Compressing objects: 100% (11/11), done.
Writing objects: 100% (12/12), 4.21 KiB | 2.11 MiB/s, done.
Total 12 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/tony1966/antigravity_cli.git
   a9aea88..239b617  main -> main

這時到 GitHub 就可以看到 todo-cli-app 專案上傳成功 :




本專案的 repo 網址 :


沒有留言 :