@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.
- package/.github/workflows/ci.yml +19 -0
- package/AGENTS.md +78 -0
- package/README.ja.md +116 -0
- package/README.md +116 -0
- package/README.zh-cn.md +116 -0
- package/biome.json +10 -0
- package/bun.lock +397 -0
- package/dist/bin.mjs +1422 -0
- package/docs/plans/2026-01-01-getrouter-cli-config-command-plan.md +231 -0
- package/docs/plans/2026-01-01-getrouter-cli-config-core-plan.md +307 -0
- package/docs/plans/2026-01-01-getrouter-cli-design.md +106 -0
- package/docs/plans/2026-01-01-getrouter-cli-scaffold-plan.md +327 -0
- package/docs/plans/2026-01-02-getrouter-cli-auth-design.md +68 -0
- package/docs/plans/2026-01-02-getrouter-cli-auth-device-design.md +73 -0
- package/docs/plans/2026-01-02-getrouter-cli-auth-device-plan.md +411 -0
- package/docs/plans/2026-01-02-getrouter-cli-auth-plan.md +435 -0
- package/docs/plans/2026-01-02-getrouter-cli-http-client-plan.md +235 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-create-update-output-design.md +24 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-create-update-output-plan.md +141 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-delete-output-design.md +22 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-delete-output-plan.md +122 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-get-output-design.md +23 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-get-output-plan.md +141 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-interactive-design.md +28 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-interactive-plan.md +247 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-output-design.md +31 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-output-plan.md +187 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-subscription-design.md +52 -0
- package/docs/plans/2026-01-02-getrouter-cli-keys-subscription-plan.md +306 -0
- package/docs/plans/2026-01-02-getrouter-cli-setup-env-design.md +67 -0
- package/docs/plans/2026-01-02-getrouter-cli-setup-env-plan.md +441 -0
- package/docs/plans/2026-01-02-getrouter-cli-subscription-output-design.md +34 -0
- package/docs/plans/2026-01-02-getrouter-cli-subscription-output-plan.md +157 -0
- package/docs/plans/2026-01-03-bun-migration-plan.md +103 -0
- package/docs/plans/2026-01-03-cli-emoji-output.md +45 -0
- package/docs/plans/2026-01-03-cli-english-output.md +123 -0
- package/docs/plans/2026-01-03-cli-simplify-design.md +62 -0
- package/docs/plans/2026-01-03-cli-simplify-implementation.md +468 -0
- package/docs/plans/2026-01-03-readme-command-descriptions.md +116 -0
- package/docs/plans/2026-01-03-tsdown-migration-plan.md +75 -0
- package/docs/plans/2026-01-04-cli-docs-cleanup-design.md +49 -0
- package/docs/plans/2026-01-04-cli-docs-cleanup-plan.md +126 -0
- package/docs/plans/2026-01-04-codex-multistep-design.md +76 -0
- package/docs/plans/2026-01-04-codex-multistep-plan.md +240 -0
- package/docs/plans/2026-01-04-env-hook-design.md +48 -0
- package/docs/plans/2026-01-04-env-hook-plan.md +173 -0
- package/docs/plans/2026-01-04-models-keys-fuzzy-design.md +75 -0
- package/docs/plans/2026-01-04-models-keys-fuzzy-implementation.md +704 -0
- package/package.json +37 -0
- package/src/.gitkeep +0 -0
- package/src/bin.ts +4 -0
- package/src/cli.ts +12 -0
- package/src/cmd/auth.ts +44 -0
- package/src/cmd/claude.ts +10 -0
- package/src/cmd/codex.ts +119 -0
- package/src/cmd/config-helpers.ts +16 -0
- package/src/cmd/config.ts +31 -0
- package/src/cmd/env.ts +103 -0
- package/src/cmd/index.ts +20 -0
- package/src/cmd/keys.ts +207 -0
- package/src/cmd/models.ts +48 -0
- package/src/cmd/status.ts +106 -0
- package/src/cmd/usages.ts +29 -0
- package/src/core/api/client.ts +79 -0
- package/src/core/auth/device.ts +105 -0
- package/src/core/auth/index.ts +37 -0
- package/src/core/config/fs.ts +13 -0
- package/src/core/config/index.ts +37 -0
- package/src/core/config/paths.ts +5 -0
- package/src/core/config/redact.ts +18 -0
- package/src/core/config/types.ts +23 -0
- package/src/core/http/errors.ts +32 -0
- package/src/core/http/request.ts +41 -0
- package/src/core/http/url.ts +12 -0
- package/src/core/interactive/clipboard.ts +61 -0
- package/src/core/interactive/codex.ts +75 -0
- package/src/core/interactive/fuzzy.ts +64 -0
- package/src/core/interactive/keys.ts +164 -0
- package/src/core/output/table.ts +34 -0
- package/src/core/output/usages.ts +75 -0
- package/src/core/paths.ts +4 -0
- package/src/core/setup/codex.ts +129 -0
- package/src/core/setup/env.ts +220 -0
- package/src/core/usages/aggregate.ts +69 -0
- package/src/generated/router/dashboard/v1/index.ts +1104 -0
- package/src/index.ts +1 -0
- package/tests/.gitkeep +0 -0
- package/tests/auth/device.test.ts +75 -0
- package/tests/auth/status.test.ts +64 -0
- package/tests/cli.test.ts +31 -0
- package/tests/cmd/auth.test.ts +90 -0
- package/tests/cmd/claude.test.ts +132 -0
- package/tests/cmd/codex.test.ts +147 -0
- package/tests/cmd/config-helpers.test.ts +18 -0
- package/tests/cmd/config.test.ts +56 -0
- package/tests/cmd/keys.test.ts +163 -0
- package/tests/cmd/models.test.ts +63 -0
- package/tests/cmd/status.test.ts +82 -0
- package/tests/cmd/usages.test.ts +42 -0
- package/tests/config/fs.test.ts +14 -0
- package/tests/config/index.test.ts +63 -0
- package/tests/config/paths.test.ts +10 -0
- package/tests/config/redact.test.ts +17 -0
- package/tests/config/types.test.ts +10 -0
- package/tests/core/api/client.test.ts +92 -0
- package/tests/core/interactive/clipboard.test.ts +44 -0
- package/tests/core/interactive/codex.test.ts +17 -0
- package/tests/core/interactive/fuzzy.test.ts +30 -0
- package/tests/core/setup/codex.test.ts +38 -0
- package/tests/core/setup/env.test.ts +84 -0
- package/tests/core/usages/aggregate.test.ts +55 -0
- package/tests/http/errors.test.ts +15 -0
- package/tests/http/request.test.ts +82 -0
- package/tests/http/url.test.ts +17 -0
- package/tests/output/table.test.ts +29 -0
- package/tests/output/usages.test.ts +71 -0
- package/tests/paths.test.ts +9 -0
- package/tsconfig.json +13 -0
- package/tsdown.config.ts +5 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
# Auth Placeholder Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Implement placeholder `auth login/logout/status` behavior with local-only auth state handling, including token masking and secure auth file permissions.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add a small `core/auth` module for status/clear logic, extend auth storage schema with `tokenType`, enforce `0600` for `auth.json` on *nix, and update CLI `auth` commands to use these helpers. Output supports `--json` and redacts secrets by default.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Node.js `fs/os/path`, commander, vitest.
|
|
10
|
+
|
|
11
|
+
### Task 1: Extend auth schema and enforce auth.json permissions
|
|
12
|
+
|
|
13
|
+
**Files:**
|
|
14
|
+
- Modify: `src/core/config/types.ts`
|
|
15
|
+
- Modify: `src/core/config/index.ts`
|
|
16
|
+
- Modify: `tests/config/index.test.ts`
|
|
17
|
+
|
|
18
|
+
**Step 1: Write the failing tests**
|
|
19
|
+
|
|
20
|
+
Add to `tests/config/index.test.ts`:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
it("defaults tokenType to Bearer", () => {
|
|
24
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
25
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
26
|
+
writeAuth({ accessToken: "a", refreshToken: "b", expiresAt: "c", tokenType: "Bearer" });
|
|
27
|
+
const auth = readAuth();
|
|
28
|
+
expect(auth.tokenType).toBe("Bearer");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("writes auth file with 0600 on unix", () => {
|
|
32
|
+
if (process.platform === "win32") return;
|
|
33
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
34
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
35
|
+
writeAuth({ accessToken: "a", refreshToken: "b", expiresAt: "c", tokenType: "Bearer" });
|
|
36
|
+
const mode = fs.statSync(path.join(dir, "auth.json")).mode & 0o777;
|
|
37
|
+
expect(mode).toBe(0o600);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Step 2: Run test to verify it fails**
|
|
42
|
+
|
|
43
|
+
Run: `npm test -- tests/config/index.test.ts`
|
|
44
|
+
Expected: FAIL (missing tokenType default and chmod)
|
|
45
|
+
|
|
46
|
+
**Step 3: Write minimal implementation**
|
|
47
|
+
|
|
48
|
+
Update `src/core/config/types.ts`:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
export type AuthState = {
|
|
52
|
+
accessToken: string;
|
|
53
|
+
refreshToken: string;
|
|
54
|
+
expiresAt: string;
|
|
55
|
+
tokenType: string;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const defaultAuthState = (): AuthState => ({
|
|
59
|
+
accessToken: "",
|
|
60
|
+
refreshToken: "",
|
|
61
|
+
expiresAt: "",
|
|
62
|
+
tokenType: "Bearer",
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Update `src/core/config/index.ts` (after `writeJsonFile`):
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import fs from "node:fs";
|
|
70
|
+
|
|
71
|
+
export const writeAuth = (auth: AuthState) => {
|
|
72
|
+
writeJsonFile(getAuthPath(), auth);
|
|
73
|
+
if (process.platform !== "win32") {
|
|
74
|
+
fs.chmodSync(getAuthPath(), 0o600);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Step 4: Run test to verify it passes**
|
|
80
|
+
|
|
81
|
+
Run: `npm test -- tests/config/index.test.ts`
|
|
82
|
+
Expected: PASS
|
|
83
|
+
|
|
84
|
+
**Step 5: Commit**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git add src/core/config/types.ts src/core/config/index.ts tests/config/index.test.ts
|
|
88
|
+
git commit -m "feat: extend auth schema and secure auth file"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Task 2: Mask secrets by default
|
|
92
|
+
|
|
93
|
+
**Files:**
|
|
94
|
+
- Modify: `src/core/config/redact.ts`
|
|
95
|
+
- Modify: `tests/config/redact.test.ts`
|
|
96
|
+
|
|
97
|
+
**Step 1: Write the failing test**
|
|
98
|
+
|
|
99
|
+
Update `tests/config/redact.test.ts`:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
expect(out.accessToken).toBe("secr...cret");
|
|
103
|
+
expect(out.refreshToken).toBe("secr...ret2");
|
|
104
|
+
expect(out.apiKey).toBe("key...");
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Step 2: Run test to verify it fails**
|
|
108
|
+
|
|
109
|
+
Run: `npm test -- tests/config/redact.test.ts`
|
|
110
|
+
Expected: FAIL (currently uses "****")
|
|
111
|
+
|
|
112
|
+
**Step 3: Write minimal implementation**
|
|
113
|
+
|
|
114
|
+
Update `src/core/config/redact.ts`:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const mask = (value: string) => {
|
|
118
|
+
if (!value) return "";
|
|
119
|
+
if (value.length <= 8) return "****";
|
|
120
|
+
return `${value.slice(0, 4)}...${value.slice(-4)}`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const redactSecrets = <T extends Record<string, any>>(obj: T): T => {
|
|
124
|
+
const out = { ...obj } as T;
|
|
125
|
+
for (const key of Object.keys(out)) {
|
|
126
|
+
if (SECRET_KEYS.has(key) && typeof out[key] === "string") {
|
|
127
|
+
out[key] = mask(out[key]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Step 4: Run test to verify it passes**
|
|
135
|
+
|
|
136
|
+
Run: `npm test -- tests/config/redact.test.ts`
|
|
137
|
+
Expected: PASS
|
|
138
|
+
|
|
139
|
+
**Step 5: Commit**
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git add src/core/config/redact.ts tests/config/redact.test.ts
|
|
143
|
+
git commit -m "feat: mask secrets in outputs"
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Task 3: Add core/auth status + clear helpers
|
|
147
|
+
|
|
148
|
+
**Files:**
|
|
149
|
+
- Create: `src/core/auth/index.ts`
|
|
150
|
+
- Create: `tests/auth/status.test.ts`
|
|
151
|
+
|
|
152
|
+
**Step 1: Write the failing tests**
|
|
153
|
+
|
|
154
|
+
Create `tests/auth/status.test.ts`:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { describe, it, expect, vi } from "vitest";
|
|
158
|
+
import fs from "node:fs";
|
|
159
|
+
import os from "node:os";
|
|
160
|
+
import path from "node:path";
|
|
161
|
+
import { writeAuth, readAuth } from "../../src/core/config";
|
|
162
|
+
import { getAuthStatus, clearAuth } from "../../src/core/auth";
|
|
163
|
+
|
|
164
|
+
const makeDir = () => fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
165
|
+
|
|
166
|
+
describe("auth status", () => {
|
|
167
|
+
it("returns logged_out when missing", () => {
|
|
168
|
+
const dir = makeDir();
|
|
169
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
170
|
+
const status = getAuthStatus();
|
|
171
|
+
expect(status.status).toBe("logged_out");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("returns logged_out when expired", () => {
|
|
175
|
+
vi.useFakeTimers();
|
|
176
|
+
vi.setSystemTime(new Date("2026-01-02T00:00:00Z"));
|
|
177
|
+
const dir = makeDir();
|
|
178
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
179
|
+
writeAuth({
|
|
180
|
+
accessToken: "a",
|
|
181
|
+
refreshToken: "b",
|
|
182
|
+
expiresAt: "2026-01-01T00:00:00Z",
|
|
183
|
+
tokenType: "Bearer",
|
|
184
|
+
});
|
|
185
|
+
const status = getAuthStatus();
|
|
186
|
+
expect(status.status).toBe("logged_out");
|
|
187
|
+
vi.useRealTimers();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("returns logged_in when valid", () => {
|
|
191
|
+
vi.useFakeTimers();
|
|
192
|
+
vi.setSystemTime(new Date("2026-01-02T00:00:00Z"));
|
|
193
|
+
const dir = makeDir();
|
|
194
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
195
|
+
writeAuth({
|
|
196
|
+
accessToken: "tokenvalue",
|
|
197
|
+
refreshToken: "refreshvalue",
|
|
198
|
+
expiresAt: "2026-01-03T00:00:00Z",
|
|
199
|
+
tokenType: "Bearer",
|
|
200
|
+
});
|
|
201
|
+
const status = getAuthStatus();
|
|
202
|
+
expect(status.status).toBe("logged_in");
|
|
203
|
+
expect(status.note).toContain("远端验证待开放");
|
|
204
|
+
vi.useRealTimers();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("clears auth state", () => {
|
|
208
|
+
const dir = makeDir();
|
|
209
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
210
|
+
writeAuth({ accessToken: "a", refreshToken: "b", expiresAt: "c", tokenType: "Bearer" });
|
|
211
|
+
clearAuth();
|
|
212
|
+
const auth = readAuth();
|
|
213
|
+
expect(auth.accessToken).toBe("");
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Step 2: Run test to verify it fails**
|
|
219
|
+
|
|
220
|
+
Run: `npm test -- tests/auth/status.test.ts`
|
|
221
|
+
Expected: FAIL (module missing)
|
|
222
|
+
|
|
223
|
+
**Step 3: Write minimal implementation**
|
|
224
|
+
|
|
225
|
+
Create `src/core/auth/index.ts`:
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { readAuth, writeAuth } from "../config";
|
|
229
|
+
import { defaultAuthState } from "../config/types";
|
|
230
|
+
|
|
231
|
+
type AuthStatus = {
|
|
232
|
+
status: "logged_in" | "logged_out";
|
|
233
|
+
note?: string;
|
|
234
|
+
expiresAt?: string;
|
|
235
|
+
accessToken?: string;
|
|
236
|
+
refreshToken?: string;
|
|
237
|
+
tokenType?: string;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const isExpired = (expiresAt: string) => {
|
|
241
|
+
if (!expiresAt) return true;
|
|
242
|
+
const t = Date.parse(expiresAt);
|
|
243
|
+
if (Number.isNaN(t)) return true;
|
|
244
|
+
return t <= Date.now();
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export const getAuthStatus = (): AuthStatus => {
|
|
248
|
+
const auth = readAuth();
|
|
249
|
+
const hasTokens = Boolean(auth.accessToken && auth.refreshToken);
|
|
250
|
+
if (!hasTokens || isExpired(auth.expiresAt)) {
|
|
251
|
+
return { status: "logged_out" };
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
status: "logged_in",
|
|
255
|
+
note: "仅本地验证,远端验证待开放",
|
|
256
|
+
expiresAt: auth.expiresAt,
|
|
257
|
+
accessToken: auth.accessToken,
|
|
258
|
+
refreshToken: auth.refreshToken,
|
|
259
|
+
tokenType: auth.tokenType,
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
export const clearAuth = () => {
|
|
264
|
+
writeAuth(defaultAuthState());
|
|
265
|
+
};
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Step 4: Run test to verify it passes**
|
|
269
|
+
|
|
270
|
+
Run: `npm test -- tests/auth/status.test.ts`
|
|
271
|
+
Expected: PASS
|
|
272
|
+
|
|
273
|
+
**Step 5: Commit**
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
git add src/core/auth/index.ts tests/auth/status.test.ts
|
|
277
|
+
git commit -m "feat: add core auth status helpers"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Task 4: Implement auth CLI commands
|
|
281
|
+
|
|
282
|
+
**Files:**
|
|
283
|
+
- Modify: `src/cmd/auth.ts`
|
|
284
|
+
- Create: `tests/cmd/auth.test.ts`
|
|
285
|
+
|
|
286
|
+
**Step 1: Write the failing tests**
|
|
287
|
+
|
|
288
|
+
Create `tests/cmd/auth.test.ts`:
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import { describe, it, expect, vi } from "vitest";
|
|
292
|
+
import fs from "node:fs";
|
|
293
|
+
import os from "node:os";
|
|
294
|
+
import path from "node:path";
|
|
295
|
+
import { createProgram } from "../../src/cli";
|
|
296
|
+
import { writeAuth } from "../../src/core/config";
|
|
297
|
+
|
|
298
|
+
const makeDir = () => fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
299
|
+
|
|
300
|
+
describe("auth commands", () => {
|
|
301
|
+
it("login prints placeholder and does not write auth", async () => {
|
|
302
|
+
const dir = makeDir();
|
|
303
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
304
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
305
|
+
const program = createProgram();
|
|
306
|
+
await program.parseAsync(["node", "getrouter", "auth", "login"]);
|
|
307
|
+
expect(log.mock.calls[0][0]).toContain("OAuth");
|
|
308
|
+
expect(fs.existsSync(path.join(dir, "auth.json"))).toBe(false);
|
|
309
|
+
log.mockRestore();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("status outputs json with redacted tokens", async () => {
|
|
313
|
+
const dir = makeDir();
|
|
314
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
315
|
+
writeAuth({
|
|
316
|
+
accessToken: "abcd1234WXYZ",
|
|
317
|
+
refreshToken: "refreshtokenVALUE",
|
|
318
|
+
expiresAt: "2026-01-03T00:00:00Z",
|
|
319
|
+
tokenType: "Bearer",
|
|
320
|
+
});
|
|
321
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
322
|
+
const program = createProgram();
|
|
323
|
+
await program.parseAsync(["node", "getrouter", "auth", "status", "--json"]);
|
|
324
|
+
const payload = JSON.parse(log.mock.calls[0][0]);
|
|
325
|
+
expect(payload.status).toBe("logged_in");
|
|
326
|
+
expect(payload.accessToken).toBe("abcd...WXYZ");
|
|
327
|
+
log.mockRestore();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("logout clears local auth", async () => {
|
|
331
|
+
const dir = makeDir();
|
|
332
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
333
|
+
writeAuth({ accessToken: "a", refreshToken: "b", expiresAt: "c", tokenType: "Bearer" });
|
|
334
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
335
|
+
const program = createProgram();
|
|
336
|
+
await program.parseAsync(["node", "getrouter", "auth", "logout"]);
|
|
337
|
+
expect(log.mock.calls[0][0]).toContain("已清除");
|
|
338
|
+
log.mockRestore();
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Step 2: Run test to verify it fails**
|
|
344
|
+
|
|
345
|
+
Run: `npm test -- tests/cmd/auth.test.ts`
|
|
346
|
+
Expected: FAIL (auth handlers missing)
|
|
347
|
+
|
|
348
|
+
**Step 3: Write minimal implementation**
|
|
349
|
+
|
|
350
|
+
Update `src/cmd/auth.ts`:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { Command } from "commander";
|
|
354
|
+
import { readConfig } from "../core/config";
|
|
355
|
+
import { getAuthStatus, clearAuth } from "../core/auth";
|
|
356
|
+
import { redactSecrets } from "../core/config/redact";
|
|
357
|
+
|
|
358
|
+
type AuthOptions = { json?: boolean; showSecret?: boolean };
|
|
359
|
+
|
|
360
|
+
const shouldJson = (options: AuthOptions) =>
|
|
361
|
+
typeof options.json === "boolean" ? options.json : readConfig().json;
|
|
362
|
+
|
|
363
|
+
const output = (payload: Record<string, unknown>, json: boolean) => {
|
|
364
|
+
if (json) {
|
|
365
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
369
|
+
if (value == null || value === "") continue;
|
|
370
|
+
console.log(`${key}=${value}`);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
export const registerAuthCommands = (program: Command) => {
|
|
375
|
+
const auth = program.command("auth").description("Authentication");
|
|
376
|
+
|
|
377
|
+
auth
|
|
378
|
+
.command("login")
|
|
379
|
+
.description("Login with GitHub OAuth")
|
|
380
|
+
.action(() => {
|
|
381
|
+
console.log("getrouter OAuth 仍在开发中,暂不支持登录。");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
auth
|
|
385
|
+
.command("logout")
|
|
386
|
+
.description("Clear local auth state")
|
|
387
|
+
.action(() => {
|
|
388
|
+
clearAuth();
|
|
389
|
+
console.log("已清除本地认证信息。");
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
auth
|
|
393
|
+
.command("status")
|
|
394
|
+
.description("Show current auth status")
|
|
395
|
+
.option("--json", "Output JSON")
|
|
396
|
+
.option("--show-secret", "Show full secrets")
|
|
397
|
+
.action((options: AuthOptions) => {
|
|
398
|
+
const status = getAuthStatus();
|
|
399
|
+
const payload: Record<string, unknown> = {
|
|
400
|
+
status: status.status,
|
|
401
|
+
note: status.note,
|
|
402
|
+
expiresAt: status.expiresAt,
|
|
403
|
+
accessToken: status.accessToken,
|
|
404
|
+
refreshToken: status.refreshToken,
|
|
405
|
+
tokenType: status.tokenType,
|
|
406
|
+
};
|
|
407
|
+
const json = shouldJson(options);
|
|
408
|
+
const outputPayload = options.showSecret ? payload : redactSecrets(payload);
|
|
409
|
+
output(outputPayload, json);
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Step 4: Run test to verify it passes**
|
|
415
|
+
|
|
416
|
+
Run: `npm test -- tests/cmd/auth.test.ts`
|
|
417
|
+
Expected: PASS
|
|
418
|
+
|
|
419
|
+
**Step 5: Commit**
|
|
420
|
+
|
|
421
|
+
```bash
|
|
422
|
+
git add src/cmd/auth.ts tests/cmd/auth.test.ts
|
|
423
|
+
git commit -m "feat: implement auth placeholder commands"
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
Plan complete and saved to `docs/plans/2026-01-02-getrouter-cli-auth-plan.md`.
|
|
429
|
+
|
|
430
|
+
Two execution options:
|
|
431
|
+
|
|
432
|
+
1. Subagent-Driven (this session) — use superpowers:subagent-driven-development
|
|
433
|
+
2. Parallel Session (separate) — open a new session in this worktree and use superpowers:executing-plans
|
|
434
|
+
|
|
435
|
+
Which approach?
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# HTTP Client Core 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 HTTP client wrapper that handles base URL building, auth headers, JSON request/response, and error normalization for getrouter CLI.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add a `core/http` module with URL building helpers and a `requestJson` function that reads config/auth state and uses `fetch` under the hood. Expose error normalization via a typed `ApiError`.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Node.js fetch, vitest.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: URL Helpers (TDD)
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Create: `src/core/http/url.ts`
|
|
17
|
+
- Test: `tests/http/url.test.ts`
|
|
18
|
+
|
|
19
|
+
**Step 1: Write the failing test**
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { describe, it, expect } from "vitest";
|
|
23
|
+
import { buildApiUrl } from "../../src/core/http/url";
|
|
24
|
+
import fs from "node:fs";
|
|
25
|
+
import os from "node:os";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
|
|
28
|
+
describe("buildApiUrl", () => {
|
|
29
|
+
it("joins base and path safely", () => {
|
|
30
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
31
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
path.join(dir, "config.json"),
|
|
34
|
+
JSON.stringify({ apiBase: "https://getrouter.dev/" })
|
|
35
|
+
);
|
|
36
|
+
expect(buildApiUrl("/v1/test")).toBe("https://getrouter.dev/v1/test");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Step 2: Run test to verify it fails**
|
|
42
|
+
|
|
43
|
+
Run: `npm test`
|
|
44
|
+
Expected: FAIL (module not found).
|
|
45
|
+
|
|
46
|
+
**Step 3: Implement minimal helpers**
|
|
47
|
+
|
|
48
|
+
`src/core/http/url.ts`
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { readConfig } from "../config";
|
|
52
|
+
|
|
53
|
+
export const getApiBase = () => {
|
|
54
|
+
const raw = readConfig().apiBase || "";
|
|
55
|
+
return raw.replace(/\\/+$/, "");
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const buildApiUrl = (path: string) => {
|
|
59
|
+
const base = getApiBase();
|
|
60
|
+
const normalized = path.replace(/^\\/+/, "");
|
|
61
|
+
return base ? `${base}/${normalized}` : `/${normalized}`;
|
|
62
|
+
};
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Step 4: Run test to verify it passes**
|
|
66
|
+
|
|
67
|
+
Run: `npm test`
|
|
68
|
+
Expected: PASS.
|
|
69
|
+
|
|
70
|
+
**Step 5: Commit**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git add src/core/http/url.ts tests/http/url.test.ts
|
|
74
|
+
git commit -m "feat: add http url helpers"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### Task 2: API Error Normalization (TDD)
|
|
80
|
+
|
|
81
|
+
**Files:**
|
|
82
|
+
- Create: `src/core/http/errors.ts`
|
|
83
|
+
- Test: `tests/http/errors.test.ts`
|
|
84
|
+
|
|
85
|
+
**Step 1: Write the failing test**
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { describe, it, expect } from "vitest";
|
|
89
|
+
import { createApiError } from "../../src/core/http/errors";
|
|
90
|
+
|
|
91
|
+
describe("api errors", () => {
|
|
92
|
+
it("normalizes error payload", () => {
|
|
93
|
+
const err = createApiError({ code: "BAD", message: "oops" }, "fallback", 400);
|
|
94
|
+
expect(err.message).toBe("oops");
|
|
95
|
+
expect(err.code).toBe("BAD");
|
|
96
|
+
expect(err.status).toBe(400);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Step 2: Run test to verify it fails**
|
|
102
|
+
|
|
103
|
+
Run: `npm test`
|
|
104
|
+
Expected: FAIL (module not found).
|
|
105
|
+
|
|
106
|
+
**Step 3: Implement minimal error helper**
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
export type ApiError = {
|
|
110
|
+
code?: string;
|
|
111
|
+
message: string;
|
|
112
|
+
details?: unknown;
|
|
113
|
+
status?: number;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const createApiError = (payload: any, fallbackMessage: string, status?: number) => {
|
|
117
|
+
const message =
|
|
118
|
+
payload && typeof payload.message === "string" ? payload.message : fallbackMessage;
|
|
119
|
+
const err = new Error(message) as Error & ApiError;
|
|
120
|
+
if (payload && typeof payload.code === "string") err.code = payload.code;
|
|
121
|
+
if (payload && payload.details != null) err.details = payload.details;
|
|
122
|
+
if (typeof status === "number") err.status = status;
|
|
123
|
+
return err;
|
|
124
|
+
};
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Step 4: Run test to verify it passes**
|
|
128
|
+
|
|
129
|
+
Run: `npm test`
|
|
130
|
+
Expected: PASS.
|
|
131
|
+
|
|
132
|
+
**Step 5: Commit**
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git add src/core/http/errors.ts tests/http/errors.test.ts
|
|
136
|
+
git commit -m "feat: add api error normalization"
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### Task 3: Request Wrapper (TDD)
|
|
142
|
+
|
|
143
|
+
**Files:**
|
|
144
|
+
- Create: `src/core/http/request.ts`
|
|
145
|
+
- Test: `tests/http/request.test.ts`
|
|
146
|
+
|
|
147
|
+
**Step 1: Write the failing test**
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { describe, it, expect, vi } from "vitest";
|
|
151
|
+
import { requestJson } from "../../src/core/http/request";
|
|
152
|
+
import fs from "node:fs";
|
|
153
|
+
import os from "node:os";
|
|
154
|
+
import path from "node:path";
|
|
155
|
+
|
|
156
|
+
describe("requestJson", () => {
|
|
157
|
+
it("adds Authorization when token exists", async () => {
|
|
158
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
159
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
160
|
+
fs.writeFileSync(path.join(dir, "auth.json"), JSON.stringify({ accessToken: "t" }));
|
|
161
|
+
|
|
162
|
+
const fetchSpy = vi.fn(async () => ({
|
|
163
|
+
ok: true,
|
|
164
|
+
json: async () => ({ ok: true }),
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
const res = await requestJson({
|
|
168
|
+
path: "/v1/test",
|
|
169
|
+
method: "GET",
|
|
170
|
+
fetchImpl: fetchSpy as any,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(res.ok).toBe(true);
|
|
174
|
+
const headers = fetchSpy.mock.calls[0][1].headers;
|
|
175
|
+
expect(headers.Authorization).toBe("Bearer t");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Step 2: Run test to verify it fails**
|
|
181
|
+
|
|
182
|
+
Run: `npm test`
|
|
183
|
+
Expected: FAIL (module not found).
|
|
184
|
+
|
|
185
|
+
**Step 3: Implement minimal request wrapper**
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { buildApiUrl } from "./url";
|
|
189
|
+
import { readAuth } from "../config";
|
|
190
|
+
import { createApiError } from "./errors";
|
|
191
|
+
|
|
192
|
+
type RequestInput = {
|
|
193
|
+
path: string;
|
|
194
|
+
method: string;
|
|
195
|
+
body?: unknown;
|
|
196
|
+
fetchImpl?: typeof fetch;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const requestJson = async <T = unknown>({
|
|
200
|
+
path,
|
|
201
|
+
method,
|
|
202
|
+
body,
|
|
203
|
+
fetchImpl,
|
|
204
|
+
}: RequestInput): Promise<T> => {
|
|
205
|
+
const headers: Record<string, string> = {
|
|
206
|
+
"Content-Type": "application/json",
|
|
207
|
+
};
|
|
208
|
+
const auth = readAuth();
|
|
209
|
+
if (auth.accessToken) {
|
|
210
|
+
headers.Authorization = `Bearer ${auth.accessToken}`;
|
|
211
|
+
}
|
|
212
|
+
const res = await (fetchImpl ?? fetch)(buildApiUrl(path), {
|
|
213
|
+
method,
|
|
214
|
+
headers,
|
|
215
|
+
body: body == null ? undefined : JSON.stringify(body),
|
|
216
|
+
});
|
|
217
|
+
if (!res.ok) {
|
|
218
|
+
const payload = await res.json().catch(() => null);
|
|
219
|
+
throw createApiError(payload, res.statusText, res.status);
|
|
220
|
+
}
|
|
221
|
+
return (await res.json()) as T;
|
|
222
|
+
};
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Step 4: Run test to verify it passes**
|
|
226
|
+
|
|
227
|
+
Run: `npm test`
|
|
228
|
+
Expected: PASS.
|
|
229
|
+
|
|
230
|
+
**Step 5: Commit**
|
|
231
|
+
|
|
232
|
+
```bash
|
|
233
|
+
git add src/core/http/request.ts tests/http/request.test.ts
|
|
234
|
+
git commit -m "feat: add http request wrapper"
|
|
235
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# getrouter CLI Keys create/update 表格输出设计
|
|
2
|
+
|
|
3
|
+
日期:2026-01-02
|
|
4
|
+
状态:已确认
|
|
5
|
+
|
|
6
|
+
## 目标
|
|
7
|
+
- `keys create` 与 `keys update` 默认输出改为单行表格,与 `keys list/get` 风格一致。
|
|
8
|
+
- `--json` 行为不变;`--show-secret` 仍控制 `apiKey` 明文/脱敏。
|
|
9
|
+
|
|
10
|
+
## 输出格式
|
|
11
|
+
- 列顺序:`ID | NAME | ENABLED | LAST_ACCESS | CREATED_AT | API_KEY`
|
|
12
|
+
- 单行表格:表头 + 1 行数据。
|
|
13
|
+
- 空值显示 `-`;超长字段按 `renderTable` 规则截断。
|
|
14
|
+
|
|
15
|
+
## 行为细节
|
|
16
|
+
- 默认人类可读:表格输出。
|
|
17
|
+
- `keys create` 表格输出后继续提示“请妥善保存 API Key。”。
|
|
18
|
+
- `keys update` 不输出提示。
|
|
19
|
+
- `--json`:输出完整对象(脱敏或明文由 `--show-secret` 决定)。
|
|
20
|
+
|
|
21
|
+
## 测试策略
|
|
22
|
+
- `keys create` 默认输出包含表头、列顺序与提示行。
|
|
23
|
+
- `keys update` 默认输出包含表头与列顺序。
|
|
24
|
+
- `--json` 输出结构不变。
|