lint fixes

This commit is contained in:
Ben Allfree
2025-12-06 17:17:13 -08:00
parent b7c5eaa8d0
commit 086a98050c
45 changed files with 691 additions and 897 deletions

View File

@@ -4,30 +4,30 @@ on:
workflow_dispatch:
inputs:
target:
description: 'Target board (e.g. rak4631)'
description: "Target board (e.g. rak4631)"
required: true
type: string
flags:
description: 'Build flags (e.g. -DMESHTASTIC_EXCLUDE_MQTT)'
description: "Build flags (e.g. -DMESHTASTIC_EXCLUDE_MQTT)"
required: false
type: string
version:
description: 'Firmware Version (Tag/Branch)'
description: "Firmware Version (Tag/Branch)"
required: true
build_id:
description: 'Convex Build ID'
description: "Convex Build ID"
required: true
type: string
build_hash:
description: 'Build hash for artifact naming'
description: "Build hash for artifact naming"
required: true
type: string
convex_url:
description: 'Convex Site URL'
description: "Convex Site URL"
required: true
type: string
plugins:
description: 'Space-separated plugin slugs to install'
description: "Space-separated plugin slugs to install"
required: false
type: string
@@ -71,7 +71,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
python-version: "3.x"
- name: Update Status - Fetching Firmware
shell: bash
@@ -160,13 +160,13 @@ jobs:
- PlatformIO dependencies (\`.pio/\`) are not included - PlatformIO will download these as needed
- The build flags above must be set exactly as shown to reproduce the build
EOF
# Create MESHFORGE.md so it gets included in the archive
# (already created above, just ensuring it exists)
# Define archive suffix for consistent naming
ARTIFACT_ARCHIVE_SUFFIX="-${{ inputs.build_hash }}-${{ github.run_id }}.tar.gz"
# Create archive from working directory to include plugins installed by mpm
# Exclude .git, .pio, and build artifacts
cd ..
@@ -177,12 +177,12 @@ jobs:
--exclude='build' \
-czf "source${ARTIFACT_ARCHIVE_SUFFIX}" \
-C firmware .
update_status uploading_source_archive
SOURCE_ARCHIVE_PATH="/source${ARTIFACT_ARCHIVE_SUFFIX}"
SOURCE_OBJECT_PATH="${R2_BUCKET_NAME}/source${ARTIFACT_ARCHIVE_SUFFIX}"
# Upload source archive to R2
wrangler r2 object put "$SOURCE_OBJECT_PATH" \
--file "source${ARTIFACT_ARCHIVE_SUFFIX}" --remote

View File

@@ -4,30 +4,30 @@ on:
workflow_dispatch:
inputs:
target:
description: 'Target board (e.g. rak4631)'
description: "Target board (e.g. rak4631)"
required: true
type: string
flags:
description: 'Build flags (e.g. -DMESHTASTIC_EXCLUDE_MQTT)'
description: "Build flags (e.g. -DMESHTASTIC_EXCLUDE_MQTT)"
required: false
type: string
version:
description: 'Firmware Version (Tag/Branch)'
description: "Firmware Version (Tag/Branch)"
required: true
build_id:
description: 'Convex Build ID'
description: "Convex Build ID"
required: true
type: string
build_hash:
description: 'Build hash for artifact naming'
description: "Build hash for artifact naming"
required: true
type: string
convex_url:
description: 'Convex Site URL'
description: "Convex Site URL"
required: true
type: string
plugins:
description: 'Space-separated plugin slugs to install'
description: "Space-separated plugin slugs to install"
required: false
type: string
@@ -71,7 +71,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
python-version: "3.x"
- name: Update Status - Fetching Firmware
shell: bash
@@ -160,13 +160,13 @@ jobs:
- PlatformIO dependencies (\`.pio/\`) are not included - PlatformIO will download these as needed
- The build flags above must be set exactly as shown to reproduce the build
EOF
# Create MESHFORGE.md so it gets included in the archive
# (already created above, just ensuring it exists)
# Define archive suffix for consistent naming
ARTIFACT_ARCHIVE_SUFFIX="-${{ inputs.build_hash }}-${{ github.run_id }}.tar.gz"
# Create archive from working directory to include plugins installed by mpm
# Exclude .git, .pio, and build artifacts
cd ..
@@ -177,12 +177,12 @@ jobs:
--exclude='build' \
-czf "source${ARTIFACT_ARCHIVE_SUFFIX}" \
-C firmware .
update_status uploading_source_archive
SOURCE_ARCHIVE_PATH="/source${ARTIFACT_ARCHIVE_SUFFIX}"
SOURCE_OBJECT_PATH="${R2_BUCKET_NAME}/source${ARTIFACT_ARCHIVE_SUFFIX}"
# Upload source archive to R2
wrangler r2 object put "$SOURCE_OBJECT_PATH" \
--file "source${ARTIFACT_ARCHIVE_SUFFIX}" --remote

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
vendor

View File

@@ -33,4 +33,3 @@ bun run lint
## License
[Add your license here]

View File

@@ -18,4 +18,4 @@
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
}

View File

@@ -1,47 +1,27 @@
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
function DiscordIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" {...props}>
<path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.1.1 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.1 16.1 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02M8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12m6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12" />
</svg>
)
}
interface DiscordButtonProps {
variant?: 'default' | 'outline' | 'ghost' | 'link' | 'destructive'
size?: 'default' | 'sm' | 'lg' | 'icon'
variant?: "default" | "outline" | "ghost" | "link" | "destructive"
size?: "default" | "sm" | "lg" | "icon"
className?: string
}
export function DiscordButton({
variant = 'outline',
size,
className,
}: DiscordButtonProps) {
export function DiscordButton({ variant = "outline", size, className }: DiscordButtonProps) {
return (
<a
href="https://discord.gg/8KgJpvjfaJ"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant={variant}
size={size}
className={cn('flex items-center gap-2', className)}
>
<a href="https://discord.gg/8KgJpvjfaJ" target="_blank" rel="noopener noreferrer">
<Button variant={variant} size={size} className={cn("flex items-center gap-2", className)}>
<DiscordIcon className="w-4 h-4" />
Discord
</Button>
</a>
)
}

View File

@@ -1,13 +1,13 @@
import { usePageContext } from "vike-react/usePageContext";
import type { ReactNode } from "react";
import type { ReactNode } from "react"
import { usePageContext } from "vike-react/usePageContext"
export function Link({ href, children }: { href: string; children: ReactNode }) {
const pageContext = usePageContext();
const { urlPathname } = pageContext;
const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href);
const pageContext = usePageContext()
const { urlPathname } = pageContext
const isActive = href === "/" ? urlPathname === href : urlPathname.startsWith(href)
return (
<a href={href} className={isActive ? "is-active" : undefined}>
{children}
</a>
);
)
}

View File

@@ -5,23 +5,14 @@ interface ModuleCardProps {
onClick: () => void
}
export function ModuleCard({
name,
description,
selected,
onClick,
}: ModuleCardProps) {
export function ModuleCard({ name, description, selected, onClick }: ModuleCardProps) {
return (
<button
type="button"
onClick={onClick}
className={`
w-full text-left p-4 rounded-lg border-2 transition-all
${
selected
? 'border-blue-500 bg-blue-500/10'
: 'border-slate-700 bg-slate-900/50 hover:border-slate-600'
}
${selected ? "border-blue-500 bg-blue-500/10" : "border-slate-700 bg-slate-900/50 hover:border-slate-600"}
`}
>
<div className="flex items-start gap-3">
@@ -29,32 +20,20 @@ export function ModuleCard({
<div
className={`
w-5 h-5 rounded border-2 flex items-center justify-center
${selected ? 'border-blue-500 bg-blue-500' : 'border-slate-500'}
${selected ? "border-blue-500 bg-blue-500" : "border-slate-500"}
`}
>
{selected && (
<svg
className="w-3 h-3 text-white"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<title>Checkmark</title>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
</svg>
)}
</div>
</div>
<div className="flex-1 min-w-0">
<h4 className="font-semibold text-sm mb-1">{name}</h4>
<p className="text-xs text-slate-400 leading-relaxed">
{description}
</p>
<p className="text-xs text-slate-400 leading-relaxed">{description}</p>
</div>
</div>
</button>

View File

@@ -1,4 +1,4 @@
import { Switch } from '@/components/ui/switch'
import { Switch } from "@/components/ui/switch"
interface ModuleToggleProps {
id: string
@@ -8,12 +8,7 @@ interface ModuleToggleProps {
onToggle: (excluded: boolean) => void
}
export function ModuleToggle({
name,
description,
isExcluded,
onToggle,
}: ModuleToggleProps) {
export function ModuleToggle({ name, description, isExcluded, onToggle }: ModuleToggleProps) {
return (
<div className="flex items-start gap-4 p-4 rounded-lg border-2 border-slate-700 bg-slate-900/50 hover:border-slate-600 transition-colors">
<div className="flex-1 min-w-0">
@@ -26,7 +21,7 @@ export function ModuleToggle({
onCheckedChange={onToggle}
labelLeft="Default"
labelRight="Excluded"
className={isExcluded ? 'bg-orange-600' : 'bg-slate-600'}
className={isExcluded ? "bg-orange-600" : "bg-slate-600"}
/>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import { ExternalLink, Star } from 'lucide-react'
import { Switch } from '@/components/ui/switch'
import { Switch } from "@/components/ui/switch"
import { ExternalLink, Star } from "lucide-react"
interface PluginToggleProps {
id: string
@@ -26,17 +26,17 @@ export function PluginToggle({
homepage,
version,
disabled = false,
enabledLabel = 'Add',
enabledLabel = "Add",
incompatibleReason,
}: PluginToggleProps) {
const isIncompatible = !!incompatibleReason
return (
<div
className={`relative flex items-start gap-4 p-4 rounded-lg border-2 transition-colors ${
isIncompatible
? 'border-slate-800 bg-slate-900/30 opacity-60 cursor-not-allowed'
: 'border-slate-700 bg-slate-900/50 hover:border-slate-600'
? "border-slate-800 bg-slate-900/30 opacity-60 cursor-not-allowed"
: "border-slate-700 bg-slate-900/50 hover:border-slate-600"
}`}
>
{/* Flash count and homepage links in lower right */}
@@ -63,7 +63,7 @@ export function PluginToggle({
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-slate-400 hover:text-slate-300 transition-colors"
onClick={(e) => e.stopPropagation()}
onClick={e => e.stopPropagation()}
>
<ExternalLink className="w-3.5 h-3.5" />
</a>
@@ -71,20 +71,14 @@ export function PluginToggle({
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 mb-1">
<h4 className={`font-semibold text-sm ${isIncompatible ? 'text-slate-500' : ''}`}>
{name}
</h4>
{featured && (
<Star className="w-4 h-4 text-yellow-400 fill-yellow-400" />
)}
<h4 className={`font-semibold text-sm ${isIncompatible ? "text-slate-500" : ""}`}>{name}</h4>
{featured && <Star className="w-4 h-4 text-yellow-400 fill-yellow-400" />}
</div>
<p className={`text-xs leading-relaxed ${isIncompatible ? 'text-slate-500' : 'text-slate-400'}`}>
<p className={`text-xs leading-relaxed ${isIncompatible ? "text-slate-500" : "text-slate-400"}`}>
{description}
</p>
{isIncompatible && incompatibleReason && (
<p className="text-xs text-red-400 mt-1 font-medium">
{incompatibleReason}
</p>
<p className="text-xs text-red-400 mt-1 font-medium">{incompatibleReason}</p>
)}
</div>
<div className="flex flex-col items-end gap-1 shrink-0">
@@ -94,7 +88,7 @@ export function PluginToggle({
disabled={disabled}
labelLeft="Skip"
labelRight={enabledLabel}
className={isEnabled ? 'bg-green-600' : 'bg-slate-600'}
className={isEnabled ? "bg-green-600" : "bg-slate-600"}
/>
</div>
</div>

View File

@@ -1,16 +1,9 @@
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="currentColor"
{...props}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" {...props}>
<mask id="SVGfUZuVbjp">
<g fill="#fff">
<path
@@ -23,48 +16,19 @@ function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
strokeWidth="2"
d="M12 9.42c4.42 0 8 2.37 8 5.29c0 2.92 -3.58 5.29 -8 5.29c-4.42 0 -8 -2.37 -8 -5.29c0 -2.92 3.58 -5.29 8 -5.29Z"
>
<animate
fill="freeze"
attributeName="fill-opacity"
begin="0.6s"
dur="0.4s"
values="0;1"
/>
<animate
fill="freeze"
attributeName="stroke-dashoffset"
dur="0.6s"
values="48;0"
/>
<animate fill="freeze" attributeName="fill-opacity" begin="0.6s" dur="0.4s" values="0;1" />
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.6s" values="48;0" />
</path>
<circle cx="7.24" cy="11.97" r="2.24" opacity="0">
<animate
fill="freeze"
attributeName="cx"
begin="1s"
dur="0.2s"
values="7.24;3.94"
/>
<animate fill="freeze" attributeName="cx" begin="1s" dur="0.2s" values="7.24;3.94" />
<set fill="freeze" attributeName="opacity" begin="1s" to="1" />
</circle>
<circle cx="16.76" cy="11.97" r="2.24" opacity="0">
<animate
fill="freeze"
attributeName="cx"
begin="1s"
dur="0.2s"
values="16.76;20.06"
/>
<animate fill="freeze" attributeName="cx" begin="1s" dur="0.2s" values="16.76;20.06" />
<set fill="freeze" attributeName="opacity" begin="1s" to="1" />
</circle>
<circle cx="18.45" cy="4.23" r="1.61" opacity="0">
<animate
attributeName="cx"
begin="2.4s"
dur="6s"
repeatCount="indefinite"
values="18.45;5.75;18.45"
/>
<animate attributeName="cx" begin="2.4s" dur="6s" repeatCount="indefinite" values="18.45;5.75;18.45" />
<set fill="freeze" attributeName="opacity" begin="2.6s" to="1" />
</circle>
</g>
@@ -85,32 +49,14 @@ function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
repeatCount="indefinite"
values="M12 8.75L13.18 3.11L18.21 4.18;M12 8.75L12 2L12 4.18;M12 8.75L10.82 3.11L5.79 4.18;M12 8.75L12 2L12 4.18;M12 8.75L13.18 3.11L18.21 4.18"
/>
<animate
fill="freeze"
attributeName="stroke-dashoffset"
begin="2.4s"
dur="0.2s"
values="12;0"
/>
<animate fill="freeze" attributeName="stroke-dashoffset" begin="2.4s" dur="0.2s" values="12;0" />
</path>
<g fillOpacity="0">
<circle cx="8.45" cy="13.59" r="1.61">
<animate
fill="freeze"
attributeName="fill-opacity"
begin="1.2s"
dur="0.4s"
values="0;1"
/>
<animate fill="freeze" attributeName="fill-opacity" begin="1.2s" dur="0.4s" values="0;1" />
</circle>
<circle cx="15.55" cy="13.59" r="1.61">
<animate
fill="freeze"
attributeName="fill-opacity"
begin="1.6s"
dur="0.4s"
values="0;1"
/>
<animate fill="freeze" attributeName="fill-opacity" begin="1.6s" dur="0.4s" values="0;1" />
</circle>
</g>
<path
@@ -123,13 +69,7 @@ function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
strokeWidth=".8"
d="M8.47 17.52c0 0 0.94 1.06 3.53 1.06c2.58 0 3.53 -1.06 3.53 -1.06"
>
<animate
fill="freeze"
attributeName="stroke-dashoffset"
begin="2s"
dur="0.2s"
values="10;0"
/>
<animate fill="freeze" attributeName="stroke-dashoffset" begin="2s" dur="0.2s" values="10;0" />
</path>
</mask>
<rect width="24" height="24" fill="currentColor" mask="url(#SVGfUZuVbjp)" />
@@ -138,31 +78,18 @@ function RedditIcon(props: React.SVGProps<SVGSVGElement>) {
}
interface RedditButtonProps {
variant?: 'default' | 'outline' | 'ghost' | 'link' | 'destructive'
size?: 'default' | 'sm' | 'lg' | 'icon'
variant?: "default" | "outline" | "ghost" | "link" | "destructive"
size?: "default" | "sm" | "lg" | "icon"
className?: string
}
export function RedditButton({
variant = 'outline',
size,
className,
}: RedditButtonProps) {
export function RedditButton({ variant = "outline", size, className }: RedditButtonProps) {
return (
<a
href="https://www.reddit.com/r/MeshForge/"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant={variant}
size={size}
className={cn('flex items-center gap-2', className)}
>
<a href="https://www.reddit.com/r/MeshForge/" target="_blank" rel="noopener noreferrer">
<Button variant={variant} size={size} className={cn("flex items-center gap-2", className)}>
<RedditIcon className="w-4 h-4" />
Reddit
</Button>
</a>
)
}

View File

@@ -7,10 +7,7 @@ interface SourceAvailableProps {
* Component that only renders children when sourcePath is available.
* Uses the sourcePath field from the build instead of polling.
*/
export function SourceAvailable({
sourcePath,
children,
}: SourceAvailableProps) {
export function SourceAvailable({ sourcePath, children }: SourceAvailableProps) {
if (!sourcePath) {
return null
}

View File

@@ -1,57 +1,46 @@
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default:
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
destructive:
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
secondary:
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: 'h-9 px-4 py-2',
sm: 'h-8 rounded-md px-3 text-xs',
lg: 'h-10 rounded-md px-8',
icon: 'h-9 w-9',
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: 'default',
size: 'default',
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
const Comp = asChild ? Slot : "button"
return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
}
)
Button.displayName = 'Button'
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -1,8 +1,8 @@
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { Check } from 'lucide-react'
import * as React from 'react'
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import * as React from "react"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
@@ -11,14 +11,12 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn('grid place-content-center text-current')}
>
<CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>

View File

@@ -1,14 +1,14 @@
import * as React from 'react'
import * as React from "react"
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
@@ -17,6 +17,6 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
)
}
)
Input.displayName = 'Input'
Input.displayName = "Input"
export { Input }

View File

@@ -1,24 +1,22 @@
import { useTheme } from 'next-themes'
import { Toaster as Sonner } from 'sonner'
import { useTheme } from "next-themes"
import { Toaster as Sonner } from "sonner"
type ToasterProps = React.ComponentProps<typeof Sonner>
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme()
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps['theme']}
theme={theme as ToasterProps["theme"]}
className="toaster group"
toastOptions={{
classNames: {
toast:
'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
description: "group-[.toast]:text-muted-foreground",
actionButton: "group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
},
}}
{...props}

View File

@@ -1,4 +1,4 @@
import { cn } from '@/lib/utils'
import { cn } from "@/lib/utils"
interface SwitchProps {
checked: boolean
@@ -9,14 +9,7 @@ interface SwitchProps {
labelRight?: string
}
export function Switch({
checked,
onCheckedChange,
disabled = false,
className,
labelLeft,
labelRight,
}: SwitchProps) {
export function Switch({ checked, onCheckedChange, disabled = false, className, labelLeft, labelRight }: SwitchProps) {
return (
<button
type="button"
@@ -25,27 +18,19 @@ export function Switch({
disabled={disabled}
onClick={() => !disabled && onCheckedChange(!checked)}
className={cn(
'relative inline-flex h-8 w-24 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 disabled:cursor-not-allowed disabled:opacity-50',
!className && (checked ? 'bg-red-600' : 'bg-slate-600'),
"relative inline-flex h-8 w-24 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-950 disabled:cursor-not-allowed disabled:opacity-50",
!className && (checked ? "bg-red-600" : "bg-slate-600"),
className
)}
>
<span
className={cn(
'inline-block h-6 w-6 transform rounded-full bg-white transition-transform',
checked ? 'translate-x-[68px]' : 'translate-x-1'
"inline-block h-6 w-6 transform rounded-full bg-white transition-transform",
checked ? "translate-x-[68px]" : "translate-x-1"
)}
/>
{checked && labelRight && (
<span className="absolute left-2 text-xs font-medium text-white">
{labelRight}
</span>
)}
{!checked && labelLeft && (
<span className="absolute right-2 text-xs font-medium text-white">
{labelLeft}
</span>
)}
{checked && labelRight && <span className="absolute left-2 text-xs font-medium text-white">{labelRight}</span>}
{!checked && labelLeft && <span className="absolute right-2 text-xs font-medium text-white">{labelLeft}</span>}
</button>
)
}

View File

@@ -217,4 +217,4 @@
"rp2350": null,
"stm32": null,
"portduino": null
}
}

View File

@@ -1,243 +1,243 @@
// This file is auto-generated by scripts/generate-versions.js
export const VERSIONS = [
'v2.7.16.a597230',
'v2.7.15.567b8ea',
'v2.7.14.e959000',
'v2.7.13.597fa0b',
'v2.7.12.45f15b8',
'v2.7.11.ee68575',
'v2.7.10.94d4bdf',
'v2.7.9.70724be',
'v2.7.8.a0c0388',
'v2.7.7.5ae4ff9',
'v2.7.6.834c3c5',
'v2.7.5.ddd1499',
'v2.7.4.c1f4f79',
'v2.7.3.cf574c7',
'v2.7.2.f6d3782',
'v2.7.1.f35ca81',
'v2.7.0.705515a',
'v2.7.0.195b7cc',
'v2.6.13.0561f2c',
'v2.6.12.9861e82',
'v2.6.11.60ec05e',
'v2.6.10.9ce4455',
'v2.6.9.f223b8a',
'v2.6.8.ef9d0d7',
'v2.6.7.2d6181f',
'v2.6.6.54c1423',
'v2.6.5.fc3d9f2',
'v2.6.4.b89355f',
'v2.6.3.d28af68',
'v2.6.3.640e731',
'v2.6.2.31c0e8f',
'v2.6.1.7c3edde',
'v2.6.0.f7afa9a',
'v2.5.23.bf958ed',
'v2.5.22.d1fa27d',
'v2.5.21.447533a',
'v2.5.20.4c97351',
'v2.5.19.f9876cf',
'v2.5.19.d5cd6f8',
'v2.5.18.89ebafc',
'v2.5.17.b4b2fd6',
'v2.5.16.f81d3b0',
'v2.5.15.79da236',
'v2.5.14.f2ee0df',
'v2.5.13.295278b',
'v2.5.13.1a06f88',
'v2.5.12.aa184e6',
'v2.5.11.8e2a3e5',
'v2.5.10.0fc5c9b',
'v2.5.9.936260f',
'v2.5.8.6485f03',
'v2.5.7.f77c87d',
'v2.5.6.d55c08d',
'v2.5.5.e182ae7',
'v2.5.4.8d288d5',
'v2.5.3.a70d5ee',
'v2.5.2.771cb52',
'v2.5.1.c13b44b',
'v2.5.0.e470619',
'v2.5.0.d6dac17',
'v2.5.0.ab7de7f',
'v2.5.0.33eb073',
'v2.5.0.9e55e6b',
'v2.5.0.9ac0e26',
'v2.4.3.efc27f2',
'v2.4.3.91d6612',
'v2.4.2.5b45303',
'v2.4.1.394e0e1',
'v2.4.0.46d7b82',
'v2.3.15.deb7c27',
'v2.3.14.64531fa',
'v2.3.13.83f5ba0',
'v2.3.12.24458a7',
'v2.3.11.2740a56',
'v2.3.10.d19607b',
'v2.3.9.f06c56a',
'v2.3.8.d490a33',
'v2.3.7.30fbcab',
'v2.3.6.7a3570a',
'v2.3.5.2f9b68e',
'v2.3.4.ea61808',
'v2.3.3.8187fa7',
'v2.3.2.63df972',
'v2.3.1.4fa7f5a',
'v2.3.0.5f47ca1',
'v2.2.24.e6a2c06',
'v2.2.23.5672e68',
'v2.2.22.404d0dd',
'v2.2.21.7f7c5cb',
'v2.2.20.af5ac32',
'v2.2.19.8f6a283',
'v2.2.18.e9bde80',
'v2.2.17.dbac2b1',
'v2.2.16.1c6acfd',
'v2.2.15.31c4693',
'v2.2.14.57542ce',
'v2.2.13.f570204',
'v2.2.12.092e6f2',
'v2.2.11.10265aa',
'v2.2.10.7cebd79',
'v2.2.9.47301a5',
'v2.2.8.61f6fb2',
'v2.2.7.e8970ad',
'v2.2.6.b53cb38',
'v2.2.5.8255128',
'v2.2.4.3bcab0e',
'v2.2.3.282cc0b',
'v2.2.2.f35c7be',
'v2.2.1.fb5f2e4',
'v2.2.0.9f6584b',
'v2.1.23.04bbdc6',
'v2.1.22.191a69d',
'v2.1.21.97d7a89',
'v2.1.20.470363d',
'v2.1.19.eb7025f',
'v2.1.18.de53280',
'v2.1.17.7ca2e81',
'v2.1.16.a2c5b92',
'v2.1.15.cd78723',
'v2.1.14.99a31c1',
'v2.1.13.7475c86',
'v2.1.12.7711b03',
'v2.1.11.5ec624d',
'v2.1.10.7ef12c7',
'v2.1.9.d43ddc9',
'v2.1.8.ee971e3',
'v2.1.7.242f880',
'v2.1.6.5679a82',
'v2.1.5.23272da',
'v2.1.4.958d2cf',
'v2.1.3.8c68d88',
'v2.1.2.6d20215',
'v2.1.1.dc2ca9c',
'v2.1.0.331a1af',
'v2.0.23.7bb281d',
'v2.0.22.fbfd0f1',
'v2.0.21.83e6cea',
'v2.0.20.7100416',
'v2.0.19.3209aea',
'v2.0.18.1a7991c',
'v2.0.17.5d1c06b',
'v2.0.16.2242b68',
'v2.0.15.aafbde0',
'v2.0.14.2baaad8',
'v2.0.13.7e27729',
'v2.0.12.2400dd4',
'v2.0.11.8914d1a',
'v2.0.10.e09b12c',
'v2.0.9.6ea0963',
'v2.0.8.090e166',
'v2.0.7.91ff7b9',
'v2.0.6.97fd5cf',
'v2.0.5.65e8209',
'v2.0.4.5417671',
'v2.0.3.09fe616',
'v2.0.2.8146e84',
'v2.0.1.ad05b91',
'v2.0.0.18ab874',
'v1.3.48.82bcd39',
'v1.3.47.05147c0',
'v1.3.46.d4ea956',
'v1.3.45.b0d0552',
'v1.3.44.4fa8d02',
'v1.3.43.aae9d2f',
'v1.3.42.9bd9252',
'v1.3.41.80ddb81',
'v1.3.40.e87ecc2',
'v1.3.39.ddc3727',
'v1.3.38.1253abd',
'v1.3.37.97712a9',
'v1.3.36.dd720f2',
'v1.3.36.64f852e',
'v1.3.36.7e03019',
'v1.3.35.3251cd5',
'v1.3.34.401b5d9',
'v1.3.33.ab0095c',
'v1.3.32.7e6c22f',
'v1.3.31.0084643',
'v1.3.30.9fe2ddb',
'v1.3.29.7afc149',
'v1.3.28.41f9541',
'v1.3.27.c88ba58',
'v1.3.26.0010231',
'v1.3.25.85f46d3',
'v1.3.24.dff6915',
'v1.3.23.5462d84',
'v1.3.22.c725a6b',
'v1.3.21.cf00ac5',
'v1.3.20.9a5ff93',
'v1.3.19.3c6a2f7',
'v1.3.17.c9822de',
'v1.3.16.97899ae',
'v1.3.15.432d067',
'v1.3.13.71a43a9',
'v1.3.12.6306c53',
'v1.3.11.0411401',
'v1.3.10.cc2a84a',
'v1.3.10.4df0e91',
'v1.3.9.92185e7',
'v1.3.8.90df7c2',
'v1.3.7.bb22b6e',
'v1.3.6.f511bab',
'v1.3.5.e5b19fd',
'v1.3.4.2b20bf3',
'v1.3.3.2fe124e',
'v1.2.testing1',
'v1.2.65.0adc5ce',
'v1.2.64.fc48fcd',
'v1.2.63.9879494',
'v1.2.62.3ddd74e',
'v1.2.61.d551c17',
'v1.2.60.ab959de',
'v1.2.59.d81c1c0',
'v1.2.58.6af1822',
'v1.2.57.f7c6955',
'v1.2.56.596a73c',
'v1.2.55.9db7c62',
'v1.2.54.288f2be',
'v1.2.53.19c1f9f',
'v1.2.52.b63802c',
'v1.2.51.f9ff06b',
'v1.2.50.41dcfdd',
'v1.2.49.5354c49',
'v1.2.48.371335e',
'v1.2.47',
'v1.2.46.dce2fe4',
'v1.2.46.9d21e58',
'v1.2.45.b674054',
'v1.2.44.f2c9c55',
'v1.2.43.a405d81',
'v1.2.42.2759c8d',
'v1.2.41.32f3682',
'v1.2.39.06892c4',
'v1.2.38.cf4e508',
'v1.2.38.451b085',
'v1.2.36',
'v1.2.30.80e4bc6',
'v1.2.29.6c95659',
"v2.7.16.a597230",
"v2.7.15.567b8ea",
"v2.7.14.e959000",
"v2.7.13.597fa0b",
"v2.7.12.45f15b8",
"v2.7.11.ee68575",
"v2.7.10.94d4bdf",
"v2.7.9.70724be",
"v2.7.8.a0c0388",
"v2.7.7.5ae4ff9",
"v2.7.6.834c3c5",
"v2.7.5.ddd1499",
"v2.7.4.c1f4f79",
"v2.7.3.cf574c7",
"v2.7.2.f6d3782",
"v2.7.1.f35ca81",
"v2.7.0.705515a",
"v2.7.0.195b7cc",
"v2.6.13.0561f2c",
"v2.6.12.9861e82",
"v2.6.11.60ec05e",
"v2.6.10.9ce4455",
"v2.6.9.f223b8a",
"v2.6.8.ef9d0d7",
"v2.6.7.2d6181f",
"v2.6.6.54c1423",
"v2.6.5.fc3d9f2",
"v2.6.4.b89355f",
"v2.6.3.d28af68",
"v2.6.3.640e731",
"v2.6.2.31c0e8f",
"v2.6.1.7c3edde",
"v2.6.0.f7afa9a",
"v2.5.23.bf958ed",
"v2.5.22.d1fa27d",
"v2.5.21.447533a",
"v2.5.20.4c97351",
"v2.5.19.f9876cf",
"v2.5.19.d5cd6f8",
"v2.5.18.89ebafc",
"v2.5.17.b4b2fd6",
"v2.5.16.f81d3b0",
"v2.5.15.79da236",
"v2.5.14.f2ee0df",
"v2.5.13.295278b",
"v2.5.13.1a06f88",
"v2.5.12.aa184e6",
"v2.5.11.8e2a3e5",
"v2.5.10.0fc5c9b",
"v2.5.9.936260f",
"v2.5.8.6485f03",
"v2.5.7.f77c87d",
"v2.5.6.d55c08d",
"v2.5.5.e182ae7",
"v2.5.4.8d288d5",
"v2.5.3.a70d5ee",
"v2.5.2.771cb52",
"v2.5.1.c13b44b",
"v2.5.0.e470619",
"v2.5.0.d6dac17",
"v2.5.0.ab7de7f",
"v2.5.0.33eb073",
"v2.5.0.9e55e6b",
"v2.5.0.9ac0e26",
"v2.4.3.efc27f2",
"v2.4.3.91d6612",
"v2.4.2.5b45303",
"v2.4.1.394e0e1",
"v2.4.0.46d7b82",
"v2.3.15.deb7c27",
"v2.3.14.64531fa",
"v2.3.13.83f5ba0",
"v2.3.12.24458a7",
"v2.3.11.2740a56",
"v2.3.10.d19607b",
"v2.3.9.f06c56a",
"v2.3.8.d490a33",
"v2.3.7.30fbcab",
"v2.3.6.7a3570a",
"v2.3.5.2f9b68e",
"v2.3.4.ea61808",
"v2.3.3.8187fa7",
"v2.3.2.63df972",
"v2.3.1.4fa7f5a",
"v2.3.0.5f47ca1",
"v2.2.24.e6a2c06",
"v2.2.23.5672e68",
"v2.2.22.404d0dd",
"v2.2.21.7f7c5cb",
"v2.2.20.af5ac32",
"v2.2.19.8f6a283",
"v2.2.18.e9bde80",
"v2.2.17.dbac2b1",
"v2.2.16.1c6acfd",
"v2.2.15.31c4693",
"v2.2.14.57542ce",
"v2.2.13.f570204",
"v2.2.12.092e6f2",
"v2.2.11.10265aa",
"v2.2.10.7cebd79",
"v2.2.9.47301a5",
"v2.2.8.61f6fb2",
"v2.2.7.e8970ad",
"v2.2.6.b53cb38",
"v2.2.5.8255128",
"v2.2.4.3bcab0e",
"v2.2.3.282cc0b",
"v2.2.2.f35c7be",
"v2.2.1.fb5f2e4",
"v2.2.0.9f6584b",
"v2.1.23.04bbdc6",
"v2.1.22.191a69d",
"v2.1.21.97d7a89",
"v2.1.20.470363d",
"v2.1.19.eb7025f",
"v2.1.18.de53280",
"v2.1.17.7ca2e81",
"v2.1.16.a2c5b92",
"v2.1.15.cd78723",
"v2.1.14.99a31c1",
"v2.1.13.7475c86",
"v2.1.12.7711b03",
"v2.1.11.5ec624d",
"v2.1.10.7ef12c7",
"v2.1.9.d43ddc9",
"v2.1.8.ee971e3",
"v2.1.7.242f880",
"v2.1.6.5679a82",
"v2.1.5.23272da",
"v2.1.4.958d2cf",
"v2.1.3.8c68d88",
"v2.1.2.6d20215",
"v2.1.1.dc2ca9c",
"v2.1.0.331a1af",
"v2.0.23.7bb281d",
"v2.0.22.fbfd0f1",
"v2.0.21.83e6cea",
"v2.0.20.7100416",
"v2.0.19.3209aea",
"v2.0.18.1a7991c",
"v2.0.17.5d1c06b",
"v2.0.16.2242b68",
"v2.0.15.aafbde0",
"v2.0.14.2baaad8",
"v2.0.13.7e27729",
"v2.0.12.2400dd4",
"v2.0.11.8914d1a",
"v2.0.10.e09b12c",
"v2.0.9.6ea0963",
"v2.0.8.090e166",
"v2.0.7.91ff7b9",
"v2.0.6.97fd5cf",
"v2.0.5.65e8209",
"v2.0.4.5417671",
"v2.0.3.09fe616",
"v2.0.2.8146e84",
"v2.0.1.ad05b91",
"v2.0.0.18ab874",
"v1.3.48.82bcd39",
"v1.3.47.05147c0",
"v1.3.46.d4ea956",
"v1.3.45.b0d0552",
"v1.3.44.4fa8d02",
"v1.3.43.aae9d2f",
"v1.3.42.9bd9252",
"v1.3.41.80ddb81",
"v1.3.40.e87ecc2",
"v1.3.39.ddc3727",
"v1.3.38.1253abd",
"v1.3.37.97712a9",
"v1.3.36.dd720f2",
"v1.3.36.64f852e",
"v1.3.36.7e03019",
"v1.3.35.3251cd5",
"v1.3.34.401b5d9",
"v1.3.33.ab0095c",
"v1.3.32.7e6c22f",
"v1.3.31.0084643",
"v1.3.30.9fe2ddb",
"v1.3.29.7afc149",
"v1.3.28.41f9541",
"v1.3.27.c88ba58",
"v1.3.26.0010231",
"v1.3.25.85f46d3",
"v1.3.24.dff6915",
"v1.3.23.5462d84",
"v1.3.22.c725a6b",
"v1.3.21.cf00ac5",
"v1.3.20.9a5ff93",
"v1.3.19.3c6a2f7",
"v1.3.17.c9822de",
"v1.3.16.97899ae",
"v1.3.15.432d067",
"v1.3.13.71a43a9",
"v1.3.12.6306c53",
"v1.3.11.0411401",
"v1.3.10.cc2a84a",
"v1.3.10.4df0e91",
"v1.3.9.92185e7",
"v1.3.8.90df7c2",
"v1.3.7.bb22b6e",
"v1.3.6.f511bab",
"v1.3.5.e5b19fd",
"v1.3.4.2b20bf3",
"v1.3.3.2fe124e",
"v1.2.testing1",
"v1.2.65.0adc5ce",
"v1.2.64.fc48fcd",
"v1.2.63.9879494",
"v1.2.62.3ddd74e",
"v1.2.61.d551c17",
"v1.2.60.ab959de",
"v1.2.59.d81c1c0",
"v1.2.58.6af1822",
"v1.2.57.f7c6955",
"v1.2.56.596a73c",
"v1.2.55.9db7c62",
"v1.2.54.288f2be",
"v1.2.53.19c1f9f",
"v1.2.52.b63802c",
"v1.2.51.f9ff06b",
"v1.2.50.41dcfdd",
"v1.2.49.5354c49",
"v1.2.48.371335e",
"v1.2.47",
"v1.2.46.dce2fe4",
"v1.2.46.9d21e58",
"v1.2.45.b674054",
"v1.2.44.f2c9c55",
"v1.2.43.a405d81",
"v1.2.42.2759c8d",
"v1.2.41.32f3682",
"v1.2.39.06892c4",
"v1.2.38.cf4e508",
"v1.2.38.451b085",
"v1.2.36",
"v1.2.30.80e4bc6",
"v1.2.29.6c95659",
] as const
export type FirmwareVersion = (typeof VERSIONS)[number]

View File

@@ -7,8 +7,8 @@ A query function that takes two arguments looks like:
```ts
// convex/myFunctions.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
import { query } from "./_generated/server"
import { v } from "convex/values"
export const myQueryFunction = query({
// Validators for arguments.
@@ -21,16 +21,16 @@ export const myQueryFunction = query({
handler: async (ctx, args) => {
// Read the database as many times as you need here.
// See https://docs.convex.dev/database/reading-data.
const documents = await ctx.db.query("tablename").collect();
const documents = await ctx.db.query("tablename").collect()
// Arguments passed from the client are properties of the args object.
console.log(args.first, args.second);
console.log(args.first, args.second)
// Write arbitrary JavaScript here: filter, aggregate, build derived data,
// remove non-public properties, or create new objects.
return documents;
return documents
},
});
})
```
Using this query function in a React component looks like:
@@ -39,15 +39,15 @@ Using this query function in a React component looks like:
const data = useQuery(api.myFunctions.myQueryFunction, {
first: 10,
second: "hello",
});
})
```
A mutation function looks like:
```ts
// convex/myFunctions.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { mutation } from "./_generated/server"
import { v } from "convex/values"
export const myMutationFunction = mutation({
// Validators for arguments.
@@ -61,27 +61,25 @@ export const myMutationFunction = mutation({
// Insert or modify documents in the database here.
// Mutations can also read from the database like queries.
// See https://docs.convex.dev/database/writing-data.
const message = { body: args.first, author: args.second };
const id = await ctx.db.insert("messages", message);
const message = { body: args.first, author: args.second }
const id = await ctx.db.insert("messages", message)
// Optionally, return a value from your mutation.
return await ctx.db.get(id);
return await ctx.db.get(id)
},
});
})
```
Using this mutation function in a React component looks like:
```ts
const mutation = useMutation(api.myFunctions.myMutationFunction);
const mutation = useMutation(api.myFunctions.myMutationFunction)
function handleButtonPress() {
// fire and forget, the most common way to use mutations
mutation({ first: "Hello!", second: "me" });
mutation({ first: "Hello!", second: "me" })
// OR
// use the result once the mutation has completed
mutation({ first: "Hello!", second: "me" }).then((result) =>
console.log(result),
);
mutation({ first: "Hello!", second: "me" }).then(result => console.log(result))
}
```

View File

@@ -1,10 +1,10 @@
import { v } from 'convex/values'
import { internal } from './_generated/api'
import { action } from './_generated/server'
import { v } from "convex/values"
import { internal } from "./_generated/api"
import { action } from "./_generated/server"
export const dispatchGithubBuild = action({
args: {
buildId: v.id('builds'),
buildId: v.id("builds"),
target: v.string(),
flags: v.string(),
version: v.string(),
@@ -14,52 +14,49 @@ export const dispatchGithubBuild = action({
handler: async (ctx, args) => {
const githubToken = process.env.GITHUB_TOKEN
if (!githubToken) {
throw new Error('GITHUB_TOKEN is not set')
throw new Error("GITHUB_TOKEN is not set")
}
const convexUrl = process.env.CONVEX_SITE_URL
if (!convexUrl) {
console.error('CONVEX_SITE_URL is not set')
console.error("CONVEX_SITE_URL is not set")
// Proceeding anyway might fail if workflow requires it
}
console.log('dispatchGithubBuild args:', JSON.stringify(args, null, 2))
console.log("dispatchGithubBuild args:", JSON.stringify(args, null, 2))
if (!args.buildHash) {
throw new Error('args.buildHash is missing or empty')
throw new Error("args.buildHash is missing or empty")
}
// Use test workflow when running in Convex dev mode
const isDev = process.env.CONVEX_ENV === 'dev'
const workflowFile = isDev ? 'custom_build_test.yml' : 'custom_build.yml'
const isDev = process.env.CONVEX_ENV === "dev"
const workflowFile = isDev ? "custom_build_test.yml" : "custom_build.yml"
const payload = {
ref: 'main', // or make this configurable
ref: "main", // or make this configurable
inputs: {
target: args.target,
flags: args.flags,
version: args.version,
build_id: args.buildId,
build_hash: args.buildHash,
convex_url: convexUrl || 'https://example.com', // Fallback to avoid missing input error if that's the cause
plugins: (args.plugins ?? []).join(' '),
convex_url: convexUrl || "https://example.com", // Fallback to avoid missing input error if that's the cause
plugins: (args.plugins ?? []).join(" "),
},
}
console.log(
`Dispatching GitHub build to ${workflowFile} with payload:`,
JSON.stringify(payload, null, 2)
)
console.log(`Dispatching GitHub build to ${workflowFile} with payload:`, JSON.stringify(payload, null, 2))
try {
const url = `https://api.github.com/repos/MeshEnvy/mesh-forge/actions/workflows/${workflowFile}/dispatches`
console.log('GitHub API URL:', url)
console.log("GitHub API URL:", url)
const response = await fetch(url, {
method: 'POST',
method: "POST",
headers: {
Authorization: `Bearer ${githubToken}`,
Accept: 'application/vnd.github.v3+json',
'Content-Type': 'application/json',
Accept: "application/vnd.github.v3+json",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})

View File

@@ -1,19 +1,19 @@
import { getAuthUserId } from '@convex-dev/auth/server'
import { v } from 'convex/values'
import { api } from './_generated/api'
import { query } from './_generated/server'
import { computeFlagsFromConfig } from './builds'
import { adminMutation, adminQuery } from './helpers'
import { getAuthUserId } from "@convex-dev/auth/server"
import { v } from "convex/values"
import { api } from "./_generated/api"
import { query } from "./_generated/server"
import { computeFlagsFromConfig } from "./builds"
import { adminMutation, adminQuery } from "./helpers"
export const isAdmin = query({
args: {},
handler: async (ctx) => {
handler: async ctx => {
const userId = await getAuthUserId(ctx)
if (!userId) return false
const userSettings = await ctx.db
.query('userSettings')
.withIndex('by_user', (q) => q.eq('userId', userId))
.query("userSettings")
.withIndex("by_user", q => q.eq("userId", userId))
.first()
return userSettings?.isAdmin === true
@@ -22,11 +22,11 @@ export const isAdmin = query({
export const listFailedBuilds = adminQuery({
args: {},
handler: async (ctx) => {
handler: async ctx => {
const failedBuilds = await ctx.db
.query('builds')
.filter((q) => q.eq(q.field('status'), 'failure'))
.order('desc')
.query("builds")
.filter(q => q.eq(q.field("status"), "failure"))
.order("desc")
.collect()
return failedBuilds
@@ -35,8 +35,8 @@ export const listFailedBuilds = adminQuery({
export const listAllBuilds = adminQuery({
args: {},
handler: async (ctx) => {
const allBuilds = await ctx.db.query('builds').order('desc').collect()
handler: async ctx => {
const allBuilds = await ctx.db.query("builds").order("desc").collect()
return allBuilds
},
@@ -44,12 +44,12 @@ export const listAllBuilds = adminQuery({
export const retryBuild = adminMutation({
args: {
buildId: v.id('builds'),
buildId: v.id("builds"),
},
handler: async (ctx, args) => {
const build = await ctx.db.get(args.buildId)
if (!build) {
throw new Error('Build not found')
throw new Error("Build not found")
}
// Compute flags from config
@@ -68,7 +68,7 @@ export const retryBuild = adminMutation({
// Update build status to queued and clear artifact paths
await ctx.db.patch(args.buildId, {
status: 'queued',
status: "queued",
updatedAt: Date.now(),
firmwarePath: undefined,
sourcePath: undefined,

View File

@@ -2,7 +2,7 @@ export default {
providers: [
{
domain: process.env.CONVEX_SITE_URL,
applicationID: 'convex',
applicationID: "convex",
},
],
}

View File

@@ -1,5 +1,5 @@
import Google from '@auth/core/providers/google'
import { convexAuth } from '@convex-dev/auth/server'
import Google from "@auth/core/providers/google"
import { convexAuth } from "@convex-dev/auth/server"
export const { auth, signIn, signOut, store } = convexAuth({
providers: [Google],

View File

@@ -1,14 +1,14 @@
import { v } from 'convex/values'
import { pick } from 'convex-helpers'
import { api, internal } from './_generated/api'
import type { Doc, Id } from './_generated/dataModel'
import { internalMutation, mutation, query } from './_generated/server'
import { generateSignedDownloadUrl } from './lib/r2'
import { buildFields } from './schema'
import { pick } from "convex-helpers"
import { v } from "convex/values"
import { api, internal } from "./_generated/api"
import type { Doc, Id } from "./_generated/dataModel"
import { internalMutation, mutation, query } from "./_generated/server"
import { generateSignedDownloadUrl } from "./lib/r2"
import { buildFields } from "./schema"
export enum ArtifactType {
Firmware = 'firmware',
Source = 'source',
Firmware = "firmware",
Source = "source",
}
type BuildUpdateData = {
@@ -17,7 +17,7 @@ type BuildUpdateData = {
}
export const get = query({
args: { id: v.id('builds') },
args: { id: v.id("builds") },
handler: async (ctx, args) => {
return await ctx.db.get(args.id)
},
@@ -27,8 +27,8 @@ export const getByHash = query({
args: { buildHash: v.string() },
handler: async (ctx, args) => {
const build = await ctx.db
.query('builds')
.filter((q) => q.eq(q.field('buildHash'), args.buildHash))
.query("builds")
.filter(q => q.eq(q.field("buildHash"), args.buildHash))
.unique()
return build ?? null
},
@@ -38,15 +38,13 @@ export const getByHash = query({
* Computes flags string from build config.
* Only excludes modules explicitly marked as excluded (config[id] === true).
*/
export function computeFlagsFromConfig(
config: Doc<'builds'>['config']
): string {
export function computeFlagsFromConfig(config: Doc<"builds">["config"]): string {
// Sort modules to ensure consistent order
return Object.keys(config.modulesExcluded)
.sort()
.filter((module) => config.modulesExcluded[module])
.filter(module => config.modulesExcluded[module])
.map((moduleExcludedName: string) => `-D${moduleExcludedName}=1`)
.join(' ')
.join(" ")
}
/**
@@ -54,7 +52,7 @@ export function computeFlagsFromConfig(
* Uses characters: 0-9, a-z, A-Z (62 characters total)
*/
function base62Encode(bytes: Uint8Array): string {
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
const chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// Convert bytes to a big-endian number
let num = BigInt(0)
@@ -63,7 +61,7 @@ function base62Encode(bytes: Uint8Array): string {
}
// Convert number to base62
if (num === BigInt(0)) return '0'
if (num === BigInt(0)) return "0"
const result: string[] = []
while (num > BigInt(0)) {
@@ -71,7 +69,7 @@ function base62Encode(bytes: Uint8Array): string {
num = num / BigInt(62)
}
return result.reverse().join('')
return result.reverse().join("")
}
/**
@@ -97,7 +95,7 @@ async function computeBuildHashInternal(
// Use Web Crypto API for SHA-256 hashing
const encoder = new TextEncoder()
const data = encoder.encode(input)
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashBuffer = await crypto.subtle.digest("SHA-256", data)
const hashBytes = new Uint8Array(hashBuffer)
// Encode to base62 instead of hex
@@ -108,17 +106,10 @@ async function computeBuildHashInternal(
* Computes buildHash from build config.
* This is the single source of truth for build hash computation.
*/
export async function computeBuildHash(
config: Doc<'builds'>['config']
): Promise<{ hash: string; flags: string }> {
export async function computeBuildHash(config: Doc<"builds">["config"]): Promise<{ hash: string; flags: string }> {
const flags = computeFlagsFromConfig(config)
const plugins = config.pluginsEnabled ?? []
const hash = await computeBuildHashInternal(
config.version,
config.target,
flags,
plugins
)
const hash = await computeBuildHashInternal(config.version, config.target, flags, plugins)
return { hash, flags }
}
@@ -127,18 +118,17 @@ export async function computeBuildHash(
* Uses {artifactType}-<buildHash>-<githubRunId>.tar.gz format.
*/
export function getR2ArtifactUrl(
build: Pick<Doc<'builds'>, 'buildHash' | 'githubRunId'>,
build: Pick<Doc<"builds">, "buildHash" | "githubRunId">,
artifactType: ArtifactType
): string {
const r2PublicUrl = process.env.R2_PUBLIC_URL
if (!r2PublicUrl) {
throw new Error('R2_PUBLIC_URL is not set')
throw new Error("R2_PUBLIC_URL is not set")
}
if (!build.githubRunId) {
throw new Error('githubRunId is required to construct artifact URL')
throw new Error("githubRunId is required to construct artifact URL")
}
const artifactTypeStr =
artifactType === ArtifactType.Source ? 'source' : 'firmware'
const artifactTypeStr = artifactType === ArtifactType.Source ? "source" : "firmware"
const path = `/${artifactTypeStr}-${build.buildHash}-${build.githubRunId}.tar.gz`
return `${r2PublicUrl}${path}`
}
@@ -147,7 +137,7 @@ export function getR2ArtifactUrl(
// This is the single source of truth for build creation
export const upsertBuild = internalMutation({
args: {
...pick(buildFields, ['buildHash', 'config']),
...pick(buildFields, ["buildHash", "config"]),
status: v.optional(v.string()),
flags: v.string(),
},
@@ -155,8 +145,8 @@ export const upsertBuild = internalMutation({
handler: async (ctx, args) => {
// Check if build already exists with this hash
const existingBuild = await ctx.db
.query('builds')
.filter((q) => q.eq(q.field('buildHash'), args.buildHash))
.query("builds")
.filter(q => q.eq(q.field("buildHash"), args.buildHash))
.unique()
const { status, buildHash, config, flags } = args
@@ -170,8 +160,8 @@ export const upsertBuild = internalMutation({
}
// Create new build (artifact paths are omitted, will be undefined)
const buildId = await ctx.db.insert('builds', {
status: 'queued',
const buildId = await ctx.db.insert("builds", {
status: "queued",
startedAt: Date.now(),
buildHash,
updatedAt: Date.now(),
@@ -203,7 +193,7 @@ export const ensureBuildFromConfig = mutation({
},
handler: async (ctx, args) => {
// Construct config for the build
const config: Doc<'builds'>['config'] = {
const config: Doc<"builds">["config"] = {
version: args.version,
modulesExcluded: args.modulesExcluded ?? {},
target: args.target,
@@ -214,8 +204,8 @@ export const ensureBuildFromConfig = mutation({
const { hash: buildHash, flags } = await computeBuildHash(config)
const existingBuild = await ctx.db
.query('builds')
.filter((q) => q.eq(q.field('buildHash'), buildHash))
.query("builds")
.filter(q => q.eq(q.field("buildHash"), buildHash))
.unique()
if (existingBuild) {
@@ -226,14 +216,11 @@ export const ensureBuildFromConfig = mutation({
}
}
const buildId: Id<'builds'> = await ctx.runMutation(
internal.builds.upsertBuild,
{
buildHash,
flags,
config,
}
)
const buildId: Id<"builds"> = await ctx.runMutation(internal.builds.upsertBuild, {
buildHash,
flags,
config,
})
return {
buildId,
@@ -245,7 +232,7 @@ export const ensureBuildFromConfig = mutation({
// Internal query to get build without auth checks (for webhooks)
export const getInternal = internalMutation({
args: { buildId: v.id('builds') },
args: { buildId: v.id("builds") },
handler: async (ctx, args) => {
return await ctx.db.get(args.buildId)
},
@@ -254,12 +241,12 @@ export const getInternal = internalMutation({
// Internal mutation to log errors from actions
export const logBuildError = internalMutation({
args: {
buildId: v.id('builds'),
buildId: v.id("builds"),
error: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.patch(args.buildId, {
status: 'failure',
status: "failure",
completedAt: Date.now(),
})
},
@@ -268,14 +255,8 @@ export const logBuildError = internalMutation({
// Internal mutation to update build status
export const updateBuildStatus = internalMutation({
args: {
...pick(buildFields, [
'status',
'completedAt',
'githubRunId',
'firmwarePath',
'sourcePath',
]),
buildId: v.id('builds'),
...pick(buildFields, ["status", "completedAt", "githubRunId", "firmwarePath", "sourcePath"]),
buildId: v.id("builds"),
},
handler: async (ctx, args) => {
const build = await ctx.db.get(args.buildId)
@@ -291,12 +272,12 @@ export const updateBuildStatus = internalMutation({
}
// Only set completedAt for final statuses
if (args.status === 'success' || args.status === 'failure') {
if (args.status === "success" || args.status === "failure") {
updateData.completedAt = Date.now()
}
// Clear artifact paths when build starts (queued status)
if (args.status === 'queued') {
if (args.status === "queued") {
updateData.firmwarePath = undefined
updateData.sourcePath = undefined
}
@@ -327,9 +308,7 @@ export const updateBuildStatus = internalMutation({
updateData.githubRunId = args.githubRunId
}
updateData.githubRunIdHistory = [...new Set(existingHistory)].filter(
(id) => id !== args.githubRunId
)
updateData.githubRunIdHistory = [...new Set(existingHistory)].filter(id => id !== args.githubRunId)
await ctx.db.patch(args.buildId, updateData)
},
@@ -337,34 +316,28 @@ export const updateBuildStatus = internalMutation({
export const generateDownloadUrl = mutation({
args: {
buildId: v.id('builds'),
artifactType: v.union(v.literal('firmware'), v.literal('source')),
profileId: v.optional(v.id('profiles')),
buildId: v.id("builds"),
artifactType: v.union(v.literal("firmware"), v.literal("source")),
profileId: v.optional(v.id("profiles")),
},
handler: async (ctx, args) => {
const build = await ctx.db.get(args.buildId)
if (!build) throw new Error('Build not found')
if (!build) throw new Error("Build not found")
if (!build.githubRunId) {
throw new Error('Build githubRunId is required for download')
throw new Error("Build githubRunId is required for download")
}
const artifactTypeEnum =
args.artifactType === 'source'
? ArtifactType.Source
: ArtifactType.Firmware
const artifactTypeEnum = args.artifactType === "source" ? ArtifactType.Source : ArtifactType.Firmware
const isSource = artifactTypeEnum === ArtifactType.Source
const artifactTypeStr =
artifactTypeEnum === ArtifactType.Source ? 'source' : 'firmware'
const contentType = isSource
? 'application/gzip'
: 'application/octet-stream'
const artifactTypeStr = artifactTypeEnum === ArtifactType.Source ? "source" : "firmware"
const contentType = isSource ? "application/gzip" : "application/octet-stream"
// Use stored path if available, otherwise construct from buildHash and githubRunId
const storedPath = isSource ? build.sourcePath : build.firmwarePath
const objectKey = storedPath
? storedPath.startsWith('/')
? storedPath.startsWith("/")
? storedPath.slice(1)
: storedPath
: `${artifactTypeStr}-${build.buildHash}-${build.githubRunId}.tar.gz`
@@ -373,7 +346,7 @@ export const generateDownloadUrl = mutation({
const profile = await (async () => {
if (!args.profileId) return
const profileDoc = await ctx.db.get(args.profileId)
if (!profileDoc) throw new Error('Profile not found')
if (!profileDoc) throw new Error("Profile not found")
return profileDoc
})()
@@ -381,9 +354,9 @@ export const generateDownloadUrl = mutation({
const profileSlug = profile
? profile.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)+/g, '')
: ''
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)+/g, "")
: ""
// Increment profile flash count for firmware downloads
if (profile && !isSource) {
@@ -395,18 +368,14 @@ export const generateDownloadUrl = mutation({
}
// Increment plugin flash counts for firmware downloads (independent of profile)
if (
!isSource &&
build.config.pluginsEnabled &&
build.config.pluginsEnabled.length > 0
) {
if (!isSource && build.config.pluginsEnabled && build.config.pluginsEnabled.length > 0) {
await ctx.runMutation(internal.plugins.incrementFlashCount, {
slugs: build.config.pluginsEnabled,
})
}
const last4Hash = build.buildHash.slice(-4)
const os = 'meshtastic' // OS/platform identifier
const os = "meshtastic" // OS/platform identifier
const version = build.config.version
const target = build.config.target
const jobId = build.githubRunId

View File

@@ -1,20 +1,15 @@
import { getAuthUserId } from '@convex-dev/auth/server'
import { ConvexError, v } from 'convex/values'
import {
customAction,
customCtx,
customMutation,
customQuery,
} from 'convex-helpers/server/customFunctions'
import { api } from './_generated/api'
import { action, mutation, query } from './_generated/server'
import { getAuthUserId } from "@convex-dev/auth/server"
import { customAction, customCtx, customMutation, customQuery } from "convex-helpers/server/customFunctions"
import { ConvexError, v } from "convex/values"
import { api } from "./_generated/api"
import { action, mutation, query } from "./_generated/server"
export const authQuery = customQuery(
query,
customCtx(async (ctx) => {
customCtx(async ctx => {
const identity = await ctx.auth.getUserIdentity()
if (identity === null) {
throw new ConvexError('Not authenticated!')
throw new ConvexError("Not authenticated!")
}
return {}
})
@@ -22,10 +17,10 @@ export const authQuery = customQuery(
export const authMutation = customMutation(
mutation,
customCtx(async (ctx) => {
customCtx(async ctx => {
const identity = await ctx.auth.getUserIdentity()
if (identity === null) {
throw new ConvexError('Not authenticated!')
throw new ConvexError("Not authenticated!")
}
return {}
})
@@ -33,10 +28,10 @@ export const authMutation = customMutation(
export const authAction = customAction(
action,
customCtx(async (ctx) => {
customCtx(async ctx => {
const identity = await ctx.auth.getUserIdentity()
if (identity === null) {
throw new ConvexError('Not authenticated!')
throw new ConvexError("Not authenticated!")
}
return {}
})
@@ -44,19 +39,19 @@ export const authAction = customAction(
export const adminQuery = customQuery(
query,
customCtx(async (ctx) => {
customCtx(async ctx => {
const userId = await getAuthUserId(ctx)
if (!userId) {
throw new ConvexError('Not authenticated!')
throw new ConvexError("Not authenticated!")
}
const userSettings = await ctx.db
.query('userSettings')
.withIndex('by_user', (q) => q.eq('userId', userId))
.query("userSettings")
.withIndex("by_user", q => q.eq("userId", userId))
.first()
if (userSettings?.isAdmin !== true) {
throw new ConvexError('Unauthorized: Admin access required')
throw new ConvexError("Unauthorized: Admin access required")
}
return {}
@@ -65,19 +60,19 @@ export const adminQuery = customQuery(
export const adminMutation = customMutation(
mutation,
customCtx(async (ctx) => {
customCtx(async ctx => {
const userId = await getAuthUserId(ctx)
if (!userId) {
throw new ConvexError('Not authenticated!')
throw new ConvexError("Not authenticated!")
}
const userSettings = await ctx.db
.query('userSettings')
.withIndex('by_user', (q) => q.eq('userId', userId))
.query("userSettings")
.withIndex("by_user", q => q.eq("userId", userId))
.first()
if (userSettings?.isAdmin !== true) {
throw new ConvexError('Unauthorized: Admin access required')
throw new ConvexError("Unauthorized: Admin access required")
}
return {}
@@ -86,10 +81,10 @@ export const adminMutation = customMutation(
export const adminAction = customAction(
action,
customCtx(async (ctx) => {
customCtx(async ctx => {
const userId = await getAuthUserId(ctx)
if (!userId) {
throw new ConvexError('Not authenticated!')
throw new ConvexError("Not authenticated!")
}
// Actions can't access ctx.db directly, so we need to use a query
@@ -97,7 +92,7 @@ export const adminAction = customAction(
userId,
})
if (!isAdmin) {
throw new ConvexError('Unauthorized: Admin access required')
throw new ConvexError("Unauthorized: Admin access required")
}
return {}
@@ -106,11 +101,11 @@ export const adminAction = customAction(
// Internal query to check if user is admin (used by action middleware)
export const checkIsAdmin = query({
args: { userId: v.id('users') },
args: { userId: v.id("users") },
handler: async (ctx, args) => {
const userSettings = await ctx.db
.query('userSettings')
.withIndex('by_user', (q) => q.eq('userId', args.userId))
.query("userSettings")
.withIndex("by_user", q => q.eq("userId", args.userId))
.first()
return userSettings?.isAdmin === true

View File

@@ -1,39 +1,39 @@
import { httpRouter } from 'convex/server'
import { internal } from './_generated/api'
import { httpAction } from './_generated/server'
import { auth } from './auth'
import { httpRouter } from "convex/server"
import { internal } from "./_generated/api"
import { httpAction } from "./_generated/server"
import { auth } from "./auth"
const http = httpRouter()
auth.addHttpRoutes(http)
http.route({
path: '/github-webhook',
method: 'POST',
path: "/github-webhook",
method: "POST",
handler: httpAction(async (ctx, request) => {
// Verify bearer token
const buildToken = process.env.CONVEX_BUILD_TOKEN
if (!buildToken) {
return new Response('CONVEX_BUILD_TOKEN not configured', { status: 500 })
return new Response("CONVEX_BUILD_TOKEN not configured", { status: 500 })
}
const authHeader = request.headers.get('Authorization')
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return new Response('Missing or invalid Authorization header', {
const authHeader = request.headers.get("Authorization")
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return new Response("Missing or invalid Authorization header", {
status: 401,
})
}
const token = authHeader.substring(7) // Remove 'Bearer ' prefix
if (token !== buildToken) {
return new Response('Invalid token', { status: 401 })
return new Response("Invalid token", { status: 401 })
}
const payload = await request.json()
// Validate build_id and state are present
if (!payload.build_id || !payload.state) {
return new Response('Missing build_id or state', { status: 400 })
return new Response("Missing build_id or state", { status: 400 })
}
// Verify build exists
@@ -42,12 +42,10 @@ http.route({
})
if (!build) {
return new Response('Build not found', { status: 404 })
return new Response("Build not found", { status: 404 })
}
const githubRunId = payload.github_run_id
? Number(payload.github_run_id)
: undefined
const githubRunId = payload.github_run_id ? Number(payload.github_run_id) : undefined
await ctx.runMutation(internal.builds.updateBuildStatus, {
buildId: payload.build_id,

View File

@@ -1,10 +1,10 @@
import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
export async function generateSignedDownloadUrl(
objectKey: string,
filename: string,
contentType: string = 'application/octet-stream'
contentType: string = "application/octet-stream"
): Promise<string> {
const accountId = process.env.R2_ACCOUNT_ID
const accessKeyId = process.env.R2_ACCESS_KEY_ID
@@ -12,11 +12,11 @@ export async function generateSignedDownloadUrl(
const bucketName = process.env.R2_BUCKET_NAME
if (!accountId || !accessKeyId || !secretAccessKey || !bucketName) {
throw new Error('R2 credentials are not set')
throw new Error("R2 credentials are not set")
}
const s3 = new S3Client({
region: 'auto',
region: "auto",
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId,

View File

@@ -1,23 +1,21 @@
import { v } from 'convex/values'
import { internalMutation, query } from './_generated/server'
import { v } from "convex/values"
import { internalMutation, query } from "./_generated/server"
export const get = query({
args: { slug: v.string() },
handler: async (ctx, args) => {
const plugin = await ctx.db
.query('plugins')
.withIndex('by_slug', (q) => q.eq('slug', args.slug))
.query("plugins")
.withIndex("by_slug", q => q.eq("slug", args.slug))
.unique()
return plugin
? { slug: plugin.slug, flashCount: plugin.flashCount }
: { slug: args.slug, flashCount: 0 }
return plugin ? { slug: plugin.slug, flashCount: plugin.flashCount } : { slug: args.slug, flashCount: 0 }
},
})
export const getAll = query({
args: {},
handler: async (ctx) => {
const plugins = await ctx.db.query('plugins').collect()
handler: async ctx => {
const plugins = await ctx.db.query("plugins").collect()
const counts: Record<string, number> = {}
for (const plugin of plugins) {
counts[plugin.slug] = plugin.flashCount
@@ -31,11 +29,11 @@ export const incrementFlashCount = internalMutation({
handler: async (ctx, args) => {
for (const pluginSpec of args.slugs) {
// Extract slug from "slug@version" format, or use as-is if no @ present
const slug = pluginSpec.split('@')[0]
const slug = pluginSpec.split("@")[0]
const existing = await ctx.db
.query('plugins')
.withIndex('by_slug', (q) => q.eq('slug', slug))
.query("plugins")
.withIndex("by_slug", q => q.eq("slug", slug))
.unique()
if (existing) {
@@ -44,7 +42,7 @@ export const incrementFlashCount = internalMutation({
updatedAt: Date.now(),
})
} else {
await ctx.db.insert('plugins', {
await ctx.db.insert("plugins", {
slug,
flashCount: 1,
updatedAt: Date.now(),

View File

@@ -1,34 +1,34 @@
import { getAuthUserId } from '@convex-dev/auth/server'
import { v } from 'convex/values'
import { internalMutation, mutation, query } from './_generated/server'
import { buildConfigFields } from './schema'
import { getAuthUserId } from "@convex-dev/auth/server"
import { v } from "convex/values"
import { internalMutation, mutation, query } from "./_generated/server"
import { buildConfigFields } from "./schema"
export const list = query({
args: {},
handler: async (ctx) => {
handler: async ctx => {
const userId = await getAuthUserId(ctx)
if (!userId) return []
return await ctx.db
.query('profiles')
.filter((q) => q.eq(q.field('userId'), userId))
.query("profiles")
.filter(q => q.eq(q.field("userId"), userId))
.collect()
},
})
export const listPublic = query({
args: {},
handler: async (ctx) => {
handler: async ctx => {
const allProfiles = await ctx.db
.query('profiles')
.filter((q) => q.eq(q.field('isPublic'), true))
.query("profiles")
.filter(q => q.eq(q.field("isPublic"), true))
.collect()
return allProfiles.sort((a, b) => (b.flashCount ?? 0) - (a.flashCount ?? 0))
},
})
export const get = query({
args: { id: v.id('profiles') },
args: { id: v.id("profiles") },
handler: async (ctx, args) => {
const profile = await ctx.db.get(args.id)
if (!profile) return null
@@ -46,7 +46,7 @@ export const get = query({
// Internal mutation to get a build by ID
export const getBuildById = internalMutation({
args: { buildId: v.id('builds') },
args: { buildId: v.id("builds") },
handler: async (ctx, args) => {
return await ctx.db.get(args.buildId)
},
@@ -54,12 +54,12 @@ export const getBuildById = internalMutation({
export const recordFlash = mutation({
args: {
profileId: v.id('profiles'),
profileId: v.id("profiles"),
},
handler: async (ctx, args) => {
const profile = await ctx.db.get(args.profileId)
if (!profile) {
throw new Error('Profile not found')
throw new Error("Profile not found")
}
const nextCount = (profile.flashCount ?? 0) + 1
@@ -75,7 +75,7 @@ export const recordFlash = mutation({
export const upsert = mutation({
args: {
id: v.optional(v.id('profiles')),
id: v.optional(v.id("profiles")),
name: v.string(),
description: v.string(),
config: v.object(buildConfigFields),
@@ -83,13 +83,13 @@ export const upsert = mutation({
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx)
if (!userId) throw new Error('Unauthorized')
if (!userId) throw new Error("Unauthorized")
if (args.id) {
// Update existing profile
const profile = await ctx.db.get(args.id)
if (!profile || profile.userId !== userId) {
throw new Error('Unauthorized')
throw new Error("Unauthorized")
}
await ctx.db.patch(args.id, {
@@ -104,7 +104,7 @@ export const upsert = mutation({
return args.id
} else {
// Create new profile
const profileId = await ctx.db.insert('profiles', {
const profileId = await ctx.db.insert("profiles", {
userId,
name: args.name,
description: args.description,
@@ -120,14 +120,14 @@ export const upsert = mutation({
})
export const remove = mutation({
args: { id: v.id('profiles') },
args: { id: v.id("profiles") },
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx)
if (!userId) throw new Error('Unauthorized')
if (!userId) throw new Error("Unauthorized")
const profile = await ctx.db.get(args.id)
if (!profile || profile.userId !== userId) {
throw new Error('Unauthorized')
throw new Error("Unauthorized")
}
await ctx.db.delete(args.id)

View File

@@ -1,6 +1,6 @@
import { authTables } from '@convex-dev/auth/server'
import { defineSchema, defineTable } from 'convex/server'
import { v } from 'convex/values'
import { authTables } from "@convex-dev/auth/server"
import { defineSchema, defineTable } from "convex/server"
import { v } from "convex/values"
export const buildConfigFields = {
version: v.string(),
@@ -10,7 +10,7 @@ export const buildConfigFields = {
}
export const profileFields = {
userId: v.id('users'),
userId: v.id("users"),
name: v.string(),
description: v.string(),
config: v.object(buildConfigFields),
@@ -43,7 +43,7 @@ export const pluginFields = {
}
export const userSettingsFields = {
userId: v.id('users'),
userId: v.id("users"),
isAdmin: v.boolean(),
}
@@ -51,8 +51,8 @@ export const schema = defineSchema({
...authTables,
profiles: defineTable(profileFields),
builds: defineTable(buildFields),
plugins: defineTable(pluginFields).index('by_slug', ['slug']),
userSettings: defineTable(userSettingsFields).index('by_user', ['userId']),
plugins: defineTable(pluginFields).index("by_slug", ["slug"]),
userSettings: defineTable(userSettingsFields).index("by_user", ["userId"]),
})
export const buildsDocValidator = schema.tables.builds.validator

View File

@@ -1,6 +1,6 @@
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
import PARENT_MAP from '../constants/architecture-hierarchy.json'
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import PARENT_MAP from "../constants/architecture-hierarchy.json"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -34,16 +34,16 @@ export function timeAgo(date: number | string | Date): string {
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'
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(' ')
.split("_")
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ")
}
/**
@@ -69,7 +69,7 @@ export function getDependedPlugins(
// 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
if (depId === "meshtastic") continue
// Only include dependencies that exist in the registry
if (depId in registry) {
@@ -101,7 +101,7 @@ export function getImplicitDependencies(
): Set<string> {
const allDependencies = getDependedPlugins(explicitlySelectedPlugins, registry)
const explicitSet = new Set(explicitlySelectedPlugins)
return new Set(allDependencies.filter((id) => !explicitSet.has(id)))
return new Set(allDependencies.filter(id => !explicitSet.has(id)))
}
/**
@@ -116,14 +116,14 @@ export function isRequiredByOther(
// 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
}
@@ -134,7 +134,7 @@ export function isRequiredByOther(
* Some sources might use "esp32_s3" (with underscores)
*/
function normalizeArchitecture(arch: string): string {
return arch.replace(/[-_]/g, '')
return arch.replace(/[-_]/g, "")
}
/**
@@ -144,27 +144,27 @@ function normalizeArchitecture(arch: string): string {
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
}
@@ -176,41 +176,41 @@ export function getBaseArchitecture(name: string): string | null {
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
@@ -226,40 +226,40 @@ export function isPluginCompatibleWithTarget(
}
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 isExcluded = pluginExcludes.some(exclude => {
const normalizedExclude = normalizeArchitecture(exclude)
return compatibleNames.has(normalizedExclude)
})
@@ -267,16 +267,16 @@ export function isPluginCompatibleWithTarget(
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) => {
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
}

View File

@@ -1,11 +1,11 @@
// https://vike.dev/Head
import logoUrl from "../assets/logo.png";
import faviconUrl from "../assets/favicon.svg";
import appleTouchIconUrl from "../assets/apple-touch-icon.png";
import siteWebmanifestUrl from "../assets/site.webmanifest";
import favicon96x96Url from "../assets/favicon-96x96.png";
import faviconIcoUrl from "../assets/favicon.ico";
import appleTouchIconUrl from "../assets/apple-touch-icon.png"
import favicon96x96Url from "../assets/favicon-96x96.png"
import faviconIcoUrl from "../assets/favicon.ico"
import faviconUrl from "../assets/favicon.svg"
import logoUrl from "../assets/logo.png"
import siteWebmanifestUrl from "../assets/site.webmanifest"
export function Head() {
return (
@@ -18,5 +18,5 @@ export function Head() {
<link rel="manifest" href={siteWebmanifestUrl} />
<link rel="icon" href={logoUrl} />
</>
);
)
}

View File

@@ -1,20 +1,18 @@
import "./Layout.css";
import "./tailwind.css";
import logoUrl from "../assets/logo.png";
import { Link } from "../components/Link";
import { ConvexReactClient } from "convex/react";
import { ConvexAuthProvider } from "@convex-dev/auth/react";
import Navbar from "@/components/Navbar";
import { usePageContext } from "vike-react/usePageContext";
import Navbar from "@/components/Navbar"
import { ConvexAuthProvider } from "@convex-dev/auth/react"
import { ConvexReactClient } from "convex/react"
import { usePageContext } from "vike-react/usePageContext"
import "./Layout.css"
import "./tailwind.css"
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string)
function ConditionalNavbar() {
const pageContext = usePageContext();
const pageContext = usePageContext()
if (pageContext.urlPathname === "/") {
return null;
return null
}
return <Navbar />;
return <Navbar />
}
export default function Layout({ children }: { children: React.ReactNode }) {
@@ -23,5 +21,5 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<ConditionalNavbar />
{children}
</ConvexAuthProvider>
);
)
}

View File

@@ -1,4 +1,4 @@
export async function onPageTransitionEnd() {
console.log("Page transition end");
document.body.classList.remove("page-transition");
console.log("Page transition end")
document.body.classList.remove("page-transition")
}

View File

@@ -1,9 +1,9 @@
// https://vike.dev/onPageTransitionStart
import type { PageContextClient } from "vike/types";
import type { PageContextClient } from "vike/types"
export async function onPageTransitionStart(pageContext: Partial<PageContextClient>) {
console.log("Page transition start");
console.log("pageContext.isBackwardNavigation", pageContext.isBackwardNavigation);
document.body.classList.add("page-transition");
console.log("Page transition start")
console.log("pageContext.isBackwardNavigation", pageContext.isBackwardNavigation)
document.body.classList.add("page-transition")
}

View File

@@ -1,19 +1,19 @@
import { usePageContext } from "vike-react/usePageContext";
import { usePageContext } from "vike-react/usePageContext"
export default function Page() {
const { is404 } = usePageContext();
const { is404 } = usePageContext()
if (is404) {
return (
<>
<h1>Page Not Found</h1>
<p>This page could not be found.</p>
</>
);
)
}
return (
<>
<h1>Internal Error</h1>
<p>Something went wrong.</p>
</>
);
)
}

View File

@@ -1,23 +1,23 @@
import { useMutation, useQuery } from "convex/react";
import { useState } from "react";
import { toast } from "sonner";
import { navigate } from "vike/client/router";
import { BuildDownloadButton } from "@/components/BuildDownloadButton";
import { Button } from "@/components/ui/button";
import { api } from "../../convex/_generated/api";
import type { Id } from "../../convex/_generated/dataModel";
import { ArtifactType } from "../../convex/builds";
import { BuildDownloadButton } from "@/components/BuildDownloadButton"
import { Button } from "@/components/ui/button"
import { useMutation, useQuery } from "convex/react"
import { useState } from "react"
import { toast } from "sonner"
import { navigate } from "vike/client/router"
import { api } from "../../convex/_generated/api"
import type { Id } from "../../convex/_generated/dataModel"
import { ArtifactType } from "../../convex/builds"
type FilterType = "all" | "failed";
type FilterType = "all" | "failed"
export default function Admin() {
const [filter, setFilter] = useState<FilterType>("failed");
const isAdmin = useQuery(api.admin.isAdmin);
const failedBuilds = useQuery(api.admin.listFailedBuilds);
const allBuilds = useQuery(api.admin.listAllBuilds);
const retryBuild = useMutation(api.admin.retryBuild);
const [filter, setFilter] = useState<FilterType>("failed")
const isAdmin = useQuery(api.admin.isAdmin)
const failedBuilds = useQuery(api.admin.listFailedBuilds)
const allBuilds = useQuery(api.admin.listAllBuilds)
const retryBuild = useMutation(api.admin.retryBuild)
const builds = filter === "failed" ? failedBuilds : allBuilds;
const builds = filter === "failed" ? failedBuilds : allBuilds
// Show loading state
if (isAdmin === undefined) {
@@ -25,7 +25,7 @@ export default function Admin() {
<div className="min-h-screen bg-slate-950 text-white flex items-center justify-center">
<div className="text-slate-400">Loading...</div>
</div>
);
)
}
// Redirect if not admin
@@ -38,25 +38,25 @@ export default function Admin() {
<Button onClick={() => navigate("/")}>Go Home</Button>
</div>
</div>
);
)
}
const handleRetry = async (buildId: Id<"builds">) => {
try {
await retryBuild({ buildId });
await retryBuild({ buildId })
toast.success("Build retry initiated", {
description: "The build has been queued with the latest YAML.",
});
})
} catch (error) {
toast.error("Failed to retry build", {
description: String(error),
});
})
}
};
}
const formatDate = (timestamp: number) => {
return new Date(timestamp).toLocaleString();
};
return new Date(timestamp).toLocaleString()
}
const getStatusBadge = (status: string) => {
const statusConfig = {
@@ -71,14 +71,14 @@ export default function Admin() {
text: "text-yellow-400",
label: "Queued",
},
};
}
const config = statusConfig[status as keyof typeof statusConfig] || {
bg: "bg-slate-500/20",
text: "text-slate-400",
label: status,
};
return <span className={`px-2 py-1 ${config.bg} ${config.text} rounded text-sm`}>{config.label}</span>;
};
}
return <span className={`px-2 py-1 ${config.bg} ${config.text} rounded text-sm`}>{config.label}</span>
}
return (
<div className="min-h-screen bg-slate-950 text-white p-8">
@@ -112,7 +112,7 @@ export default function Admin() {
<div className="text-center text-slate-400 py-12">No {filter === "failed" ? "failed " : ""}builds found.</div>
) : (
<div className="space-y-4">
{builds.map((build) => (
{builds.map(build => (
<div key={build._id} className="bg-slate-900 border border-slate-800 rounded-lg p-6">
{/* Header Section */}
<div className="flex items-center justify-between mb-4 pb-4 border-b border-slate-800">
@@ -191,7 +191,7 @@ export default function Admin() {
{build.githubRunId}
</a>
)}
{build.githubRunIdHistory?.map((id) => (
{build.githubRunIdHistory?.map(id => (
<a
key={id}
href={`https://github.com/MeshEnvy/mesh-forge/actions/runs/${id}`}
@@ -219,5 +219,5 @@ export default function Admin() {
)}
</main>
</div>
);
)
}

View File

@@ -1,5 +1,5 @@
import Builder from "./Builder";
import Builder from "./Builder"
export default function BuildNew() {
return <Builder />;
return <Builder />
}

View File

@@ -1,5 +1,5 @@
import Builder from "../Builder";
import Builder from "../Builder"
export default function BuildNew() {
return <Builder />;
return <Builder />
}

View File

@@ -1,5 +1,5 @@
import { Link } from "../../components/Link";
import { usePageContext } from "vike-react/usePageContext";
import { usePageContext } from "vike-react/usePageContext"
import { Link } from "../../components/Link"
const navSections = [
{
@@ -19,12 +19,12 @@ const navSections = [
{ href: "/docs/nRF52", label: "nRF52" },
],
},
];
]
function NavLink({ href, label }: { href: string; label: string }) {
const pageContext = usePageContext();
const { urlPathname } = pageContext;
const isActive = href === "/docs" ? urlPathname === href : urlPathname.startsWith(href);
const pageContext = usePageContext()
const { urlPathname } = pageContext
const isActive = href === "/docs" ? urlPathname === href : urlPathname.startsWith(href)
return (
<Link href={href}>
@@ -38,7 +38,7 @@ function NavLink({ href, label }: { href: string; label: string }) {
{label}
</span>
</Link>
);
)
}
export default function Layout({ children }: { children: React.ReactNode }) {
@@ -55,7 +55,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</h3>
)}
<ul className="space-y-1 mt-1">
{section.items.map((item) => (
{section.items.map(item => (
<li key={item.href}>
<NavLink href={item.href} label={item.label} />
</li>
@@ -70,5 +70,5 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<article className="prose prose-invert lg:prose-xl max-w-none">{children}</article>
</main>
</div>
);
)
}

View File

@@ -54,4 +54,3 @@ If you want to create and contribute plugins to the registry, check out the [Plu
- [MeshEnvy](https://meshenvy.org) - Built by MeshEnvy (not affiliated with Meshtastic)
- [Meshtastic](https://meshtastic.org) - Learn more about Meshtastic

View File

@@ -10,6 +10,6 @@ const config = {
tabWidth: 2,
useTabs: false,
semi: false,
};
}
export default config;
export default config

View File

@@ -1,9 +1,9 @@
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import vike from "vike/plugin";
import { defineConfig } from "vite";
import path from "node:path";
import mdx from "@mdx-js/rollup";
import mdx from "@mdx-js/rollup"
import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import path from "node:path"
import vike from "vike/plugin"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [vike(), mdx(), react(), tailwindcss()],
@@ -12,4 +12,4 @@ export default defineConfig({
"@": path.resolve(__dirname, "."),
},
},
});
})