feat(cli): add --target flag to install and uninstall#55
Open
dale-stewart wants to merge 1 commit into
Open
Conversation
Adds `--target <path>` to `nwave-ai install` and `nwave-ai uninstall`,
allowing installation to a non-default Claude config directory. Supports
two real use cases:
- alternate global: `nwave-ai install --target ~/.claude-nwave` then
`CLAUDE_CONFIG_DIR=~/.claude-nwave claude` for side-by-side configs
- per-project: `nwave-ai install --target ./.claude` for dogfooding a
fork without polluting global ~/.claude/
The flag is threaded into the install subprocess via the existing
CLAUDE_CONFIG_DIR seam in PathUtils.get_claude_config_dir (no per-callsite
plumbing). Paths are resolved with Path.expanduser().resolve(); --target
resolving to $HOME is refused at parse time with exit 2 and zero
filesystem mutation.
Also fixes the load-bearing hook-path bug: when the install target is
non-default, des_plugin._generate_hook_command now emits an absolute
<target>/lib/python rather than the portable $HOME/.claude/lib/python
form. Claude Code passes hook commands to a shell where $HOME resolves
to the user's real home (not the chosen target), so the portable form
would point at the wrong directory at runtime. The default-target case
keeps the existing $HOME-portable form for cross-machine ~/.claude
sync.
Backward compatibility: omitting --target preserves byte-identical
behavior. env.PATH writer already honored context.claude_dir; no change
needed there.
Refs: nWave-ai#40
Co-Authored-By: nWave <nwave@nwave.ai>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #40.
Summary
--target <path>tonwave-ai installandnwave-ai uninstall, enabling installation to a non-default Claude config directory.nwave-ai install --target ~/.claude-nwave→ run Claude withCLAUDE_CONFIG_DIR=~/.claude-nwave claudefor a side-by-side config) and per-project (nwave-ai install --target ./.claudefor dogfooding a fork without disturbing~/.claude/).--targetis byte-identical to the previous release.How it works
The flag is parsed in
_handle_install/_handle_uninstall(manual parsing, consistent with how--yesand--density-onlyare handled), normalized viaPath.expanduser().resolve(), and threaded into the install subprocess by settingCLAUDE_CONFIG_DIR=<absolute-target>. The existingPathUtils.get_claude_config_dir()seam already honors that env var, so no per-callsite plumbing was needed.--target $HOME(or any path whoserealpathequalsrealpath $HOME) is rejected at parse time with exit 2 and zero filesystem mutation — guards against the catastrophic global-overwrite case.Load-bearing fix: hook-command path resolution
When
nwave-ai install --target <non-default>runs, the existing installer correctly lands files under<target>/lib/python/des/, butdes_plugin._generate_hook_commandhardcodedlib_path = "$HOME/.claude/lib/python". At runtime, Claude Code passes hook commands to a shell where$HOMEis the user's real home (not the chosen target), so the portable form pointed at the wrong directory — hooks failed or silently exercised the global install.The fix is a 3-line conditional: when
context.claude_dir == Path.home() / ".claude"keep the existing portable$HOME/.claude/lib/pythonform (preserves cross-machine~/.claudesync semantics). Otherwise, emit the absolute<target>/lib/python. Non-default targets are per-machine by user choice, so the loss of cross-machine portability is the intended trade.env.PATHalready honoredcontext.claude_dir(it computesstr(context.claude_dir / "bin")at line 862), so no change was needed there — that was a happy accident from prior refactoring.Files changed
nwave_ai/cli.py_extract_target_flaghelper;--targetparsing in_handle_install; new_handle_uninstallwrapper; dispatch rewired (+79 lines)scripts/install/plugins/des_plugin.py_generate_hook_command(+13 lines)tests/installer/unit/cli/test_target_flag.py~expansion, relative-path resolution,$HOMErefusal (direct and via.from $HOME), uninstall symmetrytests/plugins/plugin-architecture/unit/test_des_plugin_target_flag.pytests/des/acceptance/test_hook_path_portability.pyPath.home()to tmp_path so the default-home assertions still hold under the test fixtureNet production: 2 files, +92 LOC. Tests: +282 LOC.
Test plan
pytest tests/installer/unit/cli/test_target_flag.py tests/plugins/plugin-architecture/unit/test_des_plugin_target_flag.py— 14/14 greenpytest tests/plugins tests/installer/unit/cli tests/des/acceptance/test_hook_path_portability.py tests/des/integration/test_hook_configuration.py tests/build/unit/shared— 275/275 green (2 skips are pre-existing container-level skips owned bytests/e2e/test_fresh_install.py)ruff check+ruff format --checkon all modified files — cleanmypy --ignore-missing-importson modified files — cleannwave-ai install --target ~/.claude-la-nwave→agents/nw/), 5 DES bin shims, settings.json, templates landed in~/.claude-la-nwave/PYTHONPATH=/home/<user>/.claude-la-nwave/lib/python(absolute, points at target)env.PATHprepends/home/<user>/.claude-la-nwave/bin~/.claude/mtime unchanged (zero global pollution)nwave-ai uninstall --target ~/.claude-la-nwave --forceremoves all DES hooks from settings.json and theagents/tree (residual files underbin/skills/scripts/templates/are the known upstream issue nwave-ai uninstall leaves residual artifacts (skills, lib, DES hooks) #39, not a regression of this PR)nwave-ai install --target $HOMErefused with exit 2 and no filesystem mutationnwave-ai install(no--target) byte-identical to today's behavior (verified by the existing test suite passing unchanged)Open follow-ups (not in this PR)
<target>/bin,<target>/skills, etc. afteruninstall --targetis the same residue tracked upstream as nwave-ai uninstall leaves residual artifacts (skills, lib, DES hooks) #39.--targetmakes the problem visible in alternate locations too; the fix belongs inuninstall_nwave.py.~/.nwave/global-config.jsonstill lives at the user's real home regardless of--target. Density / attribution settings remain shared across all targets — typically desired, but worth noting.Notes for review
The DESIGN considered (a) bash wrapper + curl distribution and (b)
HOME=<target>override + post-install sed — both were rejected after maintainer feedback in the issue thread.CLAUDE_CONFIG_DIRis strictly better thanHOME=<target>because it doesn't pollute the subprocess tree's view of$HOMEfor tools like git, gh, ssh, and pipx.The hook-path conditional preserves the
$HOME-portable form only for the default-home case, which is the one for which it was designed. Existing users who sync~/.claude/across machines are unaffected.