mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-03-28 17:42:55 +01:00
337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
import PARENT_MAP from "@/constants/architecture-hierarchy.json"
|
|
import { type ClassValue, clsx } from "clsx"
|
|
import { twMerge } from "tailwind-merge"
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
return twMerge(clsx(inputs))
|
|
}
|
|
|
|
export function timeAgo(date: number | string | Date): string {
|
|
const now = new Date()
|
|
const past = new Date(date)
|
|
const msPerMinute = 60 * 1000
|
|
const msPerHour = msPerMinute * 60
|
|
const msPerDay = msPerHour * 24
|
|
const msPerMonth = msPerDay * 30
|
|
const msPerYear = msPerDay * 365
|
|
|
|
const elapsed = now.getTime() - past.getTime()
|
|
|
|
if (elapsed < msPerMinute) {
|
|
return `${Math.round(elapsed / 1000)}s ago`
|
|
} else if (elapsed < msPerHour) {
|
|
return `${Math.round(elapsed / msPerMinute)}m ago`
|
|
} else if (elapsed < msPerDay) {
|
|
return `${Math.round(elapsed / msPerHour)}h ago`
|
|
} else if (elapsed < msPerMonth) {
|
|
return `${Math.round(elapsed / msPerDay)}d ago`
|
|
} else if (elapsed < msPerYear) {
|
|
return `${Math.round(elapsed / msPerMonth)}mo ago`
|
|
} else {
|
|
return `${Math.round(elapsed / msPerYear)}y ago`
|
|
}
|
|
}
|
|
|
|
export function humanizeStatus(status: string): string {
|
|
// Handle special statuses
|
|
if (status === "success") return "Success"
|
|
if (status === "failure") return "Failure"
|
|
if (status === "queued") return "Queued"
|
|
if (status === "in_progress") return "In Progress"
|
|
|
|
// Convert snake_case/underscore_separated to Title Case
|
|
return status
|
|
.split("_")
|
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
.join(" ")
|
|
}
|
|
|
|
/**
|
|
* Resolves plugin dependencies recursively from registry.
|
|
* Returns all plugins that should be enabled (selected plugins + their dependencies).
|
|
* Filters out "meshtastic" as it's a firmware version requirement, not a plugin.
|
|
*/
|
|
export function getDependedPlugins(
|
|
selectedPlugins: string[],
|
|
registry: Record<string, { dependencies?: Record<string, string> }>
|
|
): string[] {
|
|
const result = new Set<string>()
|
|
const visited = new Set<string>()
|
|
|
|
function resolveDependencies(pluginId: string) {
|
|
// Prevent circular dependencies
|
|
if (visited.has(pluginId)) return
|
|
visited.add(pluginId)
|
|
|
|
const plugin = registry[pluginId]
|
|
if (!plugin || !plugin.dependencies) return
|
|
|
|
// Process each dependency
|
|
for (const [depId] of Object.entries(plugin.dependencies)) {
|
|
// Skip "meshtastic" - it's a firmware version requirement, not a plugin
|
|
if (depId === "meshtastic") continue
|
|
|
|
// Only include dependencies that exist in the registry
|
|
if (depId in registry) {
|
|
result.add(depId)
|
|
// Recursively resolve transitive dependencies
|
|
resolveDependencies(depId)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Start with selected plugins
|
|
for (const pluginId of selectedPlugins) {
|
|
if (pluginId in registry) {
|
|
result.add(pluginId)
|
|
resolveDependencies(pluginId)
|
|
}
|
|
}
|
|
|
|
return Array.from(result)
|
|
}
|
|
|
|
/**
|
|
* Gets only the implicit dependencies (dependencies that are not explicitly selected).
|
|
* Returns a set of plugin IDs that are dependencies but not in the explicitly selected list.
|
|
*/
|
|
export function getImplicitDependencies(
|
|
explicitlySelectedPlugins: string[],
|
|
registry: Record<string, { dependencies?: Record<string, string> }>
|
|
): Set<string> {
|
|
const allDependencies = getDependedPlugins(explicitlySelectedPlugins, registry)
|
|
const explicitSet = new Set(explicitlySelectedPlugins)
|
|
return new Set(allDependencies.filter(id => !explicitSet.has(id)))
|
|
}
|
|
|
|
/**
|
|
* Checks if a plugin is required by any other explicitly selected plugin.
|
|
* Returns true if the plugin is a dependency (direct or transitive) of at least one explicitly selected plugin.
|
|
*/
|
|
export function isRequiredByOther(
|
|
pluginId: string,
|
|
explicitlySelectedPlugins: string[],
|
|
registry: Record<string, { dependencies?: Record<string, string> }>
|
|
): boolean {
|
|
// Check if any explicitly selected plugin depends on this plugin
|
|
for (const selectedId of explicitlySelectedPlugins) {
|
|
if (selectedId === pluginId) continue // Skip self
|
|
|
|
// Get all dependencies (including transitive) of this selected plugin
|
|
const allDeps = getDependedPlugins([selectedId], registry)
|
|
if (allDeps.includes(pluginId)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Normalize architecture name (remove hyphens and underscores to match PlatformIO format)
|
|
* PlatformIO uses "esp32s3", "nrf52840" (no hyphens, no underscores)
|
|
* Hardware list uses "esp32-s3" (with hyphens)
|
|
* Some sources might use "esp32_s3" (with underscores)
|
|
*/
|
|
function normalizeArchitecture(arch: string): string {
|
|
return arch.replace(/[-_]/g, "")
|
|
}
|
|
|
|
/**
|
|
* Trace a target/variant/architecture back to its base architecture
|
|
* Follows the parent chain until it reaches a base architecture (null parent)
|
|
*/
|
|
export function getBaseArchitecture(name: string): string | null {
|
|
const normalized = normalizeArchitecture(name)
|
|
const parentMap = PARENT_MAP as Record<string, string | null>
|
|
|
|
const visited = new Set<string>()
|
|
let current = normalized
|
|
|
|
while (current && !visited.has(current)) {
|
|
visited.add(current)
|
|
const parent = parentMap[current]
|
|
|
|
// If parent is null, we've reached a base architecture
|
|
if (parent === null) {
|
|
return current
|
|
}
|
|
|
|
// If no parent found, return current (might be unknown)
|
|
if (parent === undefined) {
|
|
return current
|
|
}
|
|
|
|
current = normalizeArchitecture(parent)
|
|
}
|
|
|
|
// Circular reference or unknown, return the last known
|
|
return current || normalized
|
|
}
|
|
|
|
/**
|
|
* Get all compatible architectures for a given architecture
|
|
* (including itself and all parent architectures up to base)
|
|
*/
|
|
export function getCompatibleArchitectures(arch: string): string[] {
|
|
const normalized = normalizeArchitecture(arch)
|
|
const parentMap = PARENT_MAP as Record<string, string | null>
|
|
|
|
const compatible = [normalized]
|
|
const visited = new Set<string>()
|
|
let current = normalized
|
|
|
|
// Follow parent chain up to base architecture
|
|
while (current && !visited.has(current)) {
|
|
visited.add(current)
|
|
const parent = parentMap[current]
|
|
|
|
if (parent === null) {
|
|
// Reached base architecture
|
|
break
|
|
}
|
|
|
|
if (parent === undefined) {
|
|
// Unknown, stop here
|
|
break
|
|
}
|
|
|
|
const normalizedParent = normalizeArchitecture(parent)
|
|
if (!compatible.includes(normalizedParent)) {
|
|
compatible.push(normalizedParent)
|
|
}
|
|
|
|
current = normalizedParent
|
|
}
|
|
|
|
return compatible
|
|
}
|
|
|
|
/**
|
|
* Check if a plugin is compatible with a target
|
|
* Plugin can specify includes/excludes arrays with targets, variant bases, or architectures
|
|
*
|
|
* @param pluginIncludes - Array of architectures/targets the plugin explicitly supports
|
|
* @param pluginExcludes - Array of architectures/targets the plugin explicitly doesn't support
|
|
* @param targetName - The target name to check compatibility against
|
|
*/
|
|
export function isPluginCompatibleWithTarget(
|
|
pluginIncludes: string[] | undefined,
|
|
pluginExcludes: string[] | undefined,
|
|
targetName: string | undefined
|
|
): boolean {
|
|
// If target not specified, can't determine compatibility
|
|
if (!targetName) {
|
|
return true // Default to compatible if unknown
|
|
}
|
|
|
|
const parentMap = PARENT_MAP as Record<string, string | null>
|
|
|
|
// Normalize target name first (all keys in parentMap are normalized)
|
|
const normalizedTarget = normalizeArchitecture(targetName)
|
|
|
|
// Get all compatible names for the target (target itself + all parents up to base architecture)
|
|
const compatibleNames = new Set<string>([normalizedTarget])
|
|
const visited = new Set<string>()
|
|
let current = normalizedTarget
|
|
|
|
// Follow parent chain (all keys and values in parentMap are already normalized)
|
|
while (current && !visited.has(current)) {
|
|
visited.add(current)
|
|
const parent = parentMap[current]
|
|
|
|
if (parent === null) {
|
|
// Reached base architecture
|
|
compatibleNames.add(current) // Add the base architecture itself
|
|
break
|
|
}
|
|
|
|
if (parent === undefined) {
|
|
// Unknown, stop here
|
|
break
|
|
}
|
|
|
|
// Parent is already normalized (from JSON)
|
|
compatibleNames.add(parent)
|
|
current = parent
|
|
}
|
|
|
|
// Check excludes first - if target matches any exclude, it's incompatible
|
|
// compatibleNames are already normalized, normalize excludes for comparison
|
|
if (pluginExcludes && pluginExcludes.length > 0) {
|
|
const isExcluded = pluginExcludes.some(exclude => {
|
|
const normalizedExclude = normalizeArchitecture(exclude)
|
|
return compatibleNames.has(normalizedExclude)
|
|
})
|
|
if (isExcluded) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// If includes are specified, target must match at least one include
|
|
// compatibleNames are already normalized, normalize includes for comparison
|
|
if (pluginIncludes && pluginIncludes.length > 0) {
|
|
return pluginIncludes.some(include => {
|
|
const normalizedInclude = normalizeArchitecture(include)
|
|
return compatibleNames.has(normalizedInclude)
|
|
})
|
|
}
|
|
|
|
// If no includes/excludes specified, assume compatible with all (backward compatible)
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Check if a plugin is compatible with a target architecture
|
|
* @deprecated Use isPluginCompatibleWithTarget instead
|
|
*/
|
|
export function isPluginCompatibleWithArchitecture(
|
|
pluginArchitectures: string[] | undefined,
|
|
targetArchitecture: string | undefined
|
|
): boolean {
|
|
// Legacy support: treat architectures array as includes
|
|
return isPluginCompatibleWithTarget(pluginArchitectures, undefined, targetArchitecture)
|
|
}
|
|
|
|
/**
|
|
* Get all targets that inherit from (are descendants of) the given architectures
|
|
* Traces backwards through the hierarchy to find all targets/variants that inherit from the includes
|
|
* Returns normalized target IDs that can be matched against TARGETS keys
|
|
*/
|
|
export function getTargetsCompatibleWithIncludes(includes: string[]): Set<string> {
|
|
const parentMap = PARENT_MAP as Record<string, string | null>
|
|
const compatibleTargets = new Set<string>()
|
|
|
|
// Normalize includes
|
|
const normalizedIncludes = new Set(includes.map(include => normalizeArchitecture(include)))
|
|
|
|
// For each target in the parent map, check if it or any of its ancestors match the includes
|
|
for (const target of Object.keys(parentMap)) {
|
|
const normalizedTarget = normalizeArchitecture(target)
|
|
const visited = new Set<string>()
|
|
let current: string | null = normalizedTarget
|
|
|
|
// Trace up the parent chain
|
|
while (current && !visited.has(current)) {
|
|
visited.add(current)
|
|
|
|
// Check if current matches any of the includes
|
|
if (normalizedIncludes.has(current)) {
|
|
// Add both the normalized version and the original (for matching against TARGETS)
|
|
compatibleTargets.add(normalizedTarget)
|
|
compatibleTargets.add(target)
|
|
break
|
|
}
|
|
|
|
// Move to parent
|
|
const parentValue = parentMap[current]
|
|
if (parentValue === null || parentValue === undefined) {
|
|
break
|
|
}
|
|
current = normalizeArchitecture(parentValue)
|
|
}
|
|
}
|
|
|
|
return compatibleTargets
|
|
}
|