Skip to content

Commit 268699e

Browse files
committed
feat(cli): simplify auth commands
1 parent 9d8a0ed commit 268699e

7 files changed

Lines changed: 148 additions & 136 deletions

File tree

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,9 @@ bee <command> [options]
4343

4444
## Commands
4545

46-
- `auth` - Authenticate the CLI with your Bee account.
47-
- `auth login` - Log in interactively, or with `--token <token>` / `--token-stdin`.
48-
- `auth status` - Show current authentication status.
49-
- `auth logout` - Log out and clear stored credentials.
46+
- `login` - Log in interactively, or with `--token <token>` / `--token-stdin`.
47+
- `status` - Show current authentication status.
48+
- `logout` - Log out and clear stored credentials.
5049

5150
- `me` - Fetch your user profile.
5251

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import type { CommandContext } from "@/commands/types";
2+
3+
type DevUser = {
4+
id: number;
5+
first_name: string;
6+
last_name: string | null;
7+
};
8+
9+
export async function fetchDeveloperMe(
10+
context: CommandContext,
11+
token: string
12+
): Promise<DevUser> {
13+
const response = await context.client.fetch("/v1/me", {
14+
method: "GET",
15+
headers: {
16+
Authorization: `Bearer ${token}`,
17+
},
18+
});
19+
20+
if (!response.ok) {
21+
const errorPayload = await safeJson(response);
22+
const message =
23+
typeof errorPayload?.["error"] === "string"
24+
? errorPayload["error"]
25+
: `Request failed with status ${response.status}`;
26+
throw new Error(message);
27+
}
28+
29+
const data = await safeJson(response);
30+
const id = data?.["id"];
31+
const firstName = data?.["first_name"];
32+
if (typeof id !== "number" || typeof firstName !== "string") {
33+
throw new Error("Invalid response from developer API.");
34+
}
35+
36+
return {
37+
id,
38+
first_name: firstName,
39+
last_name: typeof data?.["last_name"] === "string" ? data["last_name"] : null,
40+
};
41+
}
42+
43+
async function safeJson(
44+
response: Response
45+
): Promise<Record<string, unknown> | null> {
46+
try {
47+
const parsed = (await response.json()) as Record<string, unknown>;
48+
if (!parsed || typeof parsed !== "object") {
49+
return null;
50+
}
51+
return parsed;
52+
} catch {
53+
return null;
54+
}
55+
}

sources/commands/developerApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ type JsonRequestInit = RequestInit & {
1616
export async function requireToken(context: CommandContext): Promise<string> {
1717
const token = await loadToken(context.env);
1818
if (!token) {
19-
throw new Error('Not logged in. Run "bee auth login" first.');
19+
throw new Error('Not logged in. Run "bee login" first.');
2020
}
2121
return token;
2222
}
Lines changed: 11 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,36 @@
11
import { select } from "@inquirer/prompts";
22
import type { Command, CommandContext } from "@/commands/types";
3-
import { getEnvironmentConfig, type Environment } from "@/environment";
4-
import { clearToken, loadToken, saveToken } from "@/secureStore";
5-
import { requestAppPairing } from "./appPairingRequest";
3+
import type { Environment } from "@/environment";
4+
import { saveToken } from "@/secureStore";
5+
import { requestAppPairing } from "@/commands/auth/appPairingRequest";
66
import {
77
decryptAppPairingToken,
88
generateAppPairingKeyPair,
99
} from "@/utils/appPairingCrypto";
1010
import { emojiHash } from "@/utils/emojiHash";
1111
import { openBrowser } from "@/utils/browser";
1212
import { renderQrCode } from "@/utils/qrCode";
13+
import { fetchDeveloperMe } from "@/commands/auth/developerMe";
1314

1415
type LoginOptions = {
1516
token?: string;
1617
tokenStdin: boolean;
1718
};
1819

19-
type DevUser = {
20-
id: number;
21-
first_name: string;
22-
last_name: string | null;
23-
};
24-
2520
type DeviceAuthMethod = "browser" | "qr";
2621

2722
const USAGE = [
28-
"bee auth login",
29-
"bee auth login --token <token>",
30-
"bee auth login --token-stdin",
31-
"bee auth status",
32-
"bee auth logout",
23+
"bee login",
24+
"bee login --token <token>",
25+
"bee login --token-stdin",
3326
].join("\n");
3427

35-
const DESCRIPTION =
36-
"Manage developer API authentication (app tokens with embedded secrets).";
37-
38-
export const authCommand: Command = {
39-
name: "auth",
40-
description: DESCRIPTION,
28+
export const loginCommand: Command = {
29+
name: "login",
30+
description: "Authenticate the CLI with your Bee account.",
4131
usage: USAGE,
4232
run: async (args, context) => {
43-
if (args.length === 0) {
44-
throw new Error("Missing subcommand. Use login, status, or logout.");
45-
}
46-
47-
const [subcommand, ...rest] = args;
48-
switch (subcommand) {
49-
case "login":
50-
await handleLogin(rest, context);
51-
return;
52-
case "status":
53-
await handleStatus(rest, context);
54-
return;
55-
case "logout":
56-
await handleLogout(rest, context);
57-
return;
58-
default:
59-
throw new Error(`Unknown auth subcommand: ${subcommand}`);
60-
}
33+
await handleLogin(args, context);
6134
},
6235
};
6336

@@ -103,41 +76,6 @@ async function handleLogin(
10376
console.log("Token stored.");
10477
}
10578

106-
async function handleStatus(
107-
_args: readonly string[],
108-
context: CommandContext
109-
): Promise<void> {
110-
if (_args.length > 0) {
111-
throw new Error("status does not accept arguments.");
112-
}
113-
const token = await loadToken(context.env);
114-
const config = getEnvironmentConfig(context.env);
115-
116-
if (!token) {
117-
console.log("Not logged in.");
118-
console.log(`API: ${config.label} (${config.apiUrl})`);
119-
return;
120-
}
121-
122-
console.log(`API: ${config.label} (${config.apiUrl})`);
123-
console.log(`Token: ${maskToken(token)}`);
124-
125-
const user = await fetchDeveloperMe(context, token);
126-
const name = [user.first_name, user.last_name].filter(Boolean).join(" ");
127-
console.log(`Verified as ${name} (id ${user.id}).`);
128-
}
129-
130-
async function handleLogout(
131-
args: readonly string[],
132-
context: CommandContext
133-
): Promise<void> {
134-
if (args.length > 0) {
135-
throw new Error("logout does not accept arguments.");
136-
}
137-
await clearToken(context.env);
138-
console.log("Logged out.");
139-
}
140-
14179
function parseLoginArgs(args: readonly string[]): LoginOptions {
14280
let token: string | undefined;
14381
let tokenStdin = false;
@@ -207,14 +145,6 @@ async function readTokenFromStdin(): Promise<string> {
207145
});
208146
}
209147

210-
function maskToken(token: string): string {
211-
const trimmed = token.trim();
212-
if (trimmed.length <= 8) {
213-
return "********";
214-
}
215-
return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
216-
}
217-
218148
async function loginWithAppPairing(context: CommandContext): Promise<string> {
219149
if (!process.stdin.isTTY) {
220150
throw new Error(
@@ -356,51 +286,3 @@ async function sleep(durationMs: number): Promise<void> {
356286
setTimeout(resolve, durationMs);
357287
});
358288
}
359-
360-
async function fetchDeveloperMe(
361-
context: CommandContext,
362-
token: string
363-
): Promise<DevUser> {
364-
const response = await context.client.fetch("/v1/me", {
365-
method: "GET",
366-
headers: {
367-
Authorization: `Bearer ${token}`,
368-
},
369-
});
370-
371-
if (!response.ok) {
372-
const errorPayload = await safeJson(response);
373-
const message =
374-
typeof errorPayload?.["error"] === "string"
375-
? errorPayload["error"]
376-
: `Request failed with status ${response.status}`;
377-
throw new Error(message);
378-
}
379-
380-
const data = await safeJson(response);
381-
const id = data?.["id"];
382-
const firstName = data?.["first_name"];
383-
if (typeof id !== "number" || typeof firstName !== "string") {
384-
throw new Error("Invalid response from developer API.");
385-
}
386-
387-
return {
388-
id,
389-
first_name: firstName,
390-
last_name: typeof data?.["last_name"] === "string" ? data["last_name"] : null,
391-
};
392-
}
393-
394-
async function safeJson(
395-
response: Response
396-
): Promise<Record<string, unknown> | null> {
397-
try {
398-
const parsed = (await response.json()) as Record<string, unknown>;
399-
if (!parsed || typeof parsed !== "object") {
400-
return null;
401-
}
402-
return parsed;
403-
} catch {
404-
return null;
405-
}
406-
}

sources/commands/logout/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { Command, CommandContext } from "@/commands/types";
2+
import { clearToken } from "@/secureStore";
3+
4+
const USAGE = "bee logout";
5+
6+
export const logoutCommand: Command = {
7+
name: "logout",
8+
description: "Log out and clear stored credentials.",
9+
usage: USAGE,
10+
run: async (args, context) => {
11+
await handleLogout(args, context);
12+
},
13+
};
14+
15+
async function handleLogout(
16+
args: readonly string[],
17+
context: CommandContext
18+
): Promise<void> {
19+
if (args.length > 0) {
20+
throw new Error("logout does not accept arguments.");
21+
}
22+
await clearToken(context.env);
23+
console.log("Logged out.");
24+
}

sources/commands/status/index.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { Command, CommandContext } from "@/commands/types";
2+
import { getEnvironmentConfig } from "@/environment";
3+
import { loadToken } from "@/secureStore";
4+
import { fetchDeveloperMe } from "@/commands/auth/developerMe";
5+
6+
const USAGE = "bee status";
7+
8+
export const statusCommand: Command = {
9+
name: "status",
10+
description: "Show current authentication status.",
11+
usage: USAGE,
12+
run: async (args, context) => {
13+
await handleStatus(args, context);
14+
},
15+
};
16+
17+
async function handleStatus(
18+
args: readonly string[],
19+
context: CommandContext
20+
): Promise<void> {
21+
if (args.length > 0) {
22+
throw new Error("status does not accept arguments.");
23+
}
24+
25+
const token = await loadToken(context.env);
26+
const config = getEnvironmentConfig(context.env);
27+
28+
if (!token) {
29+
console.log("Not logged in.");
30+
console.log(`API: ${config.label} (${config.apiUrl})`);
31+
return;
32+
}
33+
34+
console.log(`API: ${config.label} (${config.apiUrl})`);
35+
console.log(`Token: ${maskToken(token)}`);
36+
37+
const user = await fetchDeveloperMe(context, token);
38+
const name = [user.first_name, user.last_name].filter(Boolean).join(" ");
39+
console.log(`Verified as ${name} (id ${user.id}).`);
40+
}
41+
42+
function maskToken(token: string): string {
43+
const trimmed = token.trim();
44+
if (trimmed.length <= 8) {
45+
return "********";
46+
}
47+
return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
48+
}

sources/main.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { Command, CommandContext } from "@/commands/types";
22
import { createDeveloperClient } from "@/client";
3-
import { authCommand } from "@/commands/auth";
43
import { conversationsCommand } from "@/commands/conversations";
54
import { dailyCommand } from "@/commands/daily";
65
import { factsCommand } from "@/commands/facts";
6+
import { loginCommand } from "@/commands/login";
7+
import { logoutCommand } from "@/commands/logout";
78
import { meCommand } from "@/commands/me";
89
import { pingCommand } from "@/commands/ping";
910
import { proxyCommand } from "@/commands/proxy";
1011
import { searchCommand } from "@/commands/search";
12+
import { statusCommand } from "@/commands/status";
1113
import { syncCommand } from "@/commands/sync";
1214
import { todosCommand } from "@/commands/todos";
1315
import { todayCommand } from "@/commands/today";
@@ -17,7 +19,9 @@ import type { Environment } from "@/environment";
1719
const BIN = "bee";
1820

1921
const commands = [
20-
authCommand,
22+
loginCommand,
23+
logoutCommand,
24+
statusCommand,
2125
todayCommand,
2226
conversationsCommand,
2327
dailyCommand,

0 commit comments

Comments
 (0)