trigger-tree 1.14.0__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.
- trigger_tree-1.14.0/.github/scripts/plugin_install_smoke.py +103 -0
- trigger_tree-1.14.0/.github/scripts/release_integrity.py +117 -0
- trigger_tree-1.14.0/.github/scripts/release_notes.py +57 -0
- trigger_tree-1.14.0/.gitignore +16 -0
- trigger_tree-1.14.0/CHANGELOG.md +670 -0
- trigger_tree-1.14.0/LICENSE +21 -0
- trigger_tree-1.14.0/PKG-INFO +106 -0
- trigger_tree-1.14.0/README.md +80 -0
- trigger_tree-1.14.0/docs/README.md +17 -0
- trigger_tree-1.14.0/hooks/claude-hooks.json +11 -0
- trigger_tree-1.14.0/hooks/hooks.json +9 -0
- trigger_tree-1.14.0/pyproject.toml +75 -0
- trigger_tree-1.14.0/scripts/tt-codex-hook.py +120 -0
- trigger_tree-1.14.0/scripts/tt-config.sh +32 -0
- trigger_tree-1.14.0/scripts/tt-doctor.py +288 -0
- trigger_tree-1.14.0/scripts/tt-log.py +656 -0
- trigger_tree-1.14.0/scripts/tt-open.sh +164 -0
- trigger_tree-1.14.0/scripts/tt-publish-badge.sh +26 -0
- trigger_tree-1.14.0/scripts/tt-report.py +623 -0
- trigger_tree-1.14.0/scripts/tt-setup.py +266 -0
- trigger_tree-1.14.0/scripts/tt-shell-capture.sh +48 -0
- trigger_tree-1.14.0/scripts/tt-stats.py +937 -0
- trigger_tree-1.14.0/scripts/tt-statusline.py +154 -0
- trigger_tree-1.14.0/scripts/tt-suggestions.py +128 -0
- trigger_tree-1.14.0/scripts/tt-tips.py +107 -0
- trigger_tree-1.14.0/scripts/tt-uninstall.py +60 -0
- trigger_tree-1.14.0/scripts/tt-watch.py +1013 -0
- trigger_tree-1.14.0/scripts/tt_runtime.py +27 -0
- trigger_tree-1.14.0/scripts/tt_scope.py +76 -0
- trigger_tree-1.14.0/scripts/validate_palette.js +28 -0
- trigger_tree-1.14.0/tests/fixture-project/docs/README.md +7 -0
- trigger_tree-1.14.0/trigger_tree/__init__.py +1 -0
- trigger_tree-1.14.0/trigger_tree/cli.py +52 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Prove the repository installs as a Claude Code marketplace plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
12
|
+
REQUIRED_FILES = (
|
|
13
|
+
".claude-plugin/plugin.json",
|
|
14
|
+
"hooks/claude-hooks.json",
|
|
15
|
+
"hooks/hooks.json",
|
|
16
|
+
"codex-skills/trigger-tree/SKILL.md",
|
|
17
|
+
"skills/tt/SKILL.md",
|
|
18
|
+
"scripts/tt-log.py",
|
|
19
|
+
"scripts/tt-doctor.py",
|
|
20
|
+
"scripts/tt-uninstall.py",
|
|
21
|
+
"scripts/tt_scope.py",
|
|
22
|
+
"scripts/tt_runtime.py",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def run(*args: str, config_dir: Path) -> subprocess.CompletedProcess[str]:
|
|
27
|
+
env = os.environ.copy()
|
|
28
|
+
env["CLAUDE_CONFIG_DIR"] = str(config_dir)
|
|
29
|
+
return subprocess.run(
|
|
30
|
+
args,
|
|
31
|
+
cwd=ROOT,
|
|
32
|
+
env=env,
|
|
33
|
+
check=True,
|
|
34
|
+
text=True,
|
|
35
|
+
capture_output=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> None:
|
|
40
|
+
with tempfile.TemporaryDirectory(prefix="trigger-tree-plugin-smoke-") as temp:
|
|
41
|
+
config_dir = Path(temp) / "claude"
|
|
42
|
+
config_dir.mkdir()
|
|
43
|
+
|
|
44
|
+
run(
|
|
45
|
+
"npx",
|
|
46
|
+
"claude",
|
|
47
|
+
"plugin",
|
|
48
|
+
"marketplace",
|
|
49
|
+
"add",
|
|
50
|
+
str(ROOT),
|
|
51
|
+
"--scope",
|
|
52
|
+
"user",
|
|
53
|
+
config_dir=config_dir,
|
|
54
|
+
)
|
|
55
|
+
run(
|
|
56
|
+
"npx",
|
|
57
|
+
"claude",
|
|
58
|
+
"plugin",
|
|
59
|
+
"install",
|
|
60
|
+
"trigger-tree@trigger-tree",
|
|
61
|
+
"--scope",
|
|
62
|
+
"user",
|
|
63
|
+
config_dir=config_dir,
|
|
64
|
+
)
|
|
65
|
+
result = run("npx", "claude", "plugin", "list", "--json", config_dir=config_dir)
|
|
66
|
+
|
|
67
|
+
installed = json.loads(result.stdout)
|
|
68
|
+
matches = [item for item in installed if item["id"] == "trigger-tree@trigger-tree"]
|
|
69
|
+
if len(matches) != 1 or not matches[0]["enabled"]:
|
|
70
|
+
raise SystemExit("trigger-tree was not installed and enabled exactly once")
|
|
71
|
+
|
|
72
|
+
install_path = Path(matches[0]["installPath"]).resolve()
|
|
73
|
+
if not install_path.is_relative_to(config_dir.resolve()):
|
|
74
|
+
raise SystemExit("plugin escaped the isolated Claude config directory")
|
|
75
|
+
|
|
76
|
+
missing = [name for name in REQUIRED_FILES if not (install_path / name).is_file()]
|
|
77
|
+
if missing:
|
|
78
|
+
raise SystemExit(f"installed plugin is incomplete: {', '.join(missing)}")
|
|
79
|
+
claude_skills = sorted(
|
|
80
|
+
path.parent.name for path in (install_path / "skills").glob("*/SKILL.md")
|
|
81
|
+
)
|
|
82
|
+
if claude_skills != ["tt"]:
|
|
83
|
+
raise SystemExit(f"Claude plugin exposes unexpected skills: {claude_skills}")
|
|
84
|
+
|
|
85
|
+
manifest = json.loads((install_path / ".claude-plugin/plugin.json").read_text())
|
|
86
|
+
if matches[0]["version"] != manifest["version"]:
|
|
87
|
+
raise SystemExit("installed version does not match plugin manifest")
|
|
88
|
+
|
|
89
|
+
hooks = (install_path / "hooks" / "hooks.json").read_text()
|
|
90
|
+
if "${CLAUDE_PLUGIN_ROOT}/scripts/tt-codex-hook.py" not in hooks:
|
|
91
|
+
raise SystemExit("shared hooks do not use the cross-client plugin root")
|
|
92
|
+
|
|
93
|
+
command_contract = (install_path / "skills" / "tt" / "SKILL.md").read_text()
|
|
94
|
+
if "CLAUDE_SKILL_DIR" in command_contract:
|
|
95
|
+
raise SystemExit("Claude command contract resolves scripts from the skill directory")
|
|
96
|
+
if "${CLAUDE_PLUGIN_ROOT}/scripts/tt-open.sh" not in command_contract:
|
|
97
|
+
raise SystemExit("Claude watch command does not resolve from the plugin root")
|
|
98
|
+
|
|
99
|
+
print(f"Installed trigger-tree v{manifest['version']} in an isolated Claude config")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
main()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Reject release tags that disagree with the plugin's release metadata."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import unquote
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
12
|
+
PUBLIC_OMISSIONS = {"help"}
|
|
13
|
+
CLAUDE_SECTION_ONLY = {"tips"}
|
|
14
|
+
CODEX_LABELS = {"live dashboard": "watch"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def fail(message: str) -> None:
|
|
18
|
+
raise SystemExit(f"release integrity failed: {message}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def tt_commands(text: str) -> set[str]:
|
|
22
|
+
return set(re.findall(r"/tt\s+([a-z]+)", text))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def require_commands(label: str, actual: set[str], expected: set[str]) -> None:
|
|
26
|
+
if actual != expected:
|
|
27
|
+
fail(
|
|
28
|
+
f"{label} command drift: missing {sorted(expected - actual)}, "
|
|
29
|
+
f"extra {sorted(actual - expected)}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_relative_links(root: Path) -> None:
|
|
34
|
+
documents = [root / "README.md", *(root / "docs").glob("*.md")]
|
|
35
|
+
for document in documents:
|
|
36
|
+
text = document.read_text(encoding="utf-8")
|
|
37
|
+
for raw_target in re.findall(r"!?\[[^]]*\]\(([^)]+)\)", text):
|
|
38
|
+
target = raw_target.strip().strip("<>").split("#", 1)[0]
|
|
39
|
+
if not target or re.match(r"^[a-z][a-z0-9+.-]*:", target, re.I):
|
|
40
|
+
continue
|
|
41
|
+
resolved = (document.parent / unquote(target)).resolve()
|
|
42
|
+
if not resolved.exists():
|
|
43
|
+
fail(f"broken relative link in {document.relative_to(root)}: {raw_target}")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_docs_currency(root: Path = ROOT) -> None:
|
|
47
|
+
claude = (root / "skills/tt/SKILL.md").read_text(encoding="utf-8")
|
|
48
|
+
frontmatter = claude.split("---", 2)[1]
|
|
49
|
+
help_block = claude.split('## `$1` = "help" or empty', 1)[1].split("\n## ", 1)[0]
|
|
50
|
+
canonical = tt_commands(frontmatter)
|
|
51
|
+
require_commands("Claude help table", tt_commands(help_block), canonical)
|
|
52
|
+
|
|
53
|
+
sections = set(re.findall(r'^## `\$1` = "([a-z]+)', claude, re.M))
|
|
54
|
+
require_commands("Claude handler sections", sections - CLAUDE_SECTION_ONLY, canonical)
|
|
55
|
+
|
|
56
|
+
public = canonical - PUBLIC_OMISSIONS
|
|
57
|
+
require_commands(
|
|
58
|
+
"README.md", tt_commands((root / "README.md").read_text(encoding="utf-8")), public
|
|
59
|
+
)
|
|
60
|
+
require_commands(
|
|
61
|
+
"index.html", tt_commands((root / "index.html").read_text(encoding="utf-8")), public
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
codex = (root / "codex-skills/trigger-tree/SKILL.md").read_text(encoding="utf-8")
|
|
65
|
+
labels = set(re.findall(r"^- ([A-Za-z ]+):", codex, re.M))
|
|
66
|
+
codex_commands = {CODEX_LABELS.get(label.lower(), label.lower()) for label in labels}
|
|
67
|
+
require_commands("Codex workflows", codex_commands, public | CLAUDE_SECTION_ONLY)
|
|
68
|
+
check_relative_links(root)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main(tag: str) -> None:
|
|
72
|
+
if not re.fullmatch(r"v\d+\.\d+\.\d+(?:-rc\.\d+)?", tag):
|
|
73
|
+
fail(f"tag {tag!r} is not vMAJOR.MINOR.PATCH or vMAJOR.MINOR.PATCH-rc.N")
|
|
74
|
+
|
|
75
|
+
manifest = json.loads((ROOT / ".claude-plugin/plugin.json").read_text(encoding="utf-8"))
|
|
76
|
+
codex_manifest = json.loads((ROOT / ".codex-plugin/plugin.json").read_text(encoding="utf-8"))
|
|
77
|
+
marketplace = json.loads((ROOT / ".claude-plugin/marketplace.json").read_text(encoding="utf-8"))
|
|
78
|
+
version = tag.removeprefix("v")
|
|
79
|
+
|
|
80
|
+
if manifest["version"] != version:
|
|
81
|
+
fail(f"tag {tag} does not match plugin version {manifest['version']}")
|
|
82
|
+
if codex_manifest["name"] != manifest["name"]:
|
|
83
|
+
fail("Codex and Claude plugin names disagree")
|
|
84
|
+
if codex_manifest["version"] != version:
|
|
85
|
+
fail(f"Codex plugin version {codex_manifest['version']} does not match {version}")
|
|
86
|
+
if marketplace["name"] != manifest["name"]:
|
|
87
|
+
fail("marketplace and plugin names disagree")
|
|
88
|
+
if not any(plugin["name"] == manifest["name"] for plugin in marketplace["plugins"]):
|
|
89
|
+
fail("marketplace does not expose the plugin manifest name")
|
|
90
|
+
entry = next(plugin for plugin in marketplace["plugins"] if plugin["name"] == manifest["name"])
|
|
91
|
+
if entry.get("version") != version:
|
|
92
|
+
fail(f"marketplace plugin version {entry.get('version')} does not match {version}")
|
|
93
|
+
|
|
94
|
+
pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
|
|
95
|
+
packaged = re.search(r'(?m)^version = "([^"]+)"$', pyproject)
|
|
96
|
+
if not packaged or packaged.group(1) != version:
|
|
97
|
+
found = packaged.group(1) if packaged else "missing"
|
|
98
|
+
fail(f"pyproject packaged version {found} does not match {version}")
|
|
99
|
+
|
|
100
|
+
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
|
101
|
+
heading = re.search(r"^## (?:\[)?([^]\s]+)", changelog, re.M)
|
|
102
|
+
if not heading or heading.group(1) != version:
|
|
103
|
+
fail(f"top CHANGELOG.md release is not {version}")
|
|
104
|
+
|
|
105
|
+
check_docs_currency(ROOT)
|
|
106
|
+
|
|
107
|
+
print(f"Release metadata consistently describes {tag}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
if len(sys.argv) != 2:
|
|
112
|
+
fail("expected exactly one tag argument")
|
|
113
|
+
if sys.argv[1] == "--docs":
|
|
114
|
+
check_docs_currency(ROOT)
|
|
115
|
+
print("User-facing command surfaces and relative links are current")
|
|
116
|
+
else:
|
|
117
|
+
main(sys.argv[1])
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Print the GitHub Release body for one tag from the changelog. Idempotent input."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
10
|
+
|
|
11
|
+
FOOTER = """
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
**Install (Claude Code)**
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
/plugin marketplace add Hedde/trigger_tree
|
|
18
|
+
/plugin install trigger-tree@trigger-tree
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Install (Codex)**
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
codex plugin marketplace add Hedde/trigger_tree
|
|
25
|
+
codex plugin install trigger-tree
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
[Website](https://hedde.github.io/trigger_tree/) · [Documentation](https://github.com/Hedde/trigger_tree/tree/main/docs) · [Changelog](https://github.com/Hedde/trigger_tree/blob/main/CHANGELOG.md) · [Privacy](https://github.com/Hedde/trigger_tree/blob/main/PRIVACY.md)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def changelog_section(text: str, version: str) -> str:
|
|
33
|
+
"""Return the changelog body for exactly this version, without its heading."""
|
|
34
|
+
pattern = rf"(?ms)^## {re.escape(version)} — [0-9-]+\n(.*?)(?=^## |\Z)"
|
|
35
|
+
match = re.search(pattern, text)
|
|
36
|
+
if not match:
|
|
37
|
+
raise SystemExit(f"CHANGELOG.md has no section for version {version}")
|
|
38
|
+
body = match.group(1).strip()
|
|
39
|
+
if not body:
|
|
40
|
+
raise SystemExit(f"CHANGELOG.md section for {version} is empty")
|
|
41
|
+
return body
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def release_body(tag: str, changelog_text: str) -> str:
|
|
45
|
+
version = tag.removeprefix("v")
|
|
46
|
+
return changelog_section(changelog_text, version) + "\n" + FOOTER
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main(tag: str) -> None:
|
|
50
|
+
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
|
51
|
+
sys.stdout.write(release_body(tag, changelog))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
if len(sys.argv) != 2:
|
|
56
|
+
raise SystemExit("usage: release_notes.py <tag>")
|
|
57
|
+
main(sys.argv[1])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.trigger-tree/
|
|
2
|
+
!tests/fixture-project/.trigger-tree/
|
|
3
|
+
!tests/fixture-project/.trigger-tree/**
|
|
4
|
+
tests/fixture-project/.trigger-tree/report.html
|
|
5
|
+
__pycache__/
|
|
6
|
+
.DS_Store
|
|
7
|
+
.venv/
|
|
8
|
+
.venv-*/
|
|
9
|
+
node_modules/
|
|
10
|
+
.coverage
|
|
11
|
+
htmlcov/
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.claude/worktrees/
|
|
14
|
+
dist-submissions/
|
|
15
|
+
dist/
|
|
16
|
+
*.egg-info/
|