-- 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()