Move from GitHub to Tangled
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});