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:
@@ -0,0 +1,89 @@
|
||||
/** 命令定義:名稱、說明、參數與執行。供 help 與分發使用。 */
|
||||
|
||||
export interface ParsedArgs {
|
||||
/** 位置參數(子命令後的非選項值)。 */
|
||||
positionals: string[];
|
||||
/** 選項旗標(--key value 或 --flag)。 */
|
||||
flags: Record<string, string | boolean>;
|
||||
}
|
||||
|
||||
export interface CommandContext {
|
||||
args: ParsedArgs;
|
||||
/** 輸出 JSON 到 stdout(唯一 stdout 內容)。 */
|
||||
print: (value: unknown) => void;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
/** 子命令路徑,例如 ["tokens", "verify"]。 */
|
||||
path: string[];
|
||||
summary: string;
|
||||
usage: string;
|
||||
/** 位置參數說明,例如 ["<slug>"]。與 usage 一致即可。 */
|
||||
run: (ctx: CommandContext) => Promise<void> | void;
|
||||
}
|
||||
|
||||
/** 解析 argv:--key value、--key=value、--flag(布林)、-- 結束選項。 */
|
||||
export function parseArgs(argv: string[]): ParsedArgs {
|
||||
const positionals: string[] = [];
|
||||
const flags: Record<string, string | boolean> = {};
|
||||
let i = 0;
|
||||
let noMoreFlags = false;
|
||||
while (i < argv.length) {
|
||||
const arg = argv[i++]!;
|
||||
if (!noMoreFlags && arg === "--") {
|
||||
noMoreFlags = true;
|
||||
continue;
|
||||
}
|
||||
if (!noMoreFlags && arg.startsWith("--")) {
|
||||
const eq = arg.indexOf("=");
|
||||
if (eq !== -1) {
|
||||
const key = arg.slice(2, eq);
|
||||
const value = arg.slice(eq + 1);
|
||||
flags[key] = value;
|
||||
} else {
|
||||
const key = arg.slice(2);
|
||||
const next = argv[i];
|
||||
if (next !== undefined && !next.startsWith("--")) {
|
||||
flags[key] = next;
|
||||
i += 1;
|
||||
} else {
|
||||
flags[key] = true;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
positionals.push(arg);
|
||||
}
|
||||
return { positionals, flags };
|
||||
}
|
||||
|
||||
export function flagString(flags: Record<string, string | boolean>, key: string): string | undefined {
|
||||
const v = flags[key];
|
||||
return typeof v === "string" ? v : undefined;
|
||||
}
|
||||
|
||||
/** 註冊表:以完整路徑字串("tokens verify")索引。 */
|
||||
export class Registry {
|
||||
private readonly commands = new Map<string, Command>();
|
||||
|
||||
add(command: Command): void {
|
||||
this.commands.set(command.path.join(" "), command);
|
||||
}
|
||||
|
||||
/** 找出命令;prefix 可指向群組(回傳其子命令列表供 help)。 */
|
||||
find(prefix: string[]): Command | undefined {
|
||||
return this.commands.get(prefix.join(" "));
|
||||
}
|
||||
|
||||
/** 列出 prefix 下的下一層子命令(供群組 help)。 */
|
||||
children(prefix: string[]): Command[] {
|
||||
return Array.from(this.commands.values()).filter((c) => {
|
||||
if (c.path.length !== prefix.length + 1) return false;
|
||||
return prefix.every((p, idx) => c.path[idx] === p);
|
||||
});
|
||||
}
|
||||
|
||||
all(): Command[] {
|
||||
return Array.from(this.commands.values());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user