1223 lines
42 KiB
Nim
1223 lines
42 KiB
Nim
# SPDX-License-Identifier: LSL-1.0
|
||
# Copyright (c) 2026 Markus Maiwald
|
||
# Stewardship: Self Sovereign Society Foundation
|
||
#
|
||
# This file is part of the Nexus Sovereign Core.
|
||
# See legal/LICENSE_SOVEREIGN.md for license terms.
|
||
|
||
## nimpak/lockfile_system.nim
|
||
## Lockfile generation and reproducibility system for NimPak
|
||
##
|
||
## This module implements the lockfile system for environment reproducibility
|
||
## and CI/CD integration, providing exact package version tracking and
|
||
## system state capture.
|
||
|
||
import std/[os, strutils, times, json, tables, sequtils, algorithm, hashes]
|
||
|
||
type
|
||
LockfileError* = object of CatchableError
|
||
lockfilePath*: string
|
||
|
||
LockfileManager* = object
|
||
lockfilePath*: string ## Path to nip.lock file
|
||
generationsRoot*: string ## /System/Generations - Generation metadata
|
||
programsRoot*: string ## /Programs - Package installation directory
|
||
format*: LockfileFormat ## Output format (JSON, YAML, KDL)
|
||
includeSource*: bool ## Include source attribution
|
||
includeChecksums*: bool ## Include package checksums
|
||
includeGeneration*: bool ## Include generation information
|
||
|
||
LockfileFormat* = enum
|
||
LockfileJson, ## JSON format (default)
|
||
LockfileYaml, ## YAML format
|
||
LockfileKdl ## KDL format
|
||
|
||
PackageLockEntry* = object
|
||
name*: string ## Package name
|
||
version*: string ## Exact version
|
||
stream*: string ## Package stream (stable, testing, etc.)
|
||
source*: PackageSource ## Source information
|
||
checksum*: string ## Package checksum (BLAKE3)
|
||
dependencies*: seq[string] ## Direct dependencies
|
||
installedPath*: string ## Installation path
|
||
installedSize*: int64 ## Installed size in bytes
|
||
installTime*: times.DateTime ## Installation timestamp
|
||
|
||
PackageSource* = object
|
||
sourceMethod*: string ## Source method (grafted-pacman, native, etc.)
|
||
url*: string ## Source URL or identifier
|
||
hash*: string ## Source hash
|
||
timestamp*: times.DateTime ## Source timestamp
|
||
attribution*: string ## Source attribution
|
||
|
||
SystemLockfile* = object
|
||
version*: string ## Lockfile format version
|
||
generated*: times.DateTime ## Generation timestamp
|
||
generator*: string ## Generator tool (nip)
|
||
systemGeneration*: string ## System generation ID
|
||
architecture*: string ## Target architecture
|
||
packages*: seq[PackageLockEntry] ## Locked packages
|
||
metadata*: LockfileMetadata ## Additional metadata
|
||
|
||
LockfileMetadata* = object
|
||
description*: string ## Lockfile description
|
||
environment*: string ## Environment name (production, development, etc.)
|
||
creator*: string ## Creator information
|
||
tags*: seq[string] ## Tags for categorization
|
||
totalSize*: int64 ## Total installed size
|
||
packageCount*: int ## Number of packages
|
||
|
||
# =============================================================================
|
||
# LockfileManager Creation and Configuration
|
||
# =============================================================================
|
||
|
||
proc newLockfileManager*(lockfilePath: string = "nip.lock",
|
||
generationsRoot: string = "/System/Generations",
|
||
programsRoot: string = "/Programs",
|
||
format: LockfileFormat = LockfileJson,
|
||
includeSource: bool = true,
|
||
includeChecksums: bool = true,
|
||
includeGeneration: bool = true): LockfileManager =
|
||
## Create a new LockfileManager with specified configuration
|
||
LockfileManager(
|
||
lockfilePath: lockfilePath,
|
||
generationsRoot: generationsRoot,
|
||
programsRoot: programsRoot,
|
||
format: format,
|
||
includeSource: includeSource,
|
||
includeChecksums: includeChecksums,
|
||
includeGeneration: includeGeneration
|
||
)
|
||
|
||
# =============================================================================
|
||
# Package Information Gathering
|
||
# =============================================================================
|
||
|
||
proc gatherPackageInfo*(lm: LockfileManager, packageName: string, version: string): PackageLockEntry =
|
||
## Gather comprehensive information about an installed package
|
||
let packageDir = lm.programsRoot / packageName / version
|
||
|
||
var entry = PackageLockEntry(
|
||
name: packageName,
|
||
version: version,
|
||
stream: "stable", # Default - would be read from package metadata
|
||
installedPath: packageDir,
|
||
installedSize: 0,
|
||
installTime: now(),
|
||
dependencies: @[],
|
||
checksum: "",
|
||
source: PackageSource(
|
||
sourceMethod: "unknown",
|
||
url: "",
|
||
hash: "",
|
||
timestamp: now(),
|
||
attribution: ""
|
||
)
|
||
)
|
||
|
||
# Calculate installed size
|
||
if dirExists(packageDir):
|
||
proc calculateDirSize(path: string): int64 =
|
||
var totalSize: int64 = 0
|
||
for kind, subpath in walkDir(path):
|
||
if kind == pcFile:
|
||
try:
|
||
totalSize += getFileSize(subpath)
|
||
except:
|
||
discard
|
||
elif kind == pcDir:
|
||
totalSize += calculateDirSize(subpath)
|
||
return totalSize
|
||
|
||
entry.installedSize = calculateDirSize(packageDir)
|
||
|
||
# Try to read package metadata if available
|
||
let metadataFile = packageDir / "package.json"
|
||
if fileExists(metadataFile):
|
||
try:
|
||
let metadata = parseJson(readFile(metadataFile))
|
||
|
||
if metadata.hasKey("stream"):
|
||
entry.stream = metadata["stream"].getStr()
|
||
|
||
if metadata.hasKey("dependencies"):
|
||
entry.dependencies = metadata["dependencies"].getElems().mapIt(it.getStr())
|
||
|
||
if metadata.hasKey("checksum"):
|
||
entry.checksum = metadata["checksum"].getStr()
|
||
|
||
if metadata.hasKey("source"):
|
||
let sourceNode = metadata["source"]
|
||
entry.source = PackageSource(
|
||
sourceMethod: sourceNode.getOrDefault("method").getStr("unknown"),
|
||
url: sourceNode.getOrDefault("url").getStr(""),
|
||
hash: sourceNode.getOrDefault("hash").getStr(""),
|
||
timestamp: if sourceNode.hasKey("timestamp"):
|
||
parse(sourceNode["timestamp"].getStr(), "yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc())
|
||
else:
|
||
now(),
|
||
attribution: sourceNode.getOrDefault("attribution").getStr("")
|
||
)
|
||
|
||
if metadata.hasKey("install_time"):
|
||
entry.installTime = parse(metadata["install_time"].getStr(), "yyyy-MM-dd'T'HH:mm:ss'.'fff'Z'", utc())
|
||
|
||
except:
|
||
# Use defaults if metadata parsing fails
|
||
discard
|
||
|
||
return entry
|
||
|
||
proc scanInstalledPackages*(lm: LockfileManager): seq[PackageLockEntry] =
|
||
## Scan all installed packages and gather their information
|
||
var packages: seq[PackageLockEntry] = @[]
|
||
|
||
if not dirExists(lm.programsRoot):
|
||
return packages
|
||
|
||
# Scan /Programs directory for installed packages
|
||
for kind, packagePath in walkDir(lm.programsRoot):
|
||
if kind == pcDir:
|
||
let packageName = extractFilename(packagePath)
|
||
|
||
# Skip system directories
|
||
if packageName.startsWith("."):
|
||
continue
|
||
|
||
# Scan versions for this package
|
||
for versionKind, versionPath in walkDir(packagePath):
|
||
if versionKind == pcDir:
|
||
let version = extractFilename(versionPath)
|
||
|
||
# Skip system directories
|
||
if version.startsWith("."):
|
||
continue
|
||
|
||
let packageInfo = lm.gatherPackageInfo(packageName, version)
|
||
packages.add(packageInfo)
|
||
|
||
# Sort packages by name and version for consistent output
|
||
packages.sort do (a, b: PackageLockEntry) -> int:
|
||
let nameCompare = cmp(a.name, b.name)
|
||
if nameCompare != 0:
|
||
nameCompare
|
||
else:
|
||
cmp(a.version, b.version)
|
||
|
||
return packages
|
||
|
||
# =============================================================================
|
||
# System State Capture
|
||
# =============================================================================
|
||
|
||
proc getCurrentGeneration*(lm: LockfileManager): string =
|
||
## Get the current system generation ID
|
||
try:
|
||
let currentGenFile = lm.generationsRoot / "current"
|
||
if fileExists(currentGenFile):
|
||
return readFile(currentGenFile).strip()
|
||
else:
|
||
return ""
|
||
except:
|
||
return ""
|
||
|
||
proc getSystemArchitecture*(): string =
|
||
## Get the system architecture
|
||
try:
|
||
when defined(amd64) or defined(x86_64):
|
||
return "x86_64"
|
||
elif defined(i386) or defined(x86):
|
||
return "i386"
|
||
elif defined(arm64) or defined(aarch64):
|
||
return "aarch64"
|
||
elif defined(arm):
|
||
return "arm"
|
||
else:
|
||
return "unknown"
|
||
except:
|
||
return "unknown"
|
||
|
||
proc createSystemLockfile*(lm: LockfileManager, description: string = "",
|
||
environment: string = "", creator: string = "",
|
||
tags: seq[string] = @[]): SystemLockfile =
|
||
## Create a complete system lockfile with all installed packages
|
||
let packages = lm.scanInstalledPackages()
|
||
let totalSize = packages.mapIt(it.installedSize).foldl(a + b, 0'i64)
|
||
|
||
let metadata = LockfileMetadata(
|
||
description: if description.len > 0: description else: "System lockfile generated by nip",
|
||
environment: if environment.len > 0: environment else: "default",
|
||
creator: if creator.len > 0: creator else: "nip",
|
||
tags: if tags.len > 0: tags else: @["system", "lockfile"],
|
||
totalSize: totalSize,
|
||
packageCount: packages.len
|
||
)
|
||
|
||
SystemLockfile(
|
||
version: "1.0",
|
||
generated: now(),
|
||
generator: "nip",
|
||
systemGeneration: lm.getCurrentGeneration(),
|
||
architecture: getSystemArchitecture(),
|
||
packages: packages,
|
||
metadata: metadata
|
||
)
|
||
|
||
# =============================================================================
|
||
# Lockfile Serialization
|
||
# =============================================================================
|
||
|
||
proc serializeLockfileToJson*(lockfile: SystemLockfile, pretty: bool = true): string =
|
||
## Serialize lockfile to JSON format
|
||
let jsonNode = %*{
|
||
"lockfile": {
|
||
"version": lockfile.version,
|
||
"generated": $lockfile.generated,
|
||
"generator": lockfile.generator,
|
||
"system_generation": lockfile.systemGeneration,
|
||
"architecture": lockfile.architecture
|
||
},
|
||
"metadata": {
|
||
"description": lockfile.metadata.description,
|
||
"environment": lockfile.metadata.environment,
|
||
"creator": lockfile.metadata.creator,
|
||
"tags": lockfile.metadata.tags,
|
||
"total_size": lockfile.metadata.totalSize,
|
||
"package_count": lockfile.metadata.packageCount
|
||
},
|
||
"packages": lockfile.packages.mapIt(%*{
|
||
"name": it.name,
|
||
"version": it.version,
|
||
"stream": it.stream,
|
||
"checksum": it.checksum,
|
||
"installed_path": it.installedPath,
|
||
"installed_size": it.installedSize,
|
||
"install_time": $it.installTime,
|
||
"dependencies": it.dependencies,
|
||
"source": {
|
||
"method": it.source.sourceMethod,
|
||
"url": it.source.url,
|
||
"hash": it.source.hash,
|
||
"timestamp": $it.source.timestamp,
|
||
"attribution": it.source.attribution
|
||
}
|
||
})
|
||
}
|
||
|
||
if pretty:
|
||
return jsonNode.pretty()
|
||
else:
|
||
return $jsonNode
|
||
|
||
proc serializeLockfileToKdl*(lockfile: SystemLockfile): string =
|
||
## Serialize lockfile to KDL format
|
||
result = "// NimPak System Lockfile\n"
|
||
result.add("// Generated: " & $lockfile.generated & "\n\n")
|
||
|
||
result.add("lockfile {\n")
|
||
result.add(" version \"" & lockfile.version & "\"\n")
|
||
result.add(" generated \"" & $lockfile.generated & "\"\n")
|
||
result.add(" generator \"" & lockfile.generator & "\"\n")
|
||
result.add(" system_generation \"" & lockfile.systemGeneration & "\"\n")
|
||
result.add(" architecture \"" & lockfile.architecture & "\"\n")
|
||
result.add("}\n\n")
|
||
|
||
result.add("metadata {\n")
|
||
result.add(" description \"" & lockfile.metadata.description & "\"\n")
|
||
result.add(" environment \"" & lockfile.metadata.environment & "\"\n")
|
||
result.add(" creator \"" & lockfile.metadata.creator & "\"\n")
|
||
result.add(" tags")
|
||
for tag in lockfile.metadata.tags:
|
||
result.add(" \"" & tag & "\"")
|
||
result.add("\n")
|
||
result.add(" total_size " & $lockfile.metadata.totalSize & "\n")
|
||
result.add(" package_count " & $lockfile.metadata.packageCount & "\n")
|
||
result.add("}\n\n")
|
||
|
||
for pkg in lockfile.packages:
|
||
result.add("package \"" & pkg.name & "\" {\n")
|
||
result.add(" version \"" & pkg.version & "\"\n")
|
||
result.add(" stream \"" & pkg.stream & "\"\n")
|
||
result.add(" checksum \"" & pkg.checksum & "\"\n")
|
||
result.add(" installed_path \"" & pkg.installedPath & "\"\n")
|
||
result.add(" installed_size " & $pkg.installedSize & "\n")
|
||
result.add(" install_time \"" & $pkg.installTime & "\"\n")
|
||
|
||
if pkg.dependencies.len > 0:
|
||
result.add(" dependencies")
|
||
for dep in pkg.dependencies:
|
||
result.add(" \"" & dep & "\"")
|
||
result.add("\n")
|
||
|
||
result.add(" source {\n")
|
||
result.add(" method \"" & pkg.source.sourceMethod & "\"\n")
|
||
result.add(" url \"" & pkg.source.url & "\"\n")
|
||
result.add(" hash \"" & pkg.source.hash & "\"\n")
|
||
result.add(" timestamp \"" & $pkg.source.timestamp & "\"\n")
|
||
result.add(" attribution \"" & pkg.source.attribution & "\"\n")
|
||
result.add(" }\n")
|
||
result.add("}\n\n")
|
||
|
||
# =============================================================================
|
||
# Lockfile Generation and Saving
|
||
# =============================================================================
|
||
|
||
proc generateLockfile*(lm: LockfileManager, description: string = "",
|
||
environment: string = "", creator: string = "",
|
||
tags: seq[string] = @[]): bool =
|
||
## Generate and save a lockfile for the current system state
|
||
try:
|
||
echo "🔒 Generating system lockfile..."
|
||
|
||
# Create the lockfile
|
||
let lockfile = lm.createSystemLockfile(description, environment, creator, tags)
|
||
|
||
echo "📊 Lockfile statistics:"
|
||
echo " Packages: ", lockfile.metadata.packageCount
|
||
echo " Total size: ", lockfile.metadata.totalSize, " bytes"
|
||
echo " Generation: ", lockfile.systemGeneration
|
||
echo " Architecture: ", lockfile.architecture
|
||
|
||
# Serialize based on format
|
||
let content = case lm.format:
|
||
of LockfileJson:
|
||
serializeLockfileToJson(lockfile, pretty = true)
|
||
of LockfileKdl:
|
||
serializeLockfileToKdl(lockfile)
|
||
of LockfileYaml:
|
||
# YAML serialization would be implemented here
|
||
# For now, fall back to JSON
|
||
serializeLockfileToJson(lockfile, pretty = true)
|
||
|
||
# Ensure parent directory exists
|
||
let parentDir = parentDir(lm.lockfilePath)
|
||
if parentDir.len > 0 and not dirExists(parentDir):
|
||
createDir(parentDir)
|
||
|
||
# Write the lockfile
|
||
writeFile(lm.lockfilePath, content)
|
||
|
||
echo "✅ Lockfile saved to: ", lm.lockfilePath
|
||
return true
|
||
|
||
except Exception as e:
|
||
echo "❌ Failed to generate lockfile: ", e.msg
|
||
return false
|
||
|
||
proc validateLockfile*(lm: LockfileManager): bool =
|
||
## Validate an existing lockfile against current system state
|
||
try:
|
||
if not fileExists(lm.lockfilePath):
|
||
echo "❌ Lockfile not found: ", lm.lockfilePath
|
||
return false
|
||
|
||
echo "🔍 Validating lockfile: ", lm.lockfilePath
|
||
|
||
# Load lockfile
|
||
let content = readFile(lm.lockfilePath)
|
||
let lockfileJson = parseJson(content)
|
||
|
||
# Get current system state
|
||
let currentPackages = lm.scanInstalledPackages()
|
||
let currentGeneration = lm.getCurrentGeneration()
|
||
|
||
# Validate generation
|
||
let lockfileGeneration = lockfileJson["lockfile"]["system_generation"].getStr()
|
||
if lockfileGeneration != currentGeneration:
|
||
echo "⚠️ Generation mismatch:"
|
||
echo " Lockfile: ", lockfileGeneration
|
||
echo " Current: ", currentGeneration
|
||
|
||
# Validate packages
|
||
let lockfilePackages = lockfileJson["packages"].getElems()
|
||
var missingPackages: seq[string] = @[]
|
||
var extraPackages: seq[string] = @[]
|
||
var versionMismatches: seq[string] = @[]
|
||
|
||
# Create lookup tables
|
||
var lockfilePackageMap = initTable[string, JsonNode]()
|
||
for pkg in lockfilePackages:
|
||
let key = pkg["name"].getStr() & "-" & pkg["version"].getStr()
|
||
lockfilePackageMap[key] = pkg
|
||
|
||
var currentPackageMap = initTable[string, PackageLockEntry]()
|
||
for pkg in currentPackages:
|
||
let key = pkg.name & "-" & pkg.version
|
||
currentPackageMap[key] = pkg
|
||
|
||
# Check for missing packages
|
||
for key, lockfilePkg in lockfilePackageMap:
|
||
if key notin currentPackageMap:
|
||
missingPackages.add(lockfilePkg["name"].getStr() & "-" & lockfilePkg["version"].getStr())
|
||
|
||
# Check for extra packages
|
||
for key, currentPkg in currentPackageMap:
|
||
if key notin lockfilePackageMap:
|
||
extraPackages.add(currentPkg.name & "-" & currentPkg.version)
|
||
|
||
# Report validation results
|
||
if missingPackages.len == 0 and extraPackages.len == 0:
|
||
echo "✅ Lockfile validation passed - system matches lockfile exactly"
|
||
return true
|
||
else:
|
||
echo "❌ Lockfile validation failed:"
|
||
|
||
if missingPackages.len > 0:
|
||
echo " Missing packages (", missingPackages.len, "):"
|
||
for pkg in missingPackages:
|
||
echo " - ", pkg
|
||
|
||
if extraPackages.len > 0:
|
||
echo " Extra packages (", extraPackages.len, "):"
|
||
for pkg in extraPackages:
|
||
echo " + ", pkg
|
||
|
||
return false
|
||
|
||
except Exception as e:
|
||
echo "❌ Failed to validate lockfile: ", e.msg
|
||
return false
|
||
|
||
# =============================================================================
|
||
# Lockfile Comparison and Diff
|
||
# =============================================================================
|
||
|
||
proc compareLockfiles*(lockfile1Path: string, lockfile2Path: string): bool =
|
||
## Compare two lockfiles and show differences
|
||
try:
|
||
if not fileExists(lockfile1Path):
|
||
echo "❌ Lockfile not found: ", lockfile1Path
|
||
return false
|
||
|
||
if not fileExists(lockfile2Path):
|
||
echo "❌ Lockfile not found: ", lockfile2Path
|
||
return false
|
||
|
||
echo "🔍 Comparing lockfiles:"
|
||
echo " File 1: ", lockfile1Path
|
||
echo " File 2: ", lockfile2Path
|
||
|
||
let content1 = readFile(lockfile1Path)
|
||
let content2 = readFile(lockfile2Path)
|
||
|
||
let lockfile1 = parseJson(content1)
|
||
let lockfile2 = parseJson(content2)
|
||
|
||
# Compare metadata
|
||
let gen1 = lockfile1["lockfile"]["system_generation"].getStr()
|
||
let gen2 = lockfile2["lockfile"]["system_generation"].getStr()
|
||
|
||
if gen1 != gen2:
|
||
echo "📊 Generation difference:"
|
||
echo " File 1: ", gen1
|
||
echo " File 2: ", gen2
|
||
|
||
# Compare packages
|
||
let packages1 = lockfile1["packages"].getElems()
|
||
let packages2 = lockfile2["packages"].getElems()
|
||
|
||
var packages1Map = initTable[string, JsonNode]()
|
||
var packages2Map = initTable[string, JsonNode]()
|
||
|
||
for pkg in packages1:
|
||
let key = pkg["name"].getStr() & "-" & pkg["version"].getStr()
|
||
packages1Map[key] = pkg
|
||
|
||
for pkg in packages2:
|
||
let key = pkg["name"].getStr() & "-" & pkg["version"].getStr()
|
||
packages2Map[key] = pkg
|
||
|
||
var onlyIn1: seq[string] = @[]
|
||
var onlyIn2: seq[string] = @[]
|
||
var common: seq[string] = @[]
|
||
|
||
for key in packages1Map.keys:
|
||
if key in packages2Map:
|
||
common.add(key)
|
||
else:
|
||
onlyIn1.add(key)
|
||
|
||
for key in packages2Map.keys:
|
||
if key notin packages1Map:
|
||
onlyIn2.add(key)
|
||
|
||
echo "📊 Package comparison:"
|
||
echo " Common packages: ", common.len
|
||
echo " Only in file 1: ", onlyIn1.len
|
||
echo " Only in file 2: ", onlyIn2.len
|
||
|
||
if onlyIn1.len > 0:
|
||
echo " Packages only in ", extractFilename(lockfile1Path), ":"
|
||
for pkg in onlyIn1:
|
||
echo " - ", pkg
|
||
|
||
if onlyIn2.len > 0:
|
||
echo " Packages only in ", extractFilename(lockfile2Path), ":"
|
||
for pkg in onlyIn2:
|
||
echo " + ", pkg
|
||
|
||
let identical = onlyIn1.len == 0 and onlyIn2.len == 0
|
||
if identical:
|
||
echo "✅ Lockfiles are identical"
|
||
else:
|
||
echo "❌ Lockfiles differ"
|
||
|
||
return identical
|
||
|
||
except Exception as e:
|
||
echo "❌ Failed to compare lockfiles: ", e.msg
|
||
return false
|
||
|
||
# =============================================================================
|
||
# Lockfile Restoration System
|
||
# =============================================================================
|
||
|
||
proc restoreFromLockfile*(lm: LockfileManager, lockfilePath: string,
|
||
dryRun: bool = false): bool =
|
||
## Restore system state from a lockfile
|
||
try:
|
||
if not fileExists(lockfilePath):
|
||
echo "❌ Lockfile not found: ", lockfilePath
|
||
return false
|
||
|
||
echo "🔄 Restoring system from lockfile: ", lockfilePath
|
||
|
||
# Load lockfile
|
||
let content = readFile(lockfilePath)
|
||
let lockfileJson = parseJson(content)
|
||
|
||
# Get target state
|
||
let targetGeneration = lockfileJson["lockfile"]["system_generation"].getStr()
|
||
let targetArchitecture = lockfileJson["lockfile"]["architecture"].getStr()
|
||
let targetPackages = lockfileJson["packages"].getElems()
|
||
|
||
# Verify architecture compatibility
|
||
let currentArch = getSystemArchitecture()
|
||
if targetArchitecture != currentArch:
|
||
echo "⚠️ Architecture mismatch:"
|
||
echo " Target: ", targetArchitecture
|
||
echo " Current: ", cu
|
||
echo " Proceeding anyway..."
|
||
|
||
echo "📊 Restoration plan:"
|
||
echo " Target generation: ", targetGeneration
|
||
echo " Target packages: ", targetPackages.len
|
||
|
||
# Get current system state
|
||
let currentPackages = lm.scanInstalledPackages()
|
||
|
||
# Create restoration plan
|
||
var packagesToInstall: seq[string] = @[]
|
||
var packagesToRemove: seq[string] = @[]
|
||
var packagesToUpdate: seq[string] = @[]
|
||
|
||
# Build lookup tables
|
||
var targetPackageMap = initTable[string, JsonNode]()
|
||
for pkg in targetPackages:
|
||
let key = pkg["name"].getStr()
|
||
targetPackageMap[key] = pkg
|
||
|
||
var currentPackageMap = initTable[string, PackageLockEntry]()
|
||
for pkg in currentPackages:
|
||
currentPackageMap[pkg.name] = pkg
|
||
|
||
# Determine required actions
|
||
for packageName, targetPkg in targetPackageMap:
|
||
let targetVersion = targetPkg["version"].getStr()
|
||
|
||
if packageName in currentPackageMap:
|
||
let currentVersion = currentPackageMap[packageName].version
|
||
if currentVersion != targetVersion:
|
||
packagesToUpdate.add(packageName & "-" & currentVersion & " → " & targetVersion)
|
||
else:
|
||
packagesToInstall.add(packageName & "-" & targetVersion)
|
||
|
||
for packageName, currentPkg in currentPackageMap:
|
||
if packageName notin targetPackageMap:
|
||
packagesToRemove.add(packageName & "-" & currentPkg.version)
|
||
|
||
# Display restoration plan
|
||
ifagesToInstall.len > 0:
|
||
echo "📦 Packages to install (", packagesToInstall.len, "):"
|
||
for pkg in packagesToInstall:
|
||
echo " + ", pkg
|
||
|
||
if packagesToUpdate.len > 0:
|
||
echo "🔄 Packages to update (", packagesToUpdate.len, "):"
|
||
for pkg in packagesToUpdate:
|
||
echo " ↗ ", pkg
|
||
|
||
if packagesToRemove.len > 0:
|
||
echo "🗑️ Packages to remove (", packagesToRemove.len, "):"
|
||
for pkg in packagesToRemove:
|
||
echo " - ", pkg
|
||
|
||
if packagesToInstall.len == 0 and packagesToUpdate.len == 0 and packagesToRemove.len == 0:
|
||
echo "✅ System already matches lockfile - no changes needed"
|
||
return true
|
||
|
||
if dryRun:
|
||
echo "🔍 DRY RUN: Would perform the above changes"
|
||
return true
|
||
|
||
# TODO: Implement actual package installation/removal/update
|
||
# This would integrate with the package management system
|
||
echo "⚠️ Actual package operations not yet implemented"
|
||
echo " This would require integration with the package installation system"
|
||
|
||
return true
|
||
|
||
except Exception as e:
|
||
echo "❌ Failed to restore from lockfile: ", e.msg
|
||
return false
|
||
|
||
proc showLockfileDrift*(lm: LockfileManager, lockfilePath: string): bool =
|
||
## Show drift between current system state and lockfile
|
||
try:
|
||
if not fileExists(lockfilePath):
|
||
echo "❌ Lockfile not found: ", lockfilePath
|
||
return false
|
||
|
||
echo "🔍 Analyzing system drift from lockfile: ", lockfilePath
|
||
|
||
# Load lockfile
|
||
let content = readFile(lockfilePath)
|
||
let lockfileJson = parseJson(content)
|
||
|
||
# Get lockfile metadata
|
||
let lockfileGenerated = lockfileJson["lockfile"]["generated"].getStr()
|
||
let lockfileGeneration = lockfileJson["lockfile"]["system_generation"].getStr()
|
||
let lockfilePackages = lockfileJson["packages"].getElems()
|
||
|
||
# Get current system state
|
||
let currentPackages = lm.scanInstalledPackages()
|
||
let currentGeneration = lm.getCurrentGeneration()
|
||
|
||
echo "📊 Drift Analysis:"
|
||
echo " Lockfile generated: ", lockfileGenerated
|
||
echo " Lockfile generation: ", lockfileGeneration
|
||
echo " Current generation: ", currentGeneration
|
||
|
||
# Analyze generation drift
|
||
if lockfileGeneration != currentGeneration:
|
||
echo "⚠️ Generation drift detected:"
|
||
echo " Expected: ", lockfileGeneration
|
||
echo " Current: ", currentGeneration
|
||
else:
|
||
echo "✅ Generation matches lockfile"
|
||
|
||
# Analyze package drift
|
||
var lockfilePackageMap = initTable[string, JsonNode]()
|
||
for pkg in lockfilePackages:
|
||
let key = pkg["name"].getStr() & "-" & pkg["version"].getStr()
|
||
lockfilePackageMap[key] = pkg
|
||
|
||
var currentPackageMap = initTable[string, PackageLockEntry]()
|
||
for pkgentPackages:
|
||
let key = pkg.name & "-" & pkg.version
|
||
currentPackageMap[key] = pkg
|
||
|
||
var driftDetected = false
|
||
var missingPackages: seq[string] = @[]
|
||
var extraPackages: seq[string] = @[]
|
||
var modifiedPackages: seq[string] = @[]
|
||
|
||
# Check for missing packages
|
||
for key, lockfilePkg in lockfilePackageMap:
|
||
if key notin currentPackageMap:
|
||
missingPackages.add(key)
|
||
driftDetected = true
|
||
|
||
# Check for extra packages
|
||
for key, currentPkg in currentPackageMap:
|
||
if key notin lockfilePackageMap:
|
||
extraPackages.add(key)
|
||
driftDetected = true
|
||
|
||
# Check for modified packages (same name-version but different checksums)
|
||
for key, lockfilePkg in lockfilePackageMap:
|
||
if key in currentPackageMap:
|
||
let lockfileChecksum = lockfilePkg["checksum"].getStr()
|
||
let currentChecksum = currentPackageMap[key].checksum
|
||
|
||
if lockfileChecksum.len > 0 and currentChecksum.len > 0 and lockfileChecksum != currentChecksum:
|
||
modifiedPackages.add(key & " (checksum mismatch)")
|
||
driftDetected = true
|
||
|
||
# Report drift results
|
||
if not driftDetected:
|
||
echo "✅ No package drift detected - system matches lockfile exactly"
|
||
else:
|
||
echo "⚠️ Package drift detected:"
|
||
|
||
if missingPackages.len > 0:
|
||
echo " Missing packages (", missingPackages.len, "):"
|
||
for pkg in missingPackages:
|
||
echo " - ", pkg
|
||
|
||
if extraPackages.len > 0:
|
||
echo " Extra packages (", extraPackages.len, "):"
|
||
for pkg in extraPackages:
|
||
echo " + ", pkg
|
||
|
||
if modifiedPackages.len > 0:
|
||
echo " Modified packages (", modifiedPackages.len, "):"
|
||
for pkg in modifiedPackages:
|
||
echo " ~ ", pkg
|
||
|
||
return not driftDetected
|
||
|
||
except Exception as e:
|
||
echo "❌ Failed to analyze drift: ", e.msg
|
||
return false
|
||
|
||
proc mergeLockfiles*(lockfile1Path: string, lockfile2Path: string,
|
||
outputPath: string, strategy: string = "union"): bool =
|
||
## Merge two lockfiles using specified strategy
|
||
try:
|
||
if not fileExists(lockfile1Path):
|
||
echo "❌ Lockfile 1 not found: ", lockfile1Path
|
||
return false
|
||
|
||
if not fileExists(lockfile2Path):
|
||
echo "❌ Lockfile 2 not found: ", lockfile2Path
|
||
return false
|
||
|
||
echo "🔄 Merging lockfiles:"
|
||
echo " Base: ", lockfile1Path
|
||
echo " Merge: ", lockfile2Path
|
||
echo " Output: ", outputPath
|
||
echo " Strategy: ", strategy
|
||
|
||
# Load both lockfiles
|
||
let content1 = readFile(lockfile1Path)
|
||
let content2 = readFile(lockfile2Path)
|
||
let lockfile1 = parseJson(content1)
|
||
let lockfile2 = parseJson(content2)
|
||
|
||
# Create merged lockfile structure
|
||
var mergedLockfile = lockfile1.copy()
|
||
|
||
# Update metadata
|
||
mergedLockfile["lockfile"]["generated"] = %($now())
|
||
mergedLockfile["lockfile"]["generator"] = %"nip-
|
||
p
|
||
# Get package lists
|
||
let packages1 = lockfile1["packages"].getElems()
|
||
let packages2 = lockfile2["packages"].getElems()
|
||
|
||
# Build package maps
|
||
var packages1Map = initTable[string, JsonNode]()
|
||
var packages2Map = initTable[string, JsonNode]()
|
||
|
||
for pkg in packages1:
|
||
let key = pkg["name"].getStr()
|
||
packages1Map[key] = pkg
|
||
|
||
for pkg in packages2:
|
||
let key = pkg["name"].getStr()
|
||
packages2Map[key] = pkg
|
||
|
||
# Merge packages based on strategy
|
||
var mergedPackages: seq[JsonNode] = @[]
|
||
|
||
case strategy:
|
||
of "union":
|
||
# Include all packages from both lockfiles, prefer lockfile2 for conflicts
|
||
var allPackageNames: seq[string] = @[]
|
||
for name in packages1Map.keys:
|
||
allPackageNames.add(name)
|
||
for name in packages2Map.keys:
|
||
if name notin allPackageNames:
|
||
allPackageNames.add(name)
|
||
|
||
for name in allPackageNames:
|
||
if name in packages2Map:
|
||
mergedPackages.add(packages2Map[name])
|
||
else:
|
||
mergedPackages.add(packages1Map[name])
|
||
|
||
of "intersection":
|
||
# Include only packages present in both lockfiles, prefer lockfile2 versions
|
||
for name in packages1Map.keys:
|
||
if name in packages2Map:
|
||
mergedPackages.add(packages2Map[name])
|
||
|
||
of "base-only":
|
||
# Include only packages from lockfile1
|
||
mergedPackages = packages1
|
||
|
||
of "merge-only":
|
||
# Include only packages from lockfile2
|
||
mergedPackages = packages2
|
||
|
||
else:
|
||
echo "❌ Unknown merge strategy: ", strategy
|
||
return false
|
||
|
||
# Update merged lockfile
|
||
mergedLockfile["packages"] = %mergedPackages
|
||
mergedLockfile["metadata"]["package_count"] = %mergedPackages.len
|
||
mergedLockfile["metadata"]["description"] = %("Merged lockfile using " & strategy & " strategy")
|
||
|
||
# Calculate total size
|
||
var totalSize: int64 = 0
|
||
for pkg in mergedPackages:
|
||
totalSize += pkg["installed_size"].getInt()
|
||
mergedLockfile["metadata"]["total_size"] = %totalSize
|
||
|
||
# Write merged lockfile
|
||
let parentDir = parentDir(outputPath)
|
||
if parentDir.len > 0 and not dirExists(parentDir):
|
||
createDir(parentDir)
|
||
|
||
writeFile(outputPath, mergedLockfile.pretty())
|
||
|
||
echo "✅ Lockfiles merged successfully"
|
||
echo " Merged packages: ", mergedPackages.len
|
||
echo " Total size: ", totalSize, " bytes"
|
||
|
||
return true
|
||
|
||
except Exception as e:
|
||
echo "❌ Failed to merge lockfiles: ", e.msg
|
||
return false
|
||
|
||
proc updateLockfile*(lm: LockfileManager, lockfilePath: string,
|
||
packageUpdates: seq[string] = @[]): bool =
|
||
## Update an existing lockfile with current system state or specific package changes
|
||
try:
|
||
if not fileExists(lockfilePath):
|
||
echo "❌ Lockfile not found: ", lockfilePath
|
||
return false
|
||
|
||
echo "🔄 Updating lockfile: ", lockfilePath
|
||
|
||
# Load existing lockfile
|
||
let content = readFile(lockfilePath)
|
||
let existingLockfile = parseJson(content)
|
||
|
||
# Get current system state
|
||
let currentPackages = lm.scanInstalledPackages()
|
||
let currentGeneration = lm.getCurrentGeneration()
|
||
|
||
# Create updated lockfile
|
||
var updatedLockfile = existingLroc printLoc()
|
||
|
||
# Update metadata
|
||
updatedLockfile["lockfile"]["generated"] = %($now())
|
||
updatedLockfile["lockfile"]["system_generation"] = %currentGeneration
|
||
|
||
# Update packages
|
||
if packageUpdates.len == 0:
|
||
# Full update - replace all packages with current state
|
||
let newPackages = currentPackages.mapIt(%*{
|
||
"name": it.name,
|
||
"version": it.version,
|
||
"stream": it.stream,
|
||
"checksum": it.checksum,
|
||
"installed_path": it.installedPath,
|
||
"installed_size": it.installedSize,
|
||
"install_time": $it.installTime,
|
||
"dependencies": it.dependencies,
|
||
"source": {
|
||
"method": it.source.sourceMethod,
|
||
"url": it.source.url,
|
||
"hash": it.source.hash,
|
||
"timestamp": $it.source.timestamp,
|
||
"attribution": it.source.attribution
|
||
}
|
||
})
|
||
|
||
updatedLockfile["packages"] = %newPackages
|
||
updatedLockfile["metadata"]["package_count"] = %newPackages.len
|
||
|
||
let totalSize = currentPackages.mapIt(it.installedSize).foldl(a + b, 0'i64)
|
||
updatedLockfile["metadata"]["total_size"] = %totalSize
|
||
|
||
echo "✅ Full lockfile update completed"
|
||
echo " Updated packages: ", newPackages.len
|
||
echo " Total size: ", totalSize, " bytes"
|
||
else:
|
||
# Selective update - update only specified packages
|
||
var existingPackages = existingLockfile["packages"].getElems()
|
||
var updatedCount = 0
|
||
|
||
# Build current package lookup
|
||
var currentPackageMap = initTable[string, PackageLockEntry]()
|
||
for pkg in currentPackages:
|
||
currentPackageMap[pkg.name] = pkg
|
||
|
||
# Update specified packages
|
||
for i, pkg in existingPackages.mpairs:
|
||
let packageName = pkg["name"].getStr()
|
||
|
||
if packageName in packageUpdates and packageName in currentPackageMap:
|
||
let currentPkg = currentPackageMap[packageName]
|
||
|
||
# Update package information
|
||
pkg["version"] = %currentPkg.version
|
||
pkg["stream"] = %currentPkg.stream
|
||
pkg["checksum"] = %currentPkg.checksum
|
||
pkg["installed_size"] = %currentPkg.installedSize
|
||
pkg["install_time"] = %($currentPkg.installTime)
|
||
pkg["dependencies"] = %currentPkg.dependencies
|
||
|
||
# Update source information
|
||
pkg["source"]["method"] = %currentPkg.source.sourceMethod
|
||
pkg["source"]["url"] = %currentPkg.source.url
|
||
pkg["source"]["hash"] = %currentPkg.source.hash
|
||
pkg["source"]["timestamp"] = %($currentPkg.source.timestamp)
|
||
pkg["source"]["attribution"] = %currentPkg.source.attribution
|
||
|
||
updatedCount += 1
|
||
|
||
updatedLockfile["packages"] = %existingPackages
|
||
|
||
echo "✅ Selective lockfile update completed"
|
||
echo " Updated packages: ", updatedCount, "/", packageUpdates.len
|
||
|
||
# Write updated lockfile
|
||
writeFile(lockfilePath, updatedLockfile.pretty())
|
||
|
||
return true
|
||
|
||
except Exception as e:
|
||
echo "❌ Failed to update lockfilkfi", e.msg
|
||
return false
|
||
|
||
# =============================================================================
|
||
# Advanced Diff Functionality
|
||
# =============================================================================
|
||
|
||
proc detailedLockfileDiff*(lockfile1Path: string, lockfile2Path: string): bool =
|
||
## Show detailed differences between two lockfiles
|
||
try:
|
||
if not fileExists(lockfile1Path):
|
||
echo "❌ Lockfile 1 not found: ", lockfile1Path
|
||
return false
|
||
|
||
if not fileExists(lockfile2Path):
|
||
echo "❌ Lockfile 2 not found: ", lockfile2Path
|
||
return false
|
||
|
||
echo "🔍 Detailed lockfile comparison:"
|
||
echo " File 1: ", lockfile1Path
|
||
echo " File 2: ", lockfile2Path
|
||
|
||
let content1 = readFile(lockfile1Path)
|
||
let content2 = readFile(lockfile2Path)
|
||
let lockfile1 = parseJson(content1)
|
||
let lockfile2 = parseJson(content2)
|
||
|
||
# Compare metadata
|
||
echo "\n📊 Metadata Comparison:"
|
||
|
||
let gen1 = lockfile1["lockfile"]["system_generation"].getStr()
|
||
let gen2 = lockfile2["lockfile"]["system_generation"].getStr()
|
||
let arch1 = lockfile1["lockfile"]["architecture"].getStr()
|
||
let arch2 = lockfile2["lockfile"]["architecture"].getStr()
|
||
let generated1 = lockfile1["lockfile"]["generated"].getStr()
|
||
let generated2 = lockfile2["lockfile"]["generated"].getStr()
|
||
|
||
if gen1 != gen2:
|
||
echo " Generation: ", gen1, " → ", gen2
|
||
else:
|
||
echo " Generation: ", gen1, " (same)"
|
||
|
||
if arch1 != arch2:
|
||
echo " Architecture: ", arch1, " → ", arch2
|
||
else:
|
||
echo " Architecture: ", arch1, " (same)"
|
||
|
||
echo " Generated: ", generated1, " → ", generated2
|
||
|
||
# Compare packages in detail
|
||
echo "\n📦 Package Comparison:"
|
||
|
||
let packages1 = lockfile1["packages"].getElems()
|
||
let packages2 = lockfile2["packages"].getElems()
|
||
|
||
var packages1Map = initTable[string, JsonNode]()
|
||
var packages2Map = initTable[string, JsonNode]()
|
||
|
||
for pkg in packages1:
|
||
let key = pkg["name"].getStr()
|
||
packages1Map[key] = pkg
|
||
|
||
for pkg in packages2:
|
||
let key = pkg["name"].getStr()
|
||
packages2Map[key] = pkg
|
||
|
||
# Analyze changes
|
||
var added: seq[string] = @[]
|
||
var removed: seq[string] = @[]
|
||
var modified: seq[string] = @[]
|
||
var unchanged: seq[string] = @[]
|
||
|
||
# Find added packages
|
||
for name in packages2Map.keys:
|
||
if nale notin packages1Map:
|
||
let pkg = packages2Map[name]
|
||
added.add(name & "-" & pkg["version"].getStr())
|
||
|
||
# Find removed packages
|
||
for name in packages1Map.keys:
|
||
if name notin packages2Map:
|
||
let pkg = packages1Map[name]
|
||
removed.add(name & "-" & pkg["version"].getStr())
|
||
|
||
# Find modified and unchanged packages
|
||
for name in packages1Map.keys:
|
||
if name in packages2Map:
|
||
let pkg1 = packages1Map[name]
|
||
let pkg2 = packages2Map[name]
|
||
|
||
let version1 = pkg1["version"].getStr()
|
||
let version2 = pkg2["version"].getStr()
|
||
let checksum1 = pkg1["checksum"].getStr()
|
||
let checksum2 = pkg2["checksum"].getStr()
|
||
let stream1 = pkg1["stream"].getStr()
|
||
let stream2 = pkg2["stream"].getStr()
|
||
|
||
if version1 != version2 or checksum1 != checksum2 or stream1 != stream2:
|
||
var changes: seq[string] = @[]
|
||
if version1 != version2:
|
||
changes.add("version: " & version1 & " → " & version2)
|
||
if stream1 != stream2:
|
||
changes.add("stream: " & stream1 & " → " & stream2)
|
||
if checksum1 != checksum2:
|
||
changes.add("checksum: " & checksum1[0..7] & "... → " & checksum2[0..7] & "...")
|
||
|
||
modified.add(name & " (" & changes.join(", ") & ")")
|
||
else:
|
||
unchanged.add(name & "-" & version1)
|
||
|
||
# Display results
|
||
echo " Summary:"
|
||
echo " Added: ", added.len
|
||
echo " Removed: ", removed.len
|
||
echo " Modified: ", modified.len
|
||
echo " Unchanged: ", unchanged.len
|
||
|
||
if added.len > 0:
|
||
echo "\n ➕ Added packages:"
|
||
for pkg in added:
|
||
echo " + ", pkg
|
||
|
||
if removed.len > 0:
|
||
echo "\n ➖ Removed packages:"
|
||
for pkg in removed:
|
||
echo " - ", pkg
|
||
|
||
if modified.len > 0:
|
||
echo "\n 🔄 Modified packages:"
|
||
for pkg in modified:
|
||
echo " ~ ", pkg
|
||
|
||
let identical = added.len == 0 and removed.len == 0 and modified.len == 0
|
||
if identical:
|
||
echo "\n✅ Lockfiles are functionally identical"
|
||
else:
|
||
echo "\n❌ Lockfiles differ significantly"
|
||
|
||
return identical
|
||
|
||
except Exception as e:
|
||
echo "❌ Failed to compare lockfiles: ", e.msg
|
||
return false
|
||
|
||
# =============================================================================
|
||
# CLI Integration Functions
|
||
# =============================================================================Info*(lockfilePath: string) =
|
||
## Print information about a lockfile
|
||
try:
|
||
if not fileExists(lockfilePath):
|
||
echo "❌ Lockfile not found: ", lockfilePath
|
||
return
|
||
|
||
let content = readFile(lockfilePath)
|
||
let lockfile = parseJson(content)
|
||
|
||
echo "=== Lockfile Information ==="
|
||
echo "File: ", lockfilePath
|
||
echo "Version: ", lockfile["lockfile"]["version"].getStr()
|
||
echo "Generated: ", lockfile["lockfile"]["generated"].getStr()
|
||
echo "Generator: ", lockfile["lockfile"]["generator"].getStr()
|
||
echo "System Generation: ", lockfile["lockfile"]["system_generation"].getStr()
|
||
echo "Architecture: ", lockfile["lockfile"]["architecture"].getStr()
|
||
|
||
echo "\n=== Metadata ==="
|
||
let metadata = lockfile["metadata"]
|
||
echo "Description: ", metadata["description"].getStr()
|
||
echo "Environment: ", metadata["environment"].getStr()
|
||
echo "Creator: ", metadata["creator"].getStr()
|
||
echo "Tags: ", metadata["tags"].getElems().mapIt(it.getStr()).join(", ")
|
||
echo "Total Size: ", metadata["total_size"].getInt(), " bytes"
|
||
echo "Package Count: ", metadata["package_count"].getInt()
|
||
|
||
echo "\n=== Packages ==="
|
||
let packages = lockfile["packages"].getElems()
|
||
for pkg in packages:
|
||
let name = pkg["name"].getStr()
|
||
let version = pkg["version"].getStr()
|
||
let stream = pkg["stream"].getStr()
|
||
let size = pkg["installed_size"].getInt()
|
||
echo " ", name, "-", version, " (", stream, ") - ", size, " bytes"
|
||
|
||
except Exception as e:
|
||
echo "❌ Failed to read lockfile: ", e.msg
|
||
|
||
proc generateLockfileCommand*(lockfilePath: string = "nip.lock",
|
||
format: string = "json",
|
||
description: string = "",
|
||
environment: string = "",
|
||
creator: string = "",
|
||
tags: seq[string] = @[]): bool =
|
||
## CLI command to generate a lockfile
|
||
let lockfileFormat = case format.toLowerAscii():
|
||
of "json": LockfileJson
|
||
of "kdl": LockfileKdl
|
||
of "yaml": LockfileYaml
|
||
else: LockfileJson
|
||
|
||
let lm = newLockfileManager(
|
||
lockfilePath = lockfilePath,
|
||
format = lockfileFormat
|
||
)
|
||
|
||
return lm.generateLockfile(description, environment, creator, tags)proc r
|
||
estoreLockfileCommand*(lockfilePath: string = "nip.lock",
|
||
dryRun: bool = false): bool =
|
||
## CLI command to restore from a lockfile
|
||
let lm = newLockfileManager()
|
||
return lm.restoreFromLockfile(lockfilePath, dryRun)
|
||
|
||
proc validateLockfileCommand*(lockfilePath: string = "nip.lock"): bool =
|
||
## CLI command to validate a lockfile
|
||
let lm = newLockfileManager(lockfilePath = lockfilePath)
|
||
return lm.validateLockfile()
|
||
|
||
proc diffLockfileCommand*(lockfile1Path: string, lockfile2Path: string,
|
||
detailed: bool = false): bool =
|
||
## CLI command to compare two lockfiles
|
||
if detailed:
|
||
return detailedLockfileDiff(lockfile1Path, lockfile2Path)
|
||
else:
|
||
return compareLockfiles(lockfile1Path, lockfile2Path)
|
||
|
||
proc driftLockfileCommand*(lockfilePath: string = "nip.lock"): bool =
|
||
## CLI command to show system drift from lockfile
|
||
let lm = newLockfileManager()
|
||
return lm.showLockfileDrift(lockfilePath)
|
||
|
||
proc mergeLockfileCommand*(lockfile1Path: string, lockfile2Path: string,
|
||
outputPath: string, strategy: string = "union"): bool =
|
||
## CLI command to merge two lockfiles
|
||
return mergeLockfiles(lockfile1Path, lockfile2Path, outputPath, strategy)
|
||
|
||
proc updateLockfileCommand*(lockfilePath: string = "nip.lock",
|
||
packages: seq[string] = @[]): bool =
|
||
## CLI command to update an existing lockfile
|
||
let lm = newLockfileManager()
|
||
return lm.updateLockfile(lockfilePath, packages) |