diff --git a/agents.md b/agents.md index b07492a..18c787b 100644 --- a/agents.md +++ b/agents.md @@ -103,7 +103,7 @@ The project is a monorepo. The package handler it uses is bun. Please when you w Bun workspaces: `packages/@wisp/*`, `apps/main-app`, `apps/hosting-service` -There are two typescript apps +There are three typescript apps **`apps/main-app`** - Main backend (Bun + Elysia) - OAuth authentication and session management @@ -112,14 +112,17 @@ There are two typescript apps - Admin database view in /admin - React frontend in public/ -**`apps/hosting-service`** - CDN static file server (Node + Hono) - -- Watches AT Protocol firehose for `place.wisp.fs` record changes -- Downloads and caches site files to disk +**`apps/hosting-service`** - CDN static file server (Bun + Hono) - Serves sites at `https://sites.wisp.place/{did}/{site-name}` and custom domains - Handles redirects (`_redirects` file support) and routing logic +- Pulls sites from Tiered Storage (packages/@wispplace/tiered-storage) + +**`apps/firehose-service`** - ATProto Firehose consumer (Bun or Node) +- Watches AT Protocol firehose for `place.wisp.*` record changes +- Downloads and caches site files to S3 - Backfill mode for syncing existing sites + ### Shared Packages (`packages/@wisp/*`) - **`lexicons`** - AT Protocol lexicons (`place.wisp.fs`, `place.wisp.subfs`, `place.wisp.settings`) with @@ -130,6 +133,7 @@ There are two typescript apps - **`constants`** - Shared constants (limits, file patterns, default settings) - **`observability`** - OpenTelemetry instrumentation - **`safe-fetch`** - Wrapped fetch with timeout/retry logic +- **`tiered-storage`** - KV caching where reads bubble up from cold tier to warm/hot tier and writes bubble down from selected tier down. Streaming as well as buffering support. Used to store files in S3 cold tier as source of truth ### CLI diff --git a/apps/hosting-service/src/index.ts b/apps/hosting-service/src/index.ts index 6369afa..5af7067 100644 --- a/apps/hosting-service/src/index.ts +++ b/apps/hosting-service/src/index.ts @@ -79,8 +79,6 @@ Tiered Storage Configuration: S3 Region: ${storageConfig.s3Region} S3 Endpoint: ${storageConfig.s3Endpoint} S3 Prefix: ${storageConfig.s3Prefix} - -Firehose: DISABLED (read-only) `); // Graceful shutdown diff --git a/apps/main-app/public/editor/editor.tsx b/apps/main-app/public/editor/editor.tsx index fb17c74..c4a8501 100644 --- a/apps/main-app/public/editor/editor.tsx +++ b/apps/main-app/public/editor/editor.tsx @@ -483,7 +483,7 @@ function Dashboard() { {/* Domains Tab */} - + {/* Upload Tab */} - + {/* CLI Tab */} - + diff --git a/apps/main-app/public/editor/tabs/CLITab.tsx b/apps/main-app/public/editor/tabs/CLITab.tsx index 716550e..8dee1f6 100644 --- a/apps/main-app/public/editor/tabs/CLITab.tsx +++ b/apps/main-app/public/editor/tabs/CLITab.tsx @@ -1,10 +1,3 @@ -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@public/components/ui/card"; import { Badge } from "@public/components/ui/badge"; import { Download, ExternalLink } from "lucide-react"; import { CodeBlock } from "@public/components/ui/code-block"; @@ -16,242 +9,215 @@ const BINARIES = [ { platform: "macOS (Apple Silicon)", filename: "wisp-cli-aarch64-darwin", - sha256: - "06544b3a3e27a4b8d7b3a46a39fb7205cf90b3061e19fe533b090facd604f375", + sha256: "06544b3a3e27a4b8d7b3a46a39fb7205cf90b3061e19fe533b090facd604f375", }, { platform: "macOS (Intel)", filename: "wisp-cli-x86_64-darwin", - sha256: - "9ec523e3ceef927b37adc52d449dcd9e13ea84fa49b0b77f0d5932c94cfe262e", + sha256: "9ec523e3ceef927b37adc52d449dcd9e13ea84fa49b0b77f0d5932c94cfe262e", }, { platform: "Linux (ARM64)", filename: "wisp-cli-aarch64-linux", - sha256: - "42a262668e13dce36173a4096cdc2b22358b805cf192335f84534c7f695d395b", + sha256: "42a262668e13dce36173a4096cdc2b22358b805cf192335f84534c7f695d395b", }, { platform: "Linux (x86_64)", filename: "wisp-cli-x86_64-linux", - sha256: - "589ee59f3959ddfbc12fea38d2bcb91701f1362f560ae6fd506bebea3150e2cc", - }, -] as const; - -const FEATURES = [ - { label: "Deploy", desc: "Push static sites directly from your terminal" }, - { - label: "Pull", - desc: "Download sites from the PDS for development or backup", - }, - { - label: "Serve", - desc: "Run a local server with real-time firehose updates", - }, - { - label: "Domains", - desc: "Claim, manage, and assign custom domains on wisp.place", + sha256: "589ee59f3959ddfbc12fea38d2bcb91701f1362f560ae6fd506bebea3150e2cc", }, ] as const; const LINKS = [ - { label: "CLI Documentation", href: "https://docs.wisp.place/cli" }, + { label: "Docs", href: "https://docs.wisp.place/cli" }, { - label: "Source Code", + label: "Source", href: "https://tangled.org/@nekomimi.pet/wisp.place-monorepo/tree/main/cli", }, - { label: "Tangled Spindle CI/CD", href: "https://blog.tangled.org/ci" }, + { label: "Spindle CI/CD", href: "https://blog.tangled.org/ci" }, ] as const; -export function CLITab() { +function SectionLabel({ children }: { children: React.ReactNode }) { return ( -
- {/* Header + Features */} - - -
- Wisp CLI - - v1.0.0 - -
- - Deploy static sites directly from your terminal - -
- -
    - {FEATURES.map(({ label, desc }) => ( -
  • - - > - - - {label} — {desc} - -
  • - ))} -
-
-
+

+ {children} +

+ ); +} - {/* Downloads */} - - - Download v1.0.0 - - - {BINARIES.map(({ platform, filename, sha256 }) => ( +export function CLITab() { + return ( +
+ {/* Header */} +
+
+ Wisp CLI + v1.0.0 +
+
+ {LINKS.map(({ label, href }) => ( -
- {platform} - -
- - SHA-256: {sha256} - + {label} +
))} - - - - {/* Basic Usage */} - - - Basic Usage - - -
-

Deploy a Site

- +
-# Available at: -# https://sites.wisp.place/your-handle/my-site`} - language="bash" - /> -
+ {/* Scrollable content */} +
+ {/* Quick install */} +
+ Install
-

Pull a Site

- +
+ $ + npm install -g wispctl + recommended +
+
+ $ + npm create wisp@latest + scaffold a project +
+
+
+ + {/* Binary downloads */} +
+ Binary Downloads v1.0.0 +
+ {BINARIES.map(({ platform, filename, sha256 }) => ( + +
+
{platform}
+
+ sha256: {sha256.slice(0, 12)}… +
+
+ +
+ ))}
+
-
-

Serve with Live Updates

- + Commands + +
+ + + $ + deploy · pull · serve + + + + − + +
+
+

Deploy

+ +
+
+

Pull

+ +
+
+

Serve with live updates

+ -
+./wisp-cli serve your-handle.bsky.social --site my-site --spa`} + language="bash" + /> +
+
+ -
-

Domain Management

- + + + $ + domain · site management + + + + − + +
+ -
- -
-

List Domains & Sites

- -
- - - - {/* CI/CD */} - - - CI/CD with Tangled Spindle - - Deploy automatically on every push using{" "} - - Tangled Spindle - - - - -
-
-

Simple Deploy

- - Copy Files - + language="bash" + />
- + +
+ + + $ + CI/CD — Tangled Spindle + + + + − + +
+
+

Simple deploy

+ -
- -
-
-

React / Vite Build & Deploy

- - Full Build - -
- +
+
+

React / Vite build & deploy

+ -
- -
- Note: Set{" "} - - WISP_APP_PASSWORD - {" "} - as a secret in your Tangled Spindle repository settings. -
- - - - {/* Learn More */} - - - {LINKS.map(({ label, href }) => ( - - {label} - - - ))} - - + language="yaml" + /> +
+

+ Set WISP_APP_PASSWORD as a secret in your Spindle repo settings. +

+
+ +
+
); } diff --git a/apps/main-app/public/editor/tabs/DomainsTab.tsx b/apps/main-app/public/editor/tabs/DomainsTab.tsx index 038f112..aefb5c5 100644 --- a/apps/main-app/public/editor/tabs/DomainsTab.tsx +++ b/apps/main-app/public/editor/tabs/DomainsTab.tsx @@ -1,11 +1,4 @@ -import { useState } from 'react' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from '@public/components/ui/card' +import { useState, useEffect, useRef } from 'react' import { Button } from '@public/components/ui/button' import { Input } from '@public/components/ui/input' import { Label } from '@public/components/ui/label' @@ -51,6 +44,10 @@ interface DomainsTabProps { onCheckWispAvailability: (handle: string) => Promise<{ available: boolean | null }> } +const Kbd = ({ children }: { children: React.ReactNode }) => ( + {children} +) + export function DomainsTab({ wispDomains, customDomains, @@ -79,12 +76,124 @@ export function DomainsTab({ const [viewDomainDNS, setViewDomainDNS] = useState(null) const [copiedField, setCopiedField] = useState(null) + // Keyboard nav state + const [focusedIndex, setFocusedIndex] = useState(0) + const containerRef = useRef(null) + const itemRefs = useRef<(HTMLDivElement | null)[]>([]) + const scrollContainerRef = useRef(null) + + const totalDomains = wispDomains.length + customDomains.length + + // Clamp focusedIndex when domains change + useEffect(() => { + if (totalDomains > 0 && focusedIndex >= totalDomains) { + setFocusedIndex(totalDomains - 1) + } + }, [totalDomains]) + + // Auto-focus when domains first load + useEffect(() => { + if (!domainsLoading && totalDomains > 0 && containerRef.current) { + const timer = setTimeout(() => containerRef.current?.focus(), 100) + return () => clearTimeout(timer) + } + }, [domainsLoading]) + + // Refocus container when a dialog closes + useEffect(() => { + let wasOpen = document.querySelector('[role="dialog"]') !== null + const observer = new MutationObserver(() => { + const isOpen = document.querySelector('[role="dialog"]') !== null + if (wasOpen && !isOpen) setTimeout(() => containerRef.current?.focus(), 50) + wasOpen = isOpen + }) + observer.observe(document.body, { childList: true, subtree: true }) + return () => observer.disconnect() + }, []) + + // Scroll focused item into view + useEffect(() => { + const element = itemRefs.current[focusedIndex] + if (element && scrollContainerRef.current) { + const container = scrollContainerRef.current + const elementRect = element.getBoundingClientRect() + const containerRect = container.getBoundingClientRect() + const isOutOfView = + elementRect.bottom > containerRect.bottom - 50 || + elementRect.top < containerRect.top + 50 + if (isOutOfView) element.scrollIntoView({ behavior: 'smooth', block: 'center' }) + } + }, [focusedIndex]) + + // Keyboard navigation + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + const target = e.target as HTMLElement + const isTyping = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' + const isDialogOpen = document.querySelector('[role="dialog"]') !== null + const hasFocus = containerRef.current?.contains(document.activeElement) + + if (isTyping || isDialogOpen || !hasFocus || totalDomains === 0) return + + const isWisp = focusedIndex < wispDomains.length + const domain = isWisp + ? wispDomains[focusedIndex] + : customDomains[focusedIndex - wispDomains.length] + + switch (e.key) { + case 'ArrowUp': + e.preventDefault() + setFocusedIndex(prev => Math.max(0, prev - 1)) + break + case 'ArrowDown': + e.preventDefault() + setFocusedIndex(prev => Math.min(totalDomains - 1, prev + 1)) + break + case 'd': + e.preventDefault() + if (isWisp) { + onDeleteWispDomain((domain as WispDomain).domain) + } else { + onDeleteCustomDomain((domain as CustomDomain).id) + } + break + case 'v': + if (!isWisp) { + const cd = domain as CustomDomain + if (!cd.verified && verificationStatus[cd.id] !== 'verifying') { + e.preventDefault() + onVerifyDomain(cd.id) + } + } + break + case 'Enter': + if (!isWisp) { + e.preventDefault() + setViewDomainDNS((domain as CustomDomain).id) + } + break + } + } + + window.addEventListener('keydown', handleKeyDown) + return () => window.removeEventListener('keydown', handleKeyDown) + }, [ + totalDomains, + focusedIndex, + wispDomains, + customDomains, + verificationStatus, + onDeleteWispDomain, + onDeleteCustomDomain, + onVerifyDomain + ]) + const copyToClipboard = async (value: string, label: string) => { try { await navigator.clipboard.writeText(value) setCopiedField(label) window.setTimeout(() => { - setCopiedField((current) => (current === label ? null : current)) + setCopiedField(current => (current === label ? null : current)) }, 1400) } catch { setCopiedField(null) @@ -92,26 +201,21 @@ export function DomainsTab({ } const checkWispAvailability = async (handle: string) => { - const trimmedHandle = handle.trim().toLowerCase() - if (!trimmedHandle) { + const trimmed = handle.trim().toLowerCase() + if (!trimmed) { setWispAvailability({ available: null, checking: false }) return } - setWispAvailability({ available: null, checking: true }) - const result = await onCheckWispAvailability(trimmedHandle) + const result = await onCheckWispAvailability(trimmed) setWispAvailability({ available: result.available, checking: false }) } const handleClaimWispDomain = async () => { - const trimmedHandle = wispHandle.trim().toLowerCase() - if (!trimmedHandle) { - alert('Please enter a handle') - return - } - + const trimmed = wispHandle.trim().toLowerCase() + if (!trimmed) { alert('Please enter a handle'); return } setIsClaimingWisp(true) - const result = await onClaimWispDomain(trimmedHandle) + const result = await onClaimWispDomain(trimmed) if (result.success) { setWispHandle('') setWispAvailability({ available: null, checking: false }) @@ -120,305 +224,290 @@ export function DomainsTab({ } const handleAddCustomDomain = async () => { - if (!customDomain) { - alert('Please enter a domain') - return - } - + if (!customDomain) { alert('Please enter a domain'); return } setIsAddingDomain(true) const result = await onAddCustomDomain(customDomain) setIsAddingDomain(false) - if (result.success) { setCustomDomain('') setAddDomainModalOpen(false) - // Automatically show DNS configuration for the newly added domain - if (result.id) { - setViewDomainDNS(result.id) - } + if (result.id) setViewDomainDNS(result.id) } } + const canClaimMore = wispDomains.length < 3 || !!userInfo?.isSupporter + return ( <> -
- - - wisp.place Subdomains - - {userInfo?.isSupporter - ? 'Your free subdomains on the wisp.place network (unlimited as a supporter)' - : 'Your free subdomains on the wisp.place network (up to 3)'} - - - +
{ + const t = e.target as HTMLElement + if (!t.closest('input, textarea, button, select, a, label')) { + containerRef.current?.focus() + } + }} + > + {/* Keyboard hints */} +
+ {totalDomains > 0 ? ( + <> +
+ ↑↓ + navigate +
+ • +
+ ddelete +
+ • +
+ vverify +
+ • +
+ Enterview DNS +
+ + ) : ( + No domains yet — claim a subdomain or add a custom domain below + )} +
+ + {/* Scrollable content */} +
+ + {/* Wisp Domains */} +
+
+

+ Wisp Domains +

+ {!userInfo?.isSupporter && ( + {wispDomains.length}/3 + )} +
+ {domainsLoading ? ( -
-
- {[...Array(2)].map((_, i) => ( +
+ {[...Array(2)].map((_, i) => ( +
+ +
+ ))} +
+ ) : wispDomains.length > 0 ? ( +
+ {wispDomains.map((domain, idx) => { + const isFocused = idx === focusedIndex + return (
{ itemRefs.current[idx] = el }} + className={`flex items-center justify-between p-3 border transition-colors ${ + isFocused + ? 'border-accent bg-accent/10' + : 'border-border/30 hover:bg-muted/10' + }`} > -
+
- - -
- -
- -
- ))} -
-
- -
- - -
- -
-
- ) : ( -
- {wispDomains.length > 0 && ( -
- {wispDomains.map((domain) => ( -
-
-
- - - {domain.domain} - -
- {domain.rkey && ( -

- → Mapped to site: {domain.rkey} -

- )} -
- -
- ))} -
- )} - - {(wispDomains.length < 3 || userInfo?.isSupporter) && ( -
-

- {wispDomains.length === 0 - ? 'Claim your free wisp.place subdomain' - : userInfo?.isSupporter - ? `Claim another wisp.place subdomain (${wispDomains.length} claimed)` - : `Claim another wisp.place subdomain (${wispDomains.length}/3)`} -

-
-
- -
-
- { - setWispHandle(e.target.value) - if (e.target.value.trim()) { - checkWispAvailability(e.target.value) - } else { - setWispAvailability({ available: null, checking: false }) - } - }} - disabled={isClaimingWisp} - className="pr-24" - /> - - .wisp.place - -
+ + {domain.domain} + wisp
- {wispAvailability.checking && ( -

- - Checking availability... -

- )} - {!wispAvailability.checking && wispAvailability.available === true && ( -

- - Available -

- )} - {!wispAvailability.checking && wispAvailability.available === false && ( -

- - Not available + {domain.rkey && ( +

+ → {domain.rkey}

)}
-
- )} + ) + })} +
+ ) : null} - {wispDomains.length === 3 && !userInfo?.isSupporter && ( -
-

- You have claimed the maximum of 3 wisp.place subdomains -

+ {/* Claim form */} + {!domainsLoading && canClaimMore && ( +
+

+ {wispDomains.length === 0 + ? 'Claim your free wisp.place subdomain' + : userInfo?.isSupporter + ? `Claim another (${wispDomains.length} claimed)` + : `Claim another (${wispDomains.length}/3)`} +

+
+ +
+
+ { + setWispHandle(e.target.value) + if (e.target.value.trim()) checkWispAvailability(e.target.value) + else setWispAvailability({ available: null, checking: false }) + }} + onKeyDown={e => { if (e.key === 'Enter') handleClaimWispDomain() }} + disabled={isClaimingWisp} + className="pr-24 h-8 text-sm" + /> + + .wisp.place + +
+
- )} + {wispAvailability.checking && ( +

+ + Checking... +

+ )} + {!wispAvailability.checking && wispAvailability.available === true && ( +

+ + Available +

+ )} + {!wispAvailability.checking && wispAvailability.available === false && ( +

+ + Not available +

+ )} +
)} - - - - - - Custom Domains - - Bring your own domain with DNS verification - - - - + + {!domainsLoading && wispDomains.length === 3 && !userInfo?.isSupporter && ( +

+ Maximum of 3 wisp.place subdomains claimed +

+ )} +
+ + {/* Custom Domains */} +
+
+

+ Custom Domains +

+ +
{domainsLoading ? (
{[...Array(2)].map((_, i) => ( -
-
-
- - -
- -
-
- - - -
+
+
))}
) : customDomains.length === 0 ? ( -
+

No custom domains added yet -

+

) : (
- {customDomains.map((domain) => ( -
-
-
- {domain.verified ? ( - - ) : ( - + {customDomains.map((domain, idx) => { + const globalIndex = wispDomains.length + idx + const isFocused = globalIndex === focusedIndex + const isVerifying = verificationStatus[domain.id] === 'verifying' + return ( +
{ itemRefs.current[globalIndex] = el }} + className={`flex items-center justify-between p-3 border transition-colors ${ + isFocused + ? 'border-accent bg-accent/10' + : 'border-border/30 hover:bg-muted/10' + }`} + > +
+
+ {domain.verified + ? + : + } + {domain.domain} + custom + {domain.verified + ? ✓ verified + : ⏳ pending + } +
+ {domain.rkey && domain.rkey !== 'self' && ( +

+ → {domain.rkey} +

)} - - {domain.domain} -
- {domain.rkey && domain.rkey !== 'self' && ( -

- → Mapped to site: {domain.rkey} -

- )} -
-
- - {domain.verified ? ( - - Verified - - ) : ( +
- )} - + {!domain.verified && ( + + )} + +
-
- ))} + ) + })}
)} - - +
+
{/* Add Custom Domain Modal */} @@ -438,7 +527,8 @@ export function DomainsTab({ id="new-domain" placeholder="example.com" value={customDomain} - onChange={(e) => setCustomDomain(e.target.value)} + onChange={e => setCustomDomain(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleAddCustomDomain() }} />

After adding, click "View DNS" to see the records you @@ -449,10 +539,7 @@ export function DomainsTab({

-

- Value -

-

- {userInfo.did} -

+

Value

+

{userInfo.did}

-
@@ -575,61 +619,28 @@ export function DomainsTab({
-

- Step 2 -

-

- Point your domain (CNAME) -

+

Step 2

+

Point your domain (CNAME)

- - Recommended - + Recommended
-

- Name -

-

- {domain.domain} -

+

Name

+

{domain.domain}

-
-

- Value -

-

- {domain.id}.dns.wisp.place -

+

Value

+

{domain.id}.dns.wisp.place

-
@@ -655,49 +666,26 @@ export function DomainsTab({

- {HOSTING_NODES.map((node) => ( + {HOSTING_NODES.map(node => (
-
- {node.region} -
+
{node.region}
- - Name: - {' '} - - {domain.domain} - + Name:{' '} + {domain.domain}
- - Type: - {' '} + Type:{' '} A
-

- Value -

-

- {node.ip} -

+

Value

+

{node.ip}

-
@@ -721,9 +709,7 @@ export function DomainsTab({ ) })()}
- - Scroll for more - + Scroll for more
)} diff --git a/apps/main-app/public/editor/tabs/UploadTab.tsx b/apps/main-app/public/editor/tabs/UploadTab.tsx index 0a585dc..1a7dfa6 100644 --- a/apps/main-app/public/editor/tabs/UploadTab.tsx +++ b/apps/main-app/public/editor/tabs/UploadTab.tsx @@ -1,18 +1,8 @@ import { useState, useEffect, useRef } from 'react' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from '@public/components/ui/card' import { Button } from '@public/components/ui/button' import { Input } from '@public/components/ui/input' import { Label } from '@public/components/ui/label' -import { RadioGroup, RadioGroupItem } from '@public/components/ui/radio-group' -import { Badge } from '@public/components/ui/badge' import { - Globe, Upload, AlertCircle, Loader2, @@ -428,315 +418,224 @@ export function UploadTab({ } return ( -
- - - Upload Site - - Deploy a new site from a folder or Git repository - - - -
-
- setSiteMode(value as 'existing' | 'new')} +
+ {/* Header */} +
+

Upload Site

+

100MB per file · 300MB total

+
+ + {/* Content */} +
+ + {/* Mode toggle */} +
+ + +
+ + {/* Site selector / name */} + {siteMode === 'existing' ? ( + sitesLoading ? ( +
+ + Loading sites... +
+ ) : sites.length === 0 ? ( +

+ No sites yet — switch to "Create new" above. +

+ ) : ( +
+ + +
+ ) + ) : ( +
+ + setNewSiteName(e.target.value)} + disabled={isUploading} + className="h-9" + /> +
+ )} + + {/* Drop zone */} +
!isUploading && document.getElementById('file-upload')?.click()} + > + + + {isDragging + ? 'Drop here...' + : selectedFiles && selectedFiles.length > 0 + ? {selectedFiles.length} files selected + : 'Drop a folder or click to choose' + } + + +
+ + {/* Progress */} + {uploadProgress && ( +
+
+ + {uploadProgress}
- {siteMode === 'existing' ? ( -
- - {sitesLoading ? ( -
- -
- ) : sites.length === 0 ? ( -
- No sites available. Create a new site instead. -
- ) : ( - +
)}
- ) : ( -
- - setNewSiteName(e.target.value)} - disabled={isUploading} - /> -
)} -

- File limits: 100MB per file, 300MB total -

-
- -
- - - -

- Upload Folder -

-

- {isDragging - ? 'Drop your files here...' - : 'Drag and drop or click to upload your static site files' - } -

- - - {selectedFiles && selectedFiles.length > 0 && ( -

- {selectedFiles.length} files selected -

- )} -
-
- - - - -

- Connect Git Repository -

-

- Link your GitHub, GitLab, or any Git - repository -

- Coming soon! -
-
-
- - {uploadProgress && ( -
-
-
- - {uploadProgress} + {failedFiles.length > 0 && ( +
+
+ + {failedFiles.length} file{failedFiles.length > 1 ? 's' : ''} failed + {uploadedCount > 0 && ({uploadedCount} ok)}
-
- - {fileProgressList.length > 0 && ( -
- - {showFileProgress && ( -
- {fileProgressList.map((file, idx) => ( -
- {file.status === 'checking' && ( - - )} - {file.status === 'uploading' && ( - - )} - {file.status === 'uploaded' && ( - - )} - {file.status === 'reused' && ( - - )} - {file.status === 'failed' && ( - - )} -
-
{file.name}
- {file.error && ( -
- {file.error} -
- )} - {file.status === 'checking' && ( -
Checking for changes...
- )} - {file.status === 'uploading' && ( -
Uploading to PDS...
- )} - {file.status === 'reused' && ( -
Reused (unchanged)
- )} -
-
- ))} +
+ {failedFiles.slice(0, 10).map((file, idx) => ( +
+ {file.name} + — {file.error}{file.size > 0 && ` (${(file.size / 1024).toFixed(1)}KB)`}
- )} + ))} + {failedFiles.length > 10 &&
…and {failedFiles.length - 10} more
}
- )} - - {failedFiles.length > 0 && ( -
-
- -
- - {failedFiles.length} file{failedFiles.length > 1 ? 's' : ''} failed to upload - - {uploadedCount > 0 && ( - - ({uploadedCount} uploaded successfully) - - )} -
-
-
- {failedFiles.slice(0, 10).map((file, idx) => ( -
-
{file.name}
-
- Error: {file.error} - {file.size > 0 && ` (${(file.size / 1024).toFixed(1)} KB)`} -
-
- ))} - {failedFiles.length > 10 && ( -
- ...and {failedFiles.length - 10} more -
- )} -
+
+ )} + + {skippedFiles.length > 0 && ( +
+
+ + {skippedFiles.length} file{skippedFiles.length > 1 ? 's' : ''} skipped
- )} - - {skippedFiles.length > 0 && ( -
-
- -
- - {skippedFiles.length} file{skippedFiles.length > 1 ? 's' : ''} skipped - +
+ {skippedFiles.slice(0, 5).map((file, idx) => ( +
+ {file.name} + — {file.reason}
-
-
- {skippedFiles.slice(0, 5).map((file, idx) => ( -
- {file.name} - - {file.reason} -
- ))} - {skippedFiles.length > 5 && ( -
- ...and {skippedFiles.length - 5} more -
- )} -
+ ))} + {skippedFiles.length > 5 &&
…and {skippedFiles.length - 5} more
}
- )} -
- )} - -
)} - - - +
+ )} + + {/* Upload button */} + +
) } diff --git a/bun.lock b/bun.lock index ec91b87..5b4e5fa 100644 --- a/bun.lock +++ b/bun.lock @@ -139,7 +139,7 @@ }, "cli": { "name": "wispctl", - "version": "1.0.8", + "version": "1.0.10", "bin": { "wispctl": "dist/index.js", }, diff --git a/packages/@wispplace/bun-firehose/src/firehose.ts b/packages/@wispplace/bun-firehose/src/firehose.ts index 35c3ee3..35c5941 100644 --- a/packages/@wispplace/bun-firehose/src/firehose.ts +++ b/packages/@wispplace/bun-firehose/src/firehose.ts @@ -1,6 +1,6 @@ /** * Bun-compatible AT Protocol Firehose - * Uses our BunSubscription with the SDK's parsing/validation logic + * Uses BunSubscription with the SDK's parsing/validation logic */ import { IdResolver } from '@atproto/identity'; diff --git a/packages/@wispplace/bun-firehose/src/runtime.ts b/packages/@wispplace/bun-firehose/src/runtime.ts index ae58979..5a4893a 100644 --- a/packages/@wispplace/bun-firehose/src/runtime.ts +++ b/packages/@wispplace/bun-firehose/src/runtime.ts @@ -1,17 +1,19 @@ /** * Runtime detection utilities for cross-platform compatibility */ - declare const Bun: unknown; +declare const Deno: unknown; export const isBun = typeof Bun !== 'undefined'; -export const isNode = typeof process !== 'undefined' && !isBun; +export const isDeno = typeof Deno !== 'undefined'; +export const isNode = typeof process !== 'undefined' && !isBun && !isDeno; /** * Get the current runtime name for logging */ export function getRuntimeName(): string { if (isBun) return 'Bun'; + if (isDeno) return 'Deno'; if (isNode) return 'Node.js'; return 'Unknown'; -} +} \ No newline at end of file