18 KiB
DSH install without ~/.claude — 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 --global --skills --roles ставит рабочий DSH на машине без ~/.claude, а ~/.claude остаётся только явно предупреждающим legacy-fallback'ом.
Architecture: Источник superpowers разрешается в install.sh по приоритету флаг → env → пиннинг ~/.dsh/superpowers → legacy-кэш CC; отсутствие источника — FAIL (если копий нет) или WARN (идемпотентный повтор). install_global переписывает пути ролей в установленной копии AGENTS.md. Bundle сохраняет fallback-порядок, но один раз за процесс сообщает, что взял legacy-путь.
Tech Stack: bash (set -euo pipefail), python3 stdlib, Node.js ESM (node:test), Cordis-бандл DSH.
Spec: openspec/changes/dsh-install-source/specs/dsh-install/spec.md (дизайн — openspec/changes/dsh-install-source/design.md).
Global Constraints
global/CLAUDE.mdMUST NOT меняться: его читает Claude Code.install.shMUST NOT выполнятьgit clone,curlи любые сетевые операции.- Источник superpowers различается по файлу
using-superpowers/SKILL.md; принимаются и корень сskills/, и самskills/. - Порядок источников:
--superpowers-from→$DSH_SUPERPOWERS_SOURCE→$DSH_HOME/superpowers/skills→ кэш CC (~/.claude/plugins/cache/claude-plugins-official/superpowers/*/skills, новая версия) →~/.claude/skills. - Legacy-пути остаются рабочими: удаление fallback'ов — не эта задача.
- Тесты DSH идут без DSH runtime:
bash dsh/tests/run.sh; все тесты на tempHOME. - Маркировка находок:
T<n>.<k>; решения в этом плане уже с маркерамиD-1…D-6в design.md.
Review Focus
--skillsбез источника при уже установленных~/.dsh/skills: копии не должны исчезнуть, код 0,WARN.- Явный источник без
using-superpowers/SKILL.md:FAIL, ни одного скопированного файла из legacy — иначе опечатка маскируется. DSH_SUPERPOWERS_SOURCEпустой или невалидный: пустой игнорируется, невалидный —FAIL(не откат на legacy).- Путь источника с пробелами:
--superpowers-from "$dir with space"должен работать (кавычки на всех уровнях). - Runtime: выбран legacy-путь → одно предупреждение за процесс; выбран
~/.dsh→ предупреждения нет. --globalповторно: бэкап создаётся, вAGENTS.mdнет~/.claude/roles, источник в репозитории не тронут.
Task 1: install.sh — источник superpowers, FAIL/WARN, --check
Files:
- Modify:
dsh/install.sh(superpowers_skills→ новый резолвер;check;install_skills; разбор аргументов;usage) - Modify:
dsh/tests/install.test.mjs - Modify:
dsh/README.md(quick start: получение источника, флаг, пиннинг)
Interfaces:
-
Produces:
- флаг
--superpowers-from <dir>(и--superpowers-from=<dir>), envDSH_SUPERPOWERS_SOURCE; - bash-функции
normalize_superpowers_dir <dir>(печатаетskills-каталог или rc 1) иsuperpowers_source(печатает<path>\t<explicit|pinned|legacy>, rc 1 если источника нет); - вывод
--skills:copied N superpowers skills into <dest> (source: <path>); при legacy — строкаWARN: legacy superpowers source <path>; prefer --superpowers-from or ~/.dsh/superpowers; - вывод
--check:ok: superpowers source (<path>)/WARN: legacy superpowers source (<path>); run --skills from a DSH-owned source/WARN: no superpowers source; run: git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers.
- флаг
-
Consumes:
$DSH_HOME(по умолчанию$HOME/.dsh). -
Step 1: Написать падающие тесты в
dsh/tests/install.test.mjs
Тесты (каждый — отдельный test(...), temp HOME, spawnSync('bash', [script, ...])):
test('--skills takes superpowers from --superpowers-from in a clean HOME', ...)
// HOME без ~/.claude; источник: <home>/src/superpowers/skills/using-superpowers/SKILL.md
// + brainstorming/SKILL.md; запуск: --skills --superpowers-from <home>/src/superpowers
// assert: status 0; stdout /source: <...>/src\/superpowers\/skills/;
// <dshHome>/skills/using-superpowers/SKILL.md и role/sdd на месте;
// existsSync(<home>/.claude) === false
test('--superpowers-from accepts the skills directory itself', ...)
// тот же fixture, флаг указывает на <home>/src/superpowers/skills; status 0
test('--superpowers-from wins over DSH_SUPERPOWERS_SOURCE', ...)
// два разных fixture-источника (A: body "a", B: body "b"); env=B, флаг=A;
// в <dshHome>/skills/using-superpowers/SKILL.md лежит "a"
test('--skills fails when no source and nothing installed', ...)
// пустой HOME; --skills; assert status 1; stderr /FAIL: no superpowers source/;
// stdout /git clone --depth 1 https:\/\/github.com\/obra\/superpowers/;
// role и sdd всё равно на месте
test('--skills keeps installed copies when the source is gone', ...)
// HOME с <dshHome>/skills/using-superpowers/SKILL.md (заранее), без источника;
// --skills; status 0; stdout /WARN: no superpowers source/; файл не удалён
test('explicit invalid source fails without touching legacy', ...)
// HOME с legacy-кэшем (как в первом тесте файла) и с --superpowers-from <пустой dir>;
// status 1; stderr /FAIL: .*has no using-superpowers\/SKILL\.md/;
// <dshHome>/skills/brainstorming не создан
test('legacy plugin cache still works and warns', ...)
// HOME с ~/.claude/plugins/cache/.../skills; --skills; status 0;
// stdout /WARN: legacy superpowers source/ и /copied 2 superpowers skills/
test('--check reports the pinned source', ...)
// HOME с <dshHome>/superpowers/skills/using-superpowers/SKILL.md;
// --check; stdout /ok: superpowers source \(.*superpowers\/skills\)/
- Step 2: Прогнать и увидеть падение
Run: node --test dsh/tests/install.test.mjs
Expected: новые тесты падают (status 0 !== 1, нет source: в выводе); остальные зелёные.
-
Step 3: Реализовать в
dsh/install.sh -
Разбор аргументов:
--superpowers-from(следующий аргумент) и--superpowers-from=*(через"${arg#*=}"); ошибкаunknown optionдля голого--superpowers-from. -
normalize_superpowers_dir dir→"$dir/skills"если есть"$dir/skills/using-superpowers/SKILL.md", иначе"$dir"если есть"$dir/using-superpowers/SKILL.md", иначе rc 1. -
superpowers_source— порядок из Global Constraints; для legacy-кэша сохранить текущийls -d … | sort -V | tail -1; печатать<path>\t<kind>; явный невалидный источник —FAILв stderr и rc 2, чтобыinstall_skillsне печатал «нет источника». -
install_skills: при rc 2 —return 1; при rc 1 и наличии"$DSH_HOME/skills/using-superpowers/SKILL.md"—WARN+return 0; иначеFAILс командой клона +return 1. Копирование — как сейчас (rm -rf+cp -R), плюс(source: <path>)в итоговой строке. -
check:ok: dsh skillsоставить; добавить ветку источника (ok/WARN legacy/WARN noneпо kind); legacy-источник не делаетcheckпадающим. -
Dispatch:
install_skills || status=1; install_local_skills— локальные скиллы ставятся всегда. -
usage: строка--superpowers-from <dir>с описанием трёх источников. -
Step 4: Прогнать тесты
Run: node --test dsh/tests/install.test.mjs
Expected: PASS всех тестов файла.
- Step 5: Обновить quick start в
dsh/README.md
Один блок: клон git clone --depth 1 https://github.com/obra/superpowers ~/.dsh/superpowers (или --superpowers-from <dir>), затем --all; в Known limits — «источник superpowers задаётся установщиком; ~/.claude — legacy».
- Step 6: Commit
git add dsh/install.sh dsh/tests/install.test.mjs dsh/README.md
git commit -m "feat(dsh): install superpowers from an explicit source (#49)"
Task 2: --global — пути ролей и формат инструкций
Files:
- Modify:
dsh/install.sh(install_global) - Modify:
dsh/tests/install.test.mjs
Interfaces:
-
Produces:
AGENTS.md, где~/.claude/rolesзаменён на~/.dsh/roles, шапка содержит строки:DSH adapter: role files are ~/.dsh/roles (installed by --roles); legacy ~/.claude/roles is a migration fallback.DSH adapter: superpowers skills are ~/.dsh/skills (installed by --skills from --superpowers-from, DSH_SUPERPOWERS_SOURCE, or ~/.dsh/superpowers); the ~/.claude cache is a legacy fallback.DSH adapter: a project CLAUDE.md/AGENTS.md is the instruction-file format of this harness; it does not imply a Claude Code runtime.
-
Consumes:
$ROOT/global/CLAUDE.md(только чтение). -
Step 1: Падающий тест
test('--global installs DSH role paths and explains the instruction format', ...)
// temp HOME; source global/CLAUDE.md до запуска (readFileSync);
// spawn --global; assert AGENTS.md: /~\/\.dsh\/roles\/dev\.md/, !/~\/\.claude\/roles/,
// /instruction-file format of this harness/;
// assert global/CLAUDE.md побайтно равен снимку; assert создан бэкап при повторном --global
- Step 2: Прогнать, увидеть падение
Run: node --test dsh/tests/install.test.mjs
Expected: FAIL — в AGENTS.md пока ~/.claude/roles.
- Step 3: Реализовать
В install_global тело global/CLAUDE.md подать через python3 - "$ROOT/global/CLAUDE.md" с text.replace("~/.claude/roles", "~/.dsh/roles") и sys.stdout.write; три adapter-строки выше заменить текущие про роли и bootstrap; добавить строку про формат инструкций.
- Step 4: Прогнать тесты
Run: node --test dsh/tests/install.test.mjs
Expected: PASS.
- Step 5: Commit
git add dsh/install.sh dsh/tests/install.test.mjs
git commit -m "feat(dsh): DSH-owned role paths in installed instructions (#49)"
Task 3: Runtime — legacy-предупреждение один раз
Files:
- Create:
dsh/bundle/lib/legacy.js - Modify:
dsh/bundle/lib/bootstrap.js(prepareBootstrap) - Modify:
dsh/bundle/index.js(applyпослеresolveRolesDir) - Modify:
dsh/tests/bootstrap.test.mjs,dsh/tests/role.test.mjs - Modify:
dsh/README.md(Known limits),dsh/PORT.md(§3, §5, §7, §8)
Interfaces:
-
Produces:
isLegacyClaudePath(path, home = homedir()) -> boolean— истина для<home>/.claude/...;warnLegacyOnce(logger, kind, path) -> void— пишет[dsh-harness] %s uses the legacy Claude Code source %s; run dsh/install.sh --%sне более одного раза наkind+pathза процесс;prepareBootstrapвызываетwarnLegacyOnce(logger, 'bootstrap', resolved), когда путь legacy;applyвызываетwarnLegacyOnce(ctx.logger, 'roles', rolesDir)послеresolveRolesDir.
-
Consumes:
resolveRolesDir,resolveBootstrapSkill— без изменения сигнатур. -
Step 1: Падающие тесты
// dsh/tests/bootstrap.test.mjs
test('prepareBootstrap warns once when the legacy ~/.claude skill is used', ...)
// temp home: только ~/.claude/skills/using-superpowers/SKILL.md;
// logger = { warnings: [], warn(msg, ...args){ this.warnings.push([msg, ...args]) } };
// prepareBootstrap({ home, logger, skillPath: '' }) дважды;
// assert warnings.length === 1; assert /legacy Claude Code source/
test('prepareBootstrap is silent for the DSH-owned copy', ...)
// temp home: только ~/.dsh/skills/using-superpowers/SKILL.md; warnings.length === 0
// dsh/tests/role.test.mjs
test('isLegacyClaudePath detects ~/.claude paths only', ...)
// true для join(home,'.claude','roles','dev.md'); false для join(home,'.dsh','roles','dev.md')
// и для join(home,'.claude-other','x')
- Step 2: Прогнать, увидеть падение
Run: node --test dsh/tests/bootstrap.test.mjs dsh/tests/role.test.mjs
Expected: FAIL — модуля legacy.js нет, предупреждений ноль.
- Step 3: Реализовать
lib/legacy.js: import { homedir } from 'node:os'; import { sep } from 'node:path';
isLegacyClaudePath сравнивает через startsWith(join(home, '.claude') + sep); warnLegacyOnce держит const warned = new Set() и ключ `${kind}|${path}`.
prepareBootstrap: после resolveBootstrapSkill, если isLegacyClaudePath(resolved, home) — вызвать предупреждение с kind: 'bootstrap' (суффикс --skills).
index.js: импорт warnLegacyOnce; в apply после const rolesDir = ... — if (isLegacyClaudePath(rolesDir)) warnLegacyOnce(ctx.logger, 'roles', rolesDir).
- Step 4: Прогнать тесты
Run: bash dsh/tests/run.sh
Expected: PASS всего набора.
- Step 5: Документация
dsh/README.md: Known limits — источник superpowers (--superpowers-from/env/~/.dsh/superpowers), ~/.claude — legacy с предупреждением; config-комментарий rolesDir/bootstrapSkillPath. dsh/PORT.md: §3 «install-time зависимость» убрать из deferred; §5 — источник и --superpowers-from; §7 — риск ~/.claude переписать как legacy-fallback; §8 — фаза #49 закрыта.
- Step 6: Commit
git add dsh/bundle/lib/legacy.js dsh/bundle/lib/bootstrap.js dsh/bundle/index.js \
dsh/tests/bootstrap.test.mjs dsh/tests/role.test.mjs dsh/README.md dsh/PORT.md
git commit -m "feat(dsh): warn about legacy ~/.claude sources at runtime (#49)"
Task 4: Приёмка, спека, PR
Files:
- Modify (по результатам):
openspec/changes/dsh-install-source/design.md(раздел приёмки),dsh/PORT.md(verified live)
Interfaces:
-
Consumes: всё предыдущее.
-
Step 1: Полный прогон
Run:
bash dsh/tests/run.sh
bash tests/run.sh
openspec validate dsh-install-source --strict
Expected: все три — код 0, dsh/tests зелёный, Change 'dsh-install-source' is valid.
- Step 2: Smoke в чистом HOME (без
~/.claude)
TMP=$(mktemp -d); HOME="$TMP" DSH_HOME="$TMP/.dsh" bash dsh/install.sh \
--global --skills --roles --superpowers-from <путь-к-superpowers>
# ожидание: код 0; файлы AGENTS.md, skills/{using-superpowers,role,sdd}/SKILL.md, roles/dev.md
# отсутствие $TMP/.claude; --check печатает ok: superpowers source и ok: roles
Bootstrap в этом HOME проверить Node-ом:
node -e "import('./dsh/bundle/lib/bootstrap.js').then(m=>console.log(m.prepareBootstrap({home: process.env.HOME}).marker))" — маркер на месте, предупреждений нет.
- Step 3: Зафиксировать приёмку
В design.md добавить ### Приёмка: прогон <дата> с командами и результатами; в dsh/PORT.md — что проверено, что осталось владельцу (fresh-session smoke без ~/.claude).
- Step 4: Commit, push, PR
git add -A
git commit -m "docs(dsh): acceptance for install without ~/.claude (#49)"
git push -u origin feat/49-dsh-install-source
tea pulls create --head feat/49-dsh-install-source --base main \
--title "DSH: установка без зависимости от ~/.claude (#49)" \
--description "<описание + Rulings + маркеры + what was tested>"
- Step 5: Итоговый отчёт
Таблица находок с маркерами и статусами, что проверено, что осталось человеку (merge, fresh-session smoke, --skills --roles на реальной машине).
Acceptance
- Спека
dsh-installпокрыта тестами: источник (5 сценариев), отсутствие источника (2), legacy (2), инструкции (2), чистый HOME (2). bash dsh/tests/run.sh,bash tests/run.sh,openspec validate --strict— зелёные.- Temp
HOMEбез~/.claudeпроходит--global --skills --roles, bootstrap даёт маркер,--checkзелёный. global/CLAUDE.mdне изменён; CC-контур (bash tests/run.sh) не затронут.