From 6467d9fe65ec94a9fec94c592fea98934d9a2e89 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Sat, 7 Mar 2026 22:56:01 +0000 Subject: [PATCH] tangled-sync: add build script and publish config --- packages/tangled-sync | 1 - packages/tangled-sync/.gitignore | 190 ++++++++++ packages/tangled-sync/LICENSE | 21 ++ packages/tangled-sync/README.md | 124 +++++++ packages/tangled-sync/SETUP.md | 175 +++++++++ packages/tangled-sync/USAGE.md | 197 ++++++++++ packages/tangled-sync/package-lock.json | 361 +++++++++++++++++++ packages/tangled-sync/package.json | 40 ++ packages/tangled-sync/src/.env.example | 15 + packages/tangled-sync/src/check.ts | 266 ++++++++++++++ packages/tangled-sync/src/index.ts | 318 ++++++++++++++++ packages/tangled-sync/src/test-atproto.ts | 71 ++++ packages/tangled-sync/src/validate-config.ts | 109 ++++++ packages/tangled-sync/tsconfig.json | 26 ++ 14 files changed, 1913 insertions(+), 1 deletion(-) delete mode 160000 packages/tangled-sync create mode 100644 packages/tangled-sync/.gitignore create mode 100644 packages/tangled-sync/LICENSE create mode 100644 packages/tangled-sync/README.md create mode 100644 packages/tangled-sync/SETUP.md create mode 100644 packages/tangled-sync/USAGE.md create mode 100644 packages/tangled-sync/package-lock.json create mode 100644 packages/tangled-sync/package.json create mode 100644 packages/tangled-sync/src/.env.example create mode 100644 packages/tangled-sync/src/check.ts create mode 100644 packages/tangled-sync/src/index.ts create mode 100644 packages/tangled-sync/src/test-atproto.ts create mode 100644 packages/tangled-sync/src/validate-config.ts create mode 100644 packages/tangled-sync/tsconfig.json diff --git a/packages/tangled-sync b/packages/tangled-sync deleted file mode 160000 index fc693fc..0000000 --- a/packages/tangled-sync +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fc693fcfa887121ffaf6d2b5000adbc2e10a4c9c diff --git a/packages/tangled-sync/.gitignore b/packages/tangled-sync/.gitignore new file mode 100644 index 0000000..e8bf3df --- /dev/null +++ b/packages/tangled-sync/.gitignore @@ -0,0 +1,190 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node,macos,svelte,vercel +# Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,svelte,vercel + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### Svelte ### +# gitignore template for the SvelteKit, frontend web component framework +# website: https://kit.svelte.dev/ + +.svelte-kit/ +package + +### Vercel ### +.vercel + +# End of https://www.toptal.com/developers/gitignore/api/node,macos,svelte,vercel + +git-diff*.txt +node_modules \ No newline at end of file diff --git a/packages/tangled-sync/LICENSE b/packages/tangled-sync/LICENSE new file mode 100644 index 0000000..dcf956e --- /dev/null +++ b/packages/tangled-sync/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Ewan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/tangled-sync/README.md b/packages/tangled-sync/README.md new file mode 100644 index 0000000..7f6fe3c --- /dev/null +++ b/packages/tangled-sync/README.md @@ -0,0 +1,124 @@ +# Tangled Sync + +**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. + +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. + +--- + +## Getting Started + +### Configuration + +Before running any scripts, you need to configure the project. Create a `src/.env` file based on `src/.env.example`: + +```bash +cp src/.env.example src/.env +``` + +Then edit `src/.env` with your actual values: + +* `BASE_DIR` – the local directory where GitHub repositories will be cloned. +* `GITHUB_USER` – your GitHub username or organisation. +* `ATPROTO_DID` – your ATProto DID (Decentralized Identifier). +* `BLUESKY_PDS` – the URL of your Bluesky PDS instance. +* `BLUESKY_USERNAME` – your Bluesky username. +* `BLUESKY_PASSWORD` – your Bluesky password. + +Make sure this file is properly set up before proceeding. + +--- + +### Installation + +1. Clone this repository locally. +2. Navigate to the project directory. +3. Run: + +```bash +npm install +``` + +This will install all dependencies required for syncing GitHub repositories and interacting with ATProto. + +--- + +### Verify SSH Connection to Tangled + +* 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. + +Without proper SSH authentication, repository creation and pushing will fail. + +--- + +### Testing AT Proto Connection + +**Before running the full sync**, test your AT Proto connection: + +```bash +npm run test-atproto +``` + +This will: +- Verify your Bluesky credentials +- Confirm your DID matches the configuration +- List any existing `sh.tangled.repo` records +- Validate the connection to the PDS + +### Running the Sync Script + +Once configuration, SSH verification, and AT Proto testing are complete, run: + +```bash +npm run sync +``` + +What happens during the sync: + +1. **Login to Bluesky:** The script authenticates using your credentials to allow publishing ATProto records. +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). +3. **Ensure Tangled Remotes:** For each repository, a `tangled` remote is added if it doesn’t exist. +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. +5. **Update README:** Each repository’s README is updated to include a link to its Tangled mirror, if it isn’t already present. +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. + +--- + +### Notes & Best Practices + +* **Directory Management:** The script ensures that your `BASE_DIR` exists and creates it if necessary. +* **Record Uniqueness:** ATProto records use a time-based, sortable ID (TID) to ensure uniqueness. Duplicate IDs are avoided automatically. +* **Error Handling:** If a repository cannot be pushed to Tangled, the script logs a warning but continues processing the remaining repositories. +* **Idempotency:** Running the script multiple times is safe; existing remotes and ATProto records are checked before creation to prevent duplicates. + +--- + +### Example Workflow + +```bash +# Run the sync script +npm run sync +``` + +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. + +This allows you to quickly confirm that all GitHub repositories have been mirrored and documented properly on Tangled. + +--- + +### Contribution & Development + +If you plan to contribute: + +* Ensure Node.js v18+ and npm v9+ are installed. +* Test the script in a separate directory to avoid accidentally overwriting your production repositories. +* Use `console.log` statements to debug or track progress during development. +* Maintain proper `.env` configuration to avoid leaking credentials. + +--- + +**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. + +## ☕ Support + +If you found this useful, consider [buying me a ko-fi](https://ko-fi.com/ewancroft)! diff --git a/packages/tangled-sync/SETUP.md b/packages/tangled-sync/SETUP.md new file mode 100644 index 0000000..4448683 --- /dev/null +++ b/packages/tangled-sync/SETUP.md @@ -0,0 +1,175 @@ +# Tangled Sync - Setup & Troubleshooting Guide + +## Quick Setup Checklist + +### 1. Install Dependencies +```bash +npm install +``` + +### 2. Configure Environment Variables +```bash +# Copy the example env file +cp src/.env.example src/.env + +# Edit with your actual values +nano src/.env # or use your preferred editor +``` + +**Required values:** +- `BASE_DIR`: Where to clone repos (e.g., `/Users/you/tangled-repos`) +- `GITHUB_USER`: Your GitHub username +- `ATPROTO_DID`: Your AT Proto DID (get from Bluesky settings) +- `BLUESKY_PDS`: Usually `https://bsky.social` +- `BLUESKY_USERNAME`: Your Bluesky handle (e.g., `you.bsky.social`) +- `BLUESKY_PASSWORD`: Use an **app password**, not your main password! + +### 3. Get Your AT Proto DID + +Your DID can be found by: +1. Go to https://bsky.app +2. Click your profile +3. Settings → Advanced → Account +4. Look for "DID" (starts with `did:plc:`) + +Alternatively, visit: `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=YOUR_HANDLE.bsky.social` + +### 4. Create an App Password + +**IMPORTANT:** Do NOT use your main Bluesky password! + +1. Go to https://bsky.app/settings +2. Navigate to "App Passwords" +3. Click "Add App Password" +4. Give it a name (e.g., "Tangled Sync") +5. Copy the generated password to your `.env` file + +### 5. Test AT Proto Connection +```bash +npm run test-atproto +``` + +Expected output: +``` +✓ Login successful! + DID: did:plc:... + Handle: you.bsky.social + Email: your@email.com + +✓ Found X existing Tangled repo records +``` + +### 6. Verify SSH to Tangled +```bash +ssh git@tangled.sh +``` + +You should see a message confirming your SSH key is configured. + +### 7. Run the Sync +```bash +npm run sync +``` + +--- + +## Common Issues & Solutions + +### Issue: "Missing Bluesky credentials" +**Solution:** Check that `src/.env` exists and contains `BLUESKY_USERNAME` and `BLUESKY_PASSWORD` + +### Issue: "Login failed" or "Invalid credentials" +**Solution:** +- Ensure you're using an **app password**, not your main password +- Check your username includes the full handle (e.g., `you.bsky.social`) +- Verify credentials are correct + +### Issue: "DID mismatch" +**Solution:** +- Run `npm run test-atproto` to see your actual DID +- Update `ATPROTO_DID` in `src/.env` to match + +### Issue: "Could not push to Tangled" +**Solution:** +- Verify SSH key is added to Tangled: https://tangled.org/settings/keys +- Test SSH connection: `ssh git@tangled.sh` +- Ensure the repository exists on Tangled first + +### Issue: "Failed to create ATProto record" +**Solution:** +- Check that the schema matches (required fields: `name`, `knot`, `createdAt`) +- Verify your app password has write permissions +- Check PDS is reachable: `curl https://bsky.social` + +### Issue: Rate limiting from GitHub API +**Solution:** +- GitHub has a rate limit of 60 requests/hour for unauthenticated requests +- Consider adding GitHub authentication if syncing many repos +- Wait an hour and try again + +--- + +## Understanding the Workflow + +1. **Login to AT Proto**: Authenticates with Bluesky PDS using your credentials +2. **Fetch GitHub Repos**: Retrieves all public repos from your GitHub account +3. **Clone Locally**: Downloads repos to `BASE_DIR` if not already present +4. **Add Tangled Remote**: Adds `tangled` as a git remote +5. **Push to Tangled**: Pushes the `main` branch to Tangled +6. **Update README**: Adds a Tangled mirror link to the README +7. **Create AT Proto Record**: Publishes metadata to the AT Proto network + +Each repository gets a record in the `sh.tangled.repo` collection with: +- Repository name and description +- Source URL (GitHub) +- Creation timestamp +- Knot server reference +- Optional labels and topics + +--- + +## Verifying Success + +After running the sync, you can verify: + +1. **Local repos**: Check `BASE_DIR` for cloned repositories +2. **Tangled remotes**: Run `git remote -v` in any repo directory +3. **AT Proto records**: Run `npm run test-atproto` to list records +4. **Tangled website**: Visit `https://tangled.org/YOUR_DID/REPO_NAME` + +--- + +## Advanced Configuration + +### Using a Different PDS +If you're not using the default Bluesky PDS: +```bash +BLUESKY_PDS=https://your-pds.example.com +``` + +### Syncing Specific Repos Only +Modify the `getGitHubRepos()` function to filter repos: +```typescript +return json + .filter((r: any) => r.name.startsWith('my-prefix-')) + .map(...); +``` + +### Changing the Default Branch +If your repos use `master` instead of `main`, update: +```typescript +run(`git push tangled master`, repoDir); +``` + +--- + +## Support + +For issues with: +- **Tangled**: https://github.com/tangled-dev/tangled +- **AT Proto**: https://atproto.com/docs +- **This tool**: Open an issue in the repository + +--- + +**Happy syncing! 🚀** diff --git a/packages/tangled-sync/USAGE.md b/packages/tangled-sync/USAGE.md new file mode 100644 index 0000000..3540144 --- /dev/null +++ b/packages/tangled-sync/USAGE.md @@ -0,0 +1,197 @@ +# 🎉 Tangled Sync - Ready to Use! + +## Summary of Changes + +I've improved your Tangled Sync project to ensure proper AT Proto authentication and repository record creation. Here's what was updated: + +### ✅ What's Fixed + +1. **Enhanced AT Proto Login** + - Added better error handling and validation + - Shows DID and handle on successful login + - Clearer error messages when authentication fails + +2. **Corrected Repository Schema** + - Fixed record structure to match `sh.tangled.repo` lexicon + - Required fields (`name`, `knot`, `createdAt`) now ordered correctly + - Optional fields properly marked as optional + - Added better error handling for record creation + +3. **Improved Logging** + - More detailed startup information + - Better progress tracking during sync + - Shows AT Proto record URIs when created + - Success/failure messages are clearer + +### 📁 New Files Created + +1. **`src/.env.example`** - Template for your configuration +2. **`src/test-atproto.ts`** - Test AT Proto connection before syncing +3. **`src/validate-config.ts`** - Validate your environment setup +4. **`SETUP.md`** - Comprehensive setup and troubleshooting guide + +### 🚀 How to Use + +#### Step 1: Configure Environment +```bash +# Copy the example file +cp src/.env.example src/.env + +# Edit with your actual values +nano src/.env +``` + +You need: +- Your GitHub username +- Your AT Proto DID (from Bluesky settings) +- A Bluesky **app password** (not your main password!) +- Base directory for repos + +#### Step 2: Validate Configuration +```bash +npm run validate +``` + +This checks all your environment variables are set correctly. + +#### Step 3: Test AT Proto Connection +```bash +npm run test-atproto +``` + +This verifies: +- ✅ Your credentials work +- ✅ Your DID is correct +- ✅ You can access the PDS +- ✅ Shows any existing Tangled repo records + +#### Step 4: Run the Sync +```bash +npm run sync +``` + +This will: +1. Login to AT Proto ✅ +2. Fetch your GitHub repos +3. Clone them locally (if needed) +4. Add Tangled remotes +5. Push to Tangled +6. Update READMEs +7. Create AT Proto records for each repo ✅ + +### 🔍 What to Check + +After running the sync, verify: + +1. **AT Proto Records Created** + ```bash + npm run test-atproto + ``` + Should show your repos listed + +2. **Repos on Tangled** + Visit: `https://tangled.org/YOUR_DID/REPO_NAME` + +3. **Local Git Remotes** + ```bash + cd YOUR_BASE_DIR/some-repo + git remote -v + ``` + Should show both `origin` (GitHub) and `tangled` remotes + +### 📊 Record Schema + +Each repository creates a record with this structure: + +```typescript +{ + $type: "sh.tangled.repo", + name: "your-repo-name", // required + knot: "knot1.tangled.sh", // required + createdAt: "2024-01-01T00:00:00Z", // required + description: "Repo description", // optional + source: "https://github.com/...", // optional + labels: [], // optional +} +``` + +This matches the official `sh.tangled.repo` lexicon schema. + +### ⚠️ Important Notes + +1. **Use App Password**: Never use your main Bluesky password. Create an app password in Settings → App Passwords. + +2. **Check Your DID**: Run `npm run test-atproto` first to ensure your DID in `.env` matches your actual account. + +3. **SSH Key Required**: Make sure your SSH key is added to Tangled at https://tangled.org/settings/keys + +4. **Rate Limits**: GitHub API has rate limits (60 req/hour unauthenticated). If you have many repos, consider adding GitHub auth. + +### 🐛 Troubleshooting + +**"Missing Bluesky credentials"** +- Check `src/.env` exists and has `BLUESKY_USERNAME` and `BLUESKY_PASSWORD` + +**"Login failed"** +- Verify you're using an app password, not your main password +- Check username includes full handle (e.g., `you.bsky.social`) + +**"Could not push to Tangled"** +- Verify SSH key is configured: `ssh git@tangled.sh` +- Check repo exists on Tangled + +**"Failed to create ATProto record"** +- Run `npm run test-atproto` to check connection +- Verify your app password has write permissions + +See `SETUP.md` for more detailed troubleshooting. + +### 📚 Available Commands + +```bash +npm run check # Comprehensive health check (recommended first step!) +npm run validate # Check environment configuration only +npm run test-atproto # Test AT Proto connection only +npm run sync # Run sync (only new repos without AT Proto records) +npm run sync:force # Force sync all repos (including existing) +``` + +#### `npm run check` - Comprehensive Health Check + +This is the **most useful command** for troubleshooting! It runs all checks in one go: + +- ✅ Configuration validation +- ✅ AT Proto connection test +- ✅ SSH connection to Tangled +- ✅ GitHub API access +- ✅ Dependencies verification + +**When to use:** +- Before your first sync +- When troubleshooting issues +- After changing configuration +- To verify everything is working + +#### Individual Check Commands + +**Normal sync** (recommended): Only processes repos that don't have AT Proto records yet. This is efficient and safe for regular use. + +**Force sync**: Processes all repos regardless of whether they already have records. Use this if you need to: +- Re-push repos to Tangled +- Update READMEs for all repos +- Recover from a partial sync + +### ✨ Next Steps + +1. Copy and configure `src/.env` +2. Run `npm run validate` +3. Run `npm run test-atproto` +4. Run `npm run sync` + +That's it! Your GitHub repos will be synced to Tangled with proper AT Proto records. + +--- + +**Questions?** Check `SETUP.md` for detailed instructions and troubleshooting. + +**Happy syncing! 🚀** diff --git a/packages/tangled-sync/package-lock.json b/packages/tangled-sync/package-lock.json new file mode 100644 index 0000000..6ef60cb --- /dev/null +++ b/packages/tangled-sync/package-lock.json @@ -0,0 +1,361 @@ +{ + "name": "tangled-sync", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tangled-sync", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@atproto/api": "^0.17.2", + "dotenv": "^16.0.0" + }, + "devDependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@atproto/api": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.17.2.tgz", + "integrity": "sha512-luRY9YPaRQFpm3v7a1bTOaekQ/KPCG3gb0jVyaOtfMXDSfIZJh9lr9MtmGPdEp7AvfE8urkngZ+V/p8Ial3z2g==", + "license": "MIT", + "dependencies": { + "@atproto/common-web": "^0.4.3", + "@atproto/lexicon": "^0.5.1", + "@atproto/syntax": "^0.4.1", + "@atproto/xrpc": "^0.7.5", + "await-lock": "^2.2.2", + "multiformats": "^9.9.0", + "tlds": "^1.234.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/common-web": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", + "license": "MIT", + "dependencies": { + "graphemer": "^1.4.0", + "multiformats": "^9.9.0", + "uint8arrays": "3.0.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/lexicon": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", + "license": "MIT", + "dependencies": { + "@atproto/common-web": "^0.4.3", + "@atproto/syntax": "^0.4.1", + "iso-datestring-validator": "^2.2.2", + "multiformats": "^9.9.0", + "zod": "^3.23.8" + } + }, + "node_modules/@atproto/syntax": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", + "license": "MIT" + }, + "node_modules/@atproto/xrpc": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", + "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==", + "license": "MIT", + "dependencies": { + "@atproto/lexicon": "^0.5.1", + "zod": "^3.23.8" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.7.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz", + "integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "license": "MIT" + }, + "node_modules/iso-datestring-validator": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/multiformats": { + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/tlds": { + "version": "1.260.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.260.0.tgz", + "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", + "license": "MIT", + "bin": { + "tlds": "bin.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8arrays": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", + "license": "MIT", + "dependencies": { + "multiformats": "^9.4.2" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/tangled-sync/package.json b/packages/tangled-sync/package.json new file mode 100644 index 0000000..79c7fb1 --- /dev/null +++ b/packages/tangled-sync/package.json @@ -0,0 +1,40 @@ +{ + "name": "@ewanc26/tangled-sync", + "version": "1.0.0", + "description": "Sync GitHub repos to Tangled with ATProto records", + "type": "module", + "bin": { + "tangled-sync": "./dist/index.js", + "tangled-sync-check": "./dist/check.js", + "tangled-sync-validate": "./dist/validate-config.js", + "tangled-sync-test-atproto": "./dist/test-atproto.js" + }, + "publishConfig": { "access": "public" }, + "files": [ + "dist", + "README.md", + "USAGE.md" + ], + "scripts": { + "build": "tsup src/index.ts src/check.ts src/validate-config.ts src/test-atproto.ts --format esm --outDir dist --no-dts --clean", + "dev": "ts-node src/index.ts", + "check": "ts-node src/check.ts", + "validate": "ts-node src/validate-config.ts", + "test-atproto": "ts-node src/test-atproto.ts", + "sync": "ts-node src/index.ts", + "sync:force": "ts-node src/index.ts --force", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@atproto/api": "^0.17.2", + "dotenv": "^16.0.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "ts-node": "^10.9.2", + "tsup": "^8.5.0", + "typescript": "^5.9.3" + }, + "author": "Ewan Croft", + "license": "AGPL-3.0-only" +} diff --git a/packages/tangled-sync/src/.env.example b/packages/tangled-sync/src/.env.example new file mode 100644 index 0000000..00665ee --- /dev/null +++ b/packages/tangled-sync/src/.env.example @@ -0,0 +1,15 @@ +# Base directory where GitHub repos will be cloned +BASE_DIR=/path/to/your/repos + +# Your GitHub username +GITHUB_USER=your-github-username + +# Your ATProto DID (e.g., did:plc:abc123...) +ATPROTO_DID=did:plc:your-did-here + +# Bluesky PDS URL (usually https://bsky.social) +BLUESKY_PDS=https://bsky.social + +# Your Bluesky credentials +BLUESKY_USERNAME=your-handle.bsky.social +BLUESKY_PASSWORD=your-app-password diff --git a/packages/tangled-sync/src/check.ts b/packages/tangled-sync/src/check.ts new file mode 100644 index 0000000..4b4af5d --- /dev/null +++ b/packages/tangled-sync/src/check.ts @@ -0,0 +1,266 @@ +import { AtpAgent } from "@atproto/api"; +import dotenv from "dotenv"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { execSync } from "child_process"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +dotenv.config(); + +async function runHealthCheck() { + +console.log("🔍 Running Tangled Sync Health Check...\n"); + +const checks: { category: string; name: string; status: boolean; message: string }[] = []; +let errors = 0; +let warnings = 0; + +// ===== CONFIGURATION CHECKS ===== +console.log("📋 Configuration Checks\n"); + +const envPath = path.join(__dirname, ".env"); +const envExists = fs.existsSync(envPath); +checks.push({ + category: "config", + name: ".env file", + status: envExists, + message: envExists ? "Found at src/.env" : "Missing! Copy src/.env.example to src/.env" +}); +if (!envExists) errors++; + +const requiredVars = [ + { name: "BASE_DIR", description: "Base directory for repos" }, + { name: "GITHUB_USER", description: "GitHub username" }, + { name: "ATPROTO_DID", description: "AT Proto DID" }, + { name: "BLUESKY_PDS", description: "Bluesky PDS URL" }, + { name: "BLUESKY_USERNAME", description: "Bluesky username" }, + { name: "BLUESKY_PASSWORD", description: "Bluesky app password" }, +]; + +requiredVars.forEach(({ name, description }) => { + const value = process.env[name]; + const exists = !!value && value.trim().length > 0; + checks.push({ + category: "config", + name: name, + status: exists, + message: exists ? `Set` : `Missing (${description})` + }); + if (!exists) errors++; +}); + +// Check BASE_DIR +const baseDir = process.env.BASE_DIR; +if (baseDir) { + const baseDirExists = fs.existsSync(baseDir); + checks.push({ + category: "config", + name: "BASE_DIR path", + status: baseDirExists, + message: baseDirExists ? `Exists: ${baseDir}` : `Missing (will be created): ${baseDir}` + }); + if (!baseDirExists) warnings++; +} + +// Check DID format +const did = process.env.ATPROTO_DID; +if (did) { + const validDid = did.startsWith("did:plc:") || did.startsWith("did:web:"); + checks.push({ + category: "config", + name: "DID format", + status: validDid, + message: validDid ? "Valid" : "Invalid! Should start with 'did:plc:' or 'did:web:'" + }); + if (!validDid) errors++; +} + +// Check PDS URL +const pds = process.env.BLUESKY_PDS; +if (pds) { + const validPds = pds.startsWith("http://") || pds.startsWith("https://"); + checks.push({ + category: "config", + name: "PDS URL", + status: validPds, + message: validPds ? pds : "Invalid! Should start with 'https://'" + }); + if (!validPds) errors++; +} + +// Print config results +checks.filter(c => c.category === "config").forEach((check) => { + const icon = check.status ? "✅" : "❌"; + console.log(`${icon} ${check.name}: ${check.message}`); +}); + +// ===== AT PROTO CONNECTION CHECK ===== +console.log("\n🔐 AT Proto Connection Check\n"); + +const canTestConnection = process.env.BLUESKY_USERNAME && + process.env.BLUESKY_PASSWORD && + process.env.BLUESKY_PDS && + process.env.ATPROTO_DID; + +if (canTestConnection) { + try { + const agent = new AtpAgent({ service: process.env.BLUESKY_PDS! }); + + const loginResponse = await agent.login({ + identifier: process.env.BLUESKY_USERNAME!, + password: process.env.BLUESKY_PASSWORD! + }); + + console.log(`✅ Login successful`); + console.log(` DID: ${loginResponse.data.did}`); + console.log(` Handle: ${loginResponse.data.handle}`); + + if (loginResponse.data.did !== process.env.ATPROTO_DID) { + console.log(`⚠️ DID mismatch!`); + console.log(` Expected: ${process.env.ATPROTO_DID}`); + console.log(` Got: ${loginResponse.data.did}`); + warnings++; + } + + // Test fetching records + const records = await agent.api.com.atproto.repo.listRecords({ + repo: loginResponse.data.did, + collection: "sh.tangled.repo", + limit: 5, + }); + + console.log(`✅ Can access AT Proto records`); + console.log(` Found ${records.data.records.length} sample records`); + + } catch (error: any) { + console.log(`❌ AT Proto connection failed`); + console.log(` Error: ${error.message}`); + errors++; + } +} else { + console.log("⏭️ Skipped (missing credentials)"); +} + +// ===== SSH CONNECTION CHECK ===== +console.log("\n🔑 SSH Connection Check\n"); + +try { + const sshTest = execSync("ssh -T git@tangled.sh 2>&1", { + encoding: "utf-8", + timeout: 5000 + }); + + if (sshTest.includes("successfully authenticated") || sshTest.includes("Hi")) { + console.log("✅ SSH connection to Tangled works"); + console.log(` ${sshTest.trim().split('\n')[0]}`); + } else { + console.log("⚠️ SSH connection uncertain"); + console.log(` Response: ${sshTest.trim()}`); + warnings++; + } +} catch (error: any) { + const output = error.stdout?.toString() || error.message; + + if (output.includes("successfully authenticated") || output.includes("Hi")) { + console.log("✅ SSH connection to Tangled works"); + } else { + console.log("❌ SSH connection to Tangled failed"); + console.log(" Make sure your SSH key is added at https://tangled.org/settings/keys"); + errors++; + } +} + +// ===== GITHUB API CHECK ===== +console.log("\n🐙 GitHub API Check\n"); + +if (process.env.GITHUB_USER) { + try { + const response = execSync(`curl -s "https://api.github.com/users/${process.env.GITHUB_USER}"`, { + encoding: "utf-8", + timeout: 5000 + }); + + const data = JSON.parse(response); + + if (data.login) { + console.log(`✅ GitHub user found: ${data.login}`); + console.log(` Public repos: ${data.public_repos || 0}`); + } else { + console.log(`❌ GitHub user not found: ${process.env.GITHUB_USER}`); + errors++; + } + } catch (error: any) { + console.log(`⚠️ Could not check GitHub API`); + console.log(` ${error.message}`); + warnings++; + } +} else { + console.log("⏭️ Skipped (no GITHUB_USER set)"); +} + +// ===== DEPENDENCIES CHECK ===== +console.log("\n📦 Dependencies Check\n"); + +let hasAtproto = false; +let hasDotenv = false; + +try { + await import("@atproto/api"); + hasAtproto = true; + console.log("✅ @atproto/api installed"); +} catch { + console.log("❌ @atproto/api not installed (run: npm install)"); + errors++; +} + +try { + await import("dotenv"); + hasDotenv = true; + console.log("✅ dotenv installed"); +} catch { + console.log("❌ dotenv not installed (run: npm install)"); + errors++; +} + +// ===== SUMMARY ===== +console.log("\n" + "=".repeat(50)); + +if (errors === 0 && warnings === 0) { + console.log("✅ All checks passed! Ready to sync."); + console.log("\nNext steps:"); + console.log(" npm run sync # Sync new repos only"); + console.log(" npm run sync:force # Force sync all repos"); +} else { + if (errors > 0) { + console.log(`❌ ${errors} error(s) found - please fix before syncing`); + } + if (warnings > 0) { + console.log(`⚠️ ${warnings} warning(s) - review before syncing`); + } + + console.log("\nSee SETUP.md for detailed troubleshooting"); + + if (errors > 0) { + process.exit(1); + } +} + +console.log("=".repeat(50)); + +// Additional recommendations +if (process.env.BLUESKY_PASSWORD && !process.env.BLUESKY_PASSWORD.includes("-")) { + console.log("\n💡 Tip: Your password might be a regular password."); + console.log(" Consider using an App Password from Bluesky settings for better security."); +} + +} + +// Run the health check +runHealthCheck().catch((error) => { + console.error("\n❌ Health check failed with error:"); + console.error(error); + process.exit(1); +}); diff --git a/packages/tangled-sync/src/index.ts b/packages/tangled-sync/src/index.ts new file mode 100644 index 0000000..5a809cb --- /dev/null +++ b/packages/tangled-sync/src/index.ts @@ -0,0 +1,318 @@ +import { AtpAgent } from "@atproto/api"; +import dotenv from "dotenv"; +import fs from "fs"; +import path from "path"; +import { execSync } from "child_process"; + +dotenv.config(); + +const FORCE_SYNC = process.argv.includes("--force"); + +const BASE_DIR = process.env.BASE_DIR!; +const GITHUB_USER = process.env.GITHUB_USER!; +const ATPROTO_DID = process.env.ATPROTO_DID!; +const BLUESKY_PDS = process.env.BLUESKY_PDS!; +const TANGLED_BASE_URL = `git@tangled.sh:${ATPROTO_DID}`; + +const agent = new AtpAgent({ service: BLUESKY_PDS }); + +async function login() { + const username = process.env.BLUESKY_USERNAME; + const password = process.env.BLUESKY_PASSWORD; + if (!username || !password) { + throw new Error("Missing Bluesky credentials. Please set BLUESKY_USERNAME and BLUESKY_PASSWORD in src/.env"); + } + + try { + const response = await agent.login({ identifier: username, password }); + console.log(`[LOGIN] Successfully logged in to AT Proto as ${response.data.did}`); + console.log(`[LOGIN] Session handle: ${response.data.handle}`); + return response; + } catch (error: any) { + console.error("[ERROR] Failed to login to AT Proto:", error.message); + throw error; + } +} + +async function getGitHubRepos(): Promise<{ clone_url: string; name: string; description?: string }[]> { + const curl = `curl -s "https://api.github.com/users/${GITHUB_USER}/repos?per_page=200"`; + const output = run(curl); + const json = JSON.parse(output); + return json + .filter((r: any) => r.name !== GITHUB_USER) + .map((r: any) => ({ clone_url: r.clone_url, name: r.name, description: r.description })); +} + +async function ensureTangledRemoteAndPush(repoDir: string, repoName: string, cloneUrl: string) { + const tangledUrl = `${TANGLED_BASE_URL}/${repoName}`; + try { + const remotes = run("git remote", repoDir).split("\n"); + if (!remotes.includes("tangled")) { + console.log(`[REMOTE] Adding Tangled remote for ${repoName}`); + run(`git remote add tangled ${tangledUrl}`, repoDir); + } + + const originPushUrl = run("git remote get-url --push origin", repoDir); + if (originPushUrl.includes("tangled.sh")) { + run(`git remote set-url --push origin ${cloneUrl}`, repoDir); + console.log(`[REMOTE] Reset origin push URL to GitHub`); + } + + run(`git push tangled main`, repoDir); + console.log(`[PUSH] Pushed main to Tangled`); + } catch (error) { + console.warn(`[WARN] Could not push ${repoName} to Tangled. Check SSH or repo existence.`); + } +} + +const BASE32_SORTABLE = "234567abcdefghijklmnopqrstuvwxyz"; + +function run(cmd: string, cwd?: string): string { + const options: import("child_process").ExecSyncOptions = { + cwd, + stdio: "pipe", + shell: process.env.SHELL || "/bin/bash", + encoding: "utf-8", + }; + return execSync(cmd, options).toString().trim(); +} + +function ensureDir(dir: string) { + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); +} + +function generateClockId(): number { + return Math.floor(Math.random() * 1024); +} + +function toBase32Sortable(num: bigint): string { + if (num === 0n) return "2222222222222"; + let result = ""; + while (num > 0n) { + result = BASE32_SORTABLE[Number(num % 32n)] + result; + num = num / 32n; + } + return result.padStart(13, "2"); +} + +function generateTid(): string { + const nowMicroseconds = BigInt(Date.now()) * 1000n; + const clockId = generateClockId(); + const tidBigInt = (nowMicroseconds << 10n) | BigInt(clockId); + return toBase32Sortable(tidBigInt); +} + +// Tangled repo schema typing (matches sh.tangled.repo lexicon) +interface TangledRepoRecord { + $type: "sh.tangled.repo"; + name: string; // required + knot: string; // required + createdAt: string; // required (ISO 8601 datetime) + spindle?: string; // optional CI runner + description?: string; // optional, max 140 graphemes + website?: string; // optional URI + topics?: string[]; // optional array of topics + source?: string; // optional source URI + labels?: string[]; // optional array of at-uri labels +} + +// Cache for existing repo records +const recordCache: Record = {}; + +async function ensureTangledRecord( + agent: AtpAgent, + atprotoDid: string, + githubUser: string, + repoName: string, + description?: string +): Promise<{ tid: string; existed: boolean }> { + if (recordCache[repoName]) { + return { tid: recordCache[repoName], existed: true }; + } + + let cursor: string | undefined = undefined; + let tid: string | null = null; + + do { + const res: any = await agent.api.com.atproto.repo.listRecords({ + repo: atprotoDid, + collection: "sh.tangled.repo", + limit: 50, + cursor, + }); + + for (const record of res.data.records) { + const value = record.value as TangledRepoRecord; + if (value.name === repoName && record.rkey) { + tid = record.rkey; + recordCache[repoName] = tid; + console.log(`[FOUND] Existing record for ${repoName} (TID: ${tid})`); + return { tid, existed: true }; + } + } + + cursor = res.data.cursor; + } while (!tid && cursor); + + if (!tid) { + tid = generateTid(); + const record: TangledRepoRecord = { + $type: "sh.tangled.repo", + name: repoName, + knot: "knot1.tangled.sh", + createdAt: new Date().toISOString(), + description: description ?? repoName, + source: `https://github.com/${githubUser}/${repoName}`, + labels: [], + }; + + try { + const result = await agent.api.com.atproto.repo.putRecord({ + repo: atprotoDid, + collection: "sh.tangled.repo", + rkey: tid, + record, + }); + console.log(`[CREATED] ATProto record URI: ${result.data.uri}`); + } catch (error: any) { + console.error(`[ERROR] Failed to create ATProto record for ${repoName}:`, error.message); + throw error; + } + + recordCache[repoName] = tid; + console.log(`[CREATED] Tangled record for ${repoName} (TID: ${tid})`); + return { tid, existed: false }; + } + + return { tid, existed: false }; +} + +function updateReadme(baseDir: string, repoName: string, atprotoDid: string) { + const repoDir = path.join(baseDir, repoName); + const readmeFiles = ["README.md", "README.MD", "README.txt", "README"]; + const readmeFile = readmeFiles.find((f) => fs.existsSync(path.join(repoDir, f))); + if (!readmeFile) return; + const readmePath = path.join(repoDir, readmeFile); + const content = fs.readFileSync(readmePath, "utf-8"); + if (!/tangled\.org/i.test(content)) { + fs.appendFileSync( + readmePath, + ` +Mirrored on Tangled: https://tangled.org/${atprotoDid}/${repoName} +` + ); + run(`git add ${readmeFile}`, repoDir); + run(`git commit -m "Add Tangled mirror reference to README"`, repoDir); + run(`git push origin main`, repoDir); + console.log(`[README] Updated for ${repoName}`); + } +} + +async function main() { + console.log("[STARTUP] Starting Tangled Sync..."); + if (FORCE_SYNC) { + console.log("[MODE] Force sync enabled - will process all repos"); + } + console.log(`[CONFIG] Base directory: ${BASE_DIR}`); + console.log(`[CONFIG] GitHub user: ${GITHUB_USER}`); + console.log(`[CONFIG] ATProto DID: ${ATPROTO_DID}`); + console.log(`[CONFIG] PDS: ${BLUESKY_PDS}`); + + // Login to AT Proto + await login(); + + // Ensure base directory exists + ensureDir(BASE_DIR); + + // Fetch GitHub repositories + console.log(`[GITHUB] Fetching repositories for ${GITHUB_USER}...`); + const repos = await getGitHubRepos(); + console.log(`[GITHUB] Found ${repos.length} repositories`); + + let reposToProcess = repos; + let skippedRepos: typeof repos = []; + + if (!FORCE_SYNC) { + // Fetch all existing Tangled records upfront + console.log(`[ATPROTO] Fetching existing Tangled records...`); + let cursor: string | undefined = undefined; + const existingRepos = new Set(); + + do { + const res: any = await agent.api.com.atproto.repo.listRecords({ + repo: ATPROTO_DID, + collection: "sh.tangled.repo", + limit: 100, + cursor, + }); + + for (const record of res.data.records) { + const value = record.value as TangledRepoRecord; + if (value.name) { + existingRepos.add(value.name); + recordCache[value.name] = record.rkey; + } + } + + cursor = res.data.cursor; + } while (cursor); + + console.log(`[ATPROTO] Found ${existingRepos.size} existing Tangled records`); + + // Separate repos into new and existing + reposToProcess = repos.filter(r => !existingRepos.has(r.name)); + skippedRepos = repos.filter(r => existingRepos.has(r.name)); + + console.log(`[INFO] ${reposToProcess.length} new repos to sync`); + console.log(`[INFO] ${skippedRepos.length} repos already synced (skipping)\n`); + + if (skippedRepos.length > 0) { + console.log("[SKIPPED] The following repos already have AT Proto records:"); + skippedRepos.forEach(r => console.log(` - ${r.name}`)); + console.log(""); + } + } else { + console.log("[INFO] Processing all ${repos.length} repos (force sync mode)\n"); + } + + let syncedCount = 0; + let errorCount = 0; + + for (const { clone_url, name: repoName, description } of reposToProcess) { + console.log(`\n[PROGRESS] Processing ${repoName} (${syncedCount + 1}/${reposToProcess.length})`); + const repoDir = path.join(BASE_DIR, repoName); + + try { + if (!fs.existsSync(repoDir)) { + run(`git clone ${clone_url} ${repoDir}`); + console.log(`[CLONE] ${repoName}`); + } else { + console.log(`[EXISTS] ${repoName} already cloned`); + } + + await ensureTangledRemoteAndPush(repoDir, repoName, clone_url); + updateReadme(BASE_DIR, repoName, ATPROTO_DID); + const result = await ensureTangledRecord(agent, ATPROTO_DID, GITHUB_USER, repoName, description); + + if (!result.existed) { + syncedCount++; + } + } catch (error: any) { + console.error(`[ERROR] Failed to sync ${repoName}: ${error.message}`); + errorCount++; + } + } + + console.log(`\n${'='.repeat(50)}`); + console.log(`[COMPLETE] Sync finished!`); + console.log(` ✅ New repos synced: ${syncedCount}`); + if (!FORCE_SYNC) { + console.log(` ⏭️ Repos skipped: ${skippedRepos.length}`); + } + if (errorCount > 0) { + console.log(` ❌ Errors: ${errorCount}`); + } + console.log(`${'='.repeat(50)}`); +} + +main().catch(console.error); diff --git a/packages/tangled-sync/src/test-atproto.ts b/packages/tangled-sync/src/test-atproto.ts new file mode 100644 index 0000000..bf2ab3c --- /dev/null +++ b/packages/tangled-sync/src/test-atproto.ts @@ -0,0 +1,71 @@ +import { AtpAgent } from "@atproto/api"; +import dotenv from "dotenv"; + +dotenv.config(); + +async function testAtProtoConnection() { + console.log("Testing AT Proto Connection...\n"); + + const service = process.env.BLUESKY_PDS || "https://bsky.social"; + const username = process.env.BLUESKY_USERNAME; + const password = process.env.BLUESKY_PASSWORD; + const atprotoDid = process.env.ATPROTO_DID; + + console.log(`Service: ${service}`); + console.log(`Username: ${username}`); + console.log(`Expected DID: ${atprotoDid}\n`); + + if (!username || !password) { + console.error("ERROR: Missing BLUESKY_USERNAME or BLUESKY_PASSWORD"); + process.exit(1); + } + + const agent = new AtpAgent({ service }); + + try { + console.log("Attempting login..."); + const loginResponse = await agent.login({ + identifier: username, + password + }); + + console.log("✓ Login successful!"); + console.log(` DID: ${loginResponse.data.did}`); + console.log(` Handle: ${loginResponse.data.handle}`); + console.log(` Email: ${loginResponse.data.email || "N/A"}`); + + if (loginResponse.data.did !== atprotoDid) { + console.warn(`\n⚠ WARNING: Logged in DID (${loginResponse.data.did}) does not match ATPROTO_DID in .env (${atprotoDid})`); + console.warn(" Please update your ATPROTO_DID in src/.env"); + } + + // Test fetching existing records + console.log("\nFetching existing sh.tangled.repo records..."); + const records = await agent.api.com.atproto.repo.listRecords({ + repo: loginResponse.data.did, + collection: "sh.tangled.repo", + limit: 10, + }); + + console.log(`✓ Found ${records.data.records.length} existing Tangled repo records`); + + if (records.data.records.length > 0) { + console.log("\nSample records:"); + records.data.records.slice(0, 3).forEach((record: any) => { + console.log(` - ${record.value.name} (${record.uri})`); + }); + } + + console.log("\n✓ AT Proto connection test completed successfully!"); + + } catch (error: any) { + console.error("\n✗ AT Proto connection test failed!"); + console.error(`Error: ${error.message}`); + if (error.status) { + console.error(`HTTP Status: ${error.status}`); + } + process.exit(1); + } +} + +testAtProtoConnection(); diff --git a/packages/tangled-sync/src/validate-config.ts b/packages/tangled-sync/src/validate-config.ts new file mode 100644 index 0000000..df7965e --- /dev/null +++ b/packages/tangled-sync/src/validate-config.ts @@ -0,0 +1,109 @@ +import dotenv from "dotenv"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +dotenv.config(); + +console.log("🔍 Validating Tangled Sync Configuration...\n"); + +const checks: { name: string; status: boolean; message: string }[] = []; + +// Check .env file exists +const envPath = path.join(__dirname, ".env"); +const envExists = fs.existsSync(envPath); +checks.push({ + name: ".env file", + status: envExists, + message: envExists ? "Found at src/.env" : "Missing! Copy src/.env.example to src/.env" +}); + +// Check required environment variables +const requiredVars = [ + { name: "BASE_DIR", description: "Base directory for repos" }, + { name: "GITHUB_USER", description: "GitHub username" }, + { name: "ATPROTO_DID", description: "AT Proto DID" }, + { name: "BLUESKY_PDS", description: "Bluesky PDS URL" }, + { name: "BLUESKY_USERNAME", description: "Bluesky username" }, + { name: "BLUESKY_PASSWORD", description: "Bluesky app password" }, +]; + +requiredVars.forEach(({ name, description }) => { + const value = process.env[name]; + const exists = !!value && value.trim().length > 0; + checks.push({ + name: `${name}`, + status: exists, + message: exists ? `✓ Set (${description})` : `✗ Missing (${description})` + }); +}); + +// Validate BASE_DIR +const baseDir = process.env.BASE_DIR; +if (baseDir) { + const baseDirExists = fs.existsSync(baseDir); + checks.push({ + name: "BASE_DIR exists", + status: baseDirExists, + message: baseDirExists ? `Directory exists: ${baseDir}` : `Directory missing: ${baseDir} (will be created)` + }); +} + +// Validate DID format +const did = process.env.ATPROTO_DID; +if (did) { + const validDid = did.startsWith("did:plc:") || did.startsWith("did:web:"); + checks.push({ + name: "DID format", + status: validDid, + message: validDid ? "Valid DID format" : "Invalid! Should start with 'did:plc:' or 'did:web:'" + }); +} + +// Validate PDS URL +const pds = process.env.BLUESKY_PDS; +if (pds) { + const validPds = pds.startsWith("http://") || pds.startsWith("https://"); + checks.push({ + name: "PDS URL format", + status: validPds, + message: validPds ? `Valid URL: ${pds}` : "Invalid! Should start with 'https://'" + }); +} + +// Print results +console.log("Configuration Check Results:\n"); +let allPassed = true; + +checks.forEach((check) => { + const icon = check.status ? "✅" : "❌"; + console.log(`${icon} ${check.name}: ${check.message}`); + if (!check.status) allPassed = false; +}); + +console.log("\n" + "=".repeat(50) + "\n"); + +if (allPassed) { + console.log("✅ All checks passed! You're ready to run:"); + console.log(" npm run test-atproto # Test AT Proto connection"); + console.log(" npm run sync # Run the full sync"); +} else { + console.log("❌ Some checks failed. Please fix the issues above."); + console.log(" See SETUP.md for detailed instructions."); + process.exit(1); +} + +// Additional recommendations +console.log("\n💡 Recommendations:"); + +if (process.env.BLUESKY_PASSWORD && !process.env.BLUESKY_PASSWORD.includes("-")) { + console.log(" ⚠️ Your password looks like it might be a regular password."); + console.log(" Consider using an App Password from Bluesky settings."); +} + +console.log(" 📚 Read SETUP.md for detailed setup instructions"); +console.log(" 🔐 Never commit your .env file to version control"); +console.log(" 🔑 Make sure your SSH key is added to Tangled"); diff --git a/packages/tangled-sync/tsconfig.json b/packages/tangled-sync/tsconfig.json new file mode 100644 index 0000000..3146e5c --- /dev/null +++ b/packages/tangled-sync/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "rootDir": "./src", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noImplicitAny": true, + "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true, + "types": ["node"], + "strictNullChecks": true, + "allowJs": false + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"], + "ts-node": { + "esm": true, + "experimentalSpecifierResolution": "node" + } +} -- 2.51.2