agentsquire 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. agentsquire-0.2.2/.gitignore +12 -0
  2. agentsquire-0.2.2/CHANGELOG.md +102 -0
  3. agentsquire-0.2.2/LICENSE +21 -0
  4. agentsquire-0.2.2/PKG-INFO +190 -0
  5. agentsquire-0.2.2/README.md +166 -0
  6. agentsquire-0.2.2/docs/api.md +229 -0
  7. agentsquire-0.2.2/docs/harnesses.md +120 -0
  8. agentsquire-0.2.2/pyproject.toml +58 -0
  9. agentsquire-0.2.2/src/agentsquire/__init__.py +100 -0
  10. agentsquire-0.2.2/src/agentsquire/cli.py +198 -0
  11. agentsquire-0.2.2/src/agentsquire/harnesses.py +141 -0
  12. agentsquire-0.2.2/src/agentsquire/hashing.py +59 -0
  13. agentsquire-0.2.2/src/agentsquire/py.typed +0 -0
  14. agentsquire-0.2.2/src/agentsquire/roots.py +50 -0
  15. agentsquire-0.2.2/src/agentsquire/skills.py +95 -0
  16. agentsquire-0.2.2/src/agentsquire/sources.py +120 -0
  17. agentsquire-0.2.2/src/agentsquire/staleness.py +76 -0
  18. agentsquire-0.2.2/src/agentsquire/stamping.py +74 -0
  19. agentsquire-0.2.2/src/agentsquire/verbs.py +335 -0
  20. agentsquire-0.2.2/tests/test_api_lifecycle.py +79 -0
  21. agentsquire-0.2.2/tests/test_backends.py +110 -0
  22. agentsquire-0.2.2/tests/test_bundled_source.py +173 -0
  23. agentsquire-0.2.2/tests/test_cli_group.py +268 -0
  24. agentsquire-0.2.2/tests/test_docs_snippets.py +101 -0
  25. agentsquire-0.2.2/tests/test_entry_point.py +119 -0
  26. agentsquire-0.2.2/tests/test_harness_registry.py +207 -0
  27. agentsquire-0.2.2/tests/test_hash.py +116 -0
  28. agentsquire-0.2.2/tests/test_install.py +299 -0
  29. agentsquire-0.2.2/tests/test_package.py +5 -0
  30. agentsquire-0.2.2/tests/test_reference_doc.py +78 -0
  31. agentsquire-0.2.2/tests/test_root_overrides.py +151 -0
  32. agentsquire-0.2.2/tests/test_skill_model.py +139 -0
  33. agentsquire-0.2.2/tests/test_source_seam.py +185 -0
  34. agentsquire-0.2.2/tests/test_staleness_check.py +340 -0
  35. agentsquire-0.2.2/tests/test_status.py +186 -0
  36. agentsquire-0.2.2/tests/test_uninstall.py +164 -0
  37. agentsquire-0.2.2/tests/test_update.py +219 -0
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ .pytest_cache/
8
+ checkouts/
9
+
10
+ # Local scratch / dev notes (never committed, never published)
11
+ bugs.md
12
+ feedback.md
@@ -0,0 +1,102 @@
1
+ # Changelog
2
+
3
+ All notable changes to AgentSquire are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims
5
+ to follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ The version is declared in two places that are kept in sync by the release
8
+ tooling: `__version__` in `src/agentsquire/__init__.py` and `version` in
9
+ `pyproject.toml`.
10
+
11
+ ## [0.2.2]
12
+
13
+ ### Changed
14
+ - **Source distributions now ship an explicit, minimal file set.** The sdist is
15
+ scoped with `[tool.hatch.build.targets.sdist]` to `src/`, `tests/`,
16
+ `CHANGELOG.md`, and the two public docs (`docs/api.md`, `docs/harnesses.md`),
17
+ plus the always-included `pyproject.toml`/`README.md`/`LICENSE`. Previously the
18
+ sdist inherited hatchling's default (every VCS-tracked file), so
19
+ repository-only files could ride along; the wheel was already limited to the
20
+ package. This is a default-deny allowlist - files not listed never ship.
21
+
22
+ ## [0.2.1]
23
+
24
+ ### Fixed
25
+ - **A pre-existing symlink at a target no longer crashes install/update or
26
+ gets clobbered (BUG-02).** The classifier and copy guard tested the target
27
+ with `Path.exists()`, which follows a symlink: a dangling link read as
28
+ not-installed and then `install` hit `FileExistsError`, while a live link
29
+ reached `shutil.rmtree` on `update --force` and raised "Cannot call rmtree
30
+ on a symbolic link". Detection now uses `is_symlink`, so a symlink (dangling
31
+ or live) classifies as `LOCALLY_MODIFIED` - present but not ours. `install`
32
+ and `update` (without `--force`) skip it with a reason naming the symlink;
33
+ `update --force` unlinks the link (never `rmtree`s through it) before
34
+ writing a real install; `uninstall` leaves it in place with a reason.
35
+ Symlinking skills into `~/.claude/skills` is a mainstream hand-wired setup,
36
+ so anyone migrating to `skills install` from it hit this on the first run.
37
+
38
+ ## [0.2.0] - 2026-07-10
39
+
40
+ ### Added
41
+ - **`AGENTSQUIRE_HOME` / `AGENTSQUIRE_PROJECT` root overrides.** When
42
+ `check_stale` and the mounted `skills` subcommands are wired the production
43
+ way (no `home=`/`project=` arguments, so each invocation resolves the real
44
+ roots), these env vars redirect the home and project roots. A consumer's
45
+ CLI-level test can point the wired hook and the subcommands at fixture
46
+ directories by setting two variables instead of monkeypatching `Path.home`
47
+ and chdir. Explicit `home=`/`project=` arguments still take precedence; an
48
+ empty value is treated as unset (the `NO_COLOR` convention). Documented in
49
+ the `docs/api.md` "Testing your integration" section.
50
+
51
+ ### Fixed
52
+ - **`check_stale` now surfaces the update notice on non-TTY stderr.** The
53
+ hook previously required stderr to be an interactive TTY, which silenced
54
+ it in exactly the case that matters most for an agent-skills tool: an
55
+ agent harness runs the consumer CLI with captured (non-TTY) stderr and so
56
+ never saw that an update was available. The interactive-TTY gate is
57
+ removed; `CI` and `AGENTSQUIRE_NO_UPDATE_CHECK` (presence-disables) remain
58
+ the escape hatches for pipelines and opt-out. README and `docs/api.md`
59
+ updated accordingly.
60
+
61
+ ## [0.1.0] - 2026-07-10
62
+
63
+ Initial release.
64
+
65
+ ### Added
66
+ - **Mountable `skills` command group.** `skills_command_group(package, ...)`
67
+ returns a ready-made click group with `install`, `status`, `update`, and
68
+ `uninstall` subcommands that a consumer CLI mounts under its own name, so
69
+ its users only ever see one tool (e.g. `awiki skills install`). Works with
70
+ click directly and with typer via `typer.main.get_command`. Every
71
+ subcommand takes `--scope user|project` and `--harness NAME`.
72
+ - **Skills as package data.** Consumers ship SKILL.md directories (the
73
+ agentskills.io format) as package data inside their own wheel; skills are
74
+ enumerated straight from the installed wheel, no source checkout needed.
75
+ `BundledPackageDataSource` reads them, with structural validation against
76
+ the agentskills.io spec.
77
+ - **Four harness backends.** Claude Code, pi, Hermes, and opencode, detected
78
+ by marker directory, each with user and project scopes. Directories and
79
+ behaviour are recorded in `docs/harnesses.md`; backends resolve exactly
80
+ the paths documented there.
81
+ - **Copy-with-provenance install model.** Installs are plain copies - no
82
+ symlinks, no lockfile - stamped with an `agentsquire` provenance block
83
+ (installer, installer_version, source_package, source_version,
84
+ content_hash) in the installed SKILL.md frontmatter. The skill body stays
85
+ byte-identical to what was shipped.
86
+ - **Hash-based status and safe updates.** `status` classifies each skill as
87
+ not-installed, up-to-date, update-available, or locally-modified using
88
+ local hash compares only (no network in any verb). `update` refreshes
89
+ update-available skills and skips locally modified ones unless `--force`;
90
+ `uninstall` removes only directories whose stamp names the consumer's
91
+ package. User content is never silently overwritten or deleted.
92
+ - **Notice-only staleness hook.** `check_stale(source, prog_name=...,
93
+ update_command=...)` prints a single stderr advisory when installed skills
94
+ are stale. It never prompts, never updates, never touches stdout or the
95
+ exit code, and swallows its own errors. Gated on stderr being a TTY, `CI`
96
+ unset or empty, and `AGENTSQUIRE_NO_UPDATE_CHECK` unset or empty.
97
+ - **Python API.** Everything the CLI group does - enumerate, detect, install,
98
+ status, update, uninstall, staleness check - is available as plain Python;
99
+ see `docs/api.md`.
100
+ - **`agentsquire.skills` entry-point group.** Consumers may register their
101
+ package under it; reserved for a future environment-wide listing, changes
102
+ no behaviour today.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TacoTakumi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,190 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentsquire
3
+ Version: 0.2.2
4
+ Summary: Let a Python package carry its own agent integrations and install them into whatever agent harness is present.
5
+ Author: TacoTakumi
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: agent,agent-skills,claude-code,cli,harness,skills
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: click
22
+ Requires-Dist: pyyaml
23
+ Description-Content-Type: text/markdown
24
+
25
+ # AgentSquire
26
+
27
+ A reusable Python library + CLI that lets a Python package carry its own agent
28
+ integrations (Agent Skills) and install them into whatever agent harness is
29
+ present - the executable is the framework.
30
+
31
+ Your CLI ships its skills as package data inside its own wheel, adds
32
+ `agentsquire` as a plain pip dependency, and mounts a ready-made subcommand
33
+ group. Your users only ever see your tool:
34
+
35
+ ```
36
+ $ awiki skills install
37
+ installed awiki-search -> /home/you/.claude/skills/awiki-search
38
+ ```
39
+
40
+ Supported harnesses at launch: Claude Code, pi, Hermes, and opencode. Detection
41
+ is by marker directory; every operation is local (no network in any verb). Each
42
+ harness's directories, scopes, and behaviour are recorded in
43
+ [docs/harnesses.md](docs/harnesses.md).
44
+
45
+ ## Consumer integration guide
46
+
47
+ ### 1. Ship skills as package data
48
+
49
+ Lay each skill out as a directory containing a `SKILL.md` (the agentskills.io
50
+ format) under a `skills/` resource inside your importable package:
51
+
52
+ ```
53
+ your_pkg/
54
+ __init__.py
55
+ skills/
56
+ my-skill/
57
+ SKILL.md
58
+ reference.md
59
+ ```
60
+
61
+ The skills ride inside your wheel; make sure your build backend includes
62
+ package data (hatchling includes it by default, setuptools needs
63
+ `include-package-data`). No source checkout is needed at run time - skills are
64
+ enumerated straight from the installed wheel.
65
+
66
+ ### 2. Mount the subcommand group
67
+
68
+ One call returns a click group with `install`, `status`, `update`, and
69
+ `uninstall` subcommands, parameterized by your package name, the resource path
70
+ (default `"skills"`), and the default scope.
71
+
72
+ For a click CLI:
73
+
74
+ ```python
75
+ import click
76
+
77
+ from agentsquire.cli import skills_command_group
78
+
79
+
80
+ @click.group()
81
+ def cli():
82
+ """Your CLI."""
83
+
84
+
85
+ cli.add_command(skills_command_group("your_pkg", default_scope="user"))
86
+ ```
87
+
88
+ For a typer CLI, mount onto the underlying click command:
89
+
90
+ ```python
91
+ import typer
92
+ import typer.main
93
+
94
+ from agentsquire.cli import skills_command_group
95
+
96
+ app = typer.Typer()
97
+
98
+
99
+ @app.callback()
100
+ def main():
101
+ """Your CLI."""
102
+
103
+
104
+ cli = typer.main.get_command(app)
105
+ cli.add_command(skills_command_group("your_pkg", default_scope="user"))
106
+ ```
107
+
108
+ Your users now run `your-cli skills install` and friends. Every subcommand
109
+ takes `--scope user|project` (overriding your declared default) and
110
+ `--harness NAME` (default: all detected harnesses).
111
+
112
+ Choosing the default scope: `user` installs into the harness's per-user
113
+ skills directory and follows the user everywhere - right for general-purpose
114
+ tools. `project` installs into the current project's directory - right for
115
+ skills that only make sense inside a repository that uses your tool.
116
+
117
+ ### 3. Surface updates proactively (optional)
118
+
119
+ Place the one-call staleness hook at your CLI entry point. When installed
120
+ skills have updates available it prints a single advisory line on stderr,
121
+ for example:
122
+
123
+ your-cli: a skills update is available for 1 skill (alpha); run `your-cli skills update`
124
+
125
+ The hook is notice-only: it never prompts, never reads stdin, and never
126
+ updates anything itself - the explicit `skills update` verb stays the sole
127
+ updater. It writes nothing to stdout, never changes your exit code, and
128
+ swallows its own errors, so it can never break the command it runs inside:
129
+
130
+ ```python
131
+ from agentsquire import BundledPackageDataSource, check_stale
132
+
133
+
134
+ def main():
135
+ check_stale(
136
+ BundledPackageDataSource("your_pkg"),
137
+ prog_name="your-cli",
138
+ update_command="your-cli skills update",
139
+ )
140
+ # ... the rest of your entry point
141
+ ```
142
+
143
+ The notice shows unless a suppression gate holds: `CI` set to a non-empty
144
+ value, or `AGENTSQUIRE_NO_UPDATE_CHECK` set to a non-empty value. It is not
145
+ gated on an interactive terminal - it fires on non-TTY stderr too, so an
146
+ agent harness that runs your CLI with captured stderr still sees that an
147
+ update is available. Suppression is presence-disables, the `NO_COLOR`
148
+ convention: any non-empty value disables the notice (`CI=false` and
149
+ `AGENTSQUIRE_NO_UPDATE_CHECK=0` both suppress), while an empty string is
150
+ treated as unset.
151
+
152
+ ### 4. Mark your package as skill-carrying (optional)
153
+
154
+ One pyproject line registers your package under the `agentsquire.skills`
155
+ entry-point group. Nothing reads it today - it is reserved for a future
156
+ environment-wide listing of skill-carrying packages and changes no behaviour:
157
+
158
+ ```toml
159
+ [project.entry-points."agentsquire.skills"]
160
+ your_pkg = "your_pkg"
161
+ ```
162
+
163
+ ## The provenance and update model
164
+
165
+ Installs are plain copies - no symlinks, no lockfile, no references back into
166
+ site-packages - so an installed skill survives upgrade or removal of your
167
+ package. Each installed `SKILL.md` carries a provenance stamp in its
168
+ frontmatter `metadata.agentsquire` map: `installer`, `installer_version`,
169
+ `source_package`, `source_version`, and `content_hash`. The skill body is
170
+ byte-identical to what you shipped.
171
+
172
+ `status` classifies every skill by local hash compares only (no network,
173
+ ever): not-installed, up-to-date, update-available (your shipped copy moved
174
+ on), or locally-modified (the user edited the install, the directory carries
175
+ no stamp, or a symlink sits at the target - none of them ours to touch).
176
+ `update` refreshes update-available skills and skips locally modified ones
177
+ unless `--force` is given; `uninstall` removes only directories whose stamp
178
+ names your package. User content is never silently overwritten or deleted -
179
+ a pre-existing symlink at a target (a common hand-wired setup) is reported
180
+ and skipped, never followed or clobbered.
181
+
182
+ ## Python API
183
+
184
+ Everything the CLI group does is available as plain Python - enumerate,
185
+ detect, install, status, update, uninstall, and the staleness check - with no
186
+ CLI involved. See [docs/api.md](docs/api.md) for the reference.
187
+
188
+ ## License
189
+
190
+ MIT.
@@ -0,0 +1,166 @@
1
+ # AgentSquire
2
+
3
+ A reusable Python library + CLI that lets a Python package carry its own agent
4
+ integrations (Agent Skills) and install them into whatever agent harness is
5
+ present - the executable is the framework.
6
+
7
+ Your CLI ships its skills as package data inside its own wheel, adds
8
+ `agentsquire` as a plain pip dependency, and mounts a ready-made subcommand
9
+ group. Your users only ever see your tool:
10
+
11
+ ```
12
+ $ awiki skills install
13
+ installed awiki-search -> /home/you/.claude/skills/awiki-search
14
+ ```
15
+
16
+ Supported harnesses at launch: Claude Code, pi, Hermes, and opencode. Detection
17
+ is by marker directory; every operation is local (no network in any verb). Each
18
+ harness's directories, scopes, and behaviour are recorded in
19
+ [docs/harnesses.md](docs/harnesses.md).
20
+
21
+ ## Consumer integration guide
22
+
23
+ ### 1. Ship skills as package data
24
+
25
+ Lay each skill out as a directory containing a `SKILL.md` (the agentskills.io
26
+ format) under a `skills/` resource inside your importable package:
27
+
28
+ ```
29
+ your_pkg/
30
+ __init__.py
31
+ skills/
32
+ my-skill/
33
+ SKILL.md
34
+ reference.md
35
+ ```
36
+
37
+ The skills ride inside your wheel; make sure your build backend includes
38
+ package data (hatchling includes it by default, setuptools needs
39
+ `include-package-data`). No source checkout is needed at run time - skills are
40
+ enumerated straight from the installed wheel.
41
+
42
+ ### 2. Mount the subcommand group
43
+
44
+ One call returns a click group with `install`, `status`, `update`, and
45
+ `uninstall` subcommands, parameterized by your package name, the resource path
46
+ (default `"skills"`), and the default scope.
47
+
48
+ For a click CLI:
49
+
50
+ ```python
51
+ import click
52
+
53
+ from agentsquire.cli import skills_command_group
54
+
55
+
56
+ @click.group()
57
+ def cli():
58
+ """Your CLI."""
59
+
60
+
61
+ cli.add_command(skills_command_group("your_pkg", default_scope="user"))
62
+ ```
63
+
64
+ For a typer CLI, mount onto the underlying click command:
65
+
66
+ ```python
67
+ import typer
68
+ import typer.main
69
+
70
+ from agentsquire.cli import skills_command_group
71
+
72
+ app = typer.Typer()
73
+
74
+
75
+ @app.callback()
76
+ def main():
77
+ """Your CLI."""
78
+
79
+
80
+ cli = typer.main.get_command(app)
81
+ cli.add_command(skills_command_group("your_pkg", default_scope="user"))
82
+ ```
83
+
84
+ Your users now run `your-cli skills install` and friends. Every subcommand
85
+ takes `--scope user|project` (overriding your declared default) and
86
+ `--harness NAME` (default: all detected harnesses).
87
+
88
+ Choosing the default scope: `user` installs into the harness's per-user
89
+ skills directory and follows the user everywhere - right for general-purpose
90
+ tools. `project` installs into the current project's directory - right for
91
+ skills that only make sense inside a repository that uses your tool.
92
+
93
+ ### 3. Surface updates proactively (optional)
94
+
95
+ Place the one-call staleness hook at your CLI entry point. When installed
96
+ skills have updates available it prints a single advisory line on stderr,
97
+ for example:
98
+
99
+ your-cli: a skills update is available for 1 skill (alpha); run `your-cli skills update`
100
+
101
+ The hook is notice-only: it never prompts, never reads stdin, and never
102
+ updates anything itself - the explicit `skills update` verb stays the sole
103
+ updater. It writes nothing to stdout, never changes your exit code, and
104
+ swallows its own errors, so it can never break the command it runs inside:
105
+
106
+ ```python
107
+ from agentsquire import BundledPackageDataSource, check_stale
108
+
109
+
110
+ def main():
111
+ check_stale(
112
+ BundledPackageDataSource("your_pkg"),
113
+ prog_name="your-cli",
114
+ update_command="your-cli skills update",
115
+ )
116
+ # ... the rest of your entry point
117
+ ```
118
+
119
+ The notice shows unless a suppression gate holds: `CI` set to a non-empty
120
+ value, or `AGENTSQUIRE_NO_UPDATE_CHECK` set to a non-empty value. It is not
121
+ gated on an interactive terminal - it fires on non-TTY stderr too, so an
122
+ agent harness that runs your CLI with captured stderr still sees that an
123
+ update is available. Suppression is presence-disables, the `NO_COLOR`
124
+ convention: any non-empty value disables the notice (`CI=false` and
125
+ `AGENTSQUIRE_NO_UPDATE_CHECK=0` both suppress), while an empty string is
126
+ treated as unset.
127
+
128
+ ### 4. Mark your package as skill-carrying (optional)
129
+
130
+ One pyproject line registers your package under the `agentsquire.skills`
131
+ entry-point group. Nothing reads it today - it is reserved for a future
132
+ environment-wide listing of skill-carrying packages and changes no behaviour:
133
+
134
+ ```toml
135
+ [project.entry-points."agentsquire.skills"]
136
+ your_pkg = "your_pkg"
137
+ ```
138
+
139
+ ## The provenance and update model
140
+
141
+ Installs are plain copies - no symlinks, no lockfile, no references back into
142
+ site-packages - so an installed skill survives upgrade or removal of your
143
+ package. Each installed `SKILL.md` carries a provenance stamp in its
144
+ frontmatter `metadata.agentsquire` map: `installer`, `installer_version`,
145
+ `source_package`, `source_version`, and `content_hash`. The skill body is
146
+ byte-identical to what you shipped.
147
+
148
+ `status` classifies every skill by local hash compares only (no network,
149
+ ever): not-installed, up-to-date, update-available (your shipped copy moved
150
+ on), or locally-modified (the user edited the install, the directory carries
151
+ no stamp, or a symlink sits at the target - none of them ours to touch).
152
+ `update` refreshes update-available skills and skips locally modified ones
153
+ unless `--force` is given; `uninstall` removes only directories whose stamp
154
+ names your package. User content is never silently overwritten or deleted -
155
+ a pre-existing symlink at a target (a common hand-wired setup) is reported
156
+ and skipped, never followed or clobbered.
157
+
158
+ ## Python API
159
+
160
+ Everything the CLI group does is available as plain Python - enumerate,
161
+ detect, install, status, update, uninstall, and the staleness check - with no
162
+ CLI involved. See [docs/api.md](docs/api.md) for the reference.
163
+
164
+ ## License
165
+
166
+ MIT.