@ewanc26/tangled-sync 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/USAGE.md +197 -0
- package/dist/check.js +209 -0
- package/dist/index.js +247 -0
- package/dist/test-atproto.js +58 -0
- package/dist/validate-config.js +86 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ewan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Tangled Sync
|
|
2
|
+
|
|
3
|
+
**Tangled Sync** is a TypeScript project that automates the process of syncing GitHub repositories to Tangled and publishing ATProto records for each repository. It is designed to streamline your workflow if you want your GitHub projects mirrored on Tangled while also maintaining structured metadata in ATProto.
|
|
4
|
+
|
|
5
|
+
This tool is particularly useful for developers and organisations that want a decentralized or alternative hosting layer for their code repositories while keeping them discoverable via ATProto.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Getting Started
|
|
10
|
+
|
|
11
|
+
### Configuration
|
|
12
|
+
|
|
13
|
+
Before running any scripts, you need to configure the project. Create a `src/.env` file based on `src/.env.example`:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cp src/.env.example src/.env
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then edit `src/.env` with your actual values:
|
|
20
|
+
|
|
21
|
+
* `BASE_DIR` – the local directory where GitHub repositories will be cloned.
|
|
22
|
+
* `GITHUB_USER` – your GitHub username or organisation.
|
|
23
|
+
* `ATPROTO_DID` – your ATProto DID (Decentralized Identifier).
|
|
24
|
+
* `BLUESKY_PDS` – the URL of your Bluesky PDS instance.
|
|
25
|
+
* `BLUESKY_USERNAME` – your Bluesky username.
|
|
26
|
+
* `BLUESKY_PASSWORD` – your Bluesky password.
|
|
27
|
+
|
|
28
|
+
Make sure this file is properly set up before proceeding.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
### Installation
|
|
33
|
+
|
|
34
|
+
1. Clone this repository locally.
|
|
35
|
+
2. Navigate to the project directory.
|
|
36
|
+
3. Run:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This will install all dependencies required for syncing GitHub repositories and interacting with ATProto.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
### Verify SSH Connection to Tangled
|
|
47
|
+
|
|
48
|
+
* If the Tangled remote does not exist for a repository, the script will attempt to create it on first run. This requires a working SSH key associated with your account.
|
|
49
|
+
|
|
50
|
+
Without proper SSH authentication, repository creation and pushing will fail.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### Testing AT Proto Connection
|
|
55
|
+
|
|
56
|
+
**Before running the full sync**, test your AT Proto connection:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run test-atproto
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This will:
|
|
63
|
+
- Verify your Bluesky credentials
|
|
64
|
+
- Confirm your DID matches the configuration
|
|
65
|
+
- List any existing `sh.tangled.repo` records
|
|
66
|
+
- Validate the connection to the PDS
|
|
67
|
+
|
|
68
|
+
### Running the Sync Script
|
|
69
|
+
|
|
70
|
+
Once configuration, SSH verification, and AT Proto testing are complete, run:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm run sync
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
What happens during the sync:
|
|
77
|
+
|
|
78
|
+
1. **Login to Bluesky:** The script authenticates using your credentials to allow publishing ATProto records.
|
|
79
|
+
2. **Clone GitHub Repositories:** All repositories under your configured GitHub user are cloned locally (excluding a repository with the same name as your username to avoid recursion).
|
|
80
|
+
3. **Ensure Tangled Remotes:** For each repository, a `tangled` remote is added if it doesn’t exist.
|
|
81
|
+
4. **Push to Tangled:** The script pushes the `main` branch to Tangled. If your `origin` remote’s push URL points to Tangled, it will reset it back to GitHub.
|
|
82
|
+
5. **Update README:** Each repository’s README is updated to include a link to its Tangled mirror, if it isn’t already present.
|
|
83
|
+
6. **Create ATProto Records:** Each repository gets a structured record published in ATProto under your DID, including metadata like description, creation date, and source URL.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### Notes & Best Practices
|
|
88
|
+
|
|
89
|
+
* **Directory Management:** The script ensures that your `BASE_DIR` exists and creates it if necessary.
|
|
90
|
+
* **Record Uniqueness:** ATProto records use a time-based, sortable ID (TID) to ensure uniqueness. Duplicate IDs are avoided automatically.
|
|
91
|
+
* **Error Handling:** If a repository cannot be pushed to Tangled, the script logs a warning but continues processing the remaining repositories.
|
|
92
|
+
* **Idempotency:** Running the script multiple times is safe; existing remotes and ATProto records are checked before creation to prevent duplicates.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Example Workflow
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Run the sync script
|
|
100
|
+
npm run sync
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
After execution, you’ll see logs detailing which repositories were cloned, which remotes were added, which READMEs were updated, and which ATProto records were created.
|
|
104
|
+
|
|
105
|
+
This allows you to quickly confirm that all GitHub repositories have been mirrored and documented properly on Tangled.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
### Contribution & Development
|
|
110
|
+
|
|
111
|
+
If you plan to contribute:
|
|
112
|
+
|
|
113
|
+
* Ensure Node.js v18+ and npm v9+ are installed.
|
|
114
|
+
* Test the script in a separate directory to avoid accidentally overwriting your production repositories.
|
|
115
|
+
* Use `console.log` statements to debug or track progress during development.
|
|
116
|
+
* Maintain proper `.env` configuration to avoid leaking credentials.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
**Tangled Sync** bridges GitHub and Tangled efficiently, providing automatic mirroring, record management, and easy discoverability. Following these steps will ensure a smooth, automated workflow for syncing and publishing your repositories.
|
|
121
|
+
|
|
122
|
+
## ☕ Support
|
|
123
|
+
|
|
124
|
+
If you found this useful, consider [buying me a ko-fi](https://ko-fi.com/ewancroft)!
|
package/USAGE.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# 🎉 Tangled Sync - Ready to Use!
|
|
2
|
+
|
|
3
|
+
## Summary of Changes
|
|
4
|
+
|
|
5
|
+
I've improved your Tangled Sync project to ensure proper AT Proto authentication and repository record creation. Here's what was updated:
|
|
6
|
+
|
|
7
|
+
### ✅ What's Fixed
|
|
8
|
+
|
|
9
|
+
1. **Enhanced AT Proto Login**
|
|
10
|
+
- Added better error handling and validation
|
|
11
|
+
- Shows DID and handle on successful login
|
|
12
|
+
- Clearer error messages when authentication fails
|
|
13
|
+
|
|
14
|
+
2. **Corrected Repository Schema**
|
|
15
|
+
- Fixed record structure to match `sh.tangled.repo` lexicon
|
|
16
|
+
- Required fields (`name`, `knot`, `createdAt`) now ordered correctly
|
|
17
|
+
- Optional fields properly marked as optional
|
|
18
|
+
- Added better error handling for record creation
|
|
19
|
+
|
|
20
|
+
3. **Improved Logging**
|
|
21
|
+
- More detailed startup information
|
|
22
|
+
- Better progress tracking during sync
|
|
23
|
+
- Shows AT Proto record URIs when created
|
|
24
|
+
- Success/failure messages are clearer
|
|
25
|
+
|
|
26
|
+
### 📁 New Files Created
|
|
27
|
+
|
|
28
|
+
1. **`src/.env.example`** - Template for your configuration
|
|
29
|
+
2. **`src/test-atproto.ts`** - Test AT Proto connection before syncing
|
|
30
|
+
3. **`src/validate-config.ts`** - Validate your environment setup
|
|
31
|
+
4. **`SETUP.md`** - Comprehensive setup and troubleshooting guide
|
|
32
|
+
|
|
33
|
+
### 🚀 How to Use
|
|
34
|
+
|
|
35
|
+
#### Step 1: Configure Environment
|
|
36
|
+
```bash
|
|
37
|
+
# Copy the example file
|
|
38
|
+
cp src/.env.example src/.env
|
|
39
|
+
|
|
40
|
+
# Edit with your actual values
|
|
41
|
+
nano src/.env
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You need:
|
|
45
|
+
- Your GitHub username
|
|
46
|
+
- Your AT Proto DID (from Bluesky settings)
|
|
47
|
+
- A Bluesky **app password** (not your main password!)
|
|
48
|
+
- Base directory for repos
|
|
49
|
+
|
|
50
|
+
#### Step 2: Validate Configuration
|
|
51
|
+
```bash
|
|
52
|
+
npm run validate
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This checks all your environment variables are set correctly.
|
|
56
|
+
|
|
57
|
+
#### Step 3: Test AT Proto Connection
|
|
58
|
+
```bash
|
|
59
|
+
npm run test-atproto
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This verifies:
|
|
63
|
+
- ✅ Your credentials work
|
|
64
|
+
- ✅ Your DID is correct
|
|
65
|
+
- ✅ You can access the PDS
|
|
66
|
+
- ✅ Shows any existing Tangled repo records
|
|
67
|
+
|
|
68
|
+
#### Step 4: Run the Sync
|
|
69
|
+
```bash
|
|
70
|
+
npm run sync
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
This will:
|
|
74
|
+
1. Login to AT Proto ✅
|
|
75
|
+
2. Fetch your GitHub repos
|
|
76
|
+
3. Clone them locally (if needed)
|
|
77
|
+
4. Add Tangled remotes
|
|
78
|
+
5. Push to Tangled
|
|
79
|
+
6. Update READMEs
|
|
80
|
+
7. Create AT Proto records for each repo ✅
|
|
81
|
+
|
|
82
|
+
### 🔍 What to Check
|
|
83
|
+
|
|
84
|
+
After running the sync, verify:
|
|
85
|
+
|
|
86
|
+
1. **AT Proto Records Created**
|
|
87
|
+
```bash
|
|
88
|
+
npm run test-atproto
|
|
89
|
+
```
|
|
90
|
+
Should show your repos listed
|
|
91
|
+
|
|
92
|
+
2. **Repos on Tangled**
|
|
93
|
+
Visit: `https://tangled.org/YOUR_DID/REPO_NAME`
|
|
94
|
+
|
|
95
|
+
3. **Local Git Remotes**
|
|
96
|
+
```bash
|
|
97
|
+
cd YOUR_BASE_DIR/some-repo
|
|
98
|
+
git remote -v
|
|
99
|
+
```
|
|
100
|
+
Should show both `origin` (GitHub) and `tangled` remotes
|
|
101
|
+
|
|
102
|
+
### 📊 Record Schema
|
|
103
|
+
|
|
104
|
+
Each repository creates a record with this structure:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
{
|
|
108
|
+
$type: "sh.tangled.repo",
|
|
109
|
+
name: "your-repo-name", // required
|
|
110
|
+
knot: "knot1.tangled.sh", // required
|
|
111
|
+
createdAt: "2024-01-01T00:00:00Z", // required
|
|
112
|
+
description: "Repo description", // optional
|
|
113
|
+
source: "https://github.com/...", // optional
|
|
114
|
+
labels: [], // optional
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
This matches the official `sh.tangled.repo` lexicon schema.
|
|
119
|
+
|
|
120
|
+
### ⚠️ Important Notes
|
|
121
|
+
|
|
122
|
+
1. **Use App Password**: Never use your main Bluesky password. Create an app password in Settings → App Passwords.
|
|
123
|
+
|
|
124
|
+
2. **Check Your DID**: Run `npm run test-atproto` first to ensure your DID in `.env` matches your actual account.
|
|
125
|
+
|
|
126
|
+
3. **SSH Key Required**: Make sure your SSH key is added to Tangled at https://tangled.org/settings/keys
|
|
127
|
+
|
|
128
|
+
4. **Rate Limits**: GitHub API has rate limits (60 req/hour unauthenticated). If you have many repos, consider adding GitHub auth.
|
|
129
|
+
|
|
130
|
+
### 🐛 Troubleshooting
|
|
131
|
+
|
|
132
|
+
**"Missing Bluesky credentials"**
|
|
133
|
+
- Check `src/.env` exists and has `BLUESKY_USERNAME` and `BLUESKY_PASSWORD`
|
|
134
|
+
|
|
135
|
+
**"Login failed"**
|
|
136
|
+
- Verify you're using an app password, not your main password
|
|
137
|
+
- Check username includes full handle (e.g., `you.bsky.social`)
|
|
138
|
+
|
|
139
|
+
**"Could not push to Tangled"**
|
|
140
|
+
- Verify SSH key is configured: `ssh git@tangled.sh`
|
|
141
|
+
- Check repo exists on Tangled
|
|
142
|
+
|
|
143
|
+
**"Failed to create ATProto record"**
|
|
144
|
+
- Run `npm run test-atproto` to check connection
|
|
145
|
+
- Verify your app password has write permissions
|
|
146
|
+
|
|
147
|
+
See `SETUP.md` for more detailed troubleshooting.
|
|
148
|
+
|
|
149
|
+
### 📚 Available Commands
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
npm run check # Comprehensive health check (recommended first step!)
|
|
153
|
+
npm run validate # Check environment configuration only
|
|
154
|
+
npm run test-atproto # Test AT Proto connection only
|
|
155
|
+
npm run sync # Run sync (only new repos without AT Proto records)
|
|
156
|
+
npm run sync:force # Force sync all repos (including existing)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### `npm run check` - Comprehensive Health Check
|
|
160
|
+
|
|
161
|
+
This is the **most useful command** for troubleshooting! It runs all checks in one go:
|
|
162
|
+
|
|
163
|
+
- ✅ Configuration validation
|
|
164
|
+
- ✅ AT Proto connection test
|
|
165
|
+
- ✅ SSH connection to Tangled
|
|
166
|
+
- ✅ GitHub API access
|
|
167
|
+
- ✅ Dependencies verification
|
|
168
|
+
|
|
169
|
+
**When to use:**
|
|
170
|
+
- Before your first sync
|
|
171
|
+
- When troubleshooting issues
|
|
172
|
+
- After changing configuration
|
|
173
|
+
- To verify everything is working
|
|
174
|
+
|
|
175
|
+
#### Individual Check Commands
|
|
176
|
+
|
|
177
|
+
**Normal sync** (recommended): Only processes repos that don't have AT Proto records yet. This is efficient and safe for regular use.
|
|
178
|
+
|
|
179
|
+
**Force sync**: Processes all repos regardless of whether they already have records. Use this if you need to:
|
|
180
|
+
- Re-push repos to Tangled
|
|
181
|
+
- Update READMEs for all repos
|
|
182
|
+
- Recover from a partial sync
|
|
183
|
+
|
|
184
|
+
### ✨ Next Steps
|
|
185
|
+
|
|
186
|
+
1. Copy and configure `src/.env`
|
|
187
|
+
2. Run `npm run validate`
|
|
188
|
+
3. Run `npm run test-atproto`
|
|
189
|
+
4. Run `npm run sync`
|
|
190
|
+
|
|
191
|
+
That's it! Your GitHub repos will be synced to Tangled with proper AT Proto records.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
**Questions?** Check `SETUP.md` for detailed instructions and troubleshooting.
|
|
196
|
+
|
|
197
|
+
**Happy syncing! 🚀**
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// src/check.ts
|
|
2
|
+
import { AtpAgent } from "@atproto/api";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
var __dirname = path.dirname(__filename);
|
|
10
|
+
dotenv.config();
|
|
11
|
+
async function runHealthCheck() {
|
|
12
|
+
console.log("\u{1F50D} Running Tangled Sync Health Check...\n");
|
|
13
|
+
const checks = [];
|
|
14
|
+
let errors = 0;
|
|
15
|
+
let warnings = 0;
|
|
16
|
+
console.log("\u{1F4CB} Configuration Checks\n");
|
|
17
|
+
const envPath = path.join(__dirname, ".env");
|
|
18
|
+
const envExists = fs.existsSync(envPath);
|
|
19
|
+
checks.push({
|
|
20
|
+
category: "config",
|
|
21
|
+
name: ".env file",
|
|
22
|
+
status: envExists,
|
|
23
|
+
message: envExists ? "Found at src/.env" : "Missing! Copy src/.env.example to src/.env"
|
|
24
|
+
});
|
|
25
|
+
if (!envExists) errors++;
|
|
26
|
+
const requiredVars = [
|
|
27
|
+
{ name: "BASE_DIR", description: "Base directory for repos" },
|
|
28
|
+
{ name: "GITHUB_USER", description: "GitHub username" },
|
|
29
|
+
{ name: "ATPROTO_DID", description: "AT Proto DID" },
|
|
30
|
+
{ name: "BLUESKY_PDS", description: "Bluesky PDS URL" },
|
|
31
|
+
{ name: "BLUESKY_USERNAME", description: "Bluesky username" },
|
|
32
|
+
{ name: "BLUESKY_PASSWORD", description: "Bluesky app password" }
|
|
33
|
+
];
|
|
34
|
+
requiredVars.forEach(({ name, description }) => {
|
|
35
|
+
const value = process.env[name];
|
|
36
|
+
const exists = !!value && value.trim().length > 0;
|
|
37
|
+
checks.push({
|
|
38
|
+
category: "config",
|
|
39
|
+
name,
|
|
40
|
+
status: exists,
|
|
41
|
+
message: exists ? `Set` : `Missing (${description})`
|
|
42
|
+
});
|
|
43
|
+
if (!exists) errors++;
|
|
44
|
+
});
|
|
45
|
+
const baseDir = process.env.BASE_DIR;
|
|
46
|
+
if (baseDir) {
|
|
47
|
+
const baseDirExists = fs.existsSync(baseDir);
|
|
48
|
+
checks.push({
|
|
49
|
+
category: "config",
|
|
50
|
+
name: "BASE_DIR path",
|
|
51
|
+
status: baseDirExists,
|
|
52
|
+
message: baseDirExists ? `Exists: ${baseDir}` : `Missing (will be created): ${baseDir}`
|
|
53
|
+
});
|
|
54
|
+
if (!baseDirExists) warnings++;
|
|
55
|
+
}
|
|
56
|
+
const did = process.env.ATPROTO_DID;
|
|
57
|
+
if (did) {
|
|
58
|
+
const validDid = did.startsWith("did:plc:") || did.startsWith("did:web:");
|
|
59
|
+
checks.push({
|
|
60
|
+
category: "config",
|
|
61
|
+
name: "DID format",
|
|
62
|
+
status: validDid,
|
|
63
|
+
message: validDid ? "Valid" : "Invalid! Should start with 'did:plc:' or 'did:web:'"
|
|
64
|
+
});
|
|
65
|
+
if (!validDid) errors++;
|
|
66
|
+
}
|
|
67
|
+
const pds = process.env.BLUESKY_PDS;
|
|
68
|
+
if (pds) {
|
|
69
|
+
const validPds = pds.startsWith("http://") || pds.startsWith("https://");
|
|
70
|
+
checks.push({
|
|
71
|
+
category: "config",
|
|
72
|
+
name: "PDS URL",
|
|
73
|
+
status: validPds,
|
|
74
|
+
message: validPds ? pds : "Invalid! Should start with 'https://'"
|
|
75
|
+
});
|
|
76
|
+
if (!validPds) errors++;
|
|
77
|
+
}
|
|
78
|
+
checks.filter((c) => c.category === "config").forEach((check) => {
|
|
79
|
+
const icon = check.status ? "\u2705" : "\u274C";
|
|
80
|
+
console.log(`${icon} ${check.name}: ${check.message}`);
|
|
81
|
+
});
|
|
82
|
+
console.log("\n\u{1F510} AT Proto Connection Check\n");
|
|
83
|
+
const canTestConnection = process.env.BLUESKY_USERNAME && process.env.BLUESKY_PASSWORD && process.env.BLUESKY_PDS && process.env.ATPROTO_DID;
|
|
84
|
+
if (canTestConnection) {
|
|
85
|
+
try {
|
|
86
|
+
const agent = new AtpAgent({ service: process.env.BLUESKY_PDS });
|
|
87
|
+
const loginResponse = await agent.login({
|
|
88
|
+
identifier: process.env.BLUESKY_USERNAME,
|
|
89
|
+
password: process.env.BLUESKY_PASSWORD
|
|
90
|
+
});
|
|
91
|
+
console.log(`\u2705 Login successful`);
|
|
92
|
+
console.log(` DID: ${loginResponse.data.did}`);
|
|
93
|
+
console.log(` Handle: ${loginResponse.data.handle}`);
|
|
94
|
+
if (loginResponse.data.did !== process.env.ATPROTO_DID) {
|
|
95
|
+
console.log(`\u26A0\uFE0F DID mismatch!`);
|
|
96
|
+
console.log(` Expected: ${process.env.ATPROTO_DID}`);
|
|
97
|
+
console.log(` Got: ${loginResponse.data.did}`);
|
|
98
|
+
warnings++;
|
|
99
|
+
}
|
|
100
|
+
const records = await agent.api.com.atproto.repo.listRecords({
|
|
101
|
+
repo: loginResponse.data.did,
|
|
102
|
+
collection: "sh.tangled.repo",
|
|
103
|
+
limit: 5
|
|
104
|
+
});
|
|
105
|
+
console.log(`\u2705 Can access AT Proto records`);
|
|
106
|
+
console.log(` Found ${records.data.records.length} sample records`);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.log(`\u274C AT Proto connection failed`);
|
|
109
|
+
console.log(` Error: ${error.message}`);
|
|
110
|
+
errors++;
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
console.log("\u23ED\uFE0F Skipped (missing credentials)");
|
|
114
|
+
}
|
|
115
|
+
console.log("\n\u{1F511} SSH Connection Check\n");
|
|
116
|
+
try {
|
|
117
|
+
const sshTest = execSync("ssh -T git@tangled.sh 2>&1", {
|
|
118
|
+
encoding: "utf-8",
|
|
119
|
+
timeout: 5e3
|
|
120
|
+
});
|
|
121
|
+
if (sshTest.includes("successfully authenticated") || sshTest.includes("Hi")) {
|
|
122
|
+
console.log("\u2705 SSH connection to Tangled works");
|
|
123
|
+
console.log(` ${sshTest.trim().split("\n")[0]}`);
|
|
124
|
+
} else {
|
|
125
|
+
console.log("\u26A0\uFE0F SSH connection uncertain");
|
|
126
|
+
console.log(` Response: ${sshTest.trim()}`);
|
|
127
|
+
warnings++;
|
|
128
|
+
}
|
|
129
|
+
} catch (error) {
|
|
130
|
+
const output = error.stdout?.toString() || error.message;
|
|
131
|
+
if (output.includes("successfully authenticated") || output.includes("Hi")) {
|
|
132
|
+
console.log("\u2705 SSH connection to Tangled works");
|
|
133
|
+
} else {
|
|
134
|
+
console.log("\u274C SSH connection to Tangled failed");
|
|
135
|
+
console.log(" Make sure your SSH key is added at https://tangled.org/settings/keys");
|
|
136
|
+
errors++;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
console.log("\n\u{1F419} GitHub API Check\n");
|
|
140
|
+
if (process.env.GITHUB_USER) {
|
|
141
|
+
try {
|
|
142
|
+
const response = execSync(`curl -s "https://api.github.com/users/${process.env.GITHUB_USER}"`, {
|
|
143
|
+
encoding: "utf-8",
|
|
144
|
+
timeout: 5e3
|
|
145
|
+
});
|
|
146
|
+
const data = JSON.parse(response);
|
|
147
|
+
if (data.login) {
|
|
148
|
+
console.log(`\u2705 GitHub user found: ${data.login}`);
|
|
149
|
+
console.log(` Public repos: ${data.public_repos || 0}`);
|
|
150
|
+
} else {
|
|
151
|
+
console.log(`\u274C GitHub user not found: ${process.env.GITHUB_USER}`);
|
|
152
|
+
errors++;
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.log(`\u26A0\uFE0F Could not check GitHub API`);
|
|
156
|
+
console.log(` ${error.message}`);
|
|
157
|
+
warnings++;
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
console.log("\u23ED\uFE0F Skipped (no GITHUB_USER set)");
|
|
161
|
+
}
|
|
162
|
+
console.log("\n\u{1F4E6} Dependencies Check\n");
|
|
163
|
+
let hasAtproto = false;
|
|
164
|
+
let hasDotenv = false;
|
|
165
|
+
try {
|
|
166
|
+
await import("@atproto/api");
|
|
167
|
+
hasAtproto = true;
|
|
168
|
+
console.log("\u2705 @atproto/api installed");
|
|
169
|
+
} catch {
|
|
170
|
+
console.log("\u274C @atproto/api not installed (run: npm install)");
|
|
171
|
+
errors++;
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
await import("dotenv");
|
|
175
|
+
hasDotenv = true;
|
|
176
|
+
console.log("\u2705 dotenv installed");
|
|
177
|
+
} catch {
|
|
178
|
+
console.log("\u274C dotenv not installed (run: npm install)");
|
|
179
|
+
errors++;
|
|
180
|
+
}
|
|
181
|
+
console.log("\n" + "=".repeat(50));
|
|
182
|
+
if (errors === 0 && warnings === 0) {
|
|
183
|
+
console.log("\u2705 All checks passed! Ready to sync.");
|
|
184
|
+
console.log("\nNext steps:");
|
|
185
|
+
console.log(" npm run sync # Sync new repos only");
|
|
186
|
+
console.log(" npm run sync:force # Force sync all repos");
|
|
187
|
+
} else {
|
|
188
|
+
if (errors > 0) {
|
|
189
|
+
console.log(`\u274C ${errors} error(s) found - please fix before syncing`);
|
|
190
|
+
}
|
|
191
|
+
if (warnings > 0) {
|
|
192
|
+
console.log(`\u26A0\uFE0F ${warnings} warning(s) - review before syncing`);
|
|
193
|
+
}
|
|
194
|
+
console.log("\nSee SETUP.md for detailed troubleshooting");
|
|
195
|
+
if (errors > 0) {
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
console.log("=".repeat(50));
|
|
200
|
+
if (process.env.BLUESKY_PASSWORD && !process.env.BLUESKY_PASSWORD.includes("-")) {
|
|
201
|
+
console.log("\n\u{1F4A1} Tip: Your password might be a regular password.");
|
|
202
|
+
console.log(" Consider using an App Password from Bluesky settings for better security.");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
runHealthCheck().catch((error) => {
|
|
206
|
+
console.error("\n\u274C Health check failed with error:");
|
|
207
|
+
console.error(error);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { AtpAgent } from "@atproto/api";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
dotenv.config();
|
|
8
|
+
var FORCE_SYNC = process.argv.includes("--force");
|
|
9
|
+
var BASE_DIR = process.env.BASE_DIR;
|
|
10
|
+
var GITHUB_USER = process.env.GITHUB_USER;
|
|
11
|
+
var ATPROTO_DID = process.env.ATPROTO_DID;
|
|
12
|
+
var BLUESKY_PDS = process.env.BLUESKY_PDS;
|
|
13
|
+
var TANGLED_BASE_URL = `git@tangled.sh:${ATPROTO_DID}`;
|
|
14
|
+
var agent = new AtpAgent({ service: BLUESKY_PDS });
|
|
15
|
+
async function login() {
|
|
16
|
+
const username = process.env.BLUESKY_USERNAME;
|
|
17
|
+
const password = process.env.BLUESKY_PASSWORD;
|
|
18
|
+
if (!username || !password) {
|
|
19
|
+
throw new Error("Missing Bluesky credentials. Please set BLUESKY_USERNAME and BLUESKY_PASSWORD in src/.env");
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const response = await agent.login({ identifier: username, password });
|
|
23
|
+
console.log(`[LOGIN] Successfully logged in to AT Proto as ${response.data.did}`);
|
|
24
|
+
console.log(`[LOGIN] Session handle: ${response.data.handle}`);
|
|
25
|
+
return response;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error("[ERROR] Failed to login to AT Proto:", error.message);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function getGitHubRepos() {
|
|
32
|
+
const curl = `curl -s "https://api.github.com/users/${GITHUB_USER}/repos?per_page=200"`;
|
|
33
|
+
const output = run(curl);
|
|
34
|
+
const json = JSON.parse(output);
|
|
35
|
+
return json.filter((r) => r.name !== GITHUB_USER).map((r) => ({ clone_url: r.clone_url, name: r.name, description: r.description }));
|
|
36
|
+
}
|
|
37
|
+
async function ensureTangledRemoteAndPush(repoDir, repoName, cloneUrl) {
|
|
38
|
+
const tangledUrl = `${TANGLED_BASE_URL}/${repoName}`;
|
|
39
|
+
try {
|
|
40
|
+
const remotes = run("git remote", repoDir).split("\n");
|
|
41
|
+
if (!remotes.includes("tangled")) {
|
|
42
|
+
console.log(`[REMOTE] Adding Tangled remote for ${repoName}`);
|
|
43
|
+
run(`git remote add tangled ${tangledUrl}`, repoDir);
|
|
44
|
+
}
|
|
45
|
+
const originPushUrl = run("git remote get-url --push origin", repoDir);
|
|
46
|
+
if (originPushUrl.includes("tangled.sh")) {
|
|
47
|
+
run(`git remote set-url --push origin ${cloneUrl}`, repoDir);
|
|
48
|
+
console.log(`[REMOTE] Reset origin push URL to GitHub`);
|
|
49
|
+
}
|
|
50
|
+
run(`git push tangled main`, repoDir);
|
|
51
|
+
console.log(`[PUSH] Pushed main to Tangled`);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.warn(`[WARN] Could not push ${repoName} to Tangled. Check SSH or repo existence.`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
var BASE32_SORTABLE = "234567abcdefghijklmnopqrstuvwxyz";
|
|
57
|
+
function run(cmd, cwd) {
|
|
58
|
+
const options = {
|
|
59
|
+
cwd,
|
|
60
|
+
stdio: "pipe",
|
|
61
|
+
shell: process.env.SHELL || "/bin/bash",
|
|
62
|
+
encoding: "utf-8"
|
|
63
|
+
};
|
|
64
|
+
return execSync(cmd, options).toString().trim();
|
|
65
|
+
}
|
|
66
|
+
function ensureDir(dir) {
|
|
67
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
}
|
|
69
|
+
function generateClockId() {
|
|
70
|
+
return Math.floor(Math.random() * 1024);
|
|
71
|
+
}
|
|
72
|
+
function toBase32Sortable(num) {
|
|
73
|
+
if (num === 0n) return "2222222222222";
|
|
74
|
+
let result = "";
|
|
75
|
+
while (num > 0n) {
|
|
76
|
+
result = BASE32_SORTABLE[Number(num % 32n)] + result;
|
|
77
|
+
num = num / 32n;
|
|
78
|
+
}
|
|
79
|
+
return result.padStart(13, "2");
|
|
80
|
+
}
|
|
81
|
+
function generateTid() {
|
|
82
|
+
const nowMicroseconds = BigInt(Date.now()) * 1000n;
|
|
83
|
+
const clockId = generateClockId();
|
|
84
|
+
const tidBigInt = nowMicroseconds << 10n | BigInt(clockId);
|
|
85
|
+
return toBase32Sortable(tidBigInt);
|
|
86
|
+
}
|
|
87
|
+
var recordCache = {};
|
|
88
|
+
async function ensureTangledRecord(agent2, atprotoDid, githubUser, repoName, description) {
|
|
89
|
+
if (recordCache[repoName]) {
|
|
90
|
+
return { tid: recordCache[repoName], existed: true };
|
|
91
|
+
}
|
|
92
|
+
let cursor = void 0;
|
|
93
|
+
let tid = null;
|
|
94
|
+
do {
|
|
95
|
+
const res = await agent2.api.com.atproto.repo.listRecords({
|
|
96
|
+
repo: atprotoDid,
|
|
97
|
+
collection: "sh.tangled.repo",
|
|
98
|
+
limit: 50,
|
|
99
|
+
cursor
|
|
100
|
+
});
|
|
101
|
+
for (const record of res.data.records) {
|
|
102
|
+
const value = record.value;
|
|
103
|
+
if (value.name === repoName && record.rkey) {
|
|
104
|
+
tid = record.rkey;
|
|
105
|
+
recordCache[repoName] = tid;
|
|
106
|
+
console.log(`[FOUND] Existing record for ${repoName} (TID: ${tid})`);
|
|
107
|
+
return { tid, existed: true };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
cursor = res.data.cursor;
|
|
111
|
+
} while (!tid && cursor);
|
|
112
|
+
if (!tid) {
|
|
113
|
+
tid = generateTid();
|
|
114
|
+
const record = {
|
|
115
|
+
$type: "sh.tangled.repo",
|
|
116
|
+
name: repoName,
|
|
117
|
+
knot: "knot1.tangled.sh",
|
|
118
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
119
|
+
description: description ?? repoName,
|
|
120
|
+
source: `https://github.com/${githubUser}/${repoName}`,
|
|
121
|
+
labels: []
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
const result = await agent2.api.com.atproto.repo.putRecord({
|
|
125
|
+
repo: atprotoDid,
|
|
126
|
+
collection: "sh.tangled.repo",
|
|
127
|
+
rkey: tid,
|
|
128
|
+
record
|
|
129
|
+
});
|
|
130
|
+
console.log(`[CREATED] ATProto record URI: ${result.data.uri}`);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(`[ERROR] Failed to create ATProto record for ${repoName}:`, error.message);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
recordCache[repoName] = tid;
|
|
136
|
+
console.log(`[CREATED] Tangled record for ${repoName} (TID: ${tid})`);
|
|
137
|
+
return { tid, existed: false };
|
|
138
|
+
}
|
|
139
|
+
return { tid, existed: false };
|
|
140
|
+
}
|
|
141
|
+
function updateReadme(baseDir, repoName, atprotoDid) {
|
|
142
|
+
const repoDir = path.join(baseDir, repoName);
|
|
143
|
+
const readmeFiles = ["README.md", "README.MD", "README.txt", "README"];
|
|
144
|
+
const readmeFile = readmeFiles.find((f) => fs.existsSync(path.join(repoDir, f)));
|
|
145
|
+
if (!readmeFile) return;
|
|
146
|
+
const readmePath = path.join(repoDir, readmeFile);
|
|
147
|
+
const content = fs.readFileSync(readmePath, "utf-8");
|
|
148
|
+
if (!/tangled\.org/i.test(content)) {
|
|
149
|
+
fs.appendFileSync(
|
|
150
|
+
readmePath,
|
|
151
|
+
`
|
|
152
|
+
Mirrored on Tangled: https://tangled.org/${atprotoDid}/${repoName}
|
|
153
|
+
`
|
|
154
|
+
);
|
|
155
|
+
run(`git add ${readmeFile}`, repoDir);
|
|
156
|
+
run(`git commit -m "Add Tangled mirror reference to README"`, repoDir);
|
|
157
|
+
run(`git push origin main`, repoDir);
|
|
158
|
+
console.log(`[README] Updated for ${repoName}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async function main() {
|
|
162
|
+
console.log("[STARTUP] Starting Tangled Sync...");
|
|
163
|
+
if (FORCE_SYNC) {
|
|
164
|
+
console.log("[MODE] Force sync enabled - will process all repos");
|
|
165
|
+
}
|
|
166
|
+
console.log(`[CONFIG] Base directory: ${BASE_DIR}`);
|
|
167
|
+
console.log(`[CONFIG] GitHub user: ${GITHUB_USER}`);
|
|
168
|
+
console.log(`[CONFIG] ATProto DID: ${ATPROTO_DID}`);
|
|
169
|
+
console.log(`[CONFIG] PDS: ${BLUESKY_PDS}`);
|
|
170
|
+
await login();
|
|
171
|
+
ensureDir(BASE_DIR);
|
|
172
|
+
console.log(`[GITHUB] Fetching repositories for ${GITHUB_USER}...`);
|
|
173
|
+
const repos = await getGitHubRepos();
|
|
174
|
+
console.log(`[GITHUB] Found ${repos.length} repositories`);
|
|
175
|
+
let reposToProcess = repos;
|
|
176
|
+
let skippedRepos = [];
|
|
177
|
+
if (!FORCE_SYNC) {
|
|
178
|
+
console.log(`[ATPROTO] Fetching existing Tangled records...`);
|
|
179
|
+
let cursor = void 0;
|
|
180
|
+
const existingRepos = /* @__PURE__ */ new Set();
|
|
181
|
+
do {
|
|
182
|
+
const res = await agent.api.com.atproto.repo.listRecords({
|
|
183
|
+
repo: ATPROTO_DID,
|
|
184
|
+
collection: "sh.tangled.repo",
|
|
185
|
+
limit: 100,
|
|
186
|
+
cursor
|
|
187
|
+
});
|
|
188
|
+
for (const record of res.data.records) {
|
|
189
|
+
const value = record.value;
|
|
190
|
+
if (value.name) {
|
|
191
|
+
existingRepos.add(value.name);
|
|
192
|
+
recordCache[value.name] = record.rkey;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
cursor = res.data.cursor;
|
|
196
|
+
} while (cursor);
|
|
197
|
+
console.log(`[ATPROTO] Found ${existingRepos.size} existing Tangled records`);
|
|
198
|
+
reposToProcess = repos.filter((r) => !existingRepos.has(r.name));
|
|
199
|
+
skippedRepos = repos.filter((r) => existingRepos.has(r.name));
|
|
200
|
+
console.log(`[INFO] ${reposToProcess.length} new repos to sync`);
|
|
201
|
+
console.log(`[INFO] ${skippedRepos.length} repos already synced (skipping)
|
|
202
|
+
`);
|
|
203
|
+
if (skippedRepos.length > 0) {
|
|
204
|
+
console.log("[SKIPPED] The following repos already have AT Proto records:");
|
|
205
|
+
skippedRepos.forEach((r) => console.log(` - ${r.name}`));
|
|
206
|
+
console.log("");
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
console.log("[INFO] Processing all ${repos.length} repos (force sync mode)\n");
|
|
210
|
+
}
|
|
211
|
+
let syncedCount = 0;
|
|
212
|
+
let errorCount = 0;
|
|
213
|
+
for (const { clone_url, name: repoName, description } of reposToProcess) {
|
|
214
|
+
console.log(`
|
|
215
|
+
[PROGRESS] Processing ${repoName} (${syncedCount + 1}/${reposToProcess.length})`);
|
|
216
|
+
const repoDir = path.join(BASE_DIR, repoName);
|
|
217
|
+
try {
|
|
218
|
+
if (!fs.existsSync(repoDir)) {
|
|
219
|
+
run(`git clone ${clone_url} ${repoDir}`);
|
|
220
|
+
console.log(`[CLONE] ${repoName}`);
|
|
221
|
+
} else {
|
|
222
|
+
console.log(`[EXISTS] ${repoName} already cloned`);
|
|
223
|
+
}
|
|
224
|
+
await ensureTangledRemoteAndPush(repoDir, repoName, clone_url);
|
|
225
|
+
updateReadme(BASE_DIR, repoName, ATPROTO_DID);
|
|
226
|
+
const result = await ensureTangledRecord(agent, ATPROTO_DID, GITHUB_USER, repoName, description);
|
|
227
|
+
if (!result.existed) {
|
|
228
|
+
syncedCount++;
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error(`[ERROR] Failed to sync ${repoName}: ${error.message}`);
|
|
232
|
+
errorCount++;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
console.log(`
|
|
236
|
+
${"=".repeat(50)}`);
|
|
237
|
+
console.log(`[COMPLETE] Sync finished!`);
|
|
238
|
+
console.log(` \u2705 New repos synced: ${syncedCount}`);
|
|
239
|
+
if (!FORCE_SYNC) {
|
|
240
|
+
console.log(` \u23ED\uFE0F Repos skipped: ${skippedRepos.length}`);
|
|
241
|
+
}
|
|
242
|
+
if (errorCount > 0) {
|
|
243
|
+
console.log(` \u274C Errors: ${errorCount}`);
|
|
244
|
+
}
|
|
245
|
+
console.log(`${"=".repeat(50)}`);
|
|
246
|
+
}
|
|
247
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// src/test-atproto.ts
|
|
2
|
+
import { AtpAgent } from "@atproto/api";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
dotenv.config();
|
|
5
|
+
async function testAtProtoConnection() {
|
|
6
|
+
console.log("Testing AT Proto Connection...\n");
|
|
7
|
+
const service = process.env.BLUESKY_PDS || "https://bsky.social";
|
|
8
|
+
const username = process.env.BLUESKY_USERNAME;
|
|
9
|
+
const password = process.env.BLUESKY_PASSWORD;
|
|
10
|
+
const atprotoDid = process.env.ATPROTO_DID;
|
|
11
|
+
console.log(`Service: ${service}`);
|
|
12
|
+
console.log(`Username: ${username}`);
|
|
13
|
+
console.log(`Expected DID: ${atprotoDid}
|
|
14
|
+
`);
|
|
15
|
+
if (!username || !password) {
|
|
16
|
+
console.error("ERROR: Missing BLUESKY_USERNAME or BLUESKY_PASSWORD");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const agent = new AtpAgent({ service });
|
|
20
|
+
try {
|
|
21
|
+
console.log("Attempting login...");
|
|
22
|
+
const loginResponse = await agent.login({
|
|
23
|
+
identifier: username,
|
|
24
|
+
password
|
|
25
|
+
});
|
|
26
|
+
console.log("\u2713 Login successful!");
|
|
27
|
+
console.log(` DID: ${loginResponse.data.did}`);
|
|
28
|
+
console.log(` Handle: ${loginResponse.data.handle}`);
|
|
29
|
+
console.log(` Email: ${loginResponse.data.email || "N/A"}`);
|
|
30
|
+
if (loginResponse.data.did !== atprotoDid) {
|
|
31
|
+
console.warn(`
|
|
32
|
+
\u26A0 WARNING: Logged in DID (${loginResponse.data.did}) does not match ATPROTO_DID in .env (${atprotoDid})`);
|
|
33
|
+
console.warn(" Please update your ATPROTO_DID in src/.env");
|
|
34
|
+
}
|
|
35
|
+
console.log("\nFetching existing sh.tangled.repo records...");
|
|
36
|
+
const records = await agent.api.com.atproto.repo.listRecords({
|
|
37
|
+
repo: loginResponse.data.did,
|
|
38
|
+
collection: "sh.tangled.repo",
|
|
39
|
+
limit: 10
|
|
40
|
+
});
|
|
41
|
+
console.log(`\u2713 Found ${records.data.records.length} existing Tangled repo records`);
|
|
42
|
+
if (records.data.records.length > 0) {
|
|
43
|
+
console.log("\nSample records:");
|
|
44
|
+
records.data.records.slice(0, 3).forEach((record) => {
|
|
45
|
+
console.log(` - ${record.value.name} (${record.uri})`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
console.log("\n\u2713 AT Proto connection test completed successfully!");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error("\n\u2717 AT Proto connection test failed!");
|
|
51
|
+
console.error(`Error: ${error.message}`);
|
|
52
|
+
if (error.status) {
|
|
53
|
+
console.error(`HTTP Status: ${error.status}`);
|
|
54
|
+
}
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
testAtProtoConnection();
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// src/validate-config.ts
|
|
2
|
+
import dotenv from "dotenv";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
var __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
var __dirname = path.dirname(__filename);
|
|
8
|
+
dotenv.config();
|
|
9
|
+
console.log("\u{1F50D} Validating Tangled Sync Configuration...\n");
|
|
10
|
+
var checks = [];
|
|
11
|
+
var envPath = path.join(__dirname, ".env");
|
|
12
|
+
var envExists = fs.existsSync(envPath);
|
|
13
|
+
checks.push({
|
|
14
|
+
name: ".env file",
|
|
15
|
+
status: envExists,
|
|
16
|
+
message: envExists ? "Found at src/.env" : "Missing! Copy src/.env.example to src/.env"
|
|
17
|
+
});
|
|
18
|
+
var requiredVars = [
|
|
19
|
+
{ name: "BASE_DIR", description: "Base directory for repos" },
|
|
20
|
+
{ name: "GITHUB_USER", description: "GitHub username" },
|
|
21
|
+
{ name: "ATPROTO_DID", description: "AT Proto DID" },
|
|
22
|
+
{ name: "BLUESKY_PDS", description: "Bluesky PDS URL" },
|
|
23
|
+
{ name: "BLUESKY_USERNAME", description: "Bluesky username" },
|
|
24
|
+
{ name: "BLUESKY_PASSWORD", description: "Bluesky app password" }
|
|
25
|
+
];
|
|
26
|
+
requiredVars.forEach(({ name, description }) => {
|
|
27
|
+
const value = process.env[name];
|
|
28
|
+
const exists = !!value && value.trim().length > 0;
|
|
29
|
+
checks.push({
|
|
30
|
+
name: `${name}`,
|
|
31
|
+
status: exists,
|
|
32
|
+
message: exists ? `\u2713 Set (${description})` : `\u2717 Missing (${description})`
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
var baseDir = process.env.BASE_DIR;
|
|
36
|
+
if (baseDir) {
|
|
37
|
+
const baseDirExists = fs.existsSync(baseDir);
|
|
38
|
+
checks.push({
|
|
39
|
+
name: "BASE_DIR exists",
|
|
40
|
+
status: baseDirExists,
|
|
41
|
+
message: baseDirExists ? `Directory exists: ${baseDir}` : `Directory missing: ${baseDir} (will be created)`
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
var did = process.env.ATPROTO_DID;
|
|
45
|
+
if (did) {
|
|
46
|
+
const validDid = did.startsWith("did:plc:") || did.startsWith("did:web:");
|
|
47
|
+
checks.push({
|
|
48
|
+
name: "DID format",
|
|
49
|
+
status: validDid,
|
|
50
|
+
message: validDid ? "Valid DID format" : "Invalid! Should start with 'did:plc:' or 'did:web:'"
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
var pds = process.env.BLUESKY_PDS;
|
|
54
|
+
if (pds) {
|
|
55
|
+
const validPds = pds.startsWith("http://") || pds.startsWith("https://");
|
|
56
|
+
checks.push({
|
|
57
|
+
name: "PDS URL format",
|
|
58
|
+
status: validPds,
|
|
59
|
+
message: validPds ? `Valid URL: ${pds}` : "Invalid! Should start with 'https://'"
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
console.log("Configuration Check Results:\n");
|
|
63
|
+
var allPassed = true;
|
|
64
|
+
checks.forEach((check) => {
|
|
65
|
+
const icon = check.status ? "\u2705" : "\u274C";
|
|
66
|
+
console.log(`${icon} ${check.name}: ${check.message}`);
|
|
67
|
+
if (!check.status) allPassed = false;
|
|
68
|
+
});
|
|
69
|
+
console.log("\n" + "=".repeat(50) + "\n");
|
|
70
|
+
if (allPassed) {
|
|
71
|
+
console.log("\u2705 All checks passed! You're ready to run:");
|
|
72
|
+
console.log(" npm run test-atproto # Test AT Proto connection");
|
|
73
|
+
console.log(" npm run sync # Run the full sync");
|
|
74
|
+
} else {
|
|
75
|
+
console.log("\u274C Some checks failed. Please fix the issues above.");
|
|
76
|
+
console.log(" See SETUP.md for detailed instructions.");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
console.log("\n\u{1F4A1} Recommendations:");
|
|
80
|
+
if (process.env.BLUESKY_PASSWORD && !process.env.BLUESKY_PASSWORD.includes("-")) {
|
|
81
|
+
console.log(" \u26A0\uFE0F Your password looks like it might be a regular password.");
|
|
82
|
+
console.log(" Consider using an App Password from Bluesky settings.");
|
|
83
|
+
}
|
|
84
|
+
console.log(" \u{1F4DA} Read SETUP.md for detailed setup instructions");
|
|
85
|
+
console.log(" \u{1F510} Never commit your .env file to version control");
|
|
86
|
+
console.log(" \u{1F511} Make sure your SSH key is added to Tangled");
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ewanc26/tangled-sync",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Sync GitHub repos to Tangled with ATProto records",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tangled-sync": "./dist/index.js",
|
|
8
|
+
"tangled-sync-check": "./dist/check.js",
|
|
9
|
+
"tangled-sync-validate": "./dist/validate-config.js",
|
|
10
|
+
"tangled-sync-test-atproto": "./dist/test-atproto.js"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"USAGE.md"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@atproto/api": "^0.17.2",
|
|
22
|
+
"dotenv": "^16.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
26
|
+
"ts-node": "^10.9.2",
|
|
27
|
+
"tsup": "^8.5.0",
|
|
28
|
+
"typescript": "^5.9.3"
|
|
29
|
+
},
|
|
30
|
+
"author": "Ewan Croft",
|
|
31
|
+
"license": "AGPL-3.0-only",
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup src/index.ts src/check.ts src/validate-config.ts src/test-atproto.ts --format esm --outDir dist --no-dts --clean",
|
|
34
|
+
"dev": "ts-node src/index.ts",
|
|
35
|
+
"check": "ts-node src/check.ts",
|
|
36
|
+
"validate": "ts-node src/validate-config.ts",
|
|
37
|
+
"test-atproto": "ts-node src/test-atproto.ts",
|
|
38
|
+
"sync": "ts-node src/index.ts",
|
|
39
|
+
"sync:force": "ts-node src/index.ts --force",
|
|
40
|
+
"type-check": "tsc --noEmit"
|
|
41
|
+
}
|
|
42
|
+
}
|