mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-21 22:50:42 +08:00
feat: support packer config
This commit is contained in:
@@ -1,11 +1,18 @@
|
||||
import { PackageIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PackerCombobox } from "@/components/packer/packer-combobox";
|
||||
import { PackerCustomConfigFields } from "@/components/packer/packer-custom-config-fields";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { FieldLabel, FieldSet } from "@/components/ui/field";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Field, FieldLabel, FieldSet } from "@/components/ui/field";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import {
|
||||
findPackerEntry,
|
||||
getPackerDefaultConfig,
|
||||
getPackerSchemaFields,
|
||||
normalizePackerCategories,
|
||||
} from "@/lib/packer-schema";
|
||||
import type { PackerConfig } from "@/types/memshell";
|
||||
import type { MemShellFormSchema } from "@/types/schema";
|
||||
|
||||
@@ -28,25 +35,77 @@ export default function PackageConfigCard({
|
||||
name: "server",
|
||||
});
|
||||
|
||||
const options = useMemo(() => {
|
||||
const filteredOptions = (packerConfig ?? []).filter((name) => {
|
||||
if (!shellType || shellType === " ") {
|
||||
return true;
|
||||
const packingMethod = useWatch({
|
||||
control: form.control,
|
||||
name: "packingMethod",
|
||||
});
|
||||
|
||||
const categories = useMemo(
|
||||
() => normalizePackerCategories(packerConfig),
|
||||
[packerConfig],
|
||||
);
|
||||
|
||||
const filteredCategories = useMemo(() => {
|
||||
return categories
|
||||
.map((group) => ({
|
||||
...group,
|
||||
packers: group.packers.filter((packer) => {
|
||||
if (packer.categoryAnchor) {
|
||||
return false;
|
||||
}
|
||||
const name = packer.name;
|
||||
if (!shellType || shellType === " ") {
|
||||
return true;
|
||||
}
|
||||
if (shellType.startsWith("Agent")) {
|
||||
return name.startsWith("Agent");
|
||||
}
|
||||
if ((server ?? "").startsWith("XXL")) {
|
||||
return !name.startsWith("Agent");
|
||||
}
|
||||
return (
|
||||
!name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl")
|
||||
);
|
||||
}),
|
||||
}))
|
||||
.filter((group) => group.packers.length > 0);
|
||||
}, [categories, shellType, server]);
|
||||
|
||||
const allOptionNames = useMemo(
|
||||
() =>
|
||||
filteredCategories.flatMap((group) =>
|
||||
group.packers.map((packer) => packer.name),
|
||||
),
|
||||
[filteredCategories],
|
||||
);
|
||||
|
||||
const selectedPackerEntry = useMemo(
|
||||
() =>
|
||||
findPackerEntry(filteredCategories, packingMethod) ??
|
||||
findPackerEntry(categories, packingMethod),
|
||||
[categories, filteredCategories, packingMethod],
|
||||
);
|
||||
|
||||
const selectedSchemaFields = useMemo(
|
||||
() => getPackerSchemaFields(selectedPackerEntry),
|
||||
[selectedPackerEntry],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (allOptionNames.length > 0) {
|
||||
const current = form.getValues("packingMethod");
|
||||
if (!current || !allOptionNames.includes(current)) {
|
||||
form.setValue("packingMethod", allOptionNames[0]);
|
||||
}
|
||||
if (shellType.startsWith("Agent")) {
|
||||
return name.startsWith("Agent");
|
||||
}
|
||||
if (server.startsWith("XXL")) {
|
||||
return !name.startsWith("Agent");
|
||||
}
|
||||
return !name.startsWith("Agent") && !name.toLowerCase().startsWith("xxl");
|
||||
});
|
||||
form.setValue("packingMethod", filteredOptions[0]);
|
||||
return filteredOptions.map((name) => ({
|
||||
name: t(name),
|
||||
value: name,
|
||||
}));
|
||||
}, [packerConfig, shellType, server, t, form]);
|
||||
}
|
||||
}, [allOptionNames, form]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue(
|
||||
"packerCustomConfig",
|
||||
getPackerDefaultConfig(selectedPackerEntry) as any,
|
||||
);
|
||||
}, [form, selectedPackerEntry, packingMethod]);
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
@@ -57,32 +116,30 @@ export default function PackageConfigCard({
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{options.length > 0 ? (
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="packingMethod"
|
||||
render={({ field }) => (
|
||||
<FieldSet>
|
||||
<FieldLabel>{t("packerMethod")}</FieldLabel>
|
||||
<RadioGroup
|
||||
name={field.name}
|
||||
value={field.value}
|
||||
defaultValue={options[0].value}
|
||||
onValueChange={field.onChange}
|
||||
className="grid grid-cols-2 md:grid-cols-3"
|
||||
>
|
||||
{options.map(({ name, value }) => (
|
||||
<div key={value} className="flex items-center space-x-3">
|
||||
<RadioGroupItem value={value} id={value} />
|
||||
<FieldLabel className="text-xs" htmlFor={value}>
|
||||
{name}
|
||||
</FieldLabel>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</FieldSet>
|
||||
)}
|
||||
/>
|
||||
{allOptionNames.length > 0 ? (
|
||||
<>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="packingMethod"
|
||||
render={({ field }) => (
|
||||
<Field className="gap-1">
|
||||
<FieldLabel>{t("packerMethod")}</FieldLabel>
|
||||
<PackerCombobox
|
||||
categories={filteredCategories}
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t("selectPacker", {
|
||||
defaultValue: "Select packer",
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<PackerCustomConfigFields
|
||||
form={form}
|
||||
fields={selectedSchemaFields}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center p-4 gap-4 h-50">
|
||||
<Spinner />
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CodeViewer from "@/components/code-viewer";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { base64ToBytes, downloadBytes, downloadContent } from "@/lib/utils";
|
||||
|
||||
export function MultiPackResult({
|
||||
allPackResults,
|
||||
packMethod,
|
||||
shellClassName,
|
||||
height = 350,
|
||||
}: Readonly<{
|
||||
allPackResults: object | undefined;
|
||||
packMethod: string;
|
||||
shellClassName?: string;
|
||||
height?: number;
|
||||
}>) {
|
||||
const showCode = packMethod === "JSP";
|
||||
const { t } = useTranslation();
|
||||
const packResults = allPackResults as Record<string, string> | undefined;
|
||||
const packMethods = useMemo(
|
||||
() => Object.keys(packResults ?? {}),
|
||||
[packResults],
|
||||
);
|
||||
|
||||
const [selectedMethod, setSelectedMethod] = useState(
|
||||
() => packMethods[0] ?? "",
|
||||
);
|
||||
|
||||
const packResult = useMemo(() => {
|
||||
if (!selectedMethod) {
|
||||
return "";
|
||||
}
|
||||
return packResults?.[selectedMethod] ?? "";
|
||||
}, [packResults, selectedMethod]);
|
||||
|
||||
useEffect(() => {
|
||||
if (packMethods.length === 0) {
|
||||
if (selectedMethod !== "") {
|
||||
setSelectedMethod("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!packMethods.includes(selectedMethod)) {
|
||||
setSelectedMethod(packMethods[0]);
|
||||
}
|
||||
}, [packMethods, selectedMethod]);
|
||||
|
||||
const handleDownload = useCallback(() => {
|
||||
const fileName =
|
||||
shellClassName?.substring(shellClassName?.lastIndexOf(".") ?? 0) ?? "";
|
||||
if (packMethod === "JSP") {
|
||||
const fileExtension = selectedMethod.includes("JSPX") ? ".jspx" : ".jsp";
|
||||
const content = new Blob([packResult], { type: "text/plain" });
|
||||
return downloadContent(content, fileName, fileExtension);
|
||||
} else if (
|
||||
packMethod === "JavaDeserialize" ||
|
||||
packMethod.includes("Hessian")
|
||||
) {
|
||||
const content = new Blob([base64ToBytes(packResult)], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
return downloadContent(content, fileName, ".data");
|
||||
} else if (packMethod === "Base64") {
|
||||
const base64Content = packResults?.[packMethods[0]] ?? "";
|
||||
return downloadBytes(base64Content, shellClassName);
|
||||
}
|
||||
}, [
|
||||
packMethod,
|
||||
packMethods,
|
||||
packResult,
|
||||
packResults,
|
||||
selectedMethod,
|
||||
shellClassName,
|
||||
]);
|
||||
|
||||
return (
|
||||
<CodeViewer
|
||||
code={packResult ?? ""}
|
||||
header={
|
||||
<div className="flex items-center justify-between text-xs gap-2">
|
||||
<Select
|
||||
onValueChange={(value) => {
|
||||
setSelectedMethod(value as string);
|
||||
}}
|
||||
value={selectedMethod}
|
||||
>
|
||||
<SelectTrigger className="h-7 text-xs [&_svg]:h-4 [&_svg]:w-4">
|
||||
<span className="text-muted-foreground">
|
||||
{t("common:packerMethod")}:
|
||||
</span>
|
||||
<SelectValue data-placeholder={t("common:placeholders.select")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{packMethods.map((method) => (
|
||||
<SelectItem key={method} value={method} className="text-xs">
|
||||
{method}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-muted-foreground">({packResult?.length})</span>
|
||||
</div>
|
||||
}
|
||||
button={
|
||||
packMethod === "JSP" ||
|
||||
packMethod === "Base64" ||
|
||||
packMethod === "JavaDeserialize" ||
|
||||
packMethod.includes("Hessian") ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
type="button"
|
||||
className="h-7 w-7 [&_svg]:h-4 [&_svg]:w-4"
|
||||
onClick={handleDownload}
|
||||
>
|
||||
<DownloadIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
wrapLongLines={!showCode}
|
||||
showLineNumbers={showCode}
|
||||
language={showCode ? "java" : "text"}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -3,32 +3,19 @@ import CodeViewer from "@/components/code-viewer";
|
||||
import type { MemShellResult } from "@/types/memshell";
|
||||
import { AgentResult } from "./agent";
|
||||
import { JarResult } from "./jar-result";
|
||||
import { MultiPackResult } from "./multi-packer";
|
||||
|
||||
export function ResultComponent({
|
||||
packResult,
|
||||
allPackResults,
|
||||
packMethod,
|
||||
generateResult,
|
||||
}: Readonly<{
|
||||
packResult: string | undefined;
|
||||
allPackResults: Map<string, string> | undefined;
|
||||
packMethod: string;
|
||||
generateResult?: MemShellResult;
|
||||
}>) {
|
||||
const showCode = packMethod === "JSP";
|
||||
const isAgent = packMethod.startsWith("Agent");
|
||||
const isJar = packMethod.endsWith("Jar");
|
||||
const { t } = useTranslation();
|
||||
if (allPackResults) {
|
||||
return (
|
||||
<MultiPackResult
|
||||
allPackResults={allPackResults}
|
||||
packMethod={packMethod}
|
||||
shellClassName={generateResult?.injectorClassName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isAgent) {
|
||||
return (
|
||||
<AgentResult
|
||||
@@ -52,16 +39,16 @@ export function ResultComponent({
|
||||
<CodeViewer
|
||||
code={packResult ?? ""}
|
||||
header={
|
||||
<div className="flex items-center justify-between text-xs gap-2">
|
||||
<div className="flex items-center justify-between text-sm gap-2">
|
||||
<span>
|
||||
{t("common:packerMethod")}:{packMethod}
|
||||
</span>
|
||||
<span className="text-muted-foreground">({packResult?.length})</span>
|
||||
</div>
|
||||
}
|
||||
wrapLongLines={!showCode}
|
||||
showLineNumbers={showCode}
|
||||
language={showCode ? "java" : "text"}
|
||||
wrapLongLines={true}
|
||||
showLineNumbers={false}
|
||||
language={"text"}
|
||||
height={350}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -13,12 +13,10 @@ import { ResultComponent } from "./results/result-component";
|
||||
|
||||
export default function ShellResult({
|
||||
packResult,
|
||||
allPackResults,
|
||||
packMethod,
|
||||
generateResult,
|
||||
}: Readonly<{
|
||||
packResult: string | undefined;
|
||||
allPackResults: Map<string, string> | undefined;
|
||||
packMethod: string;
|
||||
generateResult?: MemShellResult;
|
||||
}>) {
|
||||
@@ -26,7 +24,7 @@ export default function ShellResult({
|
||||
if (!generateResult) {
|
||||
return <QuickUsage />;
|
||||
}
|
||||
const height = 800;
|
||||
const height = 600;
|
||||
return (
|
||||
<Tabs defaultValue="packResult">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
@@ -42,7 +40,6 @@ export default function ShellResult({
|
||||
<BasicInfo generateResult={generateResult} />
|
||||
<ResultComponent
|
||||
packResult={packResult}
|
||||
allPackResults={allPackResults}
|
||||
packMethod={packMethod}
|
||||
generateResult={generateResult}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxLabel,
|
||||
ComboboxList,
|
||||
} from "@/components/ui/combobox";
|
||||
import type { NormalizedPackerCategory } from "@/lib/packer-schema";
|
||||
|
||||
type PackerComboboxProps = {
|
||||
categories: NormalizedPackerCategory[];
|
||||
value?: string;
|
||||
onValueChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
emptyText?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type PreparedPacker = {
|
||||
name: string;
|
||||
label: string;
|
||||
searchText: string;
|
||||
};
|
||||
|
||||
type PreparedCategory = {
|
||||
name: string;
|
||||
label: string;
|
||||
packers: PreparedPacker[];
|
||||
};
|
||||
|
||||
export function PackerCombobox({
|
||||
categories,
|
||||
value,
|
||||
onValueChange,
|
||||
placeholder,
|
||||
emptyText,
|
||||
disabled,
|
||||
}: Readonly<PackerComboboxProps>) {
|
||||
const { t } = useTranslation("common");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
const preparedCategories = useMemo<PreparedCategory[]>(
|
||||
() =>
|
||||
categories.map((category) => {
|
||||
const categoryLabel = t(category.name, { defaultValue: category.name });
|
||||
return {
|
||||
name: category.name,
|
||||
label: categoryLabel,
|
||||
packers: category.packers.map((packer) => {
|
||||
const packerLabel = t(packer.name, { defaultValue: packer.name });
|
||||
return {
|
||||
name: packer.name,
|
||||
label: packerLabel,
|
||||
searchText:
|
||||
`${packer.name} ${packerLabel} ${category.name} ${categoryLabel}`.toLowerCase(),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
[categories, t],
|
||||
);
|
||||
|
||||
const selectedLabel = useMemo(() => {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
for (const category of preparedCategories) {
|
||||
const found = category.packers.find((packer) => packer.name === value);
|
||||
if (found) {
|
||||
return found.label;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}, [preparedCategories, value]);
|
||||
|
||||
const filteredCategories = useMemo(() => {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
if (!normalizedQuery) {
|
||||
return preparedCategories;
|
||||
}
|
||||
return preparedCategories
|
||||
.map((category) => ({
|
||||
...category,
|
||||
packers: category.packers.filter((packer) =>
|
||||
packer.searchText.includes(normalizedQuery),
|
||||
),
|
||||
}))
|
||||
.filter((category) => category.packers.length > 0);
|
||||
}, [preparedCategories, query]);
|
||||
|
||||
const resolvedPlaceholder =
|
||||
placeholder ?? t("selectPacker", { defaultValue: "Select packer" });
|
||||
return (
|
||||
<Combobox
|
||||
value={value ?? null}
|
||||
open={open}
|
||||
onOpenChange={(nextOpen) => {
|
||||
setOpen(nextOpen);
|
||||
setQuery("");
|
||||
}}
|
||||
inputValue={open ? query : selectedLabel}
|
||||
onInputValueChange={(nextValue) => {
|
||||
if (open) {
|
||||
setQuery(nextValue);
|
||||
}
|
||||
}}
|
||||
onValueChange={(nextValue) => {
|
||||
if (typeof nextValue === "string") {
|
||||
onValueChange(nextValue);
|
||||
}
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
}}
|
||||
autoComplete="none"
|
||||
autoHighlight={true}
|
||||
>
|
||||
<ComboboxInput
|
||||
className="w-full"
|
||||
placeholder={resolvedPlaceholder}
|
||||
disabled={disabled}
|
||||
showClear={false}
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxList>
|
||||
{filteredCategories.map((category) => (
|
||||
<ComboboxGroup key={category.name}>
|
||||
<ComboboxLabel>{category.label}</ComboboxLabel>
|
||||
{category.packers.map((packer) => (
|
||||
<ComboboxItem key={packer.name} value={packer.name}>
|
||||
<span className="truncate" title={packer.label}>
|
||||
{packer.label}
|
||||
</span>
|
||||
</ComboboxItem>
|
||||
))}
|
||||
</ComboboxGroup>
|
||||
))}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
Controller,
|
||||
type FieldValues,
|
||||
type UseFormReturn,
|
||||
} from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldLabel,
|
||||
FieldSet,
|
||||
} from "@/components/ui/field";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { PackerSchemaField } from "@/types/memshell";
|
||||
|
||||
type Props<T extends FieldValues> = {
|
||||
form: UseFormReturn<T>;
|
||||
fields: PackerSchemaField[];
|
||||
baseName?: string;
|
||||
};
|
||||
|
||||
export function PackerCustomConfigFields<T extends FieldValues>({
|
||||
form,
|
||||
fields,
|
||||
baseName = "packerCustomConfig",
|
||||
}: Readonly<Props<T>>) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
if (fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const supportedFields = fields.filter((field) =>
|
||||
["BOOLEAN", "STRING", "ENUM", "INTEGER"].includes(field.type),
|
||||
);
|
||||
|
||||
if (supportedFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const getFieldDescription = (schemaField: PackerSchemaField) => {
|
||||
if (!schemaField.description && !schemaField.descriptionI18nKey) {
|
||||
return undefined;
|
||||
}
|
||||
if (!schemaField.descriptionI18nKey) {
|
||||
return schemaField.description;
|
||||
}
|
||||
return t(schemaField.descriptionI18nKey, {
|
||||
defaultValue: schemaField.description ?? schemaField.descriptionI18nKey,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Field className="mt-2 gap-1">
|
||||
<FieldLabel>
|
||||
{t("packerParams", { defaultValue: "Packer Params" })}
|
||||
</FieldLabel>
|
||||
{supportedFields.map((schemaField) => {
|
||||
const fieldName = `${baseName}.${schemaField.key}` as any;
|
||||
const fieldDescription = getFieldDescription(schemaField);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
key={schemaField.key}
|
||||
control={form.control}
|
||||
name={fieldName}
|
||||
render={({ field }) => {
|
||||
switch (schemaField.type) {
|
||||
case "BOOLEAN":
|
||||
return (
|
||||
<Field orientation="horizontal">
|
||||
<Switch
|
||||
id={fieldName}
|
||||
checked={Boolean(field.value)}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={fieldName}>
|
||||
{schemaField.key}
|
||||
</FieldLabel>
|
||||
{fieldDescription ? (
|
||||
<FieldDescription>
|
||||
{fieldDescription}
|
||||
</FieldDescription>
|
||||
) : null}
|
||||
</FieldContent>
|
||||
</Field>
|
||||
);
|
||||
case "ENUM":
|
||||
return (
|
||||
<Field orientation="vertical">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={fieldName}>
|
||||
{schemaField.key}
|
||||
</FieldLabel>
|
||||
<Select
|
||||
value={
|
||||
typeof field.value === "string"
|
||||
? field.value
|
||||
: undefined
|
||||
}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger id={fieldName}>
|
||||
<SelectValue
|
||||
data-placeholder={t("placeholders.select")}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(schemaField.options ?? []).map((option) => (
|
||||
<SelectItem
|
||||
key={option.value}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label || option.value}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{fieldDescription ? (
|
||||
<FieldDescription>
|
||||
{fieldDescription}
|
||||
</FieldDescription>
|
||||
) : null}
|
||||
</FieldContent>
|
||||
</Field>
|
||||
);
|
||||
case "INTEGER":
|
||||
return (
|
||||
<Field orientation="vertical">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={fieldName}>
|
||||
{schemaField.key}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={fieldName}
|
||||
type="number"
|
||||
step={1}
|
||||
value={
|
||||
typeof field.value === "number"
|
||||
? String(field.value)
|
||||
: ""
|
||||
}
|
||||
onChange={(event) => {
|
||||
const raw = event.target.value;
|
||||
if (raw === "") {
|
||||
field.onChange(undefined);
|
||||
return;
|
||||
}
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
field.onChange(
|
||||
Number.isFinite(parsed) ? parsed : undefined,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{fieldDescription ? (
|
||||
<FieldDescription>
|
||||
{fieldDescription}
|
||||
</FieldDescription>
|
||||
) : null}
|
||||
</FieldContent>
|
||||
</Field>
|
||||
);
|
||||
case "STRING":
|
||||
return (
|
||||
<Field orientation="vertical">
|
||||
<FieldContent>
|
||||
<FieldLabel htmlFor={fieldName}>
|
||||
{schemaField.key}
|
||||
</FieldLabel>
|
||||
<Input
|
||||
id={fieldName}
|
||||
type="text"
|
||||
value={
|
||||
typeof field.value === "string" ? field.value : ""
|
||||
}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
{fieldDescription ? (
|
||||
<FieldDescription>
|
||||
{fieldDescription}
|
||||
</FieldDescription>
|
||||
) : null}
|
||||
</FieldContent>
|
||||
</Field>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
import { PackageIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, type UseFormReturn } from "react-hook-form";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Controller, type UseFormReturn, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PackerCombobox } from "@/components/packer/packer-combobox";
|
||||
import { PackerCustomConfigFields } from "@/components/packer/packer-custom-config-fields";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { FieldLabel } from "@/components/ui/field";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Field, FieldLabel, FieldSet } from "@/components/ui/field";
|
||||
import {
|
||||
findPackerEntry,
|
||||
getPackerDefaultConfig,
|
||||
getPackerSchemaFields,
|
||||
normalizePackerCategories,
|
||||
} from "@/lib/packer-schema";
|
||||
import type { PackerConfig } from "@/types/memshell";
|
||||
import type { ProbeShellFormSchema } from "@/types/schema";
|
||||
|
||||
type Option = {
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export default function PackageConfigCard({
|
||||
packerConfig,
|
||||
form,
|
||||
@@ -20,34 +22,73 @@ export default function PackageConfigCard({
|
||||
packerConfig: PackerConfig | undefined;
|
||||
form: UseFormReturn<ProbeShellFormSchema>;
|
||||
}>) {
|
||||
const [options, setOptions] = useState<Array<Option>>([]);
|
||||
const { t } = useTranslation("common");
|
||||
const packingMethod = useWatch({
|
||||
control: form.control,
|
||||
name: "packingMethod",
|
||||
});
|
||||
|
||||
const categories = useMemo(
|
||||
() => normalizePackerCategories(packerConfig),
|
||||
[packerConfig],
|
||||
);
|
||||
|
||||
const filteredCategories = useMemo(() => {
|
||||
return categories
|
||||
.map((category) => ({
|
||||
...category,
|
||||
packers: category.packers.filter((packer) => {
|
||||
if (packer.categoryAnchor) {
|
||||
return false;
|
||||
}
|
||||
const name = packer.name;
|
||||
return (
|
||||
!name.startsWith("Agent") &&
|
||||
!name.toLowerCase().startsWith("xxl") &&
|
||||
!name.toLowerCase().endsWith("jar")
|
||||
);
|
||||
}),
|
||||
}))
|
||||
.filter((category) => category.packers.length > 0);
|
||||
}, [categories]);
|
||||
|
||||
const allOptionNames = useMemo(
|
||||
() =>
|
||||
filteredCategories.flatMap((category) =>
|
||||
category.packers.map((packer) => packer.name),
|
||||
),
|
||||
[filteredCategories],
|
||||
);
|
||||
|
||||
const selectedPackerEntry = useMemo(
|
||||
() =>
|
||||
findPackerEntry(filteredCategories, packingMethod) ??
|
||||
findPackerEntry(categories, packingMethod),
|
||||
[categories, filteredCategories, packingMethod],
|
||||
);
|
||||
|
||||
const selectedSchemaFields = useMemo(
|
||||
() => getPackerSchemaFields(selectedPackerEntry),
|
||||
[selectedPackerEntry],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredOptions = (packerConfig ?? []).filter((name) => {
|
||||
return (
|
||||
!name.startsWith("Agent") &&
|
||||
!name.toLowerCase().startsWith("xxl") &&
|
||||
!name.toLowerCase().endsWith("jar")
|
||||
);
|
||||
});
|
||||
|
||||
const mappedOptions = filteredOptions.map((name) => {
|
||||
return {
|
||||
name: name,
|
||||
value: name,
|
||||
};
|
||||
});
|
||||
|
||||
setOptions(mappedOptions);
|
||||
const currentValue = form.getValues("packingMethod");
|
||||
if (
|
||||
filteredOptions.length > 0 &&
|
||||
(!currentValue || !filteredOptions.includes(currentValue))
|
||||
allOptionNames.length > 0 &&
|
||||
(!currentValue ||
|
||||
!allOptionNames.some((option) => option === currentValue))
|
||||
) {
|
||||
form.setValue("packingMethod", filteredOptions[0]);
|
||||
form.setValue("packingMethod", allOptionNames[0]);
|
||||
}
|
||||
}, [form, packerConfig]);
|
||||
}, [allOptionNames, form]);
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue(
|
||||
"packerCustomConfig",
|
||||
getPackerDefaultConfig(selectedPackerEntry) as any,
|
||||
);
|
||||
}, [form, selectedPackerEntry, packingMethod]);
|
||||
|
||||
return (
|
||||
<Card className="w-full">
|
||||
@@ -58,34 +99,30 @@ export default function PackageConfigCard({
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{options.length > 0 ? (
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="packingMethod"
|
||||
render={({ field }) => (
|
||||
<div className="space-y-3">
|
||||
<FieldLabel>{t("packerMethod")}</FieldLabel>
|
||||
<div>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
{allOptionNames.length > 0 ? (
|
||||
<>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="packingMethod"
|
||||
render={({ field }) => (
|
||||
<Field className="gap-1">
|
||||
<FieldLabel>{t("packerMethod")}</FieldLabel>
|
||||
<PackerCombobox
|
||||
categories={filteredCategories}
|
||||
value={field.value}
|
||||
className="grid grid-cols-2 md:grid-cols-3"
|
||||
>
|
||||
{options.map(({ name, value }) => (
|
||||
<div key={value} className="flex items-center space-x-3">
|
||||
<div>
|
||||
<RadioGroupItem value={value} id={value} />
|
||||
</div>
|
||||
<FieldLabel className="text-xs" htmlFor={value}>
|
||||
{name}
|
||||
</FieldLabel>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
onValueChange={field.onChange}
|
||||
placeholder={t("selectPacker", {
|
||||
defaultValue: "Select packer",
|
||||
})}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
/>
|
||||
<PackerCustomConfigFields
|
||||
form={form}
|
||||
fields={selectedSchemaFields}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center p-4">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-primary border-t-transparent" />
|
||||
|
||||
@@ -3,17 +3,14 @@ import { QuickUsage } from "@/components/probeshell/quick-usage";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import type { ProbeShellResult } from "@/types/probeshell";
|
||||
import CodeViewer from "../code-viewer";
|
||||
import { MultiPackResult } from "../memshell/results/multi-packer";
|
||||
import { BasicInfo } from "./basic-info";
|
||||
|
||||
export default function ShellResult({
|
||||
packResult,
|
||||
allPackResults,
|
||||
packMethod,
|
||||
generateResult,
|
||||
}: Readonly<{
|
||||
packResult: string | undefined;
|
||||
allPackResults: Map<string, string> | undefined;
|
||||
packMethod: string;
|
||||
generateResult?: ProbeShellResult;
|
||||
}>) {
|
||||
@@ -21,7 +18,6 @@ export default function ShellResult({
|
||||
if (!generateResult) {
|
||||
return <QuickUsage />;
|
||||
}
|
||||
const showCode = packMethod === "JSP";
|
||||
const height = 600;
|
||||
return (
|
||||
<Tabs defaultValue="packResult">
|
||||
@@ -32,14 +28,6 @@ export default function ShellResult({
|
||||
</TabsList>
|
||||
<TabsContent value="packResult" className="space-y-2">
|
||||
<BasicInfo generateResult={generateResult} />
|
||||
{allPackResults && (
|
||||
<MultiPackResult
|
||||
allPackResults={allPackResults}
|
||||
shellClassName={generateResult?.shellClassName}
|
||||
packMethod={packMethod}
|
||||
height={height}
|
||||
/>
|
||||
)}
|
||||
{packResult && (
|
||||
<CodeViewer
|
||||
code={packResult}
|
||||
@@ -53,9 +41,9 @@ export default function ShellResult({
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
wrapLongLines={!showCode}
|
||||
showLineNumbers={showCode}
|
||||
language={showCode ? "java" : "text"}
|
||||
wrapLongLines={true}
|
||||
showLineNumbers={false}
|
||||
language={"text"}
|
||||
height={height}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Combobox as ComboboxPrimitive } from "@base-ui/react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/components/ui/input-group";
|
||||
import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react";
|
||||
|
||||
const Combobox = ComboboxPrimitive.Root;
|
||||
|
||||
function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
|
||||
return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
|
||||
}
|
||||
|
||||
function ComboboxTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComboboxPrimitive.Trigger.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Trigger
|
||||
data-slot="combobox-trigger"
|
||||
className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" />
|
||||
</ComboboxPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Clear
|
||||
data-slot="combobox-clear"
|
||||
render={<InputGroupButton variant="ghost" size="icon-xs" />}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
>
|
||||
<XIcon className="pointer-events-none" />
|
||||
</ComboboxPrimitive.Clear>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxInput({
|
||||
className,
|
||||
children,
|
||||
disabled = false,
|
||||
showTrigger = true,
|
||||
showClear = false,
|
||||
...props
|
||||
}: ComboboxPrimitive.Input.Props & {
|
||||
showTrigger?: boolean;
|
||||
showClear?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<InputGroup className={cn("w-auto", className)}>
|
||||
<ComboboxPrimitive.Input
|
||||
render={<InputGroupInput disabled={disabled} />}
|
||||
{...props}
|
||||
/>
|
||||
<InputGroupAddon align="inline-end">
|
||||
{showTrigger && (
|
||||
<InputGroupButton
|
||||
size="icon-xs"
|
||||
variant="ghost"
|
||||
render={<ComboboxTrigger />}
|
||||
data-slot="input-group-button"
|
||||
className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{showClear && <ComboboxClear disabled={disabled} />}
|
||||
</InputGroupAddon>
|
||||
{children}
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxContent({
|
||||
className,
|
||||
side = "bottom",
|
||||
sideOffset = 6,
|
||||
align = "start",
|
||||
alignOffset = 0,
|
||||
anchor,
|
||||
...props
|
||||
}: ComboboxPrimitive.Popup.Props &
|
||||
Pick<
|
||||
ComboboxPrimitive.Positioner.Props,
|
||||
"side" | "align" | "sideOffset" | "alignOffset" | "anchor"
|
||||
>) {
|
||||
return (
|
||||
<ComboboxPrimitive.Portal>
|
||||
<ComboboxPrimitive.Positioner
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
anchor={anchor}
|
||||
className="isolate z-50"
|
||||
>
|
||||
<ComboboxPrimitive.Popup
|
||||
data-slot="combobox-content"
|
||||
data-chips={!!anchor}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:border-input/30 overflow-hidden rounded-md shadow-md ring-1 duration-100 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:shadow-none data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) data-[chips=true]:min-w-(--anchor-width)",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ComboboxPrimitive.Positioner>
|
||||
</ComboboxPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.List
|
||||
data-slot="combobox-list"
|
||||
className={cn(
|
||||
"no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 p-1 data-empty:p-0 overflow-y-auto overscroll-contain",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: ComboboxPrimitive.Item.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Item
|
||||
data-slot="combobox-item"
|
||||
className={cn(
|
||||
"data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm [&_svg:not([class*='size-'])]:size-4 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ComboboxPrimitive.ItemIndicator
|
||||
render={
|
||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
|
||||
}
|
||||
>
|
||||
<CheckIcon className="pointer-events-none" />
|
||||
</ComboboxPrimitive.ItemIndicator>
|
||||
</ComboboxPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Group
|
||||
data-slot="combobox-group"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxLabel({
|
||||
className,
|
||||
...props
|
||||
}: ComboboxPrimitive.GroupLabel.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.GroupLabel
|
||||
data-slot="combobox-label"
|
||||
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Empty
|
||||
data-slot="combobox-empty"
|
||||
className={cn(
|
||||
"text-muted-foreground hidden w-full justify-center py-2 text-center text-sm group-data-empty/combobox-content:flex",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxSeparator({
|
||||
className,
|
||||
...props
|
||||
}: ComboboxPrimitive.Separator.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Separator
|
||||
data-slot="combobox-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxChips({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
|
||||
ComboboxPrimitive.Chips.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Chips
|
||||
data-slot="combobox-chips"
|
||||
className={cn(
|
||||
"dark:bg-input/30 border-input focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive dark:has-aria-invalid:border-destructive/50 flex min-h-9 flex-wrap items-center gap-1.5 rounded-md border bg-transparent bg-clip-padding px-2.5 py-1.5 text-sm shadow-xs transition-[color,box-shadow] focus-within:ring-3 has-aria-invalid:ring-3 has-data-[slot=combobox-chip]:px-1.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxChip({
|
||||
className,
|
||||
children,
|
||||
showRemove = true,
|
||||
...props
|
||||
}: ComboboxPrimitive.Chip.Props & {
|
||||
showRemove?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ComboboxPrimitive.Chip
|
||||
data-slot="combobox-chip"
|
||||
className={cn(
|
||||
"bg-muted text-foreground flex h-[calc(--spacing(5.5))] w-fit items-center justify-center gap-1 rounded-sm px-1.5 text-xs font-medium whitespace-nowrap has-data-[slot=combobox-chip-remove]:pr-0 has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showRemove && (
|
||||
<ComboboxPrimitive.ChipRemove
|
||||
render={<Button variant="ghost" size="icon-xs" />}
|
||||
className="-ml-1 opacity-50 hover:opacity-100"
|
||||
data-slot="combobox-chip-remove"
|
||||
>
|
||||
<XIcon className="pointer-events-none" />
|
||||
</ComboboxPrimitive.ChipRemove>
|
||||
)}
|
||||
</ComboboxPrimitive.Chip>
|
||||
);
|
||||
}
|
||||
|
||||
function ComboboxChipsInput({
|
||||
className,
|
||||
...props
|
||||
}: ComboboxPrimitive.Input.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Input
|
||||
data-slot="combobox-chip-input"
|
||||
className={cn("min-w-16 flex-1 outline-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useComboboxAnchor() {
|
||||
return React.useRef<HTMLDivElement | null>(null);
|
||||
}
|
||||
|
||||
export {
|
||||
Combobox,
|
||||
ComboboxInput,
|
||||
ComboboxContent,
|
||||
ComboboxList,
|
||||
ComboboxItem,
|
||||
ComboboxGroup,
|
||||
ComboboxLabel,
|
||||
ComboboxCollection,
|
||||
ComboboxEmpty,
|
||||
ComboboxSeparator,
|
||||
ComboboxChips,
|
||||
ComboboxChip,
|
||||
ComboboxChipsInput,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { InputGroup, InputGroupAddon } from "@/components/ui/input-group";
|
||||
import { SearchIcon, CheckIcon } from "lucide-react";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground rounded-xl! p-1 flex size-full flex-col overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = false,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Dialog>, "children"> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
showCloseButton?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"rounded-xl! top-1/3 translate-y-0 overflow-hidden p-0",
|
||||
className,
|
||||
)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
{children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div data-slot="command-input-wrapper" className="p-1 pb-0">
|
||||
<InputGroup className="bg-input/30 border-input/30 h-8! rounded-lg! shadow-none! *:data-[slot=input-group-addon]:pl-2!">
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"w-full text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<InputGroupAddon>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"no-scrollbar max-h-72 scroll-py-1 outline-none overflow-x-hidden overflow-y-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className={cn("py-6 text-center text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground **:[[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px w-auto", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-selected:bg-muted data-selected:text-foreground data-selected:**:[svg]:text-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none in-data-[slot=dialog-content]:rounded-lg! [&_svg:not([class*='size-'])]:size-4 group/command-item data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<CheckIcon className="ml-auto opacity-0 group-has-data-[slot=command-shortcut]/command-item:hidden group-data-[checked=true]/command-item:opacity-100" />
|
||||
</CommandPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground group-data-selected/command-item:text-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Backdrop
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: DialogPrimitive.Popup.Props & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-6 rounded-xl p-6 text-sm ring-1 duration-100 sm:max-w-md fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-4 right-4"
|
||||
size="icon-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Popup>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("gap-2 flex flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||
Close
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("leading-none font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Description.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
"text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -0,0 +1,156 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="input-group"
|
||||
role="group"
|
||||
className={cn(
|
||||
"border-input dark:bg-input/30 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[[data-slot][aria-invalid=true]]:border-destructive dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 h-8 rounded-md border shadow-xs transition-[color,box-shadow] in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-3 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5 group/input-group relative flex w-full min-w-0 items-center outline-none has-[>textarea]:h-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupAddonVariants = cva(
|
||||
"text-muted-foreground h-auto gap-2 py-1.5 text-sm font-medium group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4 flex cursor-text items-center justify-center select-none",
|
||||
{
|
||||
variants: {
|
||||
align: {
|
||||
"inline-start":
|
||||
"pl-2 has-[>button]:-ml-1 has-[>kbd]:ml-[-0.15rem] order-first",
|
||||
"inline-end":
|
||||
"pr-2 has-[>button]:-mr-1 has-[>kbd]:mr-[-0.15rem] order-last",
|
||||
"block-start":
|
||||
"px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2 order-first w-full justify-start",
|
||||
"block-end":
|
||||
"px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2 order-last w-full justify-start",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
align: "inline-start",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function InputGroupAddon({
|
||||
className,
|
||||
align = "inline-start",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
data-slot="input-group-addon"
|
||||
data-align={align}
|
||||
className={cn(inputGroupAddonVariants({ align }), className)}
|
||||
onClick={(e) => {
|
||||
if ((e.target as HTMLElement).closest("button")) {
|
||||
return;
|
||||
}
|
||||
e.currentTarget.parentElement?.querySelector("input")?.focus();
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputGroupButtonVariants = cva(
|
||||
"gap-2 text-sm shadow-none flex items-center",
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
xs: "h-6 gap-1 rounded-[calc(var(--radius)-5px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
|
||||
sm: "",
|
||||
"icon-xs":
|
||||
"size-6 rounded-[calc(var(--radius)-5px)] p-0 has-[>svg]:p-0",
|
||||
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: "xs",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function InputGroupButton({
|
||||
className,
|
||||
type = "button",
|
||||
variant = "ghost",
|
||||
size = "xs",
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
|
||||
VariantProps<typeof inputGroupButtonVariants> & {
|
||||
type?: "button" | "submit" | "reset";
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type={type}
|
||||
data-size={size}
|
||||
variant={variant}
|
||||
className={cn(inputGroupButtonVariants({ size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground gap-2 text-sm [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<Input
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputGroupTextarea({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<Textarea
|
||||
data-slot="input-group-control"
|
||||
className={cn(
|
||||
"rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent flex-1 resize-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupText,
|
||||
InputGroupInput,
|
||||
InputGroupTextarea,
|
||||
};
|
||||
@@ -26,6 +26,7 @@
|
||||
"paramName.description": "Supports passing values via request parameter (param) or request header (header)",
|
||||
"placeholders.input": "Please input",
|
||||
"placeholders.select": "Please select",
|
||||
"packerParams": "Package Params",
|
||||
"ProbeShellGenerator": "ProbeShellGenerator",
|
||||
"quickUsage.title": "Quick Usage",
|
||||
"server": "Server",
|
||||
@@ -49,5 +50,8 @@
|
||||
"commandTemplate": "Command Template",
|
||||
"commandTemplate.placeholder": "e.g., sh -c \"{command}\" 2>&1",
|
||||
"commandTemplate.description": "Use {command} as placeholder",
|
||||
"unicodeEncoded.desc": "Enable Unicode encoding",
|
||||
"urlEncoded.desc": "Enable URL encoding",
|
||||
"gzipCompressed.desc": "Enable GZIP compression",
|
||||
"targetJdkVersion": "JRE Version"
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"paramName.description": "支持请求参数 param 或请求头 header 传值",
|
||||
"placeholders.input": "请输入",
|
||||
"placeholders.select": "请选择",
|
||||
"packerParams": "打包参数",
|
||||
"ProbeShellGenerator": "探测马生成器",
|
||||
"quickUsage.title": "快速使用",
|
||||
"server": "服务类型",
|
||||
@@ -49,5 +50,8 @@
|
||||
"commandTemplate": "命令模板",
|
||||
"commandTemplate.placeholder": "例如:sh -c \"{command}\" 2>&1",
|
||||
"commandTemplate.description": "使用 {command} 作为占位符",
|
||||
"unicodeEncoded.desc": "启用 Unicode 编码",
|
||||
"urlEncoded.desc": "启用 URL 编码",
|
||||
"gzipCompressed.desc": "启用 GZIP 压缩",
|
||||
"targetJdkVersion": "JRE 版本"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import type {
|
||||
LegacyPackerGroup,
|
||||
PackerCategory,
|
||||
PackerConfig,
|
||||
PackerEntry,
|
||||
PackerSchemaField,
|
||||
} from "@/types/memshell";
|
||||
|
||||
export type NormalizedPackerEntry = Pick<
|
||||
PackerEntry,
|
||||
"name" | "outputKind" | "categoryAnchor" | "schema"
|
||||
>;
|
||||
|
||||
export type NormalizedPackerCategory = {
|
||||
name: string;
|
||||
packers: NormalizedPackerEntry[];
|
||||
};
|
||||
|
||||
const isLegacyGroup = (value: unknown): value is LegacyPackerGroup => {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"group" in value &&
|
||||
"options" in value &&
|
||||
Array.isArray((value as { options?: unknown[] }).options)
|
||||
);
|
||||
};
|
||||
|
||||
const isPackerCategory = (value: unknown): value is PackerCategory => {
|
||||
return (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"name" in value &&
|
||||
"packers" in value &&
|
||||
Array.isArray((value as { packers?: unknown[] }).packers)
|
||||
);
|
||||
};
|
||||
|
||||
export function normalizePackerCategories(
|
||||
packerConfig: PackerConfig | undefined,
|
||||
): NormalizedPackerCategory[] {
|
||||
return (packerConfig ?? [])
|
||||
.map((item): NormalizedPackerCategory | null => {
|
||||
if (typeof item === "string") {
|
||||
return {
|
||||
name: item,
|
||||
packers: [{ name: item, categoryAnchor: false }],
|
||||
};
|
||||
}
|
||||
if (isLegacyGroup(item)) {
|
||||
return {
|
||||
name: item.group,
|
||||
packers: (item.options ?? []).map((name) => ({
|
||||
name,
|
||||
categoryAnchor: false,
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (isPackerCategory(item)) {
|
||||
return {
|
||||
name: item.name,
|
||||
packers: (item.packers ?? []).map((packer) => ({
|
||||
name: packer.name,
|
||||
outputKind: packer.outputKind,
|
||||
categoryAnchor: !!packer.categoryAnchor,
|
||||
schema: packer.schema,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((item): item is NormalizedPackerCategory => item !== null);
|
||||
}
|
||||
|
||||
export function findPackerEntry(
|
||||
categories: NormalizedPackerCategory[],
|
||||
packerName: string | undefined,
|
||||
): NormalizedPackerEntry | undefined {
|
||||
if (!packerName) {
|
||||
return undefined;
|
||||
}
|
||||
for (const category of categories) {
|
||||
const found = category.packers.find((packer) => packer.name === packerName);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getPackerSchemaFields(
|
||||
packer: NormalizedPackerEntry | undefined,
|
||||
): PackerSchemaField[] {
|
||||
return packer?.schema?.fields ?? [];
|
||||
}
|
||||
|
||||
export function getPackerDefaultConfig(
|
||||
packer: NormalizedPackerEntry | undefined,
|
||||
): Record<string, unknown> {
|
||||
return { ...(packer?.schema?.defaultConfig ?? {}) };
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import ShellResult from "@/components/memshell/shell-result";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { env } from "@/config";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { baseOptions } from "@/lib/layout.shared";
|
||||
import {
|
||||
type APIErrorResponse,
|
||||
type MainConfig,
|
||||
@@ -26,7 +27,6 @@ import {
|
||||
useYupValidationResolver,
|
||||
} from "@/types/schema";
|
||||
import { transformToPostData } from "@/utils/transformer";
|
||||
import { baseOptions } from "../lib/layout.shared";
|
||||
|
||||
const homeLayoutOptions = baseOptions();
|
||||
|
||||
@@ -49,6 +49,7 @@ const defaultValues: MemShellFormSchema = {
|
||||
headerValue: "",
|
||||
injectorClassName: "",
|
||||
packingMethod: "",
|
||||
packerCustomConfig: {},
|
||||
shrink: true,
|
||||
staticInitialize: true,
|
||||
shellClassBase64: "",
|
||||
@@ -95,9 +96,6 @@ export default function MemShellPage() {
|
||||
});
|
||||
|
||||
const [packResult, setPackResult] = useState<string | undefined>();
|
||||
const [allPackResults, setAllPackResults] = useState<
|
||||
Map<string, string> | undefined
|
||||
>();
|
||||
const [generateResult, setGenerateResult] = useState<MemShellResult>();
|
||||
const [packMethod, setPackMethod] = useState<string>("");
|
||||
const submitLockRef = useRef(false);
|
||||
@@ -121,7 +119,6 @@ export default function MemShellPage() {
|
||||
const result = (await response.json()) as MemShellGenerateResponse;
|
||||
setGenerateResult(result.memShellResult);
|
||||
setPackResult(result.packResult);
|
||||
setAllPackResults(result.allPackResults);
|
||||
setPackMethod(data.packingMethod);
|
||||
toast.success(t("toast.generateSuccess"));
|
||||
} catch (error) {
|
||||
@@ -179,7 +176,6 @@ export default function MemShellPage() {
|
||||
packMethod={packMethod}
|
||||
generateResult={generateResult}
|
||||
packResult={packResult}
|
||||
allPackResults={allPackResults}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -57,15 +57,13 @@ export default function ProbeShellGenerator() {
|
||||
reqParamName: "",
|
||||
seconds: 5,
|
||||
sleepServer: "Tomcat",
|
||||
packerCustomConfig: {},
|
||||
shrink: true,
|
||||
staticInitialize: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [packResult, setPackResult] = useState<string | undefined>();
|
||||
const [allPackResults, setAllPackResults] = useState<
|
||||
Map<string, string> | undefined
|
||||
>();
|
||||
const [generateResult, setGenerateResult] = useState<ProbeShellResult>();
|
||||
const [packMethod, setPackMethod] = useState<string>("");
|
||||
const submitLockRef = useRef(false);
|
||||
@@ -93,7 +91,6 @@ export default function ProbeShellGenerator() {
|
||||
const result = (await response.json()) as ProbeShellGenerateResponse;
|
||||
setGenerateResult(result.probeShellResult);
|
||||
setPackResult(result.packResult);
|
||||
setAllPackResults(result.allPackResults);
|
||||
setPackMethod(data.packingMethod);
|
||||
toast.success(t("toast.generateSuccess"));
|
||||
} catch (error) {
|
||||
@@ -132,7 +129,6 @@ export default function ProbeShellGenerator() {
|
||||
packMethod={packMethod}
|
||||
generateResult={generateResult}
|
||||
packResult={packResult}
|
||||
allPackResults={allPackResults}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -97,7 +97,44 @@ export interface MainConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export type PackerConfig = Array<string>;
|
||||
export interface LegacyPackerGroup {
|
||||
group: string;
|
||||
options: string[];
|
||||
}
|
||||
|
||||
export interface PackerSchemaFieldOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface PackerSchemaField {
|
||||
key: string;
|
||||
type: string;
|
||||
required: boolean;
|
||||
defaultValue?: unknown;
|
||||
description?: string;
|
||||
descriptionI18nKey?: string;
|
||||
options?: PackerSchemaFieldOption[];
|
||||
}
|
||||
|
||||
export interface PackerSchema {
|
||||
fields?: PackerSchemaField[];
|
||||
defaultConfig?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PackerEntry {
|
||||
name: string;
|
||||
outputKind?: string;
|
||||
categoryAnchor?: boolean;
|
||||
schema?: PackerSchema;
|
||||
}
|
||||
|
||||
export interface PackerCategory {
|
||||
name: string;
|
||||
packers: PackerEntry[];
|
||||
}
|
||||
|
||||
export type PackerConfig = Array<LegacyPackerGroup | PackerCategory | string>;
|
||||
|
||||
export interface MemShellGenerateResponse {
|
||||
memShellResult: MemShellResult;
|
||||
|
||||
@@ -61,7 +61,6 @@ export interface PayloadFormValues {
|
||||
export interface ProbeShellGenerateResponse {
|
||||
probeShellResult: ProbeShellResult;
|
||||
packResult?: string;
|
||||
allPackResults?: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface ProbeShellResult {
|
||||
|
||||
@@ -26,6 +26,7 @@ export const memShellFormSchema = yup.object({
|
||||
headerValue: yup.string().optional(),
|
||||
injectorClassName: yup.string().optional(),
|
||||
packingMethod: yup.string().required().min(1),
|
||||
packerCustomConfig: yup.object().optional(),
|
||||
shrink: yup.boolean().optional(),
|
||||
lambdaSuffix: yup.boolean().optional(),
|
||||
probe: yup.boolean().optional(),
|
||||
@@ -164,6 +165,7 @@ export const probeShellFormSchema = yup.object().shape({
|
||||
seconds: yup.number().optional(),
|
||||
sleepServer: yup.string().optional(),
|
||||
packingMethod: yup.string().required(),
|
||||
packerCustomConfig: yup.object().optional(),
|
||||
targetJdkVersion: yup.string().optional(),
|
||||
debug: yup.boolean().optional(),
|
||||
byPassJavaModule: yup.boolean().optional(),
|
||||
|
||||
@@ -43,7 +43,10 @@ export function transformToPostData(formValue: MemShellFormSchema) {
|
||||
shellConfig,
|
||||
shellToolConfig,
|
||||
injectorConfig,
|
||||
packer: formValue.packingMethod,
|
||||
packerSpec: {
|
||||
name: formValue.packingMethod,
|
||||
config: formValue.packerCustomConfig ?? {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,7 +73,10 @@ export function transformToProbePostData(formValue: ProbeShellFormSchema) {
|
||||
return {
|
||||
probeConfig,
|
||||
probeContentConfig,
|
||||
packer: formValue.packingMethod,
|
||||
packerSpec: {
|
||||
name: formValue.packingMethod,
|
||||
config: formValue.packerCustomConfig ?? {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -5,7 +5,7 @@
|
||||
"": {
|
||||
"name": "fumadocs",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.1.0",
|
||||
"@base-ui/react": "^1.2.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@orama/orama": "^3.1.18",
|
||||
"@orama/stopwords": "^3.1.18",
|
||||
@@ -14,6 +14,7 @@
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"framer-motion": "^12.33.0",
|
||||
"fumadocs-core": "^16.5.1",
|
||||
"fumadocs-mdx": "14.2.6",
|
||||
@@ -106,7 +107,7 @@
|
||||
|
||||
"@babel/preset-typescript": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="],
|
||||
|
||||
"@babel/runtime": ["@babel/[email protected].4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||
"@babel/runtime": ["@babel/[email protected].6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="],
|
||||
|
||||
"@babel/template": ["@babel/[email protected]", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
@@ -114,9 +115,9 @@
|
||||
|
||||
"@babel/types": ["@babel/[email protected]", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
|
||||
|
||||
"@base-ui/react": ["@base-ui/react@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@base-ui/utils": "0.2.4", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw=="],
|
||||
"@base-ui/react": ["@base-ui/react@1.2.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.5", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-O6aEQHcm+QyGTFY28xuwRD3SEJGZOBDpyjN2WvpfWYFVhg+3zfXPysAILqtM0C1kWC82MccOE/v1j+GHXE4qIw=="],
|
||||
|
||||
"@base-ui/utils": ["@base-ui/[email protected].4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng=="],
|
||||
"@base-ui/utils": ["@base-ui/[email protected].5", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-oYC7w0gp76RI5MxprlGLV0wze0SErZaRl3AAkeP3OnNB/UBMb6RqNf6ZSIlxOc9Qp68Ab3C2VOcJQyRs7Xc7Vw=="],
|
||||
|
||||
"@biomejs/biome": ["@biomejs/[email protected]", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.14", "@biomejs/cli-darwin-x64": "2.3.14", "@biomejs/cli-linux-arm64": "2.3.14", "@biomejs/cli-linux-arm64-musl": "2.3.14", "@biomejs/cli-linux-x64": "2.3.14", "@biomejs/cli-linux-x64-musl": "2.3.14", "@biomejs/cli-win32-arm64": "2.3.14", "@biomejs/cli-win32-x64": "2.3.14" }, "bin": { "biome": "bin/biome" } }, "sha512-QMT6QviX0WqXJCaiqVMiBUCr5WRQ1iFSjvOLoTk6auKukJMvnMzWucXpwZB0e8F00/1/BsS9DzcKgWH+CLqVuA=="],
|
||||
|
||||
@@ -544,6 +545,8 @@
|
||||
|
||||
"clsx": ["[email protected]", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"cmdk": ["[email protected]", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
|
||||
|
||||
"collapse-white-space": ["[email protected]", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
|
||||
|
||||
"color-convert": ["[email protected]", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
@@ -1304,6 +1307,8 @@
|
||||
|
||||
"ansi-align/string-width": ["[email protected]", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"babel-plugin-macros/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||
|
||||
"boxen/chalk": ["[email protected]", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"chalk-template/chalk": ["[email protected]", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
@@ -1312,10 +1317,14 @@
|
||||
|
||||
"compression/negotiator": ["[email protected]", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
|
||||
|
||||
"dom-helpers/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||
|
||||
"fumadocs-mdx/chokidar": ["[email protected]", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"fumadocs-mdx/unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"i18next/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||
|
||||
"mdast-util-to-hast/unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
|
||||
"mdast-util-to-markdown/unist-util-visit": ["[email protected]", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
|
||||
@@ -1330,10 +1339,14 @@
|
||||
|
||||
"react-d3-tree/uuid": ["[email protected]", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
|
||||
|
||||
"react-i18next/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||
|
||||
"react-router-devtools/@biomejs/cli-darwin-arm64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA=="],
|
||||
|
||||
"react-router-devtools/framer-motion": ["[email protected]", "", { "dependencies": { "motion-dom": "^12.26.2", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA=="],
|
||||
|
||||
"react-syntax-highlighter/@babel/runtime": ["@babel/[email protected]", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
|
||||
|
||||
"serve/chalk": ["[email protected]", "", {}, "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w=="],
|
||||
|
||||
"serve-handler/bytes": ["[email protected]", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="],
|
||||
|
||||
+2
-1
@@ -12,7 +12,7 @@
|
||||
"format": "biome format --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.1.0",
|
||||
"@base-ui/react": "^1.2.0",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@orama/orama": "^3.1.18",
|
||||
"@orama/stopwords": "^3.1.18",
|
||||
@@ -21,6 +21,7 @@
|
||||
"@tanstack/react-query": "^5.90.20",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"framer-motion": "^12.33.0",
|
||||
"fumadocs-core": "^16.5.1",
|
||||
"fumadocs-mdx": "14.2.6",
|
||||
|
||||
Reference in New Issue
Block a user