import { execSync } from 'node:child_process'; import { existsSync, statSync, writeFileSync } from 'node:fs'; import { copyFile, mkdir, readFile, readdir } from 'node:fs/promises'; import { basename, dirname, join, relative } from 'node:path'; import { eq } from 'drizzle-orm'; import { z } from 'zod'; import { getWellKnownCapability, isWellKnown } from '../capabilities/well-known'; import { log } from '../cli/prompts'; import { getModuleStoragePath } from '../config/paths'; import { type DbClient, getDb } from '../db/client'; import { capabilities, moduleIntegrity, modules } from '../db/schema'; import type { NewModule, NewModuleIntegrity } from '../db/schema'; import { type ModuleManifest, getSingularSystemSpec } from '../manifest/schema'; import { SUBMODULES_DIR, validateCapabilityNames, validateDeriveFromSources, validateHookContract, validateManifest, validatePrivilegedCapabilities, validateProvidesNoCrossCapabilityRefs, validateSubmoduleDeclaration, validateSubmoduleManifest, validateVariableSources, validateZoneRequirements, } from '../manifest/validate'; import { parseJsonWithValidation } from '../validation/schemas'; import { cleanupTempDir, extractPackage, verifyPackageIntegrity } from './packaging/extract'; import { classifyModulePath } from './packaging/package-rules'; /** * Phase-timing helper for `celilo module import`. Set CELILO_IMPORT_DEBUG=1 * to get a phase-by-phase breakdown to stderr — useful when an import is * inexplicably slow (e.g. a Docker bind-mount fsync amplification, a stale * lockfile triggering a fresh bun install, etc.). * * Zero overhead when disabled: the wrapper just calls the inner fn. */ async function timedPhase(label: string, fn: () => Promise | T): Promise { if (!process.env.CELILO_IMPORT_DEBUG) return await fn(); const start = Date.now(); try { return await fn(); } finally { const ms = Date.now() - start; process.stderr.write(`[import-timing] ${label.padEnd(36)} ${ms}ms\n`); } } /** * Module import options */ export interface ModuleImportOptions { sourcePath: string; targetBasePath?: string; db?: DbClient; flags?: Record; } /** * Module import result */ export interface ModuleImportSuccess { success: true; moduleId: string; targetPath: string; } export interface ModuleImportError { success: false; error: string; details?: unknown; } export type ModuleImportResult = ModuleImportSuccess | ModuleImportError; /** * Get default target base path for module storage * Uses platform-specific defaults with environment variable overrides */ function getDefaultTargetBase(): string { return getModuleStoragePath(); } /** * Validate module directory structure * * Policy function (Rule 10.1) - validates input only, no side effects * * @param sourcePath - Path to module directory * @returns Error message if invalid, null if valid */ export function validateModuleDirectory(sourcePath: string): string | null { // Check directory exists if (!existsSync(sourcePath)) { return `Module directory does not exist: ${sourcePath}`; } // A submodule is not a module (design D1). It reaches the fleet inside its // parent's package and exists only as instances the parent creates, so // importing one on its own would produce a top-level module that was never // written to be singular. The registry path is already closed — submodules // are never published separately — so a local path is the only open door. const parentDir = dirname(sourcePath.replace(/\/+$/, '')); if (basename(parentDir) === SUBMODULES_DIR) { const owner = basename(dirname(parentDir)); return `'${basename(sourcePath)}' is a submodule of '${owner}', not a module. Import '${owner}' instead — its package carries this submodule, and only '${owner}' can instantiate it.`; } // Check it's a directory const stats = statSync(sourcePath); if (!stats.isDirectory()) { return `Path is not a directory: ${sourcePath}`; } // Check manifest.yml exists const manifestPath = join(sourcePath, 'manifest.yml'); if (!existsSync(manifestPath)) { return 'manifest.yml not found in module directory'; } return null; } /** * Read and validate manifest from module directory * * Policy function - reads and validates, no database access * * @param sourcePath - Path to module directory * @returns Validated manifest or error */ export async function readModuleManifest( sourcePath: string, ): Promise<{ success: true; manifest: ModuleManifest } | { success: false; error: string }> { const manifestPath = join(sourcePath, 'manifest.yml'); let yamlContent: string; try { yamlContent = await readFile(manifestPath, 'utf-8'); } catch (error) { return { success: false, error: `Failed to read manifest.yml: ${error instanceof Error ? error.message : 'Unknown error'}`, }; } const validationResult = validateManifest(yamlContent); if (!validationResult.success) { const errorMessages = validationResult.errors.map((e) => `${e.path}: ${e.message}`).join(', '); return { success: false, error: `Manifest validation failed: ${errorMessages}`, }; } const zoneValidation = validateZoneRequirements(validationResult.data); if (zoneValidation) { const errorMessages = zoneValidation.errors.map((e) => `${e.path}: ${e.message}`).join('\n'); return { success: false, error: `Zone validation failed:\n${errorMessages}`, }; } const deriveCheck = validateDeriveFromSources(validationResult.data); if (deriveCheck) { const errorMessages = deriveCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n'); return { success: false, error: `Variable derive_from validation failed:\n${errorMessages}`, }; } const hookCheck = validateHookContract(validationResult.data); if (hookCheck) { const errorMessages = hookCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n'); return { success: false, error: `Hook contract validation failed:\n${errorMessages}`, }; } const crossCapCheck = validateProvidesNoCrossCapabilityRefs(validationResult.data); if (crossCapCheck) { const errorMessages = crossCapCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n'); return { success: false, error: `Capability data validation failed:\n${errorMessages}`, }; } const capNameCheck = validateCapabilityNames(validationResult.data); if (capNameCheck) { const errorMessages = capNameCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n'); return { success: false, error: `Capability name validation failed:\n${errorMessages}`, }; } const privilegedCapCheck = validatePrivilegedCapabilities(validationResult.data); if (privilegedCapCheck) { const errorMessages = privilegedCapCheck.errors .map((e) => `${e.path}: ${e.message}`) .join('\n'); return { success: false, error: `Privileged capability validation failed:\n${errorMessages}`, }; } const variableSourceCheck = validateVariableSources(validationResult.data); if (variableSourceCheck) { const errorMessages = variableSourceCheck.errors .map((e) => `${e.path}: ${e.message}`) .join('\n'); return { success: false, error: `Variable source validation failed:\n${errorMessages}`, }; } const submoduleCheck = await validateDeclaredSubmodules(sourcePath, validationResult.data); if (submoduleCheck) { return { success: false, error: submoduleCheck }; } return { success: true, manifest: validationResult.data, }; } /** * Read and validate every submodule a parent declares. * * Runs at PARENT IMPORT so a broken submodule fails the parent, rather than * surfacing at the first instantiation — which happens with no operator * present and is the worst possible moment to learn a manifest is wrong * (design D1). * * @param sourcePath - The parent's module directory * @param parent - The parent's validated manifest * @returns An error message if any submodule is missing or illegal, null if clean */ async function validateDeclaredSubmodules( sourcePath: string, parent: ModuleManifest, ): Promise { const declarationCheck = validateSubmoduleDeclaration(parent); if (declarationCheck) { const errorMessages = declarationCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n'); return `Submodule declaration validation failed:\n${errorMessages}`; } for (const name of parent.submodules ?? []) { const submodulePath = join(sourcePath, SUBMODULES_DIR, name); const manifestPath = join(submodulePath, 'manifest.yml'); if (!existsSync(manifestPath)) { return `Module '${parent.id}' declares submodule '${name}', but ${SUBMODULES_DIR}/${name}/manifest.yml does not exist.`; } let yamlContent: string; try { yamlContent = await readFile(manifestPath, 'utf-8'); } catch (error) { return `Failed to read ${SUBMODULES_DIR}/${name}/manifest.yml: ${error instanceof Error ? error.message : 'Unknown error'}`; } const parsed = validateManifest(yamlContent); if (!parsed.success) { const errorMessages = parsed.errors.map((e) => `${e.path}: ${e.message}`).join(', '); return `Submodule '${name}' has an invalid manifest: ${errorMessages}`; } const submoduleCheck = validateSubmoduleManifest(parent, name, parsed.data); if (submoduleCheck) { const errorMessages = submoduleCheck.errors.map((e) => `${e.path}: ${e.message}`).join('\n'); return `Submodule '${name}' validation failed:\n${errorMessages}`; } } return null; } /** * Copy module files to target directory * * Execution function (Rule 10.1) - performs file I/O * * @param sourcePath - Source module directory * @param targetPath - Target directory to copy to */ export async function copyModuleFiles(sourcePath: string, targetPath: string): Promise { // Create target directory await mkdir(targetPath, { recursive: true }); // Get all files in source directory async function copyRecursive(src: string, dest: string) { const entries = await readdir(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = join(src, entry.name); const destPath = join(dest, entry.name); const relPath = relative(sourcePath, srcPath); // `unknown` is everything that belongs to the module's SOURCE tree and // not to its install: `.git/`, `e2e/`, tests, `tsconfig.json`, the // node_modules the canonical rule drops. Skipping it here is what keeps // the `unknown` class empty on a healthy install, so any `unknown` that // audit later reports is real. `package` and `derived` both land. if (classifyModulePath(relPath) === 'unknown') { continue; } if (entry.isDirectory()) { await mkdir(destPath, { recursive: true }); await copyRecursive(srcPath, destPath); } else if (entry.isFile()) { await copyFile(srcPath, destPath); } } } await copyRecursive(sourcePath, targetPath); } /** * Insert module into database * * Execution function - performs database write * * @param manifest - Validated manifest * @param targetPath - Target path where files are stored * @param db - Database client (optional, for testing) * @returns Inserted module record */ export async function insertModuleToDb( manifest: ModuleManifest, targetPath: string, db = getDb(), ): Promise { const newModule: NewModule = { id: manifest.id, name: manifest.name, version: manifest.version, description: manifest.description, sourcePath: targetPath, manifestData: manifest as Record, state: 'IMPORTED', }; db.insert(modules).values(newModule).run(); } /** * Check if module already exists in database * * @param moduleId - Module ID to check * @param db - Database client (optional, for testing) * @returns True if module exists */ export function moduleExists(moduleId: string, db = getDb()): boolean { const existing = db.select().from(modules).where(eq(modules.id, moduleId)).get(); return existing !== undefined; } /** * Validate well-known capabilities * * Policy function - checks if module's well-known capabilities are valid: * 1. No other module provides the same well-known capability in an * overlapping scope (zone-aware uniqueness) * 2. Module's zone matches capability's required zone (zone enforcement) * * @param manifest - Module manifest * @param db - Database client * @returns Error message if invalid, null if valid */ export async function validateWellKnownCapabilities( manifest: ModuleManifest, db = getDb(), ): Promise { const providedCapabilities = manifest.provides?.capabilities ?? []; for (const capability of providedCapabilities) { // Skip non-well-known capabilities if (!isWellKnown(capability.name)) { continue; } const wellKnown = getWellKnownCapability(capability.name); // Check 1: Capability uniqueness within an overlapping scope. An explicit // zone-scoped provider may coexist with a zone-agnostic fallback because // lookup deterministically prefers the explicit match. Two agnostic // providers, or two explicit providers sharing a zone, remain ambiguous. const existingCapability = await db .select() .from(capabilities) .where(eq(capabilities.capabilityName, capability.name)) .all(); const newZones = capability.zones ?? null; const conflictingModule = existingCapability.find((candidate) => { const existingZones = candidate.zones ?? null; if (newZones === null || existingZones === null) { return newZones === null && existingZones === null; } return newZones.some((zone) => existingZones.includes(zone)); }); if (conflictingModule) { const scope = newZones ? ` zone(s) ${newZones.join(', ')}` : ' the zone-agnostic scope'; return `Well-known capability '${capability.name}' is already provided in${scope} by module '${conflictingModule.moduleId}'. Remove '${conflictingModule.moduleId}' or use a non-overlapping explicit zone scope before importing this module.`; } // Check 2: Zone enforcement - module must be in the correct zone const moduleZone = getSingularSystemSpec(manifest)?.zone; if (wellKnown.zone_enforced && moduleZone) { if (moduleZone !== wellKnown.required_zone) { return `Capability '${capability.name}' requires zone='${wellKnown.required_zone}' (security requirement). Module manifest specifies zone='${moduleZone}'. Update the module manifest to use the correct zone.`; } } } return null; } /** * Import a module from a directory or .netapp package * * Orchestration function (Rule 10.1) - coordinates policy, planning, and execution * This is the main entry point for module import * * @param options - Import options * @returns Import result */ /** * The minimum package.json auto-generated for modules that have hook * scripts but no package.json. Contains only the framework dep. */ const DEFAULT_SCRIPTS_PACKAGE_JSON = JSON.stringify( { private: true, dependencies: { '@celilo/capabilities': '^0.1.0', }, }, null, 2, ); /** * Install npm dependencies for a module's hook scripts. * * Per NPM_PACKAGE_RESOLUTION design doc (Option B): each module's * scripts/ directory has its own package.json + node_modules for * fully isolated dependency resolution. This function: * * 1. Finds the scripts/ directory (derived from manifest hook paths). * 2. If package.json exists and node_modules/ is missing, runs * `bun install`. * 3. If no package.json exists but hook scripts do, auto-generates a * default one with `@celilo/capabilities` and then installs. * 4. If no hooks are declared, does nothing. */ async function installScriptDependencies( targetPath: string, manifest: ModuleManifest, ): Promise { // A module's scripts/ holds hook scripts AND/OR capability-provider // implementations (e.g. dns_registrar's register-host.ts, referenced via // provides.capabilities, not hooks). BOTH import @celilo/capabilities and // need deps. A capability-only provider (hooks: {}) still has scripts to // resolve — keying solely off hooks left its node_modules un-vendored, so // the capability-loader's import() failed with ENOENT @celilo/capabilities. const hasHooks = Boolean(manifest.hooks && Object.keys(manifest.hooks).length > 0); const hasCapabilities = (manifest.provides?.capabilities?.length ?? 0) > 0; if (!hasHooks && !hasCapabilities) { return; // No hook or capability scripts → nothing to install } const scriptsDir = join(targetPath, 'scripts'); if (!existsSync(scriptsDir)) { return; // No scripts directory on disk } const pkgJsonPath = join(scriptsDir, 'package.json'); const nodeModulesPath = join(scriptsDir, 'node_modules'); // Auto-generate package.json if missing but hooks exist if (!existsSync(pkgJsonPath)) { log.info('Auto-generating scripts/package.json with @celilo/capabilities'); writeFileSync(pkgJsonPath, DEFAULT_SCRIPTS_PACKAGE_JSON, 'utf-8'); } // Skip install if node_modules already exists (pre-bundled via // future --bundle-deps, or a re-import of an already-installed module) if (existsSync(nodeModulesPath)) { return; } // Run bun install in the scripts directory. The bun-install can take a // few seconds on a cold cache, so we surface a status line — silent // pauses look like hangs. log.info('Installing dependencies for module hook scripts...'); execSync('bun install', { cwd: scriptsDir, timeout: 120_000, stdio: 'pipe', }); } export async function importModule(options: ModuleImportOptions): Promise { const { sourcePath, targetBasePath = getDefaultTargetBase(), db = getDb(), flags = {} } = options; // Directory imports route through the packager: build a temporary .netapp // and re-enter through the package path. This makes the .netapp pipeline // the single import path — no second flow that drifts. The packager is // also the only place a manifest build runs, with CELILO_MODULE_SOURCE_DIR // pointing at the unstaged source so sibling-package paths in a monorepo // resolve (e.g. celilo-registry's `cd $CELILO_MODULE_SOURCE_DIR/../../packages/registry-server`). // After this point the module is detached from the source tree, so a // deploy-time rebuild can't work — bake the artifacts in here, once. if (!sourcePath.endsWith('.netapp')) { const dirError = validateModuleDirectory(sourcePath); if (dirError) return { success: false, error: dirError }; const manifestResult = await readModuleManifest(sourcePath); if (!manifestResult.success) return { success: false, error: manifestResult.error }; if (moduleExists(manifestResult.manifest.id, db)) { return { success: false, error: `Module '${manifestResult.manifest.id}' already exists. Use update or remove it first.`, }; } const { mkdtempSync, rmSync } = await import('node:fs'); const { tmpdir } = await import('node:os'); const tempPkgDir = mkdtempSync(join(tmpdir(), 'celilo-import-pkg-')); const tempPkgPath = join(tempPkgDir, `${manifestResult.manifest.id}.netapp`); try { const { buildModule } = await import('./packaging/build'); const buildResult = await buildModule({ sourceDir: sourcePath, outputPath: tempPkgPath }); if (!buildResult.success) { return { success: false, error: buildResult.error || 'Failed to package module for import', }; } return await importModule({ ...options, sourcePath: tempPkgPath }); } finally { rmSync(tempPkgDir, { recursive: true, force: true }); } } let tempDir: string | null = null; let checksums: Record | null = null; let signature: string | null = null; try { // sourcePath always ends with .netapp at this point — directory inputs // were re-routed through the packager above and recursed back in here. const extractResult = await extractPackage(sourcePath); if (!extractResult.success || !extractResult.tempDir) { return { success: false, error: extractResult.error || 'Failed to extract package' }; } tempDir = extractResult.tempDir; // Verify package integrity (unless --skip-verify) const skipVerify = flags['skip-verify'] === true; if (skipVerify) { log.warn('Skipping package signature verification (--skip-verify)'); } else { const verifyResult = await verifyPackageIntegrity(tempDir); if (!verifyResult.success) { await cleanupTempDir(tempDir); return { success: false, error: verifyResult.error || 'Package integrity verification failed', }; } } // Read checksums and signature for database storage (both optional with --skip-verify) try { const checksumsJson = await readFile(join(tempDir, 'checksums.json'), 'utf-8'); const ChecksumsFileSchema = z.object({ files: z.record(z.string(), z.string()), }); const checksumsData = parseJsonWithValidation( checksumsJson, ChecksumsFileSchema, 'package checksums.json', ); checksums = checksumsData.files; } catch (err) { if (!skipVerify) throw err; } try { signature = await readFile(join(tempDir, 'signature.sig'), 'utf-8'); } catch (err) { if (!skipVerify) throw err; } const actualSourcePath = tempDir; // Policy: Validate directory structure const dirError = validateModuleDirectory(actualSourcePath); if (dirError) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: dirError }; } // Policy: Read and validate manifest const manifestResult = await readModuleManifest(actualSourcePath); if (!manifestResult.success) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: manifestResult.error }; } const manifest = manifestResult.manifest; // Policy: Check if module already exists if (moduleExists(manifest.id, db)) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: `Module '${manifest.id}' already exists. Use update or remove it first.`, }; } // Policy: Validate well-known capabilities (import-time check) const capabilityError = await validateWellKnownCapabilities(manifest, db); if (capabilityError) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: capabilityError, }; } // Policy: Validate template variable references (import-time check) const { validateModuleTemplates, formatTemplateValidationErrors } = await import( '../manifest/template-validator' ); const templateValidation = await validateModuleTemplates(actualSourcePath, manifest); if (!templateValidation.success) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: formatTemplateValidationErrors(templateValidation.errors), }; } // Policy: Check for Ansible dependency conflicts and install collections const { installCollectionsForModule } = await import('../ansible/dependencies'); const installResult = await installCollectionsForModule(manifest, db); if (!installResult.success) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: installResult.error || 'Failed to install Ansible collections', details: installResult.details, }; } // Execution: Validate capability access if module requires capabilities if (manifest.requires?.capabilities && manifest.requires.capabilities.length > 0) { const { validateCapabilityAccess } = await import('../capabilities/validation'); // Templates are where CLAUDE.md's Definition of Done tells module authors // to put `$capability:` references, so the import-time gate has to see // them (celilo#854, celilo#1027). // // This adds no parser. `validateModuleTemplates` above already reads and // parses every `.tpl` on every import, roughly 25 lines before the gate // runs — it was discarding the references it saw. So there is no new file // walk, no new failure mode and no new ordering question: the data is // already in scope and the gate simply was not looking at it. // // Fail-closed by that same ordering. An unreadable template makes // `validateModuleTemplates` return success:false with no references, and // the early return above fires BEFORE this check, so an empty reference // set can never reach the gate as a silent pass. const accessResult = await validateCapabilityAccess( manifest, db.$client, templateValidation.capabilityReferences, ); if (!accessResult.success) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: accessResult.error || 'Capability access denied', details: accessResult.details, }; } } // Planning: Determine target path const targetPath = join(targetBasePath, manifest.id); // Execution: Copy files try { await timedPhase('copyModuleFiles', () => copyModuleFiles(actualSourcePath, targetPath)); } catch (error) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: 'Failed to copy module files', details: error, }; } // Execution: Install hook script dependencies (NPM_PACKAGE_RESOLUTION Option B). // If the module's scripts/ directory has a package.json, run `bun install` // so the hook scripts can resolve their npm imports (e.g. @celilo/capabilities). // If no package.json exists but hook scripts do, auto-generate one with // just the framework dep — smooths migration for existing modules. try { await timedPhase('installScriptDependencies', () => installScriptDependencies(targetPath, manifest), ); } catch (error) { // Non-fatal: the module is importable without deps, but hooks // will fail at runtime. Warn and continue. const msg = error instanceof Error ? error.message : String(error); log.warn(`Failed to install script dependencies: ${msg}`); log.warn('Hook scripts may fail to resolve imports until this is fixed.'); } // Execution: Insert to database try { await timedPhase('insertModuleToDb', () => insertModuleToDb(manifest, targetPath, db)); } catch (error) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: 'Failed to insert module to database', details: error, }; } // Execution: Register capabilities if module provides them if (manifest.provides?.capabilities && manifest.provides.capabilities.length > 0) { const capResult = await timedPhase('registerModuleCapabilities', async () => { const { registerModuleCapabilities } = await import('../capabilities/registration'); return registerModuleCapabilities(manifest.id, manifest, db.$client, flags); }); if (!capResult.success) { if (tempDir) await cleanupTempDir(tempDir); return { success: false, error: capResult.error || 'Failed to register module capabilities', details: capResult.details, }; } } // Execution: Register event-bus subscriptions declared in the manifest. // Best-effort: a bus problem shouldn't wedge an import — the operator // can re-run the import after fixing the bus, and bus.subscribe is // idempotent. try { await timedPhase('registerModuleSubscriptions', async () => { const { registerModuleSubscriptions } = await import('../services/module-subscriptions'); registerModuleSubscriptions(manifest, targetPath); }); } catch (error) { const msg = error instanceof Error ? error.message : String(error); log.warn(`Failed to register event-bus subscriptions: ${msg}`); log.warn( 'Module imported, but reactive flows on the event bus will not fire until this is fixed.', ); } // Execution: Store integrity data from the package's checksums + signature. // Directory imports go through the packager too (see top of importModule), // so by the time we reach this point we always have these. // // UPSERT, not INSERT. `moduleId` is UNIQUE, so a re-import of an already // imported module used to collide, get caught by a warn-and-continue, and // leave the FIRST import's checksums in place forever. Every file that // legitimately changed since then read as [MODIFIED] and the baseline // described a version nobody could name. Failing to record the baseline is // not "non-fatal": it is the state that made `module verify` useless, so // this no longer swallows its own errors. const integrityData: NewModuleIntegrity = { moduleId: manifest.id, checksums: checksums ?? {}, version: manifest.version, signature: signature?.trim() ?? null, }; db.insert(moduleIntegrity) .values(integrityData) .onConflictDoUpdate({ target: moduleIntegrity.moduleId, set: { checksums: integrityData.checksums, version: integrityData.version, signature: integrityData.signature, updatedAt: new Date(), }, }) .run(); // Record a successful build entry so deploy-time validation sees the // artifacts in place and skips rebuild. The packager runs the manifest // build into the staged tree before tar, so any declared artifacts are // already on disk under targetPath at this point. if (manifest.build?.artifacts) { const recordedArtifactPaths = manifest.build.artifacts .map((a: string) => join(targetPath, a)) .filter((p: string) => existsSync(p)); if (recordedArtifactPaths.length > 0) { const { moduleBuilds } = await import('../db/schema'); db.insert(moduleBuilds) .values({ moduleId: manifest.id, version: manifest.version, artifacts: recordedArtifactPaths, status: 'success', buildLog: 'Pre-built artifacts from .netapp package', }) .run(); } } // Update dependency cache try { const { updateDependencyCache } = await import('../ansible/dependencies'); await updateDependencyCache(db); } catch (error) { // Non-fatal - cache is optional console.warn('Warning: Failed to update dependency cache', error); } // Cleanup temp directory if used if (tempDir) { await cleanupTempDir(tempDir); } return { success: true, moduleId: manifest.id, targetPath, }; } catch (error) { // Cleanup on unexpected error if (tempDir) { await cleanupTempDir(tempDir); } return { success: false, error: 'Unexpected error during import', details: error, }; } }