-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathsetup
More file actions
executable file
·238 lines (220 loc) · 9.62 KB
/
setup
File metadata and controls
executable file
·238 lines (220 loc) · 9.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#!/usr/bin/env bash
set -e
# Resolve the *physical* path so legacy-symlink cleanup below can't delete the
# directory we're currently sourcing setup from. Without -P, IDSTACK_DIR may be
# the legacy ~/.claude/skills/idstack symlink path, and removing it would leave
# the new plugin symlink pointing at a deleted target.
IDSTACK_DIR="$(cd "$(dirname "$0")" && pwd -P)"
# Default install: global plugin so /idstack:<skill> is discovered.
PLUGINS_DIR="$HOME/.claude/plugins"
LEGACY_SKILLS_DIR="$HOME/.claude/skills"
SCOPE="global"
CODEX_OPT_IN="" # "" = auto-detect, "yes" = force-install, "no" = skip
for arg in "$@"; do
case "$arg" in
--local)
PLUGINS_DIR="$(pwd)/.claude/plugins"
LEGACY_SKILLS_DIR="$(pwd)/.claude/skills"
SCOPE="local"
;;
--codex) CODEX_OPT_IN="yes" ;;
--no-codex) CODEX_OPT_IN="no" ;;
--keep-legacy) ;; # consumed later in the legacy-cleanup branch
-h|--help)
echo "Usage: ./setup [--local] [--codex|--no-codex] [--keep-legacy]"
echo " --local install to ./.claude/plugins/ instead of \$HOME/.claude/plugins/"
echo " --codex force Codex install even if codex is not on PATH"
echo " --no-codex skip Codex install even if codex is on PATH"
echo " --keep-legacy keep pre-v2 install at \$HOME/.claude/skills/idstack (default: remove)"
exit 0
;;
*) echo "ERROR: unknown argument: $arg" >&2; echo "Run ./setup --help for usage." >&2; exit 2 ;;
esac
done
# Regenerate skill files for all targets so installs are always fresh against
# the current .tmpl source. Cheap (<1s); the source-of-truth invariant is
# worth more than the cost.
"$IDSTACK_DIR/bin/idstack-gen-skills" --target all >/dev/null
echo " regenerated skill files for all targets"
TARGET="$PLUGINS_DIR/idstack"
mkdir -p "$PLUGINS_DIR"
echo "Installing idstack ($SCOPE)..."
echo " source: $IDSTACK_DIR"
echo " target: $TARGET"
echo ""
# Verify plugin manifest is present (sanity check; setup should not silently
# install something Claude Code can't discover).
if [ ! -f "$IDSTACK_DIR/.claude-plugin/plugin.json" ]; then
echo "ERROR: missing .claude-plugin/plugin.json at $IDSTACK_DIR" >&2
echo " This setup script expects a plugin layout. Aborting." >&2
exit 1
fi
# Clean up legacy v2.0 + pre-v2 installs under $LEGACY_SKILLS_DIR. Each scope
# only touches its own scope's legacy path: a --local install cleans up legacy
# entries under ./.claude/skills/, and never mutates the user's global skills
# directory.
# v2.0 install (single dispatcher symlink). Only registered /idstack, never
# exposed the 11 namespaced sub-skills.
LEGACY_SKILLS_LINK="$LEGACY_SKILLS_DIR/idstack"
if [ -L "$LEGACY_SKILLS_LINK" ]; then
legacy_target=$(readlink "$LEGACY_SKILLS_LINK" 2>/dev/null || true)
legacy_resolved=$(cd "$LEGACY_SKILLS_LINK" 2>/dev/null && pwd -P || true)
if [ "$legacy_resolved" = "$IDSTACK_DIR" ]; then
# The legacy symlink points at the same physical repo we're installing
# from. Removing it is fine — the new plugin symlink replaces it.
rm "$LEGACY_SKILLS_LINK"
echo " removed legacy symlink: $LEGACY_SKILLS_LINK (was -> $legacy_target)"
else
rm "$LEGACY_SKILLS_LINK"
echo " removed legacy symlink: $LEGACY_SKILLS_LINK"
fi
elif [ -d "$LEGACY_SKILLS_LINK" ] && [ "$LEGACY_SKILLS_LINK" != "$IDSTACK_DIR" ]; then
# Pre-v2.0.1.0 install was a real clone with a top-level dispatcher
# SKILL.md. Left in place, it shadows the plugin: Claude Code matches the
# dispatcher on /idstack and the dispatcher then tries to invoke bare-name
# sub-skills that the plugin-only layout no longer exposes. Detect that
# signature and remove it.
legacy_is_dispatcher=0
if [ -f "$LEGACY_SKILLS_LINK/SKILL.md" ] && \
grep -q '^name: idstack' "$LEGACY_SKILLS_LINK/SKILL.md" 2>/dev/null; then
legacy_is_dispatcher=1
fi
legacy_is_old_version=0
if [ -f "$LEGACY_SKILLS_LINK/VERSION" ]; then
legacy_version=$(tr -d '[:space:]' < "$LEGACY_SKILLS_LINK/VERSION")
# Two arms: explicitly skip modern/future versions first, then flag the
# legacy ones. Patterns avoid literal dots and single-digit ranges so
# multi-digit components (2.0.10.0, 2.10.0.0, 20.x, 100.0.0, 200.0.0)
# work. Covered by test/test-version-classifier.sh.
case "$legacy_version" in
2.0.[1-9]*|2.[1-9]*|[3-9]*|[1-9][0-9]*) ;;
0.*|1.*|2.0.0.*|2.0.0) legacy_is_old_version=1 ;;
esac
fi
if [ "$legacy_is_dispatcher" = "1" ] || [ "$legacy_is_old_version" = "1" ]; then
if [[ " $* " == *" --keep-legacy "* ]]; then
echo " KEEPING legacy install dir: $LEGACY_SKILLS_LINK (--keep-legacy)"
echo " NOTE: this will shadow the plugin namespace; /idstack:<skill> may fail."
else
rm -rf "$LEGACY_SKILLS_LINK"
echo " removed legacy install dir: $LEGACY_SKILLS_LINK (pre-v2.0.1.0 dispatcher)"
fi
else
echo " WARNING: directory at $LEGACY_SKILLS_LINK is not a recognized idstack install."
echo " Leaving it in place. Remove manually if it shouldn't be there:"
echo " rm -rf $LEGACY_SKILLS_LINK"
fi
fi
# Pre-v2 individual skill symlinks. Only remove ones that point into idstack.
LEGACY_SKILLS="needs-analysis learning-objectives course-quality-review course-import assessment-design course-builder course-export accessibility-review red-team pipeline learn"
for skill in $LEGACY_SKILLS; do
legacy="$LEGACY_SKILLS_DIR/$skill"
if [ -L "$legacy" ]; then
link_target=$(readlink "$legacy" 2>/dev/null || true)
if echo "$link_target" | grep -q "idstack"; then
rm "$legacy"
echo " cleaned up legacy: $legacy"
fi
fi
done
# If $TARGET is the same path as $IDSTACK_DIR (user cloned directly into
# the plugins dir), there's nothing to symlink — the plugin is already there.
# Skipping prevents `ln -snf` from creating a broken self-link inside the dir.
if [ "$TARGET" = "$IDSTACK_DIR" ]; then
echo " in place: $TARGET (no symlink needed)"
else
ln -snf "$IDSTACK_DIR" "$TARGET"
echo " linked: $TARGET -> $IDSTACK_DIR"
fi
echo ""
echo "idstack installed (Claude Code)."
# Optional: Codex CLI install. Auto-detect codex on PATH unless the user passed
# --codex (force) or --no-codex (skip). The Codex bundle lives under
# dist/codex/ and is regenerated on every setup run.
CODEX_INSTALL=0
if [ "$CODEX_OPT_IN" = "yes" ]; then
CODEX_INSTALL=1
elif [ "$CODEX_OPT_IN" = "no" ]; then
CODEX_INSTALL=0
elif command -v codex >/dev/null 2>&1; then
CODEX_INSTALL=1
fi
if [ "$CODEX_INSTALL" = "1" ]; then
# Codex install — two-part:
# 1. Per-skill symlinks at $CODEX_HOME/skills/idstack-<name>/ so Codex auto-
# discovers each skill ($needs-analysis, $pipeline, etc.).
# 2. A whole-repo symlink at ~/.agents/plugins/idstack/ -> $IDSTACK_DIR so
# the in-skill bash blocks can resolve $_IDSTACK/bin/idstack-*. The
# preamble's path-detection chain finds it via the ~/.agents/plugins/
# fallback.
CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"
if [ "$SCOPE" = "local" ]; then
CODEX_HOME_DIR="$(pwd)/.codex"
CODEX_BIN_PARENT="$(pwd)/.agents/plugins"
else
CODEX_BIN_PARENT="$HOME/.agents/plugins"
fi
CODEX_SKILLS_DIR="$CODEX_HOME_DIR/skills"
CODEX_BIN_TARGET="$CODEX_BIN_PARENT/idstack"
CODEX_BUNDLE_SKILLS="$IDSTACK_DIR/dist/codex/skills"
if [ ! -d "$CODEX_BUNDLE_SKILLS" ]; then
echo " WARNING: Codex skill bundle missing at $CODEX_BUNDLE_SKILLS — generator did not produce it. Skipping Codex install."
else
# Part 1: per-skill symlinks for auto-discovery.
mkdir -p "$CODEX_SKILLS_DIR"
for skill_dir in "$CODEX_BUNDLE_SKILLS"/idstack-*; do
[ -d "$skill_dir" ] || continue
skill_name=$(basename "$skill_dir")
target="$CODEX_SKILLS_DIR/$skill_name"
# Replace pre-existing real dir so ln -snf doesn't symlink-into-dir.
if [ -d "$target" ] && [ ! -L "$target" ]; then
if [ -z "$HOME" ] || [[ "$target" != */idstack-* ]]; then
echo "ERROR: refusing to rm $target" >&2
exit 1
fi
rm -rf "$target"
fi
ln -snf "$skill_dir" "$target"
done
echo " linked 11 skills into: $CODEX_SKILLS_DIR/idstack-*"
# Part 2: whole-repo symlink so $_IDSTACK/bin/idstack-* resolves in skill bodies.
mkdir -p "$CODEX_BIN_PARENT"
if [ "$CODEX_BIN_TARGET" = "$IDSTACK_DIR" ]; then
echo " in place: $CODEX_BIN_TARGET (no bin-resolution symlink needed)"
else
if [ -d "$CODEX_BIN_TARGET" ] && [ ! -L "$CODEX_BIN_TARGET" ]; then
if [ -z "$HOME" ] || [[ "$CODEX_BIN_TARGET" != */idstack ]]; then
echo "ERROR: refusing to rm $CODEX_BIN_TARGET (HOME=$HOME)" >&2
exit 1
fi
rm -rf "$CODEX_BIN_TARGET"
echo " removed pre-existing dir: $CODEX_BIN_TARGET"
fi
ln -snf "$IDSTACK_DIR" "$CODEX_BIN_TARGET"
echo " linked: $CODEX_BIN_TARGET -> $IDSTACK_DIR (for in-skill bin/ resolution)"
fi
echo ""
echo "idstack installed (Codex CLI)."
echo " If Codex CLI is already running, restart it."
echo " Usage: \$<skill> (e.g., \$needs-analysis, \$pipeline)"
fi
fi
echo ""
echo " If Claude Code is already running, restart it (plugins load at session start)."
echo ""
echo " Usage: /idstack:<skill>"
echo ""
echo " Have an existing course?"
echo " /idstack:course-import -> /idstack:course-quality-review"
echo ""
echo " Starting fresh?"
echo " /idstack:needs-analysis"
echo ""
echo " Run the full pipeline:"
echo " /idstack:pipeline"
echo ""
echo " All skills: needs-analysis, learning-objectives, assessment-design,"
echo " course-builder, course-quality-review, accessibility-review,"
echo " red-team, course-export, course-import, pipeline, learn"
echo ""
echo " More info: https://idstack.org"