fix(dsh): валидировать legacy-кэш и пустой --superpowers-from (#49)

This commit is contained in:
Andrey Epifancev 2026-10-24 21:20:29 +04:00
parent ab458b58d2
commit 4054e13806
2 changed files with 59 additions and 3 deletions

View File

@ -28,6 +28,7 @@ DO_SKILLS=0
DO_ROLES=0
DO_OPENSPEC=0
SUPERPOWERS_FROM=""
SUPERPOWERS_FROM_SET=0
CLONE_HINT="git clone --depth 1 https://github.com/obra/superpowers $DSH_HOME/superpowers"
usage() {
@ -73,15 +74,26 @@ while [ "$#" -gt 0 ]; do
exit 2
fi
SUPERPOWERS_FROM="$2"
SUPERPOWERS_FROM_SET=1
shift
;;
--superpowers-from=*) SUPERPOWERS_FROM="${1#*=}" ;;
--superpowers-from=*)
SUPERPOWERS_FROM="${1#*=}"
SUPERPOWERS_FROM_SET=1
;;
-h|--help) usage; exit 0 ;;
*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
esac
shift
done
# An explicit flag with an empty value is a typo, not "use the default source":
# falling through to legacy would mask exactly the misconfiguration D-2 forbids.
if [ "$SUPERPOWERS_FROM_SET" = 1 ] && [ -z "$SUPERPOWERS_FROM" ]; then
echo "option --superpowers-from needs a non-empty directory" >&2
exit 2
fi
# Normalize a superpowers source: accept the repo root (with skills/) or the
# skills directory itself. Prints the skills directory, returns 1 otherwise.
normalize_superpowers_dir() {
@ -117,8 +129,11 @@ superpowers_source() {
return 0
fi
legacy="$(ls -d "$HOME"/.claude/plugins/cache/claude-plugins-official/superpowers/*/skills 2>/dev/null | sort -V | tail -1 || true)"
if [ -n "$legacy" ] && [ -d "$legacy" ]; then
printf '%s\tlegacy\n' "$legacy"
# A cache directory without using-superpowers is a broken cache, not a source:
# accepting it would copy 0 skills and report success (the silent empty
# bootstrap this change removes).
if [ -n "$legacy" ] && dir="$(normalize_superpowers_dir "$legacy")"; then
printf '%s\tlegacy\n' "$dir"
return 0
fi
if dir="$(normalize_superpowers_dir "$HOME/.claude/skills")"; then

View File

@ -303,3 +303,44 @@ test('--global installs DSH role paths and explains the instruction format', ()
cleanup(home)
}
})
test('a legacy cache without using-superpowers is not a source', () => {
const home = tempDir()
try {
const cache = join(
home,
'.claude',
'plugins',
'cache',
'claude-plugins-official',
'superpowers',
'7.0.0',
'skills',
)
write(join(cache, 'brainstorming', 'SKILL.md'), '---\nname: brainstorming\n---\nbody\n')
const run = runInstaller(home, ['--skills'])
const out = run.stdout + run.stderr
assert.equal(run.status, 1, out)
assert.match(out, /FAIL: no superpowers source/)
assert.doesNotMatch(out, /copied 0 superpowers/)
assert.equal(existsSync(join(home, '.dsh', 'skills', 'brainstorming')), false)
} finally {
cleanup(home)
}
})
test('an empty --superpowers-from is rejected, not ignored', () => {
const home = tempDir()
try {
makeLegacyCache(home)
for (const args of [['--skills', '--superpowers-from='], ['--skills', '--superpowers-from', '']]) {
const run = runInstaller(home, args)
const out = run.stdout + run.stderr
assert.equal(run.status, 2, out)
assert.match(out, /needs a non-empty directory/)
assert.equal(existsSync(join(home, '.dsh', 'skills', 'using-superpowers')), false)
}
} finally {
cleanup(home)
}
})