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

Compare changes

Choose any two refs to compare.

Changed files
+177 -55
cli
public
editor
tabs
src
+1 -1
cli/Cargo.lock
··· 4913 4914 [[package]] 4915 name = "wisp-cli" 4916 - version = "0.1.0" 4917 dependencies = [ 4918 "axum", 4919 "base64 0.22.1",
··· 4913 4914 [[package]] 4915 name = "wisp-cli" 4916 + version = "0.2.0" 4917 dependencies = [ 4918 "axum", 4919 "base64 0.22.1",
+1 -1
cli/Cargo.toml
··· 1 [package] 2 name = "wisp-cli" 3 - version = "0.1.0" 4 edition = "2024" 5 6 [features]
··· 1 [package] 2 name = "wisp-cli" 3 + version = "0.2.0" 4 edition = "2024" 5 6 [features]
+28 -1
crates.nix
··· 19 targets.x86_64-pc-windows-gnu.latest.rust-std 20 targets.x86_64-unknown-linux-gnu.latest.rust-std 21 targets.aarch64-apple-darwin.latest.rust-std 22 ]; 23 # configure crates 24 nci.crates."wisp-cli" = { ··· 26 dev.runTests = false; 27 release.runTests = false; 28 }; 29 - targets."x86_64-unknown-linux-gnu" = { 30 default = true; 31 }; 32 targets."x86_64-pc-windows-gnu" = let 33 targetPkgs = pkgs.pkgsCross.mingwW64; ··· 46 }; 47 targets."aarch64-apple-darwin" = let 48 targetPkgs = pkgs.pkgsCross.aarch64-darwin; 49 targetCC = targetPkgs.stdenv.cc; 50 targetCargoEnvVarTarget = targetPkgs.stdenv.hostPlatform.rust.cargoEnvVarTarget; 51 in rec {
··· 19 targets.x86_64-pc-windows-gnu.latest.rust-std 20 targets.x86_64-unknown-linux-gnu.latest.rust-std 21 targets.aarch64-apple-darwin.latest.rust-std 22 + targets.aarch64-unknown-linux-gnu.latest.rust-std 23 ]; 24 # configure crates 25 nci.crates."wisp-cli" = { ··· 27 dev.runTests = false; 28 release.runTests = false; 29 }; 30 + targets."x86_64-unknown-linux-gnu" = let 31 + targetPkgs = pkgs.pkgsCross.gnu64; 32 + targetCC = targetPkgs.stdenv.cc; 33 + targetCargoEnvVarTarget = targetPkgs.stdenv.hostPlatform.rust.cargoEnvVarTarget; 34 + in rec { 35 default = true; 36 + depsDrvConfig.mkDerivation = { 37 + nativeBuildInputs = [targetCC]; 38 + }; 39 + depsDrvConfig.env = rec { 40 + TARGET_CC = "${targetCC.targetPrefix}cc"; 41 + "CARGO_TARGET_${targetCargoEnvVarTarget}_LINKER" = TARGET_CC; 42 + }; 43 + drvConfig = depsDrvConfig; 44 }; 45 targets."x86_64-pc-windows-gnu" = let 46 targetPkgs = pkgs.pkgsCross.mingwW64; ··· 59 }; 60 targets."aarch64-apple-darwin" = let 61 targetPkgs = pkgs.pkgsCross.aarch64-darwin; 62 + targetCC = targetPkgs.stdenv.cc; 63 + targetCargoEnvVarTarget = targetPkgs.stdenv.hostPlatform.rust.cargoEnvVarTarget; 64 + in rec { 65 + depsDrvConfig.mkDerivation = { 66 + nativeBuildInputs = [targetCC]; 67 + }; 68 + depsDrvConfig.env = rec { 69 + TARGET_CC = "${targetCC.targetPrefix}cc"; 70 + "CARGO_TARGET_${targetCargoEnvVarTarget}_LINKER" = TARGET_CC; 71 + }; 72 + drvConfig = depsDrvConfig; 73 + }; 74 + targets."aarch64-unknown-linux-gnu" = let 75 + targetPkgs = pkgs.pkgsCross.aarch64-multiplatform; 76 targetCC = targetPkgs.stdenv.cc; 77 targetCargoEnvVarTarget = targetPkgs.stdenv.hostPlatform.rust.cargoEnvVarTarget; 78 in rec {
+25 -2
flake.nix
··· 26 ... 27 }: let 28 crateOutputs = config.nci.outputs."wisp-cli"; 29 in { 30 devShells.default = crateOutputs.devShell; 31 packages.default = crateOutputs.packages.release; 32 - packages.wisp-cli-windows = crateOutputs.allTargets."x86_64-pc-windows-gnu".packages.release; 33 - packages.wisp-cli-darwin = crateOutputs.allTargets."aarch64-apple-darwin".packages.release; 34 }; 35 }; 36 }
··· 26 ... 27 }: let 28 crateOutputs = config.nci.outputs."wisp-cli"; 29 + mkRenamedPackage = name: pkg: isWindows: pkgs.runCommand name {} '' 30 + mkdir -p $out/bin 31 + if [ -f ${pkg}/bin/wisp-cli.exe ]; then 32 + cp ${pkg}/bin/wisp-cli.exe $out/bin/${name} 33 + elif [ -f ${pkg}/bin/wisp-cli ]; then 34 + cp ${pkg}/bin/wisp-cli $out/bin/${name} 35 + else 36 + echo "Error: Could not find wisp-cli binary in ${pkg}/bin/" 37 + ls -la ${pkg}/bin/ || true 38 + exit 1 39 + fi 40 + ''; 41 in { 42 devShells.default = crateOutputs.devShell; 43 packages.default = crateOutputs.packages.release; 44 + packages.wisp-cli-x86_64-linux = mkRenamedPackage "wisp-cli-x86_64-linux" crateOutputs.packages.release false; 45 + packages.wisp-cli-aarch64-linux = mkRenamedPackage "wisp-cli-aarch64-linux" crateOutputs.allTargets."aarch64-unknown-linux-gnu".packages.release false; 46 + packages.wisp-cli-x86_64-windows = mkRenamedPackage "wisp-cli-x86_64-windows.exe" crateOutputs.allTargets."x86_64-pc-windows-gnu".packages.release true; 47 + packages.wisp-cli-aarch64-darwin = mkRenamedPackage "wisp-cli-aarch64-darwin" crateOutputs.allTargets."aarch64-apple-darwin".packages.release false; 48 + packages.all = pkgs.symlinkJoin { 49 + name = "wisp-cli-all"; 50 + paths = [ 51 + config.packages.wisp-cli-x86_64-linux 52 + config.packages.wisp-cli-aarch64-linux 53 + config.packages.wisp-cli-x86_64-windows 54 + config.packages.wisp-cli-aarch64-darwin 55 + ]; 56 + }; 57 }; 58 }; 59 }
+78 -14
public/editor/tabs/CLITab.tsx
··· 16 <CardHeader> 17 <div className="flex items-center gap-2 mb-2"> 18 <CardTitle>Wisp CLI Tool</CardTitle> 19 - <Badge variant="secondary" className="text-xs">v0.1.0</Badge> 20 <Badge variant="outline" className="text-xs">Alpha</Badge> 21 </div> 22 <CardDescription> ··· 32 </div> 33 34 <div className="space-y-3"> 35 - <h3 className="text-sm font-semibold">Download CLI</h3> 36 <div className="grid gap-2"> 37 <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> 38 <a 39 - href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-macos-arm64" 40 target="_blank" 41 rel="noopener noreferrer" 42 className="flex items-center justify-between mb-2" ··· 45 <ExternalLink className="w-4 h-4 text-muted-foreground" /> 46 </a> 47 <div className="text-xs text-muted-foreground"> 48 - <span className="font-mono">SHA256: 637e325d9668ca745e01493d80dfc72447ef0a889b313e28913ca65c94c7aaae</span> 49 </div> 50 </div> 51 <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> ··· 59 <ExternalLink className="w-4 h-4 text-muted-foreground" /> 60 </a> 61 <div className="text-xs text-muted-foreground"> 62 - <span className="font-mono">SHA256: 01561656b64826f95b39f13c65c97da8bcc63ecd9f4d7e4e369c8ba8c903c22a</span> 63 </div> 64 </div> 65 <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> ··· 73 <ExternalLink className="w-4 h-4 text-muted-foreground" /> 74 </a> 75 <div className="text-xs text-muted-foreground"> 76 - <span className="font-mono">SHA256: 1ff485b9bcf89bc5721a862863c4843cf4530cbcd2489cf200cb24a44f7865a2</span> 77 </div> 78 </div> 79 </div> 80 </div> 81 82 <div className="space-y-3"> 83 - <h3 className="text-sm font-semibold">Basic Usage</h3> 84 <CodeBlock 85 code={`# Download and make executable 86 - curl -O https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-macos-arm64 87 - chmod +x wisp-cli-macos-arm64 88 89 - # Deploy your site (will use OAuth) 90 - ./wisp-cli-macos-arm64 your-handle.bsky.social \\ 91 --path ./dist \\ 92 - --site my-site 93 94 # Your site will be available at: 95 # https://sites.wisp.place/your-handle/my-site`} ··· 98 </div> 99 100 <div className="space-y-3"> 101 <h3 className="text-sm font-semibold">CI/CD with Tangled Spindle</h3> 102 <p className="text-xs text-muted-foreground"> 103 Deploy automatically on every push using{' '} ··· 147 chmod +x wisp-cli 148 149 # Deploy to Wisp 150 - ./wisp-cli \\ 151 "$WISP_HANDLE" \\ 152 --path "$SITE_PATH" \\ 153 --site "$SITE_NAME" \\ ··· 210 chmod +x wisp-cli 211 212 # Deploy to Wisp 213 - ./wisp-cli \\ 214 "$WISP_HANDLE" \\ 215 --path "$SITE_PATH" \\ 216 --site "$SITE_NAME" \\
··· 16 <CardHeader> 17 <div className="flex items-center gap-2 mb-2"> 18 <CardTitle>Wisp CLI Tool</CardTitle> 19 + <Badge variant="secondary" className="text-xs">v0.2.0</Badge> 20 <Badge variant="outline" className="text-xs">Alpha</Badge> 21 </div> 22 <CardDescription> ··· 32 </div> 33 34 <div className="space-y-3"> 35 + <h3 className="text-sm font-semibold">Features</h3> 36 + <ul className="text-sm text-muted-foreground space-y-2 list-disc list-inside"> 37 + <li><strong>Deploy:</strong> Push static sites directly from your terminal</li> 38 + <li><strong>Pull:</strong> Download sites from the PDS for development or backup</li> 39 + <li><strong>Serve:</strong> Run a local server with real-time firehose updates</li> 40 + </ul> 41 + </div> 42 + 43 + <div className="space-y-3"> 44 + <h3 className="text-sm font-semibold">Download v0.2.0</h3> 45 <div className="grid gap-2"> 46 <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> 47 <a 48 + href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-aarch64-darwin" 49 target="_blank" 50 rel="noopener noreferrer" 51 className="flex items-center justify-between mb-2" ··· 54 <ExternalLink className="w-4 h-4 text-muted-foreground" /> 55 </a> 56 <div className="text-xs text-muted-foreground"> 57 + <span className="font-mono">SHA-1: a8c27ea41c5e2672bfecb3476ece1c801741d759</span> 58 </div> 59 </div> 60 <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> ··· 68 <ExternalLink className="w-4 h-4 text-muted-foreground" /> 69 </a> 70 <div className="text-xs text-muted-foreground"> 71 + <span className="font-mono">SHA-1: fd7ee689c7600fc953179ea755b0357c8481a622</span> 72 </div> 73 </div> 74 <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> ··· 82 <ExternalLink className="w-4 h-4 text-muted-foreground" /> 83 </a> 84 <div className="text-xs text-muted-foreground"> 85 + <span className="font-mono">SHA-1: 8bca6992559e19e1d29ab3d2fcc6d09b28e5a485</span> 86 + </div> 87 + </div> 88 + <div className="p-3 bg-muted/50 hover:bg-muted rounded-lg transition-colors border border-border"> 89 + <a 90 + href="https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-x86_64-windows.exe" 91 + target="_blank" 92 + rel="noopener noreferrer" 93 + className="flex items-center justify-between mb-2" 94 + > 95 + <span className="font-mono text-sm">Windows (x86_64)</span> 96 + <ExternalLink className="w-4 h-4 text-muted-foreground" /> 97 + </a> 98 + <div className="text-xs text-muted-foreground"> 99 + <span className="font-mono">SHA-1: 90ea3987a06597fa6c42e1df9009e9758e92dd54</span> 100 </div> 101 </div> 102 </div> 103 </div> 104 105 <div className="space-y-3"> 106 + <h3 className="text-sm font-semibold">Deploy a Site</h3> 107 <CodeBlock 108 code={`# Download and make executable 109 + curl -O https://sites.wisp.place/nekomimi.pet/wisp-cli-binaries/wisp-cli-aarch64-darwin 110 + chmod +x wisp-cli-aarch64-darwin 111 112 + # Deploy your site 113 + ./wisp-cli-aarch64-darwin deploy your-handle.bsky.social \\ 114 --path ./dist \\ 115 + --site my-site \\ 116 + --password your-app-password 117 118 # Your site will be available at: 119 # https://sites.wisp.place/your-handle/my-site`} ··· 122 </div> 123 124 <div className="space-y-3"> 125 + <h3 className="text-sm font-semibold">Pull a Site from PDS</h3> 126 + <p className="text-xs text-muted-foreground"> 127 + Download a site from the PDS to your local machine (uses OAuth authentication): 128 + </p> 129 + <CodeBlock 130 + code={`# Pull a site to a specific directory 131 + wisp-cli pull your-handle.bsky.social \\ 132 + --site my-site \\ 133 + --output ./my-site 134 + 135 + # Pull to current directory 136 + wisp-cli pull your-handle.bsky.social \\ 137 + --site my-site 138 + 139 + # Opens browser for OAuth authentication on first run`} 140 + language="bash" 141 + /> 142 + </div> 143 + 144 + <div className="space-y-3"> 145 + <h3 className="text-sm font-semibold">Serve a Site Locally with Real-Time Updates</h3> 146 + <p className="text-xs text-muted-foreground"> 147 + Run a local server that monitors the firehose for real-time updates (uses OAuth authentication): 148 + </p> 149 + <CodeBlock 150 + code={`# Serve on http://localhost:8080 (default) 151 + wisp-cli serve your-handle.bsky.social \\ 152 + --site my-site 153 + 154 + # Serve on a custom port 155 + wisp-cli serve your-handle.bsky.social \\ 156 + --site my-site \\ 157 + --port 3000 158 + 159 + # Downloads site, serves it, and watches firehose for live updates!`} 160 + language="bash" 161 + /> 162 + </div> 163 + 164 + <div className="space-y-3"> 165 <h3 className="text-sm font-semibold">CI/CD with Tangled Spindle</h3> 166 <p className="text-xs text-muted-foreground"> 167 Deploy automatically on every push using{' '} ··· 211 chmod +x wisp-cli 212 213 # Deploy to Wisp 214 + ./wisp-cli deploy \\ 215 "$WISP_HANDLE" \\ 216 --path "$SITE_PATH" \\ 217 --site "$SITE_NAME" \\ ··· 274 chmod +x wisp-cli 275 276 # Deploy to Wisp 277 + ./wisp-cli deploy \\ 278 "$WISP_HANDLE" \\ 279 --path "$SITE_PATH" \\ 280 --site "$SITE_NAME" \\
+5 -5
src/index.ts
··· 70 }, 71 cookie: { 72 secrets: cookieSecret, 73 - sign: true 74 } 75 }) 76 // Observability middleware ··· 105 .onError(observabilityMiddleware('main-app').onError) 106 .use(csrfProtection()) 107 .use(authRoutes(client, cookieSecret)) 108 - .use(wispRoutes(client)) 109 - .use(domainRoutes(client)) 110 - .use(userRoutes(client)) 111 - .use(siteRoutes(client)) 112 .use(adminRoutes(cookieSecret)) 113 .use( 114 await staticPlugin({
··· 70 }, 71 cookie: { 72 secrets: cookieSecret, 73 + sign: ['did'] 74 } 75 }) 76 // Observability middleware ··· 105 .onError(observabilityMiddleware('main-app').onError) 106 .use(csrfProtection()) 107 .use(authRoutes(client, cookieSecret)) 108 + .use(wispRoutes(client, cookieSecret)) 109 + .use(domainRoutes(client, cookieSecret)) 110 + .use(userRoutes(client, cookieSecret)) 111 + .use(siteRoutes(client, cookieSecret)) 112 .use(adminRoutes(cookieSecret)) 113 .use( 114 await staticPlugin({
+6 -22
src/routes/auth.ts
··· 5 import { authenticateRequest } from '../lib/wisp-auth' 6 import { logger } from '../lib/observability' 7 8 - export const authRoutes = (client: NodeOAuthClient, cookieSecret: string) => new Elysia() 9 .post('/api/auth/signin', async (c) => { 10 let handle = 'unknown' 11 try { ··· 74 c.cookie.did.remove() 75 return c.redirect('/?error=auth_failed') 76 } 77 - }, { 78 - cookie: t.Cookie({ 79 - did: t.Optional(t.String()) 80 - }, { 81 - secrets: cookieSecret, 82 - sign: ['did'] 83 - }) 84 }) 85 .post('/api/auth/logout', async (c) => { 86 try { ··· 106 logger.error('[Auth] Logout error', err) 107 return { error: 'Logout failed' } 108 } 109 - }, { 110 - cookie: t.Cookie({ 111 - did: t.Optional(t.String()) 112 - }, { 113 - secrets: cookieSecret, 114 - sign: ['did'] 115 - }) 116 }) 117 .get('/api/auth/status', async (c) => { 118 try { ··· 132 c.cookie.did.remove() 133 return { authenticated: false } 134 } 135 - }, { 136 - cookie: t.Cookie({ 137 - did: t.Optional(t.String()) 138 - }, { 139 - secrets: cookieSecret, 140 - sign: ['did'] 141 - }) 142 })
··· 5 import { authenticateRequest } from '../lib/wisp-auth' 6 import { logger } from '../lib/observability' 7 8 + export const authRoutes = (client: NodeOAuthClient, cookieSecret: string) => new Elysia({ 9 + cookie: { 10 + secrets: cookieSecret, 11 + sign: ['did'] 12 + } 13 + }) 14 .post('/api/auth/signin', async (c) => { 15 let handle = 'unknown' 16 try { ··· 79 c.cookie.did.remove() 80 return c.redirect('/?error=auth_failed') 81 } 82 }) 83 .post('/api/auth/logout', async (c) => { 84 try { ··· 104 logger.error('[Auth] Logout error', err) 105 return { error: 'Logout failed' } 106 } 107 }) 108 .get('/api/auth/status', async (c) => { 109 try { ··· 123 c.cookie.did.remove() 124 return { authenticated: false } 125 } 126 })
+8 -2
src/routes/domain.ts
··· 24 import { verifyCustomDomain } from '../lib/dns-verify' 25 import { logger } from '../lib/logger' 26 27 - export const domainRoutes = (client: NodeOAuthClient) => 28 - new Elysia({ prefix: '/api/domain' }) 29 // Public endpoints (no auth required) 30 .get('/check', async ({ query }) => { 31 try {
··· 24 import { verifyCustomDomain } from '../lib/dns-verify' 25 import { logger } from '../lib/logger' 26 27 + export const domainRoutes = (client: NodeOAuthClient, cookieSecret: string) => 28 + new Elysia({ 29 + prefix: '/api/domain', 30 + cookie: { 31 + secrets: cookieSecret, 32 + sign: ['did'] 33 + } 34 + }) 35 // Public endpoints (no auth required) 36 .get('/check', async ({ query }) => { 37 try {
+8 -2
src/routes/site.ts
··· 5 import { deleteSite } from '../lib/db' 6 import { logger } from '../lib/logger' 7 8 - export const siteRoutes = (client: NodeOAuthClient) => 9 - new Elysia({ prefix: '/api/site' }) 10 .derive(async ({ cookie }) => { 11 const auth = await requireAuth(client, cookie) 12 return { auth }
··· 5 import { deleteSite } from '../lib/db' 6 import { logger } from '../lib/logger' 7 8 + export const siteRoutes = (client: NodeOAuthClient, cookieSecret: string) => 9 + new Elysia({ 10 + prefix: '/api/site', 11 + cookie: { 12 + secrets: cookieSecret, 13 + sign: ['did'] 14 + } 15 + }) 16 .derive(async ({ cookie }) => { 17 const auth = await requireAuth(client, cookie) 18 return { auth }
+9 -3
src/routes/user.ts
··· 1 - import { Elysia } from 'elysia' 2 import { requireAuth } from '../lib/wisp-auth' 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 import { Agent } from '@atproto/api' ··· 6 import { syncSitesFromPDS } from '../lib/sync-sites' 7 import { logger } from '../lib/logger' 8 9 - export const userRoutes = (client: NodeOAuthClient) => 10 - new Elysia({ prefix: '/api/user' }) 11 .derive(async ({ cookie }) => { 12 const auth = await requireAuth(client, cookie) 13 return { auth }
··· 1 + import { Elysia, t } from 'elysia' 2 import { requireAuth } from '../lib/wisp-auth' 3 import { NodeOAuthClient } from '@atproto/oauth-client-node' 4 import { Agent } from '@atproto/api' ··· 6 import { syncSitesFromPDS } from '../lib/sync-sites' 7 import { logger } from '../lib/logger' 8 9 + export const userRoutes = (client: NodeOAuthClient, cookieSecret: string) => 10 + new Elysia({ 11 + prefix: '/api/user', 12 + cookie: { 13 + secrets: cookieSecret, 14 + sign: ['did'] 15 + } 16 + }) 17 .derive(async ({ cookie }) => { 18 const auth = await requireAuth(client, cookie) 19 return { auth }
+8 -2
src/routes/wisp.ts
··· 37 return true; 38 } 39 40 - export const wispRoutes = (client: NodeOAuthClient) => 41 - new Elysia({ prefix: '/wisp' }) 42 .derive(async ({ cookie }) => { 43 const auth = await requireAuth(client, cookie) 44 return { auth }
··· 37 return true; 38 } 39 40 + export const wispRoutes = (client: NodeOAuthClient, cookieSecret: string) => 41 + new Elysia({ 42 + prefix: '/wisp', 43 + cookie: { 44 + secrets: cookieSecret, 45 + sign: ['did'] 46 + } 47 + }) 48 .derive(async ({ cookie }) => { 49 const auth = await requireAuth(client, cookie) 50 return { auth }