/* dropshell-tool dropshell-tool install - confirms dropshell-tool is fully initialised: - adds a line to the user's .bashrc file to source the dropshell-tool ~/.bashrc_dropshell_tool script, if it doesn't exist - creates an empty ~/.bashrc_dropshell_tool script if it doesn't exist - creates the ~/.config/dropshell-tool directory, if it does not exist - creates the ~/.local/bin/dropshell-tool directory, if it does not exist - removes the tool from the user's system if it is already installed, and the tool's entries in the ~/.bashrc_dropshell_tool script - downloads the tool archive (tgz) from tools.dropshell.app (tool_name:ARCH), where ARCH is the architecture of the user's system, and unpacks it to the new tool directory: ~/.local/bin/dropshell-tool// - sets the PATH to include the tool directory, by modifying the ~/.bashrc_dropshell_tool script - adds an entry for autocompletion for the tool to the ~/.bashrc_dropshell_tool script, where autocomplete just runs autocomplete - creates a ~/.config/dropshell-tool/tool_name.json file, which contains the tool's name, version (by running version), hash from tools.dropshell.app, and architecture - reads the json file from the tgz called dropshell-tool-config.json: - for each alias in the aliases array: - check if another command is using the alias, and continue only if not - check that the alias is not already in the ~/.bashrc_dropshell_tool script and continue only if not - add an entry to the ~/.bashrc_dropshell_tool script to run the tool via the alias - if the json file has a setup_script entry, run the script named in the entry in the tool directory - using sudo if the entry has sudo set to true. dropshell-tool publish - checks that dropshell-tool-config.json exists in the folder, and is valid (see above) - creates a tgz archive of the folder, and uploads it to tools.dropshell.app, a simple object server. - prints the URL and hash of the uploaded archive - uses the token from env variable SOS_WRITE_TOKEN to write to tools.dropshell.app dropshell-tool update - compares the hash from the ~/.config/dropshell-tool/tool_name.json file with the hash from tools.dropshell.app (tool_name:ARCH), and continues only if they are different - checks the version from the ~/.config/dropshell-tool/tool_name.json file with the version from tools.dropshell.app (tool_name:ARCH), and continues only if the remote version is newer (installed is older) - installs the tool as per the install command dropshell-tool update all - runs update on all installed tools dropshell-tool autocomplete - shows autocomplete for dropshell-tool, and then exits - the tool list to choose from when calling install is hard coded in the autocomplete function dropshell-tool version - prints the version of dropshell-tool dropshell-tool create - creates a new tool source directory in relative path if it doesn't exist - creates a dropshell-tool-config.json file in the tool source directory if it doesn't exist, with the following entries: - aliases: an array of aliases for the tool - setup_script: the name of the setup script to run (setup_script.sh) - creates a setup_script.sh file in the tool source directory if it doesn't exist, that just prints a completion message and exits dropshell-tool help - shows this help message */ #include "version.hpp" #include "BashrcEditor.hpp" #include "DropshellScriptManager.hpp" #include "GetbinClient.hpp" #include "ArchiveManager.hpp" #include #include #include #include #include #include namespace { using json = nlohmann::json; std::string get_arch() { #if defined(__x86_64__) || defined(_M_X64) return "x86_64"; #elif defined(__aarch64__) return "aarch64"; #else return "unknown"; #endif } std::string get_home() { const char* home = getenv("HOME"); return home ? std::string(home) : std::string(""); } int install_tool(int argc, char* argv[]) { if (argc < 3) { std::cerr << "Usage: dropshell-tool install " << std::endl; return 1; } std::string toolName = argv[2]; std::string arch = get_arch(); std::string home = get_home(); std::filesystem::path configDir = std::filesystem::path(home) / ".config/dropshell-tool"; std::filesystem::path binDir = std::filesystem::path(home) / ".local/bin/dropshell-tool" / toolName; std::filesystem::path archivePath = configDir / (toolName + ".tgz"); std::filesystem::create_directories(configDir); std::filesystem::create_directories(binDir); dropshelltool::BashrcEditor bashrcEditor; bashrcEditor.addSourceLine(); DropshellScriptManager scriptManager; scriptManager.removeToolEntry(toolName); if (std::filesystem::exists(binDir)) std::filesystem::remove_all(binDir); GetbinClient getbin; std::cout << "Downloading " << toolName << ":" << arch << "..." << std::endl; if (!getbin.download(toolName, arch, archivePath.string())) { std::cerr << "Failed to download tool archive." << std::endl; return 1; } ArchiveManager archiver; if (!archiver.unpack(archivePath.string(), binDir.string())) { std::cerr << "Failed to unpack tool archive." << std::endl; return 1; } scriptManager.addToolEntry(toolName, binDir.string()); scriptManager.addAutocomplete(toolName); std::string hash; getbin.getHash(toolName, arch, hash); std::string version; std::string toolPath = binDir.string() + "/" + toolName; FILE* fp = popen((toolPath + " version").c_str(), "r"); if (fp) { char buf[128]; if (fgets(buf, sizeof(buf), fp)) version = std::string(buf); pclose(fp); } json toolInfo = { {"name", toolName}, {"version", version}, {"hash", hash}, {"arch", arch} }; std::ofstream toolInfoFile(configDir / (toolName + ".json")); toolInfoFile << toolInfo.dump(2); toolInfoFile.close(); std::string configJson; if (archiver.readConfigJson(archivePath.string(), configJson)) { try { auto config = json::parse(configJson); if (config.contains("aliases")) { for (const auto& alias : config["aliases"]) { std::string aliasStr = alias.get(); if (!scriptManager.hasAlias(aliasStr)) { scriptManager.addAlias(aliasStr, toolName); } } } if (config.contains("setup_script")) { std::string setupScript = config["setup_script"].get(); bool useSudo = config.value("sudo", false); std::string setupPath = binDir.string() + "/" + setupScript; std::string cmd = (useSudo ? "sudo " : "") + setupPath; std::system(cmd.c_str()); } } catch (...) { std::cerr << "Warning: failed to parse dropshell-tool-config.json" << std::endl; } } std::cout << "Installed " << toolName << " successfully." << std::endl; return 0; } int publish_tool(int argc, char* argv[]) { if (argc < 4) { std::cerr << "Usage: dropshell-tool publish " << std::endl; return 1; } std::string labeltag = argv[2]; std::string folder = argv[3]; std::string home = get_home(); std::filesystem::path configPath = std::filesystem::path(folder) / "dropshell-tool-config.json"; if (!std::filesystem::exists(configPath)) { std::cerr << "dropshell-tool-config.json not found in " << folder << std::endl; return 1; } std::filesystem::path archivePath = std::filesystem::path(home) / ".tmp" / (labeltag + ".tgz"); std::filesystem::create_directories(archivePath.parent_path()); ArchiveManager archiver; if (!archiver.pack(folder, archivePath.string())) { std::cerr << "Failed to create archive." << std::endl; return 1; } std::filesystem::path tokenPath = std::filesystem::path(home) / ".config/tools.dropshell.app/write_token.txt"; std::string token; if (std::filesystem::exists(tokenPath)) { std::ifstream tfile(tokenPath); std::getline(tfile, token); } else { std::cout << "Enter tools.dropshell.app write token: "; std::getline(std::cin, token); std::filesystem::create_directories(tokenPath.parent_path()); std::ofstream tfile(tokenPath); tfile << token << std::endl; } GetbinClient getbin; std::string url, hash; if (!getbin.upload(archivePath.string(), url, hash, token)) { std::cerr << "Failed to upload archive." << std::endl; return 1; } std::cout << "Published! URL: " << url << "\nHash: " << hash << std::endl; return 0; } int update_tool(int argc, char* argv[]) { if (argc < 3) { std::cerr << "Usage: dropshell-tool update " << std::endl; return 1; } std::string toolName = argv[2]; std::string home = get_home(); std::filesystem::path configDir = std::filesystem::path(home) / ".config/dropshell-tool"; if (toolName == "all") { for (const auto& entry : std::filesystem::directory_iterator(configDir)) { if (entry.path().extension() == ".json") { std::string tname = entry.path().stem(); char* fakeArgv[] = {argv[0], (char*)"update", (char*)tname.c_str()}; update_tool(3, fakeArgv); } } return 0; } std::filesystem::path toolInfoPath = configDir / (toolName + ".json"); if (!std::filesystem::exists(toolInfoPath)) { std::cerr << "Tool not installed: " << toolName << std::endl; return 1; } std::ifstream tfile(toolInfoPath); json toolInfo; tfile >> toolInfo; tfile.close(); std::string arch = toolInfo.value("arch", get_arch()); std::string localHash = toolInfo.value("hash", ""); std::string localVersion = toolInfo.value("version", ""); GetbinClient getbin; std::string remoteHash; getbin.getHash(toolName, arch, remoteHash); if (remoteHash.empty() || remoteHash == localHash) { std::cout << "No update needed for " << toolName << std::endl; return 0; } std::cout << "Updating " << toolName << "..." << std::endl; char* fakeArgv[] = {argv[0], (char*)"install", (char*)toolName.c_str()}; return install_tool(3, fakeArgv); } int create_tool(int argc, char* argv[]) { if (argc < 4) { std::cerr << "Usage: dropshell-tool create " << std::endl; return 1; } std::string toolName = argv[2]; std::string directoryName = argv[3]; std::filesystem::path toolDir = std::filesystem::current_path() / directoryName; if (!std::filesystem::exists(toolDir)) { std::filesystem::create_directories(toolDir); std::cout << "Created directory: " << toolDir << std::endl; } else { std::cout << "Directory already exists: " << toolDir << std::endl; } std::filesystem::path configPath = toolDir / "dropshell-tool-config.json"; if (!std::filesystem::exists(configPath)) { nlohmann::json config = { {"aliases", nlohmann::json::array()}, {"setup_script", "setup_script.sh"} }; std::ofstream configFile(configPath); configFile << config.dump(2); configFile.close(); std::cout << "Created config: " << configPath << std::endl; } else { std::cout << "Config already exists: " << configPath << std::endl; } std::filesystem::path setupScriptPath = toolDir / "setup_script.sh"; if (!std::filesystem::exists(setupScriptPath)) { std::ofstream setupFile(setupScriptPath); setupFile << "#!/bin/bash\necho 'Setup complete.'\n"; setupFile.close(); std::filesystem::permissions(setupScriptPath, std::filesystem::perms::owner_exec | std::filesystem::perms::owner_write | std::filesystem::perms::owner_read); std::cout << "Created setup script: " << setupScriptPath << std::endl; } else { std::cout << "Setup script already exists: " << setupScriptPath << std::endl; } return 0; } } // end anonymous namespace int main(int argc, char* argv[]) { if (argc < 2) { std::cout << "Usage: dropshell-tool [args...]" << std::endl; return 1; } std::string command = argv[1]; if (command == "install") { return install_tool(argc, argv); } else if (command == "publish") { return publish_tool(argc, argv); } else if (command == "update") { return update_tool(argc, argv); } else if (command == "autocomplete") { std::vector args(argv + 2, argv + argc); if (args.empty()) std::cout << R"(install publish update version create help )"; } else if (command == "version") { std::cout << dropshell::VERSION << std::endl; } else if (command == "create") { return create_tool(argc, argv); } else if (command == "help") { std::cout << "Usage: dropshell-tool [args...]" << std::endl; std::cout << "Commands:" << std::endl; std::cout << " install " << std::endl; std::cout << " publish " << std::endl; std::cout << " update " << std::endl; std::cout << " version" << std::endl; } else { std::cout << "Unknown command: " << command << std::endl; return 1; } return 0; }