- 專案骨架:Elixir + Mix escript(單檔可執行 bear),HTTP 一律 Req - 設定/憑證檔讀寫:0600、原子寫入(rename)、O_CREAT|O_EXCL 防 symlink - PAT Bearer 呼叫 /userinfo;--json 輸出;規格化退出碼 - 補齊 main 規格承諾的 BEAR_TOKEN 環境變數(優先於憑證檔、不寫入) - login 無效 PAT 退出碼 3(依 docs/commands.md §3.1);token --refresh 用法錯誤 2(§3.3) - 35 個單元測試;mix precommit(compile --warnings-as-errors + format + test)全綠 - 沿用已關閉 PR #3 的實作(feat/pat-mode-mvp 分支),文件改依 main 現行版本更新 apps 管理指令待 #5 規格與 alterminal/bear#28 API 就緒後實作。
19 KiB
Bear CLI 指令規劃(Command Spec)
狀態:P2 PAT 模式 MVP 已實作(見第 9 節);Device Flow 待伺服器端 P1。 範圍:本文件為指令介面規格;App 管理指令(
bear appsCRUD)另見 #5 規劃。 登入採用的 OIDC Device Authorization Grant(RFC 8628)細節,見 bear 倉庫docs/cli-feature-plan.md(issue #17)。 另支援以**個人存取權杖(PAT)**登入(bear 已於 profile 頁提供 PAT 建立/列表/撤銷,/userinfo接受 PAT 作為 Bearer)。
1. 命名與呼叫慣例
- 可執行檔名:
bear(Linux 終端機呼叫bear <command>)。 - 指令採「動詞在前」的扁平結構,不設多層子指令(首版維持簡單,後續需要再擴充)。
- 全部指令支援
--json輸出機器可讀結果;人類可讀輸出為預設。 - 輸出語言跟隨系統 Locale,優先繁體中文(zh_TW),英文(en)為後備。
2. 全域選項(Global Options)
| 選項 | 型別 | 預設 | 說明 |
|---|---|---|---|
--issuer URL |
string | https://alterminal.com |
Bear(OIDC Provider)位址;可被設定檔覆寫 |
--config PATH |
string | ~/.config/bear/config.json |
設定檔路徑 |
--json |
bool | false |
機器可讀輸出(JSON) |
-v, --verbose |
bool | false |
顯示詳細日誌(含 HTTP 請求摘要,但不含 token) |
-h, --help |
bool | — | 顯示說明 |
--version |
bool | — | 顯示版本後離開 |
優先序:CLI flag > 環境變數(BEAR_ISSUER、BEAR_TOKEN)> 設定檔 > 內建預設。
BEAR_TOKEN為一次性/CI 用的個人存取權杖(PAT);提供後優先於本機憑證檔,但不寫入憑證檔。
3. 指令詳述
3.1 bear login
登入 Bear,取得並保存憑證。預設走 Device Flow;亦可用 --token 直接以**個人存取權杖(PAT)**登入。
bear login [--scope "openid profile email"] [--client-id ID]
bear login --token <PAT>
| 選項 | 預設 | 說明 |
|---|---|---|
--scope |
openid profile email |
Device Flow 請求的 scope(空白分隔) |
--client-id |
設定檔內值 | OIDC client id |
--token <PAT> |
— | 直接以個人存取權杖登入(跳過 Device Flow) |
行為(Device Flow,未給 --token):
-
讀取設定 → 向
{issuer}/.well-known/openid-configuration取device_authorization_endpoint(動態發現,不寫死網址)。 -
POST /device_authorization(client_id+scope)。 -
終端印出:
請在瀏覽器開啟:https://alterminal.com/device 輸入代碼:ABCD-EFGH若終端支援,同步印出 QR(掃碼直達
verification_uri_complete)。 -
以
interval週期輪詢POST /token(grant_type=device_code):authorization_pending→ 繼續等待;slow_down→ 加大輪詢間隔;access_denied→ 中止並提示;- 成功 → 寫入憑證檔(0600),印出
已登入:<email>。
行為(PAT,給 --token <PAT>):
- 以該 PAT 作為 Bearer 呼叫
GET /userinfo驗證可用性並取得身分。 - 成功 → 將 PAT 直接寫入憑證檔(0600),印出
已登入:<email>(無 refresh token)。 - 失敗(401/無效或已撤銷)→ 提示 PAT 無效,不寫入任何資料。
退出碼:0 成功;3 PAT 無效或已撤銷;4 等待逾時(expires_in);5 使用者拒絕;6 網路/伺服器錯誤。
--json 輸出範例(成功):
{"ok": true, "email": "alice@example.com", "expires_in": 3600}
PAT 模式下 expires_in 依權杖到期日回傳;無到期日則省略該欄位。
3.2 bear whoami
顯示目前登入身分(GET /userinfo,Bearer access token)。
bear whoami
行為:讀取憑證——Device Flow 讀取快取的 access token(過期則先用 refresh token 輪轉換新);PAT 直接使用該 token——呼叫 /userinfo,印出 claims(name、email、picture 等)。/userinfo 同時接受 OIDC access token 與 PAT 作為 Bearer。
退出碼:0 成功;3 未登入。
輸出範例:
sub : 0190...uuid
name : Alice Chen
email : alice@example.com
picture : https://...
3.3 bear token [--refresh]
印出 access token 到 stdout,供 pipe 給其他工具(如 curl、腳本)。
bear token [--refresh]
| 選項 | 說明 |
|---|---|
--refresh |
強制先以 refresh token 輪轉換新 access token |
行為:
- 若無
--refresh且快取的 access token 未過期 → 直接印出。 - 若過期(或指定
--refresh)→ 以 refresh token 向/token換新,更新快取後印出。 - PAT 模式:直接印出該 PAT(無 refresh token;
--refresh為無效選項,報用法錯誤2)。 - 輸出只含 token 本體(單行、無前綴),方便 pipe;診斷訊息一律走 stderr。
退出碼:0 成功;3 未登入;1 refresh 失敗(token 已撤銷或過期,需重新 login)。
3.4 bear logout
撤銷 refresh token 並清除本機憑證。
bear logout
行為:
- Device Flow:呼叫
POST /revoke(撤銷 refresh token),無論伺服器回應如何,皆刪除本機憑證檔。 - PAT:僅刪除本機憑證檔(PAT 的撤銷需於網頁
/profile/tokens進行,CLI 不提供撤銷)。
退出碼:0 成功;6 伺服器呼叫失敗(但本機憑證已清除)。
3.5 bear status
顯示登入狀態與 token 剩餘效期(不呼叫網路,純本機判定)。
bear status
行為:讀取本機憑證,印出登入與否、憑證類型(Device Flow/PAT)、access token 剩餘秒數、refresh token 是否存在。
退出碼:0 已登入;3 未登入。
輸出範例(Device Flow):
已登入:alice@example.com
憑證類型:Device Flow
access token 剩餘:3210 秒
refresh token:有
issuer:https://alterminal.com
輸出範例(PAT,無到期日):
已登入:alice@example.com
憑證類型:Personal Access Token
access token 剩餘:無到期
refresh token:無
issuer:https://alterminal.com
3.6 bear apps:App 管理指令群(P3,規劃中)
依賴:bear 伺服器端 App 管理 JSON API(alterminal/bear#28,資源前綴
/api/v1/apps,PAT Bearer 認證、管理操作限 admin)。API 落地前本節僅為規格。
bear apps 是「資源群組」指令:bear apps <動詞>。這是第 1 節「扁平結構」原則的首個例外——App 的管理操作(列表、檢視、建立、更新、輪轉 secret、啟停用)若攤平命名會造成指令名爆炸(bear apps-create…),故收斂為單一群組;未來同類資源管理指令比照。
bear apps list [選項]
bear apps show <client-id>
bear apps create [選項]
bear apps update <client-id> [選項]
bear apps rotate-secret <client-id>
bear apps toggle <client-id>
認證與授權(群組共通):
- 一律使用既有 PAT 憑證(
bear login --token登入,或BEAR_TOKEN環境變數)作為 Bearer 呼叫 API;不提供其他登入方式。 - 管理操作僅限 admin 角色帳號。
401(token 無效/已撤銷)→ 退出碼3,提示重新bear login;403(非 admin)→ 退出碼8,明確提示「此操作需 admin 權限」,不重試。 client_secret只在create/rotate-secret成功當下一次性顯示於 stdout,不寫入憑證檔、log、--verbose輸出或版本庫;list/show/update/toggle的回應永遠不含 secret(API 端保證)。- App 識別一律以
client_id(人類可讀、唯一),不暴露資料庫 UUID。若 API 路徑參數僅接受 UUID,由 CLI 先以 list 比對解析(見第 7 節開放問題 6)。
群組共通退出碼:
| 碼 | 語意 |
|---|---|
0 |
成功 |
1 |
目標不存在或驗證失敗(HTTP 404/422;含欄位格式錯誤、client_id 重複) |
2 |
用法錯誤(缺必選參數、列舉值非法、PKCE 未綁 --jwk-id 等,由 CLI 本地檢查) |
3 |
未登入或憑證無效(HTTP 401) |
6 |
網路/伺服器錯誤 |
8 |
權限不足(HTTP 403,管理操作僅限 admin) |
3.6.1 bear apps list
列出 App 清單。
bear apps list [--visibility public|internal] [--status active|inactive]
[--page N] [--per-page N] [--json]
| 選項 | 預設 | 說明 |
|---|---|---|
--visibility |
全部 | 過濾 public 或 internal |
--status |
全部 | 過濾 active 或 inactive |
--page |
1 |
分頁頁碼(對應 API page) |
--per-page |
20 |
每頁筆數(對應 API per_page,上限同伺服器) |
行為:GET /api/v1/apps(Bearer PAT),套用過濾與分頁;人類可讀輸出為表格,依 client_id 排序。
CLIENT ID TITLE VISIBILITY STATUS METHOD SCOPES
my-app My App public active client_secret openid, profile
internal-tool Internal Tool internal inactive PKCE openid
--json 輸出範例:
{"ok": true, "page": 1, "per_page": 20, "apps": [{"client_id": "my-app", "title": "My App", "url": "https://example.com", "method": "client_secret", "status": "active", "visibility": "public", "scopes": ["openid", "profile"], "redirect_urls": ["https://example.com/callback"], "post_logout_redirect_uris": [], "sub": "id", "jwk_id": null, "created_at": "2026-09-07T00:00:00Z", "updated_at": "2026-09-07T00:00:00Z"}]}
3.6.2 bear apps show <client-id>
顯示單一 App 完整資訊。
行為:GET /api/v1/apps/{id}(以 client-id 解析);印出全部欄位(同 create 選項集合,另含 status、created_at/updated_at、jwk_id);client_secret 永不回傳。
輸出範例:
client_id : my-app
title : My App
url : https://example.com
method : client_secret
visibility : public
status : active
scopes : openid, profile
redirect_urls : https://example.com/callback
post_logout_redirect_uris : (無)
sub : id
jwk_id : (無)
created_at : 2026-09-07T00:00:00Z
updated_at : 2026-09-07T00:00:00Z
--json 輸出範例:{"ok": true, "app": {…同 list 之單一物件…}}
3.6.3 bear apps create
建立 App。method=client_secret 且未給 --secret 時,由伺服器自動產生並一次性回傳明文。
bear apps create --client-id ID --url URL --title TITLE
[--method client_secret|PKCE] [--visibility public|internal]
[--redirect-url URL]… [--post-logout-redirect-uri URI]…
[--scope SCOPE]… [--sub FIELD] [--jwk-id ID] [--secret SECRET] [--json]
| 選項 | 預設 | 說明 |
|---|---|---|
--client-id |
(必填) | 唯一識別碼;英數字與連字號/底線 |
--url |
(必填) | 絕對 URI(http/https 或自訂 scheme 如 myapp://) |
--title |
(必填) | 顯示名稱 |
--method |
client_secret |
client_secret 或 PKCE;PKCE 必須給 --jwk-id |
--visibility |
internal |
public 或 internal |
--redirect-url |
[] |
可重複給予,附加為允許的 redirect URL 清單 |
--post-logout-redirect-uri |
[] |
可重複給予,登出後跳轉 URI 清單 |
--scope |
["openid"] |
可重複給予,允許的 scope 清單 |
--sub |
id |
作為 sub claim 的欄位名 |
--jwk-id |
— | 綁定的 JWK id(method=PKCE 時必填) |
--secret |
自動產生 | 自備 client secret;省略則由伺服器產生並一次性回傳 |
行為:POST /api/v1/apps(Bearer PAT)。成功後印出 App 摘要與一次性 secret 區塊:
App 已建立:my-app(public/active)
client_secret(只顯示這一次,請立即保存):
4f9c1d2e-…
--json 輸出範例:{"ok": true, "app": {…}, "client_secret": "4f9c1d2e-…"}(secret 同樣只出現這一次。)
3.6.4 bear apps update <client-id>
更新既有 App。只更新有給的欄位(部分更新);未給的欄位保持不變。client_id 不可變更(伺服器 update 不允許);status 不在此改(用 toggle)。
bear apps update <client-id> [--url URL] [--title TITLE]
[--method client_secret|PKCE] [--visibility public|internal]
[--redirect-url URL]… [--post-logout-redirect-uri URI]…
[--scope SCOPE]… [--sub FIELD] [--jwk-id ID] [--json]
行為:PUT /api/v1/apps/{id}(Bearer PAT),只送有給的欄位。清單類欄位(--redirect-url、--post-logout-redirect-uri、--scope)為整組覆寫語意:有給即取代整份清單。成功後印出更新後的 App 摘要(同 show 的精簡版)。422 驗證失敗時逐欄位列出錯誤。
3.6.5 bear apps rotate-secret <client-id>
輪轉 client secret。舊 secret 立即失效。
行為:POST /api/v1/apps/{id}/rotate-secret(Bearer PAT)。成功後一次性顯示新 secret(提示同 create)。method=PKCE 的 App 不使用 client secret → 退出碼 1,提示該 App 無 secret 可輪轉。
--json 輸出範例:{"ok": true, "client_secret": "9a7b3c…"}(secret 只出現這一次)
3.6.6 bear apps toggle <client-id>
切換 App 狀態:active ↔ inactive。
行為:POST /api/v1/apps/{id}/toggle(Bearer PAT)。成功後印出新狀態,如 my-app:active → inactive。
--json 輸出範例:{"ok": true, "app": {"client_id": "my-app", "status": "inactive"}}
4. 退出碼總表
| 碼 | 語意 |
|---|---|
0 |
成功 |
1 |
一般錯誤(含 refresh 失敗、憑證檔讀寫失敗) |
2 |
用法錯誤(未知指令/參數) |
3 |
未登入(需先 bear login) |
4 |
授權等待逾時(authorization_pending 超過 expires_in) |
5 |
授權被拒絕(access_denied) |
6 |
網路/伺服器錯誤 |
7 |
設定檔/憑證檔格式損毀 |
8 |
權限不足(HTTP 403;App 管理操作僅限 admin) |
5. 設定檔與憑證檔
| 檔 | 路徑 | 內容 | 權限 |
|---|---|---|---|
| 設定 | ~/.config/bear/config.json |
issuer、client_id、scope |
0644 |
| 憑證 | ~/.local/state/bear/credentials.json |
Device Flow:refresh_token+快取 access_token/expires_at;PAT:access_token(無 refresh) |
0600 |
- Device Flow 只以 refresh token 為主:access token 短命快取,過期以 refresh token 輪轉換新(沿用 Bear 既有 refresh rotation + 重用偵測)。
- PAT 直接存 access token:無 refresh token、無輪轉;效期由權杖本身的
expires_at決定(可無到期日)。 - 寫入憑證檔務必 0600,且以
O_CREAT|O_EXCL防止 symlink 攻擊。 - 未來可擴充系統 keyring(libsecret / Keychain),避免明文檔。
6. 錯誤處理與安全
authorization_pending、slow_down、access_denied、invalid_grant依 RFC 8628 §3.5 顯示對應中文提示。- 所有診斷訊息走 stderr;
--json模式將錯誤以{"ok": false, "error": "...", "code": N}輸出。 - token、user_code、device_code 一律不入 log、不入 stdout(除
bear token之目的外)、不入版本庫。 client_secret(App 管理)僅於apps create/apps rotate-secret成功當下一次性輸出;403顯示「此操作需 admin 權限」並以退出碼8結束,不重試。- 網路層一律使用
Req,遵循 bear 倉庫 HTTP 客戶端慣例。
7. 開放問題(待確認)
- CLI 的 OIDC client 採新
method: "device"(client auth 為none)或沿用PKCE+JWK?傾向前者(見 bearcli-feature-plan.md§5.5)。 - 發行格式:escript(目標機需 Erlang)或 self-contained release(bakeware/burrito)?
bear apps list --visibility public未來是否開放非 admin(僅公開/active 清單,同網頁 Launch)?目前規劃為 admin 限定。- 是否支援
offline_accessscope,或沿用現行 refresh 輪轉即可? - 是否在 CLI 提供「建立/撤銷 PAT」?目前 PAT 管理僅限網頁(需 session),傾向 CLI 只「使用」不「管理」。
- App 管理 API 的路徑參數最終採 UUID 或
client_id(alterminal/bear#28 規格為{id})?若為 UUID,CLI 需先以 list 解析client_id→ UUID,或伺服器提供以client_id直取的端點。
8. 對應關係
| 本指令 | 對應 OIDC/Bear 端點 |
|---|---|
login |
/.well-known/openid-configuration → POST /device_authorization → POST /token(device_code) |
login --token <PAT> |
GET /userinfo(Bearer PAT 驗證身分,無 OIDC 端點) |
whoami |
GET /userinfo(接受 OIDC access token 或 PAT) |
token --refresh |
POST /token(refresh_token,僅 Device Flow) |
logout |
POST /revoke(僅 Device Flow;PAT 僅本機刪除,撤銷走網頁 /profile/tokens) |
apps list |
GET /api/v1/apps(Bearer PAT;分頁 page/per_page) |
apps show |
GET /api/v1/apps/{id} |
apps create |
POST /api/v1/apps |
apps update |
PUT /api/v1/apps/{id} |
apps rotate-secret |
POST /api/v1/apps/{id}/rotate-secret |
apps toggle |
POST /api/v1/apps/{id}/toggle |
apps群組依賴 alterminal/bear#28 的 App 管理 JSON API(PAT Bearer、admin 限定)。
9. 現行實作狀態(PAT 模式 MVP,issue #6)
目前實作為 PAT 模式 MVP(Elixir + Req、mix escript),與上方 Device Flow 規格的差異:
| 指令 | 原規格(Device Flow) | 現行實作(PAT 模式) |
|---|---|---|
login |
裝置碼流程 | --token <PAT> 驗證 GET /userinfo 後保存(- 可從 stdin 讀);未給 --token 時提示 Device Flow 尚未開放(退出碼 2) |
whoami |
GET /userinfo(refresh 輪轉) |
GET /userinfo(Bearer PAT,無輪轉);BEAR_TOKEN 提供時優先使用 |
token |
refresh token 輪轉換新 | 直接印出 PAT;--refresh 為用法錯誤(退出碼 2);BEAR_TOKEN 提供時優先印出 |
logout |
POST /revoke(撤銷 refresh token) |
僅清除本機憑證;PAT 需至網頁 /profile/tokens 撤銷 |
status |
顯示 access token 剩餘秒數 | 純本機判定:顯示模式(PAT/BEAR_TOKEN 環境變數)與 issuer |
apps |
P3 暫緩 | 未實作;App 管理指令規格見 #5,伺服器端 API 見 alterminal/bear#28 |
退出碼差異:login/whoami 的 token 無效(401)依 §3.1 為 3(本節實作一致)。
憑證檔結構(PAT 模式,0600、原子寫入、O_EXCL):
{"issuer": "https://alterminal.com", "access_token": "<PAT>", "email": "..."}
建置:
mix deps.get && mix test && mix escript.build # 產生單檔可執行 bear