@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,38 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { mergeAuthJson, mergeCodexToml } from "../../../src/core/setup/codex";
|
|
3
|
+
|
|
4
|
+
describe("codex setup helpers", () => {
|
|
5
|
+
it("merges codex toml at root and provider table", () => {
|
|
6
|
+
const input = [
|
|
7
|
+
'other = "keep"',
|
|
8
|
+
'model = "old-model"',
|
|
9
|
+
"",
|
|
10
|
+
"[model_providers.other]",
|
|
11
|
+
'name = "x"',
|
|
12
|
+
"",
|
|
13
|
+
"[model_providers.getrouter]",
|
|
14
|
+
'name = "old"',
|
|
15
|
+
'extra = "keep"',
|
|
16
|
+
"",
|
|
17
|
+
].join("\n");
|
|
18
|
+
const output = mergeCodexToml(input, {
|
|
19
|
+
model: "gpt-5.2-codex",
|
|
20
|
+
reasoning: "xhigh",
|
|
21
|
+
});
|
|
22
|
+
expect(output).toContain('model = "gpt-5.2-codex"');
|
|
23
|
+
expect(output).toContain('model_reasoning_effort = "xhigh"');
|
|
24
|
+
expect(output).toContain('model_provider = "getrouter"');
|
|
25
|
+
expect(output).toContain("[model_providers.getrouter]");
|
|
26
|
+
expect(output).toContain('base_url = "https://api.getrouter.dev/codex"');
|
|
27
|
+
expect(output).toContain('wire_api = "responses"');
|
|
28
|
+
expect(output).toContain("requires_openai_auth = true");
|
|
29
|
+
expect(output).toContain('other = "keep"');
|
|
30
|
+
expect(output).toContain('extra = "keep"');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("merges auth json", () => {
|
|
34
|
+
const output = mergeAuthJson({ existing: "keep" }, "key-123");
|
|
35
|
+
expect(output.OPENAI_API_KEY).toBe("key-123");
|
|
36
|
+
expect(output.existing).toBe("keep");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
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
|
+
appendRcIfMissing,
|
|
7
|
+
getEnvFilePath,
|
|
8
|
+
getHookFilePath,
|
|
9
|
+
renderEnv,
|
|
10
|
+
renderHook,
|
|
11
|
+
resolveShellRcPath,
|
|
12
|
+
writeEnvFile,
|
|
13
|
+
} from "../../../src/core/setup/env";
|
|
14
|
+
|
|
15
|
+
const vars = {
|
|
16
|
+
openaiBaseUrl: "https://api.getrouter.dev/codex",
|
|
17
|
+
openaiApiKey: "key-123",
|
|
18
|
+
anthropicBaseUrl: "https://api.getrouter.dev/claude",
|
|
19
|
+
anthropicApiKey: "key-123",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe("setup env helpers", () => {
|
|
23
|
+
it("renders sh env", () => {
|
|
24
|
+
const output = renderEnv("sh", vars);
|
|
25
|
+
expect(output).toContain(
|
|
26
|
+
"export OPENAI_BASE_URL=https://api.getrouter.dev/codex",
|
|
27
|
+
);
|
|
28
|
+
expect(output).toContain("export ANTHROPIC_API_KEY=key-123");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders ps1 env", () => {
|
|
32
|
+
const output = renderEnv("ps1", vars);
|
|
33
|
+
expect(output).toContain(
|
|
34
|
+
'$env:OPENAI_BASE_URL="https://api.getrouter.dev/codex"',
|
|
35
|
+
);
|
|
36
|
+
expect(output).toContain('$env:ANTHROPIC_API_KEY="key-123"');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("writes env file", () => {
|
|
40
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-env-"));
|
|
41
|
+
const filePath = getEnvFilePath("sh", dir);
|
|
42
|
+
writeEnvFile(filePath, "hello");
|
|
43
|
+
expect(fs.readFileSync(filePath, "utf8")).toBe("hello");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders sh hook", () => {
|
|
47
|
+
const output = renderHook("bash");
|
|
48
|
+
expect(output).toContain("getrouter() {");
|
|
49
|
+
expect(output).toContain("command getrouter");
|
|
50
|
+
expect(output).toContain("source");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("renders pwsh hook", () => {
|
|
54
|
+
const output = renderHook("pwsh");
|
|
55
|
+
expect(output).toContain("function getrouter");
|
|
56
|
+
expect(output).toContain("$LASTEXITCODE");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("resolves shell rc paths", () => {
|
|
60
|
+
expect(resolveShellRcPath("zsh", "/tmp")).toBe("/tmp/.zshrc");
|
|
61
|
+
expect(resolveShellRcPath("bash", "/tmp")).toBe("/tmp/.bashrc");
|
|
62
|
+
expect(resolveShellRcPath("fish", "/tmp")).toBe(
|
|
63
|
+
"/tmp/.config/fish/config.fish",
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("resolves hook file paths", () => {
|
|
68
|
+
expect(getHookFilePath("bash", "/tmp")).toBe("/tmp/hook.sh");
|
|
69
|
+
expect(getHookFilePath("zsh", "/tmp")).toBe("/tmp/hook.sh");
|
|
70
|
+
expect(getHookFilePath("fish", "/tmp")).toBe("/tmp/hook.fish");
|
|
71
|
+
expect(getHookFilePath("pwsh", "/tmp")).toBe("/tmp/hook.ps1");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("appends rc line once", () => {
|
|
75
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-rc-"));
|
|
76
|
+
const rcPath = path.join(dir, "rc");
|
|
77
|
+
const line = "source ~/.getrouter/env.sh";
|
|
78
|
+
fs.writeFileSync(rcPath, `${line}\n`);
|
|
79
|
+
expect(appendRcIfMissing(rcPath, line)).toBe(false);
|
|
80
|
+
expect(appendRcIfMissing(rcPath, line)).toBe(false);
|
|
81
|
+
const content = fs.readFileSync(rcPath, "utf8");
|
|
82
|
+
expect(content.split(line).length - 1).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { aggregateUsages } from "../../../src/core/usages/aggregate";
|
|
3
|
+
|
|
4
|
+
describe("aggregateUsages", () => {
|
|
5
|
+
it("groups by local day and totals tokens", () => {
|
|
6
|
+
const rows = [
|
|
7
|
+
{
|
|
8
|
+
createdAt: "2026-01-03T10:00:00",
|
|
9
|
+
inputTokens: 5,
|
|
10
|
+
outputTokens: 7,
|
|
11
|
+
totalTokens: 12,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
createdAt: "2026-01-03T18:00:00",
|
|
15
|
+
inputTokens: 3,
|
|
16
|
+
outputTokens: 2,
|
|
17
|
+
totalTokens: 5,
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
const result = aggregateUsages(rows, 7);
|
|
21
|
+
expect(result).toHaveLength(1);
|
|
22
|
+
expect(result[0].totalTokens).toBe(17);
|
|
23
|
+
expect(result[0].inputTokens).toBe(8);
|
|
24
|
+
expect(result[0].outputTokens).toBe(9);
|
|
25
|
+
expect(result[0].requests).toBe(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("limits results to the most recent days", () => {
|
|
29
|
+
const rows = [
|
|
30
|
+
{ createdAt: "2026-01-05T00:00:00Z", totalTokens: 1 },
|
|
31
|
+
{ createdAt: "2026-01-04T00:00:00Z", totalTokens: 1 },
|
|
32
|
+
{ createdAt: "2026-01-03T00:00:00Z", totalTokens: 1 },
|
|
33
|
+
];
|
|
34
|
+
const result = aggregateUsages(rows, 2);
|
|
35
|
+
expect(result).toHaveLength(2);
|
|
36
|
+
expect(result[0].day).toBe("2026-01-05");
|
|
37
|
+
expect(result[1].day).toBe("2026-01-04");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("coerces string token values to numbers", () => {
|
|
41
|
+
const rows = [
|
|
42
|
+
{
|
|
43
|
+
createdAt: "2026-01-03T10:00:00Z",
|
|
44
|
+
inputTokens: "0123",
|
|
45
|
+
outputTokens: "045",
|
|
46
|
+
totalTokens: "0168",
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
const result = aggregateUsages(rows, 7);
|
|
50
|
+
expect(result).toHaveLength(1);
|
|
51
|
+
expect(result[0].inputTokens).toBe(123);
|
|
52
|
+
expect(result[0].outputTokens).toBe(45);
|
|
53
|
+
expect(result[0].totalTokens).toBe(168);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createApiError } from "../../src/core/http/errors";
|
|
3
|
+
|
|
4
|
+
describe("api errors", () => {
|
|
5
|
+
it("normalizes error payload", () => {
|
|
6
|
+
const err = createApiError(
|
|
7
|
+
{ code: "BAD", message: "oops" },
|
|
8
|
+
"fallback",
|
|
9
|
+
400,
|
|
10
|
+
);
|
|
11
|
+
expect(err.message).toBe("oops");
|
|
12
|
+
expect(err.code).toBe("BAD");
|
|
13
|
+
expect(err.status).toBe(400);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { requestJson } from "../../src/core/http/request";
|
|
6
|
+
|
|
7
|
+
const originalCookieName = process.env.GETROUTER_AUTH_COOKIE;
|
|
8
|
+
const originalKratosCookie = process.env.KRATOS_AUTH_COOKIE;
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
if (originalCookieName === undefined) {
|
|
12
|
+
delete process.env.GETROUTER_AUTH_COOKIE;
|
|
13
|
+
} else {
|
|
14
|
+
process.env.GETROUTER_AUTH_COOKIE = originalCookieName;
|
|
15
|
+
}
|
|
16
|
+
if (originalKratosCookie === undefined) {
|
|
17
|
+
delete process.env.KRATOS_AUTH_COOKIE;
|
|
18
|
+
} else {
|
|
19
|
+
process.env.KRATOS_AUTH_COOKIE = originalKratosCookie;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("requestJson", () => {
|
|
24
|
+
it("adds Authorization when token exists", async () => {
|
|
25
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
26
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
path.join(dir, "auth.json"),
|
|
29
|
+
JSON.stringify({ accessToken: "t" }),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const fetchSpy = vi.fn(
|
|
33
|
+
async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
34
|
+
({
|
|
35
|
+
ok: true,
|
|
36
|
+
json: async () => ({ ok: true }),
|
|
37
|
+
}) as Response,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const res = await requestJson<{ ok: boolean }>({
|
|
41
|
+
path: "/v1/test",
|
|
42
|
+
method: "GET",
|
|
43
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(res.ok).toBe(true);
|
|
47
|
+
const call = fetchSpy.mock.calls[0] as Parameters<typeof fetch> | undefined;
|
|
48
|
+
const init = call?.[1];
|
|
49
|
+
const headers = (init?.headers ?? {}) as Record<string, string>;
|
|
50
|
+
expect(headers.Authorization).toBe("Bearer t");
|
|
51
|
+
expect(headers.Cookie).toBe("access_token=t");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("uses GETROUTER_AUTH_COOKIE when set", async () => {
|
|
55
|
+
process.env.GETROUTER_AUTH_COOKIE = "router_auth";
|
|
56
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
57
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
58
|
+
fs.writeFileSync(
|
|
59
|
+
path.join(dir, "auth.json"),
|
|
60
|
+
JSON.stringify({ accessToken: "t2" }),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const fetchSpy = vi.fn(
|
|
64
|
+
async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
65
|
+
({
|
|
66
|
+
ok: true,
|
|
67
|
+
json: async () => ({ ok: true }),
|
|
68
|
+
}) as Response,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await requestJson({
|
|
72
|
+
path: "/v1/test",
|
|
73
|
+
method: "GET",
|
|
74
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const call = fetchSpy.mock.calls[0] as Parameters<typeof fetch> | undefined;
|
|
78
|
+
const init = call?.[1];
|
|
79
|
+
const headers = (init?.headers ?? {}) as Record<string, string>;
|
|
80
|
+
expect(headers.Cookie).toBe("router_auth=t2");
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
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 { buildApiUrl } from "../../src/core/http/url";
|
|
6
|
+
|
|
7
|
+
describe("buildApiUrl", () => {
|
|
8
|
+
it("joins base and path safely", () => {
|
|
9
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
10
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
11
|
+
fs.writeFileSync(
|
|
12
|
+
path.join(dir, "config.json"),
|
|
13
|
+
JSON.stringify({ apiBase: "https://getrouter.dev/" }),
|
|
14
|
+
);
|
|
15
|
+
expect(buildApiUrl("/v1/test")).toBe("https://getrouter.dev/v1/test");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderTable } from "../../src/core/output/table";
|
|
3
|
+
|
|
4
|
+
describe("table renderer", () => {
|
|
5
|
+
it("renders headers and rows with alignment", () => {
|
|
6
|
+
const out = renderTable(
|
|
7
|
+
["ID", "NAME"],
|
|
8
|
+
[
|
|
9
|
+
["1", "alpha"],
|
|
10
|
+
["2", "beta"],
|
|
11
|
+
],
|
|
12
|
+
);
|
|
13
|
+
expect(out).toContain("ID");
|
|
14
|
+
expect(out).toContain("NAME");
|
|
15
|
+
expect(out).toContain("alpha");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("truncates long cells", () => {
|
|
19
|
+
const out = renderTable(["ID"], [["0123456789ABCDEFGHIJ"]], {
|
|
20
|
+
maxColWidth: 8,
|
|
21
|
+
});
|
|
22
|
+
expect(out).toContain("01234...");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("fills empty with dash", () => {
|
|
26
|
+
const out = renderTable(["ID"], [[""]]);
|
|
27
|
+
expect(out).toContain("-");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderUsageChart } from "../../src/core/output/usages";
|
|
3
|
+
|
|
4
|
+
describe("renderUsageChart", () => {
|
|
5
|
+
it("renders stacked bars", () => {
|
|
6
|
+
const output = renderUsageChart([
|
|
7
|
+
{
|
|
8
|
+
day: "2026-01-03",
|
|
9
|
+
inputTokens: 10,
|
|
10
|
+
outputTokens: 20,
|
|
11
|
+
totalTokens: 30,
|
|
12
|
+
requests: 2,
|
|
13
|
+
},
|
|
14
|
+
]);
|
|
15
|
+
expect(output).toContain("2026-01-03");
|
|
16
|
+
expect(output).toMatch(/โ/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("uses an emoji header and token-friendly numbers", () => {
|
|
20
|
+
const output = renderUsageChart([
|
|
21
|
+
{
|
|
22
|
+
day: "2026-01-03",
|
|
23
|
+
inputTokens: 1000,
|
|
24
|
+
outputTokens: 1000,
|
|
25
|
+
totalTokens: 2000,
|
|
26
|
+
requests: 2,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
expect(output.startsWith("๐ Usage (last 7 days)")).toBe(true);
|
|
30
|
+
expect(output).toContain("Tokens");
|
|
31
|
+
expect(output).toContain("โ");
|
|
32
|
+
expect(output).toContain("โ");
|
|
33
|
+
expect(output).toContain("I:1K");
|
|
34
|
+
expect(output).toContain("O:1K");
|
|
35
|
+
expect(output).toContain("๐ Usage (last 7 days) ยท Tokens\n\n2026-01-03");
|
|
36
|
+
expect(output).toContain("O:1K\n\nLegend: โ input โ output");
|
|
37
|
+
expect(output).toContain("Legend: โ input โ output");
|
|
38
|
+
expect(output).not.toContain("O:0179");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("prints input and output totals separately", () => {
|
|
42
|
+
const output = renderUsageChart([
|
|
43
|
+
{
|
|
44
|
+
day: "2026-01-03",
|
|
45
|
+
inputTokens: 1200,
|
|
46
|
+
outputTokens: 3400,
|
|
47
|
+
totalTokens: 4600,
|
|
48
|
+
requests: 2,
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
expect(output).toContain("I:1.2K");
|
|
52
|
+
expect(output).toContain("O:3.4K");
|
|
53
|
+
expect(output).not.toContain("4.6K");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("handles numeric strings without skewing bars", () => {
|
|
57
|
+
const output = renderUsageChart(
|
|
58
|
+
[
|
|
59
|
+
{
|
|
60
|
+
day: "2026-01-03",
|
|
61
|
+
inputTokens: "1000",
|
|
62
|
+
outputTokens: "1000",
|
|
63
|
+
totalTokens: "2000",
|
|
64
|
+
requests: 1,
|
|
65
|
+
} as unknown as Parameters<typeof renderUsageChart>[0][number],
|
|
66
|
+
],
|
|
67
|
+
10,
|
|
68
|
+
);
|
|
69
|
+
expect(output).toContain("โโโโโโโโโโ");
|
|
70
|
+
});
|
|
71
|
+
});
|
package/tsconfig.json
ADDED
package/tsdown.config.ts
ADDED