Move from GitHub to Tangled
at main 7.7 kB view raw
1import { AtpAgent } from "@atproto/api"; 2import dotenv from "dotenv"; 3import fs from "fs"; 4import path from "path"; 5import { fileURLToPath } from "url"; 6import { execSync } from "child_process"; 7 8const __filename = fileURLToPath(import.meta.url); 9const __dirname = path.dirname(__filename); 10 11dotenv.config({ path: "./src/.env" }); 12 13async function runHealthCheck() { 14 15console.log("🔍 Running Tangled Sync Health Check...\n"); 16 17const checks: { category: string; name: string; status: boolean; message: string }[] = []; 18let errors = 0; 19let warnings = 0; 20 21// ===== CONFIGURATION CHECKS ===== 22console.log("📋 Configuration Checks\n"); 23 24const envPath = path.join(__dirname, ".env"); 25const envExists = fs.existsSync(envPath); 26checks.push({ 27 category: "config", 28 name: ".env file", 29 status: envExists, 30 message: envExists ? "Found at src/.env" : "Missing! Copy src/.env.example to src/.env" 31}); 32if (!envExists) errors++; 33 34const requiredVars = [ 35 { name: "BASE_DIR", description: "Base directory for repos" }, 36 { name: "GITHUB_USER", description: "GitHub username" }, 37 { name: "ATPROTO_DID", description: "AT Proto DID" }, 38 { name: "BLUESKY_PDS", description: "Bluesky PDS URL" }, 39 { name: "BLUESKY_USERNAME", description: "Bluesky username" }, 40 { name: "BLUESKY_PASSWORD", description: "Bluesky app password" }, 41]; 42 43requiredVars.forEach(({ name, description }) => { 44 const value = process.env[name]; 45 const exists = !!value && value.trim().length > 0; 46 checks.push({ 47 category: "config", 48 name: name, 49 status: exists, 50 message: exists ? `Set` : `Missing (${description})` 51 }); 52 if (!exists) errors++; 53}); 54 55// Check BASE_DIR 56const baseDir = process.env.BASE_DIR; 57if (baseDir) { 58 const baseDirExists = fs.existsSync(baseDir); 59 checks.push({ 60 category: "config", 61 name: "BASE_DIR path", 62 status: baseDirExists, 63 message: baseDirExists ? `Exists: ${baseDir}` : `Missing (will be created): ${baseDir}` 64 }); 65 if (!baseDirExists) warnings++; 66} 67 68// Check DID format 69const did = process.env.ATPROTO_DID; 70if (did) { 71 const validDid = did.startsWith("did:plc:") || did.startsWith("did:web:"); 72 checks.push({ 73 category: "config", 74 name: "DID format", 75 status: validDid, 76 message: validDid ? "Valid" : "Invalid! Should start with 'did:plc:' or 'did:web:'" 77 }); 78 if (!validDid) errors++; 79} 80 81// Check PDS URL 82const pds = process.env.BLUESKY_PDS; 83if (pds) { 84 const validPds = pds.startsWith("http://") || pds.startsWith("https://"); 85 checks.push({ 86 category: "config", 87 name: "PDS URL", 88 status: validPds, 89 message: validPds ? pds : "Invalid! Should start with 'https://'" 90 }); 91 if (!validPds) errors++; 92} 93 94// Print config results 95checks.filter(c => c.category === "config").forEach((check) => { 96 const icon = check.status ? "✅" : "❌"; 97 console.log(`${icon} ${check.name}: ${check.message}`); 98}); 99 100// ===== AT PROTO CONNECTION CHECK ===== 101console.log("\n🔐 AT Proto Connection Check\n"); 102 103const canTestConnection = process.env.BLUESKY_USERNAME && 104 process.env.BLUESKY_PASSWORD && 105 process.env.BLUESKY_PDS && 106 process.env.ATPROTO_DID; 107 108if (canTestConnection) { 109 try { 110 const agent = new AtpAgent({ service: process.env.BLUESKY_PDS! }); 111 112 const loginResponse = await agent.login({ 113 identifier: process.env.BLUESKY_USERNAME!, 114 password: process.env.BLUESKY_PASSWORD! 115 }); 116 117 console.log(`✅ Login successful`); 118 console.log(` DID: ${loginResponse.data.did}`); 119 console.log(` Handle: ${loginResponse.data.handle}`); 120 121 if (loginResponse.data.did !== process.env.ATPROTO_DID) { 122 console.log(`⚠️ DID mismatch!`); 123 console.log(` Expected: ${process.env.ATPROTO_DID}`); 124 console.log(` Got: ${loginResponse.data.did}`); 125 warnings++; 126 } 127 128 // Test fetching records 129 const records = await agent.api.com.atproto.repo.listRecords({ 130 repo: loginResponse.data.did, 131 collection: "sh.tangled.repo", 132 limit: 5, 133 }); 134 135 console.log(`✅ Can access AT Proto records`); 136 console.log(` Found ${records.data.records.length} sample records`); 137 138 } catch (error: any) { 139 console.log(`❌ AT Proto connection failed`); 140 console.log(` Error: ${error.message}`); 141 errors++; 142 } 143} else { 144 console.log("⏭️ Skipped (missing credentials)"); 145} 146 147// ===== SSH CONNECTION CHECK ===== 148console.log("\n🔑 SSH Connection Check\n"); 149 150try { 151 const sshTest = execSync("ssh -T git@tangled.sh 2>&1", { 152 encoding: "utf-8", 153 timeout: 5000 154 }); 155 156 if (sshTest.includes("successfully authenticated") || sshTest.includes("Hi")) { 157 console.log("✅ SSH connection to Tangled works"); 158 console.log(` ${sshTest.trim().split('\n')[0]}`); 159 } else { 160 console.log("⚠️ SSH connection uncertain"); 161 console.log(` Response: ${sshTest.trim()}`); 162 warnings++; 163 } 164} catch (error: any) { 165 const output = error.stdout?.toString() || error.message; 166 167 if (output.includes("successfully authenticated") || output.includes("Hi")) { 168 console.log("✅ SSH connection to Tangled works"); 169 } else { 170 console.log("❌ SSH connection to Tangled failed"); 171 console.log(" Make sure your SSH key is added at https://tangled.org/settings/keys"); 172 errors++; 173 } 174} 175 176// ===== GITHUB API CHECK ===== 177console.log("\n🐙 GitHub API Check\n"); 178 179if (process.env.GITHUB_USER) { 180 try { 181 const response = execSync(`curl -s "https://api.github.com/users/${process.env.GITHUB_USER}"`, { 182 encoding: "utf-8", 183 timeout: 5000 184 }); 185 186 const data = JSON.parse(response); 187 188 if (data.login) { 189 console.log(`✅ GitHub user found: ${data.login}`); 190 console.log(` Public repos: ${data.public_repos || 0}`); 191 } else { 192 console.log(`❌ GitHub user not found: ${process.env.GITHUB_USER}`); 193 errors++; 194 } 195 } catch (error: any) { 196 console.log(`⚠️ Could not check GitHub API`); 197 console.log(` ${error.message}`); 198 warnings++; 199 } 200} else { 201 console.log("⏭️ Skipped (no GITHUB_USER set)"); 202} 203 204// ===== DEPENDENCIES CHECK ===== 205console.log("\n📦 Dependencies Check\n"); 206 207let hasAtproto = false; 208let hasDotenv = false; 209 210try { 211 await import("@atproto/api"); 212 hasAtproto = true; 213 console.log("✅ @atproto/api installed"); 214} catch { 215 console.log("❌ @atproto/api not installed (run: npm install)"); 216 errors++; 217} 218 219try { 220 await import("dotenv"); 221 hasDotenv = true; 222 console.log("✅ dotenv installed"); 223} catch { 224 console.log("❌ dotenv not installed (run: npm install)"); 225 errors++; 226} 227 228// ===== SUMMARY ===== 229console.log("\n" + "=".repeat(50)); 230 231if (errors === 0 && warnings === 0) { 232 console.log("✅ All checks passed! Ready to sync."); 233 console.log("\nNext steps:"); 234 console.log(" npm run sync # Sync new repos only"); 235 console.log(" npm run sync:force # Force sync all repos"); 236} else { 237 if (errors > 0) { 238 console.log(`${errors} error(s) found - please fix before syncing`); 239 } 240 if (warnings > 0) { 241 console.log(`⚠️ ${warnings} warning(s) - review before syncing`); 242 } 243 244 console.log("\nSee SETUP.md for detailed troubleshooting"); 245 246 if (errors > 0) { 247 process.exit(1); 248 } 249} 250 251console.log("=".repeat(50)); 252 253// Additional recommendations 254if (process.env.BLUESKY_PASSWORD && !process.env.BLUESKY_PASSWORD.includes("-")) { 255 console.log("\n💡 Tip: Your password might be a regular password."); 256 console.log(" Consider using an App Password from Bluesky settings for better security."); 257} 258 259} 260 261// Run the health check 262runHealthCheck().catch((error) => { 263 console.error("\n❌ Health check failed with error:"); 264 console.error(error); 265 process.exit(1); 266});