Merge sai changes with integration fixes
This commit is contained in:
@@ -43,6 +43,9 @@ export const makeAgentOsRuntime = Effect.gen(function* () {
|
|||||||
catch: (cause) =>
|
catch: (cause) =>
|
||||||
new AgentOsCommandError({ method: command.method, cause }),
|
new AgentOsCommandError({ method: command.method, cause }),
|
||||||
}).pipe(
|
}).pipe(
|
||||||
|
Effect.map((result) =>
|
||||||
|
result instanceof Uint8Array ? result.slice().buffer : result
|
||||||
|
),
|
||||||
Effect.tap((result) =>
|
Effect.tap((result) =>
|
||||||
controlPlane.recordEvent({
|
controlPlane.recordEvent({
|
||||||
commandId: command._id,
|
commandId: command._id,
|
||||||
|
|||||||
5
apps/native/app/(auth)/_layout.tsx
Normal file
5
apps/native/app/(auth)/_layout.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Stack } from "expo-router";
|
||||||
|
|
||||||
|
export default function AuthLayout() {
|
||||||
|
return <Stack screenOptions={{ headerShown: false }} />;
|
||||||
|
}
|
||||||
24
apps/native/app/(auth)/login.tsx
Normal file
24
apps/native/app/(auth)/login.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { NativeLoginForm } from "@code/auth/native";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
|
||||||
|
|
||||||
|
export default function LoginScreen() {
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||||
|
className="flex-1 bg-background"
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerClassName="flex-grow justify-center p-6"
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
>
|
||||||
|
<View className="mx-auto w-full max-w-md">
|
||||||
|
<NativeLoginForm
|
||||||
|
onNavigateSignUp={() => router.push("/(auth)/signup")}
|
||||||
|
onSuccess={() => router.replace("/")}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
apps/native/app/(auth)/signup.tsx
Normal file
24
apps/native/app/(auth)/signup.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { NativeSignupForm } from "@code/auth/native";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { KeyboardAvoidingView, Platform, ScrollView, View } from "react-native";
|
||||||
|
|
||||||
|
export default function SignupScreen() {
|
||||||
|
return (
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||||
|
className="flex-1 bg-background"
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerClassName="flex-grow justify-center p-6"
|
||||||
|
keyboardShouldPersistTaps="handled"
|
||||||
|
>
|
||||||
|
<View className="mx-auto w-full max-w-md py-6">
|
||||||
|
<NativeSignupForm
|
||||||
|
onNavigateSignIn={() => router.push("/(auth)/login")}
|
||||||
|
onSuccess={() => router.replace("/")}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,72 +1,53 @@
|
|||||||
|
import { authClient } from "@code/auth/native";
|
||||||
import { api } from "@code/backend/convex/_generated/api";
|
import { api } from "@code/backend/convex/_generated/api";
|
||||||
import { useConvexAuth, useQuery } from "convex/react";
|
import { useQuery } from "convex/react";
|
||||||
import { Button, Chip, Separator, Spinner, Surface, useThemeColor } from "heroui-native";
|
import { Button, Spinner, Surface } from "heroui-native";
|
||||||
import { Text, View } from "react-native";
|
import { Text, View } from "react-native";
|
||||||
|
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import { SignIn } from "@/components/sign-in";
|
|
||||||
import { SignUp } from "@/components/sign-up";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const healthCheck = useQuery(api.healthCheck.get);
|
const healthCheck = useQuery(api.healthCheck.get);
|
||||||
const { isAuthenticated } = useConvexAuth();
|
const user = useQuery(api.auth.getCurrentUser);
|
||||||
const user = useQuery(api.auth.getCurrentUser, isAuthenticated ? {} : "skip");
|
|
||||||
const successColor = useThemeColor("success");
|
|
||||||
const dangerColor = useThemeColor("danger");
|
|
||||||
|
|
||||||
const isConnected = healthCheck === "OK";
|
|
||||||
const isLoading = healthCheck === undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="px-4 pb-4">
|
<Container className="px-4 pb-4">
|
||||||
<View className="py-6 mb-5">
|
<View className="mb-5 py-6">
|
||||||
<Text className="text-3xl font-semibold text-foreground tracking-tight">
|
<Text className="text-3xl font-semibold tracking-tight text-foreground">Zopu</Text>
|
||||||
Better T Stack
|
<Text className="mt-1 text-sm text-muted">Your authenticated workspace</Text>
|
||||||
</Text>
|
|
||||||
<Text className="text-muted text-sm mt-1">Full-stack TypeScript starter</Text>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{user ? (
|
<Surface className="mb-4 rounded-xl p-4" variant="secondary">
|
||||||
<Surface variant="secondary" className="mb-4 p-4 rounded-xl">
|
{user === undefined ? (
|
||||||
<View className="flex-row items-center justify-between">
|
<Spinner size="sm" />
|
||||||
|
) : (
|
||||||
|
<View className="flex-row items-center justify-between gap-4">
|
||||||
<View className="flex-1">
|
<View className="flex-1">
|
||||||
<Text className="text-foreground font-medium">{user.name}</Text>
|
<Text className="font-medium text-foreground">{user?.name ?? "Signed in"}</Text>
|
||||||
<Text className="text-muted text-xs mt-0.5">{user.email}</Text>
|
<Text className="mt-0.5 text-xs text-muted">{user?.email}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Button
|
<Button onPress={() => void authClient.signOut()} size="sm" variant="danger">
|
||||||
variant="danger"
|
<Button.Label>Sign out</Button.Label>
|
||||||
size="sm"
|
|
||||||
onPress={() => {
|
|
||||||
authClient.signOut();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Sign Out
|
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
</Surface>
|
)}
|
||||||
) : null}
|
</Surface>
|
||||||
<Surface variant="secondary" className="p-4 rounded-xl">
|
|
||||||
<Text className="text-foreground font-medium mb-2">API Status</Text>
|
<Surface className="rounded-xl p-4" variant="secondary">
|
||||||
|
<Text className="mb-2 font-medium text-foreground">API status</Text>
|
||||||
<View className="flex-row items-center gap-2">
|
<View className="flex-row items-center gap-2">
|
||||||
<View
|
<View
|
||||||
className={`w-2 h-2 rounded-full ${healthCheck === "OK" ? "bg-success" : "bg-danger"}`}
|
className={`h-2 w-2 rounded-full ${healthCheck === "OK" ? "bg-success" : "bg-danger"}`}
|
||||||
/>
|
/>
|
||||||
<Text className="text-muted text-xs">
|
<Text className="text-xs text-muted">
|
||||||
{healthCheck === undefined
|
{healthCheck === undefined
|
||||||
? "Checking..."
|
? "Checking…"
|
||||||
: healthCheck === "OK"
|
: healthCheck === "OK"
|
||||||
? "Connected to API"
|
? "Connected to Convex"
|
||||||
: "API Disconnected"}
|
: "Convex unavailable"}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Surface>
|
</Surface>
|
||||||
{!user && (
|
|
||||||
<View className="mt-5 gap-4">
|
|
||||||
<SignIn />
|
|
||||||
<SignUp />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,53 @@
|
|||||||
import "@/global.css";
|
import "@/global.css";
|
||||||
import { env } from "@code/env/native";
|
import { NativeAuthProvider } from "@code/auth/native";
|
||||||
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
|
import { useConvexAuth } from "convex/react";
|
||||||
import { ConvexReactClient } from "convex/react";
|
|
||||||
import { Stack } from "expo-router";
|
import { Stack } from "expo-router";
|
||||||
import { HeroUINativeProvider } from "heroui-native";
|
import { HeroUINativeProvider, Spinner } from "heroui-native";
|
||||||
|
import { View } from "react-native";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
import { KeyboardProvider } from "react-native-keyboard-controller";
|
import { KeyboardProvider } from "react-native-keyboard-controller";
|
||||||
|
|
||||||
import { AppThemeProvider } from "@/contexts/app-theme-context";
|
import { AppThemeProvider } from "@/contexts/app-theme-context";
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
export const unstable_settings = {
|
function RootNavigator() {
|
||||||
initialRouteName: "(drawer)",
|
const { isAuthenticated, isLoading } = useConvexAuth();
|
||||||
};
|
|
||||||
|
|
||||||
const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL, {
|
if (isLoading) {
|
||||||
unsavedChangesWarning: false,
|
return (
|
||||||
});
|
<View className="flex-1 items-center justify-center bg-background">
|
||||||
|
<Spinner size="lg" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function StackLayout() {
|
|
||||||
return (
|
return (
|
||||||
<Stack screenOptions={{}}>
|
<Stack screenOptions={{ headerShown: false }}>
|
||||||
<Stack.Screen name="(drawer)" options={{ headerShown: false }} />
|
<Stack.Protected guard={!isAuthenticated}>
|
||||||
<Stack.Screen name="modal" options={{ title: "Modal", presentation: "modal" }} />
|
<Stack.Screen name="(auth)" />
|
||||||
|
</Stack.Protected>
|
||||||
|
<Stack.Protected guard={isAuthenticated}>
|
||||||
|
<Stack.Screen name="(drawer)" />
|
||||||
|
<Stack.Screen
|
||||||
|
name="modal"
|
||||||
|
options={{ headerShown: true, presentation: "modal", title: "Modal" }}
|
||||||
|
/>
|
||||||
|
</Stack.Protected>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout() {
|
export default function Layout() {
|
||||||
return (
|
return (
|
||||||
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
|
<NativeAuthProvider>
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<KeyboardProvider>
|
<KeyboardProvider>
|
||||||
<AppThemeProvider>
|
<AppThemeProvider>
|
||||||
<HeroUINativeProvider>
|
<HeroUINativeProvider>
|
||||||
<StackLayout />
|
<RootNavigator />
|
||||||
</HeroUINativeProvider>
|
</HeroUINativeProvider>
|
||||||
</AppThemeProvider>
|
</AppThemeProvider>
|
||||||
</KeyboardProvider>
|
</KeyboardProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
</ConvexBetterAuthProvider>
|
</NativeAuthProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,164 +0,0 @@
|
|||||||
import { useForm } from "@tanstack/react-form";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
FieldError,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Spinner,
|
|
||||||
Surface,
|
|
||||||
TextField,
|
|
||||||
useToast,
|
|
||||||
} from "heroui-native";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import { Text, TextInput, View } from "react-native";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
const signInSchema = z.object({
|
|
||||||
email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"),
|
|
||||||
password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"),
|
|
||||||
});
|
|
||||||
|
|
||||||
function getErrorMessage(error: unknown): string | null {
|
|
||||||
if (!error) return null;
|
|
||||||
|
|
||||||
if (typeof error === "string") {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(error)) {
|
|
||||||
for (const issue of error) {
|
|
||||||
const message = getErrorMessage(issue);
|
|
||||||
if (message) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error === "object" && error !== null) {
|
|
||||||
const maybeError = error as { message?: unknown };
|
|
||||||
if (typeof maybeError.message === "string") {
|
|
||||||
return maybeError.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SignIn() {
|
|
||||||
const passwordInputRef = useRef<TextInput>(null);
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onSubmit: signInSchema,
|
|
||||||
},
|
|
||||||
onSubmit: async ({ value, formApi }) => {
|
|
||||||
await authClient.signIn.email(
|
|
||||||
{
|
|
||||||
email: value.email.trim(),
|
|
||||||
password: value.password,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onError(error) {
|
|
||||||
toast.show({
|
|
||||||
variant: "danger",
|
|
||||||
label: error.error?.message || "Failed to sign in",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess() {
|
|
||||||
formApi.reset();
|
|
||||||
toast.show({
|
|
||||||
variant: "success",
|
|
||||||
label: "Signed in successfully",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Surface variant="secondary" className="p-4 rounded-lg">
|
|
||||||
<Text className="text-foreground font-medium mb-4">Sign In</Text>
|
|
||||||
|
|
||||||
<form.Subscribe
|
|
||||||
selector={(state) => ({
|
|
||||||
isSubmitting: state.isSubmitting,
|
|
||||||
validationError: getErrorMessage(state.errorMap.onSubmit),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{({ isSubmitting, validationError }) => {
|
|
||||||
const formError = validationError;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FieldError isInvalid={!!formError} className="mb-3">
|
|
||||||
{formError}
|
|
||||||
</FieldError>
|
|
||||||
|
|
||||||
<View className="gap-3">
|
|
||||||
<form.Field name="email">
|
|
||||||
{(field) => (
|
|
||||||
<TextField>
|
|
||||||
<Label>Email</Label>
|
|
||||||
<Input
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChangeText={field.handleChange}
|
|
||||||
placeholder="email@example.com"
|
|
||||||
keyboardType="email-address"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="email"
|
|
||||||
textContentType="emailAddress"
|
|
||||||
returnKeyType="next"
|
|
||||||
blurOnSubmit={false}
|
|
||||||
onSubmitEditing={() => {
|
|
||||||
passwordInputRef.current?.focus();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<form.Field name="password">
|
|
||||||
{(field) => (
|
|
||||||
<TextField>
|
|
||||||
<Label>Password</Label>
|
|
||||||
<Input
|
|
||||||
ref={passwordInputRef}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChangeText={field.handleChange}
|
|
||||||
placeholder="••••••••"
|
|
||||||
secureTextEntry
|
|
||||||
autoComplete="password"
|
|
||||||
textContentType="password"
|
|
||||||
returnKeyType="go"
|
|
||||||
onSubmitEditing={form.handleSubmit}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<Button onPress={form.handleSubmit} isDisabled={isSubmitting} className="mt-1">
|
|
||||||
{isSubmitting ? (
|
|
||||||
<Spinner size="sm" color="default" />
|
|
||||||
) : (
|
|
||||||
<Button.Label>Sign In</Button.Label>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</form.Subscribe>
|
|
||||||
</Surface>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import { useForm } from "@tanstack/react-form";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
FieldError,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Spinner,
|
|
||||||
Surface,
|
|
||||||
TextField,
|
|
||||||
useToast,
|
|
||||||
} from "heroui-native";
|
|
||||||
import { useRef } from "react";
|
|
||||||
import { Text, TextInput, View } from "react-native";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
const signUpSchema = z.object({
|
|
||||||
name: z.string().trim().min(1, "Name is required").min(2, "Name must be at least 2 characters"),
|
|
||||||
email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"),
|
|
||||||
password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"),
|
|
||||||
});
|
|
||||||
|
|
||||||
function getErrorMessage(error: unknown): string | null {
|
|
||||||
if (!error) return null;
|
|
||||||
|
|
||||||
if (typeof error === "string") {
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(error)) {
|
|
||||||
for (const issue of error) {
|
|
||||||
const message = getErrorMessage(issue);
|
|
||||||
if (message) {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof error === "object" && error !== null) {
|
|
||||||
const maybeError = error as { message?: unknown };
|
|
||||||
if (typeof maybeError.message === "string") {
|
|
||||||
return maybeError.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function SignUp() {
|
|
||||||
const emailInputRef = useRef<TextInput>(null);
|
|
||||||
const passwordInputRef = useRef<TextInput>(null);
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
name: "",
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onSubmit: signUpSchema,
|
|
||||||
},
|
|
||||||
onSubmit: async ({ value, formApi }) => {
|
|
||||||
await authClient.signUp.email(
|
|
||||||
{
|
|
||||||
name: value.name.trim(),
|
|
||||||
email: value.email.trim(),
|
|
||||||
password: value.password,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onError(error) {
|
|
||||||
toast.show({
|
|
||||||
variant: "danger",
|
|
||||||
label: error.error?.message || "Failed to sign up",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess() {
|
|
||||||
formApi.reset();
|
|
||||||
toast.show({
|
|
||||||
variant: "success",
|
|
||||||
label: "Account created successfully",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Surface variant="secondary" className="p-4 rounded-lg">
|
|
||||||
<Text className="text-foreground font-medium mb-4">Create Account</Text>
|
|
||||||
|
|
||||||
<form.Subscribe
|
|
||||||
selector={(state) => ({
|
|
||||||
isSubmitting: state.isSubmitting,
|
|
||||||
validationError: getErrorMessage(state.errorMap.onSubmit),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{({ isSubmitting, validationError }) => {
|
|
||||||
const formError = validationError;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FieldError isInvalid={!!formError} className="mb-3">
|
|
||||||
{formError}
|
|
||||||
</FieldError>
|
|
||||||
|
|
||||||
<View className="gap-3">
|
|
||||||
<form.Field name="name">
|
|
||||||
{(field) => (
|
|
||||||
<TextField>
|
|
||||||
<Label>Name</Label>
|
|
||||||
<Input
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChangeText={field.handleChange}
|
|
||||||
placeholder="John Doe"
|
|
||||||
autoComplete="name"
|
|
||||||
textContentType="name"
|
|
||||||
returnKeyType="next"
|
|
||||||
blurOnSubmit={false}
|
|
||||||
onSubmitEditing={() => {
|
|
||||||
emailInputRef.current?.focus();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<form.Field name="email">
|
|
||||||
{(field) => (
|
|
||||||
<TextField>
|
|
||||||
<Label>Email</Label>
|
|
||||||
<Input
|
|
||||||
ref={emailInputRef}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChangeText={field.handleChange}
|
|
||||||
placeholder="email@example.com"
|
|
||||||
keyboardType="email-address"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoComplete="email"
|
|
||||||
textContentType="emailAddress"
|
|
||||||
returnKeyType="next"
|
|
||||||
blurOnSubmit={false}
|
|
||||||
onSubmitEditing={() => {
|
|
||||||
passwordInputRef.current?.focus();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<form.Field name="password">
|
|
||||||
{(field) => (
|
|
||||||
<TextField>
|
|
||||||
<Label>Password</Label>
|
|
||||||
<Input
|
|
||||||
ref={passwordInputRef}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChangeText={field.handleChange}
|
|
||||||
placeholder="••••••••"
|
|
||||||
secureTextEntry
|
|
||||||
autoComplete="new-password"
|
|
||||||
textContentType="newPassword"
|
|
||||||
returnKeyType="go"
|
|
||||||
onSubmitEditing={form.handleSubmit}
|
|
||||||
/>
|
|
||||||
</TextField>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
|
|
||||||
<Button onPress={form.handleSubmit} isDisabled={isSubmitting} className="mt-1">
|
|
||||||
{isSubmitting ? (
|
|
||||||
<Spinner size="sm" color="default" />
|
|
||||||
) : (
|
|
||||||
<Button.Label>Create Account</Button.Label>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</form.Subscribe>
|
|
||||||
</Surface>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -12,24 +12,18 @@
|
|||||||
"web": "bun --env-file=../../.env expo start --web"
|
"web": "bun --env-file=../../.env expo start --web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/expo": "catalog:",
|
"@code/auth": "workspace:*",
|
||||||
"@code/backend": "workspace:*",
|
"@code/backend": "workspace:*",
|
||||||
"@code/env": "workspace:*",
|
|
||||||
"@convex-dev/better-auth": "catalog:",
|
|
||||||
"@expo/metro-runtime": "~57.0.2",
|
"@expo/metro-runtime": "~57.0.2",
|
||||||
"@expo/vector-icons": "^15.1.1",
|
"@expo/vector-icons": "^15.1.1",
|
||||||
"@gorhom/bottom-sheet": "^5.2.14",
|
"@gorhom/bottom-sheet": "^5.2.14",
|
||||||
"@tanstack/react-form": "catalog:",
|
|
||||||
"better-auth": "catalog:",
|
|
||||||
"convex": "catalog:",
|
"convex": "catalog:",
|
||||||
"expo": "~57.0.1",
|
"expo": "~57.0.1",
|
||||||
"expo-constants": "~57.0.2",
|
|
||||||
"expo-font": "~57.0.0",
|
"expo-font": "~57.0.0",
|
||||||
"expo-haptics": "~57.0.0",
|
"expo-haptics": "~57.0.0",
|
||||||
"expo-linking": "~57.0.1",
|
"expo-linking": "~57.0.1",
|
||||||
"expo-network": "~57.0.0",
|
"expo-network": "~57.0.0",
|
||||||
"expo-router": "~57.0.2",
|
"expo-router": "~57.0.2",
|
||||||
"expo-secure-store": "~57.0.0",
|
|
||||||
"expo-status-bar": "~57.0.0",
|
"expo-status-bar": "~57.0.0",
|
||||||
"expo-web-browser": "~57.0.0",
|
"expo-web-browser": "~57.0.0",
|
||||||
"heroui-native": "^1.0.5",
|
"heroui-native": "^1.0.5",
|
||||||
@@ -47,8 +41,7 @@
|
|||||||
"tailwind-merge": "catalog:",
|
"tailwind-merge": "catalog:",
|
||||||
"tailwind-variants": "^3.2.2",
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"uniwind": "^1.10.0",
|
"uniwind": "^1.10.0"
|
||||||
"zod": "catalog:"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@code/config": "workspace:*",
|
"@code/config": "workspace:*",
|
||||||
|
|||||||
3
apps/web/.gitignore
vendored
3
apps/web/.gitignore
vendored
@@ -40,7 +40,8 @@ npm-debug.log*
|
|||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
*.log*
|
*.log
|
||||||
|
*.log.*
|
||||||
|
|
||||||
# TypeScript
|
# TypeScript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|||||||
@@ -10,17 +10,15 @@
|
|||||||
"typecheck": "react-router typegen && tsc"
|
"typecheck": "react-router typegen && tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@code/auth": "workspace:*",
|
||||||
"@code/backend": "workspace:*",
|
"@code/backend": "workspace:*",
|
||||||
"@code/env": "workspace:*",
|
"@code/env": "workspace:*",
|
||||||
"@code/ui": "workspace:*",
|
"@code/ui": "workspace:*",
|
||||||
"@convex-dev/better-auth": "catalog:",
|
|
||||||
"@flue/react": "1.0.0-beta.9",
|
"@flue/react": "1.0.0-beta.9",
|
||||||
"@flue/sdk": "1.0.0-beta.9",
|
"@flue/sdk": "1.0.0-beta.9",
|
||||||
"@react-router/fs-routes": "^8.1.0",
|
"@react-router/fs-routes": "^8.1.0",
|
||||||
"@react-router/node": "^8.1.0",
|
"@react-router/node": "^8.1.0",
|
||||||
"@react-router/serve": "^8.1.0",
|
"@react-router/serve": "^8.1.0",
|
||||||
"@tanstack/react-form": "catalog:",
|
|
||||||
"better-auth": "catalog:",
|
|
||||||
"convex": "catalog:",
|
"convex": "catalog:",
|
||||||
"isbot": "^5.1.44",
|
"isbot": "^5.1.44",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
@@ -29,8 +27,7 @@
|
|||||||
"react-dom": "^19.2.7",
|
"react-dom": "^19.2.7",
|
||||||
"react-router": "^8.1.0",
|
"react-router": "^8.1.0",
|
||||||
"sonner": "catalog:",
|
"sonner": "catalog:",
|
||||||
"streamdown": "2.5.0",
|
"streamdown": "2.5.0"
|
||||||
"zod": "catalog:"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@code/config": "workspace:*",
|
"@code/config": "workspace:*",
|
||||||
|
|||||||
445
apps/web/src/components/projects/project-workspace-page.tsx
Normal file
445
apps/web/src/components/projects/project-workspace-page.tsx
Normal file
@@ -0,0 +1,445 @@
|
|||||||
|
import type { Doc, Id } from "@code/backend/convex/_generated/dataModel";
|
||||||
|
import { Button } from "@code/ui/components/button";
|
||||||
|
import { Input } from "@code/ui/components/input";
|
||||||
|
import { Textarea } from "@code/ui/components/textarea";
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
ExternalLink,
|
||||||
|
FileText,
|
||||||
|
GitFork,
|
||||||
|
GitPullRequest,
|
||||||
|
Play,
|
||||||
|
Plus,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import UserMenu from "@/components/user-menu";
|
||||||
|
import { useProjectWorkspace } from "@/hooks/use-project-workspace";
|
||||||
|
|
||||||
|
const statusStyle: Record<Doc<"projectIssues">["status"], string> = {
|
||||||
|
completed:
|
||||||
|
"border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
|
||||||
|
failed: "border-destructive/40 bg-destructive/10 text-destructive",
|
||||||
|
"needs-input":
|
||||||
|
"border-orange-500/40 bg-orange-500/10 text-orange-700 dark:text-orange-300",
|
||||||
|
open: "border-border text-muted-foreground",
|
||||||
|
queued:
|
||||||
|
"border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
|
||||||
|
working: "border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-300",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RepositoryFormProps {
|
||||||
|
readonly busy: boolean;
|
||||||
|
readonly onChange: (value: string) => void;
|
||||||
|
readonly onSubmit: () => Promise<void>;
|
||||||
|
readonly value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RepositoryForm = ({
|
||||||
|
busy,
|
||||||
|
onChange,
|
||||||
|
onSubmit,
|
||||||
|
value,
|
||||||
|
}: RepositoryFormProps) => (
|
||||||
|
<form
|
||||||
|
className="grid gap-3 border-b p-4"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void onSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-xs font-medium" htmlFor="github-repository">
|
||||||
|
Public GitHub repository
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Connect with an owner/name slug or repository URL.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id="github-repository"
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
placeholder="rivet-dev/rivet"
|
||||||
|
required
|
||||||
|
value={value}
|
||||||
|
/>
|
||||||
|
<Button disabled={busy || value.trim().length === 0} type="submit">
|
||||||
|
<GitFork data-icon="inline-start" />
|
||||||
|
{busy ? "Connecting" : "Connect repository"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ProjectListProps {
|
||||||
|
readonly projects: readonly Doc<"projects">[] | undefined;
|
||||||
|
readonly selectedId: Id<"projects"> | null;
|
||||||
|
readonly onSelect: (projectId: Id<"projects">) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectList = ({ projects, selectedId, onSelect }: ProjectListProps) => {
|
||||||
|
const renderContent = () => {
|
||||||
|
if (projects === undefined) {
|
||||||
|
return (
|
||||||
|
<p className="p-3 text-xs text-muted-foreground">Loading projects...</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (projects.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="p-3 text-xs leading-relaxed text-muted-foreground">
|
||||||
|
Connect a repository to create its project artifacts and issue queue.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{projects.map((project) => (
|
||||||
|
<button
|
||||||
|
className={`w-full border px-3 py-2 text-left transition-colors ${
|
||||||
|
selectedId === project._id
|
||||||
|
? "border-foreground/30 bg-muted"
|
||||||
|
: "border-transparent hover:bg-muted/60"
|
||||||
|
}`}
|
||||||
|
key={project._id}
|
||||||
|
onClick={() => onSelect(project._id)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="block truncate text-sm font-medium">
|
||||||
|
{project.name}
|
||||||
|
</span>
|
||||||
|
<span className="block truncate text-xs text-muted-foreground">
|
||||||
|
{project.repoOwner}/{project.repoName}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
aria-label="Connected projects"
|
||||||
|
className="min-h-0 flex-1 overflow-y-auto p-2"
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ArtifactGridProps {
|
||||||
|
readonly artifacts: readonly Doc<"projectArtifacts">[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArtifactGrid = ({ artifacts }: ArtifactGridProps) => (
|
||||||
|
<section aria-labelledby="artifact-heading" className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold" id="artifact-heading">
|
||||||
|
Project artifacts
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Canonical context staged into every issue workspace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{artifacts === undefined ? (
|
||||||
|
<p className="text-sm text-muted-foreground">Loading artifacts...</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid gap-2 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{artifacts.map((artifact) => (
|
||||||
|
<article className="min-w-0 border bg-card p-3" key={artifact._id}>
|
||||||
|
<div className="mb-2 flex items-center justify-between gap-3">
|
||||||
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
|
<FileText className="size-4 shrink-0 text-muted-foreground" />
|
||||||
|
<h3 className="truncate font-mono text-xs font-medium">
|
||||||
|
{artifact.path}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 text-[11px] text-muted-foreground">
|
||||||
|
r{artifact.revision}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="line-clamp-4 whitespace-pre-wrap text-xs leading-relaxed text-muted-foreground">
|
||||||
|
{artifact.content}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IssueComposerProps {
|
||||||
|
readonly body: string;
|
||||||
|
readonly busy: boolean;
|
||||||
|
readonly onBodyChange: (value: string) => void;
|
||||||
|
readonly onSubmit: () => Promise<void>;
|
||||||
|
readonly onTitleChange: (value: string) => void;
|
||||||
|
readonly title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IssueComposer = ({
|
||||||
|
body,
|
||||||
|
busy,
|
||||||
|
onBodyChange,
|
||||||
|
onSubmit,
|
||||||
|
onTitleChange,
|
||||||
|
title,
|
||||||
|
}: IssueComposerProps) => (
|
||||||
|
<form
|
||||||
|
className="grid gap-3 border p-4"
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
void onSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold">Raise an issue</h2>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
The issue becomes the identity for one Flue agent and AgentOS workspace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-medium" htmlFor="issue-title">
|
||||||
|
Title
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="issue-title"
|
||||||
|
maxLength={160}
|
||||||
|
onChange={(event) => onTitleChange(event.target.value)}
|
||||||
|
required
|
||||||
|
value={title}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="text-xs font-medium" htmlFor="issue-body">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
id="issue-body"
|
||||||
|
maxLength={10_000}
|
||||||
|
onChange={(event) => onBodyChange(event.target.value)}
|
||||||
|
required
|
||||||
|
rows={5}
|
||||||
|
value={body}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
disabled={busy || title.trim().length < 3 || body.trim().length < 10}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<Plus data-icon="inline-start" />
|
||||||
|
{busy ? "Creating" : "Create issue"}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface IssueListProps {
|
||||||
|
readonly issues: readonly Doc<"projectIssues">[] | undefined;
|
||||||
|
readonly pendingAction: string | null;
|
||||||
|
readonly onStart: (
|
||||||
|
issueId: Id<"projectIssues">,
|
||||||
|
issueNumber: number,
|
||||||
|
title: string
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IssueList = ({ issues, pendingAction, onStart }: IssueListProps) => {
|
||||||
|
const renderContent = () => {
|
||||||
|
if (issues === undefined) {
|
||||||
|
return <p className="text-sm text-muted-foreground">Loading issues...</p>;
|
||||||
|
}
|
||||||
|
if (issues.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="border p-4 text-sm text-muted-foreground">
|
||||||
|
No issues yet. Describe a concrete change to start the workflow.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2">
|
||||||
|
{issues.map((issue) => {
|
||||||
|
const canStart = ["open", "needs-input", "failed"].includes(
|
||||||
|
issue.status
|
||||||
|
);
|
||||||
|
const busy = pendingAction === `issue:${issue._id}`;
|
||||||
|
let actionLabel: string = issue.status;
|
||||||
|
if (canStart) {
|
||||||
|
actionLabel = "Start agent";
|
||||||
|
}
|
||||||
|
if (busy) {
|
||||||
|
actionLabel = "Dispatching";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className="grid gap-3 border bg-card p-4 md:grid-cols-[1fr_auto] md:items-center"
|
||||||
|
key={issue._id}
|
||||||
|
>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="mb-1 flex flex-wrap items-center gap-2">
|
||||||
|
<span className="font-mono text-xs text-muted-foreground">
|
||||||
|
#{issue.number}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`border px-1.5 py-0.5 text-[11px] font-medium ${statusStyle[issue.status]}`}
|
||||||
|
>
|
||||||
|
{issue.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-medium">{issue.title}</h3>
|
||||||
|
<p className="mt-1 line-clamp-2 text-sm text-muted-foreground">
|
||||||
|
{issue.body}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
disabled={!canStart || busy}
|
||||||
|
onClick={() =>
|
||||||
|
void onStart(issue._id, issue.number, issue.title)
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant={canStart ? "default" : "outline"}
|
||||||
|
>
|
||||||
|
{canStart ? (
|
||||||
|
<Play data-icon="inline-start" />
|
||||||
|
) : (
|
||||||
|
<Bot data-icon="inline-start" />
|
||||||
|
)}
|
||||||
|
{actionLabel}
|
||||||
|
</Button>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section aria-labelledby="issue-heading" className="space-y-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<GitPullRequest className="size-4 text-muted-foreground" />
|
||||||
|
<h2 className="text-lg font-semibold" id="issue-heading">
|
||||||
|
Issue queue
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
{renderContent()}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ProjectWorkspacePage = () => {
|
||||||
|
const workspace = useProjectWorkspace();
|
||||||
|
const handleRepositoryChange = workspace.setRepository;
|
||||||
|
const handleRepositorySubmit = workspace.connectRepository;
|
||||||
|
const handleProjectSelect = workspace.setSelectedProjectId;
|
||||||
|
const handleIssueBodyChange = workspace.setIssueBody;
|
||||||
|
const handleIssueSubmit = workspace.raiseIssue;
|
||||||
|
const handleIssueTitleChange = workspace.setIssueTitle;
|
||||||
|
const handleIssueStart = workspace.startIssue;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-svh bg-background text-foreground">
|
||||||
|
<header className="border-b">
|
||||||
|
<div className="mx-auto flex max-w-[1400px] items-center justify-between px-4 py-3 md:px-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="grid size-8 place-items-center border bg-muted">
|
||||||
|
<Bot className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold">Project manager</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
GitHub, Flue, AgentOS
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<UserMenu />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="mx-auto grid min-h-[calc(100svh-57px)] max-w-[1400px] md:grid-cols-[260px_1fr]">
|
||||||
|
<aside className="flex min-h-0 flex-col border-b md:border-r md:border-b-0">
|
||||||
|
<RepositoryForm
|
||||||
|
busy={workspace.pendingAction === "connect"}
|
||||||
|
onChange={handleRepositoryChange}
|
||||||
|
onSubmit={handleRepositorySubmit}
|
||||||
|
value={workspace.repository}
|
||||||
|
/>
|
||||||
|
<ProjectList
|
||||||
|
onSelect={handleProjectSelect}
|
||||||
|
projects={workspace.projects}
|
||||||
|
selectedId={workspace.selectedProjectId}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="min-w-0 p-4 md:p-6 lg:p-8">
|
||||||
|
{workspace.error ? (
|
||||||
|
<div className="mb-4 border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive">
|
||||||
|
{workspace.error}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{workspace.selectedProject ? (
|
||||||
|
<div className="grid gap-8">
|
||||||
|
<div className="flex flex-col gap-3 border-b pb-5 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Connected GitHub project
|
||||||
|
</p>
|
||||||
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
|
{workspace.selectedProject.repoOwner}/
|
||||||
|
{workspace.selectedProject.repoName}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
||||||
|
{workspace.selectedProject.description ??
|
||||||
|
"No repository description is available."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
window.open(
|
||||||
|
workspace.selectedProject?.repoUrl,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<ExternalLink data-icon="inline-start" />
|
||||||
|
Open GitHub
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ArtifactGrid artifacts={workspace.artifacts} />
|
||||||
|
|
||||||
|
<div className="grid items-start gap-5 lg:grid-cols-[minmax(280px,380px)_1fr]">
|
||||||
|
<IssueComposer
|
||||||
|
body={workspace.issueBody}
|
||||||
|
busy={workspace.pendingAction === "issue"}
|
||||||
|
onBodyChange={handleIssueBodyChange}
|
||||||
|
onSubmit={handleIssueSubmit}
|
||||||
|
onTitleChange={handleIssueTitleChange}
|
||||||
|
title={workspace.issueTitle}
|
||||||
|
/>
|
||||||
|
<IssueList
|
||||||
|
issues={workspace.issues}
|
||||||
|
onStart={handleIssueStart}
|
||||||
|
pendingAction={workspace.pendingAction}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid min-h-[60svh] place-items-center text-center">
|
||||||
|
<div className="max-w-sm space-y-2">
|
||||||
|
<GitFork className="mx-auto size-7 text-muted-foreground" />
|
||||||
|
<h1 className="text-xl font-semibold">
|
||||||
|
Connect your first repository
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Zopu creates the project artifact set and opens an issue queue
|
||||||
|
for agent work.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { Button } from "@code/ui/components/button";
|
|
||||||
import { Input } from "@code/ui/components/input";
|
|
||||||
import { Label } from "@code/ui/components/label";
|
|
||||||
import { useForm } from "@tanstack/react-form";
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
export default function SignInForm({ onSwitchToSignUp }: { onSwitchToSignUp: () => void }) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
onSubmit: async ({ value }) => {
|
|
||||||
await authClient.signIn.email(
|
|
||||||
{
|
|
||||||
email: value.email,
|
|
||||||
password: value.password,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
navigate("/dashboard");
|
|
||||||
toast.success("Sign in successful");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.error.message || error.error.statusText);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onSubmit: z.object({
|
|
||||||
email: z.email("Invalid email address"),
|
|
||||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto mt-10 w-full max-w-md p-6">
|
|
||||||
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
form.handleSubmit();
|
|
||||||
}}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<form.Field name="email">
|
|
||||||
{(field) => (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={field.name}>Email</Label>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type="email"
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
{field.state.meta.errors.map((error, index) => (
|
|
||||||
<p key={`${field.name}-error-${index}`} className="text-red-500">
|
|
||||||
{error?.message}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<form.Field name="password">
|
|
||||||
{(field) => (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={field.name}>Password</Label>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type="password"
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
{field.state.meta.errors.map((error, index) => (
|
|
||||||
<p key={`${field.name}-error-${index}`} className="text-red-500">
|
|
||||||
{error?.message}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form.Subscribe
|
|
||||||
selector={(state) => ({
|
|
||||||
canSubmit: state.canSubmit,
|
|
||||||
isSubmitting: state.isSubmitting,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{({ canSubmit, isSubmitting }) => (
|
|
||||||
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
|
|
||||||
{isSubmitting ? "Submitting..." : "Sign In"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</form.Subscribe>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={onSwitchToSignUp}
|
|
||||||
className="text-indigo-600 hover:text-indigo-800"
|
|
||||||
>
|
|
||||||
Need an account? Sign Up
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
import { Button } from "@code/ui/components/button";
|
|
||||||
import { Input } from "@code/ui/components/input";
|
|
||||||
import { Label } from "@code/ui/components/label";
|
|
||||||
import { useForm } from "@tanstack/react-form";
|
|
||||||
import { useNavigate } from "react-router";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
export default function SignUpForm({ onSwitchToSignIn }: { onSwitchToSignIn: () => void }) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const form = useForm({
|
|
||||||
defaultValues: {
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
name: "",
|
|
||||||
},
|
|
||||||
onSubmit: async ({ value }) => {
|
|
||||||
await authClient.signUp.email(
|
|
||||||
{
|
|
||||||
email: value.email,
|
|
||||||
password: value.password,
|
|
||||||
name: value.name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess: () => {
|
|
||||||
navigate("/dashboard");
|
|
||||||
toast.success("Sign up successful");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast.error(error.error.message || error.error.statusText);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
validators: {
|
|
||||||
onSubmit: z.object({
|
|
||||||
name: z.string().min(2, "Name must be at least 2 characters"),
|
|
||||||
email: z.email("Invalid email address"),
|
|
||||||
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mx-auto mt-10 w-full max-w-md p-6">
|
|
||||||
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
|
|
||||||
|
|
||||||
<form
|
|
||||||
onSubmit={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
form.handleSubmit();
|
|
||||||
}}
|
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<form.Field name="name">
|
|
||||||
{(field) => (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={field.name}>Name</Label>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
{field.state.meta.errors.map((error, index) => (
|
|
||||||
<p key={`${field.name}-error-${index}`} className="text-red-500">
|
|
||||||
{error?.message}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<form.Field name="email">
|
|
||||||
{(field) => (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={field.name}>Email</Label>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type="email"
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
{field.state.meta.errors.map((error, index) => (
|
|
||||||
<p key={`${field.name}-error-${index}`} className="text-red-500">
|
|
||||||
{error?.message}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<form.Field name="password">
|
|
||||||
{(field) => (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor={field.name}>Password</Label>
|
|
||||||
<Input
|
|
||||||
id={field.name}
|
|
||||||
name={field.name}
|
|
||||||
type="password"
|
|
||||||
value={field.state.value}
|
|
||||||
onBlur={field.handleBlur}
|
|
||||||
onChange={(e) => field.handleChange(e.target.value)}
|
|
||||||
/>
|
|
||||||
{field.state.meta.errors.map((error, index) => (
|
|
||||||
<p key={`${field.name}-error-${index}`} className="text-red-500">
|
|
||||||
{error?.message}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</form.Field>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form.Subscribe
|
|
||||||
selector={(state) => ({
|
|
||||||
canSubmit: state.canSubmit,
|
|
||||||
isSubmitting: state.isSubmitting,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{({ canSubmit, isSubmitting }) => (
|
|
||||||
<Button type="submit" className="w-full" disabled={!canSubmit || isSubmitting}>
|
|
||||||
{isSubmitting ? "Submitting..." : "Sign Up"}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</form.Subscribe>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
|
||||||
<Button
|
|
||||||
variant="link"
|
|
||||||
onClick={onSwitchToSignIn}
|
|
||||||
className="text-indigo-600 hover:text-indigo-800"
|
|
||||||
>
|
|
||||||
Already have an account? Sign In
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authClient } from "@code/auth/web";
|
||||||
import { api } from "@code/backend/convex/_generated/api";
|
import { api } from "@code/backend/convex/_generated/api";
|
||||||
import { Button } from "@code/ui/components/button";
|
import { Button } from "@code/ui/components/button";
|
||||||
import {
|
import {
|
||||||
@@ -12,15 +13,15 @@ import {
|
|||||||
import { useQuery } from "convex/react";
|
import { useQuery } from "convex/react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
export default function UserMenu() {
|
export default function UserMenu() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useQuery(api.auth.getCurrentUser);
|
const user = useQuery(api.auth.getCurrentUser);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger render={<Button variant="outline" />}>{user?.name}</DropdownMenuTrigger>
|
<DropdownMenuTrigger render={<Button variant="outline" />}>
|
||||||
|
{user?.name}
|
||||||
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="bg-card">
|
<DropdownMenuContent className="bg-card">
|
||||||
<DropdownMenuGroup>
|
<DropdownMenuGroup>
|
||||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||||
@@ -32,7 +33,7 @@ export default function UserMenu() {
|
|||||||
authClient.signOut({
|
authClient.signOut({
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
navigate("/dashboard");
|
navigate("/login", { replace: true });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
113
apps/web/src/hooks/use-project-workspace.ts
Normal file
113
apps/web/src/hooks/use-project-workspace.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { api } from "@code/backend/convex/_generated/api";
|
||||||
|
import type { Id } from "@code/backend/convex/_generated/dataModel";
|
||||||
|
import { useAction, useMutation, useQuery } from "convex/react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { flueClient } from "@/root";
|
||||||
|
|
||||||
|
const errorMessage = (error: unknown) =>
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
export const useProjectWorkspace = () => {
|
||||||
|
const projects = useQuery(api.projects.list);
|
||||||
|
const [selectedProjectId, setSelectedProjectId] =
|
||||||
|
useState<Id<"projects"> | null>(null);
|
||||||
|
const [repository, setRepository] = useState("");
|
||||||
|
const [issueTitle, setIssueTitle] = useState("");
|
||||||
|
const [issueBody, setIssueBody] = useState("");
|
||||||
|
const [pendingAction, setPendingAction] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const connectGitHub = useAction(api.projects.connectGitHub);
|
||||||
|
const createIssue = useMutation(api.projectIssues.create);
|
||||||
|
const beginIssue = useMutation(api.projectIssues.begin);
|
||||||
|
const markDispatchFailed = useMutation(api.projectIssues.markDispatchFailed);
|
||||||
|
const activeProjectId = selectedProjectId ?? projects?.[0]?._id ?? null;
|
||||||
|
const artifacts = useQuery(
|
||||||
|
api.projectArtifacts.list,
|
||||||
|
activeProjectId ? { projectId: activeProjectId } : "skip"
|
||||||
|
);
|
||||||
|
const issues = useQuery(
|
||||||
|
api.projectIssues.list,
|
||||||
|
activeProjectId ? { projectId: activeProjectId } : "skip"
|
||||||
|
);
|
||||||
|
|
||||||
|
const connectRepository = async () => {
|
||||||
|
setPendingAction("connect");
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const projectId = await connectGitHub({ repository });
|
||||||
|
setSelectedProjectId(projectId);
|
||||||
|
setRepository("");
|
||||||
|
} catch (caughtError) {
|
||||||
|
setError(errorMessage(caughtError));
|
||||||
|
} finally {
|
||||||
|
setPendingAction(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const raiseIssue = async () => {
|
||||||
|
if (!activeProjectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPendingAction("issue");
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await createIssue({
|
||||||
|
body: issueBody,
|
||||||
|
projectId: activeProjectId,
|
||||||
|
title: issueTitle,
|
||||||
|
});
|
||||||
|
setIssueTitle("");
|
||||||
|
setIssueBody("");
|
||||||
|
} catch (caughtError) {
|
||||||
|
setError(errorMessage(caughtError));
|
||||||
|
} finally {
|
||||||
|
setPendingAction(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startIssue = async (
|
||||||
|
issueId: Id<"projectIssues">,
|
||||||
|
issueNumber: number,
|
||||||
|
title: string
|
||||||
|
) => {
|
||||||
|
const actionKey = `issue:${issueId}`;
|
||||||
|
setPendingAction(actionKey);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await beginIssue({ issueId });
|
||||||
|
await flueClient.agents.send("project-manager", String(issueId), {
|
||||||
|
message: `Start project issue ${issueNumber}: ${title}. Read the bound context and complete the workflow.`,
|
||||||
|
});
|
||||||
|
} catch (caughtError) {
|
||||||
|
const message = errorMessage(caughtError);
|
||||||
|
await markDispatchFailed({ error: message, issueId });
|
||||||
|
setError(message);
|
||||||
|
} finally {
|
||||||
|
setPendingAction(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedProject =
|
||||||
|
projects?.find((project) => project._id === activeProjectId) ?? null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
artifacts,
|
||||||
|
connectRepository,
|
||||||
|
error,
|
||||||
|
issueBody,
|
||||||
|
issueTitle,
|
||||||
|
issues,
|
||||||
|
pendingAction,
|
||||||
|
projects,
|
||||||
|
raiseIssue,
|
||||||
|
repository,
|
||||||
|
selectedProject,
|
||||||
|
selectedProjectId: activeProjectId,
|
||||||
|
setIssueBody,
|
||||||
|
setIssueTitle,
|
||||||
|
setRepository,
|
||||||
|
setSelectedProjectId,
|
||||||
|
startIssue,
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
|
import { WebAuthProvider } from "@code/auth/web";
|
||||||
import { env } from "@code/env/web";
|
import { env } from "@code/env/web";
|
||||||
import { Toaster } from "@code/ui/components/sonner";
|
import { Toaster } from "@code/ui/components/sonner";
|
||||||
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
|
|
||||||
import { FlueProvider } from "@flue/react";
|
import { FlueProvider } from "@flue/react";
|
||||||
|
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
import { createFlueClient } from "@flue/sdk";
|
import { createFlueClient } from "@flue/sdk";
|
||||||
import { ConvexReactClient } from "convex/react";
|
|
||||||
import {
|
import {
|
||||||
isRouteErrorResponse,
|
isRouteErrorResponse,
|
||||||
Links,
|
Links,
|
||||||
@@ -15,8 +14,6 @@ import {
|
|||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
} from "react-router";
|
} from "react-router";
|
||||||
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
import type { Route } from "./+types/root";
|
import type { Route } from "./+types/root";
|
||||||
import { ThemeProvider } from "./components/theme-provider";
|
import { ThemeProvider } from "./components/theme-provider";
|
||||||
|
|
||||||
@@ -67,7 +64,7 @@ const flueFetch: typeof fetch = async (input, init) => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const flueClient = createFlueClient({
|
export const flueClient = createFlueClient({
|
||||||
baseUrl: flueBaseUrl.toString(),
|
baseUrl: flueBaseUrl.toString(),
|
||||||
fetch: flueFetch,
|
fetch: flueFetch,
|
||||||
});
|
});
|
||||||
@@ -87,26 +84,23 @@ export const Layout = ({ children }: { children: React.ReactNode }) => (
|
|||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
||||||
const App = () => {
|
const App = () => (
|
||||||
const convex = new ConvexReactClient(env.VITE_CONVEX_URL);
|
<FlueProvider client={flueClient}>
|
||||||
return (
|
<WebAuthProvider>
|
||||||
<FlueProvider client={flueClient}>
|
<ThemeProvider
|
||||||
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
|
attribute="class"
|
||||||
<ThemeProvider
|
defaultTheme="dark"
|
||||||
attribute="class"
|
disableTransitionOnChange
|
||||||
defaultTheme="dark"
|
storageKey="vite-ui-theme"
|
||||||
disableTransitionOnChange
|
>
|
||||||
storageKey="vite-ui-theme"
|
<div className="h-svh min-h-0 overflow-hidden">
|
||||||
>
|
<Outlet />
|
||||||
<div className="h-svh min-h-0 overflow-hidden">
|
</div>
|
||||||
<Outlet />
|
<Toaster richColors />
|
||||||
</div>
|
</ThemeProvider>
|
||||||
<Toaster richColors />
|
</WebAuthProvider>
|
||||||
</ThemeProvider>
|
</FlueProvider>
|
||||||
</ConvexBetterAuthProvider>
|
);
|
||||||
</FlueProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ChatPage } from "@/components/chat/chat-page";
|
import { ChatPage } from "@/components/chat/chat-page";
|
||||||
|
|
||||||
import type { Route } from "./+types/_index";
|
import type { Route } from "./+types/_app._index";
|
||||||
|
|
||||||
export const meta = (_args: Route.MetaArgs) => [
|
export const meta = (_args: Route.MetaArgs) => [
|
||||||
{ title: "Zopu" },
|
{ title: "Zopu" },
|
||||||
5
apps/web/src/routes/_app.dashboard.tsx
Normal file
5
apps/web/src/routes/_app.dashboard.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ProjectWorkspacePage } from "@/components/projects/project-workspace-page";
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
return <ProjectWorkspacePage />;
|
||||||
|
}
|
||||||
17
apps/web/src/routes/_app.tsx
Normal file
17
apps/web/src/routes/_app.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { useConvexAuth } from "convex/react";
|
||||||
|
import { Navigate, Outlet, useLocation } from "react-router";
|
||||||
|
|
||||||
|
export default function AppLayout() {
|
||||||
|
const { isAuthenticated, isLoading } = useConvexAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="grid min-h-svh place-items-center text-sm text-muted-foreground">Loading…</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate replace state={{ from: location.pathname }} to="/login" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
15
apps/web/src/routes/_auth.login.tsx
Normal file
15
apps/web/src/routes/_auth.login.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { LoginForm } from "@code/auth/web";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
import type { Route } from "./+types/_auth.login";
|
||||||
|
|
||||||
|
export const meta = (_args: Route.MetaArgs) => [
|
||||||
|
{ title: "Sign in | Zopu" },
|
||||||
|
{ content: "Sign in to Zopu", name: "description" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function LoginRoute() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return <LoginForm onSuccess={() => navigate("/", { replace: true })} />;
|
||||||
|
}
|
||||||
15
apps/web/src/routes/_auth.signup.tsx
Normal file
15
apps/web/src/routes/_auth.signup.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { SignupForm } from "@code/auth/web";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
|
||||||
|
import type { Route } from "./+types/_auth.signup";
|
||||||
|
|
||||||
|
export const meta = (_args: Route.MetaArgs) => [
|
||||||
|
{ title: "Create account | Zopu" },
|
||||||
|
{ content: "Create your Zopu account", name: "description" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SignupRoute() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return <SignupForm onSuccess={() => navigate("/", { replace: true })} />;
|
||||||
|
}
|
||||||
20
apps/web/src/routes/_auth.tsx
Normal file
20
apps/web/src/routes/_auth.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { useConvexAuth } from "convex/react";
|
||||||
|
import { Navigate, Outlet } from "react-router";
|
||||||
|
|
||||||
|
export default function AuthLayout() {
|
||||||
|
const { isAuthenticated, isLoading } = useConvexAuth();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div className="grid min-h-svh place-items-center text-sm text-muted-foreground">Loading…</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthenticated) return <Navigate replace to="/" />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="grid min-h-svh place-items-center bg-muted/30 p-6 md:p-10">
|
||||||
|
<div className="w-full max-w-sm">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { api } from "@code/backend/convex/_generated/api";
|
|
||||||
import { Authenticated, AuthLoading, Unauthenticated, useQuery } from "convex/react";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import SignInForm from "@/components/sign-in-form";
|
|
||||||
import SignUpForm from "@/components/sign-up-form";
|
|
||||||
import UserMenu from "@/components/user-menu";
|
|
||||||
|
|
||||||
function PrivateDashboardContent() {
|
|
||||||
const privateData = useQuery(api.privateData.get);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Dashboard</h1>
|
|
||||||
<p>privateData: {privateData?.message}</p>
|
|
||||||
<UserMenu />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Dashboard() {
|
|
||||||
const [showSignIn, setShowSignIn] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Authenticated>
|
|
||||||
<PrivateDashboardContent />
|
|
||||||
</Authenticated>
|
|
||||||
<Unauthenticated>
|
|
||||||
{showSignIn ? (
|
|
||||||
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
|
|
||||||
) : (
|
|
||||||
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
|
|
||||||
)}
|
|
||||||
</Unauthenticated>
|
|
||||||
<AuthLoading>
|
|
||||||
<div>Loading...</div>
|
|
||||||
</AuthLoading>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
282
bun.lock
282
bun.lock
@@ -52,24 +52,18 @@
|
|||||||
"name": "native",
|
"name": "native",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-auth/expo": "catalog:",
|
"@code/auth": "workspace:*",
|
||||||
"@code/backend": "workspace:*",
|
"@code/backend": "workspace:*",
|
||||||
"@code/env": "workspace:*",
|
|
||||||
"@convex-dev/better-auth": "catalog:",
|
|
||||||
"@expo/metro-runtime": "~57.0.2",
|
"@expo/metro-runtime": "~57.0.2",
|
||||||
"@expo/vector-icons": "^15.1.1",
|
"@expo/vector-icons": "^15.1.1",
|
||||||
"@gorhom/bottom-sheet": "^5.2.14",
|
"@gorhom/bottom-sheet": "^5.2.14",
|
||||||
"@tanstack/react-form": "catalog:",
|
|
||||||
"better-auth": "catalog:",
|
|
||||||
"convex": "catalog:",
|
"convex": "catalog:",
|
||||||
"expo": "~57.0.1",
|
"expo": "~57.0.1",
|
||||||
"expo-constants": "~57.0.2",
|
|
||||||
"expo-font": "~57.0.0",
|
"expo-font": "~57.0.0",
|
||||||
"expo-haptics": "~57.0.0",
|
"expo-haptics": "~57.0.0",
|
||||||
"expo-linking": "~57.0.1",
|
"expo-linking": "~57.0.1",
|
||||||
"expo-network": "~57.0.0",
|
"expo-network": "~57.0.0",
|
||||||
"expo-router": "~57.0.2",
|
"expo-router": "~57.0.2",
|
||||||
"expo-secure-store": "~57.0.0",
|
|
||||||
"expo-status-bar": "~57.0.0",
|
"expo-status-bar": "~57.0.0",
|
||||||
"expo-web-browser": "~57.0.0",
|
"expo-web-browser": "~57.0.0",
|
||||||
"heroui-native": "^1.0.5",
|
"heroui-native": "^1.0.5",
|
||||||
@@ -88,7 +82,6 @@
|
|||||||
"tailwind-variants": "^3.2.2",
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwindcss": "catalog:",
|
"tailwindcss": "catalog:",
|
||||||
"uniwind": "^1.10.0",
|
"uniwind": "^1.10.0",
|
||||||
"zod": "catalog:",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@code/config": "workspace:*",
|
"@code/config": "workspace:*",
|
||||||
@@ -114,17 +107,15 @@
|
|||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "web",
|
"name": "web",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@code/auth": "workspace:*",
|
||||||
"@code/backend": "workspace:*",
|
"@code/backend": "workspace:*",
|
||||||
"@code/env": "workspace:*",
|
"@code/env": "workspace:*",
|
||||||
"@code/ui": "workspace:*",
|
"@code/ui": "workspace:*",
|
||||||
"@convex-dev/better-auth": "catalog:",
|
|
||||||
"@flue/react": "1.0.0-beta.9",
|
"@flue/react": "1.0.0-beta.9",
|
||||||
"@flue/sdk": "1.0.0-beta.9",
|
"@flue/sdk": "1.0.0-beta.9",
|
||||||
"@react-router/fs-routes": "^8.1.0",
|
"@react-router/fs-routes": "^8.1.0",
|
||||||
"@react-router/node": "^8.1.0",
|
"@react-router/node": "^8.1.0",
|
||||||
"@react-router/serve": "^8.1.0",
|
"@react-router/serve": "^8.1.0",
|
||||||
"@tanstack/react-form": "catalog:",
|
|
||||||
"better-auth": "catalog:",
|
|
||||||
"convex": "catalog:",
|
"convex": "catalog:",
|
||||||
"isbot": "^5.1.44",
|
"isbot": "^5.1.44",
|
||||||
"lucide-react": "catalog:",
|
"lucide-react": "catalog:",
|
||||||
@@ -134,7 +125,6 @@
|
|||||||
"react-router": "^8.1.0",
|
"react-router": "^8.1.0",
|
||||||
"sonner": "catalog:",
|
"sonner": "catalog:",
|
||||||
"streamdown": "2.5.0",
|
"streamdown": "2.5.0",
|
||||||
"zod": "catalog:",
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@code/config": "workspace:*",
|
"@code/config": "workspace:*",
|
||||||
@@ -154,17 +144,47 @@
|
|||||||
"name": "@code/agents",
|
"name": "@code/agents",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@code/backend": "workspace:*",
|
||||||
"@code/env": "workspace:*",
|
"@code/env": "workspace:*",
|
||||||
"@flue/runtime": "latest",
|
"@flue/runtime": "latest",
|
||||||
"convex": "catalog:",
|
"convex": "catalog:",
|
||||||
"hono": "^4.12.30",
|
"hono": "^4.12.30",
|
||||||
|
"valibot": "^1.4.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@code/config": "workspace:*",
|
"@code/config": "workspace:*",
|
||||||
"@flue/cli": "latest",
|
"@flue/cli": "latest",
|
||||||
|
"@types/bun": "latest",
|
||||||
"typescript": "^6",
|
"typescript": "^6",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"packages/auth": {
|
||||||
|
"name": "@code/auth",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@better-auth/expo": "catalog:",
|
||||||
|
"@code/backend": "workspace:*",
|
||||||
|
"@code/env": "workspace:*",
|
||||||
|
"@code/ui": "workspace:*",
|
||||||
|
"@convex-dev/better-auth": "catalog:",
|
||||||
|
"@tanstack/react-form": "catalog:",
|
||||||
|
"better-auth": "catalog:",
|
||||||
|
"convex": "catalog:",
|
||||||
|
"expo-constants": "~57.0.2",
|
||||||
|
"expo-secure-store": "~57.0.0",
|
||||||
|
"heroui-native": "^1.0.5",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-native": "0.86.0",
|
||||||
|
"sonner": "catalog:",
|
||||||
|
"zod": "catalog:",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@code/config": "workspace:*",
|
||||||
|
"@types/react": "~19.2.17",
|
||||||
|
"typescript": "^6",
|
||||||
|
"vite": "^7.3.6",
|
||||||
|
},
|
||||||
|
},
|
||||||
"packages/backend": {
|
"packages/backend": {
|
||||||
"name": "@code/backend",
|
"name": "@code/backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -572,6 +592,8 @@
|
|||||||
|
|
||||||
"@code/agents": ["@code/agents@workspace:packages/agents"],
|
"@code/agents": ["@code/agents@workspace:packages/agents"],
|
||||||
|
|
||||||
|
"@code/auth": ["@code/auth@workspace:packages/auth"],
|
||||||
|
|
||||||
"@code/backend": ["@code/backend@workspace:packages/backend"],
|
"@code/backend": ["@code/backend@workspace:packages/backend"],
|
||||||
|
|
||||||
"@code/config": ["@code/config@workspace:packages/config"],
|
"@code/config": ["@code/config@workspace:packages/config"],
|
||||||
@@ -1784,7 +1806,7 @@
|
|||||||
|
|
||||||
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
|
||||||
|
|
||||||
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
||||||
|
|
||||||
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
"clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="],
|
||||||
|
|
||||||
@@ -2508,29 +2530,29 @@
|
|||||||
|
|
||||||
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
|
"lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||||
|
|
||||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||||
|
|
||||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||||
|
|
||||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||||
|
|
||||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||||
|
|
||||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||||
|
|
||||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||||
|
|
||||||
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
|
||||||
|
|
||||||
@@ -3218,7 +3240,7 @@
|
|||||||
|
|
||||||
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||||
|
|
||||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
|
|
||||||
@@ -3532,9 +3554,9 @@
|
|||||||
|
|
||||||
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
"yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="],
|
||||||
|
|
||||||
"yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
|
"yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
|
||||||
|
|
||||||
"yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
||||||
|
|
||||||
@@ -3614,8 +3636,6 @@
|
|||||||
|
|
||||||
"@expo/metro-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"@expo/metro-config/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
|
||||||
|
|
||||||
"@expo/package-manager/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"@expo/package-manager/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
"@expo/package-manager/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="],
|
"@expo/package-manager/ora": ["ora@3.4.0", "", { "dependencies": { "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-spinners": "^2.0.0", "log-symbols": "^2.2.0", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1" } }, "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg=="],
|
||||||
@@ -3648,8 +3668,6 @@
|
|||||||
|
|
||||||
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
"@opentui/core/diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||||
|
|
||||||
"@react-native/codegen/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
|
|
||||||
|
|
||||||
"@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
"@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="],
|
||||||
|
|
||||||
"@react-native/dev-middleware/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
"@react-native/dev-middleware/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="],
|
||||||
@@ -3662,8 +3680,6 @@
|
|||||||
|
|
||||||
"@rivetkit/react/@tanstack/react-store": ["@tanstack/react-store@0.7.7", "", { "dependencies": { "@tanstack/store": "0.7.7", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg=="],
|
"@rivetkit/react/@tanstack/react-store": ["@tanstack/react-store@0.7.7", "", { "dependencies": { "@tanstack/store": "0.7.7", "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg=="],
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||||
@@ -3692,8 +3708,6 @@
|
|||||||
|
|
||||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
|
||||||
|
|
||||||
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||||
|
|
||||||
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
"accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
|
||||||
@@ -3720,10 +3734,14 @@
|
|||||||
|
|
||||||
"cli-highlight/yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="],
|
"cli-highlight/yargs": ["yargs@16.2.2", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w=="],
|
||||||
|
|
||||||
"cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
|
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
|
"concurrently/yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="],
|
||||||
|
|
||||||
"conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
"conf/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="],
|
||||||
|
|
||||||
"conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="],
|
"conf/json-schema-typed": ["json-schema-typed@7.0.3", "", {}, "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A=="],
|
||||||
@@ -3732,6 +3750,8 @@
|
|||||||
|
|
||||||
"connect/finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="],
|
"connect/finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="],
|
||||||
|
|
||||||
|
"css-tree/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
|
"cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="],
|
||||||
|
|
||||||
"d3/d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
"d3/d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="],
|
||||||
@@ -3756,6 +3776,8 @@
|
|||||||
|
|
||||||
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"enquirer/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
"eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
|
||||||
|
|
||||||
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||||
@@ -3828,18 +3850,10 @@
|
|||||||
|
|
||||||
"metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="],
|
"metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="],
|
||||||
|
|
||||||
"metro/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
|
||||||
|
|
||||||
"metro/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="],
|
"metro/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="],
|
||||||
|
|
||||||
"metro/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
|
|
||||||
|
|
||||||
"metro-babel-transformer/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="],
|
"metro-babel-transformer/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="],
|
||||||
|
|
||||||
"metro-source-map/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
|
||||||
|
|
||||||
"metro-symbolicate/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="],
|
|
||||||
|
|
||||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"morgan/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
@@ -3900,12 +3914,12 @@
|
|||||||
|
|
||||||
"react-native/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="],
|
"react-native/ws": ["ws@7.5.13", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA=="],
|
||||||
|
|
||||||
"react-native/yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="],
|
|
||||||
|
|
||||||
"react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="],
|
"react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="],
|
||||||
|
|
||||||
"react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
|
"react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="],
|
||||||
|
|
||||||
|
"recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||||
|
|
||||||
"rivetkit/uuid": ["uuid@12.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-9obBF8sMIHJWNQaO6IGOG8giGa/jUpKX34bz6o4whVs8M0WAvhID2tNxYp6A2XEBJPuZSX8wsS/6TEKfIDc+nw=="],
|
"rivetkit/uuid": ["uuid@12.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-9obBF8sMIHJWNQaO6IGOG8giGa/jUpKX34bz6o4whVs8M0WAvhID2tNxYp6A2XEBJPuZSX8wsS/6TEKfIDc+nw=="],
|
||||||
@@ -3928,6 +3942,8 @@
|
|||||||
|
|
||||||
"simple-swizzle/is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
|
"simple-swizzle/is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
|
||||||
|
|
||||||
|
"source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"stacktrace-parser/type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
|
"stacktrace-parser/type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="],
|
||||||
|
|
||||||
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
|
||||||
@@ -3942,7 +3958,7 @@
|
|||||||
|
|
||||||
"uniwind/@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="],
|
"uniwind/@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="],
|
||||||
|
|
||||||
"vite/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
"uniwind/lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||||
|
|
||||||
"vite-plus/oxfmt": ["oxfmt@0.57.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.57.0", "@oxfmt/binding-android-arm64": "0.57.0", "@oxfmt/binding-darwin-arm64": "0.57.0", "@oxfmt/binding-darwin-x64": "0.57.0", "@oxfmt/binding-freebsd-x64": "0.57.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.57.0", "@oxfmt/binding-linux-arm-musleabihf": "0.57.0", "@oxfmt/binding-linux-arm64-gnu": "0.57.0", "@oxfmt/binding-linux-arm64-musl": "0.57.0", "@oxfmt/binding-linux-ppc64-gnu": "0.57.0", "@oxfmt/binding-linux-riscv64-gnu": "0.57.0", "@oxfmt/binding-linux-riscv64-musl": "0.57.0", "@oxfmt/binding-linux-s390x-gnu": "0.57.0", "@oxfmt/binding-linux-x64-gnu": "0.57.0", "@oxfmt/binding-linux-x64-musl": "0.57.0", "@oxfmt/binding-openharmony-arm64": "0.57.0", "@oxfmt/binding-win32-arm64-msvc": "0.57.0", "@oxfmt/binding-win32-ia32-msvc": "0.57.0", "@oxfmt/binding-win32-x64-msvc": "0.57.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-ZB7Bi+rGDSqmVIo9jwcLyFgjxXvQhDdU+jx+ZrVy6VRiVXK2+CHc4hO3J4dUQjHe7V0ymHB+MDuv5z+NhK07HA=="],
|
"vite-plus/oxfmt": ["oxfmt@0.57.0", "", { "dependencies": { "tinypool": "2.1.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.57.0", "@oxfmt/binding-android-arm64": "0.57.0", "@oxfmt/binding-darwin-arm64": "0.57.0", "@oxfmt/binding-darwin-x64": "0.57.0", "@oxfmt/binding-freebsd-x64": "0.57.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.57.0", "@oxfmt/binding-linux-arm-musleabihf": "0.57.0", "@oxfmt/binding-linux-arm64-gnu": "0.57.0", "@oxfmt/binding-linux-arm64-musl": "0.57.0", "@oxfmt/binding-linux-ppc64-gnu": "0.57.0", "@oxfmt/binding-linux-riscv64-gnu": "0.57.0", "@oxfmt/binding-linux-riscv64-musl": "0.57.0", "@oxfmt/binding-linux-s390x-gnu": "0.57.0", "@oxfmt/binding-linux-x64-gnu": "0.57.0", "@oxfmt/binding-linux-x64-musl": "0.57.0", "@oxfmt/binding-openharmony-arm64": "0.57.0", "@oxfmt/binding-win32-arm64-msvc": "0.57.0", "@oxfmt/binding-win32-ia32-msvc": "0.57.0", "@oxfmt/binding-win32-x64-msvc": "0.57.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-ZB7Bi+rGDSqmVIo9jwcLyFgjxXvQhDdU+jx+ZrVy6VRiVXK2+CHc4hO3J4dUQjHe7V0ymHB+MDuv5z+NhK07HA=="],
|
||||||
|
|
||||||
@@ -3962,6 +3978,8 @@
|
|||||||
|
|
||||||
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
"xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||||
|
|
||||||
|
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"youch/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
"youch/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||||
|
|
||||||
"@code/backend/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
"@code/backend/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||||
@@ -4022,26 +4040,6 @@
|
|||||||
|
|
||||||
"@expo/metro-config/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"@expo/metro-config/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
|
||||||
|
|
||||||
"@expo/metro-config/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
|
||||||
|
|
||||||
"@expo/package-manager/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"@expo/package-manager/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
"@expo/package-manager/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"@expo/package-manager/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
@@ -4068,60 +4066,14 @@
|
|||||||
|
|
||||||
"@mariozechner/pi-coding-agent/hosted-git-info/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
"@mariozechner/pi-coding-agent/hosted-git-info/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
|
||||||
|
|
||||||
"@react-native/codegen/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
|
||||||
|
|
||||||
"@react-native/codegen/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
|
||||||
|
|
||||||
"@react-native/codegen/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
|
||||||
|
|
||||||
"@rivetkit/react/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="],
|
"@rivetkit/react/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.7.7", "", {}, "sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ=="],
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
|
||||||
|
|
||||||
"@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
|
||||||
|
|
||||||
"@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
"@testing-library/dom/pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||||
|
|
||||||
"@types/ws/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
"@types/ws/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
"@types/yauzl/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
"@types/yauzl/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
|
||||||
|
|
||||||
"@voidzero-dev/vite-plus-core/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
|
||||||
|
|
||||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||||
|
|
||||||
"bun-types/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
"bun-types/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
@@ -4140,10 +4092,14 @@
|
|||||||
|
|
||||||
"cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
"cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="],
|
||||||
|
|
||||||
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
|
"concurrently/yargs/cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
||||||
|
|
||||||
|
"concurrently/yargs/yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="],
|
||||||
|
|
||||||
"connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"connect/finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
"connect/finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
|
||||||
@@ -4196,12 +4152,6 @@
|
|||||||
|
|
||||||
"metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="],
|
"metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="],
|
||||||
|
|
||||||
"metro/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
|
||||||
|
|
||||||
"metro/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
|
||||||
|
|
||||||
"metro/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
|
||||||
|
|
||||||
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"morgan/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"native/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
"native/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
@@ -4210,12 +4160,6 @@
|
|||||||
|
|
||||||
"protobufjs/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
"protobufjs/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
|
||||||
|
|
||||||
"react-native/yargs/cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
|
|
||||||
|
|
||||||
"react-native/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
|
||||||
|
|
||||||
"react-native/yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
|
||||||
|
|
||||||
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
"send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
"uniwind/@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
@@ -4246,6 +4190,26 @@
|
|||||||
|
|
||||||
"uniwind/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="],
|
"uniwind/@tailwindcss/oxide/@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
||||||
|
|
||||||
|
"uniwind/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||||
|
|
||||||
"vite-plus/oxfmt/@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-qVBsEO+KugOsCmUHcO8iqNnqc65p7PCKpCs8M66mPZ+Ri+CWbcpoQOEJBg2OTu03+0qu++NK1jj6IzvQVs0Sig=="],
|
"vite-plus/oxfmt/@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-qVBsEO+KugOsCmUHcO8iqNnqc65p7PCKpCs8M66mPZ+Ri+CWbcpoQOEJBg2OTu03+0qu++NK1jj6IzvQVs0Sig=="],
|
||||||
|
|
||||||
"vite-plus/oxfmt/@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-mp6PibWbao3aizijcheOeHQaYEhcUAt8pwLniYbtLfHxL/psFF0BykAwCj+s3c6qIpa8yN8keZICWrqtZ70w8g=="],
|
"vite-plus/oxfmt/@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-mp6PibWbao3aizijcheOeHQaYEhcUAt8pwLniYbtLfHxL/psFF0BykAwCj+s3c6qIpa8yN8keZICWrqtZ70w8g=="],
|
||||||
@@ -4322,26 +4286,6 @@
|
|||||||
|
|
||||||
"vite-plus/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.72.0", "", { "os": "win32", "cpu": "x64" }, "sha512-b2eKFD2hX7tIwmo/cyH6TDq8vzWRZ2qNHrzoGntUTmq0h3zQh/uX3eTSHCwI8OB/ADQfJCRelLItK8BsxuucDA=="],
|
"vite-plus/oxlint/@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.72.0", "", { "os": "win32", "cpu": "x64" }, "sha512-b2eKFD2hX7tIwmo/cyH6TDq8vzWRZ2qNHrzoGntUTmq0h3zQh/uX3eTSHCwI8OB/ADQfJCRelLItK8BsxuucDA=="],
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
|
||||||
|
|
||||||
"vite/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
|
||||||
|
|
||||||
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
"wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||||
|
|
||||||
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
"wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||||
@@ -4396,6 +4340,10 @@
|
|||||||
|
|
||||||
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
|
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
"@expo/cli/ora/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="],
|
||||||
|
|
||||||
"@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
"@expo/cli/ora/chalk/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="],
|
||||||
@@ -4416,18 +4364,14 @@
|
|||||||
|
|
||||||
"@expo/package-manager/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
|
"@expo/package-manager/ora/strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="],
|
||||||
|
|
||||||
"@react-native/codegen/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"@react-native/codegen/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
|
||||||
|
|
||||||
"@react-native/codegen/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"cli-highlight/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"cli-highlight/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
|
"concurrently/yargs/cliui/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||||
|
|
||||||
"googleapis-common/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
"googleapis-common/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
||||||
|
|
||||||
"googleapis/google-auth-library/gaxios/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
"googleapis/google-auth-library/gaxios/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
@@ -4436,42 +4380,10 @@
|
|||||||
|
|
||||||
"googleapis/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
"googleapis/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
||||||
|
|
||||||
"metro/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"metro/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
|
||||||
|
|
||||||
"metro/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
"pkg-up/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
|
||||||
|
|
||||||
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
"pkg-up/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
|
||||||
|
|
||||||
"react-native/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"react-native/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
|
||||||
|
|
||||||
"react-native/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
|
||||||
|
|
||||||
"uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
"uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="],
|
||||||
|
|
||||||
"uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
"uniwind/@tailwindcss/oxide/@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="],
|
||||||
@@ -4500,6 +4412,8 @@
|
|||||||
|
|
||||||
"@expo/package-manager/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
"@expo/package-manager/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||||
|
|
||||||
|
"concurrently/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
"pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
"pkg-up/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||||
|
|
||||||
"@expo/cli/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
"@expo/cli/ora/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"run:zopu": "bun --env-file=../../.env flue run zopu"
|
"run:zopu": "bun --env-file=../../.env flue run zopu"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@code/backend": "workspace:*",
|
||||||
"@code/env": "workspace:*",
|
"@code/env": "workspace:*",
|
||||||
"@flue/runtime": "latest",
|
"@flue/runtime": "latest",
|
||||||
"convex": "catalog:",
|
"convex": "catalog:",
|
||||||
|
|||||||
30
packages/agents/src/agents/project-manager.ts
Normal file
30
packages/agents/src/agents/project-manager.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { parseAgentEnv } from "@code/env/agent";
|
||||||
|
import { defineAgent } from "@flue/runtime";
|
||||||
|
import type { AgentRouteHandler } from "@flue/runtime";
|
||||||
|
|
||||||
|
import { agentOs } from "../sandboxes/agent-os";
|
||||||
|
import { createProjectTools } from "../tools/project";
|
||||||
|
|
||||||
|
export const description =
|
||||||
|
"Works one repository issue inside an isolated AgentOS workspace.";
|
||||||
|
|
||||||
|
export const route: AgentRouteHandler = (_context, next) => next();
|
||||||
|
|
||||||
|
export default defineAgent(({ env, id }) => {
|
||||||
|
const { AGENT_MODEL_NAME, AGENT_MODEL_PROVIDER } = parseAgentEnv(env);
|
||||||
|
|
||||||
|
return {
|
||||||
|
cwd: "/workspace",
|
||||||
|
description,
|
||||||
|
instructions: `You are the issue-scoped project manager and coding agent.
|
||||||
|
|
||||||
|
Start every run by calling lookup_issue_context, reading issue.md, project.md, business.md, design.md, agent.md, work.md, steps.md, artifacts.md, signals.md, agent-manager.md, context.md, and card.md from the AgentOS workspace. Call report_work_status with working before editing.
|
||||||
|
|
||||||
|
Work only on the bound issue. Inspect existing files before changing them. If a missing product decision makes safe progress impossible, publish the current work.md and steps.md, report needs-input, then ask one focused question. Otherwise implement the complete issue, run the relevant command or scenario in AgentOS, and preserve command evidence.
|
||||||
|
|
||||||
|
Before finishing, update the local work.md, steps.md, artifacts.md, and context.md files and publish each changed canonical artifact with publish_project_artifact. Report completed only after verification. On an unrecoverable error, report failed with the exact blocker. Never claim a repository change or command result you did not observe.`,
|
||||||
|
model: `${AGENT_MODEL_PROVIDER}/${AGENT_MODEL_NAME}`,
|
||||||
|
sandbox: agentOs(env),
|
||||||
|
tools: createProjectTools(id, env),
|
||||||
|
};
|
||||||
|
});
|
||||||
234
packages/agents/src/sandboxes/agent-os.ts
Normal file
234
packages/agents/src/sandboxes/agent-os.ts
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import { api } from "@code/backend/convex/_generated/api";
|
||||||
|
import type { Id } from "@code/backend/convex/_generated/dataModel";
|
||||||
|
import { parseAgentEnv } from "@code/env/agent";
|
||||||
|
import { createSandboxSessionEnv } from "@flue/runtime";
|
||||||
|
import type { FileStat, SandboxApi, SandboxFactory } from "@flue/runtime";
|
||||||
|
import { ConvexHttpClient } from "convex/browser";
|
||||||
|
import * as v from "valibot";
|
||||||
|
|
||||||
|
const execResultSchema = v.object({
|
||||||
|
exitCode: v.number(),
|
||||||
|
stderr: v.string(),
|
||||||
|
stdout: v.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const statResultSchema = v.object({
|
||||||
|
isDirectory: v.boolean(),
|
||||||
|
isSymbolicLink: v.boolean(),
|
||||||
|
mtimeMs: v.number(),
|
||||||
|
size: v.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ExecuteOptions {
|
||||||
|
readonly signal?: AbortSignal;
|
||||||
|
readonly timeoutMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AgentOsSandboxApi implements SandboxApi {
|
||||||
|
readonly #actorKey: string[];
|
||||||
|
readonly #client: ConvexHttpClient;
|
||||||
|
readonly #daemonId: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
client: ConvexHttpClient,
|
||||||
|
daemonId: string,
|
||||||
|
actorKey: readonly string[]
|
||||||
|
) {
|
||||||
|
this.#client = client;
|
||||||
|
this.#daemonId = daemonId;
|
||||||
|
this.#actorKey = [...actorKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
async #execute(
|
||||||
|
method: string,
|
||||||
|
args: unknown[],
|
||||||
|
options: ExecuteOptions = {}
|
||||||
|
): Promise<unknown> {
|
||||||
|
const commandId = await this.#client.mutation(api.daemonCommands.enqueue, {
|
||||||
|
actorKey: this.#actorKey,
|
||||||
|
args,
|
||||||
|
daemonId: this.#daemonId,
|
||||||
|
method,
|
||||||
|
});
|
||||||
|
const timeoutMs = options.timeoutMs ?? 120_000;
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (options.signal?.aborted) {
|
||||||
|
// oxlint-disable-next-line no-await-in-loop -- cancellation must settle before the poll exits.
|
||||||
|
await this.#client.mutation(api.daemonCommands.cancel, { commandId });
|
||||||
|
throw options.signal.reason ?? new Error("AgentOS command aborted");
|
||||||
|
}
|
||||||
|
// oxlint-disable-next-line no-await-in-loop -- each query observes the next durable command state.
|
||||||
|
const command = await this.#client.query(api.daemonCommands.get, {
|
||||||
|
commandId,
|
||||||
|
});
|
||||||
|
if (!command) {
|
||||||
|
throw new Error(`AgentOS command ${commandId} disappeared`);
|
||||||
|
}
|
||||||
|
if (command.status === "succeeded") {
|
||||||
|
return command.result;
|
||||||
|
}
|
||||||
|
if (command.status === "failed" || command.status === "cancelled") {
|
||||||
|
throw new Error(
|
||||||
|
command.error ?? `AgentOS command ${method} ${command.status}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const { promise, resolve } = Promise.withResolvers<null>();
|
||||||
|
setTimeout(() => resolve(null), 200);
|
||||||
|
// oxlint-disable-next-line no-await-in-loop -- polling intentionally waits between sequential reads.
|
||||||
|
await promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#client.mutation(api.daemonCommands.cancel, { commandId });
|
||||||
|
throw new Error(`AgentOS command ${method} timed out after ${timeoutMs}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(path: string): Promise<string> {
|
||||||
|
return new TextDecoder().decode(await this.readFileBuffer(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFileBuffer(path: string): Promise<Uint8Array> {
|
||||||
|
const result = await this.#execute("readFile", [path]);
|
||||||
|
if (result instanceof ArrayBuffer) {
|
||||||
|
return new Uint8Array(result);
|
||||||
|
}
|
||||||
|
if (ArrayBuffer.isView(result)) {
|
||||||
|
return new Uint8Array(
|
||||||
|
result.buffer.slice(
|
||||||
|
result.byteOffset,
|
||||||
|
result.byteOffset + result.byteLength
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new TypeError(
|
||||||
|
`AgentOS readFile returned non-binary data for ${path}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(path: string, content: string | Uint8Array): Promise<void> {
|
||||||
|
const serialized =
|
||||||
|
typeof content === "string" ? content : Uint8Array.from(content).buffer;
|
||||||
|
await this.#execute("writeFile", [path, serialized]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async stat(path: string): Promise<FileStat> {
|
||||||
|
const result = v.parse(
|
||||||
|
statResultSchema,
|
||||||
|
await this.#execute("stat", [path])
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
isDirectory: result.isDirectory,
|
||||||
|
isFile: !result.isDirectory && !result.isSymbolicLink,
|
||||||
|
isSymbolicLink: result.isSymbolicLink,
|
||||||
|
mtime: new Date(result.mtimeMs),
|
||||||
|
size: result.size,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async readdir(path: string): Promise<string[]> {
|
||||||
|
return v.parse(v.array(v.string()), await this.#execute("readdir", [path]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(path: string): Promise<boolean> {
|
||||||
|
return v.parse(v.boolean(), await this.#execute("exists", [path]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async mkdir(
|
||||||
|
path: string,
|
||||||
|
options?: { readonly recursive?: boolean }
|
||||||
|
): Promise<void> {
|
||||||
|
if (!options?.recursive) {
|
||||||
|
await this.#execute("mkdir", [path]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const quotedPath = `'${path.replaceAll("'", `'"'"'`)}'`;
|
||||||
|
const result = v.parse(
|
||||||
|
execResultSchema,
|
||||||
|
await this.#execute("exec", [`mkdir -p ${quotedPath}`])
|
||||||
|
);
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
throw new Error(result.stderr || `Could not create ${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async rm(
|
||||||
|
path: string,
|
||||||
|
options?: { readonly force?: boolean; readonly recursive?: boolean }
|
||||||
|
): Promise<void> {
|
||||||
|
if (options?.force && !(await this.exists(path))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.#execute("deleteFile", [
|
||||||
|
path,
|
||||||
|
{ recursive: options?.recursive },
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exec(
|
||||||
|
command: string,
|
||||||
|
options?: {
|
||||||
|
readonly cwd?: string;
|
||||||
|
readonly env?: Record<string, string>;
|
||||||
|
readonly signal?: AbortSignal;
|
||||||
|
readonly timeoutMs?: number;
|
||||||
|
}
|
||||||
|
): Promise<{ exitCode: number; stderr: string; stdout: string }> {
|
||||||
|
return v.parse(
|
||||||
|
execResultSchema,
|
||||||
|
await this.#execute(
|
||||||
|
"exec",
|
||||||
|
[
|
||||||
|
command,
|
||||||
|
{
|
||||||
|
...(options?.cwd === undefined ? {} : { cwd: options.cwd }),
|
||||||
|
...(options?.env === undefined ? {} : { env: options.env }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ signal: options?.signal, timeoutMs: options?.timeoutMs }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const agentOs = (
|
||||||
|
runtimeEnv: Record<string, string | undefined>
|
||||||
|
): SandboxFactory => {
|
||||||
|
const env = parseAgentEnv(runtimeEnv);
|
||||||
|
const client = new ConvexHttpClient(env.CONVEX_URL);
|
||||||
|
|
||||||
|
return {
|
||||||
|
async createSessionEnv({ id }) {
|
||||||
|
// Flue controls this id from the issue document selected by the web app.
|
||||||
|
const issueId = id as Id<"projectIssues">;
|
||||||
|
const run = await client.mutation(api.agentWorkspace.ensureRun, {
|
||||||
|
daemonId: env.DAEMON_ID,
|
||||||
|
issueId,
|
||||||
|
token: env.FLUE_DB_TOKEN,
|
||||||
|
});
|
||||||
|
if (!run) {
|
||||||
|
throw new Error(`Could not initialize AgentOS work run for ${id}`);
|
||||||
|
}
|
||||||
|
const sandbox = new AgentOsSandboxApi(
|
||||||
|
client,
|
||||||
|
env.DAEMON_ID,
|
||||||
|
run.actorKey
|
||||||
|
);
|
||||||
|
const context = await client.query(api.agentWorkspace.get, {
|
||||||
|
issueId,
|
||||||
|
token: env.FLUE_DB_TOKEN,
|
||||||
|
});
|
||||||
|
await sandbox.mkdir("/workspace", { recursive: true });
|
||||||
|
await Promise.all(
|
||||||
|
context.artifacts.map((artifact) =>
|
||||||
|
sandbox.writeFile(`/workspace/${artifact.path}`, artifact.content)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await sandbox.writeFile(
|
||||||
|
"/workspace/issue.md",
|
||||||
|
`# Issue ${context.issue.number}: ${context.issue.title}\n\n${context.issue.body}\n`
|
||||||
|
);
|
||||||
|
return createSandboxSessionEnv(sandbox, "/workspace");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
70
packages/agents/src/tools/project.ts
Normal file
70
packages/agents/src/tools/project.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { api } from "@code/backend/convex/_generated/api";
|
||||||
|
import type { Id } from "@code/backend/convex/_generated/dataModel";
|
||||||
|
import { ARTIFACT_PATHS } from "@code/backend/convex/artifactModel";
|
||||||
|
import { parseAgentEnv } from "@code/env/agent";
|
||||||
|
import { defineTool } from "@flue/runtime";
|
||||||
|
import { ConvexHttpClient } from "convex/browser";
|
||||||
|
import * as v from "valibot";
|
||||||
|
|
||||||
|
export const createProjectTools = (
|
||||||
|
issueAgentId: string,
|
||||||
|
runtimeEnv: Record<string, string | undefined>
|
||||||
|
) => {
|
||||||
|
const env = parseAgentEnv(runtimeEnv);
|
||||||
|
const client = new ConvexHttpClient(env.CONVEX_URL);
|
||||||
|
// Flue receives this controlled id from the issue card that starts the run.
|
||||||
|
const issueId = issueAgentId as Id<"projectIssues">;
|
||||||
|
|
||||||
|
return [
|
||||||
|
defineTool({
|
||||||
|
description:
|
||||||
|
"Read the repository, issue, and canonical project artifacts bound to this agent.",
|
||||||
|
name: "lookup_issue_context",
|
||||||
|
async run() {
|
||||||
|
return await client.query(api.agentWorkspace.get, {
|
||||||
|
issueId,
|
||||||
|
token: env.FLUE_DB_TOKEN,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
defineTool({
|
||||||
|
description:
|
||||||
|
"Publish the complete current content of one canonical project markdown artifact after changing it in AgentOS.",
|
||||||
|
input: v.object({
|
||||||
|
content: v.pipe(v.string(), v.maxLength(200_000)),
|
||||||
|
path: v.picklist(ARTIFACT_PATHS),
|
||||||
|
}),
|
||||||
|
name: "publish_project_artifact",
|
||||||
|
async run({ input }) {
|
||||||
|
const revision = await client.mutation(
|
||||||
|
api.agentWorkspace.updateArtifact,
|
||||||
|
{
|
||||||
|
content: input.content,
|
||||||
|
issueId,
|
||||||
|
path: input.path,
|
||||||
|
token: env.FLUE_DB_TOKEN,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { path: input.path, revision };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
defineTool({
|
||||||
|
description:
|
||||||
|
"Report the durable issue state. Use needs-input before asking a blocking question and completed only after verification.",
|
||||||
|
input: v.object({
|
||||||
|
status: v.picklist(["working", "needs-input", "completed", "failed"]),
|
||||||
|
summary: v.pipe(v.string(), v.maxLength(4000)),
|
||||||
|
}),
|
||||||
|
name: "report_work_status",
|
||||||
|
async run({ input }) {
|
||||||
|
await client.mutation(api.agentWorkspace.setStatus, {
|
||||||
|
issueId,
|
||||||
|
status: input.status,
|
||||||
|
summary: input.summary,
|
||||||
|
token: env.FLUE_DB_TOKEN,
|
||||||
|
});
|
||||||
|
return { recorded: true, status: input.status };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
};
|
||||||
37
packages/auth/package.json
Normal file
37
packages/auth/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "@code/auth",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./native": "./src/native/index.ts",
|
||||||
|
"./server": "./src/server/index.ts",
|
||||||
|
"./web": "./src/web/index.ts"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"check-types": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@better-auth/expo": "catalog:",
|
||||||
|
"@code/backend": "workspace:*",
|
||||||
|
"@code/env": "workspace:*",
|
||||||
|
"@code/ui": "workspace:*",
|
||||||
|
"@convex-dev/better-auth": "catalog:",
|
||||||
|
"@tanstack/react-form": "catalog:",
|
||||||
|
"better-auth": "catalog:",
|
||||||
|
"convex": "catalog:",
|
||||||
|
"expo-constants": "~57.0.2",
|
||||||
|
"expo-secure-store": "~57.0.0",
|
||||||
|
"heroui-native": "^1.0.5",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-native": "0.86.0",
|
||||||
|
"sonner": "catalog:",
|
||||||
|
"zod": "catalog:"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@code/config": "workspace:*",
|
||||||
|
"@types/react": "~19.2.17",
|
||||||
|
"vite": "^7.3.6",
|
||||||
|
"typescript": "^6"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,16 +6,18 @@ import Constants from "expo-constants";
|
|||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
|
const scheme = Constants.expoConfig?.scheme;
|
||||||
|
|
||||||
|
if (typeof scheme !== "string") {
|
||||||
|
throw new Error("Expo auth requires a string app scheme in app.json");
|
||||||
|
}
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL: env.EXPO_PUBLIC_CONVEX_SITE_URL,
|
baseURL: env.EXPO_PUBLIC_CONVEX_SITE_URL,
|
||||||
plugins: [
|
plugins: [
|
||||||
convexClient(),
|
convexClient(),
|
||||||
Platform.OS === "web"
|
Platform.OS === "web"
|
||||||
? crossDomainClient()
|
? crossDomainClient()
|
||||||
: expoClient({
|
: expoClient({ scheme, storage: SecureStore, storagePrefix: scheme }),
|
||||||
scheme: Constants.expoConfig?.scheme as string,
|
|
||||||
storagePrefix: Constants.expoConfig?.scheme as string,
|
|
||||||
storage: SecureStore,
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
16
packages/auth/src/native/auth-provider.tsx
Normal file
16
packages/auth/src/native/auth-provider.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { env } from "@code/env/native";
|
||||||
|
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
|
||||||
|
import { ConvexReactClient } from "convex/react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { authClient } from "./auth-client";
|
||||||
|
|
||||||
|
const convex = new ConvexReactClient(env.EXPO_PUBLIC_CONVEX_URL, {
|
||||||
|
unsavedChangesWarning: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NativeAuthProvider = ({ children }: { children: ReactNode }) => (
|
||||||
|
<ConvexBetterAuthProvider authClient={authClient} client={convex}>
|
||||||
|
{children}
|
||||||
|
</ConvexBetterAuthProvider>
|
||||||
|
);
|
||||||
62
packages/auth/src/native/hooks.ts
Normal file
62
packages/auth/src/native/hooks.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useForm } from "@tanstack/react-form";
|
||||||
|
import { useToast } from "heroui-native";
|
||||||
|
|
||||||
|
import { getErrorMessage, signInSchema, signUpSchema } from "../shared/forms";
|
||||||
|
import { authClient } from "./auth-client";
|
||||||
|
|
||||||
|
type AuthFormOptions = {
|
||||||
|
readonly onSuccess?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNativeSignInForm = ({ onSuccess }: AuthFormOptions = {}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
return useForm({
|
||||||
|
defaultValues: { email: "", password: "" },
|
||||||
|
onSubmit: async ({ formApi, value }) => {
|
||||||
|
await authClient.signIn.email(
|
||||||
|
{ email: value.email.trim(), password: value.password },
|
||||||
|
{
|
||||||
|
onError: ({ error }) => {
|
||||||
|
toast.show({ label: error.message || "Failed to sign in", variant: "danger" });
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
formApi.reset();
|
||||||
|
toast.show({ label: "Signed in successfully", variant: "success" });
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
validators: { onSubmit: signInSchema },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNativeSignUpForm = ({ onSuccess }: AuthFormOptions = {}) => {
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
return useForm({
|
||||||
|
defaultValues: { confirmPassword: "", email: "", name: "", password: "" },
|
||||||
|
onSubmit: async ({ formApi, value }) => {
|
||||||
|
await authClient.signUp.email(
|
||||||
|
{ email: value.email.trim(), name: value.name.trim(), password: value.password },
|
||||||
|
{
|
||||||
|
onError: ({ error }) => {
|
||||||
|
toast.show({
|
||||||
|
label: error.message || "Failed to create account",
|
||||||
|
variant: "danger",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
formApi.reset();
|
||||||
|
toast.show({ label: "Account created successfully", variant: "success" });
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
validators: { onSubmit: signUpSchema },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getErrorMessage };
|
||||||
4
packages/auth/src/native/index.ts
Normal file
4
packages/auth/src/native/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { authClient } from "./auth-client";
|
||||||
|
export { NativeAuthProvider } from "./auth-provider";
|
||||||
|
export { NativeLoginForm } from "./login-form";
|
||||||
|
export { NativeSignupForm } from "./signup-form";
|
||||||
107
packages/auth/src/native/login-form.tsx
Normal file
107
packages/auth/src/native/login-form.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FieldError,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Spinner,
|
||||||
|
Surface,
|
||||||
|
TextField,
|
||||||
|
useThemeColor,
|
||||||
|
} from "heroui-native";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { Text, type TextInput, View } from "react-native";
|
||||||
|
|
||||||
|
import { getErrorMessage, useNativeSignInForm } from "./hooks";
|
||||||
|
|
||||||
|
type NativeLoginFormProps = {
|
||||||
|
readonly onNavigateSignUp: () => void;
|
||||||
|
readonly onSuccess?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NativeLoginForm = ({
|
||||||
|
onNavigateSignUp,
|
||||||
|
onSuccess,
|
||||||
|
}: NativeLoginFormProps) => {
|
||||||
|
const passwordInputRef = useRef<TextInput>(null);
|
||||||
|
const form = useNativeSignInForm({ onSuccess });
|
||||||
|
const foregroundColor = useThemeColor("foreground");
|
||||||
|
const mutedColor = useThemeColor("muted");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Surface className="rounded-xl p-5" variant="secondary">
|
||||||
|
<Text style={{ color: foregroundColor, fontSize: 24, fontWeight: "600", marginBottom: 4 }}>
|
||||||
|
Welcome back
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: mutedColor, fontSize: 14, marginBottom: 20 }}>
|
||||||
|
Sign in with your email and password.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
submitError: getErrorMessage(state.errorMap.onSubmit),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, submitError }) => (
|
||||||
|
<View style={{ gap: 16 }}>
|
||||||
|
<FieldError className="mb-1" isInvalid={!!submitError}>
|
||||||
|
{submitError}
|
||||||
|
</FieldError>
|
||||||
|
|
||||||
|
<form.Field name="email">
|
||||||
|
{(field) => (
|
||||||
|
<TextField>
|
||||||
|
<Label>Email</Label>
|
||||||
|
<Input
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="email"
|
||||||
|
blurOnSubmit={false}
|
||||||
|
keyboardType="email-address"
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChangeText={field.handleChange}
|
||||||
|
onSubmitEditing={() => passwordInputRef.current?.focus()}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
returnKeyType="next"
|
||||||
|
textContentType="emailAddress"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="password">
|
||||||
|
{(field) => (
|
||||||
|
<TextField>
|
||||||
|
<Label>Password</Label>
|
||||||
|
<Input
|
||||||
|
autoComplete="password"
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChangeText={field.handleChange}
|
||||||
|
onSubmitEditing={() => void form.handleSubmit()}
|
||||||
|
ref={passwordInputRef}
|
||||||
|
returnKeyType="go"
|
||||||
|
secureTextEntry
|
||||||
|
textContentType="password"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-1"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onPress={() => void form.handleSubmit()}
|
||||||
|
>
|
||||||
|
{isSubmitting ? <Spinner color="default" size="sm" /> : <Button.Label>Sign in</Button.Label>}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onPress={onNavigateSignUp} variant="ghost">
|
||||||
|
<Button.Label>Need an account? Sign up</Button.Label>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
</Surface>
|
||||||
|
);
|
||||||
|
};
|
||||||
138
packages/auth/src/native/signup-form.tsx
Normal file
138
packages/auth/src/native/signup-form.tsx
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FieldError,
|
||||||
|
Input,
|
||||||
|
Label,
|
||||||
|
Spinner,
|
||||||
|
Surface,
|
||||||
|
TextField,
|
||||||
|
useThemeColor,
|
||||||
|
} from "heroui-native";
|
||||||
|
import { Text, View } from "react-native";
|
||||||
|
|
||||||
|
import { getErrorMessage, useNativeSignUpForm } from "./hooks";
|
||||||
|
|
||||||
|
type NativeSignupFormProps = {
|
||||||
|
readonly onNavigateSignIn: () => void;
|
||||||
|
readonly onSuccess?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NativeSignupForm = ({
|
||||||
|
onNavigateSignIn,
|
||||||
|
onSuccess,
|
||||||
|
}: NativeSignupFormProps) => {
|
||||||
|
const form = useNativeSignUpForm({ onSuccess });
|
||||||
|
const foregroundColor = useThemeColor("foreground");
|
||||||
|
const mutedColor = useThemeColor("muted");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Surface className="rounded-xl p-5" variant="secondary">
|
||||||
|
<Text style={{ color: foregroundColor, fontSize: 24, fontWeight: "600", marginBottom: 4 }}>
|
||||||
|
Create an account
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: mutedColor, fontSize: 14, marginBottom: 20 }}>
|
||||||
|
Use your name, email, and password.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
submitError: getErrorMessage(state.errorMap.onSubmit),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, submitError }) => (
|
||||||
|
<View style={{ gap: 16 }}>
|
||||||
|
<FieldError className="mb-1" isInvalid={!!submitError}>
|
||||||
|
{submitError}
|
||||||
|
</FieldError>
|
||||||
|
|
||||||
|
<form.Field name="name">
|
||||||
|
{(field) => (
|
||||||
|
<TextField>
|
||||||
|
<Label>Full name</Label>
|
||||||
|
<Input
|
||||||
|
autoCapitalize="words"
|
||||||
|
autoComplete="name"
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChangeText={field.handleChange}
|
||||||
|
placeholder="Jane Doe"
|
||||||
|
textContentType="name"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="email">
|
||||||
|
{(field) => (
|
||||||
|
<TextField>
|
||||||
|
<Label>Email</Label>
|
||||||
|
<Input
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoComplete="email"
|
||||||
|
keyboardType="email-address"
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChangeText={field.handleChange}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
textContentType="emailAddress"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="password">
|
||||||
|
{(field) => (
|
||||||
|
<TextField>
|
||||||
|
<Label>Password</Label>
|
||||||
|
<Input
|
||||||
|
autoComplete="new-password"
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChangeText={field.handleChange}
|
||||||
|
secureTextEntry
|
||||||
|
textContentType="newPassword"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="confirmPassword">
|
||||||
|
{(field) => (
|
||||||
|
<TextField>
|
||||||
|
<Label>Confirm password</Label>
|
||||||
|
<Input
|
||||||
|
autoComplete="new-password"
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChangeText={field.handleChange}
|
||||||
|
onSubmitEditing={() => void form.handleSubmit()}
|
||||||
|
returnKeyType="go"
|
||||||
|
secureTextEntry
|
||||||
|
textContentType="newPassword"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
</TextField>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mt-1"
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
onPress={() => void form.handleSubmit()}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<Spinner color="default" size="sm" />
|
||||||
|
) : (
|
||||||
|
<Button.Label>Create account</Button.Label>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onPress={onNavigateSignIn} variant="ghost">
|
||||||
|
<Button.Label>Already have an account? Sign in</Button.Label>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
</Surface>
|
||||||
|
);
|
||||||
|
};
|
||||||
1
packages/auth/src/server/index.ts
Normal file
1
packages/auth/src/server/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { authComponent, createAuth } from "@code/backend/convex/auth";
|
||||||
38
packages/auth/src/shared/forms.ts
Normal file
38
packages/auth/src/shared/forms.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const signInSchema = z.object({
|
||||||
|
email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"),
|
||||||
|
password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const signUpSchema = z
|
||||||
|
.object({
|
||||||
|
confirmPassword: z.string().min(1, "Confirm your password"),
|
||||||
|
email: z.string().trim().min(1, "Email is required").email("Enter a valid email address"),
|
||||||
|
name: z.string().trim().min(2, "Name must be at least 2 characters"),
|
||||||
|
password: z.string().min(1, "Password is required").min(8, "Use at least 8 characters"),
|
||||||
|
})
|
||||||
|
.refine(({ confirmPassword, password }) => confirmPassword === password, {
|
||||||
|
message: "Passwords do not match",
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getErrorMessage = (error: unknown): string | null => {
|
||||||
|
if (!error) return null;
|
||||||
|
if (typeof error === "string") return error;
|
||||||
|
|
||||||
|
if (Array.isArray(error)) {
|
||||||
|
for (const issue of error) {
|
||||||
|
const message = getErrorMessage(issue);
|
||||||
|
if (message) return message;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error === "object" && error !== null && "message" in error) {
|
||||||
|
const { message } = error as { message?: unknown };
|
||||||
|
return typeof message === "string" ? message : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
14
packages/auth/src/web/auth-provider.tsx
Normal file
14
packages/auth/src/web/auth-provider.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { env } from "@code/env/web";
|
||||||
|
import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
|
||||||
|
import { ConvexReactClient } from "convex/react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import { authClient } from "./auth-client";
|
||||||
|
|
||||||
|
const convex = new ConvexReactClient(env.VITE_CONVEX_URL);
|
||||||
|
|
||||||
|
export const WebAuthProvider = ({ children }: { children: ReactNode }) => (
|
||||||
|
<ConvexBetterAuthProvider authClient={authClient} client={convex}>
|
||||||
|
{children}
|
||||||
|
</ConvexBetterAuthProvider>
|
||||||
|
);
|
||||||
51
packages/auth/src/web/hooks.ts
Normal file
51
packages/auth/src/web/hooks.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { useForm } from "@tanstack/react-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
import { signInSchema, signUpSchema } from "../shared/forms";
|
||||||
|
import { authClient } from "./auth-client";
|
||||||
|
|
||||||
|
type AuthFormOptions = {
|
||||||
|
readonly onSuccess?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSignInForm = ({ onSuccess }: AuthFormOptions = {}) =>
|
||||||
|
useForm({
|
||||||
|
defaultValues: { email: "", password: "" },
|
||||||
|
onSubmit: async ({ formApi, value }) => {
|
||||||
|
await authClient.signIn.email(
|
||||||
|
{ email: value.email.trim(), password: value.password },
|
||||||
|
{
|
||||||
|
onError: ({ error }) => {
|
||||||
|
toast.error(error.message || error.statusText);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
formApi.reset();
|
||||||
|
toast.success("Signed in successfully");
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
validators: { onSubmit: signInSchema },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useSignUpForm = ({ onSuccess }: AuthFormOptions = {}) =>
|
||||||
|
useForm({
|
||||||
|
defaultValues: { confirmPassword: "", email: "", name: "", password: "" },
|
||||||
|
onSubmit: async ({ formApi, value }) => {
|
||||||
|
await authClient.signUp.email(
|
||||||
|
{ email: value.email.trim(), name: value.name.trim(), password: value.password },
|
||||||
|
{
|
||||||
|
onError: ({ error }) => {
|
||||||
|
toast.error(error.message || error.statusText);
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
formApi.reset();
|
||||||
|
toast.success("Account created successfully");
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
validators: { onSubmit: signUpSchema },
|
||||||
|
});
|
||||||
4
packages/auth/src/web/index.ts
Normal file
4
packages/auth/src/web/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { authClient } from "./auth-client";
|
||||||
|
export { WebAuthProvider } from "./auth-provider";
|
||||||
|
export { LoginForm } from "./login-form";
|
||||||
|
export { SignupForm } from "./signup-form";
|
||||||
114
packages/auth/src/web/login-form.tsx
Normal file
114
packages/auth/src/web/login-form.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { Button } from "@code/ui/components/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@code/ui/components/card";
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldDescription,
|
||||||
|
FieldError,
|
||||||
|
FieldGroup,
|
||||||
|
FieldLabel,
|
||||||
|
} from "@code/ui/components/field";
|
||||||
|
import { Input } from "@code/ui/components/input";
|
||||||
|
import { cn } from "@code/ui/lib/utils";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
|
import { getErrorMessage } from "../shared/forms";
|
||||||
|
import { useSignInForm } from "./hooks";
|
||||||
|
|
||||||
|
type LoginFormProps = ComponentProps<"div"> & {
|
||||||
|
readonly onSuccess?: () => void;
|
||||||
|
readonly signUpHref?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LoginForm = ({
|
||||||
|
className,
|
||||||
|
onSuccess,
|
||||||
|
signUpHref = "/signup",
|
||||||
|
...props
|
||||||
|
}: LoginFormProps) => {
|
||||||
|
const form = useSignInForm({ onSuccess });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Welcome back</CardTitle>
|
||||||
|
<CardDescription>Enter your email and password to continue.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
void form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FieldGroup>
|
||||||
|
<form.Field name="email">
|
||||||
|
{(field) => (
|
||||||
|
<Field data-invalid={!field.state.meta.isValid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||||
|
<Input
|
||||||
|
autoComplete="email"
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
type="email"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
<FieldError errors={field.state.meta.errors} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="password">
|
||||||
|
{(field) => (
|
||||||
|
<Field data-invalid={!field.state.meta.isValid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
||||||
|
<Input
|
||||||
|
autoComplete="current-password"
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
type="password"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
<FieldError errors={field.state.meta.errors} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
canSubmit: state.canSubmit,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
submitError: getErrorMessage(state.errorMap.onSubmit),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ canSubmit, isSubmitting, submitError }) => (
|
||||||
|
<Field>
|
||||||
|
<FieldError>{submitError}</FieldError>
|
||||||
|
<Button disabled={!canSubmit || isSubmitting} type="submit">
|
||||||
|
{isSubmitting ? "Signing in…" : "Sign in"}
|
||||||
|
</Button>
|
||||||
|
<FieldDescription className="text-center">
|
||||||
|
Don't have an account? <a href={signUpHref}>Sign up</a>
|
||||||
|
</FieldDescription>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
</FieldGroup>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
147
packages/auth/src/web/signup-form.tsx
Normal file
147
packages/auth/src/web/signup-form.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { Button } from "@code/ui/components/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@code/ui/components/card";
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldDescription,
|
||||||
|
FieldError,
|
||||||
|
FieldGroup,
|
||||||
|
FieldLabel,
|
||||||
|
} from "@code/ui/components/field";
|
||||||
|
import { Input } from "@code/ui/components/input";
|
||||||
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
|
import { getErrorMessage } from "../shared/forms";
|
||||||
|
import { useSignUpForm } from "./hooks";
|
||||||
|
|
||||||
|
type SignupFormProps = ComponentProps<typeof Card> & {
|
||||||
|
readonly onSuccess?: () => void;
|
||||||
|
readonly signInHref?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SignupForm = ({
|
||||||
|
onSuccess,
|
||||||
|
signInHref = "/login",
|
||||||
|
...props
|
||||||
|
}: SignupFormProps) => {
|
||||||
|
const form = useSignUpForm({ onSuccess });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card {...props}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Create an account</CardTitle>
|
||||||
|
<CardDescription>Use your name, email, and password to get started.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
void form.handleSubmit();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FieldGroup>
|
||||||
|
<form.Field name="name">
|
||||||
|
{(field) => (
|
||||||
|
<Field data-invalid={!field.state.meta.isValid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>Full name</FieldLabel>
|
||||||
|
<Input
|
||||||
|
autoComplete="name"
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
placeholder="Jane Doe"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
<FieldError errors={field.state.meta.errors} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="email">
|
||||||
|
{(field) => (
|
||||||
|
<Field data-invalid={!field.state.meta.isValid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
||||||
|
<Input
|
||||||
|
autoComplete="email"
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
type="email"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
<FieldError errors={field.state.meta.errors} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="password">
|
||||||
|
{(field) => (
|
||||||
|
<Field data-invalid={!field.state.meta.isValid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
||||||
|
<Input
|
||||||
|
autoComplete="new-password"
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
type="password"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
<FieldDescription>Use at least 8 characters.</FieldDescription>
|
||||||
|
<FieldError errors={field.state.meta.errors} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Field name="confirmPassword">
|
||||||
|
{(field) => (
|
||||||
|
<Field data-invalid={!field.state.meta.isValid}>
|
||||||
|
<FieldLabel htmlFor={field.name}>Confirm password</FieldLabel>
|
||||||
|
<Input
|
||||||
|
autoComplete="new-password"
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(event) => field.handleChange(event.target.value)}
|
||||||
|
type="password"
|
||||||
|
value={field.state.value}
|
||||||
|
/>
|
||||||
|
<FieldError errors={field.state.meta.errors} />
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
|
||||||
|
<form.Subscribe
|
||||||
|
selector={(state) => ({
|
||||||
|
canSubmit: state.canSubmit,
|
||||||
|
isSubmitting: state.isSubmitting,
|
||||||
|
submitError: getErrorMessage(state.errorMap.onSubmit),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{({ canSubmit, isSubmitting, submitError }) => (
|
||||||
|
<Field>
|
||||||
|
<FieldError>{submitError}</FieldError>
|
||||||
|
<Button disabled={!canSubmit || isSubmitting} type="submit">
|
||||||
|
{isSubmitting ? "Creating account…" : "Create account"}
|
||||||
|
</Button>
|
||||||
|
<FieldDescription className="text-center">
|
||||||
|
Already have an account? <a href={signInHref}>Sign in</a>
|
||||||
|
</FieldDescription>
|
||||||
|
</Field>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
</FieldGroup>
|
||||||
|
</form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
10
packages/auth/tsconfig.json
Normal file
10
packages/auth/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "@code/config/tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["node", "vite/client"],
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
10
packages/backend/.oxlintrc.json
Normal file
10
packages/backend/.oxlintrc.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["convex/**/*.ts"],
|
||||||
|
"rules": {
|
||||||
|
"unicorn/filename-case": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
13
packages/backend/CLAUDE.md
Normal file
13
packages/backend/CLAUDE.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!-- convex-ai-start -->
|
||||||
|
|
||||||
|
This project uses [Convex](https://convex.dev) as its backend.
|
||||||
|
|
||||||
|
When working on Convex code, **always read
|
||||||
|
`convex/_generated/ai/guidelines.md` first** for important guidelines on
|
||||||
|
how to correctly use Convex APIs and patterns. The file contains rules that
|
||||||
|
override what you may have learned about Convex from training data.
|
||||||
|
|
||||||
|
Convex agent skills for common tasks can be installed by running
|
||||||
|
`npx convex ai-files install`.
|
||||||
|
|
||||||
|
<!-- convex-ai-end -->
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"guidelinesHash": "e72f83c02fca6a3a20f4d53731bd803a6c22ae4f9507ef575407aff86f35aa06",
|
"guidelinesHash": "f730e6620e882fef21a3e00c5539cc0b472ef26688efd92bf3a42f0711de6888",
|
||||||
"agentsMdSectionHash": "5934f676ea9a332e7cd4a4f64aa23b59d926e9faca026c758d4b1f87d2101cc3",
|
"agentsMdSectionHash": "5934f676ea9a332e7cd4a4f64aa23b59d926e9faca026c758d4b1f87d2101cc3",
|
||||||
"claudeMdHash": "5934f676ea9a332e7cd4a4f64aa23b59d926e9faca026c758d4b1f87d2101cc3",
|
"claudeMdHash": "5934f676ea9a332e7cd4a4f64aa23b59d926e9faca026c758d4b1f87d2101cc3",
|
||||||
"agentSkillsSha": "ec1e6baae7d86c7843c22938c75979c016f5c6e9"
|
"agentSkillsSha": "ec1e6baae7d86c7843c22938c75979c016f5c6e9"
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ export default defineSchema({
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Here are the valid Convex types along with their respective validators:
|
- Here are the valid Convex types along with their respective validators:
|
||||||
Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes |
|
| Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes |
|
||||||
| ----------- | ------------| -----------------------| -----------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
| ----------- | ----------- | -------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| Id | string | `doc._id` | `v.id(tableName)` | |
|
| Id | string | `doc._id` | `v.id(tableName)` | |
|
||||||
| Null | null | `null` | `v.null()` | JavaScript's `undefined` is not a valid Convex value. Functions the return `undefined` or do not return will return `null` when called from a client. Use `null` instead. |
|
| Null | null | `null` | `v.null()` | JavaScript's `undefined` is not a valid Convex value. Functions the return `undefined` or do not return will return `null` when called from a client. Use `null` instead. |
|
||||||
| Int64 | bigint | `3n` | `v.int64()` | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in most modern browsers. |
|
| Int64 | bigint | `3n` | `v.int64()` | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in most modern browsers. |
|
||||||
@@ -77,8 +77,9 @@ export default defineSchema({
|
|||||||
| String | string | `"abc"` | `v.string()` | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. |
|
| String | string | `"abc"` | `v.string()` | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. |
|
||||||
| Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. |
|
| Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. |
|
||||||
| Array | Array | `[1, 3.2, "abc"]` | `v.array(values)` | Arrays can have at most 8192 values. |
|
| Array | Array | `[1, 3.2, "abc"]` | `v.array(values)` | Arrays can have at most 8192 values. |
|
||||||
| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". |
|
| Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "\_". |
|
||||||
| Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "\_". |
|
|
||||||
|
| Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "\_". |
|
||||||
|
|
||||||
### Function registration
|
### Function registration
|
||||||
|
|
||||||
@@ -317,6 +318,9 @@ export default app;
|
|||||||
- Component reads and writes participate in the calling mutation's transaction. When a component mirrors state from one of your tables (like an aggregate over a table), update the component in the SAME mutation as every insert, patch, replace, or delete of that table - never from a separate function - so the two can never drift.
|
- Component reads and writes participate in the calling mutation's transaction. When a component mirrors state from one of your tables (like an aggregate over a table), update the component in the SAME mutation as every insert, patch, replace, or delete of that table - never from a separate function - so the two can never drift.
|
||||||
- To author a LOCAL component: a directory under `convex/` with its own `convex.config.ts` (`export default defineComponent("myName");` - the argument is the name string), its own `schema.ts`, and functions built from that directory's own `_generated/server`. Mount it from the root config (`app.use(myName)` - no options), and reference its functions through the generated `components` object INCLUDING the module segment: a function in `convex/myName/index.ts` is `components.myName.index.myFunction`, never `components.myName.myFunction`.
|
- To author a LOCAL component: a directory under `convex/` with its own `convex.config.ts` (`export default defineComponent("myName");` - the argument is the name string), its own `schema.ts`, and functions built from that directory's own `_generated/server`. Mount it from the root config (`app.use(myName)` - no options), and reference its functions through the generated `components` object INCLUDING the module segment: a function in `convex/myName/index.ts` is `components.myName.index.myFunction`, never `components.myName.myFunction`.
|
||||||
- For per-key quotas, cooldowns, or throttling (N operations per period, retry-after), use the `@convex-dev/rate-limiter` component - hand-rolled counter or window-scan implementations admit races under concurrency and lose quota when a mutation fails.
|
- For per-key quotas, cooldowns, or throttling (N operations per period, retry-after), use the `@convex-dev/rate-limiter` component - hand-rolled counter or window-scan implementations admit races under concurrency and lose quota when a mutation fails.
|
||||||
|
- For chat or assistant features where an LLM replies inside a durable conversation - per-user resumable histories, recorded tool-call steps, several assistants sharing one conversation - use the `@convex-dev/agent` component: mount it, create one component thread per conversation, and generate/read through it (`createThread(ctx, components.agent, ...)`, `new Agent(components.agent, { name, languageModel, tools }).generateText(ctx, { threadId }, { prompt })`, `listMessages`). Do not hand-roll a messages table or call an LLM SDK directly from your functions for these.
|
||||||
|
- For async Convex functions needing bounded parallelism, serialized mutation work, or completion callbacks, use `@convex-dev/workpool`; retry only idempotent actions.
|
||||||
|
- For ephemeral presence - who is online/viewing/typing in a room, tracked by client heartbeats with session tokens, multi-session aggregation (one entry per user across tabs), and timeout-to-offline - use the `@convex-dev/presence` component - hand-rolled lastSeen tables need wall-clock query filters that go stale, and per-session rows break the one-entry-per-user contract.
|
||||||
- Calling a component mutation is a subtransaction: if it throws and the caller catches the error, the component's writes roll back while the calling mutation continues and can still commit its own writes.
|
- Calling a component mutation is a subtransaction: if it throws and the caller catches the error, the component's writes roll back while the calling mutation continues and can still commit its own writes.
|
||||||
- To pass a function across a component boundary, mint a handle in the app: `const handle = await createFunctionHandle(internal.index.myCallback);` (from `convex/server`; async, takes only the function reference - `getFunctionHandle` and `getFunctionName` are not this API). Send it as a string; the receiver casts it back and invokes it: `await ctx.runMutation(args.handle as FunctionHandle<"mutation">, callbackArgs);`.
|
- To pass a function across a component boundary, mint a handle in the app: `const handle = await createFunctionHandle(internal.index.myCallback);` (from `convex/server`; async, takes only the function reference - `getFunctionHandle` and `getFunctionName` are not this API). Send it as a string; the receiver casts it back and invokes it: `await ctx.runMutation(args.handle as FunctionHandle<"mutation">, callbackArgs);`.
|
||||||
|
|
||||||
|
|||||||
12
packages/backend/convex/_generated/api.d.ts
vendored
12
packages/backend/convex/_generated/api.d.ts
vendored
@@ -8,7 +8,10 @@
|
|||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import type * as agentWorkspace from "../agentWorkspace.js";
|
||||||
|
import type * as artifactModel from "../artifactModel.js";
|
||||||
import type * as auth from "../auth.js";
|
import type * as auth from "../auth.js";
|
||||||
|
import type * as authz from "../authz.js";
|
||||||
import type * as daemonCommands from "../daemonCommands.js";
|
import type * as daemonCommands from "../daemonCommands.js";
|
||||||
import type * as daemonRuntime from "../daemonRuntime.js";
|
import type * as daemonRuntime from "../daemonRuntime.js";
|
||||||
import type * as daemons from "../daemons.js";
|
import type * as daemons from "../daemons.js";
|
||||||
@@ -16,6 +19,9 @@ import type * as fluePersistence from "../fluePersistence.js";
|
|||||||
import type * as healthCheck from "../healthCheck.js";
|
import type * as healthCheck from "../healthCheck.js";
|
||||||
import type * as http from "../http.js";
|
import type * as http from "../http.js";
|
||||||
import type * as privateData from "../privateData.js";
|
import type * as privateData from "../privateData.js";
|
||||||
|
import type * as projectArtifacts from "../projectArtifacts.js";
|
||||||
|
import type * as projectIssues from "../projectIssues.js";
|
||||||
|
import type * as projects from "../projects.js";
|
||||||
import type * as todos from "../todos.js";
|
import type * as todos from "../todos.js";
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@@ -25,7 +31,10 @@ import type {
|
|||||||
} from "convex/server";
|
} from "convex/server";
|
||||||
|
|
||||||
declare const fullApi: ApiFromModules<{
|
declare const fullApi: ApiFromModules<{
|
||||||
|
agentWorkspace: typeof agentWorkspace;
|
||||||
|
artifactModel: typeof artifactModel;
|
||||||
auth: typeof auth;
|
auth: typeof auth;
|
||||||
|
authz: typeof authz;
|
||||||
daemonCommands: typeof daemonCommands;
|
daemonCommands: typeof daemonCommands;
|
||||||
daemonRuntime: typeof daemonRuntime;
|
daemonRuntime: typeof daemonRuntime;
|
||||||
daemons: typeof daemons;
|
daemons: typeof daemons;
|
||||||
@@ -33,6 +42,9 @@ declare const fullApi: ApiFromModules<{
|
|||||||
healthCheck: typeof healthCheck;
|
healthCheck: typeof healthCheck;
|
||||||
http: typeof http;
|
http: typeof http;
|
||||||
privateData: typeof privateData;
|
privateData: typeof privateData;
|
||||||
|
projectArtifacts: typeof projectArtifacts;
|
||||||
|
projectIssues: typeof projectIssues;
|
||||||
|
projects: typeof projects;
|
||||||
todos: typeof todos;
|
todos: typeof todos;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|||||||
174
packages/backend/convex/agentWorkspace.ts
Normal file
174
packages/backend/convex/agentWorkspace.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import { env } from "@code/env/convex";
|
||||||
|
import { ConvexError, v } from "convex/values";
|
||||||
|
|
||||||
|
import { mutation, query } from "./_generated/server";
|
||||||
|
import { artifactPath } from "./artifactModel";
|
||||||
|
|
||||||
|
const requireAgent = (token: string) => {
|
||||||
|
if (token !== env.FLUE_DB_TOKEN) {
|
||||||
|
throw new ConvexError("Invalid agent control token");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const agentStatus = v.union(
|
||||||
|
v.literal("working"),
|
||||||
|
v.literal("needs-input"),
|
||||||
|
v.literal("completed"),
|
||||||
|
v.literal("failed")
|
||||||
|
);
|
||||||
|
|
||||||
|
export const get = query({
|
||||||
|
args: { issueId: v.id("projectIssues"), token: v.string() },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
requireAgent(args.token);
|
||||||
|
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||||
|
if (!issue) {
|
||||||
|
throw new ConvexError("Issue not found");
|
||||||
|
}
|
||||||
|
const project = await ctx.db.get("projects", issue.projectId);
|
||||||
|
if (!project) {
|
||||||
|
throw new ConvexError("Project not found");
|
||||||
|
}
|
||||||
|
const artifacts = await ctx.db
|
||||||
|
.query("projectArtifacts")
|
||||||
|
.withIndex("by_project", (q) => q.eq("projectId", project._id))
|
||||||
|
.take(25);
|
||||||
|
return { artifacts, issue, project };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ensureRun = mutation({
|
||||||
|
args: {
|
||||||
|
daemonId: v.string(),
|
||||||
|
issueId: v.id("projectIssues"),
|
||||||
|
token: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
requireAgent(args.token);
|
||||||
|
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||||
|
if (!issue) {
|
||||||
|
throw new ConvexError("Issue not found");
|
||||||
|
}
|
||||||
|
const existing = await ctx.db
|
||||||
|
.query("projectWorkRuns")
|
||||||
|
.withIndex("by_issue", (q) => q.eq("issueId", issue._id))
|
||||||
|
.unique();
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const actorKey = [
|
||||||
|
"project",
|
||||||
|
String(issue.projectId),
|
||||||
|
"issue",
|
||||||
|
String(issue._id),
|
||||||
|
];
|
||||||
|
const runId = await ctx.db.insert("projectWorkRuns", {
|
||||||
|
actorKey,
|
||||||
|
agentId: String(issue._id),
|
||||||
|
createdAt: timestamp,
|
||||||
|
daemonId: args.daemonId,
|
||||||
|
issueId: issue._id,
|
||||||
|
projectId: issue.projectId,
|
||||||
|
status: "queued",
|
||||||
|
updatedAt: timestamp,
|
||||||
|
});
|
||||||
|
await ctx.db.insert("projectSignals", {
|
||||||
|
createdAt: timestamp,
|
||||||
|
data: { actorKey, daemonId: args.daemonId },
|
||||||
|
issueId: issue._id,
|
||||||
|
kind: "agent.workspace.created",
|
||||||
|
projectId: issue.projectId,
|
||||||
|
runId,
|
||||||
|
});
|
||||||
|
return await ctx.db.get("projectWorkRuns", runId);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateArtifact = mutation({
|
||||||
|
args: {
|
||||||
|
content: v.string(),
|
||||||
|
issueId: v.id("projectIssues"),
|
||||||
|
path: artifactPath,
|
||||||
|
token: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
requireAgent(args.token);
|
||||||
|
if (args.content.length > 200_000) {
|
||||||
|
throw new ConvexError("Artifact content exceeds 200000 characters");
|
||||||
|
}
|
||||||
|
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||||
|
if (!issue) {
|
||||||
|
throw new ConvexError("Issue not found");
|
||||||
|
}
|
||||||
|
const artifact = await ctx.db
|
||||||
|
.query("projectArtifacts")
|
||||||
|
.withIndex("by_project_and_path", (q) =>
|
||||||
|
q.eq("projectId", issue.projectId).eq("path", args.path)
|
||||||
|
)
|
||||||
|
.unique();
|
||||||
|
if (!artifact) {
|
||||||
|
throw new ConvexError(`Artifact ${args.path} not found`);
|
||||||
|
}
|
||||||
|
const timestamp = Date.now();
|
||||||
|
await ctx.db.patch("projectArtifacts", artifact._id, {
|
||||||
|
content: args.content,
|
||||||
|
revision: artifact.revision + 1,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
});
|
||||||
|
await ctx.db.insert("projectSignals", {
|
||||||
|
createdAt: timestamp,
|
||||||
|
data: { path: args.path, revision: artifact.revision + 1 },
|
||||||
|
issueId: issue._id,
|
||||||
|
kind: "artifact.updated",
|
||||||
|
projectId: issue.projectId,
|
||||||
|
});
|
||||||
|
return artifact.revision + 1;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setStatus = mutation({
|
||||||
|
args: {
|
||||||
|
issueId: v.id("projectIssues"),
|
||||||
|
status: agentStatus,
|
||||||
|
summary: v.optional(v.string()),
|
||||||
|
token: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
requireAgent(args.token);
|
||||||
|
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||||
|
if (!issue) {
|
||||||
|
throw new ConvexError("Issue not found");
|
||||||
|
}
|
||||||
|
const run = await ctx.db
|
||||||
|
.query("projectWorkRuns")
|
||||||
|
.withIndex("by_issue", (q) => q.eq("issueId", issue._id))
|
||||||
|
.unique();
|
||||||
|
if (!run) {
|
||||||
|
throw new ConvexError("Agent work run not found");
|
||||||
|
}
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const terminal = args.status === "completed" || args.status === "failed";
|
||||||
|
await ctx.db.patch("projectIssues", issue._id, {
|
||||||
|
status: args.status,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
});
|
||||||
|
await ctx.db.patch("projectWorkRuns", run._id, {
|
||||||
|
status: args.status,
|
||||||
|
summary: args.summary?.slice(0, 4000),
|
||||||
|
updatedAt: timestamp,
|
||||||
|
...(args.status === "working" && run.startedAt === undefined
|
||||||
|
? { startedAt: timestamp }
|
||||||
|
: {}),
|
||||||
|
...(terminal ? { completedAt: timestamp } : {}),
|
||||||
|
});
|
||||||
|
await ctx.db.insert("projectSignals", {
|
||||||
|
createdAt: timestamp,
|
||||||
|
data: { summary: args.summary?.slice(0, 1000) },
|
||||||
|
issueId: issue._id,
|
||||||
|
kind: `agent.${args.status}`,
|
||||||
|
projectId: issue.projectId,
|
||||||
|
runId: run._id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
100
packages/backend/convex/artifactModel.ts
Normal file
100
packages/backend/convex/artifactModel.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { v } from "convex/values";
|
||||||
|
|
||||||
|
export const ARTIFACT_PATHS = [
|
||||||
|
"project.md",
|
||||||
|
"business.md",
|
||||||
|
"design.md",
|
||||||
|
"agent.md",
|
||||||
|
"work.md",
|
||||||
|
"steps.md",
|
||||||
|
"artifacts.md",
|
||||||
|
"signals.md",
|
||||||
|
"agent-manager.md",
|
||||||
|
"context.md",
|
||||||
|
"card.md",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type ArtifactPath = (typeof ARTIFACT_PATHS)[number];
|
||||||
|
|
||||||
|
export const artifactPath = v.union(
|
||||||
|
v.literal("project.md"),
|
||||||
|
v.literal("business.md"),
|
||||||
|
v.literal("design.md"),
|
||||||
|
v.literal("agent.md"),
|
||||||
|
v.literal("work.md"),
|
||||||
|
v.literal("steps.md"),
|
||||||
|
v.literal("artifacts.md"),
|
||||||
|
v.literal("signals.md"),
|
||||||
|
v.literal("agent-manager.md"),
|
||||||
|
v.literal("context.md"),
|
||||||
|
v.literal("card.md")
|
||||||
|
);
|
||||||
|
|
||||||
|
interface ProjectSeed {
|
||||||
|
readonly defaultBranch: string;
|
||||||
|
readonly description?: string;
|
||||||
|
readonly name: string;
|
||||||
|
readonly repoName: string;
|
||||||
|
readonly repoOwner: string;
|
||||||
|
readonly repoUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createInitialArtifacts = (
|
||||||
|
project: ProjectSeed
|
||||||
|
): readonly { path: ArtifactPath; content: string }[] => {
|
||||||
|
const repository = `${project.repoOwner}/${project.repoName}`;
|
||||||
|
const purpose = project.description ?? `Maintain and improve ${repository}.`;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
content: `# ${project.name}\n\nRepository: [${repository}](${project.repoUrl})\n\n## Project knowledge\n\n- [Business](business.md)\n- [Design](design.md)\n- [Agent](agent.md)\n\n## Delivery\n\n- [Work](work.md)\n- [Steps](steps.md)\n- [Artifacts](artifacts.md)\n`,
|
||||||
|
path: "project.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: `# Business\n\n## Purpose\n\n${purpose}\n\n## Source of truth\n\nThe connected GitHub repository and its project issues define the current product work.\n`,
|
||||||
|
path: "business.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: `# Design\n\n## Repository boundary\n\nWork targets \`${repository}\` on \`${project.defaultBranch}\`. Preserve its established architecture and conventions before introducing new patterns.\n\n## Decision record\n\nRecord issue-specific architecture decisions here when they affect later work.\n`,
|
||||||
|
path: "design.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: `# Agent\n\n## Role\n\nOwn one project issue at a time. Read the project artifacts before editing code, ask a focused question when blocked, and support completion claims with command output.\n\n## Guardrails\n\n- Keep issue work isolated in its AgentOS workspace.\n- Update work.md, steps.md, artifacts.md, and context.md as facts change.\n- Never mark work complete without a relevant runtime check.\n`,
|
||||||
|
path: "agent.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"# Work\n\nNo issue has entered the agent queue yet. New issues are appended here by the control plane.\n",
|
||||||
|
path: "work.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"# Steps\n\n1. Read the issue and project artifacts.\n2. Inspect the repository context.\n3. Ask for missing decisions only when work cannot continue safely.\n4. Implement the smallest complete change.\n5. Run the relevant command or scenario.\n6. Publish changed artifacts and the final signal.\n",
|
||||||
|
path: "steps.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"# Artifacts\n\nThis file indexes concrete outputs from issue runs, including changed paths, command results, and preview links.\n",
|
||||||
|
path: "artifacts.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"# Signals\n\nThe agent manager emits `issue.queued`, `agent.started`, `agent.needs-input`, `agent.completed`, and `agent.failed`. Signals are durable project events and drive the web status.\n",
|
||||||
|
path: "signals.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"# Agent manager\n\n## State machine\n\n`open -> queued -> working -> completed`\n\nA blocked run moves from `working` to `needs-input`; a resumed run returns to `queued`. An unrecoverable run moves to `failed`.\n\nEach issue owns one Flue agent identity and one AgentOS actor key, so conversation and workspace state remain isolated.\n",
|
||||||
|
path: "agent-manager.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: `# Context\n\n- Provider: GitHub\n- Repository: ${repository}\n- URL: ${project.repoUrl}\n- Default branch: ${project.defaultBranch}\n\nIssue-specific facts belong below this repository context.\n`,
|
||||||
|
path: "context.md",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content:
|
||||||
|
"# Project card\n\nThe web project card is the interface between UI and packages. It reads `projects.list`, `project-artifacts.list`, and `project-issues.list`; it writes through `projects.connectGitHub`, `project-issues.create`, and `project-issues.begin`. The issue-scoped Flue agent reads `agent-workspace.get`, publishes with `agent-workspace.updateArtifact`, and reports state with `agent-workspace.setStatus`. Filesystem and shell operations are executed by the AgentOS sandbox adapter through durable daemon commands.\n",
|
||||||
|
path: "card.md",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
17
packages/backend/convex/authz.ts
Normal file
17
packages/backend/convex/authz.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { ConvexError } from "convex/values";
|
||||||
|
|
||||||
|
export interface AuthContext {
|
||||||
|
readonly auth: {
|
||||||
|
readonly getUserIdentity: () => Promise<{
|
||||||
|
readonly tokenIdentifier: string;
|
||||||
|
} | null>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const requireOwnerId = async (ctx: AuthContext): Promise<string> => {
|
||||||
|
const identity = await ctx.auth.getUserIdentity();
|
||||||
|
if (!identity) {
|
||||||
|
throw new ConvexError("Authentication required");
|
||||||
|
}
|
||||||
|
return identity.tokenIdentifier;
|
||||||
|
};
|
||||||
@@ -52,6 +52,11 @@ export const list = query({
|
|||||||
.take(100);
|
.take(100);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
export const get = query({
|
||||||
|
args: { commandId: v.id("daemonCommands") },
|
||||||
|
handler: async (ctx, args) =>
|
||||||
|
await ctx.db.get("daemonCommands", args.commandId),
|
||||||
|
});
|
||||||
|
|
||||||
export const available = query({
|
export const available = query({
|
||||||
args: { daemonId: v.string() },
|
args: { daemonId: v.string() },
|
||||||
|
|||||||
19
packages/backend/convex/projectArtifacts.ts
Normal file
19
packages/backend/convex/projectArtifacts.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { v } from "convex/values";
|
||||||
|
|
||||||
|
import { query } from "./_generated/server";
|
||||||
|
import { requireOwnerId } from "./authz";
|
||||||
|
|
||||||
|
export const list = query({
|
||||||
|
args: { projectId: v.id("projects") },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const ownerId = await requireOwnerId(ctx);
|
||||||
|
const project = await ctx.db.get("projects", args.projectId);
|
||||||
|
if (!project || project.ownerId !== ownerId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return await ctx.db
|
||||||
|
.query("projectArtifacts")
|
||||||
|
.withIndex("by_project", (q) => q.eq("projectId", args.projectId))
|
||||||
|
.take(25);
|
||||||
|
},
|
||||||
|
});
|
||||||
149
packages/backend/convex/projectIssues.ts
Normal file
149
packages/backend/convex/projectIssues.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { ConvexError, v } from "convex/values";
|
||||||
|
|
||||||
|
import type { Id } from "./_generated/dataModel";
|
||||||
|
import { mutation, query } from "./_generated/server";
|
||||||
|
import type { MutationCtx } from "./_generated/server";
|
||||||
|
import { requireOwnerId } from "./authz";
|
||||||
|
|
||||||
|
const requireProjectOwner = async (
|
||||||
|
ctx: MutationCtx,
|
||||||
|
projectId: Id<"projects">,
|
||||||
|
ownerId: string
|
||||||
|
) => {
|
||||||
|
const project = await ctx.db.get("projects", projectId);
|
||||||
|
if (!project || project.ownerId !== ownerId) {
|
||||||
|
throw new ConvexError("Project not found");
|
||||||
|
}
|
||||||
|
return project;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const create = mutation({
|
||||||
|
args: {
|
||||||
|
body: v.string(),
|
||||||
|
projectId: v.id("projects"),
|
||||||
|
title: v.string(),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const ownerId = await requireOwnerId(ctx);
|
||||||
|
await requireProjectOwner(ctx, args.projectId, ownerId);
|
||||||
|
const title = args.title.trim();
|
||||||
|
const body = args.body.trim();
|
||||||
|
if (title.length < 3 || title.length > 160) {
|
||||||
|
throw new ConvexError("Issue title must be between 3 and 160 characters");
|
||||||
|
}
|
||||||
|
if (body.length < 10 || body.length > 10_000) {
|
||||||
|
throw new ConvexError(
|
||||||
|
"Issue description must be between 10 and 10000 characters"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const latest = await ctx.db
|
||||||
|
.query("projectIssues")
|
||||||
|
.withIndex("by_project_and_number", (q) =>
|
||||||
|
q.eq("projectId", args.projectId)
|
||||||
|
)
|
||||||
|
.order("desc")
|
||||||
|
.first();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const number = (latest?.number ?? 0) + 1;
|
||||||
|
const issueId = await ctx.db.insert("projectIssues", {
|
||||||
|
body,
|
||||||
|
createdAt: timestamp,
|
||||||
|
number,
|
||||||
|
projectId: args.projectId,
|
||||||
|
status: "open",
|
||||||
|
title,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
});
|
||||||
|
const workArtifact = await ctx.db
|
||||||
|
.query("projectArtifacts")
|
||||||
|
.withIndex("by_project_and_path", (q) =>
|
||||||
|
q.eq("projectId", args.projectId).eq("path", "work.md")
|
||||||
|
)
|
||||||
|
.unique();
|
||||||
|
if (workArtifact) {
|
||||||
|
await ctx.db.patch("projectArtifacts", workArtifact._id, {
|
||||||
|
content: `${workArtifact.content}\n## Issue ${number}: ${title}\n\nStatus: open\n\n${body}\n`,
|
||||||
|
revision: workArtifact.revision + 1,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await ctx.db.insert("projectSignals", {
|
||||||
|
createdAt: timestamp,
|
||||||
|
data: { number, title },
|
||||||
|
issueId,
|
||||||
|
kind: "issue.created",
|
||||||
|
projectId: args.projectId,
|
||||||
|
});
|
||||||
|
return issueId;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const begin = mutation({
|
||||||
|
args: { issueId: v.id("projectIssues") },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const ownerId = await requireOwnerId(ctx);
|
||||||
|
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||||
|
if (!issue) {
|
||||||
|
throw new ConvexError("Issue not found");
|
||||||
|
}
|
||||||
|
await requireProjectOwner(ctx, issue.projectId, ownerId);
|
||||||
|
if (issue.status === "queued" || issue.status === "working") {
|
||||||
|
return issue.status;
|
||||||
|
}
|
||||||
|
const timestamp = Date.now();
|
||||||
|
await ctx.db.patch("projectIssues", issue._id, {
|
||||||
|
status: "queued",
|
||||||
|
updatedAt: timestamp,
|
||||||
|
});
|
||||||
|
await ctx.db.insert("projectSignals", {
|
||||||
|
createdAt: timestamp,
|
||||||
|
data: { number: issue.number },
|
||||||
|
issueId: issue._id,
|
||||||
|
kind: "issue.queued",
|
||||||
|
projectId: issue.projectId,
|
||||||
|
});
|
||||||
|
return "queued" as const;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const markDispatchFailed = mutation({
|
||||||
|
args: { error: v.string(), issueId: v.id("projectIssues") },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const ownerId = await requireOwnerId(ctx);
|
||||||
|
const issue = await ctx.db.get("projectIssues", args.issueId);
|
||||||
|
if (!issue) {
|
||||||
|
throw new ConvexError("Issue not found");
|
||||||
|
}
|
||||||
|
await requireProjectOwner(ctx, issue.projectId, ownerId);
|
||||||
|
const timestamp = Date.now();
|
||||||
|
await ctx.db.patch("projectIssues", issue._id, {
|
||||||
|
status: "failed",
|
||||||
|
updatedAt: timestamp,
|
||||||
|
});
|
||||||
|
await ctx.db.insert("projectSignals", {
|
||||||
|
createdAt: timestamp,
|
||||||
|
data: { error: args.error.slice(0, 1000), phase: "dispatch" },
|
||||||
|
issueId: issue._id,
|
||||||
|
kind: "agent.failed",
|
||||||
|
projectId: issue.projectId,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const list = query({
|
||||||
|
args: { projectId: v.id("projects") },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const ownerId = await requireOwnerId(ctx);
|
||||||
|
const project = await ctx.db.get("projects", args.projectId);
|
||||||
|
if (!project || project.ownerId !== ownerId) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return await ctx.db
|
||||||
|
.query("projectIssues")
|
||||||
|
.withIndex("by_project_and_number", (q) =>
|
||||||
|
q.eq("projectId", args.projectId)
|
||||||
|
)
|
||||||
|
.order("desc")
|
||||||
|
.take(100);
|
||||||
|
},
|
||||||
|
});
|
||||||
176
packages/backend/convex/projects.ts
Normal file
176
packages/backend/convex/projects.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { ConvexError, v } from "convex/values";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { internal } from "./_generated/api";
|
||||||
|
import type { Id } from "./_generated/dataModel";
|
||||||
|
import { action, internalMutation, query } from "./_generated/server";
|
||||||
|
import { createInitialArtifacts } from "./artifactModel";
|
||||||
|
import { requireOwnerId } from "./authz";
|
||||||
|
|
||||||
|
const gitHubRepositorySchema = z.object({
|
||||||
|
default_branch: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
html_url: z.url(),
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
owner: z.object({ login: z.string() }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const parseRepository = (value: string): { owner: string; repo: string } => {
|
||||||
|
const normalized = value.trim().replace(/\.git$/u, "");
|
||||||
|
const match = normalized.match(
|
||||||
|
/^(?:https?:\/\/github\.com\/)?(?<owner>[^/\s]+)\/(?<repo>[^/\s]+)$/iu
|
||||||
|
);
|
||||||
|
if (!match?.groups?.owner || !match.groups.repo) {
|
||||||
|
throw new ConvexError("Use a GitHub repository in owner/name format");
|
||||||
|
}
|
||||||
|
return { owner: match.groups.owner, repo: match.groups.repo };
|
||||||
|
};
|
||||||
|
|
||||||
|
const readGitHubRepository = (value: unknown) => {
|
||||||
|
const result = gitHubRepositorySchema.safeParse(value);
|
||||||
|
if (!result.success) {
|
||||||
|
throw new ConvexError("GitHub returned incomplete repository metadata");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
defaultBranch: result.data.default_branch,
|
||||||
|
...(result.data.description === null
|
||||||
|
? {}
|
||||||
|
: { description: result.data.description }),
|
||||||
|
id: result.data.id,
|
||||||
|
name: result.data.name,
|
||||||
|
owner: result.data.owner.login,
|
||||||
|
url: result.data.html_url,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectGitHub = action({
|
||||||
|
args: { repository: v.string() },
|
||||||
|
handler: async (ctx, args): Promise<Id<"projects">> => {
|
||||||
|
const ownerId = await requireOwnerId(ctx);
|
||||||
|
const { owner, repo } = parseRepository(args.repository);
|
||||||
|
const response = await fetch(
|
||||||
|
`https://api.github.com/repos/${owner}/${repo}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"User-Agent": "zopu-code",
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
throw new ConvexError(
|
||||||
|
"Repository not found. This first flow supports public GitHub repositories."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw new ConvexError(`GitHub connection failed with ${response.status}`);
|
||||||
|
}
|
||||||
|
const repository = readGitHubRepository(await response.json());
|
||||||
|
const projectId: Id<"projects"> = await ctx.runMutation(
|
||||||
|
internal.projects.storeGitHubProject,
|
||||||
|
{ ownerId, repository }
|
||||||
|
);
|
||||||
|
return projectId;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const storeGitHubProject = internalMutation({
|
||||||
|
args: {
|
||||||
|
ownerId: v.string(),
|
||||||
|
repository: v.object({
|
||||||
|
defaultBranch: v.string(),
|
||||||
|
description: v.optional(v.string()),
|
||||||
|
id: v.number(),
|
||||||
|
name: v.string(),
|
||||||
|
owner: v.string(),
|
||||||
|
url: v.string(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const existing = await ctx.db
|
||||||
|
.query("projects")
|
||||||
|
.withIndex("by_owner_and_repository", (q) =>
|
||||||
|
q
|
||||||
|
.eq("ownerId", args.ownerId)
|
||||||
|
.eq("provider", "github")
|
||||||
|
.eq("repoOwner", args.repository.owner)
|
||||||
|
.eq("repoName", args.repository.name)
|
||||||
|
)
|
||||||
|
.unique();
|
||||||
|
const timestamp = Date.now();
|
||||||
|
if (existing) {
|
||||||
|
await ctx.db.patch("projects", existing._id, {
|
||||||
|
defaultBranch: args.repository.defaultBranch,
|
||||||
|
description: args.repository.description,
|
||||||
|
providerRepoId: args.repository.id,
|
||||||
|
repoUrl: args.repository.url,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
});
|
||||||
|
return existing._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectId = await ctx.db.insert("projects", {
|
||||||
|
createdAt: timestamp,
|
||||||
|
defaultBranch: args.repository.defaultBranch,
|
||||||
|
description: args.repository.description,
|
||||||
|
name: args.repository.name,
|
||||||
|
ownerId: args.ownerId,
|
||||||
|
provider: "github",
|
||||||
|
providerRepoId: args.repository.id,
|
||||||
|
repoName: args.repository.name,
|
||||||
|
repoOwner: args.repository.owner,
|
||||||
|
repoUrl: args.repository.url,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
});
|
||||||
|
const artifacts = createInitialArtifacts({
|
||||||
|
defaultBranch: args.repository.defaultBranch,
|
||||||
|
description: args.repository.description,
|
||||||
|
name: args.repository.name,
|
||||||
|
repoName: args.repository.name,
|
||||||
|
repoOwner: args.repository.owner,
|
||||||
|
repoUrl: args.repository.url,
|
||||||
|
});
|
||||||
|
await Promise.all(
|
||||||
|
artifacts.map(({ content, path }) =>
|
||||||
|
ctx.db.insert("projectArtifacts", {
|
||||||
|
content,
|
||||||
|
createdAt: timestamp,
|
||||||
|
path,
|
||||||
|
projectId,
|
||||||
|
revision: 1,
|
||||||
|
updatedAt: timestamp,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await ctx.db.insert("projectSignals", {
|
||||||
|
createdAt: timestamp,
|
||||||
|
data: { provider: "github", repository: args.repository.url },
|
||||||
|
kind: "project.connected",
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
return projectId;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const list = query({
|
||||||
|
args: {},
|
||||||
|
handler: async (ctx) => {
|
||||||
|
const ownerId = await requireOwnerId(ctx);
|
||||||
|
return await ctx.db
|
||||||
|
.query("projects")
|
||||||
|
.withIndex("by_owner_and_createdAt", (q) => q.eq("ownerId", ownerId))
|
||||||
|
.order("desc")
|
||||||
|
.take(50);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const get = query({
|
||||||
|
args: { projectId: v.id("projects") },
|
||||||
|
handler: async (ctx, args) => {
|
||||||
|
const ownerId = await requireOwnerId(ctx);
|
||||||
|
const project = await ctx.db.get("projects", args.projectId);
|
||||||
|
return project?.ownerId === ownerId ? project : null;
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -62,6 +62,86 @@ export default defineSchema({
|
|||||||
})
|
})
|
||||||
.index("by_daemonId_and_createdAt", ["daemonId", "createdAt"])
|
.index("by_daemonId_and_createdAt", ["daemonId", "createdAt"])
|
||||||
.index("by_commandId_and_createdAt", ["commandId", "createdAt"]),
|
.index("by_commandId_and_createdAt", ["commandId", "createdAt"]),
|
||||||
|
projects: defineTable({
|
||||||
|
ownerId: v.string(),
|
||||||
|
provider: v.literal("github"),
|
||||||
|
providerRepoId: v.number(),
|
||||||
|
name: v.string(),
|
||||||
|
description: v.optional(v.string()),
|
||||||
|
repoOwner: v.string(),
|
||||||
|
repoName: v.string(),
|
||||||
|
repoUrl: v.string(),
|
||||||
|
defaultBranch: v.string(),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("by_owner_and_createdAt", ["ownerId", "createdAt"])
|
||||||
|
.index("by_owner_and_repository", [
|
||||||
|
"ownerId",
|
||||||
|
"provider",
|
||||||
|
"repoOwner",
|
||||||
|
"repoName",
|
||||||
|
]),
|
||||||
|
projectArtifacts: defineTable({
|
||||||
|
projectId: v.id("projects"),
|
||||||
|
path: v.string(),
|
||||||
|
content: v.string(),
|
||||||
|
revision: v.number(),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("by_project", ["projectId"])
|
||||||
|
.index("by_project_and_path", ["projectId", "path"]),
|
||||||
|
projectIssues: defineTable({
|
||||||
|
projectId: v.id("projects"),
|
||||||
|
number: v.number(),
|
||||||
|
title: v.string(),
|
||||||
|
body: v.string(),
|
||||||
|
status: v.union(
|
||||||
|
v.literal("open"),
|
||||||
|
v.literal("queued"),
|
||||||
|
v.literal("working"),
|
||||||
|
v.literal("needs-input"),
|
||||||
|
v.literal("completed"),
|
||||||
|
v.literal("failed")
|
||||||
|
),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("by_project_and_number", ["projectId", "number"])
|
||||||
|
.index("by_project_and_status", ["projectId", "status"]),
|
||||||
|
projectWorkRuns: defineTable({
|
||||||
|
projectId: v.id("projects"),
|
||||||
|
issueId: v.id("projectIssues"),
|
||||||
|
agentId: v.string(),
|
||||||
|
daemonId: v.string(),
|
||||||
|
actorKey: v.array(v.string()),
|
||||||
|
status: v.union(
|
||||||
|
v.literal("ready"),
|
||||||
|
v.literal("queued"),
|
||||||
|
v.literal("working"),
|
||||||
|
v.literal("needs-input"),
|
||||||
|
v.literal("completed"),
|
||||||
|
v.literal("failed")
|
||||||
|
),
|
||||||
|
summary: v.optional(v.string()),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
startedAt: v.optional(v.number()),
|
||||||
|
completedAt: v.optional(v.number()),
|
||||||
|
})
|
||||||
|
.index("by_issue", ["issueId"])
|
||||||
|
.index("by_project_and_createdAt", ["projectId", "createdAt"]),
|
||||||
|
projectSignals: defineTable({
|
||||||
|
projectId: v.id("projects"),
|
||||||
|
issueId: v.optional(v.id("projectIssues")),
|
||||||
|
runId: v.optional(v.id("projectWorkRuns")),
|
||||||
|
kind: v.string(),
|
||||||
|
data: v.any(),
|
||||||
|
createdAt: v.number(),
|
||||||
|
})
|
||||||
|
.index("by_project_and_createdAt", ["projectId", "createdAt"])
|
||||||
|
.index("by_issue_and_createdAt", ["issueId", "createdAt"]),
|
||||||
todos: defineTable({
|
todos: defineTable({
|
||||||
text: v.string(),
|
text: v.string(),
|
||||||
completed: v.boolean(),
|
completed: v.boolean(),
|
||||||
@@ -117,11 +197,7 @@ export default defineSchema({
|
|||||||
settlementRecordId: v.optional(v.string()),
|
settlementRecordId: v.optional(v.string()),
|
||||||
settlementRecordJson: v.optional(v.string()),
|
settlementRecordJson: v.optional(v.string()),
|
||||||
settledOutcome: v.optional(
|
settledOutcome: v.optional(
|
||||||
v.union(
|
v.union(v.literal("completed"), v.literal("failed"), v.literal("aborted"))
|
||||||
v.literal("completed"),
|
|
||||||
v.literal("failed"),
|
|
||||||
v.literal("aborted")
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
updatedAt: v.number(),
|
updatedAt: v.number(),
|
||||||
})
|
})
|
||||||
@@ -155,8 +231,7 @@ export default defineSchema({
|
|||||||
nextOffset: v.number(),
|
nextOffset: v.number(),
|
||||||
closed: v.boolean(),
|
closed: v.boolean(),
|
||||||
createdAt: v.number(),
|
createdAt: v.number(),
|
||||||
})
|
}).index("by_path", ["path"]),
|
||||||
.index("by_path", ["path"]),
|
|
||||||
|
|
||||||
// Conversation-stream batches: one row per appended batch. Offset is
|
// Conversation-stream batches: one row per appended batch. Offset is
|
||||||
// 0-based; `seq` is the row's position in the stream.
|
// 0-based; `seq` is the row's position in the stream.
|
||||||
@@ -172,10 +247,12 @@ export default defineSchema({
|
|||||||
appendedAt: v.number(),
|
appendedAt: v.number(),
|
||||||
})
|
})
|
||||||
.index("by_path_and_seq", ["path", "seq"])
|
.index("by_path_and_seq", ["path", "seq"])
|
||||||
.index(
|
.index("by_path_producer_epoch_producerSequence", [
|
||||||
"by_path_producer_epoch_producerSequence",
|
"path",
|
||||||
["path", "producerId", "producerEpoch", "producerSequence"]
|
"producerId",
|
||||||
),
|
"producerEpoch",
|
||||||
|
"producerSequence",
|
||||||
|
]),
|
||||||
|
|
||||||
// Event-stream metadata: one row per stream path.
|
// Event-stream metadata: one row per stream path.
|
||||||
flueEventStreams: defineTable({
|
flueEventStreams: defineTable({
|
||||||
@@ -249,5 +326,5 @@ export default defineSchema({
|
|||||||
"streamPath",
|
"streamPath",
|
||||||
"conversationId",
|
"conversationId",
|
||||||
"attachmentId",
|
"attachmentId",
|
||||||
])
|
]),
|
||||||
});
|
});
|
||||||
|
|||||||
41
packages/backend/skills-lock.json
Normal file
41
packages/backend/skills-lock.json
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"skills": {
|
||||||
|
"convex": {
|
||||||
|
"source": "get-convex/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/convex/SKILL.md",
|
||||||
|
"computedHash": "c5f3622c64ef550aac27d1dbc041f0c7c40d9119863c9fb8bac180b0498ee8ed"
|
||||||
|
},
|
||||||
|
"convex-create-component": {
|
||||||
|
"source": "get-convex/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/convex-create-component/SKILL.md",
|
||||||
|
"computedHash": "012acb639fccc22a47e89ef69941689f9328ac9ff5b872d77af6328407ec8876"
|
||||||
|
},
|
||||||
|
"convex-migration-helper": {
|
||||||
|
"source": "get-convex/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/convex-migration-helper/SKILL.md",
|
||||||
|
"computedHash": "1ed5ef230d484e9884d881ddbd15158f0070382f8c6097364cb8ed2ec458decd"
|
||||||
|
},
|
||||||
|
"convex-performance-audit": {
|
||||||
|
"source": "get-convex/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/convex-performance-audit/SKILL.md",
|
||||||
|
"computedHash": "2eaf22b20f74ab39f336b41845b834b8796b6d2b380bc078a1ab54769c3f233c"
|
||||||
|
},
|
||||||
|
"convex-quickstart": {
|
||||||
|
"source": "get-convex/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/convex-quickstart/SKILL.md",
|
||||||
|
"computedHash": "54e5271d5d613cc4c0068baa250613b5abb6e4533df1e67adc26d2e0c7d68ded"
|
||||||
|
},
|
||||||
|
"convex-setup-auth": {
|
||||||
|
"source": "get-convex/agent-skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/convex-setup-auth/SKILL.md",
|
||||||
|
"computedHash": "b1a940758751c5b2fdc6ced105b19927a1655f0c1d4bd2fd5536dc3264202c00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
packages/env/src/agent.ts
vendored
3
packages/env/src/agent.ts
vendored
@@ -8,6 +8,9 @@ const agentEnvSchema = z.object({
|
|||||||
AGENT_MODEL_MAX_TOKENS: z.coerce.number().int().positive(),
|
AGENT_MODEL_MAX_TOKENS: z.coerce.number().int().positive(),
|
||||||
AGENT_MODEL_NAME: z.string().min(1),
|
AGENT_MODEL_NAME: z.string().min(1),
|
||||||
AGENT_MODEL_PROVIDER: z.string().min(1),
|
AGENT_MODEL_PROVIDER: z.string().min(1),
|
||||||
|
CONVEX_URL: z.url(),
|
||||||
|
DAEMON_ID: z.string().min(1),
|
||||||
|
FLUE_DB_TOKEN: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AgentEnv = z.infer<typeof agentEnvSchema>;
|
export type AgentEnv = z.infer<typeof agentEnvSchema>;
|
||||||
|
|||||||
236
packages/ui/src/components/field.tsx
Normal file
236
packages/ui/src/components/field.tsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
import { useMemo } from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@code/ui/lib/utils"
|
||||||
|
import { Label } from "@code/ui/components/label"
|
||||||
|
import { Separator } from "@code/ui/components/separator"
|
||||||
|
|
||||||
|
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
|
||||||
|
return (
|
||||||
|
<fieldset
|
||||||
|
data-slot="field-set"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldLegend({
|
||||||
|
className,
|
||||||
|
variant = "legend",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
|
||||||
|
return (
|
||||||
|
<legend
|
||||||
|
data-slot="field-legend"
|
||||||
|
data-variant={variant}
|
||||||
|
className={cn(
|
||||||
|
"mb-2.5 font-medium data-[variant=label]:text-xs data-[variant=legend]:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="field-group"
|
||||||
|
className={cn(
|
||||||
|
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldVariants = cva(
|
||||||
|
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
|
||||||
|
horizontal:
|
||||||
|
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||||
|
responsive:
|
||||||
|
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
orientation: "vertical",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
className,
|
||||||
|
orientation = "vertical",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="group"
|
||||||
|
data-slot="field"
|
||||||
|
data-orientation={orientation}
|
||||||
|
className={cn(fieldVariants({ orientation }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="field-content"
|
||||||
|
className={cn(
|
||||||
|
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Label>) {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
data-slot="field-label"
|
||||||
|
className={cn(
|
||||||
|
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded-none has-[>[data-slot=field]]:border *:data-[slot=field]:p-2 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
|
||||||
|
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="field-label"
|
||||||
|
className={cn(
|
||||||
|
"flex w-fit items-center gap-2 text-xs/relaxed group-data-[disabled=true]/field:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
data-slot="field-description"
|
||||||
|
className={cn(
|
||||||
|
"text-left text-xs/relaxed leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
||||||
|
"last:mt-0 nth-last-2:-mt-1",
|
||||||
|
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldSeparator({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
children?: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="field-separator"
|
||||||
|
data-content={!!children}
|
||||||
|
className={cn(
|
||||||
|
"relative -my-2 h-5 text-xs group-data-[variant=outline]/field-group:-mb-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Separator className="absolute inset-0 top-1/2" />
|
||||||
|
{children && (
|
||||||
|
<span
|
||||||
|
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
|
||||||
|
data-slot="field-separator-content"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldError({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
errors,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & {
|
||||||
|
errors?: Array<{ message?: string } | undefined>
|
||||||
|
}) {
|
||||||
|
const content = useMemo(() => {
|
||||||
|
if (children) {
|
||||||
|
return children
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errors?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueErrors = [
|
||||||
|
...new Map(errors.map((error) => [error?.message, error])).values(),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (uniqueErrors?.length == 1) {
|
||||||
|
return uniqueErrors[0]?.message
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="ml-4 flex list-disc flex-col gap-1">
|
||||||
|
{uniqueErrors.map(
|
||||||
|
(error, index) =>
|
||||||
|
error?.message && <li key={index}>{error.message}</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}, [children, errors])
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="alert"
|
||||||
|
data-slot="field-error"
|
||||||
|
className={cn("text-xs font-normal text-destructive", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Field,
|
||||||
|
FieldLabel,
|
||||||
|
FieldDescription,
|
||||||
|
FieldError,
|
||||||
|
FieldGroup,
|
||||||
|
FieldLegend,
|
||||||
|
FieldSeparator,
|
||||||
|
FieldSet,
|
||||||
|
FieldContent,
|
||||||
|
FieldTitle,
|
||||||
|
}
|
||||||
25
packages/ui/src/components/separator.tsx
Normal file
25
packages/ui/src/components/separator.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"
|
||||||
|
|
||||||
|
import { cn } from "@code/ui/lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
...props
|
||||||
|
}: SeparatorPrimitive.Props) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive
|
||||||
|
data-slot="separator"
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
Reference in New Issue
Block a user