diff --git a/dsh/install.sh b/dsh/install.sh index 6baab09..d95fd5c 100755 --- a/dsh/install.sh +++ b/dsh/install.sh @@ -412,6 +412,15 @@ ensure_pinned_clone() { echo "FAIL: move it away and run: $CLONE_HINT" >&2 return 2 fi + # A foreign git repository must never be mutated by fetch/checkout: a real + # superpowers clone always carries skills/using-superpowers/SKILL.md (I-1). + # The marker also holds for the moved-tag flow — skills are present in + # every pinned clone. + if [ ! -f "$dir/skills/using-superpowers/SKILL.md" ]; then + echo "FAIL: $dir is not a superpowers clone (no skills/using-superpowers/SKILL.md); left untouched" >&2 + echo "FAIL: move it away or use --superpowers-from " >&2 + return 2 + fi # Online: refresh the pin. Fetching from $DSH_SUPERPOWERS_REPO (not from # the clone's origin) makes a repo change take effect on the next run # (T3.3); the force refspec updates the LOCAL tag as well as FETCH_HEAD, so diff --git a/dsh/tests/install.test.mjs b/dsh/tests/install.test.mjs index ae6b717..3f2d1ed 100644 --- a/dsh/tests/install.test.mjs +++ b/dsh/tests/install.test.mjs @@ -282,6 +282,40 @@ test('--skills removes a half-cloned dir after a failed fresh clone (RF5)', () = } }) +test('a foreign git repo in $DSH_HOME/superpowers is never touched (I-1)', () => { + const work = tempDir() + try { + const home = join(work, 'home') + const pinned = makePinnedRepo(work) + const foreign = join(home, '.dsh', 'superpowers') + mkdirSync(foreign, { recursive: true }) + git(foreign, ['init', '-b', 'main']) + git(foreign, ['config', 'user.email', 'dsh-harness@example.invalid']) + git(foreign, ['config', 'user.name', 'DSH Harness Test']) + write(join(foreign, 'witness.txt'), 'do not touch\n') + git(foreign, ['add', 'witness.txt']) + git(foreign, ['commit', '-m', 'foreign repo']) + const headBefore = git(foreign, ['rev-parse', 'HEAD']) + + const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: `file://${pinned}` }) + const out = run.stdout + run.stderr + assert.equal(run.status, 1, out) + assert.match(out, /not a superpowers clone/) + assert.match(out, /move it away or use --superpowers-from/) + assert.equal(readFileSync(join(foreign, 'witness.txt'), 'utf8'), 'do not touch\n') + assert.equal(git(foreign, ['rev-parse', 'HEAD']), headBefore, 'HEAD must not move') + assert.equal(existsSync(join(foreign, 'skills')), false, 'no skills tree may appear') + + // --check agrees: the foreign repo is not reported as a source. + const check = runInstaller(home, ['--check'], { DSH_SUPERPOWERS_REPO: `file://${pinned}` }) + const checkOut = check.stdout + check.stderr + assert.equal(check.status, 0, checkOut) + assert.doesNotMatch(checkOut, /ok: superpowers source/) + } finally { + cleanup(work) + } +}) + test('--skills reuses an on-ref clone when offline (WARN)', () => { const work = tempDir() try {