2 Commits
Author SHA1 Message Date
qiudl a58dd1aff3 Merge PR #5: make Codex the default skill target
Validated Codex and explicit Claude install layouts, stable content digests, exclusions, and real user-scope installation.
2026-08-20 02:37:34 +00:00
qiudl bb5e6be73e feat(skills): make Codex the default install target 2026-08-20 12:07:10 +09:30
7 changed files with 139 additions and 49 deletions
+3 -3
View File
@@ -1,14 +1,14 @@
# ai-proj-helper # ai-proj-helper
Claude Code 技能市场 + MCP 配置管理工具。 Codex 优先、兼容 Claude Code 的 Agent Skills 市场与 MCP 配置管理工具。
## 快速开始 ## 快速开始
```bash ```bash
./init.sh ./install-skills.sh
``` ```
交互式配置 MCP 连接(默认 SSE 模式)+ 自动注册技能市场到 `~/.claude/plugins/known_marketplaces.json`。支持命令行参数 默认安装到 Codex 标准目录 `~/.agents/skills`。Claude Code 的 MCP 与 marketplace 初始化使用
```bash ```bash
./init.sh --mode sse --token aiproj_pk_xxx ./init.sh --mode sse --token aiproj_pk_xxx
+13 -11
View File
@@ -1,6 +1,6 @@
# ai-proj-helper — 让 Claude Code 变成你的项目经理 # ai-proj-helper — 让 Codex / Claude Code 变成你的项目经理
> 一套开箱即用的 Claude Code 技能包 + MCP 服务,帮你用自然语言管理需求、写代码、做评审、同步飞书,把 AI 助手变成真正的项目经理 > 一套遵循 Agent Skills 标准的技能包 + MCP 服务,支持 Codex,并兼容 Claude Code
## 它能帮你做什么 ## 它能帮你做什么
@@ -66,7 +66,7 @@ PRD 文档存储在思源笔记中,可以导出发送到飞书群,通过飞
### 前置条件 ### 前置条件
- **Claude Code** 已安装([安装指南](https://docs.anthropic.com/en/docs/claude-code/overview) - **Codex**[Skills 文档](https://developers.openai.com/codex/skills))或 **Claude Code** 已安装
- **ai-proj 账号 + MCP API Key**:联系管理员获取(Key 格式: `aiproj_pk_xxx` - **ai-proj 账号 + MCP API Key**:联系管理员获取(Key 格式: `aiproj_pk_xxx`
### 一键部署(2 步搞定) ### 一键部署(2 步搞定)
@@ -76,18 +76,16 @@ PRD 文档存储在思源笔记中,可以导出发送到飞书群,通过飞
git clone https://gitea.pipexerp.com/pipexerp/ai-proj-helper.git git clone https://gitea.pipexerp.com/pipexerp/ai-proj-helper.git
cd ai-proj-helper cd ai-proj-helper
# 2. 运行初始化(按提示输入 API Key 即可) # 2. 默认安装到 Codex 的用户级标准目录 ~/.agents/skills
./init.sh ./install-skills.sh
``` ```
脚本会自动完成: 安装器会复制完整技能目录,包括 `SKILL.md`、references、scripts 和 assets。Codex 会自动发现 `~/.agents/skills` 中的技能;若没有出现,重启 Codex。
- 配置 MCP 服务器连接(`~/.claude/.mcp.json`
- 注册技能市场到 Claude Code`~/.claude/plugins/known_marketplaces.json`
- 安装完整技能目录到 `~/.claude/skills/`(包括 references、scripts 和 assets
也支持命令行参数跳过交互 Claude Code 用户显式选择 Claude 目标;需要同时配置 MCP 和 marketplace 时运行 `init.sh`
```bash ```bash
./install-skills.sh --agent claude
./init.sh --mode sse --token aiproj_pk_xxx ./init.sh --mode sse --token aiproj_pk_xxx
``` ```
@@ -172,13 +170,17 @@ skills:
A: 需要先联系管理员获取 MCP API Key(格式 `aiproj_pk_xxx`),然后在提示处输入。 A: 需要先联系管理员获取 MCP API Key(格式 `aiproj_pk_xxx`),然后在提示处输入。
**Q: 安装后 Codex 没有识别到技能?**
A: 确认技能位于 `~/.agents/skills/<name>/SKILL.md`,然后重启 Codex。Codex CLI 也可用 `/skills` 查看。
**Q: 安装后 Claude Code 没有识别到技能?** **Q: 安装后 Claude Code 没有识别到技能?**
A: 重启 Claude Code 后生效。如果仍不生效,检查 `~/.claude/plugins/known_marketplaces.json` 中是否包含 `ai-proj-helper` 条目。 A: 重启 Claude Code 后生效。如果仍不生效,检查 `~/.claude/plugins/known_marketplaces.json` 中是否包含 `ai-proj-helper` 条目。
**Q: 如何更新到最新版本?** **Q: 如何更新到最新版本?**
A: 进入项目目录执行 `git pull`,然后重新运行 `./init.sh` A: 进入项目目录执行 `git pull`,然后运行 `./install-skills.sh`Claude Code 用户增加 `--agent claude`
**Q: 如何禁用不需要的技能?** **Q: 如何禁用不需要的技能?**
+4 -2
View File
@@ -1,4 +1,6 @@
# Setup Guide # Claude Code Marketplace Setup
本页只描述 Claude Code marketplace。Codex 用户直接运行 `./install-skills.sh`,技能默认安装到 `~/.agents/skills`
## 1. Clone the Gitea Repository ## 1. Clone the Gitea Repository
@@ -114,7 +116,7 @@ ai-proj-helper/
- Check plugin name is correct - Check plugin name is correct
- Ensure marketplace.json is valid: `cat .claude-plugin/marketplace.json | jq` - Ensure marketplace.json is valid: `cat .claude-plugin/marketplace.json | jq`
**"Skills not working"** **"Skills not working in Claude Code"**
- Skills are Agent Skills (auto-invoked by Claude when relevant) - Skills are Agent Skills (auto-invoked by Claude when relevant)
- They don't create slash commands - They don't create slash commands
- Check plugin installation: `/plugin list` - Check plugin installation: `/plugin list`
+10 -3
View File
@@ -1,6 +1,6 @@
# Skill Sync Guide # Skill Sync Guide
仓库中的插件是团队技能的发布源,本机 `~/.claude/skills/` 是安装目标。个人技能保留在 仓库中的插件是团队技能的发布源。默认安装目标是 Codex 的用户级标准目录 `~/.agents/skills/`。个人技能保留在
`skills-personal/` 或其他本机目录,不会进入公开 marketplace。 `skills-personal/` 或其他本机目录,不会进入公开 marketplace。
## 从仓库更新本机 ## 从仓库更新本机
@@ -11,6 +11,12 @@ git pull
./install-skills.sh ./install-skills.sh
``` ```
Claude Code 需要显式选择目标:
```bash
./install-skills.sh --agent claude
```
安装器会复制完整技能目录,包括 `SKILL.md``references/``scripts/``assets/`。它用内容摘要区分仓库升级和本地修改: 安装器会复制完整技能目录,包括 `SKILL.md``references/``scripts/``assets/`。它用内容摘要区分仓库升级和本地修改:
- 目标未修改时,版本升级会自动安装。 - 目标未修改时,版本升级会自动安装。
@@ -23,11 +29,12 @@ git pull
```bash ```bash
./install-skills.sh --list ./install-skills.sh --list
./install-skills.sh --category dev ./install-skills.sh --category dev
./install-skills.sh --exclude ai-proj-cicd-release
``` ```
## 将本机技能发布到仓库 ## 将本机技能发布到仓库
不要批量复制整个 `~/.claude/skills/`。系统技能、第三方托管技能、包含机器路径或凭据的技能不应发布。 不要批量复制整个 `~/.agents/skills/` 或其他 Agent 的安装目录。系统技能、第三方托管技能、包含机器路径或凭据的技能不应发布。
1. 选择确实属于本仓库、可供团队复用的技能。 1. 选择确实属于本仓库、可供团队复用的技能。
2. 在对应 `skills-*/<name>-plugin/` 下放置 `.claude-plugin/plugin.json` 和完整 `skills/` 目录。 2. 在对应 `skills-*/<name>-plugin/` 下放置 `.claude-plugin/plugin.json` 和完整 `skills/` 目录。
@@ -51,7 +58,7 @@ git pull
**本地修改被跳过怎么办?** **本地修改被跳过怎么办?**
先比较仓库源和 `~/.claude/skills/<name>/`。保留本地修改时将其整理成插件变更;确认丢弃时再对该次安装使用 `--force`。 先比较仓库源和 `~/.agents/skills/<name>/`。保留本地修改时将其整理成插件变更;确认丢弃时再对该次安装使用 `--force`。Claude 目标改查 `~/.claude/skills/`。
**marketplace 没更新?** **marketplace 没更新?**
+4 -3
View File
@@ -1,6 +1,6 @@
#!/bin/bash #!/bin/bash
# ai-proj-helper 初始化脚本 # ai-proj-helper 初始化脚本
# 配置 MCP 连接 + 安装技能到 ~/.claude/skills/ # 配置 Claude MCP 连接 + 安装 Claude 技能
set -e set -e
@@ -187,7 +187,8 @@ fi
# Use the versioned installer as the single installation path so references, # Use the versioned installer as the single installation path so references,
# scripts and assets stay beside SKILL.md and local edits are not overwritten. # scripts and assets stay beside SKILL.md and local edits are not overwritten.
echo "📦 安装技能到 ~/.claude/skills/ ..." echo "📦 安装技能到 ~/.claude/skills/ ..."
"$SCRIPT_DIR/install-skills.sh" "$SCRIPT_DIR/install-skills.sh" --agent claude
SKILL_COUNT=$(python3 -c 'import json, os; p=os.path.expanduser("~/.claude/.installed-skills.json"); print(len(json.load(open(p))) if os.path.exists(p) else 0)' 2>/dev/null || echo 0)
echo "✅ 技能安装完成" echo "✅ 技能安装完成"
# ── Verify MCP connection ──────────────────────────────────────────── # ── Verify MCP connection ────────────────────────────────────────────
@@ -250,7 +251,7 @@ if $HAS_CLAUDE; then
else else
echo " ✅ MCP 服务器 → $MCP_CONFIG" echo " ✅ MCP 服务器 → $MCP_CONFIG"
fi fi
echo " ✅ 技能 ($SKILL_COUNT 个) → $SKILLS_DIR" echo " ✅ 技能 ($SKILL_COUNT 个) → ~/.claude/skills"
echo "" echo ""
echo "重启 Claude Code 即可使用。" echo "重启 Claude Code 即可使用。"
echo "如需更改配置,编辑 claude-config.yaml 后重新运行 ./init.sh" echo "如需更改配置,编辑 claude-config.yaml 后重新运行 ./init.sh"
+58 -16
View File
@@ -1,13 +1,15 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# install-skills.sh — Cross-machine Claude skill sync from ai-proj-helper # install-skills.sh — Cross-agent skill sync from ai-proj-helper
# #
# Usage: # Usage:
# ./install-skills.sh [options] # ./install-skills.sh [options]
# #
# Options: # Options:
# --agent <agent> Install target: codex (default) or claude
# --dry-run Preview changes without writing anything # --dry-run Preview changes without writing anything
# --category <cat> Only install plugins in dir_category=<cat> # --category <cat> Only install plugins in dir_category=<cat>
# Valid values: biz, core, dev, integration, personal, req # Valid values: biz, core, dev, integration, personal, req
# --exclude <name> Skip one install_name (repeatable)
# --force Overwrite even if local files were modified # --force Overwrite even if local files were modified
# --cleanup Remove locally installed skills that are no longer in repo # --cleanup Remove locally installed skills that are no longer in repo
# --list List all available plugins without installing # --list List all available plugins without installing
@@ -16,12 +18,14 @@
set -euo pipefail set -euo pipefail
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILLS_DIR="${HOME}/.claude/skills" SKILLS_DIR=""
COMMANDS_DIR="${HOME}/.claude/commands" COMMANDS_DIR=""
STATE_FILE="${HOME}/.claude/.installed-skills.json" STATE_FILE=""
AGENT_TARGET="codex"
DRY_RUN=false DRY_RUN=false
CATEGORY_FILTER="" CATEGORY_FILTER=""
EXCLUDED_NAMES=()
FORCE=false FORCE=false
CLEANUP=false CLEANUP=false
LIST_ONLY=false LIST_ONLY=false
@@ -43,11 +47,19 @@ dry() { echo -e "${YELLOW}[dry]${RESET} $*"; }
# ── Argument parsing ─────────────────────────────────────────────────────────── # ── Argument parsing ───────────────────────────────────────────────────────────
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
--agent)
[[ $# -ge 2 ]] || { error "--agent requires codex or claude"; exit 1; }
AGENT_TARGET="$2"; shift ;;
--dry-run) DRY_RUN=true ;; --dry-run) DRY_RUN=true ;;
--force) FORCE=true ;; --force) FORCE=true ;;
--cleanup) CLEANUP=true ;; --cleanup) CLEANUP=true ;;
--list) LIST_ONLY=true ;; --list) LIST_ONLY=true ;;
--category) CATEGORY_FILTER="$2"; shift ;; --category)
[[ $# -ge 2 ]] || { error "--category requires a value"; exit 1; }
CATEGORY_FILTER="$2"; shift ;;
--exclude)
[[ $# -ge 2 ]] || { error "--exclude requires an install_name"; exit 1; }
EXCLUDED_NAMES+=("$2"); shift ;;
--help|-h) --help|-h)
grep '^#' "$0" | grep -v '!/usr' | sed 's/^# \?//' grep '^#' "$0" | grep -v '!/usr' | sed 's/^# \?//'
exit 0 ;; exit 0 ;;
@@ -58,6 +70,24 @@ while [[ $# -gt 0 ]]; do
shift shift
done done
case "$AGENT_TARGET" in
codex)
# ~/.agents/skills is the current user-level Codex discovery location and
# is intentionally agent-neutral. Commands are installed as normal skills.
SKILLS_DIR="${AI_PROJ_HELPER_SKILLS_DIR:-${HOME}/.agents/skills}"
STATE_FILE="${AI_PROJ_HELPER_STATE_FILE:-${HOME}/.agents/.ai-proj-helper-installed-skills.json}"
;;
claude)
SKILLS_DIR="${AI_PROJ_HELPER_SKILLS_DIR:-${HOME}/.claude/skills}"
COMMANDS_DIR="${AI_PROJ_HELPER_COMMANDS_DIR:-${HOME}/.claude/commands}"
STATE_FILE="${AI_PROJ_HELPER_STATE_FILE:-${HOME}/.claude/.installed-skills.json}"
;;
*)
error "Unsupported agent: $AGENT_TARGET (expected codex or claude)"
exit 1
;;
esac
# ── State helpers (plain JSON via python3) ───────────────────────────────────── # ── State helpers (plain JSON via python3) ─────────────────────────────────────
state_get() { state_get() {
# state_get <install_name> -> prints version or empty string # state_get <install_name> -> prints version or empty string
@@ -94,7 +124,7 @@ state_set() {
import json,os import json,os
f='$STATE_FILE' f='$STATE_FILE'
d=json.load(open(f)) if os.path.exists(f) else {} d=json.load(open(f)) if os.path.exists(f) else {}
d['$name']={'version':'$ver','install_type':'$itype','content_digest':'$digest'} d['$name']={'version':'$ver','install_type':'$itype','content_digest':'$digest','agent':'$AGENT_TARGET'}
json.dump(d,open(f,'w'),indent=2) json.dump(d,open(f,'w'),indent=2)
" 2>/dev/null " 2>/dev/null
} }
@@ -149,7 +179,9 @@ files = [target] if target.is_file() else sorted(
path for path in target.rglob("*") if path.is_file() or path.is_symlink() path for path in target.rglob("*") if path.is_file() or path.is_symlink()
) )
for path in files: for path in files:
relative = path.name if target.is_file() else path.relative_to(target).as_posix() # A single-file command is renamed when installed for Claude. Hash its
# content under a stable logical name so source and target compare equally.
relative = "." if target.is_file() else path.relative_to(target).as_posix()
digest.update(relative.encode("utf-8")) digest.update(relative.encode("utf-8"))
digest.update(b"\0") digest.update(b"\0")
if path.is_symlink(): if path.is_symlink():
@@ -222,6 +254,11 @@ install_plugin() {
dir_category="$(read_field "$json_path" dir_category)" dir_category="$(read_field "$json_path" dir_category)"
version="$(read_field "$json_path" version)" version="$(read_field "$json_path" version)"
local excluded
for excluded in ${EXCLUDED_NAMES[@]+"${EXCLUDED_NAMES[@]}"}; do
[[ "$install_name" == "$excluded" ]] && return
done
# Skip if no install metadata (legacy plugin without our new fields) # Skip if no install metadata (legacy plugin without our new fields)
if [[ -z "$install_name" || -z "$install_type" ]]; then if [[ -z "$install_name" || -z "$install_type" ]]; then
warn "$(basename "$plugin_dir"): missing install_name/install_type, skipping" warn "$(basename "$plugin_dir"): missing install_name/install_type, skipping"
@@ -239,8 +276,13 @@ install_plugin() {
return return
fi fi
local effective_install_type="$install_type"
if [[ "$AGENT_TARGET" == "codex" ]]; then
effective_install_type="skill"
fi
if [[ "$LIST_ONLY" == true ]]; then if [[ "$LIST_ONLY" == true ]]; then
echo " [$dir_category] $install_type:$install_name v$version" echo " [$dir_category] $effective_install_type:$install_name v$version"
return return
fi fi
@@ -250,7 +292,7 @@ install_plugin() {
src_dir="$(resolve_skills_src "$skills_dir")" src_dir="$(resolve_skills_src "$skills_dir")"
local source_path target_path local source_path target_path
if [[ "$install_type" == "command" ]]; then if [[ "$effective_install_type" == "command" ]]; then
source_path="$src_dir/SKILL.md" source_path="$src_dir/SKILL.md"
target_path="$COMMANDS_DIR/${install_name}.md" target_path="$COMMANDS_DIR/${install_name}.md"
else else
@@ -274,13 +316,13 @@ install_plugin() {
if [[ "$FORCE" == false && -n "$target_digest" && "$target_digest" == "$source_digest" ]]; then if [[ "$FORCE" == false && -n "$target_digest" && "$target_digest" == "$source_digest" ]]; then
if [[ "$DRY_RUN" == false && ( "$current_version" != "$version" || "$recorded_digest" != "$source_digest" ) ]]; then if [[ "$DRY_RUN" == false && ( "$current_version" != "$version" || "$recorded_digest" != "$source_digest" ) ]]; then
state_set "$install_name" "$version" "$install_type" "$source_digest" state_set "$install_name" "$version" "$effective_install_type" "$source_digest"
fi fi
return return
fi fi
local legacy_subset=false local legacy_subset=false
if [[ "$install_type" == "skill" && -z "$recorded_digest" && -n "$target_digest" ]]; then if [[ "$effective_install_type" == "skill" && -z "$recorded_digest" && -n "$target_digest" ]]; then
if is_compatible_subset "$source_path" "$target_path"; then if is_compatible_subset "$source_path" "$target_path"; then
legacy_subset=true legacy_subset=true
fi fi
@@ -294,7 +336,7 @@ install_plugin() {
fi fi
# Perform install # Perform install
if [[ "$install_type" == "command" ]]; then if [[ "$effective_install_type" == "command" ]]; then
# Single-file command → ~/.claude/commands/<name>.md # Single-file command → ~/.claude/commands/<name>.md
local src_md="$src_dir/SKILL.md" local src_md="$src_dir/SKILL.md"
if [[ ! -f "$src_md" ]]; then if [[ ! -f "$src_md" ]]; then
@@ -308,13 +350,13 @@ install_plugin() {
else else
mkdir -p "$COMMANDS_DIR" mkdir -p "$COMMANDS_DIR"
cp "$src_md" "$COMMANDS_DIR/${install_name}.md" cp "$src_md" "$COMMANDS_DIR/${install_name}.md"
state_set "$install_name" "$version" "$install_type" "$source_digest" state_set "$install_name" "$version" "$effective_install_type" "$source_digest"
ok "$install_name → command (v$version)" ok "$install_name → command (v$version)"
INSTALL_ACTION=true INSTALL_ACTION=true
fi fi
else else
# Skill directory → ~/.claude/skills/<name>/ # Standard skill directory → the selected agent's discovery root.
local dst_dir="$SKILLS_DIR/$install_name" local dst_dir="$SKILLS_DIR/$install_name"
if [[ "$DRY_RUN" == true ]]; then if [[ "$DRY_RUN" == true ]]; then
@@ -324,7 +366,7 @@ install_plugin() {
mkdir -p "$dst_dir" mkdir -p "$dst_dir"
# rsync resolved source (handles nested skills/ structures) # rsync resolved source (handles nested skills/ structures)
rsync -a --delete "$src_dir/" "$dst_dir/" rsync -a --delete "$src_dir/" "$dst_dir/"
state_set "$install_name" "$version" "$install_type" "$source_digest" state_set "$install_name" "$version" "$effective_install_type" "$source_digest"
ok "$install_name → skill (v$version)" ok "$install_name → skill (v$version)"
INSTALL_ACTION=true INSTALL_ACTION=true
fi fi
@@ -387,7 +429,7 @@ main() {
return return
fi fi
info "Installing Claude skills from: $REPO_DIR" info "Installing skills for $AGENT_TARGET from: $REPO_DIR"
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be written" [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no files will be written"
[[ -n "$CATEGORY_FILTER" ]] && info "Category filter: $CATEGORY_FILTER" [[ -n "$CATEGORY_FILTER" ]] && info "Category filter: $CATEGORY_FILTER"
+47 -11
View File
@@ -13,10 +13,10 @@ mkdir -p "$TEST_HOME"
cp "$PROJECT_DIR/install-skills.sh" "$FIXTURE_REPO/install-skills.sh" cp "$PROJECT_DIR/install-skills.sh" "$FIXTURE_REPO/install-skills.sh"
write_manifest() { write_manifest() {
local plugin="$1" name="$2" version="$3" local plugin="$1" name="$2" version="$3" install_type="${4:-skill}"
mkdir -p "$FIXTURE_REPO/skills-dev/${plugin}-plugin/.claude-plugin" mkdir -p "$FIXTURE_REPO/skills-dev/${plugin}-plugin/.claude-plugin"
cat > "$FIXTURE_REPO/skills-dev/${plugin}-plugin/.claude-plugin/plugin.json" <<JSON cat > "$FIXTURE_REPO/skills-dev/${plugin}-plugin/.claude-plugin/plugin.json" <<JSON
{"name":"${plugin}-plugin","version":"${version}","install_name":"${name}","install_type":"skill","dir_category":"dev"} {"name":"${plugin}-plugin","version":"${version}","install_name":"${name}","install_type":"${install_type}","dir_category":"dev"}
JSON JSON
} }
@@ -31,7 +31,7 @@ EOF
printf 'reference one\n' > "$FIXTURE_REPO/skills-dev/example-plugin/skills/references/guide.md" printf 'reference one\n' > "$FIXTURE_REPO/skills-dev/example-plugin/skills/references/guide.md"
HOME="$TEST_HOME" "$FIXTURE_REPO/install-skills.sh" >/dev/null HOME="$TEST_HOME" "$FIXTURE_REPO/install-skills.sh" >/dev/null
test -f "$TEST_HOME/.claude/skills/example/references/guide.md" test -f "$TEST_HOME/.agents/skills/example/references/guide.md"
# A repository version upgrade replaces an unchanged prior install. # A repository version upgrade replaces an unchanged prior install.
write_manifest example example 2.0.0 write_manifest example example 2.0.0
@@ -43,17 +43,17 @@ description: Installer fixture version two.
version two version two
EOF EOF
HOME="$TEST_HOME" "$FIXTURE_REPO/install-skills.sh" >/dev/null HOME="$TEST_HOME" "$FIXTURE_REPO/install-skills.sh" >/dev/null
grep -q 'version two' "$TEST_HOME/.claude/skills/example/SKILL.md" grep -q 'version two' "$TEST_HOME/.agents/skills/example/SKILL.md"
grep -q '"version": "2.0.0"' "$TEST_HOME/.claude/.installed-skills.json" grep -q '"version": "2.0.0"' "$TEST_HOME/.agents/.ai-proj-helper-installed-skills.json"
# A local edit is preserved even when the repository advances again. # A local edit is preserved even when the repository advances again.
printf 'local edit\n' >> "$TEST_HOME/.claude/skills/example/SKILL.md" printf 'local edit\n' >> "$TEST_HOME/.agents/skills/example/SKILL.md"
write_manifest example example 3.0.0 write_manifest example example 3.0.0
printf 'repository version three\n' >> "$FIXTURE_REPO/skills-dev/example-plugin/skills/SKILL.md" printf 'repository version three\n' >> "$FIXTURE_REPO/skills-dev/example-plugin/skills/SKILL.md"
output="$(HOME="$TEST_HOME" "$FIXTURE_REPO/install-skills.sh")" output="$(HOME="$TEST_HOME" "$FIXTURE_REPO/install-skills.sh")"
grep -q 'local files were modified' <<<"$output" grep -q 'local files were modified' <<<"$output"
grep -q 'local edit' "$TEST_HOME/.claude/skills/example/SKILL.md" grep -q 'local edit' "$TEST_HOME/.agents/skills/example/SKILL.md"
if grep -q 'repository version three' "$TEST_HOME/.claude/skills/example/SKILL.md"; then if grep -q 'repository version three' "$TEST_HOME/.agents/skills/example/SKILL.md"; then
echo 'local modification was overwritten' >&2 echo 'local modification was overwritten' >&2
exit 1 exit 1
fi fi
@@ -70,9 +70,45 @@ description: Legacy installation fixture.
legacy content legacy content
EOF EOF
printf 'legacy reference\n' > "$FIXTURE_REPO/skills-dev/legacy-plugin/skills/references/guide.md" printf 'legacy reference\n' > "$FIXTURE_REPO/skills-dev/legacy-plugin/skills/references/guide.md"
mkdir -p "$TEST_HOME/.claude/skills/legacy" mkdir -p "$TEST_HOME/.agents/skills/legacy"
cp "$FIXTURE_REPO/skills-dev/legacy-plugin/skills/SKILL.md" "$TEST_HOME/.claude/skills/legacy/SKILL.md" cp "$FIXTURE_REPO/skills-dev/legacy-plugin/skills/SKILL.md" "$TEST_HOME/.agents/skills/legacy/SKILL.md"
HOME="$TEST_HOME" "$FIXTURE_REPO/install-skills.sh" >/dev/null HOME="$TEST_HOME" "$FIXTURE_REPO/install-skills.sh" >/dev/null
test -f "$TEST_HOME/.claude/skills/legacy/references/guide.md" test -f "$TEST_HOME/.agents/skills/legacy/references/guide.md"
# Command manifests become standard Codex skills, while explicit Claude
# installs retain the legacy single-file command layout. Both must be stable on
# a second run despite the Claude filename change.
write_manifest sample-command sample-command 1.0.0 command
mkdir -p "$FIXTURE_REPO/skills-dev/sample-command-plugin/skills"
cat > "$FIXTURE_REPO/skills-dev/sample-command-plugin/skills/SKILL.md" <<'EOF'
---
name: sample-command
description: Command installation fixture.
---
command content
EOF
CODEX_TEST_HOME="$TEST_ROOT/codex-home"
mkdir -p "$CODEX_TEST_HOME"
HOME="$CODEX_TEST_HOME" "$FIXTURE_REPO/install-skills.sh" >/dev/null
test -f "$CODEX_TEST_HOME/.agents/skills/sample-command/SKILL.md"
test ! -e "$CODEX_TEST_HOME/.claude/commands/sample-command.md"
codex_output="$(HOME="$CODEX_TEST_HOME" "$FIXTURE_REPO/install-skills.sh" --dry-run)"
grep -q '0 plugins would be installed/updated' <<<"$codex_output"
if grep -q 'sample-command: local files were modified' <<<"$codex_output"; then
echo 'Codex command skill was reported as modified' >&2
exit 1
fi
CLAUDE_TEST_HOME="$TEST_ROOT/claude-home"
mkdir -p "$CLAUDE_TEST_HOME"
HOME="$CLAUDE_TEST_HOME" "$FIXTURE_REPO/install-skills.sh" --agent claude >/dev/null
test -f "$CLAUDE_TEST_HOME/.claude/commands/sample-command.md"
claude_output="$(HOME="$CLAUDE_TEST_HOME" "$FIXTURE_REPO/install-skills.sh" --agent claude --dry-run)"
grep -q '0 plugins would be installed/updated' <<<"$claude_output"
if grep -q 'sample-command: local files were modified' <<<"$claude_output"; then
echo 'Claude command was reported as modified after filename conversion' >&2
exit 1
fi
echo 'install-skills tests passed' echo 'install-skills tests passed'