@toolu/opencode 6.8.0
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.
- package/generated/GENERATED-NOTES.md +16 -0
- package/generated/agents/toolu--architect.md +52 -0
- package/generated/agents/toolu--deep-explore.md +65 -0
- package/generated/agents/toolu--implementer.md +50 -0
- package/generated/agents/toolu--quick-task.md +54 -0
- package/generated/agents/toolu--research-agent.md +101 -0
- package/generated/commands/toolu--commit.md +8 -0
- package/generated/commands/toolu--review-and-commit.md +8 -0
- package/generated/opencode.toolu.json +116 -0
- package/generated/skills/toolu--brainstorm/SKILL.md +57 -0
- package/generated/skills/toolu--brainstorm/references/design-questions.md +36 -0
- package/generated/skills/toolu--commit--skill/SKILL.md +11 -0
- package/generated/skills/toolu--debug/SKILL.md +54 -0
- package/generated/skills/toolu--deep-research/SKILL.md +51 -0
- package/generated/skills/toolu--execution/SKILL.md +77 -0
- package/generated/skills/toolu--orchestrator/SKILL.md +145 -0
- package/generated/skills/toolu--orchestrator/references/delegation-cost.md +104 -0
- package/generated/skills/toolu--orchestrator/references/model-routing.md +112 -0
- package/generated/skills/toolu--plan/SKILL.md +56 -0
- package/generated/skills/toolu--plan/references/ledger.md +18 -0
- package/generated/skills/toolu--plan-review/SKILL.md +48 -0
- package/generated/skills/toolu--review-and-commit--skill/SKILL.md +10 -0
- package/generated/skills/toolu--setup/SKILL.md +24 -0
- package/generated/skills/toolu--setup/scripts/setup.sh +206 -0
- package/generated/skills/toolu--spec/SKILL.md +67 -0
- package/generated/skills/toolu--spec-review/SKILL.md +50 -0
- package/generated/skills/toolu--test/SKILL.md +49 -0
- package/package.json +51 -0
- package/plugins/agent-browser/.claude-plugin/plugin.json +18 -0
- package/plugins/agent-browser/.codex-plugin/plugin.json +7 -0
- package/plugins/agent-browser/README.md +18 -0
- package/plugins/agent-browser/hooks/hooks.json +15 -0
- package/plugins/agent-browser/hooks/session-start.sh +42 -0
- package/plugins/agent-browser/skills/agent-browser/SKILL.md +77 -0
- package/plugins/agent-browser/skills/agent-browser/scripts/agent-browser.sh +80 -0
- package/plugins/ast-grep/.claude-plugin/plugin.json +18 -0
- package/plugins/ast-grep/.codex-plugin/plugin.json +7 -0
- package/plugins/ast-grep/README.md +21 -0
- package/plugins/ast-grep/hooks/hooks.json +15 -0
- package/plugins/ast-grep/hooks/post-tools.d/byte-savings.sh +67 -0
- package/plugins/ast-grep/hooks/pre-tools.d/search-nudge.sh +170 -0
- package/plugins/ast-grep/hooks/register.sh +68 -0
- package/plugins/ast-grep/scripts/byte-savings-report.sh +28 -0
- package/plugins/ast-grep/skills/ast-grep/SKILL.md +127 -0
- package/plugins/ast-grep/skills/ast-grep/references/ast-grep-advanced.md +44 -0
- package/plugins/ast-grep/skills/ast-grep/scripts/ast-grep.sh +103 -0
- package/plugins/context7/.claude-plugin/plugin.json +18 -0
- package/plugins/context7/.codex-plugin/plugin.json +7 -0
- package/plugins/context7/README.md +19 -0
- package/plugins/context7/hooks/hooks.json +15 -0
- package/plugins/context7/hooks/session-start.sh +42 -0
- package/plugins/context7/skills/context7/SKILL.md +64 -0
- package/plugins/context7/skills/context7/scripts/search.sh +151 -0
- package/plugins/exa-search/.claude-plugin/plugin.json +18 -0
- package/plugins/exa-search/.codex-plugin/plugin.json +7 -0
- package/plugins/exa-search/README.md +19 -0
- package/plugins/exa-search/hooks/hooks.json +15 -0
- package/plugins/exa-search/hooks/session-start.sh +34 -0
- package/plugins/exa-search/skills/exa-search/SKILL.md +91 -0
- package/plugins/exa-search/skills/exa-search/scripts/search.sh +221 -0
- package/plugins/jev/.claude-plugin/plugin.json +19 -0
- package/plugins/jev/.codex-plugin/plugin.json +7 -0
- package/plugins/jev/README.md +61 -0
- package/plugins/jev/hooks/hooks.json +25 -0
- package/plugins/jev/hooks/lib/common.sh +49 -0
- package/plugins/jev/hooks/session-start.sh +45 -0
- package/plugins/jev/hooks/user-prompt-submit.sh +41 -0
- package/plugins/jev/skills/jev/SKILL.md +81 -0
- package/plugins/jev/skills/jev/evals/README.md +98 -0
- package/plugins/jev/skills/jev/references/problem-solving.md +198 -0
- package/plugins/jev/skills/jev/scripts/jev.sh +405 -0
- package/plugins/jira/.claude-plugin/plugin.json +18 -0
- package/plugins/jira/.codex-plugin/plugin.json +7 -0
- package/plugins/jira/README.md +42 -0
- package/plugins/jira/hooks/hooks.json +15 -0
- package/plugins/jira/hooks/session-start.sh +34 -0
- package/plugins/jira/skills/jira/SKILL.md +123 -0
- package/plugins/jira/skills/jira/scripts/jira.sh +70 -0
- package/plugins/jira/skills/jira/scripts/lib/adf.sh +17 -0
- package/plugins/jira/skills/jira/scripts/lib/attachment.sh +94 -0
- package/plugins/jira/skills/jira/scripts/lib/board.sh +58 -0
- package/plugins/jira/skills/jira/scripts/lib/http.sh +184 -0
- package/plugins/jira/skills/jira/scripts/lib/issue.sh +159 -0
- package/plugins/jira/skills/jira/scripts/lib/paginate.sh +45 -0
- package/plugins/jira/skills/jira/scripts/lib/plan-parse.sh +70 -0
- package/plugins/jira/skills/jira/scripts/lib/plan-run.sh +115 -0
- package/plugins/jira/skills/jira/scripts/lib/plan-store.sh +168 -0
- package/plugins/jira/skills/jira/scripts/lib/plan.sh +108 -0
- package/plugins/jira/skills/jira/scripts/lib/project.sh +42 -0
- package/plugins/jira/skills/jira/scripts/lib/raw.sh +15 -0
- package/plugins/jira/skills/jira/scripts/lib/search.sh +41 -0
- package/plugins/jira/skills/jira/scripts/lib/sprint.sh +88 -0
- package/plugins/jira/skills/jira/scripts/lib/user.sh +45 -0
- package/plugins/jira/skills/jira/scripts/lib/worklog.sh +56 -0
- package/plugins/pr-babysit/.claude-plugin/plugin.json +25 -0
- package/plugins/pr-babysit/.codex-plugin/plugin.json +7 -0
- package/plugins/pr-babysit/README.md +38 -0
- package/plugins/pr-babysit/commands/babysit.md +13 -0
- package/plugins/pr-babysit/hooks/check-toolu.sh +20 -0
- package/plugins/pr-babysit/hooks/hooks.json +15 -0
- package/plugins/pr-babysit/scripts/babysit-tick.sh +133 -0
- package/plugins/pr-babysit/scripts/collect-pr.sh +224 -0
- package/plugins/pr-babysit/scripts/lib/common.sh +146 -0
- package/plugins/pr-babysit/scripts/lib/gh.sh +150 -0
- package/plugins/pr-babysit/scripts/lib/lock.sh +84 -0
- package/plugins/pr-babysit/scripts/lib/normalize.sh +98 -0
- package/plugins/pr-babysit/scripts/lib/state.sh +37 -0
- package/plugins/pr-babysit/scripts/parse-verdict.sh +171 -0
- package/plugins/pr-babysit/scripts/record.sh +69 -0
- package/plugins/pr-babysit/scripts/reduce-state.sh +245 -0
- package/plugins/pr-babysit/scripts/reply-thread.sh +90 -0
- package/plugins/pr-babysit/scripts/resolve-thread.sh +71 -0
- package/plugins/pr-babysit/skills/babysit/SKILL.md +31 -0
- package/plugins/pr-babysit/skills/babysit/references/helper.md +209 -0
- package/plugins/pr-babysit/workflows/babysit.md +722 -0
- package/plugins/python-quality/.claude-plugin/plugin.json +24 -0
- package/plugins/python-quality/.codex-plugin/plugin.json +6 -0
- package/plugins/python-quality/README.md +23 -0
- package/plugins/python-quality/hooks/check-toolu.sh +20 -0
- package/plugins/python-quality/hooks/concerns/00-preamble.sh +85 -0
- package/plugins/python-quality/hooks/concerns/10-size-file.sh +6 -0
- package/plugins/python-quality/hooks/concerns/20-tests.sh +37 -0
- package/plugins/python-quality/hooks/concerns/30-suppression.sh +33 -0
- package/plugins/python-quality/hooks/concerns/50-size-fn.sh +49 -0
- package/plugins/python-quality/hooks/concerns/70-no-mocks.sh +76 -0
- package/plugins/python-quality/hooks/concerns/90-docs.sh +52 -0
- package/plugins/python-quality/hooks/concerns/99-finalize.sh +36 -0
- package/plugins/python-quality/hooks/hooks.json +19 -0
- package/plugins/python-quality/hooks/register.sh +65 -0
- package/plugins/rust-quality/.claude-plugin/plugin.json +24 -0
- package/plugins/rust-quality/.codex-plugin/plugin.json +6 -0
- package/plugins/rust-quality/README.md +24 -0
- package/plugins/rust-quality/hooks/check-toolu.sh +20 -0
- package/plugins/rust-quality/hooks/concerns/00-preamble.sh +93 -0
- package/plugins/rust-quality/hooks/concerns/10-size-file.sh +10 -0
- package/plugins/rust-quality/hooks/concerns/20-tests.sh +84 -0
- package/plugins/rust-quality/hooks/concerns/30-suppression.sh +26 -0
- package/plugins/rust-quality/hooks/concerns/40-unsafe.sh +39 -0
- package/plugins/rust-quality/hooks/concerns/50-size-fn.sh +38 -0
- package/plugins/rust-quality/hooks/concerns/55-size-impl.sh +28 -0
- package/plugins/rust-quality/hooks/concerns/60-error-handling.sh +149 -0
- package/plugins/rust-quality/hooks/concerns/70-no-mocks.sh +139 -0
- package/plugins/rust-quality/hooks/concerns/90-docs.sh +32 -0
- package/plugins/rust-quality/hooks/concerns/99-finalize.sh +36 -0
- package/plugins/rust-quality/hooks/hooks.json +19 -0
- package/plugins/rust-quality/hooks/register.sh +65 -0
- package/plugins/statusline/.claude-plugin/plugin.json +19 -0
- package/plugins/statusline/.codex-plugin/plugin.json +7 -0
- package/plugins/statusline/README.md +108 -0
- package/plugins/statusline/commands/setup.md +30 -0
- package/plugins/statusline/hooks/hooks.json +15 -0
- package/plugins/statusline/hooks/session-start.sh +45 -0
- package/plugins/statusline/scripts/collect-status.sh +147 -0
- package/plugins/statusline/scripts/setup.sh +93 -0
- package/plugins/statusline/scripts/status.sh +55 -0
- package/plugins/statusline/skills/status/SKILL.md +13 -0
- package/plugins/statusline/statusline.sh +177 -0
- package/plugins/toolu/.claude-plugin/plugin.json +19 -0
- package/plugins/toolu/.codex-plugin/plugin.json +7 -0
- package/plugins/toolu/README.md +35 -0
- package/plugins/toolu/agents/.gitkeep +0 -0
- package/plugins/toolu/agents/architect.md +52 -0
- package/plugins/toolu/agents/deep-explore.md +65 -0
- package/plugins/toolu/agents/implementer.md +50 -0
- package/plugins/toolu/agents/quick-task.md +54 -0
- package/plugins/toolu/agents/research-agent.md +107 -0
- package/plugins/toolu/assets/agents/architect.toml +9 -0
- package/plugins/toolu/assets/agents/deep-explore.toml +9 -0
- package/plugins/toolu/assets/agents/implementer.toml +9 -0
- package/plugins/toolu/assets/agents/quick-task.toml +9 -0
- package/plugins/toolu/assets/agents/research-agent.toml +9 -0
- package/plugins/toolu/commands/.gitkeep +0 -0
- package/plugins/toolu/commands/commit.md +5 -0
- package/plugins/toolu/commands/review-and-commit.md +5 -0
- package/plugins/toolu/hooks/docs/gates.md +157 -0
- package/plugins/toolu/hooks/docs/model-routing.md +14 -0
- package/plugins/toolu/hooks/docs/post-compaction.md +3 -0
- package/plugins/toolu/hooks/docs/push-review.md +123 -0
- package/plugins/toolu/hooks/docs/session-start-python.md +3 -0
- package/plugins/toolu/hooks/docs/session-start-rust.md +4 -0
- package/plugins/toolu/hooks/docs/session-start-ts.md +3 -0
- package/plugins/toolu/hooks/docs/session-start.md +16 -0
- package/plugins/toolu/hooks/hooks.json +76 -0
- package/plugins/toolu/hooks/lib/config.sh +355 -0
- package/plugins/toolu/hooks/lib/detect.sh +929 -0
- package/plugins/toolu/hooks/lib/diff-sha.sh +32 -0
- package/plugins/toolu/hooks/lib/dispatch.sh +365 -0
- package/plugins/toolu/hooks/lib/docs-sync-config.sh +81 -0
- package/plugins/toolu/hooks/lib/edit-records.sh +157 -0
- package/plugins/toolu/hooks/lib/gate-file.sh +201 -0
- package/plugins/toolu/hooks/lib/gate-mode.sh +244 -0
- package/plugins/toolu/hooks/lib/host.sh +205 -0
- package/plugins/toolu/hooks/lib/permissions.sh +125 -0
- package/plugins/toolu/hooks/lib/plan-ledger-parse.sh +219 -0
- package/plugins/toolu/hooks/lib/plan-ledger-preflight.sh +137 -0
- package/plugins/toolu/hooks/lib/plan-ledger.sh +749 -0
- package/plugins/toolu/hooks/lib/push-waiver.sh +116 -0
- package/plugins/toolu/hooks/lib/quality-config.sh +184 -0
- package/plugins/toolu/hooks/lib/registry.sh +72 -0
- package/plugins/toolu/hooks/lib/state-sweeper.sh +194 -0
- package/plugins/toolu/hooks/lib/telemetry.sh +94 -0
- package/plugins/toolu/hooks/lib/verdict.sh +477 -0
- package/plugins/toolu/hooks/post-tools/mod.sh +42 -0
- package/plugins/toolu/hooks/post-tools/modules/gate-status.sh +140 -0
- package/plugins/toolu/hooks/post-tools/modules/push-waiver.sh +97 -0
- package/plugins/toolu/hooks/pre-compact.sh +18 -0
- package/plugins/toolu/hooks/pre-tools/agent-tier.sh +111 -0
- package/plugins/toolu/hooks/pre-tools/mod.sh +37 -0
- package/plugins/toolu/hooks/pre-tools/modules/bash-commands.sh +187 -0
- package/plugins/toolu/hooks/pre-tools/modules/code-edit-rules.sh +78 -0
- package/plugins/toolu/hooks/pre-tools/modules/commit-gate.sh +70 -0
- package/plugins/toolu/hooks/pre-tools/modules/docs-sync.sh +137 -0
- package/plugins/toolu/hooks/pre-tools/modules/mcp-blocker.sh +151 -0
- package/plugins/toolu/hooks/pre-tools/modules/plan-ledger.sh +201 -0
- package/plugins/toolu/hooks/pre-tools/modules/protected-files.sh +177 -0
- package/plugins/toolu/hooks/pre-tools/modules/push-review.sh +273 -0
- package/plugins/toolu/hooks/pre-tools/modules/quality-gate.sh +87 -0
- package/plugins/toolu/hooks/session-start.sh +379 -0
- package/plugins/toolu/hooks/user-prompt-submit.sh +197 -0
- package/plugins/toolu/scripts/context-budget.sh +111 -0
- package/plugins/toolu/scripts/debug-log.sh +98 -0
- package/plugins/toolu/scripts/debug-stack.sh +108 -0
- package/plugins/toolu/scripts/debug-testfail.sh +97 -0
- package/plugins/toolu/settings/.gitkeep +0 -0
- package/plugins/toolu/settings/README.md +127 -0
- package/plugins/toolu/settings/bash-allowlist.txt +12 -0
- package/plugins/toolu/settings/bash-denylist.txt +25 -0
- package/plugins/toolu/settings/code-edit-rules.json +67 -0
- package/plugins/toolu/settings/commit-prefixes.txt +12 -0
- package/plugins/toolu/settings/mcp-blocklist.txt +8 -0
- package/plugins/toolu/settings/permissions.fragment.json +36 -0
- package/plugins/toolu/settings/protected-files.txt +30 -0
- package/plugins/toolu/settings/rust-unsafe-exemptions.txt +5 -0
- package/plugins/toolu/settings/toolu.config.example.json +130 -0
- package/plugins/toolu/skills/.gitkeep +0 -0
- package/plugins/toolu/skills/brainstorm/SKILL.md +57 -0
- package/plugins/toolu/skills/brainstorm/references/design-questions.md +36 -0
- package/plugins/toolu/skills/commit/SKILL.md +11 -0
- package/plugins/toolu/skills/debug/SKILL.md +54 -0
- package/plugins/toolu/skills/debug/evals/README.md +17 -0
- package/plugins/toolu/skills/debug/evals/cases.json +24 -0
- package/plugins/toolu/skills/debug/evals/results.json +34 -0
- package/plugins/toolu/skills/deep-research/SKILL.md +51 -0
- package/plugins/toolu/skills/execution/SKILL.md +77 -0
- package/plugins/toolu/skills/orchestrator/SKILL.md +145 -0
- package/plugins/toolu/skills/orchestrator/references/delegation-cost.md +104 -0
- package/plugins/toolu/skills/orchestrator/references/model-routing.md +112 -0
- package/plugins/toolu/skills/plan/SKILL.md +56 -0
- package/plugins/toolu/skills/plan/references/ledger.md +18 -0
- package/plugins/toolu/skills/plan-review/SKILL.md +48 -0
- package/plugins/toolu/skills/review-and-commit/SKILL.md +10 -0
- package/plugins/toolu/skills/setup/SKILL.md +24 -0
- package/plugins/toolu/skills/setup/scripts/setup.sh +206 -0
- package/plugins/toolu/skills/spec/SKILL.md +67 -0
- package/plugins/toolu/skills/spec-review/SKILL.md +50 -0
- package/plugins/toolu/skills/test/SKILL.md +49 -0
- package/plugins/toolu/workflows/commit.md +21 -0
- package/plugins/toolu/workflows/host-mapping.md +22 -0
- package/plugins/toolu/workflows/review-and-commit.md +54 -0
- package/plugins/toolu/workflows/semantic-judgments.md +35 -0
- package/plugins/toolu-review/.claude-plugin/plugin.json +18 -0
- package/plugins/toolu-review/.codex-plugin/plugin.json +7 -0
- package/plugins/toolu-review/README.md +17 -0
- package/plugins/toolu-review/hooks/hooks.json +15 -0
- package/plugins/toolu-review/hooks/session-start.sh +34 -0
- package/plugins/toolu-review/skills/review/SKILL.md +90 -0
- package/plugins/toolu-review/skills/review/scripts/write-state.sh +189 -0
- package/plugins/ts-quality/.claude-plugin/plugin.json +24 -0
- package/plugins/ts-quality/.codex-plugin/plugin.json +6 -0
- package/plugins/ts-quality/README.md +24 -0
- package/plugins/ts-quality/hooks/check-toolu.sh +20 -0
- package/plugins/ts-quality/hooks/concerns/00-preamble.sh +123 -0
- package/plugins/ts-quality/hooks/concerns/10-imports.sh +33 -0
- package/plugins/ts-quality/hooks/concerns/15-type-as.sh +9 -0
- package/plugins/ts-quality/hooks/concerns/20-tests.sh +26 -0
- package/plugins/ts-quality/hooks/concerns/25-size-file.sh +40 -0
- package/plugins/ts-quality/hooks/concerns/30-size-fn.sh +53 -0
- package/plugins/ts-quality/hooks/concerns/35-react-hooks.sh +7 -0
- package/plugins/ts-quality/hooks/concerns/40-factory.sh +5 -0
- package/plugins/ts-quality/hooks/concerns/45-typeguard.sh +6 -0
- package/plugins/ts-quality/hooks/concerns/50-type-dup.sh +16 -0
- package/plugins/ts-quality/hooks/concerns/55-naming.sh +11 -0
- package/plugins/ts-quality/hooks/concerns/60-console.sh +6 -0
- package/plugins/ts-quality/hooks/concerns/65-suppression.sh +12 -0
- package/plugins/ts-quality/hooks/concerns/70-ui-confirm.sh +8 -0
- package/plugins/ts-quality/hooks/concerns/72-ui-radix.sh +12 -0
- package/plugins/ts-quality/hooks/concerns/74-react-props.sh +6 -0
- package/plugins/ts-quality/hooks/concerns/76-toast.sh +10 -0
- package/plugins/ts-quality/hooks/concerns/78-error-ast.sh +242 -0
- package/plugins/ts-quality/hooks/concerns/80-throw-literal.sh +23 -0
- package/plugins/ts-quality/hooks/concerns/85-no-mocks.sh +109 -0
- package/plugins/ts-quality/hooks/concerns/90-duplication.sh +30 -0
- package/plugins/ts-quality/hooks/concerns/92-docs.sh +36 -0
- package/plugins/ts-quality/hooks/concerns/94-handler.sh +15 -0
- package/plugins/ts-quality/hooks/concerns/99-finalize.sh +46 -0
- package/plugins/ts-quality/hooks/hooks.json +19 -0
- package/plugins/ts-quality/hooks/register.sh +65 -0
- package/src/adapter/__tests__/evaluate.test.ts +82 -0
- package/src/adapter/__tests__/permission-map.test.ts +91 -0
- package/src/adapter/evaluate.ts +54 -0
- package/src/adapter/permission-map.ts +177 -0
- package/src/bootstrap/__tests__/bootstrap.test.ts +148 -0
- package/src/bootstrap/entrypoint.ts +18 -0
- package/src/bootstrap/index.ts +5 -0
- package/src/bootstrap/readiness.ts +59 -0
- package/src/bootstrap/result.ts +21 -0
- package/src/bootstrap/runtime.ts +99 -0
- package/src/bootstrap/test-helpers.ts +25 -0
- package/src/host/__tests__/host.test.ts +37 -0
- package/src/host/detect.ts +46 -0
- package/src/host/index.ts +10 -0
- package/src/host/roots.ts +59 -0
- package/src/host/types.ts +14 -0
- package/src/inventory/__tests__/inventory.test.ts +69 -0
- package/src/inventory/index.ts +9 -0
- package/src/inventory/manifest.ts +46 -0
- package/src/inventory/scan.ts +66 -0
- package/src/inventory/selection.ts +92 -0
- package/src/inventory/types.ts +22 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +7 -0
- package/src/lifecycle/index.ts +2 -0
- package/src/lifecycle/table.ts +42 -0
- package/src/plugin/__tests__/plugin-stub.test.ts +6 -0
- package/src/plugin/plugin-stub.ts +7 -0
- package/src/plugin/toolu.ts +142 -0
- package/src/preflight/__tests__/preflight.test.ts +20 -0
- package/src/preflight/check.ts +75 -0
- package/src/preflight/index.ts +2 -0
- package/src/select/__tests__/select.test.ts +52 -0
- package/src/select/index.ts +2 -0
- package/src/select/resolve.ts +87 -0
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared detection helpers for project-agnostic hooks.
|
|
3
|
+
# Source via: . "${BASH_SOURCE%/*}/../lib/detect.sh"
|
|
4
|
+
|
|
5
|
+
_TOOLU_DETECT_LIB_DIR="$(cd "${BASH_SOURCE%/*}" && pwd)"
|
|
6
|
+
# shellcheck source=host.sh
|
|
7
|
+
. "$_TOOLU_DETECT_LIB_DIR/host.sh"
|
|
8
|
+
|
|
9
|
+
# Print the absolute project root (git toplevel) or "" if not in a git repo.
|
|
10
|
+
detect_project_root() {
|
|
11
|
+
git rev-parse --show-toplevel 2>/dev/null || true
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Print the project name (basename of the git toplevel) or "" if not in a git repo.
|
|
15
|
+
# Returns 0 even outside a git repo: a bare `[ -n "$root" ] && basename` would
|
|
16
|
+
# exit 1 when root is empty, which under `set -e` aborts callers before their
|
|
17
|
+
# own "unknown"-style fallback can run.
|
|
18
|
+
detect_project_name() {
|
|
19
|
+
local root
|
|
20
|
+
root=$(detect_project_root)
|
|
21
|
+
if [ -n "$root" ]; then basename "$root"; fi
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Print the package manager: bun | pnpm | npm | yarn | "" (none detected).
|
|
25
|
+
detect_node_pm() {
|
|
26
|
+
local root
|
|
27
|
+
root=$(detect_project_root)
|
|
28
|
+
[ -z "$root" ] && return 0
|
|
29
|
+
[ -f "$root/bun.lock" ] && echo bun && return
|
|
30
|
+
[ -f "$root/bun.lockb" ] && echo bun && return
|
|
31
|
+
[ -f "$root/pnpm-lock.yaml" ] && echo pnpm && return
|
|
32
|
+
[ -f "$root/yarn.lock" ] && echo yarn && return
|
|
33
|
+
[ -f "$root/package-lock.json" ] && echo npm && return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Echo "rust" if a Cargo.toml exists at the project root.
|
|
37
|
+
detect_rust() {
|
|
38
|
+
local root
|
|
39
|
+
root=$(detect_project_root)
|
|
40
|
+
[ -z "$root" ] && return 0
|
|
41
|
+
[ -f "$root/Cargo.toml" ] && echo rust
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Echo "python" if any of pyproject.toml, setup.py, setup.cfg, or
|
|
45
|
+
# requirements.txt exists at the project root.
|
|
46
|
+
detect_python() {
|
|
47
|
+
local root
|
|
48
|
+
root=$(detect_project_root)
|
|
49
|
+
[ -z "$root" ] && return 0
|
|
50
|
+
{ [ -f "$root/pyproject.toml" ] || [ -f "$root/setup.py" ] \
|
|
51
|
+
|| [ -f "$root/setup.cfg" ] || [ -f "$root/requirements.txt" ]; } && echo python
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# Echo "ts" if a tsconfig*.json exists anywhere in the project.
|
|
55
|
+
detect_ts() {
|
|
56
|
+
local root
|
|
57
|
+
root=$(detect_project_root)
|
|
58
|
+
[ -z "$root" ] && return 0
|
|
59
|
+
git -C "$root" ls-files '**/tsconfig*.json' 'tsconfig*.json' 2>/dev/null \
|
|
60
|
+
| grep -q . && echo ts
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Echo the project's TS linter: biome | oxc | eslint | "" (presence-only, by
|
|
64
|
+
# config-file at the git root; precedence biome > oxc > eslint). Used to point
|
|
65
|
+
# the agent at the real tool and to suppress our own overlapping nits — we
|
|
66
|
+
# never invoke the tool.
|
|
67
|
+
detect_ts_linter() {
|
|
68
|
+
local root
|
|
69
|
+
root=$(detect_project_root)
|
|
70
|
+
[ -z "$root" ] && return 0
|
|
71
|
+
{ [ -f "$root/biome.json" ] || [ -f "$root/biome.jsonc" ]; } && echo biome && return
|
|
72
|
+
[ -f "$root/.oxlintrc.json" ] && echo oxc && return
|
|
73
|
+
{ compgen -G "$root/.eslintrc*" >/dev/null 2>&1 \
|
|
74
|
+
|| compgen -G "$root/eslint.config.*" >/dev/null 2>&1; } && echo eslint && return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Echo the project's Python linter: ruff | "" (presence-only, by config-file
|
|
78
|
+
# or a `[tool.ruff` section in pyproject.toml at the project root). Used to
|
|
79
|
+
# point the agent at the real tool and to suppress our own overlapping nits —
|
|
80
|
+
# we never invoke the tool.
|
|
81
|
+
detect_python_linter() {
|
|
82
|
+
local root
|
|
83
|
+
root=$(detect_project_root)
|
|
84
|
+
[ -z "$root" ] && return 0
|
|
85
|
+
{ [ -f "$root/ruff.toml" ] || [ -f "$root/.ruff.toml" ]; } && echo ruff && return
|
|
86
|
+
[ -f "$root/pyproject.toml" ] && grep -q '^\[tool\.ruff' "$root/pyproject.toml" 2>/dev/null && echo ruff
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Echo "clippy" if a clippy config exists at the git root.
|
|
90
|
+
detect_clippy() {
|
|
91
|
+
local root
|
|
92
|
+
root=$(detect_project_root)
|
|
93
|
+
[ -z "$root" ] && return 0
|
|
94
|
+
{ [ -f "$root/clippy.toml" ] || [ -f "$root/.clippy.toml" ]; } && echo clippy
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Echo the plugin spec ("name@marketplace") if installed at any scope.
|
|
98
|
+
# Reads ~/.claude/plugins/installed_plugins.json (Claude Code's authoritative
|
|
99
|
+
# install registry).
|
|
100
|
+
#
|
|
101
|
+
# Exit codes:
|
|
102
|
+
# 0 + spec on stdout — installed.
|
|
103
|
+
# 0 + empty stdout — registry parsed; spec not present.
|
|
104
|
+
# 2 + empty stdout — INDETERMINATE: registry missing, jq missing, or
|
|
105
|
+
# malformed JSON. Callers should suppress install
|
|
106
|
+
# warnings rather than spam users on a box where the
|
|
107
|
+
# registry was moved or jq was uninstalled.
|
|
108
|
+
#
|
|
109
|
+
# Usage: detect_plugin_installed "ts-quality@toolu"
|
|
110
|
+
detect_plugin_installed() {
|
|
111
|
+
local spec="$1"
|
|
112
|
+
[ -z "$spec" ] && return 0
|
|
113
|
+
if [ "$(toolu_host)" = codex ]; then
|
|
114
|
+
toolu_codex_plugin_installed "$spec"
|
|
115
|
+
return $?
|
|
116
|
+
fi
|
|
117
|
+
# Same config-root resolution as registry.sh, so the install gate and the
|
|
118
|
+
# registry modules it gates follow CLAUDE_CONFIG_DIR together.
|
|
119
|
+
local registry="${CLAUDE_PLUGINS_REGISTRY:-${TOOLU_CONFIG_DIR:-${CLAUDE_CONFIG_DIR:-$HOME/.claude}}/plugins/installed_plugins.json}"
|
|
120
|
+
[ -f "$registry" ] || return 2
|
|
121
|
+
command -v jq >/dev/null 2>&1 || return 2
|
|
122
|
+
# Guard against malformed registry (top-level `plugins` missing or wrong
|
|
123
|
+
# type) — treat as indeterminate, not "not installed".
|
|
124
|
+
if ! jq -e '.plugins | type == "object"' "$registry" >/dev/null 2>&1; then
|
|
125
|
+
return 2
|
|
126
|
+
fi
|
|
127
|
+
# `has($s)` is tolerant of value type — array, object, number, or null at
|
|
128
|
+
# the spec key all parse cleanly. The prior `.plugins[$s] | length > 0`
|
|
129
|
+
# filter errored on non-array values and silently false-positived a WARN
|
|
130
|
+
# if Claude Code ever wrote a non-array there.
|
|
131
|
+
if jq -e --arg s "$spec" '.plugins | has($s)' "$registry" >/dev/null 2>&1; then
|
|
132
|
+
echo "$spec"
|
|
133
|
+
return 0
|
|
134
|
+
fi
|
|
135
|
+
return 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Echo "ast-grep" if either `sg` or `ast-grep` is on PATH.
|
|
139
|
+
detect_ast_grep() {
|
|
140
|
+
if command -v sg >/dev/null 2>&1 || command -v ast-grep >/dev/null 2>&1; then
|
|
141
|
+
echo ast-grep
|
|
142
|
+
fi
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Return the base branch from origin/HEAD, or "main" if remote is missing.
|
|
146
|
+
# Takes an optional repo root as $1 — callers gating a push must pass the root
|
|
147
|
+
# the push targets, since a worktree and its main checkout can sit on different
|
|
148
|
+
# branches with different origin/HEAD resolution.
|
|
149
|
+
detect_base_branch() {
|
|
150
|
+
local root ref
|
|
151
|
+
root="${1:-$(detect_project_root)}"
|
|
152
|
+
[ -z "$root" ] && { echo main; return; }
|
|
153
|
+
ref=$(git -C "$root" symbolic-ref --quiet refs/remotes/origin/HEAD 2>/dev/null)
|
|
154
|
+
if [ -n "$ref" ]; then
|
|
155
|
+
echo "${ref#refs/remotes/origin/}"
|
|
156
|
+
else
|
|
157
|
+
echo main
|
|
158
|
+
fi
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Print the data dir for settings/ lookups. Override with $TOOLU_SETTINGS_DIR.
|
|
162
|
+
toolu_settings_dir() {
|
|
163
|
+
if [ -n "${TOOLU_SETTINGS_DIR:-}" ]; then
|
|
164
|
+
echo "$TOOLU_SETTINGS_DIR"
|
|
165
|
+
elif [ -d "${HOME}/.claude/settings" ]; then
|
|
166
|
+
echo "${HOME}/.claude/settings"
|
|
167
|
+
else
|
|
168
|
+
local self
|
|
169
|
+
self=$(cd "${BASH_SOURCE%/*}/.." && pwd)
|
|
170
|
+
echo "$self/../settings"
|
|
171
|
+
fi
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Strip heredoc bodies from a shell command read on stdin.
|
|
175
|
+
#
|
|
176
|
+
# Handles `<<TAG`, `<<-TAG` (tab-stripped form), quoted/unquoted tags, and
|
|
177
|
+
# tolerates trailing redirections/pipes on the heredoc-start line
|
|
178
|
+
# (e.g. `<<EOF > /tmp/x`, `<<EOF | tee`). Any [A-Za-z_][A-Za-z0-9_]* identifier
|
|
179
|
+
# is accepted as a tag — the previous sed recipe hardcoded `EOF` and silently
|
|
180
|
+
# missed `<<-EOF`, `<<END`, or `<<EOF > file`.
|
|
181
|
+
#
|
|
182
|
+
# Why this matters: bash-commands.sh, quality-gate.sh, push-review.sh, and
|
|
183
|
+
# search-nudge.sh all run their deny/match patterns over the command. Without
|
|
184
|
+
# stripping the heredoc body, prose like a commit message containing the
|
|
185
|
+
# substring `cargo test` would false-positive a deny rule.
|
|
186
|
+
strip_heredocs() {
|
|
187
|
+
awk '
|
|
188
|
+
BEGIN { in_heredoc = 0; tag = ""; tag_tab = "" }
|
|
189
|
+
{
|
|
190
|
+
if (in_heredoc) {
|
|
191
|
+
line = $0
|
|
192
|
+
if (line == tag || line == tag_tab) { in_heredoc = 0; tag = ""; tag_tab = "" }
|
|
193
|
+
next
|
|
194
|
+
}
|
|
195
|
+
if (match($0, /<<-?[ \t]*"?'\''?[A-Za-z_][A-Za-z0-9_]*"?'\''?/)) {
|
|
196
|
+
m = substr($0, RSTART, RLENGTH)
|
|
197
|
+
# Strip leading `<<`, optional `-`, quotes, and surrounding whitespace.
|
|
198
|
+
gsub(/^<<-?[ \t]*"?'\''?/, "", m)
|
|
199
|
+
gsub(/"?'\''?$/, "", m)
|
|
200
|
+
tag = m
|
|
201
|
+
tag_tab = "\t" m
|
|
202
|
+
in_heredoc = 1
|
|
203
|
+
print
|
|
204
|
+
next
|
|
205
|
+
}
|
|
206
|
+
print
|
|
207
|
+
}
|
|
208
|
+
'
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Echo the bodies of UNQUOTED heredocs read on stdin, one line each.
|
|
212
|
+
#
|
|
213
|
+
# The mirror of strip_heredocs, for the one case stripping gets wrong: bash
|
|
214
|
+
# expands `$(...)` inside an UNQUOTED heredoc (`<<EOF`), so a command
|
|
215
|
+
# substitution in that body really runs. A QUOTED tag (`<<'EOF'`, `<<"EOF"`)
|
|
216
|
+
# expands nothing, so its body is literal text and is NOT emitted here.
|
|
217
|
+
#
|
|
218
|
+
# Used by bash_write_targets to catch a write hidden in a heredoc body.
|
|
219
|
+
_toolu_unquoted_heredoc_bodies() {
|
|
220
|
+
awk '
|
|
221
|
+
BEGIN { in_heredoc = 0; tag = ""; tag_tab = ""; quoted = 0 }
|
|
222
|
+
{
|
|
223
|
+
if (in_heredoc) {
|
|
224
|
+
if ($0 == tag || $0 == tag_tab) { in_heredoc = 0; tag = ""; tag_tab = ""; quoted = 0 }
|
|
225
|
+
else if (!quoted) { print }
|
|
226
|
+
next
|
|
227
|
+
}
|
|
228
|
+
if (match($0, /<<-?[ \t]*"?'\''?[A-Za-z_][A-Za-z0-9_]*"?'\''?/)) {
|
|
229
|
+
m = substr($0, RSTART, RLENGTH)
|
|
230
|
+
# A quote anywhere in the tag token means the body never expands.
|
|
231
|
+
quoted = (m ~ /["'\'']/) ? 1 : 0
|
|
232
|
+
gsub(/^<<-?[ \t]*"?'\''?/, "", m)
|
|
233
|
+
gsub(/"?'\''?$/, "", m)
|
|
234
|
+
tag = m
|
|
235
|
+
tag_tab = "\t" m
|
|
236
|
+
in_heredoc = 1
|
|
237
|
+
next
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
'
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# Echo a filesystem-safe slug for a branch name: '/'→'_', strip to
|
|
244
|
+
# [a-zA-Z0-9_-], empty → "_default". Used to key per-branch transient state
|
|
245
|
+
# files (push-review, plan-ledger). Takes the branch name as $1.
|
|
246
|
+
branch_slug() {
|
|
247
|
+
local branch="$1"
|
|
248
|
+
local slug
|
|
249
|
+
slug=$(echo "$branch" | tr '/' '_' | tr -cd 'a-zA-Z0-9_-')
|
|
250
|
+
[[ -z "$slug" ]] && slug="_default"
|
|
251
|
+
echo "$slug"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Find the end of a `$(...)` body that starts at index $2 of string $1.
|
|
255
|
+
#
|
|
256
|
+
# Quotes inside the body hide parens: in `$(echo ")")` the first `)` is data,
|
|
257
|
+
# not the terminator. A depth counter that ignores quoting stops there, returns
|
|
258
|
+
# a truncated body, and the rest of the command line is parsed as if it were
|
|
259
|
+
# inside the substitution — which is how a real `git push` after one used to go
|
|
260
|
+
# unseen.
|
|
261
|
+
#
|
|
262
|
+
# Sets $_TOOLU_SUBST_BODY (the body, parens excluded) and $_TOOLU_SUBST_END
|
|
263
|
+
# (the index just past the closing paren).
|
|
264
|
+
_toolu_subst_body() {
|
|
265
|
+
local s="$1" i="$2" start="$2"
|
|
266
|
+
local n=${#s} depth=1 q="" c
|
|
267
|
+
while [ "$i" -lt "$n" ] && [ "$depth" -gt 0 ]; do
|
|
268
|
+
c="${s:$i:1}"
|
|
269
|
+
if [ -n "$q" ]; then
|
|
270
|
+
if [ "$c" = '\' ] && [ "$q" = '"' ]; then
|
|
271
|
+
i=$((i + 2))
|
|
272
|
+
continue
|
|
273
|
+
fi
|
|
274
|
+
[ "$c" = "$q" ] && q=""
|
|
275
|
+
i=$((i + 1))
|
|
276
|
+
continue
|
|
277
|
+
fi
|
|
278
|
+
case "$c" in
|
|
279
|
+
"'"|'"') q="$c" ;;
|
|
280
|
+
'\') i=$((i + 1)) ;;
|
|
281
|
+
"(") depth=$((depth + 1)) ;;
|
|
282
|
+
")") depth=$((depth - 1)) ;;
|
|
283
|
+
esac
|
|
284
|
+
i=$((i + 1))
|
|
285
|
+
done
|
|
286
|
+
_TOOLU_SUBST_BODY="${s:$start:$((i - start - 1))}"
|
|
287
|
+
_TOOLU_SUBST_END="$i"
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Split a command string into simple statements, honouring shell quoting.
|
|
291
|
+
#
|
|
292
|
+
# Statements keep their original text (quotes included) — the tokenizer below
|
|
293
|
+
# removes those. Only UNQUOTED `;`, `&&`, `||`, `|`, `&` and newlines separate
|
|
294
|
+
# statements, so an operator inside a quoted argument does not split it.
|
|
295
|
+
#
|
|
296
|
+
# Command-substitution bodies — `$(...)` and backticks — are lifted out and
|
|
297
|
+
# emitted as statements in their own right, because `foo $(git push)` really
|
|
298
|
+
# does push. Inside single quotes nothing is substituted, so those are left as
|
|
299
|
+
# literal text.
|
|
300
|
+
#
|
|
301
|
+
# Results land in the global array $_TOOLU_STMTS (bash 3.2 has no nameref).
|
|
302
|
+
_toolu_split_statements() {
|
|
303
|
+
local s="$1"
|
|
304
|
+
local i=0 n=${#s} c nxt q="" cur="" body depth start
|
|
305
|
+
_TOOLU_STMTS=()
|
|
306
|
+
|
|
307
|
+
_toolu_flush_stmt() {
|
|
308
|
+
case "$cur" in
|
|
309
|
+
*[![:space:]]*) _TOOLU_STMTS+=("$cur") ;;
|
|
310
|
+
esac
|
|
311
|
+
cur=""
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
while [ "$i" -lt "$n" ]; do
|
|
315
|
+
c="${s:$i:1}"
|
|
316
|
+
|
|
317
|
+
# Single quotes: literal to the closing quote. No operators, no expansion.
|
|
318
|
+
if [ "$q" = "'" ]; then
|
|
319
|
+
cur+="$c"
|
|
320
|
+
[ "$c" = "'" ] && q=""
|
|
321
|
+
i=$((i + 1))
|
|
322
|
+
continue
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
# Double quotes: operators are literal, but substitutions still run.
|
|
326
|
+
if [ "$q" = '"' ]; then
|
|
327
|
+
if [ "$c" = '\' ]; then
|
|
328
|
+
cur+="${s:$i:2}"
|
|
329
|
+
i=$((i + 2))
|
|
330
|
+
continue
|
|
331
|
+
fi
|
|
332
|
+
if [ "$c" = '"' ]; then
|
|
333
|
+
cur+="$c"
|
|
334
|
+
q=""
|
|
335
|
+
i=$((i + 1))
|
|
336
|
+
continue
|
|
337
|
+
fi
|
|
338
|
+
if [ "$c" = '$' ] && [ "${s:$((i + 1)):1}" = "(" ]; then
|
|
339
|
+
_toolu_subst_body "$s" "$((i + 2))"
|
|
340
|
+
[ -n "$_TOOLU_SUBST_BODY" ] && _TOOLU_SUBSTS+=("$_TOOLU_SUBST_BODY")
|
|
341
|
+
# A space, not nothing: the text on either side of a removed
|
|
342
|
+
# substitution was never one word, and joining it could manufacture a
|
|
343
|
+
# command that was not written (`gi$(x)t push`).
|
|
344
|
+
cur+=" "
|
|
345
|
+
i="$_TOOLU_SUBST_END"
|
|
346
|
+
continue
|
|
347
|
+
fi
|
|
348
|
+
cur+="$c"
|
|
349
|
+
i=$((i + 1))
|
|
350
|
+
continue
|
|
351
|
+
fi
|
|
352
|
+
|
|
353
|
+
# Unquoted.
|
|
354
|
+
case "$c" in
|
|
355
|
+
"'"|'"')
|
|
356
|
+
q="$c"
|
|
357
|
+
cur+="$c"
|
|
358
|
+
;;
|
|
359
|
+
'\')
|
|
360
|
+
cur+="${s:$i:2}"
|
|
361
|
+
i=$((i + 1))
|
|
362
|
+
;;
|
|
363
|
+
'`')
|
|
364
|
+
start=$((i + 1))
|
|
365
|
+
i=$start
|
|
366
|
+
while [ "$i" -lt "$n" ] && [ "${s:$i:1}" != '`' ]; do
|
|
367
|
+
i=$((i + 1))
|
|
368
|
+
done
|
|
369
|
+
body="${s:$start:$((i - start))}"
|
|
370
|
+
[ -n "$body" ] && _TOOLU_SUBSTS+=("$body")
|
|
371
|
+
cur+=" "
|
|
372
|
+
;;
|
|
373
|
+
'$')
|
|
374
|
+
if [ "${s:$((i + 1)):1}" = "(" ]; then
|
|
375
|
+
_toolu_subst_body "$s" "$((i + 2))"
|
|
376
|
+
[ -n "$_TOOLU_SUBST_BODY" ] && _TOOLU_SUBSTS+=("$_TOOLU_SUBST_BODY")
|
|
377
|
+
cur+=" "
|
|
378
|
+
i="$_TOOLU_SUBST_END"
|
|
379
|
+
continue
|
|
380
|
+
fi
|
|
381
|
+
cur+="$c"
|
|
382
|
+
;;
|
|
383
|
+
'('|')')
|
|
384
|
+
# A command can follow an unquoted paren: a case-arm pattern
|
|
385
|
+
# (`a) git push;;`), a subshell (`(cd x && git push)`), a function body.
|
|
386
|
+
# Without treating these as boundaries the arm's command was glued to
|
|
387
|
+
# the pattern, so the statement's first token was `a)` and the push was
|
|
388
|
+
# never seen. A paren opening a substitution is consumed above and never
|
|
389
|
+
# reaches here.
|
|
390
|
+
_toolu_flush_stmt
|
|
391
|
+
;;
|
|
392
|
+
';'|'&'|'|'|$'\n')
|
|
393
|
+
nxt="${s:$((i + 1)):1}"
|
|
394
|
+
if { [ "$c" = '&' ] && [ "$nxt" = '&' ]; } || { [ "$c" = '|' ] && [ "$nxt" = '|' ]; }; then
|
|
395
|
+
i=$((i + 1))
|
|
396
|
+
fi
|
|
397
|
+
_toolu_flush_stmt
|
|
398
|
+
;;
|
|
399
|
+
*)
|
|
400
|
+
cur+="$c"
|
|
401
|
+
;;
|
|
402
|
+
esac
|
|
403
|
+
i=$((i + 1))
|
|
404
|
+
done
|
|
405
|
+
_toolu_flush_stmt
|
|
406
|
+
unset -f _toolu_flush_stmt
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
# Tokenize ONE statement into argv, dropping quote characters and keeping a
|
|
410
|
+
# quoted run as a single token. This is what makes prose safe: in
|
|
411
|
+
# `echo "no git push rules"` the quoted text is one argument token, so the
|
|
412
|
+
# command is `echo`, not `git`.
|
|
413
|
+
#
|
|
414
|
+
# Results land in the global array $_TOOLU_TOKENS.
|
|
415
|
+
_toolu_statement_tokens() {
|
|
416
|
+
local s="$1"
|
|
417
|
+
local i=0 n=${#s} c q="" tok="" started=0
|
|
418
|
+
_TOOLU_TOKENS=()
|
|
419
|
+
while [ "$i" -lt "$n" ]; do
|
|
420
|
+
c="${s:$i:1}"
|
|
421
|
+
if [ -n "$q" ]; then
|
|
422
|
+
if [ "$c" = "$q" ]; then
|
|
423
|
+
q=""
|
|
424
|
+
elif [ "$c" = '\' ] && [ "$q" = '"' ]; then
|
|
425
|
+
tok+="${s:$((i + 1)):1}"
|
|
426
|
+
i=$((i + 1))
|
|
427
|
+
else
|
|
428
|
+
tok+="$c"
|
|
429
|
+
fi
|
|
430
|
+
i=$((i + 1))
|
|
431
|
+
continue
|
|
432
|
+
fi
|
|
433
|
+
case "$c" in
|
|
434
|
+
"'"|'"')
|
|
435
|
+
q="$c"
|
|
436
|
+
started=1
|
|
437
|
+
;;
|
|
438
|
+
'\')
|
|
439
|
+
tok+="${s:$((i + 1)):1}"
|
|
440
|
+
i=$((i + 1))
|
|
441
|
+
started=1
|
|
442
|
+
;;
|
|
443
|
+
' '|$'\t'|$'\n')
|
|
444
|
+
if [ "$started" = 1 ]; then
|
|
445
|
+
_TOOLU_TOKENS+=("$tok")
|
|
446
|
+
tok=""
|
|
447
|
+
started=0
|
|
448
|
+
fi
|
|
449
|
+
;;
|
|
450
|
+
*)
|
|
451
|
+
tok+="$c"
|
|
452
|
+
started=1
|
|
453
|
+
;;
|
|
454
|
+
esac
|
|
455
|
+
i=$((i + 1))
|
|
456
|
+
done
|
|
457
|
+
[ "$started" = 1 ] && _TOOLU_TOKENS+=("$tok")
|
|
458
|
+
return 0
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# Print the git subcommand a statement invokes, or nothing.
|
|
462
|
+
#
|
|
463
|
+
# Git's global options sit between `git` and the subcommand, and two of them
|
|
464
|
+
# (-C, -c) take a separate value — consuming those is what keeps
|
|
465
|
+
# `git -c push.default=simple push` a push and `git commit -m "push"` a commit.
|
|
466
|
+
_toolu_git_subcommand() {
|
|
467
|
+
_toolu_statement_tokens "$1"
|
|
468
|
+
[ "${#_TOOLU_TOKENS[@]}" -gt 1 ] || return 1
|
|
469
|
+
|
|
470
|
+
# A command is not always the statement's first token. Grouping and keyword
|
|
471
|
+
# tokens can precede it (`deploy() { git push; }`, `if ! git push`), and so
|
|
472
|
+
# can environment assignments (`GIT_DIR=x git push`). Skipping them is what
|
|
473
|
+
# keeps those a push; `echo git push` is untouched, because `echo` is a
|
|
474
|
+
# command, not a prefix.
|
|
475
|
+
local start=0
|
|
476
|
+
while [ "$start" -lt "${#_TOOLU_TOKENS[@]}" ]; do
|
|
477
|
+
case "${_TOOLU_TOKENS[$start]}" in
|
|
478
|
+
"{"|"}"|"!"|if|while|until|then|else|elif|do|time|exec|command|builtin|env|nohup|sudo)
|
|
479
|
+
start=$((start + 1))
|
|
480
|
+
;;
|
|
481
|
+
[A-Za-z_]*=*)
|
|
482
|
+
start=$((start + 1))
|
|
483
|
+
;;
|
|
484
|
+
*)
|
|
485
|
+
break
|
|
486
|
+
;;
|
|
487
|
+
esac
|
|
488
|
+
done
|
|
489
|
+
|
|
490
|
+
[ "$((start + 1))" -lt "${#_TOOLU_TOKENS[@]}" ] || return 1
|
|
491
|
+
[ "${_TOOLU_TOKENS[$start]}" = "git" ] || return 1
|
|
492
|
+
|
|
493
|
+
local i=$((start + 1)) t
|
|
494
|
+
while [ "$i" -lt "${#_TOOLU_TOKENS[@]}" ]; do
|
|
495
|
+
t="${_TOOLU_TOKENS[$i]}"
|
|
496
|
+
case "$t" in
|
|
497
|
+
-C|-c)
|
|
498
|
+
# These take a separate value. With nothing after them the command line
|
|
499
|
+
# is malformed (`git -C` alone is a git usage error), so there is no
|
|
500
|
+
# subcommand to report rather than one to guess at.
|
|
501
|
+
[ "$((i + 2))" -lt "${#_TOOLU_TOKENS[@]}" ] || return 1
|
|
502
|
+
i=$((i + 2))
|
|
503
|
+
;;
|
|
504
|
+
--*=*|--*|-?)
|
|
505
|
+
i=$((i + 1))
|
|
506
|
+
;;
|
|
507
|
+
*)
|
|
508
|
+
printf '%s' "$t"
|
|
509
|
+
return 0
|
|
510
|
+
;;
|
|
511
|
+
esac
|
|
512
|
+
done
|
|
513
|
+
return 1
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
# Return 0 iff COMMAND ($1) invokes `git <SUBCOMMAND>` ($2) anywhere.
|
|
517
|
+
#
|
|
518
|
+
# Structural, not textual. The old regex matched the raw string, so any prose
|
|
519
|
+
# containing the two words in sequence — `echo "no git push rules"`, a commit
|
|
520
|
+
# message, a doc line — fired the push gates. Resolving the actual command of
|
|
521
|
+
# each statement is the difference between "these words appear" and "this runs".
|
|
522
|
+
toolu_runs_git_subcommand() {
|
|
523
|
+
local cmd sub="$2" stmt found=1
|
|
524
|
+
cmd=$(printf '%s\n' "$1" | strip_heredocs)
|
|
525
|
+
|
|
526
|
+
_TOOLU_SUBSTS=()
|
|
527
|
+
_toolu_split_statements "$cmd"
|
|
528
|
+
local -a stmts=("${_TOOLU_STMTS[@]}")
|
|
529
|
+
local -a substs=("${_TOOLU_SUBSTS[@]}")
|
|
530
|
+
|
|
531
|
+
if [ "${#stmts[@]}" -gt 0 ]; then
|
|
532
|
+
for stmt in "${stmts[@]}"; do
|
|
533
|
+
if [ "$(_toolu_git_subcommand "$stmt")" = "$sub" ]; then
|
|
534
|
+
found=0
|
|
535
|
+
break
|
|
536
|
+
fi
|
|
537
|
+
done
|
|
538
|
+
fi
|
|
539
|
+
|
|
540
|
+
# A substitution body is its own command line; scan each recursively so a
|
|
541
|
+
# push hidden in `$(...)` is not missed.
|
|
542
|
+
if [ "$found" -ne 0 ]; then
|
|
543
|
+
if [ "${#substs[@]}" -gt 0 ]; then
|
|
544
|
+
for stmt in "${substs[@]}"; do
|
|
545
|
+
if toolu_runs_git_subcommand "$stmt" "$sub"; then
|
|
546
|
+
found=0
|
|
547
|
+
break
|
|
548
|
+
fi
|
|
549
|
+
done
|
|
550
|
+
fi
|
|
551
|
+
fi
|
|
552
|
+
return "$found"
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# Return 0 iff COMMAND ($1) runs `git push`.
|
|
556
|
+
#
|
|
557
|
+
# Structural, via toolu_runs_git_subcommand: the command is parsed into
|
|
558
|
+
# statements and git's real subcommand is resolved, so the two words appearing
|
|
559
|
+
# in prose (`echo "no git push rules"`, a commit message, a doc line) is not a
|
|
560
|
+
# push. Heredoc bodies are stripped first, and `$(...)`/backtick bodies are
|
|
561
|
+
# scanned as commands of their own.
|
|
562
|
+
is_git_push() {
|
|
563
|
+
toolu_runs_git_subcommand "$1" push
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
# Return 0 iff COMMAND ($1) runs `git commit`.
|
|
567
|
+
#
|
|
568
|
+
# Same structural detection as is_git_push — `git commit-tree` is a different
|
|
569
|
+
# subcommand and does not match, and `echo "remember to git commit"` is prose.
|
|
570
|
+
is_git_commit() {
|
|
571
|
+
toolu_runs_git_subcommand "$1" commit
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
# Echo candidate file-write targets a Bash COMMAND ($1) may write to, one per
|
|
575
|
+
# line -- best-effort, not a full shell grammar. Extends protected-file
|
|
576
|
+
# coverage (protected-files.sh) to the Bash tool: Edit/Write/MultiEdit carry a
|
|
577
|
+
# path-shaped tool_input the hook can check directly, but `Bash` does not, so
|
|
578
|
+
# `sed -i` or a redirect against the same path had no structured field to
|
|
579
|
+
# inspect and sailed through (github.com/Falconiere/toolu/issues/176).
|
|
580
|
+
#
|
|
581
|
+
# Covers the realistic bypasses, not every one a shell can express:
|
|
582
|
+
# - output redirection: >, >>, and fd-qualified/duplicated forms
|
|
583
|
+
# (1>, 2>>, &>, &>>) -- the token right after the operator. `2>&1` (fd
|
|
584
|
+
# duplication, not a file) is excluded.
|
|
585
|
+
# - `tee [-a] FILE...` -- every non-flag token after `tee`
|
|
586
|
+
# - `sed -i[SUFFIX] ...` / `perl -i[SUFFIX] ...` -- every non-flag token once
|
|
587
|
+
# `-i` is seen (over-includes the script argument; harmless, since
|
|
588
|
+
# protected patterns are path-shaped and a sed/perl script is not)
|
|
589
|
+
# - `cp ... DEST` / `mv ... DEST` / `install ... DEST` -- the last non-flag
|
|
590
|
+
# token
|
|
591
|
+
# - `dd of=FILE`
|
|
592
|
+
# - `python3 -c '...open("FILE", "w"...)...'` (and `python -c`) -- the first
|
|
593
|
+
# string literal passed to `open()` in a write/append mode; the exact
|
|
594
|
+
# bypass reported in the issue above
|
|
595
|
+
#
|
|
596
|
+
# Heredoc bodies are stripped first via strip_heredocs, and the command is
|
|
597
|
+
# split into statements/tokens via _toolu_split_statements /
|
|
598
|
+
# _toolu_statement_tokens -- the same quote- and heredoc-safe parsing
|
|
599
|
+
# toolu_runs_git_subcommand uses -- so a path mentioned in prose or a commit
|
|
600
|
+
# message is not mistaken for a write target.
|
|
601
|
+
bash_write_targets() {
|
|
602
|
+
local cmd stmt
|
|
603
|
+
cmd=$(printf '%s\n' "$1" | strip_heredocs)
|
|
604
|
+
|
|
605
|
+
_TOOLU_SUBSTS=()
|
|
606
|
+
_toolu_split_statements "$cmd"
|
|
607
|
+
local -a stmts=("${_TOOLU_STMTS[@]}")
|
|
608
|
+
local -a substs=("${_TOOLU_SUBSTS[@]}")
|
|
609
|
+
|
|
610
|
+
for stmt in "${stmts[@]}"; do
|
|
611
|
+
_toolu_statement_tokens "$stmt"
|
|
612
|
+
local -a tokens=("${_TOOLU_TOKENS[@]}")
|
|
613
|
+
local n=${#tokens[@]} i first bin
|
|
614
|
+
[ "$n" -eq 0 ] && continue
|
|
615
|
+
|
|
616
|
+
# Resolve the command token past env-var assignments / sudo / env / command.
|
|
617
|
+
first=0
|
|
618
|
+
while [ "$first" -lt "$n" ]; do
|
|
619
|
+
case "${tokens[$first]}" in
|
|
620
|
+
sudo|env|command|nohup) first=$((first + 1)) ;;
|
|
621
|
+
[A-Za-z_]*=*) first=$((first + 1)) ;;
|
|
622
|
+
*) break ;;
|
|
623
|
+
esac
|
|
624
|
+
done
|
|
625
|
+
bin="${tokens[$first]:-}"
|
|
626
|
+
|
|
627
|
+
for ((i = 0; i < n; i++)); do
|
|
628
|
+
local tok="${tokens[$i]}" next
|
|
629
|
+
|
|
630
|
+
# Redirection operator as its own token: >, >>, N>, N>>, &>, &>>.
|
|
631
|
+
if [[ "$tok" =~ ^([0-9]*|&)\>{1,2}$ ]]; then
|
|
632
|
+
next="${tokens[$((i + 1))]:-}"
|
|
633
|
+
[[ "$next" =~ ^\&[0-9]+$ ]] && continue
|
|
634
|
+
[ -n "$next" ] && printf '%s\n' "$next"
|
|
635
|
+
continue
|
|
636
|
+
fi
|
|
637
|
+
|
|
638
|
+
[ "$i" -eq "$first" ] || continue
|
|
639
|
+
local j
|
|
640
|
+
case "$bin" in
|
|
641
|
+
tee)
|
|
642
|
+
for ((j = i + 1; j < n; j++)); do
|
|
643
|
+
[[ "${tokens[$j]}" == -* ]] && continue
|
|
644
|
+
printf '%s\n' "${tokens[$j]}"
|
|
645
|
+
done
|
|
646
|
+
;;
|
|
647
|
+
sed|perl)
|
|
648
|
+
local seen_i=0
|
|
649
|
+
for ((j = i + 1; j < n; j++)); do
|
|
650
|
+
if [ "$seen_i" -eq 0 ]; then
|
|
651
|
+
[[ "${tokens[$j]}" == -i* ]] && seen_i=1
|
|
652
|
+
continue
|
|
653
|
+
fi
|
|
654
|
+
[[ "${tokens[$j]}" == -* ]] && continue
|
|
655
|
+
printf '%s\n' "${tokens[$j]}"
|
|
656
|
+
done
|
|
657
|
+
;;
|
|
658
|
+
cp|mv|install)
|
|
659
|
+
local last=""
|
|
660
|
+
for ((j = i + 1; j < n; j++)); do
|
|
661
|
+
[[ "${tokens[$j]}" == -* ]] && continue
|
|
662
|
+
last="${tokens[$j]}"
|
|
663
|
+
done
|
|
664
|
+
[ -n "$last" ] && printf '%s\n' "$last"
|
|
665
|
+
;;
|
|
666
|
+
dd)
|
|
667
|
+
for ((j = i + 1; j < n; j++)); do
|
|
668
|
+
[[ "${tokens[$j]}" == of=* ]] && printf '%s\n' "${tokens[$j]#of=}"
|
|
669
|
+
done
|
|
670
|
+
;;
|
|
671
|
+
python|python3)
|
|
672
|
+
for ((j = i + 1; j < n; j++)); do
|
|
673
|
+
if [ "${tokens[$j]}" = "-c" ]; then
|
|
674
|
+
local script="${tokens[$((j + 1))]:-}"
|
|
675
|
+
# EVERY open() in the script, not just the first: a second write
|
|
676
|
+
# hidden behind a benign one is a bypass, not a footnote. The
|
|
677
|
+
# matched text is removed with a QUOTED expansion so a glob
|
|
678
|
+
# metacharacter in the script can't corrupt the walk.
|
|
679
|
+
#
|
|
680
|
+
# Modes: w/a/x always write; r is read-only UNLESS it carries a
|
|
681
|
+
# '+' (r+, rb+), which opens for writing too. Bare r/rb stay out.
|
|
682
|
+
while [[ "$script" =~ open\([\'\"]([^\'\"]+)[\'\"][[:space:]]*,[[:space:]]*[\'\"]([wax][^\'\"]*|r[^\'\"]*\+[^\'\"]*)[\'\"] ]]; do
|
|
683
|
+
printf '%s\n' "${BASH_REMATCH[1]}"
|
|
684
|
+
script="${script#*"${BASH_REMATCH[0]}"}"
|
|
685
|
+
done
|
|
686
|
+
break
|
|
687
|
+
fi
|
|
688
|
+
done
|
|
689
|
+
;;
|
|
690
|
+
esac
|
|
691
|
+
done
|
|
692
|
+
done
|
|
693
|
+
|
|
694
|
+
# A path hidden in a command substitution is still a write once that
|
|
695
|
+
# substitution runs -- recurse the same way toolu_runs_git_subcommand does.
|
|
696
|
+
for stmt in "${substs[@]}"; do
|
|
697
|
+
bash_write_targets "$stmt"
|
|
698
|
+
done
|
|
699
|
+
|
|
700
|
+
# strip_heredocs dropped every heredoc body above, which is right for prose
|
|
701
|
+
# -- but bash EXPANDS `$(...)` inside an UNQUOTED heredoc, so
|
|
702
|
+
# `cat <<EOF` / `$(echo x > .env)` / `EOF` really does write .env. Scan the
|
|
703
|
+
# substitutions in those bodies (and only those: a quoted tag, <<'EOF',
|
|
704
|
+
# expands nothing, so its body stays literal text).
|
|
705
|
+
local hd_bodies
|
|
706
|
+
hd_bodies=$(printf '%s\n' "$1" | _toolu_unquoted_heredoc_bodies)
|
|
707
|
+
if [ -n "$hd_bodies" ]; then
|
|
708
|
+
_TOOLU_SUBSTS=()
|
|
709
|
+
_toolu_split_statements "$hd_bodies"
|
|
710
|
+
local -a hd_substs=("${_TOOLU_SUBSTS[@]}")
|
|
711
|
+
for stmt in "${hd_substs[@]}"; do
|
|
712
|
+
bash_write_targets "$stmt"
|
|
713
|
+
done
|
|
714
|
+
fi
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
# Echo the absolute root of the repo a `git push` command ($1) targets.
|
|
718
|
+
#
|
|
719
|
+
# The hook's own cwd is not authoritative: `git -C <path> push` targets another
|
|
720
|
+
# checkout entirely. Resolving from the command keeps a worktree push gated
|
|
721
|
+
# against the worktree's own state, instead of reading the main checkout's state
|
|
722
|
+
# file (wrong branch, wrong diff) or — when the main checkout happens to sit on
|
|
723
|
+
# the base branch — skipping the gate altogether.
|
|
724
|
+
#
|
|
725
|
+
# Falls back to the cwd's toplevel, then the host-native project root, then cwd.
|
|
726
|
+
push_target_root() {
|
|
727
|
+
local command="$1" dir root
|
|
728
|
+
local -a cd_args=()
|
|
729
|
+
|
|
730
|
+
# Split on statement separators first: in `git -C a status && git -C b push`
|
|
731
|
+
# only the `push` segment's own -C counts. Within that segment, collect EVERY
|
|
732
|
+
# -C preceding `push` and replay them all — git applies them cumulatively
|
|
733
|
+
# (each relative to the last), so honouring only the first or last would
|
|
734
|
+
# resolve a different directory than the push itself uses.
|
|
735
|
+
while IFS= read -r dir; do
|
|
736
|
+
[ -n "$dir" ] || continue
|
|
737
|
+
dir=${dir%\"}
|
|
738
|
+
dir=${dir#\"}
|
|
739
|
+
cd_args+=(-C "$dir")
|
|
740
|
+
done < <(printf '%s\n' "$command" | strip_heredocs \
|
|
741
|
+
| awk '{ gsub(/&&|\|\||;|\|/, "\n"); print }' \
|
|
742
|
+
| awk '{
|
|
743
|
+
for (i = 1; i <= NF; i++)
|
|
744
|
+
if ($i == "push") {
|
|
745
|
+
for (j = 1; j < i; j++) if ($j == "-C") print $(j + 1)
|
|
746
|
+
exit
|
|
747
|
+
}
|
|
748
|
+
}')
|
|
749
|
+
|
|
750
|
+
if [ ${#cd_args[@]} -gt 0 ]; then
|
|
751
|
+
root=$(git "${cd_args[@]}" rev-parse --show-toplevel 2>/dev/null)
|
|
752
|
+
fi
|
|
753
|
+
[ -n "$root" ] || root=$(git rev-parse --show-toplevel 2>/dev/null)
|
|
754
|
+
[ -n "$root" ] || root=$(toolu_project_root)
|
|
755
|
+
[ -n "$root" ] || root=$(pwd)
|
|
756
|
+
echo "$root"
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
# push_target_branch COMMAND REPO_ROOT -> the branch the push should be judged
|
|
760
|
+
# as, or empty when it cannot be named.
|
|
761
|
+
#
|
|
762
|
+
# Attached HEAD: the checked-out branch, exactly as before — the review state
|
|
763
|
+
# file is keyed by it and a refspec must not silently re-key it.
|
|
764
|
+
# Detached HEAD (pr-babysit's `git worktree add --detach` + `push origin
|
|
765
|
+
# HEAD:<branch>` contract): the refspec DESTINATION of the push segment —
|
|
766
|
+
# `HEAD:x`, `+HEAD:refs/heads/x`, `src:x`, bare `x` all name `x`; a delete
|
|
767
|
+
# (`:x`), bare `HEAD`, or a wildcard yields empty, and the gate's detached
|
|
768
|
+
# deny stands.
|
|
769
|
+
push_target_branch() {
|
|
770
|
+
local command="$1" root="$2" branch refspec dst
|
|
771
|
+
branch=$(git -C "$root" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
|
|
772
|
+
if [ -n "$branch" ] && [ "$branch" != "HEAD" ]; then
|
|
773
|
+
echo "$branch"
|
|
774
|
+
return 0
|
|
775
|
+
fi
|
|
776
|
+
# First positional after the remote in the `push` segment (same statement
|
|
777
|
+
# split as push_target_root). Option values for the few value-taking push
|
|
778
|
+
# options are skipped so `-o ci.skip origin HEAD:x` still finds the refspec.
|
|
779
|
+
refspec=$(printf '%s\n' "$command" | strip_heredocs \
|
|
780
|
+
| awk '{ gsub(/&&|\|\||;|\|/, "\n"); print }' \
|
|
781
|
+
| awk '{
|
|
782
|
+
for (i = 1; i <= NF; i++)
|
|
783
|
+
if ($i == "push") {
|
|
784
|
+
n = 0
|
|
785
|
+
for (j = i + 1; j <= NF; j++) {
|
|
786
|
+
if ($j == "--") continue
|
|
787
|
+
if ($j ~ /^-/) {
|
|
788
|
+
if ($j == "-o" || $j == "--push-option" || $j == "--receive-pack" || $j == "--exec" || $j == "--repo") j++
|
|
789
|
+
continue
|
|
790
|
+
}
|
|
791
|
+
n++
|
|
792
|
+
if (n == 2) { print $j; exit }
|
|
793
|
+
}
|
|
794
|
+
exit
|
|
795
|
+
}
|
|
796
|
+
}')
|
|
797
|
+
refspec=${refspec#+}
|
|
798
|
+
[ -n "$refspec" ] || return 0
|
|
799
|
+
case "$refspec" in
|
|
800
|
+
:*) return 0 ;; # delete refspec: no destination to review
|
|
801
|
+
HEAD) return 0 ;; # bare HEAD on a detached checkout names nothing
|
|
802
|
+
*:*) dst=${refspec#*:} ;;
|
|
803
|
+
*) dst=$refspec ;;
|
|
804
|
+
esac
|
|
805
|
+
dst=${dst#refs/heads/}
|
|
806
|
+
case "$dst" in *'*'*|'') return 0 ;; esac
|
|
807
|
+
echo "$dst"
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
# Read non-comment non-blank lines from a settings file. Returns 0 with no
|
|
811
|
+
# output if the file is missing.
|
|
812
|
+
read_list() {
|
|
813
|
+
[ -f "$1" ] || return 0
|
|
814
|
+
grep -vE '^\s*(#|$)' "$1"
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
# count_code_lines FILE -> lines of real code (blank lines and comments
|
|
818
|
+
# excluded). Handles // line comments and /* ... */ blocks (incl. multi-line and
|
|
819
|
+
# inline), for both TS and Rust (/// and //! reduce to // and are dropped).
|
|
820
|
+
# Heuristic: does not track // or /* inside string literals — consistent with
|
|
821
|
+
# the other comment-stripping passes in the quality modules. Known edge: a
|
|
822
|
+
# string like `let s = "/* x";` flips block mode on and under-counts following
|
|
823
|
+
# code until a `*/` appears. Rare in practice; full literal-aware parsing isn't
|
|
824
|
+
# worth it here. Lives in detect.sh (not quality-config.sh) so the lang modules
|
|
825
|
+
# get one honest definition with no fallback — they already hard-require detect.sh.
|
|
826
|
+
count_code_lines() {
|
|
827
|
+
awk '
|
|
828
|
+
BEGIN { inblock=0; n=0 }
|
|
829
|
+
{
|
|
830
|
+
line=$0
|
|
831
|
+
if (inblock) {
|
|
832
|
+
idx=index(line,"*/")
|
|
833
|
+
if (idx>0) { line=substr(line, idx+2); inblock=0 } else next
|
|
834
|
+
}
|
|
835
|
+
while ((s=index(line,"/*"))>0) {
|
|
836
|
+
rest=substr(line, s+2); e=index(rest,"*/")
|
|
837
|
+
if (e>0) { line=substr(line,1,s-1) substr(rest, e+2) }
|
|
838
|
+
else { line=substr(line,1,s-1); inblock=1; break }
|
|
839
|
+
}
|
|
840
|
+
c=index(line,"//"); if (c>0) line=substr(line,1,c-1)
|
|
841
|
+
gsub(/^[ \t]+|[ \t]+$/, "", line)
|
|
842
|
+
if (length(line)>0) n++
|
|
843
|
+
}
|
|
844
|
+
# If we ended still inside a /* block, an unterminated comment OR (more
|
|
845
|
+
# likely) a string literal containing /* flipped block mode on and swallowed
|
|
846
|
+
# the rest of the file. Undercounting there would let an oversized file slip
|
|
847
|
+
# the size gate, so fall back to the raw line count — fail toward flagging.
|
|
848
|
+
# Intentional: the raw NR over-counts (it includes the blanks/comments we
|
|
849
|
+
# normally exclude). That is the fail-toward-flagging choice — better to
|
|
850
|
+
# over-count and flag than under-count and let an oversized file pass.
|
|
851
|
+
END { if (inblock) print NR; else print n }
|
|
852
|
+
' "$1" 2>/dev/null
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
# count_python_code_lines FILE -> lines of real code (blank lines and
|
|
856
|
+
# full-line `#` comments excluded). Trailing `#` comments on a code line, and
|
|
857
|
+
# docstring lines (they are plain string-literal lines, not `#`-prefixed),
|
|
858
|
+
# COUNT as code — deliberate fail-toward-flagging: distinguishing a real
|
|
859
|
+
# comment from a `#` inside a string, or tracking docstring state, isn't worth
|
|
860
|
+
# it here, and over-counting risks a false positive, not a missed oversized file.
|
|
861
|
+
count_python_code_lines() {
|
|
862
|
+
awk '
|
|
863
|
+
{
|
|
864
|
+
line=$0
|
|
865
|
+
gsub(/^[ \t]+|[ \t]+$/, "", line)
|
|
866
|
+
if (length(line) == 0) next
|
|
867
|
+
if (substr(line, 1, 1) == "#") next
|
|
868
|
+
n++
|
|
869
|
+
}
|
|
870
|
+
END { print n+0 }
|
|
871
|
+
' "$1" 2>/dev/null
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
# Return 0 if the file has more `/*` openers than `*/` closers — i.e. an
|
|
875
|
+
# unterminated block comment, or (more often) a string literal containing `/*`
|
|
876
|
+
# that confuses count_code_lines into the raw-line-count fallback. Lets the lang
|
|
877
|
+
# modules tell the user the size figure is approximate, not exact.
|
|
878
|
+
has_unterminated_block() {
|
|
879
|
+
[ -f "$1" ] || return 1
|
|
880
|
+
local open close
|
|
881
|
+
open=$(grep -o '/\*' "$1" 2>/dev/null | wc -l | tr -d ' ')
|
|
882
|
+
close=$(grep -o '\*/' "$1" 2>/dev/null | wc -l | tr -d ' ')
|
|
883
|
+
[ "${open:-0}" -gt "${close:-0}" ]
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
# Echo a repo-relative path for the given (potentially absolute) file_path.
|
|
887
|
+
# Falls back to the input unchanged if it cannot determine the project root
|
|
888
|
+
# or if the path is not under that root.
|
|
889
|
+
#
|
|
890
|
+
# Claude's Edit/Write tools send absolute paths; settings globs are written
|
|
891
|
+
# repo-relative. Without this helper, [[ /abs/path == hooks/lib/** ]] is
|
|
892
|
+
# always false, silently no-op'ing trusted-script protection.
|
|
893
|
+
to_relative_path() {
|
|
894
|
+
local p="${1:-}"
|
|
895
|
+
[ -z "$p" ] && { echo ""; return 0; }
|
|
896
|
+
local root
|
|
897
|
+
root=$(detect_project_root)
|
|
898
|
+
if [ -n "$root" ]; then
|
|
899
|
+
case "$p" in
|
|
900
|
+
"$root"/*) echo "${p#"$root"/}" ;;
|
|
901
|
+
*) echo "$p" ;;
|
|
902
|
+
esac
|
|
903
|
+
else
|
|
904
|
+
echo "$p"
|
|
905
|
+
fi
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
# toolu_plugin_active SPEC
|
|
909
|
+
# Boolean wrapper over detect_plugin_installed, used to gate "registry" hook
|
|
910
|
+
# modules on whether the contributing plugin is still installed.
|
|
911
|
+
#
|
|
912
|
+
# SPEC is the full "name@marketplace" spec (same arg detect_plugin_installed
|
|
913
|
+
# takes — it matches by exact spec via `.plugins | has($spec)`).
|
|
914
|
+
#
|
|
915
|
+
# Returns:
|
|
916
|
+
# 0 = plugin installed, OR indeterminate (no manifest / jq missing /
|
|
917
|
+
# malformed) -> fail open, so enforcement isn't silently lost on
|
|
918
|
+
# environments without the install registry.
|
|
919
|
+
# 1 = plugin definitively absent (registry parsed, spec not present).
|
|
920
|
+
toolu_plugin_active() {
|
|
921
|
+
local rc out
|
|
922
|
+
out=$(detect_plugin_installed "$1")
|
|
923
|
+
rc=$?
|
|
924
|
+
# Indeterminate (exit 2): fail open.
|
|
925
|
+
[ "$rc" -eq 2 ] && return 0
|
|
926
|
+
# Installed: detect_plugin_installed echoes the spec.
|
|
927
|
+
[ -n "$out" ] && return 0
|
|
928
|
+
return 1
|
|
929
|
+
}
|