-
Notifications
You must be signed in to change notification settings - Fork 5
feat: add skill install/update command #164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cf710ed
f539928
943c8eb
c7e179d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| #!/bin/bash | ||
|
|
||
| # E2E for `fc3 skill install/update`. | ||
| # Fully sandboxed and offline: installs the bundled s-fc3 skill into temp | ||
| # project/home roots and asserts the files land where each tool expects them. | ||
| # Needs NO cloud credentials. | ||
| # | ||
| # The local component build is loaded through a minimal s.yaml that points at | ||
| # the repo root (component: <fc3_dir>) and is driven with `s skill ...`. We do | ||
| # NOT use `s cli <abs-path> ...`: on Windows the CLI joins that absolute path | ||
| # into its per-run log directory, which breaks when cwd and the repo live on | ||
| # different drives. The s.yaml path mirrors how every other e2e drives the CLI. | ||
|
|
||
| set -e | ||
| set -v | ||
|
|
||
| current_dir="$PWD" | ||
| fc3_dir=$(dirname $(dirname $(dirname "$current_dir"))) | ||
| echo "fc3 root dir: $fc3_dir" | ||
|
|
||
| TOOLS="claude codex cursor qoder agents" | ||
|
|
||
| # Sandbox roots; cleaned up on exit. | ||
| PROJECT_ROOT=$(mktemp -d) | ||
| HOME_ROOT=$(mktemp -d) | ||
| WORK_ROOT=$(mktemp -d) | ||
| FILTER_ROOT=$(mktemp -d) | ||
| cleanup() { | ||
| rm -rf "$PROJECT_ROOT" "$HOME_ROOT" "$WORK_ROOT" "$FILTER_ROOT" | ||
| } | ||
| trap cleanup EXIT | ||
|
|
||
| # write_syaml <dir> — drop a minimal offline s.yaml that loads the local build. | ||
| write_syaml() { | ||
| cat > "$1/s.yaml" <<YAML | ||
| edition: 3.0.0 | ||
| name: skill-e2e | ||
| resources: | ||
| fc3: | ||
| component: '$fc3_dir' | ||
| props: {} | ||
| YAML | ||
| } | ||
|
|
||
| # assert_file <path> — fail the script if the file is missing. | ||
| assert_file() { | ||
| if [ ! -f "$1" ]; then | ||
| echo "ASSERT FAILED: expected file not found: $1" | ||
| exit 1 | ||
| fi | ||
| echo "OK: $1" | ||
| } | ||
|
|
||
| assert_missing() { | ||
| if [ -e "$1" ]; then | ||
| echo "ASSERT FAILED: expected path to be gone: $1" | ||
| exit 1 | ||
| fi | ||
| echo "OK (absent): $1" | ||
| } | ||
|
|
||
| skill_path() { | ||
| # <root> <toolDir> | ||
| echo "$1/.$2/skills/s-fc3/SKILL.md" | ||
| } | ||
|
|
||
| write_syaml "$PROJECT_ROOT" | ||
| write_syaml "$WORK_ROOT" | ||
| write_syaml "$FILTER_ROOT" | ||
|
|
||
| echo "=== project-scope install (all tools) ===" | ||
| cd "$PROJECT_ROOT" | ||
| s skill install --project | ||
| for t in $TOOLS; do | ||
| assert_file "$(skill_path "$PROJECT_ROOT" "$t")" | ||
| done | ||
|
|
||
| echo "=== install is idempotent: existing target is skipped, local edits preserved ===" | ||
| MARKER="$PROJECT_ROOT/.claude/skills/s-fc3/LOCAL_MARKER" | ||
| echo "keep-me" > "$MARKER" | ||
| s skill install --project --tools claude | ||
| assert_file "$MARKER" | ||
|
|
||
| echo "=== --force overwrites and cleans stale files ===" | ||
| s skill install --project --tools claude --force | ||
| assert_missing "$MARKER" | ||
| assert_file "$(skill_path "$PROJECT_ROOT" claude)" | ||
|
|
||
| echo "=== update overwrites existing installations ===" | ||
| echo "stale" > "$MARKER" | ||
| s skill update --project --tools claude | ||
| assert_missing "$MARKER" | ||
| assert_file "$(skill_path "$PROJECT_ROOT" claude)" | ||
|
|
||
| echo "=== --tools filter installs only the requested tools ===" | ||
| cd "$FILTER_ROOT" | ||
| s skill install --project --tools codex | ||
| assert_file "$(skill_path "$FILTER_ROOT" codex)" | ||
| assert_missing "$FILTER_ROOT/.cursor/skills/s-fc3" | ||
|
|
||
| echo "=== global-scope install writes under the (sandboxed) home directory ===" | ||
| cd "$WORK_ROOT" | ||
| HOME="$HOME_ROOT" s skill install --global --tools claude,codex | ||
| assert_file "$(skill_path "$HOME_ROOT" claude)" | ||
| assert_file "$(skill_path "$HOME_ROOT" codex)" | ||
|
|
||
| echo "=== skill e2e passed ===" |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,117 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # E2E for `fc3 skill install/update` on Windows. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fully sandboxed and offline; needs NO cloud credentials. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The local component build is loaded through a minimal s.yaml that points at | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # the repo root (component: <fc3_dir>) and is driven with `s skill ...`. We do | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # NOT use `s cli <abs-path> ...`: on Windows the CLI joins that absolute path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # into its per-run log directory, which breaks when cwd (the temp sandbox on | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # C:) and the repo (on D:) live on different drives. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $ErrorActionPreference = "Stop" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $current_dir = $PWD.Path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $fc3_dir = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $current_dir)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "fc3 root dir: $fc3_dir" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $tools = @("claude", "codex", "cursor", "qoder", "agents") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $projectRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("fc3-skill-proj-" + [System.Guid]::NewGuid().ToString("N")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $homeRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("fc3-skill-home-" + [System.Guid]::NewGuid().ToString("N")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $workRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("fc3-skill-work-" + [System.Guid]::NewGuid().ToString("N")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $filterRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("fc3-skill-filter-" + [System.Guid]::NewGuid().ToString("N")) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| New-Item -ItemType Directory -Force -Path $projectRoot | Out-Null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| New-Item -ItemType Directory -Force -Path $homeRoot | Out-Null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| New-Item -ItemType Directory -Force -Path $workRoot | Out-Null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| New-Item -ItemType Directory -Force -Path $filterRoot | Out-Null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Drop a minimal offline s.yaml that loads the local build. The component path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # is single-quoted so backslashes stay literal in YAML. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function Write-SYaml($dir) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $yaml = @" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| edition: 3.0.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| name: skill-e2e | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| resources: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fc3: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| component: '$fc3_dir' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| props: {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Set-Content -Path (Join-Path $dir "s.yaml") -Value $yaml -Encoding utf8 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+38
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Escape A single quote in a valid Windows path is not escaped. For example, Proposed fix function Write-SYaml($dir) {
+ $yamlPath = $fc3_dir -replace "'", "''"
$yaml = @"
...
- component: '$fc3_dir'
+ component: '$yamlPath'
...📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function Assert-File($p) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (-not (Test-Path -PathType Leaf $p)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "ASSERT FAILED: expected file not found: $p" -ForegroundColor Red | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "OK: $p" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function Assert-Missing($p) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Test-Path $p) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "ASSERT FAILED: expected path to be gone: $p" -ForegroundColor Red | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exit 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "OK (absent): $p" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function Skill-Path($root, $tool) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (Join-Path $root ".$tool\skills\s-fc3\SKILL.md") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-SYaml $projectRoot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-SYaml $workRoot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-SYaml $filterRoot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "=== project-scope install (all tools) ===" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Set-Location $projectRoot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s skill install --project | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| foreach ($t in $tools) { Assert-File (Skill-Path $projectRoot $t) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "=== install is idempotent: existing target is skipped ===" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $marker = Join-Path $projectRoot ".claude\skills\s-fc3\LOCAL_MARKER" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Set-Content -Path $marker -Value "keep-me" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s skill install --project --tools claude | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert-File $marker | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "=== --force overwrites and cleans stale files ===" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s skill install --project --tools claude --force | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert-Missing $marker | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert-File (Skill-Path $projectRoot "claude") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "=== update overwrites existing installations ===" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Set-Content -Path $marker -Value "stale" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s skill update --project --tools claude | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert-Missing $marker | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert-File (Skill-Path $projectRoot "claude") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "=== --tools filter installs only the requested tools ===" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Set-Location $filterRoot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s skill install --project --tools codex | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert-File (Skill-Path $filterRoot "codex") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert-Missing (Join-Path $filterRoot ".cursor\skills\s-fc3") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+88
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Assert that every non-selected tool is absent. The test proves only that Proposed assertion loop- Assert-Missing (Join-Path $filterRoot ".cursor\skills\s-fc3")
+ foreach ($t in $tools) {
+ if ($t -ne "codex") {
+ Assert-Missing (Join-Path $filterRoot ".$t\skills\s-fc3")
+ }
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "=== global-scope install writes under the sandboxed home directory ===" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Set-Location $workRoot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $oldHome = $env:USERPROFILE | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $env:USERPROFILE = $homeRoot | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| s skill install --global --tools "claude,codex" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $env:USERPROFILE = $oldHome | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert-File (Skill-Path $homeRoot "claude") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Assert-File (Skill-Path $homeRoot "codex") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Write-Host "=== skill e2e passed ===" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Restore cwd to where this script started (the `skill` dir) so the caller's | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # `cd ..` returns to the e2e root. Restoring to $fc3_dir instead would leave | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # the caller one level too high. $current_dir is outside the temp sandboxes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # so it is safe to sit here while they are removed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Set-Location $current_dir | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Remove-Item -Recurse -Force $projectRoot -ErrorAction SilentlyContinue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Remove-Item -Recurse -Force $homeRoot -ErrorAction SilentlyContinue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Remove-Item -Recurse -Force $workRoot -ErrorAction SilentlyContinue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Remove-Item -Recurse -Force $filterRoot -ErrorAction SilentlyContinue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.