This commit is contained in:
2025-12-03 09:11:51 -06:00
parent ee3a890d4a
commit ddfa636ac0
6 changed files with 345 additions and 176 deletions

View File

@@ -1,29 +1,19 @@
// cell get <locator> - Fetch a module and add it to dependencies
// cell get <locator> [alias] - Add and install a package with its dependencies
var fd = use('fd')
var shop = use('shop')
if (args.length < 1) {
log.console("Usage: cell get <locator> [alias]")
log.console("Examples:")
log.console(" cell get git.world/jj/mod@v0.6.3")
log.console(" cell get git.world/jj/mod (uses head/master)")
log.console(" cell get gitea.pockle.world/john/prosperon@main")
log.console(" cell get github.com/user/repo@v1.0.0 myalias")
$_.stop()
return
}
var locator = args[0]
var parsed = shop.parse_locator(locator)
var alias = args.length > 1 ? args[1] : null
// Use the module name as the default alias
var alias = parsed.name
if (args.length > 1)
alias = args[1]
if (!alias)
throw new Error("Failed to determine alias");
log.console("Adding dependency: " + alias + " = " + locator)
shop.add_dependency(alias, locator)
shop.get(locator, alias)
$_.stop()

View File

@@ -12,7 +12,7 @@ json.encode = function encode(val,replacer,space = 1,whitelist)
// The optional reviver input is a method that will be called for every key and value at every level of the result. Each value will be replaced by the result of the reviver function. This can be used to reform data-only records into method-bearing records, or to transform date strings into seconds.
json.decode = function decode(text,reviver)
{
if (typeof json_in != 'string')
if (typeof text != 'string')
throw new Error("couldn't parse text: not a string")
return JSON.parse(text,reviver)
}

View File

@@ -1,7 +1,6 @@
// cell remove <alias> - Remove a module from dependencies
// cell remove <alias> - Remove a package from dependencies
var shop = use('shop')
var fd = use('fd')
if (args.length < 1) {
log.console("Usage: cell remove <alias>")
@@ -9,35 +8,6 @@ if (args.length < 1) {
return
}
var alias = args[0]
// Check if cell.toml exists
if (!fd.stat('.cell/cell.toml').isFile) {
log.error("No cell.toml found.")
$_.stop()
return
}
// Get module directory before removing dependency
var module_dir = shop.get_module_dir(alias)
// Remove from dependencies
if (shop.remove_dependency(alias)) {
log.console("Removed dependency: " + alias)
// Remove module directory
if (module_dir && fd.stat(module_dir).isDirectory) {
log.console("Removing module directory: " + module_dir)
try {
fd.rmdir(module_dir)
} catch (e) {
log.error("Failed to remove module directory: " + e)
}
} else {
log.console("Module directory not found or already removed.")
}
} else {
log.error("Dependency not found: " + alias)
}
shop.remove(args[0])
$_.stop()

View File

@@ -152,42 +152,34 @@ Shop.save_config = function(config) {
slurpwrite(shop_path, toml.encode(config));
}
function lock_path(pkg)
{
if (pkg)
return `.cell/modules/${pkg}/.cell/lock.toml`
else
return '.cell/lock.toml'
}
// Load lock.toml configuration
Shop.load_lock = function(pkg) {
var path = lock_path(pkg)
Shop.load_lock = function() {
var path = '.cell/lock.toml'
if (!fd.is_file(path))
return null
return {}
var content = text(fd.slurp(lock_path))
var content = text(fd.slurp(path))
if (!content.length) return {}
return toml.decode(content)
}
// Save lock.toml configuration
Shop.save_lock = function(pkg, lock) {
var path = lock_path(pkg)
slurpwrite(path, toml.encode(lock));
Shop.save_lock = function(lock) {
slurpwrite('.cell/lock.toml', toml.encode(lock));
}
// Initialize .cell directory structure
Shop.init = function() {
if (!fd.is_directory('.cell')) {
if (!fd.is_dir('.cell')) {
fd.mkdir('.cell')
}
if (!fd.is_directory('.cell/modules')) {
if (!fd.is_dir('.cell/modules')) {
fd.mkdir('.cell/modules')
}
if (!fd.is_directory('.cell/build')) {
if (!fd.is_dir('.cell/build')) {
fd.mkdir('.cell/build')
}
@@ -230,46 +222,25 @@ Shop.parse_locator = function(locator) {
}
// Convert module locator to download URL
Shop.get_download_url = function(locator) {
Shop.get_download_url = function(locator, commit_hash) {
var parsed = Shop.parse_locator(locator)
if (!parsed) return null
// Handle different git hosting patterns
if (locator.startsWith('https://')) {
// Remove https:// prefix for parsing
var cleanLocator = locator.substring(8)
var hostAndPath = cleanLocator.split('@')[0]
if (parsed.path.includes('gitea.')) {
var parts = parsed.path.split('/')
var host = parts[0]
var user = parts[1]
var repo = parts[2]
// Gitea pattern: gitea.pockle.world/user/repo@branch
if (hostAndPath.includes('gitea.')) {
return 'https://' + hostAndPath + '/archive/' + parsed.version + '.zip'
if (!commit_hash) {
log.error("No commit hash available for download URL")
return null
}
// GitHub pattern: github.com/user/repo@tag
if (hostAndPath.includes('github.com')) {
return 'https://' + hostAndPath + '/archive/refs/tags/' + parsed.version + '.zip'
}
// GitLab pattern: gitlab.com/user/repo@tag
if (hostAndPath.includes('gitlab.')) {
return 'https://' + hostAndPath + '/-/archive/' + parsed.version + '/' + parsed.name + '-' + parsed.version + '.zip'
}
} else {
// Implicit https
var hostAndPath = parsed.path
// Gitea pattern: gitea.pockle.world/user/repo@branch
if (hostAndPath.includes('gitea.')) {
return 'https://' + hostAndPath + '/archive/' + parsed.version + '.zip'
}
// GitHub pattern: github.com/user/repo@tag
if (hostAndPath.includes('github.com')) {
return 'https://' + hostAndPath + '/archive/refs/tags/' + parsed.version + '.zip'
}
return 'https://' + host + '/' + user + '/' + repo + '/archive/' + commit_hash + '.zip'
}
// Fallback to original locator if no pattern matches
return locator
return null
}
// Add a dependency
@@ -310,63 +281,31 @@ Shop.remove_dependency = function(alias) {
Shop.get_api_url = function(locator) {
var parsed = Shop.parse_locator(locator)
if (!parsed) return null
var hostAndPath = parsed.path
if (locator.startsWith('https://')) {
hostAndPath = locator.substring(8).split('@')[0]
}
var parts = hostAndPath.split('/')
var parts = parsed.path.split('/')
// Gitea pattern: gitea.pockle.world/user/repo@branch
if (hostAndPath.includes('gitea.')) {
if (parsed.path.includes('gitea.')) {
var host = parts[0]
var user = parts[1]
var repo = parts[2]
return 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/' + parsed.version
var url = 'https://' + host + '/api/v1/repos/' + user + '/' + repo + '/branches/'
if (parsed.version) url += parsed.version
return url
}
// GitHub pattern: github.com/user/repo@tag or @branch
if (hostAndPath.includes('github.com')) {
var user = parts[1]
var repo = parts[2]
// Try branch first, then tag
return 'https://api.github.com/repos/' + user + '/' + repo + '/branches/' + parsed.version
}
// GitLab pattern: gitlab.com/user/repo@tag
if (hostAndPath.includes('gitlab.')) {
var user = parts[1]
var repo = parts[2]
var projectId = encodeURIComponent(user + '/' + repo)
return 'https://' + parts[0] + '/api/v4/projects/' + projectId + '/repository/branches/' + parsed.version
}
// Fallback - return null if no API pattern matches
return null
}
// Extract commit hash from API response
Shop.extract_commit_hash = function(locator, response) {
if (!response) return null
var data
try {
data = json.decode(response)
} catch (e) {
log.console("Failed to parse API response: " + e)
return null
}
// Handle different git hosting response formats
var data = json.decode(response)
if (locator.includes('gitea.')) {
// Gitea: response.commit.id
return data.commit && data.commit.id
} else if (locator.includes('github.com')) {
// GitHub: response.commit.sha
return data.commit && data.commit.sha
} else if (locator.includes('gitlab.')) {
// GitLab: response.commit.id
if (Array.isArray(data))
data = data[0]
return data.commit && data.commit.id
}
@@ -449,7 +388,7 @@ Shop.install = function(alias) {
var count = zip.count()
for (var i = 0; i < count; i++) {
if (zip.is_directory(i)) continue
if (zip.is_dir(i)) continue
var filename = zip.get_filename(i)
// Strip top-level directory
@@ -467,19 +406,9 @@ Shop.install = function(alias) {
}
}
// 4. Update Lock
if (commit_hash) {
var lock = Shop.load_lock()
lock[alias] = {
locator: locator,
commit: commit_hash,
updated: time.number()
}
Shop.save_lock(lock)
}
// 4. Update Lock (only for root package)
log.console("Installed " + alias)
return true
return { commit: commit_hash, locator: locator }
}
// Verify dependencies
@@ -620,36 +549,309 @@ Shop.use = function(path, package_context) {
Shop.resolve_locator = resolve_locator
// Check for updates
Shop.update = function() {
var config = Shop.load_config()
if (!config || !config.dependencies) return
// Install a package and all its transitive dependencies
// This is the internal workhorse - installs from a specific package context
function install_package_deps(canonical_name, installed) {
installed = installed || {}
var lock = Shop.load_lock()
// Load the package's config to find its dependencies
var pkg_config = Shop.load_config(canonical_name)
if (!pkg_config || !pkg_config.dependencies) return installed
for (var alias in config.dependencies) {
var locator = config.dependencies[alias]
var api_url = Shop.get_api_url(locator)
for (var alias in pkg_config.dependencies) {
var locator = pkg_config.dependencies[alias]
var parsed = Shop.parse_locator(locator)
var dep_canonical = parsed.path
// Skip if already installed in this run
if (installed[dep_canonical]) continue
// Check if already exists on disk
var target_dir = '.cell/modules/' + dep_canonical
if (fd.is_dir(target_dir)) {
log.console(" " + alias + " already installed")
installed[dep_canonical] = true
// Still recurse into its deps
install_package_deps(dep_canonical, installed)
continue
}
// Install this dependency
log.console(" Installing transitive dependency: " + alias + " (" + locator + ")")
var result = install_from_locator(locator)
if (result) {
installed[dep_canonical] = true
// Recurse into this package's dependencies
install_package_deps(dep_canonical, installed)
}
}
return installed
}
// Install from a raw locator (not from config)
function install_from_locator(locator, locked_hash) {
var parsed = Shop.parse_locator(locator)
var target_dir = '.cell/modules/' + parsed.path
// 1. Get Commit Hash - use locked hash if provided, otherwise fetch
var commit_hash = locked_hash
if (!commit_hash) {
var api_url = Shop.get_api_url(locator)
if (api_url) {
try {
var resp = http.fetch(api_url)
var resp_text = text(resp)
var remote_hash = Shop.extract_commit_hash(locator, resp_text)
var local_hash = lock[alias] ? lock[alias].commit : null
if (remote_hash && remote_hash != local_hash) {
log.console("Update available for " + alias + ": " + local_hash + " -> " + remote_hash)
Shop.install(alias)
} else {
log.console(alias + " is up to date.")
}
commit_hash = Shop.extract_commit_hash(locator, resp_text)
} catch (e) {
log.error("Failed to check update for " + alias + ": " + e)
log.console("Warning: Failed to fetch API info: " + e)
}
}
} else {
log.console("Using locked commit: " + commit_hash)
}
// 2. Download Zip
var download_url = Shop.get_download_url(locator, commit_hash)
if (!download_url) {
log.error("Could not determine download URL for " + locator)
return null
}
log.console("Downloading from " + download_url)
var zip_blob
try {
zip_blob = http.fetch(download_url)
} catch (e) {
log.error("Download failed: " + e)
return null
}
// 3. Unpack
log.console("Unpacking to " + target_dir)
ensure_dir(target_dir)
var zip = miniz.read(zip_blob)
if (!zip)
throw new Error("Failed to read zip archive")
var count = zip.count()
log.console(`zip contains ${count} entries`)
for (var i = 0; i < count; i++) {
if (zip.is_dir(i)) continue
var filename = zip.get_filename(i)
log.console(filename)
var parts = filename.split('/')
if (parts.length > 1) {
parts.shift()
var rel_path = parts.join('/')
var full_path = target_dir + '/' + rel_path
var dir_path = full_path.substring(0, full_path.lastIndexOf('/'))
ensure_dir(dir_path)
var content = zip.slurp(filename)
slurpwrite(full_path, content)
}
}
return { commit: commit_hash, locator: locator, path: parsed.path }
}
// High-level: Add a package, install it, and install all transitive dependencies
// Like `bun add` or `npm install <pkg>`
Shop.get = function(locator, alias) {
Shop.init()
var parsed = Shop.parse_locator(locator)
if (!alias) alias = parsed.name
log.console("Adding dependency: " + alias + " = " + locator)
// Add to config
var config = Shop.load_config() || { dependencies: {} }
if (!config.dependencies) config.dependencies = {}
config.dependencies[alias] = locator
Shop.save_config(config)
// Install the package
var result = install_from_locator(locator)
if (!result) {
log.error("Failed to install " + alias)
return false
}
// Update lock file for root
var lock = Shop.load_lock(null)
lock[alias] = {
locator: locator,
commit: result.commit,
updated: time.number()
}
Shop.save_lock(lock)
log.console("Installed " + alias)
// Install transitive dependencies
log.console("Resolving transitive dependencies...")
install_package_deps(parsed.path, {})
log.console("Done.")
return true
}
// High-level: Update a specific package or all packages
// Like `bun update` or `bun update <pkg>`
Shop.update_all = function(alias) {
var config = Shop.load_config()
if (!config || !config.dependencies) {
log.console("No dependencies to update.")
return
}
var lock = Shop.load_lock()
var to_update = alias ? [alias] : Object.keys(config.dependencies)
for (var i = 0; i < to_update.length; i++) {
var dep_alias = to_update[i]
var locator = config.dependencies[dep_alias]
if (!locator) {
log.error("Dependency not found: " + dep_alias)
continue
}
var api_url = Shop.get_api_url(locator)
if (!api_url) {
log.console(dep_alias + ": cannot check for updates (no API URL)")
continue
}
try {
var parsed = Shop.parse_locator(locator)
var target_dir = `.cell/modules/${parsed.path}`
var resp = http.fetch(api_url)
var resp_text = text(resp)
var remote_hash = Shop.extract_commit_hash(locator, resp_text)
var local_hash = lock[dep_alias] ? lock[dep_alias].commit : null
if (!fd.is_dir(target_dir) || remote_hash != local_hash) {
log.console(dep_alias + ": updating " + (local_hash ? local_hash.substring(0,8) : "(new)") + " -> " + remote_hash.substring(0,8))
// Remove old directory
if (fd.is_dir(target_dir))
fd.rmdir(target_dir)
// Reinstall
var result = install_from_locator(locator, remote_hash)
if (result) {
lock[dep_alias] = {
locator: locator,
commit: result.commit,
updated: time.number()
}
// Reinstall transitive deps
install_package_deps(parsed.path, {})
}
} else {
log.console(dep_alias + ": up to date")
}
} catch (e) {
log.error("Failed to check " + dep_alias)
log.error(e)
}
}
Shop.save_lock(lock)
log.console("Update complete.")
}
// High-level: Remove a package and clean up
// Like `bun remove`
Shop.remove = function(alias) {
var config = Shop.load_config()
if (!config || !config.dependencies || !config.dependencies[alias]) {
log.error("Dependency not found: " + alias)
return false
}
var locator = config.dependencies[alias]
var parsed = Shop.parse_locator(locator)
var target_dir = '.cell/modules/' + parsed.path
// Remove from config
delete config.dependencies[alias]
Shop.save_config(config)
// Remove from lock
var lock = Shop.load_lock()
delete lock[alias]
Shop.save_lock(lock)
// Remove directory
if (fd.is_dir(target_dir)) {
log.console("Removing " + target_dir)
try {
fd.rmdir(target_dir)
} catch (e) {
log.error("Failed to remove directory: " + e)
}
}
log.console("Removed " + alias)
return true
}
// Install all dependencies from config (like `bun install`)
Shop.install_all = function() {
Shop.init()
var config = Shop.load_config()
if (!config || !config.dependencies) {
log.console("No dependencies to install.")
return true
}
var lock = Shop.load_lock(null)
var installed = {}
for (var alias in config.dependencies) {
var locator = config.dependencies[alias]
var parsed = Shop.parse_locator(locator)
var target_dir = '.cell/modules/' + parsed.path
// Check if already installed
if (fd.is_dir(target_dir)) {
log.console(alias + ": already installed")
installed[parsed.path] = true
continue
}
log.console("Installing " + alias + "...")
var locked_hash = lock[alias] ? lock[alias].commit : null
var result = install_from_locator(locator, locked_hash)
if (result) {
installed[parsed.path] = true
lock[alias] = {
locator: locator,
commit: result.commit,
updated: time.number()
}
}
}
// Now install transitive dependencies for all root deps
log.console("Resolving transitive dependencies...")
for (var alias in config.dependencies) {
var locator = config.dependencies[alias]
var parsed = Shop.parse_locator(locator)
install_package_deps(parsed.path, installed)
}
Shop.save_lock(lock)
log.console("Done.")
return true
}
// Compile a module

View File

@@ -137,6 +137,7 @@ function text(...arguments) {
// Default: interpret as UTF-8 text
// Use the utf8 module to decode the blob
if (arg.length == 0) return ""
return utf8.decode(arg);
}

View File

@@ -1,4 +1,10 @@
// cell update [alias] - Update packages to latest versions
var shop = use('shop')
var alias = args.length > 0 ? args[0] : null
log.console("Checking for updates...")
shop.update()
shop.update_all(alias)
$_.stop()