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 blob = use('blob') var build_utils = use('build') // 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 = {} // Global shop path - this is where all packages, builds, and cache are stored // Located at ~/.cell on the user's machine var global_shop_path = null // Current package context - the package we're running from (resolved from program path) var current_package_path = null 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 var $_ // Initialize the global shop path function init_global_shop() { if (global_shop_path) return global_shop_path var home = os.getenv('HOME') if (!home) { // Try Windows-style home = os.getenv('USERPROFILE') } if (!home) { throw new Error('Could not determine home directory. Set HOME environment variable.') } global_shop_path = home + '/.cell' // Verify the shop exists if (!fd.is_dir(global_shop_path)) { throw new Error('Cell shop not found at ' + global_shop_path + '. Run "cell install" to set up.') } return global_shop_path } // Get the global shop path Shop.get_shop_path = function() { return init_global_shop() } // Find the package directory containing a file by looking for cell.toml // Returns the absolute path to the package directory, or null if not in a package Shop.find_package_dir = function(file_path) { if (!file_path) return null // Walk up directories looking for cell.toml var dir = file_path if (fd.is_file(dir)) { // if it's a file, start from parent var last_slash = dir.lastIndexOf('/') if (last_slash > 0) dir = dir.substring(0, last_slash) } while (dir && dir.length > 0) { var toml_path = dir + '/cell.toml' if (fd.is_file(toml_path)) { return fd.realpath(dir) } var last_slash = dir.lastIndexOf('/') if (last_slash <= 0) break dir = dir.substring(0, last_slash) } // Check current directory as fallback if (fd.is_file('cell.toml')) { return fd.realpath('.') } return null } // Link a local package into the shop function ensure_package_link(abs_path) { if (!abs_path || !abs_path.startsWith('/')) return false var packages_dir = get_modules_dir() var target_link = packages_dir + abs_path // If link already exists and points to correct place, we are good if (fd.is_link(target_link)) { var points_to = fd.readlink(target_link) if (points_to == abs_path) { update_local_lock(abs_path) return true } // Incorrect link, remove it fd.unlink(target_link) } else if (fd.is_dir(target_link)) { // If it's a real directory, that's weird for a local package mirror, but let's assume it might be a copy? // User wants symlinks. // If it's not a link, maybe we should leave it or warn? // Use user instructions: "shop should ... ensure it's there ... symlink local dirs" // safely assuming we can replace if it's not the right thing might be dangerous if user put real files there. // For now, if it's a dir, check if it's the package itself? // simpler: proceed to link logic } // Create parent dirs var parent = target_link.substring(0, target_link.lastIndexOf('/')) ensure_dir(parent) try { fd.symlink(abs_path, target_link) // log.console("Linked " + abs_path + " -> " + target_link) update_local_lock(abs_path) return true } catch (e) { log.error("Failed to link package " + abs_path + ": " + e) return false } } function update_local_lock(abs_path) { var lock = Shop.load_lock() var name = abs_path.split('/').pop() // Check if we can find a better name from cell.toml var toml_path = abs_path + '/cell.toml' if (fd.is_file(toml_path)) { // We could parse it, but for now directory name is usually the package alias } lock[name] = { package: abs_path, type: 'local', updated: time.number() } Shop.save_lock(lock) } Shop.ensure_package_link = ensure_package_link // Set the current package context from a program path Shop.set_current_package = function(program_path) { current_package_path = Shop.find_package_dir(program_path) if (current_package_path && current_package_path.startsWith('/')) { // It's a local package, ensure it is linked in the shop ensure_package_link(current_package_path) } return current_package_path } // Get the current package path Shop.get_current_package = function() { return current_package_path } Shop.set_os = function(o, $guy) { os = o $_ = $guy use_cache = os.use_cache platform = os.platform() if (platform == 'macOS') dylib_ext = '.dylib' else if (platform == 'Windows') dylib_ext = '.dll' // Initialize the global shop init_global_shop() } var config = null // Get the config path for a package // If package_path is null, uses current_package_path function get_config_path(package_path) { if (package_path) { return package_path + '/cell.toml' } if (current_package_path) { return current_package_path + '/cell.toml' } // Fallback to current directory return 'cell.toml' } // Get the lock file path (in the global shop) function get_lock_path() { return global_shop_path + '/lock.toml' } // Get the packages directory (in the global shop) function get_modules_dir() { return global_shop_path + '/packages' } // Get the cache directory (in the global shop) function get_cache_dir() { return global_shop_path + '/cache' } // Get the build directory (in the global shop) function get_global_build_dir() { return global_shop_path + '/build' } // Get the core directory (in the global shop) Shop.get_core_dir = function() { return global_shop_path + '/core' } // Get the links file path (in the global shop) function get_links_path() { return global_shop_path + '/link.toml' } // Get the reports directory (in the global shop) Shop.get_reports_dir = function() { return global_shop_path + '/reports' } var open_dl = {} function get_import_package(name) { var parts = name.split('/') if (parts.length > 1) return parts[0] return null } Shop.file_info = function(file) { var info = { path: file, is_module: false, is_actor: false, package: null, name: null } if (file.endsWith(MOD_EXT)) { info.is_module = true } else if (file.endsWith(ACTOR_EXT)) { info.is_actor = true } // Strip extension for name var name_without_ext = file if (info.is_module) { name_without_ext = file.substring(0, file.length - MOD_EXT.length) } else if (info.is_actor) { name_without_ext = file.substring(0, file.length - ACTOR_EXT.length) } // Check if file is in a package (in global shop packages dir) var packages_prefix = get_modules_dir() + '/' if (file.startsWith(packages_prefix)) { var rest = file.substring(packages_prefix.length) // Get all packages and find which one matches this path // With the new structure, everything in packages/ is a package // gitea.pockle.world/user/repo/file.cm var parts = rest.split('/') // Heuristic: packages form is host/user/repo if (parts.length >= 3) { var pkg_path = parts.slice(0, 3).join('/') info.package = pkg_path if (rest.length > pkg_path.length) { info.name = rest.substring(pkg_path.length + 1) } else { info.name = "" // root? } } else { // fallback info.package = rest } if (info.name) { if (info.is_module && info.name.endsWith(MOD_EXT)) info.name = info.name.substring(0, info.name.length - MOD_EXT.length) if (info.is_actor && info.name.endsWith(ACTOR_EXT)) info.name = info.name.substring(0, info.name.length - ACTOR_EXT.length) } } else { // Check if it's in any other package via cell.toml lookup var pkg_dir = Shop.find_package_dir(file) if (pkg_dir) { info.package = pkg_dir if (file.startsWith(pkg_dir + '/')) { var rel = file.substring(pkg_dir.length + 1) info.name = rel if (info.is_module && info.name.endsWith(MOD_EXT)) info.name = info.name.substring(0, info.name.length - MOD_EXT.length) if (info.is_actor && info.name.endsWith(ACTOR_EXT)) info.name = info.name.substring(0, info.name.length - ACTOR_EXT.length) } } else { info.package = 'local' // Should we keep 'local' for truly orphan files? info.name = name_without_ext } } return info } 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 = get_modules_dir() + '/' + 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) } // Use ensure_dir from build_utils var ensure_dir = build_utils.ensure_dir Shop.load_config = function(module) { var content var config_path if (!module) { config_path = get_config_path() if (!fd.is_file(config_path)) return null content = fd.slurp(config_path) } else { // Module config is at //cell.toml var module_path = get_modules_dir() + '/' + module + '/cell.toml' if (!fd.is_file(module_path)) return null content = fd.slurp(module_path) } if (!(content instanceof blob)) { log.console(`critical error`) for (var k in content) log.console(k) throw new Error("fucked up bad") } 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 (changed && !module) { Shop.save_config(cfg) } } return cfg } // Save cell.toml configuration Shop.save_config = function(config) { var config_path = get_config_path() fd.slurpwrite(config_path, utf8.encode(toml.encode(config))); } // Load lock.toml configuration (from global shop) Shop.load_lock = function() { var path = get_lock_path() 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 (to global shop) Shop.save_lock = function(lock) { var path = get_lock_path() ensure_dir(global_shop_path) fd.slurpwrite(path, utf8.encode(toml.encode(lock))); } var link_cache = null Shop.load_links = 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 } Shop.save_links = function(links) { link_cache = links var cfg = { links: links } var path = get_links_path() ensure_dir(global_shop_path) fd.slurpwrite(path, utf8.encode(toml.encode(cfg))) } Shop.add_link = function(canonical, target) { var links = Shop.load_links() links[canonical] = target Shop.save_links(links) log.console("Linked " + canonical + " -> " + target) return true } Shop.remove_link = function(canonical) { var links = Shop.load_links() if (!links[canonical]) return false delete links[canonical] Shop.save_links(links) log.console("Unlinked " + canonical) return true } Shop.clear_links = function() { Shop.save_links({}) log.console("Cleared all links") return true } // 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] } // Check for links var links = Shop.load_links() if (links[path]) { path = links[path] } // Handle absolute paths (local modules) // /User/john/mod -> User/john/mod if (path.startsWith('/')) { path = path.substring(1) } // Extract name (last part of path) var name = path.split('/').pop() return { path, name, version } } // Get information about how to resolve a package Shop.resolve_package_info = function(pkg) { // Check links first // We need to check the raw package string against links // But we also need to handle the case where pkg is already a local path // If pkg is "gitea...", check if it's linked var path = pkg if (path.includes('@')) path = path.split('@')[0] var links = Shop.load_links() if (links[path]) { var resolved = links[path] if (resolved.startsWith('/')) { return { type: 'local', path: resolved } } return { type: 'package', path: resolved } } if (pkg.startsWith('/')) { return { type: 'local', path: pkg } } if (pkg.includes('gitea.')) { return { type: 'gitea' } } // If it looks like a path but isn't absolute, might be relative in future, but for now strict return { type: 'unknown' } } // Resolve a filesystem path to a package identifier in the shop Shop.resolve_path_to_package = function(path_str) { var abs = fd.realpath(path_str) if (!abs) return null var modules_dir = get_modules_dir() // Case 1: Path is inside the modules directory (e.g. downloaded package) if (abs.startsWith(modules_dir + '/')) { var rel = abs.substring(modules_dir.length + 1) // Remove trailing slash if any if (rel.endsWith('/')) rel = rel.substring(0, rel.length - 1) return rel } // Case 2: Path is a local directory linked into the shop // Local links are stored as modules_dir + abs_path (mirrored) var target = modules_dir + abs if (fd.is_dir(target)) { if (abs.startsWith('/')) return abs.substring(1) return abs } // Case 3: Path is not linked. Return null. return null } // 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 info = Shop.resolve_package_info(pkg) if (info.type == 'local') return null 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 info = Shop.resolve_package_info(pkg) if (info.type == 'local') return null 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 get_modules_dir() + '/' + parsed.path } 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 = get_cache_dir() + '/' + parsed.path + '.zip' if (fd.is_file(cache_path)) { log.console("Found cached zip: " + cache_path) return true } return false } var open_dls = {} // for script forms, path is the canonical path of the module var script_forms = [] // Construct a descriptive compile name with extension preserved. // Formats: // core: // : (package is the full canonical package name) // local: // package: (fallback when pkg isn't provided but path is under modules) function make_compile_name(path, rel_path, pkg, scope) { if (scope == SCOPE_CORE) return 'core:' + rel_path if (pkg) return pkg + ':' + rel_path var modules_dir = get_modules_dir() if (path && path.startsWith(modules_dir + '/')) return 'package:' + rel_path return 'local:' + rel_path } script_forms['.cm'] = function(path, script, pkg) { var pkg_arg = pkg ? `'${pkg}'` : 'null' var relative_use_fn = `def use = function(path) { return globalThis.use(path, ${pkg_arg});}` var fn = `(function setup_module($_){ ${relative_use_fn}; ${script}})` return fn } script_forms['.ce'] = function(path, script, pkg) { var pkg_arg = pkg ? `'${pkg}'` : 'null' var relative_use_fn = `def use = function(path) { return globalThis.use(path, ${pkg_arg});}` return `(function start($_, arg) { ${relative_use_fn}; 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) { if (!pkg) pkg = current_package_path if (!pkg) return get_global_build_dir() + '/local' // Fallback for non-package scripts // If pkg is absolute path, mirror it if (pkg.startsWith('/')) { // Accio folder should be .cell/packages/Users/... // But build should be .cell/build/packages/Users/... return get_global_build_dir() + '/packages' + pkg } // Otherwise it's a relative package path (from packages dir) return get_global_build_dir() + '/packages/' + pkg } Shop.get_build_dir = get_build_dir function get_rel_path(path, pkg) { if (!pkg) { // For local files, strip the current package path prefix if present if (current_package_path && path.startsWith(current_package_path + '/')) { return path.substring(current_package_path.length + 1) } return path } var prefix if (pkg.startsWith('/')) { prefix = pkg + '/' } else { prefix = get_modules_dir() + '/' + 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) // Determine build directory based on context var build_dir if (pkg) { build_dir = get_build_dir(pkg) } else if (current_package_path) { // Local file in current package - use package path (strip leading /) var pkg_id = current_package_path.substring(1) // Remove leading / build_dir = get_global_build_dir() + '/' + pkg_id } else { build_dir = get_build_dir('local') } 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 compile_name = make_compile_name(path, rel_path, pkg, pkg ? SCOPE_PACKAGE : SCOPE_LOCAL) var script = script_form(path, text(fd.slurp(path)), pkg) var fn = js.compile(compile_name, script) ensure_dir(cache_path.substring(0, cache_path.lastIndexOf('/'))) fd.slurpwrite(cache_path, js.compile_blob(fn)) return js.eval_compile(fn) } // Resolve and cache a core module function resolve_core_mod_fn(core_path, rel_path) { var build_dir = get_global_build_dir() + '/core' var cache_path = build_dir + '/' + rel_path + '.o' if (fd.is_file(cache_path) && fd.stat(core_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 = core_path.substring(core_path.lastIndexOf('.')) var form = script_forms[ext] if (!form) throw new Error(`No script form for extension ${ext}`) var compile_name = make_compile_name(core_path, rel_path, null, SCOPE_CORE) var script = form(null, text(fd.slurp(core_path))) var fn = js.compile(compile_name, 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) { // In static_only mode, only look in the embedded pack var static_only = cell.static_only if (!static_only) { // First, check if file exists in current package directory if (current_package_path) { var pkg_local_path = current_package_path + '/' + path + ext if (fd.is_file(pkg_local_path)) { var fn = resolve_mod_fn(pkg_local_path, null) return {path: pkg_local_path, scope: SCOPE_LOCAL, symbol:fn} } } // Check CWD for local file var local_path if (ctx) local_path = get_modules_dir() + '/' + 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} } // Check installed packages var canonical_pkg = get_normalized_package(path, ctx) var pkg_path = get_path_in_package(path, ctx) var mod_path = get_modules_dir() + '/' + 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} } } // Check core directory for core modules var core_dir = Shop.get_core_dir() // For packages, try the full package path first in core if (ctx) { var pkg_rel = ctx + '/' + path + ext var pkg_core_path = core_dir + '/' + pkg_rel if (fd.is_file(pkg_core_path)) { var fn = resolve_core_mod_fn(pkg_core_path, pkg_rel) return {path: pkg_rel, scope: SCOPE_CORE, symbol:fn}; } } // Check core directory for the module // Core scripts are now in .cell/core/scripts var core_dir = Shop.get_core_dir() var core_file_path = core_dir + '/scripts/' + path + ext if (path == 'text') log.console("Checking core mod: " + core_file_path + " exists: " + fd.is_file(core_file_path)) if (fd.is_file(core_file_path)) { var fn = resolve_core_mod_fn(core_file_path, path + ext) return {path: path + ext, scope: SCOPE_CORE, symbol:fn}; } return null; } function c_sym_path(path) { return path.replace(/\//g, '_').replace(/\\/g, '_').replace(/\./g, '_').replace(/-/g, '_') } function resolve_c_symbol(path, package_context) { var static_only = cell.static_only var local_path = package_context ? package_context : 'local' var local_sym_base = c_sym_path(path) var local function symbol_candidates(pkg_path, mod_sym) { var variants = [] var paths = [pkg_path] if (!pkg_path.startsWith('/')) paths.push('/' + pkg_path) for (var i = 0; i < paths.length; i++) { var candidate = `js_${c_sym_path(paths[i])}_${mod_sym}_use` if (variants.indexOf(candidate) < 0) variants.push(candidate) } return variants } if (!package_context) { local = `js_local_${local_sym_base}_use` } else { local = null // handled via candidates below } // First check for statically linked/internal symbols var local_candidates = package_context ? symbol_candidates(local_path, local_sym_base) : [local] for (var li = 0; li < local_candidates.length; li++) { var lc = local_candidates[li] if (os.internal_exists(lc)) return { symbol: function() { return os.load_internal(lc); }, scope: SCOPE_LOCAL, path: lc }; } // In static_only mode, skip dynamic library lookups if (!static_only) { // Then try dynamic library 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]) { var locals = package_context ? symbol_candidates(local_path, local_sym_base) : [local] for (var i = 0; i < locals.length; i++) { var candidate = locals[i] if (os.dylib_has_symbol(open_dls[local_dl_name], candidate)) return { symbol: function() { return os.dylib_symbol(open_dls[local_dl_name], candidate); }, scope: SCOPE_LOCAL, path: candidate }; } } } } // 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 mod_name = get_import_name(path) var mod_sym = mod_name.replace(/\//g, '_').replace(/-/g, '_').replace(/\./g, '_') var sym_names = symbol_candidates(canon_pkg, mod_sym) // First check internal/static symbols for package for (var sii = 0; sii < sym_names.length; sii++) { var sym_name = sym_names[sii] if (os.internal_exists(sym_name)) return { symbol: function() { return os.load_internal(sym_name) }, scope: SCOPE_PACKAGE, package: canon_pkg, path: sym_name }; } // Then try dynamic library for package (skip in static_only mode) if (!static_only) { var pkg_build_dir = get_build_dir(canon_pkg) var dl_path = pkg_build_dir + '/cellmod' + dylib_ext 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]) { for (var si = 0; si < sym_names.length; si++) { var sym_name = sym_names[si] 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, path: sym_name } } } } } } } 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, path: core_sym }; return null } function resolve_module_info(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) return null var resolved_path if (mod_resolve.scope != 999) resolved_path = mod_resolve.path else resolved_path = c_resolve.path var cache_scope = min_scope == SCOPE_CORE ? 2 : 0 var cache_key if (min_scope == SCOPE_CORE) cache_key = `2::${path}` else cache_key = `${text(cache_scope)}::${resolved_path}` cache_key = cache_key.replace('//', '/') return { cache_key: cache_key, c_resolve: c_resolve, mod_resolve: mod_resolve, min_scope: min_scope } } function get_module_cache_key(path, package_context) { var info = resolve_module_info(path, package_context) return info ? info.cache_key : null } Shop.is_loaded = function(path, package_context) { var cache_key = get_module_cache_key(path, package_context) return use_cache[cache_key] != null } function execute_module(info) { var c_resolve = info.c_resolve var mod_resolve = info.mod_resolve var cache_key = info.cache_key var used if (c_resolve.scope < mod_resolve.scope) used = c_resolve.symbol(null, $_) else if (mod_resolve.scope < c_resolve.scope) used = mod_resolve.symbol.call(null, $_) else used = mod_resolve.symbol.call(c_resolve.symbol(), $_) if (!used) throw new Error(`Module ${json.encode(info)} returned null`) return used } function get_module(path, package_context) { var info = resolve_module_info(path, package_context) if (!info) throw new Error(`Module ${path} could not be found in ${package_context}`) return execute_module(info) } // 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 info = resolve_module_info(path, package_context) if (!info) throw new Error(`Module ${path} could not be found in ${package_context}`) if (use_cache[info.cache_key]) return use_cache[info.cache_key] use_cache[info.cache_key] = execute_module(info) return use_cache[info.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 get_cache_dir() + '/' + 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 } // High-level: Add a package, install it, and install all transitive dependencies // Like `bun add` or `npm install ` Shop.get = function(pkg, alias) { if (fd.is_dir(pkg)) { log.console("Found directory: " + pkg) pkg = fd.realpath(pkg) log.console("Resolved to: " + pkg) } var info = Shop.resolve_package_info(pkg) if (info.type == 'unknown') { log.error("Could not resolve package: " + pkg) return false } 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) return true } // Update a specific package Shop.update = function(pkg) { var config = Shop.load_config() var lock = Shop.load_lock() var parsed = Shop.parse_package(pkg) var info = Shop.resolve_package_info(pkg) var target_dir = get_modules_dir() + '/' + parsed.path var result = info.type == 'local' ? update_local(pkg, info, target_dir) : update_remote(pkg, info, target_dir, lock[pkg]) if (!result) { log.error("Failed to update " + parsed.path) return false } lock[pkg] = { package: pkg, commit: result.commit, zip_hash: result.zip_hash, updated: time.number() } Shop.save_lock(lock) log.console("Updated " + parsed.path + ".") return true } function update_local(pkg, info, target_dir) { if (fd.is_link(target_dir)) { if (fd.readlink(target_dir) == info.path) return { commit: "local", package: pkg, zip_hash: "local" } else fd.unlink(target_dir) } var parent_dir = target_dir.substring(0, target_dir.lastIndexOf('/')) ensure_dir(parent_dir) if (fd.is_dir(target_dir)) fd.rmdir(target_dir) try { fd.symlink(info.path, target_dir) log.console("Linked " + target_dir + " -> " + info.path) return { commit: "local", package: pkg, zip_hash: "local" } } catch(e) { log.error("Failed to create symlink: " + e) return null } } function update_remote(pkg, info, target_dir, lock_info) { var local_hash = lock_info ? lock_info.commit : null var remote_hash = fetch_remote_hash(pkg) if (!remote_hash && !local_hash) { log.error("Could not resolve commit for " + pkg) return null } var target_hash = remote_hash || local_hash if (local_hash == target_hash) { log.console(pkg + " is already up to date.") return lock_info } if (local_hash) log.console("Updating " + pkg + " " + local_hash.substring(0,8) + " -> " + target_hash.substring(0,8)) else log.console("Installing " + pkg + "...") var zip_blob = get_or_download_zip(pkg, target_hash) if (!zip_blob) return null var zip_hash = text(crypto.blake2(zip_blob), 'h') install_zip(zip_blob, target_dir) return { commit: target_hash, package: pkg, zip_hash: zip_hash } } function fetch_remote_hash(pkg) { var api_url = Shop.get_api_url(pkg) if (!api_url) return null try { var resp = http.fetch(api_url) return Shop.extract_commit_hash(pkg, text(resp)) } catch (e) { log.console("Warning: Could not check for updates for " + pkg) return null } } function get_or_download_zip(pkg, commit_hash) { var cache_path = get_cache_path(pkg, commit_hash) if (fd.is_file(cache_path)) { log.console("Found cached zip: " + cache_path) try { return fd.slurp(cache_path) } catch (e) { log.error("Failed to read cache: " + e) } } 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 { var zip_blob = http.fetch(download_url) ensure_dir(cache_path.substring(0, cache_path.lastIndexOf('/'))) fd.slurpwrite(cache_path, zip_blob) log.console("Cached to " + cache_path) return zip_blob } catch (e) { log.error(e) return null } } function install_zip(zip_blob, target_dir) { var zip = miniz.read(zip_blob) if (!zip) throw new Error("Failed to read zip archive") if (fd.is_link(target_dir)) fd.unlink(target_dir) log.console("Syncing to " + target_dir) ensure_dir(target_dir) var zip_files = {} var count = zip.count() 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) continue parts.shift() var rel_path = parts.join('/') zip_files[rel_path] = { index: i, filename: filename } } var existing_files = fd.is_dir(target_dir) ? get_all_files(target_dir) : [] for (var i = 0; i < existing_files.length; i++) { var rel_path = existing_files[i] if (!zip_files[rel_path]) { var full_path = target_dir + '/' + rel_path log.console("Removing " + rel_path) fd.rm(full_path) } } for (var rel_path in zip_files) { var zip_info = zip_files[rel_path] var full_path = target_dir + '/' + rel_path var dir_path = full_path.substring(0, full_path.lastIndexOf('/')) var zip_content = zip.slurp(zip_info.filename) var needs_write = true if (fd.is_file(full_path)) { var disk_content = fd.slurp(full_path) if (disk_content.length == zip_content.length && disk_content.length != 0) { var hash_zip = text(crypto.blake2(zip_content), 'h') var hash_disk = text(crypto.blake2(disk_content), 'h') if (hash_zip == hash_disk) { needs_write = false } } } if (needs_write) { ensure_dir(dir_path) log.console("Writing " + rel_path) fd.slurpwrite(full_path, zip_content) } } } // High-level: Remove a package and clean up // Like `bun remove` Shop.remove = function(alias_or_path) { var config = Shop.load_config() var is_dependency = config && config.dependencies && config.dependencies[alias_or_path] var target_pkg = null var locator = null if (is_dependency) { locator = config.dependencies[alias_or_path] var parsed = Shop.parse_package(locator) target_pkg = parsed.path // Remove from config delete config.dependencies[alias_or_path] Shop.save_config(config) } else { // Check if it's a resolvable path var resolved = Shop.resolve_path_to_package(alias_or_path) if (resolved) { target_pkg = resolved } else { // Check if alias_or_path exists directly in modules dir? var direct_path = get_modules_dir() + '/' + alias_or_path if (fd.exists(direct_path)) { target_pkg = alias_or_path } } } if (!target_pkg) { log.error("Package/Dependency not found: " + alias_or_path) return false } var target_dir = get_modules_dir() + '/' + target_pkg // Remove from lock var lock = Shop.load_lock() var lock_changed = false if (locator && lock[locator]) { delete lock[locator] lock_changed = true } if (lock[alias_or_path]) { delete lock[alias_or_path] lock_changed = true } // Scan for any entry pointing to this target for (var k in lock) { if (lock[k].package == target_pkg || lock[k].package == '/' + target_pkg) { delete lock[k] lock_changed = true } } if (lock_changed) Shop.save_lock(lock) // Remove directory if (fd.is_link(target_dir)) { log.console("Unlinking " + target_dir) fd.unlink(target_dir) } else if (fd.is_dir(target_dir)) { log.console("Removing " + target_dir) fd.rmdir(target_dir) } log.console("Removed " + (is_dependency ? alias_or_path : target_pkg)) 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 } // Compile a module // List all files in a package Shop.list_files = function(pkg) { var dir if (!pkg) dir = current_package_path || '.' else dir = get_modules_dir() + '/' + 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 st_obj.mtime) { log.console(`${header_full} out of date`) needs_compile = true; break; } } } } } } if (needs_compile) { log.console("Compiling " + src_path + " -> " + obj_path) // 1. Generate dependencies var deps_file = comp_obj + '.d' // Use same flags but with -M var deps_cmd = 'cd ' + module_dir + ' && ' + base_cmd + '-MM ' + compile_flags + ' > ' + deps_file os.system(deps_cmd) var headers = [] var deps_full_path = module_dir + '/' + deps_file if (fd.is_file(deps_full_path)) { var deps_content = text(fd.slurp(deps_full_path)) deps_content = deps_content.replace(/\\\n/g, ' ').replace(/\\\r\n/g, ' ').replace(/\n/g, ' ') var parts = deps_content.split(' ') for (var p=0; p 0) { var lib_name = build_dir + '/cellmod' + dylib_ext var lib_meta_path = lib_name + '.meta' var link_flags = '-fPIC -shared' var ldflags = get_flags(config, platform, 'LDFLAGS') if (ldflags != '') link_flags += ' ' + ldflags var temp_lib = 'cellmod' + dylib_ext var objs_str = '' for (var i=0; i lib_time) { needs_link = true break } } } } if (needs_link) { log.console("Linking " + lib_name) var ret = os.system(link_cmd) if (ret != 0) { log.error("Linking failed") return false } os.system('mv ' + module_dir + '/' + temp_lib + ' ' + lib_name) fd.slurpwrite(lib_meta_path, utf8.encode(json.encode({ cmd_hash: link_cmd_hash }))) } log.console("Built " + lib_name) } return true } // Get dependencies for a specific context (package canonical path) // If ctx is null, returns dependencies for the local project Shop.dependencies = function(ctx) { var config = Shop.load_config(ctx) if (!config || !config.dependencies) { return {} } return config.dependencies } Shop.list_packages = function(root) { var queue = [] var processed = {} var result = [] var deps = Shop.dependencies(root) for (var alias in deps) { var pkg = deps[alias] if (!processed[pkg]) { queue.push(pkg) } } while (queue.length > 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