feat(dsh): install superpowers from an explicit source (#49)
This commit is contained in:
parent
15cc2e38d7
commit
05178869fb
@ -33,6 +33,13 @@ risks.
|
|||||||
# Static check
|
# Static check
|
||||||
bash dsh/install.sh --check
|
bash dsh/install.sh --check
|
||||||
|
|
||||||
|
# Superpowers source: get it once per machine. install.sh never downloads
|
||||||
|
# anything and no longer depends on ~/.claude.
|
||||||
|
git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers
|
||||||
|
# ...or point at any checkout of the superpowers repo / its skills directory:
|
||||||
|
bash dsh/install.sh --skills --superpowers-from /path/to/superpowers
|
||||||
|
# ...or export DSH_SUPERPOWERS_SOURCE=/path/to/superpowers
|
||||||
|
|
||||||
# Global instructions, superpowers skills, local role skill, and roles
|
# Global instructions, superpowers skills, local role skill, and roles
|
||||||
# (run from a terminal; these write outside the session workspace)
|
# (run from a terminal; these write outside the session workspace)
|
||||||
bash dsh/install.sh --global
|
bash dsh/install.sh --global
|
||||||
@ -47,6 +54,12 @@ bash <harness>/dsh/install.sh --openspec
|
|||||||
# or ask the agent to call plugin_manager install_bundle with that target.
|
# or ask the agent to call plugin_manager install_bundle with that target.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`--skills` resolves superpowers in this order: `--superpowers-from` →
|
||||||
|
`$DSH_SUPERPOWERS_SOURCE` → `~/.dsh/superpowers` → the Claude Code plugin cache
|
||||||
|
(legacy, warns). A clean HOME with no source and no installed copy fails with
|
||||||
|
the clone command above instead of pretending success; the local `role` and
|
||||||
|
`sdd` skills are copied either way.
|
||||||
|
|
||||||
Do not hand-edit `$DSH_HOME/profiles/desktop/package.json` or
|
Do not hand-edit `$DSH_HOME/profiles/desktop/package.json` or
|
||||||
`cordis.patch.yml`: the Desktop profile is Electron-managed and the plugin
|
`cordis.patch.yml`: the Desktop profile is Electron-managed and the plugin
|
||||||
manager owns those writes.
|
manager owns those writes.
|
||||||
|
|||||||
157
dsh/install.sh
157
dsh/install.sh
@ -10,7 +10,7 @@
|
|||||||
# Usage:
|
# Usage:
|
||||||
# bash dsh/install.sh --check
|
# bash dsh/install.sh --check
|
||||||
# bash dsh/install.sh --global
|
# bash dsh/install.sh --global
|
||||||
# bash dsh/install.sh --skills
|
# bash dsh/install.sh --skills [--superpowers-from <dir>]
|
||||||
# bash dsh/install.sh --roles
|
# bash dsh/install.sh --roles
|
||||||
# bash dsh/install.sh --all
|
# bash dsh/install.sh --all
|
||||||
# bash dsh/install.sh --openspec
|
# bash dsh/install.sh --openspec
|
||||||
@ -27,17 +27,27 @@ DO_GLOBAL=0
|
|||||||
DO_SKILLS=0
|
DO_SKILLS=0
|
||||||
DO_ROLES=0
|
DO_ROLES=0
|
||||||
DO_OPENSPEC=0
|
DO_OPENSPEC=0
|
||||||
|
SUPERPOWERS_FROM=""
|
||||||
|
CLONE_HINT="git clone --depth 1 https://github.com/obra/superpowers $DSH_HOME/superpowers"
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Usage: bash dsh/install.sh [--check] [--global] [--skills] [--roles] [--all] [--openspec]
|
Usage: bash dsh/install.sh [--check] [--global] [--skills] [--roles] [--all] [--openspec]
|
||||||
|
bash dsh/install.sh --skills --superpowers-from <dir>
|
||||||
|
|
||||||
--check verify python3, git, node, hooks, bundle, skills, and roles
|
--check verify python3, git, node, hooks, bundle, skills, and roles
|
||||||
--global back up and write \$DSH_HOME/AGENTS.md from global/CLAUDE.md
|
--global back up and write \$DSH_HOME/AGENTS.md from global/CLAUDE.md
|
||||||
--skills copy superpowers skills and local dsh/skills into \$DSH_HOME/skills
|
--skills copy superpowers and local dsh/skills into \$DSH_HOME/skills
|
||||||
--roles copy roles/ into \$DSH_HOME/roles
|
--roles copy roles/ into \$DSH_HOME/roles
|
||||||
--all --check + --global + --skills + --roles, then print the bundle step
|
--all --check + --global + --skills + --roles, then print the bundle step
|
||||||
--openspec add the agents skill adapter to the current OpenSpec project
|
--openspec add the agents skill adapter to the current OpenSpec project
|
||||||
|
--superpowers-from DIR copy superpowers skills from DIR (the repo root or its skills/)
|
||||||
|
|
||||||
|
Superpowers source, first match wins. The script never downloads anything:
|
||||||
|
1. --superpowers-from DIR
|
||||||
|
2. \$DSH_SUPERPOWERS_SOURCE
|
||||||
|
3. \$DSH_HOME/superpowers -- get it once: $CLONE_HINT
|
||||||
|
4. legacy: the Claude Code plugin cache, then ~/.claude/skills (warns)
|
||||||
|
|
||||||
Bundle installation itself is done in DSH:
|
Bundle installation itself is done in DSH:
|
||||||
Plugins -> install a bundle from $BUNDLE
|
Plugins -> install a bundle from $BUNDLE
|
||||||
@ -49,29 +59,88 @@ if [ "$#" -eq 0 ]; then
|
|||||||
DO_CHECK=1
|
DO_CHECK=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for arg in "$@"; do
|
while [ "$#" -gt 0 ]; do
|
||||||
case "$arg" in
|
case "$1" in
|
||||||
--check) DO_CHECK=1 ;;
|
--check) DO_CHECK=1 ;;
|
||||||
--global) DO_GLOBAL=1 ;;
|
--global) DO_GLOBAL=1 ;;
|
||||||
--skills) DO_SKILLS=1 ;;
|
--skills) DO_SKILLS=1 ;;
|
||||||
--roles) DO_ROLES=1 ;;
|
--roles) DO_ROLES=1 ;;
|
||||||
--openspec) DO_OPENSPEC=1 ;;
|
--openspec) DO_OPENSPEC=1 ;;
|
||||||
--all) DO_CHECK=1; DO_GLOBAL=1; DO_SKILLS=1; DO_ROLES=1 ;;
|
--all) DO_CHECK=1; DO_GLOBAL=1; DO_SKILLS=1; DO_ROLES=1 ;;
|
||||||
|
--superpowers-from)
|
||||||
|
if [ "$#" -lt 2 ]; then
|
||||||
|
echo "option --superpowers-from needs a directory" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
SUPERPOWERS_FROM="$2"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--superpowers-from=*) SUPERPOWERS_FROM="${1#*=}" ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;;
|
*) echo "unknown option: $1" >&2; usage >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
shift
|
||||||
done
|
done
|
||||||
|
|
||||||
superpowers_skills() {
|
# Normalize a superpowers source: accept the repo root (with skills/) or the
|
||||||
local dir
|
# skills directory itself. Prints the skills directory, returns 1 otherwise.
|
||||||
dir="$(ls -d "$HOME"/.claude/plugins/cache/claude-plugins-official/superpowers/*/skills 2>/dev/null | sort -V | tail -1 || true)"
|
normalize_superpowers_dir() {
|
||||||
if [ -n "$dir" ] && [ -d "$dir" ]; then
|
local dir="${1:-}"
|
||||||
|
[ -n "$dir" ] || return 1
|
||||||
|
if [ -f "$dir/skills/using-superpowers/SKILL.md" ]; then
|
||||||
|
printf '%s\n' "$dir/skills"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -f "$dir/using-superpowers/SKILL.md" ]; then
|
||||||
printf '%s\n' "$dir"
|
printf '%s\n' "$dir"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Print "<skills-dir>\t<explicit|pinned|legacy>". Returns 2 when an explicitly
|
||||||
|
# configured source is invalid (never fall back silently), 1 when none is found.
|
||||||
|
superpowers_source() {
|
||||||
|
local dir legacy
|
||||||
|
if [ -n "$SUPERPOWERS_FROM" ]; then
|
||||||
|
dir="$(normalize_superpowers_dir "$SUPERPOWERS_FROM")" || return 2
|
||||||
|
printf '%s\texplicit\n' "$dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if [ -n "${DSH_SUPERPOWERS_SOURCE:-}" ]; then
|
||||||
|
dir="$(normalize_superpowers_dir "$DSH_SUPERPOWERS_SOURCE")" || return 2
|
||||||
|
printf '%s\texplicit\n' "$dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if dir="$(normalize_superpowers_dir "$DSH_HOME/superpowers")"; then
|
||||||
|
printf '%s\tpinned\n' "$dir"
|
||||||
|
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"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if dir="$(normalize_superpowers_dir "$HOME/.claude/skills")"; then
|
||||||
|
printf '%s\tlegacy\n' "$dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Human-readable name of the source that made superpowers_source() return 2.
|
||||||
|
explicit_source_name() {
|
||||||
|
if [ -n "$SUPERPOWERS_FROM" ]; then
|
||||||
|
printf '%s\n' "--superpowers-from $SUPERPOWERS_FROM"
|
||||||
|
else
|
||||||
|
printf '%s\n' "DSH_SUPERPOWERS_SOURCE=$DSH_SUPERPOWERS_SOURCE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
report_invalid_explicit_source() {
|
||||||
|
echo "FAIL: explicit superpowers source $(explicit_source_name) has no using-superpowers/SKILL.md (expected <dir>/skills/... or <dir>/...)" >&2
|
||||||
|
}
|
||||||
|
|
||||||
check() {
|
check() {
|
||||||
local failed=0
|
local failed=0
|
||||||
for cmd in python3 git node; do
|
for cmd in python3 git node; do
|
||||||
@ -134,12 +203,30 @@ check() {
|
|||||||
else
|
else
|
||||||
echo "WARN: .agents/skills missing; run --openspec in the project" >&2
|
echo "WARN: .agents/skills missing; run --openspec in the project" >&2
|
||||||
fi
|
fi
|
||||||
|
local src="" rc=0 kind
|
||||||
|
src="$(superpowers_source)" || rc=$?
|
||||||
|
if [ "$rc" -eq 2 ]; then
|
||||||
|
report_invalid_explicit_source
|
||||||
|
failed=1
|
||||||
|
elif [ "$rc" -ne 0 ]; then
|
||||||
|
if [ -f "$DSH_HOME/skills/using-superpowers/SKILL.md" ]; then
|
||||||
|
echo "WARN: no superpowers source; installed skills in $DSH_HOME/skills are used" >&2
|
||||||
|
else
|
||||||
|
echo "WARN: no superpowers source; run: $CLONE_HINT" >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
kind="${src##*$'\t'}"
|
||||||
|
src="${src%%$'\t'*}"
|
||||||
|
if [ "$kind" = legacy ]; then
|
||||||
|
echo "WARN: legacy superpowers source ($src); prefer --superpowers-from or $DSH_HOME/superpowers" >&2
|
||||||
|
else
|
||||||
|
echo "ok: superpowers source ($src)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
if [ -f "$DSH_HOME/skills/using-superpowers/SKILL.md" ]; then
|
if [ -f "$DSH_HOME/skills/using-superpowers/SKILL.md" ]; then
|
||||||
echo "ok: dsh skills ($DSH_HOME/skills)"
|
echo "ok: dsh skills ($DSH_HOME/skills)"
|
||||||
elif superpowers_skills >/dev/null 2>&1; then
|
elif [ "$rc" -eq 0 ]; then
|
||||||
echo "ok: superpowers source ($(superpowers_skills)); run --skills to copy"
|
echo "WARN: superpowers skills are not copied yet; run --skills" >&2
|
||||||
else
|
|
||||||
echo "WARN: superpowers plugin not found; the bootstrap will be skipped" >&2
|
|
||||||
fi
|
fi
|
||||||
if [ -f "$DSH_HOME/skills/role/SKILL.md" ]; then
|
if [ -f "$DSH_HOME/skills/role/SKILL.md" ]; then
|
||||||
echo "ok: role skill"
|
echo "ok: role skill"
|
||||||
@ -190,14 +277,30 @@ install_global() {
|
|||||||
|
|
||||||
install_skills() {
|
install_skills() {
|
||||||
mkdir -p "$DSH_HOME/skills"
|
mkdir -p "$DSH_HOME/skills"
|
||||||
local skills
|
local line="" rc=0 source kind count=0
|
||||||
if ! skills="$(superpowers_skills)"; then
|
line="$(superpowers_source)" || rc=$?
|
||||||
echo "WARN: superpowers skills not found; nothing copied" >&2
|
if [ "$rc" -eq 2 ]; then
|
||||||
return 0
|
report_invalid_explicit_source
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
if [ -f "$DSH_HOME/skills/using-superpowers/SKILL.md" ]; then
|
||||||
|
echo "WARN: no superpowers source found; keeping installed skills in $DSH_HOME/skills" >&2
|
||||||
|
echo "WARN: next time run: $CLONE_HINT" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "FAIL: no superpowers source found and $DSH_HOME/skills has no using-superpowers" >&2
|
||||||
|
echo "FAIL: get the source once: $CLONE_HINT" >&2
|
||||||
|
echo "FAIL: or pass --superpowers-from <dir>; local skills (role, sdd) are installed by this run" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
kind="${line##*$'\t'}"
|
||||||
|
source="${line%%$'\t'*}"
|
||||||
|
if [ "$kind" = legacy ]; then
|
||||||
|
echo "WARN: legacy superpowers source $source; prefer --superpowers-from or $DSH_HOME/superpowers" >&2
|
||||||
fi
|
fi
|
||||||
local count=0
|
|
||||||
local dir name dest
|
local dir name dest
|
||||||
for dir in "$skills"/*; do
|
for dir in "$source"/*; do
|
||||||
[ -f "$dir/SKILL.md" ] || continue
|
[ -f "$dir/SKILL.md" ] || continue
|
||||||
name="$(basename "$dir")"
|
name="$(basename "$dir")"
|
||||||
dest="$DSH_HOME/skills/$name"
|
dest="$DSH_HOME/skills/$name"
|
||||||
@ -205,7 +308,7 @@ install_skills() {
|
|||||||
cp -R "$dir" "$dest"
|
cp -R "$dir" "$dest"
|
||||||
count=$((count + 1))
|
count=$((count + 1))
|
||||||
done
|
done
|
||||||
echo "copied $count superpowers skills into $DSH_HOME/skills"
|
echo "copied $count superpowers skills into $DSH_HOME/skills (source: $source)"
|
||||||
}
|
}
|
||||||
|
|
||||||
install_local_skills() {
|
install_local_skills() {
|
||||||
@ -321,7 +424,9 @@ if [ "$DO_GLOBAL" = 1 ]; then
|
|||||||
install_global
|
install_global
|
||||||
fi
|
fi
|
||||||
if [ "$DO_SKILLS" = 1 ]; then
|
if [ "$DO_SKILLS" = 1 ]; then
|
||||||
install_skills
|
# Local skills do not depend on superpowers: install them even when the
|
||||||
|
# superpowers source is missing (the FAIL is reported by the exit status).
|
||||||
|
install_skills || status=1
|
||||||
install_local_skills
|
install_local_skills
|
||||||
fi
|
fi
|
||||||
if [ "$DO_ROLES" = 1 ]; then
|
if [ "$DO_ROLES" = 1 ]; then
|
||||||
|
|||||||
@ -76,3 +76,206 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', ()
|
|||||||
function countNotes(text) {
|
function countNotes(text) {
|
||||||
return (text.match(/В DSH-сессии исполнение плана/g) ?? []).length
|
return (text.match(/В DSH-сессии исполнение плана/g) ?? []).length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeSuperpowers(root, body = 'body') {
|
||||||
|
for (const name of ['using-superpowers', 'brainstorming']) {
|
||||||
|
write(join(root, 'skills', name, 'SKILL.md'), `---\nname: ${name}\n---\n${body}\n`)
|
||||||
|
}
|
||||||
|
return root
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLegacyCache(home) {
|
||||||
|
const root = join(home, '.claude', 'plugins', 'cache', 'claude-plugins-official', 'superpowers', '7.0.0')
|
||||||
|
makeSuperpowers(root)
|
||||||
|
return join(root, 'skills')
|
||||||
|
}
|
||||||
|
|
||||||
|
function installerEnv(home) {
|
||||||
|
const env = { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh') }
|
||||||
|
delete env.DSH_SUPERPOWERS_SOURCE
|
||||||
|
return env
|
||||||
|
}
|
||||||
|
|
||||||
|
function runInstaller(home, args, extraEnv = {}) {
|
||||||
|
return spawnSync('bash', [script, ...args], {
|
||||||
|
env: { ...installerEnv(home), ...extraEnv },
|
||||||
|
encoding: 'utf8',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test('--skills takes superpowers from --superpowers-from in a clean HOME', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
const src = makeSuperpowers(join(home, 'src', 'superpowers'))
|
||||||
|
const run = runInstaller(home, ['--skills', '--superpowers-from', src])
|
||||||
|
const out = run.stdout + run.stderr
|
||||||
|
assert.equal(run.status, 0, out)
|
||||||
|
assert.match(run.stdout, /copied 2 superpowers skills/)
|
||||||
|
assert.ok(run.stdout.includes(`source: ${join(src, 'skills')}`), out)
|
||||||
|
assert.ok(existsSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')))
|
||||||
|
assert.ok(existsSync(join(home, '.dsh', 'skills', 'role', 'SKILL.md')))
|
||||||
|
assert.ok(existsSync(join(home, '.dsh', 'skills', 'sdd', 'SKILL.md')))
|
||||||
|
assert.equal(existsSync(join(home, '.claude')), false, 'clean HOME must stay clean')
|
||||||
|
assert.doesNotMatch(out, /legacy/)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('--superpowers-from accepts the skills directory itself', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
const src = makeSuperpowers(join(home, 'src', 'superpowers'))
|
||||||
|
const run = runInstaller(home, ['--skills', '--superpowers-from', join(src, 'skills')])
|
||||||
|
assert.equal(run.status, 0, run.stdout + run.stderr)
|
||||||
|
assert.match(run.stdout, /copied 2 superpowers skills/)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('--superpowers-from accepts a path with spaces', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
const src = makeSuperpowers(join(home, 'super powers'))
|
||||||
|
const run = runInstaller(home, ['--skills', '--superpowers-from', src])
|
||||||
|
assert.equal(run.status, 0, run.stdout + run.stderr)
|
||||||
|
assert.ok(existsSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')))
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('--superpowers-from wins over DSH_SUPERPOWERS_SOURCE', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
const fromFlag = makeSuperpowers(join(home, 'flag-source'), 'from-flag')
|
||||||
|
const fromEnv = makeSuperpowers(join(home, 'env-source'), 'from-env')
|
||||||
|
const run = runInstaller(home, ['--skills', '--superpowers-from', fromFlag], {
|
||||||
|
DSH_SUPERPOWERS_SOURCE: fromEnv,
|
||||||
|
})
|
||||||
|
assert.equal(run.status, 0, run.stdout + run.stderr)
|
||||||
|
assert.match(
|
||||||
|
readFileSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md'), 'utf8'),
|
||||||
|
/from-flag/,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('DSH_SUPERPOWERS_SOURCE is used when the flag is absent', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
const fromEnv = makeSuperpowers(join(home, 'env-source'), 'from-env')
|
||||||
|
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_SOURCE: fromEnv })
|
||||||
|
assert.equal(run.status, 0, run.stdout + run.stderr)
|
||||||
|
assert.match(
|
||||||
|
readFileSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md'), 'utf8'),
|
||||||
|
/from-env/,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('--skills fails when there is no source and nothing installed', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
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.match(out, /git clone --depth 1 https:\/\/github\.com\/obra\/superpowers/)
|
||||||
|
assert.ok(existsSync(join(home, '.dsh', 'skills', 'role', 'SKILL.md')), 'local skills still installed')
|
||||||
|
assert.ok(existsSync(join(home, '.dsh', 'skills', 'sdd', 'SKILL.md')), 'local skills still installed')
|
||||||
|
assert.equal(existsSync(join(home, '.dsh', 'skills', 'using-superpowers')), false)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('--skills keeps installed copies when the source is gone', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
const installed = join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')
|
||||||
|
write(installed, '---\nname: using-superpowers\n---\ninstalled\n')
|
||||||
|
const run = runInstaller(home, ['--skills'])
|
||||||
|
const out = run.stdout + run.stderr
|
||||||
|
assert.equal(run.status, 0, out)
|
||||||
|
assert.match(out, /WARN: no superpowers source/)
|
||||||
|
assert.match(readFileSync(installed, 'utf8'), /installed/)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('empty DSH_SUPERPOWERS_SOURCE counts as unset', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_SOURCE: '' })
|
||||||
|
const out = run.stdout + run.stderr
|
||||||
|
assert.equal(run.status, 1, out)
|
||||||
|
assert.match(out, /FAIL: no superpowers source/)
|
||||||
|
assert.doesNotMatch(out, /explicit superpowers source/)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('an invalid explicit source fails without touching the legacy cache', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
makeLegacyCache(home)
|
||||||
|
const empty = join(home, 'not-superpowers')
|
||||||
|
const run = runInstaller(home, ['--skills', '--superpowers-from', empty])
|
||||||
|
const out = run.stdout + run.stderr
|
||||||
|
assert.equal(run.status, 1, out)
|
||||||
|
assert.match(out, /FAIL: explicit superpowers source .* has no using-superpowers\/SKILL\.md/)
|
||||||
|
assert.equal(existsSync(join(home, '.dsh', 'skills', 'brainstorming')), false)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('the legacy plugin cache still works and warns', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
makeLegacyCache(home)
|
||||||
|
const run = runInstaller(home, ['--skills'])
|
||||||
|
const out = run.stdout + run.stderr
|
||||||
|
assert.equal(run.status, 0, out)
|
||||||
|
assert.match(out, /WARN: legacy superpowers source/)
|
||||||
|
assert.match(run.stdout, /copied 2 superpowers skills/)
|
||||||
|
assert.ok(existsSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')))
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('--check reports the pinned source', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
const pinned = join(home, '.dsh', 'superpowers')
|
||||||
|
makeSuperpowers(pinned)
|
||||||
|
const run = runInstaller(home, ['--check'])
|
||||||
|
const out = run.stdout + run.stderr
|
||||||
|
assert.equal(run.status, 0, out)
|
||||||
|
assert.match(out, /ok: superpowers source \(.*superpowers\/skills\)/)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('--check fails on an invalid explicit source', () => {
|
||||||
|
const home = tempDir()
|
||||||
|
try {
|
||||||
|
const empty = join(home, 'not-superpowers')
|
||||||
|
const run = runInstaller(home, ['--check'], { DSH_SUPERPOWERS_SOURCE: empty })
|
||||||
|
const out = run.stdout + run.stderr
|
||||||
|
assert.equal(run.status, 1, out)
|
||||||
|
assert.match(out, /FAIL: explicit superpowers source/)
|
||||||
|
} finally {
|
||||||
|
cleanup(home)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@ -84,7 +84,10 @@ Issue: #49. Спека: `openspec/changes/dsh-install-source/specs/dsh-install/s
|
|||||||
|
|
||||||
`--check` — диагностика, а не установка: отсутствие superpowers-источника
|
`--check` — диагностика, а не установка: отсутствие superpowers-источника
|
||||||
остаётся `WARN`, код возврата меняют только жёсткие `FAIL` (python3, git, node,
|
остаётся `WARN`, код возврата меняют только жёсткие `FAIL` (python3, git, node,
|
||||||
хуки, бандл). Так же уже сделано для openspec (#42, R-4).
|
хуки, бандл). Так же уже сделано для openspec (#42, R-4). Исключение —
|
||||||
|
невалидный **явный** источник (`--superpowers-from`/`DSH_SUPERPOWERS_SOURCE`):
|
||||||
|
это ошибка конфигурации пользователя, `--check` даёт `FAIL`, иначе опечатка
|
||||||
|
выглядит как «источника нет».
|
||||||
|
|
||||||
## 3. Формулировки, которые попадут в установленный AGENTS.md
|
## 3. Формулировки, которые попадут в установленный AGENTS.md
|
||||||
|
|
||||||
|
|||||||
@ -84,7 +84,8 @@ SHALL оставаться fallback'ом для машин, где `--skills --r
|
|||||||
источник, но `--skills` печатает `WARN: legacy superpowers source` с
|
источник, но `--skills` печатает `WARN: legacy superpowers source` с
|
||||||
подсказкой перейти на пиннинг/флаг.
|
подсказкой перейти на пиннинг/флаг.
|
||||||
- `--check`: legacy-источник — `WARN`, явный/пиннинг — `ok`, отсутствие —
|
- `--check`: legacy-источник — `WARN`, явный/пиннинг — `ok`, отсутствие —
|
||||||
`WARN` с подсказкой.
|
`WARN` с подсказкой, невалидный явный источник — `FAIL` и код 1
|
||||||
|
(пользователь задал его сам, подсказка «нет источника» здесь неверна).
|
||||||
- Runtime: bootstrap и резолвер ролей сохраняют порядок `~/.dsh` → `~/.claude`,
|
- Runtime: bootstrap и резолвер ролей сохраняют порядок `~/.dsh` → `~/.claude`,
|
||||||
но при выборе legacy-пути один раз за процесс пишут в лог предупреждение.
|
но при выборе legacy-пути один раз за процесс пишут в лог предупреждение.
|
||||||
- Удаление fallback'ов — отдельное изменение; в этом change они не удаляются.
|
- Удаление fallback'ов — отдельное изменение; в этом change они не удаляются.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user