Agentic 實戰:讓 AI Agent 完成 CLI 開發與 npm Trusted Publishing 發佈

最近我陸續上架了好幾個專門給 Agent 用的 CLI 工具,讓 Agent 可以直接透過終端機呼叫功能、傳入參數,再讀取結構化的結果。工具做好之後,接下來就是把它們發佈到 npm,讓其他人和 Agent 也能直接安裝使用。我原本想,這件事以前就做過了,而且一月還寫過如何利用全新的 Trusted publishing 方法發佈 npm 套件,這次應該可以把整個流程交給 Agent 處理。

結果還是有幾個地方值得記錄:Agent 會寫程式,不代表它已經登入 npm;Workflow 寫好了,不代表 npm 已經信任它;終端機印出發佈成功,也不代表使用者馬上就能安裝。最後還有一個小插曲,我只是合併一份驗證文件,npm 就多出了一個 0.1.1 版本。回頭看才發現,這其實完全符合我自己下的指令。

這篇就用這次的實作過程,整理如何讓 Agent 從建立 CLI、準備 npm 套件、建立 GitHub 儲存庫,一路做到 Trusted Publisher 與 CI/CD 驗證。重點不只是那份 YAML,而是把要做的事、發佈的時機,以及怎樣才算完成,事先講清楚。

AI Agent 從終端機建立 CLI,經過測試與 OIDC 信任驗證,將套件送到 npm registry;人工負責首次雙因素認證

本文整理 2026 年 10 月的操作經驗,開發環境為 macOS 26.6.2、Node.js 24.22.1、npm 12.2.1,CI 另外驗證 Node.js 22 與 24。下文統一用 my-agent-cli 作為示意套件名稱、my-agent 作為命令名稱,your-github-account 與 your-npm-account 則是示意帳號;它們不代表本文作者已發佈的特定作品。套用範例時請換成自己的資料。文中不包含登入連結、OTP、Access Token 或其他一次性認證資訊。


先釐清:Agent、登入帳號與 CI 是三件事

我這裡說的 Agent,是指能讀寫專案檔案、執行終端機命令,必要時還能操作瀏覽器的 AI 工具。它不是只在聊天視窗裡貼一段設定給我,而是真的會去建立檔案、執行測試、查詢遠端狀態,再根據結果繼續處理。

不過,這個能力不等於帳號權限。這次的分工可以整理成下面這張表:

工作 由誰執行 怎樣才算完成
建立 CLI、測試與 npm 封裝 Agent 操作本機專案 測試通過,封裝內容正確,安裝後能執行
首次 npm 登入與人工驗證 Agent 啟動流程,我完成認證 npm whoami 回傳預期帳號
建立公開 GitHub 儲存庫 Agent 使用已授權的 GitHub CLI 遠端儲存庫存在,而且能取得程式碼
設定 Trusted Publisher Agent 操作 npm 網站,我完成必要的 2FA 設定確實儲存,第一次 OIDC 發佈驗證成功
後續自動發佈 GitHub Actions 測試、OIDC 發佈、registry 驗證都成功

Agent 負責操作,帳號擁有者負責完成必要的身分驗證。 兩者可以接力,但不要把「請 AI 幫我發佈」理解成「AI 應該想辦法跳過登入」。

文中的操作提示詞也整理成純文字附件,可以下載後換成自己的套件名稱與 GitHub 資料。

另外,npm 帳號與 GitHub 帳號不一定相同。假設 npm 使用者是 your-npm-account,GitHub 使用者是 your-github-account,Trusted Publisher 裡面就應該填入後者。這裡要核對的是 GitHub 的 owner 與 repository,不是看見 npm 帳號就直接照填。這種看似很小的細節,反而很適合讓 Agent 查完之後列出來,再繼續設定。


步驟一:先把 CLI 的需求與驗收方式交代完整

專門給 Agent 使用的 CLI,我會特別在意輸入與輸出能不能穩定地被程式處理。例如參數要有清楚的說明、支援 STDIN、輸出 JSON、錯誤訊息與結果分流,以及用退出碼表示成功或失敗。這些約定比互動式選單更適合放進 Agent 的工作流程。

如果要重新做一次,我會把提示詞整理成這樣:

請在目前工作目錄建立 my-agent-cli 子資料夾,實作一個給 Agent 使用的 Node.js CLI。

請先閱讀需求及相關 API 的官方文件,列出功能、輸入與輸出契約。

需求:
- npm 套件名稱為 my-agent-cli,命令名稱為 my-agent。
- 支援以 --input 指定輸入檔案,也能從 STDIN 接收內容。
- 支援 --format json,讓 Agent 能穩定解析結果。
- 結果寫入 STDOUT;進度與錯誤寫入 STDERR。
- 以明確的退出碼區分成功、參數錯誤與執行失敗。
- 寫入檔案或呼叫遠端服務的操作,提供不產生副作用的 --dry-run。
- 提供 --help、--version、Makefile 與 README。
- 加入參數、STDIN、錯誤處理及核心功能的自動測試。

請實際執行測試、檢查 npm 封裝,並從封裝安裝後驗證 CLI。
請建立獨立 Git 儲存庫,每個完成階段使用詳細繁體中文 commit 記錄。
涉及外部服務的功能,請區分離線測試與真實服務驗證,不要混為一談。
這一步先完成本機專案,不要先發佈到 npm。

我特別保留 --dry-run,因為這會讓後續驗證簡單很多。Agent 可以先檢查參數解析、STDIN 與預計執行的操作,不必在每次測試時真的修改檔案或呼叫遠端服務。下面以支援檔案與文字輸入的示意 CLI 為例:

# 以旗標指定輸入檔案
node src/cli.js --input ./input.txt --format json --dry-run

# 從 STDIN 輸入內容
printf '%s\n' '待處理的內容' |
  node src/cli.js --format json --dry-run

這裡還有一個很實用的驗收點:第二個例子的 STDOUT 應該是乾淨的 JSON,不能混進「正在讀檔」或 npm script 的提示文字。CLI 要能接管線,這些小地方比多做幾個參數更重要。

如果 CLI 要存取外部服務,也要在 README 說明認證方式、可測試的範圍,以及哪些操作真的會產生副作用。不要讓 Agent 把「離線測試通過」寫成「所有外部服務都已驗證」。


步驟二:先確認 npm 裝到的到底是什麼

npm 的套件名稱與使用者在終端機輸入的命令名稱可以不同。以下使用示意名稱:

{
  "name": "my-agent-cli",
  "version": "0.1.0",
  "type": "module",
  "bin": {
    "my-agent": "src/cli.js"
  },
  "files": ["src", "README.md", "examples", "LICENSE"],
  "engines": {
    "node": ">=22"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/your-github-account/my-agent-cli.git"
  },
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/"
  }
}

這是重點欄位節錄,實際套件還有相依套件與 scripts。bin 指到的檔案要有 Node.js 的 shebang,像這樣:

#!/usr/bin/env node

我會要求 Agent 檢查下面幾件事,而不是看到 package.json 就算完成:

請驗證 npm 封裝:
1. 執行 npm pack --dry-run,列出將發佈的檔案。
2. 確認沒有 .env、API 金鑰、node_modules、測試暫存資料。
3. 實際產生 tgz,安裝到獨立暫存目錄。
4. 使用安裝後的 my-agent 執行 --version、--help 與 --dry-run。
5. 確認 --version 讀的是封裝內的 package.json,不是寫死在程式裡。

最後一點在自動升版時很容易出錯。假設 CI 把版本改成 0.1.1,但 CLI 裡仍然寫死 0.1.0,使用者安裝成功後看到的版本就會不一致。這次後來也把版本顯示改成讀取套件本身的 metadata。

repository.url 也要指向實際發佈的 GitHub 儲存庫,不能留著範本專案的網址。後面要驗證 provenance 時,這個來源資訊會派上用場。


步驟三:用最小套件完成第一次發佈

Trusted Publisher 是針對 npm 上已經存在的套件設定。這次我先發佈一個最小版本,把套件建立起來,再接 GitHub Actions。

這是這次選用的初始化方式,不代表每個專案都應該先發佈空套件。 如果你的 CLI 已經準備好,也可以直接手動發佈第一個可用版本;如果這個 npm 套件原本就存在,更不需要再做一次初始化。

我當時已經有完整 CLI,所以沒有把根目錄的程式刪掉,而是另外建立 release/placeholder/:

release/placeholder/
├── package.json
├── README.md
└── LICENSE

裡面的 package.json 只有最基本的資訊:

{
  "name": "my-agent-cli",
  "version": "0.0.0",
  "description": "Initial placeholder for an agent-oriented CLI.",
  "license": "MIT",
  "files": ["README.md", "LICENSE"],
  "publishConfig": {
    "access": "public",
    "registry": "https://registry.npmjs.org/"
  }
}

我下給 Agent 的指令是:

請先確認 my-agent-cli 在 npm 的名稱狀態與目前登入帳號。
準備 0.0.0 的最小套件,清楚標示這個版本不含可執行 CLI。
保留根目錄已完成的實作,只發佈 release/placeholder/。
先檢查 tarball 內容,再發佈;需要登入或 2FA 時讓我在瀏覽器完成。

實際會用到這幾個命令:

npm whoami --registry=https://registry.npmjs.org/
npm view my-agent-cli versions --json
npm pack ./release/placeholder --dry-run
npm login --registry=https://registry.npmjs.org/
npm publish ./release/placeholder --access public

第一次的 npm whoami 回傳 401 Unauthorized,因此 Agent 啟動登入流程,讓我在 npm 的網頁完成認證。登入成功後,再次查詢才看到預期的帳號。

這裡不要把查詢套件時的 404 當成「這個名稱一定可以註冊」的保證。它只能說明當下這次查詢沒有取得套件,真正能不能發佈,還是要看 registry 的驗證結果。


步驟四:建立 GitHub 儲存庫,先決定什麼時候發佈

接著我要求 Agent 把專案放到公開 GitHub 儲存庫,並且加入 PR 驗證與 npm Trusted Publishing。這一步建議把 GitHub 帳號、儲存庫名稱、workflow 檔名與發佈規則一起寫清楚:

請將這個專案發佈到我的公開 GitHub 儲存庫:
your-github-account/my-agent-cli

建立以下 CI/CD:
- PR 對 main 執行測試與封裝檢查,不發布套件。
- 測試 Node.js 22 與 24,作業系統包含 Linux、macOS、Windows。
- 所有測試通過後才允許發布。
- npm 發布 workflow 檔名固定為 publish.yml。
- 使用 GitHub-hosted runner 與名為 npm 的 GitHub environment。
- 使用 OIDC Trusted Publishing,不設定 NPM_TOKEN 或 NODE_AUTH_TOKEN。
- workflow_dispatch 可以手動重跑。

這次採用的發布規則:每次 main push 都發布新的穩定版本,
包含文件變更;第一個完整版本是 0.1.0,之後自動遞增 patch。
請不要把 CI 產生的版本更新再 commit 回 main。
相同 commit 已發布時,重跑必須跳過,不能重複發布。

最後那一段,最好在一開始就決定。

我這次最初只說「main 有新的 commits 就發佈」,後來 Agent 合併一份驗證文件,果然就觸發了 0.1.1。當我問「為什麼又發佈一個版本」,回頭看規則才發現,文件 commit 也是 commit。

如果你的預期是「只有調整 package.json 版本才發佈」,就要明確指定這個條件,並要求已存在的版本直接跳過。不要一邊採用每次 push 自動升版,一邊期待文件變更不會產生版本。

這不是要多加一道確認流程,而是避免把兩種不同的發佈策略混在一起。


步驟五:把 PR 測試與發佈權限分開

其中一個專案的 CI 使用六組測試環境:三種作業系統,各跑 Node.js 22 與 24。當時共有 28 項自動測試,涵蓋參數、STDIN、錯誤處理、核心功能與版本選擇。不同工具的測試內容會不同,但都要驗證 Agent 實際依賴的輸入與輸出契約。

ci.yml 的結構如下。下面保留主要設定;實際專案會把 Actions 固定到完整 commit SHA,可以交給 Agent 依專案的測試命令產生對應檔案。

name: CI

on:
  pull_request:
    branches: [main]
  workflow_call:
  workflow_dispatch:

permissions:
  contents: read

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [22, 24]
    steps:
      - uses: actions/[email protected]
        with:
          persist-credentials: false
      - uses: actions/[email protected]
        with:
          node-version: ${{ matrix.node }}
          cache: npm
      - run: npm ci
      - run: npm run check
      - run: npm test
      - run: npm pack --dry-run

  passed:
    name: Tests passed
    needs: test
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Require all test jobs to pass
        env:
          TEST_RESULT: ${{ needs.test.result }}
        run: test "$TEST_RESULT" = success

Tests passed 是一個統一的檢查名稱,可以放進 main 分支的必要檢查。if: always() 是讓前面的測試失敗時,這個檢查仍然有機會執行;它接著會檢查測試結果,並不是把失敗變成成功。

不過,看到 Workflow 有這個 job,還不能直接說「PR 已經不能亂合併」。要再確認 GitHub 的分支保護規則真的把它列為必要檢查,以及管理員是否保留略過規則的權限。這次的設定有保留管理員略過,因此不能宣稱任何人都無法直接推送。

我另外讓 Agent 建立一個小型文件 PR,實際跑完六組 CI,看到必要檢查通過後才合併。這比只檢查 YAML 語法,更能證明 PR 事件與檢查名稱接得起來。


步驟六:在 npm 網站設定 Trusted Publisher

這次我直接要求 Agent 使用瀏覽器操作 npm 網站:

請使用瀏覽器,開啟 my-agent-cli 的 npm Settings。
新增 GitHub Actions Trusted Publisher,設定為:

Organization or user: your-github-account
Repository: my-agent-cli
Workflow filename: publish.yml
Environment name: npm
Allowed actions: 允許 npm publish

請實際完成儲存並核對畫面上的設定。
如果出現登入、2FA 或需要我操作的瀏覽器認證,
停在該畫面交由我完成;完成後請繼續,不要把它當成設定已成功。

這幾個欄位要能對得起來:

npm 設定 對應的 GitHub 設定
Organization or user:your-github-account 儲存庫擁有者
Repository:my-agent-cli 儲存庫名稱
Workflow filename:publish.yml .github/workflows/publish.yml,欄位只填檔名
Environment name:npm 發佈 job 的 environment: npm
允許 npm publish Workflow 確實執行 npm publish

Environment name 不是 npm 的帳號,也不是儲存庫名稱。我另外把 GitHub 的 npm environment 限制成只允許 main 分支部署,讓這個發佈入口有明確範圍。

還有一個與一月文章不同的地方:目前的介面有 Allowed actions。依官方文件,新連線會允許 npm stage publish;如果要像這次一樣直接發佈,還要允許 npm publish,不能只把前面幾個文字欄位填好就結束。

這次我先完成了一次登入認證,送出 Trusted Publisher 表單時,npm 又要求了一次 2FA。Agent 因此停在驗證畫面等我接手。已登入 npm 網站,不代表每一個帳號設定操作都不需要再次驗證。

完成後,畫面先顯示連線已加入,狀態則是 Pending validation。等到 GitHub Actions 真正用 OIDC 發佈成功,才看到 Valid。所以我把「已填表」、「已儲存」與「已驗證」當成三個不同的進度,不再只看 Agent 說了一句完成。


步驟七:讓 GitHub Actions 自己取得發佈身分

先更新一個版本資訊:一月那篇文章的環境說明,不能原封不動沿用到現在。本文查閱時,npm 的 Trusted Publishing 文件列出 npm CLI 至少 11.5.1、Node.js 至少 22.14.0。我這次的發佈 job 使用 Node.js 24,另外明確安裝 npm 12.2.1。

不要只寫「LTS 應該夠新」就算了,請讓 Agent 查目前支援範圍,再把實際測試版本記錄下來。

下面是搭配前面 ci.yml 的發佈流程。為了閱讀方便,Actions 使用版本標籤;實際專案固定完整 SHA。這份範例在發佈 job 關閉 setup-node 快取,CI 測試仍可使用自己的 npm 快取。它會用到下方說明的兩個版本管理腳本,並不是單獨貼一份 YAML 就能完成的範例。

name: Publish npm

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: npm-publish
  queue: max
  cancel-in-progress: false

jobs:
  ci:
    uses: ./.github/workflows/ci.yml

  publish:
    needs: ci
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: npm
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/[email protected]
        with:
          persist-credentials: false
      - uses: actions/[email protected]
        with:
          node-version: 24
          package-manager-cache: false
      - run: npm install --global [email protected]
      - run: npm ci
      - name: Select an unpublished version
        id: release
        run: node scripts/prepare-release.mjs
      - name: Publish using GitHub OIDC
        if: steps.release.outputs.publish == 'true'
        run: npm publish --provenance --access public --tag latest
      - name: Verify registry propagation
        if: steps.release.outputs.publish == 'true'
        run: node scripts/verify-release.mjs

這裡有幾個設計是刻意分開的:

  • PR 測試只需要 contents: read,沒有 id-token: write。
  • 發佈 job 才取得 OIDC 權限,而且必須等 ci 成功。
  • 沒有設定 NPM_TOKEN 或 NODE_AUTH_TOKEN 來執行 npm publish。
  • environment: npm 必須與 npm 網站的設定一致。
  • 測試可以重用 workflow,但這次把真正的 npm publish 留在 publish.yml,減少辨識發佈 workflow 時的混淆。

範例保留 --provenance,讓命令的意圖清楚。以本文使用的公開 GitHub 儲存庫與 npm 套件而言,目前 Trusted Publishing 也會自動產生 provenance;它不是要求另外建立一個祕密變數。

這次都是公開相依套件。如果你的 npm ci 必須安裝私有套件,那是另一個需要處理的讀取授權,不能因為發佈已經有 OIDC,就推論私有套件安裝也不用認證。

套用時請讓 Agent 把這兩份 workflow 與專案的 scripts 一起驗證。上面的發佈範例另外需要 prepare-release.mjs 與 verify-release.mjs;下載後放到專案的 scripts/,或由 Agent 依相同規則實作。


步驟八:處理自動升版與重跑,避免下一次發佈撞版本

如果選擇「每次 main push 都發佈」,就不能永遠把 package.json 留在同一個版本,然後期待 npm publish 自動幫你遞增。

範例中的 prepare-release.mjs 會先讀取 registry,再決定這次是否需要發佈:

查詢 registry 的所有版本
如果目前 commit 已經發布過:
    輸出 publish=false,跳過
否則:
    找出最高的穩定版本
    如果專案設定的版本比它高,使用專案版本
    否則,把最高穩定版本的 patch 加一
    只更新 CI 工作目錄的版本與 gitHead

例如,registry 只有初始化的 0.0.0,專案版本是 0.1.0,第一次就發佈 0.1.0。下一次 main push 再變成 0.1.1。如果我要開始 0.2.0,則可以明確調整專案版本。

不要為了自動升版,把版本更新再推回 main,然後又觸發下一次自動升版。 這次只修改 runner 的工作目錄,不建立版本 commit。

我也要求 Agent 把「同一個 commit 重跑」納入測試。發佈失敗後重試很正常,但若前一次其實已經成功,下一次就應該辨識出來並跳過,而不是憑空多產生一個版本。

另外,這個查版本、再發佈的過程需要排隊,避免兩個工作同時算出同一個下一版。上面的 queue: max 是依本文查閱時的 GitHub concurrency 文件設定;一般 concurrency 預設只保留一個 pending 工作,不能想當然地以為每次 push 都會完整排隊等待。這個設定也不是無限佇列,官方上限是 100 個等待工作。


步驟九:不要只看見成功訊息,就說已經上架

這次最值得補進驗收提示詞的,就是下面這段:

請逐項驗證,不要把不同階段混成一句「已完成」:

1. Trusted Publisher 的設定已儲存,欄位與 GitHub 相符。
2. GitHub Actions 的測試工作全部通過。
3. 發佈 job 使用 OIDC 成功,不需要人工 npm login 或寫入 token。
4. npm registry 能查到指定版本,latest 指向預期版本。
5. registry 的 gitHead 對應本次發佈 commit。
6. tarball 能下載,且可找到 provenance 資訊。
7. 用指定版本安裝後,實際執行 CLI 的 --version 與 --dry-run。

如果 npm 仍在處理,請回報「發布請求已接受,尚未驗證可下載」,
不要重複發布同一版,也不要改用另一個版本掩蓋尚未查明的問題。

我第一次發佈時,終端機印出了成功訊息,但 registry 還查不到指定版本。Agent 一度只確認到名稱已建立,後來才確認版本真的能下載。這兩個時間點應該分開記錄。

完整版本的檢查可以這樣做:

npm view [email protected] version gitHead dist.tarball --json
npm view my-agent-cli dist-tags --json
npm view [email protected] dist.attestations --json

npm exec --yes [email protected] -- my-agent --version
npm exec --yes [email protected] -- \
  my-agent --input ./input.txt --format json --dry-run

上面的套件名稱與命令名稱都是示意,請換成自己已審查並發佈的工具,再執行 npm exec。最後一個命令只驗證安裝後的 CLI 與 dry-run 行為;如果工具會存取外部服務,不能拿這個結果宣稱真實服務的整合也已完成。

這次最後確認到:0.1.0 是透過 GitHub Actions 的 OIDC 發佈,registry 有對應的 gitHead 與 provenance 資訊,安裝後的 --version 與 --dry-run 也能正常執行。後續文件合併觸發的 0.1.1,同樣有成功的 workflow 紀錄。


初始化版本用完了,清理也要精準指定

正式版本完成後,我要求 Agent 下架初始化的 0.0.0 與當時出現的 0.0.0-stage,留下可用的 CLI 版本:

npm unpublish [email protected]
npm unpublish [email protected]

這裡要明確指定版本。不要把只想移除兩個測試版本的需求,改寫成移除整個套件。 能否下架仍受 npm unpublish 規則限制,而且同一個套件名稱與版本在下架後不能重新使用。

0.0.0-stage 也不能跟我自己建立的 0.0.0 混為一談。npm 的 staged publishing 文件有說明這個 placeholder 版本;這次確實在 registry 看過它,但當時的 npm stage list 是空的,所以我沒有要求 Agent 憑這個版本名稱就推論還有一個等待人工核准的工作。

實際清理時,npm 對兩次下架分別要求了瀏覽器驗證。完成後,Agent 再查版本清單與兩份 tarball,確認版本已消失、下載回傳 404,同時 0.1.0 與 0.1.1 都還存在。這才算清理完成。


總結

這次從 CLI 開發到 npm 上架,大部分的檔案與網站操作確實都交給 Agent 做了。它可以查文件、寫測試、建立儲存庫、設定 workflow、操作 npm 表單,也可以在失敗後繼續找原因。我需要做的,則是說清楚需求與發佈策略,完成必要的帳號驗證,再看它拿出的證據。

我覺得 Agentic 版本最有價值的地方,是把原本散在終端機、GitHub 與 npm 網站的操作接起來。但接起來之後,更要分清楚哪一步只是已送出、哪一步已經被接受、哪一步真的可以交付給使用者。

下次要交給 Agent 做類似工作,我會先講清楚三件事:發佈什麼、何時發佈、如何驗收。至於「main 有 commit 就發佈」這句話,現在我會連文件變更也一起想清楚,再交出去。


相關連結

留言評論