Compare commits
No commits in common. "db2ebd23d2c77b156e94a7eb80d61a18bfccbf7f" and "ec7c028f6b579a94443ad1cd83764eee54ce6e87" have entirely different histories.
db2ebd23d2
...
ec7c028f6b
@ -1,223 +0,0 @@
|
||||
# Install pulls superpowers from git — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** `dsh/install.sh --skills` сам клонирует и обновляет superpowers из git, оставляя явные источники и офлайн-кэш рабочими.
|
||||
|
||||
**Architecture:** В `install.sh` появляется git-шаг: клон `$DSH_HOME/superpowers` из `$DSH_SUPERPOWERS_REPO` (по умолчанию GitHub) или `fetch --depth 1` + `reset --hard FETCH_HEAD` для существующего клона. Резолвер для `--check` отдельный и офлайновый. Тесты используют локальный `file://`-репозиторий.
|
||||
|
||||
**Tech Stack:** bash (`set -euo pipefail`), git CLI, Node.js ESM (node:test), OpenSpec.
|
||||
|
||||
**Spec:** `openspec/changes/dsh-superpowers-git/specs/dsh-install/spec.md` (дизайн — `design.md`).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Порядок источников: `--superpowers-from` → `$DSH_SUPERPOWERS_SOURCE` → git (`$DSH_HOME/superpowers`) → legacy-кэш CC → `~/.claude/skills`.
|
||||
- Явный валидный источник MUST NOT запускать git и не создавать `$DSH_HOME/superpowers`.
|
||||
- Репозиторий: `$DSH_SUPERPOWERS_REPO`, default `https://github.com/obra/superpowers`; глубина 1; обновление `fetch --depth 1 origin` + `reset --hard FETCH_HEAD`.
|
||||
- Фактический вывод `--skills` для git: `(source: <dir>, git <short-sha>)`.
|
||||
- `--check` MUST NOT клонировать/обновлять; показывает `ok: superpowers source (<dir>, git <sha>)`.
|
||||
- Неудачный первый клон MUST NOT оставлять каталог: `rm -rf $DSH_HOME/superpowers`.
|
||||
- Ручная копия `$DSH_HOME/superpowers` без `.git` — `pinned`, без обновления, с `WARN`.
|
||||
- Тесты не выходят в сеть: `installerEnv` задаёт недоступный `DSH_SUPERPOWERS_REPO`, git-тесты — `file://`-фикстуру.
|
||||
- Маркировка: `T<n>.<k>` (ревью задачи), `C/I/M` (ревью ветки), `R-n`/`O-n` (рулинги/наблюдения).
|
||||
|
||||
## Review Focus
|
||||
|
||||
- Клон падает на середине: каталог не остаётся, следующий запуск клонирует заново.
|
||||
- Офлайн при существующем клоне: `WARN`, копии из кэша, код 0 (не FAIL).
|
||||
- Ручная копия без `.git`: используется, `WARN` об отсутствии обновления, копии на месте.
|
||||
- Явный источник при пустом `DSH_HOME`: git не вызывается, `$DSH_HOME/superpowers` не появляется.
|
||||
- `--check` на пустом HOME с недоступным репозиторием: `WARN`, код 0, клон не создан.
|
||||
- Путь `$DSH_HOME` с пробелами: клон в кавычках, sha/копирование работают.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: git-источник в `--skills`
|
||||
|
||||
**Files:**
|
||||
- Modify: `dsh/install.sh` (`CLONE_HINT`, `git_superpowers_source`, `superpowers_source`, `install_skills`, `usage`)
|
||||
- Modify: `dsh/tests/install.test.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `git_superpowers_source() -> rc 0` и stdout `<skills-dir>\tgit:<sha>`; `rc 1` без источника (печатает `WARN`/`FAIL` причину);
|
||||
- `superpowers_source()` — прежний контракт `<dir>\t<explicit|pinned|legacy|git:<sha>>`;
|
||||
- `install_skills` печатает `copied N superpowers skills into <dest> (source: <dir>, git <sha>)` для git и `(source: <dir>)` иначе.
|
||||
- Consumes: `normalize_superpowers_dir`, `$DSH_HOME`, `$DSH_SUPERPOWERS_REPO`.
|
||||
|
||||
- [ ] **Step 1: Тестовый хелпер и падающие тесты**
|
||||
|
||||
В `dsh/tests/install.test.mjs` импортировать `mkdirSync` из `node:fs`, `git` из `./helpers.mjs`, `pathToFileURL` из `node:url`; добавить:
|
||||
|
||||
```js
|
||||
function makeSuperpowersRepo(home, name = 'superpowers-src') {
|
||||
const repo = join(home, name)
|
||||
mkdirSync(repo, { recursive: true })
|
||||
git(repo, ['init', '-b', 'main'])
|
||||
git(repo, ['config', 'user.email', 'test@example.invalid'])
|
||||
git(repo, ['config', 'user.name', 'Test'])
|
||||
makeSuperpowers(repo)
|
||||
git(repo, ['add', '.'])
|
||||
git(repo, ['commit', '-m', 'init'])
|
||||
return repo
|
||||
}
|
||||
```
|
||||
|
||||
`installerEnv` дополнительно: `delete env.DSH_SUPERPOWERS_REPO` и
|
||||
`env.DSH_SUPERPOWERS_REPO = join(home, 'no-superpowers-repo')` (недоступный путь — сеть в тестах не нужна). Git-тесты переопределяют его через `file://`-URL.
|
||||
|
||||
Тесты:
|
||||
|
||||
```js
|
||||
test('--skills clones the git source on first run', ...)
|
||||
// home; repo = makeSuperpowersRepo(home); env repo = pathToFileURL(repo).href;
|
||||
// run --skills; status 0; <dshHome>/superpowers/.git существует;
|
||||
// stdout /copied 2 superpowers skills/ и new RegExp(`git ${git(repo,['rev-parse','--short','HEAD'])}`);
|
||||
// skills/using-superpowers на месте
|
||||
|
||||
test('--skills updates an existing clone to the new commit', ...)
|
||||
// после первого прогона: write(join(repo,'skills','new-skill','SKILL.md'), ...);
|
||||
// git add/commit; повторить --skills; status 0;
|
||||
// <dshHome>/skills/new-skill/SKILL.md существует; stdout содержит новый short-sha
|
||||
|
||||
test('an offline update keeps the cached clone', ...)
|
||||
// после клона: env repo = join(home,'gone'); run --skills; status 0;
|
||||
// stdout/stderr /WARN: cannot update superpowers/; /copied 2 superpowers skills/
|
||||
|
||||
test('a failed first clone leaves no checkout', ...)
|
||||
// пустой home (repo недоступен); run --skills; status 1;
|
||||
// <dshHome>/superpowers НЕ существует; FAIL упоминает no-superpowers-repo и --superpowers-from
|
||||
|
||||
test('an explicit source never touches git', ...)
|
||||
// --skills --superpowers-from <fixture>; status 0; <dshHome>/superpowers НЕ существует
|
||||
|
||||
test('a manual non-git checkout is used but not updated', ...)
|
||||
// write <dshHome>/superpowers/skills/using-superpowers/SKILL.md; repo недоступен;
|
||||
// run --skills; status 0; stderr /not a git checkout/; /copied 1 superpowers skills/
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Прогнать, увидеть падение**
|
||||
|
||||
Run: `node --test dsh/tests/install.test.mjs`
|
||||
Expected: новые тесты падают (нет клона/обновления/сообщений).
|
||||
|
||||
- [ ] **Step 3: Реализовать в `dsh/install.sh`**
|
||||
|
||||
- `SUPERPOWERS_REPO="${DSH_SUPERPOWERS_REPO:-https://github.com/obra/superpowers}"` рядом с `CLONE_HINT`; `CLONE_HINT` переписать на `git clone --depth 1 "$SUPERPOWERS_REPO" "$DSH_HOME/superpowers"`.
|
||||
- `git_superpowers_source` по описанию из Interfaces: git-клон → fetch+reset → `normalize_superpowers_dir`; на неудачном клоне `rm -rf "$DSH_HOME/superpowers"` и `return 1`; на неудачном fetch — `WARN` и работа из кэша.
|
||||
- `superpowers_source`: после явных источников вызвать `git_superpowers_source`; её успех вернуть как есть, `rc 1` — продолжить на legacy-кэш и `~/.claude/skills`.
|
||||
- `install_skills`: разобрать kind `git:<sha>` → в итоговой строке `(source: $source, git $sha)`; `FAIL`-сообщение — репозиторий и `--superpowers-from` вместо ручного клона.
|
||||
- `usage`: пункт 3 — git-клон `$SUPERPOWERS_REPO` → `$DSH_HOME/superpowers`, обновляется при каждом `--skills`.
|
||||
|
||||
- [ ] **Step 4: Прогнать тесты**
|
||||
|
||||
Run: `node --test dsh/tests/install.test.mjs`
|
||||
Expected: PASS всех тестов файла.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add dsh/install.sh dsh/tests/install.test.mjs
|
||||
git commit -m "feat(dsh): install pulls superpowers from git (#67)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: офлайновый `--check` с git-версией
|
||||
|
||||
**Files:**
|
||||
- Modify: `dsh/install.sh` (`static_superpowers_source`, блок `check`)
|
||||
- Modify: `dsh/tests/install.test.mjs`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `static_superpowers_source()` — тот же формат, но без сети: явные источники → существующий git-клон (`git:<sha>`) → ручная копия (`pinned`) → legacy; `--check` печатает `ok: superpowers source (<dir>, git <sha>)`, `WARN` для `pinned`/legacy/отсутствия, `FAIL` для невалидного явного.
|
||||
|
||||
- [ ] **Step 1: Падающие тесты**
|
||||
|
||||
```js
|
||||
test('--check reports the git sha and does not clone', ...)
|
||||
// сначала --skills с file://-репозиторием (клон создан);
|
||||
// затем env repo = недоступный путь; run --check;
|
||||
// stdout /ok: superpowers source \(.*superpowers\/skills, git [0-9a-f]+\)/; status 0
|
||||
|
||||
test('--check never clones on an empty HOME', ...)
|
||||
// empty home, repo недоступен; run --check; status 0;
|
||||
// <dshHome>/superpowers НЕ существует; stderr /WARN: no superpowers source/
|
||||
|
||||
test('--check warns about a manual non-git checkout', ...)
|
||||
// write <dshHome>/superpowers/skills/using-superpowers/SKILL.md;
|
||||
// run --check; stdout/stderr /not a git checkout/
|
||||
```
|
||||
|
||||
Существующий тест `--check reports the pinned source` заменить этим кейсом (ручная копия больше не `ok`), сценарий спеки #49 обновлён в change.
|
||||
|
||||
- [ ] **Step 2: Прогнать, увидеть падение**
|
||||
|
||||
Run: `node --test dsh/tests/install.test.mjs`
|
||||
Expected: FAIL на новых и переименованном тестах.
|
||||
|
||||
- [ ] **Step 3: Реализовать**
|
||||
|
||||
- Вынести `static_superpowers_source` (без вызова git-команд, меняющих состояние; `rev-parse --short HEAD` — чтение).
|
||||
- `check()` переключить с `superpowers_source` на `static_superpowers_source`; разобрать kind: `git:<sha>` → `ok: superpowers source ($src, git $sha)`, `pinned` → `WARN: ... is not a git checkout; --skills will not update it`, `legacy` → `WARN`, нет — `WARN` с `$SUPERPOWERS_REPO` и `--superpowers-from`.
|
||||
- `--check` не должен создавать `$DSH_HOME/superpowers` и не вызывать `git clone`/`fetch`.
|
||||
|
||||
- [ ] **Step 4: Прогнать тесты**
|
||||
|
||||
Run: `bash dsh/tests/run.sh`
|
||||
Expected: PASS всего набора.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add dsh/install.sh dsh/tests/install.test.mjs
|
||||
git commit -m "feat(dsh): offline --check reports the git source (#67)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: документация, приёмка, PR
|
||||
|
||||
**Files:**
|
||||
- Modify: `dsh/README.md` (Quick start, Known limits, config)
|
||||
- Modify: `dsh/PORT.md` (§5 install, §7 risks, §8 phases)
|
||||
- Modify: `openspec/changes/dsh-superpowers-git/design.md` (раздел приёмки)
|
||||
|
||||
- [ ] **Step 1: README/PORT**
|
||||
|
||||
Quick start: убрать ручной `git clone`, оставить `bash dsh/install.sh --global --skills --roles`; описать `DSH_SUPERPOWERS_REPO`, офлайн-кэш и `--superpowers-from`. Known limits: свежий апстрим тянется при каждом `--skills`; ручная копия без `.git` не обновляется. PORT §5: git-источник и sha; §7: сеть в установке и откат на `--superpowers-from`; §8: #67.
|
||||
|
||||
- [ ] **Step 2: Полный прогон и приёмка**
|
||||
|
||||
```bash
|
||||
bash dsh/tests/run.sh
|
||||
bash tests/run.sh
|
||||
openspec validate dsh-superpowers-git --strict
|
||||
```
|
||||
|
||||
Живой прогон на машине владельца (после merge): `bash dsh/install.sh --skills`
|
||||
→ клон GitHub, `--check` → `git <sha>`, повторный `--skills` → обновление.
|
||||
В design.md записать `### Приёмка: прогон <дата>` с результатами.
|
||||
|
||||
- [ ] **Step 3: Commit, push, PR**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "docs(dsh): git source для superpowers (#67)"
|
||||
git push -u origin feat/67-dsh-superpowers-git
|
||||
tea pulls create --head feat/67-dsh-superpowers-git --base main \
|
||||
--title "DSH: install сам тянет свежий superpowers с git (#67)" \
|
||||
--description-file <подготовленный файл>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Ревью ветки и итог**
|
||||
|
||||
Свежий ревьюер (`deepseek-v4-pro`) по `review-package`; Critical/Important → один fix pass по TDD; Minor → ledger и PR; итоговая таблица маркеров.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Спека `dsh-install` (delta) покрыта тестами: клон, обновление, офлайн-кэш, неудачный клон, явный источник без сети, ручная копия, `--check` без сети и с sha.
|
||||
- `bash dsh/tests/run.sh`, `bash tests/run.sh`, `openspec validate --strict` — зелёные.
|
||||
- Тесты не обращаются к GitHub: единственный источник — `file://`-фикстура или недоступный путь.
|
||||
- После merge: `--skills` на машине владельца тянет GitHub и убирает `WARN: legacy superpowers source`.
|
||||
38
dsh/PORT.md
38
dsh/PORT.md
@ -210,14 +210,11 @@ with "requires ... in the Host scope".
|
||||
real files there; the Claude Code locations are a legacy fallback and the
|
||||
bundle logs one warning per process when it resolves one of them.
|
||||
|
||||
Install-time source resolution is separate and stricter (changes
|
||||
`dsh-install-source` and `dsh-superpowers-git`): `--superpowers-from` →
|
||||
`$DSH_SUPERPOWERS_SOURCE` → the managed git checkout `$DSH_HOME/superpowers`
|
||||
(clone or `fetch`+`reset` from `$DSH_SUPERPOWERS_REPO`) → the legacy cache →
|
||||
`~/.claude/skills`. An invalid explicit source is a hard `FAIL`; a failed git
|
||||
update reuses the cached checkout (`WARN`); no source at all is a `FAIL` on a
|
||||
fresh install and a `WARN` when `~/.dsh/skills` already holds the skills. The
|
||||
git sha of the checkout is printed by `--skills` and by the offline `--check`.
|
||||
Install-time source resolution is separate and stricter (change
|
||||
`dsh-install-source`): `--superpowers-from` → `$DSH_SUPERPOWERS_SOURCE` →
|
||||
`$DSH_HOME/superpowers/skills` → the legacy cache → `~/.claude/skills`. An
|
||||
invalid explicit source is a hard `FAIL`; no source at all is a `FAIL` on a
|
||||
fresh install and a `WARN` when `~/.dsh/skills` already holds the skills.
|
||||
|
||||
### OpenSpec skills
|
||||
|
||||
@ -246,21 +243,20 @@ openspec archive <id> -y
|
||||
|
||||
## 6. Install and verify
|
||||
|
||||
Prerequisites: the `claude-harness` checkout, DSH 0.2.1-rc.3, `python3`, `git`,
|
||||
Node, and network access for the superpowers checkout. Claude Code is not
|
||||
required at all: `--skills` clones `https://github.com/obra/superpowers` into
|
||||
`~/.dsh/superpowers` on the first run and updates it on later runs
|
||||
(`DSH_SUPERPOWERS_REPO` for a mirror); `--superpowers-from` /
|
||||
`DSH_SUPERPOWERS_SOURCE` skip the network. `--roles` copies roles; the hooks
|
||||
come from this checkout.
|
||||
Prerequisites: the `claude-harness` checkout, a superpowers source, DSH
|
||||
0.2.1-rc.3, `python3`, `git`, Node. Claude Code is not required at all: get the
|
||||
superpowers source once with
|
||||
`git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers`,
|
||||
then `--skills --roles` copy everything into `~/.dsh`; the hooks come from this
|
||||
checkout.
|
||||
|
||||
```bash
|
||||
# 1. Static checks
|
||||
bash dsh/install.sh --check
|
||||
|
||||
# 2. Global instructions, skills, and roles (run from a terminal; writes
|
||||
# outside the session workspace). --skills clones/updates ~/.dsh/superpowers;
|
||||
# --superpowers-from or DSH_SUPERPOWERS_SOURCE skip the network.
|
||||
# outside the session workspace). The superpowers source comes from
|
||||
# --superpowers-from, DSH_SUPERPOWERS_SOURCE, or ~/.dsh/superpowers.
|
||||
bash dsh/install.sh --global
|
||||
bash dsh/install.sh --skills # or: --skills --superpowers-from DIR
|
||||
bash dsh/install.sh --roles
|
||||
@ -365,11 +361,6 @@ profile was removed; the real `desktop` profile was not touched.
|
||||
- **Model tiers.** `sonnet`/`haiku`/`opus` are not DSH models. The port uses two
|
||||
live routes (`deepseek-flash`, `deepseek-v4-pro`) from the opt-in overlay in
|
||||
`dsh/subagent-models.yml`; without it every dispatch runs on the session model.
|
||||
- **Install needs network (or a source).** `--skills` clones/fetches from
|
||||
`DSH_SUPERPOWERS_REPO`; on an air-gapped or rate-limited machine use a mirror
|
||||
or `--superpowers-from`. The checkout is shallow and updated by
|
||||
`fetch`+`reset --hard`, so local edits inside it are lost; a fresh upstream
|
||||
commit is taken as-is, and `--superpowers-from` pins a known-good copy.
|
||||
- **`~/.claude` dependence.** No longer needed for install, bootstrap, or
|
||||
roles: the install-time source is explicit, and `--skills --roles` copy real
|
||||
files into `~/.dsh`. The Claude Code locations remain a **legacy migration
|
||||
@ -407,6 +398,3 @@ profile was removed; the real `desktop` profile was not touched.
|
||||
`--superpowers-from`/`DSH_SUPERPOWERS_SOURCE`/`~/.dsh/superpowers`, hard
|
||||
`FAIL` instead of a silent empty bootstrap, DSH-owned role paths in the
|
||||
installed `AGENTS.md`, legacy warnings at install and runtime.
|
||||
10. Install pulls superpowers from git — **done** (change `dsh-superpowers-git`):
|
||||
clone/update `$DSH_HOME/superpowers` from `DSH_SUPERPOWERS_REPO`, `--check`
|
||||
stays offline and reports the sha, explicit sources skip the network.
|
||||
|
||||
@ -33,19 +33,19 @@ risks.
|
||||
# Static 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
|
||||
# (run from a terminal; these write outside the session workspace).
|
||||
# --skills clones https://github.com/obra/superpowers into ~/.dsh/superpowers
|
||||
# on the first run and updates that checkout on every later run.
|
||||
# (run from a terminal; these write outside the session workspace)
|
||||
bash dsh/install.sh --global
|
||||
bash dsh/install.sh --skills
|
||||
bash dsh/install.sh --roles
|
||||
|
||||
# Offline, mirror, or pinned version: skip the network entirely.
|
||||
bash dsh/install.sh --skills --superpowers-from /path/to/superpowers
|
||||
# or: DSH_SUPERPOWERS_SOURCE=/path/to/superpowers bash dsh/install.sh --skills
|
||||
# or: DSH_SUPERPOWERS_REPO=https://mirror/superpowers.git bash dsh/install.sh --skills
|
||||
|
||||
# В корне проекта, где уже есть openspec/: добавить agents-скиллы
|
||||
bash <harness>/dsh/install.sh --openspec
|
||||
|
||||
@ -55,11 +55,10 @@ bash <harness>/dsh/install.sh --openspec
|
||||
```
|
||||
|
||||
`--skills` resolves superpowers in this order: `--superpowers-from` →
|
||||
`$DSH_SUPERPOWERS_SOURCE` → the git checkout `~/.dsh/superpowers` (clone or
|
||||
`fetch`+`reset` from `$DSH_SUPERPOWERS_REPO`) → the Claude Code plugin cache
|
||||
(legacy, warns). With no network and no other source, an existing checkout is
|
||||
reused with a warning and a fresh install fails instead of pretending success;
|
||||
the local `role` and `sdd` skills are copied either way.
|
||||
`$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
|
||||
`cordis.patch.yml`: the Desktop profile is Electron-managed and the plugin
|
||||
@ -157,13 +156,11 @@ repository layout:
|
||||
bundle needs a real copy of `hooks/` (already listed in `package.json`
|
||||
`files`).
|
||||
- `using-superpowers` is read from `~/.dsh/skills` after `--skills`. The
|
||||
install-time source is `--superpowers-from`, `DSH_SUPERPOWERS_SOURCE`, or the
|
||||
managed git checkout `~/.dsh/superpowers` (`DSH_SUPERPOWERS_REPO`, default
|
||||
GitHub), which `--skills` clones/updates and reports as `git <sha>`. `--check`
|
||||
is offline and reports that sha. A manual copy of `~/.dsh/superpowers` without
|
||||
`.git` is used as-is and never updated. Claude Code locations are a **legacy
|
||||
migration fallback** and log one warning per process when used. Set
|
||||
`bootstrapSkillPath` or `DSH_SUPERPOWERS_SKILL` for another layout.
|
||||
install-time source is `--superpowers-from`, `DSH_SUPERPOWERS_SOURCE`, or
|
||||
`~/.dsh/superpowers`; Claude Code locations are a **legacy migration
|
||||
fallback** and log one warning per process when used, so a partial Claude
|
||||
Code rollback does not break the bootstrap. Set `bootstrapSkillPath` or
|
||||
`DSH_SUPERPOWERS_SKILL` for another layout.
|
||||
- Roles resolve `~/.dsh/roles` first; `~/.claude/roles` and the repository
|
||||
`roles/` are fallbacks. After `--roles` the runtime never needs `~/.claude`.
|
||||
- Role loading is model-driven through the `role` skill; there is no automatic
|
||||
|
||||
164
dsh/install.sh
164
dsh/install.sh
@ -29,8 +29,7 @@ DO_ROLES=0
|
||||
DO_OPENSPEC=0
|
||||
SUPERPOWERS_FROM=""
|
||||
SUPERPOWERS_FROM_SET=0
|
||||
SUPERPOWERS_REPO="${DSH_SUPERPOWERS_REPO:-https://github.com/obra/superpowers}"
|
||||
CLONE_HINT="git clone --depth 1 $SUPERPOWERS_REPO $DSH_HOME/superpowers"
|
||||
CLONE_HINT="git clone --depth 1 https://github.com/obra/superpowers $DSH_HOME/superpowers"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
@ -45,14 +44,11 @@ Usage: bash dsh/install.sh [--check] [--global] [--skills] [--roles] [--all] [--
|
||||
--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:
|
||||
Superpowers source, first match wins. The script never downloads anything:
|
||||
1. --superpowers-from DIR
|
||||
2. \$DSH_SUPERPOWERS_SOURCE
|
||||
3. git: clone or update \$DSH_HOME/superpowers from \$DSH_SUPERPOWERS_REPO
|
||||
(default: https://github.com/obra/superpowers); no network when 1 or 2 is used
|
||||
3. \$DSH_HOME/superpowers -- get it once: $CLONE_HINT
|
||||
4. legacy: the Claude Code plugin cache, then ~/.claude/skills (warns)
|
||||
Offline: an existing git checkout is reused with a warning; a manual copy in
|
||||
\$DSH_HOME/superpowers without .git is used as-is and is never updated.
|
||||
|
||||
Bundle installation itself is done in DSH:
|
||||
Plugins -> install a bundle from $BUNDLE
|
||||
@ -114,62 +110,8 @@ normalize_superpowers_dir() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Update (or create) the managed git checkout and print "<dir>\tgit:<sha>".
|
||||
# A failed update keeps the cached checkout with a WARN; a failed first clone
|
||||
# removes the directory and returns 1 so the caller can try legacy sources.
|
||||
git_superpowers_source() {
|
||||
local target="$DSH_HOME/superpowers"
|
||||
local dir sha
|
||||
if ! command -v git >/dev/null 2>&1; then
|
||||
echo "WARN: git not found; cannot fetch superpowers from $SUPERPOWERS_REPO" >&2
|
||||
return 1
|
||||
fi
|
||||
if [ -d "$target/.git" ]; then
|
||||
# The configured repo must win over the URL recorded at clone time: a mirror
|
||||
# switch (or a test fixture) has to affect an existing checkout as well.
|
||||
local origin_url
|
||||
origin_url="$(git -C "$target" remote get-url origin 2>/dev/null || true)"
|
||||
if [ "$origin_url" != "$SUPERPOWERS_REPO" ]; then
|
||||
git -C "$target" remote set-url origin "$SUPERPOWERS_REPO" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if ! { git -C "$target" fetch --depth 1 origin >/dev/null 2>&1 \
|
||||
&& git -C "$target" reset --hard FETCH_HEAD >/dev/null 2>&1; }; then
|
||||
echo "WARN: cannot update superpowers ($SUPERPOWERS_REPO); using the cached checkout $target" >&2
|
||||
fi
|
||||
elif [ -d "$target" ]; then
|
||||
# A manual copy from dsh-install-source (#49): usable, but not ours to update.
|
||||
if dir="$(normalize_superpowers_dir "$target")"; then
|
||||
echo "WARN: $target is not a git checkout; it will not be updated (use --superpowers-from to pin, or replace it with a clone)" >&2
|
||||
printf '%s\tpinned\n' "$dir"
|
||||
return 0
|
||||
fi
|
||||
echo "WARN: $target is not a git checkout and has no using-superpowers/SKILL.md" >&2
|
||||
return 1
|
||||
else
|
||||
mkdir -p "$(dirname "$target")"
|
||||
if ! git clone --depth 1 "$SUPERPOWERS_REPO" "$target" >/dev/null 2>&1; then
|
||||
rm -rf "$target"
|
||||
echo "WARN: git clone $SUPERPOWERS_REPO failed (offline?); no managed checkout" >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if ! dir="$(normalize_superpowers_dir "$target")"; then
|
||||
echo "WARN: $target has no using-superpowers/SKILL.md after update" >&2
|
||||
return 1
|
||||
fi
|
||||
sha="$(git -C "$target" rev-parse --short HEAD 2>/dev/null || true)"
|
||||
if [ -n "$sha" ]; then
|
||||
printf '%s\tgit:%s\n' "$dir" "$sha"
|
||||
else
|
||||
printf '%s\tgit\n' "$dir"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Network-capable resolver used by --skills. Explicit sources win and never
|
||||
# touch git; then the managed checkout; then legacy Claude Code locations.
|
||||
# Print "<skills-dir>\t<kind>". Returns 2 when an explicitly configured source
|
||||
# is invalid (never fall back silently), 1 when none is found.
|
||||
# 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
|
||||
@ -182,7 +124,8 @@ superpowers_source() {
|
||||
printf '%s\texplicit\n' "$dir"
|
||||
return 0
|
||||
fi
|
||||
if git_superpowers_source; then
|
||||
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)"
|
||||
@ -200,45 +143,6 @@ superpowers_source() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Offline resolver used by --check: it never clones, fetches or resets.
|
||||
# Same output format, plus git:<sha> for a managed checkout.
|
||||
static_superpowers_source() {
|
||||
local dir legacy sha target="$DSH_HOME/superpowers"
|
||||
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 "$target")"; then
|
||||
if [ -d "$target/.git" ]; then
|
||||
sha="$(git -C "$target" rev-parse --short HEAD 2>/dev/null || true)"
|
||||
if [ -n "$sha" ]; then
|
||||
printf '%s\tgit:%s\n' "$dir" "$sha"
|
||||
else
|
||||
printf '%s\tgit\n' "$dir"
|
||||
fi
|
||||
else
|
||||
printf '%s\tpinned\n' "$dir"
|
||||
fi
|
||||
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" ] && dir="$(normalize_superpowers_dir "$legacy")"; then
|
||||
printf '%s\tlegacy\n' "$dir"
|
||||
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
|
||||
@ -314,9 +218,8 @@ check() {
|
||||
else
|
||||
echo "WARN: .agents/skills missing; run --openspec in the project" >&2
|
||||
fi
|
||||
local src="" rc=0 kind sha
|
||||
# --check must stay offline: this resolver reads the existing checkout only.
|
||||
src="$(static_superpowers_source)" || rc=$?
|
||||
local src="" rc=0 kind
|
||||
src="$(superpowers_source)" || rc=$?
|
||||
if [ "$rc" -eq 2 ]; then
|
||||
report_invalid_explicit_source
|
||||
failed=1
|
||||
@ -324,29 +227,16 @@ check() {
|
||||
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 --skills to clone $SUPERPOWERS_REPO" >&2
|
||||
echo "WARN: no superpowers source; run: $CLONE_HINT" >&2
|
||||
fi
|
||||
else
|
||||
kind="${src##*$'\t'}"
|
||||
src="${src%%$'\t'*}"
|
||||
case "$kind" in
|
||||
legacy)
|
||||
echo "WARN: legacy superpowers source ($src); prefer a git checkout or --superpowers-from" >&2
|
||||
;;
|
||||
pinned)
|
||||
echo "WARN: superpowers source $src is not a git checkout; --skills will not update it" >&2
|
||||
;;
|
||||
git:*)
|
||||
sha="${kind#git:}"
|
||||
echo "ok: superpowers source ($src, git $sha)"
|
||||
;;
|
||||
git)
|
||||
echo "ok: superpowers source ($src, git)"
|
||||
;;
|
||||
*)
|
||||
echo "ok: superpowers source ($src)"
|
||||
;;
|
||||
esac
|
||||
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
|
||||
echo "ok: dsh skills ($DSH_HOME/skills)"
|
||||
@ -420,25 +310,19 @@ install_skills() {
|
||||
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 --skills (git source: $SUPERPOWERS_REPO) or pass --superpowers-from" >&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: git source $SUPERPOWERS_REPO is unavailable; pass --superpowers-from <dir> or set DSH_SUPERPOWERS_REPO" >&2
|
||||
echo "FAIL: local skills (role, sdd) are installed by this run" >&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'*}"
|
||||
local sha=""
|
||||
case "$kind" in
|
||||
legacy)
|
||||
echo "WARN: legacy superpowers source $source; prefer a git checkout or --superpowers-from" >&2
|
||||
;;
|
||||
git:*)
|
||||
sha="${kind#git:}"
|
||||
;;
|
||||
esac
|
||||
if [ "$kind" = legacy ]; then
|
||||
echo "WARN: legacy superpowers source $source; prefer --superpowers-from or $DSH_HOME/superpowers" >&2
|
||||
fi
|
||||
local dir name dest
|
||||
for dir in "$source"/*; do
|
||||
[ -f "$dir/SKILL.md" ] || continue
|
||||
@ -448,11 +332,7 @@ install_skills() {
|
||||
cp -R "$dir" "$dest"
|
||||
count=$((count + 1))
|
||||
done
|
||||
if [ -n "$sha" ]; then
|
||||
echo "copied $count superpowers skills into $DSH_HOME/skills (source: $source, git $sha)"
|
||||
else
|
||||
echo "copied $count superpowers skills into $DSH_HOME/skills (source: $source)"
|
||||
fi
|
||||
echo "copied $count superpowers skills into $DSH_HOME/skills (source: $source)"
|
||||
}
|
||||
|
||||
install_local_skills() {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { existsSync, lstatSync, mkdirSync, readFileSync } from 'node:fs'
|
||||
import { existsSync, lstatSync, readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { cleanup, git, tempDir, write } from './helpers.mjs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { cleanup, tempDir, write } from './helpers.mjs'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const script = join(here, '..', 'install.sh')
|
||||
@ -26,12 +26,7 @@ test('install.sh copies skills and roles instead of linking into ~/.claude', ()
|
||||
write(join(cache, 'using-superpowers', 'SKILL.md'), '---\nname: using-superpowers\n---\nbody\n')
|
||||
write(join(cache, 'brainstorming', 'SKILL.md'), '---\nname: brainstorming\n---\nbody\n')
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: home,
|
||||
DSH_HOME: dshHome,
|
||||
DSH_SUPERPOWERS_REPO: join(home, 'no-superpowers-repo'),
|
||||
}
|
||||
const env = { ...process.env, HOME: home, DSH_HOME: dshHome }
|
||||
const run = () => spawnSync('bash', [script, '--skills', '--roles'], { env, encoding: 'utf8' })
|
||||
|
||||
const first = run()
|
||||
@ -98,29 +93,9 @@ function makeLegacyCache(home) {
|
||||
function installerEnv(home) {
|
||||
const env = { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh') }
|
||||
delete env.DSH_SUPERPOWERS_SOURCE
|
||||
delete env.DSH_SUPERPOWERS_REPO
|
||||
// No test may reach GitHub: the git source defaults to a path that does not
|
||||
// exist, and git tests opt in with a file:// fixture.
|
||||
env.DSH_SUPERPOWERS_REPO = join(home, 'no-superpowers-repo')
|
||||
return env
|
||||
}
|
||||
|
||||
function makeSuperpowersRepo(home, name = 'superpowers-src') {
|
||||
const repo = join(home, name)
|
||||
mkdirSync(repo, { recursive: true })
|
||||
git(repo, ['init', '-b', 'main'])
|
||||
git(repo, ['config', 'user.email', 'test@example.invalid'])
|
||||
git(repo, ['config', 'user.name', 'Test'])
|
||||
makeSuperpowers(repo)
|
||||
git(repo, ['add', '.'])
|
||||
git(repo, ['commit', '-m', 'init'])
|
||||
return repo
|
||||
}
|
||||
|
||||
function gitUrl(repo) {
|
||||
return pathToFileURL(repo).href
|
||||
}
|
||||
|
||||
function runInstaller(home, args, extraEnv = {}) {
|
||||
return spawnSync('bash', [script, ...args], {
|
||||
env: { ...installerEnv(home), ...extraEnv },
|
||||
@ -211,8 +186,7 @@ test('--skills fails when there is no source and nothing installed', () => {
|
||||
const out = run.stdout + run.stderr
|
||||
assert.equal(run.status, 1, out)
|
||||
assert.match(out, /FAIL: no superpowers source/)
|
||||
assert.match(out, /no-superpowers-repo/)
|
||||
assert.match(out, /pass --superpowers-from/)
|
||||
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)
|
||||
@ -279,14 +253,15 @@ test('the legacy plugin cache still works and warns', () => {
|
||||
}
|
||||
})
|
||||
|
||||
test('--check warns about a manual non-git checkout', () => {
|
||||
test('--check reports the pinned source', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
makeSuperpowers(join(home, '.dsh', 'superpowers'))
|
||||
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, /not a git checkout; --skills will not update it/)
|
||||
assert.match(out, /ok: superpowers source \(.*superpowers\/skills\)/)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
@ -369,126 +344,3 @@ test('an empty --superpowers-from is rejected, not ignored', () => {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('--skills clones the git source on first run', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
const repo = makeSuperpowersRepo(home)
|
||||
const sha = git(repo, ['rev-parse', '--short', 'HEAD'])
|
||||
const run = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: gitUrl(repo) })
|
||||
const out = run.stdout + run.stderr
|
||||
assert.equal(run.status, 0, out)
|
||||
assert.ok(existsSync(join(home, '.dsh', 'superpowers', '.git')), 'clone is missing')
|
||||
assert.match(run.stdout, /copied 2 superpowers skills/)
|
||||
assert.ok(run.stdout.includes(`git ${sha}`), out)
|
||||
assert.ok(existsSync(join(home, '.dsh', 'skills', 'using-superpowers', 'SKILL.md')))
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('--skills updates an existing clone to the new commit', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
const repo = makeSuperpowersRepo(home)
|
||||
const env = { DSH_SUPERPOWERS_REPO: gitUrl(repo) }
|
||||
assert.equal(runInstaller(home, ['--skills'], env).status, 0)
|
||||
|
||||
write(join(repo, 'skills', 'new-skill', 'SKILL.md'), '---\nname: new-skill\n---\nbody\n')
|
||||
git(repo, ['add', '.'])
|
||||
git(repo, ['commit', '-m', 'add new-skill'])
|
||||
const sha = git(repo, ['rev-parse', '--short', 'HEAD'])
|
||||
|
||||
const second = runInstaller(home, ['--skills'], env)
|
||||
const out = second.stdout + second.stderr
|
||||
assert.equal(second.status, 0, out)
|
||||
assert.ok(existsSync(join(home, '.dsh', 'skills', 'new-skill', 'SKILL.md')), 'clone was not updated')
|
||||
assert.ok(second.stdout.includes(`git ${sha}`), out)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('an offline update keeps the cached clone', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
const repo = makeSuperpowersRepo(home)
|
||||
assert.equal(runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: gitUrl(repo) }).status, 0)
|
||||
|
||||
const offline = runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: join(home, 'gone') })
|
||||
const out = offline.stdout + offline.stderr
|
||||
assert.equal(offline.status, 0, out)
|
||||
assert.match(out, /WARN: cannot update superpowers/)
|
||||
assert.match(offline.stdout, /copied 2 superpowers skills/)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('a failed first clone leaves no checkout behind', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
const run = runInstaller(home, ['--skills'])
|
||||
const out = run.stdout + run.stderr
|
||||
assert.equal(run.status, 1, out)
|
||||
assert.equal(existsSync(join(home, '.dsh', 'superpowers')), false, 'broken clone must not stay')
|
||||
assert.match(out, /FAIL: no superpowers source/)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('an explicit source never touches git', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
const src = makeSuperpowers(join(home, 'src', 'superpowers'))
|
||||
const run = runInstaller(home, ['--skills', '--superpowers-from', src])
|
||||
assert.equal(run.status, 0, run.stdout + run.stderr)
|
||||
assert.equal(existsSync(join(home, '.dsh', 'superpowers')), false)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('a manual non-git checkout is used but not updated', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
makeSuperpowers(join(home, '.dsh', 'superpowers'))
|
||||
const run = runInstaller(home, ['--skills'])
|
||||
const out = run.stdout + run.stderr
|
||||
assert.equal(run.status, 0, out)
|
||||
assert.match(out, /not a git checkout/)
|
||||
assert.match(run.stdout, /copied 2 superpowers skills/)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('--check reports the git sha of the managed checkout', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
const repo = makeSuperpowersRepo(home)
|
||||
const sha = git(repo, ['rev-parse', '--short', 'HEAD'])
|
||||
assert.equal(runInstaller(home, ['--skills'], { DSH_SUPERPOWERS_REPO: gitUrl(repo) }).status, 0)
|
||||
const run = runInstaller(home, ['--check'])
|
||||
const out = run.stdout + run.stderr
|
||||
assert.equal(run.status, 0, out)
|
||||
assert.match(out, new RegExp(`ok: superpowers source \\(.*superpowers/skills, git ${sha}\\)`))
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
test('--check never clones on an empty HOME', () => {
|
||||
const home = tempDir()
|
||||
try {
|
||||
const repo = makeSuperpowersRepo(home)
|
||||
const run = runInstaller(home, ['--check'], { DSH_SUPERPOWERS_REPO: gitUrl(repo) })
|
||||
const out = run.stdout + run.stderr
|
||||
assert.equal(run.status, 0, out)
|
||||
assert.equal(existsSync(join(home, '.dsh', 'superpowers')), false, '--check must not clone')
|
||||
assert.match(out, /WARN: no superpowers source/)
|
||||
} finally {
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-24
|
||||
@ -1,124 +0,0 @@
|
||||
# Design: install тянет superpowers с git
|
||||
|
||||
Issue: #67. Спека: `openspec/changes/dsh-superpowers-git/specs/dsh-install/spec.md`.
|
||||
Контекст: #49 (`dsh-install-source`) — источник был явным, сеть в установке
|
||||
запрещена; это решение пересматривается по команде владельца 24.10.2026.
|
||||
|
||||
## 1. Что меняется в контракте #49
|
||||
|
||||
| | #49 | #67 |
|
||||
|---|---|---|
|
||||
| Источник по умолчанию | ручной клон/флаг | git: клон в `$DSH_HOME/superpowers` |
|
||||
| Обновление | нет | `fetch --depth 1` + `reset --hard FETCH_HEAD` при каждом `--skills` |
|
||||
| Сеть в установке | запрещена | разрешена только для git-источника |
|
||||
| Явный источник | главнее | главнее, сеть не трогает |
|
||||
| `--check` | офлайн | офлайн + git sha |
|
||||
| Офлайн-машина | явный источник | кэш клона (WARN) → legacy (WARN) → FAIL |
|
||||
|
||||
## 2. Решения
|
||||
|
||||
### D-7 (решение владельца). Git — источник по умолчанию
|
||||
|
||||
`--skills` без флага/переменной клонирует репозиторий и обновляет его при
|
||||
следующих запусках. Обоснование владельца: ручной клон не обновляется и
|
||||
оставляет `WARN: legacy superpowers source` навсегда. Цена: установка требует
|
||||
сеть (кроме явного источника и кэша), а свежий апстрим может принести
|
||||
несовместимость — она лечится `--superpowers-from` на любой коммит.
|
||||
|
||||
### D-8. Явный источник полностью отключает сеть
|
||||
|
||||
`--superpowers-from` и `$DSH_SUPERPOWERS_SOURCE` проверяются первыми и, если
|
||||
валидны, git не вызывается вообще. Это и офлайн-путь, и отладка, и пиннинг на
|
||||
конкретную версию. Цена: две ветки кода, покрыты тестами, включая «явный
|
||||
источник не создаёт `$DSH_HOME/superpowers`».
|
||||
|
||||
### D-9. Офлайн не ломает установку, если есть чем ставить
|
||||
|
||||
Порядок при недоступном git: существующий клон (в т.ч. с неудачным fetch) →
|
||||
legacy-кэш CC → `~/.claude/skills` → FAIL. Первые два шага печатают `WARN` с
|
||||
причиной (репозиторий), чтобы деградация была видна, но машина без сети
|
||||
продолжала работать. Цена: опечатка в `DSH_SUPERPOWERS_REPO` может привести к
|
||||
legacy-источнику — сообщение называет репозиторий и путь кэша.
|
||||
|
||||
### D-10. Ручная копия без `.git` — пиннинг, а не ошибка
|
||||
|
||||
Если `$DSH_HOME/superpowers` существует, валиден, но не является git-клоном,
|
||||
он используется как есть с `WARN`, что обновляться не будет. Это совместимость
|
||||
с #49: у кого уже лежит ручная копия, после обновления обвязки установка не
|
||||
падает. Цена: такой каталог стареет молча — предупреждение на каждом `--skills`.
|
||||
|
||||
### D-11. `--check` остаётся офлайновым
|
||||
|
||||
`--check` — диагностика; клонировать или обновлять из него нельзя (в #42 уже
|
||||
принято: `--check` не должен иметь побочных эффектов). Он показывает
|
||||
`ok: superpowers source (<path>, git <sha>)` для клона, `WARN` для ручной копии
|
||||
и для legacy, и подсказку про `--skills`/`DSH_SUPERPOWERS_REPO`, когда клона нет.
|
||||
Цена: `--check` не скажет, доступен ли репозиторий; это выясняется при установке.
|
||||
|
||||
### D-12. Тесты идут через локальный `file://`-репозиторий
|
||||
|
||||
`DSH_SUPERPOWERS_REPO` — не только фича для зеркал, но и тестовый шов: набор не
|
||||
ходит в GitHub. Тестовый хелпер создаёт git-репозиторий с `skills/*/SKILL.md`,
|
||||
запускает `--skills` с `DSH_SUPERPOWERS_REPO=file://<путь>`, затем добавляет
|
||||
коммит и проверяет обновление копий. Офлайн-кейс — репозиторий, которого нет.
|
||||
|
||||
## 3. Решения, принятые без обсуждения
|
||||
|
||||
- **2026-10-24. `--depth 1` и `reset --hard FETCH_HEAD`, без тегов и подписей.**
|
||||
«Свежая версия» = последний коммит default-ветки; глубина 1 экономит сеть,
|
||||
`reset --hard` гарантирует чистое управляемое дерево. Цена: локальные правки в
|
||||
`~/.dsh/superpowers` теряются, подлинность апстрима не проверяется (TLS и
|
||||
доверие к GitHub); откат — `--superpowers-from` на известный коммит.
|
||||
- **2026-10-24. Неудачный первый клон удаляет каталог.**
|
||||
`rm -rf $DSH_HOME/superpowers` после неудачи, чтобы следующий запуск снова
|
||||
клонировал, а не принимал половину за кэш. Цена: если пользователь положил
|
||||
туда что-то своё и не было `.git` — этот путь не трогается (D-10), удаление
|
||||
только для созданного нами каталога в текущем запуске.
|
||||
- **2026-10-24. Отдельного флага `--offline` нет.**
|
||||
Офлайн покрыт существующим клоном (D-9) и явным источником (D-8). Цена: нет
|
||||
способа гарантированно запретить сеть, кроме явного источника; для установки
|
||||
это приемлемо.
|
||||
- **2026-10-24. `DSH_SUPERPOWERS_REPO` виден и в `--check`.**
|
||||
В `WARN`/`FAIL` печатается фактический репозиторий, чтобы зеркало и опечатка
|
||||
различались. Цена: строка вывода зависит от env — приемлемо.
|
||||
- **2026-10-24. `tasks.md` OpenSpec не заполняется** (как в прошлых change):
|
||||
задачи в плане superpowers.
|
||||
|
||||
## 4. Риски
|
||||
|
||||
- **Свежий апстрим ломает установку.** `--skills` тянет последний коммит; если
|
||||
superpowers переименует `skills/using-superpowers`, `FAIL` скажет, что искать.
|
||||
Обход — `--superpowers-from` с проверенной копией; sha в выводе помогает
|
||||
зафиксировать версию.
|
||||
- **Сеть в установке.** Машины без доступа к GitHub должны использовать
|
||||
`DSH_SUPERPOWERS_REPO` (зеркало) или `--superpowers-from`; ошибка клона
|
||||
называет репозиторий и оба пути.
|
||||
- **Тесты и сеть.** Любой тест, забывший выставить `DSH_SUPERPOWERS_REPO`, уйдёт
|
||||
в GitHub. Поэтому `installerEnv` в тестах задаёт недоступный repo по
|
||||
умолчанию, а тесты git-источника — `file://`-фикстуру; проверяется, что
|
||||
`--check` не создаёт клон.
|
||||
- **Офлайн-деградация незаметна.** WARN есть на каждом шаге деградации; `--check`
|
||||
показывает, какой источник будет использован при следующем `--skills`.
|
||||
- **Совместимость с #49.** Ручная копия без `.git` и legacy-кэш остаются
|
||||
рабочими; тесты #49 переиспользуются, меняются только ожидания сообщений.
|
||||
|
||||
## 5. Приёмка
|
||||
|
||||
- `openspec validate dsh-superpowers-git --strict` — change валиден.
|
||||
- `bash dsh/tests/run.sh` — зелёный, включая клон/обновление/офлайн/`--check`.
|
||||
- `bash tests/run.sh` — регрессий в CC-контуре нет.
|
||||
- Живой прогон на машине владельца: `--skills` клонирует GitHub-репозиторий,
|
||||
`--check` показывает `git <sha>`, повторный `--skills` обновляет.
|
||||
|
||||
### Приёмка: прогон 24.10.2026
|
||||
|
||||
- `openspec validate dsh-superpowers-git --strict` — change валиден.
|
||||
- `bash dsh/tests/run.sh` — 71/71 pass; `bash tests/run.sh` — см. PR.
|
||||
- Живой клон GitHub в temp HOME без `~/.claude`: `--skills` → 15
|
||||
superpowers-скиллов, `source: .../.dsh/superpowers/skills, git 2ce8aa1`;
|
||||
повторный `--skills` → обновление того же клона, снова `git 2ce8aa1`;
|
||||
`--check` → `ok: superpowers source (.../.dsh/superpowers/skills, git 2ce8aa1)`.
|
||||
- Тесты сети не касаются: `installerEnv` подставляет недоступный
|
||||
`DSH_SUPERPOWERS_REPO`, git-кейсы — локальный `file://`-репозиторий; мутацией
|
||||
проверено, что `--check never clones on an empty HOME` падает, если вернуть
|
||||
`--check` на сетевой резолвер.
|
||||
@ -1,52 +0,0 @@
|
||||
# Proposal: install сам тянет свежий superpowers с git
|
||||
|
||||
Issue: #67. Связано: #49 (закрыт), #48 (роадмап), change `dsh-install-source`.
|
||||
Ответ владельца 24.10.2026: «а давай инстал будет тащить сам свежую версию с
|
||||
гита».
|
||||
|
||||
## Why
|
||||
|
||||
После #49 install-time источник задаётся явно: `--superpowers-from`,
|
||||
`$DSH_SUPERPOWERS_SOURCE` или ручной клон в `~/.dsh/superpowers`. Это снимает
|
||||
зависимость от `~/.claude`, но перекладывает получение источника на человека и
|
||||
не обновляет его: клон, сделанный полгода назад, остаётся «свежим» навсегда, а
|
||||
`WARN: legacy superpowers source` на машине владельца так и висит, потому что
|
||||
пиннинга нет.
|
||||
|
||||
Владелец хочет, чтобы `dsh/install.sh --skills` сам получал свежий superpowers:
|
||||
первый запуск — клон, следующие — обновление. Явные флаги остаются для офлайна,
|
||||
зеркал и отладки и сеть не трогают.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **git — источник по умолчанию.** `--skills` клонирует
|
||||
`https://github.com/obra/superpowers` в `$DSH_HOME/superpowers` (depth 1), если
|
||||
клона нет, и обновляет `fetch --depth 1` + `reset --hard FETCH_HEAD`, если
|
||||
есть. URL переопределяется `$DSH_SUPERPOWERS_REPO` (зеркала, локальные
|
||||
репозитории в тестах).
|
||||
- **Порядок источников:** явный источник (`--superpowers-from`,
|
||||
`$DSH_SUPERPOWERS_SOURCE`) → git-клон (обновить/создать) → legacy-кэш Claude
|
||||
Code → legacy `~/.claude/skills`. Ручная копия `~/.dsh/superpowers` без `.git`
|
||||
используется как есть с `WARN` (совместимость с #49).
|
||||
- **Сеть и ошибки.** Нет сети при существующем клоне — `WARN`, работа из кэша.
|
||||
Нет сети, нет клона, нет других источников — `FAIL` с кодом 1 и подсказкой
|
||||
`--superpowers-from`. Оборванный клон не оставляется на диске.
|
||||
- **`--check` остаётся офлайновым:** не клонирует и не обновляет, показывает
|
||||
`ok: superpowers source (<path>, git <sha>)` для git-клона, `WARN` для ручной
|
||||
копии без `.git`, legacy и «нет источника» — как в #49.
|
||||
- **Тесты без сети:** локальный git-репозиторий через `DSH_SUPERPOWERS_REPO`
|
||||
(`file://`), проверка первого клона, обновления до нового коммита, офлайна и
|
||||
`--check`.
|
||||
- **Документация.** README/PORT: quick start без ручного клона, офлайн-путь,
|
||||
`DSH_SUPERPOWERS_REPO`, `--check` с sha.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Вендоринг superpowers в репозиторий — отклонён в #49 (D-1) и остаётся
|
||||
отклонённым.
|
||||
- Пиннинг тега/коммита и проверка подписи: «свежая версия» = последний коммит
|
||||
default-ветки; воспроизводимость установки обеспечивается глубиной 1 и
|
||||
записанным sha в выводе, а не неизменностью источника.
|
||||
- Субмодули и LFS в клоне superpowers.
|
||||
- Обновление ролей/локальных скиллов из сети: они по-прежнему из репозитория
|
||||
обвязки.
|
||||
@ -1,164 +0,0 @@
|
||||
# Spec Delta: dsh-superpowers-git
|
||||
|
||||
## Purpose
|
||||
|
||||
`dsh/install.sh --skills` получает superpowers из git сам: первый запуск —
|
||||
клон, следующие — обновление до последнего коммита default-ветки. Явные
|
||||
источники остаются приоритетнее и не ходят в сеть; `~/.claude` — по-прежнему
|
||||
legacy-fallback с предупреждением.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Свежий superpowers из git
|
||||
`dsh/install.sh --skills` MUST получать superpowers-скиллы из git, когда явный
|
||||
источник не задан: клонировать при отсутствии клона и обновлять при каждом
|
||||
следующем запуске.
|
||||
|
||||
- Репозиторий по умолчанию — `https://github.com/obra/superpowers`;
|
||||
`$DSH_SUPERPOWERS_REPO` MUST переопределять его (зеркала, локальные
|
||||
репозитории в тестах) и MUST применяться к уже существующему клону: перед
|
||||
`fetch` URL `origin` приводится к заданному репозиторию.
|
||||
- Клон — `git clone --depth 1 <repo> $DSH_HOME/superpowers`.
|
||||
- Обновление — `git -C $DSH_HOME/superpowers fetch --depth 1 origin` и
|
||||
`git reset --hard FETCH_HEAD`; рабочее дерево управляемое, локальные правки
|
||||
там не сохраняются.
|
||||
- После клона/обновления источником MUST считаться `$DSH_HOME/superpowers/skills`
|
||||
(или сам каталог), только если в нём есть `using-superpowers/SKILL.md`.
|
||||
- Вывод `--skills` MUST называть источник и git-версию:
|
||||
`(source: <path>, git <short-sha>)`.
|
||||
- `--check` MUST NOT выполнять сетевые операции: он показывает состояние
|
||||
существующего клона (`ok: superpowers source (<path>, git <sha>)`) и не
|
||||
создаёт `$DSH_HOME/superpowers`.
|
||||
|
||||
#### Scenario: Первый запуск клонирует источник
|
||||
- **WHEN** `$DSH_HOME/superpowers` отсутствует, `DSH_SUPERPOWERS_REPO` указывает
|
||||
на репозиторий superpowers
|
||||
- **THEN** `--skills` клонирует его в `$DSH_HOME/superpowers`, копирует скиллы в
|
||||
`$DSH_HOME/skills` и печатает `git <sha>`
|
||||
|
||||
#### Scenario: Повторный запуск обновляет клон
|
||||
- **WHEN** клон уже есть, а в источнике появился новый коммит со скиллом
|
||||
- **THEN** повторный `--skills` обновляет клон до нового коммита, и новый скилл
|
||||
оказывается в `$DSH_HOME/skills`
|
||||
|
||||
#### Scenario: Нет сети, клон есть
|
||||
- **WHEN** обновление клона не удалось (недоступный репозиторий), но
|
||||
`$DSH_HOME/superpowers` содержит `using-superpowers/SKILL.md`
|
||||
- **THEN** `--skills` печатает `WARN`, использует кэш, копирует скиллы и
|
||||
завершается кодом 0
|
||||
|
||||
#### Scenario: Нет сети и нет клона
|
||||
- **WHEN** клонирование не удалось, клона и других источников нет,
|
||||
`$DSH_HOME/skills` пуст
|
||||
- **THEN** `--skills` печатает `FAIL` с репозиторием и подсказкой
|
||||
`--superpowers-from`, код 1; недоклонированный каталог не остаётся на диске
|
||||
|
||||
#### Scenario: Ручная копия без git
|
||||
- **WHEN** `$DSH_HOME/superpowers` существует, не является git-клоном и содержит
|
||||
`using-superpowers/SKILL.md`
|
||||
- **THEN** каталог используется как есть, `--skills` печатает `WARN` о том, что
|
||||
он не будет обновляться
|
||||
|
||||
#### Scenario: --check не ходит в сеть
|
||||
- **WHEN** клон существует, а `DSH_SUPERPOWERS_REPO` указывает на недоступный
|
||||
репозиторий
|
||||
- **THEN** `--check` печатает `ok: superpowers source (<path>, git <sha>)`,
|
||||
завершается без сетевых ошибок и ничего не клонирует
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Источник superpowers задаётся при установке
|
||||
`dsh/install.sh --skills` SHALL разрешать источник superpowers-скиллов в
|
||||
порядке приоритета.
|
||||
|
||||
- Порядок: `--superpowers-from <dir>` → `$DSH_SUPERPOWERS_SOURCE` → git-клон
|
||||
`$DSH_HOME/superpowers` (обновить существующий или склонировать из
|
||||
`$DSH_SUPERPOWERS_REPO`/`https://github.com/obra/superpowers`) →
|
||||
legacy-кэш Claude Code → legacy `~/.claude/skills`.
|
||||
- Для явного источника (`--superpowers-from`, `$DSH_SUPERPOWERS_SOURCE`)
|
||||
допускаются обе формы: корень superpowers (есть `skills/using-superpowers/SKILL.md`)
|
||||
или сам каталог `skills`; сеть при явном источнике MUST NOT использоваться.
|
||||
- Явно заданный, но невалидный источник MUST приводить к `FAIL` и коду
|
||||
возврата 1: тихий откат на git/legacy запрещён.
|
||||
- Копирование MUST быть заменой каталога назначения (`rm -rf` + `cp -R`), а не
|
||||
симлинком: `~/.dsh/skills` не должен зависеть от каталога-источника.
|
||||
|
||||
#### Scenario: Явный источник без ~/.claude
|
||||
- **WHEN** `HOME` — пустой temp-каталог без `~/.claude`, а `--superpowers-from`
|
||||
указывает на каталог superpowers
|
||||
- **THEN** `--skills` копирует скиллы в `$DSH_HOME/skills`, печатает источник и
|
||||
завершается кодом 0
|
||||
|
||||
#### Scenario: Переменная окружения вместо флага
|
||||
- **WHEN** задан `DSH_SUPERPOWERS_SOURCE=<dir>` и флаг `--superpowers-from` не передан
|
||||
- **THEN** источник берётся из переменной; поведение совпадает с флагом
|
||||
|
||||
#### Scenario: Флаг важнее переменной
|
||||
- **WHEN** заданы и `--superpowers-from`, и `DSH_SUPERPOWERS_SOURCE`
|
||||
- **THEN** используется каталог из флага
|
||||
|
||||
#### Scenario: Невалидный явный источник
|
||||
- **WHEN** `--superpowers-from` указывает на каталог без
|
||||
`using-superpowers/SKILL.md`
|
||||
- **THEN** `--skills` печатает `FAIL` с ожидаемой структурой и завершается
|
||||
кодом 1, ничего не копируя из git/legacy-источников
|
||||
|
||||
#### Scenario: Явный источник не трогает git
|
||||
- **WHEN** `--superpowers-from` задан, а `$DSH_HOME/superpowers` отсутствует
|
||||
- **THEN** `--skills` не создаёт `$DSH_HOME/superpowers`
|
||||
|
||||
#### Scenario: Пиннинг в ~/.dsh
|
||||
- **WHEN** существует ручная (не git) копия
|
||||
`$DSH_HOME/superpowers/skills/using-superpowers/SKILL.md`, флаг и переменная не
|
||||
заданы
|
||||
- **THEN** источником служит эта копия, `--skills` печатает `WARN`, что она не
|
||||
будет обновляться, и не превращает каталог в git-клон
|
||||
|
||||
### Requirement: Отсутствие источника не бывает тихим
|
||||
Если источник superpowers не найден, `--skills` MUST различать первичную и
|
||||
повторную установку и MUST NOT завершаться успехом молча.
|
||||
|
||||
- Если `$DSH_HOME/skills/using-superpowers/SKILL.md` уже существует —
|
||||
`WARN` «источник не найден, копии сохранены» и код 0 (идемпотентный повтор).
|
||||
- Если копий нет — `FAIL` с репозиторием (`$DSH_SUPERPOWERS_REPO`) и подсказкой
|
||||
`--superpowers-from <dir>`; код возврата 1.
|
||||
- Локальные скиллы обвязки (`dsh/skills/*`: `role`, `sdd`) MUST ставиться в
|
||||
обоих случаях: они не зависят от superpowers.
|
||||
|
||||
#### Scenario: Первая установка без источника
|
||||
- **WHEN** `HOME` пуст, `$DSH_HOME/skills` не существует, git-источник недоступен
|
||||
и других источников нет
|
||||
- **THEN** `--skills` печатает `FAIL`, код 1, но `$DSH_HOME/skills/role/SKILL.md`
|
||||
и `$DSH_HOME/skills/sdd/SKILL.md` на месте
|
||||
|
||||
#### Scenario: Повторная установка без источника
|
||||
- **WHEN** `$DSH_HOME/skills/using-superpowers/SKILL.md` уже есть, источника нет
|
||||
- **THEN** `--skills` печатает `WARN`, код 0, установленные скиллы не удалены
|
||||
|
||||
### Requirement: ~/.claude — только legacy-fallback с предупреждением
|
||||
superpowers-скиллы и роли MUST иметь рабочий путь `~/.dsh`; пути `~/.claude`
|
||||
SHALL оставаться fallback'ом для машин, где `--skills --roles` ещё не
|
||||
перезапускали, и MUST сопровождаться предупреждением.
|
||||
|
||||
- Установка: legacy-кэш Claude Code и `~/.claude/skills` допустимы как
|
||||
источник, но `--skills` печатает `WARN: legacy superpowers source` с
|
||||
подсказкой на git-источник или `--superpowers-from`.
|
||||
- `--check`: legacy-источник — `WARN`, git-клон/явный источник — `ok`,
|
||||
отсутствие — `WARN` с подсказкой, невалидный явный источник — `FAIL` и код 1.
|
||||
- Runtime: bootstrap и резолвер ролей сохраняют порядок `~/.dsh` → `~/.claude`,
|
||||
но при выборе legacy-пути один раз за процесс пишут в лог предупреждение.
|
||||
- Удаление fallback'ов — отдельное изменение; в этом change они не удаляются.
|
||||
|
||||
#### Scenario: Legacy-кэш как источник установки
|
||||
- **WHEN** в `HOME` есть только кэш Claude Code с superpowers, `$DSH_HOME` пуст,
|
||||
git-источник недоступен
|
||||
- **THEN** `--skills` копирует скиллы и печатает `WARN: legacy superpowers source`
|
||||
|
||||
#### Scenario: Runtime-предупреждение
|
||||
- **WHEN** bootstrap резолвит `using-superpowers` из `~/.claude/...`
|
||||
- **THEN** в лог один раз за процесс попадает предупреждение с предложением
|
||||
выполнить `dsh/install.sh --skills`
|
||||
|
||||
#### Scenario: ~/.dsh приоритетнее
|
||||
- **WHEN** существуют и `~/.dsh/skills/using-superpowers/SKILL.md`, и legacy-копия
|
||||
- **THEN** используется `~/.dsh`, предупреждения нет
|
||||
Loading…
x
Reference in New Issue
Block a user