diff --git a/add.ce b/add.ce index 1d23f00b..0ae17bb6 100644 --- a/add.ce +++ b/add.ce @@ -1,17 +1,26 @@ -// cell get [alias] - Add and install a package with its dependencies +// cell add [alias] - Add and install a package with its dependencies var shop = use('internal/shop') +var fd = use('fd') if (args.length < 1) { - log.console("Usage: cell get [alias]") + log.console("Usage: cell add [alias]") log.console("Examples:") - log.console(" cell get gitea.pockle.world/john/prosperon@main") - log.console(" cell get github.com/user/repo@v1.0.0 myalias") + log.console(" cell add gitea.pockle.world/john/prosperon@main") + log.console(" cell add github.com/user/repo@v1.0.0 myalias") $stop() return } var locator = args[0] + +// Resolve relative paths to absolute paths +if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || fd.is_dir(locator)) { + var resolved = fd.realpath(locator) + if (resolved) { + locator = resolved + } +} var alias = args.length > 1 ? args[1] : null shop.get(locator, alias) diff --git a/clone.ce b/clone.ce new file mode 100644 index 00000000..e87ca9ff --- /dev/null +++ b/clone.ce @@ -0,0 +1,122 @@ +// cell clone +// Clones a cell package to the local , and links it. + +var shop = use('internal/shop') +var link = use('link') +var fd = use('fd') +var http = use('http') +var miniz = use('miniz') + +if (args.length < 2) { + log.console("Usage: cell clone ") + log.console("Clones a cell package to a local path and links it.") + $stop() + return +} + +var origin = args[0] +var target_path = args[1] + +// Resolve target path to absolute +if (target_path == '.' || target_path.startsWith('./') || target_path.startsWith('../')) { + var resolved = fd.realpath(target_path) + if (resolved) { + target_path = resolved + } else { + // Path doesn't exist yet, resolve relative to cwd + var cwd = fd.realpath('.') + if (target_path == '.') { + target_path = cwd + } else if (target_path.startsWith('./')) { + target_path = cwd + target_path.substring(1) + } else if (target_path.startsWith('../')) { + // Go up one directory from cwd + var parent = cwd.substring(0, cwd.lastIndexOf('/')) + target_path = parent + target_path.substring(2) + } + } +} + +// Check if target already exists +if (fd.is_dir(target_path)) { + log.console("Error: " + target_path + " already exists") + $stop() + return +} + +log.console("Cloning " + origin + " to " + target_path + "...") + +// Get the latest commit +var info = shop.resolve_package_info(origin) +if (!info || info == 'local') { + log.console("Error: " + origin + " is not a remote package") + $stop() + return +} + +// Update to get the commit hash +var update_result = shop.update(origin) +if (!update_result) { + log.console("Error: Could not fetch " + origin) + $stop() + return +} + +// Fetch and extract to the target path +var lock = shop.load_lock() +var entry = lock[origin] +if (!entry || !entry.commit) { + log.console("Error: No commit found for " + origin) + $stop() + return +} + +var download_url = shop.get_download_url(origin, entry.commit) +log.console("Downloading from " + download_url) + +try { + var zip_blob = http.fetch(download_url) + + // Extract zip to target path + var zip = miniz.read(zip_blob) + if (!zip) { + log.console("Error: Failed to read zip archive") + $stop() + return + } + + // Create target directory + fd.mkdir(target_path) + + var count = zip.count() + for (var i = 0; i < count; i++) { + if (zip.is_directory(i)) continue + var filename = zip.get_filename(i) + var parts = filename.split('/') + if (parts.length <= 1) continue + + // Skip the first directory (repo-commit prefix) + parts.shift() + var rel_path = parts.join('/') + var full_path = target_path + '/' + rel_path + var dir_path = full_path.substring(0, full_path.lastIndexOf('/')) + + // Ensure directory exists + if (!fd.is_dir(dir_path)) { + fd.mkdir(dir_path) + } + fd.slurpwrite(full_path, zip.slurp(filename)) + } + + log.console("Extracted to " + target_path) + + // Link the origin to the cloned path + link.add(origin, target_path, shop) + log.console("Linked " + origin + " -> " + target_path) + +} catch (e) { + log.console("Error: " + e.message) + if (e.stack) log.console(e.stack) +} + +$stop() diff --git a/install.ce b/install.ce index b5400237..5e0cf547 100644 --- a/install.ce +++ b/install.ce @@ -3,6 +3,7 @@ var shop = use('internal/shop') var build = use('build') +var fd = use('fd') if (args.length < 1) { log.console("Usage: cell install ") @@ -12,21 +13,50 @@ if (args.length < 1) { var locator = args[0] +// Resolve relative paths to absolute paths +// Local paths like '.' or '../foo' need to be converted to absolute paths +if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || fd.is_dir(locator)) { + var resolved = fd.realpath(locator) + if (resolved) { + locator = resolved + } +} + log.console("Installing " + locator + "...") -if (!shop.update(locator)) { - log.console("Failed to install " + locator) - $stop() - return + +var pkg = use('package') + +// Recursive install function that handles dependencies +function install_package(pkg_locator, visited) { + if (visited[pkg_locator]) return + visited[pkg_locator] = true + + // First, add to lock.toml + shop.update(pkg_locator) + + // Extract/symlink the package so we can read its cell.toml + shop.extract(pkg_locator) + + // Now get direct dependencies and install them first + try { + var deps = pkg.dependencies(pkg_locator) + if (deps) { + for (var alias in deps) { + var dep_locator = deps[alias] + log.console("Installing dependency " + dep_locator) + install_package(dep_locator, visited) + } + } + } catch (e) { + // Package might not have dependencies or cell.toml issue + log.console("Warning: Could not read dependencies for " + pkg_locator + ": " + e.message) + } + + // Build the package after all dependencies are installed + build.build_package(pkg_locator) } -var deps = shop.list_packages(locator) -for (var dep of deps) { - log.console("Installing dependency " + dep) - shop.update(dep) - build.build_package(dep) -} - -build.build_package(locator) +install_package(locator, {}) log.console("Installed " + locator) $stop() diff --git a/internal/shop.cm b/internal/shop.cm index 018605bf..4b665e27 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -224,6 +224,10 @@ function get_canonical_package(alias, package_context) { // guaranteed to be validated function safe_package_path(pkg) { + // For absolute paths, replace / with _ to create a valid directory name + // Also replace @ with _ + if (pkg && pkg.startsWith('/')) + return pkg.replaceAll('/', '_').replaceAll('@', '_') return pkg.replaceAll('@', '_') } @@ -459,7 +463,15 @@ function resolve_locator(path, ctx) } // check in ctx package - var ctx_path = get_packages_dir() + '/' + safe_package_path(ctx) + '/' + path + // If ctx is an absolute path (starts with /), use it directly + // Otherwise, look it up in the packages directory + var ctx_dir + if (ctx.startsWith('/')) { + ctx_dir = ctx + } else { + ctx_dir = get_packages_dir() + '/' + safe_package_path(ctx) + } + var ctx_path = ctx_dir + '/' + path if (fd.is_file(ctx_path)) { var fn = resolve_mod_fn(ctx_path, ctx) @@ -818,7 +830,7 @@ function get_cache_path(pkg, commit) { function get_package_abs_dir(package) { - return get_packages_dir() + '/' + package + return get_packages_dir() + '/' + safe_package_path(package) } // Fetch the latest commit hash from remote for a package @@ -1195,4 +1207,29 @@ Shop.audit_packages = function() { return bad } +// Parse a package locator and return info about it +// Returns { path: canonical_path, name: package_name, type: 'local'|'gitea'|null } +Shop.parse_package = function(locator) { + if (!locator) return null + + // Strip version suffix if present + var clean = locator + if (locator.includes('@')) { + clean = locator.split('@')[0] + } + + var info = Shop.resolve_package_info(clean) + if (!info) return null + + // Extract package name (last component of path) + var parts = clean.split('/') + var name = parts[parts.length - 1] + + return { + path: clean, + name: name, + type: info + } +} + return Shop \ No newline at end of file diff --git a/link.cm b/link.cm index 370d18f6..78747a08 100644 --- a/link.cm +++ b/link.cm @@ -20,11 +20,14 @@ function get_packages_dir() { // return the safe path for the package function safe_package_path(pkg) { + // For absolute paths, replace / with _ to create a valid directory name + if (pkg && pkg.startsWith('/')) + return pkg.replaceAll('/', '_').replaceAll('@', '_') return pkg.replaceAll('@', '_') } function get_package_abs_dir(package) { - return get_packages_dir() + '/' + package + return get_packages_dir() + '/' + safe_package_path(package) } function ensure_dir(path) { diff --git a/package.cm b/package.cm index 755836bc..77554bb7 100644 --- a/package.cm +++ b/package.cm @@ -4,9 +4,26 @@ var fd = use('fd') var toml = use('toml') var os = use('os') +// Convert package name to a safe directory name +// For absolute paths (local packages), replace / with _ +// For remote packages, keep slashes as they use nested directories +function safe_package_path(pkg) { + if (!pkg) return pkg + if (pkg.startsWith('/')) + return pkg.replaceAll('/', '_').replaceAll('@', '_') + return pkg.replaceAll('@', '_') +} + function get_path(name) { - return os.global_shop_path + '/packages/' + name + // If name is null, return the current project directory + if (!name) + return fd.realpath('.') + // If name is already an absolute path, use it directly + if (name.startsWith('/')) + return name + // Remote packages use nested directories, so don't transform slashes + return os.global_shop_path + '/packages/' + name.replaceAll('@', '_') } package.load_config = function(name) diff --git a/remove.ce b/remove.ce index 0e7d96d0..47920119 100644 --- a/remove.ce +++ b/remove.ce @@ -1,6 +1,7 @@ // cell remove - Remove a package from dependencies or shop var shop = use('internal/shop') +var fd = use('fd') if (args.length < 1) { log.console("Usage: cell remove ") @@ -8,6 +9,16 @@ if (args.length < 1) { return } -shop.remove(args[0]) +var pkg = args[0] + +// Resolve relative paths to absolute paths +if (pkg == '.' || pkg.startsWith('./') || pkg.startsWith('../') || fd.is_dir(pkg)) { + var resolved = fd.realpath(pkg) + if (resolved) { + pkg = resolved + } +} + +shop.remove(pkg) $stop() diff --git a/search.ce b/search.ce new file mode 100644 index 00000000..1ea23786 --- /dev/null +++ b/search.ce @@ -0,0 +1,81 @@ +// cell search +// Searches for packages matching , or actors or modules within them. + +var shop = use('internal/shop') +var pkg = use('package') + +if (args.length < 1) { + log.console("Usage: cell search ") + log.console("Searches for packages, actors, or modules matching the query.") + $stop() + return +} + +var query = args[0].toLowerCase() +var found_packages = [] +var found_modules = [] +var found_actors = [] + +// Search through all installed packages +var packages = shop.list_packages() + +for (var package_name of packages) { + // Check if package name matches + if (package_name.toLowerCase().includes(query)) { + found_packages.push(package_name) + } + + // Search modules and actors within the package + try { + var modules = pkg.list_modules(package_name) + for (var mod of modules) { + if (mod.toLowerCase().includes(query)) { + found_modules.push(package_name + ':' + mod) + } + } + + var actors = pkg.list_programs(package_name) + for (var actor of actors) { + if (actor.toLowerCase().includes(query)) { + found_actors.push(package_name + ':' + actor) + } + } + } catch (e) { + // Skip packages that can't be read + } +} + +// Print results +var total = found_packages.length + found_modules.length + found_actors.length + +if (total == 0) { + log.console("No results found for '" + query + "'") +} else { + log.console("Found " + text(total) + " result(s) for '" + query + "':") + log.console("") + + if (found_packages.length > 0) { + log.console("Packages:") + for (var p of found_packages) { + log.console(" " + p) + } + log.console("") + } + + if (found_modules.length > 0) { + log.console("Modules:") + for (var m of found_modules) { + log.console(" " + m) + } + log.console("") + } + + if (found_actors.length > 0) { + log.console("Actors:") + for (var a of found_actors) { + log.console(" " + a) + } + } +} + +$stop() diff --git a/unlink.ce b/unlink.ce new file mode 100644 index 00000000..100d7f19 --- /dev/null +++ b/unlink.ce @@ -0,0 +1,33 @@ +// cell unlink +// Unlinks from its from a previous link. + +var link = use('link') +var shop = use('internal/shop') + +if (args.length < 1) { + log.console("Usage: cell unlink ") + log.console("Removes a link and restores the original package.") + $stop() + return +} + +var origin = args[0] + +if (link.remove(origin)) { + log.console("Removed link for " + origin) + + // Try to restore the original package + log.console("Restoring " + origin + "...") + try { + shop.fetch(origin) + shop.extract(origin) + log.console("Restored " + origin) + } catch (e) { + log.console("Could not restore: " + e.message) + log.console("Run 'cell update " + origin + "' to restore") + } +} else { + log.console("No link found for " + origin) +} + +$stop() diff --git a/update.ce b/update.ce index 7cda4eb5..0fc2c990 100644 --- a/update.ce +++ b/update.ce @@ -9,6 +9,7 @@ // cell update - Update a specific package var shop = use('internal/shop') +var fd = use('fd') var target_pkg = null @@ -26,6 +27,13 @@ for (var i = 0; i < args.length; i++) { $stop() } else if (!args[i].startsWith('-')) { target_pkg = args[i] + // Resolve relative paths to absolute paths + if (target_pkg == '.' || target_pkg.startsWith('./') || target_pkg.startsWith('../') || fd.is_dir(target_pkg)) { + var resolved = fd.realpath(target_pkg) + if (resolved) { + target_pkg = resolved + } + } } } @@ -37,9 +45,16 @@ function update_and_fetch(pkg) var new_entry = shop.update(pkg) - if (new_entry && new_entry.commit) { - log.console(" " + pkg + " " + old_commit.substring(0, 8) + " -> " + new_entry.commit.substring(0, 8)) - shop.fetch(pkg) + if (new_entry) { + if (new_entry.commit) { + var old_str = old_commit ? old_commit.substring(0, 8) : "(new)" + log.console(" " + pkg + " " + old_str + " -> " + new_entry.commit.substring(0, 8)) + shop.fetch(pkg) + } else { + // Local package - just ensure symlink is correct + log.console(" " + pkg + " (local)") + } + shop.extract(pkg) shop.build_package_scripts(pkg) return true }