at://Press
1#!/usr/bin/env node
2
3/**
4 * at://press setup script
5 * Interactively configures your .env file by connecting to your PDS.
6 *
7 * Usage: npm run setup
8 */
9
10import { createInterface } from "node:readline/promises";
11import { writeFileSync, existsSync, readFileSync } from "node:fs";
12import { stdin, stdout } from "node:process";
13
14const rl = createInterface({ input: stdin, output: stdout });
15
16function log(msg) { console.log(`\x1b[36m>\x1b[0m ${msg}`); }
17function success(msg) { console.log(`\x1b[32m✓\x1b[0m ${msg}`); }
18function error(msg) { console.error(`\x1b[31m✗\x1b[0m ${msg}`); }
19
20async function main() {
21 console.log("\n \x1b[1mat://press\x1b[0m setup\n");
22
23 // 1. PDS URL
24 const pdsUrl = (await rl.question(" PDS URL (e.g., https://bsky.social): ")).trim().replace(/\/+$/, "");
25 if (!pdsUrl) { error("PDS URL is required."); process.exit(1); }
26
27 // Validate PDS is reachable
28 log("Checking PDS...");
29 try {
30 const desc = await fetch(`${pdsUrl}/xrpc/com.atproto.server.describeServer`);
31 if (!desc.ok) throw new Error(`HTTP ${desc.status}`);
32 const data = await desc.json();
33 success(`Connected to PDS (${data.availableUserDomains?.join(", ") || "custom"})`);
34 } catch (e) {
35 error(`Cannot reach PDS at ${pdsUrl}: ${e.message}`);
36 process.exit(1);
37 }
38
39 // 2. Handle
40 const handle = (await rl.question(" Your handle (e.g., you.bsky.social): ")).trim();
41 if (!handle) { error("Handle is required."); process.exit(1); }
42
43 // Resolve DID from handle
44 log("Resolving DID...");
45 let did;
46 try {
47 const res = await fetch(`${pdsUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
48 if (!res.ok) {
49 // Try public API as fallback
50 const pub = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`);
51 if (!pub.ok) throw new Error("Handle not found");
52 did = (await pub.json()).did;
53 } else {
54 did = (await res.json()).did;
55 }
56 success(`Resolved: ${did}`);
57 } catch (e) {
58 error(`Could not resolve handle "${handle}": ${e.message}`);
59 process.exit(1);
60 }
61
62 // 3. App password
63 const appPassword = (await rl.question(" App password (create in PDS Settings > App Passwords): ")).trim();
64 if (!appPassword) { error("App password is required."); process.exit(1); }
65
66 // Test authentication
67 log("Testing authentication...");
68 try {
69 const res = await fetch(`${pdsUrl}/xrpc/com.atproto.server.createSession`, {
70 method: "POST",
71 headers: { "Content-Type": "application/json" },
72 body: JSON.stringify({ identifier: did, password: appPassword }),
73 });
74 if (!res.ok) {
75 const body = await res.json().catch(() => ({}));
76 throw new Error(body.message || `HTTP ${res.status}`);
77 }
78 success("Authentication successful!");
79 } catch (e) {
80 error(`Authentication failed: ${e.message}`);
81 process.exit(1);
82 }
83
84 // 4. Blog URL
85 const blogUrl = (await rl.question(" Blog URL (e.g., https://blog.example.com) [http://localhost:4321]: ")).trim() || "http://localhost:4321";
86
87 // Write .env
88 const envContent = `# Generated by at://press setup
89PDS_URL=${pdsUrl}
90DID=${did}
91HANDLE=${handle}
92PDS_APP_PASSWORD=${appPassword}
93BLOG_URL=${blogUrl}
94
95# Optional: ATAuth gateway for login/editing
96# ATAUTH_GATEWAY_URL=
97# ATAUTH_PUBLIC_URL=
98
99# Optional: Lexicon collections (defaults to WhiteWind)
100# BLOG_COLLECTION=com.whtwnd.blog.entry
101# ABOUT_COLLECTION=xyz.arcnode.blog.about
102`;
103
104 const envPath = ".env";
105 if (existsSync(envPath)) {
106 const overwrite = (await rl.question(" .env already exists. Overwrite? [y/N]: ")).trim().toLowerCase();
107 if (overwrite !== "y") {
108 log("Keeping existing .env");
109 rl.close();
110 return;
111 }
112 }
113
114 writeFileSync(envPath, envContent);
115 success("Wrote .env file");
116
117 console.log(`
118 \x1b[1mSetup complete!\x1b[0m
119
120 Start the dev server:
121 \x1b[36mnpm run dev\x1b[0m
122
123 Or deploy with Docker:
124 \x1b[36mdocker compose up -d\x1b[0m
125`);
126
127 rl.close();
128}
129
130main().catch((e) => {
131 error(e.message);
132 process.exit(1);
133});