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

chore: cleaned up types

+49 -43
+4 -1
packages/cli/src/commands/init.ts
··· 287 287 defaultValue: "7", 288 288 placeholder: "7", 289 289 validate: (value) => { 290 - const num = parseInt(value, 10); 290 + if (!value) { 291 + return "Please enter a number"; 292 + } 293 + const num = Number.parseInt(value, 10); 291 294 if (Number.isNaN(num) || num < 1) { 292 295 return "Please enter a positive number"; 293 296 }
-1
packages/cli/src/commands/login.ts
··· 4 4 import { resolveHandleToDid } from "../lib/atproto"; 5 5 import { 6 6 getCallbackPort, 7 - getCallbackUrl, 8 7 getOAuthClient, 9 8 getOAuthScope, 10 9 } from "../lib/oauth-client";
+1 -1
packages/cli/src/commands/publish.ts
··· 209 209 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 210 210 try { 211 211 agent = await createAgent(credentials); 212 - s.stop(`Logged in as ${agent.session?.handle}`); 212 + s.stop(`Logged in as ${agent.did}`); 213 213 } catch (error) { 214 214 s.stop("Failed to login"); 215 215 log.error(`Failed to login: ${error}`);
+1 -1
packages/cli/src/commands/sync.ts
··· 76 76 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 77 77 try { 78 78 agent = await createAgent(credentials); 79 - s.stop(`Logged in as ${agent.session?.handle}`); 79 + s.stop(`Logged in as ${agent.did}`); 80 80 } catch (error) { 81 81 s.stop("Failed to login"); 82 82 log.error(`Failed to login: ${error}`);
+35 -29
packages/cli/src/lib/atproto.ts
··· 13 13 } from "./types"; 14 14 import { isAppPasswordCredentials, isOAuthCredentials } from "./types"; 15 15 16 + /** 17 + * Type guard to check if a record value is a DocumentRecord 18 + */ 19 + function isDocumentRecord(value: unknown): value is DocumentRecord { 20 + if (!value || typeof value !== "object") return false; 21 + const v = value as Record<string, unknown>; 22 + return ( 23 + v.$type === "site.standard.document" && 24 + typeof v.title === "string" && 25 + typeof v.site === "string" && 26 + typeof v.path === "string" && 27 + typeof v.textContent === "string" && 28 + typeof v.publishedAt === "string" 29 + ); 30 + } 31 + 16 32 async function fileExists(filePath: string): Promise<boolean> { 17 33 try { 18 34 await fs.access(filePath); ··· 96 112 showInDiscover?: boolean; 97 113 } 98 114 99 - export async function createAgent(credentials: Credentials): Promise<AtpAgent> { 115 + export async function createAgent(credentials: Credentials): Promise<Agent> { 100 116 if (isOAuthCredentials(credentials)) { 101 117 // OAuth flow - restore session from stored tokens 102 118 const client = await getOAuthClient(); 103 119 try { 104 120 const oauthSession = await client.restore(credentials.did); 105 121 // Wrap the OAuth session in an Agent which provides the atproto API 106 - const agent = new Agent(oauthSession) as unknown as AtpAgent; 107 - 108 - // The Agent class doesn't have session.did like AtpAgent does 109 - // We need to set up a compatible session object for the rest of our code 110 - agent.session = { 111 - did: oauthSession.did, 112 - handle: credentials.handle, 113 - accessJwt: "", 114 - refreshJwt: "", 115 - active: true, 116 - }; 117 - 118 - return agent; 122 + return new Agent(oauthSession); 119 123 } catch (error) { 120 124 if (error instanceof Error) { 121 125 // Check for common OAuth errors ··· 147 151 } 148 152 149 153 export async function uploadImage( 150 - agent: AtpAgent, 154 + agent: Agent, 151 155 imagePath: string, 152 156 ): Promise<BlobObject | undefined> { 153 157 if (!(await fileExists(imagePath))) { ··· 216 220 } 217 221 218 222 export async function createDocument( 219 - agent: AtpAgent, 223 + agent: Agent, 220 224 post: BlogPost, 221 225 config: PublisherConfig, 222 226 coverImage?: BlobObject, ··· 259 263 } 260 264 261 265 const response = await agent.com.atproto.repo.createRecord({ 262 - repo: agent.session!.did, 266 + repo: agent.did!, 263 267 collection: "site.standard.document", 264 268 record, 265 269 }); ··· 268 272 } 269 273 270 274 export async function updateDocument( 271 - agent: AtpAgent, 275 + agent: Agent, 272 276 post: BlogPost, 273 277 atUri: string, 274 278 config: PublisherConfig, ··· 321 325 } 322 326 323 327 await agent.com.atproto.repo.putRecord({ 324 - repo: agent.session!.did, 328 + repo: agent.did!, 325 329 collection: collection!, 326 330 rkey: rkey!, 327 331 record, ··· 361 365 } 362 366 363 367 export async function listDocuments( 364 - agent: AtpAgent, 368 + agent: Agent, 365 369 publicationUri?: string, 366 370 ): Promise<ListDocumentsResult[]> { 367 371 const documents: ListDocumentsResult[] = []; ··· 369 373 370 374 do { 371 375 const response = await agent.com.atproto.repo.listRecords({ 372 - repo: agent.session!.did, 376 + repo: agent.did!, 373 377 collection: "site.standard.document", 374 378 limit: 100, 375 379 cursor, 376 380 }); 377 381 378 382 for (const record of response.data.records) { 379 - const value = record.value as unknown as DocumentRecord; 383 + if (!isDocumentRecord(record.value)) { 384 + continue; 385 + } 380 386 381 387 // If publicationUri is specified, only include documents from that publication 382 - if (publicationUri && value.site !== publicationUri) { 388 + if (publicationUri && record.value.site !== publicationUri) { 383 389 continue; 384 390 } 385 391 386 392 documents.push({ 387 393 uri: record.uri, 388 394 cid: record.cid, 389 - value, 395 + value: record.value, 390 396 }); 391 397 } 392 398 ··· 397 403 } 398 404 399 405 export async function createPublication( 400 - agent: AtpAgent, 406 + agent: Agent, 401 407 options: CreatePublicationOptions, 402 408 ): Promise<string> { 403 409 let icon: BlobObject | undefined; ··· 428 434 } 429 435 430 436 const response = await agent.com.atproto.repo.createRecord({ 431 - repo: agent.session!.did, 437 + repo: agent.did!, 432 438 collection: "site.standard.publication", 433 439 record, 434 440 }); ··· 481 487 * Create a Bluesky post with external link embed 482 488 */ 483 489 export async function createBlueskyPost( 484 - agent: AtpAgent, 490 + agent: Agent, 485 491 options: CreateBlueskyPostOptions, 486 492 ): Promise<StrongRef> { 487 493 const { title, description, canonicalUrl, coverImage, publishedAt } = options; ··· 576 582 }; 577 583 578 584 const response = await agent.com.atproto.repo.createRecord({ 579 - repo: agent.session!.did, 585 + repo: agent.did!, 580 586 collection: "app.bsky.feed.post", 581 587 record, 582 588 }); ··· 591 597 * Add bskyPostRef to an existing document record 592 598 */ 593 599 export async function addBskyPostRefToDocument( 594 - agent: AtpAgent, 600 + agent: Agent, 595 601 documentAtUri: string, 596 602 bskyPostRef: StrongRef, 597 603 ): Promise<void> {
+3 -8
packages/cli/src/lib/credentials.ts
··· 95 95 } 96 96 } 97 97 98 - // Otherwise, check all OAuth sessions to find a matching handle 99 - // (This is a fallback - handle matching isn't perfect without storing handles) 100 - const sessions = await listOAuthSessions(); 101 - for (const did of sessions) { 102 - // Could enhance this by storing handle with session, but for now 103 - // just return null if profile isn't a DID 104 - } 105 - 98 + // Otherwise, we would need to check all OAuth sessions to find a matching handle, 99 + // but handle matching isn't perfect without storing handles alongside sessions. 100 + // For now, just return null if profile isn't a DID. 106 101 return null; 107 102 } 108 103
+5 -2
packages/cli/src/lib/oauth-client.ts
··· 18 18 // This prevents the "No lock mechanism provided" warning 19 19 const locks = new Map<string, Promise<void>>(); 20 20 21 - async function requestLock(key: string, fn: () => Promise<void>): Promise<void> { 21 + async function requestLock<T>( 22 + key: string, 23 + fn: () => T | PromiseLike<T>, 24 + ): Promise<T> { 22 25 // Wait for any existing lock on this key 23 26 while (locks.has(key)) { 24 27 await locks.get(key); ··· 32 35 locks.set(key, lockPromise); 33 36 34 37 try { 35 - await fn(); 38 + return await fn(); 36 39 } finally { 37 40 locks.delete(key); 38 41 resolve!();