feat(dsh): --openspec добавляет agents-адаптер в проект (#42)
Co-Authored-By: DeepSeek Harness <noreply@deepseek.com>
This commit is contained in:
parent
5d0fdc4c0d
commit
748f8e115a
@ -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 <<EOF
|
||||
Usage: bash dsh/install.sh [--check] [--global] [--skills] [--roles] [--all]
|
||||
Usage: bash dsh/install.sh [--check] [--global] [--skills] [--roles] [--all] [--openspec]
|
||||
|
||||
--check verify python3, git, node, hooks, bundle, skills, and roles
|
||||
--global back up and write \$DSH_HOME/AGENTS.md from global/CLAUDE.md
|
||||
--skills copy superpowers skills and local dsh/skills into \$DSH_HOME/skills
|
||||
--roles copy roles/ into \$DSH_HOME/roles
|
||||
--all --check + --global + --skills + --roles, then print the bundle step
|
||||
--openspec add the agents skill adapter to the current OpenSpec project
|
||||
|
||||
Bundle installation itself is done in DSH:
|
||||
Plugins -> 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 <<EOF
|
||||
|
||||
@ -307,6 +325,11 @@ if [ "$DO_ROLES" = 1 ]; then
|
||||
install_roles
|
||||
install_role_dsh_notes
|
||||
fi
|
||||
# A failed --openspec step must not abort the other steps: the explicit return 1
|
||||
# becomes the script status here, like check() above.
|
||||
if [ "$DO_OPENSPEC" = 1 ]; then
|
||||
install_openspec "$PWD" || status=1
|
||||
fi
|
||||
if [ "$DO_GLOBAL" = 1 ] || [ "$DO_SKILLS" = 1 ] || [ "$DO_ROLES" = 1 ]; then
|
||||
print_bundle_step
|
||||
fi
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, symlinkSync } from 'node:fs'
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, symlinkSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import test from 'node:test'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
@ -66,3 +66,47 @@ test('install.sh --check warns but does not fail without the openspec CLI', () =
|
||||
cleanup(home)
|
||||
}
|
||||
})
|
||||
|
||||
function fakeOpenspec(dir, log) {
|
||||
const bin = join(dir, 'bin')
|
||||
write(join(bin, 'openspec'), `#!/bin/sh\necho "$PWD $@" >> ${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)
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user