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:
+102
@@ -0,0 +1,102 @@
|
||||
import { UsageError } from "./errors.js";
|
||||
import type { Command, CommandContext, Registry } from "./command.js";
|
||||
import { flagString } from "./command.js";
|
||||
import { loadConfig } from "./config.js";
|
||||
import { request } from "./http.js";
|
||||
|
||||
/** 共用:載入設定並執行一次 API 請求,把結果印成 JSON。 */
|
||||
async function call(
|
||||
ctx: CommandContext,
|
||||
path: string,
|
||||
options: { method?: string; body?: unknown; auth?: boolean; print?: boolean } = {},
|
||||
): Promise<unknown> {
|
||||
const { print = true } = options;
|
||||
const config = await loadConfig({
|
||||
apiUrl: flagString(ctx.args.flags, "api-url"),
|
||||
token: flagString(ctx.args.flags, "token"),
|
||||
});
|
||||
const data = await request(config, path, options);
|
||||
if (print) ctx.print(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function registerAuthCommands(registry: Registry): void {
|
||||
registry.add({
|
||||
path: ["auth", "status"],
|
||||
summary: "查詢登入/SSO 設定(公開端點)",
|
||||
usage: "fox auth status",
|
||||
run: async (ctx) => {
|
||||
await call(ctx, "auth/status");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerTokenCommands(registry: Registry): void {
|
||||
registry.add({
|
||||
path: ["tokens", "verify"],
|
||||
summary: "驗證手中 token 是否有效(無效回非零 exit code)",
|
||||
usage: "fox tokens verify",
|
||||
run: async (ctx) => {
|
||||
await call(ctx, "access-tokens/verify", { auth: true });
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["tokens", "create"],
|
||||
summary: "建立新 token(原始值只在這次輸出顯示)",
|
||||
usage: "fox tokens create --name <名稱> [--expires-at <ISO8601>]",
|
||||
run: async (ctx) => {
|
||||
const name = flagString(ctx.args.flags, "name");
|
||||
if (!name || !name.trim()) {
|
||||
throw new UsageError("缺少 --name <名稱>;用法:fox tokens create --name <名稱> [--expires-at <ISO8601>]");
|
||||
}
|
||||
const expiresAt = flagString(ctx.args.flags, "expires-at");
|
||||
if (expiresAt !== undefined && Number.isNaN(Date.parse(expiresAt))) {
|
||||
throw new UsageError(`--expires-at 不是有效的 ISO 8601 日期:${expiresAt}`);
|
||||
}
|
||||
const body: Record<string, string> = { name: name.trim() };
|
||||
if (expiresAt !== undefined) body.expiresAt = new Date(expiresAt).toISOString();
|
||||
await call(ctx, "access-tokens", { method: "POST", body });
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["tokens", "list"],
|
||||
summary: "列出 token(不含 hash/原始值)",
|
||||
usage: "fox tokens list",
|
||||
run: async (ctx) => {
|
||||
await call(ctx, "access-tokens");
|
||||
},
|
||||
});
|
||||
|
||||
registry.add({
|
||||
path: ["tokens", "revoke"],
|
||||
summary: "撤銷 token(冪等)",
|
||||
usage: "fox tokens revoke <id>",
|
||||
run: async (ctx) => {
|
||||
const id = ctx.args.positionals[0];
|
||||
const n = Number(id);
|
||||
if (id === undefined || !/^\d+$/.test(id) || !Number.isSafeInteger(n)) {
|
||||
throw new UsageError("缺少或無效的 token id;用法:fox tokens revoke <id>");
|
||||
}
|
||||
await call(ctx, `access-tokens/${n}`, { method: "DELETE", print: false });
|
||||
ctx.print({ ok: true, id: n });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function registerNovelCommands(registry: Registry): void {
|
||||
registry.add({
|
||||
path: ["novels", "get"],
|
||||
summary: "查單一作品(公開端點)",
|
||||
usage: "fox novels get <slug>",
|
||||
run: async (ctx) => {
|
||||
const slug = ctx.args.positionals[0];
|
||||
if (!slug) throw new UsageError("缺少 <slug>;用法:fox novels get <slug>");
|
||||
await call(ctx, `novels/${encodeURIComponent(slug)}`);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 共用型別:命令實作可引用的介面(保持單一來源)。 */
|
||||
export type { Command };
|
||||
Reference in New Issue
Block a user