+78
-14
public/editor/tabs/CLITab.tsx
+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
+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
+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
+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
+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, 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
+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
+8
-2
src/routes/wisp.ts
···
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 }