A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing
58
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 174 lines 4.2 kB view raw
1import { AtpAgent } from "@atproto/api"; 2import { 3 confirm, 4 log, 5 note, 6 password, 7 select, 8 spinner, 9 text, 10} from "@clack/prompts"; 11import { command, flag, option, optional, string } from "cmd-ts"; 12import { resolveHandleToPDS } from "../lib/atproto"; 13import { 14 deleteCredentials, 15 getCredentials, 16 getCredentialsPath, 17 listCredentials, 18 saveCredentials, 19} from "../lib/credentials"; 20import { exitOnCancel } from "../lib/prompts"; 21 22export 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 type: "app-password", 162 pdsUrl, 163 identifier: identifier, 164 password: appPassword, 165 }); 166 167 log.success(`Credentials saved to ${getCredentialsPath()}`); 168 } catch (error) { 169 s.stop("Failed to login"); 170 log.error(`Failed to login: ${error}`); 171 process.exit(1); 172 } 173 }, 174});