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

chore: initial refactor

+323 -290
+9 -7
bun.lock
··· 30 30 }, 31 31 "dependencies": { 32 32 "@atproto/api": "^0.18.17", 33 + "@clack/prompts": "^1.0.0", 33 34 "cmd-ts": "^0.14.3", 34 - "consola": "^3.4.2", 35 35 }, 36 36 "devDependencies": { 37 37 "@types/bun": "latest", ··· 112 112 113 113 "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], 114 114 115 - "@clack/core": ["@clack/core@0.3.5", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ=="], 115 + "@clack/core": ["@clack/core@1.0.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ=="], 116 116 117 - "@clack/prompts": ["@clack/prompts@0.7.0", "", { "dependencies": { "@clack/core": "^0.3.3", "is-unicode-supported": "*", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA=="], 117 + "@clack/prompts": ["@clack/prompts@1.0.0", "", { "dependencies": { "@clack/core": "1.0.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A=="], 118 118 119 119 "@emotion/hash": ["@emotion/hash@0.9.2", "", {}, "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="], 120 120 ··· 629 629 "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], 630 630 631 631 "confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], 632 - 633 - "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], 634 632 635 633 "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], 636 634 ··· 1424 1422 1425 1423 "@chevrotain/gast/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], 1426 1424 1427 - "@clack/prompts/is-unicode-supported": ["is-unicode-supported@1.3.0", "", { "bundled": true }, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], 1428 - 1429 1425 "@radix-ui/react-form/@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="], 1430 1426 1431 1427 "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], ··· 1449 1445 "chevrotain/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], 1450 1446 1451 1447 "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], 1448 + 1449 + "create-vocs/@clack/prompts": ["@clack/prompts@0.7.0", "", { "dependencies": { "@clack/core": "^0.3.3", "is-unicode-supported": "*", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA=="], 1452 1450 1453 1451 "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], 1454 1452 ··· 1491 1489 "@shikijs/twoslash/twoslash/twoslash-protocol": ["twoslash-protocol@0.2.12", "", {}, "sha512-5qZLXVYfZ9ABdjqbvPc4RWMr7PrpPaaDSeaYY55vl/w1j6H6kzsWK/urAEIXlzYlyrFmyz1UbwIt+AA0ck+wbg=="], 1492 1490 1493 1491 "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], 1492 + 1493 + "create-vocs/@clack/prompts/@clack/core": ["@clack/core@0.3.5", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ=="], 1494 + 1495 + "create-vocs/@clack/prompts/is-unicode-supported": ["is-unicode-supported@1.3.0", "", { "bundled": true }, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], 1494 1496 1495 1497 "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], 1496 1498
+1 -1
packages/cli/package.json
··· 28 28 "dependencies": { 29 29 "@atproto/api": "^0.18.17", 30 30 "cmd-ts": "^0.14.3", 31 - "consola": "^3.4.2" 31 + "@clack/prompts": "^1.0.0" 32 32 } 33 33 }
+46 -45
packages/cli/src/commands/auth.ts
··· 1 1 import { command, flag, option, optional, string } from "cmd-ts"; 2 - import { consola } from "consola"; 2 + import { note, text, password, confirm, select, spinner, log } from "@clack/prompts"; 3 3 import { AtpAgent } from "@atproto/api"; 4 4 import { 5 5 saveCredentials, ··· 9 9 getCredentialsPath, 10 10 } from "../lib/credentials"; 11 11 import { resolveHandleToPDS } from "../lib/atproto"; 12 + import { exitOnCancel } from "../lib/prompts"; 12 13 13 14 export const authCommand = command({ 14 15 name: "auth", ··· 29 30 if (list) { 30 31 const identities = await listCredentials(); 31 32 if (identities.length === 0) { 32 - consola.info("No stored identities"); 33 + log.info("No stored identities"); 33 34 } else { 34 - consola.info("Stored identities:"); 35 + log.info("Stored identities:"); 35 36 for (const id of identities) { 36 37 console.log(` - ${id}`); 37 38 } ··· 48 49 // No identifier provided - show available and prompt 49 50 const identities = await listCredentials(); 50 51 if (identities.length === 0) { 51 - consola.info("No saved credentials found"); 52 + log.info("No saved credentials found"); 52 53 return; 53 54 } 54 55 if (identities.length === 1) { 55 56 const deleted = await deleteCredentials(identities[0]); 56 57 if (deleted) { 57 - consola.success(`Removed credentials for ${identities[0]}`); 58 + log.success(`Removed credentials for ${identities[0]}`); 58 59 } 59 60 return; 60 61 } 61 62 // Multiple identities - prompt 62 - const selected = await consola.prompt("Select identity to remove:", { 63 - type: "select", 64 - options: identities, 65 - }); 66 - const deleted = await deleteCredentials(selected as string); 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); 67 68 if (deleted) { 68 - consola.success(`Removed credentials for ${selected}`); 69 + log.success(`Removed credentials for ${selected}`); 69 70 } 70 71 return; 71 72 } 72 73 73 74 const deleted = await deleteCredentials(identifier); 74 75 if (deleted) { 75 - consola.success(`Removed credentials for ${identifier}`); 76 + log.success(`Removed credentials for ${identifier}`); 76 77 } else { 77 - consola.info(`No credentials found for ${identifier}`); 78 + log.info(`No credentials found for ${identifier}`); 78 79 } 79 80 return; 80 81 } 81 82 82 - consola.box( 83 + note( 83 84 "To authenticate, you'll need an App Password.\n\n" + 84 85 "Create one at: https://bsky.app/settings/app-passwords\n\n" + 85 - "App Passwords are safer than your main password and can be revoked." 86 + "App Passwords are safer than your main password and can be revoked.", 87 + "Authentication" 86 88 ); 87 89 88 - const identifier = await consola.prompt("Handle or DID:", { 89 - type: "text", 90 + const identifier = exitOnCancel(await text({ 91 + message: "Handle or DID:", 90 92 placeholder: "yourhandle.bsky.social", 91 - }); 93 + })); 92 94 93 - const password = await consola.prompt("App Password:", { 94 - type: "text", 95 - placeholder: "xxxx-xxxx-xxxx-xxxx", 96 - }); 95 + const appPassword = exitOnCancel(await password({ 96 + message: "App Password:", 97 + })); 97 98 98 - if (!identifier || !password) { 99 - consola.error("Handle and password are required"); 99 + if (!identifier || !appPassword) { 100 + log.error("Handle and password are required"); 100 101 process.exit(1); 101 102 } 102 103 103 104 // Check if this identity already exists 104 - const existing = await getCredentials(identifier as string); 105 + const existing = await getCredentials(identifier); 105 106 if (existing) { 106 - const overwrite = await consola.prompt( 107 - `Credentials for ${identifier} already exist. Update?`, 108 - { 109 - type: "confirm", 110 - initial: false, 111 - } 112 - ); 107 + const overwrite = exitOnCancel(await confirm({ 108 + message: `Credentials for ${identifier} already exist. Update?`, 109 + initialValue: false, 110 + })); 113 111 if (!overwrite) { 114 - consola.info("Keeping existing credentials"); 112 + log.info("Keeping existing credentials"); 115 113 return; 116 114 } 117 115 } 118 116 119 117 // Resolve PDS from handle 120 - consola.start("Resolving PDS..."); 118 + const s = spinner(); 119 + s.start("Resolving PDS..."); 121 120 let pdsUrl: string; 122 121 try { 123 - pdsUrl = await resolveHandleToPDS(identifier as string); 124 - consola.success(`Found PDS: ${pdsUrl}`); 122 + pdsUrl = await resolveHandleToPDS(identifier); 123 + s.stop(`Found PDS: ${pdsUrl}`); 125 124 } catch (error) { 126 - consola.error("Failed to resolve PDS from handle:", error); 125 + s.stop("Failed to resolve PDS"); 126 + log.error(`Failed to resolve PDS from handle: ${error}`); 127 127 process.exit(1); 128 128 } 129 129 130 130 // Verify credentials 131 - consola.start("Verifying credentials..."); 131 + s.start("Verifying credentials..."); 132 132 133 133 try { 134 134 const agent = new AtpAgent({ service: pdsUrl }); 135 135 await agent.login({ 136 - identifier: identifier as string, 137 - password: password as string, 136 + identifier: identifier, 137 + password: appPassword, 138 138 }); 139 139 140 - consola.success(`Logged in as ${agent.session?.handle}`); 140 + s.stop(`Logged in as ${agent.session?.handle}`); 141 141 142 142 // Save credentials 143 143 await saveCredentials({ 144 144 pdsUrl, 145 - identifier: identifier as string, 146 - password: password as string, 145 + identifier: identifier, 146 + password: appPassword, 147 147 }); 148 148 149 - consola.success(`Credentials saved to ${getCredentialsPath()}`); 149 + log.success(`Credentials saved to ${getCredentialsPath()}`); 150 150 } catch (error) { 151 - consola.error("Failed to login:", error); 151 + s.stop("Failed to login"); 152 + log.error(`Failed to login: ${error}`); 152 153 process.exit(1); 153 154 } 154 155 },
+160 -146
packages/cli/src/commands/init.ts
··· 1 1 import { command } from "cmd-ts"; 2 - import { consola } from "consola"; 2 + import { 3 + intro, 4 + outro, 5 + note, 6 + text, 7 + confirm, 8 + select, 9 + spinner, 10 + log, 11 + } from "@clack/prompts"; 3 12 import * as path from "path"; 4 13 import { findConfig, generateConfigTemplate } from "../lib/config"; 5 14 import { loadCredentials } from "../lib/credentials"; 6 15 import { createAgent, createPublication } from "../lib/atproto"; 7 16 import type { FrontmatterMapping } from "../lib/types"; 17 + import { exitOnCancel } from "../lib/prompts"; 8 18 9 19 export const initCommand = command({ 10 20 name: "init", 11 21 description: "Initialize a new publisher configuration", 12 22 args: {}, 13 23 handler: async () => { 14 - // Handle Ctrl+C to exit immediately instead of cancelling one prompt at a time 15 - const exitHandler = () => { 16 - consola.info("\nCancelled"); 17 - process.exit(0); 18 - }; 19 - process.on("SIGINT", exitHandler); 24 + intro("Sequoia Configuration Setup"); 20 25 21 26 // Check if config already exists 22 27 const existingConfig = await findConfig(); 23 28 if (existingConfig) { 24 - const overwrite = await consola.prompt( 25 - `Config already exists at ${existingConfig}. Overwrite?`, 26 - { 27 - type: "confirm", 28 - initial: false, 29 - }, 29 + const overwrite = exitOnCancel( 30 + await confirm({ 31 + message: `Config already exists at ${existingConfig}. Overwrite?`, 32 + initialValue: false, 33 + }), 30 34 ); 31 35 if (!overwrite) { 32 - consola.info("Keeping existing configuration"); 36 + log.info("Keeping existing configuration"); 33 37 return; 34 38 } 35 39 } 36 40 37 - consola.box( 38 - "Sequoia Configuration Setup\n\n" + 39 - "Follow the prompts to build your config for publishing", 40 - ); 41 + note("Follow the prompts to build your config for publishing", "Setup"); 41 42 42 - const siteUrl = await consola.prompt( 43 - "Site URL (canonical URL of your site):", 44 - { 45 - type: "text", 43 + const siteUrl = exitOnCancel( 44 + await text({ 45 + message: "Site URL (canonical URL of your site):", 46 46 placeholder: "https://example.com", 47 - }, 47 + }), 48 48 ); 49 49 50 50 if (!siteUrl) { 51 - consola.error("Site URL is required"); 51 + log.error("Site URL is required"); 52 52 process.exit(1); 53 53 } 54 54 55 - const contentDir = await consola.prompt( 56 - "Content directory (relative path):", 57 - { 58 - type: "text", 59 - default: "./content", 60 - placeholder: "./content", 61 - }, 55 + const contentDir = exitOnCancel( 56 + await text({ 57 + message: "Content directory:", 58 + placeholder: "./src/content/blog", 59 + }), 62 60 ); 63 61 64 - const imagesDir = await consola.prompt( 65 - "Cover images directory (where cover/og images are stored, leave empty to skip):", 66 - { 67 - type: "text", 68 - placeholder: "./public/images", 69 - }, 62 + const imagesDir = exitOnCancel( 63 + await text({ 64 + message: "Cover images directory (leave empty to skip):", 65 + placeholder: "./src/assets", 66 + }), 70 67 ); 71 68 72 69 // Public/static directory for .well-known files 73 - const publicDir = await consola.prompt( 74 - "Public/static directory (for .well-known files):", 75 - { 76 - type: "text", 77 - default: "./public", 78 - placeholder: "./public (Astro, Next.js) or ./static (Hugo)", 79 - }, 70 + const publicDir = exitOnCancel( 71 + await text({ 72 + message: "Public/static directory (for .well-known files):", 73 + placeholder: "./public", 74 + }), 80 75 ); 81 76 82 77 // Output directory for inject command 83 - const outputDir = await consola.prompt( 84 - "Build output directory (for link tag injection):", 85 - { 86 - type: "text", 87 - default: "./dist", 88 - placeholder: "./dist (Astro) or ./public (Hugo) or ./out (Next.js)", 89 - }, 78 + const outputDir = exitOnCancel( 79 + await text({ 80 + message: "Build output directory (for link tag injection):", 81 + placeholder: "./dist", 82 + }), 90 83 ); 91 84 92 85 // Path prefix for posts 93 - const pathPrefix = await consola.prompt("URL path prefix for posts:", { 94 - type: "text", 95 - default: "/posts", 96 - placeholder: "/posts, /blog, /articles, etc.", 97 - }); 86 + const pathPrefix = exitOnCancel( 87 + await text({ 88 + message: "URL path prefix for posts:", 89 + placeholder: "/posts, /blog, /articles, etc.", 90 + }), 91 + ); 98 92 99 93 // Frontmatter mapping configuration 100 - consola.info( 94 + log.info( 101 95 "Configure your frontmatter field mappings (press Enter to use defaults):", 102 96 ); 103 97 104 - const titleField = await consola.prompt("Field name for title:", { 105 - type: "text", 106 - default: "title", 107 - placeholder: "title", 108 - }); 98 + const titleField = exitOnCancel( 99 + await text({ 100 + message: "Field name for title:", 101 + defaultValue: "title", 102 + placeholder: "title", 103 + }), 104 + ); 109 105 110 - const descField = await consola.prompt("Field name for description:", { 111 - type: "text", 112 - default: "description", 113 - placeholder: "description", 114 - }); 106 + const descField = exitOnCancel( 107 + await text({ 108 + message: "Field name for description:", 109 + defaultValue: "description", 110 + placeholder: "description", 111 + }), 112 + ); 115 113 116 - const dateField = await consola.prompt("Field name for publish date:", { 117 - type: "text", 118 - default: "publishDate", 119 - placeholder: "publishDate, pubDate, date, etc.", 120 - }); 114 + const dateField = exitOnCancel( 115 + await text({ 116 + message: "Field name for publish date:", 117 + defaultValue: "publishDate", 118 + placeholder: "publishDate, pubDate, date, etc.", 119 + }), 120 + ); 121 121 122 - const coverField = await consola.prompt("Field name for cover image:", { 123 - type: "text", 124 - default: "ogImage", 125 - placeholder: "ogImage, coverImage, image, hero, etc.", 126 - }); 122 + const coverField = exitOnCancel( 123 + await text({ 124 + message: "Field name for cover image:", 125 + defaultValue: "ogImage", 126 + placeholder: "ogImage, coverImage, image, hero, etc.", 127 + }), 128 + ); 127 129 128 - const tagsField = await consola.prompt("Field name for tags:", { 129 - type: "text", 130 - default: "tags", 131 - placeholder: "tags, categories, keywords, etc.", 132 - }); 130 + const tagsField = exitOnCancel( 131 + await text({ 132 + message: "Field name for tags:", 133 + defaultValue: "tags", 134 + placeholder: "tags, categories, keywords, etc.", 135 + }), 136 + ); 133 137 134 138 let frontmatterMapping: FrontmatterMapping | undefined = {}; 135 139 136 140 if (titleField && titleField !== "title") { 137 - frontmatterMapping.title = titleField as string; 141 + frontmatterMapping.title = titleField; 138 142 } 139 143 if (descField && descField !== "description") { 140 - frontmatterMapping.description = descField as string; 144 + frontmatterMapping.description = descField; 141 145 } 142 146 if (dateField && dateField !== "publishDate") { 143 - frontmatterMapping.publishDate = dateField as string; 147 + frontmatterMapping.publishDate = dateField; 144 148 } 145 149 if (coverField && coverField !== "ogImage") { 146 - frontmatterMapping.coverImage = coverField as string; 150 + frontmatterMapping.coverImage = coverField; 147 151 } 148 152 if (tagsField && tagsField !== "tags") { 149 - frontmatterMapping.tags = tagsField as string; 153 + frontmatterMapping.tags = tagsField; 150 154 } 151 155 152 156 // Only keep frontmatterMapping if it has any custom fields ··· 155 159 } 156 160 157 161 // Publication setup 158 - const publicationChoice = await consola.prompt("Publication setup:", { 159 - type: "select", 160 - options: [ 161 - { label: "Create a new publication", value: "create" }, 162 - { label: "Use an existing publication AT URI", value: "existing" }, 163 - ], 164 - }); 162 + const publicationChoice = exitOnCancel( 163 + await select({ 164 + message: "Publication setup:", 165 + options: [ 166 + { label: "Create a new publication", value: "create" }, 167 + { label: "Use an existing publication AT URI", value: "existing" }, 168 + ], 169 + }), 170 + ); 165 171 166 172 let publicationUri: string; 167 173 let credentials = await loadCredentials(); ··· 169 175 if (publicationChoice === "create") { 170 176 // Need credentials to create a publication 171 177 if (!credentials) { 172 - consola.error( 178 + log.error( 173 179 "You must authenticate first. Run 'sequoia auth' before creating a publication.", 174 180 ); 175 181 process.exit(1); 176 182 } 177 183 178 - consola.start("Connecting to ATProto..."); 184 + const s = spinner(); 185 + s.start("Connecting to ATProto..."); 179 186 let agent; 180 187 try { 181 188 agent = await createAgent(credentials); 182 - consola.success("Connected!"); 189 + s.stop("Connected!"); 183 190 } catch (error) { 184 - consola.error( 191 + s.stop("Failed to connect"); 192 + log.error( 185 193 "Failed to connect. Check your credentials with 'sequoia auth'.", 186 194 ); 187 195 process.exit(1); 188 196 } 189 197 190 - const pubName = await consola.prompt("Publication name:", { 191 - type: "text", 192 - placeholder: "My Blog", 193 - }); 198 + const pubName = exitOnCancel( 199 + await text({ 200 + message: "Publication name:", 201 + placeholder: "My Blog", 202 + }), 203 + ); 194 204 195 205 if (!pubName) { 196 - consola.error("Publication name is required"); 206 + log.error("Publication name is required"); 197 207 process.exit(1); 198 208 } 199 209 200 - const pubDescription = await consola.prompt( 201 - "Publication description (optional):", 202 - { 203 - type: "text", 210 + const pubDescription = exitOnCancel( 211 + await text({ 212 + message: "Publication description (optional):", 204 213 placeholder: "A blog about...", 205 - }, 214 + }), 206 215 ); 207 216 208 - const iconPath = await consola.prompt( 209 - "Icon image path (leave empty to skip):", 210 - { 211 - type: "text", 212 - placeholder: "./icon.png", 213 - }, 217 + const iconPath = exitOnCancel( 218 + await pathPrompt({ 219 + message: "Icon image path (leave empty to skip):", 220 + }), 214 221 ); 215 222 216 - const showInDiscover = await consola.prompt("Show in Discover feed?", { 217 - type: "confirm", 218 - initial: true, 219 - }); 223 + const showInDiscover = exitOnCancel( 224 + await confirm({ 225 + message: "Show in Discover feed?", 226 + initialValue: true, 227 + }), 228 + ); 220 229 221 - consola.start("Creating publication..."); 230 + s.start("Creating publication..."); 222 231 try { 223 232 publicationUri = await createPublication(agent, { 224 - url: siteUrl as string, 225 - name: pubName as string, 226 - description: (pubDescription as string) || undefined, 227 - iconPath: (iconPath as string) || undefined, 233 + url: siteUrl, 234 + name: pubName, 235 + description: pubDescription || undefined, 236 + iconPath: iconPath || undefined, 228 237 showInDiscover, 229 238 }); 230 - consola.success(`Publication created: ${publicationUri}`); 239 + s.stop(`Publication created: ${publicationUri}`); 231 240 } catch (error) { 232 - consola.error("Failed to create publication:", error); 241 + s.stop("Failed to create publication"); 242 + log.error(`Failed to create publication: ${error}`); 233 243 process.exit(1); 234 244 } 235 245 } else { 236 - const uri = await consola.prompt("Publication AT URI:", { 237 - type: "text", 238 - placeholder: "at://did:plc:.../site.standard.publication/...", 239 - }); 246 + const uri = exitOnCancel( 247 + await text({ 248 + message: "Publication AT URI:", 249 + placeholder: "at://did:plc:.../site.standard.publication/...", 250 + }), 251 + ); 240 252 241 253 if (!uri) { 242 - consola.error("Publication URI is required"); 254 + log.error("Publication URI is required"); 243 255 process.exit(1); 244 256 } 245 - publicationUri = uri as string; 257 + publicationUri = uri; 246 258 } 247 259 248 260 // Get PDS URL from credentials (already loaded earlier) ··· 250 262 251 263 // Generate config file 252 264 const configContent = generateConfigTemplate({ 253 - siteUrl: siteUrl as string, 254 - contentDir: contentDir as string, 265 + siteUrl: siteUrl, 266 + contentDir: contentDir || "./content", 255 267 imagesDir: imagesDir || undefined, 256 - publicDir: publicDir as string, 257 - outputDir: outputDir as string, 258 - pathPrefix: pathPrefix as string, 268 + publicDir: publicDir || "./public", 269 + outputDir: outputDir || "./dist", 270 + pathPrefix: pathPrefix || "/posts", 259 271 publicationUri, 260 272 pdsUrl, 261 273 frontmatter: frontmatterMapping, ··· 264 276 const configPath = path.join(process.cwd(), "sequoia.json"); 265 277 await Bun.write(configPath, configContent); 266 278 267 - consola.success(`Configuration saved to ${configPath}`); 279 + log.success(`Configuration saved to ${configPath}`); 268 280 269 281 // Create .well-known/site.standard.publication file 270 - const resolvedPublicDir = path.isAbsolute(publicDir as string) 271 - ? (publicDir as string) 272 - : path.join(process.cwd(), publicDir as string); 282 + const resolvedPublicDir = path.isAbsolute(publicDir || "./public") 283 + ? publicDir || "./public" 284 + : path.join(process.cwd(), publicDir || "./public"); 273 285 const wellKnownDir = path.join(resolvedPublicDir, ".well-known"); 274 286 const wellKnownPath = path.join(wellKnownDir, "site.standard.publication"); 275 287 ··· 277 289 await Bun.write(path.join(wellKnownDir, ".gitkeep"), ""); 278 290 await Bun.write(wellKnownPath, publicationUri); 279 291 280 - consola.success(`Created ${wellKnownPath}`); 292 + log.success(`Created ${wellKnownPath}`); 281 293 282 294 // Update .gitignore 283 295 const gitignorePath = path.join(process.cwd(), ".gitignore"); ··· 291 303 gitignorePath, 292 304 gitignoreContent + `\n${stateFilename}\n`, 293 305 ); 294 - consola.info(`Added ${stateFilename} to .gitignore`); 306 + log.info(`Added ${stateFilename} to .gitignore`); 295 307 } 296 308 } else { 297 309 await Bun.write(gitignorePath, `${stateFilename}\n`); 298 - consola.info(`Created .gitignore with ${stateFilename}`); 310 + log.info(`Created .gitignore with ${stateFilename}`); 299 311 } 300 312 301 - consola.box( 302 - "Setup complete!\n\n" + 303 - "Next steps:\n" + 313 + note( 314 + "Next steps:\n" + 304 315 "1. Run 'sequoia publish --dry-run' to preview\n" + 305 316 "2. Run 'sequoia publish' to publish your content", 317 + "Setup complete!", 306 318 ); 319 + 320 + outro("Happy publishing!"); 307 321 }, 308 322 });
+17 -17
packages/cli/src/commands/inject.ts
··· 1 1 import { command, flag, option, optional, string } from "cmd-ts"; 2 - import { consola } from "consola"; 2 + import { log } from "@clack/prompts"; 3 3 import * as path from "path"; 4 4 import { Glob } from "bun"; 5 5 import { loadConfig, loadState, findConfig } from "../lib/config"; ··· 25 25 // Load config 26 26 const configPath = await findConfig(); 27 27 if (!configPath) { 28 - consola.error("No sequoia.json found. Run 'sequoia init' first."); 28 + log.error("No sequoia.json found. Run 'sequoia init' first."); 29 29 process.exit(1); 30 30 } 31 31 ··· 38 38 ? outputDir 39 39 : path.join(configDir, outputDir); 40 40 41 - consola.info(`Scanning for HTML files in: ${resolvedOutputDir}`); 41 + log.info(`Scanning for HTML files in: ${resolvedOutputDir}`); 42 42 43 43 // Load state to get atUri mappings 44 44 const state = await loadState(configDir); ··· 88 88 } 89 89 90 90 if (pathToAtUri.size === 0) { 91 - consola.warn( 91 + log.warn( 92 92 "No published posts found in state. Run 'sequoia publish' first.", 93 93 ); 94 94 return; 95 95 } 96 96 97 - consola.info(`Found ${pathToAtUri.size} published posts in state`); 97 + log.info(`Found ${pathToAtUri.size} published posts in state`); 98 98 99 99 // Scan for HTML files 100 100 const glob = new Glob("**/*.html"); ··· 105 105 } 106 106 107 107 if (htmlFiles.length === 0) { 108 - consola.warn(`No HTML files found in ${resolvedOutputDir}`); 108 + log.warn(`No HTML files found in ${resolvedOutputDir}`); 109 109 return; 110 110 } 111 111 112 - consola.info(`Found ${htmlFiles.length} HTML files`); 112 + log.info(`Found ${htmlFiles.length} HTML files`); 113 113 114 114 let injectedCount = 0; 115 115 let skippedCount = 0; ··· 165 165 // Find </head> and inject before it 166 166 const headCloseIndex = content.indexOf("</head>"); 167 167 if (headCloseIndex === -1) { 168 - consola.warn(` No </head> found in ${relativePath}, skipping`); 168 + log.warn(` No </head> found in ${relativePath}, skipping`); 169 169 skippedCount++; 170 170 continue; 171 171 } 172 172 173 173 if (dryRun) { 174 - consola.log(` Would inject into: ${relativePath}`); 175 - consola.log(` ${linkTag}`); 174 + log.message(` Would inject into: ${relativePath}`); 175 + log.message(` ${linkTag}`); 176 176 injectedCount++; 177 177 continue; 178 178 } ··· 185 185 content.slice(headCloseIndex); 186 186 187 187 await Bun.write(htmlPath, content); 188 - consola.success(` Injected into: ${relativePath}`); 188 + log.success(` Injected into: ${relativePath}`); 189 189 injectedCount++; 190 190 } 191 191 192 192 // Summary 193 - consola.log("\n---"); 193 + log.message("\n---"); 194 194 if (dryRun) { 195 - consola.info("Dry run complete. No changes made."); 195 + log.info("Dry run complete. No changes made."); 196 196 } 197 - consola.info(`Injected: ${injectedCount}`); 198 - consola.info(`Already has tag: ${alreadyHasCount}`); 199 - consola.info(`Skipped (no match): ${skippedCount}`); 197 + log.info(`Injected: ${injectedCount}`); 198 + log.info(`Already has tag: ${alreadyHasCount}`); 199 + log.info(`Skipped (no match): ${skippedCount}`); 200 200 201 201 if (skippedCount > 0 && !dryRun) { 202 - consola.info( 202 + log.info( 203 203 "\nTip: Skipped files had no matching published post. This is normal for non-post pages.", 204 204 ); 205 205 }
+39 -35
packages/cli/src/commands/publish.ts
··· 1 1 import { command, flag } from "cmd-ts"; 2 - import { consola } from "consola"; 2 + import { select, spinner, log } from "@clack/prompts"; 3 3 import * as path from "path"; 4 4 import { loadConfig, loadState, saveState, findConfig } from "../lib/config"; 5 5 import { loadCredentials, listCredentials, getCredentials } from "../lib/credentials"; ··· 10 10 updateFrontmatterWithAtUri, 11 11 } from "../lib/markdown"; 12 12 import type { BlogPost, BlobObject } from "../lib/types"; 13 + import { exitOnCancel } from "../lib/prompts"; 13 14 14 15 export const publishCommand = command({ 15 16 name: "publish", ··· 30 31 // Load config 31 32 const configPath = await findConfig(); 32 33 if (!configPath) { 33 - consola.error("No publisher.config.ts found. Run 'publisher init' first."); 34 + log.error("No publisher.config.ts found. Run 'publisher init' first."); 34 35 process.exit(1); 35 36 } 36 37 37 38 const config = await loadConfig(configPath); 38 39 const configDir = path.dirname(configPath); 39 40 40 - consola.info(`Site: ${config.siteUrl}`); 41 - consola.info(`Content directory: ${config.contentDir}`); 41 + log.info(`Site: ${config.siteUrl}`); 42 + log.info(`Content directory: ${config.contentDir}`); 42 43 43 44 // Load credentials 44 45 let credentials = await loadCredentials(config.identity); ··· 47 48 if (!credentials) { 48 49 const identities = await listCredentials(); 49 50 if (identities.length === 0) { 50 - consola.error("No credentials found. Run 'sequoia auth' first."); 51 - consola.info("Or set ATP_IDENTIFIER and ATP_APP_PASSWORD environment variables."); 51 + log.error("No credentials found. Run 'sequoia auth' first."); 52 + log.info("Or set ATP_IDENTIFIER and ATP_APP_PASSWORD environment variables."); 52 53 process.exit(1); 53 54 } 54 55 55 56 // Multiple identities exist but none selected - prompt user 56 - consola.info("Multiple identities found. Select one to use:"); 57 - const selected = await consola.prompt("Identity:", { 58 - type: "select", 59 - options: identities, 60 - }); 57 + log.info("Multiple identities found. Select one to use:"); 58 + const selected = exitOnCancel(await select({ 59 + message: "Identity:", 60 + options: identities.map(id => ({ value: id, label: id })), 61 + })); 61 62 62 - credentials = await getCredentials(selected as string); 63 + credentials = await getCredentials(selected); 63 64 if (!credentials) { 64 - consola.error("Failed to load selected credentials."); 65 + log.error("Failed to load selected credentials."); 65 66 process.exit(1); 66 67 } 67 68 68 - consola.info(`Tip: Add "identity": "${selected}" to sequoia.json to use this by default.`); 69 + log.info(`Tip: Add "identity": "${selected}" to sequoia.json to use this by default.`); 69 70 } 70 71 71 72 // Resolve content directory ··· 83 84 const state = await loadState(configDir); 84 85 85 86 // Scan for posts 86 - consola.start("Scanning for posts..."); 87 + const s = spinner(); 88 + s.start("Scanning for posts..."); 87 89 const posts = await scanContentDirectory(contentDir, config.frontmatter, config.ignore); 88 - consola.info(`Found ${posts.length} posts`); 90 + s.stop(`Found ${posts.length} posts`); 89 91 90 92 // Determine which posts need publishing 91 93 const postsToPublish: Array<{ ··· 123 125 } 124 126 125 127 if (postsToPublish.length === 0) { 126 - consola.success("All posts are up to date. Nothing to publish."); 128 + log.success("All posts are up to date. Nothing to publish."); 127 129 return; 128 130 } 129 131 130 - consola.info(`\n${postsToPublish.length} posts to publish:\n`); 132 + log.info(`\n${postsToPublish.length} posts to publish:\n`); 131 133 for (const { post, action, reason } of postsToPublish) { 132 134 const icon = action === "create" ? "+" : "~"; 133 - consola.log(` ${icon} ${post.frontmatter.title} (${reason})`); 135 + log.message(` ${icon} ${post.frontmatter.title} (${reason})`); 134 136 } 135 137 136 138 if (dryRun) { 137 - consola.info("\nDry run complete. No changes made."); 139 + log.info("\nDry run complete. No changes made."); 138 140 return; 139 141 } 140 142 141 143 // Create agent 142 - consola.start(`\nConnecting to ${credentials.pdsUrl}...`); 144 + s.start(`Connecting to ${credentials.pdsUrl}...`); 143 145 let agent; 144 146 try { 145 147 agent = await createAgent(credentials); 146 - consola.success(`Logged in as ${agent.session?.handle}`); 148 + s.stop(`Logged in as ${agent.session?.handle}`); 147 149 } catch (error) { 148 - consola.error("Failed to login:", error); 150 + s.stop("Failed to login"); 151 + log.error(`Failed to login: ${error}`); 149 152 process.exit(1); 150 153 } 151 154 ··· 155 158 let errorCount = 0; 156 159 157 160 for (const { post, action } of postsToPublish) { 158 - consola.start(`Publishing: ${post.frontmatter.title}`); 161 + s.start(`Publishing: ${post.frontmatter.title}`); 159 162 160 163 try { 161 164 // Handle cover image upload ··· 168 171 ); 169 172 170 173 if (imagePath) { 171 - consola.info(` Uploading cover image: ${path.basename(imagePath)}`); 174 + log.info(` Uploading cover image: ${path.basename(imagePath)}`); 172 175 coverImage = await uploadImage(agent, imagePath); 173 176 if (coverImage) { 174 - consola.info(` Uploaded image blob: ${coverImage.ref.$link}`); 177 + log.info(` Uploaded image blob: ${coverImage.ref.$link}`); 175 178 } 176 179 } else { 177 - consola.warn(` Cover image not found: ${post.frontmatter.ogImage}`); 180 + log.warn(` Cover image not found: ${post.frontmatter.ogImage}`); 178 181 } 179 182 } 180 183 ··· 184 187 185 188 if (action === "create") { 186 189 atUri = await createDocument(agent, post, config, coverImage); 187 - consola.success(` Created: ${atUri}`); 190 + s.stop(`Created: ${atUri}`); 188 191 189 192 // Update frontmatter with atUri 190 193 const updatedContent = updateFrontmatterWithAtUri(post.rawContent, atUri); 191 194 await Bun.write(post.filePath, updatedContent); 192 - consola.info(` Updated frontmatter in ${path.basename(post.filePath)}`); 195 + log.info(` Updated frontmatter in ${path.basename(post.filePath)}`); 193 196 194 197 // Use updated content (with atUri) for hash so next run sees matching hash 195 198 contentForHash = updatedContent; ··· 197 200 } else { 198 201 atUri = post.frontmatter.atUri!; 199 202 await updateDocument(agent, post, atUri, config, coverImage); 200 - consola.success(` Updated: ${atUri}`); 203 + s.stop(`Updated: ${atUri}`); 201 204 202 205 // For updates, rawContent already has atUri 203 206 contentForHash = post.rawContent; ··· 214 217 }; 215 218 } catch (error) { 216 219 const errorMessage = error instanceof Error ? error.message : String(error); 217 - consola.error(` Error publishing "${path.basename(post.filePath)}": ${errorMessage}`); 220 + s.stop(`Error publishing "${path.basename(post.filePath)}"`); 221 + log.error(` ${errorMessage}`); 218 222 errorCount++; 219 223 } 220 224 } ··· 223 227 await saveState(configDir, state); 224 228 225 229 // Summary 226 - consola.log("\n---"); 227 - consola.info(`Published: ${publishedCount}`); 228 - consola.info(`Updated: ${updatedCount}`); 230 + log.message("\n---"); 231 + log.info(`Published: ${publishedCount}`); 232 + log.info(`Updated: ${updatedCount}`); 229 233 if (errorCount > 0) { 230 - consola.warn(`Errors: ${errorCount}`); 234 + log.warn(`Errors: ${errorCount}`); 231 235 } 232 236 }, 233 237 });
+42 -39
packages/cli/src/commands/sync.ts
··· 1 1 import { command, flag } from "cmd-ts"; 2 - import { consola } from "consola"; 2 + import { select, spinner, log } from "@clack/prompts"; 3 3 import * as path from "path"; 4 4 import { loadConfig, loadState, saveState, findConfig } from "../lib/config"; 5 5 import { loadCredentials, listCredentials, getCredentials } from "../lib/credentials"; 6 6 import { createAgent, listDocuments } from "../lib/atproto"; 7 7 import { scanContentDirectory, getContentHash, updateFrontmatterWithAtUri } from "../lib/markdown"; 8 + import { exitOnCancel } from "../lib/prompts"; 8 9 9 10 export const syncCommand = command({ 10 11 name: "sync", ··· 25 26 // Load config 26 27 const configPath = await findConfig(); 27 28 if (!configPath) { 28 - consola.error("No sequoia.json found. Run 'sequoia init' first."); 29 + log.error("No sequoia.json found. Run 'sequoia init' first."); 29 30 process.exit(1); 30 31 } 31 32 32 33 const config = await loadConfig(configPath); 33 34 const configDir = path.dirname(configPath); 34 35 35 - consola.info(`Site: ${config.siteUrl}`); 36 - consola.info(`Publication: ${config.publicationUri}`); 36 + log.info(`Site: ${config.siteUrl}`); 37 + log.info(`Publication: ${config.publicationUri}`); 37 38 38 39 // Load credentials 39 40 let credentials = await loadCredentials(config.identity); ··· 41 42 if (!credentials) { 42 43 const identities = await listCredentials(); 43 44 if (identities.length === 0) { 44 - consola.error("No credentials found. Run 'sequoia auth' first."); 45 + log.error("No credentials found. Run 'sequoia auth' first."); 45 46 process.exit(1); 46 47 } 47 48 48 - consola.info("Multiple identities found. Select one to use:"); 49 - const selected = await consola.prompt("Identity:", { 50 - type: "select", 51 - options: identities, 52 - }); 49 + log.info("Multiple identities found. Select one to use:"); 50 + const selected = exitOnCancel(await select({ 51 + message: "Identity:", 52 + options: identities.map(id => ({ value: id, label: id })), 53 + })); 53 54 54 - credentials = await getCredentials(selected as string); 55 + credentials = await getCredentials(selected); 55 56 if (!credentials) { 56 - consola.error("Failed to load selected credentials."); 57 + log.error("Failed to load selected credentials."); 57 58 process.exit(1); 58 59 } 59 60 } 60 61 61 62 // Create agent 62 - consola.start(`Connecting to ${credentials.pdsUrl}...`); 63 + const s = spinner(); 64 + s.start(`Connecting to ${credentials.pdsUrl}...`); 63 65 let agent; 64 66 try { 65 67 agent = await createAgent(credentials); 66 - consola.success(`Logged in as ${agent.session?.handle}`); 68 + s.stop(`Logged in as ${agent.session?.handle}`); 67 69 } catch (error) { 68 - consola.error("Failed to login:", error); 70 + s.stop("Failed to login"); 71 + log.error(`Failed to login: ${error}`); 69 72 process.exit(1); 70 73 } 71 74 72 75 // Fetch documents from PDS 73 - consola.start("Fetching documents from PDS..."); 76 + s.start("Fetching documents from PDS..."); 74 77 const documents = await listDocuments(agent, config.publicationUri); 75 - consola.info(`Found ${documents.length} documents on PDS`); 78 + s.stop(`Found ${documents.length} documents on PDS`); 76 79 77 80 if (documents.length === 0) { 78 - consola.info("No documents found for this publication."); 81 + log.info("No documents found for this publication."); 79 82 return; 80 83 } 81 84 ··· 85 88 : path.join(configDir, config.contentDir); 86 89 87 90 // Scan local posts 88 - consola.start("Scanning local content..."); 91 + s.start("Scanning local content..."); 89 92 const localPosts = await scanContentDirectory(contentDir, config.frontmatter); 90 - consola.info(`Found ${localPosts.length} local posts`); 93 + s.stop(`Found ${localPosts.length} local posts`); 91 94 92 95 // Build a map of path -> local post for matching 93 96 // Document path is like /posts/my-post-slug ··· 106 109 let unmatchedCount = 0; 107 110 let frontmatterUpdates: Array<{ filePath: string; atUri: string }> = []; 108 111 109 - consola.log("\nMatching documents to local files:\n"); 112 + log.message("\nMatching documents to local files:\n"); 110 113 111 114 for (const doc of documents) { 112 115 const docPath = doc.value.path; ··· 114 117 115 118 if (localPost) { 116 119 matchedCount++; 117 - consola.log(` ✓ ${doc.value.title}`); 118 - consola.log(` Path: ${docPath}`); 119 - consola.log(` URI: ${doc.uri}`); 120 - consola.log(` File: ${path.basename(localPost.filePath)}`); 120 + log.message(` ✓ ${doc.value.title}`); 121 + log.message(` Path: ${docPath}`); 122 + log.message(` URI: ${doc.uri}`); 123 + log.message(` File: ${path.basename(localPost.filePath)}`); 121 124 122 125 // Update state (use relative path from config directory) 123 126 const contentHash = await getContentHash(localPost.rawContent); ··· 134 137 filePath: localPost.filePath, 135 138 atUri: doc.uri, 136 139 }); 137 - consola.log(` → Will update frontmatter`); 140 + log.message(` → Will update frontmatter`); 138 141 } 139 142 } else { 140 143 unmatchedCount++; 141 - consola.log(` ✗ ${doc.value.title} (no matching local file)`); 142 - consola.log(` Path: ${docPath}`); 143 - consola.log(` URI: ${doc.uri}`); 144 + log.message(` ✗ ${doc.value.title} (no matching local file)`); 145 + log.message(` Path: ${docPath}`); 146 + log.message(` URI: ${doc.uri}`); 144 147 } 145 - consola.log(""); 148 + log.message(""); 146 149 } 147 150 148 151 // Summary 149 - consola.log("---"); 150 - consola.info(`Matched: ${matchedCount} documents`); 152 + log.message("---"); 153 + log.info(`Matched: ${matchedCount} documents`); 151 154 if (unmatchedCount > 0) { 152 - consola.warn(`Unmatched: ${unmatchedCount} documents (exist on PDS but not locally)`); 155 + log.warn(`Unmatched: ${unmatchedCount} documents (exist on PDS but not locally)`); 153 156 } 154 157 155 158 if (dryRun) { 156 - consola.info("\nDry run complete. No changes made."); 159 + log.info("\nDry run complete. No changes made."); 157 160 return; 158 161 } 159 162 160 163 // Save updated state 161 164 await saveState(configDir, state); 162 165 const newPostCount = Object.keys(state.posts).length; 163 - consola.success(`\nSaved .sequoia-state.json (${originalPostCount} → ${newPostCount} entries)`); 166 + log.success(`\nSaved .sequoia-state.json (${originalPostCount} → ${newPostCount} entries)`); 164 167 165 168 // Update frontmatter if requested 166 169 if (frontmatterUpdates.length > 0) { 167 - consola.start(`Updating frontmatter in ${frontmatterUpdates.length} files...`); 170 + s.start(`Updating frontmatter in ${frontmatterUpdates.length} files...`); 168 171 for (const { filePath, atUri } of frontmatterUpdates) { 169 172 const file = Bun.file(filePath); 170 173 const content = await file.text(); 171 174 const updated = updateFrontmatterWithAtUri(content, atUri); 172 175 await Bun.write(filePath, updated); 173 - consola.log(` Updated: ${path.basename(filePath)}`); 176 + log.message(` Updated: ${path.basename(filePath)}`); 174 177 } 175 - consola.success("Frontmatter updated"); 178 + s.stop("Frontmatter updated"); 176 179 } 177 180 178 - consola.success("\nSync complete!"); 181 + log.success("\nSync complete!"); 179 182 }, 180 183 });
+9
packages/cli/src/lib/prompts.ts
··· 1 + import { isCancel, cancel } from "@clack/prompts"; 2 + 3 + export function exitOnCancel<T>(value: T | symbol): T { 4 + if (isCancel(value)) { 5 + cancel("Cancelled"); 6 + process.exit(0); 7 + } 8 + return value as T; 9 + }