-
- {category.label}
-
-
- {category.skills.map((skill) => (
+ {recentTutorials.length > 0 ? (
+
+ {recentTutorials.map((tutorial) => {
+ const isCompleted = progress[tutorial.id]?.completed;
+ return (
router.push(`/tutorial/${tutorial.id}`)}
>
-
- {skill.name}
-
-
+
+
+
+
+ {getDifficultyText(tutorial.difficulty)}
+
+ {isCompleted && (
+
+
+ 已完成
+
+ )}
+
+
+ {tutorial.title}
+
+
+
+ {isCompleted ? : }
+
+
-
-
-
{skill.time}
-
-
{skill.platforms.join(" / ")}
+
+ {tutorial.description}
+
+
+
+
+
+ {tutorial.duration} 分钟
+
+
+
- ))}
+ );
+ })}
+
+ ) : (
+
+
+
没有找到匹配的教程
+
+
+ )}
+
+
+ {/* Quick Actions */}
+
+
+
+
- ))}
-
- 共 {totalSkills} 个教程 | 从入门到高级逐步解锁
+
+
+
+
+
-
+
);
diff --git a/playground/apps/desktop/src/app/series/[id]/client.tsx b/playground/apps/desktop/src/app/series/[id]/client.tsx
new file mode 100644
index 0000000..84f74e5
--- /dev/null
+++ b/playground/apps/desktop/src/app/series/[id]/client.tsx
@@ -0,0 +1,279 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { useAppStore } from "@/store/useAppStore";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ Button,
+ Badge,
+} from "@innate/ui";
+import {
+ ArrowLeft,
+ BookOpen,
+ Clock,
+ BarChart3,
+ Trophy,
+ Play,
+ Sparkles,
+ CheckCircle,
+ Circle,
+} from "lucide-react";
+
+interface SeriesDetailClientProps {
+ id: string;
+}
+
+export default function SeriesDetailClient({ id }: SeriesDetailClientProps) {
+ const router = useRouter();
+ const seriesId = id;
+
+ const { series, getTutorialsBySeries, progress } = useAppStore();
+
+ const currentSeries = series.find((s) => s.id === seriesId);
+ const tutorials = getTutorialsBySeries(seriesId);
+
+ if (!currentSeries) {
+ return (
+
+ );
+ }
+
+ const completedCount = tutorials.filter((t) => progress[t.id]?.completed).length;
+ const progressPercent = tutorials.length > 0 ? (completedCount / tutorials.length) * 100 : 0;
+ const totalDuration = tutorials.reduce((sum, t) => sum + t.duration, 0);
+ const nextTutorial = tutorials.find((t) => !progress[t.id]?.completed);
+
+ const getDifficultyColor = (difficulty: string) => {
+ switch (difficulty) {
+ case "beginner":
+ return "from-emerald-500 to-teal-500";
+ case "intermediate":
+ return "from-amber-500 to-orange-500";
+ case "advanced":
+ return "from-rose-500 to-pink-500";
+ default:
+ return "from-primary to-secondary";
+ }
+ };
+
+ const getDifficultyText = (difficulty: string) => {
+ switch (difficulty) {
+ case "beginner":
+ return "入门";
+ case "intermediate":
+ return "进阶";
+ case "advanced":
+ return "高级";
+ default:
+ return "入门";
+ }
+ };
+
+ return (
+
+ {/* Hero Header */}
+
+
+
+
+
+
+
+
+
+ {currentSeries.icon || "📚"}
+
+
+
+
+
+ {getDifficultyText(currentSeries.difficulty)}
+
+
+
+ {tutorials.length} 个教程
+
+
+
+ {totalDuration} 分钟
+
+
+
+
{currentSeries.title}
+
+ {currentSeries.description}
+
+
+ {/* Progress Section */}
+
+
+
+
+ 学习进度
+
+
+ {Math.round(progressPercent)}%
+
+ ({completedCount}/{tutorials.length})
+
+
+
+
+
+ {nextTutorial && progressPercent < 100 && (
+
+ )}
+
+ {progressPercent === 100 && (
+
+
+ 恭喜!你已完成本系列所有教程
+
+ )}
+
+
+
+
+
+
+ {/* Tutorials List */}
+
+
+
+ {tutorials.length > 0 ? (
+
+ {tutorials.map((tutorial) => {
+ const isCompleted = progress[tutorial.id]?.completed;
+ return (
+
router.push(`/tutorial/${tutorial.id}`)}
+ >
+
+
+
+
+
+ {getDifficultyText(tutorial.difficulty)}
+
+ {isCompleted && (
+
+
+ 已完成
+
+ )}
+
+
+ {tutorial.title}
+
+
+
+ {isCompleted ? : }
+
+
+
+
+
+ {tutorial.description}
+
+
+
+
+
+ {tutorial.duration} 分钟
+
+
+
+
+
+
+ );
+ })}
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/playground/apps/desktop/src/app/series/[id]/page.tsx b/playground/apps/desktop/src/app/series/[id]/page.tsx
new file mode 100644
index 0000000..4cdd915
--- /dev/null
+++ b/playground/apps/desktop/src/app/series/[id]/page.tsx
@@ -0,0 +1,11 @@
+import { series } from "@/data/series";
+import SeriesDetailClient from "./client";
+
+export function generateStaticParams() {
+ return series.map((s) => ({ id: s.id }));
+}
+
+export default async function SeriesDetailPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ return
;
+}
diff --git a/playground/apps/desktop/src/app/tutorial/[id]/client.tsx b/playground/apps/desktop/src/app/tutorial/[id]/client.tsx
new file mode 100644
index 0000000..9cdf57b
--- /dev/null
+++ b/playground/apps/desktop/src/app/tutorial/[id]/client.tsx
@@ -0,0 +1,285 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useAppStore } from "@/store/useAppStore";
+import {
+ Card,
+ CardContent,
+ Button,
+ Badge,
+ Separator,
+ ScrollArea,
+} from "@innate/ui";
+import {
+ ArrowLeft,
+ Play,
+ CheckCircle,
+ Clock,
+ Terminal,
+ Copy,
+ Check,
+ Sparkles,
+ RotateCcw,
+} from "lucide-react";
+
+interface TutorialDetailClientProps {
+ id: string;
+}
+
+export default function TutorialDetailClient({ id }: TutorialDetailClientProps) {
+ const router = useRouter();
+ const tutorialId = id;
+ const [copiedId, setCopiedId] = useState
(null);
+
+ const {
+ tutorials,
+ showTerminal,
+ addTerminalOutput,
+ setIsExecuting,
+ updateProgress,
+ progress,
+ } = useAppStore();
+
+ const tutorial = tutorials.find((t) => t.id === tutorialId);
+ const tutorialProgress = progress[tutorialId];
+
+ if (!tutorial) {
+ return (
+
+ );
+ }
+
+ const handleRun = (code: string, id: string) => {
+ showTerminal();
+ setIsExecuting(true);
+
+ addTerminalOutput(`$ ${code}`);
+
+ setTimeout(() => {
+ addTerminalOutput("");
+ addTerminalOutput("\x1b[36mℹ\x1b[0m 正在执行命令...");
+ addTerminalOutput("");
+
+ setTimeout(() => {
+ addTerminalOutput("\x1b[32m✓\x1b[0m 命令执行成功!");
+ addTerminalOutput("");
+ setIsExecuting(false);
+ }, 800);
+ }, 300);
+ };
+
+ const handleCopy = (code: string, id: string) => {
+ navigator.clipboard.writeText(code);
+ setCopiedId(id);
+ setTimeout(() => setCopiedId(null), 2000);
+ };
+
+ const handleMarkComplete = () => {
+ updateProgress({
+ tutorialId,
+ completed: true,
+ completedSections: [],
+ completedAt: new Date().toISOString(),
+ });
+ };
+
+ const handleReset = () => {
+ updateProgress({
+ tutorialId,
+ completed: false,
+ completedSections: [],
+ });
+ };
+
+ const getDifficultyConfig = (difficulty: string) => {
+ switch (difficulty) {
+ case "beginner":
+ return { text: "入门", color: "text-emerald-500", bg: "bg-emerald-500/10", border: "border-emerald-500/20" };
+ case "intermediate":
+ return { text: "进阶", color: "text-amber-500", bg: "bg-amber-500/10", border: "border-amber-500/20" };
+ case "advanced":
+ return { text: "高级", color: "text-rose-500", bg: "bg-rose-500/10", border: "border-rose-500/20" };
+ default:
+ return { text: "入门", color: "text-emerald-500", bg: "bg-emerald-500/10", border: "border-emerald-500/20" };
+ }
+ };
+
+ const difficulty = getDifficultyConfig(tutorial.difficulty);
+
+ const sections = [
+ {
+ type: "text" as const,
+ content: `## 什么是 ${tutorial.title}?\n\n这是一个关于 ${tutorial.title} 的教程。在这里,你将学习如何使用相关工具,并通过实际操作来掌握核心概念。本教程适合${difficulty.text}水平的用户。`,
+ },
+ {
+ type: "text" as const,
+ content: "## 前置条件\n\n在开始之前,请确保你已经:\n- 安装了终端工具\n- 具备基本的命令行知识\n- 有稳定的网络连接",
+ },
+ {
+ type: "executable" as const,
+ id: "step-1",
+ title: "安装",
+ description: "执行以下命令进行安装:",
+ code: "brew install node",
+ language: "bash",
+ },
+ {
+ type: "text" as const,
+ content: "安装完成后,你可以通过运行 `node -v` 来验证安装是否成功。如果看到版本号输出,说明安装成功。",
+ },
+ {
+ type: "executable" as const,
+ id: "step-2",
+ title: "验证安装",
+ description: "验证 Node.js 和 npm 是否正确安装:",
+ code: "node -v && npm -v",
+ language: "bash",
+ },
+ {
+ type: "text" as const,
+ content: "## 总结\n\n恭喜!你已经完成了本教程的学习。现在你可以开始使用这个工具了。建议继续学习系列中的其他教程,以获得更全面的知识。",
+ },
+ ];
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+
+ {difficulty.text}
+
+
+
+ {tutorial.duration} 分钟
+
+ {tutorialProgress?.completed && (
+
+
+ 已完成
+
+ )}
+
+
+
{tutorial.title}
+
{tutorial.description}
+
+
+
+ {tutorialProgress?.completed ? (
+ <>
+
+
+
+ 已完成
+
+ >
+ ) : (
+
+ )}
+
+
+
+
+ {/* Content */}
+
+
+ {sections.map((section, index) => (
+
+ {section.type === "text" ? (
+
+
$1')
+ .replace(/- (.*)/g, '
$1')
+ .replace(/`([^`]+)`/g, '$1'),
+ }}
+ />
+
+ ) : section.type === "executable" ? (
+
+
+
+
+
+
+
+
{(section as any).title}
+
{(section as any).description}
+
+
+
+
+
+
+
+
+
+ {(section as any).code}
+
+
+
+ ) : null}
+
+ ))}
+
+
+ {/* Footer CTA */}
+
+
+
+
+
+
+
+ {tutorialProgress?.completed ? "想要学习更多?" : "完成本教程!"}
+
+
+ {tutorialProgress?.completed
+ ? "继续探索系列中的其他教程,提升你的技能。"
+ : "完成上面的步骤,然后点击\"标记完成\"按钮。"}
+
+
+
+
+
+
+ );
+}
diff --git a/playground/apps/desktop/src/app/tutorial/[id]/page.tsx b/playground/apps/desktop/src/app/tutorial/[id]/page.tsx
new file mode 100644
index 0000000..564f2cd
--- /dev/null
+++ b/playground/apps/desktop/src/app/tutorial/[id]/page.tsx
@@ -0,0 +1,11 @@
+import { tutorials } from "@/data/tutorials";
+import TutorialDetailClient from "./client";
+
+export function generateStaticParams() {
+ return tutorials.map((t) => ({ id: t.id }));
+}
+
+export default async function TutorialDetailPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ return
;
+}
diff --git a/playground/apps/desktop/src/app/tutorial/[slug]/page.tsx b/playground/apps/desktop/src/app/tutorial/[slug]/page.tsx
deleted file mode 100644
index 7cbe9f3..0000000
--- a/playground/apps/desktop/src/app/tutorial/[slug]/page.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { notFound } from "next/navigation";
-import { TutorialPageClient } from "./tutorial-client";
-
-const tutorialMeta: Record
= {
- "terminal-setup": {
- title: "终端环境配置",
- files: ["/tutorials/terminal-setup-mac.md"],
- tag: "入门",
- },
- "cmd-basics": {
- title: "命令行 5 分钟入门",
- files: ["/tutorials/cmd-basics.md"],
- tag: "入门",
- },
-};
-
-export function generateStaticParams() {
- return Object.keys(tutorialMeta).map((slug) => ({ slug }));
-}
-
-export default function TutorialPage({ params }: { params: Promise<{ slug: string }> }) {
- return ;
-}
-
-async function TutorialPageClientWrapper({ params }: { params: Promise<{ slug: string }> }) {
- const { slug } = await params;
- const meta = tutorialMeta[slug];
- if (!meta) notFound();
- return ;
-}
diff --git a/playground/apps/desktop/src/app/tutorial/[slug]/tutorial-client.tsx b/playground/apps/desktop/src/app/tutorial/[slug]/tutorial-client.tsx
deleted file mode 100644
index 0aa9ee5..0000000
--- a/playground/apps/desktop/src/app/tutorial/[slug]/tutorial-client.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import {
- Button,
- Separator,
- SidebarTrigger,
- Badge,
-} from "@innate/ui";
-import {
- ResizablePanelGroup,
- ResizablePanel,
- ResizableHandle,
-} from "@innate/ui";
-import { ArrowLeft, BookOpen } from "lucide-react";
-import { useRouter } from "next/navigation";
-import { TutorialMarkdown } from "@/components/tutorial/tutorial-markdown";
-import { TerminalPanel } from "@/components/tutorial/terminal-panel";
-import { TerminalProvider, useTerminal } from "@/components/tutorial/terminal-context";
-
-interface TutorialMeta {
- title: string;
- files: string[];
- tag: string;
-}
-
-export function TutorialPageClient({ meta }: { meta: TutorialMeta }) {
- return (
-
-
-
- );
-}
-
-function TutorialPageInner({ meta }: { meta: TutorialMeta }) {
- const router = useRouter();
- const [content, setContent] = useState(null);
- const [loading, setLoading] = useState(true);
- const { pendingCommand, sendCommand } = useTerminal();
-
- useEffect(() => {
- async function load() {
- try {
- const parts = await Promise.all(
- meta.files.map(async (file) => {
- const res = await fetch(file);
- if (!res.ok) throw new Error(`Failed to load ${file}`);
- return res.text();
- })
- );
- setContent(parts.join("\n\n---\n\n"));
- } catch {
- setContent("# 加载失败\n\n教程内容加载出错,请确认 `/public/tutorials/` 目录中有对应的 Markdown 文件。");
- } finally {
- setLoading(false);
- }
- }
- load();
- }, [meta]);
-
- const commandForTerminal = pendingCommand ? pendingCommand.split("__")[0] : null;
-
- return (
-
-
-
-
-
-
- {meta.title}
- {meta.tag}
-
-
-
-
-
-
- {loading ? (
-
加载中...
- ) : content ? (
-
- ) : null}
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/playground/apps/desktop/src/components/layout/app-shell-inner.tsx b/playground/apps/desktop/src/components/layout/app-shell-inner.tsx
index 61b8548..b26c663 100644
--- a/playground/apps/desktop/src/components/layout/app-shell-inner.tsx
+++ b/playground/apps/desktop/src/components/layout/app-shell-inner.tsx
@@ -4,15 +4,34 @@ import { ReactNode } from "react";
import { SidebarProvider, SidebarInset } from "@innate/ui";
import { AppSidebar } from "@/components/layout/app-sidebar";
import { StatusBar } from "@/components/layout/status-bar";
+import { TerminalPanel } from "@/components/terminal-panel";
+import { useAppStore } from "@/store/useAppStore";
export function AppShellInner({ children }: { children: ReactNode }) {
+ const { terminalVisible, terminalPosition } = useAppStore();
+
return (
-
- {children}
+
+
+
+ {children}
+
+
+ {/* Terminal - Right Side */}
+ {terminalVisible && terminalPosition === "right" && (
+
+ )}
+
+
+ {/* Terminal - Bottom */}
+ {terminalVisible && terminalPosition === "bottom" && (
+
+ )}
+
-
+
);
}
diff --git a/playground/apps/desktop/src/components/layout/app-shell.tsx b/playground/apps/desktop/src/components/layout/app-shell.tsx
index 80f311a..edb1d2a 100644
--- a/playground/apps/desktop/src/components/layout/app-shell.tsx
+++ b/playground/apps/desktop/src/components/layout/app-shell.tsx
@@ -1,21 +1,58 @@
"use client";
-import { ReactNode } from "react";
-import dynamic from "next/dynamic";
+import { ReactNode, useEffect, useState } from "react";
+import { SidebarProvider, SidebarInset } from "@innate/ui";
+import { AppSidebar } from "@/components/layout/app-sidebar";
+import { StatusBar } from "@/components/layout/status-bar";
+import { TerminalPanel } from "@/components/terminal-panel";
+import { useAppStore } from "@/store/useAppStore";
-const AppShellInner = dynamic(
- () =>
- import("./app-shell-inner").then((m) => m.AppShellInner),
- {
- ssr: false,
- loading: () => (
-
- Loading...
+function AppShellContent({ children }: { children: ReactNode }) {
+ const { terminalVisible, terminalPosition } = useAppStore();
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ // Prevent hydration mismatch by not rendering until mounted
+ if (!mounted) {
+ return (
+
- ),
+ );
}
-);
+
+ return (
+
+
+
+
+
+ {children}
+
+
+ {/* Terminal - Right Side */}
+ {terminalVisible && terminalPosition === "right" && (
+
+ )}
+
+
+ {/* Terminal - Bottom */}
+ {terminalVisible && terminalPosition === "bottom" && (
+
+ )}
+
+
+
+
+ );
+}
export function AppShell({ children }: { children: ReactNode }) {
- return
{children};
+ return
{children};
}
diff --git a/playground/apps/desktop/src/components/terminal-panel.tsx b/playground/apps/desktop/src/components/terminal-panel.tsx
new file mode 100644
index 0000000..3406b08
--- /dev/null
+++ b/playground/apps/desktop/src/components/terminal-panel.tsx
@@ -0,0 +1,136 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import { useAppStore } from "@/store/useAppStore";
+import { Card, Button, ScrollArea } from "@innate/ui";
+import { X, Minimize2, PanelRight, PanelBottom, Trash2, Terminal } from "lucide-react";
+
+export function TerminalPanel() {
+ const scrollRef = useRef
(null);
+ const {
+ terminalPosition,
+ terminalVisible,
+ terminalOutput,
+ isExecuting,
+ hideTerminal,
+ toggleTerminalPosition,
+ clearTerminal,
+ } = useAppStore();
+
+ useEffect(() => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ }, [terminalOutput]);
+
+ if (!terminalVisible) return null;
+
+ const renderOutput = (line: string, index: number) => {
+ if (line.startsWith("$")) {
+ return (
+
+ ➜
+ ~
+ {line.slice(2)}
+
+ );
+ }
+
+ const parts = line.split(/(\x1b\[\d+m)/g);
+ const elements: React.ReactNode[] = [];
+ let currentColor = "";
+
+ parts.forEach((part, i) => {
+ if (part.startsWith("\x1b[")) {
+ const code = part.slice(2, -1);
+ switch (code) {
+ case "32m":
+ currentColor = "text-emerald-400";
+ break;
+ case "31m":
+ currentColor = "text-rose-400";
+ break;
+ case "33m":
+ currentColor = "text-amber-400";
+ break;
+ case "36m":
+ currentColor = "text-cyan-400";
+ break;
+ case "35m":
+ currentColor = "text-fuchsia-400";
+ break;
+ case "0m":
+ currentColor = "";
+ break;
+ }
+ } else if (part) {
+ elements.push(
+
+ {part}
+
+ );
+ }
+ });
+
+ return {elements.length > 0 ? elements : line}
;
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+
+ 终端
+ {isExecuting && (
+
+
+ 执行中
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ {/* Output */}
+
+
+ {terminalOutput.length === 0 ? (
+
+ ) : (
+ terminalOutput.map((line, index) => renderOutput(line, index))
+ )}
+
+
+
+ );
+}
diff --git a/playground/apps/desktop/src/data/series.ts b/playground/apps/desktop/src/data/series.ts
new file mode 100644
index 0000000..0767c70
--- /dev/null
+++ b/playground/apps/desktop/src/data/series.ts
@@ -0,0 +1,52 @@
+import { Series } from "@/types";
+
+export const series: Series[] = [
+ {
+ id: 'nodejs-fundamentals',
+ title: 'Node.js 基础',
+ description: '从零开始学习 Node.js 开发环境配置',
+ category: 'dev-tools',
+ difficulty: 'beginner',
+ icon: '📦',
+ color: '#339933',
+ tutorials: ['tutorial-001'],
+ createdAt: '2026-04-01',
+ updatedAt: '2026-04-01',
+ },
+ {
+ id: 'git-fundamentals',
+ title: 'Git 基础',
+ description: '掌握版本控制的核心概念',
+ category: 'dev-tools',
+ difficulty: 'beginner',
+ icon: '🌲',
+ color: '#f05032',
+ tutorials: ['tutorial-002'],
+ createdAt: '2026-04-01',
+ updatedAt: '2026-04-01',
+ },
+ {
+ id: 'python-fundamentals',
+ title: 'Python 基础',
+ description: 'Python 开发环境配置指南',
+ category: 'dev-tools',
+ difficulty: 'beginner',
+ icon: '🐍',
+ color: '#3776ab',
+ tutorials: ['tutorial-003'],
+ createdAt: '2026-04-01',
+ updatedAt: '2026-04-01',
+ },
+ {
+ id: 'terminal-basics',
+ title: '终端基础',
+ description: '命令行入门必修课程',
+ category: 'terminal',
+ difficulty: 'beginner',
+ icon: '🖥️',
+ color: '#4a5568',
+ tutorials: ['tutorial-004', 'tutorial-005'],
+ createdAt: '2026-04-01',
+ updatedAt: '2026-04-01',
+ },
+];
diff --git a/playground/apps/desktop/src/data/tutorials.ts b/playground/apps/desktop/src/data/tutorials.ts
new file mode 100644
index 0000000..33ecbb1
--- /dev/null
+++ b/playground/apps/desktop/src/data/tutorials.ts
@@ -0,0 +1,74 @@
+import { Tutorial } from "@/types";
+
+export const tutorials: Tutorial[] = [
+ {
+ id: 'tutorial-001',
+ title: '安装 Node.js',
+ description: '使用 fnm 安装和管理 Node.js 版本',
+ category: 'dev-tools',
+ difficulty: 'beginner',
+ duration: 10,
+ tags: ['nodejs', 'fnm', 'javascript'],
+ series: 'nodejs-fundamentals',
+ order: 1,
+ createdAt: '2026-04-01',
+ updatedAt: '2026-04-01',
+ source: 'builtin',
+ },
+ {
+ id: 'tutorial-002',
+ title: 'Git 基础',
+ description: '学习 Git 的基本操作',
+ category: 'dev-tools',
+ difficulty: 'beginner',
+ duration: 15,
+ tags: ['git', 'version-control'],
+ series: 'git-fundamentals',
+ order: 1,
+ createdAt: '2026-04-01',
+ updatedAt: '2026-04-01',
+ source: 'builtin',
+ },
+ {
+ id: 'tutorial-003',
+ title: 'Python 环境配置',
+ description: '使用 uv 管理 Python 环境',
+ category: 'dev-tools',
+ difficulty: 'beginner',
+ duration: 10,
+ tags: ['python', 'uv'],
+ series: 'python-fundamentals',
+ order: 1,
+ createdAt: '2026-04-01',
+ updatedAt: '2026-04-01',
+ source: 'builtin',
+ },
+ {
+ id: 'tutorial-004',
+ title: 'ls 命令详解',
+ description: '掌握目录列表命令',
+ category: 'terminal',
+ difficulty: 'beginner',
+ duration: 5,
+ tags: ['terminal', 'bash'],
+ series: 'terminal-basics',
+ order: 1,
+ createdAt: '2026-04-01',
+ updatedAt: '2026-04-01',
+ source: 'builtin',
+ },
+ {
+ id: 'tutorial-005',
+ title: 'cd 和 pwd 命令',
+ description: '学习目录导航',
+ category: 'terminal',
+ difficulty: 'beginner',
+ duration: 5,
+ tags: ['terminal', 'bash'],
+ series: 'terminal-basics',
+ order: 2,
+ createdAt: '2026-04-01',
+ updatedAt: '2026-04-01',
+ source: 'builtin',
+ },
+];
diff --git a/playground/apps/desktop/src/store/useAppStore.ts b/playground/apps/desktop/src/store/useAppStore.ts
new file mode 100644
index 0000000..4fed30c
--- /dev/null
+++ b/playground/apps/desktop/src/store/useAppStore.ts
@@ -0,0 +1,119 @@
+import { create } from 'zustand';
+import { Tutorial, Series, Progress, TerminalPosition } from '../types';
+import { tutorials as initialTutorials } from '../data/tutorials';
+import { series as initialSeries } from '../data/series';
+
+interface AppState {
+ // Data
+ tutorials: Tutorial[];
+ series: Series[];
+ progress: Record;
+
+ // UI State
+ searchQuery: string;
+ selectedCategory: string | null;
+ selectedDifficulty: string | null;
+
+ // Terminal State
+ terminalPosition: TerminalPosition;
+ terminalVisible: boolean;
+ isExecuting: boolean;
+ terminalOutput: string[];
+
+ // Actions
+ setTutorials: (tutorials: Tutorial[]) => void;
+ setSeries: (series: Series[]) => void;
+ setSearchQuery: (query: string) => void;
+ setCategory: (category: string | null) => void;
+ setDifficulty: (difficulty: string | null) => void;
+
+ // Terminal Actions
+ showTerminal: () => void;
+ hideTerminal: () => void;
+ toggleTerminalPosition: () => void;
+ setTerminalPosition: (position: TerminalPosition) => void;
+ addTerminalOutput: (output: string) => void;
+ clearTerminal: () => void;
+ setIsExecuting: (executing: boolean) => void;
+
+ // Progress Actions
+ updateProgress: (progress: Progress) => void;
+
+ // Getters
+ getFilteredTutorials: () => Tutorial[];
+ getTutorialsBySeries: (seriesId: string) => Tutorial[];
+}
+
+// Re-export for backward compatibility
+const mockTutorials = initialTutorials;
+const mockSeries = initialSeries;
+
+export const useAppStore = create((set, get) => ({
+ // Initial state
+ tutorials: mockTutorials,
+ series: mockSeries,
+ progress: {},
+
+ searchQuery: '',
+ selectedCategory: null,
+ selectedDifficulty: null,
+
+ terminalPosition: 'hidden',
+ terminalVisible: false,
+ isExecuting: false,
+ terminalOutput: [],
+
+ // Actions
+ setTutorials: (tutorials) => set({ tutorials }),
+ setSeries: (series) => set({ series }),
+ setSearchQuery: (searchQuery) => set({ searchQuery }),
+ setCategory: (selectedCategory) => set({ selectedCategory }),
+ setDifficulty: (selectedDifficulty) => set({ selectedDifficulty }),
+
+ // Terminal Actions
+ showTerminal: () => set({
+ terminalVisible: true,
+ terminalPosition: 'right'
+ }),
+ hideTerminal: () => set({
+ terminalVisible: false,
+ terminalPosition: 'hidden'
+ }),
+ toggleTerminalPosition: () => set((state) => ({
+ terminalPosition: state.terminalPosition === 'right' ? 'bottom' : 'right',
+ })),
+ setTerminalPosition: (terminalPosition) => set({ terminalPosition }),
+ addTerminalOutput: (output) => set((state) => ({
+ terminalOutput: [...state.terminalOutput, output],
+ })),
+ clearTerminal: () => set({ terminalOutput: [] }),
+ setIsExecuting: (isExecuting) => set({ isExecuting }),
+
+ // Progress Actions
+ updateProgress: (progress) => set((state) => ({
+ progress: {
+ ...state.progress,
+ [progress.tutorialId]: progress,
+ },
+ })),
+
+ // Getters
+ getFilteredTutorials: () => {
+ const { tutorials, searchQuery, selectedCategory, selectedDifficulty } = get();
+ return tutorials.filter((tutorial) => {
+ const matchesSearch = !searchQuery ||
+ tutorial.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ tutorial.description.toLowerCase().includes(searchQuery.toLowerCase());
+ const matchesCategory = !selectedCategory || tutorial.category === selectedCategory;
+ const matchesDifficulty = !selectedDifficulty || tutorial.difficulty === selectedDifficulty;
+ return matchesSearch && matchesCategory && matchesDifficulty;
+ });
+ },
+
+ getTutorialsBySeries: (seriesId: string) => {
+ const { tutorials } = get();
+ return tutorials
+ .filter((t) => t.series === seriesId)
+ .sort((a, b) => (a.order || 0) - (b.order || 0));
+ },
+}));
diff --git a/playground/apps/desktop/src/types/index.ts b/playground/apps/desktop/src/types/index.ts
new file mode 100644
index 0000000..c581ba9
--- /dev/null
+++ b/playground/apps/desktop/src/types/index.ts
@@ -0,0 +1,61 @@
+export interface Tutorial {
+ id: string;
+ title: string;
+ description: string;
+ category: string;
+ difficulty: 'beginner' | 'intermediate' | 'advanced';
+ duration: number;
+ tags: string[];
+ series?: string;
+ order?: number;
+ content?: TutorialSection[];
+ executableBlocks?: ExecutableBlock[];
+ author?: string;
+ createdAt: string;
+ updatedAt: string;
+ source?: 'builtin' | 'local' | 'imported';
+ localPath?: string;
+}
+
+export interface TutorialSection {
+ id: string;
+ type: 'text' | 'code' | 'executable' | 'image' | 'video';
+ content: string;
+ language?: string;
+ executable?: boolean;
+}
+
+export interface ExecutableBlock {
+ id: string;
+ code: string;
+ language: 'bash' | 'powershell' | 'python' | 'javascript';
+ platform?: ('macos' | 'windows' | 'linux')[];
+ workingDirectory?: string;
+ environment?: Record;
+ expectedOutput?: string;
+}
+
+export interface Series {
+ id: string;
+ title: string;
+ description: string;
+ category: string;
+ difficulty: 'beginner' | 'intermediate' | 'advanced';
+ icon?: string;
+ color?: string;
+ tutorials: string[];
+ author?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Progress {
+ tutorialId: string;
+ completed: boolean;
+ lastSection?: string;
+ completedSections: string[];
+ startedAt?: string;
+ completedAt?: string;
+}
+
+export type TerminalPosition = 'hidden' | 'right' | 'bottom';
diff --git a/playground/packages/ui/package.json b/playground/packages/ui/package.json
index 7af5ec2..1d8e286 100644
--- a/playground/packages/ui/package.json
+++ b/playground/packages/ui/package.json
@@ -66,6 +66,7 @@
"clean": "rm -rf .turbo node_modules"
},
"dependencies": {
+ "@innate/utils": "workspace:*",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "1.2.12",
"@radix-ui/react-alert-dialog": "1.1.15",
diff --git a/playground/packages/ui/src/components/ui/accordion.tsx b/playground/packages/ui/src/components/ui/accordion.tsx
index 565e660..62baf4a 100644
--- a/playground/packages/ui/src/components/ui/accordion.tsx
+++ b/playground/packages/ui/src/components/ui/accordion.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDownIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Accordion({
...props
diff --git a/playground/packages/ui/src/components/ui/alert-dialog.tsx b/playground/packages/ui/src/components/ui/alert-dialog.tsx
index d50c0fe..6db4f30 100644
--- a/playground/packages/ui/src/components/ui/alert-dialog.tsx
+++ b/playground/packages/ui/src/components/ui/alert-dialog.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { buttonVariants } from './button'
function AlertDialog({
diff --git a/playground/packages/ui/src/components/ui/alert.tsx b/playground/packages/ui/src/components/ui/alert.tsx
index 4f947f2..c5bdc2e 100644
--- a/playground/packages/ui/src/components/ui/alert.tsx
+++ b/playground/packages/ui/src/components/ui/alert.tsx
@@ -1,7 +1,7 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
diff --git a/playground/packages/ui/src/components/ui/avatar.tsx b/playground/packages/ui/src/components/ui/avatar.tsx
index db3b170..edae07b 100644
--- a/playground/packages/ui/src/components/ui/avatar.tsx
+++ b/playground/packages/ui/src/components/ui/avatar.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Avatar({
className,
diff --git a/playground/packages/ui/src/components/ui/badge.tsx b/playground/packages/ui/src/components/ui/badge.tsx
index 0ff2907..9c48afb 100644
--- a/playground/packages/ui/src/components/ui/badge.tsx
+++ b/playground/packages/ui/src/components/ui/badge.tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
const badgeVariants = cva(
'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
diff --git a/playground/packages/ui/src/components/ui/breadcrumb.tsx b/playground/packages/ui/src/components/ui/breadcrumb.tsx
index 34e0d5a..a797afe 100644
--- a/playground/packages/ui/src/components/ui/breadcrumb.tsx
+++ b/playground/packages/ui/src/components/ui/breadcrumb.tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { ChevronRight, MoreHorizontal } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
return
diff --git a/playground/packages/ui/src/components/ui/button-group.tsx b/playground/packages/ui/src/components/ui/button-group.tsx
index 3d415c0..997275d 100644
--- a/playground/packages/ui/src/components/ui/button-group.tsx
+++ b/playground/packages/ui/src/components/ui/button-group.tsx
@@ -1,7 +1,7 @@
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { Separator } from './separator'
const buttonGroupVariants = cva(
diff --git a/playground/packages/ui/src/components/ui/button.tsx b/playground/packages/ui/src/components/ui/button.tsx
index c5f9ccd..6a97505 100644
--- a/playground/packages/ui/src/components/ui/button.tsx
+++ b/playground/packages/ui/src/components/ui/button.tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
diff --git a/playground/packages/ui/src/components/ui/calendar.tsx b/playground/packages/ui/src/components/ui/calendar.tsx
index c7d870f..fd85053 100644
--- a/playground/packages/ui/src/components/ui/calendar.tsx
+++ b/playground/packages/ui/src/components/ui/calendar.tsx
@@ -8,7 +8,7 @@ import {
} from 'lucide-react'
import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { Button, buttonVariants } from './button'
function Calendar({
diff --git a/playground/packages/ui/src/components/ui/card.tsx b/playground/packages/ui/src/components/ui/card.tsx
index c501595..08a004c 100644
--- a/playground/packages/ui/src/components/ui/card.tsx
+++ b/playground/packages/ui/src/components/ui/card.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
diff --git a/playground/packages/ui/src/components/ui/carousel.tsx b/playground/packages/ui/src/components/ui/carousel.tsx
index edb6e2e..c12e5c3 100644
--- a/playground/packages/ui/src/components/ui/carousel.tsx
+++ b/playground/packages/ui/src/components/ui/carousel.tsx
@@ -6,7 +6,7 @@ import useEmblaCarousel, {
} from 'embla-carousel-react'
import { ArrowLeft, ArrowRight } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { Button } from './button'
type CarouselApi = UseEmblaCarouselType[1]
diff --git a/playground/packages/ui/src/components/ui/chart.tsx b/playground/packages/ui/src/components/ui/chart.tsx
index 425551b..abc8e89 100644
--- a/playground/packages/ui/src/components/ui/chart.tsx
+++ b/playground/packages/ui/src/components/ui/chart.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as RechartsPrimitive from 'recharts'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
// Format: { THEME_NAME: CSS_SELECTOR }
const THEMES = { light: '', dark: '.dark' } as const
diff --git a/playground/packages/ui/src/components/ui/checkbox.tsx b/playground/packages/ui/src/components/ui/checkbox.tsx
index d79fcdb..2b54bfa 100644
--- a/playground/packages/ui/src/components/ui/checkbox.tsx
+++ b/playground/packages/ui/src/components/ui/checkbox.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { CheckIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Checkbox({
className,
diff --git a/playground/packages/ui/src/components/ui/command.tsx b/playground/packages/ui/src/components/ui/command.tsx
index 87747bc..8655409 100644
--- a/playground/packages/ui/src/components/ui/command.tsx
+++ b/playground/packages/ui/src/components/ui/command.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import { Command as CommandPrimitive } from 'cmdk'
import { SearchIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import {
Dialog,
DialogContent,
diff --git a/playground/packages/ui/src/components/ui/context-menu.tsx b/playground/packages/ui/src/components/ui/context-menu.tsx
index b171725..c596cce 100644
--- a/playground/packages/ui/src/components/ui/context-menu.tsx
+++ b/playground/packages/ui/src/components/ui/context-menu.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function ContextMenu({
...props
diff --git a/playground/packages/ui/src/components/ui/dialog.tsx b/playground/packages/ui/src/components/ui/dialog.tsx
index b2fbd48..4295f62 100644
--- a/playground/packages/ui/src/components/ui/dialog.tsx
+++ b/playground/packages/ui/src/components/ui/dialog.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { XIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Dialog({
...props
diff --git a/playground/packages/ui/src/components/ui/drawer.tsx b/playground/packages/ui/src/components/ui/drawer.tsx
index 8ec9985..df806c2 100644
--- a/playground/packages/ui/src/components/ui/drawer.tsx
+++ b/playground/packages/ui/src/components/ui/drawer.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import { Drawer as DrawerPrimitive } from 'vaul'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Drawer({
...props
diff --git a/playground/packages/ui/src/components/ui/dropdown-menu.tsx b/playground/packages/ui/src/components/ui/dropdown-menu.tsx
index 178eca6..e9a9b1b 100644
--- a/playground/packages/ui/src/components/ui/dropdown-menu.tsx
+++ b/playground/packages/ui/src/components/ui/dropdown-menu.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function DropdownMenu({
...props
diff --git a/playground/packages/ui/src/components/ui/empty.tsx b/playground/packages/ui/src/components/ui/empty.tsx
index 8d5b17e..0b6d63e 100644
--- a/playground/packages/ui/src/components/ui/empty.tsx
+++ b/playground/packages/ui/src/components/ui/empty.tsx
@@ -1,6 +1,6 @@
import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Empty({ className, ...props }: React.ComponentProps<'div'>) {
return (
diff --git a/playground/packages/ui/src/components/ui/field.tsx b/playground/packages/ui/src/components/ui/field.tsx
index ab6529c..3914cb9 100644
--- a/playground/packages/ui/src/components/ui/field.tsx
+++ b/playground/packages/ui/src/components/ui/field.tsx
@@ -3,7 +3,7 @@
import { useMemo } from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { Label } from './label'
import { Separator } from './separator'
diff --git a/playground/packages/ui/src/components/ui/form.tsx b/playground/packages/ui/src/components/ui/form.tsx
index c681915..ccdc68d 100644
--- a/playground/packages/ui/src/components/ui/form.tsx
+++ b/playground/packages/ui/src/components/ui/form.tsx
@@ -13,7 +13,7 @@ import {
type FieldValues,
} from 'react-hook-form'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { Label } from './label'
const Form = FormProvider
diff --git a/playground/packages/ui/src/components/ui/hover-card.tsx b/playground/packages/ui/src/components/ui/hover-card.tsx
index f0d62fb..233c475 100644
--- a/playground/packages/ui/src/components/ui/hover-card.tsx
+++ b/playground/packages/ui/src/components/ui/hover-card.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as HoverCardPrimitive from '@radix-ui/react-hover-card'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function HoverCard({
...props
diff --git a/playground/packages/ui/src/components/ui/input-group.tsx b/playground/packages/ui/src/components/ui/input-group.tsx
index 355bfb6..caf324c 100644
--- a/playground/packages/ui/src/components/ui/input-group.tsx
+++ b/playground/packages/ui/src/components/ui/input-group.tsx
@@ -2,7 +2,7 @@
import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { Button } from './button'
import { Input } from './input'
import { Textarea } from './textarea'
diff --git a/playground/packages/ui/src/components/ui/input-otp.tsx b/playground/packages/ui/src/components/ui/input-otp.tsx
index 0c6b14d..9cc9f16 100644
--- a/playground/packages/ui/src/components/ui/input-otp.tsx
+++ b/playground/packages/ui/src/components/ui/input-otp.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import { OTPInput, OTPInputContext } from 'input-otp'
import { MinusIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function InputOTP({
className,
diff --git a/playground/packages/ui/src/components/ui/input.tsx b/playground/packages/ui/src/components/ui/input.tsx
index 0b8eab5..b818eb3 100644
--- a/playground/packages/ui/src/components/ui/input.tsx
+++ b/playground/packages/ui/src/components/ui/input.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
diff --git a/playground/packages/ui/src/components/ui/item.tsx b/playground/packages/ui/src/components/ui/item.tsx
index 723480e..f9bf694 100644
--- a/playground/packages/ui/src/components/ui/item.tsx
+++ b/playground/packages/ui/src/components/ui/item.tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { Separator } from './separator'
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
diff --git a/playground/packages/ui/src/components/ui/kbd.tsx b/playground/packages/ui/src/components/ui/kbd.tsx
index ff4b6aa..ca8694e 100644
--- a/playground/packages/ui/src/components/ui/kbd.tsx
+++ b/playground/packages/ui/src/components/ui/kbd.tsx
@@ -1,4 +1,4 @@
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
return (
diff --git a/playground/packages/ui/src/components/ui/label.tsx b/playground/packages/ui/src/components/ui/label.tsx
index cdf5e5e..d6102f5 100644
--- a/playground/packages/ui/src/components/ui/label.tsx
+++ b/playground/packages/ui/src/components/ui/label.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Label({
className,
diff --git a/playground/packages/ui/src/components/ui/menubar.tsx b/playground/packages/ui/src/components/ui/menubar.tsx
index a9e6d03..d38b80c 100644
--- a/playground/packages/ui/src/components/ui/menubar.tsx
+++ b/playground/packages/ui/src/components/ui/menubar.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as MenubarPrimitive from '@radix-ui/react-menubar'
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Menubar({
className,
diff --git a/playground/packages/ui/src/components/ui/navigation-menu.tsx b/playground/packages/ui/src/components/ui/navigation-menu.tsx
index 12c0f43..4ff02f1 100644
--- a/playground/packages/ui/src/components/ui/navigation-menu.tsx
+++ b/playground/packages/ui/src/components/ui/navigation-menu.tsx
@@ -3,7 +3,7 @@ import * as NavigationMenuPrimitive from '@radix-ui/react-navigation-menu'
import { cva } from 'class-variance-authority'
import { ChevronDownIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function NavigationMenu({
className,
diff --git a/playground/packages/ui/src/components/ui/pagination.tsx b/playground/packages/ui/src/components/ui/pagination.tsx
index 4076b4d..8201fd7 100644
--- a/playground/packages/ui/src/components/ui/pagination.tsx
+++ b/playground/packages/ui/src/components/ui/pagination.tsx
@@ -5,7 +5,7 @@ import {
MoreHorizontalIcon,
} from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { Button, buttonVariants } from './button'
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
diff --git a/playground/packages/ui/src/components/ui/popover.tsx b/playground/packages/ui/src/components/ui/popover.tsx
index 8640ef5..d08c975 100644
--- a/playground/packages/ui/src/components/ui/popover.tsx
+++ b/playground/packages/ui/src/components/ui/popover.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Popover({
...props
diff --git a/playground/packages/ui/src/components/ui/progress.tsx b/playground/packages/ui/src/components/ui/progress.tsx
index 4116626..7056c2e 100644
--- a/playground/packages/ui/src/components/ui/progress.tsx
+++ b/playground/packages/ui/src/components/ui/progress.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as ProgressPrimitive from '@radix-ui/react-progress'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Progress({
className,
diff --git a/playground/packages/ui/src/components/ui/radio-group.tsx b/playground/packages/ui/src/components/ui/radio-group.tsx
index 644ef3a..747326f 100644
--- a/playground/packages/ui/src/components/ui/radio-group.tsx
+++ b/playground/packages/ui/src/components/ui/radio-group.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
import { CircleIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function RadioGroup({
className,
diff --git a/playground/packages/ui/src/components/ui/resizable.tsx b/playground/packages/ui/src/components/ui/resizable.tsx
index f4097b0..53e4f36 100644
--- a/playground/packages/ui/src/components/ui/resizable.tsx
+++ b/playground/packages/ui/src/components/ui/resizable.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import { GripVerticalIcon } from 'lucide-react'
import * as ResizablePrimitive from 'react-resizable-panels'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function ResizablePanelGroup({
className,
diff --git a/playground/packages/ui/src/components/ui/scroll-area.tsx b/playground/packages/ui/src/components/ui/scroll-area.tsx
index 262ab90..3c277f3 100644
--- a/playground/packages/ui/src/components/ui/scroll-area.tsx
+++ b/playground/packages/ui/src/components/ui/scroll-area.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function ScrollArea({
className,
diff --git a/playground/packages/ui/src/components/ui/select.tsx b/playground/packages/ui/src/components/ui/select.tsx
index 7473ce8..7868276 100644
--- a/playground/packages/ui/src/components/ui/select.tsx
+++ b/playground/packages/ui/src/components/ui/select.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Select({
...props
diff --git a/playground/packages/ui/src/components/ui/separator.tsx b/playground/packages/ui/src/components/ui/separator.tsx
index d824b3d..fa93e3b 100644
--- a/playground/packages/ui/src/components/ui/separator.tsx
+++ b/playground/packages/ui/src/components/ui/separator.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as SeparatorPrimitive from '@radix-ui/react-separator'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Separator({
className,
diff --git a/playground/packages/ui/src/components/ui/sheet.tsx b/playground/packages/ui/src/components/ui/sheet.tsx
index a1577ef..68dfdc0 100644
--- a/playground/packages/ui/src/components/ui/sheet.tsx
+++ b/playground/packages/ui/src/components/ui/sheet.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as SheetPrimitive from '@radix-ui/react-dialog'
import { XIcon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Sheet({ ...props }: React.ComponentProps) {
return
diff --git a/playground/packages/ui/src/components/ui/sidebar.tsx b/playground/packages/ui/src/components/ui/sidebar.tsx
index 6eace62..09f2ce9 100644
--- a/playground/packages/ui/src/components/ui/sidebar.tsx
+++ b/playground/packages/ui/src/components/ui/sidebar.tsx
@@ -6,7 +6,7 @@ import { cva, VariantProps } from 'class-variance-authority'
import { PanelLeftIcon } from 'lucide-react'
import { useIsMobile } from './use-mobile'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { Button } from './button'
import { Input } from './input'
import { Separator } from './separator'
diff --git a/playground/packages/ui/src/components/ui/skeleton.tsx b/playground/packages/ui/src/components/ui/skeleton.tsx
index d0b0b77..7c1bc2e 100644
--- a/playground/packages/ui/src/components/ui/skeleton.tsx
+++ b/playground/packages/ui/src/components/ui/skeleton.tsx
@@ -1,4 +1,4 @@
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
return (
diff --git a/playground/packages/ui/src/components/ui/slider.tsx b/playground/packages/ui/src/components/ui/slider.tsx
index 5430b0b..b10482b 100644
--- a/playground/packages/ui/src/components/ui/slider.tsx
+++ b/playground/packages/ui/src/components/ui/slider.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as SliderPrimitive from '@radix-ui/react-slider'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Slider({
className,
diff --git a/playground/packages/ui/src/components/ui/spinner.tsx b/playground/packages/ui/src/components/ui/spinner.tsx
index ec31997..2330fef 100644
--- a/playground/packages/ui/src/components/ui/spinner.tsx
+++ b/playground/packages/ui/src/components/ui/spinner.tsx
@@ -1,6 +1,6 @@
import { Loader2Icon } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Spinner({ className, ...props }: React.ComponentProps<'svg'>) {
return (
diff --git a/playground/packages/ui/src/components/ui/switch.tsx b/playground/packages/ui/src/components/ui/switch.tsx
index 87c3370..c3a715a 100644
--- a/playground/packages/ui/src/components/ui/switch.tsx
+++ b/playground/packages/ui/src/components/ui/switch.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as SwitchPrimitive from '@radix-ui/react-switch'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Switch({
className,
diff --git a/playground/packages/ui/src/components/ui/table.tsx b/playground/packages/ui/src/components/ui/table.tsx
index d3ba28c..118461f 100644
--- a/playground/packages/ui/src/components/ui/table.tsx
+++ b/playground/packages/ui/src/components/ui/table.tsx
@@ -2,7 +2,7 @@
import * as React from 'react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Table({ className, ...props }: React.ComponentProps<'table'>) {
return (
diff --git a/playground/packages/ui/src/components/ui/tabs.tsx b/playground/packages/ui/src/components/ui/tabs.tsx
index 24bc988..af05b54 100644
--- a/playground/packages/ui/src/components/ui/tabs.tsx
+++ b/playground/packages/ui/src/components/ui/tabs.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Tabs({
className,
diff --git a/playground/packages/ui/src/components/ui/textarea.tsx b/playground/packages/ui/src/components/ui/textarea.tsx
index 0846db6..8bd06af 100644
--- a/playground/packages/ui/src/components/ui/textarea.tsx
+++ b/playground/packages/ui/src/components/ui/textarea.tsx
@@ -1,6 +1,6 @@
import * as React from 'react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
return (
diff --git a/playground/packages/ui/src/components/ui/toast.tsx b/playground/packages/ui/src/components/ui/toast.tsx
index ae4bd1b..08d33b1 100644
--- a/playground/packages/ui/src/components/ui/toast.tsx
+++ b/playground/packages/ui/src/components/ui/toast.tsx
@@ -5,7 +5,7 @@ import * as ToastPrimitives from '@radix-ui/react-toast'
import { cva, type VariantProps } from 'class-variance-authority'
import { X } from 'lucide-react'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
const ToastProvider = ToastPrimitives.Provider
diff --git a/playground/packages/ui/src/components/ui/toggle-group.tsx b/playground/packages/ui/src/components/ui/toggle-group.tsx
index 450f46c..8d4785c 100644
--- a/playground/packages/ui/src/components/ui/toggle-group.tsx
+++ b/playground/packages/ui/src/components/ui/toggle-group.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group'
import { type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
import { toggleVariants } from './toggle'
const ToggleGroupContext = React.createContext<
diff --git a/playground/packages/ui/src/components/ui/toggle.tsx b/playground/packages/ui/src/components/ui/toggle.tsx
index eb438f3..90de938 100644
--- a/playground/packages/ui/src/components/ui/toggle.tsx
+++ b/playground/packages/ui/src/components/ui/toggle.tsx
@@ -4,7 +4,7 @@ import * as React from 'react'
import * as TogglePrimitive from '@radix-ui/react-toggle'
import { cva, type VariantProps } from 'class-variance-authority'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
const toggleVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
diff --git a/playground/packages/ui/src/components/ui/tooltip.tsx b/playground/packages/ui/src/components/ui/tooltip.tsx
index cadd5ba..cb7d9fb 100644
--- a/playground/packages/ui/src/components/ui/tooltip.tsx
+++ b/playground/packages/ui/src/components/ui/tooltip.tsx
@@ -3,7 +3,7 @@
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
-import { cn } from '../../lib/utils'
+import { cn } from '@innate/utils'
function TooltipProvider({
delayDuration = 0,
diff --git a/playground/run.sh b/playground/run.sh
deleted file mode 100755
index 56ee9b8..0000000
--- a/playground/run.sh
+++ /dev/null
@@ -1,29 +0,0 @@
-#!/bin/bash
-
-PROJECT_NAME=$1
-COMMAND=${2:-dev}
-
-if [ -z "$PROJECT_NAME" ]; then
- echo "Usage: ./run.sh [command]"
- echo ""
- echo "Examples:"
- echo " ./run.sh pytopia-website-clone dev"
- echo " ./run.sh cms-website-clone build"
- echo " ./run.sh onur-dev"
- echo ""
- echo "Available projects in apps/:"
- ls -d apps/*/ 2>/dev/null | sed 's/apps\///g' | sed 's/\///g'
- exit 1
-fi
-
-PROJECT_PATH="apps/$PROJECT_NAME"
-
-if [ ! -d "$PROJECT_PATH" ]; then
- echo "Error: Project '$PROJECT_NAME' not found in apps/"
- echo "Available projects:"
- ls -d apps/*/ 2>/dev/null | sed 's/apps\///g' | sed 's/\///g'
- exit 1
-fi
-
-echo "Running '$COMMAND' for project: $PROJECT_NAME"
-cd "$PROJECT_PATH" && pnpm $COMMAND
diff --git a/scripts/workflow/local-workflow.sh b/scripts/workflow/local-workflow.sh
new file mode 100755
index 0000000..45436dd
--- /dev/null
+++ b/scripts/workflow/local-workflow.sh
@@ -0,0 +1,363 @@
+#!/bin/bash
+#
+# Local Workflow Manager
+# 用于管理本地开发工作流,支持任务追踪和状态管理
+#
+
+set -e
+
+# Colors for output
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+BLUE='\033[0;34m'
+NC='\033[0m' # No Color
+
+# Configuration
+WORKFLOW_STATE_FILE=".local-workflow.state.json"
+TRACING_DIR="tasks/tracing"
+TASKS_DIR="tasks"
+
+# Helper functions
+print_info() {
+ echo -e "${BLUE}[INFO]${NC} $1"
+}
+
+print_success() {
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
+}
+
+print_warning() {
+ echo -e "${YELLOW}[WARNING]${NC} $1"
+}
+
+print_error() {
+ echo -e "${RED}[ERROR]${NC} $1"
+}
+
+# Initialize workflow state
+init_workflow() {
+ if [ ! -f "$WORKFLOW_STATE_FILE" ]; then
+ cat > "$WORKFLOW_STATE_FILE" << EOF
+{
+ "version": "1.0",
+ "current_task": null,
+ "completed_tasks": [],
+ "tasks": []
+}
+EOF
+ print_success "Initialized workflow state file"
+ fi
+
+ mkdir -p "$TRACING_DIR"
+}
+
+# Start a task
+start_task() {
+ local task_file="$1"
+
+ if [ ! -f "$task_file" ]; then
+ print_error "Task file not found: $task_file"
+ exit 1
+ fi
+
+ local task_name=$(basename "$task_file" .md)
+ local task_id="local-$(date +%Y%m%d)-$(openssl rand -hex 4 | cut -c1-8)"
+ local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
+
+ # Update workflow state
+ cat > "$WORKFLOW_STATE_FILE" << EOF
+{
+ "task_file": "$task_file",
+ "task_id": "$task_id",
+ "title": "$task_name",
+ "started_at": "$(date +"%Y-%m-%d %H:%M:%S")",
+ "status": "in_progress",
+ "tracing_file": "$TRACING_DIR/${task_name}.md"
+}
+EOF
+
+ # Create tracing file
+ local tracing_file="$TRACING_DIR/${task_name}.md"
+ cat > "$tracing_file" << EOF
+# Task Tracing: $task_name
+
+## Metadata
+- **Task ID**: $task_id
+- **Source**: $task_file
+- **Started**: $timestamp
+- **Status**: In Progress
+
+## Description
+$(head -20 "$task_file" | grep -v "^#" | head -10)
+
+## Progress Log
+
+### $(date -u +"%Y-%m-%d %H:%M:%S") - Task Started
+- Initial analysis and planning
+
+## Implementation Notes
+
+### Current Progress
+- [ ] Analysis complete
+- [ ] Design complete
+- [ ] Implementation complete
+- [ ] Testing complete
+
+### Blockers
+
+
+### Decisions
+
+
+## Code Changes
+
+### Files Modified
+
+
+### Commits
+
+
+## Verification
+
+### Test Results
+
+
+### Checklist
+- [ ] Feature works as expected
+- [ ] No regression issues
+- [ ] Documentation updated
+- [ ] Code reviewed
+
+## Completion
+
+### Summary
+
+
+### Next Steps
+
+
+EOF
+
+ print_success "Started task: $task_name"
+ print_info "Task ID: $task_id"
+ print_info "Tracing file: $tracing_file"
+
+ # Open tracing file in default editor if available
+ if command -v code &> /dev/null; then
+ code "$tracing_file"
+ elif command -v vim &> /dev/null; then
+ print_info "Run 'vim $tracing_file' to edit tracing file"
+ fi
+}
+
+# Complete a task
+complete_task() {
+ local notes="$1"
+
+ if [ ! -f "$WORKFLOW_STATE_FILE" ]; then
+ print_error "No active task found"
+ exit 1
+ fi
+
+ local task_file=$(jq -r '.task_file' "$WORKFLOW_STATE_FILE")
+ local task_name=$(jq -r '.title' "$WORKFLOW_STATE_FILE")
+ local tracing_file=$(jq -r '.tracing_file' "$WORKFLOW_STATE_FILE")
+
+ # Update workflow state
+ jq '.status = "completed" | .completed_at = "'$(date +"%Y-%m-%d %H:%M:%S")'"' "$WORKFLOW_STATE_FILE" > "${WORKFLOW_STATE_FILE}.tmp"
+ mv "${WORKFLOW_STATE_FILE}.tmp" "$WORKFLOW_STATE_FILE"
+
+ # Append to tracing file
+ cat >> "$tracing_file" << EOF
+
+### $(date -u +"%Y-%m-%d %H:%M:%S") - Task Completed
+$notes
+
+## Final Status: ✅ Completed
+EOF
+
+ print_success "Completed task: $task_name"
+
+ # Archive the task
+ mv "$WORKFLOW_STATE_FILE" "${WORKFLOW_STATE_FILE}.completed.$(date +%Y%m%d%H%M%S)"
+
+ print_info "Workflow state archived"
+}
+
+# Show current task status
+status() {
+ if [ ! -f "$WORKFLOW_STATE_FILE" ]; then
+ print_warning "No active task"
+ return
+ fi
+
+ local status=$(jq -r '.status' "$WORKFLOW_STATE_FILE")
+ local task_file=$(jq -r '.task_file' "$WORKFLOW_STATE_FILE")
+ local task_name=$(jq -r '.title' "$WORKFLOW_STATE_FILE")
+ local started_at=$(jq -r '.started_at' "$WORKFLOW_STATE_FILE")
+
+ echo "Current Task Status"
+ echo "=================="
+ echo "Task: $task_name"
+ echo "File: $task_file"
+ echo "Status: $status"
+ echo "Started: $started_at"
+
+ if [ "$status" = "in_progress" ]; then
+ local tracing_file=$(jq -r '.tracing_file' "$WORKFLOW_STATE_FILE")
+ echo "Tracing: $tracing_file"
+ fi
+}
+
+# List available tasks
+list_tasks() {
+ print_info "Available task files:"
+
+ find "$TASKS_DIR" -name "*.md" -type f | while read -r file; do
+ # Skip tracing files
+ if [[ "$file" == *"/tracing/"* ]]; then
+ continue
+ fi
+
+ # Get first line (title)
+ local title=$(head -1 "$file" | sed 's/^#* //')
+ echo " - $(basename "$file"): $title"
+ done
+}
+
+# Create git worktree for feature development
+create_worktree() {
+ local feature_name="$1"
+ local base_branch="${2:-main}"
+
+ if [ -z "$feature_name" ]; then
+ print_error "Feature name required"
+ exit 1
+ fi
+
+ # Sanitize feature name
+ local branch_name="feature/$(echo "$feature_name" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd '[:alnum:]-')"
+ local worktree_dir="../worktrees/${branch_name}"
+
+ print_info "Creating worktree for: $feature_name"
+ print_info "Branch: $branch_name"
+ print_info "Directory: $worktree_dir"
+
+ # Create parent directory for worktrees
+ mkdir -p "../worktrees"
+
+ # Fetch latest from origin
+ git fetch origin
+
+ # Create branch if not exists
+ if ! git rev-parse --verify "$branch_name" &>/dev/null; then
+ git checkout -b "$branch_name" origin/$base_branch
+ print_success "Created branch: $branch_name"
+ fi
+
+ # Create worktree
+ if [ ! -d "$worktree_dir" ]; then
+ git worktree add "$worktree_dir" "$branch_name"
+ print_success "Created worktree at: $worktree_dir"
+ else
+ print_warning "Worktree already exists at: $worktree_dir"
+ fi
+
+ echo ""
+ print_success "Worktree ready!"
+ echo ""
+ echo "To start working:"
+ echo " cd $worktree_dir"
+ echo ""
+ echo "To push changes:"
+ echo " git push -u origin $branch_name"
+}
+
+# Remove git worktree
+remove_worktree() {
+ local branch_name="$1"
+ local worktree_dir="../worktrees/$branch_name"
+
+ if [ ! -d "$worktree_dir" ]; then
+ print_error "Worktree not found: $worktree_dir"
+ exit 1
+ fi
+
+ git worktree remove "$worktree_dir"
+ print_success "Removed worktree: $worktree_dir"
+
+ # Optionally remove branch
+ read -p "Remove branch $branch_name? (y/N) " -n 1 -r
+ echo
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ git branch -D "$branch_name"
+ print_success "Removed branch: $branch_name"
+ fi
+}
+
+# List all worktrees
+list_worktrees() {
+ print_info "Active worktrees:"
+ git worktree list
+}
+
+# Main command dispatcher
+case "${1:-help}" in
+ init)
+ init_workflow
+ ;;
+ start)
+ if [ -z "${2:-}" ]; then
+ print_error "Task file required"
+ echo "Usage: $0 start "
+ list_tasks
+ exit 1
+ fi
+ init_workflow
+ start_task "$2"
+ ;;
+ complete)
+ complete_task "${2:-Task completed successfully}"
+ ;;
+ status)
+ status
+ ;;
+ list)
+ list_tasks
+ ;;
+ worktree-create)
+ create_worktree "${2:-}" "${3:-main}"
+ ;;
+ worktree-remove)
+ if [ -z "${2:-}" ]; then
+ print_error "Branch name required"
+ exit 1
+ fi
+ remove_worktree "$2"
+ ;;
+ worktree-list)
+ list_worktrees
+ ;;
+ help|*)
+ echo "Local Workflow Manager"
+ echo ""
+ echo "Usage: $0 [args]"
+ echo ""
+ echo "Commands:"
+ echo " init Initialize workflow system"
+ echo " start Start working on a task"
+ echo " complete [notes] Complete current task"
+ echo " status Show current task status"
+ echo " list List available tasks"
+ echo ""
+ echo "Git Worktree Commands:"
+ echo " worktree-create [base-branch] Create a new worktree"
+ echo " worktree-remove Remove a worktree"
+ echo " worktree-list List all worktrees"
+ echo ""
+ echo "Examples:"
+ echo " $0 start tasks/mindstorm/executable-tutorial.md"
+ echo " $0 worktree-create executable-tutorial"
+ ;;
+esac
diff --git a/skills/frontend-validate/SKILL.md b/skills/frontend-validate/SKILL.md
new file mode 100644
index 0000000..c1f8f2e
--- /dev/null
+++ b/skills/frontend-validate/SKILL.md
@@ -0,0 +1,254 @@
+# Frontend Validate Skill
+
+> 前端项目验证 Skill - 用于检查常见的前端项目问题和最佳实践
+> 版本: 1.0.0
+> 支持工具: Claude Code, Codex CLI, Kimi CLI, OpenCode, Cursor
+
+---
+
+## 用途
+
+本 Skill 用于在创建或维护前端项目时,自动检查常见的配置问题和最佳实践,避免常见错误。
+
+## 使用场景
+
+1. 初始化新的前端项目时
+2. 修复样式不生效问题时
+3. 检查项目配置完整性时
+4. Code Review 时作为检查清单
+
+---
+
+## 验证规则
+
+### V1. CSS/Tailwind 配置检查
+
+#### V1.1 CSS 文件导入检查
+- [ ] `main.tsx` 或 `main.js` 是否导入了 CSS 文件
+- [ ] `import "./index.css"` 或 `import "./styles.css"` 是否存在
+- [ ] CSS 文件路径是否正确
+
+**常见错误:**
+```typescript
+// ❌ 错误:没有导入 CSS
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+
+// ✅ 正确:导入了 CSS
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./index.css";
+```
+
+#### V1.2 Tailwind CSS 配置检查
+- [ ] `tailwind.config.js` 是否存在且配置正确
+- [ ] `postcss.config.js` 是否存在且配置正确
+- [ ] `content` 配置是否包含所有模板文件路径
+- [ ] `@tailwind` 指令是否在 CSS 文件中
+
+**标准配置:**
+```javascript
+// tailwind.config.js
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
+```
+
+```javascript
+// postcss.config.js
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
+```
+
+```css
+/* index.css */
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+```
+
+#### V1.3 Tailwind 版本兼容性检查
+- [ ] Tailwind CSS v4 与 v3 的配置方式不同
+- [ ] v3 使用 `tailwind.config.js`
+- [ ] v4 使用 CSS 配置 `@config`
+
+**版本检测:**
+```bash
+# 检查 package.json 中的 tailwindcss 版本
+# v3.x: 使用传统配置方式
+# v4.x: 使用新的 CSS-based 配置
+```
+
+---
+
+### V2. 构建工具配置检查
+
+#### V2.1 Vite 项目检查
+- [ ] `vite.config.ts` 是否存在
+- [ ] `index.html` 入口文件是否正确配置
+- [ ] 开发服务器端口是否配置
+
+#### V2.2 路径别名检查
+- [ ] `tsconfig.json` 中是否配置 `paths`
+- [ ] `vite.config.ts` 中是否配置 `resolve.alias`
+
+**标准配置:**
+```json
+// tsconfig.json
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
+}
+```
+
+```typescript
+// vite.config.ts
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+});
+```
+
+---
+
+### V3. React 项目检查
+
+#### V3.1 入口文件检查
+- [ ] `main.tsx` 是否存在且配置正确
+- [ ] `App.tsx` 是否存在
+- [ ] React 18 的 `createRoot` API 是否正确使用
+
+**标准配置:**
+```typescript
+// main.tsx
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "./index.css"; // 确保导入 CSS
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
+```
+
+#### V3.2 TypeScript 配置检查
+- [ ] `tsconfig.json` 是否存在
+- [ ] 严格的类型检查是否启用
+- [ ] `vite-env.d.ts` 是否存在
+
+---
+
+### V4. Tauri 桌面应用特定检查
+
+#### V4.1 Tauri 配置文件检查
+- [ ] `tauri.conf.json` 是否存在
+- [ ] `src-tauri/Cargo.toml` 是否存在
+- [ ] `devUrl` 是否指向正确的开发服务器
+
+#### V4.2 Shell 插件配置
+- [ ] `capabilities/default.json` 是否配置 shell 权限
+- [ ] `@tauri-apps/plugin-shell` 是否安装
+
+---
+
+### V5. Monorepo 配置检查
+
+#### V5.1 Workspace 检查
+- [ ] `pnpm-workspace.yaml` 或 `package.json` workspaces 是否配置
+- [ ] 子包是否正确引用 workspace 包
+
+#### V5.2 共享包引用检查
+- [ ] `@innate/ui` 等 workspace 包是否正确引用
+- [ ] TypeScript 配置是否正确继承
+
+---
+
+## 快速修复命令
+
+### 修复 CSS 导入问题
+```bash
+# 检查 main.tsx 是否有 CSS 导入
+grep -n "import.*\\.css" src/main.tsx || echo "❌ 缺少 CSS 导入"
+
+# 添加 CSS 导入(如果不存在)
+sed -i '/import App/a import "./index.css";' src/main.tsx
+```
+
+### 修复 Tailwind 配置
+```bash
+# 检查 Tailwind 配置文件
+[ -f tailwind.config.js ] || echo "❌ 缺少 tailwind.config.js"
+[ -f postcss.config.js ] || echo "❌ 缺少 postcss.config.js"
+```
+
+---
+
+## AI 使用提示
+
+### 当用户说 "样式不生效" 时:
+1. 首先检查 `main.tsx` 是否导入 CSS
+2. 检查 Tailwind 配置是否正确
+3. 检查 CSS 文件是否包含 `@tailwind` 指令
+4. 检查构建工具配置
+
+### 当用户创建新项目时:
+1. 使用此 Skill 作为检查清单
+2. 确保所有配置文件都存在且正确
+3. 验证开发服务器能正常启动
+4. 验证样式能正确渲染
+
+---
+
+## 示例对话
+
+**用户:** 我的 React 项目样式不生效
+
+**AI:** 让我使用 Frontend Validate Skill 来检查:
+
+1. 检查 CSS 导入... ❌ 发现 `main.tsx` 缺少 `import "./index.css"`
+2. 检查 Tailwind 配置... ✅ 正常
+3. 检查 PostCSS 配置... ✅ 正常
+
+**修复:** 在 `main.tsx` 中添加 CSS 导入:
+```typescript
+import "./index.css";
+```
+
+---
+
+## 更新日志
+
+### v1.0.0 (2026-04-08)
+- 初始版本
+- 添加 CSS/Tailwind 检查规则
+- 添加 React/Vite 检查规则
+- 添加 Tauri 特定检查规则
+- 添加 Monorepo 检查规则
diff --git a/skills/frontend-validate/validate-rules.json b/skills/frontend-validate/validate-rules.json
new file mode 100644
index 0000000..bc6b098
--- /dev/null
+++ b/skills/frontend-validate/validate-rules.json
@@ -0,0 +1,120 @@
+{
+ "id": "frontend-validate",
+ "name": "Frontend Validate",
+ "version": "1.0.0",
+ "description": "前端项目验证规则,检查常见配置问题和最佳实践",
+ "category": "validation",
+ "supportedTools": ["claude-code", "codex", "kimi-cli", "opencode", "cursor"],
+ "rules": [
+ {
+ "id": "V1.1",
+ "name": "CSS Import Check",
+ "description": "检查 main.tsx 是否导入 CSS 文件",
+ "severity": "error",
+ "check": {
+ "file": "src/main.tsx",
+ "pattern": "import.*\\.css['\"]",
+ "message": "缺少 CSS 文件导入,添加: import \"./index.css\";"
+ }
+ },
+ {
+ "id": "V1.2",
+ "name": "Tailwind Config Check",
+ "description": "检查 Tailwind CSS 配置是否存在",
+ "severity": "error",
+ "check": {
+ "files": ["tailwind.config.js", "tailwind.config.ts"],
+ "message": "缺少 Tailwind 配置文件"
+ }
+ },
+ {
+ "id": "V1.3",
+ "name": "PostCSS Config Check",
+ "description": "检查 PostCSS 配置是否存在",
+ "severity": "error",
+ "check": {
+ "files": ["postcss.config.js", "postcss.config.ts"],
+ "message": "缺少 PostCSS 配置文件"
+ }
+ },
+ {
+ "id": "V1.4",
+ "name": "Tailwind Directives Check",
+ "description": "检查 CSS 文件是否包含 Tailwind 指令",
+ "severity": "error",
+ "check": {
+ "file": "src/index.css",
+ "pattern": "@tailwind (base|components|utilities)",
+ "message": "CSS 文件缺少 @tailwind 指令"
+ }
+ },
+ {
+ "id": "V2.1",
+ "name": "Vite Config Check",
+ "description": "检查 Vite 配置是否存在",
+ "severity": "warning",
+ "check": {
+ "files": ["vite.config.ts", "vite.config.js"],
+ "message": "缺少 Vite 配置文件"
+ }
+ },
+ {
+ "id": "V2.2",
+ "name": "TypeScript Paths Check",
+ "description": "检查是否配置路径别名",
+ "severity": "info",
+ "check": {
+ "file": "tsconfig.json",
+ "pattern": "\"paths\":",
+ "message": "建议配置 TypeScript paths 以支持路径别名"
+ }
+ },
+ {
+ "id": "V3.1",
+ "name": "React Entry Check",
+ "description": "检查 React 入口文件配置",
+ "severity": "error",
+ "check": {
+ "files": ["src/main.tsx", "src/main.jsx"],
+ "message": "缺少 React 入口文件"
+ }
+ },
+ {
+ "id": "V4.1",
+ "name": "Tauri Config Check",
+ "description": "检查 Tauri 配置文件",
+ "severity": "error",
+ "check": {
+ "file": "src-tauri/tauri.conf.json",
+ "message": "缺少 Tauri 配置文件"
+ }
+ },
+ {
+ "id": "V5.1",
+ "name": "Package.json Check",
+ "description": "检查 package.json 是否存在",
+ "severity": "error",
+ "check": {
+ "file": "package.json",
+ "message": "缺少 package.json"
+ }
+ }
+ ],
+ "quickFixes": [
+ {
+ "id": "fix-css-import",
+ "name": "修复 CSS 导入",
+ "description": "在 main.tsx 中添加 CSS 导入",
+ "command": "sed -i '/import App/a import \"./index.css\";' src/main.tsx"
+ },
+ {
+ "id": "fix-tailwind-init",
+ "name": "初始化 Tailwind",
+ "description": "创建 Tailwind 和 PostCSS 配置文件",
+ "commands": [
+ "npx tailwindcss init -p",
+ "echo '@tailwind base; @tailwind components; @tailwind utilities;' > src/index.css"
+ ]
+ }
+ ]
+}
diff --git a/tasks/features/skill-generation.md b/tasks/features/skill-generation.md
new file mode 100644
index 0000000..07d7104
--- /dev/null
+++ b/tasks/features/skill-generation.md
@@ -0,0 +1,18 @@
+# Skill Generation
+
+这个是创建skill的任务,主要来源是发现AI 出现错误之后,把他出现的错误变成一个skill沉淀
+
+## Task 1: 创建Frontend validate SKill
+
+1. 常见一个frontend-validate skill只要用来记录常见的frontend 验证的skill
+2. 根据AI 修复的内容把修复内容加入到frontend validate skill中
+3. 比如当前这个问题没有 index.css 倒入,那么就把检查是否css已经成功导入加入到这个skill
+4. 这个新的skill应该支持所有常见的claude code,codex,kimi cli,opencode 等等
+
+## Task 2: 检查Features 是否已经实现
+
+1. 请检查 [F1.1-init](F1.1-init-tauri-nextjs.md)是否完成
+2. 请检查 [F1.2-monorepo](F1.2-monorepo-integration.md)是否完成
+3. 当前页面和UI基本上满意,因此不需要完全确认全部和这两个任务描述的一致,只需要确认没有重大的事项遗漏就可以
+4. 不修改任何当前代码,把未完成的事项写入到 backlog.md 文件中就可以
+5. 如果任务发现全部完成了,就按照目前代码结构更新两个新的文档到[docs/features](docs/features)目录下
\ No newline at end of file
diff --git a/tasks/mindstorm/analysis/01-task1-analysis.md b/tasks/mindstorm/analysis/01-task1-analysis.md
new file mode 100644
index 0000000..ff16757
--- /dev/null
+++ b/tasks/mindstorm/analysis/01-task1-analysis.md
@@ -0,0 +1,581 @@
+# Task 1 详细分析
+
+> 分析文档:Executable Tutorial 页面功能实现
+> 创建时间:2026-04-08
+> 相关任务:tasks/mindstorm/executable-tutorial.md
+
+---
+
+## 一、需求理解
+
+### 1.1 原始需求
+
+根据 `tasks/mindstorm/executable-tutorial.md` 中的 Task 1 描述:
+
+> 目前先不去考虑教程的事情,先实现页面的事情,一个总体展示的页面,一个可以上传文件的页面,或者可以添加本地目录到这个desktop中
+> 1. 目前先不去考虑教程的事情,先实现页面的事情,一个总体展示的页面,一个可以上传文件的页面,或者可以添加本地目录到这个desktop中
+> 2. 这个页面需要有一个导航栏,可以展示所有的教程,也可以展示所有的系列
+> 3. 主要内容区域就是展示,系列里面的三个教程,如果有更多的,就有一个更多按钮
+> 4. 点击每个教程,会展示这个教程的内容,点击教程里面的运行,terminal就出现,然后可以执行教程的内容
+
+### 1.2 需求拆解
+
+| 功能模块 | 具体需求 | 优先级 |
+|---------|---------|--------|
+| **总体展示页面** | 首页展示所有教程和系列 | P0 |
+| **导航栏** | 展示所有教程、所有系列 | P0 |
+| **系列展示** | 每个系列展示3个教程,更多按钮 | P0 |
+| **教程详情** | 点击展示教程内容 | P0 |
+| **终端功能** | 点击运行显示终端,执行命令 | P0 |
+| **文件上传** | 上传文件或添加本地目录 | P1 |
+
+---
+
+## 二、技术架构分析
+
+### 2.1 技术选型
+
+基于项目整体方向(Tauri + Next.js + shadcn/ui):
+
+| 层级 | 技术 | 说明 |
+|-----|------|------|
+| 桌面容器 | Tauri v2 | 轻量级,支持系统命令执行 |
+| 前端框架 | Next.js 15 + React 19 | App Router,SSR支持 |
+| UI组件 | shadcn/ui | 已有基础,组件丰富 |
+| 终端模拟 | xterm.js | 业界标准,功能完善 |
+| 状态管理 | Zustand | 轻量,适合本项目 |
+| 后端通信 | Tauri IPC | Command + Events |
+
+### 2.2 系统架构图
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Tauri Window │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ Next.js Frontend │ │
+│ │ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │ │
+│ │ │ Header │ │ Sidebar │ │ Terminal │ │ │
+│ │ │ (导航栏) │ │ (导航) │ │ (xterm) │ │ │
+│ │ └────────────┘ └────────────┘ └──────────────┘ │ │
+│ │ ┌──────────────────────────────────────────────┐ │ │
+│ │ │ Main Content │ │ │
+│ │ │ ┌────────────────────────────────────────┐ │ │ │
+│ │ │ │ Tutorial List / Grid │ │ │ │
+│ │ │ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ │
+│ │ │ │ │ Series │ │ Series │ │ Series │ ... │ │ │ │
+│ │ │ │ │ Card │ │ Card │ │ Card │ │ │ │ │
+│ │ │ │ └────────┘ └────────┘ └────────┘ │ │ │ │
+│ │ │ └────────────────────────────────────────┘ │ │ │
+│ │ │ ┌────────────────────────────────────────┐ │ │ │
+│ │ │ │ Tutorial Detail View │ │ │ │
+│ │ │ │ - Markdown渲染 │ │ │ │
+│ │ │ │ - 可执行代码块 │ │ │ │
+│ │ │ │ - 运行按钮 │ │ │ │
+│ │ │ └────────────────────────────────────────┘ │ │ │
+│ │ └──────────────────────────────────────────────┘ │ │
+│ └─────────────────────────────────────────────────────┘ │
+│ │ Tauri IPC │
+│ ┌──────────────────────┼──────────────────────────────┐ │
+│ │ Rust Backend │ │
+│ │ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │ │
+│ │ │ Shell │ │ File │ │ Config │ │ │
+│ │ │ Executor │ │ System │ │ Store │ │ │
+│ │ └────────────┘ └────────────┘ └──────────────┘ │ │
+│ └────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 三、页面设计分析
+
+### 3.1 页面结构
+
+#### 首页(总体展示页面)
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 🏠 Home 📚 Tutorials 📂 Series ⚙️ Settings │
+├─────────────────────────────────────────────────────────────┤
+│ │
+│ 🔍 Search tutorials... [Filter ▼] │
+│ │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ 📁 Featured Series │ │
+│ │ │ │
+│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
+│ │ │ 🖥️ Terminal │ │ 🐍 Python │ │ ⚛️ React │ │ │
+│ │ │ Basics │ │ 101 │ │ Basics │ │ │
+│ │ │ │ │ │ │ │ │ │
+│ │ │ [3 tutorials] [5 tutorials] [4 tutorials] │ │ │
+│ │ │ [View →] [View →] [View →] │ │ │
+│ │ └────────────┘ └────────────┘ └────────────┘ │ │
+│ │ │ │
+│ └─────────────────────────────────────────────────────┘ │
+│ │
+│ ┌─────────────────────────────────────────────────────┐ │
+│ │ 📖 Recent Tutorials │ │
+│ │ │ │
+│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
+│ │ │ Install │ │ Git │ │ Node.js │ │ │
+│ │ │ Warp │ │ Basics │ │ Setup │ │ │
+│ │ │ ⏱️ 5 min │ │ ⏱️ 10 min │ │ ⏱️ 10 min │ │ │
+│ │ │ Beginner │ │ Beginner │ │ Beginner │ │ │
+│ │ └────────────┘ └────────────┘ └────────────┘ │ │
+│ │ │ │
+│ └─────────────────────────────────────────────────────┘ │
+│ │
+├─────────────────────────────────────────────────────────────┤
+│ 📁 Add Local Directory 📤 Import Tutorial │
+└─────────────────────────────────────────────────────────────┘
+```
+
+#### 系列详情页
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 🏠 Home 📚 Tutorials 📂 Series ⚙️ Settings │
+├─────────────────────────────────────────────────────────────┤
+│ ← Back to Series │
+│ │
+│ 🖥️ Terminal Basics │
+│ ================= │
+│ Learn the fundamentals of command line interface │
+│ │
+│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
+│ │ 1. ls │ │ 2. cd │ │ 3. mkdir │ [More →] │
+│ │ Command │ │ Command │ │ Command │ │
+│ │ ⏱️ 2 min │ │ ⏱️ 2 min │ │ ⏱️ 3 min │ │
+│ └────────────┘ └────────────┘ └────────────┘ │
+│ │
+│ Progress: [████████░░] 60% (3/5 completed) │
+│ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+#### 教程详情页(含终端)
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 🏠 Home 📚 Tutorials 📂 Series ⚙️ Settings │
+├──────┬──────────────────────────────────────────┬───────────┤
+│ │ │ │
+│ 📑 │ 📖 ls Command Tutorial │ ┌─────┐ │
+│ │ ===================== │ │ ☐ │ │
+│ 📑 │ │ │ ☐ │ │
+│ │ ## What is `ls`? │ │ ☐ │ │
+│ 📑 │ │ │ ☐ │ │
+│ │ The `ls` command lists directory │ │ │ │
+│ 📑 │ contents... │ │ │ │
+│ │ │ │ │ │
+│ 📑 │ ## Try it yourself │ └─────┘ │
+│ │ │ Terminal │
+│ 📑 │ ```bash │ (hidden) │
+│ 📑 │ ls -la │ │
+│ │ ``` │ │
+│ 📑 │ │ │
+│ │ [▶️ Run in Terminal] │ │
+│ │ │ │
+│ │ ## Output Explanation │ │
+│ │ ... │ │
+│ │ │ │
+└──────┴──────────────────────────────────────────┴───────────┘
+```
+
+#### 终端展开状态
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ 🏠 Home 📚 Tutorials 📂 Series ⚙️ Settings │
+├──────┬──────────────────────────────┬───────────────────────┤
+│ │ │ │
+│ 📑 │ 📖 ls Command Tutorial │ ┌─────────────────┐ │
+│ │ │ │ $ ls -la │ │
+│ 📑 │ ## Try it yourself │ │ total 128 │ │
+│ │ │ │ drwxr-xr-x 5 │ │
+│ 📑 │ ```bash │ │ drwxr-xr-x 28 │ │
+│ │ ls -la │ │ -rw-r--r-- 1 │ │
+│ 📑 │ ``` │ │ ... │ │
+│ │ │ │ │ │
+│ 📑 │ [✅ Completed] │ │ $ █ │ │
+│ │ │ └─────────────────┘ │
+│ │ │ [Minimize] [Dock →] │
+└──────┴──────────────────────────────┴───────────────────────┘
+```
+
+### 3.2 终端位置切换
+
+终端支持三种显示模式:
+
+1. **隐藏模式(默认)**:不显示终端,点击运行后展开
+2. **右侧边栏模式**:占据右侧30%宽度
+3. **底部面板模式**:占据底部40%高度
+
+```
+右侧模式: 底部模式:
+┌────────────┬──────────┐ ┌────────────┐
+│ │ │ │ │
+│ Content │ Terminal │ │ Content │
+│ │ │ │ │
+│ │ │ ├────────────┤
+│ │ │ │ Terminal │
+└────────────┴──────────┘ └────────────┘
+```
+
+---
+
+## 四、数据模型分析
+
+### 4.1 Tutorial(教程)模型
+
+```typescript
+interface Tutorial {
+ id: string; // 唯一标识
+ title: string; // 标题
+ description: string; // 描述
+ category: string; // 分类
+ difficulty: 'beginner' | 'intermediate' | 'advanced';
+ duration: number; // 预计完成时间(分钟)
+ tags: string[]; // 标签
+ series?: string; // 所属系列ID
+ order?: number; // 在系列中的顺序
+
+ // 内容
+ content: TutorialSection[]; // 教程内容段落
+
+ // 执行配置
+ executableBlocks: ExecutableBlock[];
+
+ // 元数据
+ author?: string;
+ createdAt: string;
+ updatedAt: string;
+ source?: 'builtin' | 'local' | 'imported'; // 来源
+ localPath?: string; // 本地文件路径
+}
+
+interface TutorialSection {
+ id: string;
+ type: 'text' | 'code' | 'executable' | 'image' | 'video';
+ content: string;
+ language?: string; // 代码语言
+ executable?: boolean; // 是否可执行
+}
+
+interface ExecutableBlock {
+ id: string;
+ code: string; // 可执行代码
+ language: 'bash' | 'powershell' | 'python' | 'javascript';
+ platform?: ('macos' | 'windows' | 'linux')[];
+ workingDirectory?: string; // 执行目录
+ environment?: Record; // 环境变量
+ expectedOutput?: string; // 期望输出(用于验证)
+}
+```
+
+### 4.2 Series(系列)模型
+
+```typescript
+interface Series {
+ id: string;
+ title: string;
+ description: string;
+ category: string;
+ difficulty: 'beginner' | 'intermediate' | 'advanced';
+
+ // 系列封面
+ icon?: string;
+ color?: string;
+
+ // 包含的教程
+ tutorials: string[]; // Tutorial ID列表
+
+ // 元数据
+ author?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+```
+
+### 4.3 存储方案
+
+| 数据类型 | 存储位置 | 说明 |
+|---------|---------|------|
+| 内置教程 | `skills/` 目录 | JSON/Markdown格式,随应用分发 |
+| 用户教程 | App Data目录 | 用户导入或创建的教程 |
+| 学习进度 | SQLite/LocalStorage | 用户完成状态、偏好设置 |
+| 配置文件 | `config/` 或 App Data | 应用配置、主题等 |
+
+---
+
+## 五、功能模块设计
+
+### 5.1 模块划分
+
+```
+app/
+├── layout.tsx # 根布局
+├── page.tsx # 首页
+├── globals.css # 全局样式
+│
+├── tutorials/
+│ ├── page.tsx # 教程列表页
+│ ├── [id]/
+│ │ └── page.tsx # 教程详情页
+│ └── components/
+│ ├── TutorialCard.tsx # 教程卡片
+│ ├── TutorialGrid.tsx # 教程网格
+│ ├── TutorialFilter.tsx # 筛选组件
+│ └── TutorialSearch.tsx # 搜索组件
+│
+├── series/
+│ ├── page.tsx # 系列列表页
+│ ├── [id]/
+│ │ └── page.tsx # 系列详情页
+│ └── components/
+│ ├── SeriesCard.tsx # 系列卡片
+│ └── SeriesProgress.tsx # 进度显示
+│
+├── components/
+│ ├── layout/
+│ │ ├── Header.tsx # 顶部导航
+│ │ ├── Sidebar.tsx # 侧边栏
+│ │ └── MainLayout.tsx # 主布局
+│ │
+│ ├── terminal/
+│ │ ├── Terminal.tsx # 终端组件
+│ │ ├── TerminalProvider.tsx # 终端状态管理
+│ │ ├── TerminalControls.tsx # 终端控制按钮
+│ │ └── xterm-config.ts # xterm配置
+│ │
+│ ├── tutorial/
+│ │ ├── MarkdownRenderer.tsx # Markdown渲染
+│ │ ├── ExecutableBlock.tsx # 可执行代码块
+│ │ ├── TutorialViewer.tsx # 教程查看器
+│ │ └── TutorialProgress.tsx # 教程进度
+│ │
+│ └── ui/ # shadcn/ui组件
+│
+├── hooks/
+│ ├── useTerminal.ts # 终端Hook
+│ ├── useTutorials.ts # 教程数据Hook
+│ ├── useSeries.ts # 系列数据Hook
+│ └── useLocalStorage.ts # 本地存储Hook
+│
+├── lib/
+│ ├── tauri.ts # Tauri IPC封装
+│ ├── tutorial-loader.ts # 教程加载器
+│ ├── tutorial-parser.ts # 教程解析器
+│ └── utils.ts # 工具函数
+│
+├── store/
+│ ├── useAppStore.ts # 全局状态
+│ ├── useTerminalStore.ts # 终端状态
+│ └── useTutorialStore.ts # 教程状态
+│
+└── types/
+ ├── tutorial.ts # 教程类型定义
+ ├── series.ts # 系列类型定义
+ └── index.ts # 类型导出
+```
+
+### 5.2 核心功能实现思路
+
+#### 终端功能
+
+```typescript
+// 使用 Zustand 管理终端状态
+interface TerminalState {
+ // 显示状态
+ visible: boolean;
+ position: 'hidden' | 'right' | 'bottom';
+
+ // 终端实例
+ xterm: Terminal | null;
+ fitAddon: FitAddon | null;
+
+ // 执行状态
+ isExecuting: boolean;
+ currentProcess: number | null;
+
+ // 输出历史
+ outputHistory: string[];
+
+ // 操作
+ show: () => void;
+ hide: () => void;
+ togglePosition: () => void;
+ setPosition: (pos: 'right' | 'bottom') => void;
+ execute: (command: string) => Promise;
+ clear: () => void;
+}
+```
+
+#### 教程加载
+
+```typescript
+// 教程加载器
+class TutorialLoader {
+ // 加载内置教程
+ async loadBuiltinTutorials(): Promise;
+
+ // 加载本地教程
+ async loadLocalTutorials(): Promise;
+
+ // 从文件导入
+ async importFromFile(path: string): Promise;
+
+ // 从URL导入
+ async importFromURL(url: string): Promise;
+
+ // 解析Markdown
+ parseMarkdown(content: string): Tutorial;
+}
+```
+
+---
+
+## 六、关键技术点
+
+### 6.1 Tauri Shell 命令执行
+
+```rust
+// src-tauri/src/commands/shell.rs
+use tauri::command;
+use std::process::Command;
+
+#[command]
+async fn execute_command(
+ command: String,
+ args: Vec,
+ cwd: Option,
+) -> Result {
+ let mut cmd = Command::new(&command);
+ cmd.args(&args);
+
+ if let Some(cwd) = cwd {
+ cmd.current_dir(cwd);
+ }
+
+ let output = cmd.output().map_err(|e| e.to_string())?;
+
+ if output.status.success() {
+ Ok(String::from_utf8_lossy(&output.stdout).to_string())
+ } else {
+ Err(String::from_utf8_lossy(&output.stderr).to_string())
+ }
+}
+
+// 流式输出
+#[command]
+async fn execute_command_stream(
+ window: tauri::Window,
+ command: String,
+ args: Vec,
+) -> Result<(), String> {
+ // 使用 Event 发送流式输出到前端
+ // ...
+}
+```
+
+### 6.2 xterm.js 集成
+
+```typescript
+// 终端初始化
+import { Terminal } from 'xterm';
+import { FitAddon } from 'xterm-addon-fit';
+
+const initTerminal = (container: HTMLElement) => {
+ const terminal = new Terminal({
+ fontSize: 14,
+ fontFamily: 'JetBrains Mono, monospace',
+ theme: {
+ background: '#1e1e1e',
+ foreground: '#d4d4d4',
+ },
+ cursorBlink: true,
+ });
+
+ const fitAddon = new FitAddon();
+ terminal.loadAddon(fitAddon);
+
+ terminal.open(container);
+ fitAddon.fit();
+
+ // 监听终端输入
+ terminal.onData((data) => {
+ // 发送到后端执行
+ invoke('terminal_input', { data });
+ });
+
+ return { terminal, fitAddon };
+};
+```
+
+---
+
+## 七、风险与挑战
+
+### 7.1 技术风险
+
+| 风险 | 影响 | 缓解措施 |
+|-----|------|---------|
+| Tauri Shell 权限 | 高 | 配置 capability,用户确认 |
+| xterm.js 性能 | 中 | 限制输出缓冲区大小 |
+| 跨平台命令差异 | 高 | 使用平台检测,提供对应命令 |
+| 文件系统访问 | 中 | 使用 Tauri FS API |
+
+### 7.2 产品风险
+
+| 风险 | 影响 | 缓解措施 |
+|-----|------|---------|
+| 教程内容质量 | 高 | 建立审核机制,社区贡献 |
+| 用户学习曲线 | 中 | 提供示例教程,引导式体验 |
+| 安全风险(执行任意命令)| 高 | 白名单机制,用户确认 |
+
+---
+
+## 八、开发计划
+
+### Phase 1: 基础框架(Week 1)
+
+- [ ] 初始化 Tauri + Next.js 项目
+- [ ] 配置 shadcn/ui
+- [ ] 实现基础布局(Header, Sidebar)
+- [ ] 集成 xterm.js
+
+### Phase 2: 核心功能(Week 2)
+
+- [ ] 实现教程列表页面
+- [ ] 实现系列展示功能
+- [ ] 实现教程详情页面
+- [ ] 实现终端基础功能
+
+### Phase 3: 执行功能(Week 3)
+
+- [ ] 实现可执行代码块
+- [ ] 集成 Tauri Shell
+- [ ] 实现终端位置切换
+- [ ] 添加进度追踪
+
+### Phase 4: 数据管理(Week 4)
+
+- [ ] 实现本地文件导入
+- [ ] 实现教程存储
+- [ ] 添加学习进度持久化
+- [ ] 实现设置页面
+
+---
+
+## 九、下一步行动
+
+1. **创建 Feature Branch**: 使用 git worktree 创建工作分支
+2. **初始化项目**: 创建 Tauri + Next.js 项目结构
+3. **实现基础布局**: 按照设计实现页面框架
+4. **集成终端**: 集成 xterm.js 并实现基础功能
+
+---
+
+*分析完成时间:2026-04-08*
+*分析师:AI Agent*
+*版本:v1.0*
diff --git a/tasks/mindstorm/analysis/02-prd.md b/tasks/mindstorm/analysis/02-prd.md
new file mode 100644
index 0000000..0114eb9
--- /dev/null
+++ b/tasks/mindstorm/analysis/02-prd.md
@@ -0,0 +1,609 @@
+# Executable Tutorial - 产品需求文档 (PRD)
+
+> 版本:v1.0
+> 创建时间:2026-04-08
+> 产品名称:Executable Tutorial Desktop App
+
+---
+
+## 1. 产品概述
+
+### 1.1 产品背景
+
+Executable Tutorial 是一款面向初学者的桌面应用,旨在通过交互式教程帮助用户学习开发工具和环境配置。与传统静态教程不同,本应用的教程包含可执行的代码块,用户可以直接在应用内运行命令,实时看到执行结果。
+
+### 1.2 产品定位
+
+| 维度 | 描述 |
+|-----|------|
+| **目标用户** | 零基础或初级开发者 |
+| **核心价值** | 边学边做,理论与实践结合 |
+| **使用场景** | 本地环境搭建、工具学习、技能训练 |
+| **产品形态** | 桌面应用(Tauri + Next.js) |
+
+### 1.3 产品愿景
+
+> 让学习技术像玩游戏一样简单有趣,每个教程都是可执行的,每个概念都能立即实践。
+
+---
+
+## 2. 用户故事
+
+### 2.1 主要用户画像
+
+#### 用户一:小明(零基础小白)
+- **背景**:大学生,对编程感兴趣但毫无基础
+- **痛点**:不知道怎么开始,网上教程太零散
+- **需求**:
+ - 清晰的学习路径
+ - 一步到位的环境配置
+ - 能看到实际效果
+
+#### 用户二:小红(初级开发者)
+- **背景**:会一些基础,想学习新工具
+- **痛点**:工具配置麻烦,版本冲突多
+- **需求**:
+ - 快速配置开发环境
+ - 可靠的安装脚本
+ - 遇到问题能快速解决
+
+### 2.2 用户故事列表
+
+| ID | 作为 | 我想要 | 以便于 | 优先级 |
+|----|-----|--------|--------|--------|
+| US-001 | 小白用户 | 看到清晰的学习路径 | 知道从哪开始学 | P0 |
+| US-002 | 小白用户 | 一键执行安装命令 | 不用手动复制粘贴 | P0 |
+| US-003 | 小白用户 | 实时看到命令输出 | 了解发生了什么 | P0 |
+| US-004 | 初级用户 | 导入外部教程 | 学习更多内容 | P1 |
+| US-005 | 初级用户 | 追踪学习进度 | 知道学了哪些 | P1 |
+| US-006 | 高级用户 | 编写自己的教程 | 分享知识给他人 | P2 |
+
+---
+
+## 3. 功能需求
+
+### 3.1 功能架构
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Executable Tutorial │
+├─────────────────────────────────────────────────────────────┤
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
+│ │ Browse │ │ Learn │ │ Execute │ │
+│ │ 浏览 │ │ 学习 │ │ 执行 │ │
+│ ├─────────────┤ ├─────────────┤ ├─────────────────────┤ │
+│ │ • 教程列表 │ │ • 教程阅读 │ │ • 命令执行 │ │
+│ │ • 系列展示 │ │ • 代码高亮 │ │ • 实时输出 │ │
+│ │ • 搜索筛选 │ │ • 进度追踪 │ │ • 结果验证 │ │
+│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
+├─────────────────────────────────────────────────────────────┤
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
+│ │ Import │ │ Manage │ │ Settings │ │
+│ │ 导入 │ │ 管理 │ │ 设置 │ │
+│ ├─────────────┤ ├─────────────┤ ├─────────────────────┤ │
+│ │ • 文件导入 │ │ • 本地目录 │ │ • 终端配置 │ │
+│ │ • URL导入 │ │ • 进度重置 │ │ • 外观主题 │ │
+│ │ • 拖拽添加 │ │ • 删除教程 │ │ • 快捷键 │ │
+│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 3.2 功能详细说明
+
+#### F1. 首页(总体展示页面)
+
+**功能描述**:应用启动后的主页面,展示教程和系列概览。
+
+**功能点**:
+
+| 功能点 | 描述 | 优先级 |
+|-------|------|--------|
+| F1.1 系列卡片展示 | 以卡片形式展示系列,每个卡片显示标题、描述、教程数量 | P0 |
+| F1.2 最近教程 | 展示最近添加或更新的教程 | P0 |
+| F1.3 搜索栏 | 支持按关键词搜索教程 | P0 |
+| F1.4 筛选器 | 按分类、难度、标签筛选 | P1 |
+| F1.5 快捷操作 | 底部显示"添加本地目录"和"导入教程"按钮 | P1 |
+
+**交互流程**:
+
+```
+用户打开应用
+ ↓
+展示首页
+ ↓
+┌─────────────────┬─────────────────┐
+↓ ↓ ↓
+点击系列卡片 点击教程 使用搜索/筛选
+ ↓ ↓ ↓
+进入系列详情 进入教程详情 更新列表
+```
+
+#### F2. 导航栏
+
+**功能描述**:顶部导航栏,提供全局导航功能。
+
+**功能点**:
+
+| 功能点 | 描述 | 优先级 |
+|-------|------|--------|
+| F2.1 Logo/首页 | 点击返回首页 | P0 |
+| F2.2 教程导航 | 展示所有教程的列表 | P0 |
+| F2.3 系列导航 | 展示所有系列 | P0 |
+| F2.4 设置入口 | 进入设置页面 | P1 |
+| F2.5 面包屑 | 显示当前位置 | P1 |
+
+#### F3. 系列详情页
+
+**功能描述**:展示系列内的教程列表和进度。
+
+**功能点**:
+
+| 功能点 | 描述 | 优先级 |
+|-------|------|--------|
+| F3.1 系列信息 | 显示系列标题、描述、总进度 | P0 |
+| F3.2 教程列表 | 以卡片或列表形式展示教程 | P0 |
+| F3.3 更多按钮 | 超过3个教程时显示"查看更多" | P0 |
+| F3.4 进度追踪 | 显示每个教程的完成状态 | P1 |
+| F3.5 开始学习 | 从未完成的教程开始 | P1 |
+
+#### F4. 教程详情页
+
+**功能描述**:展示教程内容,支持可执行代码块。
+
+**功能点**:
+
+| 功能点 | 描述 | 优先级 |
+|-------|------|--------|
+| F4.1 Markdown渲染 | 渲染教程内容 | P0 |
+| F4.2 代码高亮 | 语法高亮显示 | P0 |
+| F4.3 可执行代码块 | 标识可执行的代码 | P0 |
+| F4.4 运行按钮 | 点击执行代码 | P0 |
+| F4.5 终端展示 | 执行时显示终端 | P0 |
+| F4.6 进度保存 | 记录阅读进度 | P1 |
+| F4.7 完成标记 | 手动标记教程完成 | P1 |
+
+#### F5. 终端功能
+
+**功能描述**:内置终端,用于执行教程中的命令。
+
+**功能点**:
+
+| 功能点 | 描述 | 优先级 |
+|-------|------|--------|
+| F5.1 终端显示 | 首次点击运行时展开终端 | P0 |
+| F5.2 位置切换 | 支持右侧/底部位置切换 | P0 |
+| F5.3 实时输出 | 命令执行时实时显示输出 | P0 |
+| F5.4 滚动跟随 | 输出自动滚动到底部 | P0 |
+| F5.5 手动输入 | 支持在终端中手动输入命令 | P1 |
+| F5.6 历史记录 | 保存命令历史 | P2 |
+
+**终端状态流转**:
+
+```
+隐藏状态
+ ↓ 点击运行
+展开状态(默认位置:右侧)
+ ↓ 点击切换位置
+切换为底部/右侧
+ ↓ 点击最小化
+隐藏状态
+```
+
+#### F6. 文件导入
+
+**功能描述**:支持导入外部教程内容。
+
+**功能点**:
+
+| 功能点 | 描述 | 优先级 |
+|-------|------|--------|
+| F6.1 本地文件导入 | 选择本地 Markdown 文件 | P1 |
+| F6.2 本地目录添加 | 添加整个教程目录 | P1 |
+| F6.3 URL导入 | 从 URL 导入教程(调研功能) | P2 |
+| F6.4 拖拽导入 | 拖拽文件到应用窗口 | P2 |
+
+---
+
+## 4. 非功能需求
+
+### 4.1 性能需求
+
+| 指标 | 目标值 | 说明 |
+|-----|--------|------|
+| 启动时间 | < 3秒 | 应用冷启动时间 |
+| 页面切换 | < 300ms | 页面间切换延迟 |
+| 终端响应 | < 100ms | 命令输入到显示的延迟 |
+| 内存占用 | < 300MB | 运行时内存使用 |
+| 安装包大小 | < 50MB | 最终安装包体积 |
+
+### 4.2 兼容性需求
+
+| 平台 | 支持版本 | 说明 |
+|-----|---------|------|
+| macOS | 11+ (Big Sur) | Intel & Apple Silicon |
+| Windows | 10/11 | 64位 |
+| Linux | Ubuntu 20.04+ | 实验性支持 |
+
+### 4.3 安全需求
+
+| 需求 | 说明 |
+|-----|------|
+| 命令白名单 | 只允许执行预定义的命令 |
+| 用户确认 | 危险操作前需要用户确认 |
+| 沙箱执行 | 限制命令的执行范围 |
+| 路径验证 | 验证工作目录合法性 |
+
+### 4.4 用户体验需求
+
+| 需求 | 说明 |
+|-----|------|
+| 离线可用 | 内置教程无需网络 |
+| 深色/浅色主题 | 支持主题切换 |
+| 字体可调 | 终端和内容字体大小可调 |
+| 快捷键支持 | 常用操作支持快捷键 |
+
+---
+
+## 5. 界面设计规范
+
+### 5.1 设计原则
+
+- **简洁清晰**:减少视觉干扰,聚焦内容
+- **一致性**:保持交互和视觉的一致性
+- **反馈及时**:操作后立即给出反馈
+- **容错友好**:错误提示清晰,提供解决方案
+
+### 5.2 色彩规范
+
+```css
+/* 主色调 */
+--primary: #3b82f6; /* 蓝色 - 主要操作 */
+--primary-hover: #2563eb;
+
+/* 成功/警告/错误 */
+--success: #22c55e; /* 绿色 */
+--warning: #f59e0b; /* 黄色 */
+--error: #ef4444; /* 红色 */
+
+/* 背景色(深色主题) */
+--bg-primary: #0f172a; /* 主背景 */
+--bg-secondary: #1e293b; /* 卡片背景 */
+--bg-tertiary: #334155; /* 悬停背景 */
+
+/* 文字色 */
+--text-primary: #f8fafc; /* 主要文字 */
+--text-secondary: #94a3b8; /* 次要文字 */
+--text-muted: #64748b; /* 辅助文字 */
+```
+
+### 5.3 字体规范
+
+| 用途 | 字体 | 大小 |
+|-----|------|------|
+| 标题 | Inter / system-ui | 24px / 20px / 18px |
+| 正文 | Inter / system-ui | 14px |
+| 代码 | JetBrains Mono | 13px |
+| 终端 | JetBrains Mono | 14px |
+
+### 5.4 布局规范
+
+**窗口尺寸**:
+- 最小宽度:1024px
+- 最小高度:768px
+- 默认尺寸:1280x800
+
+**间距系统**:
+```
+--space-1: 4px
+--space-2: 8px
+--space-3: 12px
+--space-4: 16px
+--space-6: 24px
+--space-8: 32px
+```
+
+---
+
+## 6. 数据规范
+
+### 6.1 Tutorial 数据格式
+
+```yaml
+# Tutorial Frontmatter
+---
+id: "tutorial-001"
+title: "安装 Node.js"
+description: "使用 fnm 安装和管理 Node.js 版本"
+category: "dev-tools"
+difficulty: "beginner"
+duration: 10
+tags: ["nodejs", "fnm", "javascript"]
+series: "nodejs-fundamentals"
+order: 1
+---
+
+# Tutorial Content
+## 什么是 Node.js?
+
+Node.js 是一个 JavaScript 运行时...
+
+## 安装 fnm
+
+首先安装 fnm(Fast Node Manager):
+
+```bash
+brew install fnm
+```
+
+## 配置 Shell
+
+将 fnm 添加到你的 shell 配置:
+
+```bash:executable
+eval "$(fnm env --use-on-cd)"
+```
+
+## 安装 Node.js
+
+```bash:executable
+fnm install --lts
+fnm use --lts
+fnm default --lts
+```
+
+## 验证安装
+
+```bash:executable
+node -v
+npm -v
+```
+```
+
+### 6.2 Series 数据格式
+
+```yaml
+---
+id: "nodejs-fundamentals"
+title: "Node.js 基础"
+description: "从零开始学习 Node.js 开发环境配置"
+category: "dev-tools"
+difficulty: "beginner"
+tutorials:
+ - "tutorial-001"
+ - "tutorial-002"
+ - "tutorial-003"
+color: "#339933"
+icon: "nodejs"
+---
+```
+
+### 6.3 存储结构
+
+```
+App Data/
+├── tutorials/
+│ ├── builtin/ # 内置教程(只读)
+│ ├── local/ # 本地导入的教程
+│ └── cache/ # 缓存的在线教程
+├── series/
+│ └── index.json # 系列索引
+├── progress/
+│ └── {user_id}.json # 用户学习进度
+├── settings/
+│ └── preferences.json # 用户偏好设置
+└── logs/
+ └── app.log # 应用日志
+```
+
+---
+
+## 7. API 接口规范
+
+### 7.1 Tauri Commands
+
+```typescript
+// 教程相关
+interface TutorialCommands {
+ // 获取所有教程
+ getTutorials(): Promise;
+
+ // 获取单个教程
+ getTutorial(id: string): Promise;
+
+ // 导入教程
+ importTutorial(source: string, type: 'file' | 'url'): Promise;
+
+ // 删除本地教程
+ deleteTutorial(id: string): Promise;
+}
+
+// 系列相关
+interface SeriesCommands {
+ // 获取所有系列
+ getSeries(): Promise;
+
+ // 获取单个系列
+ getSeriesById(id: string): Promise;
+}
+
+// 执行相关
+interface ExecuteCommands {
+ // 执行命令
+ executeCommand(
+ command: string,
+ args: string[],
+ options?: ExecuteOptions
+ ): Promise;
+
+ // 流式执行
+ executeCommandStream(
+ command: string,
+ args: string[],
+ callback: (output: string) => void
+ ): Promise;
+
+ // 终止进程
+ killProcess(pid: number): Promise;
+}
+
+// 进度相关
+interface ProgressCommands {
+ // 保存进度
+ saveProgress(tutorialId: string, progress: Progress): Promise;
+
+ // 获取进度
+ getProgress(tutorialId: string): Promise