CCCPaste

KSSH Install Script Revised

-- KSSH Storage System Dynamic Installer
local GITHUB_REPO = "Temperdox/KSSH_Storage_Revised"
local GITHUB_BRANCH = "master"
local GITHUB_PATH = "src"
local VERSION = "0.0.2"

-- Ignore patterns - files/dirs that should not be downloaded
local IGNORE_PATTERNS = {
    "^%.git",           -- .git directory
    "^%.gitignore$",    -- .gitignore file
    "README%.md$",      -- README files
    "%.backup$",        -- backup files
    "install%.lua$",    -- installer itself
    "^test",           -- test directories
    "^docs",           -- documentation
}

-- Color scheme (keeping your existing colors)
local COLORS = {
    background = colors.black,
    text = colors.white,
    highlight = colors.gray,
    highlightText = colors.black,
    success = colors.lime,
    error = colors.red,
    warning = colors.orange,
    info = colors.purple,
    skip = colors.yellow
}

-- Installation mode
local INSTALL_MODE = {
    CLEAN = "clean",
    UPDATE = "update"
}

-- GitHub API Functions
local function getGitHubAPIUrl(path)
    -- Use GitHub API to get directory contents
    return string.format(
        "https://api.github.com/repos/%s/contents/%s/%s?ref=%s",
        GITHUB_REPO,
        GITHUB_PATH,
        path or "",
        GITHUB_BRANCH
    )
end

local function getRawFileUrl(path)
    -- Direct raw file URL
    return string.format(
        "https://raw.githubusercontent.com/%s/%s/%s/%s",
        GITHUB_REPO,
        GITHUB_BRANCH,
        GITHUB_PATH,
        path
    )
end

local function shouldIgnore(path)
    -- Check if path matches any ignore pattern
    for _, pattern in ipairs(IGNORE_PATTERNS) do
        if string.match(path, pattern) then
            return true
        end
    end
    return false
end

local function fetchDirectoryContents(path, fileList, errorList)
    fileList = fileList or {}
    errorList = errorList or {}
    
    local apiUrl = getGitHubAPIUrl(path)
    
    -- Add headers to avoid rate limiting (if possible in CC)
    local headers = {
        ["User-Agent"] = "CC-Installer/" .. VERSION,
        ["Accept"] = "application/vnd.github.v3+json"
    }
    
    local response = http.get(apiUrl, headers)
    
    if not response then
        table.insert(errorList, "Failed to fetch directory: " .. (path or "root"))
        return fileList, errorList
    end
    
    local content = response.readAll()
    response.close()
    
    -- Parse JSON response
    local ok, data = pcall(textutils.unserializeJSON, content)
    if not ok or not data then
        table.insert(errorList, "Failed to parse GitHub API response for: " .. (path or "root"))
        return fileList, errorList
    end
    
    -- Handle case where data is a single file (not a directory)
    if data.type and data.type == "file" then
        data = {data}
    elseif not data[1] then
        table.insert(errorList, "No contents found in: " .. (path or "root"))
        return fileList, errorList
    end
    
    -- Process each item
    for _, item in ipairs(data) do
        local fullPath = path and (path .. "/" .. item.name) or item.name
        
        -- Check if we should ignore this item
        if not shouldIgnore(fullPath) and not shouldIgnore(item.name) then
            if item.type == "file" then
                -- Add file to list
                table.insert(fileList, {
                    path = fullPath,
                    size = item.size,
                    sha = item.sha,
                    download_url = item.download_url or getRawFileUrl(fullPath)
                })
            elseif item.type == "dir" then
                -- Recursively fetch subdirectory
                fetchDirectoryContents(fullPath, fileList, errorList)
            end
        end
    end
    
    return fileList, errorList
end

local function getSystemFiles(systemType)
    -- Dynamically fetch files from GitHub based on system type
    local basePath = systemType == "storage" and "storage" or "turtle"
    local fileList, errors = fetchDirectoryContents(basePath)
    
    return fileList, errors
end

-- Modified download function to work with dynamic file structure
local function downloadFile(fileInfo, destination, mode)
    local url = fileInfo.download_url or getRawFileUrl(fileInfo.path)
    
    local response = http.get(url)
    if not response then
        return false, "Failed to download: " .. fileInfo.path
    end
    
    local content = response.readAll()
    response.close()
    
    -- In update mode, check if file content is identical using SHA if available
    if mode == INSTALL_MODE.UPDATE and fs.exists(destination) then
        local existingFile = fs.open(destination, "r")
        if existingFile then
            local existingContent = existingFile.readAll()
            existingFile.close()
            
            if existingContent == content then
                -- Files are identical, skip
                return true, nil, true  -- third return value indicates skip
            end
        end
    end
    
    -- Create directory if needed
    local dir = fs.getDir(destination)
    if dir ~= "" and not fs.exists(dir) then
        fs.makeDir(dir)
    end
    
    -- Backup existing file in update mode
    if mode == INSTALL_MODE.UPDATE and fs.exists(destination) then
        local backupPath = destination .. ".backup"
        if fs.exists(backupPath) then
            fs.delete(backupPath)
        end
        fs.copy(destination, backupPath)
    end
    
    -- Write file
    local file = fs.open(destination, "w")
    if not file then
        return false, "Failed to write: " .. destination
    end
    
    file.write(content)
    file.close()
    
    return true, nil, false
end

-- Modified install function to work with dynamic files
local function installFiles(fileList, systemType, mode)
    local total = #fileList
    local completed = 0
    local skipped = 0
    local updated = 0
    local errors = {}
    
    -- Clean install removes everything first
    if mode == INSTALL_MODE.CLEAN then
        cleanInstall(systemType)
    end
    
    for _, fileInfo in ipairs(fileList) do
        completed = completed + 1
        local percent = (completed / total) * 100
        
        -- Extract filename for display
        local displayName = fileInfo.path:match("([^/]+)$") or fileInfo.path
        
        -- Determine local path (remove system prefix)
        local localPath = fileInfo.path
        if systemType == "storage" then
            localPath = fileInfo.path:gsub("^storage/", "")
        else
            localPath = fileInfo.path:gsub("^turtle/", "")
        end
        
        -- Show current file being processed
        local statusMsg = mode == INSTALL_MODE.UPDATE and "Checking: " .. displayName or "Downloading: " .. displayName
        
        local success, err, wasSkipped = downloadFile(fileInfo, localPath, mode)
        
        if not success then
            table.insert(errors, err)
            showProgress("Installing " .. systemType, "ERROR: " .. displayName, percent, COLORS.error)
        elseif wasSkipped then
            skipped = skipped + 1
            showProgress("Installing " .. systemType, "Skipped (unchanged): " .. displayName, percent, COLORS.skip)
        else
            updated = updated + 1
            showProgress("Installing " .. systemType, "Updated: " .. displayName, percent, COLORS.success)
        end
        
        sleep(0.05) -- Small delay for visual feedback
    end
    
    return #errors == 0, errors, {
        total = total,
        updated = updated,
        skipped = skipped,
        errors = #errors
    }
end

-- Add a prefetch check function
local function preflightCheck(systemType)
    clearScreen()
    drawHeader("Checking GitHub Repository...")
    
    local w, h = term.getSize()
    term.setCursorPos(2, 4)
    term.write("Fetching file list from GitHub...")
    
    local fileList, errors = getSystemFiles(systemType)
    
    if #errors > 0 then
        term.setCursorPos(2, 6)
        term.setTextColor(COLORS.error)
        term.write("Failed to fetch file list:")
        term.setTextColor(COLORS.text)
        
        local y = 8
        for _, err in ipairs(errors) do
            term.setCursorPos(2, y)
            term.write("- " .. err)
            y = y + 1
        end
        
        term.setCursorPos(2, y + 1)
        term.write("Press any key to exit")
        os.pullEvent("key")
        return nil
    end
    
    term.setCursorPos(2, 6)
    term.setTextColor(COLORS.success)
    term.write("Found " .. #fileList .. " files to process")
    term.setTextColor(COLORS.text)
    
    -- Show file structure preview
    term.setCursorPos(2, 8)
    term.write("File structure:")
    
    local dirs = {}
    for _, fileInfo in ipairs(fileList) do
        local dir = fs.getDir(fileInfo.path)
        if dir ~= "" and not dirs[dir] then
            dirs[dir] = true
        end
    end
    
    local y = 9
    for dir, _ in pairs(dirs) do
        if y < h - 3 then
            term.setCursorPos(4, y)
            term.write("- " .. dir .. "/")
            y = y + 1
        end
    end
    
    if y >= h - 3 then
        term.setCursorPos(4, y)
        term.write("... and more")
    end
    
    term.setCursorPos(2, h - 1)
    term.write("Press ENTER to continue or Q to cancel")
    
    while true do
        local event, key = os.pullEvent("key")
        if key == keys.enter then
            return fileList
        elseif key == keys.q then
            return nil
        end
    end
end

-- Keep all your existing UI functions (they're well done)
-- clamp, clearScreen, drawHeader, drawList, move, pickFromList, showProgress, etc.
-- ... (include all the UI functions from your original code here)

-- Modified main function
local function main()
    -- Show splash screen
    showSplash()
    
    -- Detect system type
    local systemType = detectSystemType()
    
    -- Check for existing installation
    local existingInstall = detectExistingInstallation()
    local installMode = INSTALL_MODE.CLEAN
    
    if existingInstall then
        installMode = promptInstallMode()
        if not installMode then
            clearScreen()
            print("Installation cancelled.")
            return
        end
    end
    
    -- Present options
    local options = {
        "Storage Computer",
        "Crafting Turtle",
        "Cancel"
    }
    
    local descriptions = {
        "Main storage management system with inventory, sorting, and order processing",
        "Turtle-based crafting system with recipe learning and remote control",
        "Exit installer without making changes"
    }
    
    local selection = pickFromList("KSSH Installer - Select System Type", options, descriptions)
    
    if not selection or selection == 3 then
        clearScreen()
        print("Installation cancelled.")
        return
    end
    
    local selectedType = options[selection]
    local selectedSystem = selectedType == "Storage Computer" and "storage" or "turtle"
    
    -- Confirm installation
    if not confirmInstall(systemType, selectedType, installMode) then
        clearScreen()
        print("Installation cancelled.")
        return
    end
    
    -- Preflight check - fetch file list from GitHub
    local fileList = preflightCheck(selectedSystem)
    if not fileList then
        clearScreen()
        print("Installation cancelled.")
        return
    end
    
    -- Perform installation
    local success, errors, stats = installFiles(fileList, selectedSystem, installMode)
    
    if not success then
        clearScreen()
        term.setTextColor(COLORS.error)
        print("Installation failed with errors:")
        term.setTextColor(COLORS.text)
        for _, err in ipairs(errors) do
            print("  - " .. err)
        end
        print("\nPress any key to exit")
        os.pullEvent("key")
        return
    end
    
    -- Check for startup file
    local hasStartup = fs.exists("startup.lua")
    
    -- Show completion screen
    showComplete(selectedType, hasStartup, stats, installMode)
    
    -- Set computer label if not set
    if not os.getComputerLabel() then
        if selectedType == "Storage Computer" then
            os.setComputerLabel("KSSH_Storage_" .. os.getComputerID())
        else
            os.setComputerLabel("KSSH_Turtle_" .. os.getComputerID())
        end
    end
    
    -- Final prompt for auto-start
    -- ... (keep your existing auto-start code)
end

-- Include all the missing UI functions from your original code
-- (I'll add the key ones here for completeness)

local function clamp(x, a, b)
    return math.max(a, math.min(b, x))
end

local function clearScreen()
    term.setBackgroundColor(COLORS.background)
    term.setTextColor(COLORS.text)
    term.clear()
    term.setCursorPos(1, 1)
end

local function drawHeader(title)
    local w, h = term.getSize()
    term.setCursorPos(1, 1)
    term.setBackgroundColor(COLORS.info)
    term.setTextColor(COLORS.background)
    term.clearLine()
    local titleText = " " .. title .. " "
    local x = math.floor((w - #titleText) / 2) + 1
    term.setCursorPos(x, 1)
    term.write(titleText)
    term.setBackgroundColor(COLORS.background)
    term.setTextColor(COLORS.text)
end

local function showProgress(title, message, percent, color)
    clearScreen()
    drawHeader(title)
    
    local w, h = term.getSize()
    local barWidth = math.min(40, w - 4)
    local barX = math.floor((w - barWidth) / 2)
    local barY = math.floor(h / 2)
    
    -- Message
    term.setCursorPos(math.floor((w - #message) / 2), barY - 2)
    if color then
        term.setTextColor(color)
    end
    term.write(message)
    term.setTextColor(COLORS.text)
    
    -- Progress bar
    term.setCursorPos(barX, barY)
    term.write("[")
    local filled = math.floor(barWidth * (percent / 100))
    term.setTextColor(COLORS.success)
    term.write(string.rep("=", filled))
    term.setTextColor(COLORS.text)
    term.write(string.rep(" ", barWidth - filled))
    term.write("]")
    
    -- Percentage
    local percentText = math.floor(percent) .. "%"
    term.setCursorPos(math.floor((w - #percentText) / 2), barY + 2)
    term.write(percentText)
end

-- Add the rest of your UI functions here...

-- Run the installer
main()