capt-hook 12.76.0__tar.gz → 12.76.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.
- {capt_hook-12.76.0 → capt_hook-12.76.2}/PKG-INFO +1 -1
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/.claude-plugin/plugin.json +1 -1
- capt_hook-12.76.2/captain_hook/bindings.py +657 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/_sessions.py +130 -4
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/commands.py +34 -22
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/deletions.py +44 -4
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/questions.py +8 -1
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/sessions.py +271 -29
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tasks.py +9 -4
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/hooks/filesystem.py +50 -2
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/steering.py +12 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/cmd.py +170 -8
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/command_schema.py +29 -3
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/command_schemas.py +1 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/pyproject.toml +1 -1
- {capt_hook-12.76.0 → capt_hook-12.76.2}/.gitignore +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/LICENSE +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/README.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/capt_hook_client/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/capt_hook_client/__main__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/capt_hook_client/client.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/.mcp.json +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/__init__.pyi +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/__main__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/app.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/ast_grep.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/_lib.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/scratch_writes.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/teammate_permissions.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/_lib.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/comments.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/detours.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/docs.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/hook_files.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/models.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/plain_english.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/plans.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/fragments/deliverable_rubric.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/fragments/workflow_script_header.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/browser_delegation_nudge.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/implementation_spawn_nudge.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/inline_edit_nudge.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_spawn_gate.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_workflow_nudge.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_spawn_nudge.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_workflow_nudge.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_spawn_nudge.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_workflow_nudge.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/plain_english_rules.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/tools/preload_nudge.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/review.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tombstones.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tooling.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tools.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/pack.toml +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/hooks/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/hooks/testing.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/hooks/toolchain.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/gitdir/.graphite_repo_config +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/_lib.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/queue.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/vcs.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/hooks/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/hooks/pipelining.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/style.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/testing.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/toolchain.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/teammates.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/workarounds.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/pack.toml +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/conductor.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/droid.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/lane.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/classifiers/native.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/cli.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/conditions.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/context.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/contexts.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/context.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/decision_writer.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/logsink.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/registry.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/daemon/tree.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/decisions.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/desktop/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/desktop/cli.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/desktop/client.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/dispatch.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/doc_conventions.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/durable.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/events.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/faults.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/fields.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/file.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/guard_literal.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/guard_literal.sh +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/heartbeat.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/hook_lint.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/hooks/hooks.json +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/loader.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/log.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/mcp_server.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/packs/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/packs/manager.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/packs/plugins.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/commands.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/lint.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/llm.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/nudge.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/permissions.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/provision.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/rewrite.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/primitives/workflow.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/prompt.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/py.typed +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/announce.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/cli.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/dashboard.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/fix.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/formats.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/judge.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/notify.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/pipeline.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/prompts.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/repo.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/routing.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/scan.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/settings.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/snapshot.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/status.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/store.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/sync.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/review/triage.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/scripts/install-binary.sh +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/scripts/install-mcp.sh +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/session.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/settings.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/signals/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/signals/nlp.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/client.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/contracts.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/review.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schema.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/config.schema.json +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/context.schema.json +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/host-request.schema.json +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/host-response.schema.json +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/request.schema.json +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/response.schema.json +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/tool_registry.schema.json +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/validation.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/snapshots/worker.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/state.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/ast_grep.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/matchers.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/scope.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/style/types.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/tasks.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/templates/example_hook.py.tmpl +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/fixtures.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/helpers.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/session_cache.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/snapshots.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/testing/types.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/transcripts.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/turn.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/types.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/update/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/update/cli.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/update/settings.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/update/updater.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/automode.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/caching.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/fs.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/globbing.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/http.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/model_cache.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/paths.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/payload.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/proc.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/reqenv.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/scratch.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/shell.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/text.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/userpath.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/util/vcs.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/__init__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/__main__.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/fail_open.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/protocol.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/runtime.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/captain_hook/worker/service.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/hatch_build.py +0 -0
- {capt_hook-12.76.0 → capt_hook-12.76.2}/internal/wireproto/guard.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "captain-hook",
|
|
3
|
-
"version": "12.
|
|
3
|
+
"version": "12.76.1",
|
|
4
4
|
"displayName": "Captain Hook",
|
|
5
5
|
"description": "Skills for the capt-hook declarative hook framework: bootstrap gates and nudges from a repo's conventions, and translate style guides into enforced StyleRules.",
|
|
6
6
|
"author": {
|
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import glob
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from itertools import product
|
|
7
|
+
from math import prod
|
|
8
|
+
from pathlib import PurePath
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Iterator, Mapping
|
|
13
|
+
|
|
14
|
+
NAME = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
15
|
+
ASSIGNMENT = re.compile(r"([A-Za-z_][A-Za-z0-9_]*)(\[[^\]]*\])?(\+?)=")
|
|
16
|
+
MUTATION = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*):?=|\(\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:=|\+\+|--|[-+*/%]=)")
|
|
17
|
+
SPECIAL_PARAMETER = re.compile(r"[0-9@*#?$!_-]")
|
|
18
|
+
WORD_BREAK = frozenset(" \t\n;&|()<>")
|
|
19
|
+
OPERATORS = (
|
|
20
|
+
"<<<",
|
|
21
|
+
"<<-",
|
|
22
|
+
"<<",
|
|
23
|
+
";;",
|
|
24
|
+
"&&",
|
|
25
|
+
"||",
|
|
26
|
+
"|&",
|
|
27
|
+
">>",
|
|
28
|
+
"&>",
|
|
29
|
+
"<(",
|
|
30
|
+
">(",
|
|
31
|
+
"((",
|
|
32
|
+
"))",
|
|
33
|
+
";",
|
|
34
|
+
"&",
|
|
35
|
+
"|",
|
|
36
|
+
"(",
|
|
37
|
+
")",
|
|
38
|
+
"<",
|
|
39
|
+
">",
|
|
40
|
+
)
|
|
41
|
+
HEREDOC_OPERATORS = frozenset({"<<", "<<-"})
|
|
42
|
+
REDIRECT_OPERATORS = frozenset({"<<<", "<<-", "<<", ">>", "&>", "<", ">"})
|
|
43
|
+
GROUP_OPENERS = frozenset({"(", "<(", ">(", "((", "{"})
|
|
44
|
+
GROUP_CLOSERS = frozenset({")", "))", "}"})
|
|
45
|
+
SUBSHELL_OPERATORS = frozenset({"|", "|&", "&"})
|
|
46
|
+
STATEMENT_OPERATORS = frozenset({None, ";", ";;", "\n", "&"})
|
|
47
|
+
BODY_OPENERS = frozenset({"then", "else", "do"})
|
|
48
|
+
BODY_CLOSERS = frozenset({"fi", "done"})
|
|
49
|
+
CONDITION_OPENERS = frozenset({"if", "while", "until", "elif"})
|
|
50
|
+
PREFIXES = frozenset({"!", "time", "builtin", "command"})
|
|
51
|
+
DECLARERS = frozenset({"local", "declare", "typeset", "readonly", "let"})
|
|
52
|
+
READERS = frozenset({"read", "mapfile", "readarray", "getopts"})
|
|
53
|
+
UNREADABLE_BINDERS = frozenset({"eval", "source", ".", "case", "select", "coproc"})
|
|
54
|
+
MUTATORS = READERS | DECLARERS | UNREADABLE_BINDERS | frozenset({"unset", "export", "printf"})
|
|
55
|
+
UNREADABLE_IN_SUBSTITUTION = ("#", "<<", "case")
|
|
56
|
+
SHELL_OWNED = frozenset({"PWD", "OLDPWD", "RANDOM", "SECONDS", "LINENO", "REPLY", "IFS", "BASH_COMMAND", "PIPESTATUS"})
|
|
57
|
+
ENVIRONMENT = {"HOME": "~"}
|
|
58
|
+
CANDIDATE_LIMIT = 16
|
|
59
|
+
CANDIDATE_LENGTH = 4096
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class Known:
|
|
64
|
+
"""A variable every assignment on the line set to a literal: its candidate values."""
|
|
65
|
+
|
|
66
|
+
candidates: tuple[str, ...]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True, slots=True)
|
|
70
|
+
class Unknown:
|
|
71
|
+
"""A variable the line builds at run time; ``source`` is the text feeding it, None when unreadable."""
|
|
72
|
+
|
|
73
|
+
source: str | None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
type Binding = Known | Unknown
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class Resolved:
|
|
81
|
+
"""A word whose expansions all resolve, one candidate text per combination of values.
|
|
82
|
+
|
|
83
|
+
``splittable`` is True when an unquoted expansion contributed, so the shell may split a
|
|
84
|
+
candidate into several words.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
candidates: tuple[str, ...]
|
|
88
|
+
splittable: bool
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True, slots=True)
|
|
92
|
+
class Unresolved:
|
|
93
|
+
"""A word the guard cannot resolve; ``source`` is the text its value is built from, None when unreadable."""
|
|
94
|
+
|
|
95
|
+
source: str | None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
type Resolution = Resolved | Unresolved
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass(frozen=True, slots=True)
|
|
102
|
+
class Ref:
|
|
103
|
+
name: str
|
|
104
|
+
quoted: bool
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class Literal:
|
|
109
|
+
text: str
|
|
110
|
+
quoted: bool
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass(frozen=True, slots=True)
|
|
114
|
+
class Token:
|
|
115
|
+
raw: str
|
|
116
|
+
start: int
|
|
117
|
+
end: int
|
|
118
|
+
operator: bool
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True, slots=True)
|
|
122
|
+
class Event:
|
|
123
|
+
offset: int
|
|
124
|
+
name: str
|
|
125
|
+
binding: Binding
|
|
126
|
+
until: int | None = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def source_of(binding: Binding) -> str | None:
|
|
130
|
+
match binding:
|
|
131
|
+
case Known(candidates):
|
|
132
|
+
return " ".join(candidates)
|
|
133
|
+
case Unknown(source):
|
|
134
|
+
return source
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def joined(first: Binding, second: Binding) -> Binding:
|
|
138
|
+
match first, second:
|
|
139
|
+
case Known(), Known():
|
|
140
|
+
return second
|
|
141
|
+
case _:
|
|
142
|
+
sources = (source_of(first), source_of(second))
|
|
143
|
+
return Unknown(None if None in sources else " ".join(part for part in sources if part))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def skip_quoted(text: str, index: int, quote: str) -> int:
|
|
147
|
+
end = text.find(quote, index + 1)
|
|
148
|
+
return len(text) if end < 0 else end + 1
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def skip_group(text: str, index: int, opener: str, closer: str) -> int:
|
|
152
|
+
depth = 0
|
|
153
|
+
while index < len(text):
|
|
154
|
+
match text[index]:
|
|
155
|
+
case "\\":
|
|
156
|
+
index += 2
|
|
157
|
+
continue
|
|
158
|
+
case "'" | "`" as quote:
|
|
159
|
+
index = skip_quoted(text, index, quote)
|
|
160
|
+
continue
|
|
161
|
+
case '"':
|
|
162
|
+
index = skip_double(text, index)
|
|
163
|
+
continue
|
|
164
|
+
case char if char == opener:
|
|
165
|
+
depth += 1
|
|
166
|
+
case char if char == closer:
|
|
167
|
+
depth -= 1
|
|
168
|
+
if depth == 0:
|
|
169
|
+
return index + 1
|
|
170
|
+
index += 1
|
|
171
|
+
return index
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def skip_double(text: str, index: int) -> int:
|
|
175
|
+
index += 1
|
|
176
|
+
while index < len(text):
|
|
177
|
+
match text[index]:
|
|
178
|
+
case "\\":
|
|
179
|
+
index += 2
|
|
180
|
+
continue
|
|
181
|
+
case '"':
|
|
182
|
+
return index + 1
|
|
183
|
+
case "`":
|
|
184
|
+
index = skip_quoted(text, index, "`")
|
|
185
|
+
continue
|
|
186
|
+
case "$" if text.startswith("$(", index):
|
|
187
|
+
index = skip_group(text, index + 1, "(", ")")
|
|
188
|
+
continue
|
|
189
|
+
case "$" if text.startswith("${", index):
|
|
190
|
+
index = skip_group(text, index + 1, "{", "}")
|
|
191
|
+
continue
|
|
192
|
+
index += 1
|
|
193
|
+
return index
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def read_word(text: str, index: int) -> int:
|
|
197
|
+
while index < len(text):
|
|
198
|
+
match text[index]:
|
|
199
|
+
case "\\":
|
|
200
|
+
index += 2
|
|
201
|
+
case "'" | "`" as quote:
|
|
202
|
+
index = skip_quoted(text, index, quote)
|
|
203
|
+
case '"':
|
|
204
|
+
index = skip_double(text, index)
|
|
205
|
+
case "$" if text.startswith("$(", index):
|
|
206
|
+
index = skip_group(text, index + 1, "(", ")")
|
|
207
|
+
case "$" if text.startswith("${", index):
|
|
208
|
+
index = skip_group(text, index + 1, "{", "}")
|
|
209
|
+
case char if char in WORD_BREAK:
|
|
210
|
+
return index
|
|
211
|
+
case _:
|
|
212
|
+
index += 1
|
|
213
|
+
return index
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def dequote(raw: str) -> str:
|
|
217
|
+
return "".join(
|
|
218
|
+
part.text for part in segments(raw.replace("\\\n", "")) or [Literal(raw, False)] if isinstance(part, Literal)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def segments(raw: str) -> list[Literal | Ref] | None:
|
|
223
|
+
parts: list[Literal | Ref] = []
|
|
224
|
+
index = 0
|
|
225
|
+
quoted = False
|
|
226
|
+
raw = raw.replace("\\\n", "")
|
|
227
|
+
while index < len(raw):
|
|
228
|
+
char = raw[index]
|
|
229
|
+
if char == "\\" and index + 1 < len(raw) and (not quoted or raw[index + 1] in '\\"$`'):
|
|
230
|
+
parts.append(Literal(raw[index + 1], quoted))
|
|
231
|
+
index += 2
|
|
232
|
+
elif char == "'" and not quoted:
|
|
233
|
+
end = skip_quoted(raw, index, "'")
|
|
234
|
+
parts.append(Literal(raw[index + 1 : end - 1], True))
|
|
235
|
+
index = end
|
|
236
|
+
elif char == '"':
|
|
237
|
+
quoted = not quoted
|
|
238
|
+
index += 1
|
|
239
|
+
elif char == "`" or raw.startswith("$(", index):
|
|
240
|
+
return None
|
|
241
|
+
elif char == "$":
|
|
242
|
+
if (name := NAME.match(raw, index + 1)) is not None:
|
|
243
|
+
parts.append(Ref(name.group(), quoted))
|
|
244
|
+
index = name.end()
|
|
245
|
+
elif (special := SPECIAL_PARAMETER.match(raw, index + 1)) is not None:
|
|
246
|
+
parts.append(Ref(special.group(), quoted))
|
|
247
|
+
index = special.end()
|
|
248
|
+
elif (
|
|
249
|
+
raw.startswith("${", index)
|
|
250
|
+
and (name := NAME.match(raw, index + 2)) is not None
|
|
251
|
+
and raw.startswith("}", name.end())
|
|
252
|
+
):
|
|
253
|
+
parts.append(Ref(name.group(), quoted))
|
|
254
|
+
index = name.end() + 1
|
|
255
|
+
else:
|
|
256
|
+
return None
|
|
257
|
+
else:
|
|
258
|
+
parts.append(Literal(char, quoted))
|
|
259
|
+
index += 1
|
|
260
|
+
return parts
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def skip_heredoc(text: str, index: int, terminator: str, *, strip_tabs: bool) -> int:
|
|
264
|
+
while index < len(text):
|
|
265
|
+
line_end = text.find("\n", index)
|
|
266
|
+
line_end = len(text) if line_end < 0 else line_end
|
|
267
|
+
line = text[index:line_end]
|
|
268
|
+
index = line_end + 1
|
|
269
|
+
if (line.lstrip("\t") if strip_tabs else line) == terminator:
|
|
270
|
+
return index
|
|
271
|
+
return index
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def tokens(text: str) -> Iterator[Token]:
|
|
275
|
+
index = 0
|
|
276
|
+
heredocs: list[tuple[str, bool]] = []
|
|
277
|
+
word_start = True
|
|
278
|
+
while index < len(text):
|
|
279
|
+
char = text[index]
|
|
280
|
+
if char == "\n":
|
|
281
|
+
yield Token("\n", index, index + 1, True)
|
|
282
|
+
index += 1
|
|
283
|
+
while heredocs:
|
|
284
|
+
terminator, strip_tabs = heredocs.pop(0)
|
|
285
|
+
index = skip_heredoc(text, index, terminator, strip_tabs=strip_tabs)
|
|
286
|
+
word_start = True
|
|
287
|
+
elif char in " \t":
|
|
288
|
+
index += 1
|
|
289
|
+
word_start = True
|
|
290
|
+
elif text.startswith("\\\n", index):
|
|
291
|
+
index += 2
|
|
292
|
+
elif char == "#" and word_start:
|
|
293
|
+
line_end = text.find("\n", index)
|
|
294
|
+
index = len(text) if line_end < 0 else line_end
|
|
295
|
+
elif (operator := next((op for op in OPERATORS if text.startswith(op, index)), None)) is not None:
|
|
296
|
+
end = index + len(operator)
|
|
297
|
+
if operator in HEREDOC_OPERATORS:
|
|
298
|
+
while end < len(text) and text[end] in " \t":
|
|
299
|
+
end += 1
|
|
300
|
+
word_end = read_word(text, end)
|
|
301
|
+
heredocs.append((dequote(text[end:word_end]), operator == "<<-"))
|
|
302
|
+
end = word_end
|
|
303
|
+
yield Token(operator, index, end, True)
|
|
304
|
+
index = end
|
|
305
|
+
word_start = True
|
|
306
|
+
else:
|
|
307
|
+
end = read_word(text, index)
|
|
308
|
+
if end == index:
|
|
309
|
+
index += 1
|
|
310
|
+
continue
|
|
311
|
+
yield Token(text[index:end], index, end, False)
|
|
312
|
+
index = end
|
|
313
|
+
word_start = False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def unreadable_substitution(value: str) -> bool:
|
|
317
|
+
return "$(" in value and any(marker in value for marker in UNREADABLE_IN_SUBSTITUTION)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@dataclass
|
|
321
|
+
class Scope:
|
|
322
|
+
"""Same-line variable bindings of one shell text, plus the function bodies it defines.
|
|
323
|
+
|
|
324
|
+
Walks the text once, recording every assignment, ``export``, ``for`` head, and reader as
|
|
325
|
+
an event at its offset. A binding is :class:`Known` only when every assignment to the
|
|
326
|
+
name was a literal in an unconditional top-level statement; anything conditional, built
|
|
327
|
+
at run time, read from input, or owned by the shell leaves it :class:`Unknown`.
|
|
328
|
+
``parent`` holds the bindings in force when a nested payload began. A word written inside
|
|
329
|
+
a function body never resolves, since the body runs later than it reads.
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
text: str
|
|
333
|
+
parent: Mapping[str, Binding] = field(default_factory=dict)
|
|
334
|
+
exported: set[str] = field(default_factory=set)
|
|
335
|
+
events: list[Event] = field(default_factory=list, init=False)
|
|
336
|
+
functions: list[tuple[int, int]] = field(default_factory=list, init=False)
|
|
337
|
+
unreadable_from: int | None = field(default=None, init=False)
|
|
338
|
+
mentioned: frozenset[str] = field(default=frozenset(), init=False)
|
|
339
|
+
mutated: frozenset[str] = field(default=frozenset(), init=False)
|
|
340
|
+
|
|
341
|
+
def __post_init__(self) -> None:
|
|
342
|
+
self.mentioned = frozenset(match.group(1) for match in ASSIGNMENT.finditer(self.text))
|
|
343
|
+
self.mutated = frozenset(name for match in MUTATION.finditer(self.text) for name in match.groups() if name)
|
|
344
|
+
if "IFS" in self.mentioned or "IFS" in self.mutated:
|
|
345
|
+
self.unreadable_from = 0
|
|
346
|
+
Walk(self).run()
|
|
347
|
+
|
|
348
|
+
@classmethod
|
|
349
|
+
def unreadable(cls, text: str) -> Scope:
|
|
350
|
+
scope = cls.__new__(cls)
|
|
351
|
+
scope.text, scope.parent, scope.exported = text, {}, set()
|
|
352
|
+
scope.events, scope.functions, scope.unreadable_from = [], [], 0
|
|
353
|
+
scope.mentioned, scope.mutated = frozenset(), frozenset()
|
|
354
|
+
return scope
|
|
355
|
+
|
|
356
|
+
def mark_unreadable(self, offset: int) -> None:
|
|
357
|
+
self.unreadable_from = offset if self.unreadable_from is None else min(self.unreadable_from, offset)
|
|
358
|
+
|
|
359
|
+
def child(
|
|
360
|
+
self, text: str, offset: int, *, exported_only: bool, overlay: Mapping[str, Binding] | None = None
|
|
361
|
+
) -> Scope:
|
|
362
|
+
"""The scope of a payload written at ``offset``, seeded with the bindings in force there."""
|
|
363
|
+
names = {event.name for event in self.events} | set(self.parent) | self.exported
|
|
364
|
+
inherited = {
|
|
365
|
+
name: binding
|
|
366
|
+
for name in names
|
|
367
|
+
if (binding := self.lookup(name, offset)) is not None and (not exported_only or name in self.exported)
|
|
368
|
+
}
|
|
369
|
+
return Scope(text, inherited | dict(overlay or {}), set(self.exported))
|
|
370
|
+
|
|
371
|
+
def lookup(self, name: str, offset: int) -> Binding | None:
|
|
372
|
+
unreadable = self.unreadable_from is not None and offset > self.unreadable_from
|
|
373
|
+
if unreadable or name in SHELL_OWNED or name in self.mutated:
|
|
374
|
+
return Unknown(None)
|
|
375
|
+
binding = self.parent.get(name)
|
|
376
|
+
for event in self.events:
|
|
377
|
+
if event.offset < offset and event.name == name and (event.until is None or offset < event.until):
|
|
378
|
+
binding = event.binding if binding is None else joined(binding, event.binding)
|
|
379
|
+
if binding is None and name in self.mentioned:
|
|
380
|
+
return Unknown(None)
|
|
381
|
+
return binding
|
|
382
|
+
|
|
383
|
+
def in_function(self, offset: int) -> bool:
|
|
384
|
+
return any(start <= offset < end for start, end in self.functions)
|
|
385
|
+
|
|
386
|
+
def resolve(self, raw: str, offset: int) -> Resolution:
|
|
387
|
+
"""Resolve one word written at ``offset`` through the bindings in force there."""
|
|
388
|
+
if self.in_function(offset):
|
|
389
|
+
return Unresolved(None)
|
|
390
|
+
parts = segments(raw)
|
|
391
|
+
if parts is None:
|
|
392
|
+
return Unresolved(raw)
|
|
393
|
+
refs = {part.name: part for part in parts if isinstance(part, Ref)}
|
|
394
|
+
if not refs:
|
|
395
|
+
return Resolved((dequote(raw),), False)
|
|
396
|
+
choices: dict[str, tuple[str, ...]] = {}
|
|
397
|
+
for name in refs:
|
|
398
|
+
match self.lookup(name, offset):
|
|
399
|
+
case Known(candidates):
|
|
400
|
+
choices[name] = candidates
|
|
401
|
+
case Unknown(source):
|
|
402
|
+
return Unresolved(source)
|
|
403
|
+
case None if name in ENVIRONMENT:
|
|
404
|
+
choices[name] = (ENVIRONMENT[name],)
|
|
405
|
+
case None if NAME.fullmatch(name) is not None:
|
|
406
|
+
return Unresolved("")
|
|
407
|
+
case None:
|
|
408
|
+
return Unresolved(None)
|
|
409
|
+
if prod(map(len, choices.values())) > CANDIDATE_LIMIT:
|
|
410
|
+
return Unresolved(" ".join(" ".join(choice) for choice in choices.values()))
|
|
411
|
+
candidates = tuple(
|
|
412
|
+
"".join(values[part.name] if isinstance(part, Ref) else part.text for part in parts)
|
|
413
|
+
for combination in product(*choices.values())
|
|
414
|
+
for values in (dict(zip(choices, combination, strict=True)),)
|
|
415
|
+
)
|
|
416
|
+
if any(len(candidate) > CANDIDATE_LENGTH for candidate in candidates):
|
|
417
|
+
return Unresolved(None)
|
|
418
|
+
splittable = any(not part.quoted for part in parts if isinstance(part, Ref))
|
|
419
|
+
mixed = splittable and any(part.quoted for part in parts)
|
|
420
|
+
if mixed and any(candidate.split() != [candidate] for candidate in candidates):
|
|
421
|
+
return Unresolved(None)
|
|
422
|
+
return Resolved(candidates, splittable)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def head_of(words: list[Token]) -> tuple[str, list[Token]]:
|
|
426
|
+
while words and dequote(words[0].raw) in PREFIXES:
|
|
427
|
+
words = words[1:]
|
|
428
|
+
return (dequote(words[0].raw), words[1:]) if words else ("", [])
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def literal_name(word: Token) -> str | None:
|
|
432
|
+
parts = segments(word.raw)
|
|
433
|
+
if parts is None or not all(isinstance(part, Literal) for part in parts):
|
|
434
|
+
return None
|
|
435
|
+
match = NAME.search("".join(part.text for part in parts))
|
|
436
|
+
return None if match is None else match.group()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def program_name(candidate: str) -> str:
|
|
440
|
+
return PurePath(pieces[0] if (pieces := candidate.split()) else candidate).name
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
@dataclass
|
|
444
|
+
class Walk:
|
|
445
|
+
scope: Scope
|
|
446
|
+
stack: list[tuple[str, int, int | None]] = field(default_factory=list)
|
|
447
|
+
words: list[Token] = field(default_factory=list)
|
|
448
|
+
previous: str | None = None
|
|
449
|
+
function_name: Token | None = None
|
|
450
|
+
pending_function: bool = False
|
|
451
|
+
pending_loop: int | None = None
|
|
452
|
+
skip_word: bool = False
|
|
453
|
+
|
|
454
|
+
def run(self) -> None:
|
|
455
|
+
for token in tokens(self.scope.text):
|
|
456
|
+
if not token.operator and token.raw in {"{", "}"} and (not self.words or self.words[0].raw == "function"):
|
|
457
|
+
self.operator(token)
|
|
458
|
+
elif not token.operator:
|
|
459
|
+
if unreadable_substitution(token.raw):
|
|
460
|
+
self.scope.mark_unreadable(token.start)
|
|
461
|
+
if not self.skip_word:
|
|
462
|
+
self.words.append(token)
|
|
463
|
+
self.skip_word = False
|
|
464
|
+
else:
|
|
465
|
+
self.operator(token)
|
|
466
|
+
self.flush(sure=self.sure())
|
|
467
|
+
|
|
468
|
+
def sure(self) -> bool:
|
|
469
|
+
return not self.stack and self.previous in STATEMENT_OPERATORS
|
|
470
|
+
|
|
471
|
+
def operator(self, token: Token) -> None:
|
|
472
|
+
self.skip_word = False
|
|
473
|
+
match token.raw:
|
|
474
|
+
case "(" if len(self.words) == 1 and NAME.fullmatch(self.words[0].raw) is not None and not self.stack:
|
|
475
|
+
self.function_name = self.words[0]
|
|
476
|
+
self.words = []
|
|
477
|
+
return
|
|
478
|
+
case ")" if self.function_name is not None and not self.words:
|
|
479
|
+
self.function_name = None
|
|
480
|
+
self.pending_function = True
|
|
481
|
+
return
|
|
482
|
+
case "(" | "<(" if self.words and self.words[-1].raw.endswith("="):
|
|
483
|
+
self.bind_unreadable(self.words.pop())
|
|
484
|
+
case _ if token.raw in REDIRECT_OPERATORS:
|
|
485
|
+
self.skip_word = True
|
|
486
|
+
return
|
|
487
|
+
case _:
|
|
488
|
+
self.function_name = None
|
|
489
|
+
self.flush(sure=self.sure() and token.raw not in SUBSHELL_OPERATORS)
|
|
490
|
+
if token.raw in GROUP_OPENERS:
|
|
491
|
+
self.stack.append(("function" if self.pending_function else "group", token.start, None))
|
|
492
|
+
self.pending_function = False
|
|
493
|
+
elif token.raw in GROUP_CLOSERS and self.stack:
|
|
494
|
+
kind, start, _ = self.stack.pop()
|
|
495
|
+
if kind == "function" and token.start - start <= 2:
|
|
496
|
+
self.pending_function = True
|
|
497
|
+
elif kind == "function":
|
|
498
|
+
self.scope.functions.append((start, token.end))
|
|
499
|
+
self.previous = token.raw
|
|
500
|
+
|
|
501
|
+
def bind(self, event: Event) -> None:
|
|
502
|
+
if event.name == "IFS":
|
|
503
|
+
self.scope.mark_unreadable(event.offset)
|
|
504
|
+
self.scope.events.append(event)
|
|
505
|
+
|
|
506
|
+
def bind_unreadable(self, word: Token) -> None:
|
|
507
|
+
if (match := ASSIGNMENT.match(word.raw)) is not None:
|
|
508
|
+
self.bind(Event(word.start, match.group(1), Unknown(None)))
|
|
509
|
+
|
|
510
|
+
def computed_mutator(self, word: Token) -> bool:
|
|
511
|
+
parts = segments(word.raw)
|
|
512
|
+
if parts is not None and all(isinstance(part, Literal) for part in parts):
|
|
513
|
+
return False
|
|
514
|
+
match self.scope.resolve(word.raw, word.start):
|
|
515
|
+
case Resolved(candidates, _):
|
|
516
|
+
return any(program_name(candidate) in MUTATORS for candidate in candidates)
|
|
517
|
+
case Unresolved():
|
|
518
|
+
return True
|
|
519
|
+
|
|
520
|
+
def flush(self, *, sure: bool) -> None:
|
|
521
|
+
words, self.words = self.words, []
|
|
522
|
+
if not words:
|
|
523
|
+
return
|
|
524
|
+
head, rest = head_of(words)
|
|
525
|
+
head_word = next(
|
|
526
|
+
(
|
|
527
|
+
word
|
|
528
|
+
for word in words
|
|
529
|
+
if dequote(word.raw) not in PREFIXES
|
|
530
|
+
and ASSIGNMENT.match(word.raw) is None
|
|
531
|
+
and not word.raw.startswith("-")
|
|
532
|
+
),
|
|
533
|
+
None,
|
|
534
|
+
)
|
|
535
|
+
if head_word is not None and self.computed_mutator(head_word):
|
|
536
|
+
self.scope.mark_unreadable(head_word.start)
|
|
537
|
+
if head in BODY_OPENERS:
|
|
538
|
+
if head == "else" and self.stack and self.stack[-1][0] == "then":
|
|
539
|
+
self.stack.pop()
|
|
540
|
+
self.stack.append(("then" if head != "do" else "do", words[0].start, self.pending_loop))
|
|
541
|
+
self.pending_loop = None
|
|
542
|
+
self.words = rest
|
|
543
|
+
self.flush(sure=False)
|
|
544
|
+
elif head in BODY_CLOSERS:
|
|
545
|
+
if self.stack and self.stack[-1][0] == ("then" if head == "fi" else "do"):
|
|
546
|
+
_, _, loop = self.stack.pop()
|
|
547
|
+
if loop is not None:
|
|
548
|
+
self.close_loop(loop, words[0].end)
|
|
549
|
+
self.words = rest
|
|
550
|
+
self.flush(sure=False)
|
|
551
|
+
elif head in CONDITION_OPENERS:
|
|
552
|
+
if head == "elif" and self.stack and self.stack[-1][0] == "then":
|
|
553
|
+
self.stack.pop()
|
|
554
|
+
self.words = rest
|
|
555
|
+
self.flush(sure=False)
|
|
556
|
+
elif head == "for":
|
|
557
|
+
self.bind_loop(rest)
|
|
558
|
+
elif head in UNREADABLE_BINDERS:
|
|
559
|
+
self.scope.mark_unreadable(words[-1].end)
|
|
560
|
+
elif head == "function":
|
|
561
|
+
self.pending_function = True
|
|
562
|
+
elif head in DECLARERS and any(word.raw.startswith("-") and "n" in word.raw for word in rest):
|
|
563
|
+
self.scope.mark_unreadable(words[-1].end)
|
|
564
|
+
elif head in READERS or head == "unset" or head in DECLARERS:
|
|
565
|
+
self.bind_names(head, rest)
|
|
566
|
+
elif head == "printf" and any(word.raw.startswith("-v") for word in rest):
|
|
567
|
+
self.bind_names(head, printf_destination(rest))
|
|
568
|
+
elif head == "export":
|
|
569
|
+
self.bind_exports(rest, sure)
|
|
570
|
+
elif all(ASSIGNMENT.match(word.raw) is not None for word in words):
|
|
571
|
+
for word in words:
|
|
572
|
+
self.bind_assignment(word, sure=sure)
|
|
573
|
+
|
|
574
|
+
def bind_names(self, head: str, words: list[Token]) -> None:
|
|
575
|
+
names: list[str] = []
|
|
576
|
+
for word in words:
|
|
577
|
+
if word.raw.startswith("-"):
|
|
578
|
+
continue
|
|
579
|
+
if (name := literal_name(word)) is None:
|
|
580
|
+
self.scope.mark_unreadable(word.start)
|
|
581
|
+
return
|
|
582
|
+
names.append(name)
|
|
583
|
+
if head == "read" and not names:
|
|
584
|
+
names = ["REPLY"]
|
|
585
|
+
for name in names:
|
|
586
|
+
self.bind(Event(words[0].start if words else 0, name, Unknown(None)))
|
|
587
|
+
|
|
588
|
+
def bind_exports(self, words: list[Token], sure: bool) -> None:
|
|
589
|
+
options = any(word.raw.startswith("-") for word in words)
|
|
590
|
+
for word in words:
|
|
591
|
+
if word.raw.startswith("-"):
|
|
592
|
+
continue
|
|
593
|
+
if options:
|
|
594
|
+
self.bind_unreadable(Token(f"{dequote(word.raw)}=", word.start, word.end, False))
|
|
595
|
+
elif ASSIGNMENT.match(word.raw) is not None:
|
|
596
|
+
self.bind_assignment(word, sure=sure)
|
|
597
|
+
self.scope.exported.add(ASSIGNMENT.match(word.raw).group(1))
|
|
598
|
+
elif NAME.fullmatch(word.raw) is not None:
|
|
599
|
+
self.scope.exported.add(word.raw)
|
|
600
|
+
|
|
601
|
+
def bind_assignment(self, word: Token, *, sure: bool) -> None:
|
|
602
|
+
match = ASSIGNMENT.match(word.raw)
|
|
603
|
+
if match is None:
|
|
604
|
+
return
|
|
605
|
+
name, value = match.group(1), word.raw[match.end() :]
|
|
606
|
+
binding: Binding
|
|
607
|
+
match self.scope.resolve(value, word.start):
|
|
608
|
+
case _ if match.group(3):
|
|
609
|
+
binding = Unknown(None)
|
|
610
|
+
case Resolved(candidates, _) if sure and not match.group(2):
|
|
611
|
+
binding = Known(candidates)
|
|
612
|
+
case Resolved(candidates, _):
|
|
613
|
+
binding = Unknown(" ".join(candidates))
|
|
614
|
+
case Unresolved(source):
|
|
615
|
+
binding = Unknown(source)
|
|
616
|
+
self.bind(Event(word.start, name, binding))
|
|
617
|
+
|
|
618
|
+
def bind_loop(self, words: list[Token]) -> None:
|
|
619
|
+
match [word.raw for word in words]:
|
|
620
|
+
case [name, "in", *_] if NAME.fullmatch(name) is not None:
|
|
621
|
+
candidates: list[str] = []
|
|
622
|
+
for item in words[2:]:
|
|
623
|
+
match self.scope.resolve(item.raw, item.start):
|
|
624
|
+
case Resolved(values, splittable) if not any(
|
|
625
|
+
"{" in value or glob.has_magic(value) for value in values
|
|
626
|
+
):
|
|
627
|
+
candidates.extend(
|
|
628
|
+
piece for value in values for piece in (value.split() if splittable else [value])
|
|
629
|
+
)
|
|
630
|
+
case _:
|
|
631
|
+
candidates = []
|
|
632
|
+
break
|
|
633
|
+
binding: Binding = (
|
|
634
|
+
Known(tuple(candidates))
|
|
635
|
+
if candidates
|
|
636
|
+
else Unknown(" ".join(word.raw for word in words[2:]) or None)
|
|
637
|
+
)
|
|
638
|
+
self.pending_loop = len(self.scope.events)
|
|
639
|
+
self.bind(Event(words[0].start, name, binding))
|
|
640
|
+
case [name, *_] if NAME.fullmatch(name) is not None:
|
|
641
|
+
self.bind(Event(words[0].start, name, Unknown(None)))
|
|
642
|
+
case _:
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
def close_loop(self, index: int, end: int) -> None:
|
|
646
|
+
event = self.scope.events[index]
|
|
647
|
+
self.scope.events[index] = Event(event.offset, event.name, event.binding, end)
|
|
648
|
+
self.bind(Event(end, event.name, Unknown(source_of(event.binding))))
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def printf_destination(words: list[Token]) -> list[Token]:
|
|
652
|
+
for index, word in enumerate(words):
|
|
653
|
+
if word.raw == "-v" and index + 1 < len(words):
|
|
654
|
+
return [words[index + 1]]
|
|
655
|
+
if word.raw.startswith("-v") and len(word.raw) > 2:
|
|
656
|
+
return [Token(word.raw[2:], word.start + 2, word.end, False)]
|
|
657
|
+
return []
|