diff --git a/add.ce b/add.ce index 0ae17bb6..e8ad68e6 100644 --- a/add.ce +++ b/add.ce @@ -1,18 +1,43 @@ -// cell add [alias] - Add and install a package with its dependencies +// cell add [alias] - Add a dependency to the current package +// +// Usage: +// cell add Add a dependency using default alias +// cell add Add a dependency with custom alias +// +// This adds the dependency to cell.toml and installs it to the shop. var shop = use('internal/shop') +var pkg = use('package') +var build = use('build') var fd = use('fd') -if (args.length < 1) { - log.console("Usage: cell add [alias]") - log.console("Examples:") - 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 = null +var alias = null + +for (var i = 0; i < args.length; i++) { + if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell add [alias]") + log.console("") + log.console("Add a dependency to the current package.") + log.console("") + log.console("Examples:") + log.console(" cell add gitea.pockle.world/john/prosperon") + log.console(" cell add gitea.pockle.world/john/cell-image image") + log.console(" cell add ../local-package") + $stop() + } else if (!args[i].startsWith('-')) { + if (!locator) { + locator = args[i] + } else if (!alias) { + alias = args[i] + } + } } -var locator = args[0] +if (!locator) { + log.console("Usage: cell add [alias]") + $stop() +} // Resolve relative paths to absolute paths if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || fd.is_dir(locator)) { @@ -21,8 +46,58 @@ if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || f locator = resolved } } -var alias = args.length > 1 ? args[1] : null -shop.get(locator, alias) +// Generate default alias from locator +if (!alias) { + // Use the last component of the locator as alias + var parts = locator.split('/') + alias = parts[parts.length - 1] + // Remove any version suffix + if (alias.includes('@')) { + alias = alias.split('@')[0] + } +} -$stop() \ No newline at end of file +// Check we're in a package directory +var cwd = fd.realpath('.') +if (!fd.is_file(cwd + '/cell.toml')) { + log.error("Not in a package directory (no cell.toml found)") + $stop() +} + +log.console("Adding " + locator + " as '" + alias + "'...") + +// Add to local project's cell.toml +try { + pkg.add_dependency(null, locator, alias) + log.console(" Added to cell.toml") +} catch (e) { + log.error("Failed to update cell.toml: " + e) + $stop() +} + +// Install to shop +try { + shop.get(locator) + shop.extract(locator) + + // Build scripts + shop.build_package_scripts(locator) + + // Build C code if any + try { + var target = build.detect_host_target() + build.build_dynamic(locator, target, 'release') + } catch (e) { + // Not all packages have C code + } + + log.console(" Installed to shop") +} catch (e) { + log.error("Failed to install: " + e) + $stop() +} + +log.console("Added " + alias + " (" + locator + ")") + +$stop() diff --git a/build.ce b/build.ce index 63df587e..08aa9f89 100644 --- a/build.ce +++ b/build.ce @@ -1,9 +1,11 @@ -// cell build [options] - Build dynamic libraries locally for the current machine +// cell build [] - Build dynamic libraries locally for the current machine // // Usage: -// cell build Build dynamic libraries for all packages -// cell build -p Build dynamic library for specific package +// cell build Build dynamic libraries for all packages in shop +// cell build . Build dynamic library for current directory package +// cell build Build dynamic library for specific package // cell build -t Cross-compile dynamic libraries for target platform +// cell build -b Build type: release (default), debug, or minsize var build = use('build') var shop = use('internal/shop') @@ -12,7 +14,9 @@ var fd = use('fd') var target = null var target_package = null -var buildtype = 'debug' +var buildtype = 'release' +var force_rebuild = false +var dry_run = false for (var i = 0; i < args.length; i++) { if (args[i] == '-t' || args[i] == '--target') { @@ -23,6 +27,7 @@ for (var i = 0; i < args.length; i++) { $stop() } } else if (args[i] == '-p' || args[i] == '--package') { + // Legacy support for -p flag if (i + 1 < args.length) { target_package = args[++i] } else { @@ -40,6 +45,10 @@ for (var i = 0; i < args.length; i++) { log.error('-b requires a buildtype (release, debug, minsize)') $stop() } + } else if (args[i] == '--force') { + force_rebuild = true + } else if (args[i] == '--dry-run') { + dry_run = true } else if (args[i] == '--list-targets') { log.console('Available targets:') var targets = build.list_targets() @@ -47,6 +56,19 @@ for (var i = 0; i < args.length; i++) { log.console(' ' + targets[t]) } $stop() + } else if (!args[i].startsWith('-') && !target_package) { + // Positional argument - treat as package locator + target_package = args[i] + } +} + +// Resolve local paths to absolute paths +if (target_package) { + if (target_package == '.' || target_package.startsWith('./') || target_package.startsWith('../') || fd.is_dir(target_package)) { + var resolved = fd.realpath(target_package) + if (resolved) { + target_package = resolved + } } } diff --git a/clean.ce b/clean.ce index 1f54693b..b7bf3ec3 100644 --- a/clean.ce +++ b/clean.ce @@ -1,26 +1,218 @@ -// cell clean - Remove build artifacts from global shop +// cell clean [] - Remove cached material to force refetch/rebuild +// +// Usage: +// cell clean Clean build outputs for current directory package +// cell clean . Clean build outputs for current directory package +// cell clean Clean build outputs for specific package +// cell clean shop Clean entire shop +// cell clean world Clean all world packages +// +// Options: +// --build Remove build outputs only (default) +// --fetch Remove fetched sources only +// --all Remove both build outputs and fetched sources +// --deep Apply to full dependency closure +// --dry-run Show what would be deleted -var fd = use('fd') var shop = use('internal/shop') +var pkg = use('package') +var fd = use('fd') -var build_dir = shop.get_shop_path() + '/build' +var scope = null +var clean_build = false +var clean_fetch = false +var deep = false +var dry_run = false -if (!fd.is_dir(build_dir)) { - log.console("No build directory found at " + build_dir) - $stop() - return +for (var i = 0; i < args.length; i++) { + if (args[i] == '--build') { + clean_build = true + } else if (args[i] == '--fetch') { + clean_fetch = true + } else if (args[i] == '--all') { + clean_build = true + clean_fetch = true + } else if (args[i] == '--deep') { + deep = true + } else if (args[i] == '--dry-run') { + dry_run = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell clean [] [options]") + log.console("") + log.console("Remove cached material to force refetch/rebuild.") + log.console("") + log.console("Scopes:") + log.console(" Clean specific package") + log.console(" shop Clean entire shop") + log.console(" world Clean all world packages") + log.console("") + log.console("Options:") + log.console(" --build Remove build outputs only (default)") + log.console(" --fetch Remove fetched sources only") + log.console(" --all Remove both build outputs and fetched sources") + log.console(" --deep Apply to full dependency closure") + log.console(" --dry-run Show what would be deleted") + $stop() + } else if (!args[i].startsWith('-')) { + scope = args[i] + } } -log.console("Cleaning build artifacts...") - -// Remove the build directory -try { - fd.rm(build_dir) - log.console("Build directory removed: " + build_dir) -} catch (e) { - log.error(e) +// Default to --build if nothing specified +if (!clean_build && !clean_fetch) { + clean_build = true } -log.console("Clean complete!") +// Default scope to current directory +if (!scope) { + scope = '.' +} -$stop() \ No newline at end of file +// Resolve local paths for single package scope +var is_shop_scope = (scope == 'shop') +var is_world_scope = (scope == 'world') + +if (!is_shop_scope && !is_world_scope) { + if (scope == '.' || scope.startsWith('./') || scope.startsWith('../') || fd.is_dir(scope)) { + var resolved = fd.realpath(scope) + if (resolved) { + scope = resolved + } + } +} + +var files_to_delete = [] +var dirs_to_delete = [] + +// Gather packages to clean +var packages_to_clean = [] + +if (is_shop_scope) { + packages_to_clean = shop.list_packages() +} else if (is_world_scope) { + // For now, world is the same as shop + packages_to_clean = shop.list_packages() +} else { + // Single package + packages_to_clean.push(scope) + + if (deep) { + try { + var deps = pkg.gather_dependencies(scope) + for (var dep of deps) { + packages_to_clean.push(dep) + } + } catch (e) { + // Skip if can't read dependencies + } + } +} + +// Gather files to clean +var lib_dir = shop.get_lib_dir() +var build_dir = shop.get_build_dir() +var packages_dir = shop.get_package_dir('').replace(/\/$/, '') // Get base packages dir + +if (clean_build) { + if (is_shop_scope) { + // Clean entire build and lib directories + if (fd.is_dir(build_dir)) { + dirs_to_delete.push(build_dir) + } + if (fd.is_dir(lib_dir)) { + dirs_to_delete.push(lib_dir) + } + } else { + // Clean specific package libraries + for (var p of packages_to_clean) { + if (p == 'core') continue + + var lib_name = shop.lib_name_for_package(p) + var dylib_ext = '.dylib' + var lib_path = lib_dir + '/' + lib_name + dylib_ext + + if (fd.is_file(lib_path)) { + files_to_delete.push(lib_path) + } + + // Also check for .so and .dll + var so_path = lib_dir + '/' + lib_name + '.so' + var dll_path = lib_dir + '/' + lib_name + '.dll' + if (fd.is_file(so_path)) { + files_to_delete.push(so_path) + } + if (fd.is_file(dll_path)) { + files_to_delete.push(dll_path) + } + } + } +} + +if (clean_fetch) { + if (is_shop_scope) { + // Clean entire packages directory (dangerous!) + if (fd.is_dir(packages_dir)) { + dirs_to_delete.push(packages_dir) + } + } else { + // Clean specific package directories + for (var p of packages_to_clean) { + if (p == 'core') continue + + var pkg_dir = shop.get_package_dir(p) + if (fd.is_dir(pkg_dir) || fd.is_link(pkg_dir)) { + dirs_to_delete.push(pkg_dir) + } + } + } +} + +// Execute or report +if (dry_run) { + log.console("Would delete:") + if (files_to_delete.length == 0 && dirs_to_delete.length == 0) { + log.console(" (nothing to clean)") + } else { + for (var f of files_to_delete) { + log.console(" [file] " + f) + } + for (var d of dirs_to_delete) { + log.console(" [dir] " + d) + } + } +} else { + var deleted_count = 0 + + for (var f of files_to_delete) { + try { + fd.unlink(f) + log.console("Deleted: " + f) + deleted_count++ + } catch (e) { + log.error("Failed to delete " + f + ": " + e) + } + } + + for (var d of dirs_to_delete) { + try { + if (fd.is_link(d)) { + fd.unlink(d) + } else { + fd.rmdir(d, 1) // recursive + } + log.console("Deleted: " + d) + deleted_count++ + } catch (e) { + log.error("Failed to delete " + d + ": " + e) + } + } + + if (deleted_count == 0) { + log.console("Nothing to clean.") + } else { + log.console("") + log.console("Clean complete: " + text(deleted_count) + " item(s) deleted.") + } +} + +$stop() diff --git a/graph.ce b/graph.ce new file mode 100644 index 00000000..544f8023 --- /dev/null +++ b/graph.ce @@ -0,0 +1,236 @@ +// cell graph [] - Emit dependency graph +// +// Usage: +// cell graph Graph current directory package +// cell graph . Graph current directory package +// cell graph Graph specific package +// cell graph --world Graph all packages in shop (world set) +// +// Options: +// --format Output format: tree (default), dot, json +// --resolved Show resolved view with links applied (default) +// --locked Show lock view without links +// --world Graph all packages in shop + +var shop = use('internal/shop') +var pkg = use('package') +var link = use('link') +var fd = use('fd') +var json = use('json') + +var target_locator = null +var format = 'tree' +var show_locked = false +var show_world = false + +for (var i = 0; i < args.length; i++) { + if (args[i] == '--format' || args[i] == '-f') { + if (i + 1 < args.length) { + format = args[++i] + if (format != 'tree' && format != 'dot' && format != 'json') { + log.error('Invalid format: ' + format + '. Must be tree, dot, or json') + $stop() + } + } else { + log.error('--format requires a format type') + $stop() + } + } else if (args[i] == '--resolved') { + show_locked = false + } else if (args[i] == '--locked') { + show_locked = true + } else if (args[i] == '--world') { + show_world = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell graph [] [options]") + log.console("") + log.console("Emit the dependency graph.") + log.console("") + log.console("Options:") + log.console(" --format Output format: tree (default), dot, json") + log.console(" --resolved Show resolved view with links applied (default)") + log.console(" --locked Show lock view without links") + log.console(" --world Graph all packages in shop") + $stop() + } else if (!args[i].startsWith('-')) { + target_locator = args[i] + } +} + +var links = show_locked ? {} : link.load() + +// Get effective locator (after links) +function get_effective(locator) { + return links[locator] || locator +} + +// Build graph data structure +var nodes = {} +var edges = [] + +function add_node(locator) { + if (nodes[locator]) return + + var lock = shop.load_lock() + var lock_entry = lock[locator] + var link_target = links[locator] + var info = shop.resolve_package_info(locator) + + nodes[locator] = { + id: locator, + effective: get_effective(locator), + linked: link_target != null, + local: info == 'local', + commit: lock_entry && lock_entry.commit ? lock_entry.commit.substring(0, 8) : null + } +} + +function gather_graph(locator, visited) { + if (visited[locator]) return + visited[locator] = true + + add_node(locator) + + try { + var deps = pkg.dependencies(locator) + if (deps) { + for (var alias in deps) { + var dep_locator = deps[alias] + add_node(dep_locator) + edges.push({ from: locator, to: dep_locator, alias: alias }) + gather_graph(dep_locator, visited) + } + } + } catch (e) { + // Package might not have dependencies + } +} + +// Gather graph from roots +var roots = [] + +if (show_world) { + // Use all packages in shop as roots + var packages = shop.list_packages() + for (var p of packages) { + if (p != 'core') { + roots.push(p) + } + } +} else { + // Default to current directory + if (!target_locator) { + target_locator = '.' + } + + // Resolve local paths + if (target_locator == '.' || target_locator.startsWith('./') || target_locator.startsWith('../') || fd.is_dir(target_locator)) { + var resolved = fd.realpath(target_locator) + if (resolved) { + target_locator = resolved + } + } + + roots.push(target_locator) +} + +for (var root of roots) { + gather_graph(root, {}) +} + +// Output based on format +if (format == 'tree') { + function print_tree(locator, prefix, is_last, visited) { + if (visited[locator]) { + log.console(prefix + (is_last ? "\\-- " : "|-- ") + locator + " (circular)") + return + } + visited[locator] = true + + var node = nodes[locator] + var suffix = "" + if (node.linked) suffix += " -> " + node.effective + if (node.commit) suffix += " @" + node.commit + if (node.local) suffix += " (local)" + + log.console(prefix + (is_last ? "\\-- " : "|-- ") + locator + suffix) + + // Get children + var children = [] + for (var e of edges) { + if (e.from == locator) { + children.push(e) + } + } + + for (var i = 0; i < children.length; i++) { + var child_prefix = prefix + (is_last ? " " : "| ") + print_tree(children[i].to, child_prefix, i == children.length - 1, visited) + } + } + + for (var i = 0; i < roots.length; i++) { + log.console(roots[i]) + + var children = [] + for (var e of edges) { + if (e.from == roots[i]) { + children.push(e) + } + } + + for (var j = 0; j < children.length; j++) { + print_tree(children[j].to, "", j == children.length - 1, {}) + } + + if (i < roots.length - 1) log.console("") + } + +} else if (format == 'dot') { + log.console("digraph dependencies {") + log.console(" rankdir=TB;") + log.console(" node [shape=box];") + log.console("") + + // Node definitions + for (var id in nodes) { + var node = nodes[id] + var label = id + if (node.commit) label += "\\n@" + node.commit + var attrs = 'label="' + label + '"' + if (node.linked) attrs += ', style=dashed' + if (node.local) attrs += ', color=blue' + + // Safe node ID for dot + var safe_id = id.replaceAll(/[^a-zA-Z0-9]/g, '_') + log.console(' ' + safe_id + ' [' + attrs + '];') + } + + log.console("") + + // Edges + for (var e of edges) { + var from_id = e.from.replaceAll(/[^a-zA-Z0-9]/g, '_') + var to_id = e.to.replaceAll(/[^a-zA-Z0-9]/g, '_') + var label = e.alias != e.to ? 'label="' + e.alias + '"' : '' + log.console(' ' + from_id + ' -> ' + to_id + (label ? ' [' + label + ']' : '') + ';') + } + + log.console("}") + +} else if (format == 'json') { + var output = { + nodes: [], + edges: [] + } + + for (var id in nodes) { + output.nodes.push(nodes[id]) + } + + output.edges = edges + + log.console(json.encode(output)) +} + +$stop() diff --git a/help.ce b/help.ce index cdb83fa9..6ed7a179 100644 --- a/help.ce +++ b/help.ce @@ -27,21 +27,41 @@ if (stat && stat.isFile) { log.console(content) } else { // Fallback if man file doesn't exist - log.console("cell - The Cell module system for Prosperon") + log.console("cell - The Cell package manager") log.console("") log.console("Usage: cell [arguments]") log.console("") - log.console("Commands:") - log.console(" init Initialize a new Cell project") - log.console(" get Fetch and add a module dependency") - log.console(" update Update a dependency to a new version") - log.console(" vendor Copy all dependencies locally") - log.console(" build Compile all modules to bytecode") - log.console(" patch Create a patch for a module") - log.console(" config Manage system and actor configurations") - log.console(" help Show this help message") + log.console("Package Management:") + log.console(" install Install a package and its dependencies") + log.console(" update [locator] Update packages from remote sources") + log.console(" remove Remove a package from the shop") + log.console(" add Add a dependency to current package") log.console("") - log.console("Run 'cell help ' for more information on a command.") + log.console("Building:") + log.console(" build [locator] Build dynamic libraries for packages") + log.console(" clean [scope] Remove build artifacts") + log.console("") + log.console("Linking (Local Development):") + log.console(" link Link a package to a local path") + log.console(" unlink Remove a package link") + log.console(" clone Clone and link a package locally") + log.console("") + log.console("Information:") + log.console(" list [scope] List packages and dependencies") + log.console(" ls [locator] List modules and actors in a package") + log.console(" why Show reverse dependencies") + log.console(" search Search for packages, modules, or actors") + log.console("") + log.console("Diagnostics:") + log.console(" resolve [locator] Print fully resolved dependency closure") + log.console(" graph [locator] Emit dependency graph (tree, dot, json)") + log.console(" verify [scope] Verify integrity and consistency") + log.console("") + log.console("Other:") + log.console(" help [command] Show help for a command") + log.console(" version Show cell version") + log.console("") + log.console("Run 'cell --help' for more information on a command.") } -$stop() \ No newline at end of file +$stop() diff --git a/install.ce b/install.ce index 5e0cf547..55bafc60 100644 --- a/install.ce +++ b/install.ce @@ -1,17 +1,65 @@ // cell install - Install a package to the shop -// Does not modify the current project's cell.toml +// +// Usage: +// cell install Install a package and its dependencies +// cell install . Install current directory package +// +// Options: +// --target Build for target platform +// --refresh Refresh floating refs before locking +// --dry-run Show what would be installed var shop = use('internal/shop') var build = use('build') +var pkg = use('package') var fd = use('fd') if (args.length < 1) { - log.console("Usage: cell install ") + log.console("Usage: cell install [options]") + log.console("") + log.console("Options:") + log.console(" --target Build for target platform") + log.console(" --refresh Refresh floating refs before locking") + log.console(" --dry-run Show what would be installed") $stop() - return } -var locator = args[0] +var locator = null +var target_triple = null +var refresh = false +var dry_run = false + +for (var i = 0; i < args.length; i++) { + if (args[i] == '--target' || args[i] == '-t') { + if (i + 1 < args.length) { + target_triple = args[++i] + } else { + log.error('--target requires a triple') + $stop() + } + } else if (args[i] == '--refresh') { + refresh = true + } else if (args[i] == '--dry-run') { + dry_run = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell install [options]") + log.console("") + log.console("Install a package and its dependencies to the shop.") + log.console("") + log.console("Options:") + log.console(" --target Build for target platform") + log.console(" --refresh Refresh floating refs before locking") + log.console(" --dry-run Show what would be installed") + $stop() + } else if (!args[i].startsWith('-')) { + locator = args[i] + } +} + +if (!locator) { + log.console("Usage: cell install ") + $stop() +} // Resolve relative paths to absolute paths // Local paths like '.' or '../foo' need to be converted to absolute paths @@ -22,41 +70,86 @@ if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || f } } +// Default target +if (!target_triple) { + target_triple = build.detect_host_target() +} + log.console("Installing " + locator + "...") -var pkg = use('package') +// Gather all packages that will be installed +var packages_to_install = [] +var visited = {} -// Recursive install function that handles dependencies -function install_package(pkg_locator, visited) { +function gather_packages(pkg_locator) { 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 + + packages_to_install.push(pkg_locator) + + // Try to read dependencies try { + // For packages not yet extracted, we need to update and extract first to read deps + var lock = shop.load_lock() + if (!lock[pkg_locator]) { + if (!dry_run) { + shop.update(pkg_locator) + shop.extract(pkg_locator) + } + } + 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) + gather_packages(dep_locator) } } } catch (e) { // Package might not have dependencies or cell.toml issue - log.console("Warning: Could not read dependencies for " + pkg_locator + ": " + e.message) + if (!dry_run) { + 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) } -install_package(locator, {}) -log.console("Installed " + locator) +// Gather all packages +gather_packages(locator) + +if (dry_run) { + log.console("Would install:") + for (var p of packages_to_install) { + var lock = shop.load_lock() + var exists = lock[p] != null + log.console(" " + p + (exists ? " (already installed)" : "")) + } + $stop() +} + +// Install each package +function install_package(pkg_locator) { + // Update lock entry + shop.update(pkg_locator) + + // Extract/symlink the package + shop.extract(pkg_locator) + + // Build scripts + shop.build_package_scripts(pkg_locator) + + // Build C code + try { + build.build_dynamic(pkg_locator, target_triple, 'release') + } catch (e) { + // Not all packages have C code + } +} + +for (var p of packages_to_install) { + log.console(" Installing " + p + "...") + install_package(p) +} + +log.console("Installed " + text(packages_to_install.length) + " package(s).") $stop() diff --git a/internal/shop.cm b/internal/shop.cm index 52d24e2d..24a9cbdb 100644 --- a/internal/shop.cm +++ b/internal/shop.cm @@ -986,8 +986,15 @@ Shop.update = function(pkg) { log.console(`checking ${pkg}`) - if (info == 'local') return { - updated: time.number() + if (info == 'local') { + // Local packages always get a lock entry + var new_entry = { + type: 'local', + updated: time.number() + } + lock[pkg] = new_entry + Shop.save_lock(lock) + return new_entry } var local_commit = lock_entry ? lock_entry.commit : null diff --git a/list.ce b/list.ce index a12e038b..1190d177 100644 --- a/list.ce +++ b/list.ce @@ -1,85 +1,170 @@ -// list installed packages -// cell list -> list packages installed in this package -// cell list all -> list all packages (including those that are there due to installed packages) -// cell list package -> list the packages for the package +// cell list [] - List packages and dependencies +// +// Usage: +// cell list List dependencies of current package +// cell list shop List all packages in shop with status +// cell list List dependency tree for a package var shop = use('internal/shop') var pkg = use('package') +var link = use('link') +var fd = use('fd') var mode = 'local' var target_pkg = null if (args && args.length > 0) { - if (args[0] == 'all') { - mode = 'all' - } else if (args[0] == 'shop') { - mode = 'shop' - } else if (args[0] == 'package') { - if (args.length < 2) { - log.console("Usage: cell list package ") - $stop() - return - } - mode = 'package' - target_pkg = args[1] - } else { - log.console("Usage:") - log.console(" cell list : list local packages") - log.console(" cell list all : list all recursive packages") - log.console(" cell list package : list dependencies of ") - log.console(" cell list shop : list all packages in shop") - $stop() - return + if (args[0] == 'shop') { + mode = 'shop' + } else if (args[0] == '--help' || args[0] == '-h') { + log.console("Usage: cell list []") + log.console("") + log.console("List packages and dependencies.") + log.console("") + log.console("Scopes:") + log.console(" (none) List dependencies of current package") + log.console(" shop List all packages in shop with status") + log.console(" List dependency tree for a package") + $stop() + } else { + mode = 'package' + target_pkg = args[0] + + // Resolve local 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 + } } + } +} + +var links = link.load() +var lock = shop.load_lock() + +function print_deps(ctx, indent) { + indent = indent || "" + var deps + try { + deps = pkg.dependencies(ctx) + } catch (e) { + log.console(indent + " (could not read dependencies)") + return + } + + if (!deps) { + log.console(indent + " (none)") + return + } + + var aliases = [] + for (var k in deps) aliases.push(k) + aliases.sort() + + if (aliases.length == 0) { + log.console(indent + " (none)") + return + } + + for (var i = 0; i < aliases.length; i++) { + var alias = aliases[i] + var locator = deps[alias] + var link_target = links[locator] + var lock_entry = lock[locator] + + var line = indent + " " + alias + if (alias != locator) { + line += " -> " + locator + } + + // Add status indicators + var status = [] + if (link_target) { + status.push("linked -> " + link_target) + } + if (lock_entry && lock_entry.commit) { + status.push("@" + lock_entry.commit.substring(0, 8)) + } + if (lock_entry && lock_entry.type == 'local') { + status.push("local") + } + if (!lock_entry) { + status.push("not installed") + } + + if (status.length > 0) { + line += " [" + status.join(", ") + "]" + } + + log.console(line) + } } if (mode == 'local') { - log.console("Installed Packages (Local):") - print_deps(null) + log.console("Dependencies:") + print_deps(null) } else if (mode == 'package') { - // Resolve alias to canonical package path - var canon = shop.get_canonical_package(target_pkg, null) - if (!canon) { - log.console("Package '" + target_pkg + "' not found in local dependencies.") - } else { - log.console("Dependencies for " + target_pkg + " (" + canon + "):") - print_deps(canon) - } -} else if (mode == 'all') { - log.console("All Packages:") - var all = shop.list_packages(null) - // list_packages returns an array of package strings (locators) - // We want to perhaps sort them - all.sort() - for (var i = 0; i < all.length; i++) { - log.console(" " + all[i]) - } - if (all.length == 0) log.console(" (none)") + log.console("Dependencies for " + target_pkg + ":") + print_deps(target_pkg) } else if (mode == 'shop') { - log.console("Shop Packages:") - var all = shop.list_packages() - - if (all.length == 0) - log.console(" (none)") - else - all.forEach(package => log.console(" " + package)) -} + log.console("Shop packages:") + log.console("") -function print_deps(ctx) { - var deps = pkg.dependencies(ctx) - var aliases = [] - for (var k in deps) aliases.push(k) - aliases.sort() + var packages = shop.list_packages() + if (packages.length == 0) { + log.console(" (none)") + } else { + packages.sort() - if (aliases.length == 0) { - log.console(" (none)") - } else { - for (var i = 0; i < aliases.length; i++) { - var alias = aliases[i] - var locator = deps[alias] - log.console(" " + alias + " -> " + locator) - } + // Group by type + var local_pkgs = [] + var linked_pkgs = [] + var remote_pkgs = [] + + for (var p of packages) { + if (p == 'core') continue + var lock_entry = lock[p] + var link_target = links[p] + + if (link_target) { + linked_pkgs.push(p) + } else if (lock_entry && lock_entry.type == 'local') { + local_pkgs.push(p) + } else { + remote_pkgs.push(p) + } } + + if (linked_pkgs.length > 0) { + log.console("Linked packages:") + for (var p of linked_pkgs) { + var target = links[p] + log.console(" " + p + " -> " + target) + } + log.console("") + } + + if (local_pkgs.length > 0) { + log.console("Local packages:") + for (var p of local_pkgs) { + log.console(" " + p) + } + log.console("") + } + + if (remote_pkgs.length > 0) { + log.console("Remote packages:") + for (var p of remote_pkgs) { + var lock_entry = lock[p] + var commit = lock_entry && lock_entry.commit ? " @" + lock_entry.commit.substring(0, 8) : "" + log.console(" " + p + commit) + } + log.console("") + } + + log.console("Total: " + text(packages.length) + " package(s)") + } } $stop() diff --git a/remove.ce b/remove.ce index 47920119..cb951aef 100644 --- a/remove.ce +++ b/remove.ce @@ -1,24 +1,105 @@ -// cell remove - Remove a package from dependencies or shop +// cell remove - Remove a package from the shop +// +// Usage: +// cell remove Remove a package from the shop +// cell remove . Remove current directory package from shop +// +// Options: +// --prune Also remove packages no longer needed by any root +// --dry-run Show what would be removed var shop = use('internal/shop') +var pkg = use('package') +var link = use('link') var fd = use('fd') -if (args.length < 1) { - log.console("Usage: cell remove ") - $stop() - return -} +var target_pkg = null +var prune = false +var dry_run = false -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 +for (var i = 0; i < args.length; i++) { + if (args[i] == '--prune') { + prune = true + } else if (args[i] == '--dry-run') { + dry_run = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell remove [options]") + log.console("") + log.console("Remove a package from the shop.") + log.console("") + log.console("Options:") + log.console(" --prune Also remove packages no longer needed by any root") + log.console(" --dry-run Show what would be removed") + $stop() + } else if (!args[i].startsWith('-')) { + target_pkg = args[i] } } -shop.remove(pkg) +if (!target_pkg) { + log.console("Usage: cell remove [options]") + $stop() +} + +// 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 + } +} + +var packages_to_remove = [target_pkg] + +if (prune) { + // Find packages no longer needed + // Get all dependencies of remaining packages + var lock = shop.load_lock() + var all_packages = shop.list_packages() + + // Build set of all needed packages (excluding target) + var needed = {} + for (var p of all_packages) { + if (p == target_pkg || p == 'core') continue + + // Mark this package and its deps as needed + needed[p] = true + try { + var deps = pkg.gather_dependencies(p) + for (var dep of deps) { + needed[dep] = true + } + } catch (e) { + // Skip if can't read deps + } + } + + // Find packages that are NOT needed + for (var p of all_packages) { + if (p == 'core') continue + if (!needed[p] && packages_to_remove.indexOf(p) < 0) { + packages_to_remove.push(p) + } + } +} + +if (dry_run) { + log.console("Would remove:") + for (var p of packages_to_remove) { + log.console(" " + p) + } +} else { + for (var p of packages_to_remove) { + // Remove any link for this package + if (link.is_linked(p)) { + link.remove(p) + } + + // Remove from shop + shop.remove(p) + } + + log.console("Removed " + text(packages_to_remove.length) + " package(s).") +} $stop() diff --git a/resolve.ce b/resolve.ce new file mode 100644 index 00000000..08fa752c --- /dev/null +++ b/resolve.ce @@ -0,0 +1,201 @@ +// cell resolve [] - Print fully resolved dependency closure +// +// Usage: +// cell resolve Resolve current directory package +// cell resolve . Resolve current directory package +// cell resolve Resolve specific package +// +// Options: +// --target Annotate builds for target platform +// --locked Show lock state without applying links +// --refresh Refresh floating refs before printing + +var shop = use('internal/shop') +var pkg = use('package') +var link = use('link') +var build = use('build') +var fd = use('fd') + +var target_locator = null +var target_triple = null +var show_locked = false +var refresh_first = false + +for (var i = 0; i < args.length; i++) { + if (args[i] == '--target' || args[i] == '-t') { + if (i + 1 < args.length) { + target_triple = args[++i] + } else { + log.error('--target requires a triple') + $stop() + } + } else if (args[i] == '--locked') { + show_locked = true + } else if (args[i] == '--refresh') { + refresh_first = true + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell resolve [] [options]") + log.console("") + log.console("Print the fully resolved dependency closure.") + log.console("") + log.console("Options:") + log.console(" --target Annotate builds for target platform") + log.console(" --locked Show lock state without applying links") + log.console(" --refresh Refresh floating refs before printing") + $stop() + } else if (!args[i].startsWith('-')) { + target_locator = args[i] + } +} + +// Default to current directory +if (!target_locator) { + target_locator = '.' +} + +// Resolve local paths +if (target_locator == '.' || target_locator.startsWith('./') || target_locator.startsWith('../') || fd.is_dir(target_locator)) { + var resolved = fd.realpath(target_locator) + if (resolved) { + target_locator = resolved + } +} + +// Check if it's a valid package +if (!fd.is_file(target_locator + '/cell.toml')) { + // Try to find it in the shop + var pkg_dir = shop.get_package_dir(target_locator) + if (!fd.is_file(pkg_dir + '/cell.toml')) { + log.error("Not a valid package: " + target_locator) + $stop() + } +} + +// Detect target if not specified +if (!target_triple) { + target_triple = build.detect_host_target() +} + +var lock = shop.load_lock() +var links = link.load() + +// Gather all dependencies recursively +var all_deps = {} +var visited = {} + +function gather_deps(locator, depth) { + if (visited[locator]) return + visited[locator] = true + + all_deps[locator] = { depth: depth } + + try { + var deps = pkg.dependencies(locator) + if (deps) { + for (var alias in deps) { + var dep_locator = deps[alias] + gather_deps(dep_locator, depth + 1) + } + } + } catch (e) { + // Package might not have dependencies + } +} + +gather_deps(target_locator, 0) + +// Print header +log.console("Resolved dependency closure for: " + target_locator) +log.console("Target: " + target_triple) +log.console("") + +// Sort by depth then alphabetically +var sorted = [] +for (var locator in all_deps) { + sorted.push({ locator: locator, depth: all_deps[locator].depth }) +} +sorted.sort(function(a, b) { + if (a.depth != b.depth) return a.depth - b.depth + return a.locator < b.locator ? -1 : 1 +}) + +for (var i = 0; i < sorted.length; i++) { + var locator = sorted[i].locator + var depth = sorted[i].depth + + var indent = "" + for (var j = 0; j < depth; j++) indent += " " + + // Get info about this package + var info = shop.resolve_package_info(locator) + var lock_entry = lock[locator] + var link_target = show_locked ? null : links[locator] + var effective_locator = link_target || locator + + // Check status + var is_linked = link_target != null + var is_in_lock = lock_entry != null + var is_local = info == 'local' + + // Check if fetched (package directory exists) + var pkg_dir = shop.get_package_dir(locator) + var is_fetched = fd.is_dir(pkg_dir) || fd.is_link(pkg_dir) + + // Check if built (library exists) + var lib_dir = shop.get_lib_dir() + var lib_name = shop.lib_name_for_package(locator) + var dylib_ext = '.dylib' // TODO: detect from target + var lib_path = lib_dir + '/' + lib_name + dylib_ext + var is_built = fd.is_file(lib_path) + + // Format output + var status_parts = [] + if (is_linked) status_parts.push("linked") + if (is_local) status_parts.push("local") + if (!is_in_lock) status_parts.push("not in lock") + if (!is_fetched) status_parts.push("not fetched") + if (is_built) status_parts.push("built") + + var commit_str = "" + if (lock_entry && lock_entry.commit) { + commit_str = " @" + lock_entry.commit.substring(0, 8) + } else if (lock_entry && lock_entry.type == 'local') { + commit_str = " (local)" + } + + var line = indent + locator + commit_str + + if (is_linked && !show_locked) { + line += " -> " + link_target + } + + if (status_parts.length > 0) { + line += " [" + status_parts.join(", ") + "]" + } + + log.console(line) + + // Show compilation inputs if requested (verbose) + if (depth == 0) { + try { + var cflags = pkg.get_flags(locator, 'CFLAGS', target_triple) + var ldflags = pkg.get_flags(locator, 'LDFLAGS', target_triple) + if (cflags.length > 0 || ldflags.length > 0) { + log.console(indent + " Compilation inputs:") + if (cflags.length > 0) { + log.console(indent + " CFLAGS: " + cflags.join(' ')) + } + if (ldflags.length > 0) { + log.console(indent + " LDFLAGS: " + ldflags.join(' ')) + } + } + } catch (e) { + // Skip if can't read config + } + } +} + +log.console("") +log.console("Total: " + text(sorted.length) + " package(s)") + +$stop() diff --git a/update.ce b/update.ce index 0fc2c990..6d41da21 100644 --- a/update.ce +++ b/update.ce @@ -1,30 +1,47 @@ -// cell update - Update packages from remote sources -// -// This command checks for updates to all packages and downloads new versions. -// For local packages, ensures the symlink is correct. -// For remote packages, checks the remote for new commits. +// cell update [] - Update packages from remote sources // // Usage: -// cell update - Update all packages -// cell update - Update a specific package +// cell update Update all packages in shop +// cell update . Update current directory package +// cell update Update a specific package +// +// Options: +// --build Run build after updating +// --target Target platform for build (requires --build) +// --follow-links Update link targets instead of origins var shop = use('internal/shop') +var build = use('build') var fd = use('fd') var target_pkg = null +var run_build = false +var target_triple = null +var follow_links = false // Parse arguments for (var i = 0; i < args.length; i++) { if (args[i] == '--help' || args[i] == '-h') { - log.console("Usage: cell update [package]") + log.console("Usage: cell update [] [options]") + log.console("") log.console("Update packages from remote sources.") log.console("") - log.console("Arguments:") - log.console(" package Optional package name to update. If omitted, updates all.") - log.console("") - log.console("This command checks for updates to all packages and downloads") - log.console("new versions. For local packages, ensures the symlink is correct.") + log.console("Options:") + log.console(" --build Run build after updating") + log.console(" --target Target platform for build (requires --build)") + log.console(" --follow-links Update link targets instead of origins") $stop() + } else if (args[i] == '--build') { + run_build = true + } else if (args[i] == '--target' || args[i] == '-t') { + if (i + 1 < args.length) { + target_triple = args[++i] + } else { + log.error('--target requires a triple') + $stop() + } + } else if (args[i] == '--follow-links') { + follow_links = true } else if (!args[i].startsWith('-')) { target_pkg = args[i] // Resolve relative paths to absolute paths @@ -37,56 +54,94 @@ for (var i = 0; i < args.length; i++) { } } +// Default target if building +if (run_build && !target_triple) { + target_triple = build.detect_host_target() +} + +var link = use('link') + function update_and_fetch(pkg) { var lock = shop.load_lock() var old_entry = lock[pkg] var old_commit = old_entry ? old_entry.commit : null - - var new_entry = shop.update(pkg) - + + // Handle follow-links option + var effective_pkg = pkg + if (follow_links) { + var link_target = link.get_target(pkg) + if (link_target) { + effective_pkg = link_target + log.console(" Following link: " + pkg + " -> " + effective_pkg) + } + } + + var new_entry = shop.update(effective_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) + log.console(" " + effective_pkg + " " + old_str + " -> " + new_entry.commit.substring(0, 8)) + shop.fetch(effective_pkg) } else { // Local package - just ensure symlink is correct - log.console(" " + pkg + " (local)") + log.console(" " + effective_pkg + " (local)") } - shop.extract(pkg) - shop.build_package_scripts(pkg) - return true + shop.extract(effective_pkg) + shop.build_package_scripts(effective_pkg) + return effective_pkg } - return false + return null } +var updated_packages = [] + if (target_pkg) { - if (update_and_fetch(target_pkg)) + var updated = update_and_fetch(target_pkg) + if (updated) { + updated_packages.push(updated) log.console("Updated " + target_pkg + ".") - else + } else { log.console(target_pkg + " is up to date.") + } } else { var packages = shop.list_packages() var pkg_count = packages.length log.console("Checking for updates (" + text(pkg_count) + " package" + (pkg_count == 1 ? "" : "s") + ")...") - - var updated_count = 0 - + for (var i = 0; i < packages.length; i++) { var pkg = packages[i] if (pkg == 'core') continue - - if (update_and_fetch(pkg)) { - updated_count++ + + var updated = update_and_fetch(pkg) + if (updated) { + updated_packages.push(updated) } } - - if (updated_count > 0) { - log.console("Updated " + text(updated_count) + " package" + (updated_count == 1 ? "" : "s") + ".") + + if (updated_packages.length > 0) { + log.console("Updated " + text(updated_packages.length) + " package" + (updated_packages.length == 1 ? "" : "s") + ".") } else { log.console("All packages are up to date.") } } +// Run build if requested +if (run_build && updated_packages.length > 0) { + log.console("") + log.console("Building updated packages...") + + for (var pkg of updated_packages) { + try { + var lib = build.build_dynamic(pkg, target_triple, 'release') + if (lib) { + log.console(" Built: " + lib) + } + } catch (e) { + log.error(" Failed to build " + pkg + ": " + e) + } + } +} + $stop() diff --git a/verify.ce b/verify.ce new file mode 100644 index 00000000..b93dced2 --- /dev/null +++ b/verify.ce @@ -0,0 +1,257 @@ +// cell verify [] - Verify integrity and consistency +// +// Usage: +// cell verify Verify current directory package +// cell verify . Verify current directory package +// cell verify Verify specific package +// cell verify shop Verify entire shop +// cell verify world Verify all world roots +// +// Options: +// --deep Traverse full dependency closure +// --target Verify builds for specific target + +var shop = use('internal/shop') +var pkg = use('package') +var link = use('link') +var build = use('build') +var fd = use('fd') + +var scope = null +var deep = false +var target_triple = null + +for (var i = 0; i < args.length; i++) { + if (args[i] == '--deep') { + deep = true + } else if (args[i] == '--target' || args[i] == '-t') { + if (i + 1 < args.length) { + target_triple = args[++i] + } else { + log.error('--target requires a triple') + $stop() + } + } else if (args[i] == '--help' || args[i] == '-h') { + log.console("Usage: cell verify [] [options]") + log.console("") + log.console("Verify integrity and consistency.") + log.console("") + log.console("Scopes:") + log.console(" Verify specific package") + log.console(" shop Verify entire shop") + log.console(" world Verify all world roots") + log.console("") + log.console("Options:") + log.console(" --deep Traverse full dependency closure") + log.console(" --target Verify builds for specific target") + $stop() + } else if (!args[i].startsWith('-')) { + scope = args[i] + } +} + +// Default to current directory +if (!scope) { + scope = '.' +} + +// Detect target if not specified +if (!target_triple) { + target_triple = build.detect_host_target() +} + +var errors = [] +var warnings = [] +var checked = 0 + +function add_error(msg) { + errors.push(msg) +} + +function add_warning(msg) { + warnings.push(msg) +} + +// Verify a single package +function verify_package(locator) { + checked++ + + var lock = shop.load_lock() + var lock_entry = lock[locator] + var links = link.load() + var link_target = links[locator] + + // Check lock entry exists + if (!lock_entry) { + add_error(locator + ": not in lock") + } + + // Check package directory exists + var pkg_dir = shop.get_package_dir(locator) + var dir_exists = fd.is_dir(pkg_dir) || fd.is_link(pkg_dir) + + if (!dir_exists) { + add_error(locator + ": package directory missing at " + pkg_dir) + return + } + + // Check cell.toml exists + if (!fd.is_file(pkg_dir + '/cell.toml')) { + add_error(locator + ": missing cell.toml") + return + } + + // For linked packages, verify link target + if (link_target) { + if (link_target.startsWith('/')) { + // Local path target + if (!fd.is_dir(link_target)) { + add_error(locator + ": link target does not exist: " + link_target) + } else if (!fd.is_file(link_target + '/cell.toml')) { + add_error(locator + ": link target is not a valid package: " + link_target) + } + } else { + // Package target + var target_dir = shop.get_package_dir(link_target) + if (!fd.is_dir(target_dir) && !fd.is_link(target_dir)) { + add_error(locator + ": link target package not found: " + link_target) + } + } + + // Check symlink is correct + if (fd.is_link(pkg_dir)) { + var current_target = fd.readlink(pkg_dir) + var expected_target = link_target.startsWith('/') ? link_target : shop.get_package_dir(link_target) + if (current_target != expected_target) { + add_warning(locator + ": symlink target mismatch (expected " + expected_target + ", got " + current_target + ")") + } + } else { + add_warning(locator + ": linked but directory is not a symlink") + } + } + + // Check build output exists + var lib_dir = shop.get_lib_dir() + var lib_name = shop.lib_name_for_package(locator) + var dylib_ext = '.dylib' // TODO: detect from target + var lib_path = lib_dir + '/' + lib_name + dylib_ext + + // Only check for builds if package has C files + try { + var c_files = pkg.get_c_files(locator, target_triple, true) + if (c_files && c_files.length > 0) { + if (!fd.is_file(lib_path)) { + add_warning(locator + ": library not built at " + lib_path) + } + } + } catch (e) { + // Skip build check if can't determine C files + } +} + +// Check for link cycles +function check_link_cycles() { + var links = link.load() + + function follow_chain(origin, visited) { + if (visited[origin]) { + return origin // cycle detected + } + visited[origin] = true + + var target = links[origin] + if (target && links[target]) { + return follow_chain(target, visited) + } + return null + } + + for (var origin in links) { + var cycle_start = follow_chain(origin, {}) + if (cycle_start) { + add_error("Link cycle detected starting from: " + origin) + } + } +} + +// Check for dangling links +function check_dangling_links() { + var links = link.load() + + for (var origin in links) { + var target = links[origin] + if (target.startsWith('/')) { + if (!fd.is_dir(target)) { + add_error("Dangling link: " + origin + " -> " + target + " (target does not exist)") + } + } + } +} + +// Gather packages to verify +var packages_to_verify = [] + +if (scope == 'shop') { + packages_to_verify = shop.list_packages() +} else if (scope == 'world') { + // For now, world is the same as shop + // In future, this could be a separate concept + packages_to_verify = shop.list_packages() +} else { + // Single package + var locator = scope + + // Resolve local paths + if (locator == '.' || locator.startsWith('./') || locator.startsWith('../') || fd.is_dir(locator)) { + var resolved = fd.realpath(locator) + if (resolved) { + locator = resolved + } + } + + if (deep) { + // Gather all dependencies + var all_deps = pkg.gather_dependencies(locator) + packages_to_verify.push(locator) + for (var dep of all_deps) { + packages_to_verify.push(dep) + } + } else { + packages_to_verify.push(locator) + } +} + +log.console("Verifying " + text(packages_to_verify.length) + " package(s)...") +log.console("") + +// Run verification +check_link_cycles() +check_dangling_links() + +for (var p of packages_to_verify) { + if (p == 'core') continue + verify_package(p) +} + +// Print results +if (warnings.length > 0) { + log.console("Warnings:") + for (var w of warnings) { + log.console(" " + w) + } + log.console("") +} + +if (errors.length > 0) { + log.console("Errors:") + for (var e of errors) { + log.console(" " + e) + } + log.console("") + log.console("Verification FAILED: " + text(errors.length) + " error(s), " + text(warnings.length) + " warning(s)") + // Note: would use process.exit(1) if available +} else { + log.console("Verification PASSED: " + text(checked) + " package(s) checked, " + text(warnings.length) + " warning(s)") +} + +$stop()