diff --git a/package.json b/package.json index 1603413..02ed1b0 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ }, "dependencies": { "@changesets/cli": "^2.29.7", - "@clack/prompts": "^0.11.0", "@types/node": "^24.3.3", "boxen": "^8.0.1", "commander": "^14.0.2", @@ -64,6 +63,7 @@ "dist/cli/commands/preview", "dist/cli/commands/revert", "dist/cli/commands/shared", + "dist/cli/ui", "dist/cli/utils", "dist/lib", "README.md", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 605c165..69a47bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,19 +1,17 @@ -lockfileVersion: "9.0" +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: + .: dependencies: - "@changesets/cli": + '@changesets/cli': specifier: ^2.29.7 version: 2.29.7(@types/node@24.3.3) - "@clack/prompts": - specifier: ^0.11.0 - version: 0.11.0 - "@types/node": + '@types/node': specifier: ^24.3.3 version: 24.3.3 boxen: @@ -47,1057 +45,600 @@ importers: specifier: ^1.6.1 version: 1.6.1 devDependencies: - "@types/js-yaml": + '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 packages: - "@babel/code-frame@7.27.1": - resolution: - { - integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-string-parser@7.25.9": - resolution: - { - integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-validator-identifier@7.25.9": - resolution: - { - integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==, - } - engines: { node: ">=6.9.0" } - - "@babel/helper-validator-identifier@7.27.1": - resolution: - { - integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==, - } - engines: { node: ">=6.9.0" } - - "@babel/parser@7.27.0": - resolution: - { - integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==, - } - engines: { node: ">=6.0.0" } + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.27.0': + resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} + engines: {node: '>=6.0.0'} hasBin: true - "@babel/runtime@7.27.0": - resolution: - { - integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==, - } - engines: { node: ">=6.9.0" } - - "@babel/types@7.27.0": - resolution: - { - integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==, - } - engines: { node: ">=6.9.0" } - - "@changesets/apply-release-plan@7.0.13": - resolution: - { - integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==, - } - - "@changesets/assemble-release-plan@6.0.9": - resolution: - { - integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==, - } - - "@changesets/changelog-git@0.2.1": - resolution: - { - integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==, - } - - "@changesets/cli@2.29.7": - resolution: - { - integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==, - } + '@babel/runtime@7.27.0': + resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.27.0': + resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} + engines: {node: '>=6.9.0'} + + '@changesets/apply-release-plan@7.0.13': + resolution: {integrity: sha512-BIW7bofD2yAWoE8H4V40FikC+1nNFEKBisMECccS16W1rt6qqhNTBDmIw5HaqmMgtLNz9e7oiALiEUuKrQ4oHg==} + + '@changesets/assemble-release-plan@6.0.9': + resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==} + + '@changesets/changelog-git@0.2.1': + resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} + + '@changesets/cli@2.29.7': + resolution: {integrity: sha512-R7RqWoaksyyKXbKXBTbT4REdy22yH81mcFK6sWtqSanxUCbUi9Uf+6aqxZtDQouIqPdem2W56CdxXgsxdq7FLQ==} hasBin: true - "@changesets/config@3.1.1": - resolution: - { - integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==, - } - - "@changesets/errors@0.2.0": - resolution: - { - integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==, - } - - "@changesets/get-dependents-graph@2.1.3": - resolution: - { - integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==, - } - - "@changesets/get-release-plan@4.0.13": - resolution: - { - integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==, - } - - "@changesets/get-version-range-type@0.4.0": - resolution: - { - integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==, - } - - "@changesets/git@3.0.4": - resolution: - { - integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==, - } - - "@changesets/logger@0.1.1": - resolution: - { - integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==, - } - - "@changesets/parse@0.4.1": - resolution: - { - integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==, - } - - "@changesets/pre@2.0.2": - resolution: - { - integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==, - } - - "@changesets/read@0.6.5": - resolution: - { - integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==, - } - - "@changesets/should-skip-package@0.1.2": - resolution: - { - integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==, - } - - "@changesets/types@4.1.0": - resolution: - { - integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==, - } - - "@changesets/types@6.1.0": - resolution: - { - integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==, - } - - "@changesets/write@0.4.0": - resolution: - { - integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==, - } - - "@clack/core@0.5.0": - resolution: - { - integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==, - } - - "@clack/prompts@0.11.0": - resolution: - { - integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==, - } - - "@inquirer/external-editor@1.0.1": - resolution: - { - integrity: sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==, - } - engines: { node: ">=18" } + '@changesets/config@3.1.1': + resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} + + '@changesets/errors@0.2.0': + resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + + '@changesets/get-dependents-graph@2.1.3': + resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} + + '@changesets/get-release-plan@4.0.13': + resolution: {integrity: sha512-DWG1pus72FcNeXkM12tx+xtExyH/c9I1z+2aXlObH3i9YA7+WZEVaiHzHl03thpvAgWTRaH64MpfHxozfF7Dvg==} + + '@changesets/get-version-range-type@0.4.0': + resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + + '@changesets/git@3.0.4': + resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==} + + '@changesets/logger@0.1.1': + resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + + '@changesets/parse@0.4.1': + resolution: {integrity: sha512-iwksMs5Bf/wUItfcg+OXrEpravm5rEd9Bf4oyIPL4kVTmJQ7PNDSd6MDYkpSJR1pn7tz/k8Zf2DhTCqX08Ou+Q==} + + '@changesets/pre@2.0.2': + resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==} + + '@changesets/read@0.6.5': + resolution: {integrity: sha512-UPzNGhsSjHD3Veb0xO/MwvasGe8eMyNrR/sT9gR8Q3DhOQZirgKhhXv/8hVsI0QpPjR004Z9iFxoJU6in3uGMg==} + + '@changesets/should-skip-package@0.1.2': + resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==} + + '@changesets/types@4.1.0': + resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} + + '@changesets/types@6.1.0': + resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==} + + '@changesets/write@0.4.0': + resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + + '@inquirer/external-editor@1.0.1': + resolution: {integrity: sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==} + engines: {node: '>=18'} peerDependencies: - "@types/node": ">=18" + '@types/node': '>=18' peerDependenciesMeta: - "@types/node": + '@types/node': optional: true - "@manypkg/find-root@1.1.0": - resolution: - { - integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==, - } - - "@manypkg/get-packages@1.1.3": - resolution: - { - integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==, - } - - "@nodelib/fs.scandir@2.1.5": - resolution: - { - integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, - } - engines: { node: ">= 8" } - - "@nodelib/fs.stat@2.0.5": - resolution: - { - integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==, - } - engines: { node: ">= 8" } - - "@nodelib/fs.walk@1.2.8": - resolution: - { - integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==, - } - engines: { node: ">= 8" } - - "@types/js-yaml@4.0.9": - resolution: - { - integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==, - } - - "@types/node@12.20.55": - resolution: - { - integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==, - } - - "@types/node@24.3.3": - resolution: - { - integrity: sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==, - } + '@manypkg/find-root@1.1.0': + resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + + '@manypkg/get-packages@1.1.3': + resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + + '@types/node@12.20.55': + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + + '@types/node@24.3.3': + resolution: {integrity: sha512-GKBNHjoNw3Kra1Qg5UXttsY5kiWMEfoHq2TmXb+b1rcm6N7B3wTrFYIf/oSZ1xNQ+hVVijgLkiDZh7jRRsh+Gw==} ansi-align@3.0.1: - resolution: - { - integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==, - } + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} ansi-colors@4.1.3: - resolution: - { - integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} ansi-regex@5.0.1: - resolution: - { - integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} ansi-regex@6.1.0: - resolution: - { - integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} ansi-styles@6.2.1: - resolution: - { - integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} argparse@1.0.10: - resolution: - { - integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==, - } + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: - resolution: - { - integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==, - } + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} array-union@2.1.0: - resolution: - { - integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} better-path-resolve@1.0.0: - resolution: - { - integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} + engines: {node: '>=4'} boxen@8.0.1: - resolution: - { - integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} braces@3.0.3: - resolution: - { - integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} callsites@3.1.0: - resolution: - { - integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} camelcase@8.0.0: - resolution: - { - integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==, - } - engines: { node: ">=16" } + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} chalk@5.4.1: - resolution: - { - integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==, - } - engines: { node: ^12.17.0 || ^14.13 || >=16.0.0 } + resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} chardet@2.1.0: - resolution: - { - integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==, - } + resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} ci-info@3.9.0: - resolution: - { - integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} cli-boxes@3.0.0: - resolution: - { - integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} commander@14.0.2: - resolution: - { - integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==, - } - engines: { node: ">=20" } + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} consola@3.4.2: - resolution: - { - integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==, - } - engines: { node: ^14.18.0 || >=16.10.0 } + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} cosmiconfig@9.0.0: - resolution: - { - integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==, - } - engines: { node: ">=14" } + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} peerDependencies: - typescript: ">=4.9.5" + typescript: '>=4.9.5' peerDependenciesMeta: typescript: optional: true cross-spawn@7.0.6: - resolution: - { - integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==, - } - engines: { node: ">= 8" } + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} detect-indent@6.1.0: - resolution: - { - integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} dir-glob@3.0.1: - resolution: - { - integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} emoji-regex@10.5.0: - resolution: - { - integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==, - } + resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} emoji-regex@8.0.0: - resolution: - { - integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==, - } + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} enquirer@2.4.1: - resolution: - { - integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==, - } - engines: { node: ">=8.6" } + resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} + engines: {node: '>=8.6'} env-paths@2.2.1: - resolution: - { - integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} error-ex@1.3.4: - resolution: - { - integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==, - } + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} esprima@4.0.1: - resolution: - { - integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} hasBin: true extendable-error@0.1.7: - resolution: - { - integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==, - } + resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} fast-glob@3.3.3: - resolution: - { - integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==, - } - engines: { node: ">=8.6.0" } + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} fastq@1.19.1: - resolution: - { - integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==, - } + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} fill-range@7.1.1: - resolution: - { - integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} find-up@4.1.0: - resolution: - { - integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} fs-extra@7.0.1: - resolution: - { - integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==, - } - engines: { node: ">=6 <7 || >=8" } + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} fs-extra@8.1.0: - resolution: - { - integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==, - } - engines: { node: ">=6 <7 || >=8" } + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} get-east-asian-width@1.4.0: - resolution: - { - integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==} + engines: {node: '>=18'} glob-parent@5.1.2: - resolution: - { - integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, - } - engines: { node: ">= 6" } + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} globby@11.1.0: - resolution: - { - integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} graceful-fs@4.2.11: - resolution: - { - integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, - } + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} human-id@4.1.1: - resolution: - { - integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==, - } + resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} hasBin: true iconv-lite@0.6.3: - resolution: - { - integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} ignore@5.3.2: - resolution: - { - integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==, - } - engines: { node: ">= 4" } + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} import-fresh@3.3.1: - resolution: - { - integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} is-arrayish@0.2.1: - resolution: - { - integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==, - } + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} is-extglob@2.1.1: - resolution: - { - integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, - } - engines: { node: ">=0.10.0" } + 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" } + 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" } + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} is-number@7.0.0: - resolution: - { - integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==, - } - engines: { node: ">=0.12.0" } + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} is-subdir@1.2.0: - resolution: - { - integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} + engines: {node: '>=4'} is-windows@1.0.2: - resolution: - { - integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} + engines: {node: '>=0.10.0'} isexe@2.0.0: - resolution: - { - integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==, - } + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} js-tokens@4.0.0: - resolution: - { - integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, - } + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} js-yaml@3.14.1: - resolution: - { - integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==, - } + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true js-yaml@4.1.0: - resolution: - { - integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==, - } + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true json-parse-even-better-errors@2.3.1: - resolution: - { - integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==, - } + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} jsonfile@4.0.0: - resolution: - { - integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==, - } + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} lines-and-columns@1.2.4: - resolution: - { - integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, - } + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} locate-path@5.0.0: - resolution: - { - integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} lodash.startcase@4.4.0: - resolution: - { - integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==, - } + resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} magicast@0.3.5: - resolution: - { - integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==, - } + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} merge2@1.4.1: - resolution: - { - integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==, - } - engines: { node: ">= 8" } + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} micromatch@4.0.8: - resolution: - { - integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==, - } - engines: { node: ">=8.6" } + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} mri@1.2.0: - resolution: - { - integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} outdent@0.5.0: - resolution: - { - integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==, - } + resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} p-filter@2.1.0: - resolution: - { - integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} + engines: {node: '>=8'} p-limit@2.3.0: - resolution: - { - integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} p-locate@4.1.0: - resolution: - { - integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} p-map@2.1.0: - resolution: - { - integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} + engines: {node: '>=6'} p-try@2.2.0: - resolution: - { - integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} package-manager-detector@0.2.11: - resolution: - { - integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==, - } + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} parent-module@1.0.1: - resolution: - { - integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} parse-json@5.2.0: - resolution: - { - integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} path-exists@4.0.0: - resolution: - { - integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} path-key@3.1.1: - resolution: - { - integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} path-type@4.0.0: - resolution: - { - integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} picocolors@1.1.1: - resolution: - { - integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, - } + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.1: - resolution: - { - integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==, - } - engines: { node: ">=8.6" } + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} pify@4.0.1: - resolution: - { - integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} prettier@2.8.8: - resolution: - { - integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==, - } - engines: { node: ">=10.13.0" } + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} hasBin: true prettier@3.6.2: - resolution: - { - integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==, - } - engines: { node: ">=14" } + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} hasBin: true quansync@0.2.10: - resolution: - { - integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==, - } + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} queue-microtask@1.2.3: - resolution: - { - integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==, - } + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} read-yaml-file@1.1.0: - resolution: - { - integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==, - } - engines: { node: ">=6" } + resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} + engines: {node: '>=6'} regenerator-runtime@0.14.1: - resolution: - { - integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==, - } + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} resolve-from@4.0.0: - resolution: - { - integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} resolve-from@5.0.0: - resolution: - { - integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} reusify@1.1.0: - resolution: - { - integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==, - } - engines: { iojs: ">=1.0.0", node: ">=0.10.0" } + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} run-parallel@1.2.0: - resolution: - { - integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==, - } + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} safer-buffer@2.1.2: - resolution: - { - integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, - } + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} semver@7.7.1: - resolution: - { - integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==, - } - engines: { node: ">=10" } + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} hasBin: true shebang-command@2.0.0: - resolution: - { - integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} shebang-regex@3.0.0: - resolution: - { - integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} signal-exit@4.1.0: - resolution: - { - integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==, - } - engines: { node: ">=14" } - - sisteransi@1.0.5: - resolution: - { - integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==, - } + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} slash@3.0.0: - resolution: - { - integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} source-map-js@1.2.1: - resolution: - { - integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, - } - engines: { node: ">=0.10.0" } + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} spawndamnit@3.0.1: - resolution: - { - integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==, - } + resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} sprintf-js@1.0.3: - resolution: - { - integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==, - } + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} string-width@4.2.3: - resolution: - { - integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} string-width@7.2.0: - resolution: - { - integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} strip-ansi@6.0.1: - resolution: - { - integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} strip-ansi@7.1.0: - resolution: - { - integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==, - } - engines: { node: ">=12" } + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} strip-bom@3.0.0: - resolution: - { - integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==, - } - engines: { node: ">=4" } + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} term-size@2.2.1: - resolution: - { - integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==, - } - engines: { node: ">=8" } + resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} + engines: {node: '>=8'} to-regex-range@5.0.1: - resolution: - { - integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==, - } - engines: { node: ">=8.0" } + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} type-fest@4.41.0: - resolution: - { - integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==, - } - engines: { node: ">=16" } + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} typescript@5.9.2: - resolution: - { - integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==, - } - engines: { node: ">=14.17" } + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} hasBin: true ufo@1.6.1: - resolution: - { - integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==, - } + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} undici-types@7.10.0: - resolution: - { - integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==, - } + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} universalify@0.1.2: - resolution: - { - integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==, - } - engines: { node: ">= 4.0.0" } + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} which@2.0.2: - resolution: - { - integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==, - } - engines: { node: ">= 8" } + 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" } + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} wrap-ansi@9.0.2: - resolution: - { - integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==, - } - engines: { node: ">=18" } + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} snapshots: - "@babel/code-frame@7.27.1": + + '@babel/code-frame@7.27.1': dependencies: - "@babel/helper-validator-identifier": 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 js-tokens: 4.0.0 picocolors: 1.1.1 - "@babel/helper-string-parser@7.25.9": {} + '@babel/helper-string-parser@7.25.9': {} - "@babel/helper-validator-identifier@7.25.9": {} + '@babel/helper-validator-identifier@7.25.9': {} - "@babel/helper-validator-identifier@7.27.1": {} + '@babel/helper-validator-identifier@7.27.1': {} - "@babel/parser@7.27.0": + '@babel/parser@7.27.0': dependencies: - "@babel/types": 7.27.0 + '@babel/types': 7.27.0 - "@babel/runtime@7.27.0": + '@babel/runtime@7.27.0': dependencies: regenerator-runtime: 0.14.1 - "@babel/types@7.27.0": + '@babel/types@7.27.0': dependencies: - "@babel/helper-string-parser": 7.25.9 - "@babel/helper-validator-identifier": 7.25.9 + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 - "@changesets/apply-release-plan@7.0.13": + '@changesets/apply-release-plan@7.0.13': dependencies: - "@changesets/config": 3.1.1 - "@changesets/get-version-range-type": 0.4.0 - "@changesets/git": 3.0.4 - "@changesets/should-skip-package": 0.1.2 - "@changesets/types": 6.1.0 - "@manypkg/get-packages": 1.1.3 + '@changesets/config': 3.1.1 + '@changesets/get-version-range-type': 0.4.0 + '@changesets/git': 3.0.4 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 detect-indent: 6.1.0 fs-extra: 7.0.1 lodash.startcase: 4.4.0 @@ -1106,37 +647,37 @@ snapshots: resolve-from: 5.0.0 semver: 7.7.1 - "@changesets/assemble-release-plan@6.0.9": + '@changesets/assemble-release-plan@6.0.9': dependencies: - "@changesets/errors": 0.2.0 - "@changesets/get-dependents-graph": 2.1.3 - "@changesets/should-skip-package": 0.1.2 - "@changesets/types": 6.1.0 - "@manypkg/get-packages": 1.1.3 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 semver: 7.7.1 - "@changesets/changelog-git@0.2.1": - dependencies: - "@changesets/types": 6.1.0 - - "@changesets/cli@2.29.7(@types/node@24.3.3)": - dependencies: - "@changesets/apply-release-plan": 7.0.13 - "@changesets/assemble-release-plan": 6.0.9 - "@changesets/changelog-git": 0.2.1 - "@changesets/config": 3.1.1 - "@changesets/errors": 0.2.0 - "@changesets/get-dependents-graph": 2.1.3 - "@changesets/get-release-plan": 4.0.13 - "@changesets/git": 3.0.4 - "@changesets/logger": 0.1.1 - "@changesets/pre": 2.0.2 - "@changesets/read": 0.6.5 - "@changesets/should-skip-package": 0.1.2 - "@changesets/types": 6.1.0 - "@changesets/write": 0.4.0 - "@inquirer/external-editor": 1.0.1(@types/node@24.3.3) - "@manypkg/get-packages": 1.1.3 + '@changesets/changelog-git@0.2.1': + dependencies: + '@changesets/types': 6.1.0 + + '@changesets/cli@2.29.7(@types/node@24.3.3)': + dependencies: + '@changesets/apply-release-plan': 7.0.13 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/changelog-git': 0.2.1 + '@changesets/config': 3.1.1 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/get-release-plan': 4.0.13 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/should-skip-package': 0.1.2 + '@changesets/types': 6.1.0 + '@changesets/write': 0.4.0 + '@inquirer/external-editor': 1.0.1(@types/node@24.3.3) + '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 enquirer: 2.4.1 @@ -1150,141 +691,130 @@ snapshots: spawndamnit: 3.0.1 term-size: 2.2.1 transitivePeerDependencies: - - "@types/node" + - '@types/node' - "@changesets/config@3.1.1": + '@changesets/config@3.1.1': dependencies: - "@changesets/errors": 0.2.0 - "@changesets/get-dependents-graph": 2.1.3 - "@changesets/logger": 0.1.1 - "@changesets/types": 6.1.0 - "@manypkg/get-packages": 1.1.3 + '@changesets/errors': 0.2.0 + '@changesets/get-dependents-graph': 2.1.3 + '@changesets/logger': 0.1.1 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 micromatch: 4.0.8 - "@changesets/errors@0.2.0": + '@changesets/errors@0.2.0': dependencies: extendable-error: 0.1.7 - "@changesets/get-dependents-graph@2.1.3": + '@changesets/get-dependents-graph@2.1.3': dependencies: - "@changesets/types": 6.1.0 - "@manypkg/get-packages": 1.1.3 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 picocolors: 1.1.1 semver: 7.7.1 - "@changesets/get-release-plan@4.0.13": + '@changesets/get-release-plan@4.0.13': dependencies: - "@changesets/assemble-release-plan": 6.0.9 - "@changesets/config": 3.1.1 - "@changesets/pre": 2.0.2 - "@changesets/read": 0.6.5 - "@changesets/types": 6.1.0 - "@manypkg/get-packages": 1.1.3 + '@changesets/assemble-release-plan': 6.0.9 + '@changesets/config': 3.1.1 + '@changesets/pre': 2.0.2 + '@changesets/read': 0.6.5 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 - "@changesets/get-version-range-type@0.4.0": {} + '@changesets/get-version-range-type@0.4.0': {} - "@changesets/git@3.0.4": + '@changesets/git@3.0.4': dependencies: - "@changesets/errors": 0.2.0 - "@manypkg/get-packages": 1.1.3 + '@changesets/errors': 0.2.0 + '@manypkg/get-packages': 1.1.3 is-subdir: 1.2.0 micromatch: 4.0.8 spawndamnit: 3.0.1 - "@changesets/logger@0.1.1": + '@changesets/logger@0.1.1': dependencies: picocolors: 1.1.1 - "@changesets/parse@0.4.1": + '@changesets/parse@0.4.1': dependencies: - "@changesets/types": 6.1.0 + '@changesets/types': 6.1.0 js-yaml: 3.14.1 - "@changesets/pre@2.0.2": + '@changesets/pre@2.0.2': dependencies: - "@changesets/errors": 0.2.0 - "@changesets/types": 6.1.0 - "@manypkg/get-packages": 1.1.3 + '@changesets/errors': 0.2.0 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 fs-extra: 7.0.1 - "@changesets/read@0.6.5": + '@changesets/read@0.6.5': dependencies: - "@changesets/git": 3.0.4 - "@changesets/logger": 0.1.1 - "@changesets/parse": 0.4.1 - "@changesets/types": 6.1.0 + '@changesets/git': 3.0.4 + '@changesets/logger': 0.1.1 + '@changesets/parse': 0.4.1 + '@changesets/types': 6.1.0 fs-extra: 7.0.1 p-filter: 2.1.0 picocolors: 1.1.1 - "@changesets/should-skip-package@0.1.2": + '@changesets/should-skip-package@0.1.2': dependencies: - "@changesets/types": 6.1.0 - "@manypkg/get-packages": 1.1.3 + '@changesets/types': 6.1.0 + '@manypkg/get-packages': 1.1.3 - "@changesets/types@4.1.0": {} + '@changesets/types@4.1.0': {} - "@changesets/types@6.1.0": {} + '@changesets/types@6.1.0': {} - "@changesets/write@0.4.0": + '@changesets/write@0.4.0': dependencies: - "@changesets/types": 6.1.0 + '@changesets/types': 6.1.0 fs-extra: 7.0.1 human-id: 4.1.1 prettier: 2.8.8 - "@clack/core@0.5.0": - dependencies: - picocolors: 1.1.1 - sisteransi: 1.0.5 - - "@clack/prompts@0.11.0": - dependencies: - "@clack/core": 0.5.0 - picocolors: 1.1.1 - sisteransi: 1.0.5 - - "@inquirer/external-editor@1.0.1(@types/node@24.3.3)": + '@inquirer/external-editor@1.0.1(@types/node@24.3.3)': dependencies: chardet: 2.1.0 iconv-lite: 0.6.3 optionalDependencies: - "@types/node": 24.3.3 + '@types/node': 24.3.3 - "@manypkg/find-root@1.1.0": + '@manypkg/find-root@1.1.0': dependencies: - "@babel/runtime": 7.27.0 - "@types/node": 12.20.55 + '@babel/runtime': 7.27.0 + '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 - "@manypkg/get-packages@1.1.3": + '@manypkg/get-packages@1.1.3': dependencies: - "@babel/runtime": 7.27.0 - "@changesets/types": 4.1.0 - "@manypkg/find-root": 1.1.0 + '@babel/runtime': 7.27.0 + '@changesets/types': 4.1.0 + '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 globby: 11.1.0 read-yaml-file: 1.1.0 - "@nodelib/fs.scandir@2.1.5": + '@nodelib/fs.scandir@2.1.5': dependencies: - "@nodelib/fs.stat": 2.0.5 + '@nodelib/fs.stat': 2.0.5 run-parallel: 1.2.0 - "@nodelib/fs.stat@2.0.5": {} + '@nodelib/fs.stat@2.0.5': {} - "@nodelib/fs.walk@1.2.8": + '@nodelib/fs.walk@1.2.8': dependencies: - "@nodelib/fs.scandir": 2.1.5 + '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - "@types/js-yaml@4.0.9": {} + '@types/js-yaml@4.0.9': {} - "@types/node@12.20.55": {} + '@types/node@12.20.55': {} - "@types/node@24.3.3": + '@types/node@24.3.3': dependencies: undici-types: 7.10.0 @@ -1385,8 +915,8 @@ snapshots: fast-glob@3.3.3: dependencies: - "@nodelib/fs.stat": 2.0.5 - "@nodelib/fs.walk": 1.2.8 + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 glob-parent: 5.1.2 merge2: 1.4.1 micromatch: 4.0.8 @@ -1493,8 +1023,8 @@ snapshots: magicast@0.3.5: dependencies: - "@babel/parser": 7.27.0 - "@babel/types": 7.27.0 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 source-map-js: 1.2.1 merge2@1.4.1: {} @@ -1534,7 +1064,7 @@ snapshots: parse-json@5.2.0: dependencies: - "@babel/code-frame": 7.27.1 + '@babel/code-frame': 7.27.1 error-ex: 1.3.4 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -1590,8 +1120,6 @@ snapshots: signal-exit@4.1.0: {} - sisteransi@1.0.5: {} - slash@3.0.0: {} source-map-js@1.2.1: {} diff --git a/src/cli/commands/commit/index.ts b/src/cli/commands/commit/index.ts index 9c6a12e..33fe6ba 100644 --- a/src/cli/commands/commit/index.ts +++ b/src/cli/commands/commit/index.ts @@ -33,7 +33,7 @@ import { } from "./prompts.js"; import { formatCommitMessage } from "./formatter.js"; import type { CommitState } from "./types.js"; -import { success } from "../init/colors.js"; +import { clef } from "../init/clef.js"; /** * Clear terminal screen for clean prompt display @@ -183,7 +183,7 @@ export async function commitAction(options: { // Require an actual config file - reject fallback defaults if (configResult.source === "defaults") { Logger.error("Configuration not found"); - console.error("\n Run 'lab init' to create configuration file.\n"); + clef.nudge("Hmm, no config file found. Run 'lab init' to get started!"); process.exit(1); } @@ -354,7 +354,7 @@ export async function commitAction(options: { options.verify === false, ); - console.log(`${success("✓")} Commit created successfully!`); + clef.successReaction("Commit created successfully!"); const displayMessage = formatForDisplay( formattedMessage, emojiModeActive, @@ -377,11 +377,33 @@ export async function commitAction(options: { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`\n✗ Error: Git commit failed`); - console.error(`\n ${errorMessage}`); - console.error( - "\n Fix: Check 'git status' and verify staged files, then try again\n", - ); + + // Detect GPG-specific failures + const isGpgError = + errorMessage.includes("gpg failed to sign") || + errorMessage.includes("secret key not available") || + errorMessage.includes("signing failed") || + errorMessage.includes("gpg: skipped") || + errorMessage.includes("gpg: signing failed"); + + if (isGpgError) { + clef.errorReaction("Commit signing failed"); + console.error(`\n GPG could not sign this commit.`); + console.error(`\n Possible causes:`); + console.error(` • GPG key expired or revoked`); + console.error(` • GPG agent not running`); + console.error(` • Passphrase entry failed`); + console.error( + `\n To disable signing, set 'sign_commits: false' in .labcommitr.config.yaml`, + ); + console.error(` Or run: git config --global commit.gpgsign false\n`); + } else { + clef.errorReaction("Git commit failed"); + console.error(`\n ${errorMessage}`); + console.error( + "\n Fix: Check 'git status' and verify staged files, then try again\n", + ); + } process.exit(1); } } else { @@ -518,7 +540,7 @@ export async function commitAction(options: { options.verify === false, ); - console.log(`${success("✓")} Commit created successfully!`); + clef.successReaction("Commit created successfully!"); const displayMessage = formatForDisplay( formattedMessage, emojiModeActive, @@ -541,11 +563,33 @@ export async function commitAction(options: { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`\n✗ Error: Git commit failed`); - console.error(`\n ${errorMessage}`); - console.error( - "\n Fix: Check 'git status' and verify staged files, then try again\n", - ); + + // Detect GPG-specific failures + const isGpgError = + errorMessage.includes("gpg failed to sign") || + errorMessage.includes("secret key not available") || + errorMessage.includes("signing failed") || + errorMessage.includes("gpg: skipped") || + errorMessage.includes("gpg: signing failed"); + + if (isGpgError) { + clef.errorReaction("Commit signing failed"); + console.error(`\n GPG could not sign this commit.`); + console.error(`\n Possible causes:`); + console.error(` • GPG key expired or revoked`); + console.error(` • GPG agent not running`); + console.error(` • Passphrase entry failed`); + console.error( + `\n To disable signing, set 'sign_commits: false' in .labcommitr.config.yaml`, + ); + console.error(` Or run: git config --global commit.gpgsign false\n`); + } else { + clef.errorReaction("Git commit failed"); + console.error(`\n ${errorMessage}`); + console.error( + "\n Fix: Check 'git status' and verify staged files, then try again\n", + ); + } process.exit(1); } } diff --git a/src/cli/commands/commit/prompts.ts b/src/cli/commands/commit/prompts.ts index 68e08b5..2871906 100644 --- a/src/cli/commands/commit/prompts.ts +++ b/src/cli/commands/commit/prompts.ts @@ -2,15 +2,12 @@ * Commit Command Prompts * * Interactive prompts for commit creation - * Uses same styling as init command for consistency + * Uses custom UI framework for clean, connector-free output */ -import { select, text, isCancel, log } from "@clack/prompts"; -import { labelColors, textColors, success, attention } from "../init/colors.js"; -import type { - LabcommitrConfig, - CommitType, -} from "../../../lib/config/types.js"; +import { ui } from "../../ui/index.js"; +import { textColors, attention } from "../init/colors.js"; +import type { LabcommitrConfig } from "../../../lib/config/types.js"; import type { ValidationError } from "./types.js"; import { editInEditor, detectEditor } from "./editor.js"; import { @@ -18,49 +15,6 @@ import { formatLabelWithShortcut, getShortcutForValue, } from "../../../lib/shortcuts/index.js"; -import { selectWithShortcuts } from "../../../lib/shortcuts/select-with-shortcuts.js"; - -/** - * Create compact color-coded label - * Labels are 9 characters wide (7 chars + 2 padding spaces) for alignment - * Text is centered within the label - */ -function label( - text: string, - color: "magenta" | "cyan" | "blue" | "yellow" | "green", -): string { - const colorFn = { - magenta: labelColors.bgBrightMagenta, - cyan: labelColors.bgBrightCyan, - blue: labelColors.bgBrightBlue, - yellow: labelColors.bgBrightYellow, - green: labelColors.bgBrightGreen, - }[color]; - - // Center text within 7-character width (accommodates "subject" and "preview") - // For visual centering: when padding is odd, put extra space on LEFT for better balance - const width = 7; - const textLength = Math.min(text.length, width); // Cap at width - const padding = width - textLength; - // For odd padding (1, 3, 5...), ceil puts extra space on LEFT (better visual weight) - // For even padding (2, 4, 6...), floor/ceil both work the same - const leftPad = Math.ceil(padding / 2); - const rightPad = padding - leftPad; - const centeredText = - " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); - - return colorFn(` ${centeredText} `); -} - -/** - * Handle prompt cancellation - */ -function handleCancel(value: unknown): void { - if (isCancel(value)) { - console.log("\nCommit cancelled."); - process.exit(0); - } -} /** * Prompt for commit type selection @@ -102,37 +56,42 @@ export async function promptType( const displayHints = config.advanced.shortcuts?.display_hints ?? true; - // Find initial type index if provided - const initialIndex = initialType - ? config.types.findIndex((t) => t.id === initialType) - : undefined; - // Build options with shortcuts const options = config.types.map((type) => { const shortcut = getShortcutForValue(type.id, shortcutMapping); const baseLabel = `${type.id.padEnd(8)} ${type.description}`; - const label = formatLabelWithShortcut(baseLabel, shortcut, displayHints); + const optionLabel = formatLabelWithShortcut( + baseLabel, + shortcut, + displayHints, + ); return { value: type.id, - label, + label: optionLabel, hint: type.description, }; }); - const selected = await selectWithShortcuts( - { - message: `${label("type", "magenta")} ${textColors.pureWhite("Select commit type:")}`, - options, - initialValue: - initialIndex !== undefined && initialIndex >= 0 - ? config.types[initialIndex].id - : undefined, - }, - shortcutMapping, - ); + // Find initial type value if provided + const initialValue = initialType + ? config.types.find((t) => t.id === initialType)?.id + : undefined; + + const selected = await ui.select({ + label: "type", + labelColor: "magenta", + message: "Select commit type:", + options, + initialValue, + shortcuts: shortcutMapping, + }); + + if (ui.isCancel(selected)) { + console.log("\nCommit cancelled."); + process.exit(0); + } - handleCancel(selected); const typeId = selected as string; const typeConfig = config.types.find((t) => t.id === typeId)!; @@ -191,27 +150,24 @@ export async function promptScope( }, ]; - // Find initial scope index if provided - const initialIndex = initialScope - ? allowedScopes.findIndex((s) => s === initialScope) - : undefined; - - const selected = await select({ - message: `${label("scope", "blue")} ${textColors.pureWhite( - `Enter scope ${isRequired ? "(required for '" + selectedType + "')" : "(optional)"}:`, - )}`, + const selected = await ui.select({ + label: "scope", + labelColor: "blue", + message: `Enter scope ${isRequired ? "(required for '" + selectedType + "')" : "(optional)"}:`, options, - initialValue: - initialIndex !== undefined && initialIndex >= 0 - ? allowedScopes[initialIndex] - : initialScope || undefined, + initialValue: initialScope || undefined, }); - handleCancel(selected); + if (ui.isCancel(selected)) { + console.log("\nCommit cancelled."); + process.exit(0); + } if (selected === "__custom__") { - const custom = await text({ - message: `${label("scope", "blue")} ${textColors.pureWhite("Enter custom scope:")}`, + const custom = await ui.text({ + label: "scope", + labelColor: "blue", + message: "Enter custom scope:", placeholder: initialScope || "", initialValue: initialScope, validate: (value) => { @@ -222,7 +178,10 @@ export async function promptScope( }, }); - handleCancel(custom); + if (ui.isCancel(custom)) { + console.log("\nCommit cancelled."); + process.exit(0); + } return custom ? (custom as string) : undefined; } @@ -230,10 +189,10 @@ export async function promptScope( } // Use text input for free-form scope - const scope = await text({ - message: `${label("scope", "blue")} ${textColors.pureWhite( - `Enter scope ${isRequired ? "(required)" : "(optional)"}:`, - )}`, + const scope = await ui.text({ + label: "scope", + labelColor: "blue", + message: `Enter scope ${isRequired ? "(required)" : "(optional)"}:`, placeholder: "", initialValue: initialScope, validate: (value) => { @@ -244,7 +203,10 @@ export async function promptScope( }, }); - handleCancel(scope); + if (ui.isCancel(scope)) { + console.log("\nCommit cancelled."); + process.exit(0); + } return scope ? (scope as string) : undefined; } @@ -257,7 +219,6 @@ function validateSubject( ): ValidationError[] { const errors: ValidationError[] = []; - // Check min length if (subject.length < config.validation.subject_min_length) { errors.push({ message: `Subject too short (${subject.length} characters)`, @@ -265,7 +226,6 @@ function validateSubject( }); } - // Check max length if (subject.length > config.format.subject_max_length) { errors.push({ message: `Subject too long (${subject.length} characters)`, @@ -273,7 +233,6 @@ function validateSubject( }); } - // Check prohibited words (case-insensitive) const lowerSubject = subject.toLowerCase(); const foundWords: string[] = []; for (const word of config.validation.prohibited_words) { @@ -318,7 +277,8 @@ export async function promptSubject( return providedMessage; } - let subject: string | symbol = initialSubject || ""; + let subject: string | typeof import("../../ui/types.js").CANCEL_SYMBOL = + initialSubject || ""; let errors: ValidationError[] = []; do { @@ -334,16 +294,15 @@ export async function promptSubject( console.log(); } - subject = await text({ - message: `${label("subject", "cyan")} ${textColors.pureWhite( - `Enter commit subject (max ${config.format.subject_max_length} chars):`, - )}`, + subject = await ui.text({ + label: "subject", + labelColor: "cyan", + message: `Enter commit subject (max ${config.format.subject_max_length} chars):`, placeholder: "", initialValue: typeof subject === "string" ? subject : initialSubject, validate: (value) => { const validationErrors = validateSubject(config, value); if (validationErrors.length > 0) { - // Return first error message for inline display const firstError = validationErrors[0]; let message = firstError.message; if (firstError.context) { @@ -355,7 +314,10 @@ export async function promptSubject( }, }); - handleCancel(subject); + if (ui.isCancel(subject)) { + console.log("\nCommit cancelled."); + process.exit(0); + } if (typeof subject === "string") { errors = validateSubject(config, subject); @@ -375,7 +337,6 @@ function validateBody( const errors: ValidationError[] = []; const bodyConfig = config.format.body; - // Check required if (bodyConfig.required && !body) { errors.push({ message: "Body is required", @@ -384,12 +345,10 @@ function validateBody( return errors; } - // Skip other checks if body is empty and not required if (!body) { return errors; } - // Check min length if (body.length < bodyConfig.min_length) { errors.push({ message: `Body too short (${body.length} characters)`, @@ -397,7 +356,6 @@ function validateBody( }); } - // Check max length if (bodyConfig.max_length !== null && body.length > bodyConfig.max_length) { errors.push({ message: `Body too long (${body.length} characters)`, @@ -405,7 +363,6 @@ function validateBody( }); } - // Check prohibited words (case-insensitive) const lowerBody = body.toLowerCase(); const foundWords: string[] = []; for (const word of config.validation.prohibited_words_body) { @@ -423,6 +380,7 @@ function validateBody( return errors; } + /** * Prompt for body input with editor support */ @@ -434,8 +392,6 @@ export async function promptBody( const bodyConfig = config.format.body; const editorAvailable = detectEditor() !== null; const preference = bodyConfig.editor_preference; - - // Explicitly check if body is required (handle potential type coercion) const isRequired = bodyConfig.required === true; // If body provided via CLI flag, validate it @@ -464,19 +420,15 @@ export async function promptBody( `${attention("⚠")} ${attention("Editor not available, using inline input")}`, ); console.log(); - // Fall through to inline input } else if (preference === "editor" && editorAvailable && !isRequired) { - // Optional body with editor preference - use editor directly const edited = await promptBodyWithEditor(config, initialBody || ""); return edited || undefined; } else if (preference === "editor" && editorAvailable && isRequired) { - // Required body with editor preference - use editor with validation loop return await promptBodyRequiredWithEditor(config, initialBody); } // Inline input path if (!isRequired) { - // Optional body - offer choice if editor available and preference allows if (editorAvailable && preference === "auto") { const bodyOptions = [ { value: "inline", label: "Type inline (single/multi-line)" }, @@ -495,7 +447,7 @@ export async function promptBody( const shortcut = shortcutMapping ? getShortcutForValue(option.value, shortcutMapping) : undefined; - const label = formatLabelWithShortcut( + const optionLabel = formatLabelWithShortcut( option.label, shortcut, displayHints, @@ -503,34 +455,38 @@ export async function promptBody( return { value: option.value, - label, + label: optionLabel, }; }); - const inputMethod = await selectWithShortcuts( - { - message: `${label("body", "yellow")} ${textColors.pureWhite("Enter commit body (optional):")}`, - options, - }, - shortcutMapping, - ); + const inputMethod = await ui.select({ + label: "body", + labelColor: "yellow", + message: "Enter commit body (optional):", + options, + shortcuts: shortcutMapping, + }); - handleCancel(inputMethod); + if (ui.isCancel(inputMethod)) { + console.log("\nCommit cancelled."); + process.exit(0); + } if (inputMethod === "skip") { return undefined; } else if (inputMethod === "editor") { return await promptBodyWithEditor(config, initialBody || ""); } - // Fall through to inline } - const body = await text({ - message: `${label("body", "yellow")} ${textColors.pureWhite("Enter commit body (optional):")}`, + const body = await ui.text({ + label: "body", + labelColor: "yellow", + message: "Enter commit body (optional):", placeholder: "Press Enter to skip", initialValue: initialBody, validate: (value) => { - if (!value) return undefined; // Empty is OK if optional + if (!value) return undefined; const errors = validateBody(config, value); if (errors.length > 0) { return errors[0].message; @@ -539,12 +495,16 @@ export async function promptBody( }, }); - handleCancel(body); + if (ui.isCancel(body)) { + console.log("\nCommit cancelled."); + process.exit(0); + } return body ? (body as string) : undefined; } // Required body - let body: string | symbol = initialBody || ""; + let body: string | typeof import("../../ui/types.js").CANCEL_SYMBOL = + initialBody || ""; let errors: ValidationError[] = []; do { @@ -560,7 +520,6 @@ export async function promptBody( console.log(); } - // For required body, offer editor option if available and preference allows if (editorAvailable && (preference === "auto" || preference === "inline")) { const bodyOptions = [ { value: "inline", label: "Type inline" }, @@ -578,7 +537,7 @@ export async function promptBody( const shortcut = shortcutMapping ? getShortcutForValue(option.value, shortcutMapping) : undefined; - const label = formatLabelWithShortcut( + const optionLabel = formatLabelWithShortcut( option.label, shortcut, displayHints, @@ -586,21 +545,22 @@ export async function promptBody( return { value: option.value, - label, + label: optionLabel, }; }); - const inputMethod = await selectWithShortcuts( - { - message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, - )}`, - options, - }, - shortcutMapping, - ); + const inputMethod = await ui.select({ + label: "body", + labelColor: "yellow", + message: `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, + options, + shortcuts: shortcutMapping, + }); - handleCancel(inputMethod); + if (ui.isCancel(inputMethod)) { + console.log("\nCommit cancelled."); + process.exit(0); + } if (inputMethod === "editor") { const editorBody = await promptBodyWithEditor( @@ -610,15 +570,13 @@ export async function promptBody( if (editorBody !== null && editorBody !== undefined) { body = editorBody; } else { - // Editor cancelled or failed, continue loop continue; } } else { - // Inline input - body = await text({ - message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, - )}`, + body = await ui.text({ + label: "body", + labelColor: "yellow", + message: `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, placeholder: "", initialValue: typeof body === "string" ? body : initialBody, validate: (value) => { @@ -630,14 +588,16 @@ export async function promptBody( }, }); - handleCancel(body); + if (ui.isCancel(body)) { + console.log("\nCommit cancelled."); + process.exit(0); + } } } else { - // No editor choice, just inline - body = await text({ - message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, - )}`, + body = await ui.text({ + label: "body", + labelColor: "yellow", + message: `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, placeholder: "", initialValue: typeof body === "string" ? body : initialBody, validate: (value) => { @@ -649,7 +609,10 @@ export async function promptBody( }, }); - handleCancel(body); + if (ui.isCancel(body)) { + console.log("\nCommit cancelled."); + process.exit(0); + } } if (typeof body === "string") { @@ -686,7 +649,6 @@ async function promptBodyRequiredWithEditor( const edited = await promptBodyWithEditor(config, body); if (edited === null || edited === undefined) { - // Editor cancelled, ask what to do const bodyRetryOptions = [ { value: "retry", label: "Try editor again" }, { value: "inline", label: "Switch to inline input" }, @@ -704,7 +666,7 @@ async function promptBodyRequiredWithEditor( const shortcut = shortcutMapping ? getShortcutForValue(option.value, shortcutMapping) : undefined; - const label = formatLabelWithShortcut( + const optionLabel = formatLabelWithShortcut( option.label, shortcut, displayHints, @@ -712,29 +674,31 @@ async function promptBodyRequiredWithEditor( return { value: option.value, - label, + label: optionLabel, }; }); - const choice = await selectWithShortcuts( - { - message: `${label("body", "yellow")} ${textColors.pureWhite("Editor cancelled. What would you like to do?")}`, - options, - }, - shortcutMapping, - ); + const choice = await ui.select({ + label: "body", + labelColor: "yellow", + message: "Editor cancelled. What would you like to do?", + options, + shortcuts: shortcutMapping, + }); - handleCancel(choice); + if (ui.isCancel(choice)) { + console.log("\nCommit cancelled."); + process.exit(0); + } if (choice === "cancel") { console.log("\nCommit cancelled."); process.exit(0); } else if (choice === "inline") { - // Fall back to inline for required body - const inlineBody = await text({ - message: `${label("body", "yellow")} ${textColors.pureWhite( - `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, - )}`, + const inlineBody = await ui.text({ + label: "body", + labelColor: "yellow", + message: `Enter commit body (required${bodyConfig.min_length > 0 ? `, min ${bodyConfig.min_length} chars` : ""}):`, placeholder: "", initialValue: body, validate: (value) => { @@ -746,14 +710,16 @@ async function promptBodyRequiredWithEditor( }, }); - handleCancel(inlineBody); + if (ui.isCancel(inlineBody)) { + console.log("\nCommit cancelled."); + process.exit(0); + } if (typeof inlineBody === "string") { body = inlineBody; errors = validateBody(config, body); } - break; // Exit loop after inline input + break; } - // Otherwise continue loop (retry editor) continue; } @@ -778,14 +744,12 @@ async function promptBodyWithEditor( const edited = editInEditor(initialContent); if (edited === null) { - // Editor failed or was cancelled console.log(); console.log("⚠ Editor cancelled or unavailable, returning to prompts"); console.log(); return undefined; } - // Validate the edited content const errors = validateBody(config, edited); if (errors.length > 0) { console.log(); @@ -798,7 +762,6 @@ async function promptBodyWithEditor( } console.log(); - // Ask if user wants to re-edit or go back to inline const bodyValidationOptions = [ { value: "re-edit", label: "Edit again" }, { value: "inline", label: "Type inline instead" }, @@ -816,7 +779,7 @@ async function promptBodyWithEditor( const shortcut = shortcutMapping ? getShortcutForValue(option.value, shortcutMapping) : undefined; - const label = formatLabelWithShortcut( + const optionLabel = formatLabelWithShortcut( option.label, shortcut, displayHints, @@ -824,19 +787,22 @@ async function promptBodyWithEditor( return { value: option.value, - label, + label: optionLabel, }; }); - const choice = await selectWithShortcuts( - { - message: `${label("body", "yellow")} ${textColors.pureWhite("Validation failed. What would you like to do?")}`, - options, - }, - shortcutMapping, - ); + const choice = await ui.select({ + label: "body", + labelColor: "yellow", + message: "Validation failed. What would you like to do?", + options, + shortcuts: shortcutMapping, + }); - handleCancel(choice); + if (ui.isCancel(choice)) { + console.log("\nCommit cancelled."); + process.exit(0); + } if (choice === "cancel") { console.log("\nCommit cancelled."); @@ -844,7 +810,6 @@ async function promptBodyWithEditor( } else if (choice === "re-edit") { return await promptBodyWithEditor(config, edited); } else { - // Return undefined to trigger inline prompt return undefined; } } @@ -853,27 +818,16 @@ async function promptBodyWithEditor( } /** - * Render a line with connector (│) character at the start - * Maintains visual consistency with @clack/prompts connector lines - */ -function renderWithConnector(content: string): string { - return `│ ${content}`; -} - -/** - * Display staged files verification with connector line support - * Uses @clack/prompts log.info() to start connector, then manually - * renders connector lines for multi-line content, and ends with - * a confirmation prompt to maintain visual continuity. + * Display staged files verification */ export async function displayStagedFiles(status: { - alreadyStaged: Array<{ + alreadyStaged: ReadonlyArray<{ path: string; status: string; additions?: number; deletions?: number; }>; - newlyStaged: Array<{ + newlyStaged: ReadonlyArray<{ path: string; status: string; additions?: number; @@ -881,23 +835,24 @@ export async function displayStagedFiles(status: { }>; totalStaged: number; }): Promise { - // Start connector line using @clack/prompts - log.info( - `${label("files", "green")} ${textColors.pureWhite( - `Files to be committed (${status.totalStaged} file${status.totalStaged !== 1 ? "s" : ""}):`, - )}`, + ui.section( + "files", + "green", + `Files to be committed (${status.totalStaged} file${status.totalStaged !== 1 ? "s" : ""}):`, ); - // Group files by status const groupByStatus = ( - files: Array<{ + files: ReadonlyArray<{ path: string; status: string; additions?: number; deletions?: number; }>, ) => { - const groups: Record = { + const groups: Record< + string, + typeof files extends ReadonlyArray ? U[] : never + > = { M: [], A: [], D: [], @@ -930,7 +885,7 @@ export async function displayStagedFiles(status: { return ` (${parts.join(" ")} lines)`; }; - const formatStatusName = (status: string) => { + const formatStatusName = (statusStr: string) => { const map: Record = { M: "Modified", A: "Added", @@ -938,91 +893,70 @@ export async function displayStagedFiles(status: { R: "Renamed", C: "Copied", }; - return map[status] || status; + return map[statusStr] || statusStr; }; - /** - * Color code git status indicator to match git's default colors - */ - const colorStatusCode = (status: string): string => { - switch (status) { + const colorStatusCode = (statusStr: string): string => { + switch (statusStr) { case "A": - return textColors.gitAdded(status); + return textColors.gitAdded(statusStr); case "M": - return textColors.gitModified(status); + return textColors.gitModified(statusStr); case "D": - return textColors.gitDeleted(status); + return textColors.gitDeleted(statusStr); case "R": - return textColors.gitRenamed(status); + return textColors.gitRenamed(statusStr); case "C": - return textColors.gitCopied(status); + return textColors.gitCopied(statusStr); default: - return status; + return statusStr; } }; - // Render content with connector lines - // Empty line after header - console.log(renderWithConnector("")); + ui.blank(); // Show already staged if any if (status.alreadyStaged.length > 0) { const alreadyPlural = status.alreadyStaged.length !== 1 ? "s" : ""; - console.log( - renderWithConnector( - textColors.brightCyan( - `Already staged (${status.alreadyStaged.length} file${alreadyPlural}):`, - ), + ui.indented( + textColors.brightCyan( + `Already staged (${status.alreadyStaged.length} file${alreadyPlural}):`, ), ); const groups = groupByStatus(status.alreadyStaged); for (const [statusCode, files] of Object.entries(groups)) { if (files.length > 0) { - console.log( - renderWithConnector( - ` ${formatStatusName(statusCode)} (${files.length}):`, - ), - ); + ui.indented(` ${formatStatusName(statusCode)} (${files.length}):`); for (const file of files) { - console.log( - renderWithConnector( - ` ${colorStatusCode(file.status)} ${file.path}${formatStats(file.additions, file.deletions)}`, - ), + ui.indented( + ` ${colorStatusCode(file.status)} ${file.path}${formatStats(file.additions, file.deletions)}`, ); } } } - console.log(renderWithConnector("")); + ui.blank(); } // Show newly staged if any if (status.newlyStaged.length > 0) { const newlyPlural = status.newlyStaged.length !== 1 ? "s" : ""; - console.log( - renderWithConnector( - textColors.brightYellow( - `Auto-staged (${status.newlyStaged.length} file${newlyPlural}):`, - ), + ui.indented( + textColors.brightYellow( + `Auto-staged (${status.newlyStaged.length} file${newlyPlural}):`, ), ); const groups = groupByStatus(status.newlyStaged); for (const [statusCode, files] of Object.entries(groups)) { if (files.length > 0) { - console.log( - renderWithConnector( - ` ${formatStatusName(statusCode)} (${files.length}):`, - ), - ); + ui.indented(` ${formatStatusName(statusCode)} (${files.length}):`); for (const file of files) { - console.log( - renderWithConnector( - ` ${colorStatusCode(file.status)} ${file.path}${formatStats(file.additions, file.deletions)}`, - ), + ui.indented( + ` ${colorStatusCode(file.status)} ${file.path}${formatStats(file.additions, file.deletions)}`, ); } } } - console.log(renderWithConnector("")); + ui.blank(); } // If no separation needed, show all together @@ -1030,30 +964,23 @@ export async function displayStagedFiles(status: { const groups = groupByStatus(status.newlyStaged); for (const [statusCode, files] of Object.entries(groups)) { if (files.length > 0) { - console.log( - renderWithConnector( - ` ${formatStatusName(statusCode)} (${files.length}):`, - ), - ); + ui.indented(` ${formatStatusName(statusCode)} (${files.length}):`); for (const file of files) { - console.log( - renderWithConnector( - ` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`, - ), + ui.indented( + ` ${file.status} ${file.path}${formatStats(file.additions, file.deletions)}`, ); } } } - console.log(renderWithConnector("")); + ui.blank(); } - // Separator line with connector - console.log( - renderWithConnector("─────────────────────────────────────────────"), - ); + ui.divider(); - // Use select prompt for confirmation (maintains connector continuity) - const confirmation = await select({ + // Simple keypress wait instead of single-option select hack + const confirmation = await ui.select({ + label: "files", + labelColor: "green", message: "Press Enter to continue, Esc to cancel", options: [ { @@ -1063,20 +990,21 @@ export async function displayStagedFiles(status: { ], }); - handleCancel(confirmation); + if (ui.isCancel(confirmation)) { + console.log("\nCommit cancelled."); + process.exit(0); + } } /** - * Display commit message preview with connector line support - * Uses @clack/prompts log.info() to start connector, then manually - * renders connector lines for multi-line preview content. - * Returns the action the user selected: "commit", "edit-type", "edit-scope", "edit-subject", "edit-body", or "cancel" + * Display commit message preview + * Returns the action the user selected */ export async function displayPreview( formattedMessage: string, body: string | undefined, config?: LabcommitrConfig, - emojiModeActive: boolean = true, + _emojiModeActive: boolean = true, ): Promise< | "commit" | "edit-type" @@ -1085,35 +1013,23 @@ export async function displayPreview( | "edit-body" | "cancel" > { - // Preview shows the actual commit message as it will be stored in Git - // We don't strip emojis here because the user needs to see what will be committed - // even if their terminal doesn't support emoji display const displayMessage = formattedMessage; const displayBody = body; - // Start connector line using @clack/prompts - log.info( - `${label("preview", "green")} ${textColors.pureWhite("Commit message preview:")}`, - ); - - // Render content with connector lines - // Empty line after header - console.log(renderWithConnector("")); - console.log(renderWithConnector(textColors.brightCyan(displayMessage))); + ui.section("preview", "green", "Commit message preview:"); + ui.blank(); + ui.indented(textColors.brightCyan(displayMessage)); if (displayBody) { - console.log(renderWithConnector("")); + ui.blank(); const bodyLines = displayBody.split("\n"); - for (const line of bodyLines) { - console.log(renderWithConnector(textColors.white(line))); + for (const bodyLine of bodyLines) { + ui.indented(textColors.white(bodyLine)); } } - console.log(renderWithConnector("")); - // Separator line with connector - console.log( - renderWithConnector("─────────────────────────────────────────────"), - ); + ui.blank(); + ui.divider(); // Process shortcuts for preview prompt const previewOptions = [ @@ -1131,28 +1047,34 @@ export async function displayPreview( const displayHints = config?.advanced.shortcuts?.display_hints ?? true; - // Build options with shortcuts const options = previewOptions.map((option) => { const shortcut = shortcutMapping ? getShortcutForValue(option.value, shortcutMapping) : undefined; - const label = formatLabelWithShortcut(option.label, shortcut, displayHints); + const optionLabel = formatLabelWithShortcut( + option.label, + shortcut, + displayHints, + ); return { value: option.value, - label, + label: optionLabel, }; }); - const action = await selectWithShortcuts( - { - message: `${success("✓")} ${textColors.pureWhite("Ready to commit?")}`, - options, - }, - shortcutMapping, - ); + const action = await ui.select({ + label: "action", + labelColor: "green", + message: "Ready to commit?", + options, + shortcuts: shortcutMapping, + }); - handleCancel(action); + if (ui.isCancel(action)) { + console.log("\nCommit cancelled."); + process.exit(0); + } return action as | "commit" | "edit-type" diff --git a/src/cli/commands/init/clef.ts b/src/cli/commands/init/clef.ts index e2793cd..55e0afa 100644 --- a/src/cli/commands/init/clef.ts +++ b/src/cli/commands/init/clef.ts @@ -16,6 +16,18 @@ import { setTimeout as sleep } from "timers/promises"; import { textColors, success, attention } from "./colors.js"; +/** Color map keys for tuxedo coloring */ +type ColorKey = "B" | "W" | "P" | "G" | " "; + +/** Maps color key to its color function */ +const COLOR_MAP: Record string) | null> = { + B: textColors.tuxBlack, + W: textColors.tuxWhite, + P: textColors.tuxPink, + G: textColors.tuxGreen, + " ": null, // no color — use pureWhite +}; + interface AnimationCapabilities { supportsAnimation: boolean; supportsColor: boolean; @@ -29,20 +41,38 @@ interface AnimationCapabilities { */ class Clef { private caps: AnimationCapabilities; - private currentX: number = 0; - // Raw ASCII art frames (unprocessed) + // Raw ASCII art frames — 11 chars wide x 5 lines tall private readonly rawFrames = { - standing: ` /\\_/\\ \n ( ^.^ ) \n /| | \n(|_ |_)`, - walk1: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n(_| _|)`, - walk2: ` /\\_/\\ \n ( ^.^ ) \n /| |\\ \n(|_ |_)`, - typing: ` /\\_/\\ \n ( -.- ) \n /|⌨ | \n(_|__|_)`, - celebrate: ` /\\_/\\ \n ( ^ω^ ) \n | | \n/ \\ `, - waving: ` /\\_/\\ \n ( ^.^ )~ \n /| | \n(|_ |_)`, + standing: ` /\\_/\\\n ( o.o )\n > ^ <\n /| |\\\n(_| |_)`, + walk1: ` /\\_/\\\n ( o.o )\n > ^ <\n /| |\\\n( | _|)`, + walk2: ` /\\_/\\\n ( o.o )\n > ^ <\n /| |\\\n(_| | )`, + typing: ` /\\_/\\\n ( -.- )\n > ^ <\n /|[=]|\\\n(_|___|_)`, + celebrate: ` /\\_/\\\n ( ^w^ )\n > ^ <\n \\| |/\n / \\ / \\`, + waving: ` /\\_/\\\n ( o.o )~\n > ^ <\n /| |\\\n(_| |_)`, + earL: ` /\\_ \\\n ( o.o )\n > ^ <\n /| |\\\n(_| |_)`, + earR: ` / _/\\\n ( o.o )\n > ^ <\n /| |\\\n(_| |_)`, + surprised: ` /\\_/\\\n ( O.O )!\n > ^ <\n /| |\\\n(_| |_)`, + }; + + // Parallel color maps — each row must match the exact char count of its frame row + // B=tuxBlack (ears, body, legs), W=tuxWhite (face, bib), P=tuxPink (nose), G=tuxGreen (eyes) + // space = pureWhite fallback + private readonly rawColorMaps: Record = { + standing: ` BBBBB\n W GBG W\n B P B\n B B\nBB BB`, + walk1: ` BBBBB\n W GBG W\n B P B\n B B\nB B B`, + walk2: ` BBBBB\n W GBG W\n B P B\n B B\nBB B`, + typing: ` BBBBB\n W WBW W\n B P B\n B BBB B\nBB BBB BB`, + celebrate: ` BBBBB\n W WWW W\n B P B\n B B\n B B B B`, + waving: ` BBBBB\n W GBG WW\n B P B\n B B\nBB BB`, + earL: ` BBW B\n W GBG W\n B P B\n B B\nBB BB`, + earR: ` B WBB\n W GBG W\n B P B\n B B\nBB BB`, + surprised: ` BBBBB\n W GBG WW\n B P B\n B B\nBB BB`, }; - // Normalized frames (uniform dimensions) - private frames!: typeof this.rawFrames; + // Normalized frames and color maps (uniform dimensions) + private frames!: Record; + private colorMaps!: Record; // Frame dimensions after normalization private frameWidth = 0; @@ -67,40 +97,42 @@ class Clef { } /** - * Normalize all frames to uniform width and height + * Normalize all frames and color maps to uniform width and height * Ensures consistent alignment across all animation frames - * Critical for terminal compatibility with different fonts/dimensions */ private normalizeFrames(): void { - // Find maximum width across all frames const allLines = Object.values(this.rawFrames).flatMap((frame: string) => frame.split("\n"), ); this.frameWidth = Math.max(...allLines.map((line: string) => line.length)); - - // Find maximum height across all frames this.frameHeight = Math.max( ...Object.values(this.rawFrames).map( (frame: string) => frame.split("\n").length, ), ); - // Normalize each frame to maximum dimensions - const keyedFrames: Partial = {}; - (Object.keys(this.rawFrames) as Array).forEach( - (key) => { - const lines = this.rawFrames[key].split("\n"); - const normalized = lines.map((line: string) => - line.padEnd(this.frameWidth, " "), - ); - // Pad height if necessary - while (normalized.length < this.frameHeight) { - normalized.push(" ".repeat(this.frameWidth)); - } - keyedFrames[key] = normalized.join("\n"); - }, - ); - this.frames = keyedFrames as typeof this.rawFrames; + const normalizeString = (raw: string): string => { + const lines = raw.split("\n"); + const normalized = lines.map((line: string) => + line.padEnd(this.frameWidth, " "), + ); + while (normalized.length < this.frameHeight) { + normalized.push(" ".repeat(this.frameWidth)); + } + return normalized.join("\n"); + }; + + const keyedFrames: Record = {}; + const keyedMaps: Record = {}; + const keys = Object.keys(this.rawFrames) as Array< + keyof typeof this.rawFrames + >; + keys.forEach((key) => { + keyedFrames[key] = normalizeString(this.rawFrames[key]); + keyedMaps[key] = normalizeString(this.rawColorMaps[key]); + }); + this.frames = keyedFrames; + this.colorMaps = keyedMaps; } /** @@ -150,18 +182,37 @@ class Clef { } /** - * Render ASCII art frame at specific horizontal position - * Uses absolute cursor positioning for each line - * Adds 1 line of padding above the cat (starts at line 2) + * Render frame with per-character tuxedo coloring + * Iterates frame and color map in parallel, applying mapped colors + * Falls back to pureWhite when color is not supported */ - private renderFrame(frame: string, x: number): void { - const lines = frame.split("\n"); - lines.forEach((line, idx) => { - // Move cursor to position (row, column) - // Start at line 2 (1 line padding above) + private renderColorizedFrame(frameName: string, x: number): void { + const frame = this.frames[frameName]; + const cmap = this.colorMaps[frameName]; + if (!frame || !cmap) return; + + const frameLines = frame.split("\n"); + const cmapLines = cmap.split("\n"); + + frameLines.forEach((line, idx) => { process.stdout.write(`\x1B[${idx + 2};${x}H`); - // Make cat white for better visibility - process.stdout.write(textColors.pureWhite(line)); + const mapLine = cmapLines[idx] || ""; + let colored = ""; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (ch === " ") { + colored += " "; + continue; + } + if (!this.caps.supportsColor) { + colored += ch; + continue; + } + const key = (mapLine[i] || " ") as ColorKey; + const colorFn = COLOR_MAP[key]; + colored += colorFn ? colorFn(ch) : textColors.pureWhite(ch); + } + process.stdout.write(colored); }); } @@ -196,35 +247,31 @@ class Clef { /** * Animate legs in place without horizontal movement - * Continues until shouldContinue callback returns false + * Uses colorized rendering. Continues until shouldContinue returns false */ private async animateLegs( x: number, shouldContinue: () => boolean, ): Promise { - const frames = [this.frames.walk1, this.frames.walk2]; + const frameNames = ["walk1", "walk2"]; let frameIndex = 0; while (shouldContinue()) { - this.renderFrame(frames[frameIndex % 2], x); + this.renderColorizedFrame(frameNames[frameIndex % 2], x); frameIndex++; - await sleep(200); // Leg animation speed + await sleep(250); } } /** - * Fade out cat Houston-style - * Erases cat from bottom to top for smooth disappearance + * Fade out cat bottom-to-top + * Erases each line from bottom to top for smooth disappearance */ private async fadeOut(x: number): Promise { - const catLines = this.frames.standing.split("\n"); - - // Erase from bottom to top - // Cat starts at line 2 (1 line padding), so fade from line 5 to line 2 - for (let i = catLines.length - 1; i >= 0; i--) { + for (let i = this.frameHeight - 1; i >= 0; i--) { process.stdout.write(`\x1B[${2 + i};${x}H`); - process.stdout.write(" ".repeat(20)); // Clear line with spaces - await sleep(80); + process.stdout.write(" ".repeat(this.frameWidth + 2)); + await sleep(100); } } @@ -240,89 +287,90 @@ class Clef { ): Promise { if (!this.caps.supportsAnimation) return; - const frames = [this.frames.walk1, this.frames.walk2]; + const frameNames = ["walk1", "walk2"]; const steps = 15; const delay = duration / steps; this.hideCursor(); + try { + for (let i = 0; i <= steps; i++) { + const progress = i / steps; + const currentX = Math.floor(startX + (endX - startX) * progress); - for (let i = 0; i <= steps; i++) { - const progress = i / steps; - const currentX = Math.floor(startX + (endX - startX) * progress); + this.clearScreen(); + this.renderColorizedFrame(frameNames[i % 2], currentX); - this.clearScreen(); - this.renderFrame(frames[i % 2], currentX); - - await sleep(delay); + await sleep(delay); + } + } finally { + this.showCursor(); } - - this.showCursor(); - this.currentX = endX; } /** - * Introduction sequence - * Cat appears stationary with animated legs, text types beside it - * Houston-style: text types out, clears, new text types, then fades - * Duration: approximately 5 seconds + * Introduction sequence — pop-up with ear twitch + * 1. Instant appear 2. Ear twitch (600ms) 3. Label + typewriter (700ms) + * 4. Hold (400ms) 5. Fade-out bottom-to-top (500ms) 6. Clear + * Duration: ~2.7 seconds */ async intro(): Promise { if (!this.caps.supportsAnimation) { - // Static fallback for non-TTY environments console.log(this.frames.standing); - console.log("Hey there! My name is Clef!"); - console.log("Let me help you get started...meoww!\n"); - await sleep(2000); + console.log("Clef: Hey there! Let me help you set things up.\n"); + await sleep(1500); return; } this.hideCursor(); - this.clearScreen(); + try { + this.clearScreen(); - const catX = 1; // Start at column 1 (adds 1 column of left padding) - const textX = catX + this.frameWidth + 1; // 1 space padding after normalized frame - const labelY = 3; // Line 2 of cat output - label "Clef:" - const messageY = 4; // Line 3 of cat output - message text + const catX = 1; + const textX = catX + this.frameWidth + 2; + const labelY = 3; + const messageY = 4; - // Messages to type - const messages = [ - "Hey there! My name is Clef!", - "Let me help you get started...meoww!", - ]; + // 1. Instant appear — standing frame with tuxedo coloring + this.renderColorizedFrame("standing", catX); - // Start leg animation in background (non-blocking) - let isAnimating = true; - const animationPromise = this.animateLegs(catX, () => isAnimating); + // 2. Ear twitch — 3 frames at 200ms each + await sleep(200); + this.renderColorizedFrame("earL", catX); + await sleep(200); + this.renderColorizedFrame("earR", catX); + await sleep(200); + this.renderColorizedFrame("standing", catX); - // Write static label "Clef:" in blue - process.stdout.write(`\x1B[${labelY};${textX}H`); - process.stdout.write(textColors.labelBlue("Clef: ")); + // 3. Label appear + process.stdout.write(`\x1B[${labelY};${textX}H`); + process.stdout.write(textColors.labelBlue("Clef:")); - // Type first message on line below - await this.typeText(messages[0], textX, messageY); - await sleep(1000); + // 4. Typewriter message with concurrent leg animation + const message = "Hey there! Let me help you set things up."; + let isAnimating = true; + const animationPromise = this.animateLegs(catX, () => isAnimating); - // Clear message only (keep label) - this.clearLine(messageY, textX); - await sleep(300); + await this.typeText(message, textX, messageY, 20); - // Type second message - await this.typeText(messages[1], textX, messageY); - await sleep(1200); + // 5. Hold + await sleep(400); - // Stop leg animation - isAnimating = false; - await animationPromise; + // Stop leg animation before fade + isAnimating = false; + await animationPromise; - // Fade out cat Houston-style - await this.fadeOut(catX); + // 6. Fade-out bottom-to-top, also clear text lines + await this.fadeOut(catX); + this.clearLine(labelY, textX); + this.clearLine(messageY, textX); - // Small pause before clearing - await sleep(200); + await sleep(100); - // Clear screen for prompts - this.clearScreen(); - this.showCursor(); + // 7. Clear screen for prompts + this.clearScreen(); + } finally { + this.showCursor(); + } } /** @@ -342,9 +390,13 @@ class Clef { // Walk in from left await this.walk(0, 10, 800); - // Show typing animation + // Show typing animation with tuxedo coloring this.clearScreen(); - console.log(this.frames.typing); + const typingLines = this.frames.typing.split("\n"); + const typingCmap = this.colorMaps.typing.split("\n"); + for (let i = 0; i < typingLines.length; i++) { + console.log(this.colorizeLineToString(typingLines[i], typingCmap[i])); + } console.log(` ${attention(message)}`); // Execute actual task @@ -359,57 +411,154 @@ class Clef { this.clearScreen(); } + /** Randomized farewell messages for the outro */ + private readonly farewellMessages = [ + "You're all set! Happy committing!", + "All done! Go ship something great!", + "Configuration complete! Time to commit!", + ]; + /** - * Outro sequence - * Cat and text display side by side using normal console output - * Astro Houston-style: stays on screen as final message (no clear, no walk off) - * Duration: approximately 2 seconds + * Outro sequence — build-up from bottom + dance + farewell + * 1. Build-up celebrate frame bottom-to-top (400ms) + * 2. Dance cycle (600ms) 3. Final waving frame with farewell 4. Hold (1s) + * Duration: ~2 seconds. Output stays visible. */ async outro(): Promise { + const farewell = + this.farewellMessages[ + Math.floor(Math.random() * this.farewellMessages.length) + ]; + if (!this.caps.supportsAnimation) { - // Static fallback console.log(this.frames.waving); - console.log("You're all set! Happy committing!"); - return; // No clear - message stays visible + console.log(`Clef: ${farewell}`); + return; } - // Add spacing before outro - console.log(); + console.log(); // spacing after processing steps - // Display cat and text side by side - // Cat on left, "Clef:" label and message on right - const catLines = this.frames.waving.split("\n"); - const catX = 1; // Start at column 1 (adds 1 column of left padding) - const textX = catX + this.frameWidth + 1; // 1 space padding after cat + this.hideCursor(); + try { + // 1. Build-up from bottom — render celebrate frame line-by-line + // Print blank lines to reserve space, then use relative cursor movement + const celebrateLines = this.frames.celebrate.split("\n"); + const celebrateCmap = this.colorMaps.celebrate.split("\n"); + for (let i = 0; i < this.frameHeight; i++) { + console.log(); + } + // Move cursor up to top of the reserved area + process.stdout.write(`\x1B[${this.frameHeight}A`); + // Save cursor position at top of frame area + process.stdout.write("\x1B[s"); + + // Render from bottom to top + for (let i = this.frameHeight - 1; i >= 0; i--) { + // Restore to saved position, then move down i lines + process.stdout.write("\x1B[u"); + if (i > 0) process.stdout.write(`\x1B[${i}B`); + process.stdout.write("\r"); + this.writeColorizedLine(celebrateLines[i], celebrateCmap[i]); + await sleep(100); + } - // Display cat lines with label/message beside appropriate lines - for (let i = 0; i < catLines.length; i++) { + // 2. Quick dance — 3 frames at 200ms using relative positioning + const danceSequence = ["celebrate", "waving", "waving"]; + for (const frameName of danceSequence) { + const lines = this.frames[frameName].split("\n"); + const clines = this.colorMaps[frameName].split("\n"); + for (let i = 0; i < this.frameHeight; i++) { + process.stdout.write("\x1B[u"); + if (i > 0) process.stdout.write(`\x1B[${i}B`); + process.stdout.write("\r\x1B[K"); // move to col 1 and clear line + this.writeColorizedLine(lines[i], clines[i]); + } + await sleep(200); + } + + // 3. Clear the dance area and reprint final frame via console.log (persists in scrollback) + for (let i = 0; i < this.frameHeight; i++) { + process.stdout.write("\x1B[u"); + if (i > 0) process.stdout.write(`\x1B[${i}B`); + process.stdout.write("\r\x1B[K"); + } + // Move cursor back to top of frame area + process.stdout.write("\x1B[u\r"); + } finally { + this.showCursor(); + } + + // Print final waving frame with side text via console.log (persists in scrollback) + const wavingLines = this.frames.waving.split("\n"); + const wavingCmap = this.colorMaps.waving.split("\n"); + for (let i = 0; i < wavingLines.length; i++) { + let line = this.colorizeLineToString(wavingLines[i], wavingCmap[i]); if (i === 1) { - // Line 1: Face line - display "Clef:" label - console.log( - textColors.pureWhite(catLines[i]) + - " " + - textColors.labelBlue("Clef:"), - ); + line += " " + textColors.labelBlue("Clef:"); } else if (i === 2) { - // Line 2: Body line - display message text - console.log( - textColors.pureWhite(catLines[i]) + - " " + - textColors.pureWhite("You're all set! Happy committing!"), - ); - } else { - // Other lines: just the cat in white - console.log(textColors.pureWhite(catLines[i])); + line += " " + textColors.pureWhite(farewell); + } + console.log(line); + } + + console.log(); + + // 4. Hold — let user read the message + await sleep(1000); + } + + /** + * Build a colorized line string from a frame line and its color map line + */ + private colorizeLineToString(line: string, mapLine: string): string { + let colored = ""; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (ch === " ") { + colored += " "; + continue; + } + if (!this.caps.supportsColor) { + colored += ch; + continue; } + const key = (mapLine?.[i] || " ") as ColorKey; + const colorFn = COLOR_MAP[key]; + colored += colorFn ? colorFn(ch) : textColors.pureWhite(ch); } + return colored; + } - console.log(); // Extra line at end + /** Write a single colorized line to stdout (no newline) */ + private writeColorizedLine(line: string, mapLine: string): void { + process.stdout.write(this.colorizeLineToString(line, mapLine)); + } - // Small pause to let user see the message - await sleep(1500); + /** One-line success reaction: cat face + message */ + successReaction(message: string): void { + if (!this.caps.supportsColor) { + console.log(`\u2713 ${message}`); + return; + } + console.log(` ( ^w^ ) ${success("\u2713")} ${message}`); + } + + /** One-line error reaction: cat face + message */ + errorReaction(message: string): void { + if (!this.caps.supportsColor) { + console.error(`\u2717 ${message}`); + return; + } + console.error(` ( O.O ) \u2717 ${message}`); + } - // Done - cat and message remain visible (no clear, no cursor hide) + /** One-line nudge: cat face + hint */ + nudge(message: string): void { + if (!this.caps.supportsColor) { + console.log(message); + return; + } + console.log(` ( o.o ) ${message}`); } /** diff --git a/src/cli/commands/init/colors.ts b/src/cli/commands/init/colors.ts index 1f01723..c25b7e7 100644 --- a/src/cli/commands/init/colors.ts +++ b/src/cli/commands/init/colors.ts @@ -12,20 +12,24 @@ */ /** - * Bright background colors for step labels - * Uses ANSI 256-color palette for vibrant, saturated colors - * Text is black (30m) for maximum contrast + * Distinct background colors for step labels + * Each label uses a unique ANSI 256-color for visual distinction. + * Text is bright white (97m) for readability on deeper backgrounds. + * + * | Name | ANSI 256 | Hex | Used by | + * |---------|----------|---------|----------| + * | magenta | 134 | #af5fd7 | preset | + * | cyan | 37 | #00afaf | emoji | + * | blue | 33 | #0087ff | signing | + * | yellow | 172 | #d78700 | stage | + * | green | 35 | #00af5f | body | */ export const labelColors = { - /** - * Uniform Light Blue (#77c0f7) - Consistent label color - * Used for all step labels for unified appearance - */ - bgBrightMagenta: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, - bgBrightCyan: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, - bgBrightBlue: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, - bgBrightYellow: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, - bgBrightGreen: (text: string) => `\x1b[48;5;117m\x1b[30m${text}\x1b[0m`, + bgBrightMagenta: (text: string) => `\x1b[48;5;134m\x1b[97m${text}\x1b[0m`, + bgBrightCyan: (text: string) => `\x1b[48;5;37m\x1b[97m${text}\x1b[0m`, + bgBrightBlue: (text: string) => `\x1b[48;5;33m\x1b[97m${text}\x1b[0m`, + bgBrightYellow: (text: string) => `\x1b[48;5;172m\x1b[97m${text}\x1b[0m`, + bgBrightGreen: (text: string) => `\x1b[48;5;35m\x1b[97m${text}\x1b[0m`, }; /** @@ -81,6 +85,18 @@ export const textColors = { */ labelBlue: (text: string) => `\x1b[38;5;75m${text}\x1b[0m`, + /** + * Tuxedo Cat Colors - For Clef's tuxedo pattern + */ + // Dark grey (visible on dark terminals) - ears outer, body sides, legs + tuxBlack: (text: string) => `\x1b[38;5;236m${text}\x1b[0m`, + // Near-white for bib/face + tuxWhite: (text: string) => `\x1b[38;5;255m${text}\x1b[0m`, + // Pink for nose (^) + tuxPink: (text: string) => `\x1b[38;5;218m${text}\x1b[0m`, + // Green for eyes (o) + tuxGreen: (text: string) => `\x1b[38;5;120m${text}\x1b[0m`, + /** * Git Status Colors - Match git's default color scheme */ diff --git a/src/cli/commands/init/gpg.ts b/src/cli/commands/init/gpg.ts new file mode 100644 index 0000000..e3fefef --- /dev/null +++ b/src/cli/commands/init/gpg.ts @@ -0,0 +1,466 @@ +/** + * GPG Capabilities Detection and Setup + * + * Provides centralized GPG detection, package manager detection, + * and configuration utilities for commit signing setup. + * + * Detection Flow: + * 1. Check if GPG is installed (gpg --version) + * 2. Check for existing signing keys (gpg --list-secret-keys) + * 3. Check if Git is configured for signing (git config user.signingkey) + * 4. Determine overall capability state + */ + +import { spawnSync } from "child_process"; +import { platform } from "os"; + +// ============================================================================ +// Type Definitions +// ============================================================================ + +/** + * GPG capability states representing the user's signing readiness + */ +export type GpgState = + | "fully_configured" // GPG + keys + Git configured + | "partial_config" // GPG + keys, Git not configured + | "no_keys" // GPG installed, no keys + | "not_installed"; // GPG not found + +/** + * Detailed GPG capabilities information + */ +export interface GpgCapabilities { + state: GpgState; + gpgInstalled: boolean; + gpgVersion: string | null; + keysExist: boolean; + keyId: string | null; + keyEmail: string | null; + gitConfigured: boolean; + gitSigningKey: string | null; +} + +/** + * Supported package managers for GPG installation + */ +export type PackageManager = + | "brew" // macOS + | "apt" // Debian/Ubuntu + | "dnf" // Fedora/RHEL + | "pacman" // Arch + | "winget" // Windows 10+ + | "choco" // Windows (Chocolatey) + | null; // None detected + +/** + * Platform-specific installation information + */ +export interface PlatformInfo { + os: "darwin" | "linux" | "win32" | "unknown"; + packageManager: PackageManager; + installCommand: string | null; + manualInstallUrl: string; +} + +// ============================================================================ +// Constants +// ============================================================================ + +/** + * Install commands for each package manager + */ +const INSTALL_COMMANDS: Record, string> = { + brew: "brew install gnupg", + apt: "sudo apt-get install gnupg", + dnf: "sudo dnf install gnupg2", + pacman: "sudo pacman -S gnupg", + winget: "winget install GnuPG.GnuPG", + choco: "choco install gnupg", +}; + +/** + * Manual installation URL for GPG + */ +const MANUAL_INSTALL_URL = "https://gnupg.org/download/"; + +/** + * Timeout for detection commands (5 seconds) + */ +const DETECTION_TIMEOUT = 5000; + +/** + * Timeout for key generation (2 minutes) + */ +const KEY_GENERATION_TIMEOUT = 120000; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Execute a command and return the result + * Uses spawnSync with timeout for safety + */ +function execCommand( + command: string, + args: string[], + timeout: number = DETECTION_TIMEOUT, +): { success: boolean; output: string; stderr: string } { + try { + const result = spawnSync(command, args, { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + timeout, + }); + + if (result.error) { + return { success: false, output: "", stderr: result.error.message }; + } + + return { + success: result.status === 0, + output: result.stdout?.toString().trim() || "", + stderr: result.stderr?.toString().trim() || "", + }; + } catch { + return { success: false, output: "", stderr: "Command execution failed" }; + } +} + +/** + * Check if a command exists on the system + * Uses platform-specific detection method + */ +function commandExists(command: string): boolean { + const os = platform(); + + if (os === "win32") { + // Windows: use 'where' command + const result = execCommand("where", [command]); + return result.success; + } else { + // Unix-like: use 'command -v' via shell + const result = spawnSync("sh", ["-c", `command -v ${command}`], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + timeout: DETECTION_TIMEOUT, + }); + return result.status === 0; + } +} + +// ============================================================================ +// GPG Detection Functions +// ============================================================================ + +/** + * Detect GPG installation and version + */ +function detectGpgInstallation(): { installed: boolean; version: string | null } { + const result = execCommand("gpg", ["--version"]); + + if (!result.success) { + return { installed: false, version: null }; + } + + // Parse version from first line: "gpg (GnuPG) 2.2.41" or "gpg (GnuPG/MacGPG2) 2.2.41" + const firstLine = result.output.split("\n")[0] || ""; + const versionMatch = firstLine.match(/(\d+\.\d+\.\d+)/); + const version = versionMatch ? versionMatch[1] : null; + + return { installed: true, version }; +} + +/** + * Detect existing GPG signing keys + */ +function detectGpgKeys(): { + keysExist: boolean; + keyId: string | null; + keyEmail: string | null; +} { + const result = execCommand("gpg", [ + "--list-secret-keys", + "--keyid-format", + "LONG", + ]); + + if (!result.success || !result.output) { + return { keysExist: false, keyId: null, keyEmail: null }; + } + + // Parse key ID from "sec" line: "sec rsa4096/AF08DCD261E298CB 2024-11-16" + const secMatch = result.output.match(/sec\s+\w+\/([A-F0-9]+)/i); + const keyId = secMatch ? secMatch[1] : null; + + // Parse email from "uid" line: "uid [ultimate] Name " + const uidMatch = result.output.match(/uid\s+.*<([^>]+)>/); + const keyEmail = uidMatch ? uidMatch[1] : null; + + return { + keysExist: !!keyId, + keyId, + keyEmail, + }; +} + +/** + * Check if Git is configured for commit signing + */ +function detectGitSigningConfig(): { + configured: boolean; + signingKey: string | null; +} { + const result = execCommand("git", ["config", "--get", "user.signingkey"]); + + if (!result.success || !result.output) { + return { configured: false, signingKey: null }; + } + + return { + configured: true, + signingKey: result.output, + }; +} + +/** + * Detect complete GPG capabilities + * Main entry point for GPG detection + */ +export function detectGpgCapabilities(): GpgCapabilities { + // Step 1: Check GPG installation + const gpgInstall = detectGpgInstallation(); + + if (!gpgInstall.installed) { + return { + state: "not_installed", + gpgInstalled: false, + gpgVersion: null, + keysExist: false, + keyId: null, + keyEmail: null, + gitConfigured: false, + gitSigningKey: null, + }; + } + + // Step 2: Check for signing keys + const keys = detectGpgKeys(); + + if (!keys.keysExist) { + return { + state: "no_keys", + gpgInstalled: true, + gpgVersion: gpgInstall.version, + keysExist: false, + keyId: null, + keyEmail: null, + gitConfigured: false, + gitSigningKey: null, + }; + } + + // Step 3: Check Git configuration + const gitConfig = detectGitSigningConfig(); + + if (!gitConfig.configured) { + return { + state: "partial_config", + gpgInstalled: true, + gpgVersion: gpgInstall.version, + keysExist: true, + keyId: keys.keyId, + keyEmail: keys.keyEmail, + gitConfigured: false, + gitSigningKey: null, + }; + } + + // Fully configured + return { + state: "fully_configured", + gpgInstalled: true, + gpgVersion: gpgInstall.version, + keysExist: true, + keyId: keys.keyId, + keyEmail: keys.keyEmail, + gitConfigured: true, + gitSigningKey: gitConfig.signingKey, + }; +} + +// ============================================================================ +// Package Manager Detection +// ============================================================================ + +/** + * Detect available package manager for GPG installation + */ +export function detectPackageManager(): PlatformInfo { + const os = platform(); + const manualInstallUrl = MANUAL_INSTALL_URL; + + // Map Node.js platform to our OS type + let detectedOs: PlatformInfo["os"]; + switch (os) { + case "darwin": + detectedOs = "darwin"; + break; + case "linux": + detectedOs = "linux"; + break; + case "win32": + detectedOs = "win32"; + break; + default: + detectedOs = "unknown"; + } + + // Detect package manager based on platform + let packageManager: PackageManager = null; + + switch (detectedOs) { + case "darwin": + // macOS: check for Homebrew + if (commandExists("brew")) { + packageManager = "brew"; + } + break; + + case "linux": + // Linux: check in order of popularity + if (commandExists("apt-get")) { + packageManager = "apt"; + } else if (commandExists("dnf")) { + packageManager = "dnf"; + } else if (commandExists("pacman")) { + packageManager = "pacman"; + } + break; + + case "win32": + // Windows: check winget first (built-in on Windows 10+), then Chocolatey + if (commandExists("winget")) { + packageManager = "winget"; + } else if (commandExists("choco")) { + packageManager = "choco"; + } + break; + } + + // Get install command if package manager found + const installCommand = packageManager + ? INSTALL_COMMANDS[packageManager] + : null; + + return { + os: detectedOs, + packageManager, + installCommand, + manualInstallUrl, + }; +} + +// ============================================================================ +// GPG Configuration Functions +// ============================================================================ + +/** + * Configure Git to use a specific GPG key for signing + * Sets both user.signingkey and commit.gpgsign globally + */ +export function configureGitSigning(keyId: string): boolean { + // Set the signing key + const keyResult = execCommand("git", [ + "config", + "--global", + "user.signingkey", + keyId, + ]); + + if (!keyResult.success) { + return false; + } + + // Enable commit signing by default + const signResult = execCommand("git", [ + "config", + "--global", + "commit.gpgsign", + "true", + ]); + + return signResult.success; +} + +/** + * Generate a new GPG key using the user's Git identity + * Uses gpg --quick-generate-key for non-interactive generation + */ +export async function generateGpgKey(): Promise { + // Get user info from Git config + const nameResult = execCommand("git", ["config", "--get", "user.name"]); + const emailResult = execCommand("git", ["config", "--get", "user.email"]); + + const name = nameResult.output.trim(); + const email = emailResult.output.trim(); + + if (!name || !email) { + console.error("\n Git user.name and user.email must be configured first."); + console.error(' Run: git config --global user.name "Your Name"'); + console.error(' git config --global user.email "you@example.com"'); + return false; + } + + console.log(`\n Generating GPG key for: ${name} <${email}>`); + console.log(" This may take a moment...\n"); + + // Use gpg --quick-generate-key for non-interactive generation + // RSA 4096-bit key, sign-only capability, expires in 2 years + const result = spawnSync( + "gpg", + [ + "--batch", + "--quick-generate-key", + `${name} <${email}>`, + "rsa4096", + "sign", + "2y", + ], + { + encoding: "utf-8", + stdio: ["inherit", "pipe", "pipe"], // inherit stdin for passphrase + timeout: KEY_GENERATION_TIMEOUT, + }, + ); + + if (result.status === 0) { + console.log(" GPG key generated successfully!\n"); + return true; + } else { + const errorMsg = result.stderr?.toString().trim() || "Unknown error"; + console.error(`\n Failed to generate GPG key: ${errorMsg}\n`); + return false; + } +} + +/** + * Test if GPG signing actually works with the configured key + * Useful for verifying the setup is complete + */ +export function testGpgSigning(keyId: string): boolean { + // Create a test signature + const result = spawnSync( + "gpg", + ["--batch", "--yes", "--clearsign", "--default-key", keyId], + { + input: "test", + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + timeout: DETECTION_TIMEOUT, + }, + ); + + return result.status === 0; +} diff --git a/src/cli/commands/init/index.ts b/src/cli/commands/init/index.ts index e07f433..0c2ff4f 100644 --- a/src/cli/commands/init/index.ts +++ b/src/cli/commands/init/index.ts @@ -22,8 +22,19 @@ import { promptEmoji, promptAutoStage, promptBodyRequired, + promptSignCommits, + promptGpgSetup, + promptKeyGeneration, + displayGpgStatus, + displayInstallInstructions, displayProcessingSteps, } from "./prompts.js"; +import { + detectGpgCapabilities, + detectPackageManager, + configureGitSigning, + generateGpgKey, +} from "./gpg.js"; import { buildConfig, getPreset } from "../../../lib/presets/index.js"; import { generateConfigFile } from "./config-generator.js"; import { Logger } from "../../../lib/logger.js"; @@ -99,7 +110,7 @@ async function initAction(options: { // Screen is now completely clear // Prompts: Clean labels, no cat - // Note: @clack/prompts clears each prompt after selection (their default behavior) + // Note: Prompts collapse to single line after selection const presetId = options.preset || (await promptPreset()); getPreset(presetId); @@ -107,6 +118,65 @@ async function initAction(options: { const autoStage = await promptAutoStage(); const bodyRequired = await promptBodyRequired(); + // === GPG Detection Phase === + const gpgCapabilities = detectGpgCapabilities(); + let signCommits = false; + + // Display current GPG status (returns line count for compaction) + const gpgStatusLines = displayGpgStatus(gpgCapabilities); + + switch (gpgCapabilities.state) { + case "fully_configured": { + // User has working GPG - ask if they want to enable + signCommits = await promptSignCommits( + gpgCapabilities.state, + gpgStatusLines, + ); + break; + } + + case "partial_config": { + // GPG and keys exist, just need Git config + const configure = await promptSignCommits( + gpgCapabilities.state, + gpgStatusLines, + ); + if (configure && gpgCapabilities.keyId) { + configureGitSigning(gpgCapabilities.keyId); + signCommits = true; + } + break; + } + + case "no_keys": { + // GPG installed but no keys + const action = await promptKeyGeneration(gpgStatusLines); + if (action === "generate") { + const success = await generateGpgKey(); + if (success) { + // Re-detect after key generation + const updated = detectGpgCapabilities(); + if (updated.keyId) { + configureGitSigning(updated.keyId); + signCommits = true; + } + } + } + break; + } + + case "not_installed": { + // GPG not found + const platformInfo = detectPackageManager(); + const action = await promptGpgSetup(platformInfo, gpgStatusLines); + if (action === "install") { + displayInstallInstructions(platformInfo); + // signCommits stays false - user will re-run init after installing + } + break; + } + } + // Small pause before processing await new Promise((resolve) => setTimeout(resolve, 800)); @@ -119,6 +189,7 @@ async function initAction(options: { scope: "optional", autoStage, bodyRequired, + signCommits, }); // Show title "Labcommitr initializing..." diff --git a/src/cli/commands/init/prompts.ts b/src/cli/commands/init/prompts.ts index 7bffcca..7061554 100644 --- a/src/cli/commands/init/prompts.ts +++ b/src/cli/commands/init/prompts.ts @@ -8,70 +8,20 @@ * Label pattern: [colored label] [2 spaces] [content] */ -import { select, multiselect, isCancel } from "@clack/prompts"; -import { - labelColors, - textColors, - success, - info, - attention, - highlight, -} from "./colors.js"; - -/** - * Create compact color-coded label - * Labels are 9 characters wide (7 chars + 2 padding spaces) for alignment - * Uses bright ANSI 256 colors for high visibility - * Text is centered within the label - */ -function label( - text: string, - color: "magenta" | "cyan" | "blue" | "yellow" | "green", -): string { - const colorFn = { - magenta: labelColors.bgBrightMagenta, - cyan: labelColors.bgBrightCyan, - blue: labelColors.bgBrightBlue, - yellow: labelColors.bgBrightYellow, - green: labelColors.bgBrightGreen, - }[color]; - - // Center text within 7-character width (accommodates all current labels) - // For visual centering: when padding is odd, put extra space on LEFT for better balance - const width = 7; - const textLength = Math.min(text.length, width); // Cap at width - const padding = width - textLength; - // For odd padding (1, 3, 5...), ceil puts extra space on LEFT (better visual weight) - // For even padding (2, 4, 6...), floor/ceil both work the same - const leftPad = Math.ceil(padding / 2); - const rightPad = padding - leftPad; - const centeredText = - " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); - - return colorFn(` ${centeredText} `); -} - -/** - * Handle prompt cancellation - * Exits process gracefully when user cancels - */ -function handleCancel(value: unknown): void { - if (isCancel(value)) { - console.log("\nSetup cancelled."); - process.exit(0); - } -} +import { ui } from "../../ui/index.js"; +import { textColors, success, attention, highlight } from "./colors.js"; +import type { GpgState, GpgCapabilities, PlatformInfo } from "./gpg.js"; +import { getAvailableWidth, truncateForPrompt } from "../../utils/terminal.js"; /** * Preset option data structure * Keeps descriptions for future use while labels only show examples */ -const PRESET_OPTIONS: Array<{ +const PRESET_OPTIONS: ReadonlyArray<{ value: string; name: string; description: string; example: string; - hint?: string; }> = [ { value: "conventional", @@ -82,9 +32,9 @@ const PRESET_OPTIONS: Array<{ { value: "angular", name: "Angular Convention", - description: "Strict format used by Angular and enterprise teams.", + description: + "Strict format used by Angular and enterprise teams. Includes perf, build, ci types.", example: "perf(compiler): optimize template parsing", - hint: "Includes perf, build, ci types", }, { value: "minimal", @@ -98,17 +48,40 @@ const PRESET_OPTIONS: Array<{ * Prompt for commit style preset selection */ export async function promptPreset(): Promise { - const preset = await select({ - message: `${label("preset", "magenta")} ${textColors.pureWhite("Which commit style fits your project?")}`, - options: PRESET_OPTIONS.map((option) => ({ + const hintParens = 4; + const totalAvailable = getAvailableWidth(); + + const options = PRESET_OPTIONS.map((option) => { + const labelText = option.name; + const hintText = `e.g., ${option.example}`; + const combinedLength = labelText.length + hintText.length + hintParens; + + const truncatedHint = + combinedLength > totalAvailable + ? truncateForPrompt( + hintText, + totalAvailable - labelText.length - hintParens, + ) + : hintText; + + return { value: option.value, - label: `${option.name} - e.g., ${option.example}`, - ...(option.hint && { hint: option.hint }), // Include hint if present - // description is kept in PRESET_OPTIONS for future use - })), + label: labelText, + hint: truncatedHint, + }; + }); + + const preset = await ui.select({ + label: "preset", + labelColor: "magenta", + message: "Which commit style fits your project?", + options, }); - handleCancel(preset); + if (ui.isCancel(preset)) { + console.log("\nSetup cancelled."); + process.exit(0); + } return preset as string; } @@ -116,8 +89,10 @@ export async function promptPreset(): Promise { * Prompt for emoji support preference */ export async function promptEmoji(): Promise { - const emoji = await select({ - message: `${label("emoji", "cyan")} ${textColors.pureWhite("Enable emoji support in commits?")}`, + const emoji = await ui.select({ + label: "emoji", + labelColor: "cyan", + message: "Enable emoji support in commits?", options: [ { value: false, @@ -132,7 +107,10 @@ export async function promptEmoji(): Promise { ], }); - handleCancel(emoji); + if (ui.isCancel(emoji)) { + console.log("\nSetup cancelled."); + process.exit(0); + } return emoji as boolean; } @@ -141,8 +119,10 @@ export async function promptEmoji(): Promise { * When enabled, stages modified/deleted tracked files automatically (git add -u) */ export async function promptAutoStage(): Promise { - const autoStage = await select({ - message: `${label("stage", "yellow")} ${textColors.pureWhite("Stage files automatically?")}`, + const autoStage = await ui.select({ + label: "stage", + labelColor: "yellow", + message: "Stage files automatically?", options: [ { value: false, @@ -157,7 +137,10 @@ export async function promptAutoStage(): Promise { ], }); - handleCancel(autoStage); + if (ui.isCancel(autoStage)) { + console.log("\nSetup cancelled."); + process.exit(0); + } return autoStage as boolean; } @@ -166,8 +149,10 @@ export async function promptAutoStage(): Promise { * When enabled, commit body becomes required during commit creation */ export async function promptBodyRequired(): Promise { - const bodyRequired = await select({ - message: `${label("body", "green")} ${textColors.pureWhite("Require commit body?")}`, + const bodyRequired = await ui.select({ + label: "body", + labelColor: "green", + message: "Require commit body?", options: [ { value: true, @@ -182,7 +167,10 @@ export async function promptBodyRequired(): Promise { ], }); - handleCancel(bodyRequired); + if (ui.isCancel(bodyRequired)) { + console.log("\nSetup cancelled."); + process.exit(0); + } return bodyRequired as boolean; } @@ -192,8 +180,10 @@ export async function promptBodyRequired(): Promise { export async function promptScope(): Promise< "optional" | "selective" | "always" | "never" > { - const scope = await select({ - message: `${label("scope", "blue")} ${textColors.pureWhite("How should scopes work?")}`, + const scope = await ui.select({ + label: "scope", + labelColor: "blue", + message: "How should scopes work?", options: [ { value: "optional", @@ -218,7 +208,10 @@ export async function promptScope(): Promise< ], }); - handleCancel(scope); + if (ui.isCancel(scope)) { + console.log("\nSetup cancelled."); + process.exit(0); + } return scope as "optional" | "selective" | "always" | "never"; } @@ -227,10 +220,12 @@ export async function promptScope(): Promise< * Only shown when user selects "selective" scope mode */ export async function promptScopeTypes( - types: Array<{ id: string; description: string }>, + types: ReadonlyArray<{ id: string; description: string }>, ): Promise { - const selected = await multiselect({ - message: `${label("types", "blue")} ${textColors.pureWhite("Which types require a scope?")}`, + const selected = await ui.multiselect({ + label: "types", + labelColor: "blue", + message: "Which types require a scope?", options: types.map((type) => ({ value: type.id, label: type.id, @@ -239,14 +234,16 @@ export async function promptScopeTypes( required: false, }); - handleCancel(selected); + if (ui.isCancel(selected)) { + console.log("\nSetup cancelled."); + process.exit(0); + } return selected as string[]; } /** * Display completed prompts in compact form (Astro pattern) - * Shows what the user selected after @clack/prompts clears itself - * This simulates keeping prompts visible on screen + * Shows what the user selected after prompts clear themselves */ export function displayCompletedPrompts(config: { preset: string; @@ -254,44 +251,38 @@ export function displayCompletedPrompts(config: { scope: string; }): void { console.log( - `${label("preset", "magenta")} ${textColors.brightCyan(config.preset)}`, + `${ui.label("preset", "magenta")} ${textColors.brightCyan(config.preset)}`, ); console.log( - `${label("emoji", "cyan")} ${textColors.brightCyan(config.emoji ? "Yes" : "No")}`, + `${ui.label("emoji", "cyan")} ${textColors.brightCyan(config.emoji ? "Yes" : "No")}`, ); console.log( - `${label("scope", "blue")} ${textColors.brightCyan(config.scope)}`, + `${ui.label("scope", "blue")} ${textColors.brightCyan(config.scope)}`, ); - console.log(); // Extra line + console.log(); } /** * Display processing steps as compact checklist (Astro-style) * Shows what's happening during config generation - * Each step executes its task and displays success when complete */ export async function displayProcessingSteps( - steps: Array<{ message: string; task: () => Promise }>, + steps: ReadonlyArray<{ message: string; task: () => Promise }>, ): Promise { for (const step of steps) { - // Show pending state with spinning indicator process.stdout.write(` ${textColors.brightCyan("◐")} ${step.message}...`); - - // Execute task await step.task(); - - // Clear line and show success checkmark - process.stdout.write("\r"); // Return to start of line + process.stdout.write("\r"); console.log(` ${success("✔")} ${step.message}`); } - console.log(); // Extra newline after all steps + console.log(); } /** * Display configuration file write result */ export function displayConfigResult(filename: string): void { - console.log(`${label("config", "green")} Writing ${highlight(filename)}`); + console.log(`${ui.label("config", "green")} Writing ${highlight(filename)}`); console.log(` ${success("Done")}\n`); } @@ -301,7 +292,7 @@ export function displayConfigResult(filename: string): void { export function displayNextSteps(): void { console.log(`${success("✓ Ready to commit!")}\n`); console.log( - `${label("next", "yellow")} ${attention("Get started with these commands:")}\n`, + `${ui.label("next", "yellow")} ${attention("Get started with these commands:")}\n`, ); console.log( ` ${textColors.brightCyan("lab config show")} View your configuration`, @@ -313,3 +304,224 @@ export function displayNextSteps(): void { ` ${textColors.brightYellow("Customize anytime by editing .labcommitr.config.yaml")}\n`, ); } + +// ============================================================================ +// GPG Signing Prompts +// ============================================================================ + +/** + * Display GPG capabilities status + * Returns the number of lines written (for compaction via prefixLineCount) + */ +export function displayGpgStatus(capabilities: GpgCapabilities): number { + let lineCount = 0; + + ui.section("signing", "blue", "GPG signing capabilities"); + lineCount++; + + if (capabilities.gpgInstalled) { + const version = capabilities.gpgVersion + ? ` (${capabilities.gpgVersion})` + : ""; + ui.status.success(`GPG installed${version}`); + lineCount++; + } else { + ui.status.error("GPG not installed"); + lineCount++; + return lineCount; + } + + if (capabilities.keysExist && capabilities.keyId) { + const shortKeyId = capabilities.keyId.slice(-8); + ui.status.success(`Signing key: ${shortKeyId}...`); + lineCount++; + } else { + ui.status.error("No signing keys found"); + lineCount++; + } + + if (capabilities.gitConfigured) { + ui.status.success("Git configured for signing"); + lineCount++; + } else if (capabilities.keysExist) { + ui.status.error("Git not configured for signing"); + lineCount++; + } + + return lineCount; +} + +/** + * Prompt for enabling commit signing + */ +export async function promptSignCommits( + state: GpgState, + prefixLineCount = 0, +): Promise { + if (state === "fully_configured") { + const signCommits = await ui.select({ + label: "signing", + labelColor: "blue", + message: "Enable GPG commit signing?", + options: [ + { + value: true, + label: "Yes (Recommended)", + hint: "Sign all commits with your GPG key", + }, + { + value: false, + label: "No", + hint: "Skip signing", + }, + ], + prefixLineCount, + }); + + if (ui.isCancel(signCommits)) { + console.log("\nSetup cancelled."); + process.exit(0); + } + return signCommits as boolean; + } + + const configure = await ui.select({ + label: "signing", + labelColor: "blue", + message: "GPG key found but Git not configured", + options: [ + { + value: true, + label: "Configure Git and enable signing", + hint: "Set up Git to use your GPG key", + }, + { + value: false, + label: "Skip signing for now", + hint: "Continue without signing", + }, + ], + prefixLineCount, + }); + + if (ui.isCancel(configure)) { + console.log("\nSetup cancelled."); + process.exit(0); + } + return configure as boolean; +} + +/** + * Prompt for GPG installation when not available + */ +export async function promptGpgSetup( + platformInfo: PlatformInfo, + prefixLineCount = 0, +): Promise<"install" | "skip"> { + console.log( + `${textColors.brightYellow("Commit signing requires GPG to be installed.")}`, + ); + + const action = await ui.select({ + label: "signing", + labelColor: "blue", + message: "What would you like to do?", + options: [ + { + value: "install", + label: "Show installation instructions", + hint: platformInfo.installCommand + ? `Using ${platformInfo.packageManager}` + : "Manual download", + }, + { + value: "skip", + label: "Skip signing (continue without)", + hint: "You can set this up later", + }, + ], + prefixLineCount: prefixLineCount + 1, + }); + + if (ui.isCancel(action)) { + console.log("\nSetup cancelled."); + process.exit(0); + } + return action as "install" | "skip"; +} + +/** + * Prompt for GPG key generation when GPG is installed but no keys exist + */ +export async function promptKeyGeneration( + prefixLineCount = 0, +): Promise<"generate" | "skip"> { + const action = await ui.select({ + label: "signing", + labelColor: "blue", + message: "GPG installed but no signing keys found", + options: [ + { + value: "generate", + label: "Generate a new GPG key (guided)", + hint: "Creates a 4096-bit RSA key", + }, + { + value: "skip", + label: "Skip signing for now", + hint: "You can generate a key later", + }, + ], + prefixLineCount, + }); + + if (ui.isCancel(action)) { + console.log("\nSetup cancelled."); + process.exit(0); + } + return action as "generate" | "skip"; +} + +/** + * Display GPG installation instructions based on platform + */ +export function displayInstallInstructions(platformInfo: PlatformInfo): void { + if (platformInfo.installCommand) { + ui.indented( + `${textColors.pureWhite("Install GPG using your package manager:")}`, + ); + ui.blank(); + ui.indented(` ${textColors.brightCyan(platformInfo.installCommand)}`); + } else { + ui.indented(`${textColors.pureWhite("Install GPG manually:")}`); + ui.blank(); + ui.indented( + ` ${textColors.brightCyan("Download from:")} ${platformInfo.manualInstallUrl}`, + ); + ui.blank(); + + switch (platformInfo.os) { + case "darwin": + ui.indented( + ` ${textColors.brightYellow("•")} macOS: Download "GnuPG for OS X" or install Homebrew first`, + ); + ui.indented(` ${textColors.brightCyan("https://brew.sh/")}`); + break; + case "win32": + ui.indented( + ` ${textColors.brightYellow("•")} Windows: Download "Gpg4win" for full GPG suite`, + ); + break; + case "linux": + ui.indented( + ` ${textColors.brightYellow("•")} Linux: Use your distribution's package manager`, + ); + break; + } + } + + ui.blank(); + ui.indented( + `${textColors.brightYellow("After installing, run 'lab init' again to configure signing.")}`, + ); +} diff --git a/src/cli/commands/preview/prompts.ts b/src/cli/commands/preview/prompts.ts index cd6180e..8f46f0c 100644 --- a/src/cli/commands/preview/prompts.ts +++ b/src/cli/commands/preview/prompts.ts @@ -4,39 +4,12 @@ * Interactive prompts for browsing commit history */ -import { select, isCancel } from "@clack/prompts"; -import { labelColors, textColors } from "../init/colors.js"; +import { ui } from "../../ui/index.js"; +import { textColors } from "../init/colors.js"; import { formatForDisplay } from "../../../lib/util/emoji.js"; import type { CommitInfo } from "../shared/types.js"; -import { getCommitDetails, getCommitDiff } from "../shared/git-operations.js"; import readline from "readline"; -/** - * Create compact color-coded label - */ -function label( - text: string, - color: "magenta" | "cyan" | "blue" | "yellow" | "green", -): string { - const colorFn = { - magenta: labelColors.bgBrightMagenta, - cyan: labelColors.bgBrightCyan, - blue: labelColors.bgBrightBlue, - yellow: labelColors.bgBrightYellow, - green: labelColors.bgBrightGreen, - }[color]; - - const width = 7; - const textLength = Math.min(text.length, width); - const padding = width - textLength; - const leftPad = Math.ceil(padding / 2); - const rightPad = padding - leftPad; - const centeredText = - " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); - - return colorFn(` ${centeredText} `); -} - /** * Display commit list */ @@ -51,7 +24,7 @@ export function displayCommitList( ): void { console.log(); console.log( - `${label("preview", "cyan")} ${textColors.pureWhite("Commit History")}`, + `${ui.label("preview", "cyan")} ${textColors.pureWhite("Commit History")}`, ); console.log(); @@ -131,7 +104,7 @@ export function displayCommitDetails( ): void { console.log(); console.log( - `${label("detail", "green")} ${textColors.pureWhite("Commit Details")}`, + `${ui.label("detail", "green")} ${textColors.pureWhite("Commit Details")}`, ); console.log(); console.log(` ${textColors.brightWhite("Hash:")} ${commit.hash}`); @@ -211,7 +184,7 @@ export function displayCommitDetails( export function displayHelp(): void { console.log(); console.log( - `${label("help", "yellow")} ${textColors.pureWhite("Keyboard Shortcuts")}`, + `${ui.label("help", "yellow")} ${textColors.pureWhite("Keyboard Shortcuts")}`, ); console.log(); console.log(` ${textColors.brightCyan("0-9")} View commit details`); @@ -344,14 +317,14 @@ export async function waitForListAction( } } - // Previous batch - allow if there's a previous page + // Previous batch if ((char === "p" || char === "P") && hasPreviousPage) { cleanup(); resolve("previous"); return; } - // Next batch - allow if there are more pages to show + // Next batch if ((char === "n" || char === "N") && hasMorePages) { cleanup(); resolve("next"); diff --git a/src/cli/commands/revert/prompts.ts b/src/cli/commands/revert/prompts.ts index 7683c0f..09a9de6 100644 --- a/src/cli/commands/revert/prompts.ts +++ b/src/cli/commands/revert/prompts.ts @@ -4,47 +4,11 @@ * Interactive prompts for reverting commits */ -import { select, confirm, isCancel } from "@clack/prompts"; -import { labelColors, textColors, success, attention } from "../init/colors.js"; +import { ui } from "../../ui/index.js"; +import { textColors, attention } from "../init/colors.js"; import { formatForDisplay } from "../../../lib/util/emoji.js"; import type { CommitInfo, MergeParent } from "../shared/types.js"; -/** - * Create compact color-coded label - */ -function label( - text: string, - color: "magenta" | "cyan" | "blue" | "yellow" | "green", -): string { - const colorFn = { - magenta: labelColors.bgBrightMagenta, - cyan: labelColors.bgBrightCyan, - blue: labelColors.bgBrightBlue, - yellow: labelColors.bgBrightYellow, - green: labelColors.bgBrightGreen, - }[color]; - - const width = 7; - const textLength = Math.min(text.length, width); - const padding = width - textLength; - const leftPad = Math.ceil(padding / 2); - const rightPad = padding - leftPad; - const centeredText = - " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); - - return colorFn(` ${centeredText} `); -} - -/** - * Handle prompt cancellation - */ -function handleCancel(value: unknown): void { - if (isCancel(value)) { - console.log("\nRevert cancelled."); - process.exit(0); - } -} - /** * Display commit list for revert */ @@ -53,13 +17,13 @@ export function displayRevertCommitList( startIndex: number, totalFetched: number, hasMore: boolean, - hasPreviousPage: boolean = false, - hasMorePages: boolean = false, + _hasPreviousPage: boolean = false, + _hasMorePages: boolean = false, emojiModeActive: boolean = true, ): void { console.log(); console.log( - `${label("revert", "yellow")} ${textColors.pureWhite("Select Commit to Revert")}`, + `${ui.label("revert", "yellow")} ${textColors.pureWhite("Select Commit to Revert")}`, ); console.log(); @@ -87,7 +51,6 @@ export function displayRevertCommitList( ); } - // Pagination info const endIndex = startIndex + displayCount; console.log(); @@ -114,13 +77,18 @@ export async function promptMergeParent( label: `Parent ${parent.number}${parent.branch ? `: ${parent.branch}` : ""} (${parent.shortHash})${parent.number === 1 ? " [mainline, default]" : ""}`, })); - const selected = await select({ - message: `${label("parent", "blue")} ${textColors.pureWhite("Select parent to revert to:")}`, + const selected = await ui.select({ + label: "parent", + labelColor: "blue", + message: "Select parent to revert to:", options, - initialValue: "1", // Default to parent 1 + initialValue: "1", }); - handleCancel(selected); + if (ui.isCancel(selected)) { + console.log("\nRevert cancelled."); + process.exit(0); + } return parseInt(selected as string, 10); } @@ -133,7 +101,7 @@ export function displayRevertConfirmation( ): void { console.log(); console.log( - `${label("confirm", "green")} ${textColors.pureWhite("Revert Confirmation")}`, + `${ui.label("confirm", "green")} ${textColors.pureWhite("Revert Confirmation")}`, ); console.log(); console.log( @@ -155,21 +123,30 @@ export function displayRevertConfirmation( export async function promptRevertConfirmation(): Promise< "confirm" | "edit" | "cancel" > { - const confirmed = await confirm({ - message: `${label("confirm", "green")} ${textColors.pureWhite("Proceed with revert?")}`, + const confirmed = await ui.confirm({ + label: "confirm", + labelColor: "green", + message: "Proceed with revert?", initialValue: true, }); - handleCancel(confirmed); + if (ui.isCancel(confirmed)) { + console.log("\nRevert cancelled."); + process.exit(0); + } if (confirmed) { - // Ask if user wants to edit commit message - const edit = await confirm({ - message: `${label("edit", "yellow")} ${textColors.pureWhite("Edit commit message before reverting?")}`, + const edit = await ui.confirm({ + label: "edit", + labelColor: "yellow", + message: "Edit commit message before reverting?", initialValue: false, }); - handleCancel(edit); + if (ui.isCancel(edit)) { + console.log("\nRevert cancelled."); + process.exit(0); + } return edit ? "edit" : "confirm"; } diff --git a/src/cli/ui/display.ts b/src/cli/ui/display.ts new file mode 100644 index 0000000..3d3da64 --- /dev/null +++ b/src/cli/ui/display.ts @@ -0,0 +1,78 @@ +/** + * UI Display Helpers + * + * Non-interactive output functions for sections, status lines, + * dividers, and indented content. Replaces @clack log.info(), + * log.message(), and renderWithConnector(). + */ + +import { label as renderLabel, spacing, symbols } from "./theme.js"; +import { textColors, success as successColor } from "../commands/init/colors.js"; +import type { LabelColor } from "./types.js"; + +/** + * Display a section header: [label] message + */ +export function section( + labelText: string, + color: LabelColor, + message: string, +): void { + console.log( + `${renderLabel(labelText, color)} ${textColors.pureWhite(message)}`, + ); +} + +/** + * Status line helpers (indented with symbol prefix) + */ +export const status = { + success: (msg: string) => { + const indent = " ".repeat(spacing.optionIndent); + console.log(`${indent}${successColor(symbols.check)} ${msg}`); + }, + error: (msg: string) => { + const indent = " ".repeat(spacing.optionIndent); + console.log( + `${indent}${textColors.gitDeleted(symbols.cross)} ${msg}`, + ); + }, + info: (msg: string) => { + const indent = " ".repeat(spacing.optionIndent); + console.log(`${indent}${msg}`); + }, +} as const; + +/** + * Print a blank line + */ +export function blank(): void { + console.log(); +} + +/** + * Print an indented divider line + */ +export function divider(): void { + const indent = " ".repeat(spacing.optionIndent); + console.log( + `${indent}${"─".repeat(45)}`, + ); +} + +/** + * Print content indented to option level (no connector prefix) + */ +export function indented(content: string): void { + const indent = " ".repeat(spacing.optionIndent); + console.log(`${indent}${content}`); +} + +/** + * Print multiple lines indented to option level + */ +export function block(lines: ReadonlyArray): void { + for (const l of lines) { + indented(l); + } +} diff --git a/src/cli/ui/index.ts b/src/cli/ui/index.ts new file mode 100644 index 0000000..22f1dbe --- /dev/null +++ b/src/cli/ui/index.ts @@ -0,0 +1,54 @@ +/** + * UI Framework - Barrel Export + * + * Custom, zero-dependency UI framework built on Node.js readline. + * Replaces @clack/prompts entirely. + * + * Usage: + * import { ui } from '../../ui/index.js'; + * const val = await ui.select({ label: 'type', labelColor: 'magenta', ... }); + * if (ui.isCancel(val)) process.exit(0); + */ + +import { CANCEL_SYMBOL } from "./types.js"; +import { select, text, confirm, multiselect } from "./prompts.js"; +import { section, status, blank, divider, indented, block } from "./display.js"; +import { label } from "./theme.js"; + +export { CANCEL_SYMBOL } from "./types.js"; +export type { + LabelColor, + SelectOption, + SelectConfig, + TextConfig, + ConfirmConfig, + MultiselectConfig, +} from "./types.js"; +export { label } from "./theme.js"; +export { select, text, confirm, multiselect } from "./prompts.js"; +export { section, status, blank, divider, indented, block } from "./display.js"; + +/** + * Check if a value is the cancel symbol + */ +export function isCancel(value: unknown): value is typeof CANCEL_SYMBOL { + return value === CANCEL_SYMBOL; +} + +/** + * Unified ui namespace for convenient imports + */ +export const ui = { + select, + text, + confirm, + multiselect, + section, + status, + blank, + divider, + indented, + block, + label, + isCancel, +} as const; diff --git a/src/cli/ui/prompts.ts b/src/cli/ui/prompts.ts new file mode 100644 index 0000000..68df2a3 --- /dev/null +++ b/src/cli/ui/prompts.ts @@ -0,0 +1,493 @@ +/** + * Custom UI Prompts + * + * Zero-dependency replacements for @clack/prompts select, text, confirm, + * and multiselect. Built on Node.js readline. No vertical connector lines. + * + * Design: + * - Active state: label + message header, option list below + * - Submitted state: collapses to single line (label + selected value) + * - Escape / Ctrl+C returns CANCEL_SYMBOL + * - Native shortcut integration (no wrapper hack) + * - Non-TTY fallback: returns initialValue or first option + */ + +import readline from "readline"; +import { label as renderLabel, spacing, symbols } from "./theme.js"; +import { + cursor, + line, + isTTY, + enterRawMode, + dim, + brightCyan, +} from "./renderer.js"; +import { textColors } from "../commands/init/colors.js"; +import { matchShortcut } from "../../lib/shortcuts/index.js"; +import type { + SelectConfig, + TextConfig, + ConfirmConfig, + MultiselectConfig, +} from "./types.js"; +import { CANCEL_SYMBOL } from "./types.js"; + +// --------------------------------------------------------------------------- +// select() +// --------------------------------------------------------------------------- + +/** + * Interactive select prompt. + * + * Renders a list of options with arrow-key navigation. + * Supports keyboard shortcuts for immediate selection. + * Collapses to a single line after submission. + */ +export async function select( + config: SelectConfig, +): Promise { + // Non-TTY fallback + if (!isTTY()) { + if (config.initialValue !== undefined) return config.initialValue; + if (config.options.length > 0) return config.options[0].value; + return CANCEL_SYMBOL; + } + + const { options, shortcuts } = config; + const headerLine = `${renderLabel(config.label, config.labelColor)} ${textColors.pureWhite(config.message)}`; + + // Find initial cursor position + let cursorIndex = 0; + if (config.initialValue !== undefined) { + const idx = options.findIndex((o) => o.value === config.initialValue); + if (idx >= 0) cursorIndex = idx; + } + + return new Promise((resolve) => { + const { cleanup: rawCleanup } = enterRawMode(); + readline.emitKeypressEvents(process.stdin); + cursor.hide(); + + // Track how many lines we've written (for clearing) + let renderedLines = 0; + + const render = () => { + // Clear previous render + if (renderedLines > 0) { + line.clearLines(renderedLines); + } + + const lines: string[] = []; + // Header + lines.push(headerLine); + + // Options + const indent = " ".repeat(spacing.optionIndent); + for (let i = 0; i < options.length; i++) { + const opt = options[i]; + const isActive = i === cursorIndex; + const pointer = isActive ? symbols.pointer : " "; + const labelText = isActive ? brightCyan(opt.label) : opt.label; + const hintText = opt.hint ? ` ${dim(`(${opt.hint})`)}` : ""; + lines.push(`${indent}${pointer} ${labelText}${hintText}`); + } + + const output = lines.join("\n"); + process.stdout.write(output); + renderedLines = lines.length; + }; + + const finish = (value: T | typeof CANCEL_SYMBOL) => { + process.stdin.removeListener("keypress", onKeypress); + rawCleanup(); + + // Clear the active render plus any externally-written prefix lines + const totalClear = renderedLines + (config.prefixLineCount ?? 0); + if (totalClear > 0) { + line.clearLines(totalClear); + } + + if (value === CANCEL_SYMBOL) { + // Show cancelled state + process.stdout.write(headerLine + "\n"); + } else { + // Collapse to single line with selected value + const selected = options.find((o) => o.value === value); + const selectedLabel = selected ? selected.label : String(value); + process.stdout.write( + `${renderLabel(config.label, config.labelColor)} ${textColors.brightCyan(selectedLabel)}\n`, + ); + } + + resolve(value); + }; + + const onKeypress = (char: string | undefined, key: readline.Key) => { + // Cancel: Escape or Ctrl+C + if (key.name === "escape" || (key.ctrl && key.name === "c")) { + finish(CANCEL_SYMBOL); + return; + } + + // Navigation + if (key.name === "up" || key.name === "k") { + cursorIndex = cursorIndex <= 0 ? options.length - 1 : cursorIndex - 1; + render(); + return; + } + + if (key.name === "down" || key.name === "j") { + cursorIndex = cursorIndex >= options.length - 1 ? 0 : cursorIndex + 1; + render(); + return; + } + + // Submit + if (key.name === "return") { + finish(options[cursorIndex].value); + return; + } + + // Shortcut matching + if (shortcuts && char && char.length === 1 && /^[a-z]$/i.test(char)) { + const matched = matchShortcut(char, shortcuts); + if (matched) { + const matchedOption = options.find( + (o) => String(o.value) === matched, + ); + if (matchedOption) { + finish(matchedOption.value); + return; + } + } + } + }; + + process.stdin.on("keypress", onKeypress); + render(); + }); +} + +// --------------------------------------------------------------------------- +// text() +// --------------------------------------------------------------------------- + +/** + * Interactive text input prompt. + * + * Shows a single-line text input with validation. + * Collapses to a single line after submission. + */ +export async function text( + config: TextConfig, +): Promise { + // Non-TTY fallback + if (!isTTY()) { + return config.initialValue ?? ""; + } + + const headerLine = `${renderLabel(config.label, config.labelColor)} ${textColors.pureWhite(config.message)}`; + + return new Promise((resolve) => { + const { cleanup: rawCleanup } = enterRawMode(); + readline.emitKeypressEvents(process.stdin); + + let value = config.initialValue ?? ""; + let cursorPos = value.length; + let error: string | undefined; + let renderedLines = 0; + + const render = () => { + if (renderedLines > 0) { + line.clearLines(renderedLines); + } + + const lines: string[] = []; + lines.push(headerLine); + + // Input line + const indent = " ".repeat(spacing.optionIndent); + const displayValue = value || dim(config.placeholder ?? ""); + + // Build input line with visible cursor + if (value) { + const before = value.slice(0, cursorPos); + const cursorChar = cursorPos < value.length ? value[cursorPos] : " "; + const after = + cursorPos < value.length ? value.slice(cursorPos + 1) : ""; + lines.push(`${indent}${before}\x1b[7m${cursorChar}\x1b[27m${after}`); + } else { + lines.push(`${indent}${displayValue}`); + } + + // Error line + if (error) { + lines.push(`${indent}${textColors.gitDeleted(error)}`); + } + + process.stdout.write(lines.join("\n")); + renderedLines = lines.length; + }; + + const finish = (result: string | typeof CANCEL_SYMBOL) => { + process.stdin.removeListener("keypress", onKeypress); + rawCleanup(); + + if (renderedLines > 0) { + line.clearLines(renderedLines); + } + + if (result === CANCEL_SYMBOL) { + process.stdout.write(headerLine + "\n"); + } else { + process.stdout.write( + `${renderLabel(config.label, config.labelColor)} ${textColors.brightCyan(result || dim("(empty)"))}\n`, + ); + } + + resolve(result); + }; + + const onKeypress = (char: string | undefined, key: readline.Key) => { + // Cancel + if (key.name === "escape" || (key.ctrl && key.name === "c")) { + finish(CANCEL_SYMBOL); + return; + } + + // Submit + if (key.name === "return") { + if (config.validate) { + const validationError = config.validate(value); + if (validationError) { + error = validationError; + render(); + return; + } + } + finish(value); + return; + } + + // Backspace + if (key.name === "backspace") { + if (cursorPos > 0) { + value = value.slice(0, cursorPos - 1) + value.slice(cursorPos); + cursorPos--; + error = undefined; + } + render(); + return; + } + + // Delete + if (key.name === "delete") { + if (cursorPos < value.length) { + value = value.slice(0, cursorPos) + value.slice(cursorPos + 1); + error = undefined; + } + render(); + return; + } + + // Cursor movement + if (key.name === "left") { + if (cursorPos > 0) cursorPos--; + render(); + return; + } + + if (key.name === "right") { + if (cursorPos < value.length) cursorPos++; + render(); + return; + } + + if (key.name === "home" || (key.ctrl && key.name === "a")) { + cursorPos = 0; + render(); + return; + } + + if (key.name === "end" || (key.ctrl && key.name === "e")) { + cursorPos = value.length; + render(); + return; + } + + // Printable character + if (char && !key.ctrl && !key.meta && char.length === 1) { + const code = char.charCodeAt(0); + // Filter out control characters + if (code >= 32) { + value = value.slice(0, cursorPos) + char + value.slice(cursorPos); + cursorPos++; + error = undefined; + render(); + } + } + }; + + process.stdin.on("keypress", onKeypress); + render(); + }); +} + +// --------------------------------------------------------------------------- +// confirm() +// --------------------------------------------------------------------------- + +/** + * Confirm prompt (Yes/No). + * Thin wrapper around select() with boolean options. + */ +export async function confirm( + config: ConfirmConfig, +): Promise { + const defaultYes = config.initialValue !== false; + + const options = defaultYes + ? [ + { value: true, label: "Yes" }, + { value: false, label: "No" }, + ] + : [ + { value: false, label: "No" }, + { value: true, label: "Yes" }, + ]; + + return await select({ + label: config.label, + labelColor: config.labelColor, + message: config.message, + options, + initialValue: config.initialValue ?? true, + }); +} + +// --------------------------------------------------------------------------- +// multiselect() +// --------------------------------------------------------------------------- + +/** + * Interactive multi-select prompt. + * + * Toggle items with Space, submit with Enter. + * Collapses to a single line showing comma-separated selections. + */ +export async function multiselect( + config: MultiselectConfig, +): Promise | typeof CANCEL_SYMBOL> { + // Non-TTY fallback + if (!isTTY()) { + return []; + } + + const { options } = config; + const headerLine = `${renderLabel(config.label, config.labelColor)} ${textColors.pureWhite(config.message)}`; + + let cursorIndex = 0; + const selected = new Set(); + + return new Promise | typeof CANCEL_SYMBOL>((resolve) => { + const { cleanup: rawCleanup } = enterRawMode(); + readline.emitKeypressEvents(process.stdin); + cursor.hide(); + + let renderedLines = 0; + + const render = () => { + if (renderedLines > 0) { + line.clearLines(renderedLines); + } + + const lines: string[] = []; + lines.push(headerLine); + + const indent = " ".repeat(spacing.optionIndent); + for (let i = 0; i < options.length; i++) { + const opt = options[i]; + const isActive = i === cursorIndex; + const isSelected = selected.has(i); + const pointer = isActive ? symbols.pointer : " "; + const marker = isSelected ? symbols.bullet : symbols.circle; + const labelText = isActive ? brightCyan(opt.label) : opt.label; + const hintText = opt.hint ? ` ${dim(`(${opt.hint})`)}` : ""; + lines.push(`${indent}${pointer} ${marker} ${labelText}${hintText}`); + } + + // Instruction + lines.push(`${indent} ${dim("Space to toggle, Enter to submit")}`); + + process.stdout.write(lines.join("\n")); + renderedLines = lines.length; + }; + + const finish = (result: ReadonlyArray | typeof CANCEL_SYMBOL) => { + process.stdin.removeListener("keypress", onKeypress); + rawCleanup(); + + if (renderedLines > 0) { + line.clearLines(renderedLines); + } + + if (result === CANCEL_SYMBOL) { + process.stdout.write(headerLine + "\n"); + } else { + const selectedLabels = Array.from(selected) + .sort((a, b) => a - b) + .map((i) => options[i].label) + .join(", "); + process.stdout.write( + `${renderLabel(config.label, config.labelColor)} ${textColors.brightCyan(selectedLabels || "(none)")}\n`, + ); + } + + resolve(result); + }; + + const onKeypress = (char: string | undefined, key: readline.Key) => { + // Cancel + if (key.name === "escape" || (key.ctrl && key.name === "c")) { + finish(CANCEL_SYMBOL); + return; + } + + // Navigation + if (key.name === "up" || key.name === "k") { + cursorIndex = cursorIndex <= 0 ? options.length - 1 : cursorIndex - 1; + render(); + return; + } + + if (key.name === "down" || key.name === "j") { + cursorIndex = cursorIndex >= options.length - 1 ? 0 : cursorIndex + 1; + render(); + return; + } + + // Toggle selection + if (key.name === "space" || (char === " " && !key.ctrl)) { + if (selected.has(cursorIndex)) { + selected.delete(cursorIndex); + } else { + selected.add(cursorIndex); + } + render(); + return; + } + + // Submit + if (key.name === "return") { + const values = Array.from(selected) + .sort((a, b) => a - b) + .map((i) => options[i].value); + finish(values); + return; + } + }; + + process.stdin.on("keypress", onKeypress); + render(); + }); +} diff --git a/src/cli/ui/renderer.ts b/src/cli/ui/renderer.ts new file mode 100644 index 0000000..01594ec --- /dev/null +++ b/src/cli/ui/renderer.ts @@ -0,0 +1,109 @@ +/** + * Terminal Renderer + * + * Low-level terminal primitives: cursor control, raw mode management, + * and ANSI escape helpers. Built on Node.js readline (no dependencies). + */ + +/** + * ANSI cursor control sequences + */ +export const cursor = { + hide: () => process.stdout.write("\x1b[?25l"), + show: () => process.stdout.write("\x1b[?25h"), + moveUp: (n: number) => { + if (n > 0) process.stdout.write(`\x1b[${n}A`); + }, + moveDown: (n: number) => { + if (n > 0) process.stdout.write(`\x1b[${n}B`); + }, + moveToColumn: (col: number) => process.stdout.write(`\x1b[${col}G`), + saveCursor: () => process.stdout.write("\x1b7"), + restoreCursor: () => process.stdout.write("\x1b8"), +} as const; + +/** + * Line clearing utilities + */ +export const line = { + /** Clear the current line entirely */ + clear: () => process.stdout.write("\x1b[2K\r"), + /** Clear N lines moving upward from current position */ + clearLines: (count: number) => { + for (let i = 0; i < count; i++) { + process.stdout.write("\x1b[2K"); // clear line + if (i < count - 1) { + process.stdout.write("\x1b[1A"); // move up + } + } + process.stdout.write("\r"); // return to start + }, +} as const; + +/** + * Check if both stdout and stdin are TTY + */ +export function isTTY(): boolean { + return Boolean(process.stdout.isTTY && process.stdin.isTTY); +} + +/** + * Get terminal width, falling back to 80 columns + */ +export function getTerminalWidth(): number { + return process.stdout.columns || 80; +} + +/** + * Dim text (ANSI dim) + */ +export function dim(text: string): string { + return `\x1b[2m${text}\x1b[22m`; +} + +/** + * Bright cyan text + */ +export function brightCyan(text: string): string { + return `\x1b[38;5;51m${text}\x1b[0m`; +} + +/** + * Enter raw mode for keyboard input. + * Returns a cleanup function that restores previous state. + * + * Follows the same pattern as preview/prompts.ts: + * checks stdin.isRaw before toggling. + */ +export function enterRawMode(): { cleanup: () => void } { + const stdin = process.stdin; + const wasRaw = stdin.isRaw; + + if (!wasRaw) { + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding("utf8"); + } + + // SIGINT handler for clean exit during raw mode + const sigintHandler = () => { + cursor.show(); + if (!wasRaw) { + stdin.setRawMode(false); + stdin.pause(); + } + process.exit(130); + }; + process.on("SIGINT", sigintHandler); + + const cleanup = () => { + process.removeListener("SIGINT", sigintHandler); + cursor.show(); + if (!wasRaw) { + stdin.setRawMode(false); + stdin.pause(); + } + }; + + return { cleanup }; +} diff --git a/src/cli/ui/theme.ts b/src/cli/ui/theme.ts new file mode 100644 index 0000000..b1f5a83 --- /dev/null +++ b/src/cli/ui/theme.ts @@ -0,0 +1,60 @@ +/** + * UI Theme + * + * Design tokens and consolidated label function. + * Single source of truth for visual constants used across all commands. + */ + +import { labelColors } from "../commands/init/colors.js"; +import type { LabelColor } from "./types.js"; + +/** + * Unicode symbols used in prompts and display + */ +export const symbols = { + pointer: ">", + check: "\u2714", // checkmark + cross: "\u2718", // X + bullet: "\u25CF", // filled circle + circle: "\u25CB", // empty circle +} as const; + +/** + * Spacing constants for consistent alignment + */ +export const spacing = { + /** Width of the label text area (centered within this) */ + labelWidth: 7, + /** Gap between label and content */ + labelGap: 2, + /** Total indent for option lines (label outer width + gap) */ + optionIndent: 11, +} as const; + +/** + * Create a compact, color-coded label. + * Text is centered within a fixed-width badge. + * + * Consolidates the identical `label()` function previously + * duplicated in init, commit, revert, and preview prompts. + */ +export function label(text: string, color: LabelColor): string { + const colorFn: Record string> = { + magenta: labelColors.bgBrightMagenta, + cyan: labelColors.bgBrightCyan, + blue: labelColors.bgBrightBlue, + yellow: labelColors.bgBrightYellow, + green: labelColors.bgBrightGreen, + }; + + const width = spacing.labelWidth; + const textLength = Math.min(text.length, width); + const padding = width - textLength; + // Odd padding: extra space on LEFT for better visual weight + const leftPad = Math.ceil(padding / 2); + const rightPad = padding - leftPad; + const centeredText = + " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); + + return colorFn[color](` ${centeredText} `); +} diff --git a/src/cli/ui/types.ts b/src/cli/ui/types.ts new file mode 100644 index 0000000..f7f7974 --- /dev/null +++ b/src/cli/ui/types.ts @@ -0,0 +1,77 @@ +/** + * UI Framework Types + * + * Interfaces and type definitions for the custom UI framework. + * Zero dependencies on @clack/prompts. + */ + +import type { ShortcutMapping } from "../../lib/shortcuts/types.js"; + +/** + * Sentinel symbol for cancelled prompts. + * Used the same way as @clack's cancel symbol. + */ +export const CANCEL_SYMBOL: unique symbol = Symbol("ui.cancel"); + +/** + * Available label background colors + */ +export type LabelColor = "magenta" | "cyan" | "blue" | "yellow" | "green"; + +/** + * Option for select/multiselect prompts + */ +export interface SelectOption { + value: T; + label: string; + hint?: string; +} + +/** + * Configuration for select prompt + */ +export interface SelectConfig { + label: string; + labelColor: LabelColor; + message: string; + options: ReadonlyArray>; + initialValue?: T; + shortcuts?: ShortcutMapping | null; + /** Number of externally-written lines above this prompt to clear on submit */ + prefixLineCount?: number; +} + +/** + * Configuration for text input prompt + */ +export interface TextConfig { + label: string; + labelColor: LabelColor; + message: string; + placeholder?: string; + initialValue?: string; + validate?: (value: string) => string | undefined; +} + +/** + * Configuration for confirm prompt + */ +export interface ConfirmConfig { + label: string; + labelColor: LabelColor; + message: string; + initialValue?: boolean; +} + +/** + * Configuration for multiselect prompt + */ +export interface MultiselectConfig { + label: string; + labelColor: LabelColor; + message: string; + options: ReadonlyArray>; + required?: boolean; +} + +export type { ShortcutMapping }; diff --git a/src/cli/utils/terminal.ts b/src/cli/utils/terminal.ts new file mode 100644 index 0000000..cace107 --- /dev/null +++ b/src/cli/utils/terminal.ts @@ -0,0 +1,65 @@ +/** + * Terminal-width-aware utilities for CLI prompt formatting. + * + * Provides truncation functions to prevent prompt option labels + * from wrapping past the terminal width. + */ + +import { stripVTControlCharacters } from "node:util"; +import { spacing } from "../ui/theme.js"; + +/** + * Get available width for prompt option text. + * Accounts for the UI framework's option indent + reserved right margin. + */ +export function getAvailableWidth(reservedRight: number = 0): number { + const columns = process.stdout.columns || 80; + return columns - spacing.optionIndent - reservedRight; +} + +/** + * Truncate a string to fit terminal width, accounting for ANSI codes. + * Strips ANSI for measurement, truncates visible text, preserves structure. + */ +export function truncateForPrompt( + text: string, + maxWidth: number, +): string { + const visible = stripVTControlCharacters(text); + if (visible.length <= maxWidth) return text; + + // For plain text (no ANSI), simple truncation + if (visible.length === text.length) { + return text.slice(0, maxWidth - 1) + "…"; + } + + // For text with ANSI codes, truncate the visible content + // Walk through original string tracking visible character count + let visibleCount = 0; + let i = 0; + const targetLength = maxWidth - 1; // Leave room for ellipsis + + while (i < text.length && visibleCount < targetLength) { + // Detect CSI escape sequence: ESC[ + // Final byte is any char in 0x40-0x7E range (A-Z, a-z, @, etc.) + if (text[i] === "\x1b" && i + 1 < text.length && text[i + 1] === "[") { + let j = i + 2; + // Skip parameter and intermediate bytes (0x20-0x3F) + while ( + j < text.length && + text.charCodeAt(j) >= 0x20 && + text.charCodeAt(j) <= 0x3f + ) { + j++; + } + // Skip the final byte (0x40-0x7E) + if (j < text.length) j++; + i = j; + continue; + } + visibleCount++; + i++; + } + + return text.slice(0, i) + "…\x1b[0m"; +} diff --git a/src/lib/presets/index.ts b/src/lib/presets/index.ts index d4432d0..eba5421 100644 --- a/src/lib/presets/index.ts +++ b/src/lib/presets/index.ts @@ -117,6 +117,8 @@ export function buildConfig( autoStage?: boolean; // Body requirement bodyRequired?: boolean; + // GPG commit signing (determined by GPG detection in init) + signCommits?: boolean; }, ): LabcommitrConfig { const preset = getPreset(presetId); @@ -164,8 +166,8 @@ export function buildConfig( aliases: {}, git: { auto_stage: customizations.autoStage ?? false, - // Security best-practice: enable signed commits by default - sign_commits: true, + // GPG signing based on detection result (defaults to false if not specified) + sign_commits: customizations.signCommits ?? false, }, shortcuts: { enabled: true, // Enabled by default for better UX diff --git a/src/lib/shortcuts/index.ts b/src/lib/shortcuts/index.ts index 8ebd8c2..062442e 100644 --- a/src/lib/shortcuts/index.ts +++ b/src/lib/shortcuts/index.ts @@ -2,12 +2,11 @@ * Shortcuts Module * * Handles keyboard shortcut configuration, auto-assignment, and integration - * with @clack/prompts select() function. + * with the custom UI select() function. */ -import type { ShortcutMapping, ShortcutCharacterSet } from "./types.js"; +import type { ShortcutMapping } from "./types.js"; import { autoAssignShortcuts } from "./auto-assign.js"; -import { DEFAULT_CHAR_SET } from "./types.js"; /** * Shortcuts configuration from user config diff --git a/src/lib/shortcuts/input-handler.ts b/src/lib/shortcuts/input-handler.ts index 39e7891..8b292aa 100644 --- a/src/lib/shortcuts/input-handler.ts +++ b/src/lib/shortcuts/input-handler.ts @@ -1,13 +1,9 @@ /** * Input Handler for Shortcuts * - * Intercepts keyboard input to support single-character shortcuts - * before passing to @clack/prompts select() function. - * - * Note: This is a simplified implementation. Full input interception - * would require deeper integration with @clack/prompts internals. - * For now, shortcuts are displayed in labels and users can type - * the letter to match (if @clack/prompts supports it) or use arrow keys. + * Intercepts keyboard input to support single-character shortcuts. + * Used as a utility by the custom UI select() prompt which has + * native shortcut support built in. */ import type { ShortcutMapping } from "./types.js"; @@ -15,7 +11,6 @@ import { matchShortcut } from "./index.js"; /** * Check if a character input matches a shortcut - * This can be used to pre-process input before it reaches @clack/prompts * * @param input - Single character input * @param mapping - Shortcut mapping @@ -31,17 +26,3 @@ export function handleShortcutInput( return matchShortcut(input, mapping); } - -/** - * Note: Full input interception would require: - * 1. Setting up raw mode on stdin - * 2. Listening to keypress events - * 3. Intercepting before @clack/prompts processes input - * 4. Programmatically selecting the matched option - * - * This is complex and may conflict with @clack/prompts' internal handling. - * For v1, we display shortcuts in labels and rely on @clack/prompts' - * native behavior (if it supports typing to select) or arrow keys. - * - * Future enhancement: Implement full input interception wrapper. - */ diff --git a/src/lib/shortcuts/select-with-shortcuts.ts b/src/lib/shortcuts/select-with-shortcuts.ts deleted file mode 100644 index 0e94f27..0000000 --- a/src/lib/shortcuts/select-with-shortcuts.ts +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Select with Shortcuts Support - * - * Wraps @clack/prompts select() with keyboard shortcut support. - * Intercepts single-character input to match shortcuts before - * passing control to @clack/prompts. - * - * Note: This implementation uses a workaround since @clack/prompts - * doesn't natively support shortcuts. We intercept keypress events - * and programmatically select the option if a shortcut matches. - */ - -import { select, type SelectOptions } from "@clack/prompts"; -import type { ShortcutMapping } from "./types.js"; -import { matchShortcut } from "./index.js"; -import readline from "readline"; - -/** - * Select with shortcut support - * - * Wraps @clack/prompts select() with custom input handling for shortcuts. - * If a shortcut key is pressed, immediately selects that option. - * Otherwise, passes input to @clack/prompts for normal handling. - * - * @param options - Select options from @clack/prompts - * @param shortcutMapping - Shortcut mapping (null if shortcuts disabled) - * @returns Selected value or symbol (cancel) - */ -export async function selectWithShortcuts( - options: SelectOptions, - shortcutMapping: ShortcutMapping | null, -): Promise { - // If no shortcuts, use normal select - if (!shortcutMapping) { - return await select(options); - } - - // Set up input interception using readline - const stdin = process.stdin; - const wasRaw = stdin.isRaw; - - // Enable raw mode and keypress events - if (!wasRaw) { - stdin.setRawMode(true); - stdin.resume(); - stdin.setEncoding("utf8"); - } - - readline.emitKeypressEvents(stdin); - - // Create promise that resolves when shortcut is pressed or select completes - return new Promise((resolve) => { - let shortcutResolved = false; - let selectResolved = false; - - // Keypress handler for shortcuts - const onKeypress = (char: string, key: readline.Key) => { - // Ignore if already resolved - if (shortcutResolved || selectResolved) { - return; - } - - // Check for escape (cancel) - let @clack/prompts handle it - if (key.name === "escape" || (key.ctrl && key.name === "c")) { - return; // Let @clack/prompts handle cancellation - } - - // Check for Enter - let @clack/prompts handle it - if (key.name === "return" || key.name === "enter") { - return; - } - - // Check for arrow keys - let @clack/prompts handle them - if ( - key.name === "up" || - key.name === "down" || - key.name === "left" || - key.name === "right" - ) { - return; - } - - // Check if single character matches a shortcut - if (char && char.length === 1 && /^[a-z]$/i.test(char)) { - const matchedValue = matchShortcut(char, shortcutMapping); - if (matchedValue) { - shortcutResolved = true; - cleanup(); - // Resolve with the matched value - resolve(matchedValue as T); - return; - } - } - }; - - // Cleanup function - const cleanup = () => { - stdin.removeListener("keypress", onKeypress); - if (!wasRaw) { - stdin.setRawMode(false); - stdin.pause(); - } - }; - - // Set up keypress listener BEFORE starting select - stdin.on("keypress", onKeypress); - - // Start normal select prompt - // Our listener will intercept shortcuts, but @clack/prompts - // will handle everything else (arrows, Enter, etc.) - select(options) - .then((result) => { - if (!shortcutResolved) { - selectResolved = true; - cleanup(); - resolve(result); - } - }) - .catch((error) => { - if (!shortcutResolved) { - selectResolved = true; - cleanup(); - // On error, treat as cancel - resolve(Symbol.for("clack.cancel")); - } - }); - }); -}