@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,411 @@
|
|
|
1
|
+
# Device Auth Login Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Replace placeholder OAuth login with device-style login (auth_code + browser + polling authorize) and persist tokens in auth.json.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add a `core/auth/device` helper for auth_code generation, login URL, browser open, and polling with backoff/timeout. Wire `auth login` to use `authService.Authorize` and write tokens. Extend API client to include AuthService and refresh the generated dashboard client. Update auth status note to reflect real login.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, Commander, Vitest, Node `crypto`/`child_process`.
|
|
10
|
+
|
|
11
|
+
**Skills:** @superpowers:test-driven-development, @superpowers:systematic-debugging (if failures)
|
|
12
|
+
|
|
13
|
+
### Task 1: Add failing tests for device login
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Modify: `tests/cmd/auth.test.ts`
|
|
17
|
+
- Add: `tests/auth/device.test.ts`
|
|
18
|
+
- Modify: `tests/auth/status.test.ts`
|
|
19
|
+
|
|
20
|
+
**Step 1: Update auth command tests (failing)**
|
|
21
|
+
|
|
22
|
+
Replace the placeholder login test with device login expectations:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { describe, it, expect, vi } from "vitest";
|
|
26
|
+
import fs from "node:fs";
|
|
27
|
+
import os from "node:os";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { createProgram } from "../../src/cli";
|
|
30
|
+
import { createApiClients } from "../../src/core/api/client";
|
|
31
|
+
import {
|
|
32
|
+
generateAuthCode,
|
|
33
|
+
openLoginUrl,
|
|
34
|
+
pollAuthorize,
|
|
35
|
+
buildLoginUrl,
|
|
36
|
+
} from "../../src/core/auth/device";
|
|
37
|
+
|
|
38
|
+
vi.mock("../../src/core/api/client", () => ({
|
|
39
|
+
createApiClients: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock("../../src/core/auth/device", async () => {
|
|
43
|
+
const actual = await vi.importActual<typeof import("../../src/core/auth/device")>(
|
|
44
|
+
"../../src/core/auth/device"
|
|
45
|
+
);
|
|
46
|
+
return {
|
|
47
|
+
...actual,
|
|
48
|
+
generateAuthCode: vi.fn(() => "abcde234567fg"),
|
|
49
|
+
openLoginUrl: vi.fn(async () => {}),
|
|
50
|
+
pollAuthorize: vi.fn(),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const makeDir = () => fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
55
|
+
|
|
56
|
+
it("login polls authorize and writes auth.json", async () => {
|
|
57
|
+
const dir = makeDir();
|
|
58
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
59
|
+
(createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
60
|
+
authService: { Authorize: vi.fn() },
|
|
61
|
+
consumerService: {} as any,
|
|
62
|
+
subscriptionService: {} as any,
|
|
63
|
+
});
|
|
64
|
+
(pollAuthorize as unknown as ReturnType<typeof vi.fn>).mockResolvedValue({
|
|
65
|
+
accessToken: "access",
|
|
66
|
+
refreshToken: "refresh",
|
|
67
|
+
expiresAt: "2026-01-03T00:00:00Z",
|
|
68
|
+
});
|
|
69
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
70
|
+
const program = createProgram();
|
|
71
|
+
await program.parseAsync(["node", "getrouter", "auth", "login"]);
|
|
72
|
+
const saved = JSON.parse(
|
|
73
|
+
fs.readFileSync(path.join(dir, "auth.json"), "utf-8")
|
|
74
|
+
);
|
|
75
|
+
expect(saved.accessToken).toBe("access");
|
|
76
|
+
expect(saved.refreshToken).toBe("refresh");
|
|
77
|
+
expect(saved.tokenType).toBe("Bearer");
|
|
78
|
+
expect(openLoginUrl).toHaveBeenCalledWith(
|
|
79
|
+
buildLoginUrl((generateAuthCode as unknown as ReturnType<typeof vi.fn>).mock.results[0].value)
|
|
80
|
+
);
|
|
81
|
+
log.mockRestore();
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Step 2: Add device auth polling tests (failing)**
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { describe, it, expect, vi } from "vitest";
|
|
89
|
+
import { pollAuthorize, generateAuthCode } from "../../src/core/auth/device";
|
|
90
|
+
|
|
91
|
+
const makeErr = (status: number) => Object.assign(new Error("err"), { status });
|
|
92
|
+
|
|
93
|
+
it("polls until authorize succeeds", async () => {
|
|
94
|
+
const authorize = vi
|
|
95
|
+
.fn()
|
|
96
|
+
.mockRejectedValueOnce(makeErr(404))
|
|
97
|
+
.mockResolvedValue({
|
|
98
|
+
accessToken: "a",
|
|
99
|
+
refreshToken: "b",
|
|
100
|
+
expiresAt: "2026-01-03T00:00:00Z",
|
|
101
|
+
});
|
|
102
|
+
let now = 0;
|
|
103
|
+
const res = await pollAuthorize({
|
|
104
|
+
authorize,
|
|
105
|
+
code: "abc",
|
|
106
|
+
now: () => now,
|
|
107
|
+
sleep: async (ms) => {
|
|
108
|
+
now += ms;
|
|
109
|
+
},
|
|
110
|
+
initialDelayMs: 1,
|
|
111
|
+
maxDelayMs: 2,
|
|
112
|
+
timeoutMs: 100,
|
|
113
|
+
});
|
|
114
|
+
expect(res.accessToken).toBe("a");
|
|
115
|
+
expect(authorize).toHaveBeenCalledTimes(2);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("fails on 400/403", async () => {
|
|
119
|
+
await expect(
|
|
120
|
+
pollAuthorize({
|
|
121
|
+
authorize: vi.fn().mockRejectedValue(makeErr(400)),
|
|
122
|
+
code: "abc",
|
|
123
|
+
sleep: async () => {},
|
|
124
|
+
now: () => 0,
|
|
125
|
+
timeoutMs: 10,
|
|
126
|
+
})
|
|
127
|
+
).rejects.toThrow("登录码已被使用");
|
|
128
|
+
await expect(
|
|
129
|
+
pollAuthorize({
|
|
130
|
+
authorize: vi.fn().mockRejectedValue(makeErr(403)),
|
|
131
|
+
code: "abc",
|
|
132
|
+
sleep: async () => {},
|
|
133
|
+
now: () => 0,
|
|
134
|
+
timeoutMs: 10,
|
|
135
|
+
})
|
|
136
|
+
).rejects.toThrow("登录码已过期");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("times out after deadline", async () => {
|
|
140
|
+
const authorize = vi.fn().mockRejectedValue(makeErr(404));
|
|
141
|
+
let now = 0;
|
|
142
|
+
await expect(
|
|
143
|
+
pollAuthorize({
|
|
144
|
+
authorize,
|
|
145
|
+
code: "abc",
|
|
146
|
+
now: () => now,
|
|
147
|
+
sleep: async (ms) => {
|
|
148
|
+
now += ms;
|
|
149
|
+
},
|
|
150
|
+
initialDelayMs: 5,
|
|
151
|
+
maxDelayMs: 5,
|
|
152
|
+
timeoutMs: 6,
|
|
153
|
+
})
|
|
154
|
+
).rejects.toThrow("登录超时");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("generates 13-char base32 auth code", () => {
|
|
158
|
+
const code = generateAuthCode();
|
|
159
|
+
expect(code).toMatch(/^[a-z2-7]{13}$/);
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Step 3: Update auth status test expectation (failing)**
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
const status = getAuthStatus();
|
|
167
|
+
expect(status.status).toBe("logged_in");
|
|
168
|
+
expect(status.note).toBeUndefined();
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**Step 4: Run tests to verify they fail**
|
|
172
|
+
|
|
173
|
+
Run: `npm test -- tests/cmd/auth.test.ts tests/auth/device.test.ts tests/auth/status.test.ts`
|
|
174
|
+
Expected: FAIL (device auth not implemented, module missing).
|
|
175
|
+
|
|
176
|
+
**Step 5: Commit failing tests**
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
git add tests/cmd/auth.test.ts tests/auth/device.test.ts tests/auth/status.test.ts
|
|
180
|
+
git commit -m "test: add device auth login coverage"
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Task 2: Implement device auth helpers and update auth status
|
|
184
|
+
|
|
185
|
+
**Files:**
|
|
186
|
+
- Create: `src/core/auth/device.ts`
|
|
187
|
+
- Modify: `src/core/auth/index.ts`
|
|
188
|
+
|
|
189
|
+
**Step 1: Implement device helpers**
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
import { randomInt } from "node:crypto";
|
|
193
|
+
import { spawn } from "node:child_process";
|
|
194
|
+
|
|
195
|
+
type AuthToken = {
|
|
196
|
+
accessToken: string | undefined;
|
|
197
|
+
refreshToken: string | undefined;
|
|
198
|
+
expiresAt: string | undefined;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
type AuthorizeFn = (req: { code: string }) => Promise<AuthToken>;
|
|
202
|
+
|
|
203
|
+
type PollOptions = {
|
|
204
|
+
authorize: AuthorizeFn;
|
|
205
|
+
code: string;
|
|
206
|
+
timeoutMs?: number;
|
|
207
|
+
initialDelayMs?: number;
|
|
208
|
+
maxDelayMs?: number;
|
|
209
|
+
sleep?: (ms: number) => Promise<void>;
|
|
210
|
+
now?: () => number;
|
|
211
|
+
onRetry?: (attempt: number, delayMs: number) => void;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
215
|
+
|
|
216
|
+
export const generateAuthCode = () => {
|
|
217
|
+
let out = "";
|
|
218
|
+
for (let i = 0; i < 13; i += 1) {
|
|
219
|
+
out += alphabet[randomInt(32)];
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export const buildLoginUrl = (authCode: string) =>
|
|
225
|
+
`https://getrouter.dev/#/a/${authCode}`;
|
|
226
|
+
|
|
227
|
+
export const openLoginUrl = async (url: string) => {
|
|
228
|
+
try {
|
|
229
|
+
if (process.platform === "darwin") {
|
|
230
|
+
const child = spawn("open", [url], { stdio: "ignore", detached: true });
|
|
231
|
+
child.unref();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (process.platform === "win32") {
|
|
235
|
+
const child = spawn("cmd", ["/c", "start", "", url], {
|
|
236
|
+
stdio: "ignore",
|
|
237
|
+
detached: true,
|
|
238
|
+
});
|
|
239
|
+
child.unref();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const child = spawn("xdg-open", [url], { stdio: "ignore", detached: true });
|
|
243
|
+
child.unref();
|
|
244
|
+
} catch {
|
|
245
|
+
// best effort
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export const pollAuthorize = async ({
|
|
250
|
+
authorize,
|
|
251
|
+
code,
|
|
252
|
+
timeoutMs = 5 * 60 * 1000,
|
|
253
|
+
initialDelayMs = 1000,
|
|
254
|
+
maxDelayMs = 10000,
|
|
255
|
+
sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)),
|
|
256
|
+
now = () => Date.now(),
|
|
257
|
+
onRetry,
|
|
258
|
+
}: PollOptions) => {
|
|
259
|
+
const start = now();
|
|
260
|
+
let delay = initialDelayMs;
|
|
261
|
+
let attempt = 0;
|
|
262
|
+
while (true) {
|
|
263
|
+
try {
|
|
264
|
+
return await authorize({ code });
|
|
265
|
+
} catch (err: any) {
|
|
266
|
+
const status = err?.status;
|
|
267
|
+
if (status === 404) {
|
|
268
|
+
// keep polling
|
|
269
|
+
} else if (status === 400) {
|
|
270
|
+
throw new Error("登录码已被使用,请重新登录。");
|
|
271
|
+
} else if (status === 403) {
|
|
272
|
+
throw new Error("登录码已过期,请重新登录。");
|
|
273
|
+
} else {
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (now() - start >= timeoutMs) {
|
|
278
|
+
throw new Error("登录超时,请重新执行 getrouter auth login。");
|
|
279
|
+
}
|
|
280
|
+
attempt += 1;
|
|
281
|
+
onRetry?.(attempt, delay);
|
|
282
|
+
await sleep(delay);
|
|
283
|
+
delay = Math.min(delay * 2, maxDelayMs);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**Step 2: Update auth status note**
|
|
289
|
+
|
|
290
|
+
Remove the “远端验证待开放” note when logged in:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
return {
|
|
294
|
+
status: "logged_in",
|
|
295
|
+
expiresAt: auth.expiresAt,
|
|
296
|
+
accessToken: auth.accessToken,
|
|
297
|
+
refreshToken: auth.refreshToken,
|
|
298
|
+
tokenType: auth.tokenType,
|
|
299
|
+
};
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Step 3: Run tests to verify they still fail**
|
|
303
|
+
|
|
304
|
+
Run: `npm test -- tests/auth/device.test.ts tests/auth/status.test.ts`
|
|
305
|
+
Expected: PASS for new unit tests, but `auth login` test still FAIL (cmd not wired yet).
|
|
306
|
+
|
|
307
|
+
**Step 4: Commit helper implementation**
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
git add src/core/auth/device.ts src/core/auth/index.ts tests/auth/device.test.ts tests/auth/status.test.ts
|
|
311
|
+
git commit -m "feat: add device auth helpers"
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Task 3: Wire auth login to device flow and update API client
|
|
315
|
+
|
|
316
|
+
**Files:**
|
|
317
|
+
- Modify: `src/cmd/auth.ts`
|
|
318
|
+
- Modify: `src/core/api/client.ts`
|
|
319
|
+
- Modify: `tests/cmd/auth.test.ts`
|
|
320
|
+
- Modify: `tests/core/api/client.test.ts`
|
|
321
|
+
- Modify: `src/generated/router/dashboard/v1/index.ts`
|
|
322
|
+
|
|
323
|
+
**Step 1: Refresh generated dashboard client**
|
|
324
|
+
|
|
325
|
+
Copy latest generated file that includes `AuthService.Authorize`:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
cp /Users/xus/code/github/getrouter/router/frontend/dashboard/src/services/router/dashboard/v1/index.ts \
|
|
329
|
+
src/generated/router/dashboard/v1/index.ts
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Step 2: Extend API client adapter to include authService**
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { createAuthServiceClient } from "../../generated/router/dashboard/v1";
|
|
336
|
+
|
|
337
|
+
// add to ClientFactories
|
|
338
|
+
createAuthServiceClient: (handler: RequestHandler) => unknown;
|
|
339
|
+
|
|
340
|
+
// in factories default
|
|
341
|
+
createAuthServiceClient,
|
|
342
|
+
|
|
343
|
+
// in return value
|
|
344
|
+
authService: factories.createAuthServiceClient(handler) as any,
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Update `tests/core/api/client.test.ts` fakeClients to include `createAuthServiceClient: (_handler) => ({})`.
|
|
348
|
+
|
|
349
|
+
**Step 3: Implement device login in cmd/auth**
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
import { writeAuth } from "../core/config";
|
|
353
|
+
import { createApiClients } from "../core/api/client";
|
|
354
|
+
import {
|
|
355
|
+
generateAuthCode,
|
|
356
|
+
buildLoginUrl,
|
|
357
|
+
openLoginUrl,
|
|
358
|
+
pollAuthorize,
|
|
359
|
+
} from "../core/auth/device";
|
|
360
|
+
|
|
361
|
+
.auth.command("login")
|
|
362
|
+
.description("Login with device flow")
|
|
363
|
+
.action(async () => {
|
|
364
|
+
const { authService } = createApiClients({});
|
|
365
|
+
const authCode = generateAuthCode();
|
|
366
|
+
const url = buildLoginUrl(authCode);
|
|
367
|
+
console.log("To authenticate, visit:");
|
|
368
|
+
console.log(url);
|
|
369
|
+
console.log("Waiting for confirmation...");
|
|
370
|
+
void openLoginUrl(url);
|
|
371
|
+
const token = await pollAuthorize({
|
|
372
|
+
authorize: authService.Authorize.bind(authService),
|
|
373
|
+
code: authCode,
|
|
374
|
+
onRetry: () => {},
|
|
375
|
+
});
|
|
376
|
+
writeAuth({
|
|
377
|
+
accessToken: token.accessToken ?? "",
|
|
378
|
+
refreshToken: token.refreshToken ?? "",
|
|
379
|
+
expiresAt: token.expiresAt ?? "",
|
|
380
|
+
tokenType: "Bearer",
|
|
381
|
+
});
|
|
382
|
+
console.log("登录成功");
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Step 4: Run tests to verify pass**
|
|
387
|
+
|
|
388
|
+
Run: `npm test -- tests/cmd/auth.test.ts tests/core/api/client.test.ts`
|
|
389
|
+
Expected: PASS
|
|
390
|
+
|
|
391
|
+
**Step 5: Commit**
|
|
392
|
+
|
|
393
|
+
```bash
|
|
394
|
+
git add src/cmd/auth.ts src/core/api/client.ts src/generated/router/dashboard/v1/index.ts tests/cmd/auth.test.ts tests/core/api/client.test.ts
|
|
395
|
+
git commit -m "feat: implement device auth login"
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Task 4: Full test run
|
|
399
|
+
|
|
400
|
+
**Step 1: Run full test suite**
|
|
401
|
+
|
|
402
|
+
Run: `npm test`
|
|
403
|
+
Expected: PASS
|
|
404
|
+
|
|
405
|
+
**Step 2: Commit (if needed)**
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
git status -sb
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
If clean, no commit needed.
|