diff --git a/pages/builds/new/+Page.tsx b/pages/builds/new/+Page.tsx
deleted file mode 100644
index 5cc12b7..0000000
--- a/pages/builds/new/+Page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import Builder from "./Builder"
-
-export default function BuildNew() {
- return
-}
diff --git a/pages/builds/new/@buildHash/+Page.tsx b/pages/builds/new/@buildHash/+Page.tsx
deleted file mode 100644
index ee47899..0000000
--- a/pages/builds/new/@buildHash/+Page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import Builder from "../Builder"
-
-export default function BuildNew() {
- return
-}
diff --git a/pages/builds/new/Builder.tsx b/pages/builds/new/Builder.tsx
deleted file mode 100644
index 8ef30df..0000000
--- a/pages/builds/new/Builder.tsx
+++ /dev/null
@@ -1,857 +0,0 @@
-import { ModuleToggle } from "@/components/ModuleToggle"
-import { PluginCard } from "@/components/PluginCard"
-import { Button } from "@/components/ui/button"
-import { TARGETS } from "@/constants/targets"
-import { VERSIONS } from "@/constants/versions"
-import { api } from "@/convex/_generated/api"
-import modulesData from "@/convex/modules.json"
-import {
- getDependedPlugins,
- getImplicitDependencies,
- getTargetsCompatibleWithIncludes,
- isPluginCompatibleWithTarget,
- isRequiredByOther,
-} from "@/lib/utils"
-import registryData from "@/public/registry.json"
-import { useMutation, useQuery } from "convex/react"
-import { CheckCircle2, ChevronDown, ChevronRight, Loader2 } from "lucide-react"
-import { useEffect, useState } from "react"
-import { toast } from "sonner"
-import { usePageContext } from "vike-react/usePageContext"
-import { navigate } from "vike/client/router"
-
-type TargetGroup = (typeof TARGETS)[string] & { id: string }
-
-const GROUPED_TARGETS = Object.entries(TARGETS).reduce(
- (acc, [id, meta]) => {
- const category = meta.category || "Other"
- if (!acc[category]) acc[category] = []
- acc[category].push({ id, ...meta })
- return acc
- },
- {} as Record
-)
-
-const TARGET_CATEGORIES = Object.keys(GROUPED_TARGETS).sort((a, b) => a.localeCompare(b))
-
-const DEFAULT_TARGET =
- TARGET_CATEGORIES.length > 0 && GROUPED_TARGETS[TARGET_CATEGORIES[0]]?.length
- ? GROUPED_TARGETS[TARGET_CATEGORIES[0]][0].id
- : ""
-
-export default function BuildNew() {
- const pageContext = usePageContext()
- const buildHashParam = pageContext.routeParams?.buildHash
- const ensureBuildFromConfig = useMutation(api.builds.ensureBuildFromConfig)
- const pluginFlashCounts = useQuery(api.plugins.getAll) ?? {}
- const sharedBuild = useQuery(api.builds.getByHash, buildHashParam ? { buildHash: buildHashParam } : "skip")
-
- // Get plugin from URL query parameter
- const pluginParam = typeof window !== "undefined" ? new URLSearchParams(window.location.search).get("plugin") : null
- const preselectedPlugin =
- pluginParam && pluginParam in registryData
- ? (
- registryData as Record<
- string,
- { includes?: string[]; name: string; description: string; imageUrl?: string; featured?: boolean }
- >
- )[pluginParam]
- : null
-
- const STORAGE_KEY = "quick_build_target"
- const getStorageKeyForCategory = (category: string) => `quick_build_target_${category}`
-
- const persistTargetSelection = (targetId: string, category?: string) => {
- if (typeof window === "undefined") return
- try {
- // Store global most recent selection
- window.localStorage.setItem(STORAGE_KEY, targetId)
- // Store per-brand selection if category provided
- if (category) {
- window.localStorage.setItem(getStorageKeyForCategory(category), targetId)
- }
- } catch (error) {
- console.error("Failed to persist target selection", error)
- }
- }
-
- const getSavedTargetForCategory = (category: string): string | null => {
- if (typeof window === "undefined") return null
- try {
- return window.localStorage.getItem(getStorageKeyForCategory(category))
- } catch (error) {
- console.error("Failed to read saved target for category", error)
- return null
- }
- }
-
- const [activeCategory, setActiveCategory] = useState(TARGET_CATEGORIES[0] ?? "")
- const [selectedTarget, setSelectedTarget] = useState(DEFAULT_TARGET)
- const [selectedVersion, setSelectedVersion] = useState(VERSIONS[0])
- const [moduleConfig, setModuleConfig] = useState>({})
- const [pluginConfig, setPluginConfig] = useState>({})
- const [isFlashing, setIsFlashing] = useState(false)
- const [errorMessage, setErrorMessage] = useState(null)
- const [showModuleOverrides, setShowModuleOverrides] = useState(false)
- const [showPlugins, setShowPlugins] = useState(true)
- const [isLoadingSharedBuild, setIsLoadingSharedBuild] = useState(false)
-
- // Get all enabled plugins
- const enabledPlugins = Object.keys(pluginConfig).filter(id => pluginConfig[id] === true)
-
- // Filter targets based on plugin compatibility
- // Start with preselected plugin compatibility if present
- let compatibleTargets: Set | null = preselectedPlugin?.includes
- ? getTargetsCompatibleWithIncludes(preselectedPlugin.includes)
- : null
-
- // Intersect with compatibility of all enabled plugins
- if (enabledPlugins.length > 0) {
- const pluginRegistry = registryData as Record
- const allCompatibleSets: Set[] = []
-
- // Get compatible targets for each enabled plugin
- for (const pluginId of enabledPlugins) {
- const plugin = pluginRegistry[pluginId]
- if (plugin?.includes && plugin.includes.length > 0) {
- // Plugin has includes - get compatible targets
- allCompatibleSets.push(getTargetsCompatibleWithIncludes(plugin.includes))
- }
- // If plugin has no includes, it's compatible with all targets (don't add to set)
- }
-
- // If we have compatible sets, find intersection
- if (allCompatibleSets.length > 0) {
- if (compatibleTargets) {
- // Intersect with preselected plugin compatibility
- compatibleTargets = new Set(
- Array.from(compatibleTargets).filter(target => allCompatibleSets.every(set => set.has(target)))
- )
- } else {
- // Start with first set, then intersect with others
- compatibleTargets = allCompatibleSets[0]
- for (let i = 1; i < allCompatibleSets.length; i++) {
- compatibleTargets = new Set(Array.from(compatibleTargets).filter(target => allCompatibleSets[i].has(target)))
- }
- }
- } else if (!compatibleTargets) {
- // No enabled plugins have includes, so all targets are compatible
- // (only if there's no preselected plugin with includes)
- compatibleTargets = null
- }
- }
-
- const filteredGroupedTargets = compatibleTargets
- ? Object.entries(GROUPED_TARGETS).reduce(
- (acc, [category, targets]) => {
- const filtered = targets.filter(target => {
- // Check both normalized and original target ID
- const normalizedId = target.id.replace(/[-_]/g, "")
- return compatibleTargets.has(target.id) || compatibleTargets.has(normalizedId)
- })
- if (filtered.length > 0) {
- acc[category] = filtered
- }
- return acc
- },
- {} as Record
- )
- : GROUPED_TARGETS
- const filteredTargetCategories = Object.keys(filteredGroupedTargets).sort((a, b) => a.localeCompare(b))
-
- // Preselect plugin from URL parameter
- useEffect(() => {
- if (pluginParam && preselectedPlugin && !buildHashParam) {
- setPluginConfig({ [pluginParam]: true })
- setShowPlugins(true)
- }
- }, [pluginParam, preselectedPlugin, buildHashParam])
-
- useEffect(() => {
- const categories = compatibleTargets ? filteredTargetCategories : TARGET_CATEGORIES
- if (!activeCategory && categories.length > 0) {
- setActiveCategory(categories[0])
- }
- }, [activeCategory, compatibleTargets, filteredTargetCategories])
-
- useEffect(() => {
- if (activeCategory) {
- const targets = compatibleTargets ? filteredGroupedTargets : GROUPED_TARGETS
- const categoryTargets = targets[activeCategory] || []
-
- if (categoryTargets.length === 0) return
-
- // Check if current selected target is in this category
- const isCurrentTargetInCategory = categoryTargets.some(t => t.id === selectedTarget)
-
- if (!isCurrentTargetInCategory) {
- // Try to restore per-brand selection
- const savedTargetForCategory = getSavedTargetForCategory(activeCategory)
- const isValidSavedTarget = savedTargetForCategory && categoryTargets.some(t => t.id === savedTargetForCategory)
-
- if (isValidSavedTarget) {
- setSelectedTarget(savedTargetForCategory)
- // Persist the restored selection
- persistTargetSelection(savedTargetForCategory, activeCategory)
- } else {
- // Default to first target in category and persist it
- const firstTarget = categoryTargets[0].id
- setSelectedTarget(firstTarget)
- persistTargetSelection(firstTarget, activeCategory)
- }
- }
- }
- }, [activeCategory, compatibleTargets, filteredGroupedTargets, selectedTarget])
-
- useEffect(() => {
- if (typeof window === "undefined") return
- try {
- const targets = compatibleTargets ? filteredGroupedTargets : GROUPED_TARGETS
- const categories = compatibleTargets ? filteredTargetCategories : TARGET_CATEGORIES
-
- if (categories.length === 0) return
-
- // Try to restore the most recent global selection first
- const savedTarget = localStorage.getItem(STORAGE_KEY)
- if (savedTarget && TARGETS[savedTarget]) {
- // Check if saved target exists in filtered targets
- const isCompatible = Object.values(targets).some(categoryTargets =>
- categoryTargets.some(target => target.id === savedTarget)
- )
-
- if (isCompatible) {
- const category = TARGETS[savedTarget].category || "Other"
- if (categories.includes(category)) {
- setActiveCategory(category)
- setSelectedTarget(savedTarget)
- persistTargetSelection(savedTarget, category)
- return
- }
- }
- }
-
- // Fall back to per-brand selection for first category
- const firstCategory = categories[0]
- const categoryTargets = targets[firstCategory] || []
-
- if (categoryTargets.length > 0) {
- // Try to restore per-brand selection
- const savedTargetForCategory = getSavedTargetForCategory(firstCategory)
- const isValidSavedTarget = savedTargetForCategory && categoryTargets.some(t => t.id === savedTargetForCategory)
-
- if (isValidSavedTarget) {
- setActiveCategory(firstCategory)
- setSelectedTarget(savedTargetForCategory)
- persistTargetSelection(savedTargetForCategory, firstCategory)
- } else {
- // Default to first target in category
- const firstTarget = categoryTargets[0].id
- setActiveCategory(firstCategory)
- setSelectedTarget(firstTarget)
- persistTargetSelection(firstTarget, firstCategory)
- }
- }
- } catch (error) {
- console.error("Failed to read saved target", error)
- }
- }, [compatibleTargets, filteredGroupedTargets, filteredTargetCategories])
-
- const handleSelectTarget = (targetId: string) => {
- // Validate target is compatible with selected plugins
- if (compatibleTargets) {
- const normalizedId = targetId.replace(/[-_]/g, "")
- const isCompatible = compatibleTargets.has(targetId) || compatibleTargets.has(normalizedId)
- if (!isCompatible) {
- // Target is not compatible, don't allow selection
- return
- }
- }
- setSelectedTarget(targetId)
- const category = TARGETS[targetId]?.category || "Other"
- persistTargetSelection(targetId, category)
- if (category && TARGET_CATEGORIES.includes(category)) {
- setActiveCategory(category)
- }
- }
-
- // Update selected target if it becomes incompatible with selected plugins
- useEffect(() => {
- if (!selectedTarget || !compatibleTargets) return
-
- const normalizedId = selectedTarget.replace(/[-_]/g, "")
- const isCompatible = compatibleTargets.has(selectedTarget) || compatibleTargets.has(normalizedId)
-
- if (!isCompatible) {
- // Current target is no longer compatible, find a compatible one
- const targets = filteredGroupedTargets
- const categories = filteredTargetCategories
-
- if (categories.length > 0) {
- // Try to find a compatible target in the current category first
- const currentCategory = TARGETS[selectedTarget]?.category
- if (currentCategory && targets[currentCategory] && targets[currentCategory].length > 0) {
- const savedTargetForCategory = getSavedTargetForCategory(currentCategory)
- const isValidSavedTarget =
- savedTargetForCategory && targets[currentCategory].some(t => t.id === savedTargetForCategory)
-
- if (isValidSavedTarget) {
- setSelectedTarget(savedTargetForCategory)
- persistTargetSelection(savedTargetForCategory, currentCategory)
- return
- }
-
- // Default to first target in current category
- setSelectedTarget(targets[currentCategory][0].id)
- persistTargetSelection(targets[currentCategory][0].id, currentCategory)
- return
- }
-
- // Fall back to first compatible target
- const firstCategory = categories[0]
- const firstTarget = targets[firstCategory]?.[0]?.id
- if (firstTarget) {
- setSelectedTarget(firstTarget)
- setActiveCategory(firstCategory)
- persistTargetSelection(firstTarget, firstCategory)
- }
- }
- }
- }, [compatibleTargets, filteredGroupedTargets, filteredTargetCategories, selectedTarget])
-
- useEffect(() => {
- if (typeof window === "undefined" || !selectedTarget) return
- try {
- if (!window.localStorage.getItem(STORAGE_KEY)) {
- window.localStorage.setItem(STORAGE_KEY, selectedTarget)
- }
- } catch (error) {
- console.error("Failed to initialize target storage", error)
- }
- }, [selectedTarget])
-
- // Pre-populate form from shared build
- useEffect(() => {
- if (!buildHashParam) return
- if (sharedBuild === undefined) {
- setIsLoadingSharedBuild(true)
- return
- }
- setIsLoadingSharedBuild(false)
-
- if (!sharedBuild) {
- setErrorMessage("Build not found. The shared build may have been deleted.")
- toast.error("Build not found", {
- description: "The shared build could not be loaded.",
- })
- return
- }
-
- const config = sharedBuild.config
-
- // Set target and category
- if (config.target && TARGETS[config.target]) {
- setSelectedTarget(config.target)
- const category = TARGETS[config.target].category || "Other"
- if (TARGET_CATEGORIES.includes(category)) {
- setActiveCategory(category)
- }
- }
-
- // Set version
- if (config.version && (VERSIONS as readonly string[]).includes(config.version)) {
- setSelectedVersion(config.version as (typeof VERSIONS)[number])
- }
-
- // Set module config (already in the correct format)
- if (config.modulesExcluded) {
- setModuleConfig(config.modulesExcluded)
- if (Object.keys(config.modulesExcluded).length > 0) {
- setShowModuleOverrides(true)
- }
- }
-
- // Set plugin config (convert array to object format)
- // Only add explicitly selected plugins, not implicit dependencies
- if (config.pluginsEnabled && config.pluginsEnabled.length > 0) {
- const allPluginSlugs = config.pluginsEnabled.map(pluginId => {
- return pluginId.includes("@") ? pluginId.split("@")[0] : pluginId
- })
-
- // Determine which plugins are required by others (implicit dependencies)
- const requiredByOthers = new Set()
- for (const pluginSlug of allPluginSlugs) {
- if (
- isRequiredByOther(
- pluginSlug,
- allPluginSlugs,
- registryData as Record }>
- )
- ) {
- requiredByOthers.add(pluginSlug)
- }
- }
-
- // Only add plugins that are NOT required by others (explicitly selected)
- const pluginObj: Record = {}
- allPluginSlugs.forEach(slug => {
- if (slug in registryData && !requiredByOthers.has(slug)) {
- pluginObj[slug] = true
- }
- })
- setPluginConfig(pluginObj)
- setShowPlugins(true)
- }
- }, [buildHashParam, sharedBuild])
-
- const moduleCount = Object.keys(moduleConfig).length
- const pluginCount = Object.keys(pluginConfig).filter(id => pluginConfig[id] === true).length
- const selectedTargetLabel = (selectedTarget && TARGETS[selectedTarget]?.name) || selectedTarget
-
- const handleToggleModule = (id: string, excluded: boolean) => {
- setModuleConfig(prev => {
- const next = { ...prev }
- if (excluded) {
- next[id] = true
- } else {
- delete next[id]
- }
- return next
- })
- }
-
- const handleTogglePlugin = (id: string, enabled: boolean) => {
- // Get current explicit selections
- const explicitPlugins = Object.keys(pluginConfig).filter(pluginId => pluginConfig[pluginId] === true)
-
- // Check if this plugin is currently an implicit dependency
- const implicitDeps = getImplicitDependencies(
- explicitPlugins,
- registryData as Record }>
- )
-
- // Check if this plugin is required by another explicitly selected plugin
- const isRequired = isRequiredByOther(
- id,
- explicitPlugins,
- registryData as Record }>
- )
-
- // Don't allow toggling implicit dependencies at all
- // (they should be disabled in the UI, but add this as a safeguard)
- if (implicitDeps.has(id)) {
- return // Can't toggle implicit dependencies
- }
-
- // Don't allow disabling if it's required by another explicitly selected plugin
- if (!enabled && isRequired) {
- return // Can't disable required plugins
- }
-
- setPluginConfig(prev => {
- const next = { ...prev }
- if (enabled) {
- // Enabling: add to explicit selection (even if it was implicit)
- next[id] = true
- } else {
- // Disabling: remove from explicit selection
- delete next[id]
-
- // Recompute what plugins are still needed after removal
- const remainingExplicit = Object.keys(next).filter(pluginId => next[pluginId] === true)
- const allStillNeeded = getDependedPlugins(
- remainingExplicit,
- registryData as Record }>
- )
-
- // Remove any plugins from config that are no longer needed
- // BUT preserve all plugins that are currently explicitly selected (in remainingExplicit)
- // This ensures that plugins that were explicitly selected remain explicitly selected
- // even if they temporarily became implicit and then un-implicit
- for (const pluginId of Object.keys(next)) {
- if (next[pluginId] === true && !allStillNeeded.includes(pluginId) && !remainingExplicit.includes(pluginId)) {
- // This plugin is no longer needed and is not in the remaining explicit list
- // Only remove if it's truly not needed and wasn't explicitly selected
- // Note: If a plugin is in `next` with value `true`, it should be in `remainingExplicit`
- // So this condition should rarely be true, but we keep it as a safety check
- delete next[pluginId]
- }
- }
-
- // Ensure all remaining explicitly selected plugins stay in config
- // (they should already be there, but this ensures they remain even if they're not needed)
- for (const pluginId of remainingExplicit) {
- next[pluginId] = true
- }
- }
- return next
- })
- }
-
- const handleFlash = async () => {
- if (!selectedTarget) return
- setIsFlashing(true)
- setErrorMessage(null)
- try {
- const enabledSlugs = Object.keys(pluginConfig).filter(id => pluginConfig[id] === true)
-
- // Double-check: filter out any implicit dependencies that might have snuck in
- // This ensures we only send explicitly selected plugins to the backend
- const implicitDeps = getImplicitDependencies(
- enabledSlugs,
- registryData as Record }>
- )
- const explicitOnlySlugs = enabledSlugs.filter(slug => !implicitDeps.has(slug))
-
- const pluginsEnabled = explicitOnlySlugs.map(slug => {
- const plugin = (registryData as Record)[slug]
- return `${slug}@${plugin.version}`
- })
- const result = await ensureBuildFromConfig({
- target: selectedTarget,
- version: selectedVersion,
- modulesExcluded: moduleConfig,
- pluginsEnabled: pluginsEnabled.length > 0 ? pluginsEnabled : undefined,
- })
- navigate(`/builds/${result.buildHash}`)
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error)
- setErrorMessage("Failed to start build. Please try again.")
- toast.error("Failed to start build", {
- description: message,
- })
- } finally {
- setIsFlashing(false)
- }
- }
-
- const isFlashDisabled = !selectedTarget || isFlashing
-
- if (isLoadingSharedBuild) {
- return (
-
-
-
-
Loading shared build configuration...
-
-
- )
- }
-
- return (
-
-
-
-
-
- {preselectedPlugin ? "Plugin build" : "Quick build"}
-
-
- {preselectedPlugin ? `Build firmware for ${preselectedPlugin.name}` : "Flash a custom firmware version"}
-
-
- {preselectedPlugin
- ? `Select a compatible Meshtastic target and configure your build for ${preselectedPlugin.name}. We'll send you to the build status page as soon as it starts.`
- : "Choose your Meshtastic target, adjust optional modules, and queue a new build instantly. We'll send you to the build status page as soon as it starts."}
-
-
-
-
- {preselectedPlugin && (
-
-
-
-
- {preselectedPlugin.imageUrl && (
-
- )}
-
-
-
{preselectedPlugin.name}
- {preselectedPlugin.featured && (
-
- Featured
-
- )}
-
-
{preselectedPlugin.description}
- {preselectedPlugin.includes && preselectedPlugin.includes.length > 0 && (
-
Compatible with: {preselectedPlugin.includes.join(", ")}
- )}
-
-
-
-
- )}
-
-
-
-
- {(compatibleTargets ? filteredTargetCategories : TARGET_CATEGORIES).map(category => {
- const isActive = activeCategory === category
- return (
- {
- // Always allow switching to category - the useEffect will handle target selection
- setActiveCategory(category)
- }}
- className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
- isActive ? "bg-blue-600 text-white" : "bg-slate-800 text-slate-300 hover:bg-slate-700"
- }`}
- >
- {category}
-
- )
- })}
-
-
-
-
- {(() => {
- const targets = compatibleTargets ? filteredGroupedTargets : GROUPED_TARGETS
- return (activeCategory ? targets[activeCategory] : [])?.map(target => {
- const isSelected = selectedTarget === target.id
- return (
- handleSelectTarget(target.id)}
- className={`px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${
- isSelected ? "bg-cyan-600 text-white" : "bg-slate-800 text-slate-300 hover:bg-slate-700"
- }`}
- >
- {target.name}
-
- )
- })
- })()}
-
-
-
-
-
-
- Firmware version
-
- setSelectedVersion(event.target.value)}
- className="w-full h-10 px-3 rounded-md border border-slate-800 bg-slate-950 text-sm focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-950"
- >
- {VERSIONS.map(version => (
-
- {version}
-
- ))}
-
-
-
-
-
setShowModuleOverrides(prev => !prev)}
- className="w-full flex items-center justify-between text-left"
- >
-
-
Core Modules
-
- {moduleCount === 0
- ? "Using default modules for this target."
- : `${moduleCount} module${moduleCount === 1 ? "" : "s"} excluded.`}
-
-
- {showModuleOverrides ? (
-
- ) : (
-
- )}
-
-
- {showModuleOverrides && (
-
-
-
- Core Modules are officially maintained modules by Meshtastic. They are selectively included or
- excluded by default depending on the target device. You can explicitly exclude modules you know you
- don't want.
-
-
-
- setModuleConfig({})}
- disabled={moduleCount === 0}
- >
- Reset overrides
-
-
-
- {modulesData.modules.map(module => (
- handleToggleModule(module.id, excluded)}
- />
- ))}
-
-
- )}
-
-
-
-
setShowPlugins(prev => !prev)}
- className="w-full flex items-center justify-between text-left"
- >
-
-
Plugins
-
- {pluginCount === 0
- ? "No plugins enabled."
- : `${pluginCount} plugin${pluginCount === 1 ? "" : "s"} enabled.`}
-
-
- {showPlugins ? (
-
- ) : (
-
- )}
-
-
- {showPlugins && (
-
-
-
- Plugins are 3rd party add-ons. They are not maintained, endorsed, or supported by Meshtastic. Use at
- your own risk.
-
-
-
- setPluginConfig({})}
- disabled={pluginCount === 0}
- >
- Reset plugins
-
-
-
- {(() => {
- // Get explicitly selected plugins (user-selected)
- const explicitPlugins = Object.keys(pluginConfig).filter(id => pluginConfig[id] === true)
-
- // Compute implicit dependencies (dependencies that are not explicitly selected)
- const implicitDeps = getImplicitDependencies(
- explicitPlugins,
- registryData as Record
}>
- )
-
- // Compute all enabled plugins (explicit + implicit)
- const allEnabledPlugins = getDependedPlugins(
- explicitPlugins,
- registryData as Record }>
- )
-
- return Object.entries(registryData)
- .sort(([, pluginA], [, pluginB]) => {
- // Featured plugins first
- const featuredA = pluginA.featured ?? false
- const featuredB = pluginB.featured ?? false
- if (featuredA !== featuredB) {
- return featuredA ? -1 : 1
- }
- // Then alphabetical by name
- return pluginA.name.localeCompare(pluginB.name)
- })
- .map(([slug, plugin]) => {
- // Check if plugin is required by another explicitly selected plugin
- const isRequired = isRequiredByOther(
- slug,
- explicitPlugins,
- registryData as Record }>
- )
- // Plugin is implicit if it's either:
- // 1. Not explicitly selected but is a dependency, OR
- // 2. Explicitly selected but required by another explicitly selected plugin
- const isImplicit = implicitDeps.has(slug) || (explicitPlugins.includes(slug) && isRequired)
-
- // Check plugin compatibility with selected target
- const pluginIncludes = (plugin as { includes?: string[] }).includes
- const pluginExcludes = (plugin as { excludes?: string[] }).excludes
- // Legacy support: check for old "architectures" field
- const legacyArchitectures = (plugin as { architectures?: string[] }).architectures
- const hasCompatibilityConstraints =
- (pluginIncludes && pluginIncludes.length > 0) ||
- (pluginExcludes && pluginExcludes.length > 0) ||
- (legacyArchitectures && legacyArchitectures.length > 0)
- const isCompatible =
- hasCompatibilityConstraints && selectedTarget
- ? isPluginCompatibleWithTarget(
- pluginIncludes || legacyArchitectures,
- pluginExcludes,
- selectedTarget
- )
- : true // If no constraints or no target selected, assume compatible
- // Mark as incompatible if plugin has compatibility constraints and target is not compatible
- const isIncompatible = !isCompatible && hasCompatibilityConstraints && !!selectedTarget
-
- // Check if this is the preselected plugin from URL
- const isPreselected = pluginParam === slug
-
- return (
- handleTogglePlugin(slug, enabled)}
- disabled={isImplicit || isIncompatible || isPreselected}
- enabledLabel={isPreselected ? "Locked" : isImplicit ? "Required" : "Add"}
- incompatibleReason={isIncompatible ? "Not compatible with this target" : undefined}
- featured={plugin.featured ?? false}
- flashCount={pluginFlashCounts[slug] ?? 0}
- homepage={plugin.homepage}
- version={plugin.version}
- repo={plugin.repo}
- />
- )
- })
- })()}
-
-
- )}
-
-
-
-
- {isFlashing ? (
-
-
- Queuing build...
-
- ) : (
- `Flash ${selectedTargetLabel || ""}`.trim() || "Flash"
- )}
-
- {errorMessage &&
{errorMessage}
}
-
-
-
-
- )
-}
diff --git a/pages/index/+Page.tsx b/pages/index/+Page.tsx
index 42e2a60..41371fb 100644
--- a/pages/index/+Page.tsx
+++ b/pages/index/+Page.tsx
@@ -149,7 +149,7 @@ export default function LandingPage() {
description={customBuildPlugin.description}
imageUrl={customBuildPlugin.imageUrl}
featured={false}
- href="/builds/new"
+ href="/builds"
prominent={true}
/>
diff --git a/pages/plugins/@slug/+Page.tsx b/pages/plugins/@slug/+Page.tsx
index d33d00a..6ae5f05 100644
--- a/pages/plugins/@slug/+Page.tsx
+++ b/pages/plugins/@slug/+Page.tsx
@@ -132,7 +132,7 @@ export default function PluginPage() {
Homepage
)}
-