Merge pull request 'fix(dsh): follow-ups #49 — legacy-роли тест, python3-guard, очистка скиллов, регрессия O-1 (#66)' (#84) from feat/66-followups-49 into main
This commit is contained in:
commit
ea63b3da60
@ -372,6 +372,10 @@ check() {
|
||||
}
|
||||
|
||||
install_global() {
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "FAIL: python3 is required for --global (AGENTS.md is generated from global/CLAUDE.md)" >&2
|
||||
return 1
|
||||
fi
|
||||
warn_mixed_dsh_home
|
||||
mkdir -p "$DSH_HOME"
|
||||
local target="$DSH_HOME/AGENTS.md"
|
||||
@ -494,16 +498,41 @@ sync_skills() {
|
||||
|
||||
# Copy every <dir>/SKILL.md under $1 into $DSH_HOME/skills (rm -rf dest, cp -R).
|
||||
# The optional label names where the skills came from in the report line.
|
||||
# $DSH_HOME/.superpowers-manifest remembers the installed superpowers set (one
|
||||
# name per line, sorted): a skill present in the previous manifest but missing
|
||||
# from the new set is removed from $DSH_HOME/skills. Local skills (role, sdd)
|
||||
# are installed by install_local_skills after this and never enter the manifest.
|
||||
copy_skills_from() {
|
||||
local src="$1" label="${2:-$1}" count=0 dir name dest
|
||||
local src="$1" label="${2:-$1}" count=0 dir name dest line
|
||||
local manifest="$DSH_HOME/.superpowers-manifest"
|
||||
local old=() new=()
|
||||
if [ -f "$manifest" ]; then
|
||||
while IFS= read -r line; do [ -n "$line" ] && old+=("$line"); done < "$manifest"
|
||||
fi
|
||||
for dir in "$src"/*; do
|
||||
[ -f "$dir/SKILL.md" ] || continue
|
||||
name="$(basename "$dir")"
|
||||
new+=("$name")
|
||||
dest="$DSH_HOME/skills/$name"
|
||||
rm -rf "$dest"
|
||||
cp -R "$dir" "$dest"
|
||||
count=$((count + 1))
|
||||
done
|
||||
local keep found
|
||||
for name in ${old[@]+"${old[@]}"}; do
|
||||
found=0
|
||||
for keep in ${new[@]+"${new[@]}"}; do
|
||||
if [ "$keep" = "$name" ]; then
|
||||
found=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ "$found" -eq 0 ]; then
|
||||
rm -rf "$DSH_HOME/skills/$name"
|
||||
echo "removed $DSH_HOME/skills/$name (no longer in the superpowers source)"
|
||||
fi
|
||||
done
|
||||
printf '%s\n' ${new[@]+"${new[@]}"} | sort -u > "$manifest"
|
||||
echo "copied $count superpowers skills into $DSH_HOME/skills (source: $label)"
|
||||
}
|
||||
|
||||
|
||||
@ -456,6 +456,44 @@ test('--skills syncs an explicit --superpowers-from source in a clean HOME', ()
|
||||
}
|
||||
})
|
||||
|
||||
test('--skills removes a skill that disappeared upstream (M-3)', () => {
|
||||
const work = tempDir()
|
||||
try {
|
||||
const home = join(work, 'home')
|
||||
const src = fixtureSuperpowers(home, null, join(home, 'src', 'superpowers'))
|
||||
const manifest = join(home, '.dsh', '.superpowers-manifest')
|
||||
const installed = join(home, '.dsh', 'skills')
|
||||
|
||||
const first = runInstaller(home, ['--skills', '--superpowers-from', src])
|
||||
assert.equal(first.status, 0, first.stdout + first.stderr)
|
||||
assert.match(first.stdout, /copied 15 superpowers skills/)
|
||||
assert.ok(existsSync(manifest), 'the superpowers manifest is written by --skills')
|
||||
assert.match(readFileSync(manifest, 'utf8'), /using-superpowers/)
|
||||
|
||||
// Simulate a skill the upstream no longer ships: it stays on disk and in
|
||||
// the old manifest, but the new set does not contain it. The sync itself
|
||||
// rejects a changed 15-skill upstream set, so the manifest is the only
|
||||
// place the install remembers the previous set.
|
||||
write(join(installed, 'extra-skill', 'SKILL.md'), '---\nname: extra-skill\n---\nbody\n')
|
||||
write(manifest, `${readFileSync(manifest, 'utf8')}extra-skill\n`)
|
||||
|
||||
const second = runInstaller(home, ['--skills', '--superpowers-from', src])
|
||||
assert.equal(second.status, 0, second.stdout + second.stderr)
|
||||
assert.match(second.stdout, /copied 15 superpowers skills/)
|
||||
assert.ok(
|
||||
second.stdout.includes(`removed ${join(installed, 'extra-skill')} (no longer in the superpowers source)`),
|
||||
second.stdout,
|
||||
)
|
||||
assert.equal(existsSync(join(installed, 'extra-skill')), false, 'the stale skill directory is removed')
|
||||
assert.ok(existsSync(join(installed, 'using-superpowers', 'SKILL.md')))
|
||||
assert.ok(existsSync(join(installed, 'brainstorming', 'SKILL.md')))
|
||||
assert.ok(existsSync(join(installed, 'role', 'SKILL.md')), 'local skills are not touched')
|
||||
assert.doesNotMatch(readFileSync(manifest, 'utf8'), /extra-skill/, 'the manifest reflects the new set')
|
||||
} finally {
|
||||
cleanup(work)
|
||||
}
|
||||
})
|
||||
|
||||
test('--superpowers-from accepts the skills directory itself', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
@ -641,6 +679,24 @@ test('--global installs DSH role paths and explains the instruction format', ()
|
||||
}
|
||||
})
|
||||
|
||||
test('--global fails without python3 and writes no truncated AGENTS.md (M-2)', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
const emptyBin = join(home, 'empty-bin')
|
||||
mkdirSync(emptyBin, { recursive: true })
|
||||
const run = spawnSync('/bin/bash', [script, '--global'], {
|
||||
env: { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh'), PATH: emptyBin },
|
||||
encoding: 'utf8',
|
||||
})
|
||||
const out = run.stdout + run.stderr
|
||||
assert.equal(run.status, 1, out)
|
||||
assert.match(out, /python3 is required/)
|
||||
assert.equal(existsSync(join(home, '.dsh', 'AGENTS.md')), false)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('--skills and --roles warn when DSH_HOME exists while HOME is isolated (O-2)', () => {
|
||||
const work = tempDir()
|
||||
try {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { apply } from '../bundle/index.js'
|
||||
@ -135,6 +136,34 @@ function recordingLogger(errors) {
|
||||
}
|
||||
}
|
||||
|
||||
test('apply warns once when roles resolve to a legacy Claude Code path (M-1)', () => {
|
||||
const warnings = []
|
||||
const ctx = {
|
||||
tools: { guard() {} },
|
||||
on() {},
|
||||
inject() {},
|
||||
workingDirectory: { get() {} },
|
||||
logger: {
|
||||
info() {},
|
||||
error() {},
|
||||
warn(fmt, ...args) {
|
||||
warnings.push(fmt.replace(/%s/g, () => String(args.shift() ?? '')))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
apply(ctx, { rolesDir: join(homedir(), '.claude', 'roles') })
|
||||
|
||||
assert.ok(
|
||||
warnings.some(
|
||||
(line) =>
|
||||
line.includes('legacy Claude Code source in use for roles') &&
|
||||
line.includes(join(homedir(), '.claude', 'roles')),
|
||||
),
|
||||
`no legacy-roles warning among: ${warnings.join(' | ')}`,
|
||||
)
|
||||
})
|
||||
|
||||
test('apply does not throw when ctx.on and ctx.inject are missing', () => {
|
||||
const calls = { guard: 0 }
|
||||
const errors = []
|
||||
|
||||
@ -26,6 +26,7 @@ assert_eq "вложенный worktree: git commit в нём" "$(rc "$W" Bash 'g
|
||||
assert_eq "вложенный worktree: запись в основной checkout на main" "$(rc "$W" Bash "echo x > $S/n/a.md")" 2
|
||||
assert_eq "вложенный worktree: Write в основной checkout рядом" "$(rc "$W" Write "$S/n/.claude/x.md")" 2
|
||||
assert_eq "git worktree add .claude/worktrees/… на main" "$(rc "$S/n" Bash 'git worktree add .claude/worktrees/y -b feat/y origin/main')" 0
|
||||
assert_eq "worktree add -b chore/x без start-point (O-1 #66)" "$(rc "$S/n" Bash 'git worktree add .claude/worktrees/o1 -b chore/x')" 0
|
||||
# цепочка создания worktree и записи в нём одной командой (#63, O-4): цель под новым путём — по новой ветке
|
||||
assert_eq "worktree add -b && cd && openspec archive (#63)" "$(rc "$S/n" Bash 'git worktree add .claude/worktrees/z -b feat/z origin/main && cd .claude/worktrees/z && openspec archive y -y')" 0
|
||||
assert_eq "worktree add -C && cd && openspec archive (#63)" "$(rc "$S/ws" Bash "git -C $S/n worktree add .claude/worktrees/q -b feat/q origin/main && cd $S/n/.claude/worktrees/q && openspec archive y -y")" 0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user