boost-skill-cli 1.0.92__tar.gz → 1.0.93__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.
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/PKG-INFO +1 -1
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/_version.py +2 -2
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/pkg.py +20 -1
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/logs.py +5 -0
- boost_skill_cli-1.0.93/boost_cli/core/typosquat.py +85 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/PKG-INFO +1 -1
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/SOURCES.txt +3 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/log-timestamps-mislabeled-utc.md +3 -3
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/typosquat-and-name-confusion-detection.md +3 -3
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap.html +2 -2
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_logs.py +36 -0
- boost_skill_cli-1.0.93/tests/unit/test_pkg_confusions.py +45 -0
- boost_skill_cli-1.0.93/tests/unit/test_typosquat.py +132 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/.gitattributes +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/.gitignore +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/CLAUDE.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/CONTRIBUTING.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/LICENSE +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/MANIFEST.in +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/Makefile +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/README.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/SECURITY.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/__init__.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/__main__.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/cli.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/cliparse.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/__init__.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/bmad.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/configuration.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/discovery.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/hooks.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/info.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/intelligence.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/quality.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/taps.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/commands/team.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/__init__.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/agents.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/ai.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/catalog.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/claude_settings.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/config.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/dense.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/embed.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/frontmatter.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/gitutil.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/journal.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/lockfile.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/mcp.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/output.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/paths.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/policy.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/rag.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/registry.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/staleness.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/store.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/core/util.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/data/registries.json +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/errors.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_cli/spin.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/entry_points.txt +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/requires.txt +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/top_level.txt +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/.claude/settings.local.json +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/DEBUGGING.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/demo.gif +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/demo.tape +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/design-roadmap.html +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/index.html +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/mcp-hub.html +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/rag-architecture.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D01.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D02.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D03.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D04.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D05.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D06.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D07.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D08.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D09.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D10.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D11.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D12.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D13.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D14.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D15.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D16.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D17.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D18.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D19.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D20.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D21.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D22.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D23.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D24.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D25.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/BOOST-D26.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/accessibility-audit-pa11y-ci-axe-core.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/atomic-corruption-safe-lock-file-writes.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/atomic-skill-install-temp-dir-swap.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/autonomous-ship-workflow-and-isolated-worktree.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/bring-commands-under-mutation-testing.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/broken-link-and-anchor-checking-lychee.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/build-provenance-slsa-attestations.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/cache-the-catalog-entry-set-across-rag-queries.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/capability-manifest-and-least-privilege-policy.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/clean-env-install-smoke-pip-and-pipx.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/command-reference-docs-site.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/complexity-and-dead-code-radar-xenon-vulture.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/consolidate-skill-staleness-drift-logic-into-cor.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/coverage-dashboard-codecov-free-for-oss.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/coverage-guided-fuzzing-atheris-oss-fuzz.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/crash-recorder-error-paths-core-logs-py.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/dependency-cve-gate-pip-audit.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/docstring-coverage-interrogate.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/end-of-options-guard-on-git-commands.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/extension-free-tests-core-dense-py.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/extract-mcp-http-servers-out-of-configuration-py.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/finish-mutation-hardening-across-core.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/fix-self-update-version-detection-dead-branch.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/fork-safe-network-proxy-handler.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/frontmatter-scalar-over-coercion.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/github-pages-deploy-is-broken-every-push.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/hash-pinned-reproducible-toolchain-uv-lock.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/html-validation-html-validate.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/integrity-verification-boost-verify.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/invocation-pid-logging-for-crash-correlation.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/layering-guard-import-linter.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/lighthouse-ci-on-the-pages-site.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/localize-the-stored-bm25-snippet.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/lockfile-enforcement-and-commit-pinning.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/lowest-version-resolution-uv-resolution-lowest-d.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/make-the-roadmaps-discoverable.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/markdown-consistency-markdownlint-cli2.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/mcp-launch-objc-fork-safety.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/memoize-config-load-in-process.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/modernization-smells-refurb-pyupgrade.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/mutation-hardening-core-frontmatter-py.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/mutation-hardening-core-gitutil-py.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/mutation-hardening-core-store-py.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/one-command-every-env-nox.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/one-shared-atomic-write-helper.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/package-metadata-validation-twine-check-friends.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/patch-coverage-gate-diff-cover.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/performance-regression-gate-pytest-benchmark.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/post-deploy-smoke-headless-load-check.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/pre-release-python-canary-3-14t-free-threaded.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/promote-nav-footer-into-the-shared-style-system.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/prompt-injection-scanning-of-skill-markdown.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/property-based-tests-hypothesis-on-the-parsers.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/prose-and-terminology-linting-vale.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/prune-ignored-dirs-during-scan-dir-walk.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/quality-dashboard-sonarcloud-free-for-oss.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/reconcile-the-theme-drift.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/refresh-the-marketing-surface.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/reuse-helpers-kill-minor-dead-work.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/robust-tag-argument-parsing.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/rule-install-native-materialization.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/runner-egress-monitoring-stepsecurity-harden-run.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/sbom-aware-scanning-osv-scanner.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/sbom-on-every-release-cyclonedx-syft.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/second-type-checker-pyright.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/secret-and-pii-scanning-of-installed-skills.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/secret-scanning-gitleaks-push-protection.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/security-linting-bandit-via-ruff-s-rules.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/shift-left-gate-pre-commit-pre-commit-ci.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/single-imperative-rule-extractor.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/single-tech-stack-prober.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/split-oversized-command-modules.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/startup-and-import-time-budget-x-importtime.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/stop-re-serializing-entry-meta-on-every-search.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/supply-chain-posture-openssf-scorecard.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/tap-signing-and-provenance-sigstore-minisign.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/theme-asset-linting-stylelint-eslint.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/typo-detection-codespell.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/unify-tilde-two-copies-have-a-boundary-bug.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/untrack-generated-build-noise.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/update-diff-before-apply.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/visual-regression-pass-on-the-guide.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/widen-the-ruff-rule-surface-b-sim-c4-perf-ruf.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/windows-in-the-ci-matrix.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/workflow-linting-actionlint.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/workflow-sast-zizmor.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/docs/roadmap/items/workspace-scope-install.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/index.html +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/pyproject.toml +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/scripts/build_registries.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/scripts/build_roadmap.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/scripts/mutation_gate.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/setup.cfg +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/style/README.md +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/style/boost.css +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/style/boost.js +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/style/demo.html +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/conftest.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_bmad.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_configuration.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_discovery.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_hooks.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_info.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_intelligence.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_pkg.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_quality.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_taps.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_cli_team.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/functional/test_everyday_loop.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/make_fixture.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/smoke.sh +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_agents.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_ai.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_catalog.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_claude_settings.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_cliparse.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_config.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_dense.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_dense_fallback.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_embed.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_errors_and_cli_table.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_frontmatter.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_gitutil.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_journal.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_lockfile.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_mcp.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_mutation_hardening.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_output.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_paths.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_policy.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_rag.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_registries_fresh.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_registry.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_roadmap_fresh.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_spin.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_staleness.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_store.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_token_parity.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_util.py +0 -0
- {boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/tests/unit/test_version.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 0,
|
|
21
|
+
__version__ = version = '1.0.93'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 93)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -16,7 +16,7 @@ from typing import Dict, List, Optional
|
|
|
16
16
|
|
|
17
17
|
from .. import cliparse
|
|
18
18
|
from ..core import (agents, catalog, gitutil, journal, lockfile, paths,
|
|
19
|
-
registry, staleness, store, util)
|
|
19
|
+
registry, staleness, store, typosquat, util)
|
|
20
20
|
from ..core import output as out
|
|
21
21
|
from ..errors import BoostError
|
|
22
22
|
|
|
@@ -35,6 +35,23 @@ def _plural(n: int, word: str) -> str:
|
|
|
35
35
|
return "%d %s%s" % (n, word, "" if n == 1 else "s")
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def _warn_confusions(entries: List[dict]) -> None:
|
|
39
|
+
"""Warn when a skill about to be installed is confusable with one from a
|
|
40
|
+
*different* tap — the typosquat / owner-confusion guard (core.typosquat).
|
|
41
|
+
|
|
42
|
+
Quiet by default: it only speaks up when a near-identical name resolves to
|
|
43
|
+
another owner, which is exactly the fat-finger-into-a-look-alike case.
|
|
44
|
+
"""
|
|
45
|
+
if not entries:
|
|
46
|
+
return
|
|
47
|
+
all_entries = catalog.all_entries()
|
|
48
|
+
for e in entries:
|
|
49
|
+
for other in typosquat.find_confusions(e, all_entries)[:3]:
|
|
50
|
+
out.warn("%s (%s) closely resembles %s (%s) — double-check you "
|
|
51
|
+
"meant this one" % (e["name"], e["tap"],
|
|
52
|
+
other["name"], other["tap"]))
|
|
53
|
+
|
|
54
|
+
|
|
38
55
|
def _check_agents(names: Optional[List[str]]) -> Optional[List[str]]:
|
|
39
56
|
"""Validate --agent values against configured agents."""
|
|
40
57
|
if not names:
|
|
@@ -106,6 +123,8 @@ def cmd_install(argv: List[str]) -> int:
|
|
|
106
123
|
out.warn("%s: %s" % (n, err.message))
|
|
107
124
|
failed += 1
|
|
108
125
|
|
|
126
|
+
_warn_confusions(entries)
|
|
127
|
+
|
|
109
128
|
if args.dry_run:
|
|
110
129
|
targets = [a for a in agents.enabled_agents() if not only or a in only]
|
|
111
130
|
for e in entries:
|
|
@@ -36,6 +36,7 @@ import logging.handlers
|
|
|
36
36
|
import os
|
|
37
37
|
import platform
|
|
38
38
|
import sys
|
|
39
|
+
import time
|
|
39
40
|
import traceback
|
|
40
41
|
from datetime import datetime, timezone
|
|
41
42
|
from pathlib import Path
|
|
@@ -143,6 +144,10 @@ def configure(verbose: bool = False, debug: bool = False,
|
|
|
143
144
|
"%(asctime)s %(levelname)-7s %(name)s: %(message)s",
|
|
144
145
|
datefmt="%Y-%m-%dT%H:%M:%SZ",
|
|
145
146
|
)
|
|
147
|
+
# datefmt stamps a literal ``Z`` (UTC) suffix, so the timestamp must be
|
|
148
|
+
# rendered in UTC — otherwise ``%(asctime)s`` uses local time and every
|
|
149
|
+
# line is mislabelled by the machine's offset. gmtime keeps it honest.
|
|
150
|
+
fmt.converter = time.gmtime
|
|
146
151
|
|
|
147
152
|
# File handler — always DEBUG, best-effort (never break the CLI over a log).
|
|
148
153
|
if _file_enabled():
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Detect typosquatted / confusable skill names (stdlib only).
|
|
2
|
+
|
|
3
|
+
The classic package-manager attack is a skill named one keystroke away from a
|
|
4
|
+
popular one, or a familiar name that quietly resolves to a *different*
|
|
5
|
+
owner/repo. These helpers measure how close two names are and flag catalog
|
|
6
|
+
entries that are suspiciously similar to — but not the same source as — the one
|
|
7
|
+
a user asked for, so a command can warn before installing a look-alike.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import List
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def edit_distance(a: str, b: str, cap: int = 2) -> int:
|
|
15
|
+
"""Levenshtein distance between ``a`` and ``b``, not measured past ``cap``.
|
|
16
|
+
|
|
17
|
+
Returns the true distance when it is ``<= cap``, otherwise ``cap + 1`` — a
|
|
18
|
+
sentinel meaning "farther apart than we care about". The row-minimum
|
|
19
|
+
early-exit keeps a comparison against a large catalog cheap: as soon as an
|
|
20
|
+
entire DP row exceeds ``cap`` no completion can come back under it.
|
|
21
|
+
"""
|
|
22
|
+
if a == b:
|
|
23
|
+
return 0
|
|
24
|
+
la, lb = len(a), len(b)
|
|
25
|
+
if abs(la - lb) > cap: # a length gap alone already exceeds cap
|
|
26
|
+
return cap + 1
|
|
27
|
+
if la > lb: # iterate over the shorter string's rows
|
|
28
|
+
a, b, la, lb = b, a, lb, la
|
|
29
|
+
prev = list(range(lb + 1))
|
|
30
|
+
for i in range(1, la + 1):
|
|
31
|
+
cur = [i] + [0] * lb
|
|
32
|
+
row_min = i
|
|
33
|
+
ca = a[i - 1]
|
|
34
|
+
for j in range(1, lb + 1):
|
|
35
|
+
cost = 0 if ca == b[j - 1] else 1
|
|
36
|
+
cur[j] = min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost)
|
|
37
|
+
if cur[j] < row_min:
|
|
38
|
+
row_min = cur[j]
|
|
39
|
+
if row_min > cap:
|
|
40
|
+
return cap + 1
|
|
41
|
+
prev = cur
|
|
42
|
+
return prev[lb] if prev[lb] <= cap else cap + 1
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def confusable_names(name: str, candidates, max_distance: int = 1) -> List[str]:
|
|
46
|
+
"""Candidate names within ``1..max_distance`` edits of ``name``.
|
|
47
|
+
|
|
48
|
+
Case-insensitive; exact matches (distance 0) and duplicates are dropped.
|
|
49
|
+
Ordered by ascending distance, then name, so the closest look-alike is
|
|
50
|
+
first.
|
|
51
|
+
"""
|
|
52
|
+
target = name.lower()
|
|
53
|
+
hits = []
|
|
54
|
+
seen = set()
|
|
55
|
+
for cand in candidates:
|
|
56
|
+
low = cand.lower()
|
|
57
|
+
if low == target or low in seen:
|
|
58
|
+
continue
|
|
59
|
+
d = edit_distance(target, low, max_distance)
|
|
60
|
+
if 1 <= d <= max_distance:
|
|
61
|
+
hits.append((d, cand))
|
|
62
|
+
seen.add(low)
|
|
63
|
+
hits.sort(key=lambda t: (t[0], t[1]))
|
|
64
|
+
return [c for _d, c in hits]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_confusions(target: dict, entries, max_distance: int = 1) -> List[dict]:
|
|
68
|
+
"""Catalog entries that are confusable with ``target`` but from another tap.
|
|
69
|
+
|
|
70
|
+
Flags both the owner-confusion case (same name, *different* tap → distance
|
|
71
|
+
0) and the typosquat case (name within ``max_distance`` edits, different
|
|
72
|
+
tap). Entries from ``target``'s own tap are never flagged, and ``target``
|
|
73
|
+
itself is excluded by identity. Sorted by ``(distance, name, tap)``.
|
|
74
|
+
"""
|
|
75
|
+
tname = str(target.get("name", "")).lower()
|
|
76
|
+
ttap = target.get("tap")
|
|
77
|
+
scored = []
|
|
78
|
+
for e in entries:
|
|
79
|
+
if e is target or e.get("tap") == ttap:
|
|
80
|
+
continue
|
|
81
|
+
d = edit_distance(tname, str(e.get("name", "")).lower(), max_distance)
|
|
82
|
+
if d <= max_distance:
|
|
83
|
+
scored.append((d, e))
|
|
84
|
+
scored.sort(key=lambda t: (t[0], str(t[1].get("name")), str(t[1].get("tap"))))
|
|
85
|
+
return [e for _d, e in scored]
|
|
@@ -50,6 +50,7 @@ boost_cli/core/rag.py
|
|
|
50
50
|
boost_cli/core/registry.py
|
|
51
51
|
boost_cli/core/staleness.py
|
|
52
52
|
boost_cli/core/store.py
|
|
53
|
+
boost_cli/core/typosquat.py
|
|
53
54
|
boost_cli/core/util.py
|
|
54
55
|
boost_cli/data/registries.json
|
|
55
56
|
boost_skill_cli.egg-info/PKG-INFO
|
|
@@ -222,6 +223,7 @@ tests/unit/test_mcp.py
|
|
|
222
223
|
tests/unit/test_mutation_hardening.py
|
|
223
224
|
tests/unit/test_output.py
|
|
224
225
|
tests/unit/test_paths.py
|
|
226
|
+
tests/unit/test_pkg_confusions.py
|
|
225
227
|
tests/unit/test_policy.py
|
|
226
228
|
tests/unit/test_rag.py
|
|
227
229
|
tests/unit/test_registries_fresh.py
|
|
@@ -231,5 +233,6 @@ tests/unit/test_spin.py
|
|
|
231
233
|
tests/unit/test_staleness.py
|
|
232
234
|
tests/unit/test_store.py
|
|
233
235
|
tests/unit/test_token_parity.py
|
|
236
|
+
tests/unit/test_typosquat.py
|
|
234
237
|
tests/unit/test_util.py
|
|
235
238
|
tests/unit/test_version.py
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
id: log-timestamps-mislabeled-utc
|
|
3
3
|
board: code
|
|
4
4
|
section: health
|
|
5
|
-
status:
|
|
5
|
+
status: shipped
|
|
6
6
|
category: Observability · Diagnostics
|
|
7
7
|
complexity: S
|
|
8
8
|
impact: Med
|
|
9
9
|
wow: 2
|
|
10
10
|
note: converter = time.gmtime
|
|
11
11
|
order: 10
|
|
12
|
-
owner:
|
|
13
|
-
pr:
|
|
12
|
+
owner: loop/utc-log-timestamps
|
|
13
|
+
pr: 104
|
|
14
14
|
title: Log timestamps are local time mislabeled <code>Z</code>
|
|
15
15
|
---
|
|
16
16
|
<code>core/logs.py</code> builds the <code>logging.Formatter</code> with
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
id: typosquat-and-name-confusion-detection
|
|
3
3
|
board: code
|
|
4
4
|
section: trust
|
|
5
|
-
status:
|
|
5
|
+
status: shipped
|
|
6
6
|
category: Security · Registry
|
|
7
7
|
complexity: M
|
|
8
8
|
impact: Med
|
|
9
9
|
wow: 4
|
|
10
10
|
note: edit-distance guard
|
|
11
11
|
order: 4
|
|
12
|
-
owner:
|
|
13
|
-
pr:
|
|
12
|
+
owner: loop/typosquat-detect
|
|
13
|
+
pr: 107
|
|
14
14
|
title: Typosquat & name-confusion detection
|
|
15
15
|
---
|
|
16
16
|
The classic package-manager attack: a skill named one edit-distance from
|
|
@@ -836,7 +836,7 @@
|
|
|
836
836
|
</article>
|
|
837
837
|
|
|
838
838
|
<article class="cap rcard">
|
|
839
|
-
<div class="head"><span class="pill
|
|
839
|
+
<div class="head"><span class="pill shipped">Shipped</span><span class="cat">Observability · Diagnostics</span></div>
|
|
840
840
|
<h3>Log timestamps are local time mislabeled <code>Z</code></h3>
|
|
841
841
|
<p><code>core/logs.py</code> builds the <code>logging.Formatter</code> with
|
|
842
842
|
<code>datefmt="%Y-%m-%dT%H:%M:%SZ"</code> but never sets
|
|
@@ -1560,7 +1560,7 @@ and the work stays visible.</p>
|
|
|
1560
1560
|
</article>
|
|
1561
1561
|
|
|
1562
1562
|
<article class="cap rcard">
|
|
1563
|
-
<div class="head"><span class="pill
|
|
1563
|
+
<div class="head"><span class="pill shipped">Shipped</span><span class="cat">Security · Registry</span></div>
|
|
1564
1564
|
<h3>Typosquat & name-confusion detection</h3>
|
|
1565
1565
|
<p>The classic package-manager attack: a skill named one edit-distance from
|
|
1566
1566
|
a popular one, or a familiar name that quietly resolves to an unexpected
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import logging
|
|
5
|
+
import logging.handlers
|
|
5
6
|
import pathlib
|
|
7
|
+
import time
|
|
6
8
|
|
|
7
9
|
import pytest
|
|
8
10
|
|
|
@@ -342,3 +344,37 @@ def test_doctor_reports_log_location(boost):
|
|
|
342
344
|
boost("count")
|
|
343
345
|
res = boost("doctor", expect=None)
|
|
344
346
|
assert "diagnostic log at" in res.out
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _file_formatter():
|
|
350
|
+
"""The formatter attached to the configured file handler."""
|
|
351
|
+
for h in logs.get_logger().handlers:
|
|
352
|
+
if isinstance(h, logging.handlers.RotatingFileHandler):
|
|
353
|
+
return h.formatter
|
|
354
|
+
raise AssertionError("no file handler configured")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class TestUtcTimestamps:
|
|
358
|
+
def test_formatter_uses_gmtime_converter(self, env):
|
|
359
|
+
# datefmt stamps a literal ``Z`` (UTC) suffix, so the converter MUST be
|
|
360
|
+
# gmtime; the default (localtime) would mislabel every line.
|
|
361
|
+
logs.configure()
|
|
362
|
+
assert _file_formatter().converter is time.gmtime
|
|
363
|
+
|
|
364
|
+
def test_asctime_is_utc_regardless_of_local_zone(self, env, monkeypatch):
|
|
365
|
+
# Force a non-UTC local zone; the rendered stamp must still be UTC.
|
|
366
|
+
if not hasattr(time, "tzset"):
|
|
367
|
+
pytest.skip("time.tzset unavailable on this platform")
|
|
368
|
+
monkeypatch.setenv("TZ", "America/New_York") # UTC-5/-4, never UTC
|
|
369
|
+
time.tzset()
|
|
370
|
+
try:
|
|
371
|
+
logs.configure()
|
|
372
|
+
fmt = _file_formatter()
|
|
373
|
+
record = logging.LogRecord(
|
|
374
|
+
"boost", logging.INFO, __file__, 1, "msg", None, None
|
|
375
|
+
)
|
|
376
|
+
record.created = 0.0 # 1970-01-01T00:00:00Z
|
|
377
|
+
assert fmt.formatTime(record, fmt.datefmt) == "1970-01-01T00:00:00Z"
|
|
378
|
+
finally:
|
|
379
|
+
monkeypatch.delenv("TZ", raising=False)
|
|
380
|
+
time.tzset()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Unit tests: pkg._warn_confusions — the install-time typosquat guard wiring."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from boost_cli.commands import pkg
|
|
5
|
+
from boost_cli.core import catalog
|
|
6
|
+
from boost_cli.core import output as out
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _capture(monkeypatch, entries):
|
|
10
|
+
monkeypatch.setattr(catalog, "all_entries", lambda: entries)
|
|
11
|
+
warnings = []
|
|
12
|
+
monkeypatch.setattr(out, "warn", lambda m: warnings.append(m))
|
|
13
|
+
return warnings
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_warns_on_cross_tap_lookalike(monkeypatch):
|
|
17
|
+
entries = [{"name": "deploy", "tap": "alice/skills"},
|
|
18
|
+
{"name": "deployy", "tap": "mallory/evil"}]
|
|
19
|
+
warnings = _capture(monkeypatch, entries)
|
|
20
|
+
pkg._warn_confusions([entries[0]])
|
|
21
|
+
assert len(warnings) == 1
|
|
22
|
+
assert "mallory/evil" in warnings[0] and "deploy" in warnings[0]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_quiet_when_no_confusion(monkeypatch):
|
|
26
|
+
entries = [{"name": "deploy", "tap": "alice/skills"},
|
|
27
|
+
{"name": "totally-unrelated", "tap": "bob/x"}]
|
|
28
|
+
warnings = _capture(monkeypatch, entries)
|
|
29
|
+
pkg._warn_confusions([entries[0]])
|
|
30
|
+
assert warnings == []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_same_tap_sibling_not_warned(monkeypatch):
|
|
34
|
+
entries = [{"name": "deploy", "tap": "alice/skills"},
|
|
35
|
+
{"name": "deployy", "tap": "alice/skills"}]
|
|
36
|
+
warnings = _capture(monkeypatch, entries)
|
|
37
|
+
pkg._warn_confusions([entries[0]])
|
|
38
|
+
assert warnings == []
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_empty_entries_skips_catalog_lookup(monkeypatch):
|
|
42
|
+
def boom():
|
|
43
|
+
raise AssertionError("catalog must not be queried for an empty install")
|
|
44
|
+
monkeypatch.setattr(catalog, "all_entries", boom)
|
|
45
|
+
pkg._warn_confusions([]) # returns before touching the catalog
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Unit tests: boost_cli/core/typosquat.py — edit distance & name confusion."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from boost_cli.core import typosquat
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestEditDistance:
|
|
8
|
+
def test_identical_is_zero(self):
|
|
9
|
+
assert typosquat.edit_distance("brainstorm", "brainstorm") == 0
|
|
10
|
+
|
|
11
|
+
def test_single_substitution(self):
|
|
12
|
+
assert typosquat.edit_distance("cat", "cot") == 1
|
|
13
|
+
|
|
14
|
+
def test_single_insertion(self):
|
|
15
|
+
assert typosquat.edit_distance("cat", "cart") == 1
|
|
16
|
+
|
|
17
|
+
def test_single_deletion(self):
|
|
18
|
+
assert typosquat.edit_distance("cart", "cat") == 1
|
|
19
|
+
|
|
20
|
+
def test_transposition_costs_two(self):
|
|
21
|
+
# Levenshtein (no adjacent-swap rule) scores a swap as two edits
|
|
22
|
+
assert typosquat.edit_distance("ab", "ba") == 2
|
|
23
|
+
|
|
24
|
+
def test_symmetric(self):
|
|
25
|
+
assert (typosquat.edit_distance("kitten", "sitting", cap=5)
|
|
26
|
+
== typosquat.edit_distance("sitting", "kitten", cap=5))
|
|
27
|
+
|
|
28
|
+
def test_classic_kitten_sitting_is_three(self):
|
|
29
|
+
assert typosquat.edit_distance("kitten", "sitting", cap=9) == 3
|
|
30
|
+
|
|
31
|
+
def test_empty_strings(self):
|
|
32
|
+
assert typosquat.edit_distance("", "") == 0
|
|
33
|
+
assert typosquat.edit_distance("", "abc", cap=5) == 3
|
|
34
|
+
|
|
35
|
+
def test_cap_returns_sentinel_when_farther(self):
|
|
36
|
+
# true distance is 3; with cap=1 we only learn "> cap" as cap + 1
|
|
37
|
+
assert typosquat.edit_distance("abcde", "xyzde", cap=1) == 2
|
|
38
|
+
|
|
39
|
+
def test_length_gap_shortcut(self):
|
|
40
|
+
# length difference alone exceeds the cap -> sentinel, no DP needed
|
|
41
|
+
assert typosquat.edit_distance("a", "aaaaaa", cap=2) == 3
|
|
42
|
+
|
|
43
|
+
def test_true_distance_returned_within_cap(self):
|
|
44
|
+
assert typosquat.edit_distance("foo", "foobar", cap=5) == 3
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestConfusableNames:
|
|
48
|
+
NAMES = ["brainstorm", "brainstorms", "brainstom", "commit", "unrelated"]
|
|
49
|
+
|
|
50
|
+
def test_finds_distance_one_only_by_default(self):
|
|
51
|
+
got = typosquat.confusable_names("brainstorm", self.NAMES)
|
|
52
|
+
assert got == ["brainstom", "brainstorms"] # both 1 edit away
|
|
53
|
+
|
|
54
|
+
def test_exact_match_excluded(self):
|
|
55
|
+
assert "brainstorm" not in typosquat.confusable_names(
|
|
56
|
+
"brainstorm", self.NAMES)
|
|
57
|
+
|
|
58
|
+
def test_distance_two_needs_higher_max(self):
|
|
59
|
+
# "brainstrom" is 2 edits from "brainstorm" (swap) -> hidden at max 1
|
|
60
|
+
cands = ["brainstrom", "brainstorm"]
|
|
61
|
+
assert typosquat.confusable_names("brainstorm", cands) == []
|
|
62
|
+
assert typosquat.confusable_names(
|
|
63
|
+
"brainstorm", cands, max_distance=2) == ["brainstrom"]
|
|
64
|
+
|
|
65
|
+
def test_case_insensitive(self):
|
|
66
|
+
assert typosquat.confusable_names("Commit", ["commiy"]) == ["commiy"]
|
|
67
|
+
|
|
68
|
+
def test_deduplicates_case_variants(self):
|
|
69
|
+
assert typosquat.confusable_names("cat", ["cot", "COT"]) == ["cot"]
|
|
70
|
+
|
|
71
|
+
def test_sorted_by_distance_then_name(self):
|
|
72
|
+
# "care" is 2 from "cat"; "bat"/"cot" are 1 -> closest first, then a-z
|
|
73
|
+
got = typosquat.confusable_names("cat", ["cot", "bat", "care"],
|
|
74
|
+
max_distance=2)
|
|
75
|
+
assert got == ["bat", "cot", "care"]
|
|
76
|
+
|
|
77
|
+
def test_empty_candidates(self):
|
|
78
|
+
assert typosquat.confusable_names("cat", []) == []
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _e(name, tap):
|
|
82
|
+
return {"name": name, "tap": tap}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TestFindConfusions:
|
|
86
|
+
def test_same_name_different_tap_flagged(self):
|
|
87
|
+
target = _e("deploy", "alice/skills")
|
|
88
|
+
entries = [target, _e("deploy", "mallory/evil")]
|
|
89
|
+
got = typosquat.find_confusions(target, entries)
|
|
90
|
+
assert [x["tap"] for x in got] == ["mallory/evil"]
|
|
91
|
+
|
|
92
|
+
def test_near_name_different_tap_flagged(self):
|
|
93
|
+
target = _e("deploy", "alice/skills")
|
|
94
|
+
got = typosquat.find_confusions(
|
|
95
|
+
target, [target, _e("deployy", "mallory/evil")])
|
|
96
|
+
assert [x["name"] for x in got] == ["deployy"]
|
|
97
|
+
|
|
98
|
+
def test_same_tap_never_flagged(self):
|
|
99
|
+
target = _e("deploy", "alice/skills")
|
|
100
|
+
# a sibling one edit away in the SAME tap is not a confusion
|
|
101
|
+
got = typosquat.find_confusions(
|
|
102
|
+
target, [target, _e("deployy", "alice/skills")])
|
|
103
|
+
assert got == []
|
|
104
|
+
|
|
105
|
+
def test_target_identity_excluded(self):
|
|
106
|
+
target = _e("deploy", "alice/skills")
|
|
107
|
+
assert typosquat.find_confusions(target, [target]) == []
|
|
108
|
+
|
|
109
|
+
def test_far_name_not_flagged(self):
|
|
110
|
+
target = _e("deploy", "alice/skills")
|
|
111
|
+
got = typosquat.find_confusions(
|
|
112
|
+
target, [target, _e("something-else", "mallory/evil")])
|
|
113
|
+
assert got == []
|
|
114
|
+
|
|
115
|
+
def test_respects_max_distance(self):
|
|
116
|
+
target = _e("deploy", "alice/skills")
|
|
117
|
+
entries = [target, _e("deproy", "x/y")] # 1 edit
|
|
118
|
+
assert len(typosquat.find_confusions(target, entries)) == 1
|
|
119
|
+
assert typosquat.find_confusions(
|
|
120
|
+
target, [target, _e("depployy", "x/y")]) == [] # 2 edits, max 1
|
|
121
|
+
|
|
122
|
+
def test_sorted_by_distance_then_name_then_tap(self):
|
|
123
|
+
target = _e("deploy", "alice/skills")
|
|
124
|
+
entries = [
|
|
125
|
+
target,
|
|
126
|
+
_e("deployy", "z/last"), # dist 1
|
|
127
|
+
_e("deploy", "a/first"), # dist 0 -> should come first
|
|
128
|
+
_e("deploy", "m/mid"), # dist 0
|
|
129
|
+
]
|
|
130
|
+
got = typosquat.find_confusions(target, entries)
|
|
131
|
+
assert [(x["name"], x["tap"]) for x in got] == [
|
|
132
|
+
("deploy", "a/first"), ("deploy", "m/mid"), ("deployy", "z/last")]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boost_skill_cli-1.0.92 → boost_skill_cli-1.0.93}/boost_skill_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|