A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing
at v0.2.0 156 lines 4.5 kB view raw
1import { command, flag, option, optional, string } from "cmd-ts"; 2import { note, text, password, confirm, select, spinner, log } from "@clack/prompts"; 3import { AtpAgent } from "@atproto/api"; 4import { 5 saveCredentials, 6 deleteCredentials, 7 listCredentials, 8 getCredentials, 9 getCredentialsPath, 10} from "../lib/credentials"; 11import { resolveHandleToPDS } from "../lib/atproto"; 12import { exitOnCancel } from "../lib/prompts"; 13 14export 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});