feat: add Cobra CLI, LSP/MCP registries, workflow engine, and enriched dashboard

Major changes:
- Refactor CLI entry point to Cobra commands (root, setup, scan, doctor, install, update, lsp, mcp, skills, config, version)
- Add LSP registry with health checks, auto-install, and editor config generation
- Add MCP registry with editor detection, status tracking, and per-editor configuration
- Add workflow engine with planner and step execution for automated task chains
- Add conversation search, export (Markdown/JSON), and detailed token counting
- Add streaming shell chat handler with tool call/result events
- Add skill validation, dry-run testing, and export endpoints
- Enrich dashboard with Tools/Activity/Status tabs and tool cards grid
- Add PRD documentation
- Complete i18n for both EN and FR

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
This commit is contained in:
Augustin
2026-04-22 22:22:05 +02:00
parent 61da8039bc
commit 485e085bb0
42 changed files with 6779 additions and 319 deletions

View File

@@ -113,7 +113,7 @@ func TestCreateAndGet(t *testing.T) {
Description: "Test description",
Content: "Test content body",
Author: "tester",
Version: "0.1",
Version: "1.0.0",
Target: "both",
}
@@ -198,3 +198,242 @@ func TestInstallBuiltinSkills(t *testing.T) {
t.Error("Expected env-setup skill")
}
}
func TestValidate(t *testing.T) {
skill := &Skill{
Name: "valid-skill",
Description: "A valid skill",
Content: "## Steps\nDo things",
Version: "1.0.0",
Target: "both",
}
errs := Validate(skill)
if len(errs) != 0 {
t.Errorf("Valid skill should have no errors, got %v", errs)
}
}
func TestValidateMissingFields(t *testing.T) {
skill := &Skill{}
errs := Validate(skill)
if len(errs) == 0 {
t.Error("Empty skill should have validation errors")
}
fields := map[string]bool{}
for _, e := range errs {
fields[e.Field] = true
}
if !fields["name"] {
t.Error("Should require name")
}
if !fields["description"] {
t.Error("Should require description")
}
if !fields["content"] {
t.Error("Should require content")
}
}
func TestValidateBadVersion(t *testing.T) {
skill := &Skill{
Name: "test-skill",
Description: "desc",
Content: "content",
Version: "not-semver",
}
errs := Validate(skill)
hasVersionErr := false
for _, e := range errs {
if e.Field == "version" {
hasVersionErr = true
}
}
if !hasVersionErr {
t.Error("Should reject non-semver version")
}
}
func TestValidateBadTarget(t *testing.T) {
skill := &Skill{
Name: "test",
Description: "desc",
Content: "content",
Target: "invalid",
}
errs := Validate(skill)
hasTargetErr := false
for _, e := range errs {
if e.Field == "target" {
hasTargetErr = true
}
}
if !hasTargetErr {
t.Error("Should reject invalid target")
}
}
func TestValidateBadName(t *testing.T) {
skill := &Skill{
Name: "INVALID",
Description: "desc",
Content: "content",
}
errs := Validate(skill)
hasNameErr := false
for _, e := range errs {
if e.Field == "name" {
hasNameErr = true
}
}
if !hasNameErr {
t.Error("Should reject uppercase name")
}
}
func TestValidateDependencies(t *testing.T) {
skill := &Skill{
Name: "test",
Description: "desc",
Content: "content",
Dependencies: []SkillDependency{
{Type: "mcp_server", Name: "github", Required: true},
{Type: "invalid_type", Name: "test"},
},
}
errs := Validate(skill)
hasDepErr := false
for _, e := range errs {
if e.Field == "dependencies[1].type" {
hasDepErr = true
}
}
if !hasDepErr {
t.Error("Should reject invalid dependency type")
}
}
func TestExportImport(t *testing.T) {
tmpDir := t.TempDir()
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", tmpDir)
skill := &Skill{
Name: "export-test",
Description: "Export test skill",
Content: "## Content",
Author: "tester",
Version: "1.0.0",
Target: "both",
Tags: []string{"test"},
}
Create(skill)
exportPath := filepath.Join(tmpDir, "export", "export-test.md")
if err := Export("export-test", exportPath); err != nil {
t.Fatalf("Export failed: %v", err)
}
if _, err := os.Stat(exportPath); os.IsNotExist(err) {
t.Error("Export file should exist")
}
imported, err := Import(exportPath)
if err != nil {
t.Fatalf("Import failed: %v", err)
}
if imported.Description != "Export test skill" {
t.Errorf("Expected 'Export test skill', got %s", imported.Description)
}
}
func TestDryRun(t *testing.T) {
tmpDir := t.TempDir()
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", tmpDir)
skill := &Skill{
Name: "dry-run-test",
Description: "Dry run test",
Content: "## Steps\nDo something",
Version: "1.0.0",
Target: "both",
Tags: []string{"test"},
}
Create(skill)
result := DryRun("dry-run-test", "test something")
if !result.Passed {
t.Errorf("DryRun should pass, got: %s", result.Message)
}
}
func TestDryRunMissing(t *testing.T) {
result := DryRun("nonexistent", "")
if result.Passed {
t.Error("DryRun of nonexistent skill should fail")
}
}
func TestUpdate(t *testing.T) {
tmpDir := t.TempDir()
os.Setenv("HOME", tmpDir)
defer os.Setenv("HOME", tmpDir)
skill := &Skill{
Name: "update-test",
Description: "Original",
Content: "Original content",
Version: "1.0.0",
Target: "both",
}
Create(skill)
skill.Description = "Updated"
skill.Content = "Updated content"
skill.Version = "2.0.0"
if err := Update(skill); err != nil {
t.Fatalf("Update failed: %v", err)
}
got, err := Get("update-test")
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if got.Description != "Updated" {
t.Errorf("Expected 'Updated', got %s", got.Description)
}
}
func TestBuiltinSkillCount(t *testing.T) {
if len(builtinSkills) < 5 {
t.Errorf("Expected at least 5 builtin skills, got %d", len(builtinSkills))
}
expectedSkills := []string{"env-setup", "git-workflow", "api-design", "debug-assist", "code-review", "docker-setup", "security-audit", "mcp-setup", "lsp-setup", "workflow-design"}
for _, name := range expectedSkills {
found := false
for _, s := range builtinSkills {
if s.Name == name {
found = true
break
}
}
if !found {
t.Errorf("Expected builtin skill: %s", name)
}
}
}
func TestBuiltinSkillsHaveDependencies(t *testing.T) {
hasDeps := 0
for _, s := range builtinSkills {
if len(s.Dependencies) > 0 {
hasDeps++
}
}
if hasDeps == 0 {
t.Error("At least some builtin skills should declare dependencies")
}
}