Skip to content
This repository was archived by the owner on Dec 17, 2025. It is now read-only.

Commit a887b3c

Browse files
committed
Add --repair mode for install
* Added --all mode for config to show full lockfile contents * Updated sleuth repository to use common url field instead of its own
1 parent 1ad6d72 commit a887b3c

File tree

26 files changed

+546
-136
lines changed

26 files changed

+546
-136
lines changed

internal/artifacts/tracker_test.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package artifacts
22

33
import (
4-
"os"
54
"strings"
65
"testing"
76
)
@@ -24,18 +23,13 @@ func TestGetTrackerPath(t *testing.T) {
2423
}
2524

2625
func TestTrackerOperations(t *testing.T) {
27-
// Create a temp directory and override cache for testing
28-
tmpDir, err := os.MkdirTemp("", "tracker-test-*")
29-
if err != nil {
30-
t.Fatalf("Failed to create temp dir: %v", err)
26+
// Create a fresh in-memory tracker for testing (don't load from disk)
27+
tracker := &Tracker{
28+
Version: TrackerFormatVersion,
29+
Artifacts: []InstalledArtifact{},
3130
}
32-
defer os.RemoveAll(tmpDir)
3331

34-
// Test loading empty tracker
35-
tracker, err := LoadTracker()
36-
if err != nil {
37-
t.Fatalf("LoadTracker() error = %v", err)
38-
}
32+
// Verify tracker starts empty
3933
if len(tracker.Artifacts) != 0 {
4034
t.Errorf("Expected empty tracker, got %d artifacts", len(tracker.Artifacts))
4135
}

internal/clients/claude_code/client.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/sleuth-io/skills/internal/clients"
1212
"github.com/sleuth-io/skills/internal/clients/claude_code/handlers"
1313
"github.com/sleuth-io/skills/internal/handlers/dirartifact"
14+
"github.com/sleuth-io/skills/internal/lockfile"
1415
"github.com/sleuth-io/skills/internal/metadata"
1516
)
1617

@@ -253,3 +254,32 @@ func (c *Client) UninstallHooks(ctx context.Context) error {
253254
func (c *Client) ShouldInstall(ctx context.Context) (bool, error) {
254255
return true, nil
255256
}
257+
258+
// VerifyArtifacts checks if artifacts are actually installed on the filesystem
259+
func (c *Client) VerifyArtifacts(ctx context.Context, artifacts []*lockfile.Artifact, scope *clients.InstallScope) []clients.VerifyResult {
260+
targetBase := c.determineTargetBase(scope)
261+
results := make([]clients.VerifyResult, 0, len(artifacts))
262+
263+
for _, art := range artifacts {
264+
result := clients.VerifyResult{
265+
Artifact: art,
266+
}
267+
268+
handler, err := handlers.NewHandler(art.Type, &metadata.Metadata{
269+
Artifact: metadata.Artifact{
270+
Name: art.Name,
271+
Version: art.Version,
272+
Type: art.Type,
273+
},
274+
})
275+
if err != nil {
276+
result.Message = err.Error()
277+
} else {
278+
result.Installed, result.Message = handler.VerifyInstalled(targetBase)
279+
}
280+
281+
results = append(results, result)
282+
}
283+
284+
return results
285+
}

internal/clients/claude_code/handlers/agent.go

Lines changed: 10 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ package handlers
33
import (
44
"context"
55
"fmt"
6-
"os"
76
"path/filepath"
87

98
"github.com/sleuth-io/skills/internal/artifact"
9+
"github.com/sleuth-io/skills/internal/handlers/dirartifact"
1010
"github.com/sleuth-io/skills/internal/metadata"
1111
"github.com/sleuth-io/skills/internal/utils"
1212
)
1313

14+
var agentOps = dirartifact.NewOperations("agents", &artifact.TypeAgent)
15+
1416
// AgentHandler handles agent artifact installation
1517
type AgentHandler struct {
1618
metadata *metadata.Metadata
@@ -93,43 +95,12 @@ func (h *AgentHandler) Install(ctx context.Context, zipData []byte, targetBase s
9395
return fmt.Errorf("validation failed: %w", err)
9496
}
9597

96-
// Determine installation path
97-
installPath := filepath.Join(targetBase, h.GetInstallPath())
98-
99-
// Remove existing installation if present
100-
if utils.IsDirectory(installPath) {
101-
if err := os.RemoveAll(installPath); err != nil {
102-
return fmt.Errorf("failed to remove existing installation: %w", err)
103-
}
104-
}
105-
106-
// Create installation directory
107-
if err := utils.EnsureDir(installPath); err != nil {
108-
return fmt.Errorf("failed to create installation directory: %w", err)
109-
}
110-
111-
// Extract zip to installation directory
112-
if err := utils.ExtractZip(zipData, installPath); err != nil {
113-
return fmt.Errorf("failed to extract zip: %w", err)
114-
}
115-
116-
return nil
98+
return agentOps.Install(ctx, zipData, targetBase, h.metadata.Artifact.Name)
11799
}
118100

119101
// Remove uninstalls the agent artifact
120102
func (h *AgentHandler) Remove(ctx context.Context, targetBase string) error {
121-
installPath := filepath.Join(targetBase, h.GetInstallPath())
122-
123-
if !utils.IsDirectory(installPath) {
124-
// Already removed or never installed
125-
return nil
126-
}
127-
128-
if err := os.RemoveAll(installPath); err != nil {
129-
return fmt.Errorf("failed to remove agent: %w", err)
130-
}
131-
132-
return nil
103+
return agentOps.Remove(ctx, targetBase, h.metadata.Artifact.Name)
133104
}
134105

135106
// GetInstallPath returns the installation path relative to targetBase
@@ -187,3 +158,8 @@ func (h *AgentHandler) Validate(zipData []byte) error {
187158
func (h *AgentHandler) CanDetectInstalledState() bool {
188159
return true
189160
}
161+
162+
// VerifyInstalled checks if the agent is properly installed
163+
func (h *AgentHandler) VerifyInstalled(targetBase string) (bool, string) {
164+
return agentOps.VerifyInstalled(targetBase, h.metadata.Artifact.Name, h.metadata.Artifact.Version)
165+
}

internal/clients/claude_code/handlers/command.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,30 @@ func (h *CommandHandler) removeMetadataFile(installPath string) {
228228
func (h *CommandHandler) CanDetectInstalledState() bool {
229229
return true
230230
}
231+
232+
// VerifyInstalled checks if the command is properly installed
233+
func (h *CommandHandler) VerifyInstalled(targetBase string) (bool, string) {
234+
commandPath := filepath.Join(targetBase, h.GetInstallPath())
235+
236+
if !utils.FileExists(commandPath) {
237+
return false, "command file not found"
238+
}
239+
240+
// Check metadata file for version verification
241+
metadataPath := strings.TrimSuffix(commandPath, ".md") + "-metadata.toml"
242+
if !utils.FileExists(metadataPath) {
243+
// No metadata file - can only verify file exists
244+
return true, "installed (no version info)"
245+
}
246+
247+
meta, err := metadata.ParseFile(metadataPath)
248+
if err != nil {
249+
return false, "failed to parse metadata: " + err.Error()
250+
}
251+
252+
if meta.Artifact.Version != h.metadata.Artifact.Version {
253+
return false, fmt.Sprintf("version mismatch: installed %s, expected %s", meta.Artifact.Version, h.metadata.Artifact.Version)
254+
}
255+
256+
return true, "installed"
257+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package handlers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/sleuth-io/skills/internal/artifact"
8+
"github.com/sleuth-io/skills/internal/metadata"
9+
)
10+
11+
// Handler defines the interface for artifact type handlers
12+
type Handler interface {
13+
// Install installs the artifact from zip data to the target base directory
14+
Install(ctx context.Context, zipData []byte, targetBase string) error
15+
16+
// Remove removes the artifact from the target base directory
17+
Remove(ctx context.Context, targetBase string) error
18+
19+
// GetInstallPath returns the installation path relative to targetBase
20+
GetInstallPath() string
21+
22+
// CanDetectInstalledState returns true if the handler can verify installation state
23+
CanDetectInstalledState() bool
24+
25+
// VerifyInstalled checks if the artifact is properly installed
26+
// Returns (installed bool, message string)
27+
VerifyInstalled(targetBase string) (bool, string)
28+
}
29+
30+
// NewHandler creates a handler for the given artifact type and metadata
31+
func NewHandler(artifactType artifact.Type, meta *metadata.Metadata) (Handler, error) {
32+
switch artifactType {
33+
case artifact.TypeSkill:
34+
return NewSkillHandler(meta), nil
35+
case artifact.TypeAgent:
36+
return NewAgentHandler(meta), nil
37+
case artifact.TypeCommand:
38+
return NewCommandHandler(meta), nil
39+
case artifact.TypeHook:
40+
return NewHookHandler(meta), nil
41+
case artifact.TypeMCP:
42+
return NewMCPHandler(meta), nil
43+
case artifact.TypeMCPRemote:
44+
return NewMCPRemoteHandler(meta), nil
45+
default:
46+
return nil, fmt.Errorf("unsupported artifact type: %s", artifactType.Key)
47+
}
48+
}

internal/clients/claude_code/handlers/hook.go

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ import (
88
"path/filepath"
99

1010
"github.com/sleuth-io/skills/internal/artifact"
11+
"github.com/sleuth-io/skills/internal/handlers/dirartifact"
1112
"github.com/sleuth-io/skills/internal/metadata"
1213
"github.com/sleuth-io/skills/internal/utils"
1314
)
1415

16+
var hookOps = dirartifact.NewOperations("hooks", &artifact.TypeHook)
17+
1518
// HookHandler handles hook artifact installation
1619
type HookHandler struct {
1720
metadata *metadata.Metadata
@@ -95,24 +98,9 @@ func (h *HookHandler) Install(ctx context.Context, zipData []byte, targetBase st
9598
return fmt.Errorf("validation failed: %w", err)
9699
}
97100

98-
// Determine installation path
99-
installPath := filepath.Join(targetBase, h.GetInstallPath())
100-
101-
// Remove existing installation if present
102-
if utils.IsDirectory(installPath) {
103-
if err := os.RemoveAll(installPath); err != nil {
104-
return fmt.Errorf("failed to remove existing installation: %w", err)
105-
}
106-
}
107-
108-
// Create installation directory
109-
if err := utils.EnsureDir(installPath); err != nil {
110-
return fmt.Errorf("failed to create installation directory: %w", err)
111-
}
112-
113-
// Extract zip to installation directory
114-
if err := utils.ExtractZip(zipData, installPath); err != nil {
115-
return fmt.Errorf("failed to extract zip: %w", err)
101+
// Extract to hooks directory
102+
if err := hookOps.Install(ctx, zipData, targetBase, h.metadata.Artifact.Name); err != nil {
103+
return err
116104
}
117105

118106
// Update settings.json to register the hook
@@ -125,21 +113,13 @@ func (h *HookHandler) Install(ctx context.Context, zipData []byte, targetBase st
125113

126114
// Remove uninstalls the hook artifact
127115
func (h *HookHandler) Remove(ctx context.Context, targetBase string) error {
128-
installPath := filepath.Join(targetBase, h.GetInstallPath())
129-
130116
// Remove from settings.json first
131117
if err := h.removeFromSettings(targetBase); err != nil {
132118
return fmt.Errorf("failed to remove from settings: %w", err)
133119
}
134120

135121
// Remove installation directory
136-
if utils.IsDirectory(installPath) {
137-
if err := os.RemoveAll(installPath); err != nil {
138-
return fmt.Errorf("failed to remove hook: %w", err)
139-
}
140-
}
141-
142-
return nil
122+
return hookOps.Remove(ctx, targetBase, h.metadata.Artifact.Name)
143123
}
144124

145125
// GetInstallPath returns the installation path relative to targetBase
@@ -348,3 +328,8 @@ func (h *HookHandler) buildHookConfig() map[string]interface{} {
348328
func (h *HookHandler) CanDetectInstalledState() bool {
349329
return true
350330
}
331+
332+
// VerifyInstalled checks if the hook is properly installed
333+
func (h *HookHandler) VerifyInstalled(targetBase string) (bool, string) {
334+
return hookOps.VerifyInstalled(targetBase, h.metadata.Artifact.Name, h.metadata.Artifact.Version)
335+
}

internal/clients/claude_code/handlers/mcp.go

Lines changed: 13 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import (
99
"strings"
1010

1111
"github.com/sleuth-io/skills/internal/artifact"
12+
"github.com/sleuth-io/skills/internal/handlers/dirartifact"
1213
"github.com/sleuth-io/skills/internal/metadata"
1314
"github.com/sleuth-io/skills/internal/utils"
1415
)
1516

17+
var mcpOps = dirartifact.NewOperations("mcp-servers", &artifact.TypeMCP)
18+
1619
// MCPHandler handles MCP server artifact installation
1720
type MCPHandler struct {
1821
metadata *metadata.Metadata
@@ -93,27 +96,13 @@ func (h *MCPHandler) Install(ctx context.Context, zipData []byte, targetBase str
9396
return fmt.Errorf("validation failed: %w", err)
9497
}
9598

96-
// Determine installation path
97-
installPath := filepath.Join(targetBase, h.GetInstallPath())
98-
99-
// Remove existing installation if present
100-
if utils.IsDirectory(installPath) {
101-
if err := os.RemoveAll(installPath); err != nil {
102-
return fmt.Errorf("failed to remove existing installation: %w", err)
103-
}
104-
}
105-
106-
// Create installation directory
107-
if err := utils.EnsureDir(installPath); err != nil {
108-
return fmt.Errorf("failed to create installation directory: %w", err)
109-
}
110-
111-
// Extract zip to installation directory
112-
if err := utils.ExtractZip(zipData, installPath); err != nil {
113-
return fmt.Errorf("failed to extract zip: %w", err)
99+
// Extract to mcp-servers directory
100+
if err := mcpOps.Install(ctx, zipData, targetBase, h.metadata.Artifact.Name); err != nil {
101+
return err
114102
}
115103

116104
// Update .mcp.json to register the MCP server
105+
installPath := filepath.Join(targetBase, h.GetInstallPath())
117106
if err := h.updateMCPConfig(targetBase, installPath); err != nil {
118107
return fmt.Errorf("failed to update MCP config: %w", err)
119108
}
@@ -123,21 +112,13 @@ func (h *MCPHandler) Install(ctx context.Context, zipData []byte, targetBase str
123112

124113
// Remove uninstalls the MCP server artifact
125114
func (h *MCPHandler) Remove(ctx context.Context, targetBase string) error {
126-
installPath := filepath.Join(targetBase, h.GetInstallPath())
127-
128115
// Remove from .mcp.json first
129116
if err := h.removeFromMCPConfig(targetBase); err != nil {
130117
return fmt.Errorf("failed to remove from MCP config: %w", err)
131118
}
132119

133120
// Remove installation directory
134-
if utils.IsDirectory(installPath) {
135-
if err := os.RemoveAll(installPath); err != nil {
136-
return fmt.Errorf("failed to remove MCP server: %w", err)
137-
}
138-
}
139-
140-
return nil
121+
return mcpOps.Remove(ctx, targetBase, h.metadata.Artifact.Name)
141122
}
142123

143124
// GetInstallPath returns the installation path relative to targetBase
@@ -315,3 +296,8 @@ func (h *MCPHandler) buildMCPServerConfig(installPath string) map[string]interfa
315296
func (h *MCPHandler) CanDetectInstalledState() bool {
316297
return true
317298
}
299+
300+
// VerifyInstalled checks if the MCP server is properly installed
301+
func (h *MCPHandler) VerifyInstalled(targetBase string) (bool, string) {
302+
return mcpOps.VerifyInstalled(targetBase, h.metadata.Artifact.Name, h.metadata.Artifact.Version)
303+
}

0 commit comments

Comments
 (0)