my pkgs monorepo

tangled-sync: add build script and publish config

ewancroft.uk be49bd4a 21e2fda1

verified
+1913
+190
packages/tangled-sync/.gitignore
··· 1 + # Created by https://www.toptal.com/developers/gitignore/api/node,macos,svelte,vercel 2 + # Edit at https://www.toptal.com/developers/gitignore?templates=node,macos,svelte,vercel 3 + 4 + ### macOS ### 5 + # General 6 + .DS_Store 7 + .AppleDouble 8 + .LSOverride 9 + 10 + # Icon must end with two \r 11 + Icon 12 + 13 + 14 + # Thumbnails 15 + ._* 16 + 17 + # Files that might appear in the root of a volume 18 + .DocumentRevisions-V100 19 + .fseventsd 20 + .Spotlight-V100 21 + .TemporaryItems 22 + .Trashes 23 + .VolumeIcon.icns 24 + .com.apple.timemachine.donotpresent 25 + 26 + # Directories potentially created on remote AFP share 27 + .AppleDB 28 + .AppleDesktop 29 + Network Trash Folder 30 + Temporary Items 31 + .apdisk 32 + 33 + ### macOS Patch ### 34 + # iCloud generated files 35 + *.icloud 36 + 37 + ### Node ### 38 + # Logs 39 + logs 40 + *.log 41 + npm-debug.log* 42 + yarn-debug.log* 43 + yarn-error.log* 44 + lerna-debug.log* 45 + .pnpm-debug.log* 46 + 47 + # Diagnostic reports (https://nodejs.org/api/report.html) 48 + report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 49 + 50 + # Runtime data 51 + pids 52 + *.pid 53 + *.seed 54 + *.pid.lock 55 + 56 + # Directory for instrumented libs generated by jscoverage/JSCover 57 + lib-cov 58 + 59 + # Coverage directory used by tools like istanbul 60 + coverage 61 + *.lcov 62 + 63 + # nyc test coverage 64 + .nyc_output 65 + 66 + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 67 + .grunt 68 + 69 + # Bower dependency directory (https://bower.io/) 70 + bower_components 71 + 72 + # node-waf configuration 73 + .lock-wscript 74 + 75 + # Compiled binary addons (https://nodejs.org/api/addons.html) 76 + build/Release 77 + 78 + # Dependency directories 79 + node_modules/ 80 + jspm_packages/ 81 + 82 + # Snowpack dependency directory (https://snowpack.dev/) 83 + web_modules/ 84 + 85 + # TypeScript cache 86 + *.tsbuildinfo 87 + 88 + # Optional npm cache directory 89 + .npm 90 + 91 + # Optional eslint cache 92 + .eslintcache 93 + 94 + # Optional stylelint cache 95 + .stylelintcache 96 + 97 + # Microbundle cache 98 + .rpt2_cache/ 99 + .rts2_cache_cjs/ 100 + .rts2_cache_es/ 101 + .rts2_cache_umd/ 102 + 103 + # Optional REPL history 104 + .node_repl_history 105 + 106 + # Output of 'npm pack' 107 + *.tgz 108 + 109 + # Yarn Integrity file 110 + .yarn-integrity 111 + 112 + # dotenv environment variable files 113 + .env 114 + .env.development.local 115 + .env.test.local 116 + .env.production.local 117 + .env.local 118 + 119 + # parcel-bundler cache (https://parceljs.org/) 120 + .cache 121 + .parcel-cache 122 + 123 + # Next.js build output 124 + .next 125 + out 126 + 127 + # Nuxt.js build / generate output 128 + .nuxt 129 + dist 130 + 131 + # Gatsby files 132 + .cache/ 133 + # Comment in the public line in if your project uses Gatsby and not Next.js 134 + # https://nextjs.org/blog/next-9-1#public-directory-support 135 + # public 136 + 137 + # vuepress build output 138 + .vuepress/dist 139 + 140 + # vuepress v2.x temp and cache directory 141 + .temp 142 + 143 + # Docusaurus cache and generated files 144 + .docusaurus 145 + 146 + # Serverless directories 147 + .serverless/ 148 + 149 + # FuseBox cache 150 + .fusebox/ 151 + 152 + # DynamoDB Local files 153 + .dynamodb/ 154 + 155 + # TernJS port file 156 + .tern-port 157 + 158 + # Stores VSCode versions used for testing VSCode extensions 159 + .vscode-test 160 + 161 + # yarn v2 162 + .yarn/cache 163 + .yarn/unplugged 164 + .yarn/build-state.yml 165 + .yarn/install-state.gz 166 + .pnp.* 167 + 168 + ### Node Patch ### 169 + # Serverless Webpack directories 170 + .webpack/ 171 + 172 + # Optional stylelint cache 173 + 174 + # SvelteKit build / generate output 175 + .svelte-kit 176 + 177 + ### Svelte ### 178 + # gitignore template for the SvelteKit, frontend web component framework 179 + # website: https://kit.svelte.dev/ 180 + 181 + .svelte-kit/ 182 + package 183 + 184 + ### Vercel ### 185 + .vercel 186 + 187 + # End of https://www.toptal.com/developers/gitignore/api/node,macos,svelte,vercel 188 + 189 + git-diff*.txt 190 + node_modules
+21
packages/tangled-sync/LICENSE
··· 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.
+124
packages/tangled-sync/README.md
··· 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)!
+175
packages/tangled-sync/SETUP.md
··· 1 + # Tangled Sync - Setup & Troubleshooting Guide 2 + 3 + ## Quick Setup Checklist 4 + 5 + ### 1. Install Dependencies 6 + ```bash 7 + npm install 8 + ``` 9 + 10 + ### 2. Configure Environment Variables 11 + ```bash 12 + # Copy the example env file 13 + cp src/.env.example src/.env 14 + 15 + # Edit with your actual values 16 + nano src/.env # or use your preferred editor 17 + ``` 18 + 19 + **Required values:** 20 + - `BASE_DIR`: Where to clone repos (e.g., `/Users/you/tangled-repos`) 21 + - `GITHUB_USER`: Your GitHub username 22 + - `ATPROTO_DID`: Your AT Proto DID (get from Bluesky settings) 23 + - `BLUESKY_PDS`: Usually `https://bsky.social` 24 + - `BLUESKY_USERNAME`: Your Bluesky handle (e.g., `you.bsky.social`) 25 + - `BLUESKY_PASSWORD`: Use an **app password**, not your main password! 26 + 27 + ### 3. Get Your AT Proto DID 28 + 29 + Your DID can be found by: 30 + 1. Go to https://bsky.app 31 + 2. Click your profile 32 + 3. Settings โ†’ Advanced โ†’ Account 33 + 4. Look for "DID" (starts with `did:plc:`) 34 + 35 + Alternatively, visit: `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=YOUR_HANDLE.bsky.social` 36 + 37 + ### 4. Create an App Password 38 + 39 + **IMPORTANT:** Do NOT use your main Bluesky password! 40 + 41 + 1. Go to https://bsky.app/settings 42 + 2. Navigate to "App Passwords" 43 + 3. Click "Add App Password" 44 + 4. Give it a name (e.g., "Tangled Sync") 45 + 5. Copy the generated password to your `.env` file 46 + 47 + ### 5. Test AT Proto Connection 48 + ```bash 49 + npm run test-atproto 50 + ``` 51 + 52 + Expected output: 53 + ``` 54 + โœ“ Login successful! 55 + DID: did:plc:... 56 + Handle: you.bsky.social 57 + Email: your@email.com 58 + 59 + โœ“ Found X existing Tangled repo records 60 + ``` 61 + 62 + ### 6. Verify SSH to Tangled 63 + ```bash 64 + ssh git@tangled.sh 65 + ``` 66 + 67 + You should see a message confirming your SSH key is configured. 68 + 69 + ### 7. Run the Sync 70 + ```bash 71 + npm run sync 72 + ``` 73 + 74 + --- 75 + 76 + ## Common Issues & Solutions 77 + 78 + ### Issue: "Missing Bluesky credentials" 79 + **Solution:** Check that `src/.env` exists and contains `BLUESKY_USERNAME` and `BLUESKY_PASSWORD` 80 + 81 + ### Issue: "Login failed" or "Invalid credentials" 82 + **Solution:** 83 + - Ensure you're using an **app password**, not your main password 84 + - Check your username includes the full handle (e.g., `you.bsky.social`) 85 + - Verify credentials are correct 86 + 87 + ### Issue: "DID mismatch" 88 + **Solution:** 89 + - Run `npm run test-atproto` to see your actual DID 90 + - Update `ATPROTO_DID` in `src/.env` to match 91 + 92 + ### Issue: "Could not push to Tangled" 93 + **Solution:** 94 + - Verify SSH key is added to Tangled: https://tangled.org/settings/keys 95 + - Test SSH connection: `ssh git@tangled.sh` 96 + - Ensure the repository exists on Tangled first 97 + 98 + ### Issue: "Failed to create ATProto record" 99 + **Solution:** 100 + - Check that the schema matches (required fields: `name`, `knot`, `createdAt`) 101 + - Verify your app password has write permissions 102 + - Check PDS is reachable: `curl https://bsky.social` 103 + 104 + ### Issue: Rate limiting from GitHub API 105 + **Solution:** 106 + - GitHub has a rate limit of 60 requests/hour for unauthenticated requests 107 + - Consider adding GitHub authentication if syncing many repos 108 + - Wait an hour and try again 109 + 110 + --- 111 + 112 + ## Understanding the Workflow 113 + 114 + 1. **Login to AT Proto**: Authenticates with Bluesky PDS using your credentials 115 + 2. **Fetch GitHub Repos**: Retrieves all public repos from your GitHub account 116 + 3. **Clone Locally**: Downloads repos to `BASE_DIR` if not already present 117 + 4. **Add Tangled Remote**: Adds `tangled` as a git remote 118 + 5. **Push to Tangled**: Pushes the `main` branch to Tangled 119 + 6. **Update README**: Adds a Tangled mirror link to the README 120 + 7. **Create AT Proto Record**: Publishes metadata to the AT Proto network 121 + 122 + Each repository gets a record in the `sh.tangled.repo` collection with: 123 + - Repository name and description 124 + - Source URL (GitHub) 125 + - Creation timestamp 126 + - Knot server reference 127 + - Optional labels and topics 128 + 129 + --- 130 + 131 + ## Verifying Success 132 + 133 + After running the sync, you can verify: 134 + 135 + 1. **Local repos**: Check `BASE_DIR` for cloned repositories 136 + 2. **Tangled remotes**: Run `git remote -v` in any repo directory 137 + 3. **AT Proto records**: Run `npm run test-atproto` to list records 138 + 4. **Tangled website**: Visit `https://tangled.org/YOUR_DID/REPO_NAME` 139 + 140 + --- 141 + 142 + ## Advanced Configuration 143 + 144 + ### Using a Different PDS 145 + If you're not using the default Bluesky PDS: 146 + ```bash 147 + BLUESKY_PDS=https://your-pds.example.com 148 + ``` 149 + 150 + ### Syncing Specific Repos Only 151 + Modify the `getGitHubRepos()` function to filter repos: 152 + ```typescript 153 + return json 154 + .filter((r: any) => r.name.startsWith('my-prefix-')) 155 + .map(...); 156 + ``` 157 + 158 + ### Changing the Default Branch 159 + If your repos use `master` instead of `main`, update: 160 + ```typescript 161 + run(`git push tangled master`, repoDir); 162 + ``` 163 + 164 + --- 165 + 166 + ## Support 167 + 168 + For issues with: 169 + - **Tangled**: https://github.com/tangled-dev/tangled 170 + - **AT Proto**: https://atproto.com/docs 171 + - **This tool**: Open an issue in the repository 172 + 173 + --- 174 + 175 + **Happy syncing! ๐Ÿš€**
+197
packages/tangled-sync/USAGE.md
··· 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! ๐Ÿš€**
+361
packages/tangled-sync/package-lock.json
··· 1 + { 2 + "name": "tangled-sync", 3 + "version": "1.0.0", 4 + "lockfileVersion": 3, 5 + "requires": true, 6 + "packages": { 7 + "": { 8 + "name": "tangled-sync", 9 + "version": "1.0.0", 10 + "license": "MIT", 11 + "dependencies": { 12 + "@atproto/api": "^0.17.2", 13 + "dotenv": "^16.0.0" 14 + }, 15 + "devDependencies": { 16 + "ts-node": "^10.9.2", 17 + "typescript": "^5.9.3" 18 + } 19 + }, 20 + "node_modules/@atproto/api": { 21 + "version": "0.17.2", 22 + "resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.17.2.tgz", 23 + "integrity": "sha512-luRY9YPaRQFpm3v7a1bTOaekQ/KPCG3gb0jVyaOtfMXDSfIZJh9lr9MtmGPdEp7AvfE8urkngZ+V/p8Ial3z2g==", 24 + "license": "MIT", 25 + "dependencies": { 26 + "@atproto/common-web": "^0.4.3", 27 + "@atproto/lexicon": "^0.5.1", 28 + "@atproto/syntax": "^0.4.1", 29 + "@atproto/xrpc": "^0.7.5", 30 + "await-lock": "^2.2.2", 31 + "multiformats": "^9.9.0", 32 + "tlds": "^1.234.0", 33 + "zod": "^3.23.8" 34 + } 35 + }, 36 + "node_modules/@atproto/common-web": { 37 + "version": "0.4.3", 38 + "resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz", 39 + "integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==", 40 + "license": "MIT", 41 + "dependencies": { 42 + "graphemer": "^1.4.0", 43 + "multiformats": "^9.9.0", 44 + "uint8arrays": "3.0.0", 45 + "zod": "^3.23.8" 46 + } 47 + }, 48 + "node_modules/@atproto/lexicon": { 49 + "version": "0.5.1", 50 + "resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.5.1.tgz", 51 + "integrity": "sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A==", 52 + "license": "MIT", 53 + "dependencies": { 54 + "@atproto/common-web": "^0.4.3", 55 + "@atproto/syntax": "^0.4.1", 56 + "iso-datestring-validator": "^2.2.2", 57 + "multiformats": "^9.9.0", 58 + "zod": "^3.23.8" 59 + } 60 + }, 61 + "node_modules/@atproto/syntax": { 62 + "version": "0.4.1", 63 + "resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz", 64 + "integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==", 65 + "license": "MIT" 66 + }, 67 + "node_modules/@atproto/xrpc": { 68 + "version": "0.7.5", 69 + "resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.7.5.tgz", 70 + "integrity": "sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA==", 71 + "license": "MIT", 72 + "dependencies": { 73 + "@atproto/lexicon": "^0.5.1", 74 + "zod": "^3.23.8" 75 + } 76 + }, 77 + "node_modules/@cspotcode/source-map-support": { 78 + "version": "0.8.1", 79 + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 80 + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 81 + "dev": true, 82 + "license": "MIT", 83 + "dependencies": { 84 + "@jridgewell/trace-mapping": "0.3.9" 85 + }, 86 + "engines": { 87 + "node": ">=12" 88 + } 89 + }, 90 + "node_modules/@jridgewell/resolve-uri": { 91 + "version": "3.1.2", 92 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 93 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 94 + "dev": true, 95 + "license": "MIT", 96 + "engines": { 97 + "node": ">=6.0.0" 98 + } 99 + }, 100 + "node_modules/@jridgewell/sourcemap-codec": { 101 + "version": "1.5.5", 102 + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", 103 + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", 104 + "dev": true, 105 + "license": "MIT" 106 + }, 107 + "node_modules/@jridgewell/trace-mapping": { 108 + "version": "0.3.9", 109 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 110 + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 111 + "dev": true, 112 + "license": "MIT", 113 + "dependencies": { 114 + "@jridgewell/resolve-uri": "^3.0.3", 115 + "@jridgewell/sourcemap-codec": "^1.4.10" 116 + } 117 + }, 118 + "node_modules/@tsconfig/node10": { 119 + "version": "1.0.11", 120 + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", 121 + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", 122 + "dev": true, 123 + "license": "MIT" 124 + }, 125 + "node_modules/@tsconfig/node12": { 126 + "version": "1.0.11", 127 + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 128 + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 129 + "dev": true, 130 + "license": "MIT" 131 + }, 132 + "node_modules/@tsconfig/node14": { 133 + "version": "1.0.3", 134 + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 135 + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 136 + "dev": true, 137 + "license": "MIT" 138 + }, 139 + "node_modules/@tsconfig/node16": { 140 + "version": "1.0.4", 141 + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 142 + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", 143 + "dev": true, 144 + "license": "MIT" 145 + }, 146 + "node_modules/@types/node": { 147 + "version": "24.7.1", 148 + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.1.tgz", 149 + "integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==", 150 + "dev": true, 151 + "license": "MIT", 152 + "peer": true, 153 + "dependencies": { 154 + "undici-types": "~7.14.0" 155 + } 156 + }, 157 + "node_modules/acorn": { 158 + "version": "8.15.0", 159 + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", 160 + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", 161 + "dev": true, 162 + "license": "MIT", 163 + "bin": { 164 + "acorn": "bin/acorn" 165 + }, 166 + "engines": { 167 + "node": ">=0.4.0" 168 + } 169 + }, 170 + "node_modules/acorn-walk": { 171 + "version": "8.3.4", 172 + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", 173 + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", 174 + "dev": true, 175 + "license": "MIT", 176 + "dependencies": { 177 + "acorn": "^8.11.0" 178 + }, 179 + "engines": { 180 + "node": ">=0.4.0" 181 + } 182 + }, 183 + "node_modules/arg": { 184 + "version": "4.1.3", 185 + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 186 + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 187 + "dev": true, 188 + "license": "MIT" 189 + }, 190 + "node_modules/await-lock": { 191 + "version": "2.2.2", 192 + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", 193 + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", 194 + "license": "MIT" 195 + }, 196 + "node_modules/create-require": { 197 + "version": "1.1.1", 198 + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 199 + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 200 + "dev": true, 201 + "license": "MIT" 202 + }, 203 + "node_modules/diff": { 204 + "version": "4.0.2", 205 + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 206 + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 207 + "dev": true, 208 + "license": "BSD-3-Clause", 209 + "engines": { 210 + "node": ">=0.3.1" 211 + } 212 + }, 213 + "node_modules/dotenv": { 214 + "version": "16.6.1", 215 + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", 216 + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", 217 + "license": "BSD-2-Clause", 218 + "engines": { 219 + "node": ">=12" 220 + }, 221 + "funding": { 222 + "url": "https://dotenvx.com" 223 + } 224 + }, 225 + "node_modules/graphemer": { 226 + "version": "1.4.0", 227 + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 228 + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 229 + "license": "MIT" 230 + }, 231 + "node_modules/iso-datestring-validator": { 232 + "version": "2.2.2", 233 + "resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz", 234 + "integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==", 235 + "license": "MIT" 236 + }, 237 + "node_modules/make-error": { 238 + "version": "1.3.6", 239 + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 240 + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 241 + "dev": true, 242 + "license": "ISC" 243 + }, 244 + "node_modules/multiformats": { 245 + "version": "9.9.0", 246 + "resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz", 247 + "integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==", 248 + "license": "(Apache-2.0 AND MIT)" 249 + }, 250 + "node_modules/tlds": { 251 + "version": "1.260.0", 252 + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.260.0.tgz", 253 + "integrity": "sha512-78+28EWBhCEE7qlyaHA9OR3IPvbCLiDh3Ckla593TksfFc9vfTsgvH7eS+dr3o9qr31gwGbogcI16yN91PoRjQ==", 254 + "license": "MIT", 255 + "bin": { 256 + "tlds": "bin.js" 257 + } 258 + }, 259 + "node_modules/ts-node": { 260 + "version": "10.9.2", 261 + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", 262 + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", 263 + "dev": true, 264 + "license": "MIT", 265 + "dependencies": { 266 + "@cspotcode/source-map-support": "^0.8.0", 267 + "@tsconfig/node10": "^1.0.7", 268 + "@tsconfig/node12": "^1.0.7", 269 + "@tsconfig/node14": "^1.0.0", 270 + "@tsconfig/node16": "^1.0.2", 271 + "acorn": "^8.4.1", 272 + "acorn-walk": "^8.1.1", 273 + "arg": "^4.1.0", 274 + "create-require": "^1.1.0", 275 + "diff": "^4.0.1", 276 + "make-error": "^1.1.1", 277 + "v8-compile-cache-lib": "^3.0.1", 278 + "yn": "3.1.1" 279 + }, 280 + "bin": { 281 + "ts-node": "dist/bin.js", 282 + "ts-node-cwd": "dist/bin-cwd.js", 283 + "ts-node-esm": "dist/bin-esm.js", 284 + "ts-node-script": "dist/bin-script.js", 285 + "ts-node-transpile-only": "dist/bin-transpile.js", 286 + "ts-script": "dist/bin-script-deprecated.js" 287 + }, 288 + "peerDependencies": { 289 + "@swc/core": ">=1.2.50", 290 + "@swc/wasm": ">=1.2.50", 291 + "@types/node": "*", 292 + "typescript": ">=2.7" 293 + }, 294 + "peerDependenciesMeta": { 295 + "@swc/core": { 296 + "optional": true 297 + }, 298 + "@swc/wasm": { 299 + "optional": true 300 + } 301 + } 302 + }, 303 + "node_modules/typescript": { 304 + "version": "5.9.3", 305 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", 306 + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", 307 + "dev": true, 308 + "license": "Apache-2.0", 309 + "peer": true, 310 + "bin": { 311 + "tsc": "bin/tsc", 312 + "tsserver": "bin/tsserver" 313 + }, 314 + "engines": { 315 + "node": ">=14.17" 316 + } 317 + }, 318 + "node_modules/uint8arrays": { 319 + "version": "3.0.0", 320 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 321 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 322 + "license": "MIT", 323 + "dependencies": { 324 + "multiformats": "^9.4.2" 325 + } 326 + }, 327 + "node_modules/undici-types": { 328 + "version": "7.14.0", 329 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", 330 + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", 331 + "dev": true, 332 + "license": "MIT" 333 + }, 334 + "node_modules/v8-compile-cache-lib": { 335 + "version": "3.0.1", 336 + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 337 + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 338 + "dev": true, 339 + "license": "MIT" 340 + }, 341 + "node_modules/yn": { 342 + "version": "3.1.1", 343 + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 344 + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 345 + "dev": true, 346 + "license": "MIT", 347 + "engines": { 348 + "node": ">=6" 349 + } 350 + }, 351 + "node_modules/zod": { 352 + "version": "3.25.76", 353 + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", 354 + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", 355 + "license": "MIT", 356 + "funding": { 357 + "url": "https://github.com/sponsors/colinhacks" 358 + } 359 + } 360 + } 361 + }
+40
packages/tangled-sync/package.json
··· 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": { "access": "public" }, 13 + "files": [ 14 + "dist", 15 + "README.md", 16 + "USAGE.md" 17 + ], 18 + "scripts": { 19 + "build": "tsup src/index.ts src/check.ts src/validate-config.ts src/test-atproto.ts --format esm --outDir dist --no-dts --clean", 20 + "dev": "ts-node src/index.ts", 21 + "check": "ts-node src/check.ts", 22 + "validate": "ts-node src/validate-config.ts", 23 + "test-atproto": "ts-node src/test-atproto.ts", 24 + "sync": "ts-node src/index.ts", 25 + "sync:force": "ts-node src/index.ts --force", 26 + "type-check": "tsc --noEmit" 27 + }, 28 + "dependencies": { 29 + "@atproto/api": "^0.17.2", 30 + "dotenv": "^16.0.0" 31 + }, 32 + "devDependencies": { 33 + "@types/node": "^20.0.0", 34 + "ts-node": "^10.9.2", 35 + "tsup": "^8.5.0", 36 + "typescript": "^5.9.3" 37 + }, 38 + "author": "Ewan Croft", 39 + "license": "AGPL-3.0-only" 40 + }
+15
packages/tangled-sync/src/.env.example
··· 1 + # Base directory where GitHub repos will be cloned 2 + BASE_DIR=/path/to/your/repos 3 + 4 + # Your GitHub username 5 + GITHUB_USER=your-github-username 6 + 7 + # Your ATProto DID (e.g., did:plc:abc123...) 8 + ATPROTO_DID=did:plc:your-did-here 9 + 10 + # Bluesky PDS URL (usually https://bsky.social) 11 + BLUESKY_PDS=https://bsky.social 12 + 13 + # Your Bluesky credentials 14 + BLUESKY_USERNAME=your-handle.bsky.social 15 + BLUESKY_PASSWORD=your-app-password
+266
packages/tangled-sync/src/check.ts
··· 1 + import { AtpAgent } from "@atproto/api"; 2 + import dotenv from "dotenv"; 3 + import fs from "fs"; 4 + import path from "path"; 5 + import { fileURLToPath } from "url"; 6 + import { execSync } from "child_process"; 7 + 8 + const __filename = fileURLToPath(import.meta.url); 9 + const __dirname = path.dirname(__filename); 10 + 11 + dotenv.config(); 12 + 13 + async function runHealthCheck() { 14 + 15 + console.log("๐Ÿ” Running Tangled Sync Health Check...\n"); 16 + 17 + const checks: { category: string; name: string; status: boolean; message: string }[] = []; 18 + let errors = 0; 19 + let warnings = 0; 20 + 21 + // ===== CONFIGURATION CHECKS ===== 22 + console.log("๐Ÿ“‹ Configuration Checks\n"); 23 + 24 + const envPath = path.join(__dirname, ".env"); 25 + const envExists = fs.existsSync(envPath); 26 + checks.push({ 27 + category: "config", 28 + name: ".env file", 29 + status: envExists, 30 + message: envExists ? "Found at src/.env" : "Missing! Copy src/.env.example to src/.env" 31 + }); 32 + if (!envExists) errors++; 33 + 34 + const requiredVars = [ 35 + { name: "BASE_DIR", description: "Base directory for repos" }, 36 + { name: "GITHUB_USER", description: "GitHub username" }, 37 + { name: "ATPROTO_DID", description: "AT Proto DID" }, 38 + { name: "BLUESKY_PDS", description: "Bluesky PDS URL" }, 39 + { name: "BLUESKY_USERNAME", description: "Bluesky username" }, 40 + { name: "BLUESKY_PASSWORD", description: "Bluesky app password" }, 41 + ]; 42 + 43 + requiredVars.forEach(({ name, description }) => { 44 + const value = process.env[name]; 45 + const exists = !!value && value.trim().length > 0; 46 + checks.push({ 47 + category: "config", 48 + name: name, 49 + status: exists, 50 + message: exists ? `Set` : `Missing (${description})` 51 + }); 52 + if (!exists) errors++; 53 + }); 54 + 55 + // Check BASE_DIR 56 + const baseDir = process.env.BASE_DIR; 57 + if (baseDir) { 58 + const baseDirExists = fs.existsSync(baseDir); 59 + checks.push({ 60 + category: "config", 61 + name: "BASE_DIR path", 62 + status: baseDirExists, 63 + message: baseDirExists ? `Exists: ${baseDir}` : `Missing (will be created): ${baseDir}` 64 + }); 65 + if (!baseDirExists) warnings++; 66 + } 67 + 68 + // Check DID format 69 + const did = process.env.ATPROTO_DID; 70 + if (did) { 71 + const validDid = did.startsWith("did:plc:") || did.startsWith("did:web:"); 72 + checks.push({ 73 + category: "config", 74 + name: "DID format", 75 + status: validDid, 76 + message: validDid ? "Valid" : "Invalid! Should start with 'did:plc:' or 'did:web:'" 77 + }); 78 + if (!validDid) errors++; 79 + } 80 + 81 + // Check PDS URL 82 + const pds = process.env.BLUESKY_PDS; 83 + if (pds) { 84 + const validPds = pds.startsWith("http://") || pds.startsWith("https://"); 85 + checks.push({ 86 + category: "config", 87 + name: "PDS URL", 88 + status: validPds, 89 + message: validPds ? pds : "Invalid! Should start with 'https://'" 90 + }); 91 + if (!validPds) errors++; 92 + } 93 + 94 + // Print config results 95 + checks.filter(c => c.category === "config").forEach((check) => { 96 + const icon = check.status ? "โœ…" : "โŒ"; 97 + console.log(`${icon} ${check.name}: ${check.message}`); 98 + }); 99 + 100 + // ===== AT PROTO CONNECTION CHECK ===== 101 + console.log("\n๐Ÿ” AT Proto Connection Check\n"); 102 + 103 + const canTestConnection = process.env.BLUESKY_USERNAME && 104 + process.env.BLUESKY_PASSWORD && 105 + process.env.BLUESKY_PDS && 106 + process.env.ATPROTO_DID; 107 + 108 + if (canTestConnection) { 109 + try { 110 + const agent = new AtpAgent({ service: process.env.BLUESKY_PDS! }); 111 + 112 + const loginResponse = await agent.login({ 113 + identifier: process.env.BLUESKY_USERNAME!, 114 + password: process.env.BLUESKY_PASSWORD! 115 + }); 116 + 117 + console.log(`โœ… Login successful`); 118 + console.log(` DID: ${loginResponse.data.did}`); 119 + console.log(` Handle: ${loginResponse.data.handle}`); 120 + 121 + if (loginResponse.data.did !== process.env.ATPROTO_DID) { 122 + console.log(`โš ๏ธ DID mismatch!`); 123 + console.log(` Expected: ${process.env.ATPROTO_DID}`); 124 + console.log(` Got: ${loginResponse.data.did}`); 125 + warnings++; 126 + } 127 + 128 + // Test fetching records 129 + const records = await agent.api.com.atproto.repo.listRecords({ 130 + repo: loginResponse.data.did, 131 + collection: "sh.tangled.repo", 132 + limit: 5, 133 + }); 134 + 135 + console.log(`โœ… Can access AT Proto records`); 136 + console.log(` Found ${records.data.records.length} sample records`); 137 + 138 + } catch (error: any) { 139 + console.log(`โŒ AT Proto connection failed`); 140 + console.log(` Error: ${error.message}`); 141 + errors++; 142 + } 143 + } else { 144 + console.log("โญ๏ธ Skipped (missing credentials)"); 145 + } 146 + 147 + // ===== SSH CONNECTION CHECK ===== 148 + console.log("\n๐Ÿ”‘ SSH Connection Check\n"); 149 + 150 + try { 151 + const sshTest = execSync("ssh -T git@tangled.sh 2>&1", { 152 + encoding: "utf-8", 153 + timeout: 5000 154 + }); 155 + 156 + if (sshTest.includes("successfully authenticated") || sshTest.includes("Hi")) { 157 + console.log("โœ… SSH connection to Tangled works"); 158 + console.log(` ${sshTest.trim().split('\n')[0]}`); 159 + } else { 160 + console.log("โš ๏ธ SSH connection uncertain"); 161 + console.log(` Response: ${sshTest.trim()}`); 162 + warnings++; 163 + } 164 + } catch (error: any) { 165 + const output = error.stdout?.toString() || error.message; 166 + 167 + if (output.includes("successfully authenticated") || output.includes("Hi")) { 168 + console.log("โœ… SSH connection to Tangled works"); 169 + } else { 170 + console.log("โŒ SSH connection to Tangled failed"); 171 + console.log(" Make sure your SSH key is added at https://tangled.org/settings/keys"); 172 + errors++; 173 + } 174 + } 175 + 176 + // ===== GITHUB API CHECK ===== 177 + console.log("\n๐Ÿ™ GitHub API Check\n"); 178 + 179 + if (process.env.GITHUB_USER) { 180 + try { 181 + const response = execSync(`curl -s "https://api.github.com/users/${process.env.GITHUB_USER}"`, { 182 + encoding: "utf-8", 183 + timeout: 5000 184 + }); 185 + 186 + const data = JSON.parse(response); 187 + 188 + if (data.login) { 189 + console.log(`โœ… GitHub user found: ${data.login}`); 190 + console.log(` Public repos: ${data.public_repos || 0}`); 191 + } else { 192 + console.log(`โŒ GitHub user not found: ${process.env.GITHUB_USER}`); 193 + errors++; 194 + } 195 + } catch (error: any) { 196 + console.log(`โš ๏ธ Could not check GitHub API`); 197 + console.log(` ${error.message}`); 198 + warnings++; 199 + } 200 + } else { 201 + console.log("โญ๏ธ Skipped (no GITHUB_USER set)"); 202 + } 203 + 204 + // ===== DEPENDENCIES CHECK ===== 205 + console.log("\n๐Ÿ“ฆ Dependencies Check\n"); 206 + 207 + let hasAtproto = false; 208 + let hasDotenv = false; 209 + 210 + try { 211 + await import("@atproto/api"); 212 + hasAtproto = true; 213 + console.log("โœ… @atproto/api installed"); 214 + } catch { 215 + console.log("โŒ @atproto/api not installed (run: npm install)"); 216 + errors++; 217 + } 218 + 219 + try { 220 + await import("dotenv"); 221 + hasDotenv = true; 222 + console.log("โœ… dotenv installed"); 223 + } catch { 224 + console.log("โŒ dotenv not installed (run: npm install)"); 225 + errors++; 226 + } 227 + 228 + // ===== SUMMARY ===== 229 + console.log("\n" + "=".repeat(50)); 230 + 231 + if (errors === 0 && warnings === 0) { 232 + console.log("โœ… All checks passed! Ready to sync."); 233 + console.log("\nNext steps:"); 234 + console.log(" npm run sync # Sync new repos only"); 235 + console.log(" npm run sync:force # Force sync all repos"); 236 + } else { 237 + if (errors > 0) { 238 + console.log(`โŒ ${errors} error(s) found - please fix before syncing`); 239 + } 240 + if (warnings > 0) { 241 + console.log(`โš ๏ธ ${warnings} warning(s) - review before syncing`); 242 + } 243 + 244 + console.log("\nSee SETUP.md for detailed troubleshooting"); 245 + 246 + if (errors > 0) { 247 + process.exit(1); 248 + } 249 + } 250 + 251 + console.log("=".repeat(50)); 252 + 253 + // Additional recommendations 254 + if (process.env.BLUESKY_PASSWORD && !process.env.BLUESKY_PASSWORD.includes("-")) { 255 + console.log("\n๐Ÿ’ก Tip: Your password might be a regular password."); 256 + console.log(" Consider using an App Password from Bluesky settings for better security."); 257 + } 258 + 259 + } 260 + 261 + // Run the health check 262 + runHealthCheck().catch((error) => { 263 + console.error("\nโŒ Health check failed with error:"); 264 + console.error(error); 265 + process.exit(1); 266 + });
+318
packages/tangled-sync/src/index.ts
··· 1 + import { AtpAgent } from "@atproto/api"; 2 + import dotenv from "dotenv"; 3 + import fs from "fs"; 4 + import path from "path"; 5 + import { execSync } from "child_process"; 6 + 7 + dotenv.config(); 8 + 9 + const FORCE_SYNC = process.argv.includes("--force"); 10 + 11 + const BASE_DIR = process.env.BASE_DIR!; 12 + const GITHUB_USER = process.env.GITHUB_USER!; 13 + const ATPROTO_DID = process.env.ATPROTO_DID!; 14 + const BLUESKY_PDS = process.env.BLUESKY_PDS!; 15 + const TANGLED_BASE_URL = `git@tangled.sh:${ATPROTO_DID}`; 16 + 17 + const agent = new AtpAgent({ service: BLUESKY_PDS }); 18 + 19 + async function login() { 20 + const username = process.env.BLUESKY_USERNAME; 21 + const password = process.env.BLUESKY_PASSWORD; 22 + if (!username || !password) { 23 + throw new Error("Missing Bluesky credentials. Please set BLUESKY_USERNAME and BLUESKY_PASSWORD in src/.env"); 24 + } 25 + 26 + try { 27 + const response = await agent.login({ identifier: username, password }); 28 + console.log(`[LOGIN] Successfully logged in to AT Proto as ${response.data.did}`); 29 + console.log(`[LOGIN] Session handle: ${response.data.handle}`); 30 + return response; 31 + } catch (error: any) { 32 + console.error("[ERROR] Failed to login to AT Proto:", error.message); 33 + throw error; 34 + } 35 + } 36 + 37 + async function getGitHubRepos(): Promise<{ clone_url: string; name: string; description?: string }[]> { 38 + const curl = `curl -s "https://api.github.com/users/${GITHUB_USER}/repos?per_page=200"`; 39 + const output = run(curl); 40 + const json = JSON.parse(output); 41 + return json 42 + .filter((r: any) => r.name !== GITHUB_USER) 43 + .map((r: any) => ({ clone_url: r.clone_url, name: r.name, description: r.description })); 44 + } 45 + 46 + async function ensureTangledRemoteAndPush(repoDir: string, repoName: string, cloneUrl: string) { 47 + const tangledUrl = `${TANGLED_BASE_URL}/${repoName}`; 48 + try { 49 + const remotes = run("git remote", repoDir).split("\n"); 50 + if (!remotes.includes("tangled")) { 51 + console.log(`[REMOTE] Adding Tangled remote for ${repoName}`); 52 + run(`git remote add tangled ${tangledUrl}`, repoDir); 53 + } 54 + 55 + const originPushUrl = run("git remote get-url --push origin", repoDir); 56 + if (originPushUrl.includes("tangled.sh")) { 57 + run(`git remote set-url --push origin ${cloneUrl}`, repoDir); 58 + console.log(`[REMOTE] Reset origin push URL to GitHub`); 59 + } 60 + 61 + run(`git push tangled main`, repoDir); 62 + console.log(`[PUSH] Pushed main to Tangled`); 63 + } catch (error) { 64 + console.warn(`[WARN] Could not push ${repoName} to Tangled. Check SSH or repo existence.`); 65 + } 66 + } 67 + 68 + const BASE32_SORTABLE = "234567abcdefghijklmnopqrstuvwxyz"; 69 + 70 + function run(cmd: string, cwd?: string): string { 71 + const options: import("child_process").ExecSyncOptions = { 72 + cwd, 73 + stdio: "pipe", 74 + shell: process.env.SHELL || "/bin/bash", 75 + encoding: "utf-8", 76 + }; 77 + return execSync(cmd, options).toString().trim(); 78 + } 79 + 80 + function ensureDir(dir: string) { 81 + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); 82 + } 83 + 84 + function generateClockId(): number { 85 + return Math.floor(Math.random() * 1024); 86 + } 87 + 88 + function toBase32Sortable(num: bigint): string { 89 + if (num === 0n) return "2222222222222"; 90 + let result = ""; 91 + while (num > 0n) { 92 + result = BASE32_SORTABLE[Number(num % 32n)] + result; 93 + num = num / 32n; 94 + } 95 + return result.padStart(13, "2"); 96 + } 97 + 98 + function generateTid(): string { 99 + const nowMicroseconds = BigInt(Date.now()) * 1000n; 100 + const clockId = generateClockId(); 101 + const tidBigInt = (nowMicroseconds << 10n) | BigInt(clockId); 102 + return toBase32Sortable(tidBigInt); 103 + } 104 + 105 + // Tangled repo schema typing (matches sh.tangled.repo lexicon) 106 + interface TangledRepoRecord { 107 + $type: "sh.tangled.repo"; 108 + name: string; // required 109 + knot: string; // required 110 + createdAt: string; // required (ISO 8601 datetime) 111 + spindle?: string; // optional CI runner 112 + description?: string; // optional, max 140 graphemes 113 + website?: string; // optional URI 114 + topics?: string[]; // optional array of topics 115 + source?: string; // optional source URI 116 + labels?: string[]; // optional array of at-uri labels 117 + } 118 + 119 + // Cache for existing repo records 120 + const recordCache: Record<string, string> = {}; 121 + 122 + async function ensureTangledRecord( 123 + agent: AtpAgent, 124 + atprotoDid: string, 125 + githubUser: string, 126 + repoName: string, 127 + description?: string 128 + ): Promise<{ tid: string; existed: boolean }> { 129 + if (recordCache[repoName]) { 130 + return { tid: recordCache[repoName], existed: true }; 131 + } 132 + 133 + let cursor: string | undefined = undefined; 134 + let tid: string | null = null; 135 + 136 + do { 137 + const res: any = await agent.api.com.atproto.repo.listRecords({ 138 + repo: atprotoDid, 139 + collection: "sh.tangled.repo", 140 + limit: 50, 141 + cursor, 142 + }); 143 + 144 + for (const record of res.data.records) { 145 + const value = record.value as TangledRepoRecord; 146 + if (value.name === repoName && record.rkey) { 147 + tid = record.rkey; 148 + recordCache[repoName] = tid; 149 + console.log(`[FOUND] Existing record for ${repoName} (TID: ${tid})`); 150 + return { tid, existed: true }; 151 + } 152 + } 153 + 154 + cursor = res.data.cursor; 155 + } while (!tid && cursor); 156 + 157 + if (!tid) { 158 + tid = generateTid(); 159 + const record: TangledRepoRecord = { 160 + $type: "sh.tangled.repo", 161 + name: repoName, 162 + knot: "knot1.tangled.sh", 163 + createdAt: new Date().toISOString(), 164 + description: description ?? repoName, 165 + source: `https://github.com/${githubUser}/${repoName}`, 166 + labels: [], 167 + }; 168 + 169 + try { 170 + const result = await agent.api.com.atproto.repo.putRecord({ 171 + repo: atprotoDid, 172 + collection: "sh.tangled.repo", 173 + rkey: tid, 174 + record, 175 + }); 176 + console.log(`[CREATED] ATProto record URI: ${result.data.uri}`); 177 + } catch (error: any) { 178 + console.error(`[ERROR] Failed to create ATProto record for ${repoName}:`, error.message); 179 + throw error; 180 + } 181 + 182 + recordCache[repoName] = tid; 183 + console.log(`[CREATED] Tangled record for ${repoName} (TID: ${tid})`); 184 + return { tid, existed: false }; 185 + } 186 + 187 + return { tid, existed: false }; 188 + } 189 + 190 + function updateReadme(baseDir: string, repoName: string, atprotoDid: string) { 191 + const repoDir = path.join(baseDir, repoName); 192 + const readmeFiles = ["README.md", "README.MD", "README.txt", "README"]; 193 + const readmeFile = readmeFiles.find((f) => fs.existsSync(path.join(repoDir, f))); 194 + if (!readmeFile) return; 195 + const readmePath = path.join(repoDir, readmeFile); 196 + const content = fs.readFileSync(readmePath, "utf-8"); 197 + if (!/tangled\.org/i.test(content)) { 198 + fs.appendFileSync( 199 + readmePath, 200 + ` 201 + Mirrored on Tangled: https://tangled.org/${atprotoDid}/${repoName} 202 + ` 203 + ); 204 + run(`git add ${readmeFile}`, repoDir); 205 + run(`git commit -m "Add Tangled mirror reference to README"`, repoDir); 206 + run(`git push origin main`, repoDir); 207 + console.log(`[README] Updated for ${repoName}`); 208 + } 209 + } 210 + 211 + async function main() { 212 + console.log("[STARTUP] Starting Tangled Sync..."); 213 + if (FORCE_SYNC) { 214 + console.log("[MODE] Force sync enabled - will process all repos"); 215 + } 216 + console.log(`[CONFIG] Base directory: ${BASE_DIR}`); 217 + console.log(`[CONFIG] GitHub user: ${GITHUB_USER}`); 218 + console.log(`[CONFIG] ATProto DID: ${ATPROTO_DID}`); 219 + console.log(`[CONFIG] PDS: ${BLUESKY_PDS}`); 220 + 221 + // Login to AT Proto 222 + await login(); 223 + 224 + // Ensure base directory exists 225 + ensureDir(BASE_DIR); 226 + 227 + // Fetch GitHub repositories 228 + console.log(`[GITHUB] Fetching repositories for ${GITHUB_USER}...`); 229 + const repos = await getGitHubRepos(); 230 + console.log(`[GITHUB] Found ${repos.length} repositories`); 231 + 232 + let reposToProcess = repos; 233 + let skippedRepos: typeof repos = []; 234 + 235 + if (!FORCE_SYNC) { 236 + // Fetch all existing Tangled records upfront 237 + console.log(`[ATPROTO] Fetching existing Tangled records...`); 238 + let cursor: string | undefined = undefined; 239 + const existingRepos = new Set<string>(); 240 + 241 + do { 242 + const res: any = await agent.api.com.atproto.repo.listRecords({ 243 + repo: ATPROTO_DID, 244 + collection: "sh.tangled.repo", 245 + limit: 100, 246 + cursor, 247 + }); 248 + 249 + for (const record of res.data.records) { 250 + const value = record.value as TangledRepoRecord; 251 + if (value.name) { 252 + existingRepos.add(value.name); 253 + recordCache[value.name] = record.rkey; 254 + } 255 + } 256 + 257 + cursor = res.data.cursor; 258 + } while (cursor); 259 + 260 + console.log(`[ATPROTO] Found ${existingRepos.size} existing Tangled records`); 261 + 262 + // Separate repos into new and existing 263 + reposToProcess = repos.filter(r => !existingRepos.has(r.name)); 264 + skippedRepos = repos.filter(r => existingRepos.has(r.name)); 265 + 266 + console.log(`[INFO] ${reposToProcess.length} new repos to sync`); 267 + console.log(`[INFO] ${skippedRepos.length} repos already synced (skipping)\n`); 268 + 269 + if (skippedRepos.length > 0) { 270 + console.log("[SKIPPED] The following repos already have AT Proto records:"); 271 + skippedRepos.forEach(r => console.log(` - ${r.name}`)); 272 + console.log(""); 273 + } 274 + } else { 275 + console.log("[INFO] Processing all ${repos.length} repos (force sync mode)\n"); 276 + } 277 + 278 + let syncedCount = 0; 279 + let errorCount = 0; 280 + 281 + for (const { clone_url, name: repoName, description } of reposToProcess) { 282 + console.log(`\n[PROGRESS] Processing ${repoName} (${syncedCount + 1}/${reposToProcess.length})`); 283 + const repoDir = path.join(BASE_DIR, repoName); 284 + 285 + try { 286 + if (!fs.existsSync(repoDir)) { 287 + run(`git clone ${clone_url} ${repoDir}`); 288 + console.log(`[CLONE] ${repoName}`); 289 + } else { 290 + console.log(`[EXISTS] ${repoName} already cloned`); 291 + } 292 + 293 + await ensureTangledRemoteAndPush(repoDir, repoName, clone_url); 294 + updateReadme(BASE_DIR, repoName, ATPROTO_DID); 295 + const result = await ensureTangledRecord(agent, ATPROTO_DID, GITHUB_USER, repoName, description); 296 + 297 + if (!result.existed) { 298 + syncedCount++; 299 + } 300 + } catch (error: any) { 301 + console.error(`[ERROR] Failed to sync ${repoName}: ${error.message}`); 302 + errorCount++; 303 + } 304 + } 305 + 306 + console.log(`\n${'='.repeat(50)}`); 307 + console.log(`[COMPLETE] Sync finished!`); 308 + console.log(` โœ… New repos synced: ${syncedCount}`); 309 + if (!FORCE_SYNC) { 310 + console.log(` โญ๏ธ Repos skipped: ${skippedRepos.length}`); 311 + } 312 + if (errorCount > 0) { 313 + console.log(` โŒ Errors: ${errorCount}`); 314 + } 315 + console.log(`${'='.repeat(50)}`); 316 + } 317 + 318 + main().catch(console.error);
+71
packages/tangled-sync/src/test-atproto.ts
··· 1 + import { AtpAgent } from "@atproto/api"; 2 + import dotenv from "dotenv"; 3 + 4 + dotenv.config(); 5 + 6 + async function testAtProtoConnection() { 7 + console.log("Testing AT Proto Connection...\n"); 8 + 9 + const service = process.env.BLUESKY_PDS || "https://bsky.social"; 10 + const username = process.env.BLUESKY_USERNAME; 11 + const password = process.env.BLUESKY_PASSWORD; 12 + const atprotoDid = process.env.ATPROTO_DID; 13 + 14 + console.log(`Service: ${service}`); 15 + console.log(`Username: ${username}`); 16 + console.log(`Expected DID: ${atprotoDid}\n`); 17 + 18 + if (!username || !password) { 19 + console.error("ERROR: Missing BLUESKY_USERNAME or BLUESKY_PASSWORD"); 20 + process.exit(1); 21 + } 22 + 23 + const agent = new AtpAgent({ service }); 24 + 25 + try { 26 + console.log("Attempting login..."); 27 + const loginResponse = await agent.login({ 28 + identifier: username, 29 + password 30 + }); 31 + 32 + console.log("โœ“ Login successful!"); 33 + console.log(` DID: ${loginResponse.data.did}`); 34 + console.log(` Handle: ${loginResponse.data.handle}`); 35 + console.log(` Email: ${loginResponse.data.email || "N/A"}`); 36 + 37 + if (loginResponse.data.did !== atprotoDid) { 38 + console.warn(`\nโš  WARNING: Logged in DID (${loginResponse.data.did}) does not match ATPROTO_DID in .env (${atprotoDid})`); 39 + console.warn(" Please update your ATPROTO_DID in src/.env"); 40 + } 41 + 42 + // Test fetching existing records 43 + console.log("\nFetching existing sh.tangled.repo records..."); 44 + const records = await agent.api.com.atproto.repo.listRecords({ 45 + repo: loginResponse.data.did, 46 + collection: "sh.tangled.repo", 47 + limit: 10, 48 + }); 49 + 50 + console.log(`โœ“ Found ${records.data.records.length} existing Tangled repo records`); 51 + 52 + if (records.data.records.length > 0) { 53 + console.log("\nSample records:"); 54 + records.data.records.slice(0, 3).forEach((record: any) => { 55 + console.log(` - ${record.value.name} (${record.uri})`); 56 + }); 57 + } 58 + 59 + console.log("\nโœ“ AT Proto connection test completed successfully!"); 60 + 61 + } catch (error: any) { 62 + console.error("\nโœ— AT Proto connection test failed!"); 63 + console.error(`Error: ${error.message}`); 64 + if (error.status) { 65 + console.error(`HTTP Status: ${error.status}`); 66 + } 67 + process.exit(1); 68 + } 69 + } 70 + 71 + testAtProtoConnection();
+109
packages/tangled-sync/src/validate-config.ts
··· 1 + import dotenv from "dotenv"; 2 + import fs from "fs"; 3 + import path from "path"; 4 + import { fileURLToPath } from "url"; 5 + 6 + const __filename = fileURLToPath(import.meta.url); 7 + const __dirname = path.dirname(__filename); 8 + 9 + dotenv.config(); 10 + 11 + console.log("๐Ÿ” Validating Tangled Sync Configuration...\n"); 12 + 13 + const checks: { name: string; status: boolean; message: string }[] = []; 14 + 15 + // Check .env file exists 16 + const envPath = path.join(__dirname, ".env"); 17 + const envExists = fs.existsSync(envPath); 18 + checks.push({ 19 + name: ".env file", 20 + status: envExists, 21 + message: envExists ? "Found at src/.env" : "Missing! Copy src/.env.example to src/.env" 22 + }); 23 + 24 + // Check required environment variables 25 + const requiredVars = [ 26 + { name: "BASE_DIR", description: "Base directory for repos" }, 27 + { name: "GITHUB_USER", description: "GitHub username" }, 28 + { name: "ATPROTO_DID", description: "AT Proto DID" }, 29 + { name: "BLUESKY_PDS", description: "Bluesky PDS URL" }, 30 + { name: "BLUESKY_USERNAME", description: "Bluesky username" }, 31 + { name: "BLUESKY_PASSWORD", description: "Bluesky app password" }, 32 + ]; 33 + 34 + requiredVars.forEach(({ name, description }) => { 35 + const value = process.env[name]; 36 + const exists = !!value && value.trim().length > 0; 37 + checks.push({ 38 + name: `${name}`, 39 + status: exists, 40 + message: exists ? `โœ“ Set (${description})` : `โœ— Missing (${description})` 41 + }); 42 + }); 43 + 44 + // Validate BASE_DIR 45 + const baseDir = process.env.BASE_DIR; 46 + if (baseDir) { 47 + const baseDirExists = fs.existsSync(baseDir); 48 + checks.push({ 49 + name: "BASE_DIR exists", 50 + status: baseDirExists, 51 + message: baseDirExists ? `Directory exists: ${baseDir}` : `Directory missing: ${baseDir} (will be created)` 52 + }); 53 + } 54 + 55 + // Validate DID format 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 + name: "DID format", 61 + status: validDid, 62 + message: validDid ? "Valid DID format" : "Invalid! Should start with 'did:plc:' or 'did:web:'" 63 + }); 64 + } 65 + 66 + // Validate PDS URL 67 + const pds = process.env.BLUESKY_PDS; 68 + if (pds) { 69 + const validPds = pds.startsWith("http://") || pds.startsWith("https://"); 70 + checks.push({ 71 + name: "PDS URL format", 72 + status: validPds, 73 + message: validPds ? `Valid URL: ${pds}` : "Invalid! Should start with 'https://'" 74 + }); 75 + } 76 + 77 + // Print results 78 + console.log("Configuration Check Results:\n"); 79 + let allPassed = true; 80 + 81 + checks.forEach((check) => { 82 + const icon = check.status ? "โœ…" : "โŒ"; 83 + console.log(`${icon} ${check.name}: ${check.message}`); 84 + if (!check.status) allPassed = false; 85 + }); 86 + 87 + console.log("\n" + "=".repeat(50) + "\n"); 88 + 89 + if (allPassed) { 90 + console.log("โœ… All checks passed! You're ready to run:"); 91 + console.log(" npm run test-atproto # Test AT Proto connection"); 92 + console.log(" npm run sync # Run the full sync"); 93 + } else { 94 + console.log("โŒ Some checks failed. Please fix the issues above."); 95 + console.log(" See SETUP.md for detailed instructions."); 96 + process.exit(1); 97 + } 98 + 99 + // Additional recommendations 100 + console.log("\n๐Ÿ’ก Recommendations:"); 101 + 102 + if (process.env.BLUESKY_PASSWORD && !process.env.BLUESKY_PASSWORD.includes("-")) { 103 + console.log(" โš ๏ธ Your password looks like it might be a regular password."); 104 + console.log(" Consider using an App Password from Bluesky settings."); 105 + } 106 + 107 + console.log(" ๐Ÿ“š Read SETUP.md for detailed setup instructions"); 108 + console.log(" ๐Ÿ” Never commit your .env file to version control"); 109 + console.log(" ๐Ÿ”‘ Make sure your SSH key is added to Tangled");
+26
packages/tangled-sync/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "NodeNext", 5 + "moduleResolution": "NodeNext", 6 + "rootDir": "./src", 7 + "outDir": "./dist", 8 + "strict": true, 9 + "esModuleInterop": true, 10 + "forceConsistentCasingInFileNames": true, 11 + "skipLibCheck": true, 12 + "resolveJsonModule": true, 13 + "noImplicitAny": true, 14 + "noFallthroughCasesInSwitch": true, 15 + "allowSyntheticDefaultImports": true, 16 + "types": ["node"], 17 + "strictNullChecks": true, 18 + "allowJs": false 19 + }, 20 + "include": ["src/**/*.ts"], 21 + "exclude": ["node_modules", "dist"], 22 + "ts-node": { 23 + "esm": true, 24 + "experimentalSpecifierResolution": "node" 25 + } 26 + }