Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## [0.18.9] - 2026-04-07

### New Features

- **Relative symlinks in project mode** — `skillshare sync -p` now creates relative symlinks (e.g., `../../.skillshare/skills/my-skill`) instead of absolute paths. This makes the project directory portable — rename it, move it, or clone it on another machine and all skill symlinks continue to work. Global mode continues to use absolute paths. Existing absolute symlinks are automatically upgraded to relative on the next sync

### Bug Fixes

- **Status version detection** — `skillshare status` no longer reports `! Skill: not found or missing version` when the version is stored under `metadata.version` in the SKILL.md frontmatter. Previously, the `status` command used its own local parser that only checked for a top-level `version:` key, while `doctor` (fixed in v0.18.7) and `upgrade` already used the correct shared parser

## [0.18.8] - 2026-04-06

### Bug Fixes
Expand Down
11 changes: 5 additions & 6 deletions ai_docs/tests/auto_create_target_dir_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Ref: GitHub Issue #87

- Sync auto-creates target `skills/` directory when parent (e.g., `~/.claude/`) exists
- Sync fails fast with typo hint when parent directory is missing
- Dry-run shows "Would create" without actually creating
- Dry-run shows "Will create" without actually creating
- Copy mode also auto-creates
- Notification message is visible in output

Expand Down Expand Up @@ -79,10 +79,9 @@ entire path tree may not exist yet.
```bash
rm -rf ~/.newcli 2>/dev/null || true
test -d ~/.newcli && echo "parent exists" || echo "parent missing"
cat >> ~/.config/skillshare/config.yaml << 'CFG'
newcli:
path: ~/.newcli/ai/skills
CFG
# Insert newcli target under the targets: map (sed inserts after the 'targets:' line)
CONFIG="$HOME/.config/skillshare/config.yaml"
sed -i '/^targets:/a\ newcli:\n skills:\n path: ~/.newcli/ai/skills' "$CONFIG"
ss sync -g 2>&1
test -d ~/.newcli/ai/skills && echo "deep dir created" || echo "deep dir missing"
```
Expand All @@ -105,7 +104,7 @@ test -d ~/.codex/skills && echo "dir created" || echo "dir not created"
Expected:
- exit_code: 0
- codex parent exists
- Would create target directory:
- Will create target directory:
- dir not created

## Pass Criteria
Expand Down
18 changes: 9 additions & 9 deletions ai_docs/tests/extras_flatten_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Run inside devcontainer.

```bash
ss extras remove agents --force -g >/dev/null 2>&1 || true
rm -rf ~/.claude/agents 2>/dev/null || true
rm -rf ~/.claude/agents ~/.config/skillshare/extras/agents 2>/dev/null || true
mkdir -p ~/.config/skillshare/extras/agents/curriculum
mkdir -p ~/.config/skillshare/extras/agents/software
echo "# Tactician" > ~/.config/skillshare/extras/agents/curriculum/tactician.md
Expand All @@ -45,7 +45,7 @@ Expected:

```bash
ss extras remove agents --force -g >/dev/null 2>&1 || true
rm -rf ~/.claude/agents 2>/dev/null || true
rm -rf ~/.claude/agents ~/.config/skillshare/extras/agents 2>/dev/null || true
mkdir -p ~/.config/skillshare/extras/agents/sub1 ~/.config/skillshare/extras/agents/sub2
echo "a" > ~/.config/skillshare/extras/agents/sub1/a.md
echo "b" > ~/.config/skillshare/extras/agents/sub2/b.md
Expand All @@ -66,7 +66,7 @@ Expected:

```bash
ss extras remove agents --force -g >/dev/null 2>&1 || true
rm -rf ~/.claude/agents 2>/dev/null || true
rm -rf ~/.claude/agents ~/.config/skillshare/extras/agents 2>/dev/null || true
mkdir -p ~/.config/skillshare/extras/agents
echo "x" > ~/.config/skillshare/extras/agents/x.md
mkdir -p ~/.claude/agents
Expand All @@ -82,7 +82,7 @@ Expected:

```bash
ss extras remove agents --force -g >/dev/null 2>&1 || true
rm -rf ~/.claude/agents 2>/dev/null || true
rm -rf ~/.claude/agents ~/.config/skillshare/extras/agents 2>/dev/null || true
mkdir -p ~/.config/skillshare/extras/agents/team-a
mkdir -p ~/.config/skillshare/extras/agents/team-b
echo "# From team-a" > ~/.config/skillshare/extras/agents/team-a/agent.md
Expand All @@ -102,7 +102,7 @@ Expected:

```bash
ss extras remove agents --force -g >/dev/null 2>&1 || true
rm -rf ~/.claude/agents 2>/dev/null || true
rm -rf ~/.claude/agents ~/.config/skillshare/extras/agents 2>/dev/null || true
mkdir -p ~/.config/skillshare/extras/agents/a ~/.config/skillshare/extras/agents/b
echo "1" > ~/.config/skillshare/extras/agents/a/same.md
echo "2" > ~/.config/skillshare/extras/agents/b/same.md
Expand All @@ -119,7 +119,7 @@ Expected:

```bash
ss extras remove agents --force -g >/dev/null 2>&1 || true
rm -rf ~/.claude/agents 2>/dev/null || true
rm -rf ~/.claude/agents ~/.config/skillshare/extras/agents 2>/dev/null || true
mkdir -p ~/.config/skillshare/extras/agents
echo "x" > ~/.config/skillshare/extras/agents/x.md
mkdir -p ~/.claude/agents
Expand All @@ -136,7 +136,7 @@ Expected:

```bash
ss extras remove agents --force -g >/dev/null 2>&1 || true
rm -rf ~/.claude/agents 2>/dev/null || true
rm -rf ~/.claude/agents ~/.config/skillshare/extras/agents 2>/dev/null || true
mkdir -p ~/.config/skillshare/extras/agents
echo "x" > ~/.config/skillshare/extras/agents/x.md
mkdir -p ~/.claude/agents
Expand All @@ -153,7 +153,7 @@ Expected:

```bash
ss extras remove agents --force -g >/dev/null 2>&1 || true
rm -rf ~/.claude/agents 2>/dev/null || true
rm -rf ~/.claude/agents ~/.config/skillshare/extras/agents 2>/dev/null || true
mkdir -p ~/.config/skillshare/extras/agents
echo "x" > ~/.config/skillshare/extras/agents/x.md
mkdir -p ~/.claude/agents
Expand All @@ -168,7 +168,7 @@ Expected:

```bash
ss extras remove agents --force -g >/dev/null 2>&1 || true
rm -rf ~/.claude/agents 2>/dev/null || true
rm -rf ~/.claude/agents ~/.config/skillshare/extras/agents 2>/dev/null || true
mkdir -p ~/.config/skillshare/extras/agents/sub
echo "# Keep" > ~/.config/skillshare/extras/agents/sub/keep.md
echo "# Remove" > ~/.config/skillshare/extras/agents/sub/remove.md
Expand Down
4 changes: 2 additions & 2 deletions ai_docs/tests/mdproof.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"build": "cd /workspace && make build && /workspace/.devcontainer/ensure-skillshare-linux-binary.sh && /workspace/.devcontainer/ensure-mdproof.sh",
"isolation": "per-runbook",
"step_setup": "cd $HOME",
"setup": "rm -rf /tmp/test-project /tmp/test-proj /tmp/extras-proj /tmp/copy-target /tmp/sym-target /tmp/test /tmp/bad-config.yaml /tmp/config-before.yaml 2>/dev/null; mkdir -p ~/.config ~/.local/share ~/.local/state ~/.cache ~/.claude ~/.codex; /workspace/bin/skillshare init -g --force --all-targets --no-git --no-skill",
"workdir": "$HOME",
"setup": "rm -rf /tmp/test-project /tmp/test-proj /tmp/extras-proj /tmp/copy-target /tmp/sym-target /tmp/test /tmp/bad-config.yaml /tmp/config-before.yaml 2>/dev/null; mkdir -p ~/.config ~/.local/share ~/.local/state ~/.cache ~/.claude ~/.codex; /workspace/bin/skillshare init -g --force --all-targets --no-git --no-skill 2>/dev/null || true",
"teardown": "/workspace/bin/skillshare uninstall --all -g --force 2>/dev/null || true",
"timeout": "5m"
}
6 changes: 3 additions & 3 deletions ai_docs/tests/registry_yaml_split_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ ss install /tmp/test-skill

```bash
cat ~/.config/skillshare/skills/registry.yaml
grep -c "skills:" ~/.config/skillshare/config.yaml && echo "FAIL: config has skills" || echo "PASS: config clean"
grep -c "^skills:" ~/.config/skillshare/config.yaml && echo "FAIL: config has skills" || echo "PASS: config clean"
```

**Expected:**
Expand Down Expand Up @@ -81,7 +81,7 @@ ss status

```bash
cat ~/.config/skillshare/skills/registry.yaml
grep -c "skills:" ~/.config/skillshare/config.yaml && echo "FAIL: skills still in config" || echo "PASS: migration ok"
grep -c "^skills:" ~/.config/skillshare/config.yaml && echo "FAIL: skills still in config" || echo "PASS: migration ok"
```

**Expected:**
Expand Down Expand Up @@ -197,7 +197,7 @@ SKILLSHARE_DEV_ALLOW_WORKSPACE_PROJECT=1 ss install /tmp/proj-skill -p

```bash
cat /tmp/project-test/.skillshare/registry.yaml
grep -c "skills:" /tmp/project-test/.skillshare/config.yaml && echo "FAIL: config has skills" || echo "PASS"
grep -c "^skills:" /tmp/project-test/.skillshare/config.yaml && echo "FAIL: config has skills" || echo "PASS"
```

**Expected:**
Expand Down
160 changes: 160 additions & 0 deletions ai_docs/tests/relative_symlinks_runbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Relative Symlinks E2E Test

Validates that project-mode sync creates relative symlinks (committable to git),
while global-mode sync continues to create absolute symlinks.

## Scope

- `internal/sync/sync.go` — `SyncTargetMergeWithSkills` passes `projectRoot`
- `internal/sync/symlink_unix.go` — `createLink` computes relative path
- `internal/sync/relative.go` — `shouldUseRelative` determines mode
- `internal/sync/extras.go` — extras use `createLink` with relative support

## Environment

- Devcontainer with `ssenv` isolation
- Pre-initialized via `ssenv create --init`

## Steps

### Step 1: Create project with skill and sync

```bash
rm -rf /tmp/reltest 2>/dev/null
mkdir -p /tmp/reltest/.skillshare/skills/demo
printf '%s\n' '---' 'name: demo' '---' '# Demo skill' > /tmp/reltest/.skillshare/skills/demo/SKILL.md
printf '%s\n' 'targets:' ' - claude' > /tmp/reltest/.skillshare/config.yaml
cd /tmp/reltest && ss sync -p
```

Expected:
- exit_code: 0
- 1 linked

### Step 2: Verify project-mode symlink is relative

```bash
readlink /tmp/reltest/.claude/skills/demo
```

Expected:
- exit_code: 0
- regex: ^\.\.

### Step 3: Verify content is accessible through relative symlink

```bash
cat /tmp/reltest/.claude/skills/demo/SKILL.md
```

Expected:
- exit_code: 0
- Demo skill

### Step 4: Verify target outside project root falls back to absolute

```bash
rm -rf /tmp/reltest-outside /tmp/outside-target 2>/dev/null
mkdir -p /tmp/reltest-outside/.skillshare/skills/ext
printf '%s\n' '---' 'name: ext' '---' '# External' > /tmp/reltest-outside/.skillshare/skills/ext/SKILL.md
mkdir -p /tmp/outside-target
printf '%s\n' 'targets:' ' - name: custom' ' skills:' ' path: /tmp/outside-target' > /tmp/reltest-outside/.skillshare/config.yaml
cd /tmp/reltest-outside && ss sync -p
readlink /tmp/outside-target/ext
```

Expected:
- exit_code: 0
- regex: ^/

### Step 5: Project-mode sync JSON output

```bash
cd /tmp/reltest && ss sync -p --json
```

Expected:
- exit_code: 0
- jq: .details | length == 1
- jq: .details[0].name == "claude"

### Step 6: Project-mode with multiple targets

```bash
rm -rf /tmp/reltest2 2>/dev/null
mkdir -p /tmp/reltest2/.skillshare/skills/multi
printf '%s\n' '---' 'name: multi' '---' '# Multi' > /tmp/reltest2/.skillshare/skills/multi/SKILL.md
printf '%s\n' 'targets:' ' - claude' ' - cursor' > /tmp/reltest2/.skillshare/config.yaml
cd /tmp/reltest2 && ss sync -p
CLAUDE_LINK=$(readlink .claude/skills/multi 2>/dev/null)
CURSOR_LINK=$(readlink .cursor/skills/multi 2>/dev/null)
echo "claude=$CLAUDE_LINK"
echo "cursor=$CURSOR_LINK"
```

Expected:
- exit_code: 0
- regex: claude=\.\.
- regex: cursor=\.\.

### Step 7: Relative symlink resolves correctly

```bash
cd /tmp/reltest
RESOLVED=$(readlink -f .claude/skills/demo)
EXPECTED=$(readlink -f .skillshare/skills/demo)
echo "resolved=$RESOLVED"
echo "expected=$EXPECTED"
test "$RESOLVED" = "$EXPECTED" && echo "MATCH" || echo "MISMATCH"
```

Expected:
- exit_code: 0
- MATCH

### Step 8: Re-sync is idempotent

```bash
cd /tmp/reltest && ss sync -p
LINK=$(readlink .claude/skills/demo)
echo "link=$LINK"
```

Expected:
- exit_code: 0
- regex: link=\.\.
- 1 linked

### Step 9: Symlink mode produces relative symlink

```bash
rm -rf /tmp/reltest-sym 2>/dev/null
mkdir -p /tmp/reltest-sym/.skillshare/skills/sym-skill
printf '%s\n' '---' 'name: sym-skill' '---' '# Symlink mode' > /tmp/reltest-sym/.skillshare/skills/sym-skill/SKILL.md
printf '%s\n' 'targets:' ' - name: claude' ' skills:' ' mode: symlink' > /tmp/reltest-sym/.skillshare/config.yaml
cd /tmp/reltest-sym && ss sync -p
readlink .claude/skills
```

Expected:
- exit_code: 0
- regex: ^\.\.

### Step 10: Cleanup

```bash
rm -rf /tmp/reltest /tmp/reltest2 /tmp/reltest-outside /tmp/outside-target /tmp/reltest-sym 2>/dev/null
echo "cleanup done"
```

Expected:
- exit_code: 0
- cleanup done

## Pass Criteria

- All steps pass
- Project-mode symlinks are relative (start with `..`)
- Global-mode symlinks are absolute (start with `/`)
- Content accessible through relative symlinks
- Multiple targets all get relative symlinks
6 changes: 3 additions & 3 deletions ai_docs/tests/skillignore_source_discovery_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ Expected:
### Step 9: Verify sync only creates symlinks for non-ignored skills

```bash
ss sync -g 2>/dev/null
ss sync -g >/dev/null 2>&1
ls ~/.claude/skills/ | grep team-skills | sort
```

Expand Down Expand Up @@ -256,7 +256,7 @@ Expected:
### Step 13: Verify root .skillignore hides matching skills from list

```bash
ss list --json -g | jq -r '[.[] | select(.name | test("draft")) | .name] | length'
ss list --json -g | jq -r '[.[] | select(.name | test("draft")) | select(.disabled != true) | .name] | length'
```

Expected:
Expand All @@ -270,7 +270,7 @@ SOURCE=~/.config/skillshare/skills

printf "draft-*\n_team-skills\n" > "$SOURCE/.skillignore"

ss list --json -g | jq -r '[.[] | select(.name | test("_team-skills")) | .name] | length'
ss list --json -g | jq -r '[.[] | select(.name | test("_team-skills")) | select(.disabled != true) | .name] | length'
```

Expected:
Expand Down
4 changes: 2 additions & 2 deletions ai_docs/tests/target_naming_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ Expected:
```bash
OUTPUT=$(ss sync -g 2>&1)
printf '%s\n' "$OUTPUT"
echo "$OUTPUT" | grep -q "Target 'merge-standard': skipped frontend/bad because" && echo "MERGE_INVALID_WARN=OK" || echo "MERGE_INVALID_WARN=FAIL"
echo "$OUTPUT" | grep -q "Target 'copy-standard': skipped frontend/bad because" && echo "COPY_INVALID_WARN=OK" || echo "COPY_INVALID_WARN=FAIL"
echo "$OUTPUT" | grep -q "skill(s) skipped (naming validation)" && echo "MERGE_INVALID_WARN=OK" || echo "MERGE_INVALID_WARN=FAIL"
echo "$OUTPUT" | grep -q "skill(s) skipped (naming validation)" && echo "COPY_INVALID_WARN=OK" || echo "COPY_INVALID_WARN=FAIL"
echo "$OUTPUT" | grep -q "duplicate skill names" && echo "MERGE_COLLISION_WARN=OK" || echo "MERGE_COLLISION_WARN=FAIL"
echo "$OUTPUT" | grep -q "dev" && echo "COPY_COLLISION_WARN=OK" || echo "COPY_COLLISION_WARN=FAIL"
```
Expand Down
9 changes: 9 additions & 0 deletions ai_docs/tests/ui_base_path_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ Server uses port **19421** to avoid conflicts with existing UI on 19420.
## Step 0: Setup isolated HOME and fake UI dist

```bash
# Kill any leftover servers from previous runs on ports used by this runbook
fuser -k 19421/tcp 2>/dev/null || true
fuser -k 19422/tcp 2>/dev/null || true
sleep 1

export E2E_HOME="/tmp/ss-e2e-basepath"
rm -rf "$E2E_HOME"
mkdir -p "$E2E_HOME/.config/skillshare/skills/demo"
Expand Down Expand Up @@ -218,6 +223,8 @@ export XDG_STATE_HOME="$E2E_HOME/.local/state"
export XDG_CACHE_HOME="$E2E_HOME/.cache"
export SKILLSHARE_UI_BASE_PATH="/from-env"

# Kill any leftover servers from previous steps or runs
fuser -k 19421/tcp 2>/dev/null || true
fuser -k 19422/tcp 2>/dev/null || true
sleep 1

Expand All @@ -238,8 +245,10 @@ Expected:
## Step 13: Final cleanup

```bash
# Ensure all servers from this runbook are stopped
fuser -k 19421/tcp 2>/dev/null || true
fuser -k 19422/tcp 2>/dev/null || true
sleep 1
rm -rf /tmp/ss-e2e-basepath /tmp/basepath-server.log /tmp/basepath-env.log
echo "cleanup_done=yes"
```
Expand Down
Loading
Loading