@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,231 @@
|
|
|
1
|
+
# Config Commands Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add `getrouter config get/set` commands with fixed keys (`apiBase`, `json`), validation, and JSON output option.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Implement a `config` command module under `src/cmd/config.ts` that uses the `core/config` read/write helpers. Keep parsing/validation in the command layer, with small pure helpers for value parsing and apiBase normalization to make testing easier.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** TypeScript, commander, vitest.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Parse/Normalize Helpers (TDD)
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Create: `src/cmd/config-helpers.ts`
|
|
17
|
+
- Test: `tests/cmd/config-helpers.test.ts`
|
|
18
|
+
|
|
19
|
+
**Step 1: Write the failing test**
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { describe, it, expect } from "vitest";
|
|
23
|
+
import { normalizeApiBase, parseConfigValue } from "../../src/cmd/config-helpers";
|
|
24
|
+
|
|
25
|
+
describe("config helpers", () => {
|
|
26
|
+
it("normalizes apiBase", () => {
|
|
27
|
+
expect(normalizeApiBase("https://getrouter.dev/")).toBe("https://getrouter.dev");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("parses json values", () => {
|
|
31
|
+
expect(parseConfigValue("json", "true")).toBe(true);
|
|
32
|
+
expect(parseConfigValue("json", "0")).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Step 2: Run test to verify it fails**
|
|
38
|
+
|
|
39
|
+
Run: `npm test`
|
|
40
|
+
Expected: FAIL (module not found).
|
|
41
|
+
|
|
42
|
+
**Step 3: Write minimal implementation**
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
export const normalizeApiBase = (value: string) =>
|
|
46
|
+
value.trim().replace(/\\/+$/, "");
|
|
47
|
+
|
|
48
|
+
export const parseConfigValue = (key: "apiBase" | "json", raw: string) => {
|
|
49
|
+
if (key === "apiBase") {
|
|
50
|
+
const normalized = normalizeApiBase(raw);
|
|
51
|
+
if (!/^https?:\\/\\//.test(normalized)) {
|
|
52
|
+
throw new Error("apiBase must start with http:// or https://");
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
const lowered = raw.toLowerCase();
|
|
57
|
+
if (["true", "1"].includes(lowered)) return true;
|
|
58
|
+
if (["false", "0"].includes(lowered)) return false;
|
|
59
|
+
throw new Error("json must be true/false or 1/0");
|
|
60
|
+
};
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Step 4: Run test to verify it passes**
|
|
64
|
+
|
|
65
|
+
Run: `npm test`
|
|
66
|
+
Expected: PASS.
|
|
67
|
+
|
|
68
|
+
**Step 5: Commit**
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git add src/cmd/config-helpers.ts tests/cmd/config-helpers.test.ts
|
|
72
|
+
git commit -m "feat: add config parsing helpers"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
### Task 2: Config Command (TDD)
|
|
78
|
+
|
|
79
|
+
**Files:**
|
|
80
|
+
- Create: `src/cmd/config.ts`
|
|
81
|
+
- Modify: `src/cmd/index.ts`
|
|
82
|
+
- Test: `tests/cmd/config.test.ts`
|
|
83
|
+
|
|
84
|
+
**Step 1: Write the failing test**
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { describe, it, expect, vi } from "vitest";
|
|
88
|
+
import { createProgram } from "../../src/cli";
|
|
89
|
+
|
|
90
|
+
describe("config command", () => {
|
|
91
|
+
it("prints full config for get", async () => {
|
|
92
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
93
|
+
const program = createProgram();
|
|
94
|
+
await program.parseAsync(["node", "getrouter", "config", "get"]);
|
|
95
|
+
expect(log).toHaveBeenCalled();
|
|
96
|
+
log.mockRestore();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Step 2: Run test to verify it fails**
|
|
102
|
+
|
|
103
|
+
Run: `npm test`
|
|
104
|
+
Expected: FAIL (config command missing).
|
|
105
|
+
|
|
106
|
+
**Step 3: Implement minimal config command**
|
|
107
|
+
|
|
108
|
+
`src/cmd/config.ts`
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { Command } from "commander";
|
|
112
|
+
import { readConfig, writeConfig } from "../core/config";
|
|
113
|
+
import { parseConfigValue } from "./config-helpers";
|
|
114
|
+
|
|
115
|
+
const VALID_KEYS = new Set(["apiBase", "json"]);
|
|
116
|
+
|
|
117
|
+
export const registerConfigCommands = (program: Command) => {
|
|
118
|
+
const config = program.command("config").description("Manage CLI config");
|
|
119
|
+
|
|
120
|
+
config
|
|
121
|
+
.command("get")
|
|
122
|
+
.argument("[key]")
|
|
123
|
+
.option("--json", "Output JSON")
|
|
124
|
+
.action((key: string | undefined, options: { json?: boolean }) => {
|
|
125
|
+
const cfg = readConfig();
|
|
126
|
+
if (!key) {
|
|
127
|
+
if (options.json) {
|
|
128
|
+
console.log(JSON.stringify(cfg, null, 2));
|
|
129
|
+
} else {
|
|
130
|
+
console.log(`apiBase=${cfg.apiBase}`);
|
|
131
|
+
console.log(`json=${cfg.json}`);
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!VALID_KEYS.has(key)) {
|
|
136
|
+
throw new Error("Unknown config key");
|
|
137
|
+
}
|
|
138
|
+
const value = (cfg as any)[key];
|
|
139
|
+
if (options.json) {
|
|
140
|
+
console.log(JSON.stringify({ [key]: value }, null, 2));
|
|
141
|
+
} else {
|
|
142
|
+
console.log(`${key}=${value}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
config
|
|
147
|
+
.command("set")
|
|
148
|
+
.argument("<key>")
|
|
149
|
+
.argument("<value>")
|
|
150
|
+
.option("--json", "Output JSON")
|
|
151
|
+
.action((key: string, value: string, options: { json?: boolean }) => {
|
|
152
|
+
if (!VALID_KEYS.has(key)) {
|
|
153
|
+
throw new Error("Unknown config key");
|
|
154
|
+
}
|
|
155
|
+
const cfg = readConfig();
|
|
156
|
+
const parsed = parseConfigValue(key as "apiBase" | "json", value);
|
|
157
|
+
const next = { ...cfg, [key]: parsed };
|
|
158
|
+
writeConfig(next);
|
|
159
|
+
if (options.json) {
|
|
160
|
+
console.log(JSON.stringify(next, null, 2));
|
|
161
|
+
} else {
|
|
162
|
+
console.log(`${key}=${(next as any)[key]}`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`src/cmd/index.ts` add:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { registerConfigCommands } from "./config";
|
|
172
|
+
// ...
|
|
173
|
+
registerConfigCommands(program);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Step 4: Run test to verify it passes**
|
|
177
|
+
|
|
178
|
+
Run: `npm test`
|
|
179
|
+
Expected: PASS.
|
|
180
|
+
|
|
181
|
+
**Step 5: Commit**
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
git add src/cmd/config.ts src/cmd/index.ts tests/cmd/config.test.ts
|
|
185
|
+
git commit -m "feat: add config get/set commands"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
### Task 3: Validation & Error Tests (TDD)
|
|
191
|
+
|
|
192
|
+
**Files:**
|
|
193
|
+
- Modify: `tests/cmd/config.test.ts`
|
|
194
|
+
|
|
195
|
+
**Step 1: Write failing tests**
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { describe, it, expect } from "vitest";
|
|
199
|
+
import { parseConfigValue } from "../../src/cmd/config-helpers";
|
|
200
|
+
|
|
201
|
+
describe("config validation", () => {
|
|
202
|
+
it("rejects invalid json value", () => {
|
|
203
|
+
expect(() => parseConfigValue("json", "nope")).toThrow();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("rejects invalid apiBase", () => {
|
|
207
|
+
expect(() => parseConfigValue("apiBase", "ftp://bad")).toThrow();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Step 2: Run test to verify it fails**
|
|
213
|
+
|
|
214
|
+
Run: `npm test`
|
|
215
|
+
Expected: FAIL if behavior not enforced.
|
|
216
|
+
|
|
217
|
+
**Step 3: Implement minimal fixes**
|
|
218
|
+
|
|
219
|
+
Adjust `parseConfigValue` as needed (should already throw for invalid).
|
|
220
|
+
|
|
221
|
+
**Step 4: Run test to verify it passes**
|
|
222
|
+
|
|
223
|
+
Run: `npm test`
|
|
224
|
+
Expected: PASS.
|
|
225
|
+
|
|
226
|
+
**Step 5: Commit**
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
git add tests/cmd/config.test.ts src/cmd/config-helpers.ts
|
|
230
|
+
git commit -m "test: cover config validation"
|
|
231
|
+
```
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# getrouter CLI Config Core Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Build the core config/auth file IO utilities for `~/.getrouter`, including read/write helpers, defaults, and safe redaction for output.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Add a small `core/config` module that reads/writes JSON files under `~/.getrouter`, exposes typed helpers for config and auth state, and includes a redaction utility for secrets. Use sync file IO for simplicity in CLI startup paths.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js fs/path/os, TypeScript, vitest.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### Task 1: Auth & Config Types
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
- Create: `src/core/config/types.ts`
|
|
17
|
+
- Test: `tests/config/types.test.ts`
|
|
18
|
+
|
|
19
|
+
**Step 1: Write the failing test**
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { describe, it, expect } from "vitest";
|
|
23
|
+
import { defaultConfig, defaultAuthState } from "../../src/core/config/types";
|
|
24
|
+
|
|
25
|
+
describe("config types defaults", () => {
|
|
26
|
+
it("provides sane defaults", () => {
|
|
27
|
+
expect(defaultConfig().apiBase).toBe("https://getrouter.dev");
|
|
28
|
+
expect(defaultConfig().json).toBe(false);
|
|
29
|
+
expect(defaultAuthState().accessToken).toBe("");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Step 2: Run test to verify it fails**
|
|
35
|
+
|
|
36
|
+
Run: `npm test`
|
|
37
|
+
Expected: FAIL (module not found).
|
|
38
|
+
|
|
39
|
+
**Step 3: Write minimal implementation**
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
export type ConfigFile = {
|
|
43
|
+
apiBase: string;
|
|
44
|
+
json: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type AuthState = {
|
|
48
|
+
accessToken: string;
|
|
49
|
+
refreshToken: string;
|
|
50
|
+
expiresAt: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const defaultConfig = (): ConfigFile => ({
|
|
54
|
+
apiBase: "https://getrouter.dev",
|
|
55
|
+
json: false,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const defaultAuthState = (): AuthState => ({
|
|
59
|
+
accessToken: "",
|
|
60
|
+
refreshToken: "",
|
|
61
|
+
expiresAt: "",
|
|
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/config/types.ts tests/config/types.test.ts
|
|
74
|
+
git commit -m "feat: add config/auth types and defaults"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
### Task 2: Config Paths & File Helpers (TDD)
|
|
80
|
+
|
|
81
|
+
**Files:**
|
|
82
|
+
- Create: `src/core/config/paths.ts`
|
|
83
|
+
- Create: `src/core/config/fs.ts`
|
|
84
|
+
- Test: `tests/config/paths.test.ts`
|
|
85
|
+
- Test: `tests/config/fs.test.ts`
|
|
86
|
+
|
|
87
|
+
**Step 1: Write failing tests**
|
|
88
|
+
|
|
89
|
+
`tests/config/paths.test.ts`
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { describe, it, expect } from "vitest";
|
|
93
|
+
import { getAuthPath, getConfigPath } from "../../src/core/config/paths";
|
|
94
|
+
|
|
95
|
+
describe("config paths", () => {
|
|
96
|
+
it("returns ~/.getrouter paths", () => {
|
|
97
|
+
expect(getConfigPath()).toContain(".getrouter");
|
|
98
|
+
expect(getConfigPath()).toContain("config.json");
|
|
99
|
+
expect(getAuthPath()).toContain("auth.json");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`tests/config/fs.test.ts`
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { describe, it, expect } from "vitest";
|
|
108
|
+
import { readJsonFile, writeJsonFile } from "../../src/core/config/fs";
|
|
109
|
+
import fs from "node:fs";
|
|
110
|
+
import os from "node:os";
|
|
111
|
+
import path from "node:path";
|
|
112
|
+
|
|
113
|
+
describe("config fs", () => {
|
|
114
|
+
it("writes and reads JSON", () => {
|
|
115
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
116
|
+
const file = path.join(dir, "config.json");
|
|
117
|
+
writeJsonFile(file, { hello: "world" });
|
|
118
|
+
expect(readJsonFile(file)).toEqual({ hello: "world" });
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Step 2: Run tests to verify they fail**
|
|
124
|
+
|
|
125
|
+
Run: `npm test`
|
|
126
|
+
Expected: FAIL (module not found).
|
|
127
|
+
|
|
128
|
+
**Step 3: Implement minimal helpers**
|
|
129
|
+
|
|
130
|
+
`src/core/config/paths.ts`
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import path from "node:path";
|
|
134
|
+
import { getConfigDir } from "../paths";
|
|
135
|
+
|
|
136
|
+
export const getConfigPath = () => path.join(getConfigDir(), "config.json");
|
|
137
|
+
export const getAuthPath = () => path.join(getConfigDir(), "auth.json");
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`src/core/config/fs.ts`
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import fs from "node:fs";
|
|
144
|
+
|
|
145
|
+
export const readJsonFile = <T = unknown>(filePath: string): T | null => {
|
|
146
|
+
if (!fs.existsSync(filePath)) return null;
|
|
147
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
148
|
+
return JSON.parse(raw) as T;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const writeJsonFile = (filePath: string, value: unknown) => {
|
|
152
|
+
fs.mkdirSync(require("node:path").dirname(filePath), { recursive: true });
|
|
153
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2), "utf8");
|
|
154
|
+
};
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Step 4: Run tests to verify they pass**
|
|
158
|
+
|
|
159
|
+
Run: `npm test`
|
|
160
|
+
Expected: PASS.
|
|
161
|
+
|
|
162
|
+
**Step 5: Commit**
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git add src/core/config/paths.ts src/core/config/fs.ts tests/config/paths.test.ts tests/config/fs.test.ts
|
|
166
|
+
git commit -m "feat: add config path and fs helpers"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### Task 3: Config/Auth Read & Write (TDD)
|
|
172
|
+
|
|
173
|
+
**Files:**
|
|
174
|
+
- Create: `src/core/config/index.ts`
|
|
175
|
+
- Test: `tests/config/index.test.ts`
|
|
176
|
+
|
|
177
|
+
**Step 1: Write the failing test**
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { describe, it, expect } from "vitest";
|
|
181
|
+
import { readConfig, writeConfig, readAuth, writeAuth } from "../../src/core/config";
|
|
182
|
+
import fs from "node:fs";
|
|
183
|
+
import os from "node:os";
|
|
184
|
+
import path from "node:path";
|
|
185
|
+
|
|
186
|
+
describe("config read/write", () => {
|
|
187
|
+
it("writes and reads config with defaults", () => {
|
|
188
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
189
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
190
|
+
writeConfig({ apiBase: "https://getrouter.dev", json: true });
|
|
191
|
+
const cfg = readConfig();
|
|
192
|
+
expect(cfg.apiBase).toBe("https://getrouter.dev");
|
|
193
|
+
expect(cfg.json).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Step 2: Run test to verify it fails**
|
|
199
|
+
|
|
200
|
+
Run: `npm test`
|
|
201
|
+
Expected: FAIL (module not found).
|
|
202
|
+
|
|
203
|
+
**Step 3: Implement minimal read/write**
|
|
204
|
+
|
|
205
|
+
`src/core/config/index.ts`
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import path from "node:path";
|
|
209
|
+
import { defaultConfig, defaultAuthState, ConfigFile, AuthState } from "./types";
|
|
210
|
+
import { readJsonFile, writeJsonFile } from "./fs";
|
|
211
|
+
|
|
212
|
+
const resolveConfigDir = () =>
|
|
213
|
+
process.env.GETROUTER_CONFIG_DIR ||
|
|
214
|
+
path.join(require("node:os").homedir(), ".getrouter");
|
|
215
|
+
|
|
216
|
+
const getConfigPath = () => path.join(resolveConfigDir(), "config.json");
|
|
217
|
+
const getAuthPath = () => path.join(resolveConfigDir(), "auth.json");
|
|
218
|
+
|
|
219
|
+
export const readConfig = (): ConfigFile => ({
|
|
220
|
+
...defaultConfig(),
|
|
221
|
+
...(readJsonFile<ConfigFile>(getConfigPath()) ?? {}),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
export const writeConfig = (cfg: ConfigFile) => writeJsonFile(getConfigPath(), cfg);
|
|
225
|
+
|
|
226
|
+
export const readAuth = (): AuthState => ({
|
|
227
|
+
...defaultAuthState(),
|
|
228
|
+
...(readJsonFile<AuthState>(getAuthPath()) ?? {}),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
export const writeAuth = (auth: AuthState) => writeJsonFile(getAuthPath(), auth);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Step 4: Run tests to verify they pass**
|
|
235
|
+
|
|
236
|
+
Run: `npm test`
|
|
237
|
+
Expected: PASS.
|
|
238
|
+
|
|
239
|
+
**Step 5: Commit**
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
git add src/core/config/index.ts tests/config/index.test.ts
|
|
243
|
+
git commit -m "feat: add config/auth read write helpers"
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### Task 4: Redaction Utility (TDD)
|
|
249
|
+
|
|
250
|
+
**Files:**
|
|
251
|
+
- Create: `src/core/config/redact.ts`
|
|
252
|
+
- Test: `tests/config/redact.test.ts`
|
|
253
|
+
|
|
254
|
+
**Step 1: Write the failing test**
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import { describe, it, expect } from "vitest";
|
|
258
|
+
import { redactSecrets } from "../../src/core/config/redact";
|
|
259
|
+
|
|
260
|
+
describe("redact", () => {
|
|
261
|
+
it("redacts known secret fields", () => {
|
|
262
|
+
const out = redactSecrets({
|
|
263
|
+
accessToken: "secret",
|
|
264
|
+
refreshToken: "secret2",
|
|
265
|
+
apiKey: "key",
|
|
266
|
+
other: "ok",
|
|
267
|
+
});
|
|
268
|
+
expect(out.accessToken).toBe("****");
|
|
269
|
+
expect(out.refreshToken).toBe("****");
|
|
270
|
+
expect(out.apiKey).toBe("****");
|
|
271
|
+
expect(out.other).toBe("ok");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Step 2: Run test to verify it fails**
|
|
277
|
+
|
|
278
|
+
Run: `npm test`
|
|
279
|
+
Expected: FAIL (module not found).
|
|
280
|
+
|
|
281
|
+
**Step 3: Implement minimal redaction**
|
|
282
|
+
|
|
283
|
+
```ts
|
|
284
|
+
const SECRET_KEYS = new Set(["accessToken", "refreshToken", "apiKey"]);
|
|
285
|
+
|
|
286
|
+
export const redactSecrets = <T extends Record<string, any>>(obj: T): T => {
|
|
287
|
+
const out = { ...obj } as T;
|
|
288
|
+
for (const key of Object.keys(out)) {
|
|
289
|
+
if (SECRET_KEYS.has(key)) {
|
|
290
|
+
out[key] = "****";
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
};
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Step 4: Run tests to verify they pass**
|
|
298
|
+
|
|
299
|
+
Run: `npm test`
|
|
300
|
+
Expected: PASS.
|
|
301
|
+
|
|
302
|
+
**Step 5: Commit**
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
git add src/core/config/redact.ts tests/config/redact.test.ts
|
|
306
|
+
git commit -m "feat: add secret redaction helper"
|
|
307
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# getrouter CLI 设计(v1)
|
|
2
|
+
|
|
3
|
+
日期:2026-01-01
|
|
4
|
+
状态:已确认
|
|
5
|
+
|
|
6
|
+
## 背景与目标
|
|
7
|
+
- 面向 getrouter.dev 用户提供本地 CLI,覆盖 API Key 管理、订阅查询与自动配置 Codex/Claude 环境。
|
|
8
|
+
- 与现有 dashboard API 保持一致,减少接口漂移与重复维护成本。
|
|
9
|
+
- 默认人类可读输出,支持 `--json` 供脚本自动化。
|
|
10
|
+
|
|
11
|
+
## 非目标(v1)
|
|
12
|
+
- 不支持组织/工作区切换(未来版本再做)。
|
|
13
|
+
- 不接入 admin API、账单明细、provider/model 写操作。
|
|
14
|
+
- 设备码 OAuth 流程等待后端接口就绪后再开放。
|
|
15
|
+
|
|
16
|
+
## 需求与功能范围
|
|
17
|
+
- 登录/登出/状态:GitHub OAuth(浏览器回调 + 设备码扩展点)。
|
|
18
|
+
- API Key 管理:Consumer 多 key 的 list/create/update/delete/get。
|
|
19
|
+
- 订阅与计划:当前订阅、计划列表。
|
|
20
|
+
- 只读资源:模型列表、供应商列表、当前用户信息。
|
|
21
|
+
- 环境配置:生成 `~/.getrouter/` 配置与可选 shell 注入,支持 Codex/Claude。
|
|
22
|
+
|
|
23
|
+
## 方案选型
|
|
24
|
+
**推荐**:复用由 proto 生成的 TypeScript HTTP client(dashboard 同源),CLI 仅封装配置/鉴权/输出层。
|
|
25
|
+
**理由**:与后端一致、类型稳定、交付最快。
|
|
26
|
+
|
|
27
|
+
## 架构与模块
|
|
28
|
+
```
|
|
29
|
+
cmd/ 命令与交互(login/keys/subscription/setup)
|
|
30
|
+
core/
|
|
31
|
+
auth/ OAuth 流程、token 刷新与存储
|
|
32
|
+
config/ ~/.getrouter 读写、权限、脱敏
|
|
33
|
+
output/ 表格输出、--json、错误格式化
|
|
34
|
+
api/ 统一 requestHandler + 生成 client
|
|
35
|
+
templates/ env.sh / env.ps1 模板
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 命令设计(建议)
|
|
39
|
+
- `getrouter auth login|logout|status`
|
|
40
|
+
- `getrouter keys list|create|update|delete|get`
|
|
41
|
+
- `getrouter subscription show`
|
|
42
|
+
- `getrouter plans list`
|
|
43
|
+
- `getrouter models list`
|
|
44
|
+
- `getrouter providers list`
|
|
45
|
+
- `getrouter user current`
|
|
46
|
+
- `getrouter setup env [--shell zsh|bash|fish|pwsh]`
|
|
47
|
+
- `getrouter config get|set`
|
|
48
|
+
|
|
49
|
+
## 认证与 Token
|
|
50
|
+
- 访问管理 API 统一使用 `Authorization: Bearer <access_token>`。
|
|
51
|
+
- token 保存于 `~/.getrouter/auth.json`(明文),文件权限 *nix 600。
|
|
52
|
+
- access 过期后尝试 refresh;刷新失败则提示重新登录。
|
|
53
|
+
- 设备码流程待后端完成后接入(命令入口可先预留)。
|
|
54
|
+
|
|
55
|
+
## API 映射(当前 dashboard HTTP)
|
|
56
|
+
管理 API 前缀:`https://getrouter.dev/v1`
|
|
57
|
+
注:生成 client 的 path 已包含 `v1/...`,CLI 建议配置 `api_base=https://getrouter.dev` 以避免双 `v1`。
|
|
58
|
+
|
|
59
|
+
- OAuth Connect:`GET /v1/dashboard/identities/connect?type=GITHUB`
|
|
60
|
+
- OAuth Callback:`GET /v1/dashboard/identities/GITHUB/callback?code=...&state=...`
|
|
61
|
+
- 当前用户:`GET /v1/dashboard/users/current`
|
|
62
|
+
- 订阅:`GET /v1/dashboard/subscriptions/current`
|
|
63
|
+
- 计划列表:`GET /v1/dashboard/plans`
|
|
64
|
+
- Consumer:
|
|
65
|
+
- `GET /v1/dashboard/consumers`
|
|
66
|
+
- `GET /v1/dashboard/consumers/:id`
|
|
67
|
+
- `POST /v1/dashboard/consumers/create`
|
|
68
|
+
- `PUT /v1/dashboard/consumers/update?updateMask=...`
|
|
69
|
+
- `DELETE /v1/dashboard/consumers/:id`
|
|
70
|
+
- 模型列表:`GET /v1/dashboard/models`
|
|
71
|
+
- 供应商列表:`GET /v1/dashboard/providers`
|
|
72
|
+
|
|
73
|
+
## 配置与环境变量
|
|
74
|
+
目录统一在 `~/.getrouter/`:
|
|
75
|
+
- `auth.json`:token 与过期时间
|
|
76
|
+
- `config.json`:API base、默认 consumer 等
|
|
77
|
+
- `env.sh` / `env.ps1`:可选环境变量脚本
|
|
78
|
+
|
|
79
|
+
环境变量(用于 Codex/Claude):
|
|
80
|
+
- `OPENAI_BASE_URL=https://api.getrouter.dev/v1`
|
|
81
|
+
- `OPENAI_API_KEY=<consumer api key>`
|
|
82
|
+
- `ANTHROPIC_BASE_URL=https://api.getrouter.dev/v1`
|
|
83
|
+
- `ANTHROPIC_API_KEY=<consumer api key>`
|
|
84
|
+
|
|
85
|
+
## 输出格式
|
|
86
|
+
- 默认人类可读(表格/摘要)。
|
|
87
|
+
- `--json` 输出结构化数据,错误也保持 JSON(code/message/details/status)。
|
|
88
|
+
- 默认脱敏 token 与 API key;必要时提供 `--show-secret` 明示。
|
|
89
|
+
|
|
90
|
+
## 错误处理与重试
|
|
91
|
+
- 统一错误包装:`code/message/details/status`。
|
|
92
|
+
- 仅对幂等 GET 做一次指数退避重试。
|
|
93
|
+
- 401/403 明确提示 `getrouter auth login`。
|
|
94
|
+
|
|
95
|
+
## 测试策略
|
|
96
|
+
- 单测:配置读写、URL 拼装、错误映射、脱敏、输出格式。
|
|
97
|
+
- 集成:mock server 或 staging(若未来提供)。
|
|
98
|
+
|
|
99
|
+
## 发布与分发
|
|
100
|
+
- npm 包(`npx getrouter` 或全局安装)。
|
|
101
|
+
- GitHub Releases 提供平台说明与安装指引。
|
|
102
|
+
|
|
103
|
+
## 待确认/风险
|
|
104
|
+
- refresh token 的字段名、过期策略与刷新接口。
|
|
105
|
+
- 设备码 OAuth 的具体 endpoint 与回调协议。
|
|
106
|
+
- 管理 API base 与 `v1` 路径的最终规范(避免双重 `v1`)。
|