@getrouter/getrouter-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.github/workflows/ci.yml +19 -0
  2. package/AGENTS.md +78 -0
  3. package/README.ja.md +116 -0
  4. package/README.md +116 -0
  5. package/README.zh-cn.md +116 -0
  6. package/biome.json +10 -0
  7. package/bun.lock +397 -0
  8. package/dist/bin.mjs +1422 -0
  9. package/docs/plans/2026-01-01-getrouter-cli-config-command-plan.md +231 -0
  10. package/docs/plans/2026-01-01-getrouter-cli-config-core-plan.md +307 -0
  11. package/docs/plans/2026-01-01-getrouter-cli-design.md +106 -0
  12. package/docs/plans/2026-01-01-getrouter-cli-scaffold-plan.md +327 -0
  13. package/docs/plans/2026-01-02-getrouter-cli-auth-design.md +68 -0
  14. package/docs/plans/2026-01-02-getrouter-cli-auth-device-design.md +73 -0
  15. package/docs/plans/2026-01-02-getrouter-cli-auth-device-plan.md +411 -0
  16. package/docs/plans/2026-01-02-getrouter-cli-auth-plan.md +435 -0
  17. package/docs/plans/2026-01-02-getrouter-cli-http-client-plan.md +235 -0
  18. package/docs/plans/2026-01-02-getrouter-cli-keys-create-update-output-design.md +24 -0
  19. package/docs/plans/2026-01-02-getrouter-cli-keys-create-update-output-plan.md +141 -0
  20. package/docs/plans/2026-01-02-getrouter-cli-keys-delete-output-design.md +22 -0
  21. package/docs/plans/2026-01-02-getrouter-cli-keys-delete-output-plan.md +122 -0
  22. package/docs/plans/2026-01-02-getrouter-cli-keys-get-output-design.md +23 -0
  23. package/docs/plans/2026-01-02-getrouter-cli-keys-get-output-plan.md +141 -0
  24. package/docs/plans/2026-01-02-getrouter-cli-keys-interactive-design.md +28 -0
  25. package/docs/plans/2026-01-02-getrouter-cli-keys-interactive-plan.md +247 -0
  26. package/docs/plans/2026-01-02-getrouter-cli-keys-output-design.md +31 -0
  27. package/docs/plans/2026-01-02-getrouter-cli-keys-output-plan.md +187 -0
  28. package/docs/plans/2026-01-02-getrouter-cli-keys-subscription-design.md +52 -0
  29. package/docs/plans/2026-01-02-getrouter-cli-keys-subscription-plan.md +306 -0
  30. package/docs/plans/2026-01-02-getrouter-cli-setup-env-design.md +67 -0
  31. package/docs/plans/2026-01-02-getrouter-cli-setup-env-plan.md +441 -0
  32. package/docs/plans/2026-01-02-getrouter-cli-subscription-output-design.md +34 -0
  33. package/docs/plans/2026-01-02-getrouter-cli-subscription-output-plan.md +157 -0
  34. package/docs/plans/2026-01-03-bun-migration-plan.md +103 -0
  35. package/docs/plans/2026-01-03-cli-emoji-output.md +45 -0
  36. package/docs/plans/2026-01-03-cli-english-output.md +123 -0
  37. package/docs/plans/2026-01-03-cli-simplify-design.md +62 -0
  38. package/docs/plans/2026-01-03-cli-simplify-implementation.md +468 -0
  39. package/docs/plans/2026-01-03-readme-command-descriptions.md +116 -0
  40. package/docs/plans/2026-01-03-tsdown-migration-plan.md +75 -0
  41. package/docs/plans/2026-01-04-cli-docs-cleanup-design.md +49 -0
  42. package/docs/plans/2026-01-04-cli-docs-cleanup-plan.md +126 -0
  43. package/docs/plans/2026-01-04-codex-multistep-design.md +76 -0
  44. package/docs/plans/2026-01-04-codex-multistep-plan.md +240 -0
  45. package/docs/plans/2026-01-04-env-hook-design.md +48 -0
  46. package/docs/plans/2026-01-04-env-hook-plan.md +173 -0
  47. package/docs/plans/2026-01-04-models-keys-fuzzy-design.md +75 -0
  48. package/docs/plans/2026-01-04-models-keys-fuzzy-implementation.md +704 -0
  49. package/package.json +37 -0
  50. package/src/.gitkeep +0 -0
  51. package/src/bin.ts +4 -0
  52. package/src/cli.ts +12 -0
  53. package/src/cmd/auth.ts +44 -0
  54. package/src/cmd/claude.ts +10 -0
  55. package/src/cmd/codex.ts +119 -0
  56. package/src/cmd/config-helpers.ts +16 -0
  57. package/src/cmd/config.ts +31 -0
  58. package/src/cmd/env.ts +103 -0
  59. package/src/cmd/index.ts +20 -0
  60. package/src/cmd/keys.ts +207 -0
  61. package/src/cmd/models.ts +48 -0
  62. package/src/cmd/status.ts +106 -0
  63. package/src/cmd/usages.ts +29 -0
  64. package/src/core/api/client.ts +79 -0
  65. package/src/core/auth/device.ts +105 -0
  66. package/src/core/auth/index.ts +37 -0
  67. package/src/core/config/fs.ts +13 -0
  68. package/src/core/config/index.ts +37 -0
  69. package/src/core/config/paths.ts +5 -0
  70. package/src/core/config/redact.ts +18 -0
  71. package/src/core/config/types.ts +23 -0
  72. package/src/core/http/errors.ts +32 -0
  73. package/src/core/http/request.ts +41 -0
  74. package/src/core/http/url.ts +12 -0
  75. package/src/core/interactive/clipboard.ts +61 -0
  76. package/src/core/interactive/codex.ts +75 -0
  77. package/src/core/interactive/fuzzy.ts +64 -0
  78. package/src/core/interactive/keys.ts +164 -0
  79. package/src/core/output/table.ts +34 -0
  80. package/src/core/output/usages.ts +75 -0
  81. package/src/core/paths.ts +4 -0
  82. package/src/core/setup/codex.ts +129 -0
  83. package/src/core/setup/env.ts +220 -0
  84. package/src/core/usages/aggregate.ts +69 -0
  85. package/src/generated/router/dashboard/v1/index.ts +1104 -0
  86. package/src/index.ts +1 -0
  87. package/tests/.gitkeep +0 -0
  88. package/tests/auth/device.test.ts +75 -0
  89. package/tests/auth/status.test.ts +64 -0
  90. package/tests/cli.test.ts +31 -0
  91. package/tests/cmd/auth.test.ts +90 -0
  92. package/tests/cmd/claude.test.ts +132 -0
  93. package/tests/cmd/codex.test.ts +147 -0
  94. package/tests/cmd/config-helpers.test.ts +18 -0
  95. package/tests/cmd/config.test.ts +56 -0
  96. package/tests/cmd/keys.test.ts +163 -0
  97. package/tests/cmd/models.test.ts +63 -0
  98. package/tests/cmd/status.test.ts +82 -0
  99. package/tests/cmd/usages.test.ts +42 -0
  100. package/tests/config/fs.test.ts +14 -0
  101. package/tests/config/index.test.ts +63 -0
  102. package/tests/config/paths.test.ts +10 -0
  103. package/tests/config/redact.test.ts +17 -0
  104. package/tests/config/types.test.ts +10 -0
  105. package/tests/core/api/client.test.ts +92 -0
  106. package/tests/core/interactive/clipboard.test.ts +44 -0
  107. package/tests/core/interactive/codex.test.ts +17 -0
  108. package/tests/core/interactive/fuzzy.test.ts +30 -0
  109. package/tests/core/setup/codex.test.ts +38 -0
  110. package/tests/core/setup/env.test.ts +84 -0
  111. package/tests/core/usages/aggregate.test.ts +55 -0
  112. package/tests/http/errors.test.ts +15 -0
  113. package/tests/http/request.test.ts +82 -0
  114. package/tests/http/url.test.ts +17 -0
  115. package/tests/output/table.test.ts +29 -0
  116. package/tests/output/usages.test.ts +71 -0
  117. package/tests/paths.test.ts +9 -0
  118. package/tsconfig.json +13 -0
  119. package/tsdown.config.ts +5 -0
  120. package/vitest.config.ts +7 -0
@@ -0,0 +1,247 @@
1
+ # Keys Interactive Selection Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Add interactive key selection for `keys get/update/delete` when `id` is omitted, with non-TTY fallback errors and delete confirmation.
6
+
7
+ **Architecture:** Introduce a small interactive helper using `prompts` to select a key from `ListConsumers` (sorted by `createdAt` desc) and confirm deletes. Update `cmd/keys` to make `id` optional, gate on `process.stdin.isTTY`, and route through selection/confirmation while keeping `--json` output unchanged.
8
+
9
+ **Tech Stack:** TypeScript, Commander, Vitest, prompts.
10
+
11
+ **Skills:** @superpowers:test-driven-development, @superpowers:systematic-debugging (if failures)
12
+
13
+ ### Task 1: Add prompts dependency and failing tests for interactive selection
14
+
15
+ **Files:**
16
+ - Modify: `package.json`
17
+ - Modify: `package-lock.json`
18
+ - Modify: `tests/cmd/keys.test.ts`
19
+
20
+ **Step 1: Add prompts dependency**
21
+
22
+ Run: `npm install prompts`
23
+
24
+ **Step 2: Write the failing tests**
25
+
26
+ ```ts
27
+ import prompts from "prompts";
28
+ import { describe, it, expect, vi, afterEach } from "vitest";
29
+
30
+ const originalIsTTY = process.stdin.isTTY;
31
+ const setStdinTTY = (value: boolean) => {
32
+ Object.defineProperty(process.stdin, "isTTY", {
33
+ value,
34
+ configurable: true,
35
+ });
36
+ };
37
+
38
+ afterEach(() => {
39
+ setStdinTTY(originalIsTTY);
40
+ prompts.inject([]);
41
+ });
42
+
43
+ it("get selects newest key when id is missing", async () => {
44
+ setStdinTTY(true);
45
+ prompts.inject([0]);
46
+ const listConsumers = vi.fn().mockResolvedValue({
47
+ consumers: [
48
+ { id: "old", name: "old", enabled: true, createdAt: "2026-01-01T00:00:00Z" },
49
+ { id: "new", name: "new", enabled: true, createdAt: "2026-01-02T00:00:00Z" },
50
+ ],
51
+ });
52
+ const getConsumer = vi
53
+ .fn()
54
+ .mockResolvedValue({ ...mockConsumer, id: "new" });
55
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
56
+ consumerService: { ListConsumers: listConsumers, GetConsumer: getConsumer },
57
+ subscriptionService: {} as any,
58
+ });
59
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
60
+ const program = createProgram();
61
+ await program.parseAsync(["node", "getrouter", "keys", "get"]);
62
+ expect(getConsumer).toHaveBeenCalledWith({ id: "new" });
63
+ log.mockRestore();
64
+ });
65
+
66
+ it("delete does not run when confirmation is declined", async () => {
67
+ setStdinTTY(true);
68
+ prompts.inject([0, false]);
69
+ const deleteConsumer = vi.fn().mockResolvedValue({});
70
+ const listConsumers = vi.fn().mockResolvedValue({
71
+ consumers: [
72
+ { id: "c1", name: "dev", enabled: true, createdAt: "2026-01-01T00:00:00Z" },
73
+ ],
74
+ });
75
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
76
+ consumerService: { ListConsumers: listConsumers, DeleteConsumer: deleteConsumer },
77
+ subscriptionService: {} as any,
78
+ });
79
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
80
+ const program = createProgram();
81
+ await program.parseAsync(["node", "getrouter", "keys", "delete"]);
82
+ expect(deleteConsumer).not.toHaveBeenCalled();
83
+ log.mockRestore();
84
+ });
85
+
86
+ it("get without id fails in non-tty mode", async () => {
87
+ setStdinTTY(false);
88
+ const program = createProgram();
89
+ await expect(
90
+ program.parseAsync(["node", "getrouter", "keys", "get"])
91
+ ).rejects.toThrow("缺少 id");
92
+ });
93
+ ```
94
+
95
+ **Step 3: Run test to verify it fails**
96
+
97
+ Run: `npm test -- tests/cmd/keys.test.ts`
98
+
99
+ Expected: FAIL because `keys get/delete` still require explicit `id` and no interactive selection exists.
100
+
101
+ **Step 4: Commit the failing tests**
102
+
103
+ ```bash
104
+ git add package.json package-lock.json tests/cmd/keys.test.ts
105
+ git commit -m "test: cover keys interactive selection"
106
+ ```
107
+
108
+ ### Task 2: Implement interactive selection helper and wire commands
109
+
110
+ **Files:**
111
+ - Create: `src/core/interactive/keys.ts`
112
+ - Modify: `src/cmd/keys.ts`
113
+ - Test: `tests/cmd/keys.test.ts`
114
+
115
+ **Step 1: Create interactive helper**
116
+
117
+ ```ts
118
+ import prompts from "prompts";
119
+
120
+ type Consumer = {
121
+ id?: string;
122
+ name?: string;
123
+ enabled?: boolean;
124
+ createdAt?: string;
125
+ };
126
+
127
+ type ConsumerService = {
128
+ ListConsumers: (args: { pageSize?: number; pageToken?: string }) => Promise<{ consumers?: Consumer[] }>;
129
+ };
130
+
131
+ const sortByCreatedAtDesc = (consumers: Consumer[]) =>
132
+ consumers
133
+ .slice()
134
+ .sort((a, b) => {
135
+ const aTime = Date.parse(a.createdAt ?? "") || 0;
136
+ const bTime = Date.parse(b.createdAt ?? "") || 0;
137
+ return bTime - aTime;
138
+ });
139
+
140
+ const formatChoice = (consumer: Consumer) => {
141
+ const name = consumer.name ?? "-";
142
+ const id = consumer.id ?? "-";
143
+ const enabled = consumer.enabled == null ? "-" : consumer.enabled ? "ENABLED" : "DISABLED";
144
+ const createdAt = consumer.createdAt ?? "-";
145
+ return `${name} (${id}) | ${enabled} | ${createdAt}`;
146
+ };
147
+
148
+ export const selectConsumer = async (consumerService: ConsumerService) => {
149
+ const res = await consumerService.ListConsumers({ pageSize: undefined, pageToken: undefined });
150
+ const consumers = res?.consumers ?? [];
151
+ if (consumers.length === 0) {
152
+ throw new Error("没有可用的 API key");
153
+ }
154
+ const sorted = sortByCreatedAtDesc(consumers);
155
+ const response = await prompts({
156
+ type: "select",
157
+ name: "id",
158
+ message: "选择 API key",
159
+ choices: sorted.map((consumer) => ({
160
+ title: formatChoice(consumer),
161
+ value: consumer.id,
162
+ })),
163
+ });
164
+ if (!response.id) return null;
165
+ return sorted.find((consumer) => consumer.id === response.id) ?? { id: response.id };
166
+ };
167
+
168
+ export const confirmDelete = async (consumer: Consumer) => {
169
+ const name = consumer.name ?? "-";
170
+ const id = consumer.id ?? "-";
171
+ const response = await prompts({
172
+ type: "confirm",
173
+ name: "confirm",
174
+ message: `确认删除 ${name} (${id})?`,
175
+ initial: false,
176
+ });
177
+ return Boolean(response.confirm);
178
+ };
179
+ ```
180
+
181
+ **Step 2: Wire interactive selection into commands**
182
+
183
+ ```ts
184
+ // imports
185
+ import { selectConsumer, confirmDelete } from "../core/interactive/keys";
186
+
187
+ // make args optional
188
+ .argument("[id]")
189
+
190
+ // in get/update/delete
191
+ if (!id) {
192
+ if (!process.stdin.isTTY) {
193
+ throw new Error("缺少 id");
194
+ }
195
+ const selected = await selectConsumer(consumerService);
196
+ if (!selected?.id) return;
197
+ id = selected.id;
198
+ }
199
+ ```
200
+
201
+ For delete confirmation:
202
+
203
+ ```ts
204
+ let selected: any = null;
205
+ if (!id) {
206
+ if (!process.stdin.isTTY) {
207
+ throw new Error("缺少 id");
208
+ }
209
+ selected = await selectConsumer(consumerService);
210
+ if (!selected?.id) return;
211
+ id = selected.id;
212
+ const confirmed = await confirmDelete(selected);
213
+ if (!confirmed) return;
214
+ }
215
+ ```
216
+
217
+ **Step 3: Run tests to verify pass**
218
+
219
+ Run: `npm test -- tests/cmd/keys.test.ts`
220
+
221
+ Expected: PASS
222
+
223
+ **Step 4: Commit**
224
+
225
+ ```bash
226
+ git add src/cmd/keys.ts src/core/interactive/keys.ts tests/cmd/keys.test.ts
227
+ git commit -m "feat: add interactive selection for keys"
228
+ ```
229
+
230
+ ### Task 3: Full test run
231
+
232
+ **Files:**
233
+ - None
234
+
235
+ **Step 1: Run full test suite**
236
+
237
+ Run: `npm test`
238
+
239
+ Expected: PASS
240
+
241
+ **Step 2: Commit (if needed)**
242
+
243
+ ```bash
244
+ git status -sb
245
+ ```
246
+
247
+ If clean, no commit needed.
@@ -0,0 +1,31 @@
1
+ # getrouter CLI Keys 列表输出设计(表格)
2
+
3
+ 日期:2026-01-02
4
+ 状态:已确认
5
+
6
+ ## 目标
7
+ - `keys list` 默认输出改为表格,提升可读性。
8
+ - 保留 `--json` 作为脚本接口;`apiKey` 默认脱敏。
9
+
10
+ ## 表格列与顺序
11
+ - 列顺序固定:`ID | NAME | ENABLED | LAST_ACCESS | CREATED_AT | API_KEY`
12
+ - 空值显示 `-`,避免列塌陷。
13
+ - `apiKey` 默认脱敏(前 4 后 4),`--show-secret` 明文。
14
+
15
+ ## 表格渲染组件
16
+ 新增轻量表格工具(建议 `src/core/output/table.ts`):
17
+ - 输入:`headers: string[]`、`rows: Array<Array<string>>`
18
+ - 列宽 = `max(表头宽度, 各行最大宽度)`,再受 `MAX_COL_WIDTH` 约束(如 32)。
19
+ - 超宽截断:保留前 `MAX-3` 字符并追加 `...`。
20
+ - 左对齐,列间空两格。
21
+ - 不做脱敏,仅渲染字符串。
22
+
23
+ ## 命令层行为
24
+ - `keys list` 在非 `--json` 情况使用表格输出。
25
+ - `--json` 输出完整结构(已脱敏)。
26
+ - 其他 keys 子命令暂保持 key=value 输出(可后续统一为表格)。
27
+
28
+ ## 测试策略
29
+ - 表格渲染单测:宽度计算、截断、空值、对齐。
30
+ - `keys list` 输出单测:确保表头与列顺序;校验脱敏结果。
31
+
@@ -0,0 +1,187 @@
1
+ # Keys List Table Output Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Implement a reusable table renderer and switch `keys list` default output to a readable table format with truncation and redaction.
6
+
7
+ **Architecture:** Add `core/output/table` for width calculation + truncation, then update `cmd/keys` to render list rows via table when not `--json`.
8
+
9
+ **Tech Stack:** TypeScript, Node.js, vitest.
10
+
11
+ ### Task 1: Add table renderer utility
12
+
13
+ **Files:**
14
+ - Create: `src/core/output/table.ts`
15
+ - Create: `tests/output/table.test.ts`
16
+
17
+ **Step 1: Write the failing test**
18
+
19
+ Create `tests/output/table.test.ts`:
20
+
21
+ ```ts
22
+ import { describe, it, expect } from "vitest";
23
+ import { renderTable } from "../../src/core/output/table";
24
+
25
+ describe("table renderer", () => {
26
+ it("renders headers and rows with alignment", () => {
27
+ const out = renderTable(
28
+ ["ID", "NAME"],
29
+ [
30
+ ["1", "alpha"],
31
+ ["2", "beta"],
32
+ ]
33
+ );
34
+ expect(out).toContain("ID");
35
+ expect(out).toContain("NAME");
36
+ expect(out).toContain("alpha");
37
+ });
38
+
39
+ it("truncates long cells", () => {
40
+ const out = renderTable(["ID"], [["0123456789ABCDEFGHIJ"]], {
41
+ maxColWidth: 8,
42
+ });
43
+ expect(out).toContain("0123...");
44
+ });
45
+
46
+ it("fills empty with dash", () => {
47
+ const out = renderTable(["ID"], [[""]]);
48
+ expect(out).toContain("-");
49
+ });
50
+ });
51
+ ```
52
+
53
+ **Step 2: Run test to verify it fails**
54
+
55
+ Run: `npm test -- tests/output/table.test.ts`
56
+ Expected: FAIL (module missing)
57
+
58
+ **Step 3: Write minimal implementation**
59
+
60
+ Create `src/core/output/table.ts`:
61
+
62
+ ```ts
63
+ type TableOptions = { maxColWidth?: number };
64
+
65
+ const truncate = (value: string, max: number) => {
66
+ if (value.length <= max) return value;
67
+ if (max <= 3) return value.slice(0, max);
68
+ return `${value.slice(0, max - 3)}...`;
69
+ };
70
+
71
+ export const renderTable = (
72
+ headers: string[],
73
+ rows: string[][],
74
+ options: TableOptions = {}
75
+ ) => {
76
+ const maxColWidth = options.maxColWidth ?? 32;
77
+ const normalized = rows.map((row) =>
78
+ row.map((cell) => (cell && cell.length > 0 ? cell : "-"))
79
+ );
80
+ const widths = headers.map((h, i) => {
81
+ const colValues = normalized.map((row) => row[i] ?? "-");
82
+ const maxLen = Math.max(h.length, ...colValues.map((v) => v.length));
83
+ return Math.min(maxLen, maxColWidth);
84
+ });
85
+ const renderRow = (cells: string[]) =>
86
+ cells
87
+ .map((cell, i) => {
88
+ const raw = cell ?? "-";
89
+ const clipped = truncate(raw, widths[i]);
90
+ return clipped.padEnd(widths[i], " ");
91
+ })
92
+ .join(" ");
93
+ const headerRow = renderRow(headers);
94
+ const body = normalized.map((row) => renderRow(row)).join("\n");
95
+ return `${headerRow}\n${body}`;
96
+ };
97
+ ```
98
+
99
+ **Step 4: Run test to verify it passes**
100
+
101
+ Run: `npm test -- tests/output/table.test.ts`
102
+ Expected: PASS
103
+
104
+ **Step 5: Commit**
105
+
106
+ ```bash
107
+ git add src/core/output/table.ts tests/output/table.test.ts
108
+ git commit -m "feat: add table renderer"
109
+ ```
110
+
111
+ ### Task 2: Switch keys list to table output
112
+
113
+ **Files:**
114
+ - Modify: `src/cmd/keys.ts`
115
+ - Modify: `tests/cmd/keys.test.ts`
116
+
117
+ **Step 1: Write the failing test**
118
+
119
+ Update `tests/cmd/keys.test.ts` with a human-readable output assertion:
120
+
121
+ ```ts
122
+ it("list prints table header in default mode", async () => {
123
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
124
+ consumerService: {
125
+ ListConsumers: vi.fn().mockResolvedValue({ consumers: [mockConsumer] }),
126
+ },
127
+ subscriptionService: {} as any,
128
+ });
129
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
130
+ const program = createProgram();
131
+ await program.parseAsync(["node", "getrouter", "keys", "list"]);
132
+ const output = log.mock.calls.map((c) => c[0]).join("\n");
133
+ expect(output).toContain("ID");
134
+ expect(output).toContain("NAME");
135
+ expect(output).toContain("API_KEY");
136
+ log.mockRestore();
137
+ });
138
+ ```
139
+
140
+ **Step 2: Run test to verify it fails**
141
+
142
+ Run: `npm test -- tests/cmd/keys.test.ts`
143
+ Expected: FAIL (still key=value)
144
+
145
+ **Step 3: Write minimal implementation**
146
+
147
+ Update `src/cmd/keys.ts`:
148
+ - import `renderTable`
149
+ - in `keys list`, when not `--json`, build headers/rows and `console.log(renderTable(...))`
150
+ - map row cells to strings, use `-` for empty
151
+
152
+ Example mapping:
153
+ ```ts
154
+ const headers = ["ID", "NAME", "ENABLED", "LAST_ACCESS", "CREATED_AT", "API_KEY"];
155
+ const rows = consumers.map((c) => [
156
+ String(c.id ?? ""),
157
+ String(c.name ?? ""),
158
+ String(c.enabled ?? ""),
159
+ String(c.lastAccess ?? ""),
160
+ String(c.createdAt ?? ""),
161
+ String(c.apiKey ?? ""),
162
+ ]);
163
+ console.log(renderTable(headers, rows));
164
+ ```
165
+
166
+ **Step 4: Run test to verify it passes**
167
+
168
+ Run: `npm test -- tests/cmd/keys.test.ts`
169
+ Expected: PASS
170
+
171
+ **Step 5: Commit**
172
+
173
+ ```bash
174
+ git add src/cmd/keys.ts tests/cmd/keys.test.ts
175
+ git commit -m "feat: use table output for keys list"
176
+ ```
177
+
178
+ ---
179
+
180
+ Plan complete and saved to `docs/plans/2026-01-02-getrouter-cli-keys-output-plan.md`.
181
+
182
+ Two execution options:
183
+
184
+ 1. Subagent-Driven (this session) — use superpowers:subagent-driven-development
185
+ 2. Parallel Session (separate) — open a new session with executing-plans in this worktree
186
+
187
+ Which approach?
@@ -0,0 +1,52 @@
1
+ # getrouter CLI Keys & Subscription 设计(v1)
2
+
3
+ 日期:2026-01-02
4
+ 状态:已确认
5
+
6
+ ## 背景
7
+ - `keys` 命令映射 dashboard `consumer` 资源,用于管理 API key。
8
+ - `subscription show` 用于展示当前订阅信息。
9
+ - 复用 dashboard 生成的 TypeScript client,避免路径与类型漂移。
10
+
11
+ ## 方案选型
12
+ **采用**:复用 dashboard 生成的 TS client(ConsumerService/SubscriptionService),CLI 仅做参数解析与输出层。
13
+
14
+ ## 命令设计
15
+ ### Keys(consumer)
16
+ - `getrouter keys list`
17
+ - `getrouter keys get <id>`
18
+ - `getrouter keys create --name <name> [--enabled <true|false>]`
19
+ - `getrouter keys update <id> --name <name> --enabled <true|false>`
20
+ - `getrouter keys delete <id>`
21
+
22
+ 约定:
23
+ - `list` 暂不支持分页参数(使用服务端默认)。
24
+ - `update` 仅允许更新 `name` 与 `enabled`。未提供任何可更新字段时报错。
25
+ - `apiKey` 默认脱敏展示,仅在 `--show-secret` 下明文。
26
+ - `create` 成功后默认脱敏展示 `apiKey`,并提示“请妥善保存 API Key”。
27
+
28
+ ### Subscription
29
+ - `getrouter subscription show`
30
+
31
+ 默认输出字段:
32
+ - `plan.name`, `status`, `startAt`, `endAt`, `plan.requestPerMinute`, `plan.tokenPerMinute`
33
+ - `--json` 输出原始结构
34
+
35
+ ## 组件与数据流
36
+ - `core/api`:包装生成 client(ConsumerService/SubscriptionService)。
37
+ - `core/http`:统一鉴权与错误封装(Authorization: Bearer)。
38
+ - `cmd/keys`/`cmd/subscription`:参数解析、调用 `core/api`。
39
+ - 输出层:默认人类可读,`--json` 输出结构化 JSON。
40
+
41
+ ## 错误处理
42
+ - 401/403:提示 `getrouter auth login`。
43
+ - `subscription show` 若 404/空响应,提示“未订阅”。
44
+ - 其他错误:默认输出 message;JSON 模式输出 `{code,message,details,status}`。
45
+
46
+ ## 测试策略
47
+ - `core/api`/adapter:验证路径、方法、请求体与 `updateMask` 生成。
48
+ - `cmd/keys`:list/create/update/delete/get 输出与脱敏/`--show-secret` 行为。
49
+ - `cmd/subscription`:正常与 404 场景,JSON 与默认输出。
50
+
51
+ ## 备注
52
+ - `apiBase` 默认 `https://getrouter.dev`,生成 client path 已含 `v1/...`。