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

Compare changes

Choose any two refs to compare.

+180 -23
+1
.gitignore
··· 35 36 # Bun lockfile - keep but binary cache 37 bun.lockb
··· 35 36 # Bun lockfile - keep but binary cache 37 bun.lockb 38 + packages/ui
+18 -1
CHANGELOG.md
··· 1 ## [0.3.1] - 2026-02-04 2 3 ### ๐Ÿ› Bug Fixes ··· 7 ### โš™๏ธ Miscellaneous Tasks 8 9 - Updated authentication ux 10 - 11 ## [0.3.0] - 2026-02-04 12 13 ### ๐Ÿš€ Features
··· 1 + ## [0.3.3.] - 2026-02-04 2 + 3 + ### โš™๏ธ Miscellaneous Tasks 4 + 5 + - Cleaned up remaining auth implementations 6 + - Format 7 + 8 + ## [0.3.2] - 2026-02-05 9 + 10 + ### ๐Ÿ› Bug Fixes 11 + 12 + - Fixed issue with auth selection in init command 13 + 14 + ### โš™๏ธ Miscellaneous Tasks 15 + 16 + - Release 0.3.2 17 ## [0.3.1] - 2026-02-04 18 19 ### ๐Ÿ› Bug Fixes ··· 23 ### โš™๏ธ Miscellaneous Tasks 24 25 - Updated authentication ux 26 + - Release 0.3.1 27 + - Bumped version 28 ## [0.3.0] - 2026-02-04 29 30 ### ๐Ÿš€ Features
+14 -1
bun.lock
··· 24 }, 25 "packages/cli": { 26 "name": "sequoia-cli", 27 - "version": "0.2.1", 28 "bin": { 29 "sequoia": "dist/index.js", 30 }, ··· 41 "devDependencies": { 42 "@biomejs/biome": "^2.3.13", 43 "@types/mime-types": "^3.0.1", 44 "@types/node": "^20", 45 }, 46 "peerDependencies": { ··· 1358 "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], 1359 1360 "sequoia-cli": ["sequoia-cli@workspace:packages/cli"], 1361 1362 "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], 1363
··· 24 }, 25 "packages/cli": { 26 "name": "sequoia-cli", 27 + "version": "0.3.2", 28 "bin": { 29 "sequoia": "dist/index.js", 30 }, ··· 41 "devDependencies": { 42 "@biomejs/biome": "^2.3.13", 43 "@types/mime-types": "^3.0.1", 44 + "@types/node": "^20", 45 + }, 46 + "peerDependencies": { 47 + "typescript": "^5", 48 + }, 49 + }, 50 + "packages/ui": { 51 + "name": "sequoia-ui", 52 + "version": "0.1.0", 53 + "devDependencies": { 54 + "@biomejs/biome": "^2.3.13", 55 "@types/node": "^20", 56 }, 57 "peerDependencies": { ··· 1369 "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], 1370 1371 "sequoia-cli": ["sequoia-cli@workspace:packages/cli"], 1372 + 1373 + "sequoia-ui": ["sequoia-ui@workspace:packages/ui"], 1374 1375 "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], 1376
+1 -1
packages/cli/package.json
··· 1 { 2 "name": "sequoia-cli", 3 - "version": "0.3.1", 4 "type": "module", 5 "bin": { 6 "sequoia": "dist/index.js"
··· 1 { 2 "name": "sequoia-cli", 3 + "version": "0.3.3", 4 "type": "module", 5 "bin": { 6 "sequoia": "dist/index.js"
+24 -6
packages/cli/src/commands/init.ts
··· 13 } from "@clack/prompts"; 14 import * as path from "node:path"; 15 import { findConfig, generateConfigTemplate } from "../lib/config"; 16 - import { loadCredentials } from "../lib/credentials"; 17 import { createAgent, createPublication } from "../lib/atproto"; 18 import type { FrontmatterMapping, BlueskyConfig } from "../lib/types"; 19 20 async function fileExists(filePath: string): Promise<boolean> { ··· 186 } 187 188 let publicationUri: string; 189 - const credentials = await loadCredentials(); 190 191 if (publicationChoice === "create") { 192 // Need credentials to create a publication 193 if (!credentials) { 194 log.error( 195 - "You must authenticate first. Run 'sequoia auth' before creating a publication.", 196 ); 197 process.exit(1); 198 } ··· 206 } catch (_error) { 207 s.stop("Failed to connect"); 208 log.error( 209 - "Failed to connect. Check your credentials with 'sequoia auth'.", 210 ); 211 process.exit(1); 212 } ··· 308 }; 309 } 310 311 - // Get PDS URL from credentials (already loaded earlier) 312 - const pdsUrl = credentials?.pdsUrl; 313 314 // Generate config file 315 const configContent = generateConfigTemplate({
··· 13 } from "@clack/prompts"; 14 import * as path from "node:path"; 15 import { findConfig, generateConfigTemplate } from "../lib/config"; 16 + import { loadCredentials, listAllCredentials } from "../lib/credentials"; 17 import { createAgent, createPublication } from "../lib/atproto"; 18 + import { selectCredential } from "../lib/credential-select"; 19 import type { FrontmatterMapping, BlueskyConfig } from "../lib/types"; 20 21 async function fileExists(filePath: string): Promise<boolean> { ··· 187 } 188 189 let publicationUri: string; 190 + let credentials = await loadCredentials(); 191 192 if (publicationChoice === "create") { 193 // Need credentials to create a publication 194 if (!credentials) { 195 + // Check if there are multiple identities - if so, prompt to select 196 + const allCredentials = await listAllCredentials(); 197 + if (allCredentials.length > 1) { 198 + credentials = await selectCredential(allCredentials); 199 + } else if (allCredentials.length === 1) { 200 + // Single credential exists but couldn't be loaded - try to load it explicitly 201 + credentials = await selectCredential(allCredentials); 202 + } else { 203 + log.error( 204 + "You must authenticate first. Run 'sequoia login' (recommended) or 'sequoia auth' before creating a publication.", 205 + ); 206 + process.exit(1); 207 + } 208 + } 209 + 210 + if (!credentials) { 211 log.error( 212 + "Could not load credentials. Try running 'sequoia login' again to re-authenticate.", 213 ); 214 process.exit(1); 215 } ··· 223 } catch (_error) { 224 s.stop("Failed to connect"); 225 log.error( 226 + "Failed to connect. Try re-authenticating with 'sequoia login' or 'sequoia auth'.", 227 ); 228 process.exit(1); 229 } ··· 325 }; 326 } 327 328 + // Get PDS URL from credentials (only available for app-password auth) 329 + const pdsUrl = 330 + credentials?.type === "app-password" ? credentials.pdsUrl : undefined; 331 332 // Generate config file 333 const configContent = generateConfigTemplate({
+3 -2
packages/cli/src/commands/publish.ts
··· 107 type: "oauth", 108 did: selected, 109 handle: handle || selected, 110 - pdsUrl: "https://bsky.social", 111 }; 112 } 113 } else { ··· 246 } 247 248 // Create agent 249 - s.start(`Connecting to ${credentials.pdsUrl}...`); 250 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 251 try { 252 agent = await createAgent(credentials);
··· 107 type: "oauth", 108 did: selected, 109 handle: handle || selected, 110 }; 111 } 112 } else { ··· 245 } 246 247 // Create agent 248 + const connectingTo = 249 + credentials.type === "oauth" ? credentials.handle : credentials.pdsUrl; 250 + s.start(`Connecting as ${connectingTo}...`); 251 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 252 try { 253 agent = await createAgent(credentials);
+3 -2
packages/cli/src/commands/sync.ts
··· 93 type: "oauth", 94 did: selected, 95 handle: handle || selected, 96 - pdsUrl: "https://bsky.social", 97 }; 98 } 99 } else { ··· 108 109 // Create agent 110 const s = spinner(); 111 - s.start(`Connecting to ${credentials.pdsUrl}...`); 112 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 113 try { 114 agent = await createAgent(credentials);
··· 93 type: "oauth", 94 did: selected, 95 handle: handle || selected, 96 }; 97 } 98 } else { ··· 107 108 // Create agent 109 const s = spinner(); 110 + const connectingTo = 111 + credentials.type === "oauth" ? credentials.handle : credentials.pdsUrl; 112 + s.start(`Connecting as ${connectingTo}...`); 113 let agent: Awaited<ReturnType<typeof createAgent>> | undefined; 114 try { 115 agent = await createAgent(credentials);
+60 -5
packages/cli/src/commands/update.ts
··· 11 log, 12 } from "@clack/prompts"; 13 import { findConfig, loadConfig, generateConfigTemplate } from "../lib/config"; 14 - import { loadCredentials } from "../lib/credentials"; 15 import { createAgent, getPublication, updatePublication } from "../lib/atproto"; 16 import { exitOnCancel } from "../lib/prompts"; 17 import type { ··· 438 439 async function updatePublicationFlow(config: PublisherConfig): Promise<void> { 440 // Load credentials 441 - const credentials = await loadCredentials(config.identity); 442 if (!credentials) { 443 - log.error( 444 - "No credentials found. Run 'sequoia auth' or 'sequoia login' first.", 445 ); 446 - process.exit(1); 447 } 448 449 const s = spinner();
··· 11 log, 12 } from "@clack/prompts"; 13 import { findConfig, loadConfig, generateConfigTemplate } from "../lib/config"; 14 + import { 15 + loadCredentials, 16 + listAllCredentials, 17 + getCredentials, 18 + } from "../lib/credentials"; 19 + import { getOAuthHandle, getOAuthSession } from "../lib/oauth-store"; 20 import { createAgent, getPublication, updatePublication } from "../lib/atproto"; 21 import { exitOnCancel } from "../lib/prompts"; 22 import type { ··· 443 444 async function updatePublicationFlow(config: PublisherConfig): Promise<void> { 445 // Load credentials 446 + let credentials = await loadCredentials(config.identity); 447 + 448 if (!credentials) { 449 + const identities = await listAllCredentials(); 450 + if (identities.length === 0) { 451 + log.error( 452 + "No credentials found. Run 'sequoia login' or 'sequoia auth' first.", 453 + ); 454 + process.exit(1); 455 + } 456 + 457 + // Build labels with handles for OAuth sessions 458 + const options = await Promise.all( 459 + identities.map(async (cred) => { 460 + if (cred.type === "oauth") { 461 + const handle = await getOAuthHandle(cred.id); 462 + return { 463 + value: cred.id, 464 + label: `${handle || cred.id} (OAuth)`, 465 + }; 466 + } 467 + return { 468 + value: cred.id, 469 + label: `${cred.id} (App Password)`, 470 + }; 471 + }), 472 ); 473 + 474 + log.info("Multiple identities found. Select one to use:"); 475 + const selected = exitOnCancel( 476 + await select({ 477 + message: "Identity:", 478 + options, 479 + }), 480 + ); 481 + 482 + // Load the selected credentials 483 + const selectedCred = identities.find((c) => c.id === selected); 484 + if (selectedCred?.type === "oauth") { 485 + const session = await getOAuthSession(selected); 486 + if (session) { 487 + const handle = await getOAuthHandle(selected); 488 + credentials = { 489 + type: "oauth", 490 + did: selected, 491 + handle: handle || selected, 492 + }; 493 + } 494 + } else { 495 + credentials = await getCredentials(selected); 496 + } 497 + 498 + if (!credentials) { 499 + log.error("Failed to load selected credentials."); 500 + process.exit(1); 501 + } 502 } 503 504 const s = spinner();
+1 -1
packages/cli/src/index.ts
··· 35 36 > https://tangled.org/stevedylan.dev/sequoia 37 `, 38 - version: "0.3.1", 39 cmds: { 40 auth: authCommand, 41 init: initCommand,
··· 35 36 > https://tangled.org/stevedylan.dev/sequoia 37 `, 38 + version: "0.3.3", 39 cmds: { 40 auth: authCommand, 41 init: initCommand,
+54
packages/cli/src/lib/credential-select.ts
···
··· 1 + import { select } from "@clack/prompts"; 2 + import { getOAuthHandle, getOAuthSession } from "./oauth-store"; 3 + import { getCredentials } from "./credentials"; 4 + import type { Credentials } from "./types"; 5 + import { exitOnCancel } from "./prompts"; 6 + 7 + /** 8 + * Prompt user to select from multiple credentials 9 + */ 10 + export async function selectCredential( 11 + allCredentials: Array<{ id: string; type: "app-password" | "oauth" }>, 12 + ): Promise<Credentials | null> { 13 + // Build options with friendly labels 14 + const options = await Promise.all( 15 + allCredentials.map(async ({ id, type }) => { 16 + let label = id; 17 + if (type === "oauth") { 18 + const handle = await getOAuthHandle(id); 19 + label = handle ? `${handle} (${id})` : id; 20 + } 21 + return { 22 + value: { id, type }, 23 + label: `${label} [${type}]`, 24 + }; 25 + }), 26 + ); 27 + 28 + const selected = exitOnCancel( 29 + await select({ 30 + message: "Multiple identities found. Select one:", 31 + options, 32 + }), 33 + ); 34 + 35 + // Load the full credentials for the selected identity 36 + if (selected.type === "oauth") { 37 + const session = await getOAuthSession(selected.id); 38 + if (session) { 39 + const handle = await getOAuthHandle(selected.id); 40 + return { 41 + type: "oauth", 42 + did: selected.id, 43 + handle: handle || selected.id, 44 + }; 45 + } 46 + } else { 47 + const creds = await getCredentials(selected.id); 48 + if (creds) { 49 + return creds; 50 + } 51 + } 52 + 53 + return null; 54 + }
-3
packages/cli/src/lib/credentials.ts
··· 96 type: "oauth", 97 did: profile, 98 handle: handle || profile, 99 - pdsUrl: "https://bsky.social", // Will be resolved from DID doc 100 }; 101 } 102 } ··· 109 type: "oauth", 110 did: match.did, 111 handle: match.handle || match.did, 112 - pdsUrl: "https://bsky.social", 113 }; 114 } 115 ··· 186 type: "oauth", 187 did: oauthDids[0], 188 handle: handle || oauthDids[0], 189 - pdsUrl: "https://bsky.social", 190 }; 191 } 192 }
··· 96 type: "oauth", 97 did: profile, 98 handle: handle || profile, 99 }; 100 } 101 } ··· 108 type: "oauth", 109 did: match.did, 110 handle: match.handle || match.did, 111 }; 112 } 113 ··· 184 type: "oauth", 185 did: oauthDids[0], 186 handle: handle || oauthDids[0], 187 }; 188 } 189 }
+1 -1
packages/cli/src/lib/types.ts
··· 54 } 55 56 // OAuth credentials (references stored OAuth session) 57 export interface OAuthCredentials { 58 type: "oauth"; 59 did: string; 60 handle: string; 61 - pdsUrl: string; 62 } 63 64 // Union type for all credential types
··· 54 } 55 56 // OAuth credentials (references stored OAuth session) 57 + // Note: pdsUrl is not needed for OAuth - the OAuth client resolves PDS from the DID 58 export interface OAuthCredentials { 59 type: "oauth"; 60 did: string; 61 handle: string; 62 } 63 64 // Union type for all credential types