优化项目启动器的创建功能

This commit is contained in:
xintaofei
2026-03-27 15:08:53 +08:00
parent 06580b0c9c
commit 3b080c801b
18 changed files with 374 additions and 55 deletions

View File

@@ -1,7 +1,60 @@
use serde::Serialize;
use std::path::PathBuf; use std::path::PathBuf;
use crate::app_error::AppCommandError; use crate::app_error::AppCommandError;
// ---------------------------------------------------------------------------
// Package manager detection
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Serialize)]
pub struct PackageManagerInfo {
pub name: String,
pub installed: bool,
pub version: Option<String>,
}
async fn detect_one(name: &str) -> PackageManagerInfo {
let program = match name {
"bun" => "bun",
"pnpm" => "pnpm",
"yarn" => "yarn",
_ => "npm",
};
let result = crate::process::tokio_command(program)
.arg("--version")
.output()
.await;
match result {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout)
.trim()
.to_string();
PackageManagerInfo {
name: name.to_string(),
installed: true,
version: Some(version),
}
}
_ => PackageManagerInfo {
name: name.to_string(),
installed: false,
version: None,
},
}
}
#[tauri::command]
pub async fn detect_package_manager(name: String) -> PackageManagerInfo {
detect_one(&name).await
}
// ---------------------------------------------------------------------------
// Project creation
// ---------------------------------------------------------------------------
#[tauri::command] #[tauri::command]
pub async fn create_shadcn_project( pub async fn create_shadcn_project(
project_name: String, project_name: String,

View File

@@ -270,6 +270,7 @@ pub fn run() {
windows::open_stash_window, windows::open_stash_window,
windows::open_push_window, windows::open_push_window,
windows::open_project_boot_window, windows::open_project_boot_window,
project_boot::detect_package_manager,
project_boot::create_shadcn_project, project_boot::create_shadcn_project,
system_settings::get_system_proxy_settings, system_settings::get_system_proxy_settings,
system_settings::update_system_proxy_settings, system_settings::update_system_proxy_settings,

View File

@@ -1,19 +1,37 @@
"use client" "use client"
import { useState } from "react" import { useState, useEffect, useCallback } from "react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { Loader2, FolderOpen } from "lucide-react" import {
Loader2,
FolderOpen,
ChevronsUpDown,
CircleCheck,
CircleX,
} from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Switch } from "@/components/ui/switch"
import { import {
Select, Tabs,
SelectContent, TabsList,
SelectItem, TabsTrigger,
SelectTrigger, TabsContent,
SelectValue, } from "@/components/ui/tabs"
} from "@/components/ui/select" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import {
Field,
FieldContent,
FieldLabel,
FieldTitle,
} from "@/components/ui/field"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -22,9 +40,17 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog" } from "@/components/ui/dialog"
import { openFileDialog } from "@/lib/platform" import { openFileDialog } from "@/lib/platform"
import { createShadcnProject, openFolderWindow } from "@/lib/api" import {
createShadcnProject,
openFolderWindow,
detectPackageManager,
} from "@/lib/api"
import { toErrorMessage } from "@/lib/app-error" import { toErrorMessage } from "@/lib/app-error"
import { FRAMEWORK_OPTIONS, PACKAGE_MANAGER_OPTIONS } from "./constants" import {
BASE_OPTIONS,
FRAMEWORK_OPTIONS,
PACKAGE_MANAGER_OPTIONS,
} from "./constants"
interface CreateProjectDialogProps { interface CreateProjectDialogProps {
open: boolean open: boolean
@@ -42,9 +68,38 @@ export function CreateProjectDialog({
const [framework, setFramework] = useState("next") const [framework, setFramework] = useState("next")
const [packageManager, setPackageManager] = useState("pnpm") const [packageManager, setPackageManager] = useState("pnpm")
const [saveDirectory, setSaveDirectory] = useState("") const [saveDirectory, setSaveDirectory] = useState("")
const [base, setBase] = useState("radix")
const [rtl, setRtl] = useState(false)
const [advancedOpen, setAdvancedOpen] = useState(false)
const [creating, setCreating] = useState(false) const [creating, setCreating] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [pmVersion, setPmVersion] = useState<string | null>(null)
const [pmInstalled, setPmInstalled] = useState<boolean | null>(null)
const [pmChecking, setPmChecking] = useState(false)
const checkPackageManager = useCallback(async (name: string) => {
setPmChecking(true)
setPmInstalled(null)
setPmVersion(null)
try {
const info = await detectPackageManager(name)
setPmInstalled(info.installed)
setPmVersion(info.version ?? null)
} catch {
setPmInstalled(false)
setPmVersion(null)
} finally {
setPmChecking(false)
}
}, [])
useEffect(() => {
if (open) {
checkPackageManager(packageManager)
}
}, [open, packageManager, checkPackageManager])
const handleBrowse = async () => { const handleBrowse = async () => {
const result = await openFileDialog({ directory: true, multiple: false }) const result = await openFileDialog({ directory: true, multiple: false })
if (!result) return if (!result) return
@@ -81,11 +136,18 @@ export function CreateProjectDialog({
setFramework("next") setFramework("next")
setPackageManager("pnpm") setPackageManager("pnpm")
setSaveDirectory("") setSaveDirectory("")
setBase("radix")
setRtl(false)
setAdvancedOpen(false)
setError(null) setError(null)
setPmVersion(null)
setPmInstalled(null)
} }
const canCreate = const canCreate =
projectName.trim().length > 0 && saveDirectory.trim().length > 0 projectName.trim().length > 0 &&
saveDirectory.trim().length > 0 &&
pmInstalled === true
return ( return (
<Dialog <Dialog
@@ -111,46 +173,6 @@ export function CreateProjectDialog({
/> />
</div> </div>
<div className="space-y-1.5">
<Label>{t("createDialog.frameworkTemplate")}</Label>
<Select
value={framework}
onValueChange={setFramework}
disabled={creating}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{FRAMEWORK_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5">
<Label>{t("createDialog.packageManager")}</Label>
<Select
value={packageManager}
onValueChange={setPackageManager}
disabled={creating}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{PACKAGE_MANAGER_OPTIONS.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label>{t("createDialog.saveDirectory")}</Label> <Label>{t("createDialog.saveDirectory")}</Label>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -173,6 +195,135 @@ export function CreateProjectDialog({
</div> </div>
</div> </div>
<div className="space-y-1.5">
<Label>{t("createDialog.packageManager")}</Label>
<Tabs
value={packageManager}
onValueChange={setPackageManager}
className="gap-0"
>
<TabsList className="w-full">
{PACKAGE_MANAGER_OPTIONS.map((opt) => (
<TabsTrigger
key={opt.value}
value={opt.value}
className="flex-1"
disabled={creating}
>
{opt.label}
</TabsTrigger>
))}
</TabsList>
{PACKAGE_MANAGER_OPTIONS.map((opt) => (
<TabsContent key={opt.value} value={opt.value}>
<div className="flex h-8 items-center gap-1.5 text-sm">
{pmChecking ? (
<>
<Loader2 className="size-3.5 animate-spin text-muted-foreground" />
<span className="text-muted-foreground">
{t("createDialog.pmChecking")}
</span>
</>
) : pmInstalled ? (
<>
<CircleCheck className="size-3.5 text-emerald-500" />
<span className="text-muted-foreground">
{opt.label} v{pmVersion}
</span>
</>
) : (
<>
<CircleX className="size-3.5 text-destructive" />
<span className="text-muted-foreground">
{t("createDialog.pmNotInstalled")}
</span>
</>
)}
</div>
</TabsContent>
))}
</Tabs>
</div>
<Collapsible open={advancedOpen} onOpenChange={setAdvancedOpen}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-auto gap-1 px-0 text-xs text-muted-foreground"
disabled={creating}
>
<ChevronsUpDown className="size-3.5" />
{t("createDialog.advancedOptions")}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-2">
<div className="space-y-1.5">
<Label>{t("createDialog.frameworkTemplate")}</Label>
<RadioGroup
value={framework}
onValueChange={setFramework}
disabled={creating}
className="grid grid-cols-2 gap-2"
>
{FRAMEWORK_OPTIONS.map((opt) => (
<FieldLabel key={opt.value} htmlFor={`fw-${opt.value}`}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{opt.label}</FieldTitle>
</FieldContent>
<RadioGroupItem
value={opt.value}
id={`fw-${opt.value}`}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
</div>
<div className="space-y-1.5">
<Label>{t("createDialog.base")}</Label>
<RadioGroup
value={base}
onValueChange={setBase}
disabled={creating}
className="grid grid-cols-2 gap-2"
>
{BASE_OPTIONS.map((opt) => (
<FieldLabel key={opt.value} htmlFor={`base-${opt.value}`}>
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{opt.label}</FieldTitle>
</FieldContent>
<RadioGroupItem
value={opt.value}
id={`base-${opt.value}`}
/>
</Field>
</FieldLabel>
))}
</RadioGroup>
</div>
<label className="flex cursor-pointer items-center gap-3 rounded-lg border p-3">
<Switch
checked={rtl}
onCheckedChange={setRtl}
disabled={creating}
/>
<div className="space-y-0.5">
<div className="text-sm font-medium">
{t("createDialog.enableRtl")}
</div>
<div className="text-xs text-muted-foreground">
{t("createDialog.enableRtlDescription")}
</div>
</div>
</label>
</CollapsibleContent>
</Collapsible>
{error && ( {error && (
<div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive"> <div className="rounded-lg border border-destructive/50 bg-destructive/10 px-3 py-2 text-sm text-destructive">
{error} {error}

View File

@@ -13,7 +13,6 @@ import {
} from "@/components/ui/select" } from "@/components/ui/select"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { import {
BASE_OPTIONS,
STYLE_OPTIONS, STYLE_OPTIONS,
BASE_COLOR_OPTIONS, BASE_COLOR_OPTIONS,
THEME_OPTIONS, THEME_OPTIONS,
@@ -35,7 +34,6 @@ interface ShadcnConfigPanelProps {
} }
type ConfigI18nKey = type ConfigI18nKey =
| "config.base"
| "config.style" | "config.style"
| "config.baseColor" | "config.baseColor"
| "config.theme" | "config.theme"
@@ -53,7 +51,6 @@ const CONFIG_FIELDS: {
i18nKey: ConfigI18nKey i18nKey: ConfigI18nKey
options: { value: string; label: string }[] options: { value: string; label: string }[]
}[] = [ }[] = [
{ key: "base", i18nKey: "config.base", options: BASE_OPTIONS },
{ key: "style", i18nKey: "config.style", options: STYLE_OPTIONS }, { key: "style", i18nKey: "config.style", options: STYLE_OPTIONS },
{ key: "baseColor", i18nKey: "config.baseColor", options: BASE_COLOR_OPTIONS }, { key: "baseColor", i18nKey: "config.baseColor", options: BASE_COLOR_OPTIONS },
{ key: "theme", i18nKey: "config.theme", options: THEME_OPTIONS }, { key: "theme", i18nKey: "config.theme", options: THEME_OPTIONS },

View File

@@ -0,0 +1,44 @@
"use client"
import * as React from "react"
import { RadioGroup as RadioGroupPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid w-full gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="flex size-4 items-center justify-center"
>
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

View File

@@ -63,7 +63,7 @@ function TabsTrigger({
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
data-slot="tabs-trigger" data-slot="tabs-trigger"
className={cn( className={cn(
"gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium group-data-vertical/tabs:px-2.5 group-data-vertical/tabs:py-1.5 [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", "gap-1.5 rounded-xl border border-transparent px-2 py-1 text-sm font-medium group-data-vertical/tabs:px-2.5 group-data-vertical/tabs:py-1.5 [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-full flex-1 items-center justify-center whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
"data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground", "data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground",
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:bottom-[-5px] group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "دليل الحفظ", "saveDirectory": "دليل الحفظ",
"saveDirectoryPlaceholder": "اختر الدليل...", "saveDirectoryPlaceholder": "اختر الدليل...",
"browseDirectory": "تصفح", "browseDirectory": "تصفح",
"advancedOptions": "خيارات متقدمة",
"base": "المكتبة الأساسية",
"enableRtl": "تفعيل دعم RTL",
"enableRtlDescription": "تفعيل دعم التخطيط للغات التي تُكتب من اليمين إلى اليسار (مثل العربية والعبرية)",
"pmChecking": "جارٍ التحقق...",
"pmNotInstalled": "غير مثبت",
"cancel": "إلغاء", "cancel": "إلغاء",
"create": "إنشاء", "create": "إنشاء",
"creating": "جاري إنشاء المشروع..." "creating": "جاري إنشاء المشروع..."

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "Speicherverzeichnis", "saveDirectory": "Speicherverzeichnis",
"saveDirectoryPlaceholder": "Verzeichnis auswählen...", "saveDirectoryPlaceholder": "Verzeichnis auswählen...",
"browseDirectory": "Durchsuchen", "browseDirectory": "Durchsuchen",
"advancedOptions": "Erweiterte Optionen",
"base": "Basisbibliothek",
"enableRtl": "RTL-Unterstützung aktivieren",
"enableRtlDescription": "Layout-Unterstützung für Rechts-nach-links-Sprachen (z.B. Arabisch, Hebräisch) aktivieren",
"pmChecking": "Wird überprüft...",
"pmNotInstalled": "Nicht installiert",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"create": "Erstellen", "create": "Erstellen",
"creating": "Projekt wird erstellt..." "creating": "Projekt wird erstellt..."

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "Save Directory", "saveDirectory": "Save Directory",
"saveDirectoryPlaceholder": "Select directory...", "saveDirectoryPlaceholder": "Select directory...",
"browseDirectory": "Browse", "browseDirectory": "Browse",
"advancedOptions": "Advanced Options",
"base": "Base Library",
"enableRtl": "Enable RTL Support",
"enableRtlDescription": "Enable layout support for right-to-left languages (e.g. Arabic, Hebrew)",
"pmChecking": "Checking...",
"pmNotInstalled": "Not installed",
"cancel": "Cancel", "cancel": "Cancel",
"create": "Create", "create": "Create",
"creating": "Creating project..." "creating": "Creating project..."

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "Directorio de guardado", "saveDirectory": "Directorio de guardado",
"saveDirectoryPlaceholder": "Seleccionar directorio...", "saveDirectoryPlaceholder": "Seleccionar directorio...",
"browseDirectory": "Explorar", "browseDirectory": "Explorar",
"advancedOptions": "Opciones Avanzadas",
"base": "Biblioteca Base",
"enableRtl": "Habilitar Soporte RTL",
"enableRtlDescription": "Habilitar soporte de diseño para idiomas de derecha a izquierda (ej. árabe, hebreo)",
"pmChecking": "Verificando...",
"pmNotInstalled": "No instalado",
"cancel": "Cancelar", "cancel": "Cancelar",
"create": "Crear", "create": "Crear",
"creating": "Creando proyecto..." "creating": "Creando proyecto..."

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "Répertoire de sauvegarde", "saveDirectory": "Répertoire de sauvegarde",
"saveDirectoryPlaceholder": "Sélectionner un répertoire...", "saveDirectoryPlaceholder": "Sélectionner un répertoire...",
"browseDirectory": "Parcourir", "browseDirectory": "Parcourir",
"advancedOptions": "Options avancées",
"base": "Bibliothèque de base",
"enableRtl": "Activer le support RTL",
"enableRtlDescription": "Activer la prise en charge de la mise en page pour les langues de droite à gauche (ex. arabe, hébreu)",
"pmChecking": "Vérification...",
"pmNotInstalled": "Non installé",
"cancel": "Annuler", "cancel": "Annuler",
"create": "Créer", "create": "Créer",
"creating": "Création du projet..." "creating": "Création du projet..."

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "保存先ディレクトリ", "saveDirectory": "保存先ディレクトリ",
"saveDirectoryPlaceholder": "ディレクトリを選択...", "saveDirectoryPlaceholder": "ディレクトリを選択...",
"browseDirectory": "参照", "browseDirectory": "参照",
"advancedOptions": "詳細オプション",
"base": "基盤ライブラリ",
"enableRtl": "RTL サポートを有効にする",
"enableRtlDescription": "右から左に書く言語(アラビア語、ヘブライ語など)のレイアウトサポートを有効にする",
"pmChecking": "確認中...",
"pmNotInstalled": "未インストール",
"cancel": "キャンセル", "cancel": "キャンセル",
"create": "作成", "create": "作成",
"creating": "プロジェクトを作成中..." "creating": "プロジェクトを作成中..."

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "저장 디렉토리", "saveDirectory": "저장 디렉토리",
"saveDirectoryPlaceholder": "디렉토리 선택...", "saveDirectoryPlaceholder": "디렉토리 선택...",
"browseDirectory": "찾아보기", "browseDirectory": "찾아보기",
"advancedOptions": "고급 옵션",
"base": "기본 라이브러리",
"enableRtl": "RTL 지원 활성화",
"enableRtlDescription": "오른쪽에서 왼쪽으로 쓰는 언어(예: 아랍어, 히브리어)의 레이아웃 지원 활성화",
"pmChecking": "확인 중...",
"pmNotInstalled": "설치되지 않음",
"cancel": "취소", "cancel": "취소",
"create": "만들기", "create": "만들기",
"creating": "프로젝트 생성 중..." "creating": "프로젝트 생성 중..."

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "Diretório de salvamento", "saveDirectory": "Diretório de salvamento",
"saveDirectoryPlaceholder": "Selecionar diretório...", "saveDirectoryPlaceholder": "Selecionar diretório...",
"browseDirectory": "Procurar", "browseDirectory": "Procurar",
"advancedOptions": "Opções Avançadas",
"base": "Biblioteca Base",
"enableRtl": "Ativar Suporte RTL",
"enableRtlDescription": "Ativar suporte de layout para idiomas da direita para a esquerda (ex.: árabe, hebraico)",
"pmChecking": "Verificando...",
"pmNotInstalled": "Não instalado",
"cancel": "Cancelar", "cancel": "Cancelar",
"create": "Criar", "create": "Criar",
"creating": "Criando projeto..." "creating": "Criando projeto..."

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "保存目录", "saveDirectory": "保存目录",
"saveDirectoryPlaceholder": "选择目录...", "saveDirectoryPlaceholder": "选择目录...",
"browseDirectory": "浏览", "browseDirectory": "浏览",
"advancedOptions": "高级选项",
"base": "基础库",
"enableRtl": "启用 RTL 支持",
"enableRtlDescription": "为从右到左书写的语言(如阿拉伯语、希伯来语)启用布局支持",
"pmChecking": "正在检测...",
"pmNotInstalled": "未安装",
"cancel": "取消", "cancel": "取消",
"create": "创建", "create": "创建",
"creating": "正在创建项目..." "creating": "正在创建项目..."

View File

@@ -1606,6 +1606,12 @@
"saveDirectory": "儲存目錄", "saveDirectory": "儲存目錄",
"saveDirectoryPlaceholder": "選擇目錄...", "saveDirectoryPlaceholder": "選擇目錄...",
"browseDirectory": "瀏覽", "browseDirectory": "瀏覽",
"advancedOptions": "進階選項",
"base": "基礎庫",
"enableRtl": "啟用 RTL 支援",
"enableRtlDescription": "為從右到左書寫的語言(如阿拉伯語、希伯來語)啟用佈局支援",
"pmChecking": "正在檢測...",
"pmNotInstalled": "未安裝",
"cancel": "取消", "cancel": "取消",
"create": "建立", "create": "建立",
"creating": "正在建立專案..." "creating": "正在建立專案..."

View File

@@ -44,6 +44,7 @@ import type {
SystemProxySettings, SystemProxySettings,
GitCredentials, GitCredentials,
GitDetectResult, GitDetectResult,
PackageManagerInfo,
GitSettings, GitSettings,
GitHubAccountsSettings, GitHubAccountsSettings,
GitHubTokenValidation, GitHubTokenValidation,
@@ -958,6 +959,12 @@ export async function openProjectBootWindow(): Promise<void> {
window.open("/project-boot", "project-boot") window.open("/project-boot", "project-boot")
} }
export async function detectPackageManager(
name: string
): Promise<PackageManagerInfo> {
return getTransport().call("detect_package_manager", { name })
}
export async function createShadcnProject(params: { export async function createShadcnProject(params: {
projectName: string projectName: string
template: string template: string

View File

@@ -536,6 +536,12 @@ export interface GitDetectResult {
path: string | null path: string | null
} }
export interface PackageManagerInfo {
name: string
installed: boolean
version: string | null
}
export interface GitSettings { export interface GitSettings {
custom_path: string | null custom_path: string | null
} }