Monorepo for wisp.place. A static site hosting service built on top of the AT Protocol.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 173 lines 5.5 kB view raw
1#!/usr/bin/env bun 2import { Command } from 'commander'; 3import { text, isCancel, cancel, intro, outro } from '@clack/prompts'; 4import { authenticate, clearSessions } from './lib/auth.ts'; 5import { deploy } from './commands/deploy.ts'; 6import { pull } from './commands/pull.ts'; 7import { serve } from './commands/serve.ts'; 8import { pc } from './lib/progress.ts'; 9 10const program = new Command(); 11 12program 13 .name('wisp-cli') 14 .description('CLI for wisp.place - deploy static sites to the AT Protocol') 15 .version('1.0.0'); 16 17// Deploy command (default) 18program 19 .command('deploy [handle]', { isDefault: true }) 20 .description('Deploy a static site to wisp.place') 21 .option('-p, --path <path>', 'Directory to deploy') 22 .option('-s, --site <name>', 'Site name (defaults to directory name)') 23 .option('--directory', 'Enable directory listing') 24 .option('--spa', 'Enable SPA mode (serve index.html for all routes)') 25 .option('-c, --concurrency <n>', 'Number of concurrent uploads (backs off to 2 on rate limit)', '3') 26 .option('--password <password>', 'App password for headless authentication') 27 .option('--store <path>', 'OAuth session store path') 28 .option('-y, --yes', 'Skip confirmation prompts') 29 .action(async (handle: string | undefined, options) => { 30 try { 31 let resolvedHandle = handle; 32 let resolvedPath = options.path; 33 let resolvedSite = options.site; 34 35 // If any required values are missing, show prompts 36 const needsPrompts = !resolvedHandle || !resolvedPath || !resolvedSite; 37 38 if (needsPrompts) { 39 intro(pc.cyan('wisp.place deploy')); 40 41 // Prompt for handle if not provided 42 if (!resolvedHandle) { 43 const handleResult = await text({ 44 message: 'AT Protocol handle', 45 placeholder: 'alice.bsky.social', 46 validate: (value) => { 47 if (!value) return 'Handle is required'; 48 if (!value.includes('.')) return 'Handle must include a domain (e.g., alice.bsky.social)'; 49 } 50 }); 51 52 if (isCancel(handleResult)) { 53 cancel('Deploy cancelled'); 54 process.exit(0); 55 } 56 resolvedHandle = handleResult; 57 } 58 59 // Prompt for path if not provided 60 if (!resolvedPath) { 61 const pathResult = await text({ 62 message: 'Directory to deploy', 63 placeholder: '.', 64 defaultValue: '.' 65 }); 66 67 if (isCancel(pathResult)) { 68 cancel('Deploy cancelled'); 69 process.exit(0); 70 } 71 resolvedPath = pathResult || '.'; 72 } 73 74 // Prompt for site name if not provided 75 if (!resolvedSite) { 76 const siteResult = await text({ 77 message: 'Site name', 78 placeholder: 'my-website', 79 validate: (value) => { 80 if (!value) return 'Site name is required'; 81 if (!/^[a-zA-Z0-9._~:-]{1,512}$/.test(value)) { 82 return 'Site name must be 1-512 characters of [a-zA-Z0-9._~:-]'; 83 } 84 } 85 }); 86 87 if (isCancel(siteResult)) { 88 cancel('Deploy cancelled'); 89 process.exit(0); 90 } 91 resolvedSite = siteResult; 92 } 93 } 94 95 const { agent, did } = await authenticate(resolvedHandle!, { 96 appPassword: options.password, 97 storePath: options.store 98 }); 99 100 const result = await deploy(agent, did, { 101 path: resolvedPath, 102 site: resolvedSite, 103 directory: options.directory, 104 spa: options.spa, 105 yes: options.yes, 106 concurrency: parseInt(options.concurrency, 10) 107 }); 108 109 console.log(); 110 console.log(pc.dim(` URI: ${result.uri}`)); 111 console.log(pc.cyan(` URL: ${result.url}`)); 112 113 if (needsPrompts) { 114 outro(pc.green('Deployed successfully!')); 115 } else { 116 console.log(); 117 console.log(pc.green('✓ Deployed successfully!')); 118 } 119 process.exit(0); 120 } catch (err: any) { 121 console.error(pc.red(`\nError: ${err.message}\n`)); 122 process.exit(1); 123 } 124 }); 125 126// Pull command 127program 128 .command('pull <handle>') 129 .description('Download a site from wisp.place to a local directory') 130 .requiredOption('-s, --site <name>', 'Site name to pull') 131 .option('-p, --path <path>', 'Output directory', '.') 132 .action(async (handle: string, options) => { 133 try { 134 await pull(handle, { 135 site: options.site, 136 path: options.path 137 }); 138 } catch (err: any) { 139 console.error(pc.red(`\nError: ${err.message}\n`)); 140 process.exit(1); 141 } 142 }); 143 144// Serve command 145program 146 .command('serve <handle>') 147 .description('Serve a site locally with live updates from firehose') 148 .requiredOption('-s, --site <name>', 'Site name to serve') 149 .option('-p, --path <path>', 'Local directory to cache site', '.wisp-serve') 150 .option('-P, --port <port>', 'Port to serve on', '8080') 151 .action(async (handle: string, options) => { 152 try { 153 await serve(handle, { 154 site: options.site, 155 path: options.path, 156 port: parseInt(options.port, 10) 157 }); 158 } catch (err: any) { 159 console.error(pc.red(`\nError: ${err.message}\n`)); 160 process.exit(1); 161 } 162 }); 163 164// Logout command 165program 166 .command('logout') 167 .description('Clear stored OAuth sessions') 168 .option('--store <path>', 'OAuth session store path') 169 .action((options) => { 170 clearSessions(options.store); 171 }); 172 173program.parse();