var toml = use('toml') var json = use('json') var fd = use('fd') var http = use('http') var miniz = use('miniz') var time = use('time') var js = use('js') var crypto = use('crypto') var utf8 = use('utf8') var qop var core_qop // a package string is what is used to import a module, like prosperon/sprite // in prosperon/sprite, sprite is the module, and prosperon is the package (usually, an alias) // a canonical package name relates prosperon to its source, like gitea.pockle.world/john/prosperon var Shop = {} var SCOPE_LOCAL = 0 var SCOPE_PACKAGE = 1 var SCOPE_CORE = 2 var MOD_EXT = '.cm' var ACTOR_EXT = '.ce' var dylib_ext = '.so' // Default extension var os var use_cache var platform Shop.set_os = function(o) { os = o qop = os.load_internal('js_qop_use') core_qop = os.core_qop use_cache = os.use_cache platform = os.platform() if (platform == 'macOS') dylib_ext = '.dylib' else if (platform == 'Windows') dylib_ext = '.dll' } var config = null var shop_path = '.cell/cell.toml' var open_dl = {} function get_import_package(name) { var parts = name.split('/') if (parts.length > 1) return parts[0] return null } function get_import_name(path) { var parts = path.split('/') if (parts.length < 2) return null return parts.slice(1).join('/') } // given a path, get a full package import // ie, 'prosperon/sprite' would return 'gitea.pockle.world/john/prosperon/sprite' // if prosperon were a dependency function get_path_in_package(path, ctx) { var pkg = get_import_package(path) var mod_name = get_import_name(path) if (!pkg) return null var canon_pkg = get_canonical_package(pkg, ctx) return canon_pkg + "/" + mod_name } function get_normalized_package(path, ctx) { var pkg = get_import_package(path) if (!pkg) return null return get_canonical_package(pkg, ctx) } // taking the package into account, find the canonical name function get_canonical_package(mod, ctx) { var cfg = Shop.load_config(ctx) if (!cfg || !cfg.dependencies) return null var pkg = cfg.dependencies[mod] if (!pkg) return null var parsed = Shop.parse_package(pkg) if (!parsed) return null return parsed.path } function get_import_dl(name) { var pkg = get_import_package(name) if (!pkg) return null if (open_dl[pkg]) return open_dl[pkg] var dlpath = `.cell/modules/${pkg}/${pkg}${dylib_ext}` var dl = os.dylib_open(dlpath) if (dl) { open_dl[pkg] = dl return dl } return null } Shop.get_c_symbol = function get_c_symbol(name) { var dl = get_import_dl(name) var symname = `js_${name.replace('/', '_')}_use` if (dl) return os.dylib_symbol(dl, symname) else return os.load_internal(symname) } function ensure_dir(path) { if (fd.stat(path).isDirectory) return true var parts = path.split('/') var current = '' for (var i = 0; i < parts.length; i++) { if (parts[i] == '') continue current += parts[i] + '/' if (!fd.stat(current).isDirectory) { fd.mkdir(current) } } return true } // Load cell.toml configuration // module given in canonical format (e.g., "gitea.pockle.world/john/prosperon") // If module is null, loads the root cell.toml // If module is provided, loads module/cell.toml Shop.load_config = function(module) { var content if (!module) { if (!fd.is_file(shop_path)) return null content = fd.slurp(shop_path) } else { var module_path = `.cell/modules/${module}/.cell/cell.toml` if (!fd.stat(module_path).isFile) return null content = fd.slurp(module_path) } if (!content.length) return {} var cfg = toml.decode(text(content)) if (cfg.dependencies) { var changed = false for (var k in cfg.dependencies) { if (cfg.dependencies[k].startsWith('https://')) { cfg.dependencies[k] = cfg.dependencies[k].substring(8) changed = true } else if (cfg.dependencies[k].includes('://')) { // If it has another protocol, we should probably strip it too or warn // But for now assuming mostly https/http var parts = cfg.dependencies[k].split('://') if (parts.length == 2) { cfg.dependencies[k] = parts[1] changed = true } } } // If we modified dependencies and this is the root config, save it back // But load_config is also called for modules (which we can't write to easily/shouldn't) // So we only save if module is null (root config) if (changed && !module) { Shop.save_config(cfg) } } return cfg } // Save cell.toml configuration Shop.save_config = function(config) { fd.slurpwrite(shop_path, utf8.encode(toml.encode(config))); } // Load lock.toml configuration Shop.load_lock = function() { var path = '.cell/lock.toml' if (!fd.is_file(path)) return {} var content = text(fd.slurp(path)) if (!content.length) return {} var lock = toml.decode(content) var changed = false // Clean lock file entries for (var key in lock) { var entry = lock[key] if (entry && entry.package && entry.package.includes('://')) { var parts = entry.package.split('://') entry.package = parts[1] changed = true } // Also clean keys if they are locators/packages with protocols if (key.includes('://')) { var parts = key.split('://') var new_key = parts[1] lock[new_key] = entry delete lock[key] changed = true } } if (changed) { Shop.save_lock(lock) } return lock } // Save lock.toml configuration Shop.save_lock = function(lock) { fd.slurpwrite('.cell/lock.toml', utf8.encode(toml.encode(lock))); } // Parse module package string (e.g., "git.world/jj/mod@v0.6.3") Shop.parse_package = function(pkg) { Shop.verify_package_name(pkg) var path = pkg var version = null // Extract version if present if (path.includes('@')) { var versionParts = path.split('@') path = versionParts[0] version = versionParts[1] } // Extract name (last part of path) var name = path.split('/').pop() return { path, name, version } } // Verify if a package name is valid and return status Shop.verify_package_name = function(pkg) { if (!pkg) throw new Error("Empty package name") if (pkg == 'local') throw new Error("local is not a valid package name") if (pkg == 'core') throw new Error("core is not a valid package name") if (pkg.includes('://')) throw new Error(`Invalid package name: ${pkg}; did you mean ${pkg.split('://')[1]}?`) } // Convert module package to download URL Shop.get_download_url = function(pkg, commit_hash) { var parsed = Shop.parse_package(pkg) if (!parsed) return null if (parsed.path.includes('gitea.')) { var parts = parsed.path.split('/') var host = parts[0] var user = parts[1] var repo = parts[2] if (!commit_hash) { log.error("No commit hash available for download URL") return null } return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip' } return null } // Remove a dependency Shop.remove_dependency = function(alias) { var config = Shop.load_config() if (!config) { log.error("No cell.toml found") return false } if (!config.dependencies || !config.dependencies[alias]) { return false } delete config.dependencies[alias] Shop.save_config(config) return true } // Get the API URL for checking remote git commits Shop.get_api_url = function(pkg) { var parsed = Shop.parse_package(pkg) if (!parsed) return null var parts = parsed.path.split('/') // Gitea pattern: gitea.pockle.world/user/repo@branch if (parsed.path.includes('gitea.')) { var host = parts[0] var user = parts[1] var repo = parts[2] var url = 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/' if (parsed.version) url += parsed.version return url } return null } // Extract commit hash from API response Shop.extract_commit_hash = function(pkg, response) { if (!response) return null var data = json.decode(response) if (pkg.includes('gitea.')) { // Gitea: response.commit.id if (Array.isArray(data)) data = data[0] return data.commit && data.commit.id } return null } // Get the module directory for a given alias Shop.get_module_dir = function(alias) { var config = Shop.load_config() if (!config || !config.dependencies || !config.dependencies[alias]) { return null } var pkg = config.dependencies[alias] var parsed = Shop.parse_package(pkg) if (!parsed) return null return '.cell/modules/' + parsed.path } // Install a dependency Shop.install = function(alias) { var config = Shop.load_config() if (!config || !config.dependencies || !config.dependencies[alias]) { log.error("Dependency not found in config: " + alias) return false } var pkg = config.dependencies[alias] var parsed = Shop.parse_package(pkg) var target_dir = '.cell/modules/' + parsed.path log.console("Installing " + alias + " (" + pkg + ")...") // 1. Get Commit Hash var api_url = Shop.get_api_url(pkg) var commit_hash = null if (api_url) { try { log.console("Fetching info from " + api_url) var resp = http.fetch(api_url) var resp_text = text(resp) commit_hash = Shop.extract_commit_hash(pkg, resp_text) log.console("Resolved commit: " + commit_hash) } catch (e) { log.console("Warning: Failed to fetch API info: " + e) } } // 2. Download Zip var download_url = Shop.get_download_url(pkg) if (!download_url) { log.error("Could not determine download URL for " + pkg) return false } log.console("Downloading from " + download_url) var zip_blob try { zip_blob = http.fetch(download_url) } catch (e) { log.error("Download failed: " + e) return false } // 3. Unpack log.console("Unpacking to " + target_dir) ensure_dir(target_dir) var zip = miniz.read(zip_blob) if (!zip) { log.error("Failed to read zip archive") return false } var count = zip.count() for (var i = 0; i < count; i++) { if (zip.is_dir(i)) continue var filename = zip.get_filename(i) // Strip top-level directory var parts = filename.split('/') if (parts.length > 1) { parts.shift() // Remove root folder var rel_path = parts.join('/') var full_path = target_dir + '/' + rel_path var dir_path = full_path.substring(0, full_path.lastIndexOf('/')) ensure_dir(dir_path) var content = zip.slurp(filename) fd.slurpwrite(full_path, content) } } // 4. Update Lock (only for root package) log.console("Installed " + alias) return { commit: commit_hash, package: pkg } } function lock_package(loc) { var lock = Shop.load_lock() } Shop.check_cache = function(pkg) { var parsed = Shop.parse_package(pkg) if (!parsed) return null var cache_path = `.cell/cache/${parsed.path}.zip` if (fd.is_file(cache_path)) { log.console("Found cached zip: " + cache_path) return true } return false } // Verify dependencies Shop.verify = function(pkg) { // each package should be a package } var open_dls = {} // for script forms, path is the canonical path of the module var script_forms = [] script_forms['.cm'] = function(path, script, pkg) { var pkg_arg = pkg ? `'${pkg}'` : 'null' var fn = `(function setup_module($_){ var use = function(path) { return globalThis.use(path, ${pkg_arg}); }; ${script}})` return fn } script_forms['.ce'] = function(path, script, pkg) { return `(function start($_, arg) { var args = arg; ${script} ; })` } // Get flags from config function get_flags(config, platform, key) { var flags = '' if (config.compilation && config.compilation[key]) { flags += config.compilation[key] } if (config.compilation && config.compilation[platform] && config.compilation[platform][key]) { if (flags != '') flags += ' ' flags += config.compilation[platform][key] } return flags } Shop.get_flags = get_flags function get_build_dir(pkg = 'local') { return '.cell/build/' + pkg } Shop.get_build_dir = get_build_dir function get_rel_path(path, pkg) { if (!pkg) return path var prefix = '.cell/modules/' + pkg + '/' if (path.startsWith(prefix)) { return path.substring(prefix.length) } return path } function resolve_mod_fn(path, pkg) { if (!fd.is_file(path)) throw new Error(`path ${path} is not a file`) var rel_path = get_rel_path(path, pkg) var build_dir = get_build_dir(pkg) var cache_path = build_dir + '/' + rel_path + '.o' if (fd.is_file(cache_path) && fd.stat(path).mtime <= fd.stat(cache_path).mtime) { var obj = fd.slurp(cache_path) var fn = js.compile_unblob(obj) return js.eval_compile(fn) } var ext = path.substring(path.lastIndexOf('.')) var script_form = script_forms[ext] if (!script_form) throw new Error(`No script form for extension ${ext}`) var script = script_form(path, text(fd.slurp(path)), pkg) var fn = js.compile(path, script) ensure_dir(cache_path.substring(0, cache_path.lastIndexOf('/'))) fd.slurpwrite(cache_path, js.compile_blob(fn)) return js.eval_compile(fn) } function resolve_locator(path, ext, ctx) { var local_path if (ctx) local_path = `.cell/modules/${ctx}/${path}${ext}` else local_path = path + ext if (fd.is_file(local_path)) { var fn = resolve_mod_fn(local_path, ctx) return {path: local_path, scope: SCOPE_LOCAL, symbol:fn} } var canonical_pkg = get_normalized_package(path, ctx) var pkg_path = get_path_in_package(path, ctx) var mod_path = `.cell/modules/${pkg_path}${ext}` if (fd.is_file(mod_path)) { var fn = resolve_mod_fn(mod_path, canonical_pkg) return {path: mod_path, scope: SCOPE_PACKAGE, symbol:fn} } var core = core_qop.read(path + ext) if (core) { var form = script_forms[ext] if (!form) throw new Error(`No script form for extension ${ext}`) var script = form(null,text(core)) var fn = js.compile(path, script) return {path, scope: SCOPE_CORE, symbol:js.eval_compile(fn)}; } return null; } function resolve_c_symbol(path, package_context) { var local_path = package_context ? package_context : 'local' var local_sym_base = path.replace(/\//g, '_').replace(/-/g, '_').replace(/\./g, '_') var local if (!package_context) { local = `js_local_${local_sym_base}_use` } else { local = `js_${local_path.replace(/\//g, '_').replace(/-/g, '_').replace(/\./g, '_')}_${local_sym_base}_use` } var build_dir = get_build_dir(package_context) var local_dl_name = build_dir + '/cellmod' + dylib_ext if (fd.is_file(local_dl_name)) { if (!open_dls[local_dl_name]) open_dls[local_dl_name] = os.dylib_open(local_dl_name); if (open_dls[local_dl_name]) { if (os.dylib_has_symbol(open_dls[local_dl_name], local)) return { symbol: function() { return os.dylib_symbol(open_dls[local_dl_name], local); }, scope: SCOPE_LOCAL }; } } // Try static linking fallback if (os.internal_exists(local)) return { symbol: function() { return os.load_internal(local); }, scope: SCOPE_LOCAL }; // If 'path' has a package alias (e.g. 'prosperon/sprite'), try to resolve it var pkg_alias = get_import_package(path) if (pkg_alias) { var canon_pkg = get_normalized_package(path, package_context) if (canon_pkg) { var build_dir = get_build_dir(canon_pkg) var dl_path = build_dir + '/cellmod' + dylib_ext var mod_name = get_import_name(path) var mod_sym = mod_name.replace(/\//g, '_').replace(/-/g, '_').replace(/\./g, '_') var pkg_safe = canon_pkg.replace(/\//g, '_').replace(/-/g, '_').replace(/\./g, '_') var sym_name = `js_${pkg_safe}_${mod_sym}_use` if (fd.is_file(dl_path)) { if (!open_dls[dl_path]) open_dls[dl_path] = os.dylib_open(dl_path) if (open_dls[dl_path]) { if (os.dylib_has_symbol(open_dls[dl_path], sym_name)) return { symbol: function() { return os.dylib_symbol(open_dls[dl_path], sym_name) }, scope: SCOPE_PACKAGE, package: canon_pkg } } } if (os.internal_exists(sym_name)) return { symbol: function() { return os.load_internal(sym_name) }, scope: SCOPE_PACKAGE, package: canon_pkg }; } } var core_sym = `js_${path.replace(/\//g, '_')}_use`; if (os.internal_exists(core_sym)) return { symbol: function() { return os.load_internal(core_sym); }, scope: SCOPE_CORE }; return null } // first looks in local // then in dependencies // then in core // package_context: optional package context to resolve relative paths within Shop.use = function(path, package_context) { var c_resolve = resolve_c_symbol(path, package_context) || {scope:999} var mod_resolve = resolve_locator(path, '.cm', package_context) || {scope:999} var min_scope = Math.min(c_resolve.scope, mod_resolve.scope) if (min_scope == 999) throw new Error(`Module ${path} could not be found in ${package_context}`) var cache_key = `${text(min_scope)}::${path}` if (use_cache[cache_key]) return use_cache[cache_key] if (c_resolve.scope < mod_resolve.scope) use_cache[cache_key] = c_resolve.symbol() else if (mod_resolve.scope < c_resolve.scope) use_cache[cache_key] = mod_resolve.symbol.call() else use_cache[cache_key] = mod_resolve.symbol.call(c_resolve.symbol()) return use_cache[cache_key] } Shop.resolve_locator = resolve_locator // Get cache path for a package and commit function get_cache_path(pkg, commit) { var parsed = Shop.parse_package(pkg) if (!parsed) return null var slug = parsed.path.split('/').join('_') return `.cell/cache/${slug}_${commit}.zip` } function rm_recursive(path) { try { fd.rm(path) } catch (e) { log.error("Failed to remove " + path + ": " + e) } } function get_all_files(dir, prefix, results) { prefix = prefix || "" results = results || [] var list = fd.readdir(dir) if (!list) return results for (var i = 0; i < list.length; i++) { var item = list[i] if (item == '.' || item == '..') continue var full_path = dir + "/" + item var rel_path = prefix ? prefix + "/" + item : item var st = fd.stat(full_path) if (st.isDirectory) { get_all_files(full_path, rel_path, results) } else { results.push(rel_path) } } return results } // Verify zip contents against target directory function verify_zip_contents(zip, target_dir) { var count = zip.count() var expected_files = {} 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) { parts.shift() var rel_path = parts.join('/') expected_files[rel_path] = true var full_path = target_dir + '/' + rel_path if (!fd.is_file(full_path)) return false var content_zip = zip.slurp(filename) var content_disk = fd.slurp(full_path) if (content_zip.length != content_disk.length) return false var hash_zip = text(crypto.blake2(content_zip), 'h') var hash_disk = text(crypto.blake2(content_disk), 'h') if (hash_zip != hash_disk) return false } } // Check for extra files var existing_files = get_all_files(target_dir) for (var i = 0; i < existing_files.length; i++) if (!expected_files[existing_files[i]]) return false return true } // Install from a raw package (not from config) function install_from_package(pkg, locked_hash, expected_zip_hash) { var parsed = Shop.parse_package(pkg) var target_dir = '.cell/modules/' + parsed.path // 1. Get Commit Hash - use locked hash if provided, otherwise fetch var commit_hash = locked_hash if (!commit_hash) { var api_url = Shop.get_api_url(pkg) if (api_url) { try { var resp = http.fetch(api_url) var resp_text = text(resp) commit_hash = Shop.extract_commit_hash(pkg, resp_text) } catch (e) { log.console("Warning: Failed to fetch API info: " + e) } } } else { log.console("Using locked commit: " + commit_hash) } if (!commit_hash) { log.error("Could not determine commit hash for " + pkg) return null } // 2. Check Cache / Download Zip var cache_path = get_cache_path(pkg, commit_hash) var zip_blob = null var zip_hash = null var use_cache = false if (fd.is_file(cache_path)) { log.console("Found cached zip: " + cache_path) try { var cached = fd.slurp(cache_path) var computed_hash = text(crypto.blake2(cached), 'h') if (expected_zip_hash && computed_hash != expected_zip_hash) { log.console("Cache hash mismatch. Expected: " + expected_zip_hash + ", Got: " + computed_hash) log.console("Redownloading...") } else { zip_blob = cached zip_hash = computed_hash use_cache = true } } catch (e) { log.error("Failed to read cache: " + e) } } if (!use_cache) { var download_url = Shop.get_download_url(pkg, commit_hash) if (!download_url) { log.error("Could not determine download URL for " + pkg) return null } log.console("Downloading from " + download_url) try { zip_blob = http.fetch(download_url) zip_hash = text(crypto.blake2(zip_blob), 'h') // Save to cache ensure_dir(cache_path.substring(0, cache_path.lastIndexOf('/'))) fd.slurpwrite(cache_path, zip_blob) log.console("Cached to " + cache_path) } catch (e) { log.error(e) return null } } // 3. Verify and Unpack var zip = miniz.read(zip_blob) if (!zip) throw new Error("Failed to read zip archive") var needs_unpack = !use_cache // If using cache, verify existing installation strictly if (use_cache && fd.is_dir(target_dir)) { if (!verify_zip_contents(zip, target_dir)) { log.console("Verification failed for " + pkg + ". Reinstalling...") needs_unpack = true } } else if (use_cache && !fd.is_dir(target_dir)) { needs_unpack = true } if (needs_unpack) { if (fd.is_dir(target_dir)) { log.console("Clearing module directory for fresh install...") rm_recursive(target_dir) } log.console("Unpacking to " + target_dir) ensure_dir(target_dir) var count = zip.count() for (var i = 0; i < count; i++) { if (zip.is_directory(i)) continue var filename = zip.get_filename(i) log.console(filename) var parts = filename.split('/') if (parts.length > 1) { parts.shift() var rel_path = parts.join('/') var full_path = target_dir + '/' + rel_path var dir_path = full_path.substring(0, full_path.lastIndexOf('/')) ensure_dir(dir_path) var content = zip.slurp(filename) fd.slurpwrite(full_path, content) } } } else { log.console("Verified existing installation.") } return { commit: commit_hash, package: pkg, path: parsed.path, zip_hash: zip_hash } } // High-level: Add a package, install it, and install all transitive dependencies // Like `bun add` or `npm install ` Shop.get = function(pkg, alias) { Shop.init() var parsed = Shop.parse_package(pkg) if (!alias) alias = parsed.name log.console("Adding dependency: " + alias + " = " + pkg) // Add to config var config = Shop.load_config() || { dependencies: {} } if (!config.dependencies) config.dependencies = {} config.dependencies[alias] = pkg Shop.save_config(config) // Install the package and dependencies var queue = [pkg] var processed = {} var lock = Shop.load_lock(null) while (queue.length > 0) { var current_pkg = queue.shift() if (processed[current_pkg]) continue processed[current_pkg] = true log.console("Installing " + current_pkg + "...") var lock_info = lock[current_pkg] || lock[Shop.parse_package(current_pkg).name] var locked_hash = lock_info ? lock_info.commit : null var zip_hash = lock_info ? lock_info.zip_hash : null var result = install_from_package(current_pkg, locked_hash, zip_hash) if (result) { lock[current_pkg] = { package: current_pkg, commit: result.commit, zip_hash: result.zip_hash, updated: time.number() } // Read package config to find dependencies var parsed = Shop.parse_package(current_pkg) var pkg_config = Shop.load_config(parsed.path) if (pkg_config && pkg_config.dependencies) { for (var k in pkg_config.dependencies) { var dep_pkg = pkg_config.dependencies[k] if (!processed[dep_pkg]) { queue.push(dep_pkg) } } } } else { if (current_pkg == pkg) { log.console("Failed to install requested package " + alias) return false } else { log.console("Failed to install dependency " + current_pkg) } } } Shop.save_lock(lock) log.console("Done.") return true } Shop.fetch = function(package) { } // High-level: Update a specific package // Like `bun update ` Shop.update = function(pkg) { var config = Shop.load_config() var lock = Shop.load_lock() // Check if replaced if (config.replace && config.replace[pkg]) { log.console("Skipping update for replaced package " + pkg) Shop.update(config.replace[pkg]) return false } // Find existing lock info var lock_info = lock[pkg] var local_hash = lock_info ? lock_info.commit : null var api_url = Shop.get_api_url(pkg) var remote_hash = null // Check for updates if possible if (api_url) { try { var resp = http.fetch(api_url) remote_hash = Shop.extract_commit_hash(pkg, text(resp)) } catch (e) { log.console("Warning: Could not check for updates for " + pkg) } } var target_hash = remote_hash if (!target_hash) { log.error("Could not resolve remote commit for " + pkg) return false } if (local_hash && remote_hash == local_hash) { log.console(alias + " is already up to date.") return true } if (local_hash) { log.console("Updating " + alias + " " + local_hash.substring(0,8) + " -> " + remote_hash.substring(0,8)) } else { log.console("Installing " + alias + "...") } // Install with fresh download (no zip hash to force redownload) var result = install_from_package(pkg, target_hash, null) if (result) { // Update lock lock[pkg] = { package: pkg, commit: result.commit, zip_hash: result.zip_hash, updated: time.number() } Shop.save_lock(lock) log.console("Updated " + alias + ".") return true } log.error("Failed to update " + alias) return false } // High-level: Remove a package and clean up // Like `bun remove` Shop.remove = function(alias) { var config = Shop.load_config() if (!config || !config.dependencies || !config.dependencies[alias]) { log.error("Dependency not found: " + alias) return false } var locator = config.dependencies[alias] var parsed = Shop.parse_package(locator) var target_dir = '.cell/modules/' + parsed.path // Remove from config delete config.dependencies[alias] Shop.save_config(config) // Remove from lock var lock = Shop.load_lock() if (lock[locator]) delete lock[locator] if (lock[alias]) delete lock[alias] // Cleanup old format Shop.save_lock(lock) // Remove directory if (fd.is_dir(target_dir)) { log.console("Removing " + target_dir) try { fd.rmdir(target_dir) } catch (e) { log.error("Failed to remove directory: " + e) } } log.console("Removed " + alias) return true } Shop.add_replacement = function(alias, replacement) { var config = Shop.load_config() if (!config) config = {} if (!config.replace) config.replace = {} config.replace[alias] = replacement Shop.save_config(config) log.console("Added replacement: " + alias + " = " + replacement) return true } Shop.remove_replacement = function(alias) { var config = Shop.load_config() if (!config || !config.replace || !config.replace[alias]) { log.error("No replacement found for " + alias) return false } delete config.replace[alias] Shop.save_config(config) log.console("Removed replacement for " + alias) log.console("Run 'cell update " + alias + "' to restore the package.") return true } // Install all dependencies from config (like `bun install`) Shop.install_all = function() { Shop.init() var config = Shop.load_config() if (!config || !config.dependencies) { log.console("No dependencies to install.") return true } var lock = Shop.load_lock(null) var queue = [] var processed = {} for (var alias in config.dependencies) { queue.push(config.dependencies[alias]) } while (queue.length > 0) { var pkg = queue.shift() if (processed[pkg]) continue processed[pkg] = true log.console("Installing " + pkg + "...") var lock_info = lock[pkg] || lock[Shop.parse_package(pkg).name] // Fallback to old format check var locked_hash = lock_info ? lock_info.commit : null var zip_hash = lock_info ? lock_info.zip_hash : null var result = install_from_package(pkg, locked_hash, zip_hash) if (result) { lock[pkg] = { package: pkg, commit: result.commit, zip_hash: result.zip_hash, updated: time.number() } // Read package config to find dependencies var parsed = Shop.parse_package(pkg) var pkg_config = Shop.load_config(parsed.path) if (pkg_config && pkg_config.dependencies) { for (var k in pkg_config.dependencies) { var dep_pkg = pkg_config.dependencies[k] if (!processed[dep_pkg]) { queue.push(dep_pkg) } } } } } Shop.save_lock(lock) log.console("Done.") return true } // Compile a module // List all files in a package Shop.list_files = function(pkg) { var dir if (!pkg) dir = '.' else dir = '.cell/modules/' + pkg var files = [] var walk = function(current_dir, current_prefix) { var list = fd.readdir(current_dir) if (!list) return for (var i = 0; i < list.length; i++) { var item = list[i] if (item == '.' || item == '..') continue if (item.startsWith('.')) continue // Skip build directories in root if (!pkg && (item == 'build' || item == 'build_dbg' || item == 'build_release' || item == 'build_web' || item == 'build_fast')) continue if (!pkg && item == 'cell_modules') continue // Just in case var full_path = current_dir + "/" + item var rel_path = current_prefix ? current_prefix + "/" + item : item var st = fd.stat(full_path) if (st.isDirectory) { walk(full_path, rel_path) } else { files.push(rel_path) } } } if (fd.is_dir(dir)) { walk(dir, "") } return files } Shop.get_c_objects = function(pkg) { var files = Shop.list_files(pkg) var objects = [] var build_dir = get_build_dir(pkg) for (var i=0; i " + obj_path) var comp_src = file var comp_obj = file + '.o' var cmd = 'cd ' + module_dir + ' && cc -fPIC -c ' + comp_src + ' -O3 -DCELL_USE_NAME=' + use_name + ' -o ' + comp_obj if (cflags != '') cmd += ' ' + cflags var ret = os.system(cmd) if (ret != 0) { log.error("Compilation failed for " + src_path) return false } // now move it os.system('mv ' + module_dir + '/' + comp_obj + ' ' + obj_path) } c_objects.push(obj_path) } } // Link if there are C objects if (c_objects.length > 0) { var lib_name = build_dir + '/cellmod' + dylib_ext // Check if we need to relink var needs_link = true if (fd.is_file(lib_name)) { var lib_time = fd.stat(lib_name).mtime needs_link = false for (var i=0; i lib_time) { needs_link = true break } } } if (needs_link) { log.console("Linking " + lib_name) var link_flags = '-shared' if (platform == 'macOS') link_flags = '-shared -undefined dynamic_lookup' var ldflags = get_flags(config, platform, 'LDFLAGS') log.console(platform) log.console(ldflags) log.console(json.encode(config)) if (ldflags != '') link_flags += ' ' + ldflags var temp_lib = 'cellmod_temp' + dylib_ext var objs_str = '' for (var i=0; i 0) { var pkg = queue.shift() if (processed[pkg]) continue processed[pkg] = true result.push(pkg) var parsed = Shop.parse_package(pkg) var pkg_config = Shop.load_config(parsed.path) if (pkg_config && pkg_config.dependencies) { for (var alias in pkg_config.dependencies) { var dep_pkg = pkg_config.dependencies[alias] if (!processed[dep_pkg]) { queue.push(dep_pkg) } } } } return result } // List all .cm and .ce files in a package // If ctx is null, lists local files // If ctx is a canonical path, lists files in that module Shop.list_modules = function(ctx) { var files = Shop.list_files(ctx) var modules = [] for (var i=0; i