// 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 os = use('os') var global_shop_path = os.global_shop_path // Get the links file path (in the global shop) function get_links_path() { return global_shop_path + '/link.toml' } // Get the packages directory (in the global shop) function get_packages_dir() { return global_shop_path + '/packages' } // 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() + '/' + safe_package_path(package) } function ensure_dir(path) { if (fd.stat(path).isDirectory) return var parts = path.split('/') var current = path.startsWith('/') ? '/' : '' for (var i = 0; i < parts.length; i++) { if (parts[i] == '') continue current += parts[i] + '/' if (!fd.stat(current).isDirectory) { fd.mkdir(current) } } } // 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 (target.startsWith('/')) { return target } // Target is another package - resolve to its directory return get_packages_dir() + '/' + 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 (!fd.is_file(path)) { link_cache = {} return link_cache } try { var content = text(fd.slurp(path)) var cfg = toml.decode(content) link_cache = cfg.links || {} } catch (e) { log.console("Warning: Failed to load link.toml: " + e) link_cache = {} } 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]) { throw Error('Package ' + canonical + ' is not installed. Install it first with: cell get ' + canonical) } // Validate target is a valid package if (target.startsWith('/')) { // Local path - must have cell.toml if (!fd.is_file(target + '/cell.toml')) { throw Error('Target ' + target + ' is not a valid package (no cell.toml)') } } else { // Remote package target - ensure it's installed shop.get(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 = target.startsWith('/') ? target : get_package_abs_dir(target) var toml_path = target_path + '/cell.toml' if (fd.is_file(toml_path)) { try { var content = text(fd.slurp(toml_path)) var cfg = toml.decode(content) if (cfg.dependencies) { for (var alias in cfg.dependencies) { var dep_locator = cfg.dependencies[alias] // Skip local dependencies that don't exist if (dep_locator.startsWith('/') && !fd.is_dir(dep_locator)) { log.console(" Skipping missing local dependency: " + dep_locator) continue } // Install the dependency if not already in shop try { shop.get(dep_locator) shop.extract(dep_locator) } catch (e) { log.console(` Warning: Could not install dependency ${dep_locator}: ${e.message}`) log.error(e) } } } } catch (e) { log.console(` Warning: Could not read dependencies from ${toml_path}`) } } log.console("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.console("Removed symlink at " + target_dir) } delete links[canonical] Link.save(links) log.console("Unlinked " + canonical) return true } Link.clear = function() { // Remove all symlinks first var links = Link.load() for (var canonical in links) { var target_dir = get_package_abs_dir(canonical) if (fd.is_link(target_dir)) { fd.unlink(target_dir) } } Link.save({}) log.console("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 = target_dir.substring(0, target_dir.lastIndexOf('/')) 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 = [] for (var canonical in links) { var target = links[canonical] try { // Validate target exists var link_target = resolve_link_target(target) if (!fd.is_dir(link_target)) { errors.push(canonical + ': target ' + link_target + ' does not exist') continue } if (!fd.is_file(link_target + '/cell.toml')) { errors.push(canonical + ': target ' + link_target + ' is not a valid package') continue } Link.sync_one(canonical, target, shop) // Install dependencies of the linked package var toml_path = link_target + '/cell.toml' try { var content = text(fd.slurp(toml_path)) var cfg = toml.decode(content) if (cfg.dependencies) { for (var alias in cfg.dependencies) { var dep_locator = cfg.dependencies[alias] // Skip local dependencies that don't exist if (dep_locator.startsWith('/') && !fd.is_dir(dep_locator)) { continue } // Install the dependency if not already in shop try { shop.get(dep_locator) shop.extract(dep_locator) } catch (e) { // Silently continue - dependency may already be installed } } } } catch (e) { // Could not read dependencies - continue anyway } count++ } catch (e) { errors.push(canonical + ': ' + e.message) } } return { synced: count, errors: errors } } // Check if a package is currently linked Link.is_linked = function(canonical) { var links = Link.load() return canonical in links } // 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() for (var origin in links) { if (links[origin] == target) { return origin } } return null } return Link