# 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.md` MUST NOT меняться: его читает Claude Code. - `install.sh` MUST 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`; все тесты на temp `HOME`. - Маркировка находок: `T.`; решения в этом плане уже с маркерами `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 ` (и `--superpowers-from=`), env `DSH_SUPERPOWERS_SOURCE`; - bash-функции `normalize_superpowers_dir ` (печатает `skills`-каталог или rc 1) и `superpowers_source` (печатает `\t`, rc 1 если источника нет); - вывод `--skills`: `copied N superpowers skills into (source: )`; при legacy — строка `WARN: legacy superpowers source ; prefer --superpowers-from or ~/.dsh/superpowers`; - вывод `--check`: `ok: superpowers source ()` / `WARN: legacy superpowers source (); 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, ...])`): ```js test('--skills takes superpowers from --superpowers-from in a clean HOME', ...) // HOME без ~/.claude; источник: /src/superpowers/skills/using-superpowers/SKILL.md // + brainstorming/SKILL.md; запуск: --skills --superpowers-from /src/superpowers // assert: status 0; stdout /source: <...>/src\/superpowers\/skills/; // /skills/using-superpowers/SKILL.md и role/sdd на месте; // existsSync(/.claude) === false test('--superpowers-from accepts the skills directory itself', ...) // тот же fixture, флаг указывает на /src/superpowers/skills; status 0 test('--superpowers-from wins over DSH_SUPERPOWERS_SOURCE', ...) // два разных fixture-источника (A: body "a", B: body "b"); env=B, флаг=A; // в /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 с /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/; // /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 с /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`; печатать `\t`; явный невалидный источник — `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: )` в итоговой строке. - `check`: `ok: dsh skills` оставить; добавить ветку источника (`ok`/`WARN legacy`/`WARN none` по kind); legacy-источник не делает `check` падающим. - Dispatch: `install_skills || status=1; install_local_skills` — локальные скиллы ставятся всегда. - `usage`: строка `--superpowers-from ` с описанием трёх источников. - [ ] **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 `), затем `--all`; в Known limits — «источник superpowers задаётся установщиком; `~/.claude` — legacy». - [ ] **Step 6: Commit** ```bash 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: Падающий тест** ```js 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** ```bash 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` — истина для `/.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: Падающие тесты** ```js // 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** ```bash 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 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`)** ```bash 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** ```bash 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`) не затронут.