@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
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getrouter/getrouter-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI for getrouter.dev",
|
|
6
|
+
"bin": {
|
|
7
|
+
"getrouter": "dist/bin.mjs"
|
|
8
|
+
},
|
|
9
|
+
"publishConfig": {
|
|
10
|
+
"access": "public"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsdown",
|
|
14
|
+
"dev": "tsx src/bin.ts --help",
|
|
15
|
+
"format": "biome check --write .",
|
|
16
|
+
"lint": "biome check .",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"packageManager": "bun@1.3.5",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"commander": "^12.1.0",
|
|
26
|
+
"prompts": "^2.4.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@biomejs/biome": "^2.3.10",
|
|
30
|
+
"@types/node": "^20.12.12",
|
|
31
|
+
"@types/prompts": "^2.4.9",
|
|
32
|
+
"tsdown": "^0.19.0-beta.2",
|
|
33
|
+
"tsx": "^4.19.2",
|
|
34
|
+
"typescript": "^5.7.3",
|
|
35
|
+
"vitest": "^2.1.8"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/.gitkeep
ADDED
|
File without changes
|
package/src/bin.ts
ADDED
package/src/cli.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { registerCommands } from "./cmd";
|
|
3
|
+
|
|
4
|
+
export const createProgram = () => {
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.name("getrouter")
|
|
8
|
+
.description("CLI for getrouter.dev")
|
|
9
|
+
.version("0.1.0");
|
|
10
|
+
registerCommands(program);
|
|
11
|
+
return program;
|
|
12
|
+
};
|
package/src/cmd/auth.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { createApiClients } from "../core/api/client";
|
|
3
|
+
import { clearAuth } from "../core/auth";
|
|
4
|
+
import {
|
|
5
|
+
buildLoginUrl,
|
|
6
|
+
generateAuthCode,
|
|
7
|
+
openLoginUrl,
|
|
8
|
+
pollAuthorize,
|
|
9
|
+
} from "../core/auth/device";
|
|
10
|
+
import { writeAuth } from "../core/config";
|
|
11
|
+
|
|
12
|
+
export const registerAuthCommands = (program: Command) => {
|
|
13
|
+
program
|
|
14
|
+
.command("login")
|
|
15
|
+
.description("Login with device flow")
|
|
16
|
+
.action(async () => {
|
|
17
|
+
const { authService } = createApiClients({});
|
|
18
|
+
const authCode = generateAuthCode();
|
|
19
|
+
const url = buildLoginUrl(authCode);
|
|
20
|
+
console.log("🔐 To authenticate, visit:");
|
|
21
|
+
console.log(url);
|
|
22
|
+
console.log("⏳ Waiting for confirmation...");
|
|
23
|
+
void openLoginUrl(url);
|
|
24
|
+
const token = await pollAuthorize({
|
|
25
|
+
authorize: authService.Authorize.bind(authService),
|
|
26
|
+
code: authCode,
|
|
27
|
+
});
|
|
28
|
+
writeAuth({
|
|
29
|
+
accessToken: token.accessToken ?? "",
|
|
30
|
+
refreshToken: token.refreshToken ?? "",
|
|
31
|
+
expiresAt: token.expiresAt ?? "",
|
|
32
|
+
tokenType: "Bearer",
|
|
33
|
+
});
|
|
34
|
+
console.log("✅ Login successful.");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command("logout")
|
|
39
|
+
.description("Clear local auth state")
|
|
40
|
+
.action(() => {
|
|
41
|
+
clearAuth();
|
|
42
|
+
console.log("Cleared local auth data.");
|
|
43
|
+
});
|
|
44
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { buildAnthropicEnv, registerEnvCommand } from "./env";
|
|
3
|
+
|
|
4
|
+
export const registerClaudeCommand = (program: Command) => {
|
|
5
|
+
registerEnvCommand(program, {
|
|
6
|
+
name: "claude",
|
|
7
|
+
description: "Configure Claude environment",
|
|
8
|
+
vars: buildAnthropicEnv,
|
|
9
|
+
});
|
|
10
|
+
};
|
package/src/cmd/codex.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { Command } from "commander";
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
import { createApiClients } from "../core/api/client";
|
|
7
|
+
import {
|
|
8
|
+
MODEL_CHOICES,
|
|
9
|
+
mapReasoningValue,
|
|
10
|
+
REASONING_CHOICES,
|
|
11
|
+
REASONING_FUZZY_CHOICES,
|
|
12
|
+
} from "../core/interactive/codex";
|
|
13
|
+
import { fuzzySelect } from "../core/interactive/fuzzy";
|
|
14
|
+
import { selectConsumer } from "../core/interactive/keys";
|
|
15
|
+
import { mergeAuthJson, mergeCodexToml } from "../core/setup/codex";
|
|
16
|
+
|
|
17
|
+
const CODEX_DIR = ".codex";
|
|
18
|
+
|
|
19
|
+
const readFileIfExists = (filePath: string) =>
|
|
20
|
+
fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
21
|
+
|
|
22
|
+
const readAuthJson = (filePath: string) => {
|
|
23
|
+
if (!fs.existsSync(filePath)) return {};
|
|
24
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
25
|
+
if (!raw) return {};
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
28
|
+
throw new Error("Invalid auth.json format.");
|
|
29
|
+
}
|
|
30
|
+
return parsed as Record<string, unknown>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ensureCodexDir = () => {
|
|
34
|
+
const dir = path.join(os.homedir(), CODEX_DIR);
|
|
35
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
36
|
+
return dir;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const requireInteractive = () => {
|
|
40
|
+
if (!process.stdin.isTTY) {
|
|
41
|
+
throw new Error("Interactive mode required for codex configuration.");
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const promptModel = async () =>
|
|
46
|
+
await fuzzySelect({
|
|
47
|
+
message:
|
|
48
|
+
"Select Model and Effort\nAccess legacy models by running codex -m <model_name> or in your config.toml",
|
|
49
|
+
choices: MODEL_CHOICES,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const promptReasoning = async (model: string) =>
|
|
53
|
+
await fuzzySelect({
|
|
54
|
+
message: `Select Reasoning Level for ${model}`,
|
|
55
|
+
choices: REASONING_FUZZY_CHOICES,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const formatReasoningLabel = (id: string) =>
|
|
59
|
+
REASONING_CHOICES.find((choice) => choice.id === id)?.label ?? id;
|
|
60
|
+
|
|
61
|
+
export const registerCodexCommand = (program: Command) => {
|
|
62
|
+
program
|
|
63
|
+
.command("codex")
|
|
64
|
+
.description("Configure Codex")
|
|
65
|
+
.action(async () => {
|
|
66
|
+
requireInteractive();
|
|
67
|
+
const model = await promptModel();
|
|
68
|
+
if (!model) return;
|
|
69
|
+
const reasoningId = await promptReasoning(model);
|
|
70
|
+
if (!reasoningId) return;
|
|
71
|
+
const { consumerService } = createApiClients({});
|
|
72
|
+
const selected = await selectConsumer(consumerService);
|
|
73
|
+
if (!selected?.id) return;
|
|
74
|
+
const consumer = await consumerService.GetConsumer({ id: selected.id });
|
|
75
|
+
const apiKey = consumer?.apiKey ?? "";
|
|
76
|
+
if (!apiKey) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"API key not found. Please create one or choose another.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const reasoningValue = mapReasoningValue(reasoningId);
|
|
83
|
+
const keyName = selected.name?.trim() || "(unnamed)";
|
|
84
|
+
const confirm = await prompts({
|
|
85
|
+
type: "confirm",
|
|
86
|
+
name: "confirm",
|
|
87
|
+
message: [
|
|
88
|
+
"Apply Codex configuration?",
|
|
89
|
+
`Model: ${model}`,
|
|
90
|
+
`Reasoning: ${formatReasoningLabel(reasoningId)} (${reasoningValue})`,
|
|
91
|
+
"Provider: getrouter",
|
|
92
|
+
`Key: ${keyName}`,
|
|
93
|
+
].join("\n"),
|
|
94
|
+
initial: true,
|
|
95
|
+
});
|
|
96
|
+
if (!confirm.confirm) return;
|
|
97
|
+
|
|
98
|
+
const codexDir = ensureCodexDir();
|
|
99
|
+
const configPath = path.join(codexDir, "config.toml");
|
|
100
|
+
const authPath = path.join(codexDir, "auth.json");
|
|
101
|
+
|
|
102
|
+
const existingConfig = readFileIfExists(configPath);
|
|
103
|
+
const mergedConfig = mergeCodexToml(existingConfig, {
|
|
104
|
+
model,
|
|
105
|
+
reasoning: reasoningValue,
|
|
106
|
+
});
|
|
107
|
+
fs.writeFileSync(configPath, mergedConfig, "utf8");
|
|
108
|
+
|
|
109
|
+
const existingAuth = readAuthJson(authPath);
|
|
110
|
+
const mergedAuth = mergeAuthJson(existingAuth, apiKey);
|
|
111
|
+
fs.writeFileSync(authPath, JSON.stringify(mergedAuth, null, 2));
|
|
112
|
+
if (process.platform !== "win32") {
|
|
113
|
+
fs.chmodSync(authPath, 0o600);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log("✅ Updated ~/.codex/config.toml");
|
|
117
|
+
console.log("✅ Updated ~/.codex/auth.json");
|
|
118
|
+
});
|
|
119
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const normalizeApiBase = (value: string) =>
|
|
2
|
+
value.trim().replace(/\/+$/, "");
|
|
3
|
+
|
|
4
|
+
export const parseConfigValue = (key: "apiBase" | "json", raw: string) => {
|
|
5
|
+
if (key === "apiBase") {
|
|
6
|
+
const normalized = normalizeApiBase(raw);
|
|
7
|
+
if (!/^https?:\/\//.test(normalized)) {
|
|
8
|
+
throw new Error("apiBase must start with http:// or https://");
|
|
9
|
+
}
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
const lowered = raw.toLowerCase();
|
|
13
|
+
if (["true", "1"].includes(lowered)) return true;
|
|
14
|
+
if (["false", "0"].includes(lowered)) return false;
|
|
15
|
+
throw new Error("json must be true/false or 1/0");
|
|
16
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { readConfig, writeConfig } from "../core/config";
|
|
3
|
+
import { parseConfigValue } from "./config-helpers";
|
|
4
|
+
|
|
5
|
+
const VALID_KEYS = new Set(["apiBase", "json"]);
|
|
6
|
+
|
|
7
|
+
export const registerConfigCommands = (program: Command) => {
|
|
8
|
+
program
|
|
9
|
+
.command("config")
|
|
10
|
+
.description("Manage CLI config")
|
|
11
|
+
.argument("[key]")
|
|
12
|
+
.argument("[value]")
|
|
13
|
+
.action((key: string | undefined, value: string | undefined) => {
|
|
14
|
+
const cfg = readConfig();
|
|
15
|
+
if (!key) {
|
|
16
|
+
console.log(`apiBase=${cfg.apiBase}`);
|
|
17
|
+
console.log(`json=${cfg.json}`);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!value) {
|
|
21
|
+
throw new Error("Missing config value");
|
|
22
|
+
}
|
|
23
|
+
if (!VALID_KEYS.has(key)) {
|
|
24
|
+
throw new Error("Unknown config key");
|
|
25
|
+
}
|
|
26
|
+
const parsed = parseConfigValue(key as "apiBase" | "json", value);
|
|
27
|
+
const next = { ...cfg, [key]: parsed };
|
|
28
|
+
writeConfig(next);
|
|
29
|
+
console.log(`${key}=${(next as Record<string, unknown>)[key]}`);
|
|
30
|
+
});
|
|
31
|
+
};
|
package/src/cmd/env.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { createApiClients } from "../core/api/client";
|
|
4
|
+
import { selectConsumer } from "../core/interactive/keys";
|
|
5
|
+
import {
|
|
6
|
+
appendRcIfMissing,
|
|
7
|
+
applyEnvVars,
|
|
8
|
+
detectShell,
|
|
9
|
+
type EnvVars,
|
|
10
|
+
formatSourceLine,
|
|
11
|
+
getEnvFilePath,
|
|
12
|
+
getHookFilePath,
|
|
13
|
+
renderEnv,
|
|
14
|
+
renderHook,
|
|
15
|
+
resolveConfigDir,
|
|
16
|
+
resolveEnvShell,
|
|
17
|
+
resolveShellRcPath,
|
|
18
|
+
trySourceEnv,
|
|
19
|
+
writeEnvFile,
|
|
20
|
+
} from "../core/setup/env";
|
|
21
|
+
|
|
22
|
+
const CODEX_BASE_URL = "https://api.getrouter.dev/codex";
|
|
23
|
+
const CLAUDE_BASE_URL = "https://api.getrouter.dev/claude";
|
|
24
|
+
|
|
25
|
+
type EnvCommandOptions = {
|
|
26
|
+
install?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type EnvCommandConfig = {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
vars: (apiKey: string) => EnvVars;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const registerEnvCommand = (
|
|
36
|
+
program: Command,
|
|
37
|
+
config: EnvCommandConfig,
|
|
38
|
+
) => {
|
|
39
|
+
program
|
|
40
|
+
.command(config.name)
|
|
41
|
+
.description(config.description)
|
|
42
|
+
.option("--install", "Install into shell rc")
|
|
43
|
+
.action(async (options: EnvCommandOptions) => {
|
|
44
|
+
if (!process.stdin.isTTY) {
|
|
45
|
+
throw new Error("Interactive mode required for key selection.");
|
|
46
|
+
}
|
|
47
|
+
const shell = detectShell();
|
|
48
|
+
const envShell = resolveEnvShell(shell);
|
|
49
|
+
const configDir = resolveConfigDir();
|
|
50
|
+
const { consumerService } = createApiClients({});
|
|
51
|
+
const selected = await selectConsumer(consumerService);
|
|
52
|
+
if (!selected?.id) return;
|
|
53
|
+
const consumer = await consumerService.GetConsumer({ id: selected.id });
|
|
54
|
+
if (!consumer?.apiKey) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"API key not found. Please create one or choose another.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const vars = config.vars(consumer.apiKey);
|
|
61
|
+
const envPath = getEnvFilePath(envShell, configDir);
|
|
62
|
+
writeEnvFile(envPath, renderEnv(envShell, vars));
|
|
63
|
+
|
|
64
|
+
let installed = false;
|
|
65
|
+
let rcPath: string | null = null;
|
|
66
|
+
if (options.install) {
|
|
67
|
+
const hookPath = getHookFilePath(shell, configDir);
|
|
68
|
+
writeEnvFile(hookPath, renderHook(shell));
|
|
69
|
+
rcPath = resolveShellRcPath(shell, os.homedir());
|
|
70
|
+
if (rcPath) {
|
|
71
|
+
const envLine = formatSourceLine(envShell, envPath);
|
|
72
|
+
const hookLine = formatSourceLine(envShell, hookPath);
|
|
73
|
+
const envAdded = appendRcIfMissing(rcPath, envLine);
|
|
74
|
+
const hookAdded = appendRcIfMissing(rcPath, hookLine);
|
|
75
|
+
installed = envAdded || hookAdded;
|
|
76
|
+
}
|
|
77
|
+
applyEnvVars(vars);
|
|
78
|
+
trySourceEnv(shell, envShell, envPath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const sourceLine = formatSourceLine(envShell, envPath);
|
|
82
|
+
if (options.install) {
|
|
83
|
+
if (installed && rcPath) {
|
|
84
|
+
console.log(`✅ Added to ${rcPath}`);
|
|
85
|
+
} else if (rcPath) {
|
|
86
|
+
console.log(`ℹ️ Already configured in ${rcPath}`);
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
console.log("To load the environment in your shell, run:");
|
|
90
|
+
console.log(sourceLine);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const buildOpenAIEnv = (apiKey: string): EnvVars => ({
|
|
96
|
+
openaiBaseUrl: CODEX_BASE_URL,
|
|
97
|
+
openaiApiKey: apiKey,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const buildAnthropicEnv = (apiKey: string): EnvVars => ({
|
|
101
|
+
anthropicBaseUrl: CLAUDE_BASE_URL,
|
|
102
|
+
anthropicApiKey: apiKey,
|
|
103
|
+
});
|
package/src/cmd/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { registerAuthCommands } from "./auth";
|
|
3
|
+
import { registerClaudeCommand } from "./claude";
|
|
4
|
+
import { registerCodexCommand } from "./codex";
|
|
5
|
+
import { registerConfigCommands } from "./config";
|
|
6
|
+
import { registerKeysCommands } from "./keys";
|
|
7
|
+
import { registerModelsCommands } from "./models";
|
|
8
|
+
import { registerStatusCommand } from "./status";
|
|
9
|
+
import { registerUsagesCommand } from "./usages";
|
|
10
|
+
|
|
11
|
+
export const registerCommands = (program: Command) => {
|
|
12
|
+
registerAuthCommands(program);
|
|
13
|
+
registerCodexCommand(program);
|
|
14
|
+
registerClaudeCommand(program);
|
|
15
|
+
registerConfigCommands(program);
|
|
16
|
+
registerKeysCommands(program);
|
|
17
|
+
registerModelsCommands(program);
|
|
18
|
+
registerStatusCommand(program);
|
|
19
|
+
registerUsagesCommand(program);
|
|
20
|
+
};
|
package/src/cmd/keys.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { createApiClients } from "../core/api/client";
|
|
3
|
+
import { redactSecrets } from "../core/config/redact";
|
|
4
|
+
import {
|
|
5
|
+
confirmDelete,
|
|
6
|
+
promptKeyEnabled,
|
|
7
|
+
promptKeyName,
|
|
8
|
+
selectConsumerList,
|
|
9
|
+
} from "../core/interactive/keys";
|
|
10
|
+
import { renderTable } from "../core/output/table";
|
|
11
|
+
import type {
|
|
12
|
+
ConsumerService,
|
|
13
|
+
routercommonv1_Consumer,
|
|
14
|
+
} from "../generated/router/dashboard/v1";
|
|
15
|
+
|
|
16
|
+
type ConsumerLike = Partial<routercommonv1_Consumer>;
|
|
17
|
+
|
|
18
|
+
const consumerHeaders = [
|
|
19
|
+
"NAME",
|
|
20
|
+
"ENABLED",
|
|
21
|
+
"LAST_ACCESS",
|
|
22
|
+
"CREATED_AT",
|
|
23
|
+
"API_KEY",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const consumerRow = (consumer: ConsumerLike) => [
|
|
27
|
+
String(consumer.name ?? ""),
|
|
28
|
+
String(consumer.enabled ?? ""),
|
|
29
|
+
String(consumer.lastAccess ?? ""),
|
|
30
|
+
String(consumer.createdAt ?? ""),
|
|
31
|
+
String(consumer.apiKey ?? ""),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const outputConsumerTable = (consumer: ConsumerLike) => {
|
|
35
|
+
console.log(renderTable(consumerHeaders, [consumerRow(consumer)]));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const outputConsumers = (consumers: routercommonv1_Consumer[]) => {
|
|
39
|
+
const rows = consumers.map(consumerRow);
|
|
40
|
+
console.log(renderTable(consumerHeaders, rows));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const redactConsumer = (consumer: routercommonv1_Consumer) =>
|
|
44
|
+
redactSecrets(consumer);
|
|
45
|
+
|
|
46
|
+
const requireInteractive = (message: string) => {
|
|
47
|
+
if (!process.stdin.isTTY) {
|
|
48
|
+
throw new Error(message);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const requireInteractiveForSelection = () =>
|
|
53
|
+
requireInteractive("Interactive mode required when key id is omitted.");
|
|
54
|
+
|
|
55
|
+
const requireInteractiveForAction = (action: string) =>
|
|
56
|
+
requireInteractive(`Interactive mode required for keys ${action}.`);
|
|
57
|
+
|
|
58
|
+
const updateConsumer = async (
|
|
59
|
+
consumerService: Pick<ConsumerService, "UpdateConsumer">,
|
|
60
|
+
consumer: routercommonv1_Consumer,
|
|
61
|
+
name: string | undefined,
|
|
62
|
+
enabled: boolean | undefined,
|
|
63
|
+
) => {
|
|
64
|
+
const updateMask = [
|
|
65
|
+
name !== undefined && name !== consumer.name ? "name" : null,
|
|
66
|
+
enabled !== undefined && enabled !== consumer.enabled ? "enabled" : null,
|
|
67
|
+
]
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
.join(",");
|
|
70
|
+
if (!updateMask) {
|
|
71
|
+
return consumer;
|
|
72
|
+
}
|
|
73
|
+
return consumerService.UpdateConsumer({
|
|
74
|
+
consumer: {
|
|
75
|
+
...consumer,
|
|
76
|
+
name: name ?? consumer.name,
|
|
77
|
+
enabled: enabled ?? consumer.enabled,
|
|
78
|
+
},
|
|
79
|
+
updateMask,
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const listConsumers = async (
|
|
84
|
+
consumerService: Pick<ConsumerService, "ListConsumers">,
|
|
85
|
+
) => {
|
|
86
|
+
const res = await consumerService.ListConsumers({
|
|
87
|
+
pageSize: undefined,
|
|
88
|
+
pageToken: undefined,
|
|
89
|
+
});
|
|
90
|
+
const consumers = (res?.consumers ?? []).map(redactConsumer);
|
|
91
|
+
outputConsumers(consumers);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const resolveConsumerForUpdate = async (
|
|
95
|
+
consumerService: Pick<ConsumerService, "GetConsumer" | "ListConsumers">,
|
|
96
|
+
id?: string,
|
|
97
|
+
) => {
|
|
98
|
+
if (id) {
|
|
99
|
+
return consumerService.GetConsumer({ id });
|
|
100
|
+
}
|
|
101
|
+
requireInteractiveForSelection();
|
|
102
|
+
return await selectConsumerList(consumerService, "Select key to update");
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const resolveConsumerForDelete = async (
|
|
106
|
+
consumerService: Pick<ConsumerService, "GetConsumer" | "ListConsumers">,
|
|
107
|
+
id?: string,
|
|
108
|
+
) => {
|
|
109
|
+
if (id) {
|
|
110
|
+
return consumerService.GetConsumer({ id });
|
|
111
|
+
}
|
|
112
|
+
requireInteractiveForSelection();
|
|
113
|
+
return await selectConsumerList(consumerService, "Select key to delete");
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const createConsumer = async (
|
|
117
|
+
consumerService: Pick<ConsumerService, "CreateConsumer" | "UpdateConsumer">,
|
|
118
|
+
) => {
|
|
119
|
+
requireInteractiveForAction("create");
|
|
120
|
+
const name = await promptKeyName();
|
|
121
|
+
const enabled = await promptKeyEnabled(true);
|
|
122
|
+
let consumer = await consumerService.CreateConsumer({});
|
|
123
|
+
consumer = await updateConsumer(consumerService, consumer, name, enabled);
|
|
124
|
+
outputConsumerTable(consumer);
|
|
125
|
+
console.log("Please store this API key securely.");
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const updateConsumerById = async (
|
|
129
|
+
consumerService: Pick<
|
|
130
|
+
ConsumerService,
|
|
131
|
+
"GetConsumer" | "ListConsumers" | "UpdateConsumer"
|
|
132
|
+
>,
|
|
133
|
+
id?: string,
|
|
134
|
+
) => {
|
|
135
|
+
requireInteractiveForAction("update");
|
|
136
|
+
const selected = await resolveConsumerForUpdate(consumerService, id);
|
|
137
|
+
if (!selected?.id) return;
|
|
138
|
+
const name = await promptKeyName(selected.name);
|
|
139
|
+
const enabled = await promptKeyEnabled(selected.enabled ?? true);
|
|
140
|
+
const consumer = await updateConsumer(
|
|
141
|
+
consumerService,
|
|
142
|
+
selected,
|
|
143
|
+
name,
|
|
144
|
+
enabled,
|
|
145
|
+
);
|
|
146
|
+
outputConsumerTable(redactConsumer(consumer));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const deleteConsumerById = async (
|
|
150
|
+
consumerService: Pick<
|
|
151
|
+
ConsumerService,
|
|
152
|
+
"GetConsumer" | "ListConsumers" | "DeleteConsumer"
|
|
153
|
+
>,
|
|
154
|
+
id?: string,
|
|
155
|
+
) => {
|
|
156
|
+
requireInteractiveForAction("delete");
|
|
157
|
+
const selected = await resolveConsumerForDelete(consumerService, id);
|
|
158
|
+
if (!selected?.id) return;
|
|
159
|
+
const confirmed = await confirmDelete(selected);
|
|
160
|
+
if (!confirmed) return;
|
|
161
|
+
await consumerService.DeleteConsumer({ id: selected.id });
|
|
162
|
+
outputConsumerTable(redactConsumer(selected));
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const registerKeysCommands = (program: Command) => {
|
|
166
|
+
const keys = program.command("keys").description("Manage API keys");
|
|
167
|
+
keys.allowExcessArguments(false);
|
|
168
|
+
|
|
169
|
+
keys.action(async () => {
|
|
170
|
+
const { consumerService } = createApiClients({});
|
|
171
|
+
await listConsumers(consumerService);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
keys
|
|
175
|
+
.command("list")
|
|
176
|
+
.description("List API keys")
|
|
177
|
+
.action(async () => {
|
|
178
|
+
const { consumerService } = createApiClients({});
|
|
179
|
+
await listConsumers(consumerService);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
keys
|
|
183
|
+
.command("create")
|
|
184
|
+
.description("Create an API key")
|
|
185
|
+
.action(async () => {
|
|
186
|
+
const { consumerService } = createApiClients({});
|
|
187
|
+
await createConsumer(consumerService);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
keys
|
|
191
|
+
.command("update")
|
|
192
|
+
.description("Update an API key")
|
|
193
|
+
.argument("[id]", "Key id")
|
|
194
|
+
.action(async (id?: string) => {
|
|
195
|
+
const { consumerService } = createApiClients({});
|
|
196
|
+
await updateConsumerById(consumerService, id);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
keys
|
|
200
|
+
.command("delete")
|
|
201
|
+
.description("Delete an API key")
|
|
202
|
+
.argument("[id]", "Key id")
|
|
203
|
+
.action(async (id?: string) => {
|
|
204
|
+
const { consumerService } = createApiClients({});
|
|
205
|
+
await deleteConsumerById(consumerService, id);
|
|
206
|
+
});
|
|
207
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { createApiClients } from "../core/api/client";
|
|
3
|
+
import { renderTable } from "../core/output/table";
|
|
4
|
+
import type { routercommonv1_Model } from "../generated/router/dashboard/v1";
|
|
5
|
+
|
|
6
|
+
const modelHeaders = ["NAME", "AUTHOR", "ENABLED", "UPDATED_AT"];
|
|
7
|
+
|
|
8
|
+
const modelRow = (model: routercommonv1_Model) => [
|
|
9
|
+
String(model.name ?? ""),
|
|
10
|
+
String(model.author ?? ""),
|
|
11
|
+
String(model.enabled ?? ""),
|
|
12
|
+
String(model.updatedAt ?? ""),
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const outputModels = (models: routercommonv1_Model[]) => {
|
|
16
|
+
console.log("🧠 Models");
|
|
17
|
+
console.log(renderTable(modelHeaders, models.map(modelRow)));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const listModels = async () => {
|
|
21
|
+
const { modelService } = createApiClients({});
|
|
22
|
+
const res = await modelService.ListModels({
|
|
23
|
+
pageSize: undefined,
|
|
24
|
+
pageToken: undefined,
|
|
25
|
+
filter: undefined,
|
|
26
|
+
});
|
|
27
|
+
const models = res?.models ?? [];
|
|
28
|
+
if (models.length === 0) {
|
|
29
|
+
console.log("😕 No models found");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
outputModels(models);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const registerModelsCommands = (program: Command) => {
|
|
36
|
+
const models = program.command("models").description("List models");
|
|
37
|
+
|
|
38
|
+
models.action(async () => {
|
|
39
|
+
await listModels();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
models
|
|
43
|
+
.command("list")
|
|
44
|
+
.description("List models")
|
|
45
|
+
.action(async () => {
|
|
46
|
+
await listModels();
|
|
47
|
+
});
|
|
48
|
+
};
|