From ef589cf0851b190f96762150946898c277d1968f Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Feb 2025 13:59:26 +0100 Subject: [PATCH 01/59] Sets up formatting and main --- package-lock.json | 1302 +++++++++++++++++++++++++- package.json | 12 +- src/cli/commandParser.js | 64 +- src/configmenu.js | 196 ++-- src/constants/ascii.js | 2 +- src/handlers/fileHandlers.js | 414 ++++---- src/handlers/installationHandlers.js | 446 +++++---- src/handlers/nodeHandlers.js | 582 ++++++------ src/main.js | 325 ++++--- src/services/codexapp.js | 12 +- src/services/config.js | 23 +- src/services/nodeService.js | 268 +++--- src/services/uiservice.js | 47 + src/ui/mainmenu.js | 9 + src/utils/appdata.js | 16 +- src/utils/command.js | 20 +- src/utils/messages.js | 54 +- src/utils/numberSelector.js | 17 +- src/utils/pathSelector.js | 166 ++-- 19 files changed, 2829 insertions(+), 1146 deletions(-) create mode 100644 src/services/uiservice.js create mode 100644 src/ui/mainmenu.js diff --git a/package-lock.json b/package-lock.json index c5cc140..6cb0ec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codexstorage", - "version": "1.0.10", + "version": "1.0.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codexstorage", - "version": "1.0.10", + "version": "1.0.11", "license": "MIT", "dependencies": { "axios": "^1.6.2", @@ -21,6 +21,413 @@ }, "bin": { "codexstorage": "index.js" + }, + "devDependencies": { + "prettier": "^3.4.2", + "vitest": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@inquirer/figures": { @@ -32,6 +439,371 @@ "node": ">=18" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@vitest/expect": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", + "dev": true, + "dependencies": { + "@vitest/spy": "3.0.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", + "dev": true, + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", + "dev": true, + "dependencies": { + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", @@ -87,6 +859,15 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -277,6 +1058,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/camelcase": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", @@ -289,6 +1079,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -306,6 +1112,15 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "license": "MIT" }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -396,6 +1211,32 @@ "hasInstallScript": true, "license": "MIT" }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/default-browser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", @@ -467,6 +1308,70 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -539,6 +1444,20 @@ "ramda": "^0.25.0" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -780,6 +1699,21 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -808,6 +1742,12 @@ "node": ">=6" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, "node_modules/mute-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", @@ -817,6 +1757,24 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/nanospinner": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/nanospinner/-/nanospinner-1.1.0.tgz", @@ -921,11 +1879,69 @@ "node": ">=0.10.0" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -970,6 +1986,44 @@ "node": ">=8" } }, + "node_modules/rollup": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.8", + "@rollup/rollup-android-arm64": "4.34.8", + "@rollup/rollup-darwin-arm64": "4.34.8", + "@rollup/rollup-darwin-x64": "4.34.8", + "@rollup/rollup-freebsd-arm64": "4.34.8", + "@rollup/rollup-freebsd-x64": "4.34.8", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", + "@rollup/rollup-linux-arm-musleabihf": "4.34.8", + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-arm64-musl": "4.34.8", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", + "@rollup/rollup-linux-riscv64-gnu": "4.34.8", + "@rollup/rollup-linux-s390x-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-musl": "4.34.8", + "@rollup/rollup-win32-arm64-msvc": "4.34.8", + "@rollup/rollup-win32-ia32-msvc": "4.34.8", + "@rollup/rollup-win32-x64-msvc": "4.34.8", + "fsevents": "~2.3.2" + } + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -1024,12 +2078,39 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", + "dev": true + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -1075,6 +2156,45 @@ "node": ">=8" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -1119,6 +2239,168 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/vite": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", + "dev": true, + "dependencies": { + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.7", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -1128,6 +2410,22 @@ "defaults": "^1.0.3" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/widest-line": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", diff --git a/package.json b/package.json index 16bf6b5..a9bf1ec 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,19 @@ "codexstorage": "./index.js" }, "scripts": { - "start": "node index.js" + "start": "node index.js", + "test": "vitest run", + "test:watch": "vitest", + "format": "prettier --write ./src" }, "keywords": [ "codex", "storage", "cli" ], + "engines": { + "node": ">=20" + }, "author": "Codex Storage", "license": "MIT", "dependencies": { @@ -27,5 +33,9 @@ "fs-extra": "^11.3.0", "fs-filesystem": "^2.1.2", "open": "^10.1.0" + }, + "devDependencies": { + "prettier": "^3.4.2", + "vitest": "^3.0.5" } } diff --git a/src/cli/commandParser.js b/src/cli/commandParser.js index 1e185d2..f51a005 100644 --- a/src/cli/commandParser.js +++ b/src/cli/commandParser.js @@ -1,35 +1,41 @@ -import { showErrorMessage } from '../utils/messages.js'; +import { showErrorMessage } from "../utils/messages.js"; export function handleCommandLineOperation() { - return process.argv.length > 2; + return process.argv.length > 2; } export function parseCommandLineArgs() { - const args = process.argv.slice(2); - if (args.length === 0) return null; + const args = process.argv.slice(2); + if (args.length === 0) return null; - switch (args[0]) { - case '--upload': - if (args.length !== 2) { - console.log(showErrorMessage('Usage: npx codexstorage --upload ')); - process.exit(1); - } - return { command: 'upload', value: args[1] }; - - case '--download': - if (args.length !== 2) { - console.log(showErrorMessage('Usage: npx codexstorage --download ')); - process.exit(1); - } - return { command: 'download', value: args[1] }; - - default: - console.log(showErrorMessage( - 'Invalid command. Available commands:\n\n' + - 'npx codexstorage\n' + - 'npx codexstorage --upload \n' + - 'npx codexstorage --download ' - )); - process.exit(1); - } -} \ No newline at end of file + switch (args[0]) { + case "--upload": + if (args.length !== 2) { + console.log( + showErrorMessage("Usage: npx codexstorage --upload "), + ); + process.exit(1); + } + return { command: "upload", value: args[1] }; + + case "--download": + if (args.length !== 2) { + console.log( + showErrorMessage("Usage: npx codexstorage --download "), + ); + process.exit(1); + } + return { command: "download", value: args[1] }; + + default: + console.log( + showErrorMessage( + "Invalid command. Available commands:\n\n" + + "npx codexstorage\n" + + "npx codexstorage --upload \n" + + "npx codexstorage --download ", + ), + ); + process.exit(1); + } +} diff --git a/src/configmenu.js b/src/configmenu.js index fe0aa9c..d0696a2 100644 --- a/src/configmenu.js +++ b/src/configmenu.js @@ -1,14 +1,14 @@ -import inquirer from 'inquirer'; -import chalk from 'chalk'; -import { showErrorMessage, showInfoMessage } from './utils/messages.js'; -import { isDir, showPathSelector } from './utils/pathSelector.js'; -import { saveConfig } from './services/config.js'; -import { showNumberSelector } from './utils/numberSelector.js'; -import fs from 'fs-extra'; +import inquirer from "inquirer"; +import chalk from "chalk"; +import { showErrorMessage, showInfoMessage } from "./utils/messages.js"; +import { isDir, showPathSelector } from "./utils/pathSelector.js"; +import { saveConfig } from "./services/config.js"; +import { showNumberSelector } from "./utils/numberSelector.js"; +import fs from "fs-extra"; function bytesAmountToString(numBytes) { - const units = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - + const units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + var value = numBytes; var index = 0; while (value > 1024) { @@ -22,8 +22,12 @@ function bytesAmountToString(numBytes) { async function showStorageQuotaSelector(config) { console.log(showInfoMessage('You can use: "GB" or "gb", etc.')); - const result = await showNumberSelector(config.storageQuota, "Storage quota", true); - if (result < (100 * 1024 * 1024)) { + const result = await showNumberSelector( + config.storageQuota, + "Storage quota", + true, + ); + if (result < 100 * 1024 * 1024) { console.log(showErrorMessage("Storage quote should be >= 100mb.")); return config.storageQuota; } @@ -31,68 +35,86 @@ async function showStorageQuotaSelector(config) { } export async function showConfigMenu(config) { - var newDataDir = config.dataDir; - try { - while (true) { - console.log(showInfoMessage("Codex Configuration")); - const { choice } = await inquirer.prompt([ - { - type: 'list', - name: 'choice', - message: 'Select to edit:', - choices: [ - `1. Data path = "${newDataDir}"`, - `2. Logs path = "${config.logsDir}"`, - `3. Storage quota = ${bytesAmountToString(config.storageQuota)}`, - `4. Discovery port = ${config.ports.discPort}`, - `5. P2P listen port = ${config.ports.listenPort}`, - `6. API port = ${config.ports.apiPort}`, - '7. Save changes and exit', - '8. Discard changes and exit' - ], - pageSize: 8, - loop: true - } - ]).catch(() => { - return; - }); + var newDataDir = config.dataDir; + try { + while (true) { + console.log(showInfoMessage("Codex Configuration")); + const { choice } = await inquirer + .prompt([ + { + type: "list", + name: "choice", + message: "Select to edit:", + choices: [ + `1. Data path = "${newDataDir}"`, + `2. Logs path = "${config.logsDir}"`, + `3. Storage quota = ${bytesAmountToString(config.storageQuota)}`, + `4. Discovery port = ${config.ports.discPort}`, + `5. P2P listen port = ${config.ports.listenPort}`, + `6. API port = ${config.ports.apiPort}`, + "7. Save changes and exit", + "8. Discard changes and exit", + ], + pageSize: 8, + loop: true, + }, + ]) + .catch(() => { + return; + }); - switch (choice.split('.')[0]) { - case '1': - newDataDir = await showPathSelector(config.dataDir, false); - if (isDir(newDataDir)) { - console.log(showInfoMessage("Warning: The new data path already exists. Make sure you know what you're doing.")); - } - break; - case '2': - config.logsDir = await showPathSelector(config.logsDir, true); - break; - case '3': - config.storageQuota = await showStorageQuotaSelector(config); - break; - case '4': - config.ports.discPort = await showNumberSelector(config.ports.discPort, "Discovery Port (UDP)", false); - break; - case '5': - config.ports.listenPort = await showNumberSelector(config.ports.listenPort, "Listen Port (TCP)", false); - break; - case '6': - config.ports.apiPort = await showNumberSelector(config.ports.apiPort, "API Port (TCP)", false); - break; - case '7': - // save changes, back to main menu - config = updateDataDir(config, newDataDir); - saveConfig(config); - return; - case '8': - // discard changes, back to main menu - return; - } - } - } catch (error) { - console.error(chalk.red('An error occurred:', error.message)); - return; + switch (choice.split(".")[0]) { + case "1": + newDataDir = await showPathSelector(config.dataDir, false); + if (isDir(newDataDir)) { + console.log( + showInfoMessage( + "Warning: The new data path already exists. Make sure you know what you're doing.", + ), + ); + } + break; + case "2": + config.logsDir = await showPathSelector(config.logsDir, true); + break; + case "3": + config.storageQuota = await showStorageQuotaSelector(config); + break; + case "4": + config.ports.discPort = await showNumberSelector( + config.ports.discPort, + "Discovery Port (UDP)", + false, + ); + break; + case "5": + config.ports.listenPort = await showNumberSelector( + config.ports.listenPort, + "Listen Port (TCP)", + false, + ); + break; + case "6": + config.ports.apiPort = await showNumberSelector( + config.ports.apiPort, + "API Port (TCP)", + false, + ); + break; + case "7": + // save changes, back to main menu + config = updateDataDir(config, newDataDir); + saveConfig(config); + return; + case "8": + // discard changes, back to main menu + return; + } } + } catch (error) { + console.error(chalk.red("An error occurred:", error.message)); + return; + } } function updateDataDir(config, newDataDir) { @@ -104,28 +126,34 @@ function updateDataDir(config, newDataDir) { // If the old one does exist: We move it. if (isDir(config.dataDir)) { - console.log(showInfoMessage( - 'Moving Codex data folder...\n' + - `From: "${config.dataDir}"\n` + - `To: "${newDataDir}"` - )); + console.log( + showInfoMessage( + "Moving Codex data folder...\n" + + `From: "${config.dataDir}"\n` + + `To: "${newDataDir}"`, + ), + ); try { fs.moveSync(config.dataDir, newDataDir); } catch (error) { - console.log(showErrorMessage("Error while moving dataDir: " + error.message)); + console.log( + showErrorMessage("Error while moving dataDir: " + error.message), + ); throw error; } } else { // Old data dir does not exist. if (isDir(newDataDir)) { - console.log(showInfoMessage( - "Warning: the selected data path already exists.\n" + - `New data path = "${newDataDir}"\n` + - "Codex may overwrite data in this folder.\n" + - "Codex will fail to start if this folder does not have the required\n" + - "security permissions." - )); + console.log( + showInfoMessage( + "Warning: the selected data path already exists.\n" + + `New data path = "${newDataDir}"\n` + + "Codex may overwrite data in this folder.\n" + + "Codex will fail to start if this folder does not have the required\n" + + "security permissions.", + ), + ); } } diff --git a/src/constants/ascii.js b/src/constants/ascii.js index 70a6a09..bd7f477 100644 --- a/src/constants/ascii.js +++ b/src/constants/ascii.js @@ -16,4 +16,4 @@ export const ASCII_ART = ` +--------------------------------------------------------------------+ | Docs : docs.codex.storage | Discord : discord.gg/codex-storage | +--------------------------------------------------------------------+ -`; \ No newline at end of file +`; diff --git a/src/handlers/fileHandlers.js b/src/handlers/fileHandlers.js index d0f458b..b538368 100644 --- a/src/handlers/fileHandlers.js +++ b/src/handlers/fileHandlers.js @@ -1,197 +1,263 @@ -import { createSpinner } from 'nanospinner'; -import { runCommand } from '../utils/command.js'; -import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js'; -import { isNodeRunning } from '../services/nodeService.js'; -import fs from 'fs/promises'; -import boxen from 'boxen'; -import chalk from 'chalk'; -import inquirer from 'inquirer'; -import path from 'path'; -import mime from 'mime-types'; -import axios from 'axios'; +import { createSpinner } from "nanospinner"; +import { runCommand } from "../utils/command.js"; +import { + showErrorMessage, + showInfoMessage, + showSuccessMessage, +} from "../utils/messages.js"; +import { isNodeRunning } from "../services/nodeService.js"; +import fs from "fs/promises"; +import boxen from "boxen"; +import chalk from "chalk"; +import inquirer from "inquirer"; +import path from "path"; +import mime from "mime-types"; +import axios from "axios"; -export async function uploadFile(config, filePath = null, handleCommandLineOperation, showNavigationMenu) { - const nodeRunning = await isNodeRunning(config); - if (!nodeRunning) { - console.log(showErrorMessage('Codex node is not running. Try again after starting the node')); - return handleCommandLineOperation() ? process.exit(1) : showNavigationMenu(); - } +export async function uploadFile( + config, + filePath = null, + handleCommandLineOperation, + showNavigationMenu, +) { + const nodeRunning = await isNodeRunning(config); + if (!nodeRunning) { + console.log( + showErrorMessage( + "Codex node is not running. Try again after starting the node", + ), + ); + return handleCommandLineOperation() + ? process.exit(1) + : showNavigationMenu(); + } - console.log(boxen( - chalk.yellow('⚠️ Codex does not encrypt files. Anything uploaded will be available publicly on testnet.\nThe testnet does not provide any guarantees - please do not use in production.'), - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'yellow', - title: '⚠️ Warning', - titleAlignment: 'center' - } - )); + console.log( + boxen( + chalk.yellow( + "⚠️ Codex does not encrypt files. Anything uploaded will be available publicly on testnet.\nThe testnet does not provide any guarantees - please do not use in production.", + ), + { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "yellow", + title: "⚠️ Warning", + titleAlignment: "center", + }, + ), + ); - let fileToUpload = filePath; - if (!fileToUpload) { - const { inputPath } = await inquirer.prompt([ - { - type: 'input', - name: 'inputPath', - message: 'Enter the file path to upload:', - validate: input => input.length > 0 - } - ]); - fileToUpload = inputPath; - } + let fileToUpload = filePath; + if (!fileToUpload) { + const { inputPath } = await inquirer.prompt([ + { + type: "input", + name: "inputPath", + message: "Enter the file path to upload:", + validate: (input) => input.length > 0, + }, + ]); + fileToUpload = inputPath; + } + try { + await fs.access(fileToUpload); + + const filename = path.basename(fileToUpload); + const contentType = mime.lookup(fileToUpload) || "application/octet-stream"; + + const spinner = createSpinner("Uploading file").start(); try { - await fs.access(fileToUpload); - - const filename = path.basename(fileToUpload); - const contentType = mime.lookup(fileToUpload) || 'application/octet-stream'; - - const spinner = createSpinner('Uploading file').start(); - try { - const result = await runCommand( - `curl -X POST http://localhost:${config.ports.apiPort}/api/codex/v1/data ` + - `-H 'Content-Type: ${contentType}' ` + - `-H 'Content-Disposition: attachment; filename="${filename}"' ` + - `-w '\\n' -T "${fileToUpload}"` - ); - spinner.success(); - console.log(showSuccessMessage('Successfully uploaded!\n\nCID: ' + result.trim())); - } catch (error) { - spinner.error(); - throw new Error(`Failed to upload: ${error.message}`); - } + const result = await runCommand( + `curl -X POST http://localhost:${config.ports.apiPort}/api/codex/v1/data ` + + `-H 'Content-Type: ${contentType}' ` + + `-H 'Content-Disposition: attachment; filename="${filename}"' ` + + `-w '\\n' -T "${fileToUpload}"`, + ); + spinner.success(); + console.log( + showSuccessMessage("Successfully uploaded!\n\nCID: " + result.trim()), + ); } catch (error) { - console.log(showErrorMessage(error.code === 'ENOENT' - ? `File not found: ${fileToUpload}` - : `Error uploading file: ${error.message}`)); + spinner.error(); + throw new Error(`Failed to upload: ${error.message}`); } + } catch (error) { + console.log( + showErrorMessage( + error.code === "ENOENT" + ? `File not found: ${fileToUpload}` + : `Error uploading file: ${error.message}`, + ), + ); + } - return handleCommandLineOperation() ? process.exit(0) : showNavigationMenu(); + return handleCommandLineOperation() ? process.exit(0) : showNavigationMenu(); } -export async function downloadFile(config, cid = null, handleCommandLineOperation, showNavigationMenu) { - const nodeRunning = await isNodeRunning(config); - if (!nodeRunning) { - console.log(showErrorMessage('Codex node is not running. Try again after starting the node')); - return handleCommandLineOperation() ? process.exit(1) : showNavigationMenu(); - } +export async function downloadFile( + config, + cid = null, + handleCommandLineOperation, + showNavigationMenu, +) { + const nodeRunning = await isNodeRunning(config); + if (!nodeRunning) { + console.log( + showErrorMessage( + "Codex node is not running. Try again after starting the node", + ), + ); + return handleCommandLineOperation() + ? process.exit(1) + : showNavigationMenu(); + } - let cidToDownload = cid; - if (!cidToDownload) { - const { inputCid } = await inquirer.prompt([ - { - type: 'input', - name: 'inputCid', - message: 'Enter the CID:', - validate: input => input.length > 0 - } - ]); - cidToDownload = inputCid; - } + let cidToDownload = cid; + if (!cidToDownload) { + const { inputCid } = await inquirer.prompt([ + { + type: "input", + name: "inputCid", + message: "Enter the CID:", + validate: (input) => input.length > 0, + }, + ]); + cidToDownload = inputCid; + } + try { + const spinner = createSpinner("Fetching file metadata...").start(); try { - const spinner = createSpinner('Fetching file metadata...').start(); - try { - // First, get the file metadata - const metadataResponse = await axios.post(`http://localhost:${config.ports.apiPort}/api/codex/v1/data/${cidToDownload}/network`); - const { manifest } = metadataResponse.data; - const { filename, mimetype } = manifest; + // First, get the file metadata + const metadataResponse = await axios.post( + `http://localhost:${config.ports.apiPort}/api/codex/v1/data/${cidToDownload}/network`, + ); + const { manifest } = metadataResponse.data; + const { filename, mimetype } = manifest; - spinner.success(); - spinner.start('Downloading file...'); + spinner.success(); + spinner.start("Downloading file..."); - // Then download the file with the correct filename - await runCommand(`curl "http://localhost:${config.ports.apiPort}/api/codex/v1/data/${cidToDownload}/network/stream" -o "${filename}"`); - - spinner.success(); - console.log(showSuccessMessage( - 'Successfully downloaded!\n\n' + - `Filename: ${filename}\n` + - `Type: ${mimetype}` - )); + // Then download the file with the correct filename + await runCommand( + `curl "http://localhost:${config.ports.apiPort}/api/codex/v1/data/${cidToDownload}/network/stream" -o "${filename}"`, + ); - // Show file details - console.log(boxen( - `${chalk.cyan('File Details')}\n\n` + - `${chalk.cyan('Filename:')} ${filename}\n` + - `${chalk.cyan('MIME Type:')} ${mimetype}\n` + - `${chalk.cyan('CID:')} ${cidToDownload}\n` + - `${chalk.cyan('Protected:')} ${manifest.protected ? chalk.green('Yes') : chalk.red('No')}\n` + - `${chalk.cyan('Uploaded:')} ${new Date(manifest.uploadedAt * 1000).toLocaleString()}`, - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'blue', - title: '📁 Download Complete', - titleAlignment: 'center' - } - )); - } catch (error) { - spinner.error(); - if (error.response) { - throw new Error(`Failed to download: ${error.response.data.message || 'File not found'}`); - } else { - throw new Error(`Failed to download: ${error.message}`); - } - } + spinner.success(); + console.log( + showSuccessMessage( + "Successfully downloaded!\n\n" + + `Filename: ${filename}\n` + + `Type: ${mimetype}`, + ), + ); + + // Show file details + console.log( + boxen( + `${chalk.cyan("File Details")}\n\n` + + `${chalk.cyan("Filename:")} ${filename}\n` + + `${chalk.cyan("MIME Type:")} ${mimetype}\n` + + `${chalk.cyan("CID:")} ${cidToDownload}\n` + + `${chalk.cyan("Protected:")} ${manifest.protected ? chalk.green("Yes") : chalk.red("No")}\n` + + `${chalk.cyan("Uploaded:")} ${new Date(manifest.uploadedAt * 1000).toLocaleString()}`, + { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "blue", + title: "📁 Download Complete", + titleAlignment: "center", + }, + ), + ); } catch (error) { - console.log(showErrorMessage(`Error downloading file: ${error.message}`)); + spinner.error(); + if (error.response) { + throw new Error( + `Failed to download: ${error.response.data.message || "File not found"}`, + ); + } else { + throw new Error(`Failed to download: ${error.message}`); + } } + } catch (error) { + console.log(showErrorMessage(`Error downloading file: ${error.message}`)); + } - return handleCommandLineOperation() ? process.exit(0) : showNavigationMenu(); + return handleCommandLineOperation() ? process.exit(0) : showNavigationMenu(); } export async function showLocalFiles(config, showNavigationMenu) { - const nodeRunning = await isNodeRunning(config); - if (!nodeRunning) { - console.log(showErrorMessage('Codex node is not running. Try again after starting the node')); - await showNavigationMenu(); - return; - } - - try { - const spinner = createSpinner('Fetching local files...').start(); - const filesResponse = await axios.get(`http://localhost:${config.ports.apiPort}/api/codex/v1/data`); - const filesData = filesResponse.data; - spinner.success(); - - if (filesData.content && filesData.content.length > 0) { - console.log(showInfoMessage(`Found ${filesData.content.length} local file(s)`)); - - filesData.content.forEach((file, index) => { - const { cid, manifest } = file; - const { rootHash, originalBytes, blockSize, protected: isProtected, filename, mimetype, uploadedAt } = manifest; - - const uploadedDate = new Date(uploadedAt * 1000).toLocaleString(); - const fileSize = (originalBytes / 1024).toFixed(2); - - console.log(boxen( - `${chalk.cyan('File')} ${index + 1} of ${filesData.content.length}\n\n` + - `${chalk.cyan('Filename:')} ${filename}\n` + - `${chalk.cyan('CID:')} ${cid}\n` + - `${chalk.cyan('Size:')} ${fileSize} KB\n` + - `${chalk.cyan('MIME Type:')} ${mimetype}\n` + - `${chalk.cyan('Uploaded:')} ${uploadedDate}\n` + - `${chalk.cyan('Protected:')} ${isProtected ? chalk.green('Yes') : chalk.red('No')}`, - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'blue', - title: `📁 File Details`, - titleAlignment: 'center' - } - )); - }); - } else { - console.log(showInfoMessage("Node contains no datasets.")); - } - } catch (error) { - console.log(showErrorMessage(`Failed to fetch local files: ${error.message}`)); - } - + const nodeRunning = await isNodeRunning(config); + if (!nodeRunning) { + console.log( + showErrorMessage( + "Codex node is not running. Try again after starting the node", + ), + ); await showNavigationMenu(); -} \ No newline at end of file + return; + } + + try { + const spinner = createSpinner("Fetching local files...").start(); + const filesResponse = await axios.get( + `http://localhost:${config.ports.apiPort}/api/codex/v1/data`, + ); + const filesData = filesResponse.data; + spinner.success(); + + if (filesData.content && filesData.content.length > 0) { + console.log( + showInfoMessage(`Found ${filesData.content.length} local file(s)`), + ); + + filesData.content.forEach((file, index) => { + const { cid, manifest } = file; + const { + rootHash, + originalBytes, + blockSize, + protected: isProtected, + filename, + mimetype, + uploadedAt, + } = manifest; + + const uploadedDate = new Date(uploadedAt * 1000).toLocaleString(); + const fileSize = (originalBytes / 1024).toFixed(2); + + console.log( + boxen( + `${chalk.cyan("File")} ${index + 1} of ${filesData.content.length}\n\n` + + `${chalk.cyan("Filename:")} ${filename}\n` + + `${chalk.cyan("CID:")} ${cid}\n` + + `${chalk.cyan("Size:")} ${fileSize} KB\n` + + `${chalk.cyan("MIME Type:")} ${mimetype}\n` + + `${chalk.cyan("Uploaded:")} ${uploadedDate}\n` + + `${chalk.cyan("Protected:")} ${isProtected ? chalk.green("Yes") : chalk.red("No")}`, + { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "blue", + title: `📁 File Details`, + titleAlignment: "center", + }, + ), + ); + }); + } else { + console.log(showInfoMessage("Node contains no datasets.")); + } + } catch (error) { + console.log( + showErrorMessage(`Failed to fetch local files: ${error.message}`), + ); + } + + await showNavigationMenu(); +} diff --git a/src/handlers/installationHandlers.js b/src/handlers/installationHandlers.js index 8234144..0bd5483 100644 --- a/src/handlers/installationHandlers.js +++ b/src/handlers/installationHandlers.js @@ -1,158 +1,192 @@ -import path from 'path'; -import inquirer from 'inquirer'; -import boxen from 'boxen'; -import chalk from 'chalk'; -import os from 'os'; -import fs from 'fs'; -import { createSpinner } from 'nanospinner'; -import { runCommand } from '../utils/command.js'; -import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js'; -import { checkDependencies } from '../services/nodeService.js'; -import { saveConfig } from '../services/config.js'; -import { getCodexRootPath, getCodexBinPath } from '../utils/appdata.js'; +import path from "path"; +import inquirer from "inquirer"; +import boxen from "boxen"; +import chalk from "chalk"; +import os from "os"; +import fs from "fs"; +import { createSpinner } from "nanospinner"; +import { runCommand } from "../utils/command.js"; +import { + showErrorMessage, + showInfoMessage, + showSuccessMessage, +} from "../utils/messages.js"; +import { checkDependencies } from "../services/nodeService.js"; +import { saveConfig } from "../services/config.js"; +import { getCodexRootPath, getCodexBinPath } from "../utils/appdata.js"; const platform = os.platform(); async function showPrivacyDisclaimer() { - const disclaimer = boxen(` -${chalk.yellow.bold('Privacy Disclaimer')} + const disclaimer = boxen( + ` +${chalk.yellow.bold("Privacy Disclaimer")} Codex is currently in testnet and to make your testnet experience better, we are currently tracking some of your node and network information such as: -${chalk.cyan('- Node ID')} -${chalk.cyan('- Peer ID')} -${chalk.cyan('- Public IP address')} -${chalk.cyan('- Codex node version')} -${chalk.cyan('- Number of connected peers')} -${chalk.cyan('- Discovery and listening ports')} +${chalk.cyan("- Node ID")} +${chalk.cyan("- Peer ID")} +${chalk.cyan("- Public IP address")} +${chalk.cyan("- Codex node version")} +${chalk.cyan("- Number of connected peers")} +${chalk.cyan("- Discovery and listening ports")} These information will be used for calculating various metrics that can eventually make the Codex experience better. Please agree to the following disclaimer to continue using the Codex Storage CLI or alternatively, use the manual setup instructions at docs.codex.storage. -`, { - padding: 1, - margin: 1, - borderStyle: 'double', - borderColor: 'yellow', - title: '📋 IMPORTANT', - titleAlignment: 'center' - }); +`, + { + padding: 1, + margin: 1, + borderStyle: "double", + borderColor: "yellow", + title: "📋 IMPORTANT", + titleAlignment: "center", + }, + ); - console.log(disclaimer); + console.log(disclaimer); - const { agreement } = await inquirer.prompt([ - { - type: 'input', - name: 'agreement', - message: 'Do you agree to the privacy disclaimer? (y/n):', - validate: (input) => { - const lowercased = input.toLowerCase(); - if (lowercased === 'y' || lowercased === 'n') { - return true; - } - return 'Please enter either y or n'; - } + const { agreement } = await inquirer.prompt([ + { + type: "input", + name: "agreement", + message: "Do you agree to the privacy disclaimer? (y/n):", + validate: (input) => { + const lowercased = input.toLowerCase(); + if (lowercased === "y" || lowercased === "n") { + return true; } - ]); + return "Please enter either y or n"; + }, + }, + ]); - return agreement.toLowerCase() === 'y'; + return agreement.toLowerCase() === "y"; } export async function getCodexVersion(config) { - if (config.codexExe.length < 1) return ""; + if (config.codexExe.length < 1) return ""; - try { - const version = await runCommand(`"${config.codexExe}" --version`); - if (version.length < 1) throw new Error("Version info not found."); - return version; - } catch (error) { - return ""; - } + try { + const version = await runCommand(`"${config.codexExe}" --version`); + if (version.length < 1) throw new Error("Version info not found."); + return version; + } catch (error) { + return ""; + } } export async function installCodex(config, showNavigationMenu) { - const version = await getCodexVersion(config); + const version = await getCodexVersion(config); - if (version.length > 0) { - console.log(chalk.green('Codex is already installed. Version:')); - console.log(chalk.green(version)); - await showNavigationMenu(); - return false; - } else { - console.log(chalk.cyanBright('Codex is not installed, proceeding with installation...')); - return await performInstall(config); - } + if (version.length > 0) { + console.log(chalk.green("Codex is already installed. Version:")); + console.log(chalk.green(version)); + await showNavigationMenu(); + return false; + } else { + console.log( + chalk.cyanBright( + "Codex is not installed, proceeding with installation...", + ), + ); + return await performInstall(config); + } } async function saveCodexExePath(config, codexExePath) { - config.codexExe = codexExePath; - if (!fs.existsSync(config.codexExe)) { - console.log(showErrorMessage(`Codex executable not found in expected path: ${config.codexExe}`)); - throw new Error("Exe not found"); - } - if (await getCodexVersion(config).length < 1) { - console.log(showInfoMessage("no")); - throw new Error(`Codex not found at path after install. Path: '${config.codexExe}'`); - } - saveConfig(config); + config.codexExe = codexExePath; + if (!fs.existsSync(config.codexExe)) { + console.log( + showErrorMessage( + `Codex executable not found in expected path: ${config.codexExe}`, + ), + ); + throw new Error("Exe not found"); + } + if ((await getCodexVersion(config).length) < 1) { + console.log(showInfoMessage("no")); + throw new Error( + `Codex not found at path after install. Path: '${config.codexExe}'`, + ); + } + saveConfig(config); } async function clearCodexExePathFromConfig(config) { - config.codexExe = ""; - saveConfig(config); + config.codexExe = ""; + saveConfig(config); } async function performInstall(config) { - const agreed = await showPrivacyDisclaimer(); - if (!agreed) { - console.log(showInfoMessage('You can find manual setup instructions at docs.codex.storage')); - process.exit(0); - } + const agreed = await showPrivacyDisclaimer(); + if (!agreed) { + console.log( + showInfoMessage( + "You can find manual setup instructions at docs.codex.storage", + ), + ); + process.exit(0); + } - const installPath = getCodexBinPath(); - console.log(showInfoMessage("Install location: " + installPath)); + const installPath = getCodexBinPath(); + console.log(showInfoMessage("Install location: " + installPath)); - const spinner = createSpinner('Installing Codex...').start(); + const spinner = createSpinner("Installing Codex...").start(); - try { - if (platform === 'win32') { - try { - try { - await runCommand('curl --version'); - } catch (error) { - throw new Error('curl is not available. Please install curl or update your Windows version.'); - } + try { + if (platform === "win32") { + try { + try { + await runCommand("curl --version"); + } catch (error) { + throw new Error( + "curl is not available. Please install curl or update your Windows version.", + ); + } - await runCommand('curl -LO --ssl-no-revoke https://get.codex.storage/install.cmd'); - await runCommand(`set "INSTALL_DIR=${installPath}" && "${process.cwd()}\\install.cmd"`); - - await saveCodexExePath(config, path.join(installPath, "codex.exe")); + await runCommand( + "curl -LO --ssl-no-revoke https://get.codex.storage/install.cmd", + ); + await runCommand( + `set "INSTALL_DIR=${installPath}" && "${process.cwd()}\\install.cmd"`, + ); - try { - await runCommand('del /f install.cmd'); - } catch (error) { - // Ignore cleanup errors - } - } catch (error) { - if (error.message.includes('Access is denied')) { - throw new Error('Installation failed. Please run the command prompt as Administrator and try again.'); - } else if (error.message.includes('curl')) { - throw new Error(error.message); - } else { - throw new Error(`Installation failed: "${error.message}"`); - } - } + await saveCodexExePath(config, path.join(installPath, "codex.exe")); + + try { + await runCommand("del /f install.cmd"); + } catch (error) { + // Ignore cleanup errors + } + } catch (error) { + if (error.message.includes("Access is denied")) { + throw new Error( + "Installation failed. Please run the command prompt as Administrator and try again.", + ); + } else if (error.message.includes("curl")) { + throw new Error(error.message); } else { - try { - const dependenciesInstalled = await checkDependencies(); - if (!dependenciesInstalled) { - console.log(showInfoMessage('Please install the required dependencies and try again.')); - throw new Error("Missing dependencies."); - } + throw new Error(`Installation failed: "${error.message}"`); + } + } + } else { + try { + const dependenciesInstalled = await checkDependencies(); + if (!dependenciesInstalled) { + console.log( + showInfoMessage( + "Please install the required dependencies and try again.", + ), + ); + throw new Error("Missing dependencies."); + } - const downloadCommand = 'curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh'; - await runCommand(downloadCommand); - - if (platform === 'darwin') { - const timeoutCommand = `perl -e ' + const downloadCommand = + "curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh"; + await runCommand(downloadCommand); + + if (platform === "darwin") { + const timeoutCommand = `perl -e ' eval { local $SIG{ALRM} = sub { die "timeout\\n" }; alarm(120); @@ -161,90 +195,112 @@ async function performInstall(config) { }; die if $@; '`; - await runCommand(timeoutCommand); - } else { - await runCommand(`INSTALL_DIR="${installPath}" timeout 120 bash install.sh`); - } - - await saveCodexExePath(config, path.join(installPath, "codex")); - - } catch (error) { - if (error.message.includes('ECONNREFUSED') || error.message.includes('ETIMEDOUT')) { - throw new Error('Installation failed. Please check your internet connection and try again.'); - } else if (error.message.includes('Permission denied')) { - throw new Error('Permission denied. Please try running the command with sudo.'); - } else if (error.message.includes('timeout')) { - throw new Error('Installation is taking longer than expected. Please try running with sudo.'); - } else { - throw new Error('Installation failed. Please try running with sudo if you haven\'t already.'); - } - } finally { - await runCommand('rm -f install.sh').catch(() => {}); - } - } - - try { - const version = await getCodexVersion(config); - console.log(chalk.green(version)); - - console.log(showSuccessMessage( - 'Codex is successfully installed!\n' + - `Install path: "${config.codexExe}"\n\n` + - 'The default configuration should work for most platforms.\n' + - 'Please review the configuration before starting Codex.\n' - )); - } catch (error) { - throw new Error('Installation completed but Codex command is not available. Please restart your terminal and try again.'); + await runCommand(timeoutCommand); + } else { + await runCommand( + `INSTALL_DIR="${installPath}" timeout 120 bash install.sh`, + ); } - console.log(showInfoMessage( - "Please review the configuration before starting Codex." - )); - - spinner.success(); - return true; - } catch (error) { - spinner.error(); - console.log(showErrorMessage(`Failed to install Codex: ${error.message}`)); - return false; - } -} - -function removeDir(dir) { - fs.rmSync(dir, { recursive: true, force: true }); -} - -export async function uninstallCodex(config, showNavigationMenu) { - const { confirm } = await inquirer.prompt([ - { - type: 'confirm', - name: 'confirm', - message: chalk.yellow( - '⚠️ Are you sure you want to uninstall Codex? This action cannot be undone. \n' + - 'All data stored in the local Codex node will be deleted as well.' - ), - default: false + await saveCodexExePath(config, path.join(installPath, "codex")); + } catch (error) { + if ( + error.message.includes("ECONNREFUSED") || + error.message.includes("ETIMEDOUT") + ) { + throw new Error( + "Installation failed. Please check your internet connection and try again.", + ); + } else if (error.message.includes("Permission denied")) { + throw new Error( + "Permission denied. Please try running the command with sudo.", + ); + } else if (error.message.includes("timeout")) { + throw new Error( + "Installation is taking longer than expected. Please try running with sudo.", + ); + } else { + throw new Error( + "Installation failed. Please try running with sudo if you haven't already.", + ); } - ]); - - if (!confirm) { - console.log(showInfoMessage('Uninstall cancelled.')); - await showNavigationMenu(); - return; + } finally { + await runCommand("rm -f install.sh").catch(() => {}); + } } try { - removeDir(getCodexRootPath()); - clearCodexExePathFromConfig(config); + const version = await getCodexVersion(config); + console.log(chalk.green(version)); - console.log(showSuccessMessage('Codex has been successfully uninstalled.')); - await showNavigationMenu(); + console.log( + showSuccessMessage( + "Codex is successfully installed!\n" + + `Install path: "${config.codexExe}"\n\n` + + "The default configuration should work for most platforms.\n" + + "Please review the configuration before starting Codex.\n", + ), + ); } catch (error) { - if (error.code === 'ENOENT') { - console.log(showInfoMessage('Codex binary not found, nothing to uninstall.')); - } else { - console.log(showErrorMessage('Failed to uninstall Codex. Please make sure Codex is installed before trying to remove it.')); - } - await showNavigationMenu(); + throw new Error( + "Installation completed but Codex command is not available. Please restart your terminal and try again.", + ); } + + console.log( + showInfoMessage("Please review the configuration before starting Codex."), + ); + + spinner.success(); + return true; + } catch (error) { + spinner.error(); + console.log(showErrorMessage(`Failed to install Codex: ${error.message}`)); + return false; + } +} + +function removeDir(dir) { + fs.rmSync(dir, { recursive: true, force: true }); +} + +export async function uninstallCodex(config, showNavigationMenu) { + const { confirm } = await inquirer.prompt([ + { + type: "confirm", + name: "confirm", + message: chalk.yellow( + "⚠️ Are you sure you want to uninstall Codex? This action cannot be undone. \n" + + "All data stored in the local Codex node will be deleted as well.", + ), + default: false, + }, + ]); + + if (!confirm) { + console.log(showInfoMessage("Uninstall cancelled.")); + await showNavigationMenu(); + return; + } + + try { + removeDir(getCodexRootPath()); + clearCodexExePathFromConfig(config); + + console.log(showSuccessMessage("Codex has been successfully uninstalled.")); + await showNavigationMenu(); + } catch (error) { + if (error.code === "ENOENT") { + console.log( + showInfoMessage("Codex binary not found, nothing to uninstall."), + ); + } else { + console.log( + showErrorMessage( + "Failed to uninstall Codex. Please make sure Codex is installed before trying to remove it.", + ), + ); + } + await showNavigationMenu(); + } } diff --git a/src/handlers/nodeHandlers.js b/src/handlers/nodeHandlers.js index ef0eddb..e648792 100644 --- a/src/handlers/nodeHandlers.js +++ b/src/handlers/nodeHandlers.js @@ -1,289 +1,349 @@ -import path from 'path'; -import { createSpinner } from 'nanospinner'; -import { runCommand } from '../utils/command.js'; -import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js'; -import { isNodeRunning, isCodexInstalled, startPeriodicLogging, getWalletAddress, setWalletAddress } from '../services/nodeService.js'; -import inquirer from 'inquirer'; -import boxen from 'boxen'; -import chalk from 'chalk'; -import os from 'os'; -import { exec } from 'child_process'; -import axios from 'axios'; +import path from "path"; +import { createSpinner } from "nanospinner"; +import { runCommand } from "../utils/command.js"; +import { + showErrorMessage, + showInfoMessage, + showSuccessMessage, +} from "../utils/messages.js"; +import { + isNodeRunning, + isCodexInstalled, + startPeriodicLogging, + getWalletAddress, + setWalletAddress, +} from "../services/nodeService.js"; +import inquirer from "inquirer"; +import boxen from "boxen"; +import chalk from "chalk"; +import os from "os"; +import { exec } from "child_process"; +import axios from "axios"; const platform = os.platform(); async function promptForWalletAddress() { - const { wallet } = await inquirer.prompt([ - { - type: 'input', - name: 'wallet', - message: 'Please enter your ERC20 wallet address (or press enter to skip):', - validate: (input) => { - if (!input) return true; // Allow empty input - if (/^0x[a-fA-F0-9]{40}$/.test(input)) return true; - return 'Please enter a valid ERC20 wallet address (0x followed by 40 hexadecimal characters) or press enter to skip'; - } - } - ]); - return wallet || null; + const { wallet } = await inquirer.prompt([ + { + type: "input", + name: "wallet", + message: + "Please enter your ERC20 wallet address (or press enter to skip):", + validate: (input) => { + if (!input) return true; // Allow empty input + if (/^0x[a-fA-F0-9]{40}$/.test(input)) return true; + return "Please enter a valid ERC20 wallet address (0x followed by 40 hexadecimal characters) or press enter to skip"; + }, + }, + ]); + return wallet || null; } function getCurrentLogFile(config) { - const timestamp = new Date().toISOString() - .replaceAll(":", "-") - .replaceAll(".", "-"); - return path.join(config.logsDir, `codex_${timestamp}.log`); + const timestamp = new Date() + .toISOString() + .replaceAll(":", "-") + .replaceAll(".", "-"); + return path.join(config.logsDir, `codex_${timestamp}.log`); } export async function runCodex(config, showNavigationMenu) { - const isInstalled = await isCodexInstalled(config); - if (!isInstalled) { - console.log(showErrorMessage('Codex is not installed. Please install Codex first using option 1 from the main menu.')); - await showNavigationMenu(); - return; - } + const isInstalled = await isCodexInstalled(config); + if (!isInstalled) { + console.log( + showErrorMessage( + "Codex is not installed. Please install Codex first using option 1 from the main menu.", + ), + ); + await showNavigationMenu(); + return; + } - const nodeAlreadyRunning = await isNodeRunning(config); + const nodeAlreadyRunning = await isNodeRunning(config); - if (nodeAlreadyRunning) { - console.log(showInfoMessage('A Codex node is already running.')); - await showNavigationMenu(); - } else { - try { - let nat; - if (platform === 'win32') { - const result = await runCommand('for /f "delims=" %a in (\'curl -s --ssl-reqd ip.codex.storage\') do @echo %a'); - nat = result.trim(); - } else { - nat = await runCommand('curl -s https://ip.codex.storage'); + if (nodeAlreadyRunning) { + console.log(showInfoMessage("A Codex node is already running.")); + await showNavigationMenu(); + } else { + try { + let nat; + if (platform === "win32") { + const result = await runCommand( + "for /f \"delims=\" %a in ('curl -s --ssl-reqd ip.codex.storage') do @echo %a", + ); + nat = result.trim(); + } else { + nat = await runCommand("curl -s https://ip.codex.storage"); + } + + if (config.dataDir.length < 1) throw new Error("Missing config: dataDir"); + if (config.logsDir.length < 1) throw new Error("Missing config: logsDir"); + const logFilePath = getCurrentLogFile(config); + + console.log( + showInfoMessage( + `Data location: ${config.dataDir}\n` + + `Logs: ${logFilePath}\n` + + `API port: ${config.ports.apiPort}`, + ), + ); + + const executable = config.codexExe; + const args = [ + `--data-dir="${config.dataDir}"`, + `--log-level=DEBUG`, + `--log-file="${logFilePath}"`, + `--storage-quota="${config.storageQuota}"`, + `--disc-port=${config.ports.discPort}`, + `--listen-addrs=/ip4/0.0.0.0/tcp/${config.ports.listenPort}`, + `--api-port=${config.ports.apiPort}`, + `--nat=${nat}`, + `--api-cors-origin="*"`, + `--bootstrap-node=spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P`, + ]; + + const command = `"${executable}" ${args.join(" ")}`; + + console.log( + showInfoMessage( + "🚀 Codex node is running...\n\n" + + "If your firewall ask, be sure to allow Codex to receive connections. \n" + + "Please keep this terminal open. Start a new terminal to interact with the node.\n\n" + + "Press CTRL+C to stop the node", + ), + ); + + const nodeProcess = exec(command); + + await new Promise((resolve) => setTimeout(resolve, 5000)); + + try { + const response = await axios.get( + `http://localhost:${config.ports.apiPort}/api/codex/v1/debug/info`, + ); + if (response.status === 200) { + // Check if wallet exists + try { + const existingWallet = await getWalletAddress(); + if (!existingWallet) { + console.log( + showInfoMessage( + "[OPTIONAL] Please provide your ERC20 wallet address.", + ), + ); + const wallet = await promptForWalletAddress(); + if (wallet) { + await setWalletAddress(wallet); + console.log( + showSuccessMessage("Wallet address saved successfully!"), + ); + } } + } catch (error) { + console.log( + showErrorMessage( + "Failed to process wallet address. Continuing without wallet update.", + ), + ); + } - if (config.dataDir.length < 1) throw new Error("Missing config: dataDir"); - if (config.logsDir.length < 1) throw new Error("Missing config: logsDir"); - const logFilePath = getCurrentLogFile(config); + // Start periodic logging + const stopLogging = await startPeriodicLogging(config); - console.log(showInfoMessage( - `Data location: ${config.dataDir}\n` + - `Logs: ${logFilePath}\n` + - `API port: ${config.ports.apiPort}` - )); + nodeProcess.on("exit", () => { + stopLogging(); + }); - const executable = config.codexExe; - const args = [ - `--data-dir="${config.dataDir}"`, - `--log-level=DEBUG`, - `--log-file="${logFilePath}"`, - `--storage-quota="${config.storageQuota}"`, - `--disc-port=${config.ports.discPort}`, - `--listen-addrs=/ip4/0.0.0.0/tcp/${config.ports.listenPort}`, - `--api-port=${config.ports.apiPort}`, - `--nat=${nat}`, - `--api-cors-origin="*"`, - `--bootstrap-node=spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P` - ]; - - const command = - `"${executable}" ${args.join(" ")}` - - console.log(showInfoMessage( - '🚀 Codex node is running...\n\n' + - 'If your firewall ask, be sure to allow Codex to receive connections. \n' + - 'Please keep this terminal open. Start a new terminal to interact with the node.\n\n' + - 'Press CTRL+C to stop the node' - )); - - const nodeProcess = exec(command); - - await new Promise(resolve => setTimeout(resolve, 5000)); - - try { - const response = await axios.get(`http://localhost:${config.ports.apiPort}/api/codex/v1/debug/info`); - if (response.status === 200) { - // Check if wallet exists - try { - const existingWallet = await getWalletAddress(); - if (!existingWallet) { - console.log(showInfoMessage('[OPTIONAL] Please provide your ERC20 wallet address.')); - const wallet = await promptForWalletAddress(); - if (wallet) { - await setWalletAddress(wallet); - console.log(showSuccessMessage('Wallet address saved successfully!')); - } - } - } catch (error) { - console.log(showErrorMessage('Failed to process wallet address. Continuing without wallet update.')); - } - - // Start periodic logging - const stopLogging = await startPeriodicLogging(config); - - nodeProcess.on('exit', () => { - stopLogging(); - }); - - console.log(boxen( - chalk.cyan('We are logging some of your node\'s public data for improving the Codex experience'), - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'cyan', - title: '🔒 Privacy Notice', - titleAlignment: 'center', - dimBorder: true - } - )); - } - } catch (error) { - // Silently handle any logging errors - } - - await new Promise((resolve, reject) => { - nodeProcess.on('exit', (code) => { - if (code === 0) resolve(); - else reject(new Error(`Node exited with code ${code}`)); - }); - }); - - if (platform === 'win32') { - console.log(showInfoMessage('Cleaning up firewall rules...')); - await runCommand('netsh advfirewall firewall delete rule name="Allow Codex (TCP-In)"'); - await runCommand('netsh advfirewall firewall delete rule name="Allow Codex (UDP-In)"'); - } - - } catch (error) { - console.log(showErrorMessage(`Failed to run Codex: ${error.message}`)); + console.log( + boxen( + chalk.cyan( + "We are logging some of your node's public data for improving the Codex experience", + ), + { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "cyan", + title: "🔒 Privacy Notice", + titleAlignment: "center", + dimBorder: true, + }, + ), + ); } - await showNavigationMenu(); + } catch (error) { + // Silently handle any logging errors + } + + await new Promise((resolve, reject) => { + nodeProcess.on("exit", (code) => { + if (code === 0) resolve(); + else reject(new Error(`Node exited with code ${code}`)); + }); + }); + + if (platform === "win32") { + console.log(showInfoMessage("Cleaning up firewall rules...")); + await runCommand( + 'netsh advfirewall firewall delete rule name="Allow Codex (TCP-In)"', + ); + await runCommand( + 'netsh advfirewall firewall delete rule name="Allow Codex (UDP-In)"', + ); + } + } catch (error) { + console.log(showErrorMessage(`Failed to run Codex: ${error.message}`)); } + await showNavigationMenu(); + } } async function showNodeDetails(data, showNavigationMenu) { - const { choice } = await inquirer.prompt([ - { - type: 'list', - name: 'choice', - message: 'Select information to view:', - choices: [ - '1. View Connected Peers', - '2. View Node Information', - '3. Update Wallet Address', - '4. Back to Main Menu', - '5. Exit' - ], - pageSize: 5, - loop: true + const { choice } = await inquirer.prompt([ + { + type: "list", + name: "choice", + message: "Select information to view:", + choices: [ + "1. View Connected Peers", + "2. View Node Information", + "3. Update Wallet Address", + "4. Back to Main Menu", + "5. Exit", + ], + pageSize: 5, + loop: true, + }, + ]); + + switch (choice.split(".")[0].trim()) { + case "1": + const peerCount = data.table.nodes.length; + if (peerCount > 0) { + console.log(showInfoMessage("Connected Peers")); + data.table.nodes.forEach((node, index) => { + console.log( + boxen( + `Peer ${index + 1}:\n` + + `${chalk.cyan("Peer ID:")} ${node.peerId}\n` + + `${chalk.cyan("Address:")} ${node.address}\n` + + `${chalk.cyan("Status:")} ${node.seen ? chalk.green("Active") : chalk.gray("Inactive")}`, + { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "blue", + }, + ), + ); + }); + } else { + console.log(showInfoMessage("No connected peers found.")); + } + return showNodeDetails(data, showNavigationMenu); + case "2": + console.log( + boxen( + `${chalk.cyan("Version:")} ${data.codex.version}\n` + + `${chalk.cyan("Revision:")} ${data.codex.revision}\n\n` + + `${chalk.cyan("Node ID:")} ${data.table.localNode.nodeId}\n` + + `${chalk.cyan("Peer ID:")} ${data.table.localNode.peerId}\n` + + `${chalk.cyan("Listening Address:")} ${data.table.localNode.address}\n\n` + + `${chalk.cyan("Public IP:")} ${data.announceAddresses[0].split("/")[2]}\n` + + `${chalk.cyan("Port:")} ${data.announceAddresses[0].split("/")[4]}\n` + + `${chalk.cyan("Connected Peers:")} ${data.table.nodes.length}`, + { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "yellow", + title: "📊 Node Information", + titleAlignment: "center", + }, + ), + ); + return showNodeDetails(data, showNavigationMenu); + case "3": + try { + const existingWallet = await getWalletAddress(); + + console.log( + boxen( + `${chalk.cyan("Current wallet address:")}\n${existingWallet || "Not set"}`, + { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "blue", + }, + ), + ); + + const wallet = await promptForWalletAddress(); + if (wallet) { + await setWalletAddress(wallet); + console.log( + showSuccessMessage("Wallet address updated successfully!"), + ); } - ]); - - switch (choice.split('.')[0].trim()) { - case '1': - const peerCount = data.table.nodes.length; - if (peerCount > 0) { - console.log(showInfoMessage('Connected Peers')); - data.table.nodes.forEach((node, index) => { - console.log(boxen( - `Peer ${index + 1}:\n` + - `${chalk.cyan('Peer ID:')} ${node.peerId}\n` + - `${chalk.cyan('Address:')} ${node.address}\n` + - `${chalk.cyan('Status:')} ${node.seen ? chalk.green('Active') : chalk.gray('Inactive')}`, - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'blue' - } - )); - }); - } else { - console.log(showInfoMessage('No connected peers found.')); - } - return showNodeDetails(data, showNavigationMenu); - case '2': - console.log(boxen( - `${chalk.cyan('Version:')} ${data.codex.version}\n` + - `${chalk.cyan('Revision:')} ${data.codex.revision}\n\n` + - `${chalk.cyan('Node ID:')} ${data.table.localNode.nodeId}\n` + - `${chalk.cyan('Peer ID:')} ${data.table.localNode.peerId}\n` + - `${chalk.cyan('Listening Address:')} ${data.table.localNode.address}\n\n` + - `${chalk.cyan('Public IP:')} ${data.announceAddresses[0].split('/')[2]}\n` + - `${chalk.cyan('Port:')} ${data.announceAddresses[0].split('/')[4]}\n` + - `${chalk.cyan('Connected Peers:')} ${data.table.nodes.length}`, - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'yellow', - title: '📊 Node Information', - titleAlignment: 'center' - } - )); - return showNodeDetails(data, showNavigationMenu); - case '3': - try { - const existingWallet = await getWalletAddress(); - - console.log(boxen( - `${chalk.cyan('Current wallet address:')}\n${existingWallet || 'Not set'}`, - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'blue' - } - )); - - const wallet = await promptForWalletAddress(); - if (wallet) { - await setWalletAddress(wallet); - console.log(showSuccessMessage('Wallet address updated successfully!')); - } - } catch (error) { - console.log(showErrorMessage(`Failed to update wallet address: ${error.message}`)); - } - return showNodeDetails(data, showNavigationMenu); - case '4': - return showNavigationMenu(); - case '5': - process.exit(0); - } + } catch (error) { + console.log( + showErrorMessage(`Failed to update wallet address: ${error.message}`), + ); + } + return showNodeDetails(data, showNavigationMenu); + case "4": + return showNavigationMenu(); + case "5": + process.exit(0); + } } export async function checkNodeStatus(config, showNavigationMenu) { - try { - const nodeRunning = await isNodeRunning(config); + try { + const nodeRunning = await isNodeRunning(config); - if (nodeRunning) { - const spinner = createSpinner('Checking node status...').start(); - const response = await runCommand(`curl http://localhost:${config.ports.apiPort}/api/codex/v1/debug/info`); - spinner.success(); - - const data = JSON.parse(response); - - const peerCount = data.table.nodes.length; - const isOnline = peerCount > 2; - - console.log(boxen( - isOnline - ? chalk.green('Node is ONLINE & DISCOVERABLE') - : chalk.yellow('Node is ONLINE but has few peers'), - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: isOnline ? 'green' : 'yellow', - title: '🔌 Node Status', - titleAlignment: 'center' - } - )); + if (nodeRunning) { + const spinner = createSpinner("Checking node status...").start(); + const response = await runCommand( + `curl http://localhost:${config.ports.apiPort}/api/codex/v1/debug/info`, + ); + spinner.success(); - await showNodeDetails(data, showNavigationMenu); - } else { - console.log(showErrorMessage('Codex node is not running. Try again after starting the node')); - await showNavigationMenu(); - } - } catch (error) { - console.log(showErrorMessage(`Failed to check node status: ${error.message}`)); - await showNavigationMenu(); + const data = JSON.parse(response); + + const peerCount = data.table.nodes.length; + const isOnline = peerCount > 2; + + console.log( + boxen( + isOnline + ? chalk.green("Node is ONLINE & DISCOVERABLE") + : chalk.yellow("Node is ONLINE but has few peers"), + { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: isOnline ? "green" : "yellow", + title: "🔌 Node Status", + titleAlignment: "center", + }, + ), + ); + + await showNodeDetails(data, showNavigationMenu); + } else { + console.log( + showErrorMessage( + "Codex node is not running. Try again after starting the node", + ), + ); + await showNavigationMenu(); } -} \ No newline at end of file + } catch (error) { + console.log( + showErrorMessage(`Failed to check node status: ${error.message}`), + ); + await showNavigationMenu(); + } +} diff --git a/src/main.js b/src/main.js index ffe0ca0..23194bf 100644 --- a/src/main.js +++ b/src/main.js @@ -1,155 +1,200 @@ #!/usr/bin/env node -import inquirer from 'inquirer'; -import chalk from 'chalk'; -import boxen from 'boxen'; -import { ASCII_ART } from './constants/ascii.js'; -import { handleCommandLineOperation, parseCommandLineArgs } from './cli/commandParser.js'; -import { uploadFile, downloadFile, showLocalFiles } from './handlers/fileHandlers.js'; -import { installCodex, uninstallCodex } from './handlers/installationHandlers.js'; -import { runCodex, checkNodeStatus } from './handlers/nodeHandlers.js'; -import { showInfoMessage } from './utils/messages.js'; -import { loadConfig } from './services/config.js'; -import { showConfigMenu } from './configmenu.js'; -import { openCodexApp } from './services/codexapp.js'; +import inquirer from "inquirer"; +import chalk from "chalk"; +import boxen from "boxen"; +import { ASCII_ART } from "./constants/ascii.js"; +import { + handleCommandLineOperation, + parseCommandLineArgs, +} from "./cli/commandParser.js"; +import { + uploadFile, + downloadFile, + showLocalFiles, +} from "./handlers/fileHandlers.js"; +import { + installCodex, + uninstallCodex, +} from "./handlers/installationHandlers.js"; +import { runCodex, checkNodeStatus } from "./handlers/nodeHandlers.js"; +import { showInfoMessage } from "./utils/messages.js"; +import { loadConfig } from "./services/config.js"; +import { showConfigMenu } from "./configmenu.js"; +import { openCodexApp } from "./services/codexapp.js"; + +import { MainMenu } from "./ui/mainmenu.js"; +import { UiService } from "./services/uiservice.js"; async function showNavigationMenu() { - console.log('\n') - const { choice } = await inquirer.prompt([ - { - type: 'list', - name: 'choice', - message: 'What would you like to do?', - choices: [ - '1. Back to main menu', - '2. Exit' - ], - pageSize: 2, - loop: true - } - ]); + console.log("\n"); + const { choice } = await inquirer.prompt([ + { + type: "list", + name: "choice", + message: "What would you like to do?", + choices: ["1. Back to main menu", "2. Exit"], + pageSize: 2, + loop: true, + }, + ]); - switch (choice.split('.')[0]) { - case '1': - return main(); - case '2': - handleExit(); - } + switch (choice.split(".")[0]) { + case "1": + return main(); + case "2": + handleExit(); + } } function handleExit() { - console.log(boxen( - chalk.cyanBright('👋 Thank you for using Codex Storage CLI! Goodbye!'), - { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'cyan', - title: '👋 GOODBYE', - titleAlignment: 'center' - } - )); - process.exit(0); + console.log( + boxen( + chalk.cyanBright("👋 Thank you for using Codex Storage CLI! Goodbye!"), + { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "cyan", + title: "👋 GOODBYE", + titleAlignment: "center", + }, + ), + ); + process.exit(0); } export async function main() { - const commandArgs = parseCommandLineArgs(); - if (commandArgs) { - switch (commandArgs.command) { - case 'upload': - await uploadFile(commandArgs.value, handleCommandLineOperation, showNavigationMenu); - return; - case 'download': - await downloadFile(commandArgs.value, handleCommandLineOperation, showNavigationMenu); - return; - } + const commandArgs = parseCommandLineArgs(); + if (commandArgs) { + switch (commandArgs.command) { + case "upload": + await uploadFile( + commandArgs.value, + handleCommandLineOperation, + showNavigationMenu, + ); + return; + case "download": + await downloadFile( + commandArgs.value, + handleCommandLineOperation, + showNavigationMenu, + ); + return; } + } - process.on('SIGINT', handleExit); - process.on('SIGTERM', handleExit); - process.on('SIGQUIT', handleExit); - - try { - const config = loadConfig(); - while (true) { - console.log('\n' + chalk.cyanBright(ASCII_ART)); - const { choice } = await inquirer.prompt([ - { - type: 'list', - name: 'choice', - message: 'Select an option:', - choices: [ - '1. Download and install Codex', - '2. Run Codex node', - '3. Check node status', - '4. Edit Codex configuration', - '5. Open Codex App', - '6. Upload a file', - '7. Download a file', - '8. Show local data', - '9. Uninstall Codex node', - '10. Submit feedback', - '11. Exit' - ], - pageSize: 11, - loop: true - } - ]).catch(() => { - handleExit(); - return; - }); - - switch (choice.split('.')[0]) { - case '1': - const installed = await installCodex(config, showNavigationMenu); - if (installed) { - await showConfigMenu(config); - } - break; - case '2': - await runCodex(config, showNavigationMenu); - return; - case '3': - await checkNodeStatus(config, showNavigationMenu); - break; - case '4': - await showConfigMenu(config); - break; - case '5': - openCodexApp(config); - break; - case '6': - await uploadFile(config, null, handleCommandLineOperation, showNavigationMenu); - break; - case '7': - await downloadFile(config, null, handleCommandLineOperation, showNavigationMenu); - break; - case '8': - await showLocalFiles(config, showNavigationMenu); - break; - case '9': - await uninstallCodex(config, showNavigationMenu); - break; - case '10': - const { exec } = await import('child_process'); - const url = 'https://tally.so/r/w2DlXb'; - const command = process.platform === 'win32' ? `start ${url}` : process.platform === 'darwin' ? `open ${url}` : `xdg-open ${url}`; - exec(command); - console.log(showInfoMessage('Opening feedback form in your browser...')); - break; - case '11': - handleExit(); - return; - } + process.on("SIGINT", handleExit); + process.on("SIGTERM", handleExit); + process.on("SIGQUIT", handleExit); - console.log('\n'); - } - } catch (error) { - if (error.message.includes('ExitPromptError')) { - handleExit(); - } else { - console.error(chalk.red('An error occurred:', error.message)); - handleExit(); - } + const uiService = new UiService(); + const mainMenu = new MainMenu(uiService); + + mainMenu.show(); + return; + + try { + const config = loadConfig(); + while (true) { + console.log("\n" + chalk.cyanBright(ASCII_ART)); + const { choice } = await inquirer + .prompt([ + { + type: "list", + name: "choice", + message: "Select an option:", + choices: [ + "1. Download and install Codex", + "2. Run Codex node", + "3. Check node status", + "4. Edit Codex configuration", + "5. Open Codex App", + "6. Upload a file", + "7. Download a file", + "8. Show local data", + "9. Uninstall Codex node", + "10. Submit feedback", + "11. Exit", + ], + pageSize: 11, + loop: true, + }, + ]) + .catch(() => { + handleExit(); + return; + }); + + switch (choice.split(".")[0]) { + case "1": + const installed = await installCodex(config, showNavigationMenu); + if (installed) { + await showConfigMenu(config); + } + break; + case "2": + await runCodex(config, showNavigationMenu); + return; + case "3": + await checkNodeStatus(config, showNavigationMenu); + break; + case "4": + await showConfigMenu(config); + break; + case "5": + openCodexApp(config); + break; + case "6": + await uploadFile( + config, + null, + handleCommandLineOperation, + showNavigationMenu, + ); + break; + case "7": + await downloadFile( + config, + null, + handleCommandLineOperation, + showNavigationMenu, + ); + break; + case "8": + await showLocalFiles(config, showNavigationMenu); + break; + case "9": + await uninstallCodex(config, showNavigationMenu); + break; + case "10": + const { exec } = await import("child_process"); + const url = "https://tally.so/r/w2DlXb"; + const command = + process.platform === "win32" + ? `start ${url}` + : process.platform === "darwin" + ? `open ${url}` + : `xdg-open ${url}`; + exec(command); + console.log( + showInfoMessage("Opening feedback form in your browser..."), + ); + break; + case "11": + handleExit(); + return; + } + + console.log("\n"); } -} \ No newline at end of file + } catch (error) { + if (error.message.includes("ExitPromptError")) { + handleExit(); + } else { + console.error(chalk.red("An error occurred:", error.message)); + handleExit(); + } + } +} diff --git a/src/services/codexapp.js b/src/services/codexapp.js index b76db87..838c1e8 100644 --- a/src/services/codexapp.js +++ b/src/services/codexapp.js @@ -1,4 +1,4 @@ -import open from 'open'; +import open from "open"; export function openCodexApp(config) { // TODO: Update this to the main URL when the PR for adding api-port query parameter support @@ -6,12 +6,12 @@ export function openCodexApp(config) { // See: https://github.com/codex-storage/codex-marketplace-ui/issues/92 const segments = [ - 'https://releases-v0-0-14.codex-marketplace-ui.pages.dev/', - '?', - `api-port=${config.ports.apiPort}` - ] + "https://releases-v0-0-14.codex-marketplace-ui.pages.dev/", + "?", + `api-port=${config.ports.apiPort}`, + ]; const url = segments.join(""); - + open(url); } diff --git a/src/services/config.js b/src/services/config.js index 07fddf3..9211cc5 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -1,7 +1,10 @@ -import fs from 'fs'; -import path from 'path'; -import { getAppDataDir } from '../utils/appdata.js'; -import { getCodexDataDirDefaultPath, getCodexLogsDefaultPath } from '../utils/appdata.js'; +import fs from "fs"; +import path from "path"; +import { getAppDataDir } from "../utils/appdata.js"; +import { + getCodexDataDirDefaultPath, + getCodexLogsDefaultPath, +} from "../utils/appdata.js"; const defaultConfig = { codexExe: "", @@ -12,8 +15,8 @@ const defaultConfig = { ports: { discPort: 8090, listenPort: 8070, - apiPort: 8080 - } + apiPort: 8080, + }, }; function getConfigFilename() { @@ -25,7 +28,9 @@ export function saveConfig(config) { try { fs.writeFileSync(filePath, JSON.stringify(config)); } catch (error) { - console.error(`Failed to save config file to '${filePath}' error: '${error}'.`); + console.error( + `Failed to save config file to '${filePath}' error: '${error}'.`, + ); throw error; } } @@ -39,7 +44,9 @@ export function loadConfig() { } return JSON.parse(fs.readFileSync(filePath)); } catch (error) { - console.error(`Failed to load config file from '${filePath}' error: '${error}'.`); + console.error( + `Failed to load config file from '${filePath}' error: '${error}'.`, + ); throw error; } } diff --git a/src/services/nodeService.js b/src/services/nodeService.js index daa65db..0d800b3 100644 --- a/src/services/nodeService.js +++ b/src/services/nodeService.js @@ -1,8 +1,12 @@ -import axios from 'axios'; -import { runCommand } from '../utils/command.js'; -import { showErrorMessage, showInfoMessage, showSuccessMessage } from '../utils/messages.js'; -import os from 'os'; -import { getCodexVersion } from '../handlers/installationHandlers.js'; +import axios from "axios"; +import { runCommand } from "../utils/command.js"; +import { + showErrorMessage, + showInfoMessage, + showSuccessMessage, +} from "../utils/messages.js"; +import os from "os"; +import { getCodexVersion } from "../handlers/installationHandlers.js"; const platform = os.platform(); @@ -10,145 +14,175 @@ const platform = os.platform(); let currentWallet = null; export async function setWalletAddress(wallet) { - // Basic ERC20 address validation - if (wallet && !/^0x[a-fA-F0-9]{40}$/.test(wallet)) { - throw new Error('Invalid ERC20 wallet address format'); - } - currentWallet = wallet; + // Basic ERC20 address validation + if (wallet && !/^0x[a-fA-F0-9]{40}$/.test(wallet)) { + throw new Error("Invalid ERC20 wallet address format"); + } + currentWallet = wallet; } export async function getWalletAddress() { - return currentWallet; + return currentWallet; } export async function isNodeRunning(config) { - try { - const response = await axios.get(`http://localhost:${config.ports.apiPort}/api/codex/v1/debug/info`); - return response.status === 200; - } catch (error) { - return false; - } + try { + const response = await axios.get( + `http://localhost:${config.ports.apiPort}/api/codex/v1/debug/info`, + ); + return response.status === 200; + } catch (error) { + return false; + } } export async function isCodexInstalled(config) { - try { - const version = await getCodexVersion(config); - return version.length > 0; - } catch (error) { - return false; - } + try { + const version = await getCodexVersion(config); + return version.length > 0; + } catch (error) { + return false; + } } -export async function logToSupabase(nodeData, retryCount = 3, retryDelay = 1000) { - const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); +export async function logToSupabase( + nodeData, + retryCount = 3, + retryDelay = 1000, +) { + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - for (let attempt = 1; attempt <= retryCount; attempt++) { - try { - const peerCount = nodeData.table.nodes ? nodeData.table.nodes.length : "0"; - const payload = { - nodeId: nodeData.table.localNode.nodeId, - peerId: nodeData.table.localNode.peerId, - publicIp: nodeData.announceAddresses[0].split('/')[2], - version: nodeData.codex.version, - peerCount: peerCount == 0 ? "0" : peerCount, - port: nodeData.announceAddresses[0].split('/')[4], - listeningAddress: nodeData.table.localNode.address, - timestamp: new Date().toISOString(), - wallet: currentWallet - }; + for (let attempt = 1; attempt <= retryCount; attempt++) { + try { + const peerCount = nodeData.table.nodes + ? nodeData.table.nodes.length + : "0"; + const payload = { + nodeId: nodeData.table.localNode.nodeId, + peerId: nodeData.table.localNode.peerId, + publicIp: nodeData.announceAddresses[0].split("/")[2], + version: nodeData.codex.version, + peerCount: peerCount == 0 ? "0" : peerCount, + port: nodeData.announceAddresses[0].split("/")[4], + listeningAddress: nodeData.table.localNode.address, + timestamp: new Date().toISOString(), + wallet: currentWallet, + }; - const response = await axios.post('https://vfcnsjxahocmzefhckfz.supabase.co/functions/v1/codexnodes', payload, { - headers: { - 'Content-Type': 'application/json' - }, - timeout: 5000 - }); - - return response.status === 200; - } catch (error) { - const isLastAttempt = attempt === retryCount; - const isNetworkError = error.code === 'ENOTFOUND' || error.code === 'ETIMEDOUT' || error.code === 'ECONNREFUSED'; + const response = await axios.post( + "https://vfcnsjxahocmzefhckfz.supabase.co/functions/v1/codexnodes", + payload, + { + headers: { + "Content-Type": "application/json", + }, + timeout: 5000, + }, + ); - if (isLastAttempt || !isNetworkError) { - console.error(`Failed to log node data (attempt ${attempt}/${retryCount}):`, error.message); - if (error.response) { - console.error('Error response:', { - status: error.response.status, - data: error.response.data - }); - } - if (isLastAttempt) return false; - } else { - // Only log retry attempts for network errors - console.log(`Retrying to log data (attempt ${attempt}/${retryCount})...`); - await delay(retryDelay); - } + return response.status === 200; + } catch (error) { + const isLastAttempt = attempt === retryCount; + const isNetworkError = + error.code === "ENOTFOUND" || + error.code === "ETIMEDOUT" || + error.code === "ECONNREFUSED"; + + if (isLastAttempt || !isNetworkError) { + console.error( + `Failed to log node data (attempt ${attempt}/${retryCount}):`, + error.message, + ); + if (error.response) { + console.error("Error response:", { + status: error.response.status, + data: error.response.data, + }); } + if (isLastAttempt) return false; + } else { + // Only log retry attempts for network errors + console.log( + `Retrying to log data (attempt ${attempt}/${retryCount})...`, + ); + await delay(retryDelay); + } } - return false; + } + return false; } export async function checkDependencies() { - if (platform === 'linux') { - try { - await runCommand('ldconfig -p | grep libgomp'); - return true; - } catch (error) { - console.log(showErrorMessage('Required dependency libgomp1 is not installed.')); - console.log(showInfoMessage( - 'For Debian-based Linux systems, please install it manually using:\n\n' + - 'sudo apt update && sudo apt install libgomp1' - )); - return false; - } + if (platform === "linux") { + try { + await runCommand("ldconfig -p | grep libgomp"); + return true; + } catch (error) { + console.log( + showErrorMessage("Required dependency libgomp1 is not installed."), + ); + console.log( + showInfoMessage( + "For Debian-based Linux systems, please install it manually using:\n\n" + + "sudo apt update && sudo apt install libgomp1", + ), + ); + return false; } - return true; + } + return true; } export async function startPeriodicLogging(config) { - const FIFTEEN_MINUTES = 15 * 60 * 1000; // 15 minutes in milliseconds - - const logNodeInfo = async () => { - try { - const response = await axios.get(`http://localhost:${config.ports.apiPort}/api/codex/v1/debug/info`); - if (response.status === 200) { - await logToSupabase(response.data); - } - } catch (error) { - // Silently handle any logging errors to not disrupt the node operation - console.error('Failed to log node data:', error.message); - } - }; + const FIFTEEN_MINUTES = 15 * 60 * 1000; // 15 minutes in milliseconds - // Initial log - await logNodeInfo(); + const logNodeInfo = async () => { + try { + const response = await axios.get( + `http://localhost:${config.ports.apiPort}/api/codex/v1/debug/info`, + ); + if (response.status === 200) { + await logToSupabase(response.data); + } + } catch (error) { + // Silently handle any logging errors to not disrupt the node operation + console.error("Failed to log node data:", error.message); + } + }; - // Set up periodic logging - const intervalId = setInterval(logNodeInfo, FIFTEEN_MINUTES); + // Initial log + await logNodeInfo(); - // Return cleanup function - return () => clearInterval(intervalId); + // Set up periodic logging + const intervalId = setInterval(logNodeInfo, FIFTEEN_MINUTES); + + // Return cleanup function + return () => clearInterval(intervalId); } export async function updateWalletAddress(nodeId, wallet) { - // Basic ERC20 address validation - if (!/^0x[a-fA-F0-9]{40}$/.test(wallet)) { - throw new Error('Invalid ERC20 wallet address format'); - } + // Basic ERC20 address validation + if (!/^0x[a-fA-F0-9]{40}$/.test(wallet)) { + throw new Error("Invalid ERC20 wallet address format"); + } - try { - const response = await axios.post('https://vfcnsjxahocmzefhckfz.supabase.co/functions/v1/wallet', { - nodeId, - wallet - }, { - headers: { - 'Content-Type': 'application/json' - }, - timeout: 5000 - }); - return response.status === 200; - } catch (error) { - console.error('Failed to update wallet address:', error.message); - throw error; - } -} \ No newline at end of file + try { + const response = await axios.post( + "https://vfcnsjxahocmzefhckfz.supabase.co/functions/v1/wallet", + { + nodeId, + wallet, + }, + { + headers: { + "Content-Type": "application/json", + }, + timeout: 5000, + }, + ); + return response.status === 200; + } catch (error) { + console.error("Failed to update wallet address:", error.message); + throw error; + } +} diff --git a/src/services/uiservice.js b/src/services/uiservice.js new file mode 100644 index 0000000..ee13a78 --- /dev/null +++ b/src/services/uiservice.js @@ -0,0 +1,47 @@ +import boxen from "boxen"; +import chalk from "chalk"; + +function show(msg) { + console.log(msg); +} + +export class UiService { + showSuccessMessage = (message) => { + show( + boxen(chalk.green(message), { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "green", + title: "✅ SUCCESS", + titleAlignment: "center", + }), + ); + }; + + showErrorMessage = (message) => { + show( + boxen(chalk.red(message), { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "red", + title: "❌ ERROR", + titleAlignment: "center", + }), + ); + }; + + showInfoMessage(message) { + show( + boxen(chalk.cyan(message), { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "cyan", + title: "ℹ️ INFO", + titleAlignment: "center", + }), + ); + } +} diff --git a/src/ui/mainmenu.js b/src/ui/mainmenu.js new file mode 100644 index 0000000..99967bf --- /dev/null +++ b/src/ui/mainmenu.js @@ -0,0 +1,9 @@ +export class MainMenu { + constructor(uiService) { + this.ui = uiService; + } + + show = () => { + this.ui.showInfoMessage("hello"); + }; +} diff --git a/src/utils/appdata.js b/src/utils/appdata.js index ad67024..77dc950 100644 --- a/src/utils/appdata.js +++ b/src/utils/appdata.js @@ -1,5 +1,5 @@ -import path from 'path'; -import fs from 'fs'; +import path from "path"; +import fs from "fs"; export function getAppDataDir() { return ensureExists(appData("codex-cli")); @@ -32,10 +32,15 @@ function ensureExists(dir) { function appData(...app) { let appData; - if (process.platform === 'win32') { + if (process.platform === "win32") { appData = path.join(process.env.APPDATA, ...app); - } else if (process.platform === 'darwin') { - appData = path.join(process.env.HOME, 'Library', 'Application Support', ...app); + } else if (process.platform === "darwin") { + appData = path.join( + process.env.HOME, + "Library", + "Application Support", + ...app, + ); } else { appData = path.join(process.env.HOME, ...prependDot(...app)); } @@ -51,4 +56,3 @@ function prependDot(...app) { } }); } - diff --git a/src/utils/command.js b/src/utils/command.js index 1b24af2..9ef5c91 100644 --- a/src/utils/command.js +++ b/src/utils/command.js @@ -1,14 +1,14 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; +import { exec } from "child_process"; +import { promisify } from "util"; export const execAsync = promisify(exec); export async function runCommand(command) { - try { - const { stdout, stderr } = await execAsync(command); - return stdout; - } catch (error) { - console.error('Error:', error.message); - throw error; - } -} \ No newline at end of file + try { + const { stdout, stderr } = await execAsync(command); + return stdout; + } catch (error) { + console.error("Error:", error.message); + throw error; + } +} diff --git a/src/utils/messages.js b/src/utils/messages.js index f581651..0834b9f 100644 --- a/src/utils/messages.js +++ b/src/utils/messages.js @@ -1,35 +1,35 @@ -import boxen from 'boxen'; -import chalk from 'chalk'; +import boxen from "boxen"; +import chalk from "chalk"; export function showSuccessMessage(message) { - return boxen(chalk.green(message), { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'green', - title: '✅ SUCCESS', - titleAlignment: 'center' - }); + return boxen(chalk.green(message), { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "green", + title: "✅ SUCCESS", + titleAlignment: "center", + }); } export function showErrorMessage(message) { - return boxen(chalk.red(message), { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'red', - title: '❌ ERROR', - titleAlignment: 'center' - }); + return boxen(chalk.red(message), { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "red", + title: "❌ ERROR", + titleAlignment: "center", + }); } export function showInfoMessage(message) { - return boxen(chalk.cyan(message), { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'cyan', - title: 'ℹ️ INFO', - titleAlignment: 'center' - }); -} \ No newline at end of file + return boxen(chalk.cyan(message), { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "cyan", + title: "ℹ️ INFO", + titleAlignment: "center", + }); +} diff --git a/src/utils/numberSelector.js b/src/utils/numberSelector.js index 0cb74b5..8342f6c 100644 --- a/src/utils/numberSelector.js +++ b/src/utils/numberSelector.js @@ -1,4 +1,4 @@ -import inquirer from 'inquirer'; +import inquirer from "inquirer"; function getMetricsMult(valueStr, allowMetricPostfixes) { if (!allowMetricPostfixes) return 1; @@ -27,14 +27,19 @@ function getNumericValue(valueStr) { async function promptForValueStr(promptMessage) { const response = await inquirer.prompt([ { - type: 'input', - name: 'valueStr', - message: promptMessage - }]); + type: "input", + name: "valueStr", + message: promptMessage, + }, + ]); return response.valueStr; } -export async function showNumberSelector(currentValue, promptMessage, allowMetricPostfixes) { +export async function showNumberSelector( + currentValue, + promptMessage, + allowMetricPostfixes, +) { try { var valueStr = await promptForValueStr(promptMessage); valueStr = valueStr.replaceAll(" ", ""); diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index 24d9639..c8a5aaa 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -1,29 +1,31 @@ -import path from 'path'; -import inquirer from 'inquirer'; -import boxen from 'boxen'; -import chalk from 'chalk'; -import fs from 'fs'; -import { filesystemSync } from 'fs-filesystem'; +import path from "path"; +import inquirer from "inquirer"; +import boxen from "boxen"; +import chalk from "chalk"; +import fs from "fs"; +import { filesystemSync } from "fs-filesystem"; function showMsg(msg) { - console.log(boxen(chalk.white(msg), { - padding: 1, - margin: 1, - borderStyle: 'round', - borderColor: 'white', - titleAlignment: 'center' - })); + console.log( + boxen(chalk.white(msg), { + padding: 1, + margin: 1, + borderStyle: "round", + borderColor: "white", + titleAlignment: "center", + }), + ); } function getAvailableRoots() { const devices = filesystemSync(); var mountPoints = []; - Object.keys(devices).forEach(function(key) { - var val = devices[key]; - val.volumes.forEach(function(volume) { - mountPoints.push(volume.mountPoint); - }); + Object.keys(devices).forEach(function (key) { + var val = devices[key]; + val.volumes.forEach(function (volume) { + mountPoints.push(volume.mountPoint); }); + }); if (mountPoints.length < 1) { throw new Error("Failed to detect file system devices."); @@ -37,11 +39,11 @@ function splitPath(str) { function dropEmptyParts(parts) { var result = []; - parts.forEach(function(part) { + parts.forEach(function (part) { if (part.length > 0) { result.push(part); } - }) + }); return result; } @@ -63,10 +65,10 @@ function showCurrent(currentPath) { if (len < 2) { showMsg( - 'Warning - Known issue:\n' + - 'Path selection does not work in root paths on some platforms.\n' + - 'Use "Enter path" or "Create new folder" to navigate and create folders\n' + - 'if this is the case for you.' + "Warning - Known issue:\n" + + "Path selection does not work in root paths on some platforms.\n" + + 'Use "Enter path" or "Create new folder" to navigate and create folders\n' + + "if this is the case for you.", ); } } @@ -74,7 +76,7 @@ function showCurrent(currentPath) { function hasValidRoot(roots, checkPath) { if (checkPath.length < 1) return false; var result = false; - roots.forEach(function(root) { + roots.forEach(function (root) { if (root.toLowerCase() == checkPath[0].toLowerCase()) { console.log("valid root: " + combine(checkPath)); result = true; @@ -86,26 +88,28 @@ function hasValidRoot(roots, checkPath) { async function showMain(currentPath) { showCurrent(currentPath); - const { choice } = await inquirer.prompt([ - { - type: 'list', - name: 'choice', - message: 'Select an option:', + const { choice } = await inquirer + .prompt([ + { + type: "list", + name: "choice", + message: "Select an option:", choices: [ - '1. Enter path', - '2. Go up one', - '3. Go down one', - '4. Create new folder here', - '5. Select this path', - '6. Cancel' + "1. Enter path", + "2. Go up one", + "3. Go down one", + "4. Create new folder here", + "5. Select this path", + "6. Cancel", ], pageSize: 6, - loop: true - } - ]).catch(() => { - handleExit(); - return { choice: '6' }; - }); + loop: true, + }, + ]) + .catch(() => { + handleExit(); + return { choice: "6" }; + }); return choice; } @@ -121,28 +125,28 @@ export async function showPathSelector(startingPath, pathMustExist) { const choice = await showMain(currentPath); var newCurrentPath = currentPath; - switch (choice.split('.')[0]) { - case '1': - newCurrentPath = await enterPath(currentPath, pathMustExist); - break; - case '2': - newCurrentPath = upOne(currentPath); - break; - case '3': - newCurrentPath = await downOne(currentPath); - break; - case '4': - newCurrentPath = await createSubDir(currentPath, pathMustExist); - break; - case '5': + switch (choice.split(".")[0]) { + case "1": + newCurrentPath = await enterPath(currentPath, pathMustExist); + break; + case "2": + newCurrentPath = upOne(currentPath); + break; + case "3": + newCurrentPath = await downOne(currentPath); + break; + case "4": + newCurrentPath = await createSubDir(currentPath, pathMustExist); + break; + case "5": if (pathMustExist && !isDir(combine(currentPath))) { console.log("Current path does not exist."); break; } else { return combine(currentPath); } - case '6': - return combine(currentPath); + case "6": + return combine(currentPath); } if (hasValidRoot(roots, newCurrentPath)) { @@ -156,10 +160,11 @@ export async function showPathSelector(startingPath, pathMustExist) { async function enterPath(currentPath, pathMustExist) { const response = await inquirer.prompt([ { - type: 'input', - name: 'path', - message: 'Enter Path:' - }]); + type: "input", + name: "path", + message: "Enter Path:", + }, + ]); const newPath = response.path; if (pathMustExist && !isDir(newPath)) { @@ -191,9 +196,9 @@ function getSubDirOptions(currentPath) { const entries = fs.readdirSync(fullPath); var result = []; var counter = 1; - entries.forEach(function(entry) { + entries.forEach(function (entry) { if (isSubDir(currentPath, entry)) { - result.push(counter + '. ' + entry); + result.push(counter + ". " + entry); counter = counter + 1; } }); @@ -207,30 +212,33 @@ async function downOne(currentPath) { return currentPath; } - const { choice } = await inquirer.prompt([ - { - type: 'list', - name: 'choice', - message: 'Select an subdir:', + const { choice } = await inquirer + .prompt([ + { + type: "list", + name: "choice", + message: "Select an subdir:", choices: options, pageSize: options.length, - loop: true - } - ]).catch(() => { - return currentPath; - }); + loop: true, + }, + ]) + .catch(() => { + return currentPath; + }); - const subDir = choice.split('. ')[1]; + const subDir = choice.split(". ")[1]; return [...currentPath, subDir]; } async function createSubDir(currentPath, pathMustExist) { const response = await inquirer.prompt([ { - type: 'input', - name: 'name', - message: 'Enter name:' - }]); + type: "input", + name: "name", + message: "Enter name:", + }, + ]); const name = response.name; if (name.length < 1) return; From b1e49eef871c026ee0d9e95ec7a2b3d331454db5 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Feb 2025 14:37:28 +0100 Subject: [PATCH 02/59] Abstracts out the multiple choice prompt --- src/main.js | 2 +- src/services/uiservice.js | 36 ++++++++++++++++++++++++++++++++++-- src/ui/mainmenu.js | 17 ++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/main.js b/src/main.js index 23194bf..8b25b2c 100644 --- a/src/main.js +++ b/src/main.js @@ -92,7 +92,7 @@ export async function main() { const uiService = new UiService(); const mainMenu = new MainMenu(uiService); - mainMenu.show(); + await mainMenu.show(); return; try { diff --git a/src/services/uiservice.js b/src/services/uiservice.js index ee13a78..7711403 100644 --- a/src/services/uiservice.js +++ b/src/services/uiservice.js @@ -1,5 +1,8 @@ import boxen from "boxen"; import chalk from "chalk"; +import inquirer from "inquirer"; + +import { ASCII_ART } from "../constants/ascii.js"; function show(msg) { console.log(msg); @@ -32,7 +35,7 @@ export class UiService { ); }; - showInfoMessage(message) { + showInfoMessage = (message) => { show( boxen(chalk.cyan(message), { padding: 1, @@ -43,5 +46,34 @@ export class UiService { titleAlignment: "center", }), ); - } + }; + + showLogo = () => { + console.log("\n" + chalk.cyanBright(ASCII_ART)); + }; + + askMultipleChoice = async (message, choices) => { + var counter = 1; + var promptChoices = []; + choices.forEach(function(choice) { + promptChoices.push(`${counter}. ${choice.label}`); + counter++; + }); + + const { choice } = await inquirer.prompt([ + { + type: "list", + name: "choice", + message: message, + choices: promptChoices, + pageSize: counter - 1, + loop: true + } + ]); + + const selectStr = choice.split(".")[0]; + const selectIndex = parseInt(selectStr) - 1; + + await choices[selectIndex].action(); + }; } diff --git a/src/ui/mainmenu.js b/src/ui/mainmenu.js index 99967bf..710f458 100644 --- a/src/ui/mainmenu.js +++ b/src/ui/mainmenu.js @@ -3,7 +3,22 @@ export class MainMenu { this.ui = uiService; } - show = () => { + show = async () => { + this.ui.showLogo(); this.ui.showInfoMessage("hello"); + + await this.ui.askMultipleChoice("Select an option",[ + { + label: "optionOne", + action: async function() { + console.log("A!") + } + },{ + label: "optionTwo", + action: async function() { + console.log("B!") + } + }, + ]) }; } From 2297d01966063f0d410245b8e2582fc2deccfd6f Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 25 Feb 2025 15:25:14 +0100 Subject: [PATCH 03/59] main loop --- src/ui/mainmenu.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/ui/mainmenu.js b/src/ui/mainmenu.js index 710f458..3fcf0fd 100644 --- a/src/ui/mainmenu.js +++ b/src/ui/mainmenu.js @@ -1,12 +1,21 @@ export class MainMenu { constructor(uiService) { this.ui = uiService; + this.running = true; } show = async () => { this.ui.showLogo(); this.ui.showInfoMessage("hello"); + while (this.running) { + await this.promptMainMenu(); + } + + this.ui.showInfoMessage("K-THX-BYE"); + }; + + promptMainMenu = async() => { await this.ui.askMultipleChoice("Select an option",[ { label: "optionOne", @@ -15,10 +24,13 @@ export class MainMenu { } },{ label: "optionTwo", - action: async function() { - console.log("B!") - } + action: this.closeMainMenu }, ]) }; + + closeMainMenu = async() => { + console.log("B!") + this.running = false; + }; } From 769fb06ad7ec86d79cb4ad14d1b127bab44f65c1 Mon Sep 17 00:00:00 2001 From: thatben Date: Fri, 7 Mar 2025 13:14:32 +0100 Subject: [PATCH 04/59] wip --- src/main.js | 6 +- src/services/config.js | 2 + src/services/fsservice.js | 41 +++ src/services/uiservice.js | 17 +- src/ui/installmenu.js | 38 +++ src/ui/mainmenu.js | 27 +- src/ui/mainmenu.test.js | 51 ++++ src/utils/numberSelector.js | 91 ++++--- src/utils/numberSelector.test.js | 66 +++++ src/utils/pathSelector.js | 426 ++++++++++++++----------------- 10 files changed, 470 insertions(+), 295 deletions(-) create mode 100644 src/services/fsservice.js create mode 100644 src/ui/installmenu.js create mode 100644 src/ui/mainmenu.test.js create mode 100644 src/utils/numberSelector.test.js diff --git a/src/main.js b/src/main.js index 8b25b2c..c00e9c3 100644 --- a/src/main.js +++ b/src/main.js @@ -24,6 +24,7 @@ import { showConfigMenu } from "./configmenu.js"; import { openCodexApp } from "./services/codexapp.js"; import { MainMenu } from "./ui/mainmenu.js"; +import { InstallMenu } from "./ui/installmenu.js"; import { UiService } from "./services/uiservice.js"; async function showNavigationMenu() { @@ -89,14 +90,15 @@ export async function main() { process.on("SIGTERM", handleExit); process.on("SIGQUIT", handleExit); + const config = loadConfig(); const uiService = new UiService(); - const mainMenu = new MainMenu(uiService); + const installMenu = new InstallMenu(uiService, config); + const mainMenu = new MainMenu(uiService, installMenu); await mainMenu.show(); return; try { - const config = loadConfig(); while (true) { console.log("\n" + chalk.cyanBright(ASCII_ART)); const { choice } = await inquirer diff --git a/src/services/config.js b/src/services/config.js index 9211cc5..7bbc2de 100644 --- a/src/services/config.js +++ b/src/services/config.js @@ -2,6 +2,7 @@ import fs from "fs"; import path from "path"; import { getAppDataDir } from "../utils/appdata.js"; import { + getCodexBinPath, getCodexDataDirDefaultPath, getCodexLogsDefaultPath, } from "../utils/appdata.js"; @@ -9,6 +10,7 @@ import { const defaultConfig = { codexExe: "", // User-selected config options: + codexPath: getCodexBinPath(), dataDir: getCodexDataDirDefaultPath(), logsDir: getCodexLogsDefaultPath(), storageQuota: 8 * 1024 * 1024 * 1024, diff --git a/src/services/fsservice.js b/src/services/fsservice.js new file mode 100644 index 0000000..45f22c5 --- /dev/null +++ b/src/services/fsservice.js @@ -0,0 +1,41 @@ +import path from "path"; +import fs from "fs"; +import { filesystemSync } from "fs-filesystem"; + +export class FsService { + getAvailableRoots = () => { + const devices = filesystemSync(); + var mountPoints = []; + Object.keys(devices).forEach(function (key) { + var val = devices[key]; + val.volumes.forEach(function (volume) { + mountPoints.push(volume.mountPoint); + }); + }); + + if (mountPoints.length < 1) { + throw new Error("Failed to detect file system devices."); + } + return mountPoints; + }; + + pathJoin = (parts) => { + return path.join(...parts); + }; + + isDir = (dir) => { + try { + return fs.lstatSync(dir).isDirectory(); + } catch { + return false; + } + }; + + readDir = (dir) => { + return fs.readdirSync(dir); + }; + + makeDir = (dir) => { + fs.mkdirSync(dir); + }; +} diff --git a/src/services/uiservice.js b/src/services/uiservice.js index 7711403..0ff9233 100644 --- a/src/services/uiservice.js +++ b/src/services/uiservice.js @@ -55,7 +55,7 @@ export class UiService { askMultipleChoice = async (message, choices) => { var counter = 1; var promptChoices = []; - choices.forEach(function(choice) { + choices.forEach(function (choice) { promptChoices.push(`${counter}. ${choice.label}`); counter++; }); @@ -67,8 +67,8 @@ export class UiService { message: message, choices: promptChoices, pageSize: counter - 1, - loop: true - } + loop: true, + }, ]); const selectStr = choice.split(".")[0]; @@ -76,4 +76,15 @@ export class UiService { await choices[selectIndex].action(); }; + + askPrompt = async (prompt) => { + const response = await inquirer.prompt([ + { + type: "input", + name: "valueStr", + message: prompt, + }, + ]); + return response.valueStr; + }; } diff --git a/src/ui/installmenu.js b/src/ui/installmenu.js new file mode 100644 index 0000000..86a8a45 --- /dev/null +++ b/src/ui/installmenu.js @@ -0,0 +1,38 @@ +export class InstallMenu { + constructor(uiService, config) { + this.ui = uiService; + this.config = config; + } + + show = async () => { + await this.ui.askMultipleChoice("Configure your Codex installation", [ + { + label: "Install path: " + this.config.codexPath, + action: async function () { + console.log("run path selector"); + }, + }, + { + label: "Storage provider module: Disabled (todo)", + action: this.storageProviderOption, + }, + { + label: "Install!", + action: this.performInstall, + }, + { + label: "Cancel", + action: async function () {}, + }, + ]); + }; + + storageProviderOption = async () => { + this.ui.showInfoMessage("This option is not currently available."); + await this.show(); + }; + + performInstall = async () => { + console.log("todo"); + }; +} diff --git a/src/ui/mainmenu.js b/src/ui/mainmenu.js index 3fcf0fd..867c392 100644 --- a/src/ui/mainmenu.js +++ b/src/ui/mainmenu.js @@ -1,13 +1,14 @@ export class MainMenu { - constructor(uiService) { + constructor(uiService, installMenu) { this.ui = uiService; + this.installMenu = installMenu; this.running = true; } show = async () => { this.ui.showLogo(); this.ui.showInfoMessage("hello"); - + while (this.running) { await this.promptMainMenu(); } @@ -15,22 +16,20 @@ export class MainMenu { this.ui.showInfoMessage("K-THX-BYE"); }; - promptMainMenu = async() => { - await this.ui.askMultipleChoice("Select an option",[ + promptMainMenu = async () => { + await this.ui.askMultipleChoice("Select an option", [ { - label: "optionOne", - action: async function() { - console.log("A!") - } - },{ - label: "optionTwo", - action: this.closeMainMenu + label: "Install Codex", + action: this.installMenu.show, }, - ]) + { + label: "Exit", + action: this.closeMainMenu, + }, + ]); }; - closeMainMenu = async() => { - console.log("B!") + closeMainMenu = async () => { this.running = false; }; } diff --git a/src/ui/mainmenu.test.js b/src/ui/mainmenu.test.js new file mode 100644 index 0000000..249a6c7 --- /dev/null +++ b/src/ui/mainmenu.test.js @@ -0,0 +1,51 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { MainMenu } from "./mainmenu.js"; + +describe("mainmenu", () => { + let mainmenu; + const mockUiService = { + showLogo: vi.fn(), + showInfoMessage: vi.fn(), + askMultipleChoice: vi.fn(), + }; + const mockInstallMenu = { + show: vi.fn(), + }; + + beforeEach(() => { + vi.resetAllMocks(); + + mainmenu = new MainMenu(mockUiService, mockInstallMenu); + + // Presents test getting stuck in main loop. + const originalPrompt = mainmenu.promptMainMenu; + mainmenu.promptMainMenu = async () => { + mainmenu.running = false; + await originalPrompt(); + }; + }); + + it("shows the main menu", async () => { + await mainmenu.show(); + + expect(mockUiService.showLogo).toHaveBeenCalled(); + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("hello"); // example, delete this later. + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("K-THX-BYE"); // example, delete this later. + + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Select an option", + [ + { label: "Install Codex", action: mockInstallMenu.show }, + { label: "Exit", action: mainmenu.closeMainMenu }, + ], + ); + }); + + it("sets running to false when closeMainMenu is called", async () => { + mainmenu.running = true; + + await mainmenu.closeMainMenu(); + + expect(mainmenu.running).toEqual(false); + }); +}); diff --git a/src/utils/numberSelector.js b/src/utils/numberSelector.js index 8342f6c..1dcc10c 100644 --- a/src/utils/numberSelector.js +++ b/src/utils/numberSelector.js @@ -1,52 +1,49 @@ -import inquirer from "inquirer"; +export class NumberSelector { + constructor(uiService) { + this.uiService = uiService; + } -function getMetricsMult(valueStr, allowMetricPostfixes) { - if (!allowMetricPostfixes) return 1; - const lower = valueStr.toLowerCase(); - if (lower.endsWith("tb") || lower.endsWith("t")) return Math.pow(1024, 4); - if (lower.endsWith("gb") || lower.endsWith("g")) return Math.pow(1024, 3); - if (lower.endsWith("mb") || lower.endsWith("m")) return Math.pow(1024, 2); - if (lower.endsWith("kb") || lower.endsWith("k")) return Math.pow(1024, 1); - return 1; -} - -function getNumericValue(valueStr) { - try { - const num = valueStr.match(/\d+/g); - const result = parseInt(num); - if (isNaN(result) || !isFinite(result)) { - throw new Error("Invalid input received."); + showNumberSelector = async ( + currentValue, + promptMessage, + allowMetricPostfixes, + ) => { + try { + var valueStr = await this.promptForValueStr(promptMessage); + valueStr = valueStr.replaceAll(" ", ""); + const mult = this.getMetricsMult(valueStr, allowMetricPostfixes); + const value = this.getNumericValue(valueStr); + return value * mult; + } catch { + return currentValue; } - return result; - } catch (error) { - console.log("Failed to parse input: " + error.message); - throw error; - } -} + }; -async function promptForValueStr(promptMessage) { - const response = await inquirer.prompt([ - { - type: "input", - name: "valueStr", - message: promptMessage, - }, - ]); - return response.valueStr; -} + getMetricsMult = (valueStr, allowMetricPostfixes) => { + if (!allowMetricPostfixes) return 1; + const lower = valueStr.toLowerCase(); + if (lower.endsWith("tb") || lower.endsWith("t")) return Math.pow(1024, 4); + if (lower.endsWith("gb") || lower.endsWith("g")) return Math.pow(1024, 3); + if (lower.endsWith("mb") || lower.endsWith("m")) return Math.pow(1024, 2); + if (lower.endsWith("kb") || lower.endsWith("k")) return Math.pow(1024, 1); + return 1; + }; -export async function showNumberSelector( - currentValue, - promptMessage, - allowMetricPostfixes, -) { - try { - var valueStr = await promptForValueStr(promptMessage); - valueStr = valueStr.replaceAll(" ", ""); - const mult = getMetricsMult(valueStr, allowMetricPostfixes); - const value = getNumericValue(valueStr); - return value * mult; - } catch { - return currentValue; - } + getNumericValue = (valueStr) => { + try { + const num = valueStr.match(/\d+/g); + const result = parseInt(num); + if (isNaN(result) || !isFinite(result)) { + throw new Error("Invalid input received."); + } + return result; + } catch (error) { + console.log("Failed to parse input: " + error.message); + throw error; + } + }; + + promptForValueStr = async (promptMessage) => { + return this.uiService.askPrompt(promptMessage); + }; } diff --git a/src/utils/numberSelector.test.js b/src/utils/numberSelector.test.js new file mode 100644 index 0000000..4ee3269 --- /dev/null +++ b/src/utils/numberSelector.test.js @@ -0,0 +1,66 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { NumberSelector } from "./numberSelector.js"; + +describe("number selector", () => { + let numberSelector; + const mockUiService = { + askPrompt: vi.fn(), + }; + + const prompt = "abc??"; + + beforeEach(() => { + vi.resetAllMocks(); + + numberSelector = new NumberSelector(mockUiService); + }); + + it("shows the prompt", async () => { + await numberSelector.showNumberSelector(0, prompt, false); + + expect(mockUiService.askPrompt).toHaveBeenCalledWith(prompt); + }); + + it("returns a number given valid input", async () => { + mockUiService.askPrompt.mockResolvedValue("123"); + + const number = await numberSelector.showNumberSelector(0, prompt, false); + + expect(number).toEqual(123); + }); + + it("returns the current number given invalid input", async () => { + const currentValue = 321; + + mockUiService.askPrompt.mockResolvedValue("what?!"); + + const number = await numberSelector.showNumberSelector( + currentValue, + prompt, + false, + ); + + expect(number).toEqual(currentValue); + }); + + async function run(input) { + mockUiService.askPrompt.mockResolvedValue(input); + return await numberSelector.showNumberSelector(0, prompt, true); + } + + it("allows for metric postfixes (k)", async () => { + expect(await run("1k")).toEqual(1024); + }); + + it("allows for metric postfixes (m)", async () => { + expect(await run("1m")).toEqual(1024 * 1024); + }); + + it("allows for metric postfixes (g)", async () => { + expect(await run("1g")).toEqual(1024 * 1024 * 1024); + }); + + it("allows for metric postfixes (t)", async () => { + expect(await run("1t")).toEqual(1024 * 1024 * 1024 * 1024); + }); +}); diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index c8a5aaa..0f6590b 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -1,251 +1,219 @@ -import path from "path"; -import inquirer from "inquirer"; -import boxen from "boxen"; -import chalk from "chalk"; -import fs from "fs"; -import { filesystemSync } from "fs-filesystem"; +export class PathSelector { + constructor(uiService, fsService) { + this.ui = uiService; + this.fs = fsService; -function showMsg(msg) { - console.log( - boxen(chalk.white(msg), { - padding: 1, - margin: 1, - borderStyle: "round", - borderColor: "white", - titleAlignment: "center", - }), - ); -} - -function getAvailableRoots() { - const devices = filesystemSync(); - var mountPoints = []; - Object.keys(devices).forEach(function (key) { - var val = devices[key]; - val.volumes.forEach(function (volume) { - mountPoints.push(volume.mountPoint); - }); - }); - - if (mountPoints.length < 1) { - throw new Error("Failed to detect file system devices."); + this.pathMustExist = true; } - return mountPoints; -} -function splitPath(str) { - return str.replaceAll("\\", "/").split("/"); -} - -function dropEmptyParts(parts) { - var result = []; - parts.forEach(function (part) { - if (part.length > 0) { - result.push(part); + showPathSelector = async (startingPath, pathMustExist) => { + this.pathMustExist = pathMustExist; + this.roots = this.fs.getAvailableRoots(); + this.currentPath = this.splitPath(startingPath); + if (!this.hasValidRoot(this.currentPath)) { + this.currentPath = [roots[0]]; } - }); - return result; -} + while (true) { + this.showCurrent(); + this.ui.askMultiChoice("Select an option:", [ + { + label: "Enter path", + action: this.enterPath, + }, + { + label: "Go up one", + action: this.upOne, + }, + { + label: "Go down one", + action: this.downOne, + }, + { + label: "Create new folder here", + action: this.createSubDir, + }, + { + label: "Select this path", + action: this.selectThisPath, + }, + { + label: "Cancel", + action: this.cancel, + }, + ]); + // var newCurrentPath = currentPath; + // switch (choice.split(".")[0]) { + // case "1": + // newCurrentPath = await enterPath(currentPath, pathMustExist); + // break; + // case "2": + // newCurrentPath = upOne(currentPath); + // break; + // case "3": + // newCurrentPath = await downOne(currentPath); + // break; + // case "4": + // newCurrentPath = await createSubDir(currentPath, pathMustExist); + // break; + // case "5": + // if (pathMustExist && !isDir(combine(currentPath))) { + // console.log("Current path does not exist."); + // break; + // } else { + // return combine(currentPath); + // } + // case "6": + // return combine(currentPath); + // } -function combine(parts) { - const toJoin = dropEmptyParts(parts); - if (toJoin.length == 1) return toJoin[0]; - return path.join(...toJoin); -} + // if (hasValidRoot(roots, newCurrentPath)) { + // currentPath = newCurrentPath; + // } else { + // console.log("Selected path has no valid root."); + // } + } + }; -function combineWith(parts, extra) { - const toJoin = dropEmptyParts(parts); - if (toJoin.length == 1) return path.join(toJoin[0], extra); - return path.join(...toJoin, extra); -} + splitPath = (str) => { + return str.replaceAll("\\", "/").split("/"); + }; -function showCurrent(currentPath) { - const len = currentPath.length; - showMsg(`Current path: [${len}]\n` + combine(currentPath)); + dropEmptyParts = (parts) => { + var result = []; + parts.forEach(function (part) { + if (part.length > 0) { + result.push(part); + } + }); + return result; + }; - if (len < 2) { - showMsg( - "Warning - Known issue:\n" + - "Path selection does not work in root paths on some platforms.\n" + - 'Use "Enter path" or "Create new folder" to navigate and create folders\n' + - "if this is the case for you.", + combine = (parts) => { + const toJoin = this.dropEmptyParts(parts); + if (toJoin.length == 1) return toJoin[0]; + return this.fs.pathJoin(...toJoin); + }; + + combineWith = (parts, extra) => { + const toJoin = this.dropEmptyParts(parts); + if (toJoin.length == 1) return this.fs.pathJoin(toJoin[0], extra); + return this.fs.pathJoin(...toJoin, extra); + }; + + showCurrent = () => { + const len = this.currentPath.length; + this.ui.showInfoMessage( + `Current path: [${len}]\n` + this.combine(this.currentPath), ); - } -} -function hasValidRoot(roots, checkPath) { - if (checkPath.length < 1) return false; - var result = false; - roots.forEach(function (root) { - if (root.toLowerCase() == checkPath[0].toLowerCase()) { - console.log("valid root: " + combine(checkPath)); - result = true; + if (len < 2) { + this.ui.showInfoMessage( + "Warning - Known issue:\n" + + "Path selection does not work in root paths on some platforms.\n" + + 'Use "Enter path" or "Create new folder" to navigate and create folders\n' + + "if this is the case for you.", + ); } - }); - if (!result) console.log("invalid root: " + combine(checkPath)); - return result; -} + }; -async function showMain(currentPath) { - showCurrent(currentPath); - const { choice } = await inquirer - .prompt([ - { - type: "list", - name: "choice", - message: "Select an option:", - choices: [ - "1. Enter path", - "2. Go up one", - "3. Go down one", - "4. Create new folder here", - "5. Select this path", - "6. Cancel", - ], - pageSize: 6, - loop: true, - }, - ]) - .catch(() => { - handleExit(); - return { choice: "6" }; + hasValidRoot = (checkPath) => { + if (checkPath.length < 1) return false; + var result = false; + this.roots.forEach(function (root) { + if (root.toLowerCase() == checkPath[0].toLowerCase()) { + result = true; + } }); + return result; + }; - return choice; -} - -export async function showPathSelector(startingPath, pathMustExist) { - const roots = getAvailableRoots(); - var currentPath = splitPath(startingPath); - if (!hasValidRoot(roots, currentPath)) { - currentPath = [roots[0]]; - } - - while (true) { - const choice = await showMain(currentPath); - - var newCurrentPath = currentPath; - switch (choice.split(".")[0]) { - case "1": - newCurrentPath = await enterPath(currentPath, pathMustExist); - break; - case "2": - newCurrentPath = upOne(currentPath); - break; - case "3": - newCurrentPath = await downOne(currentPath); - break; - case "4": - newCurrentPath = await createSubDir(currentPath, pathMustExist); - break; - case "5": - if (pathMustExist && !isDir(combine(currentPath))) { - console.log("Current path does not exist."); - break; - } else { - return combine(currentPath); - } - case "6": - return combine(currentPath); + updateCurrentIfValidFull = (newFullPath) => { + if (this.pathMustExist && !this.fs.isDir(newFullPath)) { + console.log("The path does not exist."); } + this.updateCurrentIfValidParts(this.splitPath(newFullPath)); + } - if (hasValidRoot(roots, newCurrentPath)) { - currentPath = newCurrentPath; - } else { - console.log("Selected path has no valid root."); + updateCurrentIfValidParts = (newParts) => { + if (!this.hasValidRoot(newParts)) { + console.log("The path has no valid root."); } - } -} - -async function enterPath(currentPath, pathMustExist) { - const response = await inquirer.prompt([ - { - type: "input", - name: "path", - message: "Enter Path:", - }, - ]); - - const newPath = response.path; - if (pathMustExist && !isDir(newPath)) { - console.log("The entered path does not exist."); - return currentPath; - } - return splitPath(response.path); -} - -function upOne(currentPath) { - return currentPath.slice(0, currentPath.length - 1); -} - -export function isDir(dir) { - try { - return fs.lstatSync(dir).isDirectory(); - } catch { - return false; - } -} - -function isSubDir(currentPath, entry) { - const newPath = combineWith(currentPath, entry); - return isDir(newPath); -} - -function getSubDirOptions(currentPath) { - const fullPath = combine(currentPath); - const entries = fs.readdirSync(fullPath); - var result = []; - var counter = 1; - entries.forEach(function (entry) { - if (isSubDir(currentPath, entry)) { - result.push(counter + ". " + entry); - counter = counter + 1; - } - }); - return result; -} - -async function downOne(currentPath) { - const options = getSubDirOptions(currentPath); - if (options.length == 0) { - console.log("There are no subdirectories here."); - return currentPath; + this.currentPath = newParts; } - const { choice } = await inquirer - .prompt([ - { - type: "list", - name: "choice", - message: "Select an subdir:", - choices: options, - pageSize: options.length, - loop: true, - }, - ]) - .catch(() => { - return currentPath; + enterPath = async () => { + const newPath = await this.ui.askPrompt("Enter Path:"); + this.updateCurrentIfValidFull(newPath); + }; + + upOne = () => { + const newParts = this.currentPath.slice(0, this.currentPath.length - 1); + this.updateCurrentIfValidParts(newParts); + }; + + isSubDir = (entry) => { + const newPath = this.combineWith(this.currentPath, entry); + return this.fs.isDir(newPath); + }; + + getSubDirOptions = () => { + const fullPath = this.combine(this.currentPath); + const entries = this.fs.readDir(fullPath); + var result = []; + entries.forEach(function (entry) { + if (this.isSubDir(entry)) { + result.push(entry); + } }); + return result; + }; - const subDir = choice.split(". ")[1]; - return [...currentPath, subDir]; -} - -async function createSubDir(currentPath, pathMustExist) { - const response = await inquirer.prompt([ - { - type: "input", - name: "name", - message: "Enter name:", - }, - ]); - - const name = response.name; - if (name.length < 1) return; - - const fullDir = combineWith(currentPath, name); - if (pathMustExist && !isDir(fullDir)) { - fs.mkdirSync(fullDir); - } - return [...currentPath, name]; + downOne = async () => { + const options = this.getSubDirOptions(); + if (options.length == 0) { + console.log("There are no subdirectories here."); + } + + var selected = ""; + const makeSelector = () => { + + }; + + const { choice } = await inquirer + .prompt([ + { + type: "list", + name: "choice", + message: "Select an subdir:", + choices: options, + pageSize: options.length, + loop: true, + }, + ]) + .catch(() => { + return currentPath; + }); + + const subDir = choice.split(". ")[1]; + return [...currentPath, subDir]; + }; + + createSubDir = async (currentPath, pathMustExist) => { + const response = await inquirer.prompt([ + { + type: "input", + name: "name", + message: "Enter name:", + }, + ]); + + const name = response.name; + if (name.length < 1) return; + + const fullDir = combineWith(currentPath, name); + if (pathMustExist && !isDir(fullDir)) { + // fs.mkdirSync(fullDir); + } + return [...currentPath, name]; + }; } From 8d0418be810ce9afaeb62475f7441b476c4de64d Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 31 Mar 2025 13:26:53 +0200 Subject: [PATCH 05/59] finishes convert of pathSelector util --- src/utils/pathSelector.js | 80 ++++++++------------------------------- 1 file changed, 15 insertions(+), 65 deletions(-) diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index 0f6590b..6e6c473 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -41,36 +41,6 @@ export class PathSelector { action: this.cancel, }, ]); - // var newCurrentPath = currentPath; - // switch (choice.split(".")[0]) { - // case "1": - // newCurrentPath = await enterPath(currentPath, pathMustExist); - // break; - // case "2": - // newCurrentPath = upOne(currentPath); - // break; - // case "3": - // newCurrentPath = await downOne(currentPath); - // break; - // case "4": - // newCurrentPath = await createSubDir(currentPath, pathMustExist); - // break; - // case "5": - // if (pathMustExist && !isDir(combine(currentPath))) { - // console.log("Current path does not exist."); - // break; - // } else { - // return combine(currentPath); - // } - // case "6": - // return combine(currentPath); - // } - - // if (hasValidRoot(roots, newCurrentPath)) { - // currentPath = newCurrentPath; - // } else { - // console.log("Selected path has no valid root."); - // } } }; @@ -175,45 +145,25 @@ export class PathSelector { } var selected = ""; - const makeSelector = () => { - - }; - - const { choice } = await inquirer - .prompt([ - { - type: "list", - name: "choice", - message: "Select an subdir:", - choices: options, - pageSize: options.length, - loop: true, + var uiOptions = []; + options.foreach(function (option) { + uiOptions.push({ + label: option, + action: () => { + selected = option; }, - ]) - .catch(() => { - return currentPath; - }); + }) + }) - const subDir = choice.split(". ")[1]; - return [...currentPath, subDir]; + await this.ui.askMultipleChoice("Select an subdir", uiOptions); + + if (selected.length < 1) return; + this.updateCurrentIfValidParts([...this.currentPath, selected]); }; - createSubDir = async (currentPath, pathMustExist) => { - const response = await inquirer.prompt([ - { - type: "input", - name: "name", - message: "Enter name:", - }, - ]); - - const name = response.name; + createSubDir = async () => { + const name = await this.ui.askPrompt("Enter name:"); if (name.length < 1) return; - - const fullDir = combineWith(currentPath, name); - if (pathMustExist && !isDir(fullDir)) { - // fs.mkdirSync(fullDir); - } - return [...currentPath, name]; + this.updateCurrentIfValidParts([...currentPath, name]); }; } From a1d605697497756bdcbbddf9b98edb6c4649754d Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 31 Mar 2025 14:26:57 +0200 Subject: [PATCH 06/59] sets up config menu and config service --- src/configmenu.js | 162 -------------------------- src/handlers/installationHandlers.js | 1 - src/main.js | 7 +- src/services/config.js | 54 --------- src/services/configService.js | 64 ++++++++++ src/ui/configmenu.js | 167 +++++++++++++++++++++++++++ src/ui/installmenu.js | 4 +- src/utils/numberSelector.js | 2 +- src/utils/pathSelector.js | 18 ++- 9 files changed, 253 insertions(+), 226 deletions(-) delete mode 100644 src/configmenu.js delete mode 100644 src/services/config.js create mode 100644 src/services/configService.js create mode 100644 src/ui/configmenu.js diff --git a/src/configmenu.js b/src/configmenu.js deleted file mode 100644 index d0696a2..0000000 --- a/src/configmenu.js +++ /dev/null @@ -1,162 +0,0 @@ -import inquirer from "inquirer"; -import chalk from "chalk"; -import { showErrorMessage, showInfoMessage } from "./utils/messages.js"; -import { isDir, showPathSelector } from "./utils/pathSelector.js"; -import { saveConfig } from "./services/config.js"; -import { showNumberSelector } from "./utils/numberSelector.js"; -import fs from "fs-extra"; - -function bytesAmountToString(numBytes) { - const units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - - var value = numBytes; - var index = 0; - while (value > 1024) { - index = index + 1; - value = value / 1024; - } - - if (index == 0) return `${numBytes} Bytes`; - return `${numBytes} Bytes (${value} ${units[index]})`; -} - -async function showStorageQuotaSelector(config) { - console.log(showInfoMessage('You can use: "GB" or "gb", etc.')); - const result = await showNumberSelector( - config.storageQuota, - "Storage quota", - true, - ); - if (result < 100 * 1024 * 1024) { - console.log(showErrorMessage("Storage quote should be >= 100mb.")); - return config.storageQuota; - } - return result; -} - -export async function showConfigMenu(config) { - var newDataDir = config.dataDir; - try { - while (true) { - console.log(showInfoMessage("Codex Configuration")); - const { choice } = await inquirer - .prompt([ - { - type: "list", - name: "choice", - message: "Select to edit:", - choices: [ - `1. Data path = "${newDataDir}"`, - `2. Logs path = "${config.logsDir}"`, - `3. Storage quota = ${bytesAmountToString(config.storageQuota)}`, - `4. Discovery port = ${config.ports.discPort}`, - `5. P2P listen port = ${config.ports.listenPort}`, - `6. API port = ${config.ports.apiPort}`, - "7. Save changes and exit", - "8. Discard changes and exit", - ], - pageSize: 8, - loop: true, - }, - ]) - .catch(() => { - return; - }); - - switch (choice.split(".")[0]) { - case "1": - newDataDir = await showPathSelector(config.dataDir, false); - if (isDir(newDataDir)) { - console.log( - showInfoMessage( - "Warning: The new data path already exists. Make sure you know what you're doing.", - ), - ); - } - break; - case "2": - config.logsDir = await showPathSelector(config.logsDir, true); - break; - case "3": - config.storageQuota = await showStorageQuotaSelector(config); - break; - case "4": - config.ports.discPort = await showNumberSelector( - config.ports.discPort, - "Discovery Port (UDP)", - false, - ); - break; - case "5": - config.ports.listenPort = await showNumberSelector( - config.ports.listenPort, - "Listen Port (TCP)", - false, - ); - break; - case "6": - config.ports.apiPort = await showNumberSelector( - config.ports.apiPort, - "API Port (TCP)", - false, - ); - break; - case "7": - // save changes, back to main menu - config = updateDataDir(config, newDataDir); - saveConfig(config); - return; - case "8": - // discard changes, back to main menu - return; - } - } - } catch (error) { - console.error(chalk.red("An error occurred:", error.message)); - return; - } -} - -function updateDataDir(config, newDataDir) { - if (config.dataDir == newDataDir) return config; - - // The Codex dataDir is a little strange: - // If the old one is empty: The new one should not exist, so that codex creates it - // with the correct security permissions. - // If the old one does exist: We move it. - - if (isDir(config.dataDir)) { - console.log( - showInfoMessage( - "Moving Codex data folder...\n" + - `From: "${config.dataDir}"\n` + - `To: "${newDataDir}"`, - ), - ); - - try { - fs.moveSync(config.dataDir, newDataDir); - } catch (error) { - console.log( - showErrorMessage("Error while moving dataDir: " + error.message), - ); - throw error; - } - } else { - // Old data dir does not exist. - if (isDir(newDataDir)) { - console.log( - showInfoMessage( - "Warning: the selected data path already exists.\n" + - `New data path = "${newDataDir}"\n` + - "Codex may overwrite data in this folder.\n" + - "Codex will fail to start if this folder does not have the required\n" + - "security permissions.", - ), - ); - } - } - - config.dataDir = newDataDir; - return config; -} diff --git a/src/handlers/installationHandlers.js b/src/handlers/installationHandlers.js index 0bd5483..a2e4d48 100644 --- a/src/handlers/installationHandlers.js +++ b/src/handlers/installationHandlers.js @@ -12,7 +12,6 @@ import { showSuccessMessage, } from "../utils/messages.js"; import { checkDependencies } from "../services/nodeService.js"; -import { saveConfig } from "../services/config.js"; import { getCodexRootPath, getCodexBinPath } from "../utils/appdata.js"; const platform = os.platform(); diff --git a/src/main.js b/src/main.js index c00e9c3..c8a2db7 100644 --- a/src/main.js +++ b/src/main.js @@ -19,8 +19,7 @@ import { } from "./handlers/installationHandlers.js"; import { runCodex, checkNodeStatus } from "./handlers/nodeHandlers.js"; import { showInfoMessage } from "./utils/messages.js"; -import { loadConfig } from "./services/config.js"; -import { showConfigMenu } from "./configmenu.js"; +import { ConfigService } from "./services/configService.js"; import { openCodexApp } from "./services/codexapp.js"; import { MainMenu } from "./ui/mainmenu.js"; @@ -90,9 +89,9 @@ export async function main() { process.on("SIGTERM", handleExit); process.on("SIGQUIT", handleExit); - const config = loadConfig(); + const configService = new ConfigService(); const uiService = new UiService(); - const installMenu = new InstallMenu(uiService, config); + const installMenu = new InstallMenu(uiService, configService); const mainMenu = new MainMenu(uiService, installMenu); await mainMenu.show(); diff --git a/src/services/config.js b/src/services/config.js deleted file mode 100644 index 7bbc2de..0000000 --- a/src/services/config.js +++ /dev/null @@ -1,54 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { getAppDataDir } from "../utils/appdata.js"; -import { - getCodexBinPath, - getCodexDataDirDefaultPath, - getCodexLogsDefaultPath, -} from "../utils/appdata.js"; - -const defaultConfig = { - codexExe: "", - // User-selected config options: - codexPath: getCodexBinPath(), - dataDir: getCodexDataDirDefaultPath(), - logsDir: getCodexLogsDefaultPath(), - storageQuota: 8 * 1024 * 1024 * 1024, - ports: { - discPort: 8090, - listenPort: 8070, - apiPort: 8080, - }, -}; - -function getConfigFilename() { - return path.join(getAppDataDir(), "config.json"); -} - -export function saveConfig(config) { - const filePath = getConfigFilename(); - try { - fs.writeFileSync(filePath, JSON.stringify(config)); - } catch (error) { - console.error( - `Failed to save config file to '${filePath}' error: '${error}'.`, - ); - throw error; - } -} - -export function loadConfig() { - const filePath = getConfigFilename(); - try { - if (!fs.existsSync(filePath)) { - saveConfig(defaultConfig); - return defaultConfig; - } - return JSON.parse(fs.readFileSync(filePath)); - } catch (error) { - console.error( - `Failed to load config file from '${filePath}' error: '${error}'.`, - ); - throw error; - } -} diff --git a/src/services/configService.js b/src/services/configService.js new file mode 100644 index 0000000..7a2539b --- /dev/null +++ b/src/services/configService.js @@ -0,0 +1,64 @@ +import fs from "fs"; +import path from "path"; +import { getAppDataDir } from "../utils/appdata.js"; +import { + getCodexBinPath, + getCodexDataDirDefaultPath, + getCodexLogsDefaultPath, +} from "../utils/appdata.js"; + +const defaultConfig = { + codexExe: "", + // User-selected config options: + codexPath: getCodexBinPath(), + dataDir: getCodexDataDirDefaultPath(), + logsDir: getCodexLogsDefaultPath(), + storageQuota: 8 * 1024 * 1024 * 1024, + ports: { + discPort: 8090, + listenPort: 8070, + apiPort: 8080, + }, +}; + +export class ConfigService { + constructor() { + this.loadConfig(); + } + + get = () => { + return this.config; + } + + loadConfig = () => { + const filePath = this.getConfigFilename(); + try { + if (!fs.existsSync(filePath)) { + this.config = defaultConfig; + this.saveConfig(); + } + this.config = JSON.parse(fs.readFileSync(filePath)); + } catch (error) { + console.error( + `Failed to load config file from '${filePath}' error: '${error}'.`, + ); + throw error; + } + } + + saveConfig = () => { + const filePath = this.getConfigFilename(); + try { + fs.writeFileSync(filePath, JSON.stringify(this.config)); + } catch (error) { + console.error( + `Failed to save config file to '${filePath}' error: '${error}'.`, + ); + throw error; + } + } + + getConfigFilename = () => { + return path.join(getAppDataDir(), "config.json"); + } +} diff --git a/src/ui/configmenu.js b/src/ui/configmenu.js new file mode 100644 index 0000000..c884412 --- /dev/null +++ b/src/ui/configmenu.js @@ -0,0 +1,167 @@ +export class ConfigMenu { + constructor(uiService, configService, pathSelector, numberSelector) { + this.ui = uiService; + this.configService = configService; + this.pathSelector = pathSelector; + this.numberSelector = numberSelector; + } + + show = async() => { + this.running = true; + this.config = this.configService.get(); + while (this.running) { + this.ui.showInfoMessage("Codex Configuration"); + await this.ui.askMultipleChoice("Select to edit:",[ + { + label: `Data path = "${this.config.dataDir}"`, + action: this.editDataDir, + }, + { + label: `Logs path = "${this.config.logsDir}"`, + action: this.editLogsDir, + }, + { + label: `Storage quota = ${bytesAmountToString(this.config.storageQuota)}`, + action: this.editStorageQuota, + }, + { + label: `Discovery port = ${this.config.ports.discPort}`, + action: this.editDiscPort, + }, + { + label: `P2P listen port = ${this.config.ports.listenPort}`, + action: this.editListenPort, + }, + { + label: `API port = ${this.config.ports.apiPort}`, + action: this.editApiPort, + }, + { + label: "Save changes and exit", + action: this.saveChangesAndExit, + }, + { + label: "Discard changes and exit", + action: this.discardChangesAndExit, + } + ] + ) + } + } + + bytesAmountToString = (numBytes) => { + const units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + + var value = numBytes; + var index = 0; + while (value > 1024) { + index = index + 1; + value = value / 1024; + } + + if (index == 0) return `${numBytes} Bytes`; + return `${numBytes} Bytes (${value} ${units[index]})`; + } + + editDataDir = async () => { + // todo + // function updateDataDir(config, newDataDir) { + // if (config.dataDir == newDataDir) return config; + + // // The Codex dataDir is a little strange: + // // If the old one is empty: The new one should not exist, so that codex creates it + // // with the correct security permissions. + // // If the old one does exist: We move it. + + // if (isDir(config.dataDir)) { + // console.log( + // showInfoMessage( + // "Moving Codex data folder...\n" + + // `From: "${config.dataDir}"\n` + + // `To: "${newDataDir}"`, + // ), + // ); + + // try { + // fs.moveSync(config.dataDir, newDataDir); + // } catch (error) { + // console.log( + // showErrorMessage("Error while moving dataDir: " + error.message), + // ); + // throw error; + // } + // } else { + // // Old data dir does not exist. + // if (isDir(newDataDir)) { + // console.log( + // showInfoMessage( + // "Warning: the selected data path already exists.\n" + + // `New data path = "${newDataDir}"\n` + + // "Codex may overwrite data in this folder.\n" + + // "Codex will fail to start if this folder does not have the required\n" + + // "security permissions.", + // ), + // ); + // } + // } + + // config.dataDir = newDataDir; + // return config; + // } + } + + editLogsDir = async () => { + this.config.logsDir = await this.pathSelector.show(this.config.logsDir, true); + } + + editStorageQuota = async () => { + this.ui.showInfoMessage("You can use: 'GB' or 'gb', etc."); + const newQuota = await this.numberSelector.show(this.config.storageQuota, "Storage quota", true); + if (newQuota < 100 * 1024 * 1024) { + this.ui.showErrorMessage("Storage quote should be >= 100mb."); + } else { + this.config.storageQuota = newQuota; + } + } + + editDiscPort = async () => { + const newPort = await this.numberSelector.show(this.config.ports.discPort, "Discovery port", false); + if (this.isInPortRange(newPort)) { + this.config.ports.discPort = newPort; + } + } + + editListenPort = async () => { + const newPort = await this.numberSelector.show(this.config.ports.listenPort, "P2P listen port", false); + if (this.isInPortRange(newPort)) { + this.config.ports.listenPort = newPort; + } + } + + editApiPort = async () => { + const newPort = await this.numberSelector.show(this.config.ports.apiPort, "API port", false); + if (this.isInPortRange(newPort)) { + this.config.ports.apiPort = newPort; + } + } + + isInPortRange = (number) => { + if (number < 1024 || number > 65535) { + this.ui.showErrorMessage("Port should be between 1024 and 65535."); + return false; + } + return true; + } + + saveChangesAndExit = async () => { + this.configService.saveConfig(); + this.ui.showInfoMessage("Configuration changes saved."); + this.running = false; + } + + discardChangesAndExit = async () => { + this.configService.loadConfig(); + this.ui.showInfoMessage("Changes discarded."); + this.running = false; + } +} diff --git a/src/ui/installmenu.js b/src/ui/installmenu.js index 86a8a45..f00c620 100644 --- a/src/ui/installmenu.js +++ b/src/ui/installmenu.js @@ -1,7 +1,7 @@ export class InstallMenu { - constructor(uiService, config) { + constructor(uiService, configService) { this.ui = uiService; - this.config = config; + this.config = configService.get(); } show = async () => { diff --git a/src/utils/numberSelector.js b/src/utils/numberSelector.js index 1dcc10c..d790a0b 100644 --- a/src/utils/numberSelector.js +++ b/src/utils/numberSelector.js @@ -3,7 +3,7 @@ export class NumberSelector { this.uiService = uiService; } - showNumberSelector = async ( + show = async ( currentValue, promptMessage, allowMetricPostfixes, diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index 6e6c473..a0de9f4 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -6,14 +6,16 @@ export class PathSelector { this.pathMustExist = true; } - showPathSelector = async (startingPath, pathMustExist) => { + show = async (startingPath, pathMustExist) => { + this.running = true; + this.startingPath = startingPath; this.pathMustExist = pathMustExist; this.roots = this.fs.getAvailableRoots(); this.currentPath = this.splitPath(startingPath); if (!this.hasValidRoot(this.currentPath)) { this.currentPath = [roots[0]]; } - while (true) { + while (this.running) { this.showCurrent(); this.ui.askMultiChoice("Select an option:", [ { @@ -42,6 +44,8 @@ export class PathSelector { }, ]); } + + return this.resultingPath; }; splitPath = (str) => { @@ -166,4 +170,14 @@ export class PathSelector { if (name.length < 1) return; this.updateCurrentIfValidParts([...currentPath, name]); }; + + selectThisPath = async () => { + this.resultingPath = this.combine(this.currentPath); + this.running = false; + } + + cancel = async () => { + this.resultingPath = this.startingPath; + this.running = false; + }; } From a9c94057e81a0594f47f6500495d255384d60ac1 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 31 Mar 2025 15:02:37 +0200 Subject: [PATCH 07/59] hooks up config menu --- src/main.js | 12 ++++++++++-- src/ui/configmenu.js | 2 +- src/ui/mainmenu.js | 7 ++++++- src/ui/mainmenu.test.js | 7 ++++++- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main.js b/src/main.js index c8a2db7..c15aadf 100644 --- a/src/main.js +++ b/src/main.js @@ -22,9 +22,13 @@ import { showInfoMessage } from "./utils/messages.js"; import { ConfigService } from "./services/configService.js"; import { openCodexApp } from "./services/codexapp.js"; +import { UiService } from "./services/uiservice.js"; +import { FsService } from "./services/fsService.js"; import { MainMenu } from "./ui/mainmenu.js"; import { InstallMenu } from "./ui/installmenu.js"; -import { UiService } from "./services/uiservice.js"; +import { ConfigMenu } from "./ui/configmenu.js"; +import { PathSelector } from "./utils/pathSelector.js"; +import { NumberSelector } from "./utils/numberSelector.js"; async function showNavigationMenu() { console.log("\n"); @@ -91,8 +95,12 @@ export async function main() { const configService = new ConfigService(); const uiService = new UiService(); + const fsService = new FsService(); + const pathSelector = new PathSelector(uiService, fsService); + const numberSelector = new NumberSelector(uiService); const installMenu = new InstallMenu(uiService, configService); - const mainMenu = new MainMenu(uiService, installMenu); + const configMenu = new ConfigMenu(uiService, configService, pathSelector, numberSelector) + const mainMenu = new MainMenu(uiService, installMenu, configMenu); await mainMenu.show(); return; diff --git a/src/ui/configmenu.js b/src/ui/configmenu.js index c884412..1d55340 100644 --- a/src/ui/configmenu.js +++ b/src/ui/configmenu.js @@ -21,7 +21,7 @@ export class ConfigMenu { action: this.editLogsDir, }, { - label: `Storage quota = ${bytesAmountToString(this.config.storageQuota)}`, + label: `Storage quota = ${this.bytesAmountToString(this.config.storageQuota)}`, action: this.editStorageQuota, }, { diff --git a/src/ui/mainmenu.js b/src/ui/mainmenu.js index 867c392..817ac54 100644 --- a/src/ui/mainmenu.js +++ b/src/ui/mainmenu.js @@ -1,7 +1,8 @@ export class MainMenu { - constructor(uiService, installMenu) { + constructor(uiService, installMenu, configMenu) { this.ui = uiService; this.installMenu = installMenu; + this.configMenu = configMenu; this.running = true; } @@ -22,6 +23,10 @@ export class MainMenu { label: "Install Codex", action: this.installMenu.show, }, + { + label: "Configure Codex", + action: this.configMenu.show, + }, { label: "Exit", action: this.closeMainMenu, diff --git a/src/ui/mainmenu.test.js b/src/ui/mainmenu.test.js index 249a6c7..d0de034 100644 --- a/src/ui/mainmenu.test.js +++ b/src/ui/mainmenu.test.js @@ -12,10 +12,14 @@ describe("mainmenu", () => { show: vi.fn(), }; + const mockConfigMenu = { + show: vi.fn(), + } + beforeEach(() => { vi.resetAllMocks(); - mainmenu = new MainMenu(mockUiService, mockInstallMenu); + mainmenu = new MainMenu(mockUiService, mockInstallMenu, mockConfigMenu); // Presents test getting stuck in main loop. const originalPrompt = mainmenu.promptMainMenu; @@ -36,6 +40,7 @@ describe("mainmenu", () => { "Select an option", [ { label: "Install Codex", action: mockInstallMenu.show }, + { label: "Configure Codex", action: mockConfigMenu.show }, { label: "Exit", action: mainmenu.closeMainMenu }, ], ); From 1f694f75348e3dafd90718eae58144848da27a91 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 1 Apr 2025 14:09:20 +0200 Subject: [PATCH 08/59] sets up mocks. rolls out menuloop --- src/__mocks__/service.mocks.js | 15 ++++ src/__mocks__/ui.mocks.js | 9 ++ src/__mocks__/utils.mocks.js | 16 ++++ src/ui/configmenu.test.js | 144 +++++++++++++++++++++++++++++++ src/ui/mainmenu.js | 17 ++-- src/ui/mainmenu.test.js | 57 ++++++------ src/utils/menuLoop.js | 20 +++++ src/utils/menuLoop.test.js | 42 +++++++++ src/utils/numberSelector.test.js | 8 +- 9 files changed, 281 insertions(+), 47 deletions(-) create mode 100644 src/__mocks__/service.mocks.js create mode 100644 src/__mocks__/ui.mocks.js create mode 100644 src/__mocks__/utils.mocks.js create mode 100644 src/ui/configmenu.test.js create mode 100644 src/utils/menuLoop.js create mode 100644 src/utils/menuLoop.test.js diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js new file mode 100644 index 0000000..09600e3 --- /dev/null +++ b/src/__mocks__/service.mocks.js @@ -0,0 +1,15 @@ +import { vi } from "vitest"; + +export const mockUiService = { + showLogo: vi.fn(), + showInfoMessage: vi.fn(), + showErrorMessage: vi.fn(), + askMultipleChoice: vi.fn(), + askPrompt: vi.fn() +}; + +export const mockConfigService = { + get: vi.fn(), + saveConfig: vi.fn(), + loadConfig: vi.fn(), +}; diff --git a/src/__mocks__/ui.mocks.js b/src/__mocks__/ui.mocks.js new file mode 100644 index 0000000..ad74c7d --- /dev/null +++ b/src/__mocks__/ui.mocks.js @@ -0,0 +1,9 @@ +import { vi } from "vitest"; + +export const mockInstallMenu = { + show: vi.fn() +}; + +export const mockConfigMenu = { + show: vi.fn() +}; diff --git a/src/__mocks__/utils.mocks.js b/src/__mocks__/utils.mocks.js new file mode 100644 index 0000000..2274803 --- /dev/null +++ b/src/__mocks__/utils.mocks.js @@ -0,0 +1,16 @@ +import { vi } from "vitest"; + +export const mockPathSelector = { + show: vi.fn(), +}; + +export const mockNumberSelector = { + show: vi.fn(), +}; + +export const mockMenuLoop = { + initialize: vi.fn(), + showOnce: vi.fn(), + showLoop: vi.fn(), + stopLoop: vi.fn(), +}; diff --git a/src/ui/configmenu.test.js b/src/ui/configmenu.test.js new file mode 100644 index 0000000..8887c81 --- /dev/null +++ b/src/ui/configmenu.test.js @@ -0,0 +1,144 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { ConfigMenu } from "./configmenu.js"; +import { mockUiService } from "../__mocks__/service.mocks.js"; +import { mockConfigService } from "../__mocks__/service.mocks.js"; +import { mockPathSelector, mockNumberSelector } from "../__mocks__/ui.mocks.js"; + +describe("ConfigMenu", () => { + let configMenu; + + beforeEach(() => { + vi.resetAllMocks(); + mockConfigService.get.mockReturnValue({ + dataDir: "/data", + logsDir: "/logs", + storageQuota: 1024 * 1024 * 1024, + ports: { + discPort: 8090, + listenPort: 8070, + apiPort: 8080, + } + }); + + configMenu = new ConfigMenu( + mockUiService, + mockConfigService, + mockPathSelector, + mockNumberSelector + ); + }); + + // it("displays the configuration menu", async () => { + // configMenu.running = false; // Prevent infinite loop + // await configMenu.show(); + + // expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + // "Codex Configuration", + // ); + // expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith([ + // { + // label: `Data path = "${mockConfigService.get().dataDir}"`, + // action: configMenu.editDataDir, + // }, + // { + // label: `Logs path = "${mockConfigService.get().logsDir}"`, + // action: configMenu.editLogsDir, + // }, + // { + // label: `Storage quota = 1Gb`, + // action: configMenu.editStorageQuota, + // }, + // { + // label: `Discovery port = ${mockConfigService.get().ports.discPort}`, + // action: configMenu.editDiscPort, + // }, + // { + // label: `P2P listen port = ${mockConfigService.get().ports.listenPort}`, + // action: configMenu.editListenPort, + // }, + // { + // label: `API port = ${mockConfigService.get().ports.apiPort}`, + // action: configMenu.editApiPort, + // }, + // { + // label: "Save changes and exit", + // action: configMenu.saveChangesAndExit, + // }, + // { + // label: "Discard changes and exit", + // action: configMenu.discardChangesAndExit, + // } + // ]); + // }); + +// it("edits the logs directory", async () => { +// mockPathSelector.show.mockResolvedValue("/new-logs"); +// await configMenu.editLogsDir(); + +// expect(mockPathSelector.show).toHaveBeenCalledWith("/logs", true); +// expect(configMenu.config.logsDir).toEqual("/new-logs"); +// }); + +// it("edits the storage quota", async () => { +// mockNumberSelector.show.mockResolvedValue(200 * 1024 * 1024); +// await configMenu.editStorageQuota(); + +// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( +// "You can use: 'GB' or 'gb', etc.", +// ); +// expect(mockNumberSelector.show).toHaveBeenCalledWith( +// 1024 * 1024 * 1024, +// "Storage quota", +// true, +// ); +// expect(configMenu.config.storageQuota).toEqual(200 * 1024 * 1024); +// }); + +// it("shows an error if storage quota is too small", async () => { +// mockNumberSelector.show.mockResolvedValue(50 * 1024 * 1024); +// await configMenu.editStorageQuota(); + +// expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( +// "Storage quote should be >= 100mb.", +// ); +// expect(configMenu.config.storageQuota).toEqual(1024 * 1024 * 1024); // Unchanged +// }); + +// it("edits the discovery port", async () => { +// mockNumberSelector.show.mockResolvedValue(9000); +// await configMenu.editDiscPort(); + +// expect(mockNumberSelector.show).toHaveBeenCalledWith(8090, "Discovery port", false); +// expect(configMenu.config.ports.discPort).toEqual(9000); +// }); + +// it("shows an error if port is out of range", async () => { +// mockNumberSelector.show.mockResolvedValue(1000); +// await configMenu.editDiscPort(); + +// expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( +// "Port should be between 1024 and 65535.", +// ); +// expect(configMenu.config.ports.discPort).toEqual(8090); // Unchanged +// }); + +// it("saves changes and exits", async () => { +// await configMenu.saveChangesAndExit(); + +// expect(mockConfigService.saveConfig).toHaveBeenCalled(); +// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( +// "Configuration changes saved.", +// ); +// expect(configMenu.running).toEqual(false); +// }); + +// it("discards changes and exits", async () => { +// await configMenu.discardChangesAndExit(); + +// expect(mockConfigService.loadConfig).toHaveBeenCalled(); +// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( +// "Changes discarded.", +// ); +// expect(configMenu.running).toEqual(false); +// }); +}); diff --git a/src/ui/mainmenu.js b/src/ui/mainmenu.js index 817ac54..7c5c928 100644 --- a/src/ui/mainmenu.js +++ b/src/ui/mainmenu.js @@ -1,18 +1,17 @@ export class MainMenu { - constructor(uiService, installMenu, configMenu) { + constructor(uiService, menuLoop, installMenu, configMenu) { this.ui = uiService; + this.loop = menuLoop; this.installMenu = installMenu; this.configMenu = configMenu; - this.running = true; + + this.loop.initialize(this.promptMainMenu); } show = async () => { this.ui.showLogo(); - this.ui.showInfoMessage("hello"); - while (this.running) { - await this.promptMainMenu(); - } + await this.loop.showLoop(); this.ui.showInfoMessage("K-THX-BYE"); }; @@ -29,12 +28,8 @@ export class MainMenu { }, { label: "Exit", - action: this.closeMainMenu, + action: this.loop.stopLoop, }, ]); }; - - closeMainMenu = async () => { - this.running = false; - }; } diff --git a/src/ui/mainmenu.test.js b/src/ui/mainmenu.test.js index d0de034..de28c54 100644 --- a/src/ui/mainmenu.test.js +++ b/src/ui/mainmenu.test.js @@ -1,56 +1,49 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { MainMenu } from "./mainmenu.js"; +import { mockUiService } from "../__mocks__/service.mocks.js"; +import { mockInstallMenu, mockConfigMenu } from "../__mocks__/ui.mocks.js"; +import { mockMenuLoop } from "../__mocks__/utils.mocks.js"; describe("mainmenu", () => { let mainmenu; - const mockUiService = { - showLogo: vi.fn(), - showInfoMessage: vi.fn(), - askMultipleChoice: vi.fn(), - }; - const mockInstallMenu = { - show: vi.fn(), - }; - - const mockConfigMenu = { - show: vi.fn(), - } beforeEach(() => { vi.resetAllMocks(); - mainmenu = new MainMenu(mockUiService, mockInstallMenu, mockConfigMenu); - - // Presents test getting stuck in main loop. - const originalPrompt = mainmenu.promptMainMenu; - mainmenu.promptMainMenu = async () => { - mainmenu.running = false; - await originalPrompt(); - }; + mainmenu = new MainMenu(mockUiService, mockMenuLoop, mockInstallMenu, mockConfigMenu); }); - it("shows the main menu", async () => { + it("initializes the menu loop with the promptMainMenu function", () => { + expect(mockMenuLoop.initialize).toHaveBeenCalledWith(mainmenu.promptMainMenu); + }); + + it("shows the logo", async () => { await mainmenu.show(); expect(mockUiService.showLogo).toHaveBeenCalled(); - expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("hello"); // example, delete this later. - expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("K-THX-BYE"); // example, delete this later. + }); + it("starts the menu loop", async () => { + await mainmenu.show(); + + expect(mockMenuLoop.showLoop).toHaveBeenCalled(); + }); + + it("shows the exit message after the menu loop", async () => { + await mainmenu.show(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("K-THX-BYE"); + }); + + it("prompts the main menu with multiple choices", async () => { + await mainmenu.promptMainMenu(); expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( "Select an option", [ { label: "Install Codex", action: mockInstallMenu.show }, { label: "Configure Codex", action: mockConfigMenu.show }, - { label: "Exit", action: mainmenu.closeMainMenu }, + { label: "Exit", action: mockMenuLoop.stopLoop }, ], ); }); - - it("sets running to false when closeMainMenu is called", async () => { - mainmenu.running = true; - - await mainmenu.closeMainMenu(); - - expect(mainmenu.running).toEqual(false); - }); }); diff --git a/src/utils/menuLoop.js b/src/utils/menuLoop.js new file mode 100644 index 0000000..a914ac7 --- /dev/null +++ b/src/utils/menuLoop.js @@ -0,0 +1,20 @@ +export class MenuLoop { + initialize = (menuPrompt) => { + this.menuPrompt = menuPrompt; + } + + showOnce = async () => { + await this.menuPrompt(); + } + + showLoop = async () => { + this.running = true; + while (this.running) { + await this.menuPrompt(); + } + } + + stopLoop = () => { + this.running = false; + } +} diff --git a/src/utils/menuLoop.test.js b/src/utils/menuLoop.test.js new file mode 100644 index 0000000..756f2b1 --- /dev/null +++ b/src/utils/menuLoop.test.js @@ -0,0 +1,42 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { MenuLoop } from "./menuLoop.js"; + +describe("MenuLoop", () => { + let menuLoop; + const mockPrompt = vi.fn(); + + beforeEach(() => { + vi.resetAllMocks(); + menuLoop = new MenuLoop(); + menuLoop.initialize(mockPrompt); + }); + + it("can show menu once", async () => { + await menuLoop.showOnce(); + expect(mockPrompt).toHaveBeenCalledTimes(1); + }); + + it("can stop the menu loop", async () => { + mockPrompt.mockImplementation(() => { + menuLoop.stopLoop(); + }); + await menuLoop.showLoop(); + + expect(mockPrompt).toHaveBeenCalledTimes(1); + expect(menuLoop.running).toBe(false); + }); + + it("can run menu in a loop", async () => { + let calls = 0; + mockPrompt.mockImplementation(() => { + calls++; + if (calls >= 3) { + menuLoop.stopLoop(); + } + }); + + await menuLoop.showLoop(); + + expect(mockPrompt).toHaveBeenCalledTimes(3); + }); +}); diff --git a/src/utils/numberSelector.test.js b/src/utils/numberSelector.test.js index 4ee3269..20d67c3 100644 --- a/src/utils/numberSelector.test.js +++ b/src/utils/numberSelector.test.js @@ -16,7 +16,7 @@ describe("number selector", () => { }); it("shows the prompt", async () => { - await numberSelector.showNumberSelector(0, prompt, false); + await numberSelector.show(0, prompt, false); expect(mockUiService.askPrompt).toHaveBeenCalledWith(prompt); }); @@ -24,7 +24,7 @@ describe("number selector", () => { it("returns a number given valid input", async () => { mockUiService.askPrompt.mockResolvedValue("123"); - const number = await numberSelector.showNumberSelector(0, prompt, false); + const number = await numberSelector.show(0, prompt, false); expect(number).toEqual(123); }); @@ -34,7 +34,7 @@ describe("number selector", () => { mockUiService.askPrompt.mockResolvedValue("what?!"); - const number = await numberSelector.showNumberSelector( + const number = await numberSelector.show( currentValue, prompt, false, @@ -45,7 +45,7 @@ describe("number selector", () => { async function run(input) { mockUiService.askPrompt.mockResolvedValue(input); - return await numberSelector.showNumberSelector(0, prompt, true); + return await numberSelector.show(0, prompt, true); } it("allows for metric postfixes (k)", async () => { From 36395b62ec86e0fd88c1a9a41b39f07014c9ec96 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 1 Apr 2025 14:33:44 +0200 Subject: [PATCH 09/59] covers configmenu with tests --- src/__mocks__/service.mocks.js | 2 +- src/__mocks__/ui.mocks.js | 4 +- src/main.js | 7 +- src/services/configService.js | 10 +- src/ui/configmenu.js | 157 ++++++++------- src/ui/configmenu.test.js | 319 ++++++++++++++++++++----------- src/ui/mainmenu.test.js | 11 +- src/utils/menuLoop.js | 28 +-- src/utils/menuLoop.test.js | 66 +++---- src/utils/numberSelector.js | 6 +- src/utils/numberSelector.test.js | 6 +- src/utils/pathSelector.js | 10 +- 12 files changed, 378 insertions(+), 248 deletions(-) diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 09600e3..f0c0158 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -5,7 +5,7 @@ export const mockUiService = { showInfoMessage: vi.fn(), showErrorMessage: vi.fn(), askMultipleChoice: vi.fn(), - askPrompt: vi.fn() + askPrompt: vi.fn(), }; export const mockConfigService = { diff --git a/src/__mocks__/ui.mocks.js b/src/__mocks__/ui.mocks.js index ad74c7d..1b0b0e2 100644 --- a/src/__mocks__/ui.mocks.js +++ b/src/__mocks__/ui.mocks.js @@ -1,9 +1,9 @@ import { vi } from "vitest"; export const mockInstallMenu = { - show: vi.fn() + show: vi.fn(), }; export const mockConfigMenu = { - show: vi.fn() + show: vi.fn(), }; diff --git a/src/main.js b/src/main.js index c15aadf..ef0bde8 100644 --- a/src/main.js +++ b/src/main.js @@ -99,7 +99,12 @@ export async function main() { const pathSelector = new PathSelector(uiService, fsService); const numberSelector = new NumberSelector(uiService); const installMenu = new InstallMenu(uiService, configService); - const configMenu = new ConfigMenu(uiService, configService, pathSelector, numberSelector) + const configMenu = new ConfigMenu( + uiService, + configService, + pathSelector, + numberSelector, + ); const mainMenu = new MainMenu(uiService, installMenu, configMenu); await mainMenu.show(); diff --git a/src/services/configService.js b/src/services/configService.js index 7a2539b..038235d 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -28,7 +28,7 @@ export class ConfigService { get = () => { return this.config; - } + }; loadConfig = () => { const filePath = this.getConfigFilename(); @@ -44,7 +44,7 @@ export class ConfigService { ); throw error; } - } + }; saveConfig = () => { const filePath = this.getConfigFilename(); @@ -56,9 +56,9 @@ export class ConfigService { ); throw error; } - } - + }; + getConfigFilename = () => { return path.join(getAppDataDir(), "config.json"); - } + }; } diff --git a/src/ui/configmenu.js b/src/ui/configmenu.js index 1d55340..ce50aa6 100644 --- a/src/ui/configmenu.js +++ b/src/ui/configmenu.js @@ -1,54 +1,66 @@ export class ConfigMenu { - constructor(uiService, configService, pathSelector, numberSelector) { + constructor( + uiService, + menuLoop, + configService, + pathSelector, + numberSelector, + ) { this.ui = uiService; + this.loop = menuLoop; this.configService = configService; this.pathSelector = pathSelector; this.numberSelector = numberSelector; + + this.loop.initialize(this.showConfigMenu); } - show = async() => { - this.running = true; + show = async () => { this.config = this.configService.get(); - while (this.running) { - this.ui.showInfoMessage("Codex Configuration"); - await this.ui.askMultipleChoice("Select to edit:",[ - { - label: `Data path = "${this.config.dataDir}"`, - action: this.editDataDir, - }, - { - label: `Logs path = "${this.config.logsDir}"`, - action: this.editLogsDir, - }, - { - label: `Storage quota = ${this.bytesAmountToString(this.config.storageQuota)}`, - action: this.editStorageQuota, - }, - { - label: `Discovery port = ${this.config.ports.discPort}`, - action: this.editDiscPort, - }, - { - label: `P2P listen port = ${this.config.ports.listenPort}`, - action: this.editListenPort, - }, - { - label: `API port = ${this.config.ports.apiPort}`, - action: this.editApiPort, - }, - { - label: "Save changes and exit", - action: this.saveChangesAndExit, - }, - { - label: "Discard changes and exit", - action: this.discardChangesAndExit, - } - ] - ) - } - } - + this.ui.showInfoMessage("Codex Configuration"); + await this.loop.showLoop(); + }; + + showConfigMenu = async () => { + await this.ui.askMultipleChoice("Select to edit:", [ + { + label: `Data path = "${this.config.dataDir}"`, + action: this.editDataDir, + }, + { + label: `Logs path = "${this.config.logsDir}"`, + action: this.editLogsDir, + }, + { + label: `Storage quota = ${this.bytesAmountToString(this.config.storageQuota)}`, + action: this.editStorageQuota, + }, + { + label: `Discovery port = ${this.config.ports.discPort}`, + action: this.editDiscPort, + }, + { + label: `P2P listen port = ${this.config.ports.listenPort}`, + action: this.editListenPort, + }, + { + label: `API port = ${this.config.ports.apiPort}`, + action: this.editApiPort, + }, + { + label: "Save changes and exit", + action: this.saveChangesAndExit, + }, + { + label: "Discard changes and exit", + action: this.discardChangesAndExit, + }, + ]); + }; + + // this and the byte-format handling in + // numberSelector should be extracted to + // their own util. bytesAmountToString = (numBytes) => { const units = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; @@ -61,18 +73,16 @@ export class ConfigMenu { if (index == 0) return `${numBytes} Bytes`; return `${numBytes} Bytes (${value} ${units[index]})`; - } + }; editDataDir = async () => { // todo // function updateDataDir(config, newDataDir) { // if (config.dataDir == newDataDir) return config; - // // The Codex dataDir is a little strange: // // If the old one is empty: The new one should not exist, so that codex creates it // // with the correct security permissions. // // If the old one does exist: We move it. - // if (isDir(config.dataDir)) { // console.log( // showInfoMessage( @@ -81,7 +91,6 @@ export class ConfigMenu { // `To: "${newDataDir}"`, // ), // ); - // try { // fs.moveSync(config.dataDir, newDataDir); // } catch (error) { @@ -104,64 +113,82 @@ export class ConfigMenu { // ); // } // } - // config.dataDir = newDataDir; // return config; // } - } + }; editLogsDir = async () => { - this.config.logsDir = await this.pathSelector.show(this.config.logsDir, true); - } + this.config.logsDir = await this.pathSelector.show( + this.config.logsDir, + true, + ); + }; editStorageQuota = async () => { this.ui.showInfoMessage("You can use: 'GB' or 'gb', etc."); - const newQuota = await this.numberSelector.show(this.config.storageQuota, "Storage quota", true); + const newQuota = await this.numberSelector.show( + this.config.storageQuota, + "Storage quota", + true, + ); if (newQuota < 100 * 1024 * 1024) { this.ui.showErrorMessage("Storage quote should be >= 100mb."); } else { this.config.storageQuota = newQuota; } - } + }; editDiscPort = async () => { - const newPort = await this.numberSelector.show(this.config.ports.discPort, "Discovery port", false); + const newPort = await this.numberSelector.show( + this.config.ports.discPort, + "Discovery port", + false, + ); if (this.isInPortRange(newPort)) { this.config.ports.discPort = newPort; } - } + }; editListenPort = async () => { - const newPort = await this.numberSelector.show(this.config.ports.listenPort, "P2P listen port", false); + const newPort = await this.numberSelector.show( + this.config.ports.listenPort, + "P2P listen port", + false, + ); if (this.isInPortRange(newPort)) { this.config.ports.listenPort = newPort; } - } + }; editApiPort = async () => { - const newPort = await this.numberSelector.show(this.config.ports.apiPort, "API port", false); + const newPort = await this.numberSelector.show( + this.config.ports.apiPort, + "API port", + false, + ); if (this.isInPortRange(newPort)) { this.config.ports.apiPort = newPort; } - } + }; isInPortRange = (number) => { if (number < 1024 || number > 65535) { this.ui.showErrorMessage("Port should be between 1024 and 65535."); return false; - } + } return true; - } + }; saveChangesAndExit = async () => { this.configService.saveConfig(); this.ui.showInfoMessage("Configuration changes saved."); - this.running = false; - } + this.loop.stopLoop(); + }; discardChangesAndExit = async () => { this.configService.loadConfig(); this.ui.showInfoMessage("Changes discarded."); - this.running = false; - } + this.loop.stopLoop(); + }; } diff --git a/src/ui/configmenu.test.js b/src/ui/configmenu.test.js index 8887c81..1135bab 100644 --- a/src/ui/configmenu.test.js +++ b/src/ui/configmenu.test.js @@ -2,143 +2,242 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { ConfigMenu } from "./configmenu.js"; import { mockUiService } from "../__mocks__/service.mocks.js"; import { mockConfigService } from "../__mocks__/service.mocks.js"; -import { mockPathSelector, mockNumberSelector } from "../__mocks__/ui.mocks.js"; +import { + mockPathSelector, + mockNumberSelector, + mockMenuLoop, +} from "../__mocks__/utils.mocks.js"; describe("ConfigMenu", () => { + const config = { + dataDir: "/data", + logsDir: "/logs", + storageQuota: 1024 * 1024 * 1024, + ports: { + discPort: 8090, + listenPort: 8070, + apiPort: 8080, + }, + }; let configMenu; beforeEach(() => { vi.resetAllMocks(); - mockConfigService.get.mockReturnValue({ - dataDir: "/data", - logsDir: "/logs", - storageQuota: 1024 * 1024 * 1024, - ports: { - discPort: 8090, - listenPort: 8070, - apiPort: 8080, - } - }); + mockConfigService.get.mockReturnValue(config); configMenu = new ConfigMenu( mockUiService, + mockMenuLoop, mockConfigService, mockPathSelector, - mockNumberSelector + mockNumberSelector, ); }); - // it("displays the configuration menu", async () => { - // configMenu.running = false; // Prevent infinite loop - // await configMenu.show(); + it("initializes the loop with the config menu", () => { + expect(mockMenuLoop.initialize).toHaveBeenCalledWith( + configMenu.showConfigMenu, + ); + }); - // expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( - // "Codex Configuration", - // ); - // expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith([ - // { - // label: `Data path = "${mockConfigService.get().dataDir}"`, - // action: configMenu.editDataDir, - // }, - // { - // label: `Logs path = "${mockConfigService.get().logsDir}"`, - // action: configMenu.editLogsDir, - // }, - // { - // label: `Storage quota = 1Gb`, - // action: configMenu.editStorageQuota, - // }, - // { - // label: `Discovery port = ${mockConfigService.get().ports.discPort}`, - // action: configMenu.editDiscPort, - // }, - // { - // label: `P2P listen port = ${mockConfigService.get().ports.listenPort}`, - // action: configMenu.editListenPort, - // }, - // { - // label: `API port = ${mockConfigService.get().ports.apiPort}`, - // action: configMenu.editApiPort, - // }, - // { - // label: "Save changes and exit", - // action: configMenu.saveChangesAndExit, - // }, - // { - // label: "Discard changes and exit", - // action: configMenu.discardChangesAndExit, - // } - // ]); - // }); + it("shows the config menu header", async () => { + await configMenu.show(); + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "Codex Configuration", + ); + }); -// it("edits the logs directory", async () => { -// mockPathSelector.show.mockResolvedValue("/new-logs"); -// await configMenu.editLogsDir(); + it("starts the menu loop", async () => { + await configMenu.show(); + expect(mockMenuLoop.showLoop).toHaveBeenCalled(); + }); -// expect(mockPathSelector.show).toHaveBeenCalledWith("/logs", true); -// expect(configMenu.config.logsDir).toEqual("/new-logs"); -// }); + it("sets the config field", async () => { + await configMenu.show(); + expect(configMenu.config).toEqual(config); + }); -// it("edits the storage quota", async () => { -// mockNumberSelector.show.mockResolvedValue(200 * 1024 * 1024); -// await configMenu.editStorageQuota(); + describe("config menu options", () => { + beforeEach(() => { + configMenu.config = config; + }); -// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( -// "You can use: 'GB' or 'gb', etc.", -// ); -// expect(mockNumberSelector.show).toHaveBeenCalledWith( -// 1024 * 1024 * 1024, -// "Storage quota", -// true, -// ); -// expect(configMenu.config.storageQuota).toEqual(200 * 1024 * 1024); -// }); + it("displays the configuration menu", async () => { + await configMenu.showConfigMenu(); + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Select to edit:", + [ + { + label: `Data path = "${mockConfigService.get().dataDir}"`, + action: configMenu.editDataDir, + }, + { + label: `Logs path = "${mockConfigService.get().logsDir}"`, + action: configMenu.editLogsDir, + }, + { + label: `Storage quota = 1073741824 Bytes (1024 MB)`, + action: configMenu.editStorageQuota, + }, + { + label: `Discovery port = ${mockConfigService.get().ports.discPort}`, + action: configMenu.editDiscPort, + }, + { + label: `P2P listen port = ${mockConfigService.get().ports.listenPort}`, + action: configMenu.editListenPort, + }, + { + label: `API port = ${mockConfigService.get().ports.apiPort}`, + action: configMenu.editApiPort, + }, + { + label: "Save changes and exit", + action: configMenu.saveChangesAndExit, + }, + { + label: "Discard changes and exit", + action: configMenu.discardChangesAndExit, + }, + ], + ); + }); -// it("shows an error if storage quota is too small", async () => { -// mockNumberSelector.show.mockResolvedValue(50 * 1024 * 1024); -// await configMenu.editStorageQuota(); + it("edits the logs directory", async () => { + const originalPath = config.logsDir; + mockPathSelector.show.mockResolvedValue("/new-logs"); + await configMenu.editLogsDir(); -// expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( -// "Storage quote should be >= 100mb.", -// ); -// expect(configMenu.config.storageQuota).toEqual(1024 * 1024 * 1024); // Unchanged -// }); + expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, true); + expect(configMenu.config.logsDir).toEqual("/new-logs"); + }); -// it("edits the discovery port", async () => { -// mockNumberSelector.show.mockResolvedValue(9000); -// await configMenu.editDiscPort(); + it("edits the storage quota", async () => { + const originalQuota = config.storageQuota; + const newQuota = 200 * 1024 * 1024; + mockNumberSelector.show.mockResolvedValue(newQuota); -// expect(mockNumberSelector.show).toHaveBeenCalledWith(8090, "Discovery port", false); -// expect(configMenu.config.ports.discPort).toEqual(9000); -// }); + await configMenu.editStorageQuota(); -// it("shows an error if port is out of range", async () => { -// mockNumberSelector.show.mockResolvedValue(1000); -// await configMenu.editDiscPort(); + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "You can use: 'GB' or 'gb', etc.", + ); + expect(mockNumberSelector.show).toHaveBeenCalledWith( + originalQuota, + "Storage quota", + true, + ); + expect(configMenu.config.storageQuota).toEqual(newQuota); + }); -// expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( -// "Port should be between 1024 and 65535.", -// ); -// expect(configMenu.config.ports.discPort).toEqual(8090); // Unchanged -// }); + it("shows an error if storage quota is too small", async () => { + const originalQuota = config.storageQuota; + mockNumberSelector.show.mockResolvedValue(50 * 1024 * 1024); -// it("saves changes and exits", async () => { -// await configMenu.saveChangesAndExit(); + await configMenu.editStorageQuota(); -// expect(mockConfigService.saveConfig).toHaveBeenCalled(); -// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( -// "Configuration changes saved.", -// ); -// expect(configMenu.running).toEqual(false); -// }); + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + "Storage quote should be >= 100mb.", + ); + expect(configMenu.config.storageQuota).toEqual(originalQuota); + }); -// it("discards changes and exits", async () => { -// await configMenu.discardChangesAndExit(); + it("edits the discovery port", async () => { + const originalPort = config.ports.discPort; + const newPort = 9000; + mockNumberSelector.show.mockResolvedValue(newPort); -// expect(mockConfigService.loadConfig).toHaveBeenCalled(); -// expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( -// "Changes discarded.", -// ); -// expect(configMenu.running).toEqual(false); -// }); + await configMenu.editDiscPort(); + + expect(mockNumberSelector.show).toHaveBeenCalledWith( + originalPort, + "Discovery port", + false, + ); + expect(configMenu.config.ports.discPort).toEqual(newPort); + }); + + it("shows an error if discovery port is out of range", async () => { + const originalPort = config.ports.discPort; + mockNumberSelector.show.mockResolvedValue(1000); + await configMenu.editDiscPort(); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + "Port should be between 1024 and 65535.", + ); + expect(configMenu.config.ports.discPort).toEqual(originalPort); + }); + + it("edits the listen port", async () => { + const originalPort = config.ports.listenPort; + const newPort = 9000; + mockNumberSelector.show.mockResolvedValue(newPort); + + await configMenu.editListenPort(); + + expect(mockNumberSelector.show).toHaveBeenCalledWith( + originalPort, + "P2P listen port", + false, + ); + expect(configMenu.config.ports.listenPort).toEqual(newPort); + }); + + it("shows an error if listen port is out of range", async () => { + const originalPort = config.ports.listenPort; + mockNumberSelector.show.mockResolvedValue(1000); + await configMenu.editListenPort(); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + "Port should be between 1024 and 65535.", + ); + expect(configMenu.config.ports.listenPort).toEqual(originalPort); + }); + + it("edits the API port", async () => { + const originalPort = config.ports.apiPort; + const newPort = 9000; + mockNumberSelector.show.mockResolvedValue(newPort); + + await configMenu.editApiPort(); + + expect(mockNumberSelector.show).toHaveBeenCalledWith( + originalPort, + "API port", + false, + ); + expect(configMenu.config.ports.apiPort).toEqual(newPort); + }); + + it("shows an error if API port is out of range", async () => { + const originalPort = config.ports.apiPort; + mockNumberSelector.show.mockResolvedValue(1000); + await configMenu.editApiPort(); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + "Port should be between 1024 and 65535.", + ); + expect(configMenu.config.ports.apiPort).toEqual(originalPort); + }); + + it("saves changes and exits", async () => { + await configMenu.saveChangesAndExit(); + + expect(mockConfigService.saveConfig).toHaveBeenCalled(); + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "Configuration changes saved.", + ); + expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); + }); + + it("discards changes and exits", async () => { + await configMenu.discardChangesAndExit(); + + expect(mockConfigService.loadConfig).toHaveBeenCalled(); + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "Changes discarded.", + ); + expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); + }); + }); }); diff --git a/src/ui/mainmenu.test.js b/src/ui/mainmenu.test.js index de28c54..116792f 100644 --- a/src/ui/mainmenu.test.js +++ b/src/ui/mainmenu.test.js @@ -10,11 +10,18 @@ describe("mainmenu", () => { beforeEach(() => { vi.resetAllMocks(); - mainmenu = new MainMenu(mockUiService, mockMenuLoop, mockInstallMenu, mockConfigMenu); + mainmenu = new MainMenu( + mockUiService, + mockMenuLoop, + mockInstallMenu, + mockConfigMenu, + ); }); it("initializes the menu loop with the promptMainMenu function", () => { - expect(mockMenuLoop.initialize).toHaveBeenCalledWith(mainmenu.promptMainMenu); + expect(mockMenuLoop.initialize).toHaveBeenCalledWith( + mainmenu.promptMainMenu, + ); }); it("shows the logo", async () => { diff --git a/src/utils/menuLoop.js b/src/utils/menuLoop.js index a914ac7..638429e 100644 --- a/src/utils/menuLoop.js +++ b/src/utils/menuLoop.js @@ -1,20 +1,20 @@ export class MenuLoop { - initialize = (menuPrompt) => { - this.menuPrompt = menuPrompt; - } + initialize = (menuPrompt) => { + this.menuPrompt = menuPrompt; + }; - showOnce = async () => { - await this.menuPrompt(); - } + showOnce = async () => { + await this.menuPrompt(); + }; - showLoop = async () => { - this.running = true; - while (this.running) { - await this.menuPrompt(); - } + showLoop = async () => { + this.running = true; + while (this.running) { + await this.menuPrompt(); } + }; - stopLoop = () => { - this.running = false; - } + stopLoop = () => { + this.running = false; + }; } diff --git a/src/utils/menuLoop.test.js b/src/utils/menuLoop.test.js index 756f2b1..e03e194 100644 --- a/src/utils/menuLoop.test.js +++ b/src/utils/menuLoop.test.js @@ -2,41 +2,41 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { MenuLoop } from "./menuLoop.js"; describe("MenuLoop", () => { - let menuLoop; - const mockPrompt = vi.fn(); + let menuLoop; + const mockPrompt = vi.fn(); - beforeEach(() => { - vi.resetAllMocks(); - menuLoop = new MenuLoop(); - menuLoop.initialize(mockPrompt); + beforeEach(() => { + vi.resetAllMocks(); + menuLoop = new MenuLoop(); + menuLoop.initialize(mockPrompt); + }); + + it("can show menu once", async () => { + await menuLoop.showOnce(); + expect(mockPrompt).toHaveBeenCalledTimes(1); + }); + + it("can stop the menu loop", async () => { + mockPrompt.mockImplementation(() => { + menuLoop.stopLoop(); + }); + await menuLoop.showLoop(); + + expect(mockPrompt).toHaveBeenCalledTimes(1); + expect(menuLoop.running).toBe(false); + }); + + it("can run menu in a loop", async () => { + let calls = 0; + mockPrompt.mockImplementation(() => { + calls++; + if (calls >= 3) { + menuLoop.stopLoop(); + } }); - it("can show menu once", async () => { - await menuLoop.showOnce(); - expect(mockPrompt).toHaveBeenCalledTimes(1); - }); + await menuLoop.showLoop(); - it("can stop the menu loop", async () => { - mockPrompt.mockImplementation(() => { - menuLoop.stopLoop(); - }); - await menuLoop.showLoop(); - - expect(mockPrompt).toHaveBeenCalledTimes(1); - expect(menuLoop.running).toBe(false); - }); - - it("can run menu in a loop", async () => { - let calls = 0; - mockPrompt.mockImplementation(() => { - calls++; - if (calls >= 3) { - menuLoop.stopLoop(); - } - }); - - await menuLoop.showLoop(); - - expect(mockPrompt).toHaveBeenCalledTimes(3); - }); + expect(mockPrompt).toHaveBeenCalledTimes(3); + }); }); diff --git a/src/utils/numberSelector.js b/src/utils/numberSelector.js index d790a0b..1accc04 100644 --- a/src/utils/numberSelector.js +++ b/src/utils/numberSelector.js @@ -3,11 +3,7 @@ export class NumberSelector { this.uiService = uiService; } - show = async ( - currentValue, - promptMessage, - allowMetricPostfixes, - ) => { + show = async (currentValue, promptMessage, allowMetricPostfixes) => { try { var valueStr = await this.promptForValueStr(promptMessage); valueStr = valueStr.replaceAll(" ", ""); diff --git a/src/utils/numberSelector.test.js b/src/utils/numberSelector.test.js index 20d67c3..166641c 100644 --- a/src/utils/numberSelector.test.js +++ b/src/utils/numberSelector.test.js @@ -34,11 +34,7 @@ describe("number selector", () => { mockUiService.askPrompt.mockResolvedValue("what?!"); - const number = await numberSelector.show( - currentValue, - prompt, - false, - ); + const number = await numberSelector.show(currentValue, prompt, false); expect(number).toEqual(currentValue); }); diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index a0de9f4..84304d9 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -106,14 +106,14 @@ export class PathSelector { console.log("The path does not exist."); } this.updateCurrentIfValidParts(this.splitPath(newFullPath)); - } + }; updateCurrentIfValidParts = (newParts) => { if (!this.hasValidRoot(newParts)) { console.log("The path has no valid root."); } this.currentPath = newParts; - } + }; enterPath = async () => { const newPath = await this.ui.askPrompt("Enter Path:"); @@ -156,8 +156,8 @@ export class PathSelector { action: () => { selected = option; }, - }) - }) + }); + }); await this.ui.askMultipleChoice("Select an subdir", uiOptions); @@ -174,7 +174,7 @@ export class PathSelector { selectThisPath = async () => { this.resultingPath = this.combine(this.currentPath); this.running = false; - } + }; cancel = async () => { this.resultingPath = this.startingPath; From 939bf03b0883cf914fb5a2e7f68503d5cdceeaab Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 1 Apr 2025 14:36:25 +0200 Subject: [PATCH 10/59] link up menuloops --- src/main.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index ef0bde8..c3a798b 100644 --- a/src/main.js +++ b/src/main.js @@ -29,6 +29,7 @@ import { InstallMenu } from "./ui/installmenu.js"; import { ConfigMenu } from "./ui/configmenu.js"; import { PathSelector } from "./utils/pathSelector.js"; import { NumberSelector } from "./utils/numberSelector.js"; +import { MenuLoop } from "./utils/menuLoop.js"; async function showNavigationMenu() { console.log("\n"); @@ -101,11 +102,12 @@ export async function main() { const installMenu = new InstallMenu(uiService, configService); const configMenu = new ConfigMenu( uiService, + new MenuLoop(), configService, pathSelector, numberSelector, ); - const mainMenu = new MainMenu(uiService, installMenu, configMenu); + const mainMenu = new MainMenu(uiService, new MenuLoop(), installMenu, configMenu); await mainMenu.show(); return; From 02f5cd02446767b211e382b27590e428d048eef8 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 2 Apr 2025 10:15:32 +0200 Subject: [PATCH 11/59] fixes pathSelector --- src/main.js | 9 +++- src/utils/pathSelector.js | 102 ++++++++++++++++++++------------------ 2 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/main.js b/src/main.js index c3a798b..a87bda0 100644 --- a/src/main.js +++ b/src/main.js @@ -97,7 +97,7 @@ export async function main() { const configService = new ConfigService(); const uiService = new UiService(); const fsService = new FsService(); - const pathSelector = new PathSelector(uiService, fsService); + const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService); const numberSelector = new NumberSelector(uiService); const installMenu = new InstallMenu(uiService, configService); const configMenu = new ConfigMenu( @@ -107,7 +107,12 @@ export async function main() { pathSelector, numberSelector, ); - const mainMenu = new MainMenu(uiService, new MenuLoop(), installMenu, configMenu); + const mainMenu = new MainMenu( + uiService, + new MenuLoop(), + installMenu, + configMenu, + ); await mainMenu.show(); return; diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index 84304d9..b8a8096 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -1,13 +1,14 @@ export class PathSelector { - constructor(uiService, fsService) { + constructor(uiService, menuLoop, fsService) { this.ui = uiService; + this.loop = menuLoop; this.fs = fsService; this.pathMustExist = true; + this.loop.initialize(this.showPathSelector); } show = async (startingPath, pathMustExist) => { - this.running = true; this.startingPath = startingPath; this.pathMustExist = pathMustExist; this.roots = this.fs.getAvailableRoots(); @@ -15,39 +16,42 @@ export class PathSelector { if (!this.hasValidRoot(this.currentPath)) { this.currentPath = [roots[0]]; } - while (this.running) { - this.showCurrent(); - this.ui.askMultiChoice("Select an option:", [ - { - label: "Enter path", - action: this.enterPath, - }, - { - label: "Go up one", - action: this.upOne, - }, - { - label: "Go down one", - action: this.downOne, - }, - { - label: "Create new folder here", - action: this.createSubDir, - }, - { - label: "Select this path", - action: this.selectThisPath, - }, - { - label: "Cancel", - action: this.cancel, - }, - ]); - } + + await this.loop.showLoop(); return this.resultingPath; }; + showPathSelector = async () => { + this.showCurrent(); + await this.ui.askMultipleChoice("Select an option:", [ + { + label: "Enter path", + action: this.enterPath, + }, + { + label: "Go up one", + action: this.upOne, + }, + { + label: "Go down one", + action: this.downOne, + }, + { + label: "Create new folder here", + action: this.createSubDir, + }, + { + label: "Select this path", + action: this.selectThisPath, + }, + { + label: "Cancel", + action: this.cancel, + }, + ]); + }; + splitPath = (str) => { return str.replaceAll("\\", "/").split("/"); }; @@ -65,13 +69,14 @@ export class PathSelector { combine = (parts) => { const toJoin = this.dropEmptyParts(parts); if (toJoin.length == 1) return toJoin[0]; - return this.fs.pathJoin(...toJoin); + const result = this.fs.pathJoin(toJoin); + return result; }; combineWith = (parts, extra) => { const toJoin = this.dropEmptyParts(parts); - if (toJoin.length == 1) return this.fs.pathJoin(toJoin[0], extra); - return this.fs.pathJoin(...toJoin, extra); + if (toJoin.length == 1) return this.fs.pathJoin([toJoin[0], extra]); + return this.fs.pathJoin([...toJoin, extra]); }; showCurrent = () => { @@ -103,14 +108,16 @@ export class PathSelector { updateCurrentIfValidFull = (newFullPath) => { if (this.pathMustExist && !this.fs.isDir(newFullPath)) { - console.log("The path does not exist."); + this.ui.showErrorMessage("The path does not exist."); + return; } this.updateCurrentIfValidParts(this.splitPath(newFullPath)); }; updateCurrentIfValidParts = (newParts) => { if (!this.hasValidRoot(newParts)) { - console.log("The path has no valid root."); + this.ui.showErrorMessage("The path has no valid root."); + return; } this.currentPath = newParts; }; @@ -133,24 +140,19 @@ export class PathSelector { getSubDirOptions = () => { const fullPath = this.combine(this.currentPath); const entries = this.fs.readDir(fullPath); - var result = []; - entries.forEach(function (entry) { - if (this.isSubDir(entry)) { - result.push(entry); - } - }); - return result; + return entries.filter(entry => this.isSubDir(entry)); }; downOne = async () => { const options = this.getSubDirOptions(); if (options.length == 0) { - console.log("There are no subdirectories here."); + this.ui.showInfoMessage("There are no subdirectories here."); + return; } var selected = ""; var uiOptions = []; - options.foreach(function (option) { + options.forEach(function (option) { uiOptions.push({ label: option, action: () => { @@ -168,16 +170,20 @@ export class PathSelector { createSubDir = async () => { const name = await this.ui.askPrompt("Enter name:"); if (name.length < 1) return; - this.updateCurrentIfValidParts([...currentPath, name]); + const newPath = [...this.currentPath, name]; + if (this.pathMustExist) { + this.fs.makeDir(this.combine(newPath)); + } + this.updateCurrentIfValidParts(newPath); }; selectThisPath = async () => { this.resultingPath = this.combine(this.currentPath); - this.running = false; + this.loop.stopLoop(); }; cancel = async () => { this.resultingPath = this.startingPath; - this.running = false; + this.loop.stopLoop(); }; } From 4d28197a97fc0531d0fdb4b219ff61168ef36a96 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 2 Apr 2025 10:29:03 +0200 Subject: [PATCH 12/59] debugging path roots --- src/__mocks__/service.mocks.js | 8 ++ src/main.js | 4 +- src/utils/pathSelector.js | 7 +- src/utils/pathSelector.test.js | 137 +++++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 src/utils/pathSelector.test.js diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index f0c0158..06c69ae 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -13,3 +13,11 @@ export const mockConfigService = { saveConfig: vi.fn(), loadConfig: vi.fn(), }; + +export const mockFsService = { + getAvailableRoots: vi.fn(), + pathJoin: vi.fn(), + isDir: vi.fn(), + readDir: vi.fn(), + makeDir: vi.fn(), +}; diff --git a/src/main.js b/src/main.js index a87bda0..3a355dc 100644 --- a/src/main.js +++ b/src/main.js @@ -114,7 +114,9 @@ export async function main() { configMenu, ); - await mainMenu.show(); + // await mainMenu.show(); + const pathResult = await pathSelector.show(configService.get().codexPath, true); + console.log("Selected path: " + pathResult); return; try { diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index b8a8096..034ff6f 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -12,9 +12,14 @@ export class PathSelector { this.startingPath = startingPath; this.pathMustExist = pathMustExist; this.roots = this.fs.getAvailableRoots(); + console.log("Roots: " + this.roots.length); + this.roots.forEach(function (root) { + console.log("Root: " + root); + }); + this.currentPath = this.splitPath(startingPath); if (!this.hasValidRoot(this.currentPath)) { - this.currentPath = [roots[0]]; + this.currentPath = [this.roots[0]]; } await this.loop.showLoop(); diff --git a/src/utils/pathSelector.test.js b/src/utils/pathSelector.test.js new file mode 100644 index 0000000..6bf96c5 --- /dev/null +++ b/src/utils/pathSelector.test.js @@ -0,0 +1,137 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { PathSelector } from "./pathSelector.js"; +import { mockUiService, mockFsService } from "../__mocks__/service.mocks.js"; +import { mockMenuLoop } from "../__mocks__/utils.mocks.js"; + +describe("PathSelector", () => { + let pathSelector; + const mockRoots = ["/", "/home"]; + const mockStartPath = "/home/user"; + + beforeEach(() => { + vi.resetAllMocks(); + mockFsService.getAvailableRoots.mockReturnValue(mockRoots); + mockFsService.pathJoin.mockImplementation((parts) => parts.join("/")); + mockFsService.isDir.mockReturnValue(true); + mockFsService.readDir.mockReturnValue(["dir1", "dir2"]); + + pathSelector = new PathSelector(mockUiService, mockMenuLoop, mockFsService); + }); + + describe("initialization", () => { + it("initializes the menu loop", () => { + expect(mockMenuLoop.initialize).toHaveBeenCalledWith(pathSelector.showPathSelector); + }); + }); + + describe("show()", () => { + it("initializes path selection with given path", async () => { + await pathSelector.show(mockStartPath, true); + expect(mockFsService.getAvailableRoots).toHaveBeenCalled(); + expect(pathSelector.currentPath).toEqual(["home", "user"]); + }); + + it("uses first root if starting path is invalid", async () => { + await pathSelector.show("invalid/path", true); + expect(pathSelector.currentPath).toEqual([mockRoots[0]]); + }); + + it("starts the menu loop", async () => { + await pathSelector.show(mockStartPath, true); + expect(mockMenuLoop.showLoop).toHaveBeenCalled(); + }); + + it("returns the resulting path after selection", async () => { + pathSelector.resultingPath = mockStartPath; + const result = await pathSelector.show(mockStartPath, true); + expect(result).toBe(mockStartPath); + }); + }); + +// describe("path operations", () => { +// beforeEach(async () => { +// await pathSelector.show(mockStartPath, true); +// }); + +// it("splits paths correctly", () => { +// const result = pathSelector.splitPath("C:\\path\\to\\dir"); +// expect(result).toEqual(["C:", "path", "to", "dir"]); +// }); + +// it("drops empty path parts", () => { +// const result = pathSelector.dropEmptyParts(["", "path", "", "dir", ""]); +// expect(result).toEqual(["path", "dir"]); +// }); + +// it("combines path parts correctly", () => { +// const result = pathSelector.combine(["home", "user", "docs"]); +// expect(result).toBe("home/user/docs"); +// }); + +// it("handles single part paths in combine", () => { +// const result = pathSelector.combine(["root"]); +// expect(result).toBe("root"); +// }); +// }); + +// describe("navigation", () => { +// beforeEach(async () => { +// await pathSelector.show(mockStartPath, true); +// }); + +// it("moves up one directory", () => { +// pathSelector.upOne(); +// expect(pathSelector.currentPath).toEqual(["home"]); +// }); + +// it("handles down directory navigation", async () => { +// mockFsService.readDir.mockReturnValue(["subdir1", "subdir2"]); +// mockFsService.isDir.mockReturnValue(true); + +// await pathSelector.downOne(); + +// expect(mockUiService.askMultipleChoice).toHaveBeenCalled(); +// expect(mockFsService.readDir).toHaveBeenCalled(); +// }); + +// it("creates new subdirectory", async () => { +// mockUiService.askPrompt.mockResolvedValue("newdir"); +// await pathSelector.createSubDir(); + +// expect(mockFsService.makeDir).toHaveBeenCalled(); +// expect(pathSelector.currentPath).toEqual(["home", "user", "newdir"]); +// }); +// }); + +// describe("path validation", () => { +// it("validates root paths", () => { +// expect(pathSelector.hasValidRoot(["/home"])).toBe(true); +// expect(pathSelector.hasValidRoot([])).toBe(false); +// expect(pathSelector.hasValidRoot(["invalid"])).toBe(false); +// }); + +// it("validates full paths", () => { +// mockFsService.isDir.mockReturnValue(false); +// pathSelector.updateCurrentIfValidFull("/invalid/path"); +// expect(mockUiService.showErrorMessage).toHaveBeenCalled(); +// }); +// }); + +// describe("selection and cancellation", () => { +// beforeEach(async () => { +// await pathSelector.show(mockStartPath, true); +// }); + +// it("selects current path", async () => { +// await pathSelector.selectThisPath(); +// expect(pathSelector.resultingPath).toBe("home/user"); +// expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); +// }); + +// it("cancels and returns to starting path", async () => { +// await pathSelector.cancel(); +// expect(pathSelector.resultingPath).toBe(mockStartPath); +// expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); +// }); +// }); +}); From 18705581752b36fddae13a3ccbc6eb24a1a9deb9 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 2 Apr 2025 10:49:17 +0200 Subject: [PATCH 13/59] fixing caps - part 1 --- package.json | 2 +- src/main.js | 6 +++--- src/services/{codexapp.js => codexAppa.js} | 0 src/services/{fsservice.js => fsServicea.js} | 0 src/services/{uiservice.js => uiServicea.js} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename src/services/{codexapp.js => codexAppa.js} (100%) rename src/services/{fsservice.js => fsServicea.js} (100%) rename src/services/{uiservice.js => uiServicea.js} (100%) diff --git a/package.json b/package.json index a9bf1ec..602892b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "start": "node index.js", "test": "vitest run", - "test:watch": "vitest", + "watch": "vitest", "format": "prettier --write ./src" }, "keywords": [ diff --git a/src/main.js b/src/main.js index 3a355dc..ac874ea 100644 --- a/src/main.js +++ b/src/main.js @@ -20,10 +20,10 @@ import { import { runCodex, checkNodeStatus } from "./handlers/nodeHandlers.js"; import { showInfoMessage } from "./utils/messages.js"; import { ConfigService } from "./services/configService.js"; -import { openCodexApp } from "./services/codexapp.js"; +import { openCodexApp } from "./services/codexAppa.js"; -import { UiService } from "./services/uiservice.js"; -import { FsService } from "./services/fsService.js"; +import { UiService } from "./services/uiServicea.js"; +import { FsService } from "./services/fsServicea.js"; import { MainMenu } from "./ui/mainmenu.js"; import { InstallMenu } from "./ui/installmenu.js"; import { ConfigMenu } from "./ui/configmenu.js"; diff --git a/src/services/codexapp.js b/src/services/codexAppa.js similarity index 100% rename from src/services/codexapp.js rename to src/services/codexAppa.js diff --git a/src/services/fsservice.js b/src/services/fsServicea.js similarity index 100% rename from src/services/fsservice.js rename to src/services/fsServicea.js diff --git a/src/services/uiservice.js b/src/services/uiServicea.js similarity index 100% rename from src/services/uiservice.js rename to src/services/uiServicea.js From 126a136deba4b2de7547b7375e377da246f441ca Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 2 Apr 2025 10:50:46 +0200 Subject: [PATCH 14/59] fixing caps - part 2 --- src/handlers/installationHandlers.js | 2 +- src/main.js | 12 ++++++------ src/services/{codexAppa.js => codexApp.js} | 0 src/services/configService.js | 4 ++-- src/services/{fsServicea.js => fsService.js} | 0 src/services/{uiServicea.js => uiService.js} | 0 src/ui/{configmenu.js => configMenua.js} | 0 src/ui/{configmenu.test.js => configMenua.test.js} | 0 src/ui/{installmenu.js => installMenua.js} | 0 src/ui/{mainmenu.js => mainMenua.js} | 0 src/ui/{mainmenu.test.js => mainMenua.test.js} | 0 src/utils/{appdata.js => appDataa.js} | 0 12 files changed, 9 insertions(+), 9 deletions(-) rename src/services/{codexAppa.js => codexApp.js} (100%) rename src/services/{fsServicea.js => fsService.js} (100%) rename src/services/{uiServicea.js => uiService.js} (100%) rename src/ui/{configmenu.js => configMenua.js} (100%) rename src/ui/{configmenu.test.js => configMenua.test.js} (100%) rename src/ui/{installmenu.js => installMenua.js} (100%) rename src/ui/{mainmenu.js => mainMenua.js} (100%) rename src/ui/{mainmenu.test.js => mainMenua.test.js} (100%) rename src/utils/{appdata.js => appDataa.js} (100%) diff --git a/src/handlers/installationHandlers.js b/src/handlers/installationHandlers.js index a2e4d48..c6afc07 100644 --- a/src/handlers/installationHandlers.js +++ b/src/handlers/installationHandlers.js @@ -12,7 +12,7 @@ import { showSuccessMessage, } from "../utils/messages.js"; import { checkDependencies } from "../services/nodeService.js"; -import { getCodexRootPath, getCodexBinPath } from "../utils/appdata.js"; +import { getCodexRootPath, getCodexBinPath } from "../utils/appDataa.js"; const platform = os.platform(); diff --git a/src/main.js b/src/main.js index ac874ea..940e457 100644 --- a/src/main.js +++ b/src/main.js @@ -20,13 +20,13 @@ import { import { runCodex, checkNodeStatus } from "./handlers/nodeHandlers.js"; import { showInfoMessage } from "./utils/messages.js"; import { ConfigService } from "./services/configService.js"; -import { openCodexApp } from "./services/codexAppa.js"; +import { openCodexApp } from "./services/codexApp.js"; -import { UiService } from "./services/uiServicea.js"; -import { FsService } from "./services/fsServicea.js"; -import { MainMenu } from "./ui/mainmenu.js"; -import { InstallMenu } from "./ui/installmenu.js"; -import { ConfigMenu } from "./ui/configmenu.js"; +import { UiService } from "./services/uiService.js"; +import { FsService } from "./services/fsService.js"; +import { MainMenu } from "./ui/mainMenua.js"; +import { InstallMenu } from "./ui/installMenua.js"; +import { ConfigMenu } from "./ui/configMenua.js"; import { PathSelector } from "./utils/pathSelector.js"; import { NumberSelector } from "./utils/numberSelector.js"; import { MenuLoop } from "./utils/menuLoop.js"; diff --git a/src/services/codexAppa.js b/src/services/codexApp.js similarity index 100% rename from src/services/codexAppa.js rename to src/services/codexApp.js diff --git a/src/services/configService.js b/src/services/configService.js index 038235d..4d3f264 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -1,11 +1,11 @@ import fs from "fs"; import path from "path"; -import { getAppDataDir } from "../utils/appdata.js"; +import { getAppDataDir } from "../utils/appDataa.js"; import { getCodexBinPath, getCodexDataDirDefaultPath, getCodexLogsDefaultPath, -} from "../utils/appdata.js"; +} from "../utils/appDataa.js"; const defaultConfig = { codexExe: "", diff --git a/src/services/fsServicea.js b/src/services/fsService.js similarity index 100% rename from src/services/fsServicea.js rename to src/services/fsService.js diff --git a/src/services/uiServicea.js b/src/services/uiService.js similarity index 100% rename from src/services/uiServicea.js rename to src/services/uiService.js diff --git a/src/ui/configmenu.js b/src/ui/configMenua.js similarity index 100% rename from src/ui/configmenu.js rename to src/ui/configMenua.js diff --git a/src/ui/configmenu.test.js b/src/ui/configMenua.test.js similarity index 100% rename from src/ui/configmenu.test.js rename to src/ui/configMenua.test.js diff --git a/src/ui/installmenu.js b/src/ui/installMenua.js similarity index 100% rename from src/ui/installmenu.js rename to src/ui/installMenua.js diff --git a/src/ui/mainmenu.js b/src/ui/mainMenua.js similarity index 100% rename from src/ui/mainmenu.js rename to src/ui/mainMenua.js diff --git a/src/ui/mainmenu.test.js b/src/ui/mainMenua.test.js similarity index 100% rename from src/ui/mainmenu.test.js rename to src/ui/mainMenua.test.js diff --git a/src/utils/appdata.js b/src/utils/appDataa.js similarity index 100% rename from src/utils/appdata.js rename to src/utils/appDataa.js From ff0304d97e4eda18081b0fa1a3b8e65af0f26239 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 2 Apr 2025 10:51:38 +0200 Subject: [PATCH 15/59] fixing caps - part 3 --- src/handlers/installationHandlers.js | 2 +- src/main.js | 6 +++--- src/services/configService.js | 4 ++-- src/ui/{configMenua.js => configMenu.js} | 0 src/ui/{configMenua.test.js => configMenu.test.js} | 0 src/ui/{installMenua.js => installMenu.js} | 0 src/ui/{mainMenua.js => mainMenu.js} | 0 src/ui/{mainMenua.test.js => mainMenu.test.js} | 0 src/utils/{appDataa.js => appData.js} | 0 9 files changed, 6 insertions(+), 6 deletions(-) rename src/ui/{configMenua.js => configMenu.js} (100%) rename src/ui/{configMenua.test.js => configMenu.test.js} (100%) rename src/ui/{installMenua.js => installMenu.js} (100%) rename src/ui/{mainMenua.js => mainMenu.js} (100%) rename src/ui/{mainMenua.test.js => mainMenu.test.js} (100%) rename src/utils/{appDataa.js => appData.js} (100%) diff --git a/src/handlers/installationHandlers.js b/src/handlers/installationHandlers.js index c6afc07..50cc860 100644 --- a/src/handlers/installationHandlers.js +++ b/src/handlers/installationHandlers.js @@ -12,7 +12,7 @@ import { showSuccessMessage, } from "../utils/messages.js"; import { checkDependencies } from "../services/nodeService.js"; -import { getCodexRootPath, getCodexBinPath } from "../utils/appDataa.js"; +import { getCodexRootPath, getCodexBinPath } from "../utils/appData.js"; const platform = os.platform(); diff --git a/src/main.js b/src/main.js index 940e457..801d6d3 100644 --- a/src/main.js +++ b/src/main.js @@ -24,9 +24,9 @@ import { openCodexApp } from "./services/codexApp.js"; import { UiService } from "./services/uiService.js"; import { FsService } from "./services/fsService.js"; -import { MainMenu } from "./ui/mainMenua.js"; -import { InstallMenu } from "./ui/installMenua.js"; -import { ConfigMenu } from "./ui/configMenua.js"; +import { MainMenu } from "./ui/mainMenu.js"; +import { InstallMenu } from "./ui/installMenu.js"; +import { ConfigMenu } from "./ui/configMenu.js"; import { PathSelector } from "./utils/pathSelector.js"; import { NumberSelector } from "./utils/numberSelector.js"; import { MenuLoop } from "./utils/menuLoop.js"; diff --git a/src/services/configService.js b/src/services/configService.js index 4d3f264..ffe5452 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -1,11 +1,11 @@ import fs from "fs"; import path from "path"; -import { getAppDataDir } from "../utils/appDataa.js"; +import { getAppDataDir } from "../utils/appData.js"; import { getCodexBinPath, getCodexDataDirDefaultPath, getCodexLogsDefaultPath, -} from "../utils/appDataa.js"; +} from "../utils/appData.js"; const defaultConfig = { codexExe: "", diff --git a/src/ui/configMenua.js b/src/ui/configMenu.js similarity index 100% rename from src/ui/configMenua.js rename to src/ui/configMenu.js diff --git a/src/ui/configMenua.test.js b/src/ui/configMenu.test.js similarity index 100% rename from src/ui/configMenua.test.js rename to src/ui/configMenu.test.js diff --git a/src/ui/installMenua.js b/src/ui/installMenu.js similarity index 100% rename from src/ui/installMenua.js rename to src/ui/installMenu.js diff --git a/src/ui/mainMenua.js b/src/ui/mainMenu.js similarity index 100% rename from src/ui/mainMenua.js rename to src/ui/mainMenu.js diff --git a/src/ui/mainMenua.test.js b/src/ui/mainMenu.test.js similarity index 100% rename from src/ui/mainMenua.test.js rename to src/ui/mainMenu.test.js diff --git a/src/utils/appDataa.js b/src/utils/appData.js similarity index 100% rename from src/utils/appDataa.js rename to src/utils/appData.js From 5df758b4f60b91ca1ab5fcb9669caf0611ff8e0d Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 2 Apr 2025 10:53:19 +0200 Subject: [PATCH 16/59] fixing caps - part 4 --- src/ui/configMenu.test.js | 2 +- src/ui/mainMenu.test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/configMenu.test.js b/src/ui/configMenu.test.js index 1135bab..8f5c1d7 100644 --- a/src/ui/configMenu.test.js +++ b/src/ui/configMenu.test.js @@ -1,5 +1,5 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; -import { ConfigMenu } from "./configmenu.js"; +import { ConfigMenu } from "./configMenu.js"; import { mockUiService } from "../__mocks__/service.mocks.js"; import { mockConfigService } from "../__mocks__/service.mocks.js"; import { diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index 116792f..d5b0a0f 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -1,5 +1,5 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; -import { MainMenu } from "./mainmenu.js"; +import { MainMenu } from "./mainMenu.js"; import { mockUiService } from "../__mocks__/service.mocks.js"; import { mockInstallMenu, mockConfigMenu } from "../__mocks__/ui.mocks.js"; import { mockMenuLoop } from "../__mocks__/utils.mocks.js"; From 2b4fd7f456471d8f2b4e6a1778c2196857b01aa9 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 2 Apr 2025 11:25:33 +0200 Subject: [PATCH 17/59] attempting to retain root slash in unix systems --- src/utils/pathSelector.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index 034ff6f..db26dcd 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -58,7 +58,11 @@ export class PathSelector { }; splitPath = (str) => { - return str.replaceAll("\\", "/").split("/"); + var result = str.replaceAll("\\", "/").split("/"); + if (str.startsWith("/") && this.roots.includes("/")) { + result = ["/", ...result]; + } + return result; }; dropEmptyParts = (parts) => { From 3dc5a38f57b2ba36043d6fde5a66205f24a4c3b7 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 2 Apr 2025 14:45:23 +0200 Subject: [PATCH 18/59] fixes tests for pathSelector --- src/utils/pathSelector.js | 20 ++-- src/utils/pathSelector.test.js | 166 +++++++++++++++++++-------------- 2 files changed, 101 insertions(+), 85 deletions(-) diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index db26dcd..0289363 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -12,11 +12,6 @@ export class PathSelector { this.startingPath = startingPath; this.pathMustExist = pathMustExist; this.roots = this.fs.getAvailableRoots(); - console.log("Roots: " + this.roots.length); - this.roots.forEach(function (root) { - console.log("Root: " + root); - }); - this.currentPath = this.splitPath(startingPath); if (!this.hasValidRoot(this.currentPath)) { this.currentPath = [this.roots[0]]; @@ -58,7 +53,7 @@ export class PathSelector { }; splitPath = (str) => { - var result = str.replaceAll("\\", "/").split("/"); + var result = this.dropEmptyParts(str.replaceAll("\\", "/").split("/")); if (str.startsWith("/") && this.roots.includes("/")) { result = ["/", ...result]; } @@ -66,19 +61,16 @@ export class PathSelector { }; dropEmptyParts = (parts) => { - var result = []; - parts.forEach(function (part) { - if (part.length > 0) { - result.push(part); - } - }); - return result; + return parts.filter(part => part.length > 0); }; combine = (parts) => { const toJoin = this.dropEmptyParts(parts); if (toJoin.length == 1) return toJoin[0]; - const result = this.fs.pathJoin(toJoin); + var result = this.fs.pathJoin(toJoin); + if (result.startsWith("//")) { + result = result.substring(1); + } return result; }; diff --git a/src/utils/pathSelector.test.js b/src/utils/pathSelector.test.js index 6bf96c5..8c24719 100644 --- a/src/utils/pathSelector.test.js +++ b/src/utils/pathSelector.test.js @@ -28,7 +28,7 @@ describe("PathSelector", () => { it("initializes path selection with given path", async () => { await pathSelector.show(mockStartPath, true); expect(mockFsService.getAvailableRoots).toHaveBeenCalled(); - expect(pathSelector.currentPath).toEqual(["home", "user"]); + expect(pathSelector.currentPath).toEqual(["/", "home", "user"]); }); it("uses first root if starting path is invalid", async () => { @@ -48,90 +48,114 @@ describe("PathSelector", () => { }); }); -// describe("path operations", () => { -// beforeEach(async () => { -// await pathSelector.show(mockStartPath, true); -// }); + describe("path operations", () => { + beforeEach(async () => { + await pathSelector.show(mockStartPath, true); + }); -// it("splits paths correctly", () => { -// const result = pathSelector.splitPath("C:\\path\\to\\dir"); -// expect(result).toEqual(["C:", "path", "to", "dir"]); -// }); + it("splits paths correctly", () => { + const result = pathSelector.splitPath("C:\\path\\to\\dir"); + expect(result).toEqual(["C:", "path", "to", "dir"]); + }); -// it("drops empty path parts", () => { -// const result = pathSelector.dropEmptyParts(["", "path", "", "dir", ""]); -// expect(result).toEqual(["path", "dir"]); -// }); + it("drops empty path parts", () => { + const result = pathSelector.dropEmptyParts(["", "path", "", "dir", ""]); + expect(result).toEqual(["path", "dir"]); + }); -// it("combines path parts correctly", () => { -// const result = pathSelector.combine(["home", "user", "docs"]); -// expect(result).toBe("home/user/docs"); -// }); + it("combines path parts correctly", () => { + const result = pathSelector.combine(["C:", "user", "docs"]); + expect(result).toBe("C:/user/docs"); + }); -// it("handles single part paths in combine", () => { -// const result = pathSelector.combine(["root"]); -// expect(result).toBe("root"); -// }); -// }); + it("combines path including root correctly", () => { + const result = pathSelector.combine(["/", "home", "user", "docs"]); + expect(result).toBe("/home/user/docs"); + }); -// describe("navigation", () => { -// beforeEach(async () => { -// await pathSelector.show(mockStartPath, true); -// }); + it("handles single part paths in combine", () => { + const result = pathSelector.combine(["root"]); + expect(result).toBe("root"); + }); + }); -// it("moves up one directory", () => { -// pathSelector.upOne(); -// expect(pathSelector.currentPath).toEqual(["home"]); -// }); + describe("navigation", () => { + beforeEach(async () => { + await pathSelector.show(mockStartPath, true); + }); -// it("handles down directory navigation", async () => { -// mockFsService.readDir.mockReturnValue(["subdir1", "subdir2"]); -// mockFsService.isDir.mockReturnValue(true); + it("moves up one directory", () => { + pathSelector.upOne(); + expect(pathSelector.currentPath).toEqual(["/", "home"]); + }); + + it("shows down directory navigation", async () => { + mockFsService.readDir.mockReturnValue(["subdir1", "subdir2"]); + mockFsService.isDir.mockReturnValue(true); -// await pathSelector.downOne(); + await pathSelector.downOne(); -// expect(mockUiService.askMultipleChoice).toHaveBeenCalled(); -// expect(mockFsService.readDir).toHaveBeenCalled(); -// }); + expect(mockUiService.askMultipleChoice).toHaveBeenCalled(); + expect(mockFsService.readDir).toHaveBeenCalledWith(mockStartPath); + }); -// it("creates new subdirectory", async () => { -// mockUiService.askPrompt.mockResolvedValue("newdir"); -// await pathSelector.createSubDir(); + it("can navigate to a subdirectory", async () => { + const subdir = "subdir1"; + mockFsService.readDir.mockReturnValue([subdir]); + mockUiService.askMultipleChoice.mockImplementation((_, options) => { + options[0].action(); // Select the first option + }); + await pathSelector.downOne(); -// expect(mockFsService.makeDir).toHaveBeenCalled(); -// expect(pathSelector.currentPath).toEqual(["home", "user", "newdir"]); -// }); -// }); + expect(pathSelector.currentPath).toEqual(["/", "home", "user", subdir]); + }); -// describe("path validation", () => { -// it("validates root paths", () => { -// expect(pathSelector.hasValidRoot(["/home"])).toBe(true); -// expect(pathSelector.hasValidRoot([])).toBe(false); -// expect(pathSelector.hasValidRoot(["invalid"])).toBe(false); -// }); + it("creates new subdirectory", async () => { + const newDir = "newdir"; + mockUiService.askPrompt.mockResolvedValue(newDir); + await pathSelector.createSubDir(); + + expect(mockUiService.askPrompt).toHaveBeenCalledWith("Enter name:"); + expect(mockFsService.makeDir).toHaveBeenCalled(mockStartPath + "/" + newDir); + expect(pathSelector.currentPath).toEqual(["/", "home", "user", newDir]); + }); + }); -// it("validates full paths", () => { -// mockFsService.isDir.mockReturnValue(false); -// pathSelector.updateCurrentIfValidFull("/invalid/path"); -// expect(mockUiService.showErrorMessage).toHaveBeenCalled(); -// }); -// }); + describe("path validation", () => { + beforeEach(async () => { + await pathSelector.show(mockStartPath, true); + }); -// describe("selection and cancellation", () => { -// beforeEach(async () => { -// await pathSelector.show(mockStartPath, true); -// }); + it("validates root paths", () => { + expect(pathSelector.hasValidRoot(["/home"])).toBe(true); + expect(pathSelector.hasValidRoot([])).toBe(false); + expect(pathSelector.hasValidRoot(["invalid"])).toBe(false); + }); -// it("selects current path", async () => { -// await pathSelector.selectThisPath(); -// expect(pathSelector.resultingPath).toBe("home/user"); -// expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); -// }); + it("validates full paths", () => { + mockFsService.isDir.mockReturnValue(false); + pathSelector.updateCurrentIfValidFull("/invalid/path"); + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith("The path does not exist."); + }); + }); -// it("cancels and returns to starting path", async () => { -// await pathSelector.cancel(); -// expect(pathSelector.resultingPath).toBe(mockStartPath); -// expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); -// }); -// }); + describe("selection and cancellation", () => { + beforeEach(async () => { + await pathSelector.show(mockStartPath, true); + }); + + it("selects current path", async () => { + pathSelector.upOne(); + await pathSelector.selectThisPath(); + expect(pathSelector.resultingPath).toBe("/home"); + expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); + }); + + it("cancels and returns to starting path", async () => { + pathSelector.upOne(); + await pathSelector.cancel(); + expect(pathSelector.resultingPath).toBe(mockStartPath); + expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); + }); + }); }); From 8365e556a042e34d05ad6a3bf0c54cbf1c10ad10 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Wed, 2 Apr 2025 14:49:19 +0200 Subject: [PATCH 19/59] restores main.js --- src/main.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main.js b/src/main.js index 801d6d3..3bbfecb 100644 --- a/src/main.js +++ b/src/main.js @@ -114,9 +114,7 @@ export async function main() { configMenu, ); - // await mainMenu.show(); - const pathResult = await pathSelector.show(configService.get().codexPath, true); - console.log("Selected path: " + pathResult); + await mainMenu.show(); return; try { From c8d96425d88ef3082d00ad970675f1c895c7aecd Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 7 Apr 2025 16:01:49 +0200 Subject: [PATCH 20/59] wip: install menu --- src/__mocks__/utils.mocks.js | 4 ++ src/main.js | 3 +- src/services/fsService.js | 4 ++ src/ui/configMenu.js | 56 ++++++++------------------- src/ui/configMenu.test.js | 32 +++++++++++++++ src/ui/installMenu.js | 24 ++++++++---- src/ui/installMenu.test.js | 71 ++++++++++++++++++++++++++++++++++ src/utils/dataDirMover.js | 53 +++++++++++++++++++++++++ src/utils/pathSelector.js | 6 +-- src/utils/pathSelector.test.js | 20 ++++++---- 10 files changed, 214 insertions(+), 59 deletions(-) create mode 100644 src/ui/installMenu.test.js create mode 100644 src/utils/dataDirMover.js diff --git a/src/__mocks__/utils.mocks.js b/src/__mocks__/utils.mocks.js index 2274803..5b0d1b6 100644 --- a/src/__mocks__/utils.mocks.js +++ b/src/__mocks__/utils.mocks.js @@ -14,3 +14,7 @@ export const mockMenuLoop = { showLoop: vi.fn(), stopLoop: vi.fn(), }; + +export const mockDataDirMover = { + moveDataDir: vi.fn(), +}; diff --git a/src/main.js b/src/main.js index 3bbfecb..4cd4c53 100644 --- a/src/main.js +++ b/src/main.js @@ -99,7 +99,7 @@ export async function main() { const fsService = new FsService(); const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService); const numberSelector = new NumberSelector(uiService); - const installMenu = new InstallMenu(uiService, configService); + const installMenu = new InstallMenu(uiService, configService, pathSelector); const configMenu = new ConfigMenu( uiService, new MenuLoop(), @@ -112,6 +112,7 @@ export async function main() { new MenuLoop(), installMenu, configMenu, + new DataDirMover(fsService, uiService) ); await mainMenu.show(); diff --git a/src/services/fsService.js b/src/services/fsService.js index 45f22c5..645a099 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -38,4 +38,8 @@ export class FsService { makeDir = (dir) => { fs.mkdirSync(dir); }; + + moveDir = (oldPath, newPath) => { + fs.moveSync(oldPath, newPath); + }; } diff --git a/src/ui/configMenu.js b/src/ui/configMenu.js index ce50aa6..6a3b241 100644 --- a/src/ui/configMenu.js +++ b/src/ui/configMenu.js @@ -5,18 +5,21 @@ export class ConfigMenu { configService, pathSelector, numberSelector, + dataDirMover, ) { this.ui = uiService; this.loop = menuLoop; this.configService = configService; this.pathSelector = pathSelector; this.numberSelector = numberSelector; + this.dataDirMover = dataDirMover; this.loop.initialize(this.showConfigMenu); } show = async () => { this.config = this.configService.get(); + this.originalDataDir = this.config.dataDir; this.ui.showInfoMessage("Codex Configuration"); await this.loop.showLoop(); }; @@ -76,46 +79,10 @@ export class ConfigMenu { }; editDataDir = async () => { - // todo - // function updateDataDir(config, newDataDir) { - // if (config.dataDir == newDataDir) return config; - // // The Codex dataDir is a little strange: - // // If the old one is empty: The new one should not exist, so that codex creates it - // // with the correct security permissions. - // // If the old one does exist: We move it. - // if (isDir(config.dataDir)) { - // console.log( - // showInfoMessage( - // "Moving Codex data folder...\n" + - // `From: "${config.dataDir}"\n` + - // `To: "${newDataDir}"`, - // ), - // ); - // try { - // fs.moveSync(config.dataDir, newDataDir); - // } catch (error) { - // console.log( - // showErrorMessage("Error while moving dataDir: " + error.message), - // ); - // throw error; - // } - // } else { - // // Old data dir does not exist. - // if (isDir(newDataDir)) { - // console.log( - // showInfoMessage( - // "Warning: the selected data path already exists.\n" + - // `New data path = "${newDataDir}"\n` + - // "Codex may overwrite data in this folder.\n" + - // "Codex will fail to start if this folder does not have the required\n" + - // "security permissions.", - // ), - // ); - // } - // } - // config.dataDir = newDataDir; - // return config; - // } + this.config.dataDir = await this.pathSelector.show( + this.config.dataDir, + false, + ); }; editLogsDir = async () => { @@ -181,6 +148,15 @@ export class ConfigMenu { }; saveChangesAndExit = async () => { + if (this.config.dataDir !== this.originalDataDir) { + // The Codex data-dir is a little special. + // Use a dedicated module to move it. + await this.dataDirMover.moveDataDir( + this.originalDataDir, + this.config.dataDir, + ); + } + this.configService.saveConfig(); this.ui.showInfoMessage("Configuration changes saved."); this.loop.stopLoop(); diff --git a/src/ui/configMenu.test.js b/src/ui/configMenu.test.js index 8f5c1d7..00d0a80 100644 --- a/src/ui/configMenu.test.js +++ b/src/ui/configMenu.test.js @@ -6,6 +6,7 @@ import { mockPathSelector, mockNumberSelector, mockMenuLoop, + mockDataDirMover, } from "../__mocks__/utils.mocks.js"; describe("ConfigMenu", () => { @@ -31,6 +32,7 @@ describe("ConfigMenu", () => { mockConfigService, mockPathSelector, mockNumberSelector, + mockDataDirMover, ); }); @@ -57,6 +59,11 @@ describe("ConfigMenu", () => { expect(configMenu.config).toEqual(config); }); + it("sets the original datadir field", async () => { + await configMenu.show(); + expect(configMenu.originalDataDir).toEqual(config.dataDir); + }); + describe("config menu options", () => { beforeEach(() => { configMenu.config = config; @@ -103,6 +110,15 @@ describe("ConfigMenu", () => { ); }); + it("edits the logs directory", async () => { + const originalPath = config.dataDir; + mockPathSelector.show.mockResolvedValue("/new-data"); + await configMenu.editDataDir(); + + expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, false); + expect(configMenu.config.dataDir).toEqual("/new-data"); + }); + it("edits the logs directory", async () => { const originalPath = config.logsDir; mockPathSelector.show.mockResolvedValue("/new-logs"); @@ -219,8 +235,11 @@ describe("ConfigMenu", () => { ); expect(configMenu.config.ports.apiPort).toEqual(originalPort); }); + }); + describe("save and discard changes", () => { it("saves changes and exits", async () => { + await configMenu.show(); await configMenu.saveChangesAndExit(); expect(mockConfigService.saveConfig).toHaveBeenCalled(); @@ -230,6 +249,19 @@ describe("ConfigMenu", () => { expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); }); + it("calls the dataDirMover when the new datadir is not equal to the original dataDir when saving changes", async () => { + config.dataDir = "/original-data"; + await configMenu.show(); + + configMenu.config.dataDir = "/new-data"; + await configMenu.saveChangesAndExit(); + + expect(mockDataDirMover.moveDataDir).toHaveBeenCalledWith( + configMenu.originalDataDir, + configMenu.config.dataDir, + ); + }); + it("discards changes and exits", async () => { await configMenu.discardChangesAndExit(); diff --git a/src/ui/installMenu.js b/src/ui/installMenu.js index f00c620..b42eb82 100644 --- a/src/ui/installMenu.js +++ b/src/ui/installMenu.js @@ -1,16 +1,16 @@ export class InstallMenu { - constructor(uiService, configService) { + constructor(uiService, configService, pathSelector) { this.ui = uiService; + this.configService = configService; this.config = configService.get(); + this.pathSelector = pathSelector; } show = async () => { await this.ui.askMultipleChoice("Configure your Codex installation", [ { label: "Install path: " + this.config.codexPath, - action: async function () { - console.log("run path selector"); - }, + action: this.selectInstallPath, }, { label: "Storage provider module: Disabled (todo)", @@ -22,17 +22,25 @@ export class InstallMenu { }, { label: "Cancel", - action: async function () {}, + action: this.doNothing, }, ]); }; + selectInstallPath = async () => { + this.config.codexPath = await this.pathSelector.show( + this.config.codexPath, + false, + ); + this.configService.saveConfig(); + }; + storageProviderOption = async () => { this.ui.showInfoMessage("This option is not currently available."); await this.show(); }; - performInstall = async () => { - console.log("todo"); - }; + performInstall = async () => {}; + + doNothing = async () => {}; } diff --git a/src/ui/installMenu.test.js b/src/ui/installMenu.test.js new file mode 100644 index 0000000..1a0c7c9 --- /dev/null +++ b/src/ui/installMenu.test.js @@ -0,0 +1,71 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { InstallMenu } from "./installMenu.js"; +import { mockUiService } from "../__mocks__/service.mocks.js"; +import { mockConfigService } from "../__mocks__/service.mocks.js"; +import { mockPathSelector } from "../__mocks__/utils.mocks.js"; + +describe("InstallMenu", () => { + const config = { + codexPath: "/codex", + }; + let installMenu; + + beforeEach(() => { + vi.resetAllMocks(); + mockConfigService.get.mockReturnValue(config); + + installMenu = new InstallMenu( + mockUiService, + mockConfigService, + mockPathSelector, + ); + }); + + it("displays the install menu", async () => { + await installMenu.show(); + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Configure your Codex installation", + [ + { + label: "Install path: " + config.codexPath, + action: installMenu.selectInstallPath, + }, + { + label: "Storage provider module: Disabled (todo)", + action: installMenu.storageProviderOption, + }, + { + label: "Install!", + action: installMenu.performInstall, + }, + { + label: "Cancel", + action: installMenu.doNothing, + }, + ], + ); + }); + + it("allows selecting the install path", async () => { + const originalPath = config.codexPath; + const newPath = "/new/path"; + mockPathSelector.show.mockResolvedValue(newPath); + + await installMenu.selectInstallPath(); + + expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, false); + expect(config.codexPath).toBe(newPath); + expect(mockConfigService.saveConfig).toHaveBeenCalled(); + }); + + it("shows storage provider option is unavailable", async () => { + const showMock = vi.fn(); + installMenu.show = showMock; + + await installMenu.storageProviderOption(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "This option is not currently available.", + ); + }); +}); diff --git a/src/utils/dataDirMover.js b/src/utils/dataDirMover.js new file mode 100644 index 0000000..9fcc827 --- /dev/null +++ b/src/utils/dataDirMover.js @@ -0,0 +1,53 @@ +export class DataDirMover { + constructor(fsService, uiService) { + this.fs = fsService; + this.ui = uiService; + } + + moveDataDir = (oldPath, newPath) => { + if (oldPath === newPath) return; + + // The Codex dataDir is a little strange: + // If the old one is empty: The new one should not exist, so that codex creates it with the correct security permissions. + // If the old one does exist: We move it. + + if (this.fs.isDir(oldPath)) { + this.moveDir(oldPath, newPath); + } else { + this.ensureDoesNotExist(newPath); + } + }; + + moveDir = (oldPath, newPath) => { + this.ui.showInfoMessage( + "Moving Codex data folder...\n" + + `From: "${oldPath}"\n` + + `To: "${newPath}"`, + ); + + try { + this.fs.moveDir(oldPath, newPath); + } catch (error) { + console.log( + this.ui.showErrorMessage( + "Error while moving dataDir: " + error.message, + ), + ); + throw error; + } + }; + + ensureDoesNotExist = (path) => { + if (this.fs.isDir(path)) { + console.log( + this.ui.showInfoMessage( + "Warning: the selected data path already exists.\n" + + `New data path = "${path}"\n` + + "Codex may overwrite data in this folder.\n" + + "Codex will fail to start if this folder does not have the required\n" + + "security permissions.", + ), + ); + } + }; +} diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index 0289363..af39af6 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -55,13 +55,13 @@ export class PathSelector { splitPath = (str) => { var result = this.dropEmptyParts(str.replaceAll("\\", "/").split("/")); if (str.startsWith("/") && this.roots.includes("/")) { - result = ["/", ...result]; + result = ["/", ...result]; } return result; }; dropEmptyParts = (parts) => { - return parts.filter(part => part.length > 0); + return parts.filter((part) => part.length > 0); }; combine = (parts) => { @@ -141,7 +141,7 @@ export class PathSelector { getSubDirOptions = () => { const fullPath = this.combine(this.currentPath); const entries = this.fs.readDir(fullPath); - return entries.filter(entry => this.isSubDir(entry)); + return entries.filter((entry) => this.isSubDir(entry)); }; downOne = async () => { diff --git a/src/utils/pathSelector.test.js b/src/utils/pathSelector.test.js index 8c24719..16d5439 100644 --- a/src/utils/pathSelector.test.js +++ b/src/utils/pathSelector.test.js @@ -20,7 +20,9 @@ describe("PathSelector", () => { describe("initialization", () => { it("initializes the menu loop", () => { - expect(mockMenuLoop.initialize).toHaveBeenCalledWith(pathSelector.showPathSelector); + expect(mockMenuLoop.initialize).toHaveBeenCalledWith( + pathSelector.showPathSelector, + ); }); }); @@ -92,9 +94,9 @@ describe("PathSelector", () => { it("shows down directory navigation", async () => { mockFsService.readDir.mockReturnValue(["subdir1", "subdir2"]); mockFsService.isDir.mockReturnValue(true); - + await pathSelector.downOne(); - + expect(mockUiService.askMultipleChoice).toHaveBeenCalled(); expect(mockFsService.readDir).toHaveBeenCalledWith(mockStartPath); }); @@ -106,7 +108,7 @@ describe("PathSelector", () => { options[0].action(); // Select the first option }); await pathSelector.downOne(); - + expect(pathSelector.currentPath).toEqual(["/", "home", "user", subdir]); }); @@ -114,9 +116,11 @@ describe("PathSelector", () => { const newDir = "newdir"; mockUiService.askPrompt.mockResolvedValue(newDir); await pathSelector.createSubDir(); - + expect(mockUiService.askPrompt).toHaveBeenCalledWith("Enter name:"); - expect(mockFsService.makeDir).toHaveBeenCalled(mockStartPath + "/" + newDir); + expect(mockFsService.makeDir).toHaveBeenCalled( + mockStartPath + "/" + newDir, + ); expect(pathSelector.currentPath).toEqual(["/", "home", "user", newDir]); }); }); @@ -135,7 +139,9 @@ describe("PathSelector", () => { it("validates full paths", () => { mockFsService.isDir.mockReturnValue(false); pathSelector.updateCurrentIfValidFull("/invalid/path"); - expect(mockUiService.showErrorMessage).toHaveBeenCalledWith("The path does not exist."); + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + "The path does not exist.", + ); }); }); From 10e1a33cc4b8479b8a0778ec39275a9227f1d0b3 Mon Sep 17 00:00:00 2001 From: thatben Date: Tue, 8 Apr 2025 07:16:34 +0200 Subject: [PATCH 21/59] wip --- src/handlers/installer.js | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/handlers/installer.js diff --git a/src/handlers/installer.js b/src/handlers/installer.js new file mode 100644 index 0000000..57ba9bd --- /dev/null +++ b/src/handlers/installer.js @@ -0,0 +1,9 @@ +export class Installer { + constructor(configService) { + this.configService = configService; + } + + isCodexInstalled = async () => { + return false; + } +} \ No newline at end of file From 574d4febe2e8a811e98a314ae904b9260ffad939 Mon Sep 17 00:00:00 2001 From: thatben Date: Tue, 8 Apr 2025 15:13:24 +0200 Subject: [PATCH 22/59] Implements and adds tests for installer --- src/__mocks__/service.mocks.js | 12 + src/handlers/installationHandlers.js | 8 - src/handlers/installer.js | 129 +++++++++- src/handlers/installer.test.js | 372 +++++++++++++++++++++++++++ src/main.js | 2 +- src/services/fsService.js | 8 + src/services/osService.js | 23 ++ src/services/shellService.js | 18 ++ src/ui/installMenu.js | 6 +- src/ui/installMenu.test.js | 8 +- src/utils/command.js | 14 - 11 files changed, 566 insertions(+), 34 deletions(-) create mode 100644 src/handlers/installer.test.js create mode 100644 src/services/osService.js create mode 100644 src/services/shellService.js delete mode 100644 src/utils/command.js diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 06c69ae..5b735f2 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -18,6 +18,18 @@ export const mockFsService = { getAvailableRoots: vi.fn(), pathJoin: vi.fn(), isDir: vi.fn(), + isFile: vi.fn(), readDir: vi.fn(), makeDir: vi.fn(), }; + +export const mockShellService = { + run: vi.fn(), +}; + +export const mockOsService = { + isWindows: vi.fn(), + isDarwin: vi.fn(), + isLinux: vi.fn(), + getWorkingDir: vi.fn(), +}; diff --git a/src/handlers/installationHandlers.js b/src/handlers/installationHandlers.js index 50cc860..d026c5c 100644 --- a/src/handlers/installationHandlers.js +++ b/src/handlers/installationHandlers.js @@ -135,14 +135,6 @@ async function performInstall(config) { try { if (platform === "win32") { try { - try { - await runCommand("curl --version"); - } catch (error) { - throw new Error( - "curl is not available. Please install curl or update your Windows version.", - ); - } - await runCommand( "curl -LO --ssl-no-revoke https://get.codex.storage/install.cmd", ); diff --git a/src/handlers/installer.js b/src/handlers/installer.js index 57ba9bd..9fb80a9 100644 --- a/src/handlers/installer.js +++ b/src/handlers/installer.js @@ -1,9 +1,130 @@ export class Installer { - constructor(configService) { + constructor(configService, shellService, osService, fsService) { + this.config = configService.get(); this.configService = configService; + this.shell = shellService; + this.os = osService; + this.fs = fsService; } isCodexInstalled = async () => { - return false; - } -} \ No newline at end of file + try { + await this.getCodexVersion(); + return true; + } catch (error) { + return false; + } + }; + + getCodexVersion = async () => { + if (this.config.codexExe.length < 1) + throw new Error("Codex not installed."); + const version = await this.shell.run(`"${this.config.codexExe}" --version`); + if (version.length < 1) throw new Error("Version info not found."); + return version; + }; + + installCodex = async (processCallbacks) => { + if (!(await this.arePrerequisitesCorrect(processCallbacks))) return; + + processCallbacks.installStarts(); + if (this.os.isWindows()) { + await this.installCodexWindows(processCallbacks); + } else { + await this.installCodexUnix(processCallbacks); + } + + if (!(await this.isCodexInstalled())) + throw new Error("Codex installation failed."); + processCallbacks.installSuccessful(); + }; + + arePrerequisitesCorrect = async (processCallbacks) => { + if (await this.isCodexInstalled()) { + processCallbacks.warn("Codex is already installed."); + return false; + } + if (this.config.codexInstallPath.length < 1) { + processCallbacks.warn("Install path not set."); + return false; + } + if (!(await this.isCurlAvailable())) { + processCallbacks.warn("Curl is not available."); + return false; + } + return true; + }; + + isCurlAvailable = async () => { + const curlVersion = await this.shell.run("curl --version"); + return curlVersion.length > 0; + }; + + installCodexWindows = async (processCallbacks) => { + await this.shell.run( + "curl -LO --ssl-no-revoke https://get.codex.storage/install.cmd", + ); + processCallbacks.downloadSuccessful(); + await this.shell.run( + `set "INSTALL_DIR=${this.config.codexInstallPath}" && ` + + `"${this.os.getWorkingDir()}\\install.cmd"`, + ); + await this.saveCodexInstallPath("codex.exe"); + await this.shell.run("del /f install.cmd"); + }; + + installCodexUnix = async (processCallbacks) => { + await this.ensureUnixDependencies(); + await this.shell.run( + "curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh", + ); + processCallbacks.downloadSuccessful(); + + if (this.os.isDarwin()) { + await this.runInstallerDarwin(); + } else { + await this.runInstallerLinux(); + } + + await this.saveCodexInstallPath("codex"); + await this.shell.run("rm -f install.sh"); + }; + + runInstallerDarwin = async () => { + const timeoutCommand = `perl -e ' + eval { + local $SIG{ALRM} = sub { die "timeout\\n" }; + alarm(120); + system("INSTALL_DIR=\\"${this.config.codexInstallPath}\\" bash install.sh"); + alarm(0); + }; + die if $@; +'`; + await this.shell.run(timeoutCommand); + }; + + runInstallerLinux = async () => { + await this.shell.run( + `INSTALL_DIR="${this.config.codexInstallPath}" timeout 120 bash install.sh`, + ); + }; + + ensureUnixDependencies = async (processCallbacks) => { + const libgompCheck = await this.shell.run("ldconfig -p | grep libgomp"); + if (libgompCheck.length < 1) { + processCallbacks.warn("libgomp not found."); + return false; + } + return true; + }; + + saveCodexInstallPath = async (codexExe) => { + this.config.codexExe = this.fs.pathJoin([ + this.config.codexInstallPath, + codexExe, + ]); + if (!this.fs.isFile(this.config.codexExe)) + throw new Error("Codex executable not found."); + await this.configService.saveConfig(); + }; +} diff --git a/src/handlers/installer.test.js b/src/handlers/installer.test.js new file mode 100644 index 0000000..4b011d9 --- /dev/null +++ b/src/handlers/installer.test.js @@ -0,0 +1,372 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { + mockShellService, + mockOsService, + mockFsService, +} from "../__mocks__/service.mocks.js"; +import { mockConfigService } from "../__mocks__/service.mocks.js"; +import { Installer } from "./installer.js"; + +describe("Installer", () => { + const config = { + codexInstallPath: "/install-codex", + }; + const workingDir = "/working-dir"; + const processCallbacks = { + installStarts: vi.fn(), + downloadSuccessful: vi.fn(), + installSuccessful: vi.fn(), + warn: vi.fn(), + }; + let installer; + + beforeEach(() => { + vi.resetAllMocks(); + mockConfigService.get.mockReturnValue(config); + mockOsService.getWorkingDir.mockReturnValue(workingDir); + + installer = new Installer( + mockConfigService, + mockShellService, + mockOsService, + mockFsService, + ); + }); + + describe("getCodexVersion", () => { + it("throws when codex exe is not set", async () => { + config.codexExe = ""; + await expect(installer.getCodexVersion()).rejects.toThrow( + "Codex not installed.", + ); + }); + + it("throws when version info is not found", async () => { + config.codexExe = "codex.exe"; + mockShellService.run.mockResolvedValueOnce(""); + await expect(installer.getCodexVersion()).rejects.toThrow( + "Version info not found.", + ); + }); + + it("returns version info", async () => { + const versionInfo = "versionInfo"; + config.codexExe = "codex.exe"; + mockShellService.run.mockResolvedValueOnce(versionInfo); + const version = await installer.getCodexVersion(); + expect(version).toBe(versionInfo); + }); + }); + + describe("isCodexInstalled", () => { + it("return true when getCodexVersion succeeds", async () => { + installer.getCodexVersion = vi.fn(); + expect(await installer.isCodexInstalled()).toBe(true); + }); + + it("returns false when getCodexVersion fails", async () => { + installer.getCodexVersion = vi.fn(() => { + throw new Error("Codex not installed."); + }); + expect(await installer.isCodexInstalled()).toBe(false); + }); + }); + + describe("installCodex", () => { + beforeEach(() => { + installer.arePrerequisitesCorrect = vi.fn(); + installer.installCodexWindows = vi.fn(); + installer.installCodexUnix = vi.fn(); + installer.isCodexInstalled = vi.fn(); + }); + + it("returns early when prerequisites are not correct", async () => { + installer.arePrerequisitesCorrect.mockResolvedValue(false); + await installer.installCodex(processCallbacks); + expect(processCallbacks.installStarts).not.toHaveBeenCalled(); + expect(processCallbacks.installSuccessful).not.toHaveBeenCalled(); + expect(processCallbacks.downloadSuccessful).not.toHaveBeenCalled(); + expect(installer.isCodexInstalled).not.toHaveBeenCalled(); + expect(installer.installCodexWindows).not.toHaveBeenCalled(); + expect(installer.installCodexUnix).not.toHaveBeenCalled(); + }); + + describe("prerequisites OK", () => { + beforeEach(() => { + installer.arePrerequisitesCorrect.mockResolvedValue(true); + installer.isCodexInstalled.mockResolvedValue(true); + }); + + it("calls installStarts when prerequisites are correct", async () => { + await installer.installCodex(processCallbacks); + expect(processCallbacks.installStarts).toHaveBeenCalled(); + }); + + it("calls installCodexWindows when OS is Windows", async () => { + mockOsService.isWindows.mockReturnValue(true); + await installer.installCodex(processCallbacks); + expect(installer.installCodexWindows).toHaveBeenCalledWith( + processCallbacks, + ); + }); + + it("calls installCodexUnix when OS is not Windows", async () => { + mockOsService.isWindows.mockReturnValue(false); + await installer.installCodex(processCallbacks); + expect(installer.installCodexUnix).toHaveBeenCalledWith( + processCallbacks, + ); + }); + + it("throws when codex is not installed after installation", async () => { + installer.isCodexInstalled.mockResolvedValue(false); + await expect(installer.installCodex(processCallbacks)).rejects.toThrow( + "Codex installation failed.", + ); + }); + + it("calls installSuccessful when installation is successful", async () => { + await installer.installCodex(processCallbacks); + expect(processCallbacks.installSuccessful).toHaveBeenCalled(); + }); + }); + }); + + describe("arePrerequisitesCorrect", () => { + beforeEach(() => { + installer.isCodexInstalled = vi.fn(); + installer.isCurlAvailable = vi.fn(); + config.codexInstallPath = "/install-codex"; + }); + + it("returns false when codex is already installed", async () => { + installer.isCodexInstalled.mockResolvedValue(true); + expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe( + false, + ); + expect(processCallbacks.warn).toHaveBeenCalledWith( + "Codex is already installed.", + ); + }); + + it("returns false when install path is not set", async () => { + config.codexInstallPath = ""; + expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe( + false, + ); + expect(processCallbacks.warn).toHaveBeenCalledWith( + "Install path not set.", + ); + }); + + it("returns false when curl is not available", async () => { + installer.isCodexInstalled.mockResolvedValue(false); + installer.isCurlAvailable.mockResolvedValue(false); + expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe( + false, + ); + expect(processCallbacks.warn).toHaveBeenCalledWith( + "Curl is not available.", + ); + }); + + it("returns true when all prerequisites are correct", async () => { + installer.isCodexInstalled.mockResolvedValue(false); + installer.isCurlAvailable.mockResolvedValue(true); + const result = await installer.arePrerequisitesCorrect(processCallbacks); + expect(result).toBe(true); + }); + }); + + describe("isCurlAvailable", () => { + it("returns true when curl version is found", async () => { + mockShellService.run.mockResolvedValueOnce("curl version"); + const result = await installer.isCurlAvailable(); + expect(mockShellService.run).toHaveBeenCalledWith("curl --version"); + expect(result).toBe(true); + }); + + it("returns false when curl version is not found", async () => { + mockShellService.run.mockResolvedValueOnce(""); + const result = await installer.isCurlAvailable(); + expect(mockShellService.run).toHaveBeenCalledWith("curl --version"); + expect(result).toBe(false); + }); + }); + + describe("install functions", () => { + beforeEach(() => { + installer.saveCodexInstallPath = vi.fn(); + }); + + describe("installCodexWindows", () => { + it("runs the curl command to download the installer", async () => { + await installer.installCodexWindows(processCallbacks); + expect(mockShellService.run).toHaveBeenCalledWith( + "curl -LO --ssl-no-revoke https://get.codex.storage/install.cmd", + ); + }); + + it("calls downloadSuccessful", async () => { + await installer.installCodexWindows(processCallbacks); + expect(processCallbacks.downloadSuccessful).toHaveBeenCalled(); + }); + + it("runs installer script", async () => { + await installer.installCodexWindows(processCallbacks); + expect(mockShellService.run).toHaveBeenCalledWith( + `set "INSTALL_DIR=${config.codexInstallPath}" && "${workingDir}\\install.cmd"`, + ); + }); + + it("saves the codex install path", async () => { + await installer.installCodexWindows(processCallbacks); + expect(installer.saveCodexInstallPath).toHaveBeenCalledWith( + "codex.exe", + ); + }); + + it("deletes the installer script", async () => { + await installer.installCodexWindows(processCallbacks); + expect(mockShellService.run).toHaveBeenCalledWith("del /f install.cmd"); + }); + }); + + describe("installCodexUnix", () => { + beforeEach(() => { + installer.ensureUnixDependencies = vi.fn(); + installer.runInstallerDarwin = vi.fn(); + installer.runInstallerLinux = vi.fn(); + }); + + it("ensures Unix dependencies", async () => { + await installer.installCodexUnix(processCallbacks); + expect(installer.ensureUnixDependencies).toHaveBeenCalled(); + }); + + it("runs the curl command to download the installer", async () => { + await installer.installCodexUnix(processCallbacks); + expect(mockShellService.run).toHaveBeenCalledWith( + "curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh", + ); + }); + + it("calls downloadSuccessful", async () => { + await installer.installCodexUnix(processCallbacks); + expect(processCallbacks.downloadSuccessful).toHaveBeenCalled(); + }); + + it("runs installer for darwin ", async () => { + mockOsService.isDarwin.mockReturnValue(true); + await installer.installCodexUnix(processCallbacks); + expect(installer.runInstallerDarwin).toHaveBeenCalled(); + }); + + it("runs installer for linux", async () => { + mockOsService.isDarwin.mockReturnValue(false); + await installer.installCodexUnix(processCallbacks); + expect(installer.runInstallerLinux).toHaveBeenCalled(); + }); + + it("saves the codex install path", async () => { + await installer.installCodexUnix(processCallbacks); + expect(installer.saveCodexInstallPath).toHaveBeenCalledWith("codex"); + }); + + it("deletes the installer script", async () => { + await installer.installCodexUnix(processCallbacks); + expect(mockShellService.run).toHaveBeenCalledWith("rm -f install.sh"); + }); + }); + + describe("runInstallerDarwin", () => { + it("runs the installer script for darwin with custom timeout command", async () => { + const timeoutCommand = `perl -e ' + eval { + local $SIG{ALRM} = sub { die "timeout\\n" }; + alarm(120); + system("INSTALL_DIR=\\"${config.codexInstallPath}\\" bash install.sh"); + alarm(0); + }; + die if $@; +'`; + await installer.runInstallerDarwin(); + expect(mockShellService.run).toHaveBeenCalledWith(timeoutCommand); + }); + }); + + describe("runInstallerLinux", () => { + it("runs the installer script using unix timeout command", async () => { + await installer.runInstallerLinux(); + expect(mockShellService.run).toHaveBeenCalledWith( + `INSTALL_DIR="${config.codexInstallPath}" timeout 120 bash install.sh`, + ); + }); + }); + }); + + describe("ensureUnixDependencies", () => { + it("returns true when libgomp is installed", async () => { + mockShellService.run.mockResolvedValueOnce("yes"); + expect(await installer.ensureUnixDependencies(processCallbacks)).toBe( + true, + ); + expect(mockShellService.run).toHaveBeenCalledWith( + "ldconfig -p | grep libgomp", + ); + }); + + it("returns false when libgomp is not found", async () => { + mockShellService.run.mockResolvedValue(""); + expect(await installer.ensureUnixDependencies(processCallbacks)).toBe( + false, + ); + expect(mockShellService.run).toHaveBeenCalledWith( + "ldconfig -p | grep libgomp", + ); + }); + + it("it calls warn in processCallbacks when libgomp is not found", async () => { + mockShellService.run.mockResolvedValue(""); + await installer.ensureUnixDependencies(processCallbacks); + expect(processCallbacks.warn).toHaveBeenCalledWith("libgomp not found."); + }); + }); + + describe("saveCodexInstallPath", () => { + const codexExe = "_codex_.exe"; + const pathJointResult = "/path-to-codex/_codex_.exe"; + + beforeEach(() => { + mockFsService.pathJoin.mockReturnValue(pathJointResult); + }); + + it("combines the install path with the exe", async () => { + mockFsService.isFile.mockReturnValue(true); + await installer.saveCodexInstallPath(codexExe); + expect(mockFsService.pathJoin).toHaveBeenCalledWith([ + config.codexInstallPath, + codexExe, + ]); + }); + + it("sets the codex exe path", async () => { + mockFsService.isFile.mockReturnValue(true); + await installer.saveCodexInstallPath(codexExe); + expect(config.codexExe).toBe(pathJointResult); + }); + + it("throws when file does not exist", async () => { + mockFsService.isFile.mockReturnValue(false); + await expect(installer.saveCodexInstallPath(codexExe)).rejects.toThrow( + "Codex executable not found.", + ); + }); + + it("saves the config", async () => { + mockFsService.isFile.mockReturnValue(true); + await installer.saveCodexInstallPath(codexExe); + expect(mockConfigService.saveConfig).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/main.js b/src/main.js index 4cd4c53..060fa86 100644 --- a/src/main.js +++ b/src/main.js @@ -112,7 +112,7 @@ export async function main() { new MenuLoop(), installMenu, configMenu, - new DataDirMover(fsService, uiService) + new DataDirMover(fsService, uiService), ); await mainMenu.show(); diff --git a/src/services/fsService.js b/src/services/fsService.js index 645a099..8590652 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -31,6 +31,14 @@ export class FsService { } }; + isFile = (path) => { + try { + return fs.lstatSync(path).isFile(); + } catch { + return false; + } + }; + readDir = (dir) => { return fs.readdirSync(dir); }; diff --git a/src/services/osService.js b/src/services/osService.js new file mode 100644 index 0000000..3ec8f05 --- /dev/null +++ b/src/services/osService.js @@ -0,0 +1,23 @@ +import os from "os"; + +export class OsService { + constructor() { + this.platform = os.platform(); + } + + isWindows = () => { + return this.platform === "win32"; + }; + + isDarwin = () => { + return this.platform === "darwin"; + }; + + isLinux = () => { + return this.platform === "linux"; + }; + + getWorkingDir = () => { + return process.cwd(); + }; +} diff --git a/src/services/shellService.js b/src/services/shellService.js new file mode 100644 index 0000000..df9f63e --- /dev/null +++ b/src/services/shellService.js @@ -0,0 +1,18 @@ +import { exec } from "child_process"; +import { promisify } from "util"; + +export class ShellService { + constructor() { + this.execAsync = promisify(exec); + } + + async run(command) { + try { + const { stdout, stderr } = await this.execAsync(command); + return stdout; + } catch (error) { + console.error("Error:", error.message); + throw error; + } + } +} diff --git a/src/ui/installMenu.js b/src/ui/installMenu.js index b42eb82..4c4b50d 100644 --- a/src/ui/installMenu.js +++ b/src/ui/installMenu.js @@ -9,7 +9,7 @@ export class InstallMenu { show = async () => { await this.ui.askMultipleChoice("Configure your Codex installation", [ { - label: "Install path: " + this.config.codexPath, + label: "Install path: " + this.config.codexInstallPath, action: this.selectInstallPath, }, { @@ -28,8 +28,8 @@ export class InstallMenu { }; selectInstallPath = async () => { - this.config.codexPath = await this.pathSelector.show( - this.config.codexPath, + this.config.codexInstallPath = await this.pathSelector.show( + this.config.codexInstallPath, false, ); this.configService.saveConfig(); diff --git a/src/ui/installMenu.test.js b/src/ui/installMenu.test.js index 1a0c7c9..2e15e89 100644 --- a/src/ui/installMenu.test.js +++ b/src/ui/installMenu.test.js @@ -6,7 +6,7 @@ import { mockPathSelector } from "../__mocks__/utils.mocks.js"; describe("InstallMenu", () => { const config = { - codexPath: "/codex", + codexInstallPath: "/codex", }; let installMenu; @@ -27,7 +27,7 @@ describe("InstallMenu", () => { "Configure your Codex installation", [ { - label: "Install path: " + config.codexPath, + label: "Install path: " + config.codexInstallPath, action: installMenu.selectInstallPath, }, { @@ -47,14 +47,14 @@ describe("InstallMenu", () => { }); it("allows selecting the install path", async () => { - const originalPath = config.codexPath; + const originalPath = config.codexInstallPath; const newPath = "/new/path"; mockPathSelector.show.mockResolvedValue(newPath); await installMenu.selectInstallPath(); expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, false); - expect(config.codexPath).toBe(newPath); + expect(config.codexInstallPath).toBe(newPath); expect(mockConfigService.saveConfig).toHaveBeenCalled(); }); diff --git a/src/utils/command.js b/src/utils/command.js deleted file mode 100644 index 9ef5c91..0000000 --- a/src/utils/command.js +++ /dev/null @@ -1,14 +0,0 @@ -import { exec } from "child_process"; -import { promisify } from "util"; - -export const execAsync = promisify(exec); - -export async function runCommand(command) { - try { - const { stdout, stderr } = await execAsync(command); - return stdout; - } catch (error) { - console.error("Error:", error.message); - throw error; - } -} From 7fa7820a53ebb6b02427efb054ad7bf55bcdc87d Mon Sep 17 00:00:00 2001 From: thatben Date: Tue, 8 Apr 2025 15:27:13 +0200 Subject: [PATCH 23/59] debug pathselector for non-existing paths --- src/main.js | 1 + src/services/configService.js | 2 +- src/utils/command.js | 14 ++++++++++++++ src/utils/pathSelector.js | 9 +++++++-- src/utils/pathSelector.test.js | 10 ++++++++++ 5 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 src/utils/command.js diff --git a/src/main.js b/src/main.js index 060fa86..4525c20 100644 --- a/src/main.js +++ b/src/main.js @@ -30,6 +30,7 @@ import { ConfigMenu } from "./ui/configMenu.js"; import { PathSelector } from "./utils/pathSelector.js"; import { NumberSelector } from "./utils/numberSelector.js"; import { MenuLoop } from "./utils/menuLoop.js"; +import { DataDirMover } from "./utils/dataDirMover.js"; async function showNavigationMenu() { console.log("\n"); diff --git a/src/services/configService.js b/src/services/configService.js index ffe5452..3b4b3b2 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -10,7 +10,7 @@ import { const defaultConfig = { codexExe: "", // User-selected config options: - codexPath: getCodexBinPath(), + codexInstallPath: getCodexBinPath(), dataDir: getCodexDataDirDefaultPath(), logsDir: getCodexLogsDefaultPath(), storageQuota: 8 * 1024 * 1024 * 1024, diff --git a/src/utils/command.js b/src/utils/command.js new file mode 100644 index 0000000..9ef5c91 --- /dev/null +++ b/src/utils/command.js @@ -0,0 +1,14 @@ +import { exec } from "child_process"; +import { promisify } from "util"; + +export const execAsync = promisify(exec); + +export async function runCommand(command) { + try { + const { stdout, stderr } = await execAsync(command); + return stdout; + } catch (error) { + console.error("Error:", error.message); + throw error; + } +} diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index af39af6..f75eba9 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -140,8 +140,13 @@ export class PathSelector { getSubDirOptions = () => { const fullPath = this.combine(this.currentPath); - const entries = this.fs.readDir(fullPath); - return entries.filter((entry) => this.isSubDir(entry)); + try { + const entries = this.fs.readDir(fullPath); + return entries.filter((entry) => this.isSubDir(entry)); + } + catch { + return []; + } }; downOne = async () => { diff --git a/src/utils/pathSelector.test.js b/src/utils/pathSelector.test.js index 16d5439..1cfebb1 100644 --- a/src/utils/pathSelector.test.js +++ b/src/utils/pathSelector.test.js @@ -101,6 +101,16 @@ describe("PathSelector", () => { expect(mockFsService.readDir).toHaveBeenCalledWith(mockStartPath); }); + it("handles non-existing paths", async () => { + mockFsService.readDir.mockImplementationOnce(() => { throw new Error("A!"); }); + + await pathSelector.downOne(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "There are no subdirectories here." + ); + }); + it("can navigate to a subdirectory", async () => { const subdir = "subdir1"; mockFsService.readDir.mockReturnValue([subdir]); From 7730176ecc3a3820c3588dd788dd60a74e79c6ac Mon Sep 17 00:00:00 2001 From: thatben Date: Wed, 9 Apr 2025 09:31:22 +0200 Subject: [PATCH 24/59] aaa --- src/handlers/installer.js | 2 +- src/handlers/installer.test.js | 70 +++++++++++++++++++++------------- src/main.js | 8 +++- src/services/uiService.js | 15 ++++++++ src/ui/installMenu.js | 49 +++++++++++++++++++++++- src/ui/mainMenu.js | 2 +- src/ui/mainMenu.test.js | 2 +- 7 files changed, 115 insertions(+), 33 deletions(-) diff --git a/src/handlers/installer.js b/src/handlers/installer.js index 9fb80a9..740e8b7 100644 --- a/src/handlers/installer.js +++ b/src/handlers/installer.js @@ -74,7 +74,7 @@ export class Installer { }; installCodexUnix = async (processCallbacks) => { - await this.ensureUnixDependencies(); + if (!await this.ensureUnixDependencies(processCallbacks)) return; await this.shell.run( "curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh", ); diff --git a/src/handlers/installer.test.js b/src/handlers/installer.test.js index 4b011d9..72cd207 100644 --- a/src/handlers/installer.test.js +++ b/src/handlers/installer.test.js @@ -239,43 +239,59 @@ describe("Installer", () => { installer.runInstallerLinux = vi.fn(); }); - it("ensures Unix dependencies", async () => { + it("ensures unix dependencies", async () => { await installer.installCodexUnix(processCallbacks); - expect(installer.ensureUnixDependencies).toHaveBeenCalled(); + expect(installer.ensureUnixDependencies).toHaveBeenCalled(processCallbacks); }); - it("runs the curl command to download the installer", async () => { + it("returns early if unix dependencies are not met", async () => { + installer.ensureUnixDependencies.mockResolvedValue(false); + await installer.installCodexUnix(processCallbacks); - expect(mockShellService.run).toHaveBeenCalledWith( - "curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh", - ); + + expect(processCallbacks.downloadSuccessful).not.toHaveBeenCalled(); + expect(installer.runInstallerDarwin).not.toHaveBeenCalled(); + expect(installer.runInstallerLinux).not.toHaveBeenCalled(); }); - it("calls downloadSuccessful", async () => { - await installer.installCodexUnix(processCallbacks); - expect(processCallbacks.downloadSuccessful).toHaveBeenCalled(); - }); + describe("when dependencies are met", () => { + beforeEach(() =>{ + installer.ensureUnixDependencies.mockResolvedValue(true); + }) - it("runs installer for darwin ", async () => { - mockOsService.isDarwin.mockReturnValue(true); - await installer.installCodexUnix(processCallbacks); - expect(installer.runInstallerDarwin).toHaveBeenCalled(); - }); + it("runs the curl command to download the installer", async () => { + await installer.installCodexUnix(processCallbacks); + expect(mockShellService.run).toHaveBeenCalledWith( + "curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh", + ); + }); - it("runs installer for linux", async () => { - mockOsService.isDarwin.mockReturnValue(false); - await installer.installCodexUnix(processCallbacks); - expect(installer.runInstallerLinux).toHaveBeenCalled(); - }); + it("calls downloadSuccessful", async () => { + await installer.installCodexUnix(processCallbacks); + expect(processCallbacks.downloadSuccessful).toHaveBeenCalled(); + }); - it("saves the codex install path", async () => { - await installer.installCodexUnix(processCallbacks); - expect(installer.saveCodexInstallPath).toHaveBeenCalledWith("codex"); - }); + it("runs installer for darwin ", async () => { + mockOsService.isDarwin.mockReturnValue(true); + await installer.installCodexUnix(processCallbacks); + expect(installer.runInstallerDarwin).toHaveBeenCalled(); + }); - it("deletes the installer script", async () => { - await installer.installCodexUnix(processCallbacks); - expect(mockShellService.run).toHaveBeenCalledWith("rm -f install.sh"); + it("runs installer for linux", async () => { + mockOsService.isDarwin.mockReturnValue(false); + await installer.installCodexUnix(processCallbacks); + expect(installer.runInstallerLinux).toHaveBeenCalled(); + }); + + it("saves the codex install path", async () => { + await installer.installCodexUnix(processCallbacks); + expect(installer.saveCodexInstallPath).toHaveBeenCalledWith("codex"); + }); + + it("deletes the installer script", async () => { + await installer.installCodexUnix(processCallbacks); + expect(mockShellService.run).toHaveBeenCalledWith("rm -f install.sh"); + }); }); }); diff --git a/src/main.js b/src/main.js index 4525c20..c3240ea 100644 --- a/src/main.js +++ b/src/main.js @@ -31,6 +31,9 @@ import { PathSelector } from "./utils/pathSelector.js"; import { NumberSelector } from "./utils/numberSelector.js"; import { MenuLoop } from "./utils/menuLoop.js"; import { DataDirMover } from "./utils/dataDirMover.js"; +import { Installer } from "./handlers/installer.js"; +import { ShellService } from "./services/shellService.js"; +import { OsService } from "./services/osService.js"; async function showNavigationMenu() { console.log("\n"); @@ -100,7 +103,10 @@ export async function main() { const fsService = new FsService(); const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService); const numberSelector = new NumberSelector(uiService); - const installMenu = new InstallMenu(uiService, configService, pathSelector); + const shellService = new ShellService(); + const osService = new OsService(); + const installer = new Installer(configService, shellService, osService, fsService); + const installMenu = new InstallMenu(uiService, configService, pathSelector, installer); const configMenu = new ConfigMenu( uiService, new MenuLoop(), diff --git a/src/services/uiService.js b/src/services/uiService.js index 0ff9233..6c4d2af 100644 --- a/src/services/uiService.js +++ b/src/services/uiService.js @@ -1,6 +1,7 @@ import boxen from "boxen"; import chalk from "chalk"; import inquirer from "inquirer"; +import { createSpinner } from "nanospinner"; import { ASCII_ART } from "../constants/ascii.js"; @@ -87,4 +88,18 @@ export class UiService { ]); return response.valueStr; }; + + createAndStartSpinner = (message) => { + return createSpinner(message).start(); + } + + stopSpinnerSuccess = (spinner) => { + if (spinner == undefined) return; + spinner.stop(); + }; + + stopSpinnerError = (spinner) => { + if (spinner == undefined) return; + spinner.error(); + }; } diff --git a/src/ui/installMenu.js b/src/ui/installMenu.js index 4c4b50d..2008fb8 100644 --- a/src/ui/installMenu.js +++ b/src/ui/installMenu.js @@ -1,12 +1,34 @@ export class InstallMenu { - constructor(uiService, configService, pathSelector) { + constructor(uiService, configService, pathSelector, installer) { this.ui = uiService; this.configService = configService; this.config = configService.get(); this.pathSelector = pathSelector; + this.installer = installer; } show = async () => { + if (await this.installer.isCodexInstalled()) { + await this.showUninstallMenu(); + } else { + await this.showInstallMenu(); + } + } + + showUninstallMenu = async () => { + await this.ui.askMultipleChoice("Codex is installed", [ + { + label: "Uninstall", + action: this.performUninstall, + }, + { + label: "Cancel", + action: this.doNothing, + }, + ]); + } + + showInstallMenu = async () => { await this.ui.askMultipleChoice("Configure your Codex installation", [ { label: "Install path: " + this.config.codexInstallPath, @@ -40,7 +62,30 @@ export class InstallMenu { await this.show(); }; - performInstall = async () => {}; + performInstall = async () => { + await this.installer.installCodex(this); + }; + + performUninstall = async () => {}; doNothing = async () => {}; + + // Progress callbacks from installer module: + installStarts = () => { + this.installSpinner = this.ui.createAndStartSpinner("Installing..."); + }; + + downloadSuccessful = () => { + this.ui.showInfoMessage("Download successful..."); + } + + installSuccessful = () => { + this.ui.showInfoMessage("Installation successful!"); + this.ui.stopSpinnerSuccess(this.installSpinner); + } + + warn = (message) => { + this.ui.showErrorMessage(message); + this.ui.stopSpinnerError(this.installSpinner); + }; } diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index 7c5c928..50c1e11 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -19,7 +19,7 @@ export class MainMenu { promptMainMenu = async () => { await this.ui.askMultipleChoice("Select an option", [ { - label: "Install Codex", + label: "Install/uninstall Codex", action: this.installMenu.show, }, { diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index d5b0a0f..73dfed7 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -47,7 +47,7 @@ describe("mainmenu", () => { expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( "Select an option", [ - { label: "Install Codex", action: mockInstallMenu.show }, + { label: "Install/uninstall Codex", action: mockInstallMenu.show }, { label: "Configure Codex", action: mockConfigMenu.show }, { label: "Exit", action: mockMenuLoop.stopLoop }, ], From 0ed0b0a2185416e053a64f08fb39a7b7566bd4f6 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 9 Apr 2025 10:27:00 +0200 Subject: [PATCH 25/59] finishes install + uninstall --- src/__mocks__/handler.mocks.js | 8 ++ src/__mocks__/service.mocks.js | 4 + src/handlers/installer.js | 7 +- src/handlers/installer.test.js | 24 +++++- src/main.js | 14 +++- src/services/fsService.js | 4 + src/services/uiService.js | 2 +- src/ui/installMenu.js | 58 ++++++++++----- src/ui/installMenu.test.js | 130 ++++++++++++++++++++++++++++++++- src/utils/pathSelector.js | 3 +- src/utils/pathSelector.test.js | 8 +- 11 files changed, 232 insertions(+), 30 deletions(-) create mode 100644 src/__mocks__/handler.mocks.js diff --git a/src/__mocks__/handler.mocks.js b/src/__mocks__/handler.mocks.js new file mode 100644 index 0000000..5b8f8df --- /dev/null +++ b/src/__mocks__/handler.mocks.js @@ -0,0 +1,8 @@ +import { vi } from "vitest"; + +export const mockInstaller = { + isCodexInstalled: vi.fn(), + getCodexVersion: vi.fn(), + installCodex: vi.fn(), + uninstallCodex: vi.fn(), +}; diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 5b735f2..3610f8e 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -6,6 +6,9 @@ export const mockUiService = { showErrorMessage: vi.fn(), askMultipleChoice: vi.fn(), askPrompt: vi.fn(), + createAndStartSpinner: vi.fn(), + stopSpinnerSuccess: vi.fn(), + stopSpinnerError: vi.fn(), }; export const mockConfigService = { @@ -21,6 +24,7 @@ export const mockFsService = { isFile: vi.fn(), readDir: vi.fn(), makeDir: vi.fn(), + deleteDir: vi.fn(), }; export const mockShellService = { diff --git a/src/handlers/installer.js b/src/handlers/installer.js index 740e8b7..ca0eb4f 100644 --- a/src/handlers/installer.js +++ b/src/handlers/installer.js @@ -39,6 +39,11 @@ export class Installer { processCallbacks.installSuccessful(); }; + uninstallCodex = () => { + this.fs.deleteDir(this.config.codexInstallPath); + this.fs.deleteDir(this.config.dataDir); + }; + arePrerequisitesCorrect = async (processCallbacks) => { if (await this.isCodexInstalled()) { processCallbacks.warn("Codex is already installed."); @@ -74,7 +79,7 @@ export class Installer { }; installCodexUnix = async (processCallbacks) => { - if (!await this.ensureUnixDependencies(processCallbacks)) return; + if (!(await this.ensureUnixDependencies(processCallbacks))) return; await this.shell.run( "curl -# --connect-timeout 10 --max-time 60 -L https://get.codex.storage/install.sh -o install.sh && chmod +x install.sh", ); diff --git a/src/handlers/installer.test.js b/src/handlers/installer.test.js index 72cd207..3eea1cf 100644 --- a/src/handlers/installer.test.js +++ b/src/handlers/installer.test.js @@ -241,7 +241,9 @@ describe("Installer", () => { it("ensures unix dependencies", async () => { await installer.installCodexUnix(processCallbacks); - expect(installer.ensureUnixDependencies).toHaveBeenCalled(processCallbacks); + expect(installer.ensureUnixDependencies).toHaveBeenCalled( + processCallbacks, + ); }); it("returns early if unix dependencies are not met", async () => { @@ -255,9 +257,9 @@ describe("Installer", () => { }); describe("when dependencies are met", () => { - beforeEach(() =>{ + beforeEach(() => { installer.ensureUnixDependencies.mockResolvedValue(true); - }) + }); it("runs the curl command to download the installer", async () => { await installer.installCodexUnix(processCallbacks); @@ -385,4 +387,20 @@ describe("Installer", () => { expect(mockConfigService.saveConfig).toHaveBeenCalled(); }); }); + + describe("uninstallCodex", () => { + it("deletes the codex install path", () => { + installer.uninstallCodex(); + + expect(mockFsService.deleteDir).toHaveBeenCalledWith( + config.codexInstallPath, + ); + }); + + it("deletes the codex data path", () => { + installer.uninstallCodex(); + + expect(mockFsService.deleteDir).toHaveBeenCalledWith(config.dataDir); + }); + }); }); diff --git a/src/main.js b/src/main.js index c3240ea..b37b231 100644 --- a/src/main.js +++ b/src/main.js @@ -105,8 +105,18 @@ export async function main() { const numberSelector = new NumberSelector(uiService); const shellService = new ShellService(); const osService = new OsService(); - const installer = new Installer(configService, shellService, osService, fsService); - const installMenu = new InstallMenu(uiService, configService, pathSelector, installer); + const installer = new Installer( + configService, + shellService, + osService, + fsService, + ); + const installMenu = new InstallMenu( + uiService, + configService, + pathSelector, + installer, + ); const configMenu = new ConfigMenu( uiService, new MenuLoop(), diff --git a/src/services/fsService.js b/src/services/fsService.js index 8590652..fee2b05 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -50,4 +50,8 @@ export class FsService { moveDir = (oldPath, newPath) => { fs.moveSync(oldPath, newPath); }; + + deleteDir = (dir) => { + fs.rmSync(dir, { recursive: true, force: true }); + }; } diff --git a/src/services/uiService.js b/src/services/uiService.js index 6c4d2af..1ce5955 100644 --- a/src/services/uiService.js +++ b/src/services/uiService.js @@ -91,7 +91,7 @@ export class UiService { createAndStartSpinner = (message) => { return createSpinner(message).start(); - } + }; stopSpinnerSuccess = (spinner) => { if (spinner == undefined) return; diff --git a/src/ui/installMenu.js b/src/ui/installMenu.js index 2008fb8..6ce3053 100644 --- a/src/ui/installMenu.js +++ b/src/ui/installMenu.js @@ -13,20 +13,7 @@ export class InstallMenu { } else { await this.showInstallMenu(); } - } - - showUninstallMenu = async () => { - await this.ui.askMultipleChoice("Codex is installed", [ - { - label: "Uninstall", - action: this.performUninstall, - }, - { - label: "Cancel", - action: this.doNothing, - }, - ]); - } + }; showInstallMenu = async () => { await this.ui.askMultipleChoice("Configure your Codex installation", [ @@ -49,6 +36,41 @@ export class InstallMenu { ]); }; + showUninstallMenu = async () => { + await this.ui.askMultipleChoice("Codex is installed", [ + { + label: "Uninstall", + action: this.showConfirmUninstall, + }, + { + label: "Cancel", + action: this.doNothing, + }, + ]); + }; + + showConfirmUninstall = async () => { + this.ui.showInfoMessage( + "You are about to:\n" + + " - Uninstall the Codex application\n" + + " - Delete the data stored in your Codex node", + ); + + await this.ui.askMultipleChoice( + "Are you sure you want to uninstall Codex?", + [ + { + label: "No", + action: this.doNothing, + }, + { + label: "Yes", + action: this.performUninstall, + }, + ], + ); + }; + selectInstallPath = async () => { this.config.codexInstallPath = await this.pathSelector.show( this.config.codexInstallPath, @@ -66,7 +88,9 @@ export class InstallMenu { await this.installer.installCodex(this); }; - performUninstall = async () => {}; + performUninstall = async () => { + this.installer.uninstallCodex(); + }; doNothing = async () => {}; @@ -77,12 +101,12 @@ export class InstallMenu { downloadSuccessful = () => { this.ui.showInfoMessage("Download successful..."); - } + }; installSuccessful = () => { this.ui.showInfoMessage("Installation successful!"); this.ui.stopSpinnerSuccess(this.installSpinner); - } + }; warn = (message) => { this.ui.showErrorMessage(message); diff --git a/src/ui/installMenu.test.js b/src/ui/installMenu.test.js index 2e15e89..1b47cd4 100644 --- a/src/ui/installMenu.test.js +++ b/src/ui/installMenu.test.js @@ -3,6 +3,7 @@ import { InstallMenu } from "./installMenu.js"; import { mockUiService } from "../__mocks__/service.mocks.js"; import { mockConfigService } from "../__mocks__/service.mocks.js"; import { mockPathSelector } from "../__mocks__/utils.mocks.js"; +import { mockInstaller } from "../__mocks__/handler.mocks.js"; describe("InstallMenu", () => { const config = { @@ -18,11 +19,35 @@ describe("InstallMenu", () => { mockUiService, mockConfigService, mockPathSelector, + mockInstaller, ); }); + describe("show", () => { + beforeEach(() => { + installMenu.showInstallMenu = vi.fn(); + installMenu.showUninstallMenu = vi.fn(); + }); + + it("shows uninstall menu when codex is installed", async () => { + mockInstaller.isCodexInstalled.mockResolvedValue(true); + + await installMenu.show(); + + expect(installMenu.showUninstallMenu).toHaveBeenCalled(); + }); + + it("shows install menu when codex is not installed", async () => { + mockInstaller.uninstallCodex.mockResolvedValue(false); + + await installMenu.show(); + + expect(installMenu.showInstallMenu).toHaveBeenCalled(); + }); + }); + it("displays the install menu", async () => { - await installMenu.show(); + await installMenu.showInstallMenu(); expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( "Configure your Codex installation", [ @@ -46,6 +71,47 @@ describe("InstallMenu", () => { ); }); + it("displays the uninstall menu", async () => { + await installMenu.showUninstallMenu(); + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Codex is installed", + [ + { + label: "Uninstall", + action: installMenu.showConfirmUninstall, + }, + { + label: "Cancel", + action: installMenu.doNothing, + }, + ], + ); + }); + + it("confirms uninstall", async () => { + await installMenu.showConfirmUninstall(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "You are about to:\n" + + " - Uninstall the Codex application\n" + + " - Delete the data stored in your Codex node", + ); + + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Are you sure you want to uninstall Codex?", + [ + { + label: "No", + action: installMenu.doNothing, + }, + { + label: "Yes", + action: installMenu.performUninstall, + }, + ], + ); + }); + it("allows selecting the install path", async () => { const originalPath = config.codexInstallPath; const newPath = "/new/path"; @@ -68,4 +134,66 @@ describe("InstallMenu", () => { "This option is not currently available.", ); }); + + it("calls installed for installation", async () => { + await installMenu.performInstall(); + + expect(mockInstaller.installCodex).toHaveBeenCalledWith(installMenu); + }); + + it("calls installer for deinstallation", async () => { + await installMenu.performUninstall(); + + expect(mockInstaller.uninstallCodex).toHaveBeenCalled(); + }); + + describe("process callback handling", () => { + const mockSpinner = { + isRealSpinner: "no srry", + }; + + beforeEach(() => { + mockUiService.createAndStartSpinner.mockReturnValue(mockSpinner); + }); + + it("creates spinner on installStarts", () => { + installMenu.installStarts(); + + expect(installMenu.installSpinner).toBe(mockSpinner); + expect(mockUiService.createAndStartSpinner).toHaveBeenCalledWith( + "Installing...", + ); + }); + + it("shows download success message", () => { + installMenu.downloadSuccessful(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "Download successful...", + ); + }); + + it("shows install success message", () => { + installMenu.installSpinner = mockSpinner; + + installMenu.installSuccessful(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "Installation successful!", + ); + expect(mockUiService.stopSpinnerSuccess).toHaveBeenCalledWith( + mockSpinner, + ); + }); + + it("shows warnings", () => { + const message = "warning!"; + installMenu.installSpinner = mockSpinner; + + installMenu.warn(message); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith(message); + expect(mockUiService.stopSpinnerError).toHaveBeenCalledWith(mockSpinner); + }); + }); }); diff --git a/src/utils/pathSelector.js b/src/utils/pathSelector.js index f75eba9..742c8bb 100644 --- a/src/utils/pathSelector.js +++ b/src/utils/pathSelector.js @@ -143,8 +143,7 @@ export class PathSelector { try { const entries = this.fs.readDir(fullPath); return entries.filter((entry) => this.isSubDir(entry)); - } - catch { + } catch { return []; } }; diff --git a/src/utils/pathSelector.test.js b/src/utils/pathSelector.test.js index 1cfebb1..6635cc4 100644 --- a/src/utils/pathSelector.test.js +++ b/src/utils/pathSelector.test.js @@ -102,12 +102,14 @@ describe("PathSelector", () => { }); it("handles non-existing paths", async () => { - mockFsService.readDir.mockImplementationOnce(() => { throw new Error("A!"); }); - + mockFsService.readDir.mockImplementationOnce(() => { + throw new Error("A!"); + }); + await pathSelector.downOne(); expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( - "There are no subdirectories here." + "There are no subdirectories here.", ); }); From 531cc5eb5b1f9f6976def887bfcb161472f7f175 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 9 Apr 2025 15:25:49 +0200 Subject: [PATCH 26/59] wip process control --- src/handlers/processControl.js | 89 ++++++++++++++++++++++++++++++++++ src/main.js | 7 +++ 2 files changed, 96 insertions(+) create mode 100644 src/handlers/processControl.js diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js new file mode 100644 index 0000000..1ba6570 --- /dev/null +++ b/src/handlers/processControl.js @@ -0,0 +1,89 @@ +export class ProcessControl { + constructor(configService, shellService, osService, fsService) { + this.config = configService.get(); + this.shell = shellService; + this.os = osService; + this.fs = fsService; + } + + getPublicIp = async () => { + if (this.os.isWindows()) { + const result = await this.shell.run( + "for /f \"delims=\" %a in ('curl -s --ssl-reqd ip.codex.storage') do @echo %a", + ); + return result.trim(); + } else { + return await this.shell.run("curl -s https://ip.codex.storage"); + } + } + + getLogFile = () =>{ + // function getCurrentLogFile(config) { + // const timestamp = new Date() + // .toISOString() + // .replaceAll(":", "-") + // .replaceAll(".", "-"); + // return path.join(config.logsDir, `codex_${timestamp}.log`); + // } + // todo, maybe use timestamp + + return this.fs.pathJoin([this.config.logsDir, "codex.log"]); + } + + doThing = async () => { + if (this.config.dataDir.length < 1) throw new Error("Missing config: dataDir"); + if (this.config.logsDir.length < 1) throw new Error("Missing config: logsDir"); + + console.log("start a codex detached"); + + console.log("nat: " + await this.getPublicIp()); + console.log("logs dir: " + this.getLogFile()); + + try { + + + + console.log( + showInfoMessage( + `Data location: ${config.dataDir}\n` + + `Logs: ${logFilePath}\n` + + `API port: ${config.ports.apiPort}`, + ), + ); + + const executable = config.codexExe; + const args = [ + `--data-dir="${config.dataDir}"`, + `--log-level=DEBUG`, + `--log-file="${logFilePath}"`, + `--storage-quota="${config.storageQuota}"`, + `--disc-port=${config.ports.discPort}`, + `--listen-addrs=/ip4/0.0.0.0/tcp/${config.ports.listenPort}`, + `--api-port=${config.ports.apiPort}`, + `--nat=${nat}`, + `--api-cors-origin="*"`, + `--bootstrap-node=spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P`, + ]; + + const command = `"${executable}" ${args.join(" ")}`; + + console.log( + showInfoMessage( + "🚀 Codex node is running...\n\n" + + "If your firewall ask, be sure to allow Codex to receive connections. \n" + + "Please keep this terminal open. Start a new terminal to interact with the node.\n\n" + + "Press CTRL+C to stop the node", + ), + ); + + const nodeProcess = exec(command); + + await new Promise((resolve) => setTimeout(resolve, 5000)); + + } + catch + { + + } + } +} diff --git a/src/main.js b/src/main.js index b37b231..ed4c841 100644 --- a/src/main.js +++ b/src/main.js @@ -34,6 +34,7 @@ import { DataDirMover } from "./utils/dataDirMover.js"; import { Installer } from "./handlers/installer.js"; import { ShellService } from "./services/shellService.js"; import { OsService } from "./services/osService.js"; +import { ProcessControl } from "./handlers/processControl.js"; async function showNavigationMenu() { console.log("\n"); @@ -132,9 +133,15 @@ export async function main() { new DataDirMover(fsService, uiService), ); + const processControl = new ProcessControl(configService, shellService, osService, fsService); + await processControl.doThing(); + return; + await mainMenu.show(); return; + + try { while (true) { console.log("\n" + chalk.cyanBright(ASCII_ART)); From 4341a67ebefb9fa31a4aae792e3c31f26e097a16 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 14 Apr 2025 09:50:42 +0200 Subject: [PATCH 27/59] working example of creating a detached codex process --- src/handlers/processControl.js | 72 +++++++++++++--------------------- 1 file changed, 28 insertions(+), 44 deletions(-) diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 1ba6570..51fc1c8 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -1,3 +1,6 @@ +import fs from "fs"; +import { spawn, exec } from "child_process"; + export class ProcessControl { constructor(configService, shellService, osService, fsService) { this.config = configService.get(); @@ -38,52 +41,33 @@ export class ProcessControl { console.log("nat: " + await this.getPublicIp()); console.log("logs dir: " + this.getLogFile()); + console.log("data dir: " + this.config.dataDir); + console.log("api port: " + this.config.ports.apiPort); + console.log("codex exe: " + this.config.codexExe); + console.log("quota: " + this.config.storageQuota); - try { - + const executable = this.config.codexExe; + const args = [ + `--data-dir=${this.config.dataDir}`, + // `--log-level=DEBUG`, + // `--log-file="${this.getLogFile()}"`, + `--storage-quota=${this.config.storageQuota}`, + `--disc-port=${this.config.ports.discPort}`, + `--listen-addrs=/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}`, + `--api-port=${this.config.ports.apiPort}`, + `--nat=extip:${await this.getPublicIp()}`, + `--api-cors-origin="*"`, + `--bootstrap-node=spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P`, + ]; + const command = `"${executable}" ${args.join(" ")}`; + console.log("command: " + command); + console.log("\n\n"); - console.log( - showInfoMessage( - `Data location: ${config.dataDir}\n` + - `Logs: ${logFilePath}\n` + - `API port: ${config.ports.apiPort}`, - ), - ); + var child = spawn(executable, args, { detached: true, stdio: ['ignore', 'ignore', 'ignore']}); + child.unref(); - const executable = config.codexExe; - const args = [ - `--data-dir="${config.dataDir}"`, - `--log-level=DEBUG`, - `--log-file="${logFilePath}"`, - `--storage-quota="${config.storageQuota}"`, - `--disc-port=${config.ports.discPort}`, - `--listen-addrs=/ip4/0.0.0.0/tcp/${config.ports.listenPort}`, - `--api-port=${config.ports.apiPort}`, - `--nat=${nat}`, - `--api-cors-origin="*"`, - `--bootstrap-node=spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P`, - ]; - - const command = `"${executable}" ${args.join(" ")}`; - - console.log( - showInfoMessage( - "🚀 Codex node is running...\n\n" + - "If your firewall ask, be sure to allow Codex to receive connections. \n" + - "Please keep this terminal open. Start a new terminal to interact with the node.\n\n" + - "Press CTRL+C to stop the node", - ), - ); - - const nodeProcess = exec(command); - - await new Promise((resolve) => setTimeout(resolve, 5000)); - - } - catch - { - - } - } + await new Promise((resolve) => setTimeout(resolve, 2000)); + return; + } } From 0f69d61e8e8f36ca8cdc02ba34e120538b35bc06 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 14 Apr 2025 11:27:58 +0200 Subject: [PATCH 28/59] sets up config service codex-config-file saving --- src/__mocks__/service.mocks.js | 3 + src/handlers/processControl.js | 33 ++++---- src/main.js | 11 ++- src/services/configService.js | 54 ++++++++++-- src/services/configService.test.js | 129 +++++++++++++++++++++++++++++ src/services/fsService.js | 13 +++ src/utils/appData.js | 4 + 7 files changed, 216 insertions(+), 31 deletions(-) create mode 100644 src/services/configService.test.js diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 3610f8e..5de6f87 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -25,6 +25,9 @@ export const mockFsService = { readDir: vi.fn(), makeDir: vi.fn(), deleteDir: vi.fn(), + readJsonFile: vi.fn(), + writeJsonFile: vi.fn(), + writeFile: vi.fn(), }; export const mockShellService = { diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 51fc1c8..31b6272 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -3,6 +3,7 @@ import { spawn, exec } from "child_process"; export class ProcessControl { constructor(configService, shellService, osService, fsService) { + this.configService = configService; this.config = configService.get(); this.shell = shellService; this.os = osService; @@ -18,28 +19,17 @@ export class ProcessControl { } else { return await this.shell.run("curl -s https://ip.codex.storage"); } - } - - getLogFile = () =>{ - // function getCurrentLogFile(config) { - // const timestamp = new Date() - // .toISOString() - // .replaceAll(":", "-") - // .replaceAll(".", "-"); - // return path.join(config.logsDir, `codex_${timestamp}.log`); - // } - // todo, maybe use timestamp - - return this.fs.pathJoin([this.config.logsDir, "codex.log"]); - } + }; doThing = async () => { - if (this.config.dataDir.length < 1) throw new Error("Missing config: dataDir"); - if (this.config.logsDir.length < 1) throw new Error("Missing config: logsDir"); + if (this.config.dataDir.length < 1) + throw new Error("Missing config: dataDir"); + if (this.config.logsDir.length < 1) + throw new Error("Missing config: logsDir"); console.log("start a codex detached"); - console.log("nat: " + await this.getPublicIp()); + console.log("nat: " + (await this.getPublicIp())); console.log("logs dir: " + this.getLogFile()); console.log("data dir: " + this.config.dataDir); console.log("api port: " + this.config.ports.apiPort); @@ -64,10 +54,15 @@ export class ProcessControl { console.log("command: " + command); console.log("\n\n"); - var child = spawn(executable, args, { detached: true, stdio: ['ignore', 'ignore', 'ignore']}); + this.configService.writeCodexConfigFile(); + + var child = spawn(executable, args, { + detached: true, + stdio: ["ignore", "ignore", "ignore"], + }); child.unref(); await new Promise((resolve) => setTimeout(resolve, 2000)); return; - } + }; } diff --git a/src/main.js b/src/main.js index ed4c841..87eb716 100644 --- a/src/main.js +++ b/src/main.js @@ -99,9 +99,9 @@ export async function main() { process.on("SIGTERM", handleExit); process.on("SIGQUIT", handleExit); - const configService = new ConfigService(); const uiService = new UiService(); const fsService = new FsService(); + const configService = new ConfigService(fsService); const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService); const numberSelector = new NumberSelector(uiService); const shellService = new ShellService(); @@ -133,15 +133,18 @@ export async function main() { new DataDirMover(fsService, uiService), ); - const processControl = new ProcessControl(configService, shellService, osService, fsService); + const processControl = new ProcessControl( + configService, + shellService, + osService, + fsService, + ); await processControl.doThing(); return; await mainMenu.show(); return; - - try { while (true) { console.log("\n" + chalk.cyanBright(ASCII_ART)); diff --git a/src/services/configService.js b/src/services/configService.js index 3b4b3b2..05d36ee 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -1,8 +1,7 @@ -import fs from "fs"; -import path from "path"; -import { getAppDataDir } from "../utils/appData.js"; import { + getAppDataDir, getCodexBinPath, + getCodexConfigFilePath, getCodexDataDirDefaultPath, getCodexLogsDefaultPath, } from "../utils/appData.js"; @@ -11,6 +10,7 @@ const defaultConfig = { codexExe: "", // User-selected config options: codexInstallPath: getCodexBinPath(), + codexConfigFilePath: getCodexConfigFilePath(), dataDir: getCodexDataDirDefaultPath(), logsDir: getCodexLogsDefaultPath(), storageQuota: 8 * 1024 * 1024 * 1024, @@ -22,7 +22,8 @@ const defaultConfig = { }; export class ConfigService { - constructor() { + constructor(fsService) { + this.fs = fsService; this.loadConfig(); } @@ -33,11 +34,12 @@ export class ConfigService { loadConfig = () => { const filePath = this.getConfigFilename(); try { - if (!fs.existsSync(filePath)) { + if (!this.fs.isFile(filePath)) { this.config = defaultConfig; this.saveConfig(); + } else { + this.config = this.fs.readJsonFile(filePath); } - this.config = JSON.parse(fs.readFileSync(filePath)); } catch (error) { console.error( `Failed to load config file from '${filePath}' error: '${error}'.`, @@ -49,7 +51,7 @@ export class ConfigService { saveConfig = () => { const filePath = this.getConfigFilename(); try { - fs.writeFileSync(filePath, JSON.stringify(this.config)); + this.fs.writeJsonFile(filePath, this.config); } catch (error) { console.error( `Failed to save config file to '${filePath}' error: '${error}'.`, @@ -59,6 +61,42 @@ export class ConfigService { }; getConfigFilename = () => { - return path.join(getAppDataDir(), "config.json"); + return this.fs.pathJoin([getAppDataDir(), "config.json"]); + }; + + getLogFilePath = () => { + // function getCurrentLogFile(config) { + // const timestamp = new Date() + // .toISOString() + // .replaceAll(":", "-") + // .replaceAll(".", "-"); + // return path.join(config.logsDir, `codex_${timestamp}.log`); + // } + // todo, maybe use timestamp + + return this.fs.pathJoin([this.config.logsDir, "codex.log"]); + }; + + writeCodexConfigFile = (publicIp, bootstrapNodes) => { + const nl = "\n"; + const bootNodes = bootstrapNodes.join(","); + + this.fs.writeFile( + this.config.codexConfigFilePath, + `data-dir="${this.format(this.config.dataDir)}"${nl}` + + `log-level=DEBUG${nl}` + + `log-file="${this.format(this.getLogFilePath())}"${nl}` + + `storage-quota=${this.config.storageQuota}${nl}` + + `disc-port=${this.config.ports.discPort}${nl}` + + `listen-addrs=/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}${nl}` + + `api-port=${this.config.ports.apiPort}${nl}` + + `nat=extip:${publicIp}${nl}` + + `api-cors-origin="*"${nl}` + + `bootstrap-node=[${bootNodes}]`, + ); + }; + + format = (str) => { + return str.replaceAll("\\", "/"); }; } diff --git a/src/services/configService.test.js b/src/services/configService.test.js new file mode 100644 index 0000000..3fc67ee --- /dev/null +++ b/src/services/configService.test.js @@ -0,0 +1,129 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { ConfigService } from "./configService.js"; +import { mockFsService } from "../__mocks__/service.mocks.js"; +import { + getAppDataDir, + getCodexBinPath, + getCodexConfigFilePath, + getCodexDataDirDefaultPath, + getCodexLogsDefaultPath, +} from "../utils/appData.js"; + +describe("ConfigService", () => { + const configPath = "/path/to/config.json"; + const expectedDefaultConfig = { + codexExe: "", + codexInstallPath: getCodexBinPath(), + codexConfigFilePath: getCodexConfigFilePath(), + dataDir: getCodexDataDirDefaultPath(), + logsDir: getCodexLogsDefaultPath(), + storageQuota: 8 * 1024 * 1024 * 1024, + ports: { + discPort: 8090, + listenPort: 8070, + apiPort: 8080, + }, + }; + + beforeEach(() => { + vi.resetAllMocks(); + + mockFsService.pathJoin.mockReturnValue(configPath); + }); + + describe("constructor", () => { + it("formats the config file path", () => { + new ConfigService(mockFsService); + + expect(mockFsService.pathJoin).toHaveBeenCalledWith([ + getAppDataDir(), + "config.json", + ]); + }); + + it("saves the default config when the config.json file does not exist", () => { + mockFsService.isFile.mockReturnValue(false); + + const service = new ConfigService(mockFsService); + + expect(mockFsService.isFile).toHaveBeenCalledWith(configPath); + expect(mockFsService.readJsonFile).not.toHaveBeenCalled(); + expect(mockFsService.writeJsonFile).toHaveBeenCalledWith( + configPath, + service.config, + ); + expect(service.config).toEqual(expectedDefaultConfig); + }); + + it("loads the config.json file when it does exist", () => { + mockFsService.isFile.mockReturnValue(true); + const savedConfig = { + isTestConfig: "Yes, very", + }; + mockFsService.readJsonFile.mockReturnValue(savedConfig); + + const service = new ConfigService(mockFsService); + + expect(mockFsService.isFile).toHaveBeenCalledWith(configPath); + expect(mockFsService.readJsonFile).toHaveBeenCalledWith(configPath); + expect(mockFsService.writeJsonFile).not.toHaveBeenCalled(); + expect(service.config).toEqual(savedConfig); + }); + }); + + describe("getLogFilePath", () => { + it("joins the logsDir with the log filename", () => { + const service = new ConfigService(mockFsService); + + const result = "path/to/codex.log"; + mockFsService.pathJoin.mockReturnValue(result); + + expect(service.getLogFilePath()).toBe(result); + expect(mockFsService.pathJoin).toHaveBeenCalledWith([ + expectedDefaultConfig.logsDir, + "codex.log", + ]); + }); + }); + + describe("writecodexConfigFile", () => { + const logsPath = "C:\\path\\codex.log"; + var configService; + + beforeEach(() => { + // use the default config: + mockFsService.isFile.mockReturnValue(false); + + configService = new ConfigService(mockFsService); + configService.getLogFilePath = vi.fn(); + configService.getLogFilePath.mockReturnValue(logsPath); + }); + + function formatPath(str) { + return str.replaceAll("\\", "/"); + } + + it("writes the config file values to the config TOML file", () => { + const publicIp = "1.2.3.4"; + const bootstrapNodes = ["boot111", "boot222", "boot333"]; + + configService.writeCodexConfigFile(publicIp, bootstrapNodes); + + const newLine = "\n"; + + expect(mockFsService.writeFile).toHaveBeenCalledWith( + expectedDefaultConfig.codexConfigFilePath, + `data-dir=\"${formatPath(expectedDefaultConfig.dataDir)}"${newLine}` + + `log-level=DEBUG${newLine}` + + `log-file="${formatPath(logsPath)}"${newLine}` + + `storage-quota=${expectedDefaultConfig.storageQuota}${newLine}` + + `disc-port=${expectedDefaultConfig.ports.discPort}${newLine}` + + `listen-addrs=/ip4/0.0.0.0/tcp/${expectedDefaultConfig.ports.listenPort}${newLine}` + + `api-port=${expectedDefaultConfig.ports.apiPort}${newLine}` + + `nat=extip:${publicIp}${newLine}` + + `api-cors-origin="*"${newLine}` + + `bootstrap-node=[${bootstrapNodes.join(",")}]`, + ); + }); + }); +}); diff --git a/src/services/fsService.js b/src/services/fsService.js index fee2b05..9b44a9a 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -54,4 +54,17 @@ export class FsService { deleteDir = (dir) => { fs.rmSync(dir, { recursive: true, force: true }); }; + + readJsonFile = (filePath) => { + return JSON.parse(fs.readFileSync(filePath)); + }; + + writeJsonFile = (filePath, jsonObject) => { + fs.writeFileSync(filePath, JSON.stringify(jsonObject)); + }; + + writeFile = (filePath, content) => { + console.log("filepath: " + filePath); + fs.writeFileSync(filePath, content); + }; } diff --git a/src/utils/appData.js b/src/utils/appData.js index 77dc950..d375cad 100644 --- a/src/utils/appData.js +++ b/src/utils/appData.js @@ -13,6 +13,10 @@ export function getCodexBinPath() { return ensureExists(path.join(appData("codex"), "bin")); } +export function getCodexConfigFilePath() { + return path.join(appData("codex"), "bin", "config.toml"); +} + export function getCodexDataDirDefaultPath() { // This path does not exist on first startup. That's good: Codex will // create it with the required access permissions. From 65e38a8bab053356edc3f7a30c2dfa87866156bd Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 14 Apr 2025 11:42:13 +0200 Subject: [PATCH 29/59] debugs config.toml formatting --- src/handlers/processControl.js | 41 +++++++++++++++--------------- src/services/configService.js | 8 +++--- src/services/configService.test.js | 12 ++++++--- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 31b6272..96e6782 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -29,37 +29,36 @@ export class ProcessControl { console.log("start a codex detached"); - console.log("nat: " + (await this.getPublicIp())); - console.log("logs dir: " + this.getLogFile()); - console.log("data dir: " + this.config.dataDir); - console.log("api port: " + this.config.ports.apiPort); - console.log("codex exe: " + this.config.codexExe); - console.log("quota: " + this.config.storageQuota); - const executable = this.config.codexExe; - const args = [ - `--data-dir=${this.config.dataDir}`, - // `--log-level=DEBUG`, - // `--log-file="${this.getLogFile()}"`, - `--storage-quota=${this.config.storageQuota}`, - `--disc-port=${this.config.ports.discPort}`, - `--listen-addrs=/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}`, - `--api-port=${this.config.ports.apiPort}`, - `--nat=extip:${await this.getPublicIp()}`, - `--api-cors-origin="*"`, - `--bootstrap-node=spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P`, + const args = [`--config-file=${this.config.codexConfigFilePath}`]; + const bootstrapNodes = [ + "spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P", ]; + const publicIp = await this.getPublicIp(); + + this.configService.writeCodexConfigFile(publicIp, bootstrapNodes); const command = `"${executable}" ${args.join(" ")}`; console.log("command: " + command); console.log("\n\n"); - this.configService.writeCodexConfigFile(); - var child = spawn(executable, args, { detached: true, - stdio: ["ignore", "ignore", "ignore"], + //stdio: ["ignore", "ignore", "ignore"], }); + + child.stdout.on("data", (data) => { + console.log(`stdout: ${data}`); + }); + + child.stderr.on("data", (data) => { + console.error(`stderr: ${data}`); + }); + + child.on("close", (code) => { + console.log(`child process exited with code ${code}`); + }); + child.unref(); await new Promise((resolve) => setTimeout(resolve, 2000)); diff --git a/src/services/configService.js b/src/services/configService.js index 05d36ee..6570220 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -79,18 +79,18 @@ export class ConfigService { writeCodexConfigFile = (publicIp, bootstrapNodes) => { const nl = "\n"; - const bootNodes = bootstrapNodes.join(","); + const bootNodes = bootstrapNodes.map((v) => `"${v}"`).join(","); this.fs.writeFile( this.config.codexConfigFilePath, `data-dir="${this.format(this.config.dataDir)}"${nl}` + - `log-level=DEBUG${nl}` + + `log-level="DEBUG"${nl}` + `log-file="${this.format(this.getLogFilePath())}"${nl}` + `storage-quota=${this.config.storageQuota}${nl}` + `disc-port=${this.config.ports.discPort}${nl}` + - `listen-addrs=/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}${nl}` + + `listen-addrs=["/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}"]${nl}` + `api-port=${this.config.ports.apiPort}${nl}` + - `nat=extip:${publicIp}${nl}` + + `nat="extip:${publicIp}"${nl}` + `api-cors-origin="*"${nl}` + `bootstrap-node=[${bootNodes}]`, ); diff --git a/src/services/configService.test.js b/src/services/configService.test.js index 3fc67ee..84a99eb 100644 --- a/src/services/configService.test.js +++ b/src/services/configService.test.js @@ -114,15 +114,19 @@ describe("ConfigService", () => { expect(mockFsService.writeFile).toHaveBeenCalledWith( expectedDefaultConfig.codexConfigFilePath, `data-dir=\"${formatPath(expectedDefaultConfig.dataDir)}"${newLine}` + - `log-level=DEBUG${newLine}` + + `log-level="DEBUG"${newLine}` + `log-file="${formatPath(logsPath)}"${newLine}` + `storage-quota=${expectedDefaultConfig.storageQuota}${newLine}` + `disc-port=${expectedDefaultConfig.ports.discPort}${newLine}` + - `listen-addrs=/ip4/0.0.0.0/tcp/${expectedDefaultConfig.ports.listenPort}${newLine}` + + `listen-addrs=["/ip4/0.0.0.0/tcp/${expectedDefaultConfig.ports.listenPort}"]${newLine}` + `api-port=${expectedDefaultConfig.ports.apiPort}${newLine}` + - `nat=extip:${publicIp}${newLine}` + + `nat="extip:${publicIp}"${newLine}` + `api-cors-origin="*"${newLine}` + - `bootstrap-node=[${bootstrapNodes.join(",")}]`, + `bootstrap-node=[${bootstrapNodes + .map((v) => { + return '"' + v + '"'; + }) + .join(",")}]`, ); }); }); From 7661d829cbe76f55e8264c064feb1844a23e78dd Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 14 Apr 2025 13:25:51 +0200 Subject: [PATCH 30/59] working example of detecting a running codex instance and stopping it --- package-lock.json | 15 ++++++++++++++- package.json | 7 ++++--- src/handlers/processControl.js | 28 ++++++++++++++++++++++++++++ src/main.js | 3 ++- src/services/configService.js | 2 +- 5 files changed, 49 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6cb0ec4..6a5cb4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "inquirer": "^9.2.12", "mime-types": "^2.1.35", "nanospinner": "^1.1.0", - "open": "^10.1.0" + "open": "^10.1.0", + "ps-list": "^8.1.1" }, "bin": { "codexstorage": "index.js" @@ -1947,6 +1948,18 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "node_modules/ps-list": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-8.1.1.tgz", + "integrity": "sha512-OPS9kEJYVmiO48u/B9qneqhkMvgCxT+Tm28VCEJpheTpl8cJ0ffZRRNgS5mrQRTrX5yRTpaJ+hRDeefXYmmorQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ramda": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.25.0.tgz", diff --git a/package.json b/package.json index 602892b..685a81e 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,13 @@ "axios": "^1.6.2", "boxen": "^7.1.1", "chalk": "^5.3.0", + "fs-extra": "^11.3.0", + "fs-filesystem": "^2.1.2", "inquirer": "^9.2.12", "mime-types": "^2.1.35", "nanospinner": "^1.1.0", - "fs-extra": "^11.3.0", - "fs-filesystem": "^2.1.2", - "open": "^10.1.0" + "open": "^10.1.0", + "ps-list": "^8.1.1" }, "devDependencies": { "prettier": "^3.4.2", diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 96e6782..30c0119 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -1,5 +1,6 @@ import fs from "fs"; import { spawn, exec } from "child_process"; +import psList from 'ps-list'; export class ProcessControl { constructor(configService, shellService, osService, fsService) { @@ -21,6 +22,33 @@ export class ProcessControl { } }; + detectThing = async () => { + console.log("detecting..."); + + const processes = await psList(); + const codexProcesses = processes.filter((p) => p.name === "codex.exe"); + if (codexProcesses.length > 0) { + console.log("Codex is already running."); + codexProcesses.forEach((p) => { + console.log(`PID: ${JSON.stringify(p)}`); + }); + + console.log("Stopping codex..."); + await this.stopThing(codexProcesses[0].pid); + await this.detectThing(); + } else { + console.log("Codex is not running."); + } + } + + stopThing = async (pid) => { + console.log("stopping process..."); + + process.kill(pid, "SIGINT"); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + } + doThing = async () => { if (this.config.dataDir.length < 1) throw new Error("Missing config: dataDir"); diff --git a/src/main.js b/src/main.js index 87eb716..b7ad534 100644 --- a/src/main.js +++ b/src/main.js @@ -139,7 +139,8 @@ export async function main() { osService, fsService, ); - await processControl.doThing(); + //await processControl.doThing(); + await processControl.detectThing(); return; await mainMenu.show(); diff --git a/src/services/configService.js b/src/services/configService.js index 6570220..1df6c13 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -92,7 +92,7 @@ export class ConfigService { `api-port=${this.config.ports.apiPort}${nl}` + `nat="extip:${publicIp}"${nl}` + `api-cors-origin="*"${nl}` + - `bootstrap-node=[${bootNodes}]`, + `bootstrap-node=[${bootNodes}]${nl}`, ); }; From 5fcb55015ace8964130c1bcc171ecafee97b2df7 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 14 Apr 2025 13:56:55 +0200 Subject: [PATCH 31/59] cleanup processControl --- src/__mocks__/service.mocks.js | 8 +++ src/handlers/processControl.js | 96 +++++++++------------------------- src/main.js | 9 +++- src/services/codexGlobals.js | 12 +++++ src/services/configService.js | 16 ++++++ src/services/osService.js | 5 ++ src/services/shellService.js | 29 ++++++++-- 7 files changed, 100 insertions(+), 75 deletions(-) create mode 100644 src/services/codexGlobals.js diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 5de6f87..ec36a5f 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -15,6 +15,7 @@ export const mockConfigService = { get: vi.fn(), saveConfig: vi.fn(), loadConfig: vi.fn(), + writeCodexConfigFile: vi.fn(), }; export const mockFsService = { @@ -32,6 +33,7 @@ export const mockFsService = { export const mockShellService = { run: vi.fn(), + spawnDetachedProcess: vi.fn(), }; export const mockOsService = { @@ -39,4 +41,10 @@ export const mockOsService = { isDarwin: vi.fn(), isLinux: vi.fn(), getWorkingDir: vi.fn(), + listProcesses: vi.fn(), +}; + +export const mockCodexGlobals = { + getPublicIp: vi.fn(), + getTestnetSPRs: vi.fn(), }; diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 30c0119..96a7d1c 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -1,95 +1,49 @@ -import fs from "fs"; -import { spawn, exec } from "child_process"; -import psList from 'ps-list'; - export class ProcessControl { - constructor(configService, shellService, osService, fsService) { + constructor(configService, shellService, osService, fsService, codexGlobals) { this.configService = configService; this.config = configService.get(); this.shell = shellService; this.os = osService; this.fs = fsService; + this.codexGlobals; } - getPublicIp = async () => { + getCodexProcesses = async () => { + const processes = await this.os.listProcesses(); if (this.os.isWindows()) { - const result = await this.shell.run( - "for /f \"delims=\" %a in ('curl -s --ssl-reqd ip.codex.storage') do @echo %a", - ); - return result.trim(); + return processes.filter((p) => p.name === "codex.exe"); } else { - return await this.shell.run("curl -s https://ip.codex.storage"); + return processes.filter((p) => p.name === "codex"); } }; - detectThing = async () => { - console.log("detecting..."); + getNumberOfCodexProcesses = async () => { + return (await this.getCodexProcesses()).length; + }; - const processes = await psList(); - const codexProcesses = processes.filter((p) => p.name === "codex.exe"); - if (codexProcesses.length > 0) { - console.log("Codex is already running."); - codexProcesses.forEach((p) => { - console.log(`PID: ${JSON.stringify(p)}`); - }); - - console.log("Stopping codex..."); - await this.stopThing(codexProcesses[0].pid); - await this.detectThing(); - } else { - console.log("Codex is not running."); - } - } - - stopThing = async (pid) => { - console.log("stopping process..."); + stopCodexProcess = async () => { + const processes = await this.getCodexProcesses(); + if (processes.length < 1) throw new Error("No codex process found"); + const pid = processes[0].pid; process.kill(pid, "SIGINT"); - await new Promise((resolve) => setTimeout(resolve, 2000)); - } + }; - doThing = async () => { - if (this.config.dataDir.length < 1) - throw new Error("Missing config: dataDir"); - if (this.config.logsDir.length < 1) - throw new Error("Missing config: logsDir"); + startCodexProcess = async () => { + this.saveCodexConfigFile(); + this.startCodex(); + }; - console.log("start a codex detached"); + saveCodexConfigFile = async () => { + const publicIp = await this.codexGlobals.getPublicIp(); + const bootstrapNodes = await this.codexGlobals.getTestnetSPRs(); + this.configService.writeCodexConfigFile(publicIp, bootstrapNodes); + }; + startCodex = async () => { const executable = this.config.codexExe; const args = [`--config-file=${this.config.codexConfigFilePath}`]; - const bootstrapNodes = [ - "spr:CiUIAhIhAiJvIcA_ZwPZ9ugVKDbmqwhJZaig5zKyLiuaicRcCGqLEgIDARo8CicAJQgCEiECIm8hwD9nA9n26BUoNuarCEllqKDnMrIuK5qJxFwIaosQ3d6esAYaCwoJBJ_f8zKRAnU6KkYwRAIgM0MvWNJL296kJ9gWvfatfmVvT-A7O2s8Mxp8l9c8EW0CIC-h-H-jBVSgFjg3Eny2u33qF7BDnWFzo7fGfZ7_qc9P", - ]; - const publicIp = await this.getPublicIp(); - - this.configService.writeCodexConfigFile(publicIp, bootstrapNodes); - - const command = `"${executable}" ${args.join(" ")}`; - console.log("command: " + command); - console.log("\n\n"); - - var child = spawn(executable, args, { - detached: true, - //stdio: ["ignore", "ignore", "ignore"], - }); - - child.stdout.on("data", (data) => { - console.log(`stdout: ${data}`); - }); - - child.stderr.on("data", (data) => { - console.error(`stderr: ${data}`); - }); - - child.on("close", (code) => { - console.log(`child process exited with code ${code}`); - }); - - child.unref(); - - await new Promise((resolve) => setTimeout(resolve, 2000)); - return; + await this.shell.spawnDetachedProcess(executable, args); }; } diff --git a/src/main.js b/src/main.js index b7ad534..ad7f22a 100644 --- a/src/main.js +++ b/src/main.js @@ -35,6 +35,7 @@ import { Installer } from "./handlers/installer.js"; import { ShellService } from "./services/shellService.js"; import { OsService } from "./services/osService.js"; import { ProcessControl } from "./handlers/processControl.js"; +import { CodexGlobals } from "./services/codexGlobals.js"; async function showNavigationMenu() { console.log("\n"); @@ -133,14 +134,20 @@ export async function main() { new DataDirMover(fsService, uiService), ); + const codexGlobals = new CodexGlobals(); + const processControl = new ProcessControl( configService, shellService, osService, fsService, ); + + console.log("ip: " + (await codexGlobals.getPublicIp())); + console.log("spr: " + (await codexGlobals.getTestnetSprs())); + //await processControl.doThing(); - await processControl.detectThing(); + // await processControl.detectThing(); return; await mainMenu.show(); diff --git a/src/services/codexGlobals.js b/src/services/codexGlobals.js new file mode 100644 index 0000000..d48fa6c --- /dev/null +++ b/src/services/codexGlobals.js @@ -0,0 +1,12 @@ +import axios from "axios"; + +export class CodexGlobals { + getPublicIp = async () => { + return (await axios.get(`https://ip.codex.storage`)).data; + }; + + getTestnetSPRs = async () => { + const result = (await axios.get(`https://spr.codex.storage/testnet`)).data; + return result.split("\n").filter((line) => line.length > 0); + }; +} diff --git a/src/services/configService.js b/src/services/configService.js index 1df6c13..91169a5 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -77,7 +77,23 @@ export class ConfigService { return this.fs.pathJoin([this.config.logsDir, "codex.log"]); }; + missing = (name) => { + throw new Error(`Missing config value: ${name}`); + }; + + validateConfiguration = () => { + if (this.config.codexExe.length < 1) this.missing("codexExe"); + if (this.config.codexConfigFilePath.length < 1) + this.missing("codexConfigFilePath"); + if (this.config.dataDir.length < 1) this.missing("dataDir"); + if (this.config.logsDir.length < 1) this.missing("logsDir"); + if (this.config.storageQuota < 1024 * 1024 * 100) + throw new Error("Storage quota must be at least 100MB"); + }; + writeCodexConfigFile = (publicIp, bootstrapNodes) => { + this.validateConfiguration(); + const nl = "\n"; const bootNodes = bootstrapNodes.map((v) => `"${v}"`).join(","); diff --git a/src/services/osService.js b/src/services/osService.js index 3ec8f05..efc12ef 100644 --- a/src/services/osService.js +++ b/src/services/osService.js @@ -1,4 +1,5 @@ import os from "os"; +import psList from "ps-list"; export class OsService { constructor() { @@ -20,4 +21,8 @@ export class OsService { getWorkingDir = () => { return process.cwd(); }; + + listProcesses = async () => { + await psList(); + }; } diff --git a/src/services/shellService.js b/src/services/shellService.js index df9f63e..79ae419 100644 --- a/src/services/shellService.js +++ b/src/services/shellService.js @@ -1,4 +1,4 @@ -import { exec } from "child_process"; +import { exec, spawn } from "child_process"; import { promisify } from "util"; export class ShellService { @@ -6,7 +6,7 @@ export class ShellService { this.execAsync = promisify(exec); } - async run(command) { + run = async (command) => { try { const { stdout, stderr } = await this.execAsync(command); return stdout; @@ -14,5 +14,28 @@ export class ShellService { console.error("Error:", error.message); throw error; } - } + }; + + spawnDetachedProcess = async (cmd, args) => { + var child = spawn(cmd, args, { + detached: true, + stdio: ["ignore", "ignore", "ignore"], + }); + + // child.stdout.on("data", (data) => { + // console.log(`stdout: ${data}`); + // }); + + // child.stderr.on("data", (data) => { + // console.error(`stderr: ${data}`); + // }); + + // child.on("close", (code) => { + // console.log(`child process exited with code ${code}`); + // }); + + child.unref(); + + await new Promise((resolve) => setTimeout(resolve, 2000)); + }; } From 0b24fc238ed431856781fbc5d9bea579bfe1951c Mon Sep 17 00:00:00 2001 From: ThatBen Date: Mon, 14 Apr 2025 14:31:22 +0200 Subject: [PATCH 32/59] wip updating main menu --- src/handlers/processControl.js | 6 +-- src/main.js | 28 ++++++-------- src/ui/mainMenu.js | 67 ++++++++++++++++++++++++++++++++-- 3 files changed, 77 insertions(+), 24 deletions(-) diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 96a7d1c..cf4423a 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -5,7 +5,7 @@ export class ProcessControl { this.shell = shellService; this.os = osService; this.fs = fsService; - this.codexGlobals; + this.codexGlobals = codexGlobals; } getCodexProcesses = async () => { @@ -31,8 +31,8 @@ export class ProcessControl { }; startCodexProcess = async () => { - this.saveCodexConfigFile(); - this.startCodex(); + await this.saveCodexConfigFile(); + await this.startCodex(); }; saveCodexConfigFile = async () => { diff --git a/src/main.js b/src/main.js index ad7f22a..ef50b55 100644 --- a/src/main.js +++ b/src/main.js @@ -100,6 +100,7 @@ export async function main() { process.on("SIGTERM", handleExit); process.on("SIGQUIT", handleExit); + const codexGlobals = new CodexGlobals(); const uiService = new UiService(); const fsService = new FsService(); const configService = new ConfigService(fsService); @@ -125,31 +126,24 @@ export async function main() { configService, pathSelector, numberSelector, + new DataDirMover(fsService, uiService), + ); + const processControl = new ProcessControl( + configService, + shellService, + osService, + fsService, + codexGlobals, ); const mainMenu = new MainMenu( uiService, new MenuLoop(), installMenu, configMenu, - new DataDirMover(fsService, uiService), + installer, + processControl, ); - const codexGlobals = new CodexGlobals(); - - const processControl = new ProcessControl( - configService, - shellService, - osService, - fsService, - ); - - console.log("ip: " + (await codexGlobals.getPublicIp())); - console.log("spr: " + (await codexGlobals.getTestnetSprs())); - - //await processControl.doThing(); - // await processControl.detectThing(); - return; - await mainMenu.show(); return; diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index 50c1e11..fc28d3f 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -1,9 +1,18 @@ export class MainMenu { - constructor(uiService, menuLoop, installMenu, configMenu) { + constructor( + uiService, + menuLoop, + installMenu, + configMenu, + installer, + processControl, + ) { this.ui = uiService; this.loop = menuLoop; this.installMenu = installMenu; this.configMenu = configMenu; + this.installer = installer; + this.processControl = processControl; this.loop.initialize(this.promptMainMenu); } @@ -17,15 +26,65 @@ export class MainMenu { }; promptMainMenu = async () => { - await this.ui.askMultipleChoice("Select an option", [ + if ((await this.processControl.getNumberOfCodexProcesses) > 0) { + await this.showRunningMenu(); + } else { + if (await this.installer.isCodexInstalled()) { + await this.showCodexNotRunningMenu(); + } else { + await this.showNotInstalledMenu(); + } + } + }; + + showNotInstalledMenu = async () => { + await this.ui.askMultipleChoice("Codex is not installed", [ { - label: "Install/uninstall Codex", + label: "Install Codex", action: this.installMenu.show, }, { - label: "Configure Codex", + label: "Exit", + action: this.loop.stopLoop, + }, + ]); + }; + + showRunningMenu = async () => { + await this.ui.askMultipleChoice("Codex is running", [ + { + label: "Stop Codex", + action: this.processControl.stopCodexProcess, + }, + { + label: "Open Codex app", + action: this.openCodexApp, + }, + { + label: "Exit", + action: this.loop.stopLoop, + }, + ]); + }; + + openCodexApp = async () => { + console.log("todo!"); + }; + + showCodexNotRunningMenu = async () => { + await this.ui.askMultipleChoice("Codex is not running", [ + { + label: "Start Codex", + action: this.processControl.startCodexProcess, + }, + { + label: "Edit Codex config", action: this.configMenu.show, }, + { + label: "Uninstall Codex", + action: this.installMenu.show, + }, { label: "Exit", action: this.loop.stopLoop, From 03dd11d57efd2b069d3e0fc39c1f0cb9ba0fb044 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 15 Apr 2025 14:01:44 +0200 Subject: [PATCH 33/59] wip --- src/__mocks__/handler.mocks.js | 6 ++ src/handlers/processControl.js | 11 ++- src/services/codexGlobals.js | 3 +- src/services/configService.test.js | 25 +++++- src/services/fsService.js | 1 - src/services/osService.js | 2 +- src/ui/mainMenu.js | 14 +-- src/ui/mainMenu.test.js | 132 ++++++++++++++++++++++++----- 8 files changed, 159 insertions(+), 35 deletions(-) diff --git a/src/__mocks__/handler.mocks.js b/src/__mocks__/handler.mocks.js index 5b8f8df..1780239 100644 --- a/src/__mocks__/handler.mocks.js +++ b/src/__mocks__/handler.mocks.js @@ -6,3 +6,9 @@ export const mockInstaller = { installCodex: vi.fn(), uninstallCodex: vi.fn(), }; + +export const mockProcessControl = { + getNumberOfCodexProcesses: vi.fn(), + stopCodexProcess: vi.fn(), + startCodexProcess: vi.fn(), +}; diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index cf4423a..8787991 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -27,12 +27,21 @@ export class ProcessControl { const pid = processes[0].pid; process.kill(pid, "SIGINT"); - await new Promise((resolve) => setTimeout(resolve, 2000)); + await this.sleep(); }; startCodexProcess = async () => { await this.saveCodexConfigFile(); await this.startCodex(); + await this.sleep(); + }; + + sleep = async () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 5000); + }); }; saveCodexConfigFile = async () => { diff --git a/src/services/codexGlobals.js b/src/services/codexGlobals.js index d48fa6c..b5bd3dc 100644 --- a/src/services/codexGlobals.js +++ b/src/services/codexGlobals.js @@ -2,7 +2,8 @@ import axios from "axios"; export class CodexGlobals { getPublicIp = async () => { - return (await axios.get(`https://ip.codex.storage`)).data; + const result = (await axios.get(`https://ip.codex.storage`)).data; + return result.replaceAll("\n", ""); }; getTestnetSPRs = async () => { diff --git a/src/services/configService.test.js b/src/services/configService.test.js index 84a99eb..9e7be04 100644 --- a/src/services/configService.test.js +++ b/src/services/configService.test.js @@ -86,6 +86,28 @@ describe("ConfigService", () => { }); }); + describe("validateConfiguration", () => { + var configService; + var config; + + beforeEach(() => { + config = expectedDefaultConfig; + + configService = new ConfigService(mockFsService); + configService.config = config; + }); + + it("throws when codexExe is not set", () => { + config.codexExe = ""; + + expect(configService.validateConfiguration).toThrow( + "Missing config value: codexExe", + ); + }); + + + }); + describe("writecodexConfigFile", () => { const logsPath = "C:\\path\\codex.log"; var configService; @@ -95,6 +117,7 @@ describe("ConfigService", () => { mockFsService.isFile.mockReturnValue(false); configService = new ConfigService(mockFsService); + configService.validateConfiguration = vi.fn(); configService.getLogFilePath = vi.fn(); configService.getLogFilePath.mockReturnValue(logsPath); }); @@ -126,7 +149,7 @@ describe("ConfigService", () => { .map((v) => { return '"' + v + '"'; }) - .join(",")}]`, + .join(",")}]${newLine}`, ); }); }); diff --git a/src/services/fsService.js b/src/services/fsService.js index 9b44a9a..68f108d 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -64,7 +64,6 @@ export class FsService { }; writeFile = (filePath, content) => { - console.log("filepath: " + filePath); fs.writeFileSync(filePath, content); }; } diff --git a/src/services/osService.js b/src/services/osService.js index efc12ef..6e9502b 100644 --- a/src/services/osService.js +++ b/src/services/osService.js @@ -23,6 +23,6 @@ export class OsService { }; listProcesses = async () => { - await psList(); + return await psList(); }; } diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index fc28d3f..0ae5703 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -26,11 +26,11 @@ export class MainMenu { }; promptMainMenu = async () => { - if ((await this.processControl.getNumberOfCodexProcesses) > 0) { + if ((await this.processControl.getNumberOfCodexProcesses()) > 0) { await this.showRunningMenu(); } else { if (await this.installer.isCodexInstalled()) { - await this.showCodexNotRunningMenu(); + await this.showNotRunningMenu(); } else { await this.showNotInstalledMenu(); } @@ -52,14 +52,14 @@ export class MainMenu { showRunningMenu = async () => { await this.ui.askMultipleChoice("Codex is running", [ - { - label: "Stop Codex", - action: this.processControl.stopCodexProcess, - }, { label: "Open Codex app", action: this.openCodexApp, }, + { + label: "Stop Codex", + action: this.processControl.stopCodexProcess, + }, { label: "Exit", action: this.loop.stopLoop, @@ -71,7 +71,7 @@ export class MainMenu { console.log("todo!"); }; - showCodexNotRunningMenu = async () => { + showNotRunningMenu = async () => { await this.ui.askMultipleChoice("Codex is not running", [ { label: "Start Codex", diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index 73dfed7..c464c9e 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -2,6 +2,10 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { MainMenu } from "./mainMenu.js"; import { mockUiService } from "../__mocks__/service.mocks.js"; import { mockInstallMenu, mockConfigMenu } from "../__mocks__/ui.mocks.js"; +import { + mockInstaller, + mockProcessControl, +} from "../__mocks__/handler.mocks.js"; import { mockMenuLoop } from "../__mocks__/utils.mocks.js"; describe("mainmenu", () => { @@ -15,42 +19,124 @@ describe("mainmenu", () => { mockMenuLoop, mockInstallMenu, mockConfigMenu, + mockInstaller, + mockProcessControl, ); }); - it("initializes the menu loop with the promptMainMenu function", () => { - expect(mockMenuLoop.initialize).toHaveBeenCalledWith( - mainmenu.promptMainMenu, - ); + describe("constructor", () => { + it("initializes the menu loop with the promptMainMenu function", () => { + expect(mockMenuLoop.initialize).toHaveBeenCalledWith( + mainmenu.promptMainMenu, + ); + }); }); - it("shows the logo", async () => { - await mainmenu.show(); + describe("show", () => { + it("shows the logo", async () => { + await mainmenu.show(); - expect(mockUiService.showLogo).toHaveBeenCalled(); + expect(mockUiService.showLogo).toHaveBeenCalled(); + }); + + it("starts the menu loop", async () => { + await mainmenu.show(); + + expect(mockMenuLoop.showLoop).toHaveBeenCalled(); + }); + + it("shows the exit message after the menu loop", async () => { + await mainmenu.show(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("K-THX-BYE"); + }); }); - it("starts the menu loop", async () => { - await mainmenu.show(); + describe("promptMainMenu", () => { + beforeEach(() => { + mainmenu.showRunningMenu = vi.fn(); + mainmenu.showNotRunningMenu = vi.fn(); + mainmenu.showNotInstalledMenu = vi.fn(); + }); - expect(mockMenuLoop.showLoop).toHaveBeenCalled(); + it("shows running menu when number of codex processes is greater than zero", async () => { + mockProcessControl.getNumberOfCodexProcesses.mockResolvedValue(1); + + await mainmenu.promptMainMenu(); + + expect(mainmenu.showRunningMenu).toHaveBeenCalled(); + expect(mainmenu.showNotRunningMenu).not.toHaveBeenCalled(); + expect(mainmenu.showNotInstalledMenu).not.toHaveBeenCalled(); + }); + + it("shows not running menu when number of codex processes is zero and codex is installed", async () => { + mockProcessControl.getNumberOfCodexProcesses.mockResolvedValue(0); + mockInstaller.isCodexInstalled.mockResolvedValue(true); + + await mainmenu.promptMainMenu(); + + expect(mainmenu.showRunningMenu).not.toHaveBeenCalled(); + expect(mainmenu.showNotRunningMenu).toHaveBeenCalled(); + expect(mainmenu.showNotInstalledMenu).not.toHaveBeenCalled(); + }); + + it("shows not installed menu when number of codex processes is zero and codex is not installed", async () => { + mockProcessControl.getNumberOfCodexProcesses.mockResolvedValue(0); + mockInstaller.isCodexInstalled.mockResolvedValue(false); + + await mainmenu.promptMainMenu(); + + expect(mainmenu.showRunningMenu).not.toHaveBeenCalled(); + expect(mainmenu.showNotRunningMenu).not.toHaveBeenCalled(); + expect(mainmenu.showNotInstalledMenu).toHaveBeenCalled(); + }); }); - it("shows the exit message after the menu loop", async () => { - await mainmenu.show(); + describe("showNotInstalledMenu", () => { + it("shows a menu with options to install Codex or exit", async () => { + await mainmenu.showNotInstalledMenu(); - expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("K-THX-BYE"); + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Codex is not installed", + [ + { label: "Install Codex", action: mockInstallMenu.show }, + { label: "Exit", action: mockMenuLoop.stopLoop }, + ], + ); + }); }); - it("prompts the main menu with multiple choices", async () => { - await mainmenu.promptMainMenu(); - expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( - "Select an option", - [ - { label: "Install/uninstall Codex", action: mockInstallMenu.show }, - { label: "Configure Codex", action: mockConfigMenu.show }, - { label: "Exit", action: mockMenuLoop.stopLoop }, - ], - ); + describe("showRunningMenu", () => { + it("shows a menu with options to stop Codex, open Codex app, or exit", async () => { + await mainmenu.showRunningMenu(); + + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Codex is running", + [ + { label: "Open Codex app", action: mainmenu.openCodexApp }, + { label: "Stop Codex", action: mockProcessControl.stopCodexProcess }, + { label: "Exit", action: mockMenuLoop.stopLoop }, + ], + ); + }); + }); + + describe("showNotRunningMenu", () => { + it("shows a menu with options to start Codex, configure, uninstall, or exit", async () => { + await mainmenu.showNotRunningMenu(); + + expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( + "Codex is not running", + [ + { + label: "Start Codex", + action: mockProcessControl.startCodexProcess, + }, + { label: "Edit Codex config", action: mockConfigMenu.show }, + { label: "Uninstall Codex", action: mockInstallMenu.show }, + { label: "Exit", action: mockMenuLoop.stopLoop }, + ], + ); + }); }); }); From 3ee64252001ff4ea7c02e00aaaab298c169bb96d Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 15 Apr 2025 15:22:39 +0200 Subject: [PATCH 34/59] fixes tests for configService --- src/services/configService.test.js | 50 +++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/services/configService.test.js b/src/services/configService.test.js index 9e7be04..70b6307 100644 --- a/src/services/configService.test.js +++ b/src/services/configService.test.js @@ -9,9 +9,8 @@ import { getCodexLogsDefaultPath, } from "../utils/appData.js"; -describe("ConfigService", () => { - const configPath = "/path/to/config.json"; - const expectedDefaultConfig = { +function getDefaultConfig() { + return { codexExe: "", codexInstallPath: getCodexBinPath(), codexConfigFilePath: getCodexConfigFilePath(), @@ -24,9 +23,15 @@ describe("ConfigService", () => { apiPort: 8080, }, }; +} + +describe("ConfigService", () => { + const configPath = "/path/to/config.json"; + var expectedDefaultConfig = getDefaultConfig(); beforeEach(() => { vi.resetAllMocks(); + expectedDefaultConfig = getDefaultConfig(); mockFsService.pathJoin.mockReturnValue(configPath); }); @@ -92,7 +97,8 @@ describe("ConfigService", () => { beforeEach(() => { config = expectedDefaultConfig; - + config.codexExe = "codex.exe"; + configService = new ConfigService(mockFsService); configService.config = config; }); @@ -105,7 +111,41 @@ describe("ConfigService", () => { ); }); - + it("throws when codexConfigFilePath is not set", () => { + config.codexConfigFilePath = ""; + + expect(configService.validateConfiguration).toThrow( + "Missing config value: codexConfigFilePath", + ); + }); + + it("throws when dataDir is not set", () => { + config.dataDir = ""; + + expect(configService.validateConfiguration).toThrow( + "Missing config value: dataDir", + ); + }); + + it("throws when logsDir is not set", () => { + config.logsDir = ""; + + expect(configService.validateConfiguration).toThrow( + "Missing config value: logsDir", + ); + }); + + it("throws when storageQuota is less than 100 MB", () => { + config.storageQuota = 1024 * 1024 * 99; + + expect(configService.validateConfiguration).toThrow( + "Storage quota must be at least 100MB", + ); + }); + + it("passes validation for default config when codexExe is set", () => { + expect(configService.validateConfiguration).not.toThrow(); + }); }); describe("writecodexConfigFile", () => { From 5f0e4105308055e97602fc97f6936fad01ce2752 Mon Sep 17 00:00:00 2001 From: ThatBen Date: Tue, 15 Apr 2025 15:54:46 +0200 Subject: [PATCH 35/59] fixes relative path requirement for datadir path. wip. --- src/__mocks__/service.mocks.js | 4 ++++ src/main.js | 4 +++- src/services/codexApp.js | 27 ++++++++++++++++----------- src/services/configService.js | 9 ++++++++- src/ui/mainMenu.js | 8 +++----- src/ui/mainMenu.test.js | 5 +++-- 6 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index ec36a5f..d9523df 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -48,3 +48,7 @@ export const mockCodexGlobals = { getPublicIp: vi.fn(), getTestnetSPRs: vi.fn(), }; + +export const mockCodexApp = { + openCodexApp: vi.fn(), +}; diff --git a/src/main.js b/src/main.js index ef50b55..f12da02 100644 --- a/src/main.js +++ b/src/main.js @@ -20,7 +20,6 @@ import { import { runCodex, checkNodeStatus } from "./handlers/nodeHandlers.js"; import { showInfoMessage } from "./utils/messages.js"; import { ConfigService } from "./services/configService.js"; -import { openCodexApp } from "./services/codexApp.js"; import { UiService } from "./services/uiService.js"; import { FsService } from "./services/fsService.js"; @@ -36,6 +35,7 @@ import { ShellService } from "./services/shellService.js"; import { OsService } from "./services/osService.js"; import { ProcessControl } from "./handlers/processControl.js"; import { CodexGlobals } from "./services/codexGlobals.js"; +import { CodexApp } from "./services/codexApp.js"; async function showNavigationMenu() { console.log("\n"); @@ -104,6 +104,7 @@ export async function main() { const uiService = new UiService(); const fsService = new FsService(); const configService = new ConfigService(fsService); + const codexApp = new CodexApp(configService); const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService); const numberSelector = new NumberSelector(uiService); const shellService = new ShellService(); @@ -142,6 +143,7 @@ export async function main() { configMenu, installer, processControl, + codexApp, ); await mainMenu.show(); diff --git a/src/services/codexApp.js b/src/services/codexApp.js index 838c1e8..a22f487 100644 --- a/src/services/codexApp.js +++ b/src/services/codexApp.js @@ -1,17 +1,22 @@ import open from "open"; -export function openCodexApp(config) { - // TODO: Update this to the main URL when the PR for adding api-port query parameter support - // has been merged and deployed. - // See: https://github.com/codex-storage/codex-marketplace-ui/issues/92 +export class CodexApp { + constructor(configService) { + this.configService = configService; + } - const segments = [ - "https://releases-v0-0-14.codex-marketplace-ui.pages.dev/", - "?", - `api-port=${config.ports.apiPort}`, - ]; + openCodexApp = async () => { + // TODO: Update this to the main URL when the PR for adding api-port query parameter support + // has been merged and deployed. + // See: https://github.com/codex-storage/codex-marketplace-ui/issues/92 - const url = segments.join(""); + const segments = [ + "https://releases-v0-0-14.codex-marketplace-ui.pages.dev/", + "?", + `api-port=${this.configService.get().ports.apiPort}`, + ]; - open(url); + const url = segments.join(""); + open(url); + }; } diff --git a/src/services/configService.js b/src/services/configService.js index 91169a5..c899b98 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -6,6 +6,8 @@ import { getCodexLogsDefaultPath, } from "../utils/appData.js"; +import path from "path"; + const defaultConfig = { codexExe: "", // User-selected config options: @@ -99,7 +101,7 @@ export class ConfigService { this.fs.writeFile( this.config.codexConfigFilePath, - `data-dir="${this.format(this.config.dataDir)}"${nl}` + + `data-dir="${this.format(this.toRelative(this.config.dataDir))}"${nl}` + `log-level="DEBUG"${nl}` + `log-file="${this.format(this.getLogFilePath())}"${nl}` + `storage-quota=${this.config.storageQuota}${nl}` + @@ -115,4 +117,9 @@ export class ConfigService { format = (str) => { return str.replaceAll("\\", "/"); }; + + toRelative = (str) => { + throw new Error("This code does not belong in this file. Move it."); + return path.relative(this.config.codexInstallPath, str); + }; } diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index 0ae5703..dd52439 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -6,6 +6,7 @@ export class MainMenu { configMenu, installer, processControl, + codexApp, ) { this.ui = uiService; this.loop = menuLoop; @@ -13,6 +14,7 @@ export class MainMenu { this.configMenu = configMenu; this.installer = installer; this.processControl = processControl; + this.codexApp = codexApp; this.loop.initialize(this.promptMainMenu); } @@ -54,7 +56,7 @@ export class MainMenu { await this.ui.askMultipleChoice("Codex is running", [ { label: "Open Codex app", - action: this.openCodexApp, + action: this.codexApp.openCodexApp, }, { label: "Stop Codex", @@ -67,10 +69,6 @@ export class MainMenu { ]); }; - openCodexApp = async () => { - console.log("todo!"); - }; - showNotRunningMenu = async () => { await this.ui.askMultipleChoice("Codex is not running", [ { diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index c464c9e..e05d2b6 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -1,6 +1,6 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { MainMenu } from "./mainMenu.js"; -import { mockUiService } from "../__mocks__/service.mocks.js"; +import { mockUiService, mockCodexApp } from "../__mocks__/service.mocks.js"; import { mockInstallMenu, mockConfigMenu } from "../__mocks__/ui.mocks.js"; import { mockInstaller, @@ -21,6 +21,7 @@ describe("mainmenu", () => { mockConfigMenu, mockInstaller, mockProcessControl, + mockCodexApp, ); }); @@ -113,7 +114,7 @@ describe("mainmenu", () => { expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( "Codex is running", [ - { label: "Open Codex app", action: mainmenu.openCodexApp }, + { label: "Open Codex app", action: mockCodexApp.openCodexApp }, { label: "Stop Codex", action: mockProcessControl.stopCodexProcess }, { label: "Exit", action: mockMenuLoop.stopLoop }, ], From c811b67807ba2ba91028a8f55407b9a1b4a9f52a Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 16 Apr 2025 09:42:11 +0200 Subject: [PATCH 36/59] moves relativePath method to fsService --- src/__mocks__/service.mocks.js | 1 + src/services/configService.js | 3 +-- src/services/configService.test.js | 10 +++++++++- src/services/fsService.js | 4 ++++ src/ui/mainMenu.js | 2 +- src/ui/mainMenu.test.js | 2 +- 6 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index d9523df..b399b1f 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -29,6 +29,7 @@ export const mockFsService = { readJsonFile: vi.fn(), writeJsonFile: vi.fn(), writeFile: vi.fn(), + toRelativePath: vi.fn(), }; export const mockShellService = { diff --git a/src/services/configService.js b/src/services/configService.js index c899b98..9185177 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -119,7 +119,6 @@ export class ConfigService { }; toRelative = (str) => { - throw new Error("This code does not belong in this file. Move it."); - return path.relative(this.config.codexInstallPath, str); + return this.fs.toRelativePath(this.config.codexInstallPath, str); }; } diff --git a/src/services/configService.test.js b/src/services/configService.test.js index 70b6307..ee58b0f 100644 --- a/src/services/configService.test.js +++ b/src/services/configService.test.js @@ -169,6 +169,9 @@ describe("ConfigService", () => { it("writes the config file values to the config TOML file", () => { const publicIp = "1.2.3.4"; const bootstrapNodes = ["boot111", "boot222", "boot333"]; + const relativeDataDirPath = "..\\../datadir"; + + mockFsService.toRelativePath.mockReturnValue(relativeDataDirPath); configService.writeCodexConfigFile(publicIp, bootstrapNodes); @@ -176,7 +179,7 @@ describe("ConfigService", () => { expect(mockFsService.writeFile).toHaveBeenCalledWith( expectedDefaultConfig.codexConfigFilePath, - `data-dir=\"${formatPath(expectedDefaultConfig.dataDir)}"${newLine}` + + `data-dir=\"${formatPath(relativeDataDirPath)}"${newLine}` + `log-level="DEBUG"${newLine}` + `log-file="${formatPath(logsPath)}"${newLine}` + `storage-quota=${expectedDefaultConfig.storageQuota}${newLine}` + @@ -191,6 +194,11 @@ describe("ConfigService", () => { }) .join(",")}]${newLine}`, ); + + expect(mockFsService.toRelativePath).toHaveBeenCalledWith( + expectedDefaultConfig.codexInstallPath, + expectedDefaultConfig.dataDir, + ); }); }); }); diff --git a/src/services/fsService.js b/src/services/fsService.js index 68f108d..52556d4 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -66,4 +66,8 @@ export class FsService { writeFile = (filePath, content) => { fs.writeFileSync(filePath, content); }; + + toRelativePath = (from, to) => { + return path.relative(from, to); + }; } diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index dd52439..bb399e7 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -70,7 +70,7 @@ export class MainMenu { }; showNotRunningMenu = async () => { - await this.ui.askMultipleChoice("Codex is not running", [ + await this.ui.askMultipleChoice("Codex is installed but not running", [ { label: "Start Codex", action: this.processControl.startCodexProcess, diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index e05d2b6..f31de9a 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -127,7 +127,7 @@ describe("mainmenu", () => { await mainmenu.showNotRunningMenu(); expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( - "Codex is not running", + "Codex is installed but not running", [ { label: "Start Codex", From c2f1f7cc7ba1acc82ef30d3d6595720c65d6fc07 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 16 Apr 2025 10:18:38 +0200 Subject: [PATCH 37/59] Adds spinners and error handling for process control in main menu --- src/ui/mainMenu.js | 26 +++++++++- src/ui/mainMenu.test.js | 110 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index bb399e7..8dcb365 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -60,7 +60,7 @@ export class MainMenu { }, { label: "Stop Codex", - action: this.processControl.stopCodexProcess, + action: this.stopCodex, }, { label: "Exit", @@ -73,7 +73,7 @@ export class MainMenu { await this.ui.askMultipleChoice("Codex is installed but not running", [ { label: "Start Codex", - action: this.processControl.startCodexProcess, + action: this.startCodex, }, { label: "Edit Codex config", @@ -89,4 +89,26 @@ export class MainMenu { }, ]); }; + + startCodex = async () => { + const spinner = this.ui.createAndStartSpinner("Starting..."); + try { + await this.processControl.startCodexProcess(); + this.ui.stopSpinnerSuccess(spinner); + } catch (exception) { + this.ui.stopSpinnerError(spinner); + this.ui.showErrorMessage(`Failed to start Codex. "${exception}"`); + } + }; + + stopCodex = async () => { + const spinner = this.ui.createAndStartSpinner("Stopping..."); + try { + await this.processControl.stopCodexProcess(); + this.ui.stopSpinnerSuccess(spinner); + } catch (exception) { + this.ui.stopSpinnerError(spinner); + this.ui.showErrorMessage(`Failed to stop Codex. "${exception}"`); + } + }; } diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index f31de9a..1f717e9 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -115,7 +115,7 @@ describe("mainmenu", () => { "Codex is running", [ { label: "Open Codex app", action: mockCodexApp.openCodexApp }, - { label: "Stop Codex", action: mockProcessControl.stopCodexProcess }, + { label: "Stop Codex", action: mainmenu.stopCodex }, { label: "Exit", action: mockMenuLoop.stopLoop }, ], ); @@ -131,7 +131,7 @@ describe("mainmenu", () => { [ { label: "Start Codex", - action: mockProcessControl.startCodexProcess, + action: mainmenu.startCodex, }, { label: "Edit Codex config", action: mockConfigMenu.show }, { label: "Uninstall Codex", action: mockInstallMenu.show }, @@ -140,4 +140,110 @@ describe("mainmenu", () => { ); }); }); + + describe("process control", () => { + const mockSpinner = { + isMock: "yes", + }; + + beforeEach(() => { + mockUiService.createAndStartSpinner.mockReturnValue(mockSpinner); + }); + + describe("startCodex", () => { + it("starts codex", async () => { + await mainmenu.startCodex(); + + expect(mockProcessControl.startCodexProcess).toHaveBeenCalled(); + }); + + it("shows error message when process control throws", async () => { + mockProcessControl.startCodexProcess.mockRejectedValueOnce( + new Error("A!"), + ); + + await mainmenu.startCodex(); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + 'Failed to start Codex. "Error: A!"', + ); + }); + + it("starts spinner", async () => { + await mainmenu.startCodex(); + + expect(mockUiService.createAndStartSpinner).toHaveBeenCalledWith( + "Starting...", + ); + }); + + it("stops spinner on success", async () => { + await mainmenu.startCodex(); + + expect(mockUiService.stopSpinnerSuccess).toHaveBeenCalledWith( + mockSpinner, + ); + }); + + it("stops spinner on failure", async () => { + mockProcessControl.startCodexProcess.mockRejectedValueOnce( + new Error("A!"), + ); + + await mainmenu.startCodex(); + + expect(mockUiService.stopSpinnerError).toHaveBeenCalledWith( + mockSpinner, + ); + }); + }); + + describe("stopCodex", () => { + it("stops codex", async () => { + await mainmenu.stopCodex(); + + expect(mockProcessControl.stopCodexProcess).toHaveBeenCalled(); + }); + + it("shows error message when process control throws", async () => { + mockProcessControl.stopCodexProcess.mockRejectedValueOnce( + new Error("A!"), + ); + + await mainmenu.stopCodex(); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + 'Failed to stop Codex. "Error: A!"', + ); + }); + + it("starts spinner", async () => { + await mainmenu.stopCodex(); + + expect(mockUiService.createAndStartSpinner).toHaveBeenCalledWith( + "Stopping...", + ); + }); + + it("stops spinner on success", async () => { + await mainmenu.stopCodex(); + + expect(mockUiService.stopSpinnerSuccess).toHaveBeenCalledWith( + mockSpinner, + ); + }); + + it("stops spinner on failure", async () => { + mockProcessControl.stopCodexProcess.mockRejectedValueOnce( + new Error("A!"), + ); + + await mainmenu.stopCodex(); + + expect(mockUiService.stopSpinnerError).toHaveBeenCalledWith( + mockSpinner, + ); + }); + }); + }); }); From 4dca83ddcb9f62403a4829270a322df0bd6bf562 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 16 Apr 2025 10:20:56 +0200 Subject: [PATCH 38/59] Pull in fixes from master --- package.json | 21 +++++---------------- src/constants/ascii.js | 10 +++++----- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 685a81e..bb34a90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codexstorage", - "version": "1.0.11", + "version": "1.0.16", "description": "CLI tool for Codex Storage", "main": "index.js", "type": "module", @@ -8,35 +8,24 @@ "codexstorage": "./index.js" }, "scripts": { - "start": "node index.js", - "test": "vitest run", - "watch": "vitest", - "format": "prettier --write ./src" + "start": "node index.js" }, "keywords": [ "codex", "storage", "cli" ], - "engines": { - "node": ">=20" - }, "author": "Codex Storage", "license": "MIT", "dependencies": { "axios": "^1.6.2", "boxen": "^7.1.1", "chalk": "^5.3.0", - "fs-extra": "^11.3.0", - "fs-filesystem": "^2.1.2", "inquirer": "^9.2.12", "mime-types": "^2.1.35", "nanospinner": "^1.1.0", - "open": "^10.1.0", - "ps-list": "^8.1.1" - }, - "devDependencies": { - "prettier": "^3.4.2", - "vitest": "^3.0.5" + "fs-extra": "^11.3.0", + "fs-filesystem": "^2.1.2", + "open": "^10.1.0" } } diff --git a/src/constants/ascii.js b/src/constants/ascii.js index bd7f477..3247832 100644 --- a/src/constants/ascii.js +++ b/src/constants/ascii.js @@ -1,13 +1,13 @@ export const ASCII_ART = ` -██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗ -██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝ +██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗ +██╔════╝██╔═══██╗██╔══██╗██╔═══╝ ╚██╗██╔╝ ██║ ██║ ██║██║ ██║█████╗ ╚███╔╝ ██║ ██║ ██║██║ ██║██╔══╝ ██╔██╗ -╚██████╗╚██████╔╝██████╔╝██████ ██╗ +╚██████╗╚██████╔╝██████╔╝███████╗██╗ ██╗ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ███████╗████████╗ ██████╗ ██████╗ █████╗ ██████╗ ███████╗ -██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔══██╗██╔════╝ █╔══��═╝ +██╔════╝╚══██╔══╝██╔═══██╗██╔══██╗██╔══██╗██╔════╝ █╔═════╝ ███████╗ ██║ ██║ ██║██████╔╝███████║██║ ███╗█████╗ ╚════██║ ██║ ██║ ██║██╔══██╗██╔══██║██║ ██║██╔══╝ ███████║ ██║ ╚██████╔╝██║ ██║██║ ██║╚██████╔╝███████╗ @@ -16,4 +16,4 @@ export const ASCII_ART = ` +--------------------------------------------------------------------+ | Docs : docs.codex.storage | Discord : discord.gg/codex-storage | +--------------------------------------------------------------------+ -`; +`; \ No newline at end of file From 29ceec82815b0744d431e96da5106d9c5d6d4290 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 16 Apr 2025 10:23:19 +0200 Subject: [PATCH 39/59] Restores package.json after merge --- package-lock.json | 4 ++-- package.json | 19 +++++++++++++++---- src/constants/ascii.js | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a5cb4d..bbf68dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codexstorage", - "version": "1.0.11", + "version": "1.0.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codexstorage", - "version": "1.0.11", + "version": "1.0.16", "license": "MIT", "dependencies": { "axios": "^1.6.2", diff --git a/package.json b/package.json index bb34a90..95ff60b 100644 --- a/package.json +++ b/package.json @@ -8,24 +8,35 @@ "codexstorage": "./index.js" }, "scripts": { - "start": "node index.js" + "start": "node index.js", + "test": "vitest run", + "watch": "vitest", + "format": "prettier --write ./src" }, "keywords": [ "codex", "storage", "cli" ], + "engines": { + "node": ">=20" + }, "author": "Codex Storage", "license": "MIT", "dependencies": { "axios": "^1.6.2", "boxen": "^7.1.1", "chalk": "^5.3.0", + "fs-extra": "^11.3.0", + "fs-filesystem": "^2.1.2", "inquirer": "^9.2.12", "mime-types": "^2.1.35", "nanospinner": "^1.1.0", - "fs-extra": "^11.3.0", - "fs-filesystem": "^2.1.2", - "open": "^10.1.0" + "open": "^10.1.0", + "ps-list": "^8.1.1" + }, + "devDependencies": { + "prettier": "^3.4.2", + "vitest": "^3.0.5" } } diff --git a/src/constants/ascii.js b/src/constants/ascii.js index 3247832..b55670d 100644 --- a/src/constants/ascii.js +++ b/src/constants/ascii.js @@ -16,4 +16,4 @@ export const ASCII_ART = ` +--------------------------------------------------------------------+ | Docs : docs.codex.storage | Discord : discord.gg/codex-storage | +--------------------------------------------------------------------+ -`; \ No newline at end of file +`; From 280b00802bcecc63b2524a1536730c8db1e7824c Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 10:41:44 +0200 Subject: [PATCH 40/59] Simplified path handling part 1 --- src/__mocks__/service.mocks.js | 8 +- src/handlers/installationHandlers.js | 1 - src/handlers/installer.js | 37 +++--- src/handlers/installer.test.js | 121 ++++++++------------ src/handlers/processControl.js | 12 +- src/handlers/processControl.test.js | 165 +++++++++++++++++++++++++++ src/main.js | 8 +- src/services/configService.js | 73 ++++-------- src/services/configService.test.js | 127 +++++++++------------ src/services/fsService.js | 7 +- src/services/osService.js | 4 + src/services/shellService.js | 3 +- src/ui/installMenu.js | 9 +- src/ui/installMenu.test.js | 11 +- src/utils/appData.js | 20 +--- 15 files changed, 347 insertions(+), 259 deletions(-) create mode 100644 src/handlers/processControl.test.js diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index b399b1f..8b90113 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -13,8 +13,10 @@ export const mockUiService = { export const mockConfigService = { get: vi.fn(), - saveConfig: vi.fn(), + getCodexExe: vi.fn(), + getCodexConfigFilePath: vi.fn(), loadConfig: vi.fn(), + saveConfig: vi.fn(), writeCodexConfigFile: vi.fn(), }; @@ -25,11 +27,12 @@ export const mockFsService = { isFile: vi.fn(), readDir: vi.fn(), makeDir: vi.fn(), + moveDir: vi.fn(), deleteDir: vi.fn(), readJsonFile: vi.fn(), writeJsonFile: vi.fn(), writeFile: vi.fn(), - toRelativePath: vi.fn(), + ensureDirExists: vi.fn(), }; export const mockShellService = { @@ -43,6 +46,7 @@ export const mockOsService = { isLinux: vi.fn(), getWorkingDir: vi.fn(), listProcesses: vi.fn(), + stopProcess: vi.fn(), }; export const mockCodexGlobals = { diff --git a/src/handlers/installationHandlers.js b/src/handlers/installationHandlers.js index d026c5c..5e3cddd 100644 --- a/src/handlers/installationHandlers.js +++ b/src/handlers/installationHandlers.js @@ -12,7 +12,6 @@ import { showSuccessMessage, } from "../utils/messages.js"; import { checkDependencies } from "../services/nodeService.js"; -import { getCodexRootPath, getCodexBinPath } from "../utils/appData.js"; const platform = os.platform(); diff --git a/src/handlers/installer.js b/src/handlers/installer.js index ca0eb4f..dffe6ec 100644 --- a/src/handlers/installer.js +++ b/src/handlers/installer.js @@ -17,14 +17,15 @@ export class Installer { }; getCodexVersion = async () => { - if (this.config.codexExe.length < 1) - throw new Error("Codex not installed."); - const version = await this.shell.run(`"${this.config.codexExe}" --version`); + const codexExe = this.configService.getCodexExe(); + if (!this.fs.isFile(codexExe)) throw new Error("Codex not installed."); + const version = await this.shell.run(`"${codexExe}" --version`); if (version.length < 1) throw new Error("Version info not found."); return version; }; installCodex = async (processCallbacks) => { + this.fs.ensureDirExists(this.config.codexRoot); if (!(await this.arePrerequisitesCorrect(processCallbacks))) return; processCallbacks.installStarts(); @@ -34,14 +35,15 @@ export class Installer { await this.installCodexUnix(processCallbacks); } - if (!(await this.isCodexInstalled())) + if (!(await this.isCodexInstalled())) { + processCallbacks.warn("Codex failed to install."); throw new Error("Codex installation failed."); + } processCallbacks.installSuccessful(); }; uninstallCodex = () => { - this.fs.deleteDir(this.config.codexInstallPath); - this.fs.deleteDir(this.config.dataDir); + this.fs.deleteDir(this.config.codexRoot); }; arePrerequisitesCorrect = async (processCallbacks) => { @@ -49,8 +51,8 @@ export class Installer { processCallbacks.warn("Codex is already installed."); return false; } - if (this.config.codexInstallPath.length < 1) { - processCallbacks.warn("Install path not set."); + if (!this.fs.isDir(this.config.codexRoot)) { + processCallbacks.warn("Root path doesn't exist."); return false; } if (!(await this.isCurlAvailable())) { @@ -71,10 +73,9 @@ export class Installer { ); processCallbacks.downloadSuccessful(); await this.shell.run( - `set "INSTALL_DIR=${this.config.codexInstallPath}" && ` + + `set "INSTALL_DIR=${this.config.codexRoot}" && ` + `"${this.os.getWorkingDir()}\\install.cmd"`, ); - await this.saveCodexInstallPath("codex.exe"); await this.shell.run("del /f install.cmd"); }; @@ -90,8 +91,6 @@ export class Installer { } else { await this.runInstallerLinux(); } - - await this.saveCodexInstallPath("codex"); await this.shell.run("rm -f install.sh"); }; @@ -100,7 +99,7 @@ export class Installer { eval { local $SIG{ALRM} = sub { die "timeout\\n" }; alarm(120); - system("INSTALL_DIR=\\"${this.config.codexInstallPath}\\" bash install.sh"); + system("INSTALL_DIR=\\"${this.config.codexRoot}\\" bash install.sh"); alarm(0); }; die if $@; @@ -110,7 +109,7 @@ export class Installer { runInstallerLinux = async () => { await this.shell.run( - `INSTALL_DIR="${this.config.codexInstallPath}" timeout 120 bash install.sh`, + `INSTALL_DIR="${this.config.codexRoot}" timeout 120 bash install.sh`, ); }; @@ -122,14 +121,4 @@ export class Installer { } return true; }; - - saveCodexInstallPath = async (codexExe) => { - this.config.codexExe = this.fs.pathJoin([ - this.config.codexInstallPath, - codexExe, - ]); - if (!this.fs.isFile(this.config.codexExe)) - throw new Error("Codex executable not found."); - await this.configService.saveConfig(); - }; } diff --git a/src/handlers/installer.test.js b/src/handlers/installer.test.js index 3eea1cf..bcbbff0 100644 --- a/src/handlers/installer.test.js +++ b/src/handlers/installer.test.js @@ -9,9 +9,10 @@ import { Installer } from "./installer.js"; describe("Installer", () => { const config = { - codexInstallPath: "/install-codex", + codexRoot: "/codex-root", }; const workingDir = "/working-dir"; + const exe = "abc.exe"; const processCallbacks = { installStarts: vi.fn(), downloadSuccessful: vi.fn(), @@ -24,6 +25,7 @@ describe("Installer", () => { vi.resetAllMocks(); mockConfigService.get.mockReturnValue(config); mockOsService.getWorkingDir.mockReturnValue(workingDir); + mockConfigService.getCodexExe.mockReturnValue(exe); installer = new Installer( mockConfigService, @@ -34,15 +36,22 @@ describe("Installer", () => { }); describe("getCodexVersion", () => { - it("throws when codex exe is not set", async () => { - config.codexExe = ""; + it("checks if the codex exe file exists", async () => { + mockFsService.isFile.mockReturnValue(true); + mockShellService.run.mockResolvedValueOnce("a"); + await installer.getCodexVersion(); + expect(mockFsService.isFile).toHaveBeenCalledWith(exe); + }); + + it("throws when codex exe is not a file", async () => { + mockFsService.isFile.mockReturnValue(false); await expect(installer.getCodexVersion()).rejects.toThrow( "Codex not installed.", ); }); it("throws when version info is not found", async () => { - config.codexExe = "codex.exe"; + mockFsService.isFile.mockReturnValue(true); mockShellService.run.mockResolvedValueOnce(""); await expect(installer.getCodexVersion()).rejects.toThrow( "Version info not found.", @@ -50,8 +59,8 @@ describe("Installer", () => { }); it("returns version info", async () => { + mockFsService.isFile.mockReturnValue(true); const versionInfo = "versionInfo"; - config.codexExe = "codex.exe"; mockShellService.run.mockResolvedValueOnce(versionInfo); const version = await installer.getCodexVersion(); expect(version).toBe(versionInfo); @@ -80,6 +89,15 @@ describe("Installer", () => { installer.isCodexInstalled = vi.fn(); }); + it("ensures codex root dir exists", async () => { + installer.arePrerequisitesCorrect.mockResolvedValue(false); + await installer.installCodex(processCallbacks); + + expect(mockFsService.ensureDirExists).toHaveBeenCalledWith( + config.codexRoot, + ); + }); + it("returns early when prerequisites are not correct", async () => { installer.arePrerequisitesCorrect.mockResolvedValue(false); await installer.installCodex(processCallbacks); @@ -125,6 +143,16 @@ describe("Installer", () => { ); }); + it("warns user when codex is not installed after installation", async () => { + installer.isCodexInstalled.mockResolvedValue(false); + await expect(installer.installCodex(processCallbacks)).rejects.toThrow( + "Codex installation failed.", + ); + expect(processCallbacks.warn).toHaveBeenCalledWith( + "Codex failed to install.", + ); + }); + it("calls installSuccessful when installation is successful", async () => { await installer.installCodex(processCallbacks); expect(processCallbacks.installSuccessful).toHaveBeenCalled(); @@ -136,7 +164,6 @@ describe("Installer", () => { beforeEach(() => { installer.isCodexInstalled = vi.fn(); installer.isCurlAvailable = vi.fn(); - config.codexInstallPath = "/install-codex"; }); it("returns false when codex is already installed", async () => { @@ -149,18 +176,26 @@ describe("Installer", () => { ); }); - it("returns false when install path is not set", async () => { - config.codexInstallPath = ""; + it("checks if the root path exists", async () => { + expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe( + false, + ); + expect(mockFsService.isDir).toHaveBeenCalledWith(config.codexRoot); + }); + + it("returns false when root path does not exist", async () => { + mockFsService.isDir.mockReturnValue(false); expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe( false, ); expect(processCallbacks.warn).toHaveBeenCalledWith( - "Install path not set.", + "Root path doesn't exist.", ); }); it("returns false when curl is not available", async () => { installer.isCodexInstalled.mockResolvedValue(false); + mockFsService.isDir.mockReturnValue(true); installer.isCurlAvailable.mockResolvedValue(false); expect(await installer.arePrerequisitesCorrect(processCallbacks)).toBe( false, @@ -172,6 +207,7 @@ describe("Installer", () => { it("returns true when all prerequisites are correct", async () => { installer.isCodexInstalled.mockResolvedValue(false); + mockFsService.isDir.mockReturnValue(true); installer.isCurlAvailable.mockResolvedValue(true); const result = await installer.arePrerequisitesCorrect(processCallbacks); expect(result).toBe(true); @@ -215,14 +251,7 @@ describe("Installer", () => { it("runs installer script", async () => { await installer.installCodexWindows(processCallbacks); expect(mockShellService.run).toHaveBeenCalledWith( - `set "INSTALL_DIR=${config.codexInstallPath}" && "${workingDir}\\install.cmd"`, - ); - }); - - it("saves the codex install path", async () => { - await installer.installCodexWindows(processCallbacks); - expect(installer.saveCodexInstallPath).toHaveBeenCalledWith( - "codex.exe", + `set "INSTALL_DIR=${config.codexRoot}" && "${workingDir}\\install.cmd"`, ); }); @@ -285,11 +314,6 @@ describe("Installer", () => { expect(installer.runInstallerLinux).toHaveBeenCalled(); }); - it("saves the codex install path", async () => { - await installer.installCodexUnix(processCallbacks); - expect(installer.saveCodexInstallPath).toHaveBeenCalledWith("codex"); - }); - it("deletes the installer script", async () => { await installer.installCodexUnix(processCallbacks); expect(mockShellService.run).toHaveBeenCalledWith("rm -f install.sh"); @@ -303,7 +327,7 @@ describe("Installer", () => { eval { local $SIG{ALRM} = sub { die "timeout\\n" }; alarm(120); - system("INSTALL_DIR=\\"${config.codexInstallPath}\\" bash install.sh"); + system("INSTALL_DIR=\\"${config.codexRoot}\\" bash install.sh"); alarm(0); }; die if $@; @@ -317,7 +341,7 @@ describe("Installer", () => { it("runs the installer script using unix timeout command", async () => { await installer.runInstallerLinux(); expect(mockShellService.run).toHaveBeenCalledWith( - `INSTALL_DIR="${config.codexInstallPath}" timeout 120 bash install.sh`, + `INSTALL_DIR="${config.codexRoot}" timeout 120 bash install.sh`, ); }); }); @@ -351,56 +375,11 @@ describe("Installer", () => { }); }); - describe("saveCodexInstallPath", () => { - const codexExe = "_codex_.exe"; - const pathJointResult = "/path-to-codex/_codex_.exe"; - - beforeEach(() => { - mockFsService.pathJoin.mockReturnValue(pathJointResult); - }); - - it("combines the install path with the exe", async () => { - mockFsService.isFile.mockReturnValue(true); - await installer.saveCodexInstallPath(codexExe); - expect(mockFsService.pathJoin).toHaveBeenCalledWith([ - config.codexInstallPath, - codexExe, - ]); - }); - - it("sets the codex exe path", async () => { - mockFsService.isFile.mockReturnValue(true); - await installer.saveCodexInstallPath(codexExe); - expect(config.codexExe).toBe(pathJointResult); - }); - - it("throws when file does not exist", async () => { - mockFsService.isFile.mockReturnValue(false); - await expect(installer.saveCodexInstallPath(codexExe)).rejects.toThrow( - "Codex executable not found.", - ); - }); - - it("saves the config", async () => { - mockFsService.isFile.mockReturnValue(true); - await installer.saveCodexInstallPath(codexExe); - expect(mockConfigService.saveConfig).toHaveBeenCalled(); - }); - }); - describe("uninstallCodex", () => { - it("deletes the codex install path", () => { + it("deletes the codex root path", () => { installer.uninstallCodex(); - expect(mockFsService.deleteDir).toHaveBeenCalledWith( - config.codexInstallPath, - ); - }); - - it("deletes the codex data path", () => { - installer.uninstallCodex(); - - expect(mockFsService.deleteDir).toHaveBeenCalledWith(config.dataDir); + expect(mockFsService.deleteDir).toHaveBeenCalledWith(config.codexRoot); }); }); }); diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 8787991..407d830 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -1,7 +1,6 @@ export class ProcessControl { constructor(configService, shellService, osService, fsService, codexGlobals) { this.configService = configService; - this.config = configService.get(); this.shell = shellService; this.os = osService; this.fs = fsService; @@ -26,7 +25,7 @@ export class ProcessControl { if (processes.length < 1) throw new Error("No codex process found"); const pid = processes[0].pid; - process.kill(pid, "SIGINT"); + this.os.stopProcess(pid); await this.sleep(); }; @@ -51,8 +50,11 @@ export class ProcessControl { }; startCodex = async () => { - const executable = this.config.codexExe; - const args = [`--config-file=${this.config.codexConfigFilePath}`]; - await this.shell.spawnDetachedProcess(executable, args); + const executable = this.configService.getCodexExe(); + const workingDir = this.configService.get().codexRoot; + const args = [ + `--config-file=${this.configService.getCodexConfigFilePath()}`, + ]; + await this.shell.spawnDetachedProcess(executable, workingDir, args); }; } diff --git a/src/handlers/processControl.test.js b/src/handlers/processControl.test.js new file mode 100644 index 0000000..70a6963 --- /dev/null +++ b/src/handlers/processControl.test.js @@ -0,0 +1,165 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { + mockShellService, + mockOsService, + mockFsService, + mockCodexGlobals, +} from "../__mocks__/service.mocks.js"; +import { mockConfigService } from "../__mocks__/service.mocks.js"; +import { ProcessControl } from "./processControl.js"; + +describe("ProcessControl", () => { + let processControl; + + beforeEach(() => { + vi.resetAllMocks(); + + processControl = new ProcessControl( + mockConfigService, + mockShellService, + mockOsService, + mockFsService, + mockCodexGlobals, + ); + + processControl.sleep = vi.fn(); + }); + + describe("getCodexProcesses", () => { + const processes = [ + { id: 0, name: "a.exe" }, + { id: 1, name: "aaa" }, + { id: 2, name: "codex" }, + { id: 3, name: "codex.exe" }, + { id: 4, name: "notcodex" }, + { id: 5, name: "alsonotcodex.exe" }, + ]; + + beforeEach(() => { + mockOsService.listProcesses.mockResolvedValue(processes); + }); + + it("returns codex.exe processes on windows", async () => { + mockOsService.isWindows.mockReturnValue(true); + + const p = await processControl.getCodexProcesses(); + + expect(p.length).toBe(1); + expect(p[0]).toBe(processes[3]); + }); + + it("returns codex processes on non-windows", async () => { + mockOsService.isWindows.mockReturnValue(false); + + const p = await processControl.getCodexProcesses(); + + expect(p.length).toBe(1); + expect(p[0]).toBe(processes[2]); + }); + }); + + describe("getNumberOfCodexProcesses", () => { + it("counts the results of getCodexProcesses", async () => { + processControl.getCodexProcesses = vi.fn(); + processControl.getCodexProcesses.mockResolvedValue(["a", "b", "c"]); + + expect(await processControl.getNumberOfCodexProcesses()).toBe(3); + }); + }); + + describe("stopCodexProcess", () => { + beforeEach(() => { + processControl.getCodexProcesses = vi.fn(); + }); + + it("throws when no codex processes are found", async () => { + processControl.getCodexProcesses.mockResolvedValue([]); + + await expect(processControl.stopCodexProcess).rejects.toThrow( + "No codex process found", + ); + }); + + it("stops the first codex process", async () => { + const pid = 12345; + processControl.getCodexProcesses.mockResolvedValue([ + { pid: pid }, + { pid: 111 }, + { pid: 222 }, + ]); + + await processControl.stopCodexProcess(); + + expect(mockOsService.stopProcess).toHaveBeenCalledWith(pid); + }); + + it("sleeps", async () => { + processControl.getCodexProcesses.mockResolvedValue([ + { pid: 111 }, + { pid: 222 }, + ]); + + await processControl.stopCodexProcess(); + + expect(processControl.sleep).toHaveBeenCalled(); + }); + }); + + describe("startCodexProcess", () => { + beforeEach(() => { + processControl.saveCodexConfigFile = vi.fn(); + processControl.startCodex = vi.fn(); + }); + + it("saves the config, starts codex, and sleeps", async () => { + await processControl.startCodexProcess(); + + expect(processControl.saveCodexConfigFile).toHaveBeenCalled(); + expect(processControl.startCodex).toHaveBeenCalled(); + expect(processControl.sleep).toHaveBeenCalled(); + }); + }); + + describe("saveCodexConfigFile", () => { + const publicIp = "1.2.3.4"; + const bootNodes = ["a", "b", "c"]; + + beforeEach(() => { + mockCodexGlobals.getPublicIp.mockResolvedValue(publicIp); + mockCodexGlobals.getTestnetSPRs.mockResolvedValue(bootNodes); + }); + + it("writes codex config file using public IP and testnet bootstrap nodes", async () => { + await processControl.saveCodexConfigFile(); + + expect(mockConfigService.writeCodexConfigFile).toHaveBeenCalledWith( + publicIp, + bootNodes, + ); + }); + }); + + describe("startCodex", () => { + const config = { + codexRoot: "/codex-root", + }; + const exe = "abc.exe"; + const configFile = "/codex/config.toml"; + + beforeEach(() => { + mockConfigService.getCodexExe.mockReturnValue(exe); + mockConfigService.get.mockReturnValue(config); + mockConfigService.getCodexConfigFilePath.mockReturnValue(configFile); + }); + + it("spawns a detached codex process in the codex root working directory with the config file as argument", async () => { + await processControl.startCodex(); + + expect(mockShellService.spawnDetachedProcess).toHaveBeenCalledWith( + exe, + config.codexRoot, + [`--config-file=${configFile}`], + ); + }); + }); +}); diff --git a/src/main.js b/src/main.js index f12da02..bef9420 100644 --- a/src/main.js +++ b/src/main.js @@ -103,12 +103,12 @@ export async function main() { const codexGlobals = new CodexGlobals(); const uiService = new UiService(); const fsService = new FsService(); - const configService = new ConfigService(fsService); - const codexApp = new CodexApp(configService); - const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService); - const numberSelector = new NumberSelector(uiService); const shellService = new ShellService(); const osService = new OsService(); + const numberSelector = new NumberSelector(uiService); + const configService = new ConfigService(fsService, osService); + const codexApp = new CodexApp(configService); + const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService); const installer = new Installer( configService, shellService, diff --git a/src/services/configService.js b/src/services/configService.js index 9185177..73627e6 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -1,20 +1,7 @@ -import { - getAppDataDir, - getCodexBinPath, - getCodexConfigFilePath, - getCodexDataDirDefaultPath, - getCodexLogsDefaultPath, -} from "../utils/appData.js"; - -import path from "path"; +import { getAppDataDir, getDefaultCodexRootPath } from "../utils/appData.js"; const defaultConfig = { - codexExe: "", - // User-selected config options: - codexInstallPath: getCodexBinPath(), - codexConfigFilePath: getCodexConfigFilePath(), - dataDir: getCodexDataDirDefaultPath(), - logsDir: getCodexLogsDefaultPath(), + codexRoot: getDefaultCodexRootPath(), storageQuota: 8 * 1024 * 1024 * 1024, ports: { discPort: 8090, @@ -23,9 +10,14 @@ const defaultConfig = { }, }; +const datadir = "datadir"; +const codexLogFile = "codex.log"; +const codexConfigFile = "config.toml"; + export class ConfigService { - constructor(fsService) { + constructor(fsService, osService) { this.fs = fsService; + this.os = osService; this.loadConfig(); } @@ -33,6 +25,19 @@ export class ConfigService { return this.config; }; + getCodexExe = () => { + var codexExe = "codex"; + if (this.os.isWindows()) { + codexExe = "codex.exe"; + } + + return this.fs.pathJoin([this.config.codexRoot, codexExe]); + }; + + getCodexConfigFilePath = () => { + return this.fs.pathJoin([this.config.codexRoot, codexConfigFile]); + }; + loadConfig = () => { const filePath = this.getConfigFilename(); try { @@ -66,29 +71,7 @@ export class ConfigService { return this.fs.pathJoin([getAppDataDir(), "config.json"]); }; - getLogFilePath = () => { - // function getCurrentLogFile(config) { - // const timestamp = new Date() - // .toISOString() - // .replaceAll(":", "-") - // .replaceAll(".", "-"); - // return path.join(config.logsDir, `codex_${timestamp}.log`); - // } - // todo, maybe use timestamp - - return this.fs.pathJoin([this.config.logsDir, "codex.log"]); - }; - - missing = (name) => { - throw new Error(`Missing config value: ${name}`); - }; - validateConfiguration = () => { - if (this.config.codexExe.length < 1) this.missing("codexExe"); - if (this.config.codexConfigFilePath.length < 1) - this.missing("codexConfigFilePath"); - if (this.config.dataDir.length < 1) this.missing("dataDir"); - if (this.config.logsDir.length < 1) this.missing("logsDir"); if (this.config.storageQuota < 1024 * 1024 * 100) throw new Error("Storage quota must be at least 100MB"); }; @@ -100,10 +83,10 @@ export class ConfigService { const bootNodes = bootstrapNodes.map((v) => `"${v}"`).join(","); this.fs.writeFile( - this.config.codexConfigFilePath, - `data-dir="${this.format(this.toRelative(this.config.dataDir))}"${nl}` + + this.getCodexConfigFilePath(), + `data-dir="${datadir}"${nl}` + `log-level="DEBUG"${nl}` + - `log-file="${this.format(this.getLogFilePath())}"${nl}` + + `log-file="${codexLogFile}"${nl}` + `storage-quota=${this.config.storageQuota}${nl}` + `disc-port=${this.config.ports.discPort}${nl}` + `listen-addrs=["/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}"]${nl}` + @@ -113,12 +96,4 @@ export class ConfigService { `bootstrap-node=[${bootNodes}]${nl}`, ); }; - - format = (str) => { - return str.replaceAll("\\", "/"); - }; - - toRelative = (str) => { - return this.fs.toRelativePath(this.config.codexInstallPath, str); - }; } diff --git a/src/services/configService.test.js b/src/services/configService.test.js index ee58b0f..990fe61 100644 --- a/src/services/configService.test.js +++ b/src/services/configService.test.js @@ -1,21 +1,11 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { ConfigService } from "./configService.js"; -import { mockFsService } from "../__mocks__/service.mocks.js"; -import { - getAppDataDir, - getCodexBinPath, - getCodexConfigFilePath, - getCodexDataDirDefaultPath, - getCodexLogsDefaultPath, -} from "../utils/appData.js"; +import { mockFsService, mockOsService } from "../__mocks__/service.mocks.js"; +import { getAppDataDir, getDefaultCodexRootPath } from "../utils/appData.js"; function getDefaultConfig() { return { - codexExe: "", - codexInstallPath: getCodexBinPath(), - codexConfigFilePath: getCodexConfigFilePath(), - dataDir: getCodexDataDirDefaultPath(), - logsDir: getCodexLogsDefaultPath(), + codexRoot: getDefaultCodexRootPath(), storageQuota: 8 * 1024 * 1024 * 1024, ports: { discPort: 8090, @@ -38,7 +28,7 @@ describe("ConfigService", () => { describe("constructor", () => { it("formats the config file path", () => { - new ConfigService(mockFsService); + new ConfigService(mockFsService, mockOsService); expect(mockFsService.pathJoin).toHaveBeenCalledWith([ getAppDataDir(), @@ -49,7 +39,7 @@ describe("ConfigService", () => { it("saves the default config when the config.json file does not exist", () => { mockFsService.isFile.mockReturnValue(false); - const service = new ConfigService(mockFsService); + const service = new ConfigService(mockFsService, mockOsService); expect(mockFsService.isFile).toHaveBeenCalledWith(configPath); expect(mockFsService.readJsonFile).not.toHaveBeenCalled(); @@ -67,7 +57,7 @@ describe("ConfigService", () => { }; mockFsService.readJsonFile.mockReturnValue(savedConfig); - const service = new ConfigService(mockFsService); + const service = new ConfigService(mockFsService, mockOsService); expect(mockFsService.isFile).toHaveBeenCalledWith(configPath); expect(mockFsService.readJsonFile).toHaveBeenCalledWith(configPath); @@ -76,17 +66,48 @@ describe("ConfigService", () => { }); }); - describe("getLogFilePath", () => { - it("joins the logsDir with the log filename", () => { - const service = new ConfigService(mockFsService); + describe("getCodexExe", () => { + var configService; + const result = "path/to/codex"; - const result = "path/to/codex.log"; + beforeEach(() => { + mockFsService.isFile.mockReturnValue(false); mockFsService.pathJoin.mockReturnValue(result); + configService = new ConfigService(mockFsService, mockOsService); + }); - expect(service.getLogFilePath()).toBe(result); + it("joins the codex root with the non-Windows specific exe name", () => { + mockOsService.isWindows.mockReturnValue(false); + + expect(configService.getCodexExe()).toBe(result); expect(mockFsService.pathJoin).toHaveBeenCalledWith([ - expectedDefaultConfig.logsDir, - "codex.log", + expectedDefaultConfig.codexRoot, + "codex", + ]); + }); + + it("joins the codex root with the Windows specific exe name", () => { + mockOsService.isWindows.mockReturnValue(true); + + expect(configService.getCodexExe()).toBe(result); + expect(mockFsService.pathJoin).toHaveBeenCalledWith([ + expectedDefaultConfig.codexRoot, + "codex.exe", + ]); + }); + }); + + describe("getCodexConfigFilePath", () => { + const result = "path/to/codex"; + + it("joins the codex root and codexConfigFile", () => { + mockFsService.pathJoin.mockReturnValue(result); + const configService = new ConfigService(mockFsService, mockOsService); + + expect(configService.getCodexConfigFilePath()).toBe(result); + expect(mockFsService.pathJoin).toHaveBeenCalledWith([ + expectedDefaultConfig.codexRoot, + "config.toml", ]); }); }); @@ -99,42 +120,10 @@ describe("ConfigService", () => { config = expectedDefaultConfig; config.codexExe = "codex.exe"; - configService = new ConfigService(mockFsService); + configService = new ConfigService(mockFsService, mockOsService); configService.config = config; }); - it("throws when codexExe is not set", () => { - config.codexExe = ""; - - expect(configService.validateConfiguration).toThrow( - "Missing config value: codexExe", - ); - }); - - it("throws when codexConfigFilePath is not set", () => { - config.codexConfigFilePath = ""; - - expect(configService.validateConfiguration).toThrow( - "Missing config value: codexConfigFilePath", - ); - }); - - it("throws when dataDir is not set", () => { - config.dataDir = ""; - - expect(configService.validateConfiguration).toThrow( - "Missing config value: dataDir", - ); - }); - - it("throws when logsDir is not set", () => { - config.logsDir = ""; - - expect(configService.validateConfiguration).toThrow( - "Missing config value: logsDir", - ); - }); - it("throws when storageQuota is less than 100 MB", () => { config.storageQuota = 1024 * 1024 * 99; @@ -148,7 +137,7 @@ describe("ConfigService", () => { }); }); - describe("writecodexConfigFile", () => { + describe("writeCodexConfigFile", () => { const logsPath = "C:\\path\\codex.log"; var configService; @@ -156,32 +145,31 @@ describe("ConfigService", () => { // use the default config: mockFsService.isFile.mockReturnValue(false); - configService = new ConfigService(mockFsService); + configService = new ConfigService(mockFsService, mockOsService); configService.validateConfiguration = vi.fn(); configService.getLogFilePath = vi.fn(); configService.getLogFilePath.mockReturnValue(logsPath); }); - function formatPath(str) { - return str.replaceAll("\\", "/"); - } - it("writes the config file values to the config TOML file", () => { const publicIp = "1.2.3.4"; const bootstrapNodes = ["boot111", "boot222", "boot333"]; - const relativeDataDirPath = "..\\../datadir"; + const expectedDataDir = "datadir"; + const expectedLogFile = "codex.log"; + const codexConfigFilePath = "/path/to/config.toml"; - mockFsService.toRelativePath.mockReturnValue(relativeDataDirPath); + configService.getCodexConfigFilePath = vi.fn(); + configService.getCodexConfigFilePath.mockReturnValue(codexConfigFilePath); configService.writeCodexConfigFile(publicIp, bootstrapNodes); const newLine = "\n"; expect(mockFsService.writeFile).toHaveBeenCalledWith( - expectedDefaultConfig.codexConfigFilePath, - `data-dir=\"${formatPath(relativeDataDirPath)}"${newLine}` + + codexConfigFilePath, + `data-dir=\"${expectedDataDir}"${newLine}` + `log-level="DEBUG"${newLine}` + - `log-file="${formatPath(logsPath)}"${newLine}` + + `log-file="${expectedLogFile}"${newLine}` + `storage-quota=${expectedDefaultConfig.storageQuota}${newLine}` + `disc-port=${expectedDefaultConfig.ports.discPort}${newLine}` + `listen-addrs=["/ip4/0.0.0.0/tcp/${expectedDefaultConfig.ports.listenPort}"]${newLine}` + @@ -194,11 +182,6 @@ describe("ConfigService", () => { }) .join(",")}]${newLine}`, ); - - expect(mockFsService.toRelativePath).toHaveBeenCalledWith( - expectedDefaultConfig.codexInstallPath, - expectedDefaultConfig.dataDir, - ); }); }); }); diff --git a/src/services/fsService.js b/src/services/fsService.js index 52556d4..67ac664 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -67,7 +67,10 @@ export class FsService { fs.writeFileSync(filePath, content); }; - toRelativePath = (from, to) => { - return path.relative(from, to); + ensureDirExists = (dir) => { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + return dir; }; } diff --git a/src/services/osService.js b/src/services/osService.js index 6e9502b..2778b58 100644 --- a/src/services/osService.js +++ b/src/services/osService.js @@ -25,4 +25,8 @@ export class OsService { listProcesses = async () => { return await psList(); }; + + stopProcess = (pid) => { + process.kill(pid, "SIGINT"); + }; } diff --git a/src/services/shellService.js b/src/services/shellService.js index 79ae419..6fde68e 100644 --- a/src/services/shellService.js +++ b/src/services/shellService.js @@ -16,8 +16,9 @@ export class ShellService { } }; - spawnDetachedProcess = async (cmd, args) => { + spawnDetachedProcess = async (cmd, workingDir, args) => { var child = spawn(cmd, args, { + cwd: workingDir, detached: true, stdio: ["ignore", "ignore", "ignore"], }); diff --git a/src/ui/installMenu.js b/src/ui/installMenu.js index 6ce3053..f87069d 100644 --- a/src/ui/installMenu.js +++ b/src/ui/installMenu.js @@ -18,7 +18,7 @@ export class InstallMenu { showInstallMenu = async () => { await this.ui.askMultipleChoice("Configure your Codex installation", [ { - label: "Install path: " + this.config.codexInstallPath, + label: "Install path: " + this.config.codexRoot, action: this.selectInstallPath, }, { @@ -53,7 +53,8 @@ export class InstallMenu { this.ui.showInfoMessage( "You are about to:\n" + " - Uninstall the Codex application\n" + - " - Delete the data stored in your Codex node", + " - Delete the data stored in your Codex node\n" + + " - Delete the log files of your Codex node", ); await this.ui.askMultipleChoice( @@ -72,8 +73,8 @@ export class InstallMenu { }; selectInstallPath = async () => { - this.config.codexInstallPath = await this.pathSelector.show( - this.config.codexInstallPath, + this.config.codexRoot = await this.pathSelector.show( + this.config.codexRoot, false, ); this.configService.saveConfig(); diff --git a/src/ui/installMenu.test.js b/src/ui/installMenu.test.js index 1b47cd4..6dd281f 100644 --- a/src/ui/installMenu.test.js +++ b/src/ui/installMenu.test.js @@ -7,7 +7,7 @@ import { mockInstaller } from "../__mocks__/handler.mocks.js"; describe("InstallMenu", () => { const config = { - codexInstallPath: "/codex", + codexRoot: "/codex", }; let installMenu; @@ -52,7 +52,7 @@ describe("InstallMenu", () => { "Configure your Codex installation", [ { - label: "Install path: " + config.codexInstallPath, + label: "Install path: " + config.codexRoot, action: installMenu.selectInstallPath, }, { @@ -94,7 +94,8 @@ describe("InstallMenu", () => { expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( "You are about to:\n" + " - Uninstall the Codex application\n" + - " - Delete the data stored in your Codex node", + " - Delete the data stored in your Codex node\n" + + " - Delete the log files of your Codex node", ); expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( @@ -113,14 +114,14 @@ describe("InstallMenu", () => { }); it("allows selecting the install path", async () => { - const originalPath = config.codexInstallPath; + const originalPath = config.codexRoot; const newPath = "/new/path"; mockPathSelector.show.mockResolvedValue(newPath); await installMenu.selectInstallPath(); expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, false); - expect(config.codexInstallPath).toBe(newPath); + expect(config.codexRoot).toBe(newPath); expect(mockConfigService.saveConfig).toHaveBeenCalled(); }); diff --git a/src/utils/appData.js b/src/utils/appData.js index d375cad..9343c40 100644 --- a/src/utils/appData.js +++ b/src/utils/appData.js @@ -5,28 +5,10 @@ export function getAppDataDir() { return ensureExists(appData("codex-cli")); } -export function getCodexRootPath() { +export function getDefaultCodexRootPath() { return ensureExists(appData("codex")); } -export function getCodexBinPath() { - return ensureExists(path.join(appData("codex"), "bin")); -} - -export function getCodexConfigFilePath() { - return path.join(appData("codex"), "bin", "config.toml"); -} - -export function getCodexDataDirDefaultPath() { - // This path does not exist on first startup. That's good: Codex will - // create it with the required access permissions. - return path.join(appData("codex"), "datadir"); -} - -export function getCodexLogsDefaultPath() { - return ensureExists(path.join(appData("codex"), "logs")); -} - function ensureExists(dir) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); From c947a711673ef880c0d74f5ec5f072b019260f45 Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 10:53:53 +0200 Subject: [PATCH 41/59] updates the config menu --- src/main.js | 3 --- src/ui/configMenu.js | 43 +------------------------------ src/ui/configMenu.test.js | 48 ----------------------------------- src/utils/dataDirMover.js | 53 --------------------------------------- 4 files changed, 1 insertion(+), 146 deletions(-) delete mode 100644 src/utils/dataDirMover.js diff --git a/src/main.js b/src/main.js index bef9420..104b0d5 100644 --- a/src/main.js +++ b/src/main.js @@ -29,7 +29,6 @@ import { ConfigMenu } from "./ui/configMenu.js"; import { PathSelector } from "./utils/pathSelector.js"; import { NumberSelector } from "./utils/numberSelector.js"; import { MenuLoop } from "./utils/menuLoop.js"; -import { DataDirMover } from "./utils/dataDirMover.js"; import { Installer } from "./handlers/installer.js"; import { ShellService } from "./services/shellService.js"; import { OsService } from "./services/osService.js"; @@ -125,9 +124,7 @@ export async function main() { uiService, new MenuLoop(), configService, - pathSelector, numberSelector, - new DataDirMover(fsService, uiService), ); const processControl = new ProcessControl( configService, diff --git a/src/ui/configMenu.js b/src/ui/configMenu.js index 6a3b241..533f952 100644 --- a/src/ui/configMenu.js +++ b/src/ui/configMenu.js @@ -1,39 +1,21 @@ export class ConfigMenu { - constructor( - uiService, - menuLoop, - configService, - pathSelector, - numberSelector, - dataDirMover, - ) { + constructor(uiService, menuLoop, configService, numberSelector) { this.ui = uiService; this.loop = menuLoop; this.configService = configService; - this.pathSelector = pathSelector; this.numberSelector = numberSelector; - this.dataDirMover = dataDirMover; this.loop.initialize(this.showConfigMenu); } show = async () => { this.config = this.configService.get(); - this.originalDataDir = this.config.dataDir; this.ui.showInfoMessage("Codex Configuration"); await this.loop.showLoop(); }; showConfigMenu = async () => { await this.ui.askMultipleChoice("Select to edit:", [ - { - label: `Data path = "${this.config.dataDir}"`, - action: this.editDataDir, - }, - { - label: `Logs path = "${this.config.logsDir}"`, - action: this.editLogsDir, - }, { label: `Storage quota = ${this.bytesAmountToString(this.config.storageQuota)}`, action: this.editStorageQuota, @@ -78,20 +60,6 @@ export class ConfigMenu { return `${numBytes} Bytes (${value} ${units[index]})`; }; - editDataDir = async () => { - this.config.dataDir = await this.pathSelector.show( - this.config.dataDir, - false, - ); - }; - - editLogsDir = async () => { - this.config.logsDir = await this.pathSelector.show( - this.config.logsDir, - true, - ); - }; - editStorageQuota = async () => { this.ui.showInfoMessage("You can use: 'GB' or 'gb', etc."); const newQuota = await this.numberSelector.show( @@ -148,15 +116,6 @@ export class ConfigMenu { }; saveChangesAndExit = async () => { - if (this.config.dataDir !== this.originalDataDir) { - // The Codex data-dir is a little special. - // Use a dedicated module to move it. - await this.dataDirMover.moveDataDir( - this.originalDataDir, - this.config.dataDir, - ); - } - this.configService.saveConfig(); this.ui.showInfoMessage("Configuration changes saved."); this.loop.stopLoop(); diff --git a/src/ui/configMenu.test.js b/src/ui/configMenu.test.js index 00d0a80..59d972d 100644 --- a/src/ui/configMenu.test.js +++ b/src/ui/configMenu.test.js @@ -3,10 +3,8 @@ import { ConfigMenu } from "./configMenu.js"; import { mockUiService } from "../__mocks__/service.mocks.js"; import { mockConfigService } from "../__mocks__/service.mocks.js"; import { - mockPathSelector, mockNumberSelector, mockMenuLoop, - mockDataDirMover, } from "../__mocks__/utils.mocks.js"; describe("ConfigMenu", () => { @@ -30,9 +28,7 @@ describe("ConfigMenu", () => { mockUiService, mockMenuLoop, mockConfigService, - mockPathSelector, mockNumberSelector, - mockDataDirMover, ); }); @@ -59,11 +55,6 @@ describe("ConfigMenu", () => { expect(configMenu.config).toEqual(config); }); - it("sets the original datadir field", async () => { - await configMenu.show(); - expect(configMenu.originalDataDir).toEqual(config.dataDir); - }); - describe("config menu options", () => { beforeEach(() => { configMenu.config = config; @@ -74,14 +65,6 @@ describe("ConfigMenu", () => { expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( "Select to edit:", [ - { - label: `Data path = "${mockConfigService.get().dataDir}"`, - action: configMenu.editDataDir, - }, - { - label: `Logs path = "${mockConfigService.get().logsDir}"`, - action: configMenu.editLogsDir, - }, { label: `Storage quota = 1073741824 Bytes (1024 MB)`, action: configMenu.editStorageQuota, @@ -110,24 +93,6 @@ describe("ConfigMenu", () => { ); }); - it("edits the logs directory", async () => { - const originalPath = config.dataDir; - mockPathSelector.show.mockResolvedValue("/new-data"); - await configMenu.editDataDir(); - - expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, false); - expect(configMenu.config.dataDir).toEqual("/new-data"); - }); - - it("edits the logs directory", async () => { - const originalPath = config.logsDir; - mockPathSelector.show.mockResolvedValue("/new-logs"); - await configMenu.editLogsDir(); - - expect(mockPathSelector.show).toHaveBeenCalledWith(originalPath, true); - expect(configMenu.config.logsDir).toEqual("/new-logs"); - }); - it("edits the storage quota", async () => { const originalQuota = config.storageQuota; const newQuota = 200 * 1024 * 1024; @@ -249,19 +214,6 @@ describe("ConfigMenu", () => { expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); }); - it("calls the dataDirMover when the new datadir is not equal to the original dataDir when saving changes", async () => { - config.dataDir = "/original-data"; - await configMenu.show(); - - configMenu.config.dataDir = "/new-data"; - await configMenu.saveChangesAndExit(); - - expect(mockDataDirMover.moveDataDir).toHaveBeenCalledWith( - configMenu.originalDataDir, - configMenu.config.dataDir, - ); - }); - it("discards changes and exits", async () => { await configMenu.discardChangesAndExit(); diff --git a/src/utils/dataDirMover.js b/src/utils/dataDirMover.js deleted file mode 100644 index 9fcc827..0000000 --- a/src/utils/dataDirMover.js +++ /dev/null @@ -1,53 +0,0 @@ -export class DataDirMover { - constructor(fsService, uiService) { - this.fs = fsService; - this.ui = uiService; - } - - moveDataDir = (oldPath, newPath) => { - if (oldPath === newPath) return; - - // The Codex dataDir is a little strange: - // If the old one is empty: The new one should not exist, so that codex creates it with the correct security permissions. - // If the old one does exist: We move it. - - if (this.fs.isDir(oldPath)) { - this.moveDir(oldPath, newPath); - } else { - this.ensureDoesNotExist(newPath); - } - }; - - moveDir = (oldPath, newPath) => { - this.ui.showInfoMessage( - "Moving Codex data folder...\n" + - `From: "${oldPath}"\n` + - `To: "${newPath}"`, - ); - - try { - this.fs.moveDir(oldPath, newPath); - } catch (error) { - console.log( - this.ui.showErrorMessage( - "Error while moving dataDir: " + error.message, - ), - ); - throw error; - } - }; - - ensureDoesNotExist = (path) => { - if (this.fs.isDir(path)) { - console.log( - this.ui.showInfoMessage( - "Warning: the selected data path already exists.\n" + - `New data path = "${path}"\n` + - "Codex may overwrite data in this folder.\n" + - "Codex will fail to start if this folder does not have the required\n" + - "security permissions.", - ), - ); - } - }; -} From c76cd357ca7a66230b8a27385f46a6b351c877bb Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 11:14:54 +0200 Subject: [PATCH 42/59] fixing volume detection for vm environment --- src/services/fsService.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/services/fsService.js b/src/services/fsService.js index 67ac664..888eb67 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -9,12 +9,17 @@ export class FsService { Object.keys(devices).forEach(function (key) { var val = devices[key]; val.volumes.forEach(function (volume) { - mountPoints.push(volume.mountPoint); + const mount = volume.mountPoint; + if (mount != null && mount != undefined && mount.length > 0) { + mountPoints.push(volume.mountPoint); + } }); }); if (mountPoints.length < 1) { - throw new Error("Failed to detect file system devices."); + // In certain containerized environments, the devices don't reveal any + // useful mounts. We'll proceed under the assumption that '/' is valid here. + return ['/']; } return mountPoints; }; From df9569314f5f1dce04fb5f705e8ae344aa56d730 Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 11:22:44 +0200 Subject: [PATCH 43/59] attempting to fix mount detection --- src/services/fsService.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/services/fsService.js b/src/services/fsService.js index 888eb67..879f621 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -11,7 +11,12 @@ export class FsService { val.volumes.forEach(function (volume) { const mount = volume.mountPoint; if (mount != null && mount != undefined && mount.length > 0) { - mountPoints.push(volume.mountPoint); + try { + if (!fs.lstatSync(mount).isFile()) { + mountPoints.push(mount); + } + } catch { + } } }); }); From 79ca49b51606c897b3e68495d51bbd6d9ae11fed Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 13:05:03 +0200 Subject: [PATCH 44/59] enables process-control to try stopping codex with sigterm when sigint fails to stop it. --- src/__mocks__/service.mocks.js | 1 + src/handlers/processControl.js | 18 +++++++++++++++++- src/services/osService.js | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 8b90113..89f730f 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -47,6 +47,7 @@ export const mockOsService = { getWorkingDir: vi.fn(), listProcesses: vi.fn(), stopProcess: vi.fn(), + terminateProcess: vi.fn(), }; export const mockCodexGlobals = { diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 407d830..8654555 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -25,9 +25,25 @@ export class ProcessControl { if (processes.length < 1) throw new Error("No codex process found"); const pid = processes[0].pid; + await this.stopProcess(pid); + }; + + stopProcess = async (pid) => { this.os.stopProcess(pid); await this.sleep(); - }; + + if (await this.isProcessRunning(pid)) { + this.os.terminateProcess(pid); + await this.sleep(); + } + } + + isProcessRunning = async (pid) => { + const processes = await this.os.listProcesses(); + const p = processes.filter((p) => p.pid == pid); + const result = p.length > 0; + return result; + } startCodexProcess = async () => { await this.saveCodexConfigFile(); diff --git a/src/services/osService.js b/src/services/osService.js index 2778b58..a42f384 100644 --- a/src/services/osService.js +++ b/src/services/osService.js @@ -29,4 +29,8 @@ export class OsService { stopProcess = (pid) => { process.kill(pid, "SIGINT"); }; + + terminateProcess = (pid) => { + process.kill(pid, "SIGTERM"); + } } From 9149bbc104ff1e88faf1c69caa7cc8915e13a931 Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 13:11:23 +0200 Subject: [PATCH 45/59] excludes defunct processes from being considered --- src/handlers/processControl.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 8654555..583789b 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -12,7 +12,8 @@ export class ProcessControl { if (this.os.isWindows()) { return processes.filter((p) => p.name === "codex.exe"); } else { - return processes.filter((p) => p.name === "codex"); + return processes.filter((p) => + p.name === "codex" && !p.cmd.includes("")); } }; From a63a8944a26e1e488dd363e526fe49843f02e8c6 Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 13:44:16 +0200 Subject: [PATCH 46/59] Fixes process detection for defunct processes in unix environment --- README.md | 7 ++++ src/handlers/processControl.js | 9 +++-- src/handlers/processControl.test.js | 61 +++++++++++++++++++++++------ src/main.js | 1 + src/services/fsService.js | 5 +-- src/services/osService.js | 2 +- src/ui/configMenu.test.js | 5 +-- src/ui/installMenu.js | 15 ++++++- src/ui/installMenu.test.js | 31 +++++++++++++-- 9 files changed, 107 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index b9e2886..f2204d8 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,13 @@ npm install npm start ``` +## Trouble? + +Is the installer crashing? Make sure these packages are installed: +``` +apt-get install fdisk procps +``` + ## License MIT diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 583789b..c6b7fbd 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -12,8 +12,9 @@ export class ProcessControl { if (this.os.isWindows()) { return processes.filter((p) => p.name === "codex.exe"); } else { - return processes.filter((p) => - p.name === "codex" && !p.cmd.includes("")); + return processes.filter( + (p) => p.name === "codex" && !p.cmd.includes(""), + ); } }; @@ -37,14 +38,14 @@ export class ProcessControl { this.os.terminateProcess(pid); await this.sleep(); } - } + }; isProcessRunning = async (pid) => { const processes = await this.os.listProcesses(); const p = processes.filter((p) => p.pid == pid); const result = p.length > 0; return result; - } + }; startCodexProcess = async () => { await this.saveCodexConfigFile(); diff --git a/src/handlers/processControl.test.js b/src/handlers/processControl.test.js index 70a6963..705884a 100644 --- a/src/handlers/processControl.test.js +++ b/src/handlers/processControl.test.js @@ -27,12 +27,12 @@ describe("ProcessControl", () => { describe("getCodexProcesses", () => { const processes = [ - { id: 0, name: "a.exe" }, - { id: 1, name: "aaa" }, - { id: 2, name: "codex" }, - { id: 3, name: "codex.exe" }, - { id: 4, name: "notcodex" }, - { id: 5, name: "alsonotcodex.exe" }, + { id: 0, name: "a.exe", cmd: "" }, + { id: 1, name: "codex", cmd: "" }, + { id: 2, name: "codex", cmd: "" }, + { id: 3, name: "codex.exe", cmd: "" }, + { id: 4, name: "notcodex", cmd: "" }, + { id: 5, name: "alsonotcodex.exe", cmd: "" }, ]; beforeEach(() => { @@ -80,7 +80,7 @@ describe("ProcessControl", () => { ); }); - it("stops the first codex process", async () => { + it("calls stopProcess with pid of first codex process", async () => { const pid = 12345; processControl.getCodexProcesses.mockResolvedValue([ { pid: pid }, @@ -88,21 +88,58 @@ describe("ProcessControl", () => { { pid: 222 }, ]); + processControl.stopProcess = vi.fn(); await processControl.stopCodexProcess(); + expect(processControl.stopProcess).toHaveBeenCalledWith(pid); + }); + }); + + describe("stopProcess", () => { + const pid = 234; + beforeEach(() => { + processControl.isProcessRunning = vi.fn(); + }); + + it("stops the process", async () => { + processControl.isProcessRunning.mockResolvedValue(false); + + await processControl.stopProcess(pid); + expect(mockOsService.stopProcess).toHaveBeenCalledWith(pid); }); it("sleeps", async () => { - processControl.getCodexProcesses.mockResolvedValue([ - { pid: 111 }, - { pid: 222 }, - ]); + processControl.isProcessRunning.mockResolvedValue(false); - await processControl.stopCodexProcess(); + await processControl.stopProcess(pid); expect(processControl.sleep).toHaveBeenCalled(); }); + + it("terminates process if it is running after stop", async () => { + processControl.isProcessRunning.mockResolvedValue(true); + + await processControl.stopProcess(pid); + + expect(mockOsService.terminateProcess).toHaveBeenCalledWith(pid); + }); + }); + + describe("isProcessRunning", () => { + const pid = 345; + + it("is true when process is in process list", async () => { + mockOsService.listProcesses.mockResolvedValue([{ pid: pid }]); + + expect(await processControl.isProcessRunning(pid)).toBeTruthy(); + }); + + it("is false when process is not in process list", async () => { + mockOsService.listProcesses.mockResolvedValue([{ pid: pid + 11 }]); + + expect(await processControl.isProcessRunning(pid)).toBeFalsy(); + }); }); describe("startCodexProcess", () => { diff --git a/src/main.js b/src/main.js index 104b0d5..9adb505 100644 --- a/src/main.js +++ b/src/main.js @@ -116,6 +116,7 @@ export async function main() { ); const installMenu = new InstallMenu( uiService, + new MenuLoop(), configService, pathSelector, installer, diff --git a/src/services/fsService.js b/src/services/fsService.js index 879f621..8c91f33 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -15,8 +15,7 @@ export class FsService { if (!fs.lstatSync(mount).isFile()) { mountPoints.push(mount); } - } catch { - } + } catch {} } }); }); @@ -24,7 +23,7 @@ export class FsService { if (mountPoints.length < 1) { // In certain containerized environments, the devices don't reveal any // useful mounts. We'll proceed under the assumption that '/' is valid here. - return ['/']; + return ["/"]; } return mountPoints; }; diff --git a/src/services/osService.js b/src/services/osService.js index a42f384..f3b4449 100644 --- a/src/services/osService.js +++ b/src/services/osService.js @@ -32,5 +32,5 @@ export class OsService { terminateProcess = (pid) => { process.kill(pid, "SIGTERM"); - } + }; } diff --git a/src/ui/configMenu.test.js b/src/ui/configMenu.test.js index 59d972d..12bf207 100644 --- a/src/ui/configMenu.test.js +++ b/src/ui/configMenu.test.js @@ -2,10 +2,7 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { ConfigMenu } from "./configMenu.js"; import { mockUiService } from "../__mocks__/service.mocks.js"; import { mockConfigService } from "../__mocks__/service.mocks.js"; -import { - mockNumberSelector, - mockMenuLoop, -} from "../__mocks__/utils.mocks.js"; +import { mockNumberSelector, mockMenuLoop } from "../__mocks__/utils.mocks.js"; describe("ConfigMenu", () => { const config = { diff --git a/src/ui/installMenu.js b/src/ui/installMenu.js index f87069d..5b99fee 100644 --- a/src/ui/installMenu.js +++ b/src/ui/installMenu.js @@ -1,13 +1,20 @@ export class InstallMenu { - constructor(uiService, configService, pathSelector, installer) { + constructor(uiService, menuLoop, configService, pathSelector, installer) { this.ui = uiService; + this.loop = menuLoop; this.configService = configService; this.config = configService.get(); this.pathSelector = pathSelector; this.installer = installer; + + this.loop.initialize(this.showMenu); } show = async () => { + await this.loop.showLoop(); + }; + + showMenu = async () => { if (await this.installer.isCodexInstalled()) { await this.showUninstallMenu(); } else { @@ -86,14 +93,18 @@ export class InstallMenu { }; performInstall = async () => { + this.loop.stopLoop(); await this.installer.installCodex(this); }; performUninstall = async () => { + this.loop.stopLoop(); this.installer.uninstallCodex(); }; - doNothing = async () => {}; + doNothing = async () => { + this.loop.stopLoop(); + }; // Progress callbacks from installer module: installStarts = () => { diff --git a/src/ui/installMenu.test.js b/src/ui/installMenu.test.js index 6dd281f..620edcb 100644 --- a/src/ui/installMenu.test.js +++ b/src/ui/installMenu.test.js @@ -2,7 +2,7 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { InstallMenu } from "./installMenu.js"; import { mockUiService } from "../__mocks__/service.mocks.js"; import { mockConfigService } from "../__mocks__/service.mocks.js"; -import { mockPathSelector } from "../__mocks__/utils.mocks.js"; +import { mockMenuLoop, mockPathSelector } from "../__mocks__/utils.mocks.js"; import { mockInstaller } from "../__mocks__/handler.mocks.js"; describe("InstallMenu", () => { @@ -17,13 +17,30 @@ describe("InstallMenu", () => { installMenu = new InstallMenu( mockUiService, + mockMenuLoop, mockConfigService, mockPathSelector, mockInstaller, ); }); + describe("constructor", () => { + it("initializes the menu loop with the showMenu function", () => { + expect(mockMenuLoop.initialize).toHaveBeenCalledWith( + installMenu.showMenu, + ); + }); + }); + describe("show", () => { + it("starts the menu loop", async () => { + await installMenu.show(); + + expect(mockMenuLoop.showLoop).toHaveBeenCalled(); + }); + }); + + describe("showMenu", () => { beforeEach(() => { installMenu.showInstallMenu = vi.fn(); installMenu.showUninstallMenu = vi.fn(); @@ -32,7 +49,7 @@ describe("InstallMenu", () => { it("shows uninstall menu when codex is installed", async () => { mockInstaller.isCodexInstalled.mockResolvedValue(true); - await installMenu.show(); + await installMenu.showMenu(); expect(installMenu.showUninstallMenu).toHaveBeenCalled(); }); @@ -40,7 +57,7 @@ describe("InstallMenu", () => { it("shows install menu when codex is not installed", async () => { mockInstaller.uninstallCodex.mockResolvedValue(false); - await installMenu.show(); + await installMenu.showMenu(); expect(installMenu.showInstallMenu).toHaveBeenCalled(); }); @@ -140,12 +157,20 @@ describe("InstallMenu", () => { await installMenu.performInstall(); expect(mockInstaller.installCodex).toHaveBeenCalledWith(installMenu); + expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); }); it("calls installer for deinstallation", async () => { await installMenu.performUninstall(); expect(mockInstaller.uninstallCodex).toHaveBeenCalled(); + expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); + }); + + it("stops the menu loop when nothing is selected", async () => { + await installMenu.doNothing(); + + expect(mockMenuLoop.stopLoop).toHaveBeenCalled(); }); describe("process callback handling", () => { From d017b5a733bc920fcd5e925d9d72666a963752c7 Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 15:03:43 +0200 Subject: [PATCH 47/59] setting up --- package-lock.json | 115 +++++++++++++++++++++++++++++++++ package.json | 4 +- src/__mocks__/service.mocks.js | 1 + src/handlers/installer.js | 11 +++- src/handlers/processControl.js | 7 ++ src/main.js | 10 ++- src/services/codexGlobals.js | 4 ++ src/services/configService.js | 36 ++++++++--- src/services/ethersService.js | 41 ++++++++++++ src/services/fsService.js | 4 ++ src/services/shellService.js | 20 +++--- src/ui/mainMenu.js | 6 +- src/ui/marketplaceSetup.js | 63 ++++++++++++++++++ 13 files changed, 296 insertions(+), 26 deletions(-) create mode 100644 src/services/ethersService.js create mode 100644 src/ui/marketplaceSetup.js diff --git a/package-lock.json b/package-lock.json index bbf68dd..fa1a663 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,8 @@ "axios": "^1.6.2", "boxen": "^7.1.1", "chalk": "^5.3.0", + "crypto": "^1.0.1", + "ethers": "^6.13.5", "fs-extra": "^11.3.0", "fs-filesystem": "^2.1.2", "inquirer": "^9.2.12", @@ -31,6 +33,12 @@ "node": ">=20" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -446,6 +454,30 @@ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.34.8", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", @@ -699,6 +731,15 @@ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, + "node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/@vitest/expect": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", @@ -805,6 +846,12 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", @@ -1212,6 +1259,13 @@ "hasInstallScript": true, "license": "MIT" }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1364,6 +1418,40 @@ "@types/estree": "^1.0.0" } }, + "node_modules/ethers": { + "version": "6.13.5", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.5.tgz", + "integrity": "sha512-+knKNieu5EKRThQJWwqaJ10a6HE9sSehGeqWN65//wE7j47ZpFhKAnHB/JJFibwwg61I/koxaPsXbXpD/skNOQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, "node_modules/expect-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", @@ -2237,6 +2325,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -2571,6 +2665,27 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", diff --git a/package.json b/package.json index 95ff60b..ae2683b 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "axios": "^1.6.2", "boxen": "^7.1.1", "chalk": "^5.3.0", + "crypto": "^1.0.1", + "ethers": "^6.13.5", "fs-extra": "^11.3.0", "fs-filesystem": "^2.1.2", "inquirer": "^9.2.12", @@ -34,7 +36,7 @@ "nanospinner": "^1.1.0", "open": "^10.1.0", "ps-list": "^8.1.1" - }, + }, "devDependencies": { "prettier": "^3.4.2", "vitest": "^3.0.5" diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 89f730f..422636b 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -30,6 +30,7 @@ export const mockFsService = { moveDir: vi.fn(), deleteDir: vi.fn(), readJsonFile: vi.fn(), + readFile: vi.fn(), writeJsonFile: vi.fn(), writeFile: vi.fn(), ensureDirExists: vi.fn(), diff --git a/src/handlers/installer.js b/src/handlers/installer.js index dffe6ec..4a561c8 100644 --- a/src/handlers/installer.js +++ b/src/handlers/installer.js @@ -1,10 +1,17 @@ export class Installer { - constructor(configService, shellService, osService, fsService) { + constructor( + configService, + shellService, + osService, + fsService, + marketplaceSetup, + ) { this.config = configService.get(); this.configService = configService; this.shell = shellService; this.os = osService; this.fs = fsService; + this.market = marketplaceSetup; } isCodexInstalled = async () => { @@ -28,6 +35,8 @@ export class Installer { this.fs.ensureDirExists(this.config.codexRoot); if (!(await this.arePrerequisitesCorrect(processCallbacks))) return; + if (!(await this.market.runClientWizard())) return; + processCallbacks.installStarts(); if (this.os.isWindows()) { await this.installCodexWindows(processCallbacks); diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index c6b7fbd..9655621 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -72,6 +72,13 @@ export class ProcessControl { const workingDir = this.configService.get().codexRoot; const args = [ `--config-file=${this.configService.getCodexConfigFilePath()}`, + + // Marketplace client parameters cannot be set via config file. + // Open issue: https://github.com/codex-storage/nim-codex/issues/1206 + // So we're setting them here. + "persistence", + `--eth-provider=https://rpc.testnet.codex.storage`, + `--eth-private-key=eth.key` ]; await this.shell.spawnDetachedProcess(executable, workingDir, args); }; diff --git a/src/main.js b/src/main.js index 9adb505..4dfc13a 100644 --- a/src/main.js +++ b/src/main.js @@ -20,7 +20,6 @@ import { import { runCodex, checkNodeStatus } from "./handlers/nodeHandlers.js"; import { showInfoMessage } from "./utils/messages.js"; import { ConfigService } from "./services/configService.js"; - import { UiService } from "./services/uiService.js"; import { FsService } from "./services/fsService.js"; import { MainMenu } from "./ui/mainMenu.js"; @@ -35,6 +34,8 @@ import { OsService } from "./services/osService.js"; import { ProcessControl } from "./handlers/processControl.js"; import { CodexGlobals } from "./services/codexGlobals.js"; import { CodexApp } from "./services/codexApp.js"; +import { EthersService } from "./services/ethersService.js"; +import { MarketplaceSetup } from "./ui/marketplaceSetup.js"; async function showNavigationMenu() { console.log("\n"); @@ -108,11 +109,18 @@ export async function main() { const configService = new ConfigService(fsService, osService); const codexApp = new CodexApp(configService); const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService); + const ethersService = new EthersService(fsService, configService); + const marketplaceSetup = new MarketplaceSetup( + uiService, + configService, + ethersService, + ); const installer = new Installer( configService, shellService, osService, fsService, + marketplaceSetup, ); const installMenu = new InstallMenu( uiService, diff --git a/src/services/codexGlobals.js b/src/services/codexGlobals.js index b5bd3dc..d3344f2 100644 --- a/src/services/codexGlobals.js +++ b/src/services/codexGlobals.js @@ -10,4 +10,8 @@ export class CodexGlobals { const result = (await axios.get(`https://spr.codex.storage/testnet`)).data; return result.split("\n").filter((line) => line.length > 0); }; + + getEthProvider = () => { + return "https://rpc.testnet.codex.storage"; + } } diff --git a/src/services/configService.js b/src/services/configService.js index 73627e6..c287fa2 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -13,6 +13,8 @@ const defaultConfig = { const datadir = "datadir"; const codexLogFile = "codex.log"; const codexConfigFile = "config.toml"; +const ethKeyFile = "eth.key"; +const ethAddressFile = "eth.address"; export class ConfigService { constructor(fsService, osService) { @@ -38,6 +40,13 @@ export class ConfigService { return this.fs.pathJoin([this.config.codexRoot, codexConfigFile]); }; + getEthFilePaths = () => { + return { + key: this.fs.pathJoin([this.config.codexRoot, ethKeyFile]), + address: this.fs.pathJoin([this.config.codexRoot, ethAddressFile]), + }; + }; + loadConfig = () => { const filePath = this.getConfigFilename(); try { @@ -76,7 +85,7 @@ export class ConfigService { throw new Error("Storage quota must be at least 100MB"); }; - writeCodexConfigFile = (publicIp, bootstrapNodes) => { + writeCodexConfigFile = (publicIp, bootstrapNodes, ethProvider) => { this.validateConfiguration(); const nl = "\n"; @@ -85,15 +94,22 @@ export class ConfigService { this.fs.writeFile( this.getCodexConfigFilePath(), `data-dir="${datadir}"${nl}` + - `log-level="DEBUG"${nl}` + - `log-file="${codexLogFile}"${nl}` + - `storage-quota=${this.config.storageQuota}${nl}` + - `disc-port=${this.config.ports.discPort}${nl}` + - `listen-addrs=["/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}"]${nl}` + - `api-port=${this.config.ports.apiPort}${nl}` + - `nat="extip:${publicIp}"${nl}` + - `api-cors-origin="*"${nl}` + - `bootstrap-node=[${bootNodes}]${nl}`, + `log-level="TRACE"${nl}` + + `log-file="${codexLogFile}"${nl}` + + `storage-quota=${this.config.storageQuota}${nl}` + + `disc-port=${this.config.ports.discPort}${nl}` + + `listen-addrs=["/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}"]${nl}` + + `api-port=${this.config.ports.apiPort}${nl}` + + `nat="extip:${publicIp}"${nl}` + + `api-cors-origin="*"${nl}` + + `bootstrap-node=[${bootNodes}]${nl}` + + // Marketplace client parameters: + // `[persistence]${nl}` + + //`eth-provider="${ethProvider}"${nl}` + + // `eth-provider="https://rpc.testnet.codex.storage"${nl}` + + // //`eth-private-key="${ethKeyFile}"${nl}` + + // `eth-private-key="notafile.no"${nl}` + + `${nl}` ); }; } diff --git a/src/services/ethersService.js b/src/services/ethersService.js new file mode 100644 index 0000000..abb1352 --- /dev/null +++ b/src/services/ethersService.js @@ -0,0 +1,41 @@ +import { ethers } from 'ethers'; +import crypto from "crypto"; + +export class EthersService { + constructor(fsService, configService) { + this.fs = fsService; + this.configService = configService; + } + + getOrCreateEthKey = () => { + const paths = this.configService.getEthFilePaths(); + + if (!this.fs.isFile(paths.key)) { + this.generateAndSaveKey(paths); + } + + const address = this.fs.readFile(paths.address); + + return { + privateKeyFilePath: paths.key, + addressFilePath: paths.address, + address: address, + }; + }; + + generateAndSaveKey = async (paths) => { + const keys = this.generateKey(); + this.fs.writeFile(paths.key, keys.key); + this.fs.writeFile(paths.address, keys.address); + }; + + generateKey = () => { + var id = crypto.randomBytes(32).toString("hex"); + var privateKey = "0x" + id; + var wallet = new ethers.Wallet(privateKey); + return { + key: privateKey, + address: wallet.address, + }; + }; +} diff --git a/src/services/fsService.js b/src/services/fsService.js index 8c91f33..7913e72 100644 --- a/src/services/fsService.js +++ b/src/services/fsService.js @@ -68,6 +68,10 @@ export class FsService { return JSON.parse(fs.readFileSync(filePath)); }; + readFile = (filePath) => { + return fs.readFileSync(filePath); + }; + writeJsonFile = (filePath, jsonObject) => { fs.writeFileSync(filePath, JSON.stringify(jsonObject)); }; diff --git a/src/services/shellService.js b/src/services/shellService.js index 6fde68e..b4c7f18 100644 --- a/src/services/shellService.js +++ b/src/services/shellService.js @@ -20,20 +20,20 @@ export class ShellService { var child = spawn(cmd, args, { cwd: workingDir, detached: true, - stdio: ["ignore", "ignore", "ignore"], + //stdio: ["ignore", "ignore", "ignore"], }); - // child.stdout.on("data", (data) => { - // console.log(`stdout: ${data}`); - // }); + child.stdout.on("data", (data) => { + console.log(`stdout: ${data}`); + }); - // child.stderr.on("data", (data) => { - // console.error(`stderr: ${data}`); - // }); + child.stderr.on("data", (data) => { + console.error(`stderr: ${data}`); + }); - // child.on("close", (code) => { - // console.log(`child process exited with code ${code}`); - // }); + child.on("close", (code) => { + console.log(`child process exited with code ${code}`); + }); child.unref(); diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index 8dcb365..e58f587 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -91,12 +91,12 @@ export class MainMenu { }; startCodex = async () => { - const spinner = this.ui.createAndStartSpinner("Starting..."); + // const spinner = this.ui.createAndStartSpinner("Starting..."); try { await this.processControl.startCodexProcess(); - this.ui.stopSpinnerSuccess(spinner); + // this.ui.stopSpinnerSuccess(spinner); } catch (exception) { - this.ui.stopSpinnerError(spinner); + // this.ui.stopSpinnerError(spinner); this.ui.showErrorMessage(`Failed to start Codex. "${exception}"`); } }; diff --git a/src/ui/marketplaceSetup.js b/src/ui/marketplaceSetup.js new file mode 100644 index 0000000..40aee2b --- /dev/null +++ b/src/ui/marketplaceSetup.js @@ -0,0 +1,63 @@ +const ethFaucetAddress = "https://faucet-eth.testnet.codex.storage/"; +const tstFaucetAddress = "https://faucet-tst.testnet.codex.storage/"; +const discordServerAddress = "https://discord.gg/codex-storage"; +const botChannelLink = + "https://discord.com/channels/895609329053474826/1230785221553819669"; + +export class MarketplaceSetup { + constructor(uiService, configService, ethersService) { + this.ui = uiService; + this.ethers = ethersService; + this.config = configService.get(); + } + + runClientWizard = async () => { + await this.generateKeyPair(); + await this.showMintInstructions(); + return this.isSuccessful; + }; + + generateKeyPair = async () => { + const ehtKey = await this.ethers.getOrCreateEthKey(); + + this.ui.showSuccessMessage( + "Your Codex node Ethereum account:\n" + + `Private key saved to '${ehtKey.privateKeyFilePath}'\n` + + `Address saved to '${ehtKey.addressFilePath}'\n` + + `Ethereum Account: '${ehtKey.address}'`, + ); + }; + + showMintInstructions = async () => { + this.ui.showInfoMessage( + "Use one of these two methods to receive your testnet tokens:\n\n" + + "Faucets:\n" + + `Use the Eth faucet: '${ethFaucetAddress}'\n` + + `Then use the TST faucet: '${tstFaucetAddress}'\n\n` + + "or\n\n" + + "Discord bot:\n" + + `Join the server: ${discordServerAddress}\n` + + `Go to the #BOT channel: ${botChannelLink}\n` + + "Use '/set' and '/mint' commands to receive tokens.\n", + ); + + await this.ui.askMultipleChoice("Take your time.", [ + { + label: "Proceed", + action: this.proceed, + }, + { + label: "Abort", + action: this.abort, + }, + ]); + }; + + proceed = async () => { + this.isSuccessful = true; + }; + + abort = async () => { + this.isSuccessful = false; + }; +} From 77a5da5be81130f84ebd1a5ab80353ed78db795a Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 15:36:35 +0200 Subject: [PATCH 48/59] working example of marketplace client install and start on win --- src/handlers/processControl.js | 4 ++-- src/main.js | 7 ++++++- src/services/codexGlobals.js | 2 +- src/services/configService.js | 28 ++++++++++++---------------- src/services/ethersService.js | 14 ++++++++++++-- src/services/osService.js | 4 ++++ 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/handlers/processControl.js b/src/handlers/processControl.js index 9655621..8edcc80 100644 --- a/src/handlers/processControl.js +++ b/src/handlers/processControl.js @@ -77,8 +77,8 @@ export class ProcessControl { // Open issue: https://github.com/codex-storage/nim-codex/issues/1206 // So we're setting them here. "persistence", - `--eth-provider=https://rpc.testnet.codex.storage`, - `--eth-private-key=eth.key` + `--eth-provider=${this.codexGlobals.getEthProvider()}`, + `--eth-private-key=eth.key`, // duplicated in configService. ]; await this.shell.spawnDetachedProcess(executable, workingDir, args); }; diff --git a/src/main.js b/src/main.js index 4dfc13a..c10ebcb 100644 --- a/src/main.js +++ b/src/main.js @@ -109,7 +109,12 @@ export async function main() { const configService = new ConfigService(fsService, osService); const codexApp = new CodexApp(configService); const pathSelector = new PathSelector(uiService, new MenuLoop(), fsService); - const ethersService = new EthersService(fsService, configService); + const ethersService = new EthersService( + fsService, + configService, + osService, + shellService, + ); const marketplaceSetup = new MarketplaceSetup( uiService, configService, diff --git a/src/services/codexGlobals.js b/src/services/codexGlobals.js index d3344f2..1c2ac57 100644 --- a/src/services/codexGlobals.js +++ b/src/services/codexGlobals.js @@ -13,5 +13,5 @@ export class CodexGlobals { getEthProvider = () => { return "https://rpc.testnet.codex.storage"; - } + }; } diff --git a/src/services/configService.js b/src/services/configService.js index c287fa2..84d0d7e 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -94,22 +94,18 @@ export class ConfigService { this.fs.writeFile( this.getCodexConfigFilePath(), `data-dir="${datadir}"${nl}` + - `log-level="TRACE"${nl}` + - `log-file="${codexLogFile}"${nl}` + - `storage-quota=${this.config.storageQuota}${nl}` + - `disc-port=${this.config.ports.discPort}${nl}` + - `listen-addrs=["/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}"]${nl}` + - `api-port=${this.config.ports.apiPort}${nl}` + - `nat="extip:${publicIp}"${nl}` + - `api-cors-origin="*"${nl}` + - `bootstrap-node=[${bootNodes}]${nl}` + - // Marketplace client parameters: - // `[persistence]${nl}` + - //`eth-provider="${ethProvider}"${nl}` + - // `eth-provider="https://rpc.testnet.codex.storage"${nl}` + - // //`eth-private-key="${ethKeyFile}"${nl}` + - // `eth-private-key="notafile.no"${nl}` + - `${nl}` + `log-level="TRACE"${nl}` + + `log-file="${codexLogFile}"${nl}` + + `storage-quota=${this.config.storageQuota}${nl}` + + `disc-port=${this.config.ports.discPort}${nl}` + + `listen-addrs=["/ip4/0.0.0.0/tcp/${this.config.ports.listenPort}"]${nl}` + + `api-port=${this.config.ports.apiPort}${nl}` + + `nat="extip:${publicIp}"${nl}` + + `api-cors-origin="*"${nl}` + + `bootstrap-node=[${bootNodes}]${nl}` + + // Marketplace client parameters cannot be set via config file. + // Open issue: https://github.com/codex-storage/nim-codex/issues/1206 + `${nl}`, ); }; } diff --git a/src/services/ethersService.js b/src/services/ethersService.js index abb1352..870a39b 100644 --- a/src/services/ethersService.js +++ b/src/services/ethersService.js @@ -1,10 +1,12 @@ -import { ethers } from 'ethers'; +import { ethers } from "ethers"; import crypto from "crypto"; export class EthersService { - constructor(fsService, configService) { + constructor(fsService, configService, osService, shellService) { this.fs = fsService; this.configService = configService; + this.os = osService; + this.shell = shellService; } getOrCreateEthKey = () => { @@ -27,6 +29,14 @@ export class EthersService { const keys = this.generateKey(); this.fs.writeFile(paths.key, keys.key); this.fs.writeFile(paths.address, keys.address); + + if (this.os.isWindows()) { + const username = this.os.getUsername(); + this.shell.run(`icacls ${paths.key} /inheritance:r >nul 2>&1`); + this.shell.run(`icacls ${paths.key} /grant:r ${username}:F >nul 2>&1`); + } else { + this.shell.run(`chmod 600 "${paths.key}"`); + } }; generateKey = () => { diff --git a/src/services/osService.js b/src/services/osService.js index f3b4449..aa05d71 100644 --- a/src/services/osService.js +++ b/src/services/osService.js @@ -33,4 +33,8 @@ export class OsService { terminateProcess = (pid) => { process.kill(pid, "SIGTERM"); }; + + getUsername = () => { + return os.userInfo().username; + }; } From 0e2776f6a3bce5f0a5c5d937a6087fa4e5b062c1 Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 15:52:58 +0200 Subject: [PATCH 49/59] Updates tests for installer, config service and process control --- src/__mocks__/service.mocks.js | 5 +++++ src/handlers/installer.test.js | 17 ++++++++++++++++- src/handlers/processControl.test.js | 9 ++++++++- src/services/configService.js | 4 ++-- src/services/configService.test.js | 27 +++++++++++++++++++++++++++ src/ui/mainMenu.js | 6 +++--- 6 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 422636b..d6416a1 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -54,8 +54,13 @@ export const mockOsService = { export const mockCodexGlobals = { getPublicIp: vi.fn(), getTestnetSPRs: vi.fn(), + getEthProvider: vi.fn(), }; export const mockCodexApp = { openCodexApp: vi.fn(), }; + +export const mockMarketplaceSetup = { + runClientWizard: vi.fn(), +}; diff --git a/src/handlers/installer.test.js b/src/handlers/installer.test.js index bcbbff0..1d4dfe3 100644 --- a/src/handlers/installer.test.js +++ b/src/handlers/installer.test.js @@ -3,8 +3,9 @@ import { mockShellService, mockOsService, mockFsService, + mockConfigService, + mockMarketplaceSetup, } from "../__mocks__/service.mocks.js"; -import { mockConfigService } from "../__mocks__/service.mocks.js"; import { Installer } from "./installer.js"; describe("Installer", () => { @@ -32,6 +33,7 @@ describe("Installer", () => { mockShellService, mockOsService, mockFsService, + mockMarketplaceSetup, ); }); @@ -109,9 +111,22 @@ describe("Installer", () => { expect(installer.installCodexUnix).not.toHaveBeenCalled(); }); + it("returns early when marketplace client wizard returns false", async () => { + installer.arePrerequisitesCorrect.mockResolvedValue(true); + mockMarketplaceSetup.runClientWizard.mockResolvedValue(false); + await installer.installCodex(processCallbacks); + expect(processCallbacks.installStarts).not.toHaveBeenCalled(); + expect(processCallbacks.installSuccessful).not.toHaveBeenCalled(); + expect(processCallbacks.downloadSuccessful).not.toHaveBeenCalled(); + expect(installer.isCodexInstalled).not.toHaveBeenCalled(); + expect(installer.installCodexWindows).not.toHaveBeenCalled(); + expect(installer.installCodexUnix).not.toHaveBeenCalled(); + }); + describe("prerequisites OK", () => { beforeEach(() => { installer.arePrerequisitesCorrect.mockResolvedValue(true); + mockMarketplaceSetup.runClientWizard.mockResolvedValue(true); installer.isCodexInstalled.mockResolvedValue(true); }); diff --git a/src/handlers/processControl.test.js b/src/handlers/processControl.test.js index 705884a..6386104 100644 --- a/src/handlers/processControl.test.js +++ b/src/handlers/processControl.test.js @@ -10,9 +10,11 @@ import { ProcessControl } from "./processControl.js"; describe("ProcessControl", () => { let processControl; + const mockEthProvider = "mockEthProvider"; beforeEach(() => { vi.resetAllMocks(); + mockCodexGlobals.getEthProvider.mockReturnValue(mockEthProvider); processControl = new ProcessControl( mockConfigService, @@ -195,7 +197,12 @@ describe("ProcessControl", () => { expect(mockShellService.spawnDetachedProcess).toHaveBeenCalledWith( exe, config.codexRoot, - [`--config-file=${configFile}`], + [ + `--config-file=${configFile}`, + "persistence", + `--eth-provider=${mockEthProvider}`, + `--eth-private-key=eth.key`, // duplicated in configService. + ], ); }); }); diff --git a/src/services/configService.js b/src/services/configService.js index 84d0d7e..66dd594 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -94,7 +94,7 @@ export class ConfigService { this.fs.writeFile( this.getCodexConfigFilePath(), `data-dir="${datadir}"${nl}` + - `log-level="TRACE"${nl}` + + `log-level="DEBUG"${nl}` + `log-file="${codexLogFile}"${nl}` + `storage-quota=${this.config.storageQuota}${nl}` + `disc-port=${this.config.ports.discPort}${nl}` + @@ -105,7 +105,7 @@ export class ConfigService { `bootstrap-node=[${bootNodes}]${nl}` + // Marketplace client parameters cannot be set via config file. // Open issue: https://github.com/codex-storage/nim-codex/issues/1206 - `${nl}`, + "", ); }; } diff --git a/src/services/configService.test.js b/src/services/configService.test.js index 990fe61..2a83342 100644 --- a/src/services/configService.test.js +++ b/src/services/configService.test.js @@ -112,6 +112,33 @@ describe("ConfigService", () => { }); }); + describe("getEthFilePaths", () => { + const result1 = "path/to/key"; + const result2 = "path/to/address"; + + it("returns the key and address file paths", () => { + const configService = new ConfigService(mockFsService, mockOsService); + + mockFsService.pathJoin = vi.fn(); + mockFsService.pathJoin.mockReturnValueOnce(result1); + mockFsService.pathJoin.mockReturnValueOnce(result2); + + expect(configService.getEthFilePaths()).toEqual({ + key: result1, + address: result2, + }); + + expect(mockFsService.pathJoin).toHaveBeenCalledWith([ + expectedDefaultConfig.codexRoot, + "eth.key", + ]); + expect(mockFsService.pathJoin).toHaveBeenCalledWith([ + expectedDefaultConfig.codexRoot, + "eth.address", + ]); + }); + }); + describe("validateConfiguration", () => { var configService; var config; diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index e58f587..8dcb365 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -91,12 +91,12 @@ export class MainMenu { }; startCodex = async () => { - // const spinner = this.ui.createAndStartSpinner("Starting..."); + const spinner = this.ui.createAndStartSpinner("Starting..."); try { await this.processControl.startCodexProcess(); - // this.ui.stopSpinnerSuccess(spinner); + this.ui.stopSpinnerSuccess(spinner); } catch (exception) { - // this.ui.stopSpinnerError(spinner); + this.ui.stopSpinnerError(spinner); this.ui.showErrorMessage(`Failed to start Codex. "${exception}"`); } }; From 79faa827af2ea15118070d37752a0fdb804aa0e1 Mon Sep 17 00:00:00 2001 From: thatben Date: Mon, 21 Apr 2025 16:04:07 +0200 Subject: [PATCH 50/59] Small fixes --- src/services/shellService.js | 20 ++++++++++---------- src/ui/installMenu.js | 1 + src/ui/installMenu.test.js | 1 + src/ui/mainMenu.js | 2 -- src/ui/mainMenu.test.js | 6 ------ 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/services/shellService.js b/src/services/shellService.js index b4c7f18..6fde68e 100644 --- a/src/services/shellService.js +++ b/src/services/shellService.js @@ -20,20 +20,20 @@ export class ShellService { var child = spawn(cmd, args, { cwd: workingDir, detached: true, - //stdio: ["ignore", "ignore", "ignore"], + stdio: ["ignore", "ignore", "ignore"], }); - child.stdout.on("data", (data) => { - console.log(`stdout: ${data}`); - }); + // child.stdout.on("data", (data) => { + // console.log(`stdout: ${data}`); + // }); - child.stderr.on("data", (data) => { - console.error(`stderr: ${data}`); - }); + // child.stderr.on("data", (data) => { + // console.error(`stderr: ${data}`); + // }); - child.on("close", (code) => { - console.log(`child process exited with code ${code}`); - }); + // child.on("close", (code) => { + // console.log(`child process exited with code ${code}`); + // }); child.unref(); diff --git a/src/ui/installMenu.js b/src/ui/installMenu.js index 5b99fee..9902a2b 100644 --- a/src/ui/installMenu.js +++ b/src/ui/installMenu.js @@ -60,6 +60,7 @@ export class InstallMenu { this.ui.showInfoMessage( "You are about to:\n" + " - Uninstall the Codex application\n" + + " - Delete your Codex ethereum keys\n" + " - Delete the data stored in your Codex node\n" + " - Delete the log files of your Codex node", ); diff --git a/src/ui/installMenu.test.js b/src/ui/installMenu.test.js index 620edcb..4a0cf41 100644 --- a/src/ui/installMenu.test.js +++ b/src/ui/installMenu.test.js @@ -111,6 +111,7 @@ describe("InstallMenu", () => { expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( "You are about to:\n" + " - Uninstall the Codex application\n" + + " - Delete your Codex ethereum keys\n" + " - Delete the data stored in your Codex node\n" + " - Delete the log files of your Codex node", ); diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index 8dcb365..19de272 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -23,8 +23,6 @@ export class MainMenu { this.ui.showLogo(); await this.loop.showLoop(); - - this.ui.showInfoMessage("K-THX-BYE"); }; promptMainMenu = async () => { diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index 1f717e9..009f739 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -45,12 +45,6 @@ describe("mainmenu", () => { expect(mockMenuLoop.showLoop).toHaveBeenCalled(); }); - - it("shows the exit message after the menu loop", async () => { - await mainmenu.show(); - - expect(mockUiService.showInfoMessage).toHaveBeenCalledWith("K-THX-BYE"); - }); }); describe("promptMainMenu", () => { From 29997f60098ee12cf4e4b548cc5ff69038c59e93 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 26 May 2025 09:37:59 +0200 Subject: [PATCH 51/59] Fixes persmission issue for eth.key on windows --- src/services/ethersService.js | 2 ++ src/ui/mainMenu.js | 2 +- src/ui/mainMenu.test.js | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/services/ethersService.js b/src/services/ethersService.js index 870a39b..d9d0782 100644 --- a/src/services/ethersService.js +++ b/src/services/ethersService.js @@ -34,6 +34,8 @@ export class EthersService { const username = this.os.getUsername(); this.shell.run(`icacls ${paths.key} /inheritance:r >nul 2>&1`); this.shell.run(`icacls ${paths.key} /grant:r ${username}:F >nul 2>&1`); + this.shell.run(`icacls ${paths.key} /remove SYSTEM >nul 2>&1`); + this.shell.run(`icacls ${paths.key} /remove Administrators >nul 2>&1`); } else { this.shell.run(`chmod 600 "${paths.key}"`); } diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index 19de272..57e60c9 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -61,7 +61,7 @@ export class MainMenu { action: this.stopCodex, }, { - label: "Exit", + label: "Exit (Codex keeps running)", action: this.loop.stopLoop, }, ]); diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index 009f739..f3318db 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -110,7 +110,7 @@ describe("mainmenu", () => { [ { label: "Open Codex app", action: mockCodexApp.openCodexApp }, { label: "Stop Codex", action: mainmenu.stopCodex }, - { label: "Exit", action: mockMenuLoop.stopLoop }, + { label: "Exit (Codex keeps running)", action: mockMenuLoop.stopLoop }, ], ); }); From 162aac8f5d9514e4e8a94860a289f558f92a2dba Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 26 May 2025 10:46:02 +0200 Subject: [PATCH 52/59] almost working upload and download examples --- package-lock.json | 37 +++++++++++++++++++++++++++++++++++++ package.json | 1 + src/ui/mainMenu.js | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/package-lock.json b/package-lock.json index fa1a663..e34cb3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.16", "license": "MIT", "dependencies": { + "@codex-storage/sdk-js": "^0.1.2", "axios": "^1.6.2", "boxen": "^7.1.1", "chalk": "^5.3.0", @@ -39,6 +40,20 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", "license": "MIT" }, + "node_modules/@codex-storage/sdk-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@codex-storage/sdk-js/-/sdk-js-0.1.2.tgz", + "integrity": "sha512-QOi2gONLA9G1LVtFJLl9WjjZbAAvf4j1VVEAm//BUjUJi0xay309fb1z0kuW771ybNA+KHnVaYQu+RJS1K518Q==", + "dependencies": { + "valibot": "^1.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "peerDependencies": { + "undici": "^7.7.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -2325,6 +2340,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", + "peer": true, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", @@ -2346,6 +2370,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/valibot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.1.0.tgz", + "integrity": "sha512-Nk8lX30Qhu+9txPYTwM0cFlWLdPFsFr6LblzqIySfbZph9+BFsAHsNvHOymEviUepeIW6KFHzpX8TKhbptBXXw==", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/vite": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", diff --git a/package.json b/package.json index ae2683b..fa105ed 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "author": "Codex Storage", "license": "MIT", "dependencies": { + "@codex-storage/sdk-js": "^0.1.2", "axios": "^1.6.2", "boxen": "^7.1.1", "chalk": "^5.3.0", diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index 57e60c9..ad7f768 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -1,3 +1,6 @@ +import { Codex } from "@codex-storage/sdk-js"; +import { NodeUploadStategy } from "@codex-storage/sdk-js/node"; + export class MainMenu { constructor( uiService, @@ -60,6 +63,10 @@ export class MainMenu { label: "Stop Codex", action: this.stopCodex, }, + { + label: "DoThing", + action: this.doThing, + }, { label: "Exit (Codex keeps running)", action: this.loop.stopLoop, @@ -109,4 +116,29 @@ export class MainMenu { this.ui.showErrorMessage(`Failed to stop Codex. "${exception}"`); } }; + + doThing = async () => { + console.log("A!"); + + const codex = new Codex("http://localhost:8080"); + const data = codex.data; + + const stategy = new NodeUploadStategy("Hello World !"); + const uploadResponse = data.upload(stategy); + + const res = await uploadResponse.result; + + if (res.error) { + console.error(res.data); + return; + } + + console.info("CID is", res.data); + const cid = res.data; + + const result = await data.networkDownloadStream(cid); + + console.log("download: " + JSON.stringify(result)); + + } } From 94c504067dea42a28377c545ab2f25aabb81ff9d Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 26 May 2025 11:22:29 +0200 Subject: [PATCH 53/59] setting up data menu and service --- src/main.js | 5 ++++ src/services/dataService.js | 56 +++++++++++++++++++++++++++++++++++++ src/ui/dataMenu.js | 36 ++++++++++++++++++++++++ src/ui/mainMenu.js | 38 ++++++------------------- src/ui/mainMenu.test.js | 5 +++- 5 files changed, 109 insertions(+), 31 deletions(-) create mode 100644 src/services/dataService.js create mode 100644 src/ui/dataMenu.js diff --git a/src/main.js b/src/main.js index c10ebcb..a4bca65 100644 --- a/src/main.js +++ b/src/main.js @@ -36,6 +36,8 @@ import { CodexGlobals } from "./services/codexGlobals.js"; import { CodexApp } from "./services/codexApp.js"; import { EthersService } from "./services/ethersService.js"; import { MarketplaceSetup } from "./ui/marketplaceSetup.js"; +import { DataService } from "./services/dataService.js"; +import { DataMenu } from "./ui/dataMenu.js"; async function showNavigationMenu() { console.log("\n"); @@ -147,6 +149,8 @@ export async function main() { fsService, codexGlobals, ); + const dataService = new DataService(configService); + const dataMenu = new DataMenu(uiService, fsService, dataService); const mainMenu = new MainMenu( uiService, new MenuLoop(), @@ -155,6 +159,7 @@ export async function main() { installer, processControl, codexApp, + dataMenu, ); await mainMenu.show(); diff --git a/src/services/dataService.js b/src/services/dataService.js new file mode 100644 index 0000000..abc1a00 --- /dev/null +++ b/src/services/dataService.js @@ -0,0 +1,56 @@ +import { Codex } from "@codex-storage/sdk-js"; +import { NodeUploadStategy } from "@codex-storage/sdk-js/node"; +import mime from "mime-types"; +import path from "path"; +import fs from "fs"; + +export class DataService { + constructor(configService) { + this.configService = configService; + } + + upload = async (filePath) => { + const data = this.getCodexData(); + const filename = path.basename(filePath); + const contentType = mime.lookup(filePath) || "application/octet-stream"; + const fileData = fs.readFileSync(filePath); + + const strategy = new NodeUploadStategy(fileData, { + filename: filename, + mimetype: contentType, + }); + const uploadResponse = data.upload(strategy); + const res = await uploadResponse.result; + + if (res.error) { + throw new Exception(res.data); + } + return res.data; + }; + + download = async (cid) => { + const data = this.getCodexData(); + const manifest = await data.fetchManifest(cid); + const filename = this.getFilename(manifest); + + const response = await data.networkDownloadStream(cid); + const fileData = response.data; + + fs.writeFileSync(filename, fileData); + }; + + getCodexData = () => { + const config = this.configService.get(); + const url = `http://localhost:${config.ports.apiPort}`; + const codex = new Codex(url); + return codex.data; + }; + + getFilename = (manifest) => { + const defaultFilename = "unknown_" + Math.random(); + const filename = manifest?.data?.manifest?.filename; + + if (filename == undefined || filename.length < 1) return defaultFilename; + return filename; + }; +} diff --git a/src/ui/dataMenu.js b/src/ui/dataMenu.js new file mode 100644 index 0000000..456b97b --- /dev/null +++ b/src/ui/dataMenu.js @@ -0,0 +1,36 @@ +export class DataMenu { + constructor(uiService, fsService, dataService) { + this.ui = uiService; + this.fs = fsService; + this.dataService = dataService; + } + + performUpload = async () => { + this.ui.showInfoMessage( + "⚠️ Codex does not encrypt files. Anything uploaded will be available publicly on testnet.", + ); + + const filePath = this.ui.askPrompt("Enter the file path"); + if (!this.fs.isFile(filePath)) { + this.ui.showErrorMessage("File not found"); + } else { + try { + const cid = this.dataService.upload(filePath); + this.ui.showInfoMessage(`Upload successful.\n CID: '${cid}'`); + } catch (exception) { + this.ui.showErrorMessage("Error during upload: " + exception); + } + } + }; + + performDownload = async () => { + const cid = this.ui.askPrompt("Enter the CID"); + if (cid.length < 1) return; + try { + const filename = this.dataService.download(cid); + this.ui.showInfoMessage(`Download successful.\n File: '${filename}'`); + } catch (exception) { + this.ui.showErrorMessage("Error during download: " + exception); + } + }; +} diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index ad7f768..1c02562 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -1,6 +1,3 @@ -import { Codex } from "@codex-storage/sdk-js"; -import { NodeUploadStategy } from "@codex-storage/sdk-js/node"; - export class MainMenu { constructor( uiService, @@ -10,6 +7,7 @@ export class MainMenu { installer, processControl, codexApp, + dataMenu, ) { this.ui = uiService; this.loop = menuLoop; @@ -18,6 +16,7 @@ export class MainMenu { this.installer = installer; this.processControl = processControl; this.codexApp = codexApp; + this.dataMenu = dataMenu; this.loop.initialize(this.promptMainMenu); } @@ -64,8 +63,12 @@ export class MainMenu { action: this.stopCodex, }, { - label: "DoThing", - action: this.doThing, + label: "Upload a file", + action: this.dataMenu.performUpload, + }, + { + label: "Download a file", + action: this.dataMenu.performDownload, }, { label: "Exit (Codex keeps running)", @@ -116,29 +119,4 @@ export class MainMenu { this.ui.showErrorMessage(`Failed to stop Codex. "${exception}"`); } }; - - doThing = async () => { - console.log("A!"); - - const codex = new Codex("http://localhost:8080"); - const data = codex.data; - - const stategy = new NodeUploadStategy("Hello World !"); - const uploadResponse = data.upload(stategy); - - const res = await uploadResponse.result; - - if (res.error) { - console.error(res.data); - return; - } - - console.info("CID is", res.data); - const cid = res.data; - - const result = await data.networkDownloadStream(cid); - - console.log("download: " + JSON.stringify(result)); - - } } diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index f3318db..445af30 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -110,7 +110,10 @@ describe("mainmenu", () => { [ { label: "Open Codex app", action: mockCodexApp.openCodexApp }, { label: "Stop Codex", action: mainmenu.stopCodex }, - { label: "Exit (Codex keeps running)", action: mockMenuLoop.stopLoop }, + { + label: "Exit (Codex keeps running)", + action: mockMenuLoop.stopLoop, + }, ], ); }); From b13089c0b276bafaf7993854685b56c1b87ee7ef Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 26 May 2025 11:39:57 +0200 Subject: [PATCH 54/59] working upload --- src/services/dataService.js | 16 +++++++++------- src/ui/dataMenu.js | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/services/dataService.js b/src/services/dataService.js index abc1a00..ff9ded2 100644 --- a/src/services/dataService.js +++ b/src/services/dataService.js @@ -1,6 +1,5 @@ import { Codex } from "@codex-storage/sdk-js"; import { NodeUploadStategy } from "@codex-storage/sdk-js/node"; -import mime from "mime-types"; import path from "path"; import fs from "fs"; @@ -11,19 +10,22 @@ export class DataService { upload = async (filePath) => { const data = this.getCodexData(); + + // We can use mime util to determine the content type of the file. But Codex will reject some + // mimetypes. So we set it to octet-stream always. + const contentType = "application/octet-stream"; + const filename = path.basename(filePath); - const contentType = mime.lookup(filePath) || "application/octet-stream"; const fileData = fs.readFileSync(filePath); - const strategy = new NodeUploadStategy(fileData, { - filename: filename, - mimetype: contentType, - }); + const metadata = { filename: filename, mimetype: contentType }; + + const strategy = new NodeUploadStategy(fileData, metadata); const uploadResponse = data.upload(strategy); const res = await uploadResponse.result; if (res.error) { - throw new Exception(res.data); + throw new Error(res.data); } return res.data; }; diff --git a/src/ui/dataMenu.js b/src/ui/dataMenu.js index 456b97b..7ea0d2c 100644 --- a/src/ui/dataMenu.js +++ b/src/ui/dataMenu.js @@ -10,12 +10,12 @@ export class DataMenu { "⚠️ Codex does not encrypt files. Anything uploaded will be available publicly on testnet.", ); - const filePath = this.ui.askPrompt("Enter the file path"); + const filePath = await this.ui.askPrompt("Enter the file path"); if (!this.fs.isFile(filePath)) { this.ui.showErrorMessage("File not found"); } else { try { - const cid = this.dataService.upload(filePath); + const cid = await this.dataService.upload(filePath); this.ui.showInfoMessage(`Upload successful.\n CID: '${cid}'`); } catch (exception) { this.ui.showErrorMessage("Error during upload: " + exception); @@ -24,10 +24,10 @@ export class DataMenu { }; performDownload = async () => { - const cid = this.ui.askPrompt("Enter the CID"); + const cid = await this.ui.askPrompt("Enter the CID"); if (cid.length < 1) return; try { - const filename = this.dataService.download(cid); + const filename = await this.dataService.download(cid); this.ui.showInfoMessage(`Download successful.\n File: '${filename}'`); } catch (exception) { this.ui.showErrorMessage("Error during download: " + exception); From 05daffc2f580ed21f06020168b801fd8ac582d7c Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 26 May 2025 12:51:16 +0200 Subject: [PATCH 55/59] Test updates for mainmenu and datamenu --- src/__mocks__/service.mocks.js | 5 ++ src/__mocks__/ui.mocks.js | 5 ++ src/services/dataService.js | 4 +- src/ui/dataMenu.test.js | 123 +++++++++++++++++++++++++++++++++ src/ui/mainMenu.test.js | 11 ++- 5 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 src/ui/dataMenu.test.js diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index d6416a1..2bf8515 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -64,3 +64,8 @@ export const mockCodexApp = { export const mockMarketplaceSetup = { runClientWizard: vi.fn(), }; + +export const mockDataService = { + upload: vi.fn(), + download: vi.fn(), +}; diff --git a/src/__mocks__/ui.mocks.js b/src/__mocks__/ui.mocks.js index 1b0b0e2..4a5be0c 100644 --- a/src/__mocks__/ui.mocks.js +++ b/src/__mocks__/ui.mocks.js @@ -7,3 +7,8 @@ export const mockInstallMenu = { export const mockConfigMenu = { show: vi.fn(), }; + +export const mockDataMenu = { + performUpload: vi.fn(), + performDownload: vi.fn(), +}; diff --git a/src/services/dataService.js b/src/services/dataService.js index ff9ded2..7f1701e 100644 --- a/src/services/dataService.js +++ b/src/services/dataService.js @@ -20,7 +20,7 @@ export class DataService { const metadata = { filename: filename, mimetype: contentType }; - const strategy = new NodeUploadStategy(fileData, metadata); + const strategy = new NodeUploadStategy(fileData, metadata); const uploadResponse = data.upload(strategy); const res = await uploadResponse.result; @@ -31,6 +31,8 @@ export class DataService { }; download = async (cid) => { + throw new Error("Waiting for fix of codex-js sdk"); + const data = this.getCodexData(); const manifest = await data.fetchManifest(cid); const filename = this.getFilename(manifest); diff --git a/src/ui/dataMenu.test.js b/src/ui/dataMenu.test.js new file mode 100644 index 0000000..df60d71 --- /dev/null +++ b/src/ui/dataMenu.test.js @@ -0,0 +1,123 @@ +import { describe, beforeEach, it, expect, vi } from "vitest"; +import { DataMenu } from "./dataMenu.js"; +import { + mockUiService, + mockFsService, + mockDataService, +} from "../__mocks__/service.mocks.js"; + +describe("DataMenu", () => { + let dataMenu; + const filePath = "testfilepath"; + const cid = "testcid"; + + beforeEach(() => { + vi.resetAllMocks(); + + dataMenu = new DataMenu(mockUiService, mockFsService, mockDataService); + }); + + describe("performUpload", () => { + beforeEach(() => { + mockUiService.askPrompt.mockResolvedValue(filePath); + mockDataService.upload.mockResolvedValue(cid); + }); + + it("shows encryption warning", async () => { + await dataMenu.performUpload(); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "⚠️ Codex does not encrypt files. Anything uploaded will be available publicly on testnet.", + ); + }); + + it("prompts the user for a filepath", async () => { + await dataMenu.performUpload(); + + expect(mockUiService.askPrompt).toHaveBeenCalledWith( + "Enter the file path", + ); + }); + + it("checks that the provided path is a file", async () => { + await dataMenu.performUpload(); + + expect(mockFsService.isFile).toHaveBeenCalledWith(filePath); + }); + + it("shows an error when the provided path is not a file", async () => { + mockFsService.isFile.mockReturnValue(false); + + await dataMenu.performUpload(); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + "File not found", + ); + }); + + it("calls the data service if the file does exist", async () => { + mockFsService.isFile.mockReturnValue(true); + + await dataMenu.performUpload(); + + expect(mockDataService.upload).toHaveBeenCalledWith(filePath); + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + `Upload successful.\n CID: '${cid}'`, + ); + }); + + it("shows an error message when dataService throws", async () => { + const error = "testError"; + mockFsService.isFile.mockReturnValue(true); + mockDataService.upload.mockRejectedValueOnce(new Error(error)); + + await dataMenu.performUpload(); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + "Error during upload: Error: " + error, + ); + }); + }); + + describe("performDownload", () => { + beforeEach(() => { + mockUiService.askPrompt.mockResolvedValue(cid); + mockDataService.download.mockResolvedValue(filePath); + }); + + it("prompts the user for a cid", async () => { + await dataMenu.performDownload(); + + expect(mockUiService.askPrompt).toHaveBeenCalledWith("Enter the CID"); + }); + + it("does nothing if provided input is empty", async () => { + mockUiService.askPrompt = vi.fn(); + mockUiService.askPrompt.mockResolvedValue(""); + + await dataMenu.performDownload(); + + expect(mockDataService.download).not.toHaveBeenCalled(); + }); + + it("calls the data service with the provided cid", async () => { + await dataMenu.performDownload(); + + expect(mockDataService.download).toHaveBeenCalledWith(cid); + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + `Download successful.\n File: '${filePath}'`, + ); + }); + + it("shows an error message when dataService throws", async () => { + const error = "testError"; + mockDataService.download.mockRejectedValueOnce(new Error(error)); + + await dataMenu.performDownload(); + + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + "Error during download: Error: " + error, + ); + }); + }); +}); diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index 445af30..7b9c6f8 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -1,7 +1,11 @@ import { describe, beforeEach, it, expect, vi } from "vitest"; import { MainMenu } from "./mainMenu.js"; import { mockUiService, mockCodexApp } from "../__mocks__/service.mocks.js"; -import { mockInstallMenu, mockConfigMenu } from "../__mocks__/ui.mocks.js"; +import { + mockInstallMenu, + mockConfigMenu, + mockDataMenu, +} from "../__mocks__/ui.mocks.js"; import { mockInstaller, mockProcessControl, @@ -22,6 +26,7 @@ describe("mainmenu", () => { mockInstaller, mockProcessControl, mockCodexApp, + mockDataMenu, ); }); @@ -102,7 +107,7 @@ describe("mainmenu", () => { }); describe("showRunningMenu", () => { - it("shows a menu with options to stop Codex, open Codex app, or exit", async () => { + it("shows a menu with options to stop Codex, open Codex app, upload, download, or exit", async () => { await mainmenu.showRunningMenu(); expect(mockUiService.askMultipleChoice).toHaveBeenCalledWith( @@ -110,6 +115,8 @@ describe("mainmenu", () => { [ { label: "Open Codex app", action: mockCodexApp.openCodexApp }, { label: "Stop Codex", action: mainmenu.stopCodex }, + { label: "Upload a file", action: mockDataMenu.performUpload }, + { label: "Download a file", action: mockDataMenu.performDownload }, { label: "Exit (Codex keeps running)", action: mockMenuLoop.stopLoop, From 25000aa807f38fc9f13602199e102941d2d9a89c Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 26 May 2025 13:30:31 +0200 Subject: [PATCH 56/59] fixes download --- src/services/dataService.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/services/dataService.js b/src/services/dataService.js index 7f1701e..d77ab40 100644 --- a/src/services/dataService.js +++ b/src/services/dataService.js @@ -31,16 +31,15 @@ export class DataService { }; download = async (cid) => { - throw new Error("Waiting for fix of codex-js sdk"); - const data = this.getCodexData(); const manifest = await data.fetchManifest(cid); const filename = this.getFilename(manifest); const response = await data.networkDownloadStream(cid); - const fileData = response.data; + const fileData = await response.data.text(); fs.writeFileSync(filename, fileData); + return filename; }; getCodexData = () => { From 367b889055be74e4794a3b3ee9f96a0e456c4457 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 3 Jun 2025 09:24:53 +0200 Subject: [PATCH 57/59] Handles the presense of previous versions of the config file --- src/services/configService.js | 6 ++++++ src/services/configService.test.js | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/services/configService.js b/src/services/configService.js index 66dd594..d17016b 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -55,6 +55,12 @@ export class ConfigService { this.saveConfig(); } else { this.config = this.fs.readJsonFile(filePath); + + if (this.config.codexRoot == undefined) { + this.config = defaultConfig; + this.saveConfig(); + } + } } catch (error) { console.error( diff --git a/src/services/configService.test.js b/src/services/configService.test.js index 2a83342..1e7cf04 100644 --- a/src/services/configService.test.js +++ b/src/services/configService.test.js @@ -53,6 +53,7 @@ describe("ConfigService", () => { it("loads the config.json file when it does exist", () => { mockFsService.isFile.mockReturnValue(true); const savedConfig = { + codexRoot: "defined", isTestConfig: "Yes, very", }; mockFsService.readJsonFile.mockReturnValue(savedConfig); @@ -64,6 +65,22 @@ describe("ConfigService", () => { expect(mockFsService.writeJsonFile).not.toHaveBeenCalled(); expect(service.config).toEqual(savedConfig); }); + + it("saves the default config when config.json exists but doesn't define the codexRoot", () => { + mockFsService.isFile.mockReturnValue(true); + const savedConfig = { + codexRoot: undefined, // it still blows my mind we have a language in which we can define things to be undefined. + isTestConfig: "Yes, very", + }; + mockFsService.readJsonFile.mockReturnValue(savedConfig); + + const service = new ConfigService(mockFsService, mockOsService); + + expect(mockFsService.isFile).toHaveBeenCalledWith(configPath); + expect(mockFsService.readJsonFile).toHaveBeenCalledWith(configPath); + expect(mockFsService.writeJsonFile).toHaveBeenCalled(); + expect(service.config).toEqual(expectedDefaultConfig); + }); }); describe("getCodexExe", () => { From 1ea946abc2815ece878d8eb3e0a79d5f61455da9 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 4 Jun 2025 13:16:31 +0200 Subject: [PATCH 58/59] setting up local data and node status --- src/main.js | 3 ++ src/services/dataService.js | 30 +++++++++++- src/ui/dataMenu.js | 42 +++++++++++++++++ src/ui/mainMenu.js | 10 ++++ src/ui/nodeStatusMenu.js | 93 +++++++++++++++++++++++++++++++++++++ 5 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 src/ui/nodeStatusMenu.js diff --git a/src/main.js b/src/main.js index a4bca65..5d98f36 100644 --- a/src/main.js +++ b/src/main.js @@ -38,6 +38,7 @@ import { EthersService } from "./services/ethersService.js"; import { MarketplaceSetup } from "./ui/marketplaceSetup.js"; import { DataService } from "./services/dataService.js"; import { DataMenu } from "./ui/dataMenu.js"; +import { NodeStatusMenu } from "./ui/nodeStatusMenu.js"; async function showNavigationMenu() { console.log("\n"); @@ -151,6 +152,7 @@ export async function main() { ); const dataService = new DataService(configService); const dataMenu = new DataMenu(uiService, fsService, dataService); + const nodeStatusMenu = new NodeStatusMenu(uiService, dataService, new MenuLoop()); const mainMenu = new MainMenu( uiService, new MenuLoop(), @@ -160,6 +162,7 @@ export async function main() { processControl, codexApp, dataMenu, + nodeStatusMenu ); await mainMenu.show(); diff --git a/src/services/dataService.js b/src/services/dataService.js index d77ab40..1633349 100644 --- a/src/services/dataService.js +++ b/src/services/dataService.js @@ -42,11 +42,37 @@ export class DataService { return filename; }; - getCodexData = () => { + debugInfo = async () => { + const debug = this.getCodexDebug(); + const res = await debug.info(); + if (res.error) { + throw new Error(res.data); + } + return res.data; + }; + + localData = async () => { + const data = this.getCodexData(); + const res = await data.cids(); + if (res.error) { + throw new Error(res.data); + } + return res.data; + }; + + getCodex = () => { const config = this.configService.get(); const url = `http://localhost:${config.ports.apiPort}`; const codex = new Codex(url); - return codex.data; + return codex; + }; + + getCodexData = () => { + return this.getCodex().data; + }; + + getCodexDebug = () => { + return this.getCodex().debug; }; getFilename = (manifest) => { diff --git a/src/ui/dataMenu.js b/src/ui/dataMenu.js index 7ea0d2c..679f5db 100644 --- a/src/ui/dataMenu.js +++ b/src/ui/dataMenu.js @@ -1,3 +1,5 @@ +import chalk from "chalk"; + export class DataMenu { constructor(uiService, fsService, dataService) { this.ui = uiService; @@ -33,4 +35,44 @@ export class DataMenu { this.ui.showErrorMessage("Error during download: " + exception); } }; + + showLocalData = async () => { + try { + const localData = await this.dataService.localData(); + this.displayLocalData(localData); + } catch (exception) { + this.ui.showErrorMessage("Failed to fetch local data: " + exception); + } + }; + + displayLocalData = (filesData) => { + if (filesData.content && filesData.content.length > 0) { + this.ui.showInfoMessage( + `Found ${filesData.content.length} local file(s)`, + ); + + filesData.content.forEach((file, index) => { + const { cid, manifest } = file; + const { + datasetSize, + protected: isProtected, + filename, + mimetype, + } = manifest; + + const fileSize = (datasetSize / 1024).toFixed(2); + + this.ui.showInfoMessage( + `${chalk.cyan("File")} ${index + 1} of ${filesData.content.length}\n\n` + + `${chalk.cyan("Filename:")} ${filename}\n` + + `${chalk.cyan("CID:")} ${cid}\n` + + `${chalk.cyan("Size:")} ${fileSize} KB\n` + + `${chalk.cyan("MIME Type:")} ${mimetype}\n` + + `${chalk.cyan("Protected:")} ${isProtected ? chalk.green("Yes") : chalk.red("No")}`, + ); + }); + } else { + this.ui.showInfoMessage("Node contains no datasets."); + } + }; } diff --git a/src/ui/mainMenu.js b/src/ui/mainMenu.js index 1c02562..5d77078 100644 --- a/src/ui/mainMenu.js +++ b/src/ui/mainMenu.js @@ -8,6 +8,7 @@ export class MainMenu { processControl, codexApp, dataMenu, + nodeStatusMenu, ) { this.ui = uiService; this.loop = menuLoop; @@ -17,6 +18,7 @@ export class MainMenu { this.processControl = processControl; this.codexApp = codexApp; this.dataMenu = dataMenu; + this.nodeStatusMenu = nodeStatusMenu; this.loop.initialize(this.promptMainMenu); } @@ -62,6 +64,10 @@ export class MainMenu { label: "Stop Codex", action: this.stopCodex, }, + { + label: "Show node status", + action: this.nodeStatusMenu.showNodeStatus, + }, { label: "Upload a file", action: this.dataMenu.performUpload, @@ -70,6 +76,10 @@ export class MainMenu { label: "Download a file", action: this.dataMenu.performDownload, }, + { + label: "Show local data", + action: this.dataMenu.showLocalData, + }, { label: "Exit (Codex keeps running)", action: this.loop.stopLoop, diff --git a/src/ui/nodeStatusMenu.js b/src/ui/nodeStatusMenu.js new file mode 100644 index 0000000..ea97f11 --- /dev/null +++ b/src/ui/nodeStatusMenu.js @@ -0,0 +1,93 @@ +import chalk from "chalk"; + +export class NodeStatusMenu { + constructor(uiService, dataService, menuLoop) { + this.ui = uiService; + this.dataService = dataService; + this.loop = menuLoop; + + this.loop.initialize(this.showNodeStatusMenu); + } + + showNodeStatus = async () => { + this.debugInfo = await this.fetchDebugInfo(); + if (this.debugInfo == undefined) return; + + const peerCount = this.debugInfo.table.nodes.length; + const isOnline = peerCount > 2; + + if (isOnline) { + this.ui.showSuccessMessage( + "Node is ONLINE & DISCOVERABLE", + "🔌 Node Status", + ); + } else { + this.ui.showInfoMessage( + "Node is ONLINE but has few peers", + "🔌 Node Status", + ); + } + + await this.loop.showLoop(); + }; + + showNodeStatusMenu = async () => { + await this.ui.askMultipleChoice("Select information to view:", [ + { + label: "View connected peers", + action: this.showPeers, + }, + { + label: "View node information", + action: this.showNodeInfo, + }, + { + label: "Back to Main Menu", + action: this.loop.stopLoop, + }, + ]); + }; + + showPeers = async () => { + const peerCount = this.debugInfo.table.nodes.length; + if (peerCount > 0) { + this.ui.showInfoMessage("Connected Peers"); + this.debugInfo.table.nodes.forEach((node, index) => { + this.ui.showInfoMessage( + `Peer ${index + 1}:\n` + + `${chalk.cyan("Peer ID:")} ${node.peerId}\n` + + `${chalk.cyan("Address:")} ${node.address}\n` + + `${chalk.cyan("Status:")} ${node.seen ? chalk.green("Active") : chalk.gray("Inactive")}`, + ); + }); + } else { + this.ui.showInfoMessage("No connected peers found."); + } + }; + + showNodeInfo = async () => { + const data = this.debugInfo; + this.ui.showInfoMessage( + `${chalk.cyan("Version:")} ${data.codex.version}\n` + + `${chalk.cyan("Revision:")} ${data.codex.revision}\n\n` + + `${chalk.cyan("Node ID:")} ${data.table.localNode.nodeId}\n` + + `${chalk.cyan("Peer ID:")} ${data.table.localNode.peerId}\n` + + `${chalk.cyan("Listening Address:")} ${data.table.localNode.address}\n\n` + + `${chalk.cyan("Public IP:")} ${data.announceAddresses[0].split("/")[2]}\n` + + `${chalk.cyan("Port:")} ${data.announceAddresses[0].split("/")[4]}\n` + + `${chalk.cyan("Connected Peers:")} ${data.table.nodes.length}`, + ); + }; + + fetchDebugInfo = async () => { + const spinner = this.ui.createAndStartSpinner("Fetching..."); + try { + return await this.dataService.debugInfo(); + } catch { + this.ui.showErrorMessage("Failed to fetch debug/info"); + return; + } finally { + this.ui.stopSpinnerSuccess(spinner); + } + }; +} From a6ff276e9a727c2c824fc18702691f4c330b53c1 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 4 Jun 2025 14:36:09 +0200 Subject: [PATCH 59/59] tests and format --- src/__mocks__/service.mocks.js | 2 + src/__mocks__/ui.mocks.js | 5 ++ src/main.js | 8 ++- src/services/configService.js | 1 - src/ui/dataMenu.js | 14 +++--- src/ui/dataMenu.test.js | 91 ++++++++++++++++++++++++++++++++++ src/ui/mainMenu.test.js | 7 +++ 7 files changed, 117 insertions(+), 11 deletions(-) diff --git a/src/__mocks__/service.mocks.js b/src/__mocks__/service.mocks.js index 2bf8515..14b1d81 100644 --- a/src/__mocks__/service.mocks.js +++ b/src/__mocks__/service.mocks.js @@ -68,4 +68,6 @@ export const mockMarketplaceSetup = { export const mockDataService = { upload: vi.fn(), download: vi.fn(), + debugInfo: vi.fn(), + localData: vi.fn(), }; diff --git a/src/__mocks__/ui.mocks.js b/src/__mocks__/ui.mocks.js index 4a5be0c..b63bcb8 100644 --- a/src/__mocks__/ui.mocks.js +++ b/src/__mocks__/ui.mocks.js @@ -11,4 +11,9 @@ export const mockConfigMenu = { export const mockDataMenu = { performUpload: vi.fn(), performDownload: vi.fn(), + showLocalData: vi.fn(), +}; + +export const mockNodeStatusMenu = { + showNodeStatus: vi.fn(), }; diff --git a/src/main.js b/src/main.js index 5d98f36..a910904 100644 --- a/src/main.js +++ b/src/main.js @@ -152,7 +152,11 @@ export async function main() { ); const dataService = new DataService(configService); const dataMenu = new DataMenu(uiService, fsService, dataService); - const nodeStatusMenu = new NodeStatusMenu(uiService, dataService, new MenuLoop()); + const nodeStatusMenu = new NodeStatusMenu( + uiService, + dataService, + new MenuLoop(), + ); const mainMenu = new MainMenu( uiService, new MenuLoop(), @@ -162,7 +166,7 @@ export async function main() { processControl, codexApp, dataMenu, - nodeStatusMenu + nodeStatusMenu, ); await mainMenu.show(); diff --git a/src/services/configService.js b/src/services/configService.js index d17016b..471711d 100644 --- a/src/services/configService.js +++ b/src/services/configService.js @@ -60,7 +60,6 @@ export class ConfigService { this.config = defaultConfig; this.saveConfig(); } - } } catch (error) { console.error( diff --git a/src/ui/dataMenu.js b/src/ui/dataMenu.js index 679f5db..6032a6a 100644 --- a/src/ui/dataMenu.js +++ b/src/ui/dataMenu.js @@ -1,5 +1,3 @@ -import chalk from "chalk"; - export class DataMenu { constructor(uiService, fsService, dataService) { this.ui = uiService; @@ -63,12 +61,12 @@ export class DataMenu { const fileSize = (datasetSize / 1024).toFixed(2); this.ui.showInfoMessage( - `${chalk.cyan("File")} ${index + 1} of ${filesData.content.length}\n\n` + - `${chalk.cyan("Filename:")} ${filename}\n` + - `${chalk.cyan("CID:")} ${cid}\n` + - `${chalk.cyan("Size:")} ${fileSize} KB\n` + - `${chalk.cyan("MIME Type:")} ${mimetype}\n` + - `${chalk.cyan("Protected:")} ${isProtected ? chalk.green("Yes") : chalk.red("No")}`, + `File ${index + 1} of ${filesData.content.length}\n\n` + + `Filename: ${filename}\n` + + `CID: ${cid}\n` + + `Size: ${fileSize} KB\n` + + `MIME Type: ${mimetype}\n` + + `Protected: ${isProtected ? "Yes" : "No"}`, ); }); } else { diff --git a/src/ui/dataMenu.test.js b/src/ui/dataMenu.test.js index df60d71..9be70a9 100644 --- a/src/ui/dataMenu.test.js +++ b/src/ui/dataMenu.test.js @@ -120,4 +120,95 @@ describe("DataMenu", () => { ); }); }); + + describe("showLocalData", () => { + beforeEach(() => { + dataMenu.displayLocalData = vi.fn(); + }); + + it("calls localData on dataService", async () => { + await dataMenu.showLocalData(); + + expect(mockDataService.localData).toHaveBeenCalled(); + }); + + it("passes localData to displayLocalData", async () => { + const someData = "yes"; + + mockDataService.localData.mockResolvedValue(someData); + + await dataMenu.showLocalData(); + + expect(dataMenu.displayLocalData).toHaveBeenCalledWith(someData); + }); + + it("shows an error message when localData raises", async () => { + const error = "Omg error!"; + mockDataService.localData.mockRejectedValue(new Error(error)); + + await dataMenu.showLocalData(); + + expect(dataMenu.displayLocalData).not.toHaveBeenCalled(); + expect(mockUiService.showErrorMessage).toHaveBeenCalledWith( + "Failed to fetch local data: Error: " + error, + ); + }); + }); + + describe("displayLocalData", () => { + const cid = "testCid"; + const datasetSize = 2048; + var isProtected = true; + const filename = "filename.test"; + const mimetype = "test"; + var fileData = {}; + + beforeEach(() => { + fileData = { + content: [ + { + cid: cid, + manifest: { + datasetSize, + protected: isProtected, + filename, + mimetype, + }, + }, + ], + }; + }); + + it("shows no datasets when content is undefined", () => { + fileData.content = undefined; + + dataMenu.displayLocalData(fileData); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "Node contains no datasets.", + ); + }); + + it("shows no datasets when content is empty array", () => { + fileData.content = []; + dataMenu.displayLocalData(fileData); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + "Node contains no datasets.", + ); + }); + + it("shows details for each entry (protected)", () => { + dataMenu.displayLocalData(fileData); + + expect(mockUiService.showInfoMessage).toHaveBeenCalledWith( + `File 1 of 1\n\n` + + `Filename: ${filename}\n` + + `CID: ${cid}\n` + + `Size: ${(datasetSize / 1024).toFixed(2)} KB\n` + + `MIME Type: ${mimetype}\n` + + `Protected: Yes`, + ); + }); + }); }); diff --git a/src/ui/mainMenu.test.js b/src/ui/mainMenu.test.js index 7b9c6f8..bf4b3b3 100644 --- a/src/ui/mainMenu.test.js +++ b/src/ui/mainMenu.test.js @@ -5,6 +5,7 @@ import { mockInstallMenu, mockConfigMenu, mockDataMenu, + mockNodeStatusMenu, } from "../__mocks__/ui.mocks.js"; import { mockInstaller, @@ -27,6 +28,7 @@ describe("mainmenu", () => { mockProcessControl, mockCodexApp, mockDataMenu, + mockNodeStatusMenu, ); }); @@ -115,8 +117,13 @@ describe("mainmenu", () => { [ { label: "Open Codex app", action: mockCodexApp.openCodexApp }, { label: "Stop Codex", action: mainmenu.stopCodex }, + { + label: "Show node status", + action: mockNodeStatusMenu.showNodeStatus, + }, { label: "Upload a file", action: mockDataMenu.performUpload }, { label: "Download a file", action: mockDataMenu.performDownload }, + { label: "Show local data", action: mockDataMenu.showLocalData }, { label: "Exit (Codex keeps running)", action: mockMenuLoop.stopLoop,