Skip to content

Commit 75eef73

Browse files
feat: add managed rules and hooks parity
1 parent 7a010a9 commit 75eef73

126 files changed

Lines changed: 27251 additions & 410 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/skillshare/backup.go

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -372,8 +372,12 @@ func cmdRestore(args []string) error {
372372
return err
373373
}
374374

375-
target, exists := cfg.Targets[targetName]
376-
if !exists {
375+
resolvedTargetName, target, err := resolveConfiguredRestoreTarget(cfg.Targets, targetName)
376+
if err != nil {
377+
return err
378+
}
379+
sc := target.SkillsConfig()
380+
if sc.Path == "" {
377381
return fmt.Errorf("target '%s' not found in config", targetName)
378382
}
379383

@@ -384,20 +388,18 @@ func cmdRestore(args []string) error {
384388
}
385389

386390
opts := backup.RestoreOptions{Force: force}
387-
388-
sc := target.SkillsConfig()
389391
if dryRun {
390392
if fromTimestamp != "" {
391-
return previewRestoreFromTimestamp(targetName, sc.Path, fromTimestamp, opts)
393+
return previewRestoreFromTimestamp(resolvedTargetName, sc.Path, fromTimestamp, opts)
392394
}
393-
return previewRestoreFromLatest(targetName, sc.Path, opts)
395+
return previewRestoreFromLatest(resolvedTargetName, sc.Path, opts)
394396
}
395397

396398
var restoreErr error
397399
if fromTimestamp != "" {
398-
restoreErr = restoreFromTimestamp(targetName, sc.Path, fromTimestamp, opts)
400+
restoreErr = restoreFromTimestamp(resolvedTargetName, sc.Path, fromTimestamp, opts)
399401
} else {
400-
restoreErr = restoreFromLatest(targetName, sc.Path, opts)
402+
restoreErr = restoreFromLatest(resolvedTargetName, sc.Path, opts)
401403
}
402404

403405
e := oplog.NewEntry("restore", statusFromErr(restoreErr), time.Since(start))
@@ -413,6 +415,22 @@ func cmdRestore(args []string) error {
413415
return restoreErr
414416
}
415417

418+
func resolveConfiguredRestoreTarget(targets map[string]config.TargetConfig, requested string) (string, config.TargetConfig, error) {
419+
candidates := make([]string, 0, len(targets))
420+
for name := range targets {
421+
candidates = append(candidates, name)
422+
}
423+
424+
resolvedName, ok, err := config.ResolveTargetNameCandidate(requested, candidates)
425+
if err != nil {
426+
return "", config.TargetConfig{}, err
427+
}
428+
if !ok {
429+
return "", config.TargetConfig{}, nil
430+
}
431+
return resolvedName, targets[resolvedName], nil
432+
}
433+
416434
// restoreTUIDispatch handles the no-args TUI flow for restore.
417435
func restoreTUIDispatch(noTUI bool) error {
418436
cfg, err := config.Load()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"skillshare/internal/backup"
8+
"skillshare/internal/config"
9+
)
10+
11+
func TestCmdRestore_AliasRestoresCanonicalLatestBackup(t *testing.T) {
12+
home := setupGlobalResourceTestEnv(t)
13+
sourceDir := filepath.Join(t.TempDir(), "source")
14+
mustAddSkill(t, sourceDir, "alpha")
15+
16+
cfg := &config.Config{
17+
Source: sourceDir,
18+
Targets: map[string]config.TargetConfig{
19+
"universal": {Path: filepath.Join(home, ".agents", "skills")},
20+
},
21+
}
22+
if err := cfg.Save(); err != nil {
23+
t.Fatalf("save config: %v", err)
24+
}
25+
26+
mustWriteFile(t, filepath.Join(backup.BackupDir(), "2025-03-20_18-45-00", "universal", "alpha", "SKILL.md"), "# Alpha\n")
27+
28+
if err := cmdRestore([]string{"agents", "--force"}); err != nil {
29+
t.Fatalf("cmdRestore(alias latest) error = %v", err)
30+
}
31+
32+
assertFileContent(t, filepath.Join(home, ".agents", "skills", "alpha", "SKILL.md"), "# Alpha\n")
33+
}

cmd/skillshare/collect.go

Lines changed: 122 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,15 @@ func cmdCollect(args []string) error {
8282

8383
applyModeLabel(mode)
8484

85+
resources, rest, err := parseResourceFlags(rest, resourceFlagOptions{
86+
defaultSelection: resourceSelection{skills: true},
87+
})
88+
if err != nil {
89+
return err
90+
}
91+
8592
if mode == modeProject {
86-
err := cmdCollectProject(rest, cwd)
93+
err := cmdCollectProject(rest, cwd, resources)
8794
logCollectOp(config.ProjectConfigPath(cwd), start, err)
8895
return err
8996
}
@@ -116,6 +123,20 @@ func cmdCollect(args []string) error {
116123
force = true
117124
}
118125

126+
if resources.onlyManaged() {
127+
if targetName != "" || collectAll {
128+
return fmt.Errorf("target selection is only supported when collecting skills")
129+
}
130+
if jsonOutput {
131+
result, collectErr := collectManagedResources("", resources, dryRun, force)
132+
logCollectOp(config.ConfigPath(), start, collectErr)
133+
return collectOutputJSON(result, dryRun, start, collectErr)
134+
}
135+
err := executeManagedCollect("", resources, dryRun, force)
136+
logCollectOp(config.ConfigPath(), start, err)
137+
return err
138+
}
139+
119140
cfg, err := config.Load()
120141
if err != nil {
121142
if jsonOutput {
@@ -146,48 +167,83 @@ func cmdCollect(args []string) error {
146167
if sp != nil {
147168
sp.Success("No local skills found")
148169
}
149-
if jsonOutput {
150-
return collectOutputJSON(nil, dryRun, start, nil)
170+
if !resources.includesManaged() {
171+
if jsonOutput {
172+
return collectOutputJSON(nil, dryRun, start, nil)
173+
}
174+
return nil
151175
}
152-
return nil
153-
}
154-
155-
if sp != nil {
176+
} else if sp != nil {
156177
sp.Success(fmt.Sprintf("Found %d local skill(s)", len(allLocalSkills)))
157178
displayLocalSkills(allLocalSkills)
158179
}
159180

160181
if dryRun {
161182
if jsonOutput {
162-
names := make([]string, len(allLocalSkills))
163-
for i, s := range allLocalSkills {
164-
names[i] = s.Name
183+
var result *sync.PullResult
184+
if len(allLocalSkills) > 0 {
185+
names := make([]string, len(allLocalSkills))
186+
for i, s := range allLocalSkills {
187+
names[i] = s.Name
188+
}
189+
result = &sync.PullResult{Pulled: names, Failed: make(map[string]error)}
190+
}
191+
if resources.includesManaged() {
192+
managedResult, managedErr := collectManagedResources("", resources, true, force)
193+
result = mergePullResults(result, managedResult)
194+
logCollectOp(config.ConfigPath(), start, managedErr)
195+
return collectOutputJSON(result, true, start, managedErr)
165196
}
166197
logCollectOp(config.ConfigPath(), start, nil)
167-
return collectOutputJSON(&sync.PullResult{Pulled: names}, true, start, nil)
198+
return collectOutputJSON(result, true, start, nil)
199+
}
200+
if resources.includesManaged() {
201+
err := executeManagedCollect("", resources, true, force)
202+
logCollectOp(config.ConfigPath(), start, err)
203+
return err
168204
}
169205
ui.Info("Dry run - no changes made")
170206
return nil
171207
}
172208

173209
// Confirm unless --force (JSON implies force)
174-
if !force {
210+
if !force && len(allLocalSkills) > 0 {
175211
if !confirmCollect() {
176212
ui.Info("Cancelled")
177213
return nil
178214
}
179215
}
180216

181217
if jsonOutput {
182-
result, collectErr := sync.PullSkills(allLocalSkills, cfg.Source, sync.PullOptions{
183-
DryRun: dryRun,
184-
Force: force,
185-
})
186-
logCollectOp(config.ConfigPath(), start, collectErr)
187-
return collectOutputJSON(result, dryRun, start, collectErr)
218+
var result *sync.PullResult
219+
var collectErr error
220+
if len(allLocalSkills) > 0 {
221+
result, collectErr = sync.PullSkills(allLocalSkills, cfg.Source, sync.PullOptions{
222+
DryRun: dryRun,
223+
Force: force,
224+
})
225+
collectErr = combineCollectErrors(collectErr, collectResultError(result))
226+
}
227+
var managedErr error
228+
if resources.includesManaged() {
229+
var managedResult *sync.PullResult
230+
managedResult, managedErr = collectManagedResources("", resources, dryRun, force)
231+
result = mergePullResults(result, managedResult)
232+
}
233+
err = combineCollectErrors(collectErr, managedErr)
234+
logCollectOp(config.ConfigPath(), start, err)
235+
return collectOutputJSON(result, dryRun, start, err)
188236
}
189237

190-
err = executeCollect(allLocalSkills, cfg.Source, dryRun, force)
238+
var collectErr error
239+
if len(allLocalSkills) > 0 {
240+
collectErr = executeCollect(allLocalSkills, cfg.Source, dryRun, force)
241+
}
242+
var managedErr error
243+
if resources.includesManaged() {
244+
managedErr = executeManagedCollect("", resources, dryRun, force)
245+
}
246+
err = combineCollectErrors(collectErr, managedErr)
191247
logCollectOp(config.ConfigPath(), start, err)
192248
return err
193249
}
@@ -261,7 +317,7 @@ func executeCollect(skills []sync.LocalSkillInfo, source string, dryRun, force b
261317
showCollectNextSteps(source)
262318
}
263319

264-
return nil
320+
return collectResultError(result)
265321
}
266322

267323
// collectOutputJSON converts a collect result to JSON and writes to stdout.
@@ -290,12 +346,58 @@ func showCollectNextSteps(source string) {
290346
ui.Info("Run 'skillshare sync' to distribute to all targets")
291347

292348
// Check if source has git
349+
if strings.TrimSpace(source) == "" {
350+
return
351+
}
293352
gitDir := filepath.Join(source, ".git")
294353
if _, err := os.Stat(gitDir); err == nil {
295354
ui.Info("Commit changes: cd %s && git add . && git commit", source)
296355
}
297356
}
298357

358+
func mergePullResults(left, right *sync.PullResult) *sync.PullResult {
359+
switch {
360+
case left == nil:
361+
return right
362+
case right == nil:
363+
return left
364+
}
365+
366+
merged := &sync.PullResult{
367+
Pulled: append(append([]string{}, left.Pulled...), right.Pulled...),
368+
Skipped: append(append([]string{}, left.Skipped...), right.Skipped...),
369+
Failed: make(map[string]error, len(left.Failed)+len(right.Failed)),
370+
}
371+
for name, err := range left.Failed {
372+
merged.Failed[name] = err
373+
}
374+
for name, err := range right.Failed {
375+
merged.Failed[name] = err
376+
}
377+
return merged
378+
}
379+
380+
func collectResultError(result *sync.PullResult) error {
381+
if result == nil || len(result.Failed) == 0 {
382+
return nil
383+
}
384+
return fmt.Errorf("some skills failed to collect")
385+
}
386+
387+
func combineCollectErrors(errs ...error) error {
388+
messages := make([]string, 0, len(errs))
389+
for _, err := range errs {
390+
if err == nil {
391+
continue
392+
}
393+
messages = append(messages, err.Error())
394+
}
395+
if len(messages) == 0 {
396+
return nil
397+
}
398+
return fmt.Errorf("%s", strings.Join(messages, "; "))
399+
}
400+
299401
func printCollectHelp() {
300402
fmt.Println(`Usage: skillshare collect [target] [options]
301403

cmd/skillshare/collect_project.go

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"skillshare/internal/ui"
99
)
1010

11-
func cmdCollectProject(args []string, root string) error {
11+
func cmdCollectProject(args []string, root string, resources resourceSelection) error {
1212
dryRun := false
1313
force := false
1414
collectAll := false
@@ -29,6 +29,13 @@ func cmdCollectProject(args []string, root string) error {
2929
}
3030
}
3131

32+
if resources.onlyManaged() {
33+
if targetName != "" || collectAll {
34+
return fmt.Errorf("target selection is only supported when collecting skills")
35+
}
36+
return executeManagedCollect(root, resources, dryRun, force)
37+
}
38+
3239
runtime, err := loadProjectRuntime(root)
3340
if err != nil {
3441
return err
@@ -47,25 +54,42 @@ func cmdCollectProject(args []string, root string) error {
4754
allLocalSkills := collectLocalSkills(targets, runtime.sourcePath, "")
4855
if len(allLocalSkills) == 0 {
4956
sp.Success("No local skills found")
57+
if resources.includesManaged() {
58+
if dryRun {
59+
return executeManagedCollect(root, resources, true, force)
60+
}
61+
return executeManagedCollect(root, resources, false, force)
62+
}
5063
return nil
5164
}
5265
sp.Success(fmt.Sprintf("Found %d local skill(s)", len(allLocalSkills)))
5366

5467
displayLocalSkills(allLocalSkills)
5568

5669
if dryRun {
70+
if resources.includesManaged() {
71+
return executeManagedCollect(root, resources, true, force)
72+
}
5773
ui.Info("Dry run - no changes made")
5874
return nil
5975
}
6076

61-
if !force {
77+
if !force && len(allLocalSkills) > 0 {
6278
if !confirmCollect() {
6379
ui.Info("Cancelled")
6480
return nil
6581
}
6682
}
6783

68-
return executeCollect(allLocalSkills, runtime.sourcePath, dryRun, force)
84+
var collectErr error
85+
if len(allLocalSkills) > 0 {
86+
collectErr = executeCollect(allLocalSkills, runtime.sourcePath, dryRun, force)
87+
}
88+
var managedErr error
89+
if resources.includesManaged() {
90+
managedErr = executeManagedCollect(root, resources, dryRun, force)
91+
}
92+
return combineCollectErrors(collectErr, managedErr)
6993
}
7094

7195
func selectCollectProjectTargets(runtime *projectRuntime, targetName string, collectAll bool) (map[string]config.TargetConfig, error) {

cmd/skillshare/init_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func TestCommitSourceFiles_CommitFailureIsReturned(t *testing.T) {
3636
runGit(t, repo, "init")
3737
runGit(t, repo, "config", "user.email", "test@example.com")
3838
runGit(t, repo, "config", "user.name", "Test User")
39+
runGit(t, repo, "config", "core.hooksPath", ".git/hooks")
3940

4041
hookPath := filepath.Join(repo, ".git", "hooks", "pre-commit")
4142
if err := os.WriteFile(hookPath, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil {

0 commit comments

Comments
 (0)