UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

238 lines 9.2 kB
/** * Package Registry Coordinator * * Orchestrates resolution and fetching across all PackageRegistryAdapters. * Priority order: local-cache lookup → gitea shorthand → github shorthand → git URL * * @implements #557 */ import { readFile } from 'fs/promises'; import { join } from 'path'; import { GitAdapter, detectManifestType } from './adapters/git.js'; import { GiteaAdapter } from './adapters/gitea.js'; import { GitHubAdapter } from './adapters/github.js'; import { ClawHubPackageAdapter } from './adapters/clawhub.js'; import { LocalCacheAdapter } from './adapters/local-cache.js'; import { setPackageEntry, listPackages as listFromRegistry, removePackageEntry, } from './package-registry.js'; /** * All adapters in resolution priority order * (Scheme-prefixed adapters first so explicit prefixes are matched before * Gitea/GitHub shorthands and the generic Git fallback) */ const ALL_ADAPTERS = [ new ClawHubPackageAdapter(), new GiteaAdapter(), new GitHubAdapter(), new GitAdapter(), ]; const CACHE_ADAPTER = new LocalCacheAdapter(); /** * Parse a raw reference string into a PackageRef * * Supported formats: * owner/name → gitea shorthand * owner/name@v1.2.0 → gitea shorthand with version * github:owner/name → github shorthand * github:owner/name@v1.2.0 → github shorthand with version * https://... → direct git URL * git@host:owner/name.git → direct SSH URL */ export function parseRef(raw) { const ref = { raw, scheme: 'unknown' }; // Scheme-prefixed: "github:owner/name[@version]" if (raw.startsWith('github:')) { const body = raw.slice('github:'.length); const [repoAndOwner, version] = body.split('@'); const parts = (repoAndOwner ?? '').split('/'); ref.scheme = 'github'; ref.owner = parts[0]; ref.name = parts.slice(1).join('/') || undefined; ref.version = version; return ref; } // Scheme-prefixed: "clawhub:owner/name[@version]" and "openclaw:owner/name[@version]" if (raw.startsWith('clawhub:') || raw.startsWith('openclaw:')) { const prefix = raw.startsWith('clawhub:') ? 'clawhub:' : 'openclaw:'; const body = raw.slice(prefix.length); const [repoAndOwner, version] = body.split('@'); const parts = (repoAndOwner ?? '').split('/'); ref.scheme = 'clawhub'; ref.owner = parts[0]; ref.name = parts.slice(1).join('/') || undefined; ref.version = version; return ref; } // Direct URL if (raw.startsWith('https://') || raw.startsWith('http://') || raw.startsWith('git@') || raw.startsWith('ssh://')) { ref.scheme = raw.startsWith('git@') ? 'ssh' : 'https'; // Strip optional @version suffix from URL (non-standard but convenient) const atIdx = raw.lastIndexOf('@'); if (atIdx > raw.indexOf('://') + 3 || raw.startsWith('git@')) { // Only treat trailing @version if it looks like a version tag const tail = raw.slice(atIdx + 1); if (/^[vV]?\d|^main$|^master$|^HEAD/.test(tail) && atIdx > 20) { ref.rawUrl = raw.slice(0, atIdx); ref.version = tail; return ref; } } ref.rawUrl = raw; return ref; } // Gitea shorthand: "owner/name[@version]" const atIdx = raw.indexOf('@'); const body = atIdx >= 0 ? raw.slice(0, atIdx) : raw; const parts = body.split('/'); ref.scheme = 'gitea'; ref.owner = parts[0]; ref.name = parts.slice(1).join('/') || undefined; ref.version = atIdx >= 0 ? raw.slice(atIdx + 1) : undefined; return ref; } /** * Resolve a ref to a PackageSource using the appropriate adapter */ export async function resolveRef(ref) { for (const adapter of ALL_ADAPTERS) { if (!adapter.canResolve(ref.raw)) continue; const source = await adapter.resolve(ref); if (source) return { source, adapter }; } return null; } /** * Read the namespace for a cached package. * * Resolution order: * 1. `namespace` field in `manifest.json` (explicit — highest priority) * 2. Owner segment parsed from `registryKey` (e.g. `roko/ring-methodology` → `roko`) * Also handles scheme-prefixed keys: `clawhub:author/name` → `author`, * `github:thirdparty/repo` → `thirdparty`. * 3. `"third-party"` — safe fallback when the key cannot be parsed. * * AIWG-owned packages (owner = `aiwg`) return `"aiwg"` which is the default * namespace used by the AIWG deploy pipeline. * * @param cachePath - Absolute path to the cloned/cached package directory * @param registryKey - The key stored in packages.yaml, e.g. `"owner/name"` or * `"github:owner/name"` or `"clawhub:owner/name"` */ export async function readPackageNamespace(cachePath, registryKey) { // 1. Prefer explicit manifest.json namespace field try { const manifestContent = await readFile(join(cachePath, 'manifest.json'), 'utf-8'); const manifest = JSON.parse(manifestContent); if (typeof manifest.namespace === 'string' && manifest.namespace.trim()) { return manifest.namespace.trim(); } } catch { // manifest missing or unreadable — fall through to key-based derivation } // 2. Derive from registry key owner segment // Strip leading scheme prefix: "github:", "clawhub:", "openclaw:" const schemeMatch = registryKey.match(/^(?:github|clawhub|openclaw):(.+)$/); const keyBody = schemeMatch ? schemeMatch[1] : registryKey; // Handle direct URLs — cannot derive meaningful owner if (keyBody.startsWith('https://') || keyBody.startsWith('http://') || keyBody.startsWith('git@') || keyBody.startsWith('ssh://')) { return 'third-party'; } // owner/name format const slashIdx = keyBody.indexOf('/'); if (slashIdx > 0) { const owner = keyBody.slice(0, slashIdx).trim(); if (owner) return owner; } // 3. Fallback return 'third-party'; } /** * Install a package from a ref string * * 1. Parse ref * 2. Resolve to PackageSource via adapters * 3. Fetch (clone/pull) to local cache * 4. Register in ~/.aiwg/packages.yaml * * Returns the cache path and resolved namespace. */ export async function installPackage(rawRef, options = {}) { const ref = parseRef(rawRef); const resolved = await resolveRef(ref); if (!resolved) { throw new Error(`Cannot resolve package reference: '${rawRef}'\n` + `Supported formats:\n` + ` owner/name (Gitea shorthand)\n` + ` github:owner/name (GitHub shorthand)\n` + ` clawhub:owner/name (ClawHub / OpenClaw registry)\n` + ` openclaw:owner/name (ClawHub / OpenClaw registry alias)\n` + ` https://... (direct Git URL)\n` + ` git@host:owner/name.git (SSH URL)`); } const { source, adapter } = resolved; const cachePath = await adapter.fetch(source, { refresh: options.refresh }); // Detect type from manifest.json const type = await detectManifestType(cachePath); // Build registry key const key = ref.owner && ref.name ? `${ref.owner}/${ref.name}` : source.label.replace(/https?:\/\/[^/]+\//, '').replace(/\.git$/, ''); const version = ref.version ?? source.ref ?? 'latest'; // Resolve namespace for artifact deployment isolation (#804) const namespace = await readPackageNamespace(cachePath, rawRef); // Register in packages.yaml await setPackageEntry(key, { version, source: source.gitUrl, type, cachePath, installedAt: new Date().toISOString(), deployedTo: [], }, options.configDir); return { cachePath, key, type, namespace }; } /** * Refresh all registered remote packages (used by `aiwg sync`) */ export async function refreshAllPackages(options = {}) { const packages = await listFromRegistry(options.configDir); const refreshed = []; for (const pkg of packages) { try { await installPackage(pkg.source.startsWith('git@') || pkg.source.startsWith('https://') ? pkg.source : pkg.source, { refresh: true, configDir: options.configDir }); refreshed.push(pkg.key); } catch { // Non-fatal — continue with other packages } } return refreshed; } /** * List all installed packages */ export async function listInstalledPackages(configDir) { return listFromRegistry(configDir); } /** * Remove a package from the registry (does not delete cache) */ export async function uninstallPackage(key, configDir) { return removePackageEntry(key, configDir); } /** * Look up the cache path for an installed package by name * (used by `aiwg use` to resolve local packages before bundled npm) */ export async function resolveInstalledPackage(name) { return CACHE_ADAPTER.resolveCachePath(name); } //# sourceMappingURL=registry.js.map