A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing
at v0.2.1 173 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 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});