diff --git a/dsh/install.sh b/dsh/install.sh index cf97ab3..2cda1f5 100755 --- a/dsh/install.sh +++ b/dsh/install.sh @@ -13,6 +13,7 @@ # bash dsh/install.sh --skills # bash dsh/install.sh --roles # bash dsh/install.sh --all +# bash dsh/install.sh --openspec # # Run it from a terminal, not from inside a DSH session: --global, --skills and # --roles write outside the session workspace and would require approval. @@ -25,16 +26,18 @@ DO_CHECK=0 DO_GLOBAL=0 DO_SKILLS=0 DO_ROLES=0 +DO_OPENSPEC=0 usage() { cat < install a bundle from $BUNDLE @@ -52,6 +55,7 @@ for arg in "$@"; do --global) DO_GLOBAL=1 ;; --skills) DO_SKILLS=1 ;; --roles) DO_ROLES=1 ;; + --openspec) DO_OPENSPEC=1 ;; --all) DO_CHECK=1; DO_GLOBAL=1; DO_SKILLS=1; DO_ROLES=1 ;; -h|--help) usage; exit 0 ;; *) echo "unknown option: $arg" >&2; usage >&2; exit 2 ;; @@ -278,6 +282,20 @@ print("ok: role dsh notes inserted") PY } +install_openspec() { + local dir="${1:-$PWD}" + if ! command -v openspec >/dev/null 2>&1; then + echo "FAIL: openspec CLI not found; install it first" >&2 + return 1 + fi + if [ ! -d "$dir/openspec" ]; then + echo "FAIL: $dir has no openspec/ root; run: openspec init --tools agents" >&2 + return 1 + fi + ( cd "$dir" && openspec init --tools agents ) + echo "ok: openspec agents adapter in $dir" +} + print_bundle_step() { cat <> ${JSON.stringify(log)}\nexit 0\n`) + chmodSync(join(bin, 'openspec'), 0o755) + return bin +} + +test('--openspec adds the agents adapter to an existing OpenSpec project', () => { + const home = tempDir() + try { + const project = join(home, 'project') + mkdirSync(join(project, 'openspec'), { recursive: true }) + // macOS: os.tmpdir() идёт через симлинк /var -> /private/var, а $PWD в шелле — уже реальный путь + const realProject = realpathSync(project) + const log = join(home, 'calls.log') + const bin = fakeOpenspec(home, log) + const env = { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh'), PATH: `${bin}:${process.env.PATH}` } + const run = spawnSync('bash', [script, '--openspec'], { cwd: project, env, encoding: 'utf8' }) + assert.equal(run.status, 0, run.stderr) + assert.match(run.stdout, /ok: openspec agents adapter in /) + assert.match(readFileSync(log, 'utf8'), new RegExp(`^${realProject} init --tools agents$`, 'm')) + } finally { + cleanup(home) + } +}) + +test('--openspec refuses a project without an openspec root', () => { + const home = tempDir() + try { + const project = join(home, 'plain') + mkdirSync(project, { recursive: true }) + const log = join(home, 'calls.log') + const bin = fakeOpenspec(home, log) + const env = { ...process.env, HOME: home, DSH_HOME: join(home, '.dsh'), PATH: `${bin}:${process.env.PATH}` } + const run = spawnSync('bash', [script, '--openspec'], { cwd: project, env, encoding: 'utf8' }) + assert.notEqual(run.status, 0) + assert.match(run.stdout + run.stderr, /has no openspec\/ root/) + assert.equal(existsSync(log), false, 'openspec must not run in a non-OpenSpec project') + assert.equal(existsSync(join(project, 'openspec')), false) + } finally { + cleanup(home) + } +})