// Link management module for cell packages // Handles creating, removing, and syncing symlinks for local development var toml = use('toml') var fd = use('fd') var blob = use('blob') var runtime = use('runtime') var global_shop_path = runtime.shop_path // Get the links file path (in the global shop) function get_links_path() { if (!global_shop_path) return null return global_shop_path + '/link.toml' } // Get the packages directory (in the global shop) function get_packages_dir() { if (!global_shop_path) return null return global_shop_path + '/packages' } function get_package_abs_dir(package) { return get_packages_dir() + '/' + fd.safe_package_path(package) } // Resolve a link target to its actual path // If target is a local path (starts with /), return it directly // If target is a package name, return the package directory function resolve_link_target(target) { if (starts_with(target, '/')) { return target } // Target is another package - resolve to its directory return get_packages_dir() + '/' + fd.safe_package_path(target) } var Link = {} var link_cache = null Link.load = function() { if (link_cache) return link_cache var path = get_links_path() if (!path || !fd.is_file(path)) { link_cache = {} return link_cache } var _load = function() { var content = text(fd.slurp(path)) var cfg = toml.decode(content) if (cfg && cfg.links) link_cache = cfg.links else link_cache = {} } disruption { log.build("Warning: Failed to load link.toml") link_cache = {} } _load() return link_cache } Link.save = function(links) { link_cache = links var cfg = { links: links } var path = get_links_path() var b = blob(toml.encode(cfg)) stone(b) fd.slurpwrite(path, b) } Link.add = function(canonical, target, shop) { // Validate canonical package exists in shop var lock = shop.load_lock() if (!lock[canonical]) { log.error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical) disrupt } // Validate target is a valid package if (starts_with(target, '/')) { // Local path - must have cell.toml if (!fd.is_file(target + '/cell.toml')) { log.error('Target ' + target + ' is not a valid package (no cell.toml)') disrupt } } else { // Remote package target - ensure it's installed shop.sync(target) } var links = Link.load() links[canonical] = target Link.save(links) // Create the symlink immediately Link.sync_one(canonical, target, shop) // Install dependencies of the linked package // Read the target's cell.toml to find its dependencies var target_path = starts_with(target, '/') ? target : get_package_abs_dir(target) var toml_path = target_path + '/cell.toml' var _install_deps = null if (fd.is_file(toml_path)) { _install_deps = function() { var content = text(fd.slurp(toml_path)) var cfg = toml.decode(content) if (cfg && cfg.dependencies) { arrfor(array(cfg.dependencies), function(alias) { var dep_locator = cfg.dependencies[alias] // Skip local dependencies that don't exist if (starts_with(dep_locator, '/') && !fd.is_dir(dep_locator)) { log.build(" Skipping missing local dependency: " + dep_locator) return } // Install the dependency if not already in shop var _get_dep = function() { shop.sync(dep_locator) } disruption { log.build(` Warning: Could not install dependency ${dep_locator}`) } _get_dep() }) } } disruption { log.build(` Warning: Could not read dependencies from ${toml_path}`) } _install_deps() } log.build("Linked " + canonical + " -> " + target) return true } Link.remove = function(canonical) { var links = Link.load() if (!links[canonical]) return false // Remove the symlink if it exists var target_dir = get_package_abs_dir(canonical) if (fd.is_link(target_dir)) { fd.unlink(target_dir) log.build("Removed symlink at " + target_dir) } delete links[canonical] Link.save(links) log.build("Unlinked " + canonical) return true } Link.clear = function() { // Remove all symlinks first var links = Link.load() arrfor(array(links), function(canonical) { var target_dir = get_package_abs_dir(canonical) if (fd.is_link(target_dir)) { fd.unlink(target_dir) } }) Link.save({}) log.build("Cleared all links") return true } // Sync a single link - ensure the symlink is in place Link.sync_one = function(canonical, target, shop) { var target_dir = get_package_abs_dir(canonical) var link_target = resolve_link_target(target) // Ensure parent directories exist var parent = fd.dirname(target_dir) fd.ensure_dir(parent) // Check current state var current_link = null if (fd.is_link(target_dir)) { current_link = fd.readlink(target_dir) } // If already correctly linked, nothing to do if (current_link == link_target) { return true } // Remove existing file/dir/link if (fd.is_link(target_dir)) { fd.unlink(target_dir) } else if (fd.is_dir(target_dir)) { fd.rmdir(target_dir, 1) } // Create symlink fd.symlink(link_target, target_dir) return true } // Sync all links - ensure all symlinks are in place and dependencies are installed Link.sync_all = function(shop) { var links = Link.load() var count = 0 var errors = [] arrfor(array(links), function(canonical) { var target = links[canonical] var _sync = function() { // Validate target exists var link_target = resolve_link_target(target) if (!fd.is_dir(link_target)) { push(errors, canonical + ': target ' + link_target + ' does not exist') return } if (!fd.is_file(link_target + '/cell.toml')) { push(errors, canonical + ': target ' + link_target + ' is not a valid package') return } Link.sync_one(canonical, target, shop) // Install dependencies of the linked package var toml_path = link_target + '/cell.toml' var _install = function() { var content = text(fd.slurp(toml_path)) var cfg = toml.decode(content) if (cfg && cfg.dependencies) { arrfor(array(cfg.dependencies), function(alias) { var dep_locator = cfg.dependencies[alias] // Skip local dependencies that don't exist if (starts_with(dep_locator, '/') && !fd.is_dir(dep_locator)) { return } // Install the dependency if not already in shop var _get = function() { shop.sync(dep_locator) } disruption { // Silently continue - dependency may already be installed } _get() }) } } disruption { // Could not read dependencies - continue anyway } _install() count = count + 1 } disruption { push(errors, canonical + ': sync failed') } _sync() }) return { synced: count, errors: errors } } // Check if a package is currently linked Link.is_linked = function(canonical) { var links = Link.load() return links[canonical] != null } // Get the link target for a package (or null if not linked) Link.get_target = function(canonical) { var links = Link.load() return links[canonical] || null } // Get the canonical package name that links to this target (reverse lookup) // Returns null if no package links to this target Link.get_origin = function(target) { var links = Link.load() var found = null arrfor(array(links), function(origin) { if (links[origin] == target) found = origin }) return found } return Link