@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,163 @@
1
+ import prompts from "prompts";
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { createProgram } from "../../src/cli";
4
+ import { createApiClients } from "../../src/core/api/client";
5
+ import type {
6
+ AuthService,
7
+ ConsumerService,
8
+ SubscriptionService,
9
+ } from "../../src/generated/router/dashboard/v1";
10
+
11
+ vi.mock("../../src/core/api/client", () => ({
12
+ createApiClients: vi.fn(),
13
+ }));
14
+
15
+ const mockConsumer = {
16
+ id: "c1",
17
+ name: "dev",
18
+ enabled: true,
19
+ apiKey: "abcd1234WXYZ",
20
+ lastAccess: "2026-01-02T00:00:00Z",
21
+ createdAt: "2026-01-01T00:00:00Z",
22
+ };
23
+
24
+ const emptyAuthService = {} as AuthService;
25
+ const emptySubscriptionService = {} as SubscriptionService;
26
+
27
+ const originalIsTTY = process.stdin.isTTY;
28
+ const setStdinTTY = (value: boolean) => {
29
+ Object.defineProperty(process.stdin, "isTTY", {
30
+ value,
31
+ configurable: true,
32
+ });
33
+ };
34
+
35
+ afterEach(() => {
36
+ setStdinTTY(originalIsTTY);
37
+ prompts.inject([]);
38
+ });
39
+
40
+ describe("keys command", () => {
41
+ it("lists keys and redacts apiKey", async () => {
42
+ setStdinTTY(false);
43
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
44
+ consumerService: {
45
+ ListConsumers: vi.fn().mockResolvedValue({ consumers: [mockConsumer] }),
46
+ } as unknown as ConsumerService,
47
+ subscriptionService: emptySubscriptionService,
48
+ authService: emptyAuthService,
49
+ });
50
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
51
+ const program = createProgram();
52
+ await program.parseAsync(["node", "getrouter", "keys", "list"]);
53
+ const output = log.mock.calls.map((c) => c[0]).join("\n");
54
+ expect(output).toContain("API_KEY");
55
+ expect(output).toContain("NAME");
56
+ expect(output).not.toContain("ID");
57
+ expect(output).toContain("abcd...WXYZ");
58
+ log.mockRestore();
59
+ });
60
+
61
+ it("lists keys when no subcommand", async () => {
62
+ setStdinTTY(true);
63
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
64
+ consumerService: {
65
+ ListConsumers: vi.fn().mockResolvedValue({ consumers: [mockConsumer] }),
66
+ } as unknown as ConsumerService,
67
+ subscriptionService: emptySubscriptionService,
68
+ authService: emptyAuthService,
69
+ });
70
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
71
+ const program = createProgram();
72
+ await program.parseAsync(["node", "getrouter", "keys"]);
73
+ const output = log.mock.calls.map((c) => c[0]).join("\n");
74
+ expect(output).toContain("NAME");
75
+ expect(output).not.toContain("ID");
76
+ expect(output).toContain("abcd...WXYZ");
77
+ log.mockRestore();
78
+ });
79
+
80
+ it("rejects removed get subcommand", async () => {
81
+ setStdinTTY(false);
82
+ const program = createProgram();
83
+ program.exitOverride();
84
+ await expect(
85
+ program.parseAsync(["node", "getrouter", "keys", "get", "c1"]),
86
+ ).rejects.toBeTruthy();
87
+ });
88
+
89
+ it("creates a key and prints reminder", async () => {
90
+ setStdinTTY(true);
91
+ prompts.inject(["dev", true]);
92
+ const createConsumer = vi.fn().mockResolvedValue(mockConsumer);
93
+ const updateConsumer = vi.fn().mockResolvedValue(mockConsumer);
94
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
95
+ consumerService: {
96
+ CreateConsumer: createConsumer,
97
+ UpdateConsumer: updateConsumer,
98
+ } as unknown as ConsumerService,
99
+ subscriptionService: emptySubscriptionService,
100
+ authService: emptyAuthService,
101
+ });
102
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
103
+ const program = createProgram();
104
+ await program.parseAsync(["node", "getrouter", "keys", "create"]);
105
+ const output = log.mock.calls.map((c) => c[0]).join("\n");
106
+ expect(output).toContain("NAME");
107
+ expect(output).toContain("API_KEY");
108
+ expect(output).toContain("Please store this API key securely.");
109
+ log.mockRestore();
110
+ });
111
+
112
+ it("updates a key from prompts", async () => {
113
+ setStdinTTY(true);
114
+ prompts.inject(["new-name", false]);
115
+ const updateConsumer = vi.fn().mockResolvedValue({
116
+ ...mockConsumer,
117
+ name: "new-name",
118
+ enabled: false,
119
+ });
120
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
121
+ consumerService: {
122
+ UpdateConsumer: updateConsumer,
123
+ GetConsumer: vi.fn().mockResolvedValue(mockConsumer),
124
+ } as unknown as ConsumerService,
125
+ subscriptionService: emptySubscriptionService,
126
+ authService: emptyAuthService,
127
+ });
128
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
129
+ const program = createProgram();
130
+ await program.parseAsync(["node", "getrouter", "keys", "update", "c1"]);
131
+ expect(updateConsumer).toHaveBeenCalledWith(
132
+ expect.objectContaining({
133
+ consumer: expect.objectContaining({
134
+ id: "c1",
135
+ name: "new-name",
136
+ enabled: false,
137
+ }),
138
+ }),
139
+ );
140
+ const output = log.mock.calls.map((c) => c[0]).join("\n");
141
+ expect(output).toContain("NAME");
142
+ log.mockRestore();
143
+ });
144
+
145
+ it("does not delete when confirmation is declined", async () => {
146
+ setStdinTTY(true);
147
+ prompts.inject([false]);
148
+ const deleteConsumer = vi.fn().mockResolvedValue({});
149
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
150
+ consumerService: {
151
+ DeleteConsumer: deleteConsumer,
152
+ GetConsumer: vi.fn().mockResolvedValue(mockConsumer),
153
+ } as unknown as ConsumerService,
154
+ subscriptionService: emptySubscriptionService,
155
+ authService: emptyAuthService,
156
+ });
157
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
158
+ const program = createProgram();
159
+ await program.parseAsync(["node", "getrouter", "keys", "delete", "c1"]);
160
+ expect(deleteConsumer).not.toHaveBeenCalled();
161
+ log.mockRestore();
162
+ });
163
+ });
@@ -0,0 +1,63 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { createProgram } from "../../src/cli";
3
+ import { createApiClients } from "../../src/core/api/client";
4
+ import type { ModelService } from "../../src/generated/router/dashboard/v1";
5
+
6
+ vi.mock("../../src/core/api/client", () => ({
7
+ createApiClients: vi.fn(),
8
+ }));
9
+
10
+ const originalIsTTY = process.stdin.isTTY;
11
+ const setStdinTTY = (value: boolean) => {
12
+ Object.defineProperty(process.stdin, "isTTY", {
13
+ value,
14
+ configurable: true,
15
+ });
16
+ };
17
+
18
+ const mockModel = {
19
+ id: "gpt-5",
20
+ name: "GPT-5",
21
+ author: "OpenAI",
22
+ enabled: true,
23
+ updatedAt: "2026-01-01T00:00:00Z",
24
+ };
25
+
26
+ afterEach(() => {
27
+ setStdinTTY(originalIsTTY);
28
+ });
29
+
30
+ describe("models command", () => {
31
+ it("lists models with list subcommand", async () => {
32
+ setStdinTTY(false);
33
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
34
+ modelService: {
35
+ ListModels: vi.fn().mockResolvedValue({ models: [mockModel] }),
36
+ } as unknown as ModelService,
37
+ });
38
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
39
+ const program = createProgram();
40
+ await program.parseAsync(["node", "getrouter", "models", "list"]);
41
+ const output = log.mock.calls.map((call) => call[0]).join("\n");
42
+ expect(output).toContain("NAME");
43
+ expect(output).not.toContain("ID");
44
+ log.mockRestore();
45
+ });
46
+
47
+ it("lists models when no subcommand", async () => {
48
+ setStdinTTY(true);
49
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
50
+ modelService: {
51
+ ListModels: vi.fn().mockResolvedValue({ models: [mockModel] }),
52
+ } as unknown as ModelService,
53
+ });
54
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
55
+ const program = createProgram();
56
+ await program.parseAsync(["node", "getrouter", "models"]);
57
+ const output = log.mock.calls.map((call) => call[0]).join("\n");
58
+ expect(output).toContain("NAME");
59
+ expect(output).not.toContain("ID");
60
+ expect(output).toContain("GPT-5");
61
+ log.mockRestore();
62
+ });
63
+ });
@@ -0,0 +1,82 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { createProgram } from "../../src/cli";
6
+ import { createApiClients } from "../../src/core/api/client";
7
+ import { writeAuth } from "../../src/core/config";
8
+ import type {
9
+ AuthService,
10
+ ConsumerService,
11
+ SubscriptionService,
12
+ } from "../../src/generated/router/dashboard/v1";
13
+
14
+ vi.mock("../../src/core/api/client", () => ({
15
+ createApiClients: vi.fn(),
16
+ }));
17
+
18
+ const makeDir = () => fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
19
+
20
+ describe("status command", () => {
21
+ it("prints logged out status with no subscription", async () => {
22
+ const dir = makeDir();
23
+ process.env.GETROUTER_CONFIG_DIR = dir;
24
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
25
+ subscriptionService: {
26
+ CurrentSubscription: vi.fn().mockResolvedValue(null),
27
+ } as SubscriptionService,
28
+ authService: {} as AuthService,
29
+ consumerService: {} as unknown as ConsumerService,
30
+ });
31
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
32
+ const program = createProgram();
33
+ await program.parseAsync(["node", "getrouter", "status"]);
34
+ const output = log.mock.calls.map((c) => c[0]).join("\n");
35
+ expect(output).toContain("🔐 Auth");
36
+ expect(output).toContain(" Status : ❌ Logged out");
37
+ expect(output).toContain("\n\n📦 Subscription");
38
+ expect(output).toContain(" Status : No active subscription");
39
+ log.mockRestore();
40
+ });
41
+
42
+ it("prints logged in status with subscription info", async () => {
43
+ const dir = makeDir();
44
+ process.env.GETROUTER_CONFIG_DIR = dir;
45
+ writeAuth({
46
+ accessToken: "token",
47
+ refreshToken: "refresh",
48
+ expiresAt: "2026-02-02T14:39:49Z",
49
+ tokenType: "Bearer",
50
+ });
51
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
52
+ subscriptionService: {
53
+ CurrentSubscription: vi.fn().mockResolvedValue({
54
+ status: "ACTIVE",
55
+ plan: { name: "Pro", requestPerMinute: 20, tokenPerMinute: "150K" },
56
+ startAt: "2026-01-01T00:00:00Z",
57
+ endAt: "2026-02-01T00:00:00Z",
58
+ }),
59
+ } as SubscriptionService,
60
+ authService: {} as AuthService,
61
+ consumerService: {} as unknown as ConsumerService,
62
+ });
63
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
64
+ const program = createProgram();
65
+ await program.parseAsync(["node", "getrouter", "status"]);
66
+ const output = log.mock.calls.map((c) => c[0]).join("\n");
67
+ expect(output).toContain("🔐 Auth");
68
+ expect(output).toContain(" Status : ✅ Logged in");
69
+ expect(output).toContain(" Expires : 2026-02-02T14:39:49Z");
70
+ expect(output).toContain(" TokenType : Bearer");
71
+ expect(output).toContain(" Access : token");
72
+ expect(output).toContain(" Refresh : refresh");
73
+ expect(output).toContain("\n\n📦 Subscription");
74
+ expect(output).toContain(" Plan : Pro");
75
+ expect(output).toContain(" Status : ACTIVE");
76
+ expect(output).toContain(
77
+ " Window : 2026-01-01T00:00:00Z → 2026-02-01T00:00:00Z",
78
+ );
79
+ expect(output).toContain(" Limits : 20 req/min · 150K tok/min");
80
+ log.mockRestore();
81
+ });
82
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createProgram } from "../../src/cli";
3
+ import { createApiClients } from "../../src/core/api/client";
4
+
5
+ vi.mock("../../src/core/api/client", () => ({
6
+ createApiClients: vi.fn(),
7
+ }));
8
+
9
+ describe("usages command", () => {
10
+ it("prints chart and table", async () => {
11
+ const listUsage = vi.fn().mockResolvedValue({
12
+ usages: [
13
+ {
14
+ createdAt: "2026-01-03T00:00:00Z",
15
+ inputTokens: 1000,
16
+ outputTokens: 1000,
17
+ totalTokens: 2000,
18
+ },
19
+ ],
20
+ });
21
+ (createApiClients as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
22
+ usageService: { ListUsage: listUsage },
23
+ });
24
+ const log = vi.spyOn(console, "log").mockImplementation(() => {});
25
+ const program = createProgram();
26
+ await program.parseAsync(["node", "getrouter", "usages"]);
27
+ expect(listUsage).toHaveBeenCalledTimes(1);
28
+ expect(listUsage).toHaveBeenCalledWith({
29
+ pageSize: 7,
30
+ pageToken: undefined,
31
+ });
32
+ expect(log).toHaveBeenCalledTimes(1);
33
+ const output = String(log.mock.calls[0][0] ?? "");
34
+ expect(output).toContain("📊 Usage (last 7 days)");
35
+ expect(output).toContain("I:1K");
36
+ expect(output).toContain("O:1K");
37
+ expect(output).toContain("Legend:");
38
+ expect(output).not.toContain("DAY");
39
+ expect(output).not.toContain("O:0179");
40
+ log.mockRestore();
41
+ });
42
+ });
@@ -0,0 +1,14 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { readJsonFile, writeJsonFile } from "../../src/core/config/fs";
6
+
7
+ describe("config fs", () => {
8
+ it("writes and reads JSON", () => {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
10
+ const file = path.join(dir, "config.json");
11
+ writeJsonFile(file, { hello: "world" });
12
+ expect(readJsonFile(file)).toEqual({ hello: "world" });
13
+ });
14
+ });
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ readAuth,
7
+ readConfig,
8
+ writeAuth,
9
+ writeConfig,
10
+ } from "../../src/core/config";
11
+
12
+ describe("config read/write", () => {
13
+ it("writes and reads config with defaults", () => {
14
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
15
+ process.env.GETROUTER_CONFIG_DIR = dir;
16
+ writeConfig({ apiBase: "https://getrouter.dev", json: true });
17
+ const cfg = readConfig();
18
+ expect(cfg.apiBase).toBe("https://getrouter.dev");
19
+ expect(cfg.json).toBe(true);
20
+ });
21
+
22
+ it("writes and reads auth state", () => {
23
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
24
+ process.env.GETROUTER_CONFIG_DIR = dir;
25
+ writeAuth({
26
+ accessToken: "a",
27
+ refreshToken: "b",
28
+ expiresAt: "c",
29
+ tokenType: "Bearer",
30
+ });
31
+ const auth = readAuth();
32
+ expect(auth.accessToken).toBe("a");
33
+ expect(auth.refreshToken).toBe("b");
34
+ expect(auth.expiresAt).toBe("c");
35
+ });
36
+
37
+ it("defaults tokenType to Bearer", () => {
38
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
39
+ process.env.GETROUTER_CONFIG_DIR = dir;
40
+ writeAuth({
41
+ accessToken: "a",
42
+ refreshToken: "b",
43
+ expiresAt: "c",
44
+ tokenType: "Bearer",
45
+ });
46
+ const auth = readAuth();
47
+ expect(auth.tokenType).toBe("Bearer");
48
+ });
49
+
50
+ it("writes auth file with 0600 on unix", () => {
51
+ if (process.platform === "win32") return;
52
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
53
+ process.env.GETROUTER_CONFIG_DIR = dir;
54
+ writeAuth({
55
+ accessToken: "a",
56
+ refreshToken: "b",
57
+ expiresAt: "c",
58
+ tokenType: "Bearer",
59
+ });
60
+ const mode = fs.statSync(path.join(dir, "auth.json")).mode & 0o777;
61
+ expect(mode).toBe(0o600);
62
+ });
63
+ });
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getAuthPath, getConfigPath } from "../../src/core/config/paths";
3
+
4
+ describe("config paths", () => {
5
+ it("returns ~/.getrouter paths", () => {
6
+ expect(getConfigPath()).toContain(".getrouter");
7
+ expect(getConfigPath()).toContain("config.json");
8
+ expect(getAuthPath()).toContain("auth.json");
9
+ });
10
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { redactSecrets } from "../../src/core/config/redact";
3
+
4
+ describe("redact", () => {
5
+ it("redacts known secret fields", () => {
6
+ const out = redactSecrets({
7
+ accessToken: "secretcret",
8
+ refreshToken: "secretret2",
9
+ apiKey: "key",
10
+ other: "ok",
11
+ });
12
+ expect(out.accessToken).toBe("secr...cret");
13
+ expect(out.refreshToken).toBe("secr...ret2");
14
+ expect(out.apiKey).toBe("****");
15
+ expect(out.other).toBe("ok");
16
+ });
17
+ });
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { defaultAuthState, defaultConfig } from "../../src/core/config/types";
3
+
4
+ describe("config types defaults", () => {
5
+ it("provides sane defaults", () => {
6
+ expect(defaultConfig().apiBase).toBe("https://getrouter.dev");
7
+ expect(defaultConfig().json).toBe(false);
8
+ expect(defaultAuthState().accessToken).toBe("");
9
+ });
10
+ });
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ type ClientFactories,
4
+ createApiClients,
5
+ type RequestHandler,
6
+ } from "../../../src/core/api/client";
7
+ import type {
8
+ AuthService,
9
+ ConsumerService,
10
+ ModelService,
11
+ SubscriptionService,
12
+ UsageService,
13
+ } from "../../../src/generated/router/dashboard/v1";
14
+
15
+ const makeFetch = () =>
16
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
17
+ const url = typeof input === "string" ? input : input.toString();
18
+ return {
19
+ ok: true,
20
+ status: 200,
21
+ statusText: "OK",
22
+ json: async () => ({ url, init }),
23
+ } as Response;
24
+ }) as unknown as typeof fetch;
25
+
26
+ describe("api client adapter", () => {
27
+ it("uses requestJson with generated paths", async () => {
28
+ const fetchImpl = makeFetch();
29
+ const fakeClients: ClientFactories = {
30
+ createConsumerServiceClient: (handler: RequestHandler) =>
31
+ ({
32
+ ListConsumers: () =>
33
+ handler(
34
+ { path: "v1/dashboard/consumers", method: "GET", body: null },
35
+ { service: "ConsumerService", method: "ListConsumers" },
36
+ ),
37
+ }) as unknown as ConsumerService,
38
+ createAuthServiceClient: (_handler: RequestHandler) =>
39
+ ({}) as AuthService,
40
+ createSubscriptionServiceClient: (_handler: RequestHandler) =>
41
+ ({}) as SubscriptionService,
42
+ createUsageServiceClient: (_handler: RequestHandler) =>
43
+ ({}) as UsageService,
44
+ createModelServiceClient: (handler: RequestHandler) =>
45
+ ({
46
+ ListModels: () =>
47
+ handler(
48
+ { path: "v1/dashboard/models", method: "GET", body: null },
49
+ { service: "ModelService", method: "ListModels" },
50
+ ),
51
+ }) as unknown as ModelService,
52
+ };
53
+ const { consumerService, modelService } = createApiClients({
54
+ fetchImpl,
55
+ clients: fakeClients,
56
+ });
57
+ const res = await consumerService.ListConsumers({
58
+ pageSize: 0,
59
+ pageToken: "",
60
+ });
61
+ const payload = res as unknown as { url: string; init: RequestInit };
62
+ expect(payload.url).toContain("/v1/dashboard/consumers");
63
+ expect(payload.init.method).toBe("GET");
64
+ const modelsRes = await modelService.ListModels({
65
+ pageSize: 0,
66
+ pageToken: "",
67
+ filter: "",
68
+ });
69
+ const modelsPayload = modelsRes as unknown as {
70
+ url: string;
71
+ init: RequestInit;
72
+ };
73
+ expect(modelsPayload.url).toContain("/v1/dashboard/models");
74
+ });
75
+
76
+ it("loads generated clients by default", async () => {
77
+ const fetchImpl = makeFetch();
78
+ const { consumerService } = createApiClients({ fetchImpl });
79
+ const res = await consumerService.ListConsumers({
80
+ pageSize: 0,
81
+ pageToken: "",
82
+ });
83
+ const payload = res as unknown as { url: string; init: RequestInit };
84
+ expect(payload.url).toContain("/v1/dashboard/consumers");
85
+ expect(payload.init.method).toBe("GET");
86
+ });
87
+
88
+ it("exposes usage service", () => {
89
+ const clients = createApiClients({});
90
+ expect("usageService" in clients).toBe(true);
91
+ });
92
+ });
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ copyToClipboard,
4
+ getClipboardCommands,
5
+ } from "../../../src/core/interactive/clipboard";
6
+
7
+ const makeSpawn = () =>
8
+ vi.fn(() => {
9
+ const handlers: Record<string, Array<(code?: number) => void>> = {};
10
+ const child = {
11
+ stdin: { write: vi.fn(), end: vi.fn() },
12
+ on: (event: string, cb: (code?: number) => void) => {
13
+ handlers[event] = handlers[event] ?? [];
14
+ handlers[event].push(cb);
15
+ return child;
16
+ },
17
+ };
18
+ queueMicrotask(() => {
19
+ handlers.close?.forEach((cb) => {
20
+ cb(0);
21
+ });
22
+ });
23
+ return child;
24
+ });
25
+
26
+ describe("getClipboardCommands", () => {
27
+ it("returns pbcopy on darwin", () => {
28
+ expect(getClipboardCommands("darwin")[0]?.command).toBe("pbcopy");
29
+ });
30
+ });
31
+
32
+ describe("copyToClipboard", () => {
33
+ it("writes to clipboard with provided spawn", async () => {
34
+ const spawnFn = makeSpawn();
35
+ const ok = await copyToClipboard("hello", {
36
+ platform: "darwin",
37
+ spawnFn,
38
+ });
39
+ expect(ok).toBe(true);
40
+ expect(spawnFn).toHaveBeenCalledWith("pbcopy", [], {
41
+ stdio: ["pipe", "ignore", "ignore"],
42
+ });
43
+ });
44
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ MODEL_CHOICES,
4
+ mapReasoningValue,
5
+ REASONING_CHOICES,
6
+ } from "../../../src/core/interactive/codex";
7
+
8
+ describe("codex interactive helpers", () => {
9
+ it("maps extra high to xhigh", () => {
10
+ expect(mapReasoningValue("extra_high")).toBe("xhigh");
11
+ });
12
+
13
+ it("exports model and reasoning choices", () => {
14
+ expect(MODEL_CHOICES.length).toBeGreaterThan(0);
15
+ expect(REASONING_CHOICES.length).toBeGreaterThan(0);
16
+ });
17
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { rankFuzzyChoices } from "../../../src/core/interactive/fuzzy";
3
+
4
+ const choices = [
5
+ { title: "gpt-5", value: "gpt-5", keywords: ["openai"] },
6
+ { title: "claude-3", value: "claude-3", keywords: ["anthropic"] },
7
+ { title: "gpt-4o", value: "gpt-4o", keywords: ["openai"] },
8
+ ];
9
+
10
+ describe("rankFuzzyChoices", () => {
11
+ it("ranks by fuzzy match and uses keywords", () => {
12
+ const ranked = rankFuzzyChoices(choices, "open");
13
+ expect(ranked[0]?.value).toBe("gpt-5");
14
+ expect(ranked[1]?.value).toBe("gpt-4o");
15
+ });
16
+
17
+ it("returns all when query is empty", () => {
18
+ const ranked = rankFuzzyChoices(choices, "");
19
+ expect(ranked.map((choice) => choice.value)).toEqual([
20
+ "gpt-5",
21
+ "claude-3",
22
+ "gpt-4o",
23
+ ]);
24
+ });
25
+
26
+ it("limits results", () => {
27
+ const ranked = rankFuzzyChoices(choices, "g", 2);
28
+ expect(ranked.length).toBe(2);
29
+ });
30
+ });