#!/usr/bin/env node /** * at://press setup script * Interactively configures your .env file by connecting to your PDS. * * Usage: npm run setup */ import { createInterface } from "node:readline/promises"; import { writeFileSync, existsSync, readFileSync } from "node:fs"; import { stdin, stdout } from "node:process"; const rl = createInterface({ input: stdin, output: stdout }); function log(msg) { console.log(`\x1b[36m>\x1b[0m ${msg}`); } function success(msg) { console.log(`\x1b[32m✓\x1b[0m ${msg}`); } function error(msg) { console.error(`\x1b[31m✗\x1b[0m ${msg}`); } async function main() { console.log("\n \x1b[1mat://press\x1b[0m setup\n"); // 1. PDS URL const pdsUrl = (await rl.question(" PDS URL (e.g., https://bsky.social): ")).trim().replace(/\/+$/, ""); if (!pdsUrl) { error("PDS URL is required."); process.exit(1); } // Validate PDS is reachable log("Checking PDS..."); try { const desc = await fetch(`${pdsUrl}/xrpc/com.atproto.server.describeServer`); if (!desc.ok) throw new Error(`HTTP ${desc.status}`); const data = await desc.json(); success(`Connected to PDS (${data.availableUserDomains?.join(", ") || "custom"})`); } catch (e) { error(`Cannot reach PDS at ${pdsUrl}: ${e.message}`); process.exit(1); } // 2. Handle const handle = (await rl.question(" Your handle (e.g., you.bsky.social): ")).trim(); if (!handle) { error("Handle is required."); process.exit(1); } // Resolve DID from handle log("Resolving DID..."); let did; try { const res = await fetch(`${pdsUrl}/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); if (!res.ok) { // Try public API as fallback const pub = await fetch(`https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`); if (!pub.ok) throw new Error("Handle not found"); did = (await pub.json()).did; } else { did = (await res.json()).did; } success(`Resolved: ${did}`); } catch (e) { error(`Could not resolve handle "${handle}": ${e.message}`); process.exit(1); } // 3. App password const appPassword = (await rl.question(" App password (create in PDS Settings > App Passwords): ")).trim(); if (!appPassword) { error("App password is required."); process.exit(1); } // Test authentication log("Testing authentication..."); try { const res = await fetch(`${pdsUrl}/xrpc/com.atproto.server.createSession`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ identifier: did, password: appPassword }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.message || `HTTP ${res.status}`); } success("Authentication successful!"); } catch (e) { error(`Authentication failed: ${e.message}`); process.exit(1); } // 4. Blog URL const blogUrl = (await rl.question(" Blog URL (e.g., https://blog.example.com) [http://localhost:4321]: ")).trim() || "http://localhost:4321"; // Write .env const envContent = `# Generated by at://press setup PDS_URL=${pdsUrl} DID=${did} HANDLE=${handle} PDS_APP_PASSWORD=${appPassword} BLOG_URL=${blogUrl} # Optional: ATAuth gateway for login/editing # ATAUTH_GATEWAY_URL= # ATAUTH_PUBLIC_URL= # Optional: Lexicon collections (defaults to WhiteWind) # BLOG_COLLECTION=com.whtwnd.blog.entry # ABOUT_COLLECTION=xyz.arcnode.blog.about `; const envPath = ".env"; if (existsSync(envPath)) { const overwrite = (await rl.question(" .env already exists. Overwrite? [y/N]: ")).trim().toLowerCase(); if (overwrite !== "y") { log("Keeping existing .env"); rl.close(); return; } } writeFileSync(envPath, envContent); success("Wrote .env file"); console.log(` \x1b[1mSetup complete!\x1b[0m Start the dev server: \x1b[36mnpm run dev\x1b[0m Or deploy with Docker: \x1b[36mdocker compose up -d\x1b[0m `); rl.close(); } main().catch((e) => { error(e.message); process.exit(1); });