diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..0d203ed --- /dev/null +++ b/biome.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "includes": ["**", "!!**/dist"] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/package.json b/package.json index 6891879..f3a8dae 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "scripts": { "build": "tsup", "dev": "tsx src/cli/index.ts", - "lint": "eslint src/", + "lint": "biome check src", + "lint:fix": "biome check --write src", + "format": "biome format --write src", "typecheck": "tsc --noEmit", "prepublishOnly": "pnpm build" }, @@ -55,8 +57,8 @@ "update-notifier": "^7.3.1" }, "devDependencies": { + "@biomejs/biome": "^2.3.10", "@types/node": "^20.0.0", - "eslint": "^9.0.0", "tsup": "^8.5.1", "tsx": "^4.0.0", "typescript": "^5.0.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2792c02..30e93cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,12 +27,12 @@ importers: specifier: ^7.3.1 version: 7.3.1 devDependencies: + '@biomejs/biome': + specifier: ^2.3.10 + version: 2.3.10 '@types/node': specifier: ^20.0.0 version: 20.19.27 - eslint: - specifier: ^9.0.0 - version: 9.39.2 tsup: specifier: ^8.5.1 version: 8.5.1(tsx@4.21.0)(typescript@5.9.3) @@ -45,6 +45,63 @@ importers: packages: + '@biomejs/biome@2.3.10': + resolution: {integrity: sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@2.3.10': + resolution: {integrity: sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.10': + resolution: {integrity: sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@2.3.10': + resolution: {integrity: sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-arm64@2.3.10': + resolution: {integrity: sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-linux-x64-musl@2.3.10': + resolution: {integrity: sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@biomejs/cli-linux-x64@2.3.10': + resolution: {integrity: sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@biomejs/cli-win32-arm64@2.3.10': + resolution: {integrity: sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.10': + resolution: {integrity: sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + '@esbuild/aix-ppc64@0.27.2': resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} engines: {node: '>=18'} @@ -201,60 +258,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.0': - resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} - engines: {node: '>=18.18'} - '@inquirer/ansi@2.0.2': resolution: {integrity: sha512-SYLX05PwJVnW+WVegZt1T4Ip1qba1ik+pNJPDiqvk6zS5Y/i8PhRzLpGEtVd7sW0G8cMtkD8t4AZYhQwm8vnww==} engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'} @@ -596,25 +599,14 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@20.19.27': resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} @@ -626,10 +618,6 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -637,15 +625,9 @@ packages: any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - atomically@2.1.0: resolution: {integrity: sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - before-after-hook@3.0.2: resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} @@ -653,9 +635,6 @@ packages: resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} engines: {node: '>=18'} - brace-expansion@1.1.12: - resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} - bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} @@ -670,18 +649,10 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - camelcase@8.0.0: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -701,13 +672,6 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -716,9 +680,6 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -733,10 +694,6 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -750,9 +707,6 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - default-browser-id@5.0.1: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} @@ -784,64 +738,9 @@ packages: resolution: {integrity: sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==} engines: {node: '>=12'} - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-scope@8.4.0: - resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - espree@10.4.0: - resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - fast-content-type-parse@2.0.1: resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -851,24 +750,9 @@ packages: picomatch: optional: true - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -881,44 +765,20 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - global-directory@4.0.1: resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} engines: {node: '>=18'} - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - graceful-fs@4.2.10: resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - iconv-lite@0.7.1: resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} engines: {node: '>=0.10.0'} - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} @@ -931,18 +791,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - is-in-ci@1.0.0: resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==} engines: {node: '>=18'} @@ -969,29 +821,10 @@ packages: resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} engines: {node: '>=16'} - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - ky@1.14.2: resolution: {integrity: sha512-q3RBbsO5A5zrPhB6CaCS8ZUv+NWCXv6JJT4Em0i264G9W0fdPB8YRfnnEi7Dm7X7omAkBIPojzYJ2D1oHTHqug==} engines: {node: '>=18'} @@ -1000,10 +833,6 @@ packages: resolution: {integrity: sha512-7W0vV3rqv5tokqkBAFV1LbR7HPOWzXQDpDgEuib/aJ1jsZZx6x3c2mBI+TJhJzOhkGeaLbCKEHXEXLfirtG2JA==} engines: {node: '>=18'} - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -1015,19 +844,9 @@ packages: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1044,9 +863,6 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1055,34 +871,10 @@ packages: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - package-json@10.0.1: resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} engines: {node: '>=18'} - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1118,17 +910,9 @@ packages: yaml: optional: true - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - pupa@3.3.0: resolution: {integrity: sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==} engines: {node: '>=12.20'} @@ -1149,10 +933,6 @@ packages: resolution: {integrity: sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==} engines: {node: '>=12'} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1177,14 +957,6 @@ packages: engines: {node: '>=10'} hasBin: true - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -1213,10 +985,6 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - stubborn-fs@2.0.0: resolution: {integrity: sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==} @@ -1228,10 +996,6 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -1277,10 +1041,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - type-fest@4.41.0: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} @@ -1303,25 +1063,13 @@ packages: resolution: {integrity: sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==} engines: {node: '>=18'} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - when-exit@2.1.5: resolution: {integrity: sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==} - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - widest-line@5.0.0: resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} engines: {node: '>=18'} - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -1334,12 +1082,43 @@ packages: resolution: {integrity: sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==} engines: {node: '>=12'} - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - snapshots: + '@biomejs/biome@2.3.10': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.10 + '@biomejs/cli-darwin-x64': 2.3.10 + '@biomejs/cli-linux-arm64': 2.3.10 + '@biomejs/cli-linux-arm64-musl': 2.3.10 + '@biomejs/cli-linux-x64': 2.3.10 + '@biomejs/cli-linux-x64-musl': 2.3.10 + '@biomejs/cli-win32-arm64': 2.3.10 + '@biomejs/cli-win32-x64': 2.3.10 + + '@biomejs/cli-darwin-arm64@2.3.10': + optional: true + + '@biomejs/cli-darwin-x64@2.3.10': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.10': + optional: true + + '@biomejs/cli-linux-arm64@2.3.10': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.10': + optional: true + + '@biomejs/cli-linux-x64@2.3.10': + optional: true + + '@biomejs/cli-win32-arm64@2.3.10': + optional: true + + '@biomejs/cli-win32-x64@2.3.10': + optional: true + '@esbuild/aix-ppc64@0.27.2': optional: true @@ -1418,63 +1197,6 @@ snapshots: '@esbuild/win32-x64@0.27.2': optional: true - '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2)': - dependencies: - eslint: 9.39.2 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/config-array@0.21.1': - dependencies: - '@eslint/object-schema': 2.1.7 - debug: 4.4.3 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 - - '@eslint/core@0.17.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.3': - dependencies: - ajv: 6.12.6 - debug: 4.4.3 - espree: 10.4.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.39.2': {} - - '@eslint/object-schema@2.1.7': {} - - '@eslint/plugin-kit@0.4.1': - dependencies: - '@eslint/core': 0.17.0 - levn: 0.4.1 - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.7': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.4.3 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.4.3': {} - '@inquirer/ansi@2.0.2': {} '@inquirer/checkbox@5.0.3(@types/node@20.19.27)': @@ -1756,25 +1478,12 @@ snapshots: '@types/estree@1.0.8': {} - '@types/json-schema@7.0.15': {} - '@types/node@20.19.27': dependencies: undici-types: 6.21.0 - acorn-jsx@5.3.2(acorn@8.15.0): - dependencies: - acorn: 8.15.0 - acorn@8.15.0: {} - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - ansi-align@3.0.1: dependencies: string-width: 4.2.3 @@ -1783,23 +1492,15 @@ snapshots: ansi-regex@6.2.2: {} - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - ansi-styles@6.2.3: {} any-promise@1.3.0: {} - argparse@2.0.1: {} - atomically@2.1.0: dependencies: stubborn-fs: 2.0.0 when-exit: 2.1.5 - balanced-match@1.0.2: {} - before-after-hook@3.0.2: {} boxen@8.0.1: @@ -1813,11 +1514,6 @@ snapshots: widest-line: 5.0.0 wrap-ansi: 9.0.2 - brace-expansion@1.1.12: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 @@ -1829,15 +1525,8 @@ snapshots: cac@6.7.14: {} - callsites@3.1.0: {} - camelcase@8.0.0: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@5.6.2: {} chardet@2.1.1: {} @@ -1850,18 +1539,10 @@ snapshots: cli-width@4.1.0: {} - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - commander@12.1.0: {} commander@4.1.1: {} - concat-map@0.0.1: {} - confbox@0.1.8: {} config-chain@1.1.13: @@ -1878,20 +1559,12 @@ snapshots: consola@3.4.2: {} - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - debug@4.4.3: dependencies: ms: 2.1.3 deep-extend@0.6.0: {} - deep-is@0.1.4: {} - default-browser-id@5.0.1: {} default-browser@5.4.0: @@ -1940,108 +1613,18 @@ snapshots: escape-goat@4.0.0: {} - escape-string-regexp@4.0.0: {} - - eslint-scope@8.4.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.1: {} - - eslint@9.39.2: - dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 - '@humanfs/node': 0.16.7 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.3 - '@types/estree': 1.0.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - eslint-scope: 8.4.0 - eslint-visitor-keys: 4.2.1 - espree: 10.4.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - json-stable-stringify-without-jsonify: 1.0.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.4 - transitivePeerDependencies: - - supports-color - - espree@10.4.0: - dependencies: - acorn: 8.15.0 - acorn-jsx: 5.3.2(acorn@8.15.0) - eslint-visitor-keys: 4.2.1 - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - esutils@2.0.3: {} - fast-content-type-parse@2.0.1: {} - fast-deep-equal@3.1.3: {} - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 mlly: 1.8.0 rollup: 4.54.0 - flat-cache@4.0.1: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - - flatted@3.3.3: {} - fsevents@2.3.3: optional: true @@ -2051,49 +1634,26 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - global-directory@4.0.1: dependencies: ini: 4.1.1 - globals@14.0.0: {} - graceful-fs@4.2.10: {} graceful-fs@4.2.11: {} - has-flag@4.0.0: {} - iconv-lite@0.7.1: dependencies: safer-buffer: 2.1.2 - ignore@5.3.2: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - imurmurhash@0.1.4: {} - ini@1.3.8: {} ini@4.1.1: {} is-docker@3.0.0: {} - is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - is-in-ci@1.0.0: {} is-inside-container@1.0.0: @@ -2113,55 +1673,24 @@ snapshots: dependencies: is-inside-container: 1.0.0 - isexe@2.0.0: {} - joycon@3.1.1: {} - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - ky@1.14.2: {} latest-version@9.0.0: dependencies: package-json: 10.0.1 - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} load-tsconfig@0.2.5: {} - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.12 - minimist@1.2.8: {} mlly@1.8.0: @@ -2181,8 +1710,6 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - natural-compare@1.4.0: {} - object-assign@4.1.1: {} open@10.2.0: @@ -2192,23 +1719,6 @@ snapshots: is-inside-container: 1.0.0 wsl-utils: 0.1.0 - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - package-json@10.0.1: dependencies: ky: 1.14.2 @@ -2216,14 +1726,6 @@ snapshots: registry-url: 6.0.1 semver: 7.7.3 - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - pathe@2.0.3: {} picocolors@1.1.1: {} @@ -2244,12 +1746,8 @@ snapshots: optionalDependencies: tsx: 4.21.0 - prelude-ls@1.2.1: {} - proto-list@1.2.4: {} - punycode@2.3.1: {} - pupa@3.3.0: dependencies: escape-goat: 4.0.0 @@ -2271,8 +1769,6 @@ snapshots: dependencies: rc: 1.2.8 - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -2311,12 +1807,6 @@ snapshots: semver@7.7.3: {} - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - signal-exit@4.1.0: {} source-map@0.7.6: {} @@ -2343,8 +1833,6 @@ snapshots: strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} - stubborn-fs@2.0.0: dependencies: stubborn-utils: 1.0.2 @@ -2361,10 +1849,6 @@ snapshots: tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -2418,10 +1902,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - type-fest@4.41.0: {} typescript@5.9.3: {} @@ -2445,22 +1925,12 @@ snapshots: semver: 7.7.3 xdg-basedir: 5.1.0 - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - when-exit@2.1.5: {} - which@2.0.2: - dependencies: - isexe: 2.0.0 - widest-line@5.0.0: dependencies: string-width: 7.2.0 - word-wrap@1.2.5: {} - wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -2472,5 +1942,3 @@ snapshots: is-wsl: 3.1.0 xdg-basedir@5.1.0: {} - - yocto-queue@0.1.0: {} diff --git a/src/cli/commands/config.ts b/src/cli/commands/config.ts index 0299544..5c70296 100644 --- a/src/cli/commands/config.ts +++ b/src/cli/commands/config.ts @@ -2,202 +2,244 @@ * config command - View and update settings */ -import { password } from '@inquirer/prompts'; -import chalk from 'chalk'; -import { Octokit } from '@octokit/rest'; -import { loadConfig, saveConfig } from '../../state/config.js'; -import { installLaunchAgent, uninstallLaunchAgent } from '../../daemon/launchagent.js'; -import type { Config, SyncType } from '../../types/index.js'; -import { ALL_SYNC_TYPES } from '../../types/index.js'; +import { password } from "@inquirer/prompts"; +import { Octokit } from "@octokit/rest"; +import chalk from "chalk"; +import { + installLaunchAgent, + uninstallLaunchAgent, +} from "../../daemon/launchagent.js"; +import { loadConfig, saveConfig } from "../../state/config.js"; +import type { Config, SyncType } from "../../types/index.js"; +import { ALL_SYNC_TYPES } from "../../types/index.js"; interface ConfigOptions { - interval?: string; - autostart?: string; - project?: string; - githubToken?: string; - thingsToken?: string; - syncTypes?: string; - verify?: boolean; - show?: boolean; + interval?: string; + autostart?: string; + project?: string; + githubToken?: string; + thingsToken?: string; + syncTypes?: string; + verify?: boolean; + show?: boolean; } export async function configCommand(options: ConfigOptions): Promise { - const config = loadConfig(); - if (!config) { - console.error(chalk.red('❌ Not configured. Run `github-things-sync init` first.')); - process.exit(1); - } - - // Show current config - if (options.show || Object.keys(options).length === 0) { - await showConfig(config); - return; - } - - // Verify tokens - if (options.verify) { - await verifyTokens(config); - return; - } - - // Update settings - let changed = false; - - if (options.interval !== undefined) { - const interval = Number.parseInt(options.interval, 10); - if (Number.isNaN(interval) || interval < 60) { - console.error(chalk.red('❌ Interval must be at least 60 seconds')); - process.exit(1); - } - config.pollInterval = interval; - console.log(chalk.green(`✅ Poll interval set to ${interval}s`)); - changed = true; - } - - if (options.autostart !== undefined) { - const enabled = options.autostart === 'true' || options.autostart === 'on'; - const disabled = options.autostart === 'false' || options.autostart === 'off'; - - if (!enabled && !disabled) { - console.error(chalk.red('❌ Use --autostart=true or --autostart=false')); - process.exit(1); - } - - config.autoStart = enabled; - - if (enabled) { - installLaunchAgent(); - console.log(chalk.green('✅ Autostart enabled (LaunchAgent installed)')); - } else { - uninstallLaunchAgent(); - console.log(chalk.green('✅ Autostart disabled (LaunchAgent removed)')); - } - changed = true; - } - - if (options.project !== undefined) { - config.thingsProject = options.project; - console.log(chalk.green(`✅ Things project set to "${options.project}"`)); - changed = true; - } - - if (options.githubToken !== undefined) { - const token = options.githubToken === 'prompt' - ? await password({ - message: 'New GitHub Token', - mask: '*', - validate: (value) => value ? true : 'Token is required', - }) - : options.githubToken; - - config.githubToken = token; - console.log(chalk.green('✅ GitHub token updated')); - changed = true; - } - - if (options.thingsToken !== undefined) { - const token = options.thingsToken === 'prompt' - ? await password({ - message: 'New Things Auth Token', - mask: '*', - validate: (value) => value ? true : 'Token is required', - }) - : options.thingsToken; - - config.thingsAuthToken = token; - console.log(chalk.green('✅ Things auth token updated')); - changed = true; - } - - if (options.syncTypes !== undefined) { - let syncTypes: SyncType[]; - if (options.syncTypes.toLowerCase() === 'all') { - syncTypes = [...ALL_SYNC_TYPES]; - } else { - const requested = options.syncTypes.split(',').map(s => s.trim()) as SyncType[]; - const valid = requested.filter(t => ALL_SYNC_TYPES.includes(t)); - if (valid.length === 0) { - console.error(chalk.red('❌ No valid sync types. Use: pr-reviews, prs-created, issues-assigned, issues-created')); - process.exit(1); - } - syncTypes = valid; - } - config.syncTypes = syncTypes; - console.log(chalk.green(`✅ Sync types set to: ${syncTypes.join(', ')}`)); - changed = true; - } - - if (changed) { - saveConfig(config); - console.log(chalk.dim('\n💾 Config saved. Restart daemon for changes to take effect.')); - } + const config = loadConfig(); + if (!config) { + console.error( + chalk.red("❌ Not configured. Run `github-things-sync init` first."), + ); + process.exit(1); + } + + // Show current config + if (options.show || Object.keys(options).length === 0) { + await showConfig(config); + return; + } + + // Verify tokens + if (options.verify) { + await verifyTokens(config); + return; + } + + // Update settings + let changed = false; + + if (options.interval !== undefined) { + const interval = Number.parseInt(options.interval, 10); + if (Number.isNaN(interval) || interval < 60) { + console.error(chalk.red("❌ Interval must be at least 60 seconds")); + process.exit(1); + } + config.pollInterval = interval; + console.log(chalk.green(`✅ Poll interval set to ${interval}s`)); + changed = true; + } + + if (options.autostart !== undefined) { + const enabled = options.autostart === "true" || options.autostart === "on"; + const disabled = + options.autostart === "false" || options.autostart === "off"; + + if (!enabled && !disabled) { + console.error(chalk.red("❌ Use --autostart=true or --autostart=false")); + process.exit(1); + } + + config.autoStart = enabled; + + if (enabled) { + installLaunchAgent(); + console.log(chalk.green("✅ Autostart enabled (LaunchAgent installed)")); + } else { + uninstallLaunchAgent(); + console.log(chalk.green("✅ Autostart disabled (LaunchAgent removed)")); + } + changed = true; + } + + if (options.project !== undefined) { + config.thingsProject = options.project; + console.log(chalk.green(`✅ Things project set to "${options.project}"`)); + changed = true; + } + + if (options.githubToken !== undefined) { + const token = + options.githubToken === "prompt" + ? await password({ + message: "New GitHub Token", + mask: "*", + validate: (value) => (value ? true : "Token is required"), + }) + : options.githubToken; + + config.githubToken = token; + console.log(chalk.green("✅ GitHub token updated")); + changed = true; + } + + if (options.thingsToken !== undefined) { + const token = + options.thingsToken === "prompt" + ? await password({ + message: "New Things Auth Token", + mask: "*", + validate: (value) => (value ? true : "Token is required"), + }) + : options.thingsToken; + + config.thingsAuthToken = token; + console.log(chalk.green("✅ Things auth token updated")); + changed = true; + } + + if (options.syncTypes !== undefined) { + let syncTypes: SyncType[]; + if (options.syncTypes.toLowerCase() === "all") { + syncTypes = [...ALL_SYNC_TYPES]; + } else { + const requested = options.syncTypes + .split(",") + .map((s) => s.trim()) as SyncType[]; + const valid = requested.filter((t) => ALL_SYNC_TYPES.includes(t)); + if (valid.length === 0) { + console.error( + chalk.red( + "❌ No valid sync types. Use: pr-reviews, prs-created, issues-assigned, issues-created", + ), + ); + process.exit(1); + } + syncTypes = valid; + } + config.syncTypes = syncTypes; + console.log(chalk.green(`✅ Sync types set to: ${syncTypes.join(", ")}`)); + changed = true; + } + + if (changed) { + saveConfig(config); + console.log( + chalk.dim( + "\n💾 Config saved. Restart daemon for changes to take effect.", + ), + ); + } } async function showConfig(config: Config): Promise { - console.log(chalk.bold('\n⚙️ github-things-sync config\n')); - console.log(chalk.bold('Settings')); - console.log(chalk.dim('────────')); - console.log(chalk.dim('Project: ') + config.thingsProject); - console.log(chalk.dim('Poll interval: ') + `${config.pollInterval}s (${config.pollInterval / 60} min)`); - console.log(chalk.dim('Autostart: ') + (config.autoStart ? chalk.green('✅ enabled') : chalk.red('❌ disabled'))); - console.log(chalk.dim('Sync types: ') + (config.syncTypes?.join(', ') || 'all (default)')); - console.log(''); - console.log(chalk.bold('Tokens')); - console.log(chalk.dim('──────')); - console.log(chalk.dim('GitHub: ') + maskToken(config.githubToken)); - console.log(chalk.dim('Things: ') + maskToken(config.thingsAuthToken)); - console.log(''); - console.log(chalk.bold('Usage')); - console.log(chalk.dim('─────')); - console.log(chalk.dim(' --interval=SECONDS ') + 'Set poll interval (min: 60)'); - console.log(chalk.dim(' --autostart=true|false ') + 'Enable/disable autostart'); - console.log(chalk.dim(' --project=NAME ') + 'Set Things project name'); - console.log(chalk.dim(' --github-token=prompt ') + 'Update GitHub token'); - console.log(chalk.dim(' --things-token=prompt ') + 'Update Things token'); - console.log(chalk.dim(' --sync-types=TYPES ') + 'Set sync types (comma-separated or "all")'); - console.log(chalk.dim(' --verify ') + 'Verify tokens work'); - console.log(''); + console.log(chalk.bold("\n⚙️ github-things-sync config\n")); + console.log(chalk.bold("Settings")); + console.log(chalk.dim("────────")); + console.log(chalk.dim("Project: ") + config.thingsProject); + console.log( + chalk.dim("Poll interval: ") + + `${config.pollInterval}s (${config.pollInterval / 60} min)`, + ); + console.log( + chalk.dim("Autostart: ") + + (config.autoStart ? chalk.green("✅ enabled") : chalk.red("❌ disabled")), + ); + console.log( + chalk.dim("Sync types: ") + + (config.syncTypes?.join(", ") || "all (default)"), + ); + console.log(""); + console.log(chalk.bold("Tokens")); + console.log(chalk.dim("──────")); + console.log(chalk.dim("GitHub: ") + maskToken(config.githubToken)); + console.log(chalk.dim("Things: ") + maskToken(config.thingsAuthToken)); + console.log(""); + console.log(chalk.bold("Usage")); + console.log(chalk.dim("─────")); + console.log( + `${chalk.dim(" --interval=SECONDS ")}Set poll interval (min: 60)`, + ); + console.log( + `${chalk.dim(" --autostart=true|false ")}Enable/disable autostart`, + ); + console.log( + `${chalk.dim(" --project=NAME ")}Set Things project name`, + ); + console.log(`${chalk.dim(" --github-token=prompt ")}Update GitHub token`); + console.log(`${chalk.dim(" --things-token=prompt ")}Update Things token`); + console.log( + chalk.dim(" --sync-types=TYPES ") + + 'Set sync types (comma-separated or "all")', + ); + console.log(`${chalk.dim(" --verify ")}Verify tokens work`); + console.log(""); } function maskToken(token: string): string { - if (token.length <= 8) return '****'; - return token.slice(0, 4) + '...' + token.slice(-4); + if (token.length <= 8) return "****"; + return `${token.slice(0, 4)}...${token.slice(-4)}`; } async function verifyTokens(config: Config): Promise { - console.log(chalk.bold('\n🔍 Verifying tokens...\n')); - - // Verify GitHub token - console.log(chalk.bold('GitHub Token')); - console.log(chalk.dim('────────────')); - try { - const octokit = new Octokit({ auth: config.githubToken }); - const { data } = await octokit.users.getAuthenticated(); - console.log(chalk.green(`✅ Valid`) + ` - logged in as @${chalk.cyan(data.login)}`); - - // Check scopes - const response = await octokit.request('GET /user'); - const scopes = response.headers['x-oauth-scopes'] || ''; - console.log(chalk.dim(` Scopes: ${scopes || '(none visible)'}`)); - } catch (error) { - console.log(chalk.red(`❌ Invalid`) + ` - ${error}`); - } - - // Verify Things token (we can only check if Things is running) - console.log(chalk.bold('\nThings Auth Token')); - console.log(chalk.dim('─────────────────')); - try { - const { exec } = await import('child_process'); - const { promisify } = await import('util'); - const execAsync = promisify(exec); - - await execAsync('osascript -e \'tell application "Things3" to return name\''); - console.log(chalk.green('✅ Things 3 is running')); - console.log(chalk.dim(` Token: ${maskToken(config.thingsAuthToken)}`)); - console.log(chalk.dim(' (Token validity is checked when updating tasks)')); - } catch { - console.log(chalk.yellow('⚠️ Things 3 is not running')); - } - - console.log(''); + console.log(chalk.bold("\n🔍 Verifying tokens...\n")); + + // Verify GitHub token + console.log(chalk.bold("GitHub Token")); + console.log(chalk.dim("────────────")); + try { + const octokit = new Octokit({ auth: config.githubToken }); + const { data } = await octokit.users.getAuthenticated(); + console.log( + `${chalk.green(`✅ Valid`)} - logged in as @${chalk.cyan(data.login)}`, + ); + + // Check scopes + const response = await octokit.request("GET /user"); + const scopes = response.headers["x-oauth-scopes"] || ""; + console.log(chalk.dim(` Scopes: ${scopes || "(none visible)"}`)); + } catch (error) { + console.log(`${chalk.red(`❌ Invalid`)} - ${error}`); + } + + // Verify Things token (we can only check if Things is running) + console.log(chalk.bold("\nThings Auth Token")); + console.log(chalk.dim("─────────────────")); + try { + const { exec } = await import("node:child_process"); + const { promisify } = await import("node:util"); + const execAsync = promisify(exec); + + await execAsync( + "osascript -e 'tell application \"Things3\" to return name'", + ); + console.log(chalk.green("✅ Things 3 is running")); + console.log(chalk.dim(` Token: ${maskToken(config.thingsAuthToken)}`)); + console.log( + chalk.dim(" (Token validity is checked when updating tasks)"), + ); + } catch { + console.log(chalk.yellow("⚠️ Things 3 is not running")); + } + + console.log(""); } diff --git a/src/cli/commands/init.ts b/src/cli/commands/init.ts index 92d9e59..5c35385 100644 --- a/src/cli/commands/init.ts +++ b/src/cli/commands/init.ts @@ -2,179 +2,239 @@ * init command - Interactive setup wizard */ -import { input, password, confirm, checkbox } from '@inquirer/prompts'; -import chalk from 'chalk'; -import { loadConfig, saveConfig, getConfigPath, getDataDir } from '../../state/config.js'; -import { installLaunchAgent } from '../../daemon/launchagent.js'; -import type { Config, SyncType } from '../../types/index.js'; -import { ALL_SYNC_TYPES } from '../../types/index.js'; +import { checkbox, confirm, input, password } from "@inquirer/prompts"; +import chalk from "chalk"; +import { installLaunchAgent } from "../../daemon/launchagent.js"; +import { + getConfigPath, + getDataDir, + loadConfig, + saveConfig, +} from "../../state/config.js"; +import type { Config, SyncType } from "../../types/index.js"; +import { ALL_SYNC_TYPES } from "../../types/index.js"; function maskToken(token: string): string { - if (token.length <= 8) return '****'; - return token.slice(0, 4) + '...' + token.slice(-4); + if (token.length <= 8) return "****"; + return `${token.slice(0, 4)}...${token.slice(-4)}`; } export async function initCommand(): Promise { - console.log(chalk.bold('\n🔧 github-things-sync setup\n')); - - // Check for existing config - const existing = loadConfig(); - if (existing) { - console.log(chalk.yellow('⚠️ Existing configuration found.\n')); - console.log('Current settings:'); - console.log(chalk.dim(' GitHub Token: ') + maskToken(existing.githubToken)); - console.log(chalk.dim(' Things Token: ') + maskToken(existing.thingsAuthToken)); - console.log(chalk.dim(' Project: ') + existing.thingsProject); - console.log(chalk.dim(' Poll Interval: ') + existing.pollInterval + 's'); - console.log(chalk.dim(' Sync Types: ') + (existing.syncTypes?.join(', ') || 'all')); - console.log(chalk.dim(' Autostart: ') + (existing.autoStart ? 'yes' : 'no') + '\n'); - - const overwrite = await confirm({ - message: 'Reconfigure?', - default: false, - }); - - if (!overwrite) { - console.log(chalk.green('\n✅ Keeping existing configuration.')); - console.log(chalk.dim('Use `github-things-sync config` to update individual settings.\n')); - return; - } - console.log(''); - } - - console.log('This wizard will configure the sync between GitHub and Things 3.\n'); - - // Step 1: GitHub Token - console.log(chalk.bold('Step 1: GitHub Personal Access Token')); - console.log(chalk.dim('─────────────────────────────────────')); - console.log(chalk.dim('Create a classic token at: ') + chalk.cyan('https://github.com/settings/tokens/new')); - console.log(chalk.dim('(Fine-grained tokens may not work with organization repos)')); - console.log(chalk.dim('Required scope: repo\n')); - - const githubToken = await password({ - message: 'GitHub Token', - mask: '*', - validate: (value) => { - if (!value && !existing?.githubToken) { - return 'GitHub token is required'; - } - return true; - }, - }) || existing?.githubToken; - - if (!githubToken) { - console.error(chalk.red('❌ GitHub token is required')); - process.exit(1); - } - - // Step 2: Things Auth Token - console.log(chalk.bold('\nStep 2: Things Auth Token')); - console.log(chalk.dim('─────────────────────────')); - console.log(chalk.dim('Find it in: Things → Settings → General → Things URLs → Manage\n')); - - const thingsAuthToken = await password({ - message: 'Things Auth Token', - mask: '*', - validate: (value) => { - if (!value && !existing?.thingsAuthToken) { - return 'Things auth token is required for auto-completing tasks'; - } - return true; - }, - }) || existing?.thingsAuthToken; - - if (!thingsAuthToken) { - console.error(chalk.red('❌ Things auth token is required')); - process.exit(1); - } - - // Step 3: Things Project - console.log(chalk.bold('\nStep 3: Things Project')); - console.log(chalk.dim('──────────────────────')); - console.log(chalk.dim('Tasks will be created in this project (must exist in Things)\n')); - - const thingsProject = await input({ - message: 'Project name', - default: existing?.thingsProject || 'GitHub', - validate: (value) => { - if (!value.trim()) { - return 'Project name is required'; - } - return true; - }, - }); - - // Step 4: Poll Interval - console.log(chalk.bold('\nStep 4: Poll Interval')); - console.log(chalk.dim('─────────────────────')); - console.log(chalk.dim('How often to check GitHub for updates (in seconds)\n')); - - const pollIntervalStr = await input({ - message: 'Interval (seconds)', - default: String(existing?.pollInterval || 300), - validate: (value) => { - const num = Number.parseInt(value, 10); - if (Number.isNaN(num) || num < 60) { - return 'Interval must be at least 60 seconds'; - } - return true; - }, - }); - const pollInterval = Number.parseInt(pollIntervalStr, 10); - - // Step 5: Sync Types - console.log(chalk.bold('\nStep 5: Sync Types')); - console.log(chalk.dim('──────────────────')); - console.log(chalk.dim('Which GitHub items should be synced to Things?\n')); - - const defaultSyncTypes = existing?.syncTypes || [...ALL_SYNC_TYPES]; - const syncTypes = await checkbox({ - message: 'Select sync types', - choices: [ - { name: 'PR Reviews - PRs where you are requested as reviewer', value: 'pr-reviews' as SyncType, checked: defaultSyncTypes.includes('pr-reviews') }, - { name: 'PRs Created - PRs you created', value: 'prs-created' as SyncType, checked: defaultSyncTypes.includes('prs-created') }, - { name: 'Issues Assigned - Issues assigned to you', value: 'issues-assigned' as SyncType, checked: defaultSyncTypes.includes('issues-assigned') }, - { name: 'Issues Created - Issues you created', value: 'issues-created' as SyncType, checked: defaultSyncTypes.includes('issues-created') }, - ], - validate: (value) => { - if (value.length === 0) { - return 'Select at least one sync type'; - } - return true; - }, - }); - - // Step 6: Auto-start - console.log(chalk.bold('\nStep 6: Auto-start')); - console.log(chalk.dim('──────────────────')); - console.log(chalk.dim('Start automatically when you log in to your Mac?\n')); - - const autoStart = await confirm({ - message: 'Enable auto-start?', - default: existing?.autoStart !== false, - }); - - // Save config - const config: Config = { - githubToken, - thingsProject, - thingsAuthToken, - pollInterval, - autoStart, - syncTypes, - }; - - saveConfig(config); - - console.log(chalk.green(`\n✅ Configuration saved to ${getConfigPath()}`)); - - // Install LaunchAgent if requested - if (autoStart) { - installLaunchAgent(); - console.log(chalk.green('✅ LaunchAgent installed (auto-start enabled)')); - } - - console.log(chalk.green('\n🎉 Setup complete!') + ' Run `github-things-sync start` to begin syncing.\n'); - console.log(chalk.dim('📁 Data directory: ') + getDataDir()); - console.log(chalk.dim('📋 Make sure the project "') + chalk.cyan(thingsProject) + chalk.dim('" exists in Things 3\n')); + console.log(chalk.bold("\n🔧 github-things-sync setup\n")); + + // Check for existing config + const existing = loadConfig(); + if (existing) { + console.log(chalk.yellow("⚠️ Existing configuration found.\n")); + console.log("Current settings:"); + console.log( + chalk.dim(" GitHub Token: ") + maskToken(existing.githubToken), + ); + console.log( + chalk.dim(" Things Token: ") + maskToken(existing.thingsAuthToken), + ); + console.log(chalk.dim(" Project: ") + existing.thingsProject); + console.log(`${chalk.dim(" Poll Interval: ") + existing.pollInterval}s`); + console.log( + chalk.dim(" Sync Types: ") + + (existing.syncTypes?.join(", ") || "all"), + ); + console.log( + chalk.dim(" Autostart: ") + + (existing.autoStart ? "yes" : "no") + + "\n", + ); + + const overwrite = await confirm({ + message: "Reconfigure?", + default: false, + }); + + if (!overwrite) { + console.log(chalk.green("\n✅ Keeping existing configuration.")); + console.log( + chalk.dim( + "Use `github-things-sync config` to update individual settings.\n", + ), + ); + return; + } + console.log(""); + } + + console.log( + "This wizard will configure the sync between GitHub and Things 3.\n", + ); + + // Step 1: GitHub Token + console.log(chalk.bold("Step 1: GitHub Personal Access Token")); + console.log(chalk.dim("─────────────────────────────────────")); + console.log( + chalk.dim("Create a classic token at: ") + + chalk.cyan("https://github.com/settings/tokens/new"), + ); + console.log( + chalk.dim("(Fine-grained tokens may not work with organization repos)"), + ); + console.log(chalk.dim("Required scope: repo\n")); + + const githubToken = + (await password({ + message: "GitHub Token", + mask: "*", + validate: (value) => { + if (!value && !existing?.githubToken) { + return "GitHub token is required"; + } + return true; + }, + })) || existing?.githubToken; + + if (!githubToken) { + console.error(chalk.red("❌ GitHub token is required")); + process.exit(1); + } + + // Step 2: Things Auth Token + console.log(chalk.bold("\nStep 2: Things Auth Token")); + console.log(chalk.dim("─────────────────────────")); + console.log( + chalk.dim( + "Find it in: Things → Settings → General → Things URLs → Manage\n", + ), + ); + + const thingsAuthToken = + (await password({ + message: "Things Auth Token", + mask: "*", + validate: (value) => { + if (!value && !existing?.thingsAuthToken) { + return "Things auth token is required for auto-completing tasks"; + } + return true; + }, + })) || existing?.thingsAuthToken; + + if (!thingsAuthToken) { + console.error(chalk.red("❌ Things auth token is required")); + process.exit(1); + } + + // Step 3: Things Project + console.log(chalk.bold("\nStep 3: Things Project")); + console.log(chalk.dim("──────────────────────")); + console.log( + chalk.dim("Tasks will be created in this project (must exist in Things)\n"), + ); + + const thingsProject = await input({ + message: "Project name", + default: existing?.thingsProject || "GitHub", + validate: (value) => { + if (!value.trim()) { + return "Project name is required"; + } + return true; + }, + }); + + // Step 4: Poll Interval + console.log(chalk.bold("\nStep 4: Poll Interval")); + console.log(chalk.dim("─────────────────────")); + console.log( + chalk.dim("How often to check GitHub for updates (in seconds)\n"), + ); + + const pollIntervalStr = await input({ + message: "Interval (seconds)", + default: String(existing?.pollInterval || 300), + validate: (value) => { + const num = Number.parseInt(value, 10); + if (Number.isNaN(num) || num < 60) { + return "Interval must be at least 60 seconds"; + } + return true; + }, + }); + const pollInterval = Number.parseInt(pollIntervalStr, 10); + + // Step 5: Sync Types + console.log(chalk.bold("\nStep 5: Sync Types")); + console.log(chalk.dim("──────────────────")); + console.log(chalk.dim("Which GitHub items should be synced to Things?\n")); + + const defaultSyncTypes = existing?.syncTypes || [...ALL_SYNC_TYPES]; + const syncTypes = await checkbox({ + message: "Select sync types", + choices: [ + { + name: "PR Reviews - PRs where you are requested as reviewer", + value: "pr-reviews" as SyncType, + checked: defaultSyncTypes.includes("pr-reviews"), + }, + { + name: "PRs Created - PRs you created", + value: "prs-created" as SyncType, + checked: defaultSyncTypes.includes("prs-created"), + }, + { + name: "Issues Assigned - Issues assigned to you", + value: "issues-assigned" as SyncType, + checked: defaultSyncTypes.includes("issues-assigned"), + }, + { + name: "Issues Created - Issues you created", + value: "issues-created" as SyncType, + checked: defaultSyncTypes.includes("issues-created"), + }, + ], + validate: (value) => { + if (value.length === 0) { + return "Select at least one sync type"; + } + return true; + }, + }); + + // Step 6: Auto-start + console.log(chalk.bold("\nStep 6: Auto-start")); + console.log(chalk.dim("──────────────────")); + console.log(chalk.dim("Start automatically when you log in to your Mac?\n")); + + const autoStart = await confirm({ + message: "Enable auto-start?", + default: existing?.autoStart !== false, + }); + + // Save config + const config: Config = { + githubToken, + thingsProject, + thingsAuthToken, + pollInterval, + autoStart, + syncTypes, + }; + + saveConfig(config); + + console.log(chalk.green(`\n✅ Configuration saved to ${getConfigPath()}`)); + + // Install LaunchAgent if requested + if (autoStart) { + installLaunchAgent(); + console.log(chalk.green("✅ LaunchAgent installed (auto-start enabled)")); + } + + console.log( + chalk.green("\n🎉 Setup complete!") + + " Run `github-things-sync start` to begin syncing.\n", + ); + console.log(chalk.dim("📁 Data directory: ") + getDataDir()); + console.log( + chalk.dim('📋 Make sure the project "') + + chalk.cyan(thingsProject) + + chalk.dim('" exists in Things 3\n'), + ); } diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index 0b15bba..f685539 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -2,65 +2,71 @@ * start command - Start the background daemon */ -import { spawn } from 'child_process'; -import * as fs from 'fs'; -import * as path from 'path'; -import chalk from 'chalk'; -import { getDataDir, loadConfig } from '../../state/config.js'; +import { spawn } from "node:child_process"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import chalk from "chalk"; +import { getDataDir, loadConfig } from "../../state/config.js"; export async function startCommand(): Promise { - const config = loadConfig(); - if (!config) { - console.error(chalk.red('❌ Not configured. Run `github-things-sync init` first.')); - process.exit(1); - } + const config = loadConfig(); + if (!config) { + console.error( + chalk.red("❌ Not configured. Run `github-things-sync init` first."), + ); + process.exit(1); + } - const pidFile = path.join(getDataDir(), 'daemon.pid'); - const logFile = path.join(getDataDir(), 'daemon.log'); + const pidFile = path.join(getDataDir(), "daemon.pid"); + const logFile = path.join(getDataDir(), "daemon.log"); - // Check if already running - if (fs.existsSync(pidFile)) { - const pid = Number.parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10); - try { - process.kill(pid, 0); // Check if process exists - console.log(chalk.yellow(`⚠️ Daemon already running (PID: ${pid})`)); - console.log(chalk.dim(` Use 'github-things-sync stop' to stop it first.`)); - return; - } catch { - // Process doesn't exist, clean up stale PID file - fs.unlinkSync(pidFile); - } - } + // Check if already running + if (fs.existsSync(pidFile)) { + const pid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); + try { + process.kill(pid, 0); // Check if process exists + console.log(chalk.yellow(`⚠️ Daemon already running (PID: ${pid})`)); + console.log( + chalk.dim(` Use 'github-things-sync stop' to stop it first.`), + ); + return; + } catch { + // Process doesn't exist, clean up stale PID file + fs.unlinkSync(pidFile); + } + } - // Find the daemon script path (.ts for dev, .js for production) - const scriptDir = path.dirname(new URL(import.meta.url).pathname); - const tsScript = path.join(scriptDir, '../../daemon/index.ts'); - const jsScript = path.join(scriptDir, '../../daemon/index.js'); + // Find the daemon script path (.ts for dev, .js for production) + const scriptDir = path.dirname(new URL(import.meta.url).pathname); + const tsScript = path.join(scriptDir, "../../daemon/index.ts"); + const jsScript = path.join(scriptDir, "../../daemon/index.js"); - // Use tsx for .ts files, node for compiled .js - const useTs = fs.existsSync(tsScript); - const daemonScript = useTs ? tsScript : jsScript; - const runtime = useTs ? 'tsx' : 'node'; + // Use tsx for .ts files, node for compiled .js + const useTs = fs.existsSync(tsScript); + const daemonScript = useTs ? tsScript : jsScript; + const runtime = useTs ? "tsx" : "node"; - // Start daemon as detached process - const out = fs.openSync(logFile, 'a'); - const err = fs.openSync(logFile, 'a'); + // Start daemon as detached process + const out = fs.openSync(logFile, "a"); + const err = fs.openSync(logFile, "a"); - const child = spawn(runtime, [daemonScript], { - detached: true, - stdio: ['ignore', out, err], - env: { ...process.env }, - }); + const child = spawn(runtime, [daemonScript], { + detached: true, + stdio: ["ignore", out, err], + env: { ...process.env }, + }); - if (child.pid) { - fs.writeFileSync(pidFile, child.pid.toString()); - child.unref(); + if (child.pid) { + fs.writeFileSync(pidFile, child.pid.toString()); + child.unref(); - console.log(chalk.green(`✅ Daemon started`) + chalk.dim(` (PID: ${child.pid})`)); - console.log(chalk.dim(` Polling every ${config.pollInterval} seconds`)); - console.log(chalk.dim(` Logs: ${logFile}`)); - } else { - console.error(chalk.red('❌ Failed to start daemon')); - process.exit(1); - } + console.log( + chalk.green(`✅ Daemon started`) + chalk.dim(` (PID: ${child.pid})`), + ); + console.log(chalk.dim(` Polling every ${config.pollInterval} seconds`)); + console.log(chalk.dim(` Logs: ${logFile}`)); + } else { + console.error(chalk.red("❌ Failed to start daemon")); + process.exit(1); + } } diff --git a/src/cli/commands/status.ts b/src/cli/commands/status.ts index 9db2e29..f4747e6 100644 --- a/src/cli/commands/status.ts +++ b/src/cli/commands/status.ts @@ -2,115 +2,127 @@ * status command - Show sync status and recent activity */ -import * as fs from 'fs'; -import * as path from 'path'; -import chalk from 'chalk'; -import { getDataDir, loadConfig } from '../../state/config.js'; -import { loadState } from '../../state/state.js'; +import * as fs from "node:fs"; +import * as path from "node:path"; +import chalk from "chalk"; +import { getDataDir, loadConfig } from "../../state/config.js"; +import { loadState } from "../../state/state.js"; export async function statusCommand(): Promise { - const config = loadConfig(); - if (!config) { - console.error(chalk.red('❌ Not configured. Run `github-things-sync init` first.')); - process.exit(1); - } - - const pidFile = path.join(getDataDir(), 'daemon.pid'); - const state = loadState(); - - // Daemon status - console.log(chalk.bold('\n📊 github-things-sync status\n')); - console.log(chalk.bold('Daemon')); - console.log(chalk.dim('──────')); - - let isRunning = false; - let pid: number | null = null; - - if (fs.existsSync(pidFile)) { - pid = Number.parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10); - try { - process.kill(pid, 0); - isRunning = true; - } catch { - isRunning = false; - } - } - - if (isRunning) { - console.log(chalk.dim('Status: ') + chalk.green('● Running') + chalk.dim(` (PID: ${pid})`)); - } else { - console.log(chalk.dim('Status: ') + chalk.red('○ Stopped')); - } - - console.log(chalk.dim('Interval: ') + `${config.pollInterval}s`); - console.log(chalk.dim('Project: ') + config.thingsProject); - - // Sync status - console.log(chalk.bold('\nSync')); - console.log(chalk.dim('────')); - - if (state.lastSync) { - const lastSync = new Date(state.lastSync); - const ago = Math.round((Date.now() - lastSync.getTime()) / 1000); - console.log(chalk.dim('Last sync: ') + formatTimeAgo(ago)); - } else { - console.log(chalk.dim('Last sync: ') + 'Never'); - } - - if (state.lastError) { - console.log(chalk.dim('Last error: ') + chalk.red(state.lastError)); - } - - // Task mappings - const mappings = Object.values(state.mappings); - console.log(chalk.bold(`\nTracked Tasks: `) + mappings.length); - - if (mappings.length > 0) { - console.log(chalk.dim('───────────────')); - - // Group by type - const byType = { - 'pr-review': mappings.filter((m) => m.type === 'pr-review'), - 'pr-created': mappings.filter((m) => m.type === 'pr-created'), - 'issue-assigned': mappings.filter((m) => m.type === 'issue-assigned'), - 'issue-created': mappings.filter((m) => m.type === 'issue-created'), - }; - - if (byType['pr-review'].length > 0) { - console.log(chalk.cyan(`\n🔍 PR Reviews (${byType['pr-review'].length})`)); - byType['pr-review'].slice(0, 5).forEach((m) => { - console.log(chalk.dim(' • ') + m.title); - }); - } - - if (byType['pr-created'].length > 0) { - console.log(chalk.cyan(`\n📝 Your PRs (${byType['pr-created'].length})`)); - byType['pr-created'].slice(0, 5).forEach((m) => { - console.log(chalk.dim(' • ') + m.title); - }); - } - - if (byType['issue-assigned'].length > 0) { - console.log(chalk.cyan(`\n📌 Assigned Issues (${byType['issue-assigned'].length})`)); - byType['issue-assigned'].slice(0, 5).forEach((m) => { - console.log(chalk.dim(' • ') + m.title); - }); - } - - if (byType['issue-created'].length > 0) { - console.log(chalk.cyan(`\n✏️ Your Issues (${byType['issue-created'].length})`)); - byType['issue-created'].slice(0, 5).forEach((m) => { - console.log(chalk.dim(' • ') + m.title); - }); - } - } - - console.log(''); + const config = loadConfig(); + if (!config) { + console.error( + chalk.red("❌ Not configured. Run `github-things-sync init` first."), + ); + process.exit(1); + } + + const pidFile = path.join(getDataDir(), "daemon.pid"); + const state = loadState(); + + // Daemon status + console.log(chalk.bold("\n📊 github-things-sync status\n")); + console.log(chalk.bold("Daemon")); + console.log(chalk.dim("──────")); + + let isRunning = false; + let pid: number | null = null; + + if (fs.existsSync(pidFile)) { + pid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); + try { + process.kill(pid, 0); + isRunning = true; + } catch { + isRunning = false; + } + } + + if (isRunning) { + console.log( + chalk.dim("Status: ") + + chalk.green("● Running") + + chalk.dim(` (PID: ${pid})`), + ); + } else { + console.log(chalk.dim("Status: ") + chalk.red("○ Stopped")); + } + + console.log(`${chalk.dim("Interval: ")}${config.pollInterval}s`); + console.log(chalk.dim("Project: ") + config.thingsProject); + + // Sync status + console.log(chalk.bold("\nSync")); + console.log(chalk.dim("────")); + + if (state.lastSync) { + const lastSync = new Date(state.lastSync); + const ago = Math.round((Date.now() - lastSync.getTime()) / 1000); + console.log(chalk.dim("Last sync: ") + formatTimeAgo(ago)); + } else { + console.log(`${chalk.dim("Last sync: ")}Never`); + } + + if (state.lastError) { + console.log(chalk.dim("Last error: ") + chalk.red(state.lastError)); + } + + // Task mappings + const mappings = Object.values(state.mappings); + console.log(chalk.bold(`\nTracked Tasks: `) + mappings.length); + + if (mappings.length > 0) { + console.log(chalk.dim("───────────────")); + + // Group by type + const byType = { + "pr-review": mappings.filter((m) => m.type === "pr-review"), + "pr-created": mappings.filter((m) => m.type === "pr-created"), + "issue-assigned": mappings.filter((m) => m.type === "issue-assigned"), + "issue-created": mappings.filter((m) => m.type === "issue-created"), + }; + + if (byType["pr-review"].length > 0) { + console.log( + chalk.cyan(`\n🔍 PR Reviews (${byType["pr-review"].length})`), + ); + byType["pr-review"].slice(0, 5).forEach((m) => { + console.log(chalk.dim(" • ") + m.title); + }); + } + + if (byType["pr-created"].length > 0) { + console.log(chalk.cyan(`\n📝 Your PRs (${byType["pr-created"].length})`)); + byType["pr-created"].slice(0, 5).forEach((m) => { + console.log(chalk.dim(" • ") + m.title); + }); + } + + if (byType["issue-assigned"].length > 0) { + console.log( + chalk.cyan(`\n📌 Assigned Issues (${byType["issue-assigned"].length})`), + ); + byType["issue-assigned"].slice(0, 5).forEach((m) => { + console.log(chalk.dim(" • ") + m.title); + }); + } + + if (byType["issue-created"].length > 0) { + console.log( + chalk.cyan(`\n✏️ Your Issues (${byType["issue-created"].length})`), + ); + byType["issue-created"].slice(0, 5).forEach((m) => { + console.log(chalk.dim(" • ") + m.title); + }); + } + } + + console.log(""); } function formatTimeAgo(seconds: number): string { - if (seconds < 60) return `${seconds}s ago`; - if (seconds < 3600) return `${Math.round(seconds / 60)}m ago`; - if (seconds < 86400) return `${Math.round(seconds / 3600)}h ago`; - return `${Math.round(seconds / 86400)}d ago`; + if (seconds < 60) return `${seconds}s ago`; + if (seconds < 3600) return `${Math.round(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.round(seconds / 3600)}h ago`; + return `${Math.round(seconds / 86400)}d ago`; } diff --git a/src/cli/commands/stop.ts b/src/cli/commands/stop.ts index d34e9f4..cb88924 100644 --- a/src/cli/commands/stop.ts +++ b/src/cli/commands/stop.ts @@ -2,33 +2,35 @@ * stop command - Stop the daemon */ -import * as fs from 'fs'; -import * as path from 'path'; -import chalk from 'chalk'; -import { getDataDir } from '../../state/config.js'; +import * as fs from "node:fs"; +import * as path from "node:path"; +import chalk from "chalk"; +import { getDataDir } from "../../state/config.js"; export async function stopCommand(): Promise { - const pidFile = path.join(getDataDir(), 'daemon.pid'); + const pidFile = path.join(getDataDir(), "daemon.pid"); - if (!fs.existsSync(pidFile)) { - console.log(chalk.cyan('ℹ️ Daemon is not running')); - return; - } + if (!fs.existsSync(pidFile)) { + console.log(chalk.cyan("ℹ️ Daemon is not running")); + return; + } - const pid = Number.parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10); + const pid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10); - try { - process.kill(pid, 'SIGTERM'); - fs.unlinkSync(pidFile); - console.log(chalk.green(`✅ Daemon stopped`) + chalk.dim(` (PID: ${pid})`)); - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'ESRCH') { - // Process doesn't exist, clean up - fs.unlinkSync(pidFile); - console.log(chalk.cyan('ℹ️ Daemon was not running (cleaned up stale PID file)')); - } else { - console.error(chalk.red(`❌ Failed to stop daemon: ${error}`)); - process.exit(1); - } - } + try { + process.kill(pid, "SIGTERM"); + fs.unlinkSync(pidFile); + console.log(chalk.green(`✅ Daemon stopped`) + chalk.dim(` (PID: ${pid})`)); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === "ESRCH") { + // Process doesn't exist, clean up + fs.unlinkSync(pidFile); + console.log( + chalk.cyan("ℹ️ Daemon was not running (cleaned up stale PID file)"), + ); + } else { + console.error(chalk.red(`❌ Failed to stop daemon: ${error}`)); + process.exit(1); + } + } } diff --git a/src/cli/commands/sync.ts b/src/cli/commands/sync.ts index f384104..704adbf 100644 --- a/src/cli/commands/sync.ts +++ b/src/cli/commands/sync.ts @@ -2,37 +2,43 @@ * sync command - Run a single sync (no daemon) */ -import chalk from 'chalk'; -import { loadConfig } from '../../state/config.js'; -import { runSync } from '../../daemon/sync.js'; +import chalk from "chalk"; +import { runSync } from "../../daemon/sync.js"; +import { loadConfig } from "../../state/config.js"; interface SyncOptions { - verbose?: boolean; + verbose?: boolean; } export async function syncCommand(options: SyncOptions): Promise { - const config = loadConfig(); - if (!config) { - console.error(chalk.red('❌ Not configured. Run `github-things-sync init` first.')); - process.exit(1); - } + const config = loadConfig(); + if (!config) { + console.error( + chalk.red("❌ Not configured. Run `github-things-sync init` first."), + ); + process.exit(1); + } - console.log(chalk.cyan('🔄 Syncing...\n')); + console.log(chalk.cyan("🔄 Syncing...\n")); - try { - const result = await runSync(config, options.verbose ?? false); + try { + const result = await runSync(config, options.verbose ?? false); - console.log(chalk.green('\n✅ Sync complete')); - console.log(chalk.dim(' Created: ') + `${result.created} tasks`); - console.log(chalk.dim(' Completed: ') + `${result.completed} tasks`); - console.log(chalk.dim(' Unchanged: ') + `${result.unchanged} tasks`); + console.log(chalk.green("\n✅ Sync complete")); + console.log(`${chalk.dim(" Created: ")}${result.created} tasks`); + console.log(`${chalk.dim(" Completed: ")}${result.completed} tasks`); + console.log(`${chalk.dim(" Unchanged: ")}${result.unchanged} tasks`); - if (result.errors.length > 0) { - console.log(chalk.dim(' Errors: ') + chalk.yellow(`${result.errors.length}`)); - result.errors.forEach((err) => console.log(chalk.dim(' • ') + chalk.red(err))); - } - } catch (error) { - console.error(chalk.red(`❌ Sync failed: ${error}`)); - process.exit(1); - } + if (result.errors.length > 0) { + console.log( + chalk.dim(" Errors: ") + chalk.yellow(`${result.errors.length}`), + ); + for (const err of result.errors) { + console.log(`${chalk.dim(" • ")}${chalk.red(err)}`); + } + } + } catch (error) { + console.error(chalk.red(`❌ Sync failed: ${error}`)); + process.exit(1); + } } diff --git a/src/cli/index.ts b/src/cli/index.ts index 4db3d9a..9d8dbf6 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,28 +1,29 @@ #!/usr/bin/env node + /** * CLI entry point for github-things-sync */ -import * as fs from 'fs'; -import { Command } from 'commander'; -import chalk from 'chalk'; -import updateNotifier from 'update-notifier'; -import { initCommand } from './commands/init.js'; -import { startCommand } from './commands/start.js'; -import { stopCommand } from './commands/stop.js'; -import { statusCommand } from './commands/status.js'; -import { syncCommand } from './commands/sync.js'; -import { configCommand } from './commands/config.js'; +import * as fs from "node:fs"; +import chalk from "chalk"; +import { Command } from "commander"; +import updateNotifier from "update-notifier"; +import { configCommand } from "./commands/config.js"; +import { initCommand } from "./commands/init.js"; +import { startCommand } from "./commands/start.js"; +import { statusCommand } from "./commands/status.js"; +import { stopCommand } from "./commands/stop.js"; +import { syncCommand } from "./commands/sync.js"; // Load package.json for version info // In dev: src/cli/index.ts -> ../../package.json // In prod: dist/index.js -> ../package.json -const pkgPath = new URL('../package.json', import.meta.url); -const pkgPathAlt = new URL('../../package.json', import.meta.url); +const pkgPath = new URL("../package.json", import.meta.url); +const pkgPathAlt = new URL("../../package.json", import.meta.url); const pkg = JSON.parse( - fs.existsSync(pkgPath) - ? fs.readFileSync(pkgPath, 'utf-8') - : fs.readFileSync(pkgPathAlt, 'utf-8') + fs.existsSync(pkgPath) + ? fs.readFileSync(pkgPath, "utf-8") + : fs.readFileSync(pkgPathAlt, "utf-8"), ); const updateCheckInterval = 1000 * 60 * 60; // 1 hour @@ -32,94 +33,98 @@ const notifier = updateNotifier({ pkg, updateCheckInterval }); // Validate cached update against current version (user may have upgraded) if (notifier.update) { - notifier.update.current = pkg.version; - // Clear if no longer outdated - if (notifier.update.current === notifier.update.latest) { - notifier.update = undefined; - } + notifier.update.current = pkg.version; + // Clear if no longer outdated + if (notifier.update.current === notifier.update.latest) { + notifier.update = undefined; + } } // Detect first run: lastUpdateCheck was just set by constructor (within last second) -const lastCheck = (notifier.config?.get('lastUpdateCheck') as number) ?? 0; +const lastCheck = (notifier.config?.get("lastUpdateCheck") as number) ?? 0; const isFirstRun = Date.now() - lastCheck < 1000; const intervalPassed = Date.now() - lastCheck >= updateCheckInterval; // Fetch immediately if no cached update and (first run OR interval passed) // This fixes the 24h delay issue where check() skips spawning on first run if (!notifier.update && (isFirstRun || intervalPassed)) { - try { - const update = await notifier.fetchInfo(); - notifier.config?.set('lastUpdateCheck', Date.now()); - if (update && update.type !== 'latest') { - notifier.update = update; - } - } catch { - // Ignore network errors - } + try { + const update = await notifier.fetchInfo(); + notifier.config?.set("lastUpdateCheck", Date.now()); + if (update && update.type !== "latest") { + notifier.update = update; + } + } catch { + // Ignore network errors + } } // Re-cache update for next run (check() deleted it when reading) // Only cache if there's actually an update available if (notifier.update && notifier.update.current !== notifier.update.latest) { - notifier.config?.set('update', notifier.update); + notifier.config?.set("update", notifier.update); } else { - notifier.config?.delete('update'); + notifier.config?.delete("update"); } // Show notification on exit (bypasses TTY check that blocks notify()) -process.on('exit', () => { - if (notifier.update && notifier.update.current !== notifier.update.latest) { - console.error( - chalk.yellow(`\n Update available: ${notifier.update.current} → ${notifier.update.latest}`) + - chalk.dim(`\n Run: npm i -g ${pkg.name}\n`) - ); - } +process.on("exit", () => { + if (notifier.update && notifier.update.current !== notifier.update.latest) { + console.error( + chalk.yellow( + `\n Update available: ${notifier.update.current} → ${notifier.update.latest}`, + ) + chalk.dim(`\n Run: npm i -g ${pkg.name}\n`), + ); + } }); const program = new Command(); program - .name('github-things-sync') - .description('Sync GitHub PRs and Issues to Things 3 on macOS') - .version(pkg.version); + .name("github-things-sync") + .description("Sync GitHub PRs and Issues to Things 3 on macOS") + .version(pkg.version); program - .command('init') - .description('Interactive setup wizard') - .action(initCommand); + .command("init") + .description("Interactive setup wizard") + .action(initCommand); program - .command('start') - .description('Start the background daemon') - .action(startCommand); + .command("start") + .description("Start the background daemon") + .action(startCommand); -program - .command('stop') - .description('Stop the daemon') - .action(stopCommand); +program.command("stop").description("Stop the daemon").action(stopCommand); program - .command('status') - .description('Show sync status and recent activity') - .action(statusCommand); + .command("status") + .description("Show sync status and recent activity") + .action(statusCommand); program - .command('sync') - .description('Run a single sync (no daemon)') - .option('-v, --verbose', 'Show detailed output') - .action(syncCommand); + .command("sync") + .description("Run a single sync (no daemon)") + .option("-v, --verbose", "Show detailed output") + .action(syncCommand); program - .command('config') - .description('View and update settings') - .option('--show', 'Show current config (default)') - .option('--interval ', 'Set poll interval (min: 60)') - .option('--autostart ', 'Enable/disable autostart (true/false)') - .option('--project ', 'Set Things project name') - .option('--github-token ', 'Update GitHub token (use "prompt" for interactive)') - .option('--things-token ', 'Update Things token (use "prompt" for interactive)') - .option('--sync-types ', 'Set sync types (comma-separated or "all")') - .option('--verify', 'Verify tokens work') - .action(configCommand); + .command("config") + .description("View and update settings") + .option("--show", "Show current config (default)") + .option("--interval ", "Set poll interval (min: 60)") + .option("--autostart ", "Enable/disable autostart (true/false)") + .option("--project ", "Set Things project name") + .option( + "--github-token ", + 'Update GitHub token (use "prompt" for interactive)', + ) + .option( + "--things-token ", + 'Update Things token (use "prompt" for interactive)', + ) + .option("--sync-types ", 'Set sync types (comma-separated or "all")') + .option("--verify", "Verify tokens work") + .action(configCommand); program.parse(); diff --git a/src/daemon/index.ts b/src/daemon/index.ts index ccba76c..9c3d933 100644 --- a/src/daemon/index.ts +++ b/src/daemon/index.ts @@ -2,70 +2,70 @@ * Daemon entry point - runs the sync loop */ -import { loadConfig } from '../state/config.js'; -import { runSync } from './sync.js'; -import { setLastError } from '../state/state.js'; +import { loadConfig } from "../state/config.js"; +import { setLastError } from "../state/state.js"; +import { runSync } from "./sync.js"; async function main() { - const config = loadConfig(); - if (!config) { - console.error('Not configured. Run `github-things-sync init` first.'); - process.exit(1); - } + const config = loadConfig(); + if (!config) { + console.error("Not configured. Run `github-things-sync init` first."); + process.exit(1); + } - console.log(`[${timestamp()}] Daemon started`); - console.log(`[${timestamp()}] Poll interval: ${config.pollInterval}s`); - console.log(`[${timestamp()}] Things project: ${config.thingsProject}`); + console.log(`[${timestamp()}] Daemon started`); + console.log(`[${timestamp()}] Poll interval: ${config.pollInterval}s`); + console.log(`[${timestamp()}] Things project: ${config.thingsProject}`); - // Handle shutdown gracefully - process.on('SIGTERM', () => { - console.log(`[${timestamp()}] Received SIGTERM, shutting down`); - process.exit(0); - }); + // Handle shutdown gracefully + process.on("SIGTERM", () => { + console.log(`[${timestamp()}] Received SIGTERM, shutting down`); + process.exit(0); + }); - process.on('SIGINT', () => { - console.log(`[${timestamp()}] Received SIGINT, shutting down`); - process.exit(0); - }); + process.on("SIGINT", () => { + console.log(`[${timestamp()}] Received SIGINT, shutting down`); + process.exit(0); + }); - // Initial sync - await doSync(config); + // Initial sync + await doSync(config); - // Start poll loop - setInterval(() => doSync(config), config.pollInterval * 1000); + // Start poll loop + setInterval(() => doSync(config), config.pollInterval * 1000); } async function doSync(config: ReturnType) { - if (!config) return; + if (!config) return; - console.log(`[${timestamp()}] Starting sync...`); + console.log(`[${timestamp()}] Starting sync...`); - try { - const result = await runSync(config, false); - console.log( - `[${timestamp()}] Sync complete: ` + - `+${result.created} created, ` + - `✓${result.completed} completed, ` + - `=${result.unchanged} unchanged` - ); + try { + const result = await runSync(config, false); + console.log( + `[${timestamp()}] Sync complete: ` + + `+${result.created} created, ` + + `✓${result.completed} completed, ` + + `=${result.unchanged} unchanged`, + ); - if (result.errors.length > 0) { - result.errors.forEach((err) => { - console.error(`[${timestamp()}] Error: ${err}`); - }); - } - } catch (error) { - const msg = `Sync failed: ${error}`; - console.error(`[${timestamp()}] ${msg}`); - setLastError(msg); - } + if (result.errors.length > 0) { + result.errors.forEach((err) => { + console.error(`[${timestamp()}] Error: ${err}`); + }); + } + } catch (error) { + const msg = `Sync failed: ${error}`; + console.error(`[${timestamp()}] ${msg}`); + setLastError(msg); + } } function timestamp(): string { - return new Date().toISOString().replace('T', ' ').slice(0, 19); + return new Date().toISOString().replace("T", " ").slice(0, 19); } main().catch((error) => { - console.error(`[${timestamp()}] Fatal error: ${error}`); - process.exit(1); + console.error(`[${timestamp()}] Fatal error: ${error}`); + process.exit(1); }); diff --git a/src/daemon/launchagent.ts b/src/daemon/launchagent.ts index 5469350..60efe3a 100644 --- a/src/daemon/launchagent.ts +++ b/src/daemon/launchagent.ts @@ -2,35 +2,37 @@ * LaunchAgent management for macOS autostart */ -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import { execSync } from 'child_process'; +import { execSync } from "node:child_process"; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; -const PLIST_NAME = 'com.github-things-sync.plist'; -const LAUNCH_AGENTS_DIR = path.join(os.homedir(), 'Library', 'LaunchAgents'); +const PLIST_NAME = "com.github-things-sync.plist"; +const LAUNCH_AGENTS_DIR = path.join(os.homedir(), "Library", "LaunchAgents"); const PLIST_PATH = path.join(LAUNCH_AGENTS_DIR, PLIST_NAME); export function installLaunchAgent(): void { - // Ensure LaunchAgents directory exists - if (!fs.existsSync(LAUNCH_AGENTS_DIR)) { - fs.mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true }); - } - - // Find the installed binary path - // This assumes global npm install puts it in a standard location - let binPath: string; - try { - binPath = execSync('which github-things-sync', { encoding: 'utf-8' }).trim(); - } catch { - // Fallback: assume it's in npm global bin - const npmPrefix = execSync('npm prefix -g', { encoding: 'utf-8' }).trim(); - binPath = path.join(npmPrefix, 'bin', 'github-things-sync'); - } - - const logPath = path.join(os.homedir(), '.github-things-sync', 'daemon.log'); - - const plist = ` + // Ensure LaunchAgents directory exists + if (!fs.existsSync(LAUNCH_AGENTS_DIR)) { + fs.mkdirSync(LAUNCH_AGENTS_DIR, { recursive: true }); + } + + // Find the installed binary path + // This assumes global npm install puts it in a standard location + let binPath: string; + try { + binPath = execSync("which github-things-sync", { + encoding: "utf-8", + }).trim(); + } catch { + // Fallback: assume it's in npm global bin + const npmPrefix = execSync("npm prefix -g", { encoding: "utf-8" }).trim(); + binPath = path.join(npmPrefix, "bin", "github-things-sync"); + } + + const logPath = path.join(os.homedir(), ".github-things-sync", "daemon.log"); + + const plist = ` @@ -64,32 +66,32 @@ export function installLaunchAgent(): void { `; - fs.writeFileSync(PLIST_PATH, plist); + fs.writeFileSync(PLIST_PATH, plist); - // Load the agent - try { - execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`); - execSync(`launchctl load "${PLIST_PATH}"`); - } catch (error) { - console.warn(`Warning: Could not load LaunchAgent: ${error}`); - console.warn('You may need to manually load it or restart your Mac.'); - } + // Load the agent + try { + execSync(`launchctl unload "${PLIST_PATH}" 2>/dev/null || true`); + execSync(`launchctl load "${PLIST_PATH}"`); + } catch (error) { + console.warn(`Warning: Could not load LaunchAgent: ${error}`); + console.warn("You may need to manually load it or restart your Mac."); + } } export function uninstallLaunchAgent(): void { - if (!fs.existsSync(PLIST_PATH)) { - return; - } + if (!fs.existsSync(PLIST_PATH)) { + return; + } - try { - execSync(`launchctl unload "${PLIST_PATH}"`); - } catch { - // Ignore if not loaded - } + try { + execSync(`launchctl unload "${PLIST_PATH}"`); + } catch { + // Ignore if not loaded + } - fs.unlinkSync(PLIST_PATH); + fs.unlinkSync(PLIST_PATH); } export function isLaunchAgentInstalled(): boolean { - return fs.existsSync(PLIST_PATH); + return fs.existsSync(PLIST_PATH); } diff --git a/src/daemon/sync.ts b/src/daemon/sync.ts index 1e45103..b21aca5 100644 --- a/src/daemon/sync.ts +++ b/src/daemon/sync.ts @@ -2,114 +2,112 @@ * Core sync logic */ -import { GitHubClient } from '../github/client.js'; -import { ThingsClient } from '../things/client.js'; +import { GitHubClient } from "../github/client.js"; import { - loadState, - saveState, - hasMapping, - addMapping, - removeMapping, - getMapping, - updateLastSync, - setLastError, -} from '../state/state.js'; -import type { Config, GitHubItem, TaskMapping } from '../types/index.js'; + addMapping, + hasMapping, + loadState, + removeMapping, + setLastError, + updateLastSync, +} from "../state/state.js"; +import { ThingsClient } from "../things/client.js"; +import type { Config, GitHubItem, TaskMapping } from "../types/index.js"; export interface SyncResult { - created: number; - completed: number; - unchanged: number; - errors: string[]; + created: number; + completed: number; + unchanged: number; + errors: string[]; } export async function runSync( - config: Config, - verbose: boolean = false + config: Config, + verbose: boolean = false, ): Promise { - const result: SyncResult = { - created: 0, - completed: 0, - unchanged: 0, - errors: [], - }; - - const github = new GitHubClient(config.githubToken, config.syncTypes); - const things = new ThingsClient(config.thingsProject, config.thingsAuthToken); - - try { - // Step 1: Fetch all open items from GitHub - if (verbose) console.log('Fetching items from GitHub...'); - const githubItems = await github.fetchAllItems(); - if (verbose) console.log(`Found ${githubItems.length} open items`); - - // Step 2: Create tasks for new items - for (const item of githubItems) { - const githubId = makeGithubId(item); - - if (hasMapping(githubId)) { - result.unchanged++; - if (verbose) console.log(` ⏭️ Already tracked: ${item.title}`); - continue; - } - - try { - if (verbose) console.log(` ➕ Creating task: ${item.title}`); - const thingsId = await things.createTask(item); - - const mapping: TaskMapping = { - githubId, - thingsId, - type: item.type, - title: item.title, - url: item.url, - createdAt: new Date().toISOString(), - }; - - addMapping(mapping); - result.created++; - } catch (error) { - const msg = `Failed to create task for ${item.title}: ${error}`; - result.errors.push(msg); - if (verbose) console.log(` ❌ ${msg}`); - } - } - - // Step 3: Complete tasks for closed items - const state = loadState(); - const openGithubIds = new Set(githubItems.map(makeGithubId)); - - for (const [githubId, mapping] of Object.entries(state.mappings)) { - // Skip if item is still open - if (openGithubIds.has(githubId)) { - continue; - } - - // Item is no longer in open list - it's been closed/merged - try { - if (verbose) console.log(` ✅ Completing task: ${mapping.title}`); - await things.completeTask(mapping.thingsId); - removeMapping(githubId); - result.completed++; - } catch (error) { - const msg = `Failed to complete task ${mapping.title}: ${error}`; - result.errors.push(msg); - if (verbose) console.log(` ❌ ${msg}`); - } - } - - updateLastSync(); - } catch (error) { - const msg = `Sync failed: ${error}`; - result.errors.push(msg); - setLastError(msg); - throw error; - } - - return result; + const result: SyncResult = { + created: 0, + completed: 0, + unchanged: 0, + errors: [], + }; + + const github = new GitHubClient(config.githubToken, config.syncTypes); + const things = new ThingsClient(config.thingsProject, config.thingsAuthToken); + + try { + // Step 1: Fetch all open items from GitHub + if (verbose) console.log("Fetching items from GitHub..."); + const githubItems = await github.fetchAllItems(); + if (verbose) console.log(`Found ${githubItems.length} open items`); + + // Step 2: Create tasks for new items + for (const item of githubItems) { + const githubId = makeGithubId(item); + + if (hasMapping(githubId)) { + result.unchanged++; + if (verbose) console.log(` ⏭️ Already tracked: ${item.title}`); + continue; + } + + try { + if (verbose) console.log(` ➕ Creating task: ${item.title}`); + const thingsId = await things.createTask(item); + + const mapping: TaskMapping = { + githubId, + thingsId, + type: item.type, + title: item.title, + url: item.url, + createdAt: new Date().toISOString(), + }; + + addMapping(mapping); + result.created++; + } catch (error) { + const msg = `Failed to create task for ${item.title}: ${error}`; + result.errors.push(msg); + if (verbose) console.log(` ❌ ${msg}`); + } + } + + // Step 3: Complete tasks for closed items + const state = loadState(); + const openGithubIds = new Set(githubItems.map(makeGithubId)); + + for (const [githubId, mapping] of Object.entries(state.mappings)) { + // Skip if item is still open + if (openGithubIds.has(githubId)) { + continue; + } + + // Item is no longer in open list - it's been closed/merged + try { + if (verbose) console.log(` ✅ Completing task: ${mapping.title}`); + await things.completeTask(mapping.thingsId); + removeMapping(githubId); + result.completed++; + } catch (error) { + const msg = `Failed to complete task ${mapping.title}: ${error}`; + result.errors.push(msg); + if (verbose) console.log(` ❌ ${msg}`); + } + } + + updateLastSync(); + } catch (error) { + const msg = `Sync failed: ${error}`; + result.errors.push(msg); + setLastError(msg); + throw error; + } + + return result; } function makeGithubId(item: GitHubItem): string { - const prefix = item.type.startsWith('pr-') ? 'pr' : 'issue'; - return `${prefix}:${item.id}`; + const prefix = item.type.startsWith("pr-") ? "pr" : "issue"; + return `${prefix}:${item.id}`; } diff --git a/src/github/client.ts b/src/github/client.ts index 842e8b0..77f03a6 100644 --- a/src/github/client.ts +++ b/src/github/client.ts @@ -2,170 +2,177 @@ * GitHub API client */ -import { Octokit } from '@octokit/rest'; -import type { GitHubItem, GitHubItemType, SyncType } from '../types/index.js'; +import { Octokit } from "@octokit/rest"; +import type { GitHubItem, GitHubItemType, SyncType } from "../types/index.js"; export class GitHubClient { - private octokit: Octokit; - private username: string | null = null; - private syncTypes: SyncType[]; - - constructor(token: string, syncTypes?: SyncType[]) { - this.octokit = new Octokit({ auth: token }); - this.syncTypes = syncTypes || ['pr-reviews', 'prs-created', 'issues-assigned', 'issues-created']; - } - - async getUsername(): Promise { - if (this.username) return this.username; - - const { data } = await this.octokit.users.getAuthenticated(); - this.username = data.login; - return this.username; - } - - /** - * Fetch all items we care about from GitHub - */ - async fetchAllItems(): Promise { - const username = await this.getUsername(); - const items: GitHubItem[] = []; - - // Build list of fetch promises based on enabled sync types - const fetches: Promise[] = []; - - if (this.syncTypes.includes('pr-reviews')) { - fetches.push(this.fetchPRReviewRequests(username)); - } - if (this.syncTypes.includes('prs-created')) { - fetches.push(this.fetchPRsCreated(username)); - } - if (this.syncTypes.includes('issues-assigned')) { - fetches.push(this.fetchIssuesAssigned(username)); - } - if (this.syncTypes.includes('issues-created')) { - fetches.push(this.fetchIssuesCreated(username)); - } - - // Fetch in parallel - const results = await Promise.all(fetches); - for (const result of results) { - items.push(...result); - } - - return items; - } - - /** - * PRs where you're requested as reviewer - */ - private async fetchPRReviewRequests(username: string): Promise { - const { data } = await this.octokit.search.issuesAndPullRequests({ - q: `is:pr is:open review-requested:${username}`, - per_page: 100, - }); - - return data.items.map((item) => this.mapToGitHubItem(item, 'pr-review')); - } - - /** - * PRs you created - */ - private async fetchPRsCreated(username: string): Promise { - const { data } = await this.octokit.search.issuesAndPullRequests({ - q: `is:pr is:open author:${username}`, - per_page: 100, - }); - - return data.items.map((item) => this.mapToGitHubItem(item, 'pr-created')); - } - - /** - * Issues assigned to you - */ - private async fetchIssuesAssigned(username: string): Promise { - const { data } = await this.octokit.search.issuesAndPullRequests({ - q: `is:issue is:open assignee:${username}`, - per_page: 100, - }); - - return data.items.map((item) => - this.mapToGitHubItem(item, 'issue-assigned') - ); - } - - /** - * Issues you created - */ - private async fetchIssuesCreated(username: string): Promise { - const { data } = await this.octokit.search.issuesAndPullRequests({ - q: `is:issue is:open author:${username}`, - per_page: 100, - }); - - return data.items.map((item) => this.mapToGitHubItem(item, 'issue-created')); - } - - /** - * Check if an item is still open - */ - async isItemOpen(item: GitHubItem): Promise { - // Extract owner/repo from URL - // URL format: https://github.com/owner/repo/issues/123 or .../pull/123 - const match = item.url.match(/github\.com\/([^/]+)\/([^/]+)/); - if (!match) return false; - - const [, owner, repo] = match; - - try { - if (item.type.startsWith('pr-')) { - const { data } = await this.octokit.pulls.get({ - owner, - repo, - pull_number: item.number, - }); - return data.state === 'open'; - } else { - const { data } = await this.octokit.issues.get({ - owner, - repo, - issue_number: item.number, - }); - return data.state === 'open'; - } - } catch { - // If we can't fetch, assume it's closed - return false; - } - } - - private mapToGitHubItem( - item: { - id: number; - title: string; - html_url: string; - repository_url: string; - number: number; - state?: string; - created_at: string; - updated_at: string; - pull_request?: unknown; - }, - type: GitHubItemType - ): GitHubItem { - // Extract repo name from repository_url - const repoMatch = item.repository_url.match(/repos\/(.+)$/); - const repo = repoMatch ? repoMatch[1] : 'unknown'; - - return { - id: item.id, - type, - title: item.title, - url: item.html_url, - repo, - number: item.number, - state: (item.state as 'open' | 'closed') ?? 'open', - createdAt: item.created_at, - updatedAt: item.updated_at, - }; - } + private octokit: Octokit; + private username: string | null = null; + private syncTypes: SyncType[]; + + constructor(token: string, syncTypes?: SyncType[]) { + this.octokit = new Octokit({ auth: token }); + this.syncTypes = syncTypes || [ + "pr-reviews", + "prs-created", + "issues-assigned", + "issues-created", + ]; + } + + async getUsername(): Promise { + if (this.username) return this.username; + + const { data } = await this.octokit.users.getAuthenticated(); + this.username = data.login; + return this.username; + } + + /** + * Fetch all items we care about from GitHub + */ + async fetchAllItems(): Promise { + const username = await this.getUsername(); + const items: GitHubItem[] = []; + + // Build list of fetch promises based on enabled sync types + const fetches: Promise[] = []; + + if (this.syncTypes.includes("pr-reviews")) { + fetches.push(this.fetchPRReviewRequests(username)); + } + if (this.syncTypes.includes("prs-created")) { + fetches.push(this.fetchPRsCreated(username)); + } + if (this.syncTypes.includes("issues-assigned")) { + fetches.push(this.fetchIssuesAssigned(username)); + } + if (this.syncTypes.includes("issues-created")) { + fetches.push(this.fetchIssuesCreated(username)); + } + + // Fetch in parallel + const results = await Promise.all(fetches); + for (const result of results) { + items.push(...result); + } + + return items; + } + + /** + * PRs where you're requested as reviewer + */ + private async fetchPRReviewRequests(username: string): Promise { + const { data } = await this.octokit.search.issuesAndPullRequests({ + q: `is:pr is:open review-requested:${username}`, + per_page: 100, + }); + + return data.items.map((item) => this.mapToGitHubItem(item, "pr-review")); + } + + /** + * PRs you created + */ + private async fetchPRsCreated(username: string): Promise { + const { data } = await this.octokit.search.issuesAndPullRequests({ + q: `is:pr is:open author:${username}`, + per_page: 100, + }); + + return data.items.map((item) => this.mapToGitHubItem(item, "pr-created")); + } + + /** + * Issues assigned to you + */ + private async fetchIssuesAssigned(username: string): Promise { + const { data } = await this.octokit.search.issuesAndPullRequests({ + q: `is:issue is:open assignee:${username}`, + per_page: 100, + }); + + return data.items.map((item) => + this.mapToGitHubItem(item, "issue-assigned"), + ); + } + + /** + * Issues you created + */ + private async fetchIssuesCreated(username: string): Promise { + const { data } = await this.octokit.search.issuesAndPullRequests({ + q: `is:issue is:open author:${username}`, + per_page: 100, + }); + + return data.items.map((item) => + this.mapToGitHubItem(item, "issue-created"), + ); + } + + /** + * Check if an item is still open + */ + async isItemOpen(item: GitHubItem): Promise { + // Extract owner/repo from URL + // URL format: https://github.com/owner/repo/issues/123 or .../pull/123 + const match = item.url.match(/github\.com\/([^/]+)\/([^/]+)/); + if (!match) return false; + + const [, owner, repo] = match; + + try { + if (item.type.startsWith("pr-")) { + const { data } = await this.octokit.pulls.get({ + owner, + repo, + pull_number: item.number, + }); + return data.state === "open"; + } else { + const { data } = await this.octokit.issues.get({ + owner, + repo, + issue_number: item.number, + }); + return data.state === "open"; + } + } catch { + // If we can't fetch, assume it's closed + return false; + } + } + + private mapToGitHubItem( + item: { + id: number; + title: string; + html_url: string; + repository_url: string; + number: number; + state?: string; + created_at: string; + updated_at: string; + pull_request?: unknown; + }, + type: GitHubItemType, + ): GitHubItem { + // Extract repo name from repository_url + const repoMatch = item.repository_url.match(/repos\/(.+)$/); + const repo = repoMatch ? repoMatch[1] : "unknown"; + + return { + id: item.id, + type, + title: item.title, + url: item.html_url, + repo, + number: item.number, + state: (item.state as "open" | "closed") ?? "open", + createdAt: item.created_at, + updatedAt: item.updated_at, + }; + } } diff --git a/src/state/config.ts b/src/state/config.ts index 7bf3f3f..b6fbbeb 100644 --- a/src/state/config.ts +++ b/src/state/config.ts @@ -2,52 +2,52 @@ * Configuration management */ -import * as fs from 'fs'; -import * as path from 'path'; -import * as os from 'os'; -import type { Config } from '../types/index.js'; -import { ALL_SYNC_TYPES } from '../types/index.js'; +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; +import type { Config } from "../types/index.js"; +import { ALL_SYNC_TYPES } from "../types/index.js"; -const DATA_DIR = path.join(os.homedir(), '.github-things-sync'); -const CONFIG_FILE = path.join(DATA_DIR, 'config.json'); +const DATA_DIR = path.join(os.homedir(), ".github-things-sync"); +const CONFIG_FILE = path.join(DATA_DIR, "config.json"); export function getDataDir(): string { - return DATA_DIR; + return DATA_DIR; } export function getConfigPath(): string { - return CONFIG_FILE; + return CONFIG_FILE; } export function ensureDataDir(): void { - if (!fs.existsSync(DATA_DIR)) { - fs.mkdirSync(DATA_DIR, { recursive: true }); - } + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } } export function loadConfig(): Config | null { - if (!fs.existsSync(CONFIG_FILE)) { - return null; - } - - try { - const content = fs.readFileSync(CONFIG_FILE, 'utf-8'); - const config = JSON.parse(content) as Config; - - // Migration: add syncTypes if missing (for existing configs) - if (!config.syncTypes) { - config.syncTypes = [...ALL_SYNC_TYPES]; - } - - return config; - } catch { - return null; - } + if (!fs.existsSync(CONFIG_FILE)) { + return null; + } + + try { + const content = fs.readFileSync(CONFIG_FILE, "utf-8"); + const config = JSON.parse(content) as Config; + + // Migration: add syncTypes if missing (for existing configs) + if (!config.syncTypes) { + config.syncTypes = [...ALL_SYNC_TYPES]; + } + + return config; + } catch { + return null; + } } export function saveConfig(config: Config): void { - ensureDataDir(); - fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); - // Restrict permissions since it contains tokens - fs.chmodSync(CONFIG_FILE, 0o600); + ensureDataDir(); + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); + // Restrict permissions since it contains tokens + fs.chmodSync(CONFIG_FILE, 0o600); } diff --git a/src/state/state.ts b/src/state/state.ts index dd9139e..dff70e7 100644 --- a/src/state/state.ts +++ b/src/state/state.ts @@ -2,70 +2,70 @@ * Sync state management (task mappings) */ -import * as fs from 'fs'; -import * as path from 'path'; -import { getDataDir, ensureDataDir } from './config.js'; -import type { SyncState, TaskMapping } from '../types/index.js'; +import * as fs from "node:fs"; +import * as path from "node:path"; +import type { SyncState, TaskMapping } from "../types/index.js"; +import { ensureDataDir, getDataDir } from "./config.js"; -const STATE_FILE = path.join(getDataDir(), 'state.json'); +const STATE_FILE = path.join(getDataDir(), "state.json"); function getDefaultState(): SyncState { - return { - mappings: {}, - lastSync: null, - lastError: null, - }; + return { + mappings: {}, + lastSync: null, + lastError: null, + }; } export function loadState(): SyncState { - if (!fs.existsSync(STATE_FILE)) { - return getDefaultState(); - } + if (!fs.existsSync(STATE_FILE)) { + return getDefaultState(); + } - try { - const content = fs.readFileSync(STATE_FILE, 'utf-8'); - return JSON.parse(content) as SyncState; - } catch { - return getDefaultState(); - } + try { + const content = fs.readFileSync(STATE_FILE, "utf-8"); + return JSON.parse(content) as SyncState; + } catch { + return getDefaultState(); + } } export function saveState(state: SyncState): void { - ensureDataDir(); - fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); + ensureDataDir(); + fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2)); } export function addMapping(mapping: TaskMapping): void { - const state = loadState(); - state.mappings[mapping.githubId] = mapping; - saveState(state); + const state = loadState(); + state.mappings[mapping.githubId] = mapping; + saveState(state); } export function removeMapping(githubId: string): void { - const state = loadState(); - delete state.mappings[githubId]; - saveState(state); + const state = loadState(); + delete state.mappings[githubId]; + saveState(state); } export function getMapping(githubId: string): TaskMapping | null { - const state = loadState(); - return state.mappings[githubId] ?? null; + const state = loadState(); + return state.mappings[githubId] ?? null; } export function hasMapping(githubId: string): boolean { - const state = loadState(); - return githubId in state.mappings; + const state = loadState(); + return githubId in state.mappings; } export function updateLastSync(): void { - const state = loadState(); - state.lastSync = new Date().toISOString(); - state.lastError = null; - saveState(state); + const state = loadState(); + state.lastSync = new Date().toISOString(); + state.lastError = null; + saveState(state); } export function setLastError(error: string): void { - const state = loadState(); - state.lastError = error; - saveState(state); + const state = loadState(); + state.lastError = error; + saveState(state); } diff --git a/src/things/client.ts b/src/things/client.ts index 5983836..14bc0ea 100644 --- a/src/things/client.ts +++ b/src/things/client.ts @@ -2,29 +2,29 @@ * Things 3 client using URL Scheme */ -import { exec } from 'child_process'; -import { promisify } from 'util'; -import type { GitHubItem, GitHubItemType } from '../types/index.js'; +import { exec } from "node:child_process"; +import { promisify } from "node:util"; +import type { GitHubItem, GitHubItemType } from "../types/index.js"; const execAsync = promisify(exec); export class ThingsClient { - private project: string; - private authToken: string; - private projectVerified: boolean = false; - - constructor(project: string, authToken: string) { - this.project = project; - this.authToken = authToken; - } - - /** - * Ensure the project exists in Things, create if not - */ - async ensureProjectExists(): Promise { - if (this.projectVerified) return; - - const script = ` + private project: string; + private authToken: string; + private projectVerified: boolean = false; + + constructor(project: string, authToken: string) { + this.project = project; + this.authToken = authToken; + } + + /** + * Ensure the project exists in Things, create if not + */ + async ensureProjectExists(): Promise { + if (this.projectVerified) return; + + const script = ` tell application "Things3" try set proj to project "${this.project}" @@ -36,72 +36,74 @@ export class ThingsClient { end tell `; - try { - const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`); - const result = stdout.trim(); - if (result === 'created') { - console.log(`📁 Created Things project: ${this.project}`); - } - this.projectVerified = true; - } catch (error) { - console.warn(`Warning: Could not verify/create project: ${error}`); - } - } - - /** - * Create a task in Things for a GitHub item - * Returns the Things task ID - */ - async createTask(item: GitHubItem): Promise { - // Ensure project exists before creating task - await this.ensureProjectExists(); - - const title = this.formatTitle(item); - const notes = this.formatNotes(item); - const tags = this.formatTags(item); - - // Try AppleScript first (gives us the ID back) - try { - const thingsId = await this.createTaskViaAppleScript(title, notes, tags); - return thingsId; - } catch (error) { - // Fallback: use URL scheme (more reliable but no ID) - console.warn('AppleScript failed, falling back to URL scheme'); - await this.createTaskViaUrlScheme(title, notes, tags); - return `url-${Date.now()}`; - } - } - - /** - * Complete a task in Things - */ - async completeTask(thingsId: string): Promise { - const params = new URLSearchParams({ - id: thingsId, - 'auth-token': this.authToken, - completed: 'true', - }); - - const url = `things:///update?${params.toString()}`; - // -g flag opens in background without stealing focus - await execAsync(`open -g "${url}"`); - } - - /** - * Create task via AppleScript (more reliable for getting ID) - */ - private async createTaskViaAppleScript( - title: string, - notes: string, - tags: string - ): Promise { - // Escape special characters for AppleScript - const escapedTitle = title.replace(/"/g, '\\"').replace(/\\/g, '\\\\'); - const escapedNotes = notes.replace(/"/g, '\\"').replace(/\\/g, '\\\\'); - const escapedProject = this.project.replace(/"/g, '\\"'); - - // Create task directly in the project using "at beginning of" - const script = ` + try { + const { stdout } = await execAsync( + `osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, + ); + const result = stdout.trim(); + if (result === "created") { + console.log(`📁 Created Things project: ${this.project}`); + } + this.projectVerified = true; + } catch (error) { + console.warn(`Warning: Could not verify/create project: ${error}`); + } + } + + /** + * Create a task in Things for a GitHub item + * Returns the Things task ID + */ + async createTask(item: GitHubItem): Promise { + // Ensure project exists before creating task + await this.ensureProjectExists(); + + const title = this.formatTitle(item); + const notes = this.formatNotes(item); + const tags = this.formatTags(item); + + // Try AppleScript first (gives us the ID back) + try { + const thingsId = await this.createTaskViaAppleScript(title, notes, tags); + return thingsId; + } catch (_error) { + // Fallback: use URL scheme (more reliable but no ID) + console.warn("AppleScript failed, falling back to URL scheme"); + await this.createTaskViaUrlScheme(title, notes, tags); + return `url-${Date.now()}`; + } + } + + /** + * Complete a task in Things + */ + async completeTask(thingsId: string): Promise { + const params = new URLSearchParams({ + id: thingsId, + "auth-token": this.authToken, + completed: "true", + }); + + const url = `things:///update?${params.toString()}`; + // -g flag opens in background without stealing focus + await execAsync(`open -g "${url}"`); + } + + /** + * Create task via AppleScript (more reliable for getting ID) + */ + private async createTaskViaAppleScript( + title: string, + notes: string, + tags: string, + ): Promise { + // Escape special characters for AppleScript + const escapedTitle = title.replace(/"/g, '\\"').replace(/\\/g, "\\\\"); + const escapedNotes = notes.replace(/"/g, '\\"').replace(/\\/g, "\\\\"); + const escapedProject = this.project.replace(/"/g, '\\"'); + + // Create task directly in the project using "at beginning of" + const script = ` tell application "Things3" set proj to project "${escapedProject}" set newToDo to make new to do with properties {name:"${escapedTitle}", notes:"${escapedNotes}", tag names:"${tags}"} at beginning of proj @@ -109,87 +111,89 @@ export class ThingsClient { end tell `; - const { stdout } = await execAsync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`); - const rawId = stdout.trim(); - // AppleScript returns "to do id XYZ", extract just the ID - const match = rawId.match(/to do id (.+)/); - const thingsId = match ? match[1] : rawId; - - // Set "when=today" via URL scheme (AppleScript can't do this reliably) - await this.setTaskToToday(thingsId); - - return thingsId; - } - - /** - * Set a task to appear in Today using URL scheme - */ - private async setTaskToToday(thingsId: string): Promise { - const params = new URLSearchParams({ - id: thingsId, - 'auth-token': this.authToken, - when: 'today', - }); - - const url = `things:///update?${params.toString()}`; - // -g flag opens in background without stealing focus - await execAsync(`open -g "${url}"`); - // Small delay to let Things process the update - await new Promise((resolve) => setTimeout(resolve, 100)); - } - - /** - * Fallback: create via URL scheme - */ - private async createTaskViaUrlScheme( - title: string, - notes: string, - tags: string - ): Promise { - const params = new URLSearchParams({ - title, - notes, - tags, - when: 'today', - list: this.project, - }); - - const url = `things:///add?${params.toString()}`; - // -g flag opens in background without stealing focus - await execAsync(`open -g "${url}"`); - } - - private formatTitle(item: GitHubItem): string { - const prefixes: Record = { - 'pr-review': 'Review', - 'pr-created': 'PR', - 'issue-assigned': 'Issue', - 'issue-created': 'My Issue', - }; - - const prefix = prefixes[item.type]; - // Include repo name for context - const shortRepo = item.repo.split('/').pop() ?? item.repo; - return `${prefix}: ${item.title} (${shortRepo})`; - } - - private formatNotes(item: GitHubItem): string { - return `${item.url}\n\nRepo: ${item.repo}\n#${item.number}`; - } - - private formatTags(item: GitHubItem): string { - const baseTags = ['github']; - - if (item.type.startsWith('pr-')) { - baseTags.push('pr'); - } else { - baseTags.push('issue'); - } - - if (item.type === 'pr-review') { - baseTags.push('review'); - } - - return baseTags.join(','); - } + const { stdout } = await execAsync( + `osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, + ); + const rawId = stdout.trim(); + // AppleScript returns "to do id XYZ", extract just the ID + const match = rawId.match(/to do id (.+)/); + const thingsId = match ? match[1] : rawId; + + // Set "when=today" via URL scheme (AppleScript can't do this reliably) + await this.setTaskToToday(thingsId); + + return thingsId; + } + + /** + * Set a task to appear in Today using URL scheme + */ + private async setTaskToToday(thingsId: string): Promise { + const params = new URLSearchParams({ + id: thingsId, + "auth-token": this.authToken, + when: "today", + }); + + const url = `things:///update?${params.toString()}`; + // -g flag opens in background without stealing focus + await execAsync(`open -g "${url}"`); + // Small delay to let Things process the update + await new Promise((resolve) => setTimeout(resolve, 100)); + } + + /** + * Fallback: create via URL scheme + */ + private async createTaskViaUrlScheme( + title: string, + notes: string, + tags: string, + ): Promise { + const params = new URLSearchParams({ + title, + notes, + tags, + when: "today", + list: this.project, + }); + + const url = `things:///add?${params.toString()}`; + // -g flag opens in background without stealing focus + await execAsync(`open -g "${url}"`); + } + + private formatTitle(item: GitHubItem): string { + const prefixes: Record = { + "pr-review": "Review", + "pr-created": "PR", + "issue-assigned": "Issue", + "issue-created": "My Issue", + }; + + const prefix = prefixes[item.type]; + // Include repo name for context + const shortRepo = item.repo.split("/").pop() ?? item.repo; + return `${prefix}: ${item.title} (${shortRepo})`; + } + + private formatNotes(item: GitHubItem): string { + return `${item.url}\n\nRepo: ${item.repo}\n#${item.number}`; + } + + private formatTags(item: GitHubItem): string { + const baseTags = ["github"]; + + if (item.type.startsWith("pr-")) { + baseTags.push("pr"); + } else { + baseTags.push("issue"); + } + + if (item.type === "pr-review") { + baseTags.push("review"); + } + + return baseTags.join(","); + } } diff --git a/src/types/index.ts b/src/types/index.ts index b9eacbf..66ae3e3 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -4,69 +4,73 @@ // GitHub item types we track export type GitHubItemType = - | 'pr-review' // PR where you're requested as reviewer - | 'pr-created' // PR you created - | 'issue-assigned' // Issue assigned to you - | 'issue-created'; // Issue you created + | "pr-review" // PR where you're requested as reviewer + | "pr-created" // PR you created + | "issue-assigned" // Issue assigned to you + | "issue-created"; // Issue you created // Sync type identifiers for config -export type SyncType = 'pr-reviews' | 'prs-created' | 'issues-assigned' | 'issues-created'; +export type SyncType = + | "pr-reviews" + | "prs-created" + | "issues-assigned" + | "issues-created"; // All available sync types (for validation) export const ALL_SYNC_TYPES: SyncType[] = [ - 'pr-reviews', - 'prs-created', - 'issues-assigned', - 'issues-created', + "pr-reviews", + "prs-created", + "issues-assigned", + "issues-created", ]; // A GitHub item (PR or Issue) export interface GitHubItem { - id: number; - type: GitHubItemType; - title: string; - url: string; - repo: string; - number: number; - state: 'open' | 'closed' | 'merged'; - createdAt: string; - updatedAt: string; + id: number; + type: GitHubItemType; + title: string; + url: string; + repo: string; + number: number; + state: "open" | "closed" | "merged"; + createdAt: string; + updatedAt: string; } // Mapping between GitHub item and Things task export interface TaskMapping { - githubId: string; // e.g., "pr:123456" or "issue:789" - thingsId: string; // Things task ID returned from URL scheme - type: GitHubItemType; - title: string; - url: string; - createdAt: string; - completedAt?: string; + githubId: string; // e.g., "pr:123456" or "issue:789" + thingsId: string; // Things task ID returned from URL scheme + type: GitHubItemType; + title: string; + url: string; + createdAt: string; + completedAt?: string; } // Persisted state export interface SyncState { - mappings: Record; - lastSync: string | null; - lastError: string | null; + mappings: Record; + lastSync: string | null; + lastError: string | null; } // User configuration export interface Config { - githubToken: string; - thingsProject: string; // Default: "GitHub" - thingsAuthToken: string; // Required for updating tasks - pollInterval: number; // Seconds, default: 300 (5 min) - autoStart: boolean; // Install LaunchAgent - syncTypes: SyncType[]; // Which item types to sync (default: all) + githubToken: string; + thingsProject: string; // Default: "GitHub" + thingsAuthToken: string; // Required for updating tasks + pollInterval: number; // Seconds, default: 300 (5 min) + autoStart: boolean; // Install LaunchAgent + syncTypes: SyncType[]; // Which item types to sync (default: all) } // Daemon status export interface DaemonStatus { - running: boolean; - pid: number | null; - lastSync: string | null; - lastError: string | null; - taskCount: number; - uptime: number | null; // Seconds + running: boolean; + pid: number | null; + lastSync: string | null; + lastError: string | null; + taskCount: number; + uptime: number | null; // Seconds } diff --git a/src/types/update-notifier.d.ts b/src/types/update-notifier.d.ts index 7616587..4f4f26e 100644 --- a/src/types/update-notifier.d.ts +++ b/src/types/update-notifier.d.ts @@ -1,33 +1,33 @@ -declare module 'update-notifier' { - interface Package { - name: string; - version: string; - } +declare module "update-notifier" { + interface Package { + name: string; + version: string; + } - interface UpdateInfo { - latest: string; - current: string; - type: string; - name: string; - } + interface UpdateInfo { + latest: string; + current: string; + type: string; + name: string; + } - interface NotifierConfig { - get(key: string): unknown; - set(key: string, value: unknown): void; - delete(key: string): void; - } + interface NotifierConfig { + get(key: string): unknown; + set(key: string, value: unknown): void; + delete(key: string): void; + } - interface UpdateNotifier { - update?: UpdateInfo; - config?: NotifierConfig; - fetchInfo(): Promise; - notify(options?: object): void; - } + interface UpdateNotifier { + update?: UpdateInfo; + config?: NotifierConfig; + fetchInfo(): Promise; + notify(options?: object): void; + } - interface Options { - pkg: Package; - updateCheckInterval?: number; - } + interface Options { + pkg: Package; + updateCheckInterval?: number; + } - export default function updateNotifier(options: Options): UpdateNotifier; + export default function updateNotifier(options: Options): UpdateNotifier; }