Move from GitHub to Tangled

refactor: migrate to ESM, update Bluesky agent, and clean repo sync

- Converted project to ES modules (`type: "module"` in package.json)
- Replaced BskyAgent with AtpAgent and updated login logic
- Removed `src/config.env`; now using `.env` for credentials
- Cleaned README formatting and minor typos
- Improved git remote handling:
* Adds Tangled remote if missing
* Ensures 'origin' push URL remains GitHub
* Adds clearer logging for pushes and warnings
- Updated utility functions:
* `run` now uses typed ExecSyncOptions
* `ensureTangledRecord` improved for error handling and deterministic TID generation
- General code cleanup: consistent formatting, optional chaining, and destructuring

ewancroft.uk 22724970 e640192d

verified
+190
.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
+3 -5
README.md
··· 1 - 2 1 # Tangled Sync 3 2 4 - This bootstrap creates a TypeScript project that syncs GitHub repos to Tangled and 5 - publishes ATProto records for each repository. 3 + a TypeScript project that syncs GitHub repos to Tangled and publishes ATProto records for each repository. 6 4 7 5 See `src/config.env` for configuration. After running this script, run `npm install` 8 6 and then `npm run sync` from the project directory. 9 7 10 8 **Crucially**, before running `npm run sync`, you must **verify your SSH connection** to Tangled: 11 9 12 - 1. Run `ssh -T git@tangled.sh` and ensure it succeeds. 13 - 2. If the tangled remote does not exist for a GitHub repo, the script will attempt to create it on first run, but this requires an active, working SSH key. 10 + 1. Run `ssh -T git@tangled.sh` and ensure it succeeds. 11 + 2. If the tangled remote does not exist for a GitHub repo, the script will attempt to create it on first run, but this requires an active, working SSH key.
+361
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.0.0", 17 + "typescript": "^5.0.0" 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 + "bin": { 310 + "tsc": "bin/tsc", 311 + "tsserver": "bin/tsserver" 312 + }, 313 + "engines": { 314 + "node": ">=14.17" 315 + } 316 + }, 317 + "node_modules/uint8arrays": { 318 + "version": "3.0.0", 319 + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz", 320 + "integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==", 321 + "license": "MIT", 322 + "dependencies": { 323 + "multiformats": "^9.4.2" 324 + } 325 + }, 326 + "node_modules/undici-types": { 327 + "version": "7.14.0", 328 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", 329 + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", 330 + "dev": true, 331 + "license": "MIT", 332 + "peer": true 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 + }
+1
package.json
··· 4 4 "version": "1.0.0", 5 5 "description": "Sync GitHub repos to Tangled with ATProto records", 6 6 "main": "src/index.ts", 7 + "type": "module", 7 8 "scripts": { 8 9 "sync": "ts-node src/index.ts" 9 10 },
-7
src/config.env
··· 1 - 2 - BLUESKY_USERNAME=your_bsky_username 3 - BLUESKY_PASSWORD=your_bsky_password 4 - BLUESKY_PDS=https://bsky.social # <-- ADDED PDS URL 5 - BASE_DIR=/Volumes/Storage/Developer/Git 6 - GITHUB_USER=ewanc26 7 - ATPROTO_DID=did:plc:ofrbh253gwicbkc5nktqepol
+19 -33
src/index.ts
··· 1 - 2 - import { BskyAgent } from "@atproto/api"; 3 - import * as dotenv from "dotenv"; 4 - import * as fs from "fs"; 5 - import * as path from "path"; 1 + import { AtpAgent } from "@atproto/api"; 2 + import dotenv from "dotenv"; 3 + import fs from "fs"; 4 + import path from "path"; 6 5 import { run, ensureDir, ensureTangledRecord, updateReadme } from "./repo-utils"; 7 6 8 - dotenv.config({ path: "./src/config.env" }); 7 + dotenv.config({ path: "./src/.env" }); 9 8 10 9 const BASE_DIR = process.env.BASE_DIR!; 11 10 const GITHUB_USER = process.env.GITHUB_USER!; 12 11 const ATPROTO_DID = process.env.ATPROTO_DID!; 13 - const BLUESKY_PDS = process.env.BLUESKY_PDS!; // <-- GET PDS URL 12 + const BLUESKY_PDS = process.env.BLUESKY_PDS!; 14 13 const TANGLED_BASE_URL = `git@tangled.sh:${ATPROTO_DID}`; 15 14 16 - // Initialize BskyAgent with the specified PDS URL 17 - const agent = new BskyAgent({ service: BLUESKY_PDS }); // <-- USE PDS URL 15 + const agent = new AtpAgent({ service: BLUESKY_PDS }); 18 16 19 17 async function login() { 20 - if (!process.env.BLUESKY_USERNAME || !process.env.BLUESKY_PASSWORD) { 21 - throw new Error("Missing Bluesky credentials"); 22 - } 23 - await agent.login({ identifier: process.env.BLUESKY_USERNAME, password: process.env.BLUESKY_PASSWORD }); 18 + const username = process.env.BLUESKY_USERNAME; 19 + const password = process.env.BLUESKY_PASSWORD; 20 + if (!username || !password) throw new Error("Missing Bluesky credentials"); 21 + await agent.login({ identifier: username, password }); 24 22 console.log("[LOGIN] Logged in to Bluesky"); 25 23 } 26 24 27 25 async function getGitHubRepos(): Promise<{ clone_url: string, name: string, description?: string }[]> { 28 - // We use `shell: true` in repo-utils for this command as it contains quotes and a pipe is complex to pass as an array. 29 26 const curl = `curl -s "https://api.github.com/users/${GITHUB_USER}/repos?per_page=200"`; 30 27 const output = run(curl); 31 28 const json = JSON.parse(output); 32 - return json.filter((r: any) => r.name !== GITHUB_USER).map((r: any) => ({ clone_url: r.clone_url, name: r.name, description: r.description })) as any[]; 29 + return json.filter((r: any) => r.name !== GITHUB_USER) 30 + .map((r: any) => ({ clone_url: r.clone_url, name: r.name, description: r.description })); 33 31 } 34 32 35 - // ----------------------------------------------------- 36 - // NEW/MODIFIED LOGIC HERE: ENSURE REMOTE AND PUSH SAFELY 37 - // ----------------------------------------------------- 38 33 async function ensureTangledRemoteAndPush(repoDir: string, repoName: string, cloneUrl: string) { 39 34 const tangledUrl = `${TANGLED_BASE_URL}/${repoName}`; 40 - 41 35 try { 42 36 const remotes = run("git remote", repoDir).split("\n"); 43 - 44 37 if (!remotes.includes("tangled")) { 45 - console.log(`[REMOTE] 'tangled' remote not found for ${repoName}. Attempting to add it...`); 38 + console.log(`[REMOTE] Adding Tangled remote for ${repoName}`); 46 39 run(`git remote add tangled ${tangledUrl}`, repoDir); 47 - console.log(`[REMOTE] Added Tangled remote: ${tangledUrl}`); 48 40 } 49 41 50 - // Safety check: ensure 'origin' push URL is not pointing to tangled.sh 51 42 const originPushUrl = run("git remote get-url --push origin", repoDir); 52 43 if (originPushUrl.includes("tangled.sh")) { 53 44 run(`git remote set-url --push origin ${cloneUrl}`, repoDir); 54 - console.log(`[REMOTE] Removed Tangled from origin push URL and set to: ${cloneUrl}`); 45 + console.log(`[REMOTE] Reset origin push URL to GitHub`); 55 46 } 56 47 57 - // Attempt to push. This is the point where the SSH connection is tested for this repo. 58 48 run(`git push tangled main`, repoDir); 59 - console.log(`[PUSH] Pushed main branch to Tangled successfully.`); 60 - 49 + console.log(`[PUSH] Pushed main to Tangled`); 61 50 } catch (error) { 62 - console.log(`[FAIL] Push to Tangled failed for ${repoName}. This is expected if the remote repository does not exist on tangled.sh or SSH is not configured.`); 63 - console.log(`[HINT] You must ensure 'ssh -T git@tangled.sh' works before running the sync.`); 64 - // The script continues to the next repo/record creation even if the push fails. 51 + console.warn(`[WARN] Could not push ${repoName} to Tangled. Check SSH or repo existence.`); 65 52 } 66 53 } 67 54 ··· 71 58 const repos = await getGitHubRepos(); 72 59 73 60 for (const { clone_url, name: repoName, description } of repos) { 74 - console.log(` 75 - [PROGRESS] Processing ${repoName}`); 61 + console.log(`[PROGRESS] Processing ${repoName}`); 76 62 const repoDir = path.join(BASE_DIR, repoName); 63 + 77 64 if (!fs.existsSync(repoDir)) { 78 65 run(`git clone ${clone_url} ${repoDir}`); 79 66 console.log(`[CLONE] ${repoName}`); 80 67 } 81 68 82 69 await ensureTangledRemoteAndPush(repoDir, repoName, clone_url); 83 - 84 70 updateReadme(BASE_DIR, repoName, ATPROTO_DID); 85 71 await ensureTangledRecord(agent, ATPROTO_DID, GITHUB_USER, repoName, description); 86 72 }
+25 -13
src/repo-utils.ts
··· 1 - 2 - import { BskyAgent } from "@atproto/api"; 3 - import * as child_process from "child_process"; 4 - import * as fs from "fs"; 5 - import * as path from "path"; 1 + import { AtpAgent } from "@atproto/api"; 2 + import { execSync, ExecSyncOptions } from "child_process"; 3 + import fs from "fs"; 4 + import path from "path"; 6 5 7 6 const BASE32_SORTABLE = '234567abcdefghijklmnopqrstuvwxyz'; 8 7 9 8 export function run(cmd: string, cwd?: string): string { 10 - // Use shell: true for commands that contain pipes or shell built-ins 11 - return child_process.execSync(cmd, { cwd, stdio: "pipe", shell: true }).toString().trim(); 9 + const options: ExecSyncOptions = { 10 + cwd, 11 + stdio: "pipe", 12 + shell: process.env.SHELL || "/bin/bash", 13 + encoding: "utf-8" 14 + }; 15 + return execSync(cmd, options).toString().trim(); 12 16 } 13 17 14 18 export function ensureDir(dir: string) { 15 19 if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); 16 20 } 17 21 18 - function generateClockId(): number { return Math.floor(Math.random() * 1024); } 22 + function generateClockId(): number { 23 + return Math.floor(Math.random() * 1024); 24 + } 19 25 20 26 function toBase32Sortable(num: bigint): string { 21 27 if (num === 0n) return '2222222222222'; ··· 34 40 return toBase32Sortable(tidBigInt); 35 41 } 36 42 37 - export async function ensureTangledRecord(agent: BskyAgent, atprotoDid: string, githubUser: string, repoName: string, description?: string) { 38 - let tid: string; 43 + export async function ensureTangledRecord(agent: AtpAgent, atprotoDid: string, githubUser: string, repoName: string, description?: string) { 44 + let tid: string = generateTid(); 39 45 let exists = true; 46 + 40 47 while (exists) { 41 48 tid = generateTid(); 42 49 try { 43 50 await agent.api.com.atproto.repo.getRecord({ repo: atprotoDid, collection: "sh.tangled.repo", rkey: tid }); 44 51 exists = true; 45 - } catch { 46 - exists = false; 52 + } catch (e: any) { 53 + if (e.message && e.message.includes('Record not found')) { 54 + exists = false; 55 + } else { 56 + console.error("Error checking ATProto record existence:", e); 57 + throw e; 58 + } 47 59 } 48 60 } 49 61 ··· 54 66 createdAt: new Date().toISOString(), 55 67 description: description || repoName, 56 68 labels: [], 57 - source: `https://github.com/${github_user}/${repoName}`, 69 + source: `https://github.com/${githubUser}/${repoName}`, 58 70 spindle: "", 59 71 }; 60 72
+22
tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2022", 4 + "module": "ESNext", 5 + "moduleResolution": "bundler", 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 + }