feat: 實作 bear CLI 骨架與 PAT 模式共用基礎(issue #6)

- 專案骨架: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 就緒後實作。
This commit is contained in:
2026-09-07 22:59:05 +08:00
parent c26420303e
commit 93176c0eb1
15 changed files with 1279 additions and 3 deletions
+54
View File
@@ -0,0 +1,54 @@
defmodule BearCli.Api do
@moduledoc """
Bear 伺服器 HTTP 介面(一律使用 `Req`)。
目前僅實作 PAT 模式所需的 `GET /userinfo`。
"""
@finch BearCli.Finch
@doc """
呼叫 `GET {issuer}/userinfo`(Bearer token)。
回傳:
- `{:ok, claims}` — 200,claims 為 JSON map
- `{:error, :unauthorized, description}` — 401
- `{:error, :server_error, status}` — 其他非 2xx
- `{:error, :network, reason}` — 傳輸層錯誤
"""
def userinfo(issuer, token) do
url = String.trim_trailing(issuer, "/") <> "/userinfo"
case Req.get(url,
headers: [authorization: "Bearer " <> token, accept: "application/json"],
retry: false,
finch: [name: @finch]
) do
{:ok, %Req.Response{status: 200, body: body}} ->
{:ok, decode_body(body)}
{:ok, %Req.Response{status: 401, body: body}} ->
{:error, :unauthorized, error_description(body)}
{:ok, %Req.Response{status: status}} ->
{:error, :server_error, status}
{:error, exception} ->
{:error, :network, Exception.message(exception)}
end
end
defp decode_body(%{} = body), do: body
defp decode_body(body) when is_binary(body), do: Jason.decode!(body)
defp decode_body(_), do: %{}
defp error_description(body) when is_binary(body) do
case Jason.decode(body) do
{:ok, %{"error_description" => desc}} when is_binary(desc) -> desc
{:ok, %{"error" => err}} when is_binary(err) -> err
_ -> "invalid_token"
end
end
defp error_description(_), do: "invalid_token"
end