diff --git a/build/homebrew.rb.tmpl b/build/homebrew.rb.tmpl index 5abe1e08..2efea276 100644 --- a/build/homebrew.rb.tmpl +++ b/build/homebrew.rb.tmpl @@ -52,6 +52,7 @@ __KEG_ONLY_LINE__ Pathname.new(File.join(Dir.home, ".amp/skills/dws")), Pathname.new(File.join(Dir.home, ".kiro/skills/dws")), Pathname.new(File.join(Dir.home, ".trae/skills/dws")), + Pathname.new(File.join(Dir.home, ".openclaw/skills/dws")), ] targets.each_with_index do |dest, index| diff --git a/build/npm/install.js b/build/npm/install.js index ed1bf0fe..4243e837 100755 --- a/build/npm/install.js +++ b/build/npm/install.js @@ -7,6 +7,7 @@ const os = require("os"); const path = require("path"); const childProcess = require("child_process"); +// Canonical list: keep scripts/install.sh, scripts/install.ps1, scripts/install-skills.sh in sync. const AGENT_DIRS = [ ".agents/skills", ".claude/skills", @@ -20,6 +21,7 @@ const AGENT_DIRS = [ ".amp/skills", ".kiro/skills", ".trae/skills", + ".openclaw/skills", ]; const PLATFORM_MAP = { diff --git a/internal/upgrade/paths.go b/internal/upgrade/paths.go index 9bbe0678..f56e5f92 100644 --- a/internal/upgrade/paths.go +++ b/internal/upgrade/paths.go @@ -35,6 +35,7 @@ var knownSkillDirs = []string{ ".amp/skills", ".kiro/skills", ".trae/skills", + ".openclaw/skills", } // skillDirBlacklist contains parent directories whose skills are managed by diff --git a/scripts/install-skills.sh b/scripts/install-skills.sh index 05604bae..0fc43fb5 100755 --- a/scripts/install-skills.sh +++ b/scripts/install-skills.sh @@ -1,21 +1,23 @@ #!/bin/sh set -eu -# Install DWS agent skills from GitHub into detected agent directories. +# Install DWS agent skills from GitHub Releases into agent skill directories. # Usage: # curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace-cli/main/scripts/install-skills.sh | sh # -# The script downloads the dws-skills.zip release asset from GitHub Releases -# and copies it into every detected agent skills directory in the current -# project. +# Downloads dws-skills.zip from GitHub Releases and copies it under each target +# path using the same rules as build/npm/install.js installSkillsToHomes +# (AGENT_DIRS + parent-directory gate), with root defaulting to the current +# directory. Set DWS_SKILLS_ROOT=$HOME to match npm install layout exactly. +# +# Environment variables (optional): +# DWS_VERSION — release tag (default: latest) +# DWS_SKILLS_ROOT — base path for agent dirs (default: $PWD) REPO="DingTalk-Real-AI/dingtalk-workspace-cli" VERSION="${DWS_VERSION:-latest}" SKILL_NAME="dws" - -# ── Agent directory to install skills into ─────────────────────────────────── -# Only install to .agents/skills — most agents can fall back to this directory. -AGENT_DIR=".agents/skills" +ROOT="${DWS_SKILLS_ROOT:-$PWD}" # ── Helpers ────────────────────────────────────────────────────────────────── @@ -51,14 +53,107 @@ extract_zip() { exit 1 } +# One-line summary copy (2nd+ targets). +_copy_skill_summary() { + _src="$1" + _dest="$2" + _label="$3" + + if [ -d "$_dest" ]; then + rm -rf "$_dest" + fi + + mkdir -p "$_dest" + cp -R "$_src/"* "$_dest/" 2>/dev/null || cp -r "$_src/"* "$_dest/" + file_count="$(find "$_dest" -type f | wc -l | tr -d ' ')" + + printf ' ✅ Skills → %s (%s files)\n' "$_label" "$file_count" +} + +# Full copy with top-level listing (1st target). +_copy_skill() { + _src="$1" + _dest="$2" + _label="$3" + + if [ -d "$_dest" ]; then + rm -rf "$_dest" + fi + + mkdir -p "$_dest" + cp -R "$_src/"* "$_dest/" 2>/dev/null || cp -r "$_src/"* "$_dest/" + file_count="$(find "$_dest" -type f | wc -l | tr -d ' ')" + + printf ' ✅ Skills → %s (%s files)\n' "$_label" "$file_count" + + for entry in "$_dest"/*; do + entry_name="$(basename "$entry")" + if [ -d "$entry" ]; then + sub_count="$(find "$entry" -type f | wc -l | tr -d ' ')" + printf ' 📁 %s/ (%s files)\n' "$entry_name" "$sub_count" + else + printf ' 📄 %s\n' "$entry_name" + fi + done +} + +# Same semantics as build/npm/install.js installSkillsToHomes (root = DWS_SKILLS_ROOT or PWD). +install_skills_to_root() { + skill_src="$1" + root="$2" + installed=0 + idx=0 + for agent_dir in \ + ".agents/skills" \ + ".claude/skills" \ + ".cursor/skills" \ + ".gemini/skills" \ + ".codex/skills" \ + ".github/skills" \ + ".windsurf/skills" \ + ".augment/skills" \ + ".cline/skills" \ + ".amp/skills" \ + ".kiro/skills" \ + ".trae/skills" \ + ".openclaw/skills" + do + base_dir="$root/$agent_dir" + parent_gate="$(dirname "$base_dir")" + if [ "$idx" -gt 0 ] && [ ! -e "$parent_gate" ]; then + idx=$((idx + 1)) + continue + fi + dest="$base_dir/$SKILL_NAME" + if [ "$root" = "$HOME" ]; then + label="~/$agent_dir/$SKILL_NAME" + else + label="$root/$agent_dir/$SKILL_NAME" + fi + if [ "$installed" -eq 0 ]; then + _copy_skill "$skill_src" "$dest" "$label" + else + _copy_skill_summary "$skill_src" "$dest" "$label" + fi + installed=$((installed + 1)) + idx=$((idx + 1)) + done + if [ "$installed" -eq 0 ]; then + if [ "$root" = "$HOME" ]; then + flabel="~/.agents/skills/$SKILL_NAME" + else + flabel="$root/.agents/skills/$SKILL_NAME" + fi + _copy_skill "$skill_src" "$root/.agents/skills/$SKILL_NAME" "$flabel" + fi +} + # ── Main ───────────────────────────────────────────────────────────────────── main() { need_cmd curl resolve_version - CWD="$(pwd)" - printf '\n' printf ' ┌──────────────────────────────────────┐\n' printf ' │ DWS Skill Installer │\n' @@ -66,7 +161,6 @@ main() { printf ' └──────────────────────────────────────┘\n' printf '\n' - # Download the tarball to a temp directory TMPDIR_WORK="$(mktemp -d)" trap 'rm -rf "$TMPDIR_WORK"' EXIT INT TERM @@ -85,21 +179,9 @@ main() { exit 1 fi - # Install to .agents/skills only - dest="$CWD/$AGENT_DIR/$SKILL_NAME" - - # Remove existing installation - if [ -d "$dest" ]; then - rm -rf "$dest" - fi - - # Copy skill files - mkdir -p "$dest" - cp -R "$SKILL_SRC/"* "$dest/" - file_count="$(find "$dest" -type f | wc -l | tr -d ' ')" - - printf ' ✅ Universal (.agents)\n' - printf ' → %s/%s (%s files)\n' "$AGENT_DIR" "$SKILL_NAME" "$file_count" + printf '\n' + printf ' Installing under root: %s\n' "$ROOT" + install_skills_to_root "$SKILL_SRC" "$ROOT" printf '\n' printf ' 📖 Skill includes:\n' diff --git a/scripts/install.ps1 b/scripts/install.ps1 index d98a69ab..2bbdd836 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -17,6 +17,8 @@ # DWS_ARCH — architecture override (amd64 or arm64) # DWS_NO_SKILLS — set to 1 to skip skills install # DWS_SKILLS_ONLY — set to 1 to install only skills +# +# Agent skills paths follow build/npm/install.js AGENT_DIRS (order and entries must match). $ErrorActionPreference = "Stop" @@ -28,8 +30,22 @@ $NoSkills = $env:DWS_NO_SKILLS -eq "1" $SkillsOnly = $env:DWS_SKILLS_ONLY -eq "1" $SkillName = "dws" -# Agent directory to install skills into — most agents can fall back to .agents\skills -$AgentDir = ".agents\skills" +# Agent skill base directories (same order as build/npm/install.js AGENT_DIRS). +$AgentDirs = @( + ".agents\skills", + ".claude\skills", + ".cursor\skills", + ".gemini\skills", + ".codex\skills", + ".github\skills", + ".windsurf\skills", + ".augment\skills", + ".cline\skills", + ".amp\skills", + ".kiro\skills", + ".trae\skills", + ".openclaw\skills" +) # ── Helpers ────────────────────────────────────────────────────────────────── @@ -170,6 +186,17 @@ function Copy-SkillToDir { } } +function Copy-SkillToDirSummary { + param([string]$SkillSrc, [string]$Dest, [string]$Label) + + if (Test-Path $Dest) { + Remove-Item -Path $Dest -Recurse -Force + } + + $fileCount = Copy-DirRecursive -Source $SkillSrc -Destination $Dest + Write-Say "✅ Skills → $Label ($fileCount files)" +} + function Resolve-SourceRoot { $scriptPath = $PSScriptRoot if (-not $scriptPath) { return $null } @@ -280,9 +307,45 @@ function Install-SkillsLocal { Write-Say "" Write-Say "📦 Installing agent skills from local source: $skillSrc" - $dest = Join-Path (Join-Path $HOME $AgentDir) $SkillName - $label = "~\$AgentDir\$SkillName" - Copy-SkillToDir -SkillSrc $skillSrc -Dest $dest -Label $label + Install-SkillsToHomes -SkillSrc $skillSrc -Root $HOME +} + +function Install-SkillsToHomes { + param( + [string]$SkillSrc, + [string]$Root = $HOME + ) + + $installed = 0 + for ($i = 0; $i -lt $AgentDirs.Count; $i++) { + $agentDir = $AgentDirs[$i] + $baseDir = Join-Path $Root $agentDir + $parentGate = Split-Path $baseDir -Parent + if ($i -gt 0 -and !(Test-Path $parentGate)) { + continue + } + $dest = Join-Path $baseDir $SkillName + if ($Root -eq $HOME) { + $label = "~\$agentDir\$SkillName" + } else { + $label = Join-Path $Root (Join-Path $agentDir $SkillName) + } + if ($installed -eq 0) { + Copy-SkillToDir -SkillSrc $SkillSrc -Dest $dest -Label $label + } else { + Copy-SkillToDirSummary -SkillSrc $SkillSrc -Dest $dest -Label $label + } + $installed++ + } + if ($installed -eq 0) { + $fallback = Join-Path (Join-Path $Root ".agents\skills") $SkillName + if ($Root -eq $HOME) { + $flabel = "~\.agents\skills\$SkillName" + } else { + $flabel = Join-Path $Root (Join-Path ".agents\skills" $SkillName) + } + Copy-SkillToDir -SkillSrc $SkillSrc -Dest $fallback -Label $flabel + } } # ── Install Binary from Source ─────────────────────────────────────────────── @@ -359,9 +422,7 @@ function Install-Skills { return } - $dest = Join-Path (Join-Path $HOME $AgentDir) $SkillName - $label = "~\$AgentDir\$SkillName" - Copy-SkillToDir -SkillSrc $skillSrc -Dest $dest -Label $label + Install-SkillsToHomes -SkillSrc $skillSrc -Root $HOME } finally { Remove-Item -Path $tmpDir -Recurse -Force -ErrorAction SilentlyContinue } diff --git a/scripts/install.sh b/scripts/install.sh index 716b0bf9..8f83d3a5 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -14,6 +14,8 @@ # DWS_VERSION — version to install (default: latest) # DWS_NO_SKILLS — set to 1 to skip skills install # DWS_SKILLS_ONLY — set to 1 to install only skills (skip binary) +# +# Agent skills paths follow build/npm/install.js AGENT_DIRS (order and entries must match). set -eu @@ -26,10 +28,6 @@ NO_SKILLS="${DWS_NO_SKILLS:-0}" SKILLS_ONLY="${DWS_SKILLS_ONLY:-0}" SKILL_NAME="dws" -# ── Agent directory to install skills into ─────────────────────────────────── -# Only install to .agents/skills — most agents can fall back to this directory. -AGENT_DIR=".agents/skills" - # ── Helpers ────────────────────────────────────────────────────────────────── say() { @@ -179,13 +177,85 @@ install_skills_local() { say "" say "📦 Installing agent skills from local source: ${skill_src}" - dest="$HOME/$AGENT_DIR/$SKILL_NAME" - display_path="~/$AGENT_DIR/$SKILL_NAME" - _copy_skill "$skill_src" "$dest" "$display_path" + install_skills_to_homes "$skill_src" return 0 } +# Install skill tree into all agent homes (same rules as build/npm/install.js installSkillsToHomes). +install_skills_to_homes() { + skill_src="$1" + root="${HOME}" + installed=0 + idx=0 + for agent_dir in \ + ".agents/skills" \ + ".claude/skills" \ + ".cursor/skills" \ + ".gemini/skills" \ + ".codex/skills" \ + ".github/skills" \ + ".windsurf/skills" \ + ".augment/skills" \ + ".cline/skills" \ + ".amp/skills" \ + ".kiro/skills" \ + ".trae/skills" \ + ".openclaw/skills" + do + base_dir="$root/$agent_dir" + parent_gate="$(dirname "$base_dir")" + if [ "$idx" -gt 0 ] && [ ! -e "$parent_gate" ]; then + idx=$((idx + 1)) + continue + fi + dest="$base_dir/$SKILL_NAME" + case "$root" in + "$HOME") + label="~/$agent_dir/$SKILL_NAME" + ;; + *) + label="$root/$agent_dir/$SKILL_NAME" + ;; + esac + if [ "$installed" -eq 0 ]; then + _copy_skill "$skill_src" "$dest" "$label" + else + _copy_skill_summary "$skill_src" "$dest" "$label" + fi + installed=$((installed + 1)) + idx=$((idx + 1)) + done + if [ "$installed" -eq 0 ]; then + case "$root" in + "$HOME") + flabel="~/.agents/skills/$SKILL_NAME" + ;; + *) + flabel="$root/.agents/skills/$SKILL_NAME" + ;; + esac + _copy_skill "$skill_src" "$root/.agents/skills/$SKILL_NAME" "$flabel" + fi +} + +# One-line summary copy (used for 2nd+ agent targets). +_copy_skill_summary() { + _src="$1" + _dest="$2" + _label="$3" + + if [ -d "$_dest" ]; then + rm -rf "$_dest" + fi + + mkdir -p "$_dest" + cp -R "$_src/"* "$_dest/" 2>/dev/null || cp -r "$_src/"* "$_dest/" + file_count="$(find "$_dest" -type f | wc -l | tr -d ' ')" + + say "✅ Skills → ${_label} (${file_count} files)" +} + # Helper: copy skill files to a destination and print details _copy_skill() { _src="$1" @@ -349,9 +419,7 @@ install_skills() { fi fi - dest="$HOME/$AGENT_DIR/$SKILL_NAME" - display_path="~/$AGENT_DIR/$SKILL_NAME" - _copy_skill "$skill_src" "$dest" "$display_path" + install_skills_to_homes "$skill_src" rm -rf "$tmpdir_skills" } diff --git a/scripts/release/verify-package-managers.sh b/scripts/release/verify-package-managers.sh index b12d7c47..e2af3355 100755 --- a/scripts/release/verify-package-managers.sh +++ b/scripts/release/verify-package-managers.sh @@ -36,6 +36,7 @@ HOME_AGENT_PARENTS=" .amp .kiro .trae +.openclaw " HOME_SKILL_TARGETS=" .agents/skills/dws @@ -50,6 +51,7 @@ HOME_SKILL_TARGETS=" .amp/skills/dws .kiro/skills/dws .trae/skills/dws +.openclaw/skills/dws " cleanup() { if command -v brew >/dev/null 2>&1; then diff --git a/test/scripts/install_script_test.go b/test/scripts/install_script_test.go index 35fb4a22..c53c6e7e 100644 --- a/test/scripts/install_script_test.go +++ b/test/scripts/install_script_test.go @@ -10,6 +10,7 @@ import ( var expectedHomeSkillTargets = []string{ ".agents/skills/dws", + ".cursor/skills/dws", } func TestInstallScriptSourceModeInstallsBinary(t *testing.T) { @@ -107,6 +108,11 @@ done mustWriteFile(t, filepath.Join(stubRoot, "make"), []byte(makeStub), 0o755) mustWriteFile(t, filepath.Join(stubRoot, "go"), []byte("#!/bin/sh\ntrue\n"), 0o755) + // Gate for index>0 agent dirs (matches build/npm/install.js): parent must exist. + if err := os.MkdirAll(filepath.Join(fakeHome, ".cursor"), 0o755); err != nil { + t.Fatalf("MkdirAll(.cursor) error = %v", err) + } + cmd := exec.Command("sh", scriptPath) cmd.Env = append(os.Environ(), "HOME="+fakeHome, @@ -147,6 +153,9 @@ func TestInstallPowerShellScriptInstallsToAgentsDir(t *testing.T) { if !strings.Contains(text, ".agents\\skills") { t.Fatalf("install.ps1 missing .agents\\skills") } + if !strings.Contains(text, ".cursor\\skills") { + t.Fatalf("install.ps1 missing .cursor\\skills (AGENT_DIRS must match build/npm/install.js)") + } } func TestInstallScriptsUseGitHubReleaseSkillsAsset(t *testing.T) { diff --git a/test/scripts/package_script_test.go b/test/scripts/package_script_test.go index 98029c84..383eea5a 100644 --- a/test/scripts/package_script_test.go +++ b/test/scripts/package_script_test.go @@ -22,6 +22,7 @@ var expectedPackagedSkillTargets = []string{ ".amp/skills/dws", ".kiro/skills/dws", ".trae/skills/dws", + ".openclaw/skills/dws", } // seedDistArtifacts creates fake goreleaser output archives (empty tar.gz/zip