diff --git a/scripts/get.ce b/scripts/get.ce index 12f7d249..a3702a94 100644 --- a/scripts/get.ce +++ b/scripts/get.ce @@ -1,29 +1,19 @@ -// cell get - Fetch a module and add it to dependencies +// cell get [alias] - Add and install a package with its dependencies -var fd = use('fd') var shop = use('shop') if (args.length < 1) { log.console("Usage: cell get [alias]") log.console("Examples:") - log.console(" cell get git.world/jj/mod@v0.6.3") - log.console(" cell get git.world/jj/mod (uses head/master)") + log.console(" cell get gitea.pockle.world/john/prosperon@main") + log.console(" cell get github.com/user/repo@v1.0.0 myalias") $_.stop() return } var locator = args[0] -var parsed = shop.parse_locator(locator) +var alias = args.length > 1 ? args[1] : null -// Use the module name as the default alias -var alias = parsed.name -if (args.length > 1) - alias = args[1] - -if (!alias) - throw new Error("Failed to determine alias"); - -log.console("Adding dependency: " + alias + " = " + locator) -shop.add_dependency(alias, locator) +shop.get(locator, alias) $_.stop() \ No newline at end of file diff --git a/scripts/json.cm b/scripts/json.cm index 57673a74..60bc7105 100644 --- a/scripts/json.cm +++ b/scripts/json.cm @@ -12,7 +12,7 @@ json.encode = function encode(val,replacer,space = 1,whitelist) // The optional reviver input is a method that will be called for every key and value at every level of the result. Each value will be replaced by the result of the reviver function. This can be used to reform data-only records into method-bearing records, or to transform date strings into seconds. json.decode = function decode(text,reviver) { - if (typeof json_in != 'string') + if (typeof text != 'string') throw new Error("couldn't parse text: not a string") return JSON.parse(text,reviver) } diff --git a/scripts/remove.ce b/scripts/remove.ce index 33895ec2..394791b2 100644 --- a/scripts/remove.ce +++ b/scripts/remove.ce @@ -1,7 +1,6 @@ -// cell remove - Remove a module from dependencies +// cell remove - Remove a package from dependencies var shop = use('shop') -var fd = use('fd') if (args.length < 1) { log.console("Usage: cell remove ") @@ -9,35 +8,6 @@ if (args.length < 1) { return } -var alias = args[0] - -// Check if cell.toml exists -if (!fd.stat('.cell/cell.toml').isFile) { - log.error("No cell.toml found.") - $_.stop() - return -} - -// Get module directory before removing dependency -var module_dir = shop.get_module_dir(alias) - -// Remove from dependencies -if (shop.remove_dependency(alias)) { - log.console("Removed dependency: " + alias) - - // Remove module directory - if (module_dir && fd.stat(module_dir).isDirectory) { - log.console("Removing module directory: " + module_dir) - try { - fd.rmdir(module_dir) - } catch (e) { - log.error("Failed to remove module directory: " + e) - } - } else { - log.console("Module directory not found or already removed.") - } -} else { - log.error("Dependency not found: " + alias) -} +shop.remove(args[0]) $_.stop() diff --git a/scripts/shop.cm b/scripts/shop.cm index f5178257..1d63fb95 100644 --- a/scripts/shop.cm +++ b/scripts/shop.cm @@ -152,42 +152,34 @@ Shop.save_config = function(config) { slurpwrite(shop_path, toml.encode(config)); } -function lock_path(pkg) -{ - if (pkg) - return `.cell/modules/${pkg}/.cell/lock.toml` - else - return '.cell/lock.toml' -} - // Load lock.toml configuration -Shop.load_lock = function(pkg) { - var path = lock_path(pkg) +Shop.load_lock = function() { + var path = '.cell/lock.toml' if (!fd.is_file(path)) - return null + return {} - var content = text(fd.slurp(lock_path)) + var content = text(fd.slurp(path)) + if (!content.length) return {} return toml.decode(content) } // Save lock.toml configuration -Shop.save_lock = function(pkg, lock) { - var path = lock_path(pkg) - slurpwrite(path, toml.encode(lock)); +Shop.save_lock = function(lock) { + slurpwrite('.cell/lock.toml', toml.encode(lock)); } // Initialize .cell directory structure Shop.init = function() { - if (!fd.is_directory('.cell')) { + if (!fd.is_dir('.cell')) { fd.mkdir('.cell') } - if (!fd.is_directory('.cell/modules')) { + if (!fd.is_dir('.cell/modules')) { fd.mkdir('.cell/modules') } - if (!fd.is_directory('.cell/build')) { + if (!fd.is_dir('.cell/build')) { fd.mkdir('.cell/build') } @@ -230,46 +222,25 @@ Shop.parse_locator = function(locator) { } // Convert module locator to download URL -Shop.get_download_url = function(locator) { +Shop.get_download_url = function(locator, commit_hash) { 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] + + if (parsed.path.includes('gitea.')) { + var parts = parsed.path.split('/') + var host = parts[0] + var user = parts[1] + var repo = parts[2] - // Gitea pattern: gitea.pockle.world/user/repo@branch - if (hostAndPath.includes('gitea.')) { - return 'https://' + hostAndPath + '/archive/' + parsed.version + '.zip' + if (!commit_hash) { + log.error("No commit hash available for download URL") + return null } - // GitHub pattern: github.com/user/repo@tag - if (hostAndPath.includes('github.com')) { - return 'https://' + hostAndPath + '/archive/refs/tags/' + parsed.version + '.zip' - } - - // GitLab pattern: gitlab.com/user/repo@tag - if (hostAndPath.includes('gitlab.')) { - return 'https://' + hostAndPath + '/-/archive/' + parsed.version + '/' + parsed.name + '-' + parsed.version + '.zip' - } - } else { - // Implicit https - var hostAndPath = parsed.path - // Gitea pattern: gitea.pockle.world/user/repo@branch - if (hostAndPath.includes('gitea.')) { - return 'https://' + hostAndPath + '/archive/' + parsed.version + '.zip' - } - - // GitHub pattern: github.com/user/repo@tag - if (hostAndPath.includes('github.com')) { - return 'https://' + hostAndPath + '/archive/refs/tags/' + parsed.version + '.zip' - } + return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip' } - - // Fallback to original locator if no pattern matches - return locator + + return null } // Add a dependency @@ -310,63 +281,31 @@ Shop.remove_dependency = function(alias) { Shop.get_api_url = function(locator) { var parsed = Shop.parse_locator(locator) if (!parsed) return null - - var hostAndPath = parsed.path - if (locator.startsWith('https://')) { - hostAndPath = locator.substring(8).split('@')[0] - } - - var parts = hostAndPath.split('/') - + + var parts = parsed.path.split('/') // Gitea pattern: gitea.pockle.world/user/repo@branch - if (hostAndPath.includes('gitea.')) { + if (parsed.path.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 + var url = 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/' + if (parsed.version) url += parsed.version + return url } - // 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 + + var data = json.decode(response) + 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 + if (Array.isArray(data)) + data = data[0] return data.commit && data.commit.id } @@ -449,7 +388,7 @@ Shop.install = function(alias) { var count = zip.count() for (var i = 0; i < count; i++) { - if (zip.is_directory(i)) continue + if (zip.is_dir(i)) continue var filename = zip.get_filename(i) // Strip top-level directory @@ -467,19 +406,9 @@ Shop.install = function(alias) { } } - // 4. Update Lock - if (commit_hash) { - var lock = Shop.load_lock() - lock[alias] = { - locator: locator, - commit: commit_hash, - updated: time.number() - } - Shop.save_lock(lock) - } - + // 4. Update Lock (only for root package) log.console("Installed " + alias) - return true + return { commit: commit_hash, locator: locator } } // Verify dependencies @@ -620,36 +549,309 @@ Shop.use = function(path, package_context) { Shop.resolve_locator = resolve_locator -// Check for updates -Shop.update = function() { - var config = Shop.load_config() - if (!config || !config.dependencies) return +// Install a package and all its transitive dependencies +// This is the internal workhorse - installs from a specific package context +function install_package_deps(canonical_name, installed) { + installed = installed || {} - var lock = Shop.load_lock() + // Load the package's config to find its dependencies + var pkg_config = Shop.load_config(canonical_name) + if (!pkg_config || !pkg_config.dependencies) return installed - for (var alias in config.dependencies) { - var locator = config.dependencies[alias] - var api_url = Shop.get_api_url(locator) + for (var alias in pkg_config.dependencies) { + var locator = pkg_config.dependencies[alias] + var parsed = Shop.parse_locator(locator) + var dep_canonical = parsed.path + // Skip if already installed in this run + if (installed[dep_canonical]) continue + + // Check if already exists on disk + var target_dir = '.cell/modules/' + dep_canonical + if (fd.is_dir(target_dir)) { + log.console(" " + alias + " already installed") + installed[dep_canonical] = true + // Still recurse into its deps + install_package_deps(dep_canonical, installed) + continue + } + + // Install this dependency + log.console(" Installing transitive dependency: " + alias + " (" + locator + ")") + var result = install_from_locator(locator) + if (result) { + installed[dep_canonical] = true + // Recurse into this package's dependencies + install_package_deps(dep_canonical, installed) + } + } + + return installed +} + +// Install from a raw locator (not from config) +function install_from_locator(locator, locked_hash) { + var parsed = Shop.parse_locator(locator) + var target_dir = '.cell/modules/' + parsed.path + + // 1. Get Commit Hash - use locked hash if provided, otherwise fetch + var commit_hash = locked_hash + if (!commit_hash) { + var api_url = Shop.get_api_url(locator) if (api_url) { try { var resp = http.fetch(api_url) var resp_text = text(resp) - var remote_hash = Shop.extract_commit_hash(locator, resp_text) - - var local_hash = lock[alias] ? lock[alias].commit : null - - if (remote_hash && remote_hash != local_hash) { - log.console("Update available for " + alias + ": " + local_hash + " -> " + remote_hash) - Shop.install(alias) - } else { - log.console(alias + " is up to date.") - } + commit_hash = Shop.extract_commit_hash(locator, resp_text) } catch (e) { - log.error("Failed to check update for " + alias + ": " + e) + log.console("Warning: Failed to fetch API info: " + e) + } + } + } else { + log.console("Using locked commit: " + commit_hash) + } + + // 2. Download Zip + var download_url = Shop.get_download_url(locator, commit_hash) + if (!download_url) { + log.error("Could not determine download URL for " + locator) + return null + } + + log.console("Downloading from " + download_url) + var zip_blob + try { + zip_blob = http.fetch(download_url) + } catch (e) { + log.error("Download failed: " + e) + return null + } + + // 3. Unpack + log.console("Unpacking to " + target_dir) + ensure_dir(target_dir) + + var zip = miniz.read(zip_blob) + if (!zip) + throw new Error("Failed to read zip archive") + + var count = zip.count() + log.console(`zip contains ${count} entries`) + for (var i = 0; i < count; i++) { + if (zip.is_dir(i)) continue + + var filename = zip.get_filename(i) + log.console(filename) + var parts = filename.split('/') + if (parts.length > 1) { + parts.shift() + var rel_path = parts.join('/') + + var full_path = target_dir + '/' + rel_path + var dir_path = full_path.substring(0, full_path.lastIndexOf('/')) + ensure_dir(dir_path) + + var content = zip.slurp(filename) + slurpwrite(full_path, content) + } + } + + return { commit: commit_hash, locator: locator, path: parsed.path } +} + +// High-level: Add a package, install it, and install all transitive dependencies +// Like `bun add` or `npm install ` +Shop.get = function(locator, alias) { + Shop.init() + + var parsed = Shop.parse_locator(locator) + if (!alias) alias = parsed.name + + log.console("Adding dependency: " + alias + " = " + locator) + + // Add to config + var config = Shop.load_config() || { dependencies: {} } + if (!config.dependencies) config.dependencies = {} + config.dependencies[alias] = locator + Shop.save_config(config) + + // Install the package + var result = install_from_locator(locator) + if (!result) { + log.error("Failed to install " + alias) + return false + } + + // Update lock file for root + var lock = Shop.load_lock(null) + lock[alias] = { + locator: locator, + commit: result.commit, + updated: time.number() + } + Shop.save_lock(lock) + + log.console("Installed " + alias) + + // Install transitive dependencies + log.console("Resolving transitive dependencies...") + install_package_deps(parsed.path, {}) + + log.console("Done.") + return true +} + +// High-level: Update a specific package or all packages +// Like `bun update` or `bun update ` +Shop.update_all = function(alias) { + var config = Shop.load_config() + if (!config || !config.dependencies) { + log.console("No dependencies to update.") + return + } + + var lock = Shop.load_lock() + var to_update = alias ? [alias] : Object.keys(config.dependencies) + + for (var i = 0; i < to_update.length; i++) { + var dep_alias = to_update[i] + var locator = config.dependencies[dep_alias] + + if (!locator) { + log.error("Dependency not found: " + dep_alias) + continue + } + + var api_url = Shop.get_api_url(locator) + if (!api_url) { + log.console(dep_alias + ": cannot check for updates (no API URL)") + continue + } + + try { + var parsed = Shop.parse_locator(locator) + var target_dir = `.cell/modules/${parsed.path}` + var resp = http.fetch(api_url) + var resp_text = text(resp) + var remote_hash = Shop.extract_commit_hash(locator, resp_text) + var local_hash = lock[dep_alias] ? lock[dep_alias].commit : null + if (!fd.is_dir(target_dir) || remote_hash != local_hash) { + log.console(dep_alias + ": updating " + (local_hash ? local_hash.substring(0,8) : "(new)") + " -> " + remote_hash.substring(0,8)) + + // Remove old directory + if (fd.is_dir(target_dir)) + fd.rmdir(target_dir) + + // Reinstall + var result = install_from_locator(locator, remote_hash) + if (result) { + lock[dep_alias] = { + locator: locator, + commit: result.commit, + updated: time.number() + } + + // Reinstall transitive deps + install_package_deps(parsed.path, {}) + } + } else { + log.console(dep_alias + ": up to date") + } + } catch (e) { + log.error("Failed to check " + dep_alias) + log.error(e) + } + } + + Shop.save_lock(lock) + log.console("Update complete.") +} + +// High-level: Remove a package and clean up +// Like `bun remove` +Shop.remove = function(alias) { + var config = Shop.load_config() + if (!config || !config.dependencies || !config.dependencies[alias]) { + log.error("Dependency not found: " + alias) + return false + } + + var locator = config.dependencies[alias] + var parsed = Shop.parse_locator(locator) + var target_dir = '.cell/modules/' + parsed.path + + // Remove from config + delete config.dependencies[alias] + Shop.save_config(config) + + // Remove from lock + var lock = Shop.load_lock() + delete lock[alias] + Shop.save_lock(lock) + + // Remove directory + if (fd.is_dir(target_dir)) { + log.console("Removing " + target_dir) + try { + fd.rmdir(target_dir) + } catch (e) { + log.error("Failed to remove directory: " + e) + } + } + + log.console("Removed " + alias) + return true +} + +// Install all dependencies from config (like `bun install`) +Shop.install_all = function() { + Shop.init() + + var config = Shop.load_config() + if (!config || !config.dependencies) { + log.console("No dependencies to install.") + return true + } + + var lock = Shop.load_lock(null) + var installed = {} + + for (var alias in config.dependencies) { + var locator = config.dependencies[alias] + var parsed = Shop.parse_locator(locator) + var target_dir = '.cell/modules/' + parsed.path + + // Check if already installed + if (fd.is_dir(target_dir)) { + log.console(alias + ": already installed") + installed[parsed.path] = true + continue + } + + log.console("Installing " + alias + "...") + var locked_hash = lock[alias] ? lock[alias].commit : null + var result = install_from_locator(locator, locked_hash) + if (result) { + installed[parsed.path] = true + lock[alias] = { + locator: locator, + commit: result.commit, + updated: time.number() } } } + + // Now install transitive dependencies for all root deps + log.console("Resolving transitive dependencies...") + for (var alias in config.dependencies) { + var locator = config.dependencies[alias] + var parsed = Shop.parse_locator(locator) + install_package_deps(parsed.path, installed) + } + + Shop.save_lock(lock) + log.console("Done.") + return true } // Compile a module diff --git a/scripts/text.cm b/scripts/text.cm index 28b964f3..e012278e 100644 --- a/scripts/text.cm +++ b/scripts/text.cm @@ -137,6 +137,7 @@ function text(...arguments) { // Default: interpret as UTF-8 text // Use the utf8 module to decode the blob + if (arg.length == 0) return "" return utf8.decode(arg); } diff --git a/scripts/update.ce b/scripts/update.ce index 4e4a3e7f..eff8e8e7 100644 --- a/scripts/update.ce +++ b/scripts/update.ce @@ -1,4 +1,10 @@ +// cell update [alias] - Update packages to latest versions + var shop = use('shop') + +var alias = args.length > 0 ? args[0] : null + log.console("Checking for updates...") -shop.update() +shop.update_all(alias) + $_.stop() \ No newline at end of file