feat(cli): 實作 CLI 各指令與 help(M1+M2,issue #13)

- 加入 pnpm workspace;Node.js ≥22 + TypeScript 實作
- 指令:auth status、tokens verify/create/list/revoke、novels get
- 非互動、stdout 僅輸出單一 JSON;exit code 0/1/2
- 設定優先順序:--token/--api-url > FOX_API_TOKEN/FOX_API_URL > ~/.config/fox/cli.json
- novels list/create/update 待後端接受 Bearer token 後補齊(README 授權缺口)
- 已對模擬後端完成端到端驗證(含 401/404/網路錯誤、冪等 revoke)
This commit is contained in:
2026-09-04 20:26:11 +08:00
parent 1686322eff
commit 80b784a9f3
9 changed files with 548 additions and 4 deletions
+53
View File
@@ -0,0 +1,53 @@
import { readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { ApiError } from "./errors.js";
/** CLI 設定:API 位址與 access token。 */
export interface FoxConfig {
apiUrl: string;
token: string | null;
}
const DEFAULT_API_URL = "http://localhost:3000";
/**
* 讀取設定。優先順序(高 → 低):
* 1. 環境變數 FOX_API_URL / FOX_API_TOKEN
* 2. --api-url / --token 參數(由 caller 傳入)
* 3. 本機設定檔 ~/.config/fox/cli.json({"apiUrl": "...", "token": "..."})
*
* apiUrl 預設 http://localhost:3000;token 找不到時為 null,
* 需要 token 的命令由 caller 檢查並報錯。
*/
export async function loadConfig(flags: { apiUrl?: string; token?: string }): Promise<FoxConfig> {
const configPath = join(homedir(), ".config", "fox", "cli.json");
let fileApiUrl: string | undefined;
let fileToken: string | undefined;
try {
const raw = await readFile(configPath, "utf-8");
const parsed: unknown = JSON.parse(raw);
if (parsed && typeof parsed === "object") {
const obj = parsed as Record<string, unknown>;
if (typeof obj.apiUrl === "string") fileApiUrl = obj.apiUrl;
if (typeof obj.token === "string") fileToken = obj.token;
}
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if (code !== "ENOENT") {
// 設定檔存在但壞掉:明確報錯,而不是靜默忽略。
throw new ApiError(`設定檔 ${configPath} 無法解析:${(err as Error).message}`);
}
}
const envApiUrl = process.env.FOX_API_URL?.trim() || undefined;
const envToken = process.env.FOX_API_TOKEN?.trim() || undefined;
const apiUrl =
flags.apiUrl ??
envApiUrl ??
fileApiUrl ??
DEFAULT_API_URL;
const token = flags.token ?? envToken ?? fileToken ?? null;
return { apiUrl: apiUrl.replace(/\/+$/, ""), token };
}