diff --git a/docs/hackatime-doctor/README.md b/docs/hackatime-doctor/README.md new file mode 100644 index 00000000..7013038d --- /dev/null +++ b/docs/hackatime-doctor/README.md @@ -0,0 +1,150 @@ +# HackaTime Doctor ⚕️ + +![Terminal Screenshot](terminal-screenshot.png) + +A diagnostic tool that verifies your development environment meets all requirements for Hack Club's HackaTime and help in its setup and fixing. + +## Features + +### Core Checks +- ✅ Git installation check +- ✅ API connectivity test (HackaTime server) +- ✅ WakaTime config validation + +### Optional Checks +- ✅ Project structure validation (README.md, LICENSE, .gitignore) +- ✅ Node.js installation & version check (v16+) + +### Installation & Setup +- 🔄 Auto-install for missing packages +- 🔍 Multi-package manager support: + - Windows: Chocolatey (`choco`) + - macOS: Homebrew (`brew`) + - Linux: + - APT (Debian/Ubuntu) + - DNF/YUM (RHEL/Fedora) + - Pacman (Arch) + - Zypper (openSUSE) + - APK (Alpine) + - And many other package systems +- ⚙️ Interactive WakaTime config setup +- 🔐 Secure API key handling + +### Reporting & Output +- 📊 JSON report generation +- 📈 CSV report generation +- 🎨 Color-coded terminal output +- 📋 Detailed error messages with remediation steps + +### Cross-Platform Support +- 🖥️ Windows compatibility +- 🍏 macOS compatibility +- 🐧 Linux compatibility +- 🔄 Automatic path resolution (WAKATIME_HOME, XDG_CONFIG_HOME, etc.) + +### Advanced Features +- 📂 Multi-location config file detection: + - `~/.wakatime.cfg` + - `~/.config/wakatime/.wakatime.cfg` + - `/etc/wakatime.cfg` +- 🚦 Environment variable validation + +### Quick Install (Recommended) + +Download the latest release for your platform: + +**[📥 Download Latest Release](https://github.com/arungeorgesaji/hackatime-doctor/releases/latest)** + +#### Windows +1. Download the Windows release (`.zip` file) +2. Extract the zip file to your desired location +3. Open PowerShell in the extracted folder +4. Choco (Chocolatey) is required for the installation. If you don't have it installed, follow these steps: + + 1. Open PowerShell as Administrator + 2. Run the following command to install Chocolatey: + + ```powershell + iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + ``` + +5. Install OpenSSL if not already installed (required for HTTPS requests), follow these steps: + + 1. Open PowerShell as Administrator + 2. Run the following command to install OpenSSL: + + ```powershell + choco install openssl + ``` +6. Set console encoding to UTF-8 (recommended for proper output display in powershell): + + ```powershell + # Temporary (for current session only): + [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + + # Permanent (add to PowerShell profile): + Add-Content -Path $PROFILE -Value "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8" + ``` + +6. Run the executable directly from the extracted folder, or for global access, copy hackatime-doctor.exe to a directory in your system PATH (like C:\Program Files\HackaTime Doctor) +Note: You may need to run PowerShell as Administrator while running the script. + +#### Linux/macOS +1. Download the appropriate release for your platform in your desired location +2. Extract the archive: + + ```bash + tar -xzf hackatime-doctor-*.tar.gz + ``` +3. Run the installation script: + + ```bash + chmod +x install.sh + sudo ./install.sh + ``` + +Note: Some minimal linux distros may not have which installedby default, most likely you wouldnt face an issue with this +command, but if you do, you can install it using your package manager. + +### Build from Source + +If you prefer to compile from source: + +```bash +git clone https://github.com/arungeorgesaji/hackatime-doctor.git +cd hackatime-doctor +make +sudo make install +``` + +## Usage + +After installation, run the diagnostic tool: + +```bash +hackatime-doctor +``` + +Run extended diagnostics including project structure checks and checks for popular developer packages: + +```bash +hackatime-doctor --full-check +``` + +> 🚧 **Beta Feature** +> The `--full-check` currently includes basic extended validation. + +## Output Formats + +Generate reports in multiple formats: + +```bash +# Output to terminal +hackatime-doctor + +# JSON report +hackatime-doctor --json + +# CSV report +hackatime-doctor --csv +``` diff --git a/docs/hackatime-doctor/terminal-screenshot.png b/docs/hackatime-doctor/terminal-screenshot.png new file mode 100644 index 00000000..5caa3629 Binary files /dev/null and b/docs/hackatime-doctor/terminal-screenshot.png differ diff --git a/public/hackatime/hackatime-doctor/Makefile b/public/hackatime/hackatime-doctor/Makefile new file mode 100644 index 00000000..862e59a9 --- /dev/null +++ b/public/hackatime/hackatime-doctor/Makefile @@ -0,0 +1,83 @@ +CXX := g++ +MKDIR := mkdir -p +RM := rm -rf + +SRC_DIR := src +INCLUDE_DIR := include +OBJ_DIR := obj +BUILD_DIR := bin +TEST_DIR := tests + +SRCS := $(wildcard $(SRC_DIR)/*.cpp) +MAIN_SRC := $(SRC_DIR)/main.cpp +CHECK_SRCS := $(filter-out $(MAIN_SRC), $(SRCS)) +OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRCS)) +MAIN_OBJ := $(OBJ_DIR)/main.o +CHECK_OBJS := $(filter-out $(MAIN_OBJ), $(OBJS)) +DEPS := $(OBJS:.o=.d) + +TARGET := $(BUILD_DIR)/hackatime-doctor +TEST_TARGET := $(BUILD_DIR)/hackatime-tests + +CXXFLAGS := -std=c++17 -Wall -Wextra -pedantic -I$(INCLUDE_DIR) +DEBUG_FLAGS := -g -O0 +RELEASE_FLAGS := -O3 -DNDEBUG + +LDFLAGS := -lssl -lcrypto +TEST_LDFLAGS := -lgtest -lgtest_main -lpthread + +BUILD_TYPE ?= debug +ifeq ($(BUILD_TYPE),release) + CXXFLAGS += $(RELEASE_FLAGS) +else + CXXFLAGS += $(DEBUG_FLAGS) +endif + +all: $(TARGET) + +$(TARGET): $(OBJS) + @$(MKDIR) $(@D) + $(CXX) $(CXXFLAGS) $^ -o $@ $(LDFLAGS) + +test: $(TEST_TARGET) + @$(TEST_TARGET) + +$(TEST_TARGET): $(CHECK_OBJS) $(TEST_DIR)/tests.cpp + @$(MKDIR) $(@D) + $(CXX) $(CXXFLAGS) $^ -o $@ $(LDFLAGS) $(TEST_LDFLAGS) + +$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp + @$(MKDIR) $(@D) + $(CXX) $(CXXFLAGS) -MMD -MP -c $< -o $@ + +-include $(DEPS) + +release: BUILD_TYPE=release +release: clean all + +install: + @echo "To install, copy $(TARGET) to a directory in your PATH" + @echo "Example: sudo cp $(TARGET) /usr/local/bin/" + +uninstall: + @echo "To uninstall, remove the binary from your PATH" + @echo "Example: sudo rm /usr/local/bin/hackatime-doctor" + +format: + find $(SRC_DIR) $(INCLUDE_DIR) -name '*.cpp' -o -name '*.h' | xargs clang-format -i + +clean: + $(RM) $(OBJ_DIR) $(BUILD_DIR) + +help: + @echo "Available targets:" + @echo " all - Build the application (debug mode)" + @echo " release - Build optimized release version" + @echo " test - Build and run tests" + @echo " clean - Clean build files" + @echo " format - Format source code with clang-format" + @echo " install - Show install instructions" + @echo " uninstall- Show uninstall instructions" + @echo " help - Show this help" + +.PHONY: all test clean install uninstall format release help diff --git a/public/hackatime/hackatime-doctor/include/checks.h b/public/hackatime/hackatime-doctor/include/checks.h new file mode 100644 index 00000000..9b0b10a5 --- /dev/null +++ b/public/hackatime/hackatime-doctor/include/checks.h @@ -0,0 +1,48 @@ +#ifndef CHECKS_H +#define CHECKS_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __has_include() + #include + namespace fs = std::filesystem; +#elif __has_include() + #include + namespace fs = std::experimental::filesystem; +#else + #error "No filesystem support!" +#endif + +#if defined(_WIN32) + #include + #include + #include + #include + #pragma comment(lib, "ws2_32.lib") + #pragma comment(lib, "crypt32.lib") + #define close_socket closesocket + using socket_t = SOCKET; +#else + #include + #include + #include + #include + #include + #define close_socket close + using socket_t = int; + #define INVALID_SOCKET -1 +#endif + +#include +#include + +#endif diff --git a/public/hackatime/hackatime-doctor/include/hackatime_doctor.h b/public/hackatime/hackatime-doctor/include/hackatime_doctor.h new file mode 100644 index 00000000..be27c864 --- /dev/null +++ b/public/hackatime/hackatime-doctor/include/hackatime_doctor.h @@ -0,0 +1,31 @@ +#ifndef HACKATIME_DOCTOR_H +#define HACKATIME_DOCTOR_H + +#include +#include + +#define COLOR_RED "\x1b[31m" +#define COLOR_GREEN "\x1b[32m" +#define COLOR_YELLOW "\x1b[33m" +#define COLOR_BLUE "\x1b[34m" +#define COLOR_RESET "\x1b[0m" + +struct CheckResult { + bool success; + std::string message; + std::string name; + + CheckResult(bool s, std::string m, std::string n = "") + : success(s), message(std::move(m)), name(std::move(n)) {} +}; + +CheckResult check_wakatime_config(); +CheckResult check_git_installed(); +CheckResult check_node_installed(); +CheckResult check_folder_structure(); +CheckResult check_api_tokens(); +void print_summary(const std::vector& results); +void suggest_debug_tips(const std::vector& results); +int run_hackatime_doctor(int argc, char* argv[]); + +#endif diff --git a/public/hackatime/hackatime-doctor/include/main.h b/public/hackatime/hackatime-doctor/include/main.h new file mode 100644 index 00000000..09d81e49 --- /dev/null +++ b/public/hackatime/hackatime-doctor/include/main.h @@ -0,0 +1,12 @@ +#ifndef OUTPUT_H +#define OUTPUT_H + +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +#endif diff --git a/public/hackatime/hackatime-doctor/include/output.h b/public/hackatime/hackatime-doctor/include/output.h new file mode 100644 index 00000000..0db6b274 --- /dev/null +++ b/public/hackatime/hackatime-doctor/include/output.h @@ -0,0 +1,7 @@ +#ifndef OUTPUT_H +#define OUTPUT_H + +#include +#include + +#endif diff --git a/public/hackatime/hackatime-doctor/install.sh b/public/hackatime/hackatime-doctor/install.sh new file mode 100755 index 00000000..06d88117 --- /dev/null +++ b/public/hackatime/hackatime-doctor/install.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash +set -e + +TARGET="hackatime-doctor" +PLATFORM="$(uname -s)" + +if [[ "$PLATFORM" == *"MINGW"* || "$PLATFORM" == *"MSYS"* || "$PLATFORM" == *"CYGWIN"* ]]; then + BIN_EXT=".exe" +else + BIN_EXT="" +fi + +BIN_PATH="" +if [ -f "$TARGET$BIN_EXT" ]; then + BIN_PATH="./$TARGET$BIN_EXT" +elif [ -f "bin/$TARGET$BIN_EXT" ]; then + BIN_PATH="bin/$TARGET$BIN_EXT" +else + echo "Error: Could not find $TARGET binary in current directory or bin/" + echo "Run this script from your extracted release package directory" + exit 1 +fi + +case "$PLATFORM" in + Linux*) + PREFIX="${1:-/usr/local}" + INSTALL_DIR="$PREFIX/bin" + DEST_PATH="$INSTALL_DIR/$TARGET$BIN_EXT" + INSTALL_CMD="install -m 755 \"$BIN_PATH\" \"$DEST_PATH\"" + ;; + Darwin*) + PREFIX="${1:-/usr/local}" + INSTALL_DIR="$PREFIX/bin" + DEST_PATH="$INSTALL_DIR/$TARGET$BIN_EXT" + INSTALL_CMD="install -m 755 \"$BIN_PATH\" \"$DEST_PATH\"" + ;; + MINGW*|MSYS*|CYGWIN*) + INSTALL_DIR="${1:-/c/Program Files/hackatime-doctor}" + DEST_PATH="$INSTALL_DIR/$TARGET$BIN_EXT" + INSTALL_CMD="mkdir -p \"$INSTALL_DIR\" && cp \"$BIN_PATH\" \"$DEST_PATH\"" + ;; + *) + echo "Unsupported platform: $PLATFORM" + exit 1 + ;; +esac + +echo "Installing hackatime-doctor to: $DEST_PATH" +eval "$INSTALL_CMD" + +case "$PLATFORM" in + Linux*|Darwin*) + echo "✅ Successfully installed to $DEST_PATH" + + if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then + echo "ℹ️ Note: $INSTALL_DIR is not in your PATH" + echo "Add this to your shell config:" + echo " export PATH=\"\$PATH:$INSTALL_DIR\"" + fi + ;; + MINGW*|MSYS*|CYGWIN*) + echo "✅ Successfully installed to $DEST_PATH" + + if [[ "$PATH" != *"$INSTALL_DIR"* ]]; then + echo "ℹ️ Note: $INSTALL_DIR is not in your PATH" + echo "Consider adding it to your system environment variables" + fi + ;; +esac + +if "$DEST_PATH" --help >/dev/null 2>&1; then + echo "✔️ Verification:" + "$DEST_PATH" --help +else + echo "⚠️ Warning: Could not verify installation" +fi diff --git a/public/hackatime/hackatime-doctor/src/checks.cpp b/public/hackatime/hackatime-doctor/src/checks.cpp new file mode 100644 index 00000000..2be7ee16 --- /dev/null +++ b/public/hackatime/hackatime-doctor/src/checks.cpp @@ -0,0 +1,569 @@ +#include "hackatime_doctor.h" +#include "checks.h" + +namespace fs = std::filesystem; + +bool try_install_package(const std::string& package_name) { + #ifdef _WIN32 + const std::string CHOCO_INSTALL_CMD = "@\"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoProfile -InputFormat None -ExecutionPolicy Bypass -Command \"[System.Net.ServicePointManager]::SecurityProtocol = 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))\" && SET \"PATH=%PATH%;%ALLUSERSPROFILE%\\chocolatey\\bin\""; + + if (system(("where choco > temp.txt")) != 0) { + std::cout << "Chocolatey package manager is required but not found.\n"; + std::cout << "Would you like to install Chocolatey now? [Y/n]: "; + std::string response; + std::getline(std::cin, response); + + if (response.empty() || tolower(response[0]) == 'y') { + std::cout << "Installing Chocolatey (Admin privileges required)...\n"; + int result = system(CHOCO_INSTALL_CMD.c_str()); + if (result != 0) { + std::cerr << "Failed to install Chocolatey.\n"; + return false; + } + } else { + std::cout << "Skipping Chocolatey installation.\n"; + return false; + } + } + + std::cout << "Attempting to install " << package_name << " using choco...\n"; + std::string cmd = "choco install -y " + package_name; + if (system(cmd.c_str()) == 0) { + return true; + } + return false; + + #elif __APPLE__ + const std::string BREW_INSTALL_CMD = "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""; + + if (system("which brew > temp.txt") != 0) { + std::cout << "Homebrew package manager is required but not found.\n"; + std::cout << "Would you like to install Homebrew now? [Y/n]: "; + std::string response; + std::getline(std::cin, response); + + if (response.empty() || tolower(response[0]) == 'y') { + std::cout << "Installing Homebrew...\n"; + int result = system(BREW_INSTALL_CMD.c_str()); + if (result != 0) { + std::cerr << "Failed to install Homebrew.\n"; + return false; + } + + system("eval \"$(/opt/homebrew/bin/brew shellenv)\""); + } else { + std::cout << "Skipping Homebrew installation.\n"; + return false; + } + } + + std::cout << "Attempting to install " << package_name << " using brew...\n"; + std::string cmd = "brew install " + package_name; + if (system(cmd.c_str()) == 0) { + return true; + } + return false; + #endif + + std::vector> package_managers = { + {"pacman", "sudo pacman -S --noconfirm " + package_name}, + {"apt", "sudo apt-get install -y " + package_name}, + {"dnf", "sudo dnf install -y " + package_name}, + {"aptitude", "sudo aptitude install -y " + package_name}, + {"yum", "sudo yum install -y " + package_name}, + {"zypper", "sudo zypper --non-interactive install " + package_name}, + {"apk", "sudo apk add " + package_name}, + {"emerge", "sudo emerge --ask n " + package_name}, + {"xbps-install", "sudo xbps-install -y " + package_name}, + {"eopkg", "sudo eopkg install -y " + package_name}, + {"nix-env", "nix-env -i " + package_name}, + {"slackpkg", "sudo slackpkg install " + package_name}, + {"swupd", "sudo swupd bundle-add " + package_name} + }; + + for (const auto& [manager, cmd] : package_managers) { + if (system(("which " + manager + " > temp.txt").c_str()) == 0) { + std::cout << "Attempting to install " << package_name + << " using " << manager << "...\n"; + int result = system(cmd.c_str()); + if (result == 0) { + return true; + } + } + } + return false; +} + +CheckResult check_git_installed() { + int result = system("git --version > temp.txt"); + if (result == 0) { + return CheckResult{true, "Git is installed", "git_check"}; + } + + std::cout << COLOR_YELLOW + << "Git is not installed. Would you like to install it now? [Y/n] " + << COLOR_RESET; + std::string response; + std::getline(std::cin, response); + + if (!response.empty() && tolower(response[0]) == 'n') { + return CheckResult{false, "Git is not installed (user declined installation)", "git_check"}; + } + + if (try_install_package("git")) { + if (system("git --version > temp.txt") == 0) { + return CheckResult{true, "Git was successfully installed", "git_check"}; + } else { + #ifdef _WIN32 + return CheckResult{true, + "Git was probably installed successfully.\n" + "Please RESTART YOUR TERMINAL for Git to become available.\n" + "After restarting, run 'git --version' to verify.", + "git_check" + }; + #endif + } + } + + return CheckResult{false, + "Failed to install Git. Please install it manually:\n" + " Windows: https://git-scm.com/download/win\n" + " Mac: brew install git\n" + " Linux: Use your package manager (apt, dnf, pacman, etc.)", + "git_check"}; +} + +CheckResult check_node_installed() { + int result = system("node --version > temp.txt"); + if (result != 0) { + std::cout << COLOR_YELLOW + << "Node.js is not installed. Would you like to install it now? [Y/n] " + << COLOR_RESET; + std::string response; + std::getline(std::cin, response); + + if (!response.empty() && tolower(response[0]) == 'n') { + return CheckResult{false, "Node.js is not installed (user declined installation)", "nodejs_check"}; + } + + if (try_install_package("nodejs") || try_install_package("node")) { + if (system("node --version > temp.txt") == 0) { + #ifdef _WIN32 + return CheckResult{true, + "Node.js was probably installed successfully.\n" + "Please RESTART YOUR TERMINAL for Node.js to become available.\n" + "After restarting, run 'node --version' to verify.", + "nodejs_check" + }; + #else + return check_node_installed(); + #endif + } + } + + return CheckResult{false, + "Failed to install Node.js. Please install it manually:\n" + " Windows: https://nodejs.org/en/download/\n" + " Mac: brew install node\n" + " Linux: Use your package manager (apt, dnf, pacman, etc.)\n" + " Recommended: Use nvm (https://github.com/nvm-sh/nvm)", + "nodejs_check"}; + } + + FILE* pipe = popen("node --version", "r"); + if (!pipe) return CheckResult{true, "Node.js is installed (version check failed)", "nodejs_check"}; + + char buffer[128]; + std::string version; + while (!feof(pipe)) { + if (fgets(buffer, 128, pipe) != NULL) + version += buffer; + } + pclose(pipe); + + int major_version = 0; + if (sscanf(version.c_str(), "v%d", &major_version) == 1) { + if (major_version >= 16) { + return CheckResult{true, "Node.js v" + std::to_string(major_version) + " is installed", "nodejs_check"}; + } + + std::cout << COLOR_YELLOW + << "Node.js version is too old (v" << major_version << "). Would you like to update? [Y/n] " + << COLOR_RESET; + std::string response; + std::getline(std::cin, response); + + if (!response.empty() && tolower(response[0]) == 'n') { + return CheckResult{false, "Node.js version too old (v" + std::to_string(major_version) + "), need v16+", "nodejs_check"}; + } + + if (try_install_package("nodejs") || try_install_package("node")) { + return check_node_installed(); + } + + return CheckResult{false, + "Failed to update Node.js. Please update manually:\n" + " Recommended: Use nvm (https://github.com/nvm-sh/nvm)\n" + " Or use your system package manager", + "nodejs_check"}; + } + + return CheckResult{true, "Node.js is installed (version check inconclusive)", "nodejs_check"}; +} + +CheckResult check_folder_structure() { + const std::vector required_files = { + "README.md", + "LICENSE", + ".gitignore" + }; + + bool all_exist = true; + for (const auto& file : required_files) { + if (!fs::exists(file) || !fs::is_regular_file(file)) { + all_exist = false; + break; + } + } + + if (all_exist) { + return CheckResult{true, "All required files present", "folder_structure_check"}; + } + + std::string missing; + for (const auto& file : required_files) { + if (!fs::exists(file)) { + if (!missing.empty()) missing += ", "; + missing += file; + } + } + + return CheckResult{false, "Missing required files: " + missing, "folder_structure_check"}; +} + +CheckResult check_api_tokens() { + const char* api_key = std::getenv("HACKATIME_API_KEY"); + const char* api_url = std::getenv("HACKATIME_API_URL"); + + if (!api_key || !api_url) { + std::string error = "Missing environment variables:"; + if (!api_key) error += "\n - HACKATIME_API_KEY"; + if (!api_url) error += "\n - HACKATIME_API_URL"; + error += "\nGet them from: https://hackatime.hackclub.com/my/wakatime_setup"; + return CheckResult{false, error, "api_connection_check"}; + } + + std::string full_url = std::string(api_url) + "/users/current/heartbeats"; + + size_t protocol_end = full_url.find("://"); + if (protocol_end == std::string::npos) { + return CheckResult{false, "Invalid API URL format (missing protocol)", "api_connection_check"}; + } + + size_t host_start = protocol_end + 3; + size_t path_start = full_url.find('/', host_start); + + std::string host = full_url.substr(host_start, path_start - host_start); + std::string path = path_start != std::string::npos ? full_url.substr(path_start) : "/"; + + int port = full_url.find("https://") == 0 ? 443 : 80; + + SSL_CTX* ctx = nullptr; + SSL* ssl = nullptr; + if (port == 443) { + SSL_library_init(); + SSL_load_error_strings(); + OpenSSL_add_all_algorithms(); + ctx = SSL_CTX_new(TLS_client_method()); + if (!ctx) { + return CheckResult{false, "SSL context creation failed", "api_connection_check"}; + } + } + + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) { + if (ctx) SSL_CTX_free(ctx); + return CheckResult{false, "Socket creation failed", "api_connection_check"}; + } + + hostent* server = gethostbyname(host.c_str()); + if (!server) { + close_socket(sock); + if (ctx) SSL_CTX_free(ctx); + return CheckResult{false, "Host resolution failed", "api_connection_check"}; + } + + sockaddr_in serv_addr{}; + serv_addr.sin_family = AF_INET; + serv_addr.sin_port = htons(port); + memcpy(&serv_addr.sin_addr.s_addr, server->h_addr, server->h_length); + + if (connect(sock, (sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { + close_socket(sock); + if (ctx) SSL_CTX_free(ctx); + return CheckResult{false, "Connection failed", "api_connection_check"}; + } + + if (port == 443) { + ssl = SSL_new(ctx); + SSL_set_fd(ssl, sock); + if (SSL_connect(ssl) != 1) { + SSL_free(ssl); + close_socket(sock); + SSL_CTX_free(ctx); + return CheckResult{false, "SSL handshake failed" , "api_connection_check"}; + } + } + + std::time_t now = std::time(nullptr); + std::string payload = R"([{ + "type": "file", + "time": )" + std::to_string(now) + R"(, + "entity": "hackatime-doctor-validate.txt", + "language": "Text" + }])"; + + std::string request = "POST " + path + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Authorization: Bearer " + std::string(api_key) + "\r\n" + "Content-Type: application/json\r\n" + "Content-Length: " + std::to_string(payload.length()) + "\r\n\r\n" + + payload; + + int bytes_sent; + if (port == 443) { + bytes_sent = SSL_write(ssl, request.c_str(), request.length()); + } else { + bytes_sent = send(sock, request.c_str(), request.length(), 0); + } + + if (bytes_sent <= 0) { + if (ssl) SSL_free(ssl); + close_socket(sock); + if (ctx) SSL_CTX_free(ctx); + return CheckResult{false, "Failed to send heartbeat", "api_connection_check"}; + } + + char buffer[4096]; + int bytes_received; + if (port == 443) { + bytes_received = SSL_read(ssl, buffer, sizeof(buffer)-1); + } else { + bytes_received = recv(sock, buffer, sizeof(buffer)-1, 0); + } + + if (bytes_received <= 0) { + if (ssl) SSL_free(ssl); + close_socket(sock); + if (ctx) SSL_CTX_free(ctx); + return CheckResult{false, "No response from server", "api_connection_check"}; + } + buffer[bytes_received] = '\0'; + + if (ssl) { + SSL_shutdown(ssl); + SSL_free(ssl); + } + close_socket(sock); + if (ctx) SSL_CTX_free(ctx); + + std::string response(buffer); + if (response.find("HTTP/1.1 20") != std::string::npos) { + return CheckResult{true, "Heartbeat sent successfully, hackatime is working!", "api_connection_check"}; + } + + return CheckResult{false, "API request failed: " + response.substr(0, response.find("\r\n\r\n")), "api_connection_check"}; +} + +#ifdef _WIN32 +std::string expandWindowsPath(const std::string& path_template) { + std::string result = path_template; + + if (result.front() == '~') { + char homeDir[MAX_PATH]; + if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_PROFILE, NULL, 0, homeDir))) { + result = std::string(homeDir) + result.substr(1); + } + } + + size_t start = 0; + while ((start = result.find('%', start)) != std::string::npos) { + size_t end = result.find('%', start + 1); + if (end != std::string::npos) { + std::string varName = result.substr(start + 1, end - start - 1); + char varValue[MAX_PATH]; + DWORD varLen = GetEnvironmentVariableA(varName.c_str(), varValue, MAX_PATH); + if (varLen > 0 && varLen < MAX_PATH) { + result.replace(start, end - start + 1, varValue); + start += varLen; + } else { + start = end + 1; + } + } else { + break; + } + } + + return result; +} +#else +std::string expandPosixPath(const std::string& path_template) { + wordexp_t expanded; + if (wordexp(path_template.c_str(), &expanded, WRDE_NOCMD) == 0) { + if (expanded.we_wordc > 0) { + std::string result = expanded.we_wordv[0]; + wordfree(&expanded); + return result; + } + wordfree(&expanded); + } + return path_template; +} +#endif + +CheckResult check_wakatime_config() { + std::vector search_paths = { + "${WAKATIME_HOME}/.wakatime.cfg", + "${XDG_CONFIG_HOME:-$HOME/.config}/wakatime/.wakatime.cfg", + "$HOME/.wakatime.cfg", + "/etc/wakatime.cfg", + "/usr/local/etc/wakatime.cfg" + }; + + std::string config_path; + std::string config_content; + + for (const auto& path_template : search_paths) { + #ifdef _WIN32 + std::string path = expandWindowsPath(path_template); + #else + std::string path = expandPosixPath(path_template); + #endif + + std::ifstream file(path); + if (file.is_open()) { + config_path = path; + config_content.assign((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + file.close(); + if (config_content.find("[settings]") != std::string::npos) { + break; + } + } + } + + + if (config_path.empty()) { + std::cout << "No WakaTime config found. Would you like to create one? [Y/n] "; + std::string response; + std::getline(std::cin, response); + + if (!response.empty() && tolower(response[0]) == 'n') { + return CheckResult{false, + "WakaTime config not found and user declined to create one", + "wakatime_config"}; + } + + const char* home = std::getenv("HOME"); + if (!home) home = std::getenv("USERPROFILE"); + if (!home) { + return CheckResult{false, + "Could not determine home directory for config creation", + "wakatime_config"}; + } + + config_path = std::string(home) + "/.wakatime.cfg"; + + const char* api_url = std::getenv("HACKATIME_API_URL"); + const char* api_key = std::getenv("HACKATIME_API_KEY"); + + if (!api_url || !api_key) { + return CheckResult{false, + "Missing HACKATIME_API_URL or HACKATIME_API_KEY environment variables", + "wakatime_config"}; + } + + std::ofstream config_file(config_path); + if (!config_file) { + return CheckResult{false, + "Failed to create config file at " + config_path, + "wakatime_config"}; + } + + config_file << "[settings]\n" + << "api_url = " << api_url << "\n" + << "api_key = " << api_key << "\n" + << "heartbeat_rate_limit_seconds = 30\n"; + + std::cout << "Created WakaTime config at " << config_path << "\n"; + return CheckResult{true, + "Successfully created WakaTime config", + "wakatime_config"}; + } + + bool has_url = false; + bool has_key = false; + std::vector issues; + std::string current_section; + + std::istringstream content_stream(config_content); + std::string line; + int line_num = 0; + + while (std::getline(content_stream, line)) { + line_num++; + std::string trimmed = line; + trimmed.erase(trimmed.begin(), + std::find_if(trimmed.begin(), trimmed.end(), + [](int ch) { return !std::isspace(ch); })); + + if (trimmed.empty()) continue; + + if (trimmed[0] == '[' && trimmed.back() == ']') { + current_section = trimmed.substr(1, trimmed.length() - 2); + continue; + } + + if (current_section != "settings") continue; + + size_t eq_pos = trimmed.find('='); + if (eq_pos == std::string::npos) continue; + + std::string key = trimmed.substr(0, eq_pos); + key.erase(std::find_if(key.rbegin(), key.rend(), + [](int ch) { return !std::isspace(ch); }).base(), key.end()); + + std::string value = trimmed.substr(eq_pos + 1); + value.erase(value.begin(), + std::find_if(value.begin(), value.end(), + [](int ch) { return !std::isspace(ch); })); + + if (key == "api_url") { + has_url = true; + if (value != "https://hackatime.hackclub.com/api/hackatime/v1") { + issues.push_back("Incorrect API URL: " + value); + } + } else if (key == "api_key") { + has_key = true; + if (value.empty()) { + issues.push_back("Empty API key"); + } + } + } + + if (!has_url) issues.push_back("Missing API URL"); + if (!has_key) issues.push_back("Missing API key"); + + if (issues.empty()) { + return CheckResult{true, + "Valid WakaTime config at " + config_path, + "wakatime_config"}; + } else { + std::string message = "Issues in " + config_path + ":\n"; + for (const auto& issue : issues) { + message += " • " + issue + "\n"; + } + message += "Please update your config or set environment variables"; + return CheckResult{false, message, "wakatime_config"}; + } +} diff --git a/public/hackatime/hackatime-doctor/src/main.cpp b/public/hackatime/hackatime-doctor/src/main.cpp new file mode 100644 index 00000000..197699e7 --- /dev/null +++ b/public/hackatime/hackatime-doctor/src/main.cpp @@ -0,0 +1,147 @@ +#include "hackatime_doctor.h" +#include "main.h" + +void print_help() { + std::cout << + "hackatime-doctor - Diagnostic tool for HackTime environments\n" + "Usage:\n" + " hackatime-doctor [options]\n\n" + "Options:\n" + " -f, --full-check Performs extended environment checks\n" + " -j, --json [file] Export results as JSON\n" + " -c, --csv [file] Export results as CSV\n" + " -h, --help Show this help message\n"; +} + +void export_to_json(const std::vector& results, const std::string& filename) { + json j; + for (const auto& result : results) { + j[result.name] = { + {"success", result.success}, + {"message", result.message}, + {"timestamp", std::time(nullptr)} + }; + } + + std::ofstream out(filename); + if (!out) { + throw std::runtime_error("Failed to open " + filename + " for writing"); + } + out << j.dump(4); +} + +void export_to_csv(const std::vector& results, const std::string& filename) { + std::ofstream out(filename); + if (!out) { + throw std::runtime_error("Failed to open " + filename + " for writing"); + } + + out << "Check,Status,Message\n"; + + for (const auto& result : results) { + out << std::quoted(result.name) << "," + << (result.success ? "PASS" : "FAIL") << "," + << std::quoted(result.message) << "\n"; + } +} + +bool has_failures(const std::vector& results) { + return std::any_of(results.begin(), results.end(), + [](const auto& r) { return !r.success; }); +} + +int run_hackatime_doctor(int argc, char* argv[]) { + std::cout << COLOR_BLUE << "⚕️ HackaTime Doctor - Checking your development environment...\n" << COLOR_RESET; + + bool export_json = false; + bool export_csv = false; + bool full_check = false; + std::string output_file; + std::vector unknown_args; + + for (int i = 1; i < argc; ++i) { + std::string arg = argv[i]; + + if (arg == "--json" || arg == "-j") { + export_json = true; + if (i+1 < argc && argv[i+1][0] != '-') { + output_file = argv[++i]; + } + } + else if (arg == "--csv" || arg == "-c") { + export_csv = true; + if (i+1 < argc && argv[i+1][0] != '-') { + output_file = argv[++i]; + } + } + else if (arg == "--full-check" || arg == "-f") { + full_check = true; + } + else if (arg == "--help" || arg == "-h") { + print_help(); + return 0; + } + else { + unknown_args.push_back(arg); + } + } + + if (!unknown_args.empty()) { + std::cerr << "Error: Unknown arguments:\n"; + for (const auto& arg : unknown_args) { + std::cerr << " " << arg << "\n"; + } + std::cerr << "\nUsage:\n"; + print_help(); + return 1; + } + + std::vector results = { + check_git_installed(), + check_wakatime_config(), + check_api_tokens() + }; + + if (full_check) { + results.push_back(check_node_installed()); + results.push_back(check_folder_structure()); + } + + try { + if (export_json) { + std::string filename = output_file.empty() ? "hackatime-report.json" : output_file; + export_to_json(results, filename); + std::cout << "✓ Report saved to " << filename << "\n"; + return has_failures(results) ? 1 : 0; + } + else if (export_csv) { + std::string filename = output_file.empty() ? "hackatime-report.csv" : output_file; + export_to_csv(results, filename); + std::cout << "✓ Report saved to " << filename << "\n"; + return has_failures(results) ? 1 : 0; + } + else { + print_summary(results); + if (has_failures(results)) { + suggest_debug_tips(results); + return 1; + } + return 0; + } + } + catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << "\n"; + return 2; + } +} + +int main(int argc, char* argv[]) { + try { + run_hackatime_doctor(argc, argv); + return 0; + } + catch (const std::exception& e) { + std::cerr << "Fatal error: " << e.what() << "\n"; + return 2; + } +} diff --git a/public/hackatime/hackatime-doctor/src/output.cpp b/public/hackatime/hackatime-doctor/src/output.cpp new file mode 100644 index 00000000..1db8c46d --- /dev/null +++ b/public/hackatime/hackatime-doctor/src/output.cpp @@ -0,0 +1,97 @@ +#include "hackatime_doctor.h" +#include "output.h" + +void print_summary(const std::vector& results) { + std::cout << "\n" << COLOR_BLUE << "=== Check Summary ===\n" << COLOR_RESET; + + int success_count = 0; + bool git_failed = false; + bool node_failed = false; + bool folders_failed = false; + bool token_failed = false; + + for (const auto& result : results) { + if (result.success) { + success_count++; + } else { + if (result.message.find("Git") != std::string::npos) git_failed = true; + if (result.message.find("Node") != std::string::npos) node_failed = true; + if (result.message.find("folder") != std::string::npos) folders_failed = true; + if (result.message.find("API") != std::string::npos) token_failed = true; + } + } + + for (const auto& result : results) { + if (result.success) { + std::cout << COLOR_GREEN << "[✓] " << result.message << COLOR_RESET << "\n"; + } else { + std::cout << COLOR_RED << "[✗] " << result.message << COLOR_RESET << "\n"; + } + } + + std::cout << "\n" << success_count << "/" << results.size() << " checks passed\n\n"; + + if (static_cast(success_count) == results.size()) { + std::cout << COLOR_GREEN << "🎉 Everything looks perfect! You're ready to hack!\n" << COLOR_RESET; + } else { + std::cout << COLOR_YELLOW << "🔍 Focus Areas:\n" << COLOR_RESET; + if (git_failed) std::cout << "- Git installation needs attention\n"; + if (node_failed) std::cout << "- Node.js setup requires fixes\n"; + if (folders_failed) std::cout << "- Project structure is incomplete\n"; + if (token_failed) std::cout << "- API token configuration is invalid\n"; + } +} + +void suggest_debug_tips(const std::vector& results) { + bool needs_git_help = false; + bool needs_node_help = false; + bool needs_folder_help = false; + bool needs_token_help = false; + + for (const auto& result : results) { + if (!result.success) { + if (result.name == "git_check") needs_git_help = true; + if (result.name == "nodejs_check") needs_node_help = true; + if (result.name == "folder_structure_check") needs_folder_help = true; + if (result.name == "api_connection_check") needs_token_help = true; + } + } + + std::cout << COLOR_YELLOW << "💡 Targeted Debug Tips:\n" << COLOR_RESET; + + if (needs_git_help) { + std::cout << COLOR_YELLOW << "1. Git Issues:\n" + << " - Install: https://git-scm.com/downloads or if you are on linux/mac please use your respective package managers\n" + << " - Verify PATH: Run 'which git'\n" + << COLOR_RESET; + } + + if (needs_token_help) { + std::cout << COLOR_YELLOW << "4. API Configuration Issues:\n" + << " - Follow instructions from: https://hackatime.hackclub.com/my/wakatime_setup\n" + << COLOR_RESET; + } + + if (needs_node_help) { + std::cout << COLOR_YELLOW << "2. Node.js Issues:\n" + << " - Install NodeJS: https://nodejs.org/ or if you are on linux/mac please use your respective package managers\n" + << " - Check version: 'node --version' (Need v16+)\n" + << COLOR_RESET; + } + + if (needs_folder_help) { + std::cout << COLOR_YELLOW << "3. Missing Files/Folders:\n" + << " - Expected: README.md, .gitignore and LICENSE\n" + << " - Try: 'These files are essential for any git project please search up what these are if you havent heard of it'\n" + << COLOR_RESET; + } + + if (needs_git_help || needs_node_help || needs_folder_help || needs_token_help) { + std::cout << COLOR_YELLOW << "\n🛠️ General Troubleshooting:\n" + << " - Restart your terminal after installations\n" + << " - Verify current directory is the project root\n" + << " - Check file permissions\n" + << COLOR_RESET; + } +} + diff --git a/public/hackatime/hackatime-doctor/uninstall.sh b/public/hackatime/hackatime-doctor/uninstall.sh new file mode 100755 index 00000000..ceeec0e3 --- /dev/null +++ b/public/hackatime/hackatime-doctor/uninstall.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +set -e + +TARGET="hackatime-doctor" +PLATFORM="$(uname -s)" + +case "$PLATFORM" in + Linux*|Darwin*) + PREFIX="${1:-/usr/local}" + BIN_PATH="$PREFIX/bin/$TARGET" + CMD="rm -f \"$BIN_PATH\"" + ;; + MINGW*|MSYS*|CYGWIN*) + INSTALL_DIR="${1:-/c/Program Files/hackatime-doctor}" + BIN_PATH="$INSTALL_DIR/$TARGET.exe" + CMD="rm -f \"$BIN_PATH\" && rmdir \"$INSTALL_DIR\" 2>/dev/null || true" + ;; + *) + echo "Unsupported platform: $PLATFORM" + exit 1 + ;; +esac + +echo "Removing hackatime-doctor from: $BIN_PATH" +eval "$CMD" +echo "Uninstallation complete"