Files
cell/shop.cm
2025-12-08 19:50:09 -06:00

2048 lines
58 KiB
Plaintext

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')
var core = "gitea.pockle.world/john/cell"
function content_hash(content)
{
return text(crypto.blake2(utf8.encode(content)), 'h')
}
// 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
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_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'
}
// 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
}
// Given a path like 'prosperon/sprite', extract the package alias ('prosperon')
// and resolve it to its canonical path using the dependencies in ctx's config.
// Returns the canonical package path (e.g., 'gitea.pockle.world/john/prosperon')
// or null if the path has no package prefix or the package is not found.
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 <modules_dir>/<module>/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_form = 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
}
// 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
// Resolve module function
function resolve_mod_fn(path, pkg) {
if (!fd.is_file(path)) throw new Error(`path ${path} is not a file`)
var content = fd.slurp(path)
var hash = content_hash(content)
var hash_path = get_global_build_dir() + '/' + hash
if (fd.is_file(hash_path)) {
var obj = fd.slurp(hash_path)
var fn = js.compile_unblob(obj)
return js.eval_compile(fn)
}
var form = script_form
// We don't really use pkg scope for compilation anymore since it's just hash based cache
// But we need to pass a package context for the 'use' function inside the module
var script = form(path, text(content), pkg);
// Compile name is just for debug/stack traces
var compile_name = pkg ? pkg + ':' + path : 'local:' + path
var fn = js.compile(compile_name, script)
// Ensure build dir exists
ensure_dir(get_global_build_dir())
fd.slurpwrite(hash_path, js.compile_blob(fn))
return js.eval_compile(fn)
}
// resolve_core_mod_fn is no longer needed as core modules are just modules in a package (or local)
function resolve_locator(path, ext, ctx)
{
if (path.endsWith(ext)) ext = ''
// 1. Check local file (relative to current directory if no context, or relative to package 'ctx' if provided)
// If ctx is provided, it's a package alias or path.
var local_path
if (ctx) {
var mod_dir = get_modules_dir()
// Check if ctx is an absolute path (local package)
if (ctx.startsWith('/')) {
local_path = ctx + '/' + path + ext
} else {
local_path = mod_dir + '/' + ctx + '/' + path + ext
}
} else {
// No context, just check simple local path
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}
} else {
// Fallback: check if the path itself is a file (ignoring required extension)
// This allows running scripts like 'test.cm' even if engine asks for '.ce'
if (ext && path != local_path && fd.is_file(path)) {
var fn = resolve_mod_fn(path, ctx)
return {path: path, scope: SCOPE_LOCAL, symbol:fn}
}
}
// 2. Check installed packages (if path suggests a package import)
// This handles imports like 'prosperon/sprite'
var canonical_pkg = get_normalized_package(path, ctx)
if (canonical_pkg) {
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}
}
}
// 3. Check core (as a fallback)
// "core" is now just another package, potentially.
// But if the user really wants to load "time", "js", etc, which are in core.
// We can try to resolve them in the core package.
// Ideally 'core' is defined in dependencies if needed, or we hardcode a fallback.
// Hardcoded fallback for now to match behavior:
var core_dir = Shop.get_core_dir()
var core_file_path = core_dir + '/' + path + ext
if (fd.is_file(core_file_path)) {
// Core is treated as a package now, essentially
var fn = resolve_mod_fn(core_file_path, 'core') // using 'core' string as package for now
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_sym_base = c_sym_path(path)
// Candidates for symbol names
function symbol_candidates(pkg_path, mod_sym) {
var variants = []
// if pkg_path is 'gitea.pockle.world/john/prosperon', we want:
// js_gitea_pockle_world_john_prosperon_mod_use
// and maybe with leading slash?
var paths = [pkg_path]
// if (!pkg_path.startsWith('/')) paths.push('/' + pkg_path) // unlikely to need slash variant for standard pkgs
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
}
// 1. Check internal symbols (statically linked)
// If we have a package context, we check package-prefixed symbols
// If not, we look for 'js_local_...' or 'js_path_use' directly?
if (package_context) {
// Check package internal symbols
var variants = symbol_candidates(package_context, local_sym_base)
for (var i = 0; i < variants.length; i++) {
if (os.internal_exists(variants[i])) {
return {
symbol: function() { return os.load_internal(variants[i]); },
scope: SCOPE_PACKAGE,
path: variants[i]
}
}
}
// In static_only mode, skip dynamic library lookups
if (!static_only) {
// Then try dynamic library
// Use ./libcellmod for local?
var local_dl_name = './libcellmod' + dylib_ext
// Or check package context dir if it's a local package path?
if (package_context && package_context.startsWith('/')) {
local_dl_name = package_context + '/libcellmod' + 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(package_context, local_sym_base) : [`js_local_${local_sym_base}_use`]
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
};
}
}
}
}
} else {
// Local context
var local_sym = `js_local_${local_sym_base}_use`
if (os.internal_exists(local_sym)) {
return {
symbol: function() { return os.load_internal(local_sym); },
scope: SCOPE_LOCAL,
path: local_sym
}
}
}
// 2. Check if valid package import (e.g. 'prosperon/sprite')
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)
for (var i = 0; i < sym_names.length; i++) {
if (os.internal_exists(sym_names[i])) {
return {
symbol: function() { return os.load_internal(sym_names[i]); },
scope: SCOPE_PACKAGE,
package: canon_pkg,
path: sym_names[i]
}
}
}
// Then try dynamic library for package (skip in static_only mode)
if (!static_only) {
// Check package dir for libcellmod
var pkg_build_dir = get_modules_dir() + '/' + canon_pkg
var dl_path = pkg_build_dir + '/libcellmod' + 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
}
}
}
}
}
}
}
// 3. Check Core/General fallback (js_path_use)
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 we have a script, it always takes precedence (containing the C symbol if available)
// This supports "Hybrid" modules where JS wraps C.
if (mod_resolve.scope < 900) {
var context = null
if (c_resolve.scope < 900) {
context = c_resolve.symbol(null, $_)
}
used = mod_resolve.symbol.call(context, $_)
} else if (c_resolve.scope < 900) {
// C only
used = c_resolve.symbol(null, $_)
} else {
throw new Error(`Module ${info.path} could not be found`)
} 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)
}
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 <pkg>`
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 = '.'
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<files.length; i++) {
var file = files[i]
if (file.endsWith('.c') || file.endsWith('.cpp')) {
objects.push(build_dir + '/' + file + '.o')
}
}
return objects
}
Shop.compile_module = function(alias) {
var module_dir = Shop.get_module_dir(alias)
if (!module_dir) {
log.error("Module not found: " + alias)
return false
}
log.console("Would compile module: " + alias + " from " + module_dir)
return true
}
var debug = use('debug')
Shop.module_reload = function(path, package) {
if (!Shop.is_loaded(path,package)) return
var info = resolve_module_info(path, package)
if (!info) return
var cache_key = info.cache_key
var old = use_cache[cache_key]
var newmod = get_module(path, package)
for (var i in newmod)
old[i] = newmod[i]
for (var i in old) {
if (!(i in newmod))
old[i] = null
}
}
// Filter out main*.c files (they're only for static builds)
function is_main_file(file) {
var basename = file
var slash = file.lastIndexOf('/')
if (slash >= 0) basename = file.substring(slash + 1)
if (basename == 'main.c') return true
if (basename.startsWith('main_') && basename.endsWith('.c')) return true
return false
}
// Get the core build directory for linking
function get_core_build_dir() {
return get_global_build_dir() + '/core'
}
Shop.build_package = function(package)
{
if (package == 'local') package = null
if (package) package = Shop.parse_package(package).path
var files = Shop.list_files(package)
var build_dir = get_build_dir(package)
ensure_dir(build_dir)
var module_dir = package ? get_modules_dir() + '/' + package : (current_package_path || '.')
log.console(`Building package ${package ? package : 'local'} to ${build_dir}`)
// For C compilation
var config = Shop.load_config(package) || {}
var cflags = get_flags(config, platform, 'CFLAGS')
// Determine usage prefix for C symbols
var use_prefix
if (!package) use_prefix = 'js_local_'
else use_prefix = 'js_' + package.replace(/\//g, '_').replace(/\./g, '_').replace(/-/g, '_') + '_'
var c_objects = []
// Use get_hash from build_utils
var get_hash = build_utils.get_hash
for (var i=0; i<files.length; i++) {
var file = files[i]
// Skip main*.c files - they're only for static builds
if (is_main_file(file)) continue
var src_path
if (!package) {
src_path = (current_package_path || '.') + '/' + file
} else if (package.startsWith('/')) {
// Local absolute package
src_path = package + '/' + file
} else {
// Regular package
src_path = get_modules_dir() + '/' + package + '/' + file
}
if (file.endsWith('.cm') || file.endsWith('.ce')) {
// Compile module
try {
resolve_mod_fn(src_path, package)
} catch (e) {
log.error(`Failed to compile ${src_path}: ${e}`)
log.error(e)
return false
}
} else if (file.endsWith('.c') || file.endsWith('.cpp')) {
// Compile C
var obj_path = build_dir + '/' + file + '.o'
var meta_path = obj_path + '.meta'
ensure_dir(obj_path.substring(0, obj_path.lastIndexOf('/')))
var safe_path = c_sym_path(file.substring(0, file.lastIndexOf('.')))
var use_name = use_prefix + safe_path + '_use'
var comp_src = file
var comp_obj = file + '.o'
var base_cmd = 'cc -fPIC '
var compile_flags = '-c ' + comp_src + ' -O3 -DCELL_USE_NAME=' + use_name
if (cflags != '') compile_flags += ' ' + cflags
var full_compile_cmd = 'cd ' + module_dir + ' && ' + base_cmd + compile_flags + ' -o ' + comp_obj
var cmd_hash = get_hash(full_compile_cmd)
var needs_compile = true
var meta = null
if (fd.is_file(obj_path) && fd.is_file(meta_path)) {
try {
meta = json.decode(text(fd.slurp(meta_path)))
} catch(e) {}
if (meta && meta.cmd_hash == cmd_hash) {
var st_src = fd.stat(src_path)
var st_obj = fd.stat(obj_path)
if (st_src && st_obj && st_src.mtime <= st_obj.mtime) {
needs_compile = false
// Check headers
if (meta.headers) {
for (var h = 0; h < meta.headers.length; h++) {
var header_rel = meta.headers[h]
var header_full
if (header_rel.startsWith('/'))
header_full = header_rel
else
header_full = module_dir + '/' + header_rel
if (!fd.is_file(header_full)) {
log.console(`coulnd't find header ${header_full}`)
needs_compile = true; break;
}
var st_h = fd.stat(header_full)
if (st_h.mtime > 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<parts.length; p++) {
var part = parts[p].trim()
if (part == '' || part.endsWith(':')) continue
if (part == comp_src) continue
headers.push(part)
}
fd.rm(deps_full_path)
}
// 2. Compile
var ret = os.system(full_compile_cmd)
if (ret != 0) {
log.error("Compilation failed for " + src_path)
return false
}
// 3. Move object
os.system('mv ' + module_dir + '/' + comp_obj + ' ' + obj_path)
// 4. Write meta
var new_meta = {
cmd_hash: cmd_hash,
headers: headers
}
fd.slurpwrite(meta_path, utf8.encode(json.encode(new_meta)))
}
c_objects.push(obj_path)
}
}
// Link if there are C objects
if (c_objects.length > 0) {
var lib_name = build_dir + '/libcellmod' + dylib_ext
var lib_meta_path = lib_name + '.meta'
var link_flags = '-fPIC -shared'
// Link against core cellmod.dylib
var core_build = get_core_build_dir()
if (platform == 'macOS') {
link_flags += ' -L' + core_build + ' -Wl,-rpath,@loader_path/../../core'
} else if (platform == 'Linux' || platform == 'linux') {
link_flags += ' -L' + core_build + ' -Wl,-rpath,$ORIGIN/../../core'
} else if (platform == 'Windows') {
link_flags += ' -L' + core_build
}
link_flags += ' -lcellmod'
var ldflags = get_flags(config, platform, 'LDFLAGS')
if (ldflags != '') link_flags += ' ' + ldflags
var temp_lib = 'libcellmod' + dylib_ext
var objs_str = ''
for (var i=0; i<c_objects.length; i++) {
objs_str += '"' + c_objects[i] + '" '
}
var link_cmd = 'cd ' + module_dir + ' && cc ' + link_flags + ' ' + objs_str + ' -lc -lc++ -o ' + temp_lib
var link_cmd_hash = get_hash(link_cmd)
// Check if we need to relink
var needs_link = true
if (fd.is_file(lib_name) && fd.is_file(lib_meta_path)) {
var meta = null
try {
meta = json.decode(text(fd.slurp(lib_meta_path)))
} catch(e) {}
if (meta && meta.cmd_hash == link_cmd_hash) {
var lib_time = fd.stat(lib_name).mtime
needs_link = false
for (var i=0; i<c_objects.length; i++) {
if (fd.stat(c_objects[i]).mtime > 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<files.length; i++) {
var f = files[i]
if (f.endsWith('.cm') || f.endsWith('.ce')) {
modules.push(f)
}
}
return modules
}
// Resolve a module path given a package context
// Returns { path, package_name } or null if not found
// Resolution order:
// 1. Local to the current package (if package_name is set)
// 2. Declared dependencies (by alias)
// 3. Core modules (handled by caller)
Shop.resolve_module = function(module_name, package_name, is_file_fn) {
var config = Shop.load_config()
var dependencies = (config && config.dependencies) || {}
// If we're in a package context, check the package first
if (package_name) {
var pkg_path = get_modules_dir() + '/' + package_name + '/' + module_name + '.cm'
if (is_file_fn(pkg_path)) {
return { path: pkg_path, package_name: package_name }
}
}
// Check if module_name contains a slash (explicit package reference)
if (module_name.includes('/')) {
var parts = module_name.split('/')
var pkg_alias = parts[0]
var sub_module = parts.slice(1).join('/')
// Check if it's a declared dependency
if (dependencies[pkg_alias]) {
// Need to resolve alias to canonical path
var pkg = dependencies[pkg_alias]
var parsed = Shop.parse_package(pkg)
var canonical_path = parsed.path
var dep_path = get_modules_dir() + '/' + canonical_path + '/' + sub_module + '.cm'
if (is_file_fn(dep_path)) {
return { path: dep_path, package_name: pkg_alias }
}
}
// Check local path (relative to current package)
var local_base = current_package_path || '.'
var local_path = local_base + '/' + module_name + '.cm'
if (is_file_fn(local_path)) {
return { path: local_path, package_name: null }
}
} else {
// Simple module name - check local first, then dependencies
var local_base = current_package_path || '.'
var local_path = local_base + '/' + module_name + '.cm'
if (is_file_fn(local_path)) {
return { path: local_path, package_name: null }
}
// Check each declared dependency for this module
for (var alias in dependencies) {
var pkg = dependencies[alias]
var parsed = Shop.parse_package(pkg)
var canonical_path = parsed.path
var dep_path = get_modules_dir() + '/' + canonical_path + '/' + module_name + '.cm'
if (is_file_fn(dep_path)) {
return { path: dep_path, package_name: alias }
}
}
}
return null
}
Shop.resolve_alias = function(name, ctx) {
var deps = Shop.dependencies(ctx)
if (deps && deps[name]) return { alias: name, pkg: Shop.get_canonical_package(name, ctx) }
for (var alias in deps) {
var pkg = deps[alias]
var parsed = Shop.parse_package(pkg)
if (parsed && (parsed.name == name || parsed.path == name)) {
return { alias: alias, pkg: parsed.path }
}
}
return null
}
Shop.get_canonical_package = get_canonical_package
Shop.upgrade_core = function() {
var pkg = "gitea.pockle.world/john/cell"
var hash = fetch_remote_hash(pkg)
if (!hash) {
// Fallback to master?
log.console("Could not fetch remote hash for core, assuming master.")
hash = "master"
}
var zip = get_or_download_zip(pkg, hash)
if (!zip) return false
var core_dir = Shop.get_core_dir()
install_zip(zip, core_dir)
var lock = Shop.load_lock()
lock['core'] = {
package: pkg,
commit: hash,
updated: time.number()
}
Shop.save_lock(lock)
log.console("Core updated to " + hash)
return true
}
Shop.link_core = function(target) {
if (!fd.is_dir(target)) return false
var core_dir = Shop.get_core_dir()
if (fd.is_link(core_dir)) fd.unlink(core_dir)
else if (fd.is_dir(core_dir)) {
// Safety check?
// For now, we assume user knows what they are doing.
// We can't easily rm recursive if it's a real dir unless we use system rm or implement it.
// shop.cm has rm_recursive? No, I implemented it?
// I saw `function rm_recursive` at line 1141. internal.
rm_recursive(core_dir)
}
fd.symlink(fd.realpath(target), core_dir)
var lock = Shop.load_lock()
lock['core'] = {
package: fd.realpath(target),
type: 'local',
updated: time.number()
}
Shop.save_lock(lock)
return true
}
Shop.unlink_core = function() {
var core_dir = Shop.get_core_dir()
if (fd.is_link(core_dir)) {
fd.unlink(core_dir)
return true
}
return false
}
Shop.is_core_linked = function() {
var core_dir = Shop.get_core_dir()
return fd.is_link(core_dir)
}
Shop.list_shop_packages = function() {
var lock = Shop.load_lock()
var list = []
for (var k in lock) {
if (lock[k]) list.push(lock[k])
}
return list
}
return Shop