Files
cell/link.cm
2026-01-21 00:52:18 -06:00

293 lines
7.9 KiB
Plaintext

// 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 && starts_with(pkg, '/'))
return replace(replace(pkg, '/', '_'), '@', '_')
return replace(pkg, '@', '_')
}
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 = array(path, '/')
var current = starts_with(path, '/') ? '/' : ''
for (var i = 0; i < length(parts); 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 (starts_with(target, '/')) {
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 (starts_with(target, '/')) {
// 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 = starts_with(target, '/') ? 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) {
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.console(" Skipping missing local dependency: " + dep_locator)
return
}
// 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()
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.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 = fd.dirname(target_dir)
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]
try {
// 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'
try {
var content = text(fd.slurp(toml_path))
var cfg = toml.decode(content)
if (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
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) {
push(errors, 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()
var found = null
arrfor(array(links), function(origin) {
if (links[origin] == target) found = origin
})
return found
}
return Link