Resolve operator paths against the CWD, making uv tool install usable - #43
Merged
Conversation
dir_path was derived from os.path.dirname(os.path.realpath(__file__)), which decides where config.json, exclusions.txt, and scan output resolve. Under `uv tool install`, that directory is inside uv's managed tool venv -- not a place an operator can put a config or would want engagement results written, and one uv tool upgrade rebuilds from scratch. Extract the derivation into a module-level _operator_dir() helper (os.getcwd()) so it is testable outside main()'s pragma-no-cover region, and switch main()'s dir_path to it. _DIR/_NSE_DIR are left untouched -- they anchor the bundled NSE scripts, which are program data and must keep resolving from the module's own location regardless of the caller's CWD. Also drop the vestigial `global dir_path` in main(): nothing outside main() reads a module-level dir_path (both _cleanup_cmd and _load_config take it as a parameter), unlike output_path, whose global is genuinely read by tests via spoonmap.output_path. The two config.json.sample guidance messages now name the resolved path (via _DIR) instead of a bare filename, since that file lives next to the module -- nowhere near an installed user's CWD -- so a bare name is unactionable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
config.json, exclusions.txt, default output, relative config values, and --cleanup now resolve against the CWD the command was run from, not the directory containing spoonmap.py. Document that plainly in both README.md (a new "Where Files Live" section, plus an updated config.json parameter table entry for output_path) and CLAUDE.md (a "Operator path resolution" note tied to _operator_dir()), including an explicit call-out that invoking by absolute/relative path from another directory now behaves differently than before, and the one exception: bundled NSE scripts under nse/ are program data and keep resolving from the module's own location (_DIR/_NSE_DIR) regardless of CWD. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The docstring and a test comment justified CWD-vs-_DIR by citing `uv tool install`'s managed venv, which cites a packaging mechanism this project has not committed to shipping. Reworded to the install-agnostic reasoning already used in README.md: operator data belongs where the command was run from, not wherever the module happens to be installed -- true regardless of install mechanism. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Packaging decision is resolved -- the wheel stays exactly as built by pyproject.toml's hatch config, so the earlier hold on documenting the install path is lifted. Adds an "Installation" section to README.md covering `uv tool install git+https://github.com/trustedsec/spoonmap` (no PyPI package involved -- naming just `spoonmap` will not work), `uv tool upgrade`, and an explicit note that masscan/nmap still need separate installation either way. Cross-references "Where Files Live" rather than restating it, since an installed spoonmap is the case where the CWD rule matters most. CLAUDE.md gets one paragraph noting the wheel is a supported consumption path contributors must keep working (config.json.sample and nse/ landing in the wheel, per the build CI job) -- no end-user walkthrough, since that file is contributor-facing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dings - _operator_dir() docstring: restore the concrete hazard the round-1 override stripped. The packaging decision has since landed on keeping the wheel, so the install-agnostic wording was no longer the right call — an installed spoonmap's module directory lives inside uv's managed tool environment, which `uv tool upgrade` rebuilds from scratch, destroying anything stored there. That's what actually makes CWD the only defensible choice. - pyproject.toml's force-include comment and _load_config's docstring both described dir_path as script-relative, which this branch made false; reworded to describe the operator directory and _DIR correctly (config.json.sample ships at _DIR because that's where the error messages point, not because config.json itself lives there). - CLAUDE.md: "Operator path resolution" was a level-3 heading nested under "Running the Tool" with nothing else at that level between it and "Architecture", so ~135 lines of CI/test documentation read as nested under a path-resolution subsection. Moved it to its own level-2 "Operator Path Resolution" section between "Running the Tool" and "Architecture". - README: name both nse/ (checkout) and spoonmap_nse/ (installed wheel) as the NSE directory forms, since the Installation section now points installed users at this same paragraph. Soften the PyPI guidance to not diagnose a future `uv tool install spoonmap` failure as user error, in case the name is ever claimed on PyPI. Add a one-line forward reference from Usage to "Where Files Live". - tests/test_spoonmap.py: TestOperatorDirResolution's CWD comparisons now compare os.path.realpath() on both sides instead of raw str(tmp_path), since the prior form only passed because pytest happens to hand out an already-resolved tmp_path (macOS's /tmp -> /private/tmp makes this fragile). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"uv tool upgrade rebuilds from scratch" read as though any invocation destroys the module directory. Verified directly: `uv tool upgrade` with nothing to upgrade prints "Nothing to upgrade" and leaves planted sentinel files intact; only an invocation that actually installs a new version (equivalent to `uv tool install --force`) rebuilds the environment and loses them. Reworded to "rebuilt whenever uv tool upgrade actually installs a new version" so the comment doesn't die to someone testing a no-op upgrade and concluding the rationale is wrong. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Why
PR #42 made SpooNMAP installable —
uv tool install git+https://github.com/trustedsec/spoonmapbuilds a self-contained wheel and resolves all 19 bundled NSE scripts from the isolated tool venv. But the installed tool was not yet usable, becausespoonmap.pyderived its operator directory from the module's own location:Under
uv tool installthat is~/.local/share/uv/tools/spoonmap/lib/python3.12/site-packages. Anddir_pathdecided whereconfig.jsonwas read from, whereexclusions.txtdefaulted to, and — by default — where scan output was written. So an installed spoonmap wanted its config inside uv's managed tool environment and wrote engagement results there, into a directory that gets rebuilt whenever an upgrade installs a new version.The root cause is that
dir_pathconflated where the program lives with where the operator's data lives. Those are the same directory in a checkout, which is why it went unnoticed. PR #42 already split out the program-data half (bundled NSE scripts moved to_NSE_DIR); this finishes the job.What changed
Operator paths now resolve against the current working directory, via a single
_operator_dir()helper. That coversconfig.json,exclusions.txt, the default output location, relativetarget_file/output_path/exclusions_filevalues insideconfig.json, and--cleanup.The derivation was extracted out of
main()deliberately:main()carries# pragma: no cover, so the one line that mattered was the one line no test could reach. It is now a tested helper._DIRand_NSE_DIRare untouched — bundled NSE scripts ship inside the wheel and must keep resolving from the module's location, orscript_scanbreaks on an installed tool. The__file__anchor count went from two to one: the operator path no longer has any module anchor at all.Also fixed: the two config-error messages printed a bare
config.json.samplefilename, which is unactionable for an installed user whose copy sits next to the module. They now print the resolved path.No packaging or CI changes.
pyproject.toml's only edit is a comment this branch falsified.Verification
Proven against a real installed wheel rather than argued from source.
Runs from any directory, output stays local. Two unrelated directories, each with its own
config.json({"output_path": "results"}) and prior output:Config loading follows the CWD.
_load_configon the installed wheel, from an arbitrary directory:Nothing writes to
_DIR: its only remaining uses are the NSE anchor, the twoconfig.json.samplemessages, and a docstring. Everyos.makedirssits underoutput_path.Gate:
978 passed, 5 skipped, 100% coverage (floor 95%) · ruff clean ·uv lock --checkunaffected · bandit 32 baseline / 0 new. No suppressions, coverage floor untouched.Behaviour change
Invoking by absolute path from elsewhere (
cd /tmp && /opt/spoonmap/spoonmap.py) previously read/opt/spoonmap/config.jsonand wrote output under/opt/spoonmap/. It now uses/tmp. For the documented invocation — from inside the checkout — nothing changes, because the CWD and the checkout are the same directory.This is disclosed in the README's new "Where Files Live" section, with a pointer from "Usage" so someone reads it before running rather than after wondering where their output went.
Docs
## Installationsection coveringuv tool install git+...anduv tool upgrade, stating plainly that no PyPI package exists, and noting that installing does not remove the need for masscan and nmap.## Where Files Live, documenting the CWD rule, the behaviour change, and the one exception — bundled NSE scripts, named in both forms (nse/in a checkout,spoonmap_nse/installed) so a reader can match it to what they have on disk.## Operator Path Resolutioncovering the same semantics and the_DIR/_NSE_DIRboundary.🤖 Generated with Claude Code