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