packages/cli/src/commands/auth.ts 4.1 K raw
1
import { AtpAgent } from "@atproto/api";
2
import {
3
	confirm,
4
	log,
5
	note,
6
	password,
7
	select,
8
	spinner,
9
	text,
10
} from "@clack/prompts";
11
import { command, flag, option, optional, string } from "cmd-ts";
12
import { resolveHandleToPDS } from "../lib/atproto";
13
import {
14
	deleteCredentials,
15
	getCredentials,
16
	getCredentialsPath,
17
	listCredentials,
18
	saveCredentials,
19
} from "../lib/credentials";
20
import { exitOnCancel } from "../lib/prompts";
21
22
export const authCommand = command({
23
	name: "auth",
24
	description: "Authenticate with your ATProto PDS",
25
	args: {
26
		logout: option({
27
			long: "logout",
28
			description:
29
				"Remove credentials for a specific identity (or all if only one exists)",
30
			type: optional(string),
31
		}),
32
		list: flag({
33
			long: "list",
34
			description: "List all stored identities",
35
		}),
36
	},
37
	handler: async ({ logout, list }) => {
38
		// List identities
39
		if (list) {
40
			const identities = await listCredentials();
41
			if (identities.length === 0) {
42
				log.info("No stored identities");
43
			} else {
44
				log.info("Stored identities:");
45
				for (const id of identities) {
46
					console.log(`  - ${id}`);
47
				}
48
			}
49
			return;
50
		}
51
52
		// Logout
53
		if (logout !== undefined) {
54
			// If --logout was passed without a value, it will be an empty string
55
			const identifier = logout || undefined;
56
57
			if (!identifier) {
58
				// No identifier provided - show available and prompt
59
				const identities = await listCredentials();
60
				if (identities.length === 0) {
61
					log.info("No saved credentials found");
62
					return;
63
				}
64
				if (identities.length === 1) {
65
					const deleted = await deleteCredentials(identities[0]);
66
					if (deleted) {
67
						log.success(`Removed credentials for ${identities[0]}`);
68
					}
69
					return;
70
				}
71
				// Multiple identities - prompt
72
				const selected = exitOnCancel(
73
					await select({
74
						message: "Select identity to remove:",
75
						options: identities.map((id) => ({ value: id, label: id })),
76
					}),
77
				);
78
				const deleted = await deleteCredentials(selected);
79
				if (deleted) {
80
					log.success(`Removed credentials for ${selected}`);
81
				}
82
				return;
83
			}
84
85
			const deleted = await deleteCredentials(identifier);
86
			if (deleted) {
87
				log.success(`Removed credentials for ${identifier}`);
88
			} else {
89
				log.info(`No credentials found for ${identifier}`);
90
			}
91
			return;
92
		}
93
94
		note(
95
			"To authenticate, you'll need an App Password.\n\n" +
96
				"Create one at: https://bsky.app/settings/app-passwords\n\n" +
97
				"App Passwords are safer than your main password and can be revoked.",
98
			"Authentication",
99
		);
100
101
		const identifier = exitOnCancel(
102
			await text({
103
				message: "Handle or DID:",
104
				placeholder: "yourhandle.bsky.social",
105
			}),
106
		);
107
108
		const appPassword = exitOnCancel(
109
			await password({
110
				message: "App Password:",
111
			}),
112
		);
113
114
		if (!identifier || !appPassword) {
115
			log.error("Handle and password are required");
116
			process.exit(1);
117
		}
118
119
		// Check if this identity already exists
120
		const existing = await getCredentials(identifier);
121
		if (existing) {
122
			const overwrite = exitOnCancel(
123
				await confirm({
124
					message: `Credentials for ${identifier} already exist. Update?`,
125
					initialValue: false,
126
				}),
127
			);
128
			if (!overwrite) {
129
				log.info("Keeping existing credentials");
130
				return;
131
			}
132
		}
133
134
		// Resolve PDS from handle
135
		const s = spinner();
136
		s.start("Resolving PDS...");
137
		let pdsUrl: string;
138
		try {
139
			pdsUrl = await resolveHandleToPDS(identifier);
140
			s.stop(`Found PDS: ${pdsUrl}`);
141
		} catch (error) {
142
			s.stop("Failed to resolve PDS");
143
			log.error(`Failed to resolve PDS from handle: ${error}`);
144
			process.exit(1);
145
		}
146
147
		// Verify credentials
148
		s.start("Verifying credentials...");
149
150
		try {
151
			const agent = new AtpAgent({ service: pdsUrl });
152
			await agent.login({
153
				identifier: identifier,
154
				password: appPassword,
155
			});
156
157
			s.stop(`Logged in as ${agent.session?.handle}`);
158
159
			// Save credentials
160
			await saveCredentials({
161
				pdsUrl,
162
				identifier: identifier,
163
				password: appPassword,
164
			});
165
166
			log.success(`Credentials saved to ${getCredentialsPath()}`);
167
		} catch (error) {
168
			s.stop("Failed to login");
169
			log.error(`Failed to login: ${error}`);
170
			process.exit(1);
171
		}
172
	},
173
});