From e689679aac685cbef658dd096c60eef2b07d6bfb Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Mon, 2 Jun 2025 12:12:05 -0500 Subject: [PATCH] add checking for new mod versions --- .cell/cell.toml | 3 +- .cell/lock.toml | 5 +- scripts/clean.ce | 27 +--------- scripts/engine.cm | 1 + scripts/io.cm | 27 ++++++++++ scripts/mod.ce | 36 ++++++++++++- scripts/shop.cm | 68 ++++++++++++++++++++++++ scripts/update.ce | 131 +++++++++++++++++++++++++++++++++++----------- tests/http.ce | 18 +------ 9 files changed, 236 insertions(+), 80 deletions(-) diff --git a/.cell/cell.toml b/.cell/cell.toml index cb3146c1..473031f4 100644 --- a/.cell/cell.toml +++ b/.cell/cell.toml @@ -1,3 +1,2 @@ - [dependencies] -extramath = "https://gitea.pockle.world/john/extramath@master" \ No newline at end of file +extramath = "https://gitea.pockle.world/john/extramath@master" diff --git a/.cell/lock.toml b/.cell/lock.toml index be7397ef..ea5e875a 100644 --- a/.cell/lock.toml +++ b/.cell/lock.toml @@ -1,5 +1,6 @@ [modules] [modules.extramath] -hash = "4244JXYZT7IMYQFYXOSPRK7VFCH4FBYQCQ5FCKYXMGA4QMN6RMPA====" +hash = "MCLZT3JABTAENS4WVXKGWJ7JPBLZER4YQ5VN2PE7ZD2Z4WYGTIMA====" url = "https://gitea.pockle.world/john/extramath@master" -downloaded = "Monday June 2 10:41:16.23 AM -5 2025 AD" \ No newline at end of file +downloaded = "Monday June 2 12:07:20.42 PM -5 2025 AD" +commit = "84d81a19a8455bcf8dc494739e9e6d545df6ff2c" \ No newline at end of file diff --git a/scripts/clean.ce b/scripts/clean.ce index e412d548..426c7f44 100644 --- a/scripts/clean.ce +++ b/scripts/clean.ce @@ -12,34 +12,9 @@ if (!io.exists('.cell/build')) { log.console("Cleaning build artifacts...") -// Recursively delete directory contents -function remove_dir(path) { - var files = io.enumerate(path, false) // non-recursive first - - // Delete all files and subdirectories - for (var i = 0; i < files.length; i++) { - var file = files[i] - if (io.is_directory(file)) { - remove_dir(file) // Recurse into subdirectory - } else { - try { - io.rm(file) - } catch (e) { - log.error("Failed to remove " + file + ": " + e) - } - } - } - - // Now remove the empty directory - try { - io.rm(path) - } catch (e) { - log.error("Failed to remove directory " + path + ": " + e) - } -} - // Remove the build directory try { + io.rmdir('.cell/build') remove_dir('.cell/build') log.console("Build directory removed") } catch (e) { diff --git a/scripts/engine.cm b/scripts/engine.cm index 1c2af36a..76ddea78 100644 --- a/scripts/engine.cm +++ b/scripts/engine.cm @@ -250,6 +250,7 @@ if (io.exists(configPath)) { } globalThis.json = use('json') +globalThis.text = use('text') var time = use('time') var blob = use('blob') diff --git a/scripts/io.cm b/scripts/io.cm index d3e60f34..c68e8d73 100644 --- a/scripts/io.cm +++ b/scripts/io.cm @@ -1,5 +1,32 @@ var io = this +function remove_dir(path) { + var files = io.enumerate(path, false) // non-recursive first + + // Delete all files and subdirectories + for (var i = 0; i < files.length; i++) { + var file = files[i] + if (io.is_directory(file)) { + remove_dir(file) // Recurse into subdirectory + } else { + try { + io.rm(file) + } catch (e) { + log.error("Failed to remove " + file + ": " + e) + } + } + } + + // Now remove the empty directory + try { + io.rm(path) + } catch (e) { + log.error("Failed to remove directory " + path + ": " + e) + } +} + +io.rmdir = remove_dir + io.rm[cell.DOC] = `Remove the file or empty directory at the given path. :param path: The file or empty directory to remove. Must be empty if a directory. diff --git a/scripts/mod.ce b/scripts/mod.ce index 10a41885..844dda63 100644 --- a/scripts/mod.ce +++ b/scripts/mod.ce @@ -36,6 +36,22 @@ uses.download = function() var zip var need_download = false + var remote_commit = null + + // Check remote commit if this is a git repository + var api_url = shop.get_api_url(mods[mod]) + if (api_url) { + log.console(`${mod}: checking remote commit...`) + try { + var api_response = http.fetch(api_url) + remote_commit = shop.extract_commit_hash(mods[mod], text(api_response)) + if (remote_commit) { + log.console(`${mod}: remote commit = ${remote_commit}`) + } + } catch (e) { + log.console(`${mod}: failed to check remote commit`) + } + } // Check if module exists in lock file if (!lock.modules[mod] || !lock.modules[mod].hash) { @@ -44,6 +60,11 @@ uses.download = function() } else if (!io.exists(cache_path)) { log.console(`${mod}: cache missing, will download`) need_download = true + } else if (remote_commit && (!lock.modules[mod].commit || lock.modules[mod].commit !== remote_commit)) { + log.console(`${mod}: remote has new commit`) + log.console(` local: ${lock.modules[mod].commit || 'unknown'}`) + log.console(` remote: ${remote_commit}`) + need_download = true } if (!need_download) { @@ -79,6 +100,12 @@ uses.download = function() url: mods[mod], downloaded: time.text() } + + // Store commit hash if available + if (remote_commit) { + lock.modules[mod].commit = remote_commit + } + log.console(`${mod}: hash = ${hash_b32}`) // Save updated lock file @@ -91,9 +118,14 @@ uses.download = function() log.console(`extracting ${mod} (${count} files)...`) + // Remove existing module directory if it exists (for clean updates) + if (io.exists(module_path)) { + log.console(`${mod}: removing old version...`) + io.rmdir(module_path) + } + // Create module directory - if (!io.exists(module_path)) - io.mkdir(module_path) + io.mkdir(module_path) // Extract each file for (var i = 0; i < count; i++) { diff --git a/scripts/shop.cm b/scripts/shop.cm index 3a69d734..f87606ca 100644 --- a/scripts/shop.cm +++ b/scripts/shop.cm @@ -109,6 +109,74 @@ Shop.add_dependency = function(alias, locator) { return true } +// Get the API URL for checking remote git commits +Shop.get_api_url = function(locator) { + var parsed = Shop.parse_locator(locator) + if (!parsed) return null + + // Handle different git hosting patterns + if (locator.startsWith('https://')) { + // Remove https:// prefix for parsing + var cleanLocator = locator.substring(8) + var hostAndPath = cleanLocator.split('@')[0] + var parts = hostAndPath.split('/') + + // Gitea pattern: gitea.pockle.world/user/repo@branch + if (hostAndPath.includes('gitea.')) { + var host = parts[0] + var user = parts[1] + var repo = parts[2] + return 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/' + parsed.version + } + + // GitHub pattern: github.com/user/repo@tag or @branch + if (hostAndPath.includes('github.com')) { + var user = parts[1] + var repo = parts[2] + // Try branch first, then tag + return 'https://api.github.com/repos/' + user + '/' + repo + '/branches/' + parsed.version + } + + // GitLab pattern: gitlab.com/user/repo@tag + if (hostAndPath.includes('gitlab.')) { + var user = parts[1] + var repo = parts[2] + var projectId = encodeURIComponent(user + '/' + repo) + return 'https://' + parts[0] + '/api/v4/projects/' + projectId + '/repository/branches/' + parsed.version + } + } + + // Fallback - return null if no API pattern matches + return null +} + +// Extract commit hash from API response +Shop.extract_commit_hash = function(locator, response) { + if (!response) return null + + var data + try { + data = json.decode(response) + } catch (e) { + log.console("Failed to parse API response: " + e) + return null + } + + // Handle different git hosting response formats + if (locator.includes('gitea.')) { + // Gitea: response.commit.id + return data.commit && data.commit.id + } else if (locator.includes('github.com')) { + // GitHub: response.commit.sha + return data.commit && data.commit.sha + } else if (locator.includes('gitlab.')) { + // GitLab: response.commit.id + return data.commit && data.commit.id + } + + return null +} + // Get the module directory for a given alias Shop.get_module_dir = function(alias) { var config = Shop.load_config() diff --git a/scripts/update.ce b/scripts/update.ce index e3fe7500..59f1cf60 100644 --- a/scripts/update.ce +++ b/scripts/update.ce @@ -1,51 +1,118 @@ -// cell update - Update a dependency to a new version +// cell update [alias] - Check for updates and optionally install them var io = use('io') var shop = use('shop') +var http = use('http') +var toml = use('toml') +var json = use('json') +var os = use('os') -if (args.length < 1) { - log.console("Usage: cell update [new-version]") - log.console("Example: cell update jj_mod v0.7.0") - $_.stop() - return -} - -var alias = args[0] - -if (!io.exists('.cell/shop.toml')) { - log.error("No shop.toml found. Run 'cell init' first.") +if (!io.exists('.cell/cell.toml')) { + log.error("No cell.toml found. Run 'cell init' first.") $_.stop() return } var config = shop.load_config() -if (!config || !config.dependencies || !config.dependencies[alias]) { - log.error("Dependency '" + alias + "' not found") +if (!config || !config.dependencies) { + log.console("No dependencies to update") $_.stop() return } -var current_version = config.dependencies[alias] -log.console("Current version: " + current_version) +// Load lock file +var lock_path = '.cell/lock.toml' +var lock = {} +if (io.exists(lock_path)) { + var lock_content = io.slurp(lock_path) + lock = toml.decode(lock_content) +} +if (!lock.modules) lock.modules = {} -if (args.length > 1) { - // Update to specific version - var new_version = args[1] - - // Parse the current locator to keep the host/path - var parsed = shop.parse_locator(current_version) - if (parsed) { - var new_locator = parsed.path + '@' + new_version - config.dependencies[alias] = new_locator - shop.save_config(config) - - log.console("Updated " + alias + " to " + new_locator) - log.console("Run 'cell get " + new_locator + "' to fetch the new version") +var updates_available = [] + +// Check specific dependency or all +var deps_to_check = {} +if (args.length > 0) { + var alias = args[0] + if (!config.dependencies[alias]) { + log.error("Dependency '" + alias + "' not found") + $_.stop() + return } + deps_to_check[alias] = config.dependencies[alias] } else { - // TODO: Check for latest version - log.console("TODO: Check for latest version of " + alias) - log.console("For now, specify version: cell update " + alias + " ") + deps_to_check = config.dependencies +} + +// Check each dependency for updates +for (var alias in deps_to_check) { + var locator = deps_to_check[alias] + log.console("Checking " + alias + " (" + locator + ")...") + + // Get API URL to check commits + var api_url = shop.get_api_url(locator) + if (!api_url) { + log.console(" Cannot check updates (no API support)") + continue + } + + try { + log.console(api_url) + var api_response = http.fetch(api_url) + var remote_commit = shop.extract_commit_hash(locator, text(api_response)) + + if (!remote_commit) { + log.console(" Failed to get remote commit") + continue + } + + var local_commit = lock.modules[alias] && lock.modules[alias].commit + + if (!local_commit) { + log.console(" No local commit tracked") + updates_available.push({ + alias: alias, + locator: locator, + local_commit: null, + remote_commit: remote_commit + }) + } else if (local_commit !== remote_commit) { + log.console(" Update available!") + log.console(" Local: " + local_commit.substring(0, 8)) + log.console(" Remote: " + remote_commit.substring(0, 8)) + updates_available.push({ + alias: alias, + locator: locator, + local_commit: local_commit, + remote_commit: remote_commit + }) + } else { + log.console(" Up to date (" + local_commit.substring(0, 8) + ")") + } + } catch (e) { + log.console(" Failed to check: " + e) + } +} + +if (updates_available.length === 0) { + log.console("\nAll dependencies are up to date!") + $_.stop() + return +} + +log.console("\n" + updates_available.length + " update(s) available:") +for (var i = 0; i < updates_available.length; i++) { + var update = updates_available[i] + log.console(" - " + update.alias) +} + +// If specific dependency was requested, auto-install +if (args.length > 0 && updates_available.length > 0) { + log.console("\nDownloading update...") + os.system("cell mod download") +} else if (updates_available.length > 0) { + log.console("\nRun 'cell mod download' to install updates") } $_.stop() \ No newline at end of file diff --git a/tests/http.ce b/tests/http.ce index 3e51da5a..a458a2ba 100644 --- a/tests/http.ce +++ b/tests/http.ce @@ -1,25 +1,11 @@ var http = use('http') var text = use('text') -// Test with a simpler endpoint first -log.console("Testing httpbin.org chunked response...") try { - var b = http.fetch("https://httpbin.org/stream/3") - log.console(b.length) - var text1 = text(b) - log.console("httpbin response length:", text1.length) - log.console("httpbin response:", text1) -} catch (e) { - log.console("httpbin error:", e) -} - -log.console("\nTesting dictionary.ink...") -try { - var b2 = http.fetch("https://dictionary.ink/find?word=theological") + var b2 = http.fetch("https://gitea.pockle.world/api/v1/repos/john/prosperon/branches/master") log.console(b2.length) var text2 = text(b2) - log.console("dictionary response length:", text2.length) - log.console("dictionary first 500 chars:", text2.substring(0, 500)) + log.console(text(b2)) } catch (e) { log.console("dictionary error:", e) } \ No newline at end of file