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