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
+96
View File
@@ -0,0 +1,96 @@
defmodule BearCli.Config do
@moduledoc """
設定檔與憑證檔的路徑及讀取。
設定優先序:CLI flag > 環境變數(`BEAR_ISSUER`)> 設定檔 > 內建預設。
"""
@default_issuer "https://alterminal.com"
@doc "內建預設 issuer。"
def default_issuer, do: @default_issuer
@doc """
設定檔路徑:`~/.config/bear/config.json`(可用 `BEAR_CONFIG` 覆寫)。
"""
def config_path do
System.get_env("BEAR_CONFIG") || Path.join(config_home(), "config.json")
end
@doc """
憑證檔路徑:`~/.local/state/bear/credentials.json`(可用 `BEAR_CREDENTIALS` 覆寫)。
"""
def credentials_path do
System.get_env("BEAR_CREDENTIALS") || Path.join(state_home(), "credentials.json")
end
defp config_home do
base =
System.get_env("XDG_CONFIG_HOME") ||
Path.join(System.user_home!(), ".config")
Path.join(base, "bear")
end
defp state_home do
base =
System.get_env("XDG_STATE_HOME") ||
Path.join(System.user_home!(), ".local/state")
Path.join(base, "bear")
end
@doc """
讀取設定檔,回傳 `{:ok, map}` 或 `:error`(檔案不存在/JSON 損毀)。
"""
def read_config(path \\ config_path()) do
case File.read(path) do
{:ok, content} -> Jason.decode(content)
{:error, _} -> :error
end
end
@doc """
解析最終 issuer,依序取第一個非空白值:
1. `cli_issuer`(`--issuer` flag)
2. `BEAR_ISSUER` 環境變數
3. 設定檔的 `issuer` 欄位(讀自 `config_path_override` 或預設路徑)
4. 內建預設 `https://alterminal.com`
"""
def resolve_issuer(cli_issuer, config_path_override \\ nil) do
path = config_path_override || config_path()
config =
case read_config(path) do
{:ok, map} when is_map(map) -> map
_ -> %{}
end
Enum.find(
[
cli_issuer,
System.get_env("BEAR_ISSUER"),
config["issuer"],
@default_issuer
],
&(not blank?(&1))
)
end
@doc """
`BEAR_TOKEN` 環境變數(一次性/CI 用的 PAT)。
提供後優先於本機憑證檔,但不寫入憑證檔。空值或純空白視為未提供。
"""
def env_token do
case System.get_env("BEAR_TOKEN") do
nil -> nil
token -> if blank?(String.trim(token)), do: nil, else: String.trim(token)
end
end
defp blank?(nil), do: true
defp blank?(""), do: true
defp blank?(_), do: false
end