@oriro/orirocli 0.1.9 → 0.1.12
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/README.md +16 -18
- package/dist/cli.js +4776 -2964
- package/package.json +2 -2
- package/skills/craft/ai-engineering/SKILL.md +2 -2
- package/skills/graphify/SKILL.md +0 -619
- package/skills/graphify/__init__.py +0 -28
- package/skills/graphify/__main__.py +0 -4582
- package/skills/graphify/affected.py +0 -154
- package/skills/graphify/always_on/agents-md.md +0 -12
- package/skills/graphify/always_on/antigravity-rules.md +0 -14
- package/skills/graphify/always_on/claude-md.md +0 -9
- package/skills/graphify/always_on/gemini-md.md +0 -9
- package/skills/graphify/always_on/kiro-steering.md +0 -5
- package/skills/graphify/always_on/vscode-instructions.md +0 -17
- package/skills/graphify/analyze.py +0 -724
- package/skills/graphify/benchmark.py +0 -155
- package/skills/graphify/build.py +0 -487
- package/skills/graphify/cache.py +0 -417
- package/skills/graphify/callflow_html.py +0 -2020
- package/skills/graphify/cluster.py +0 -272
- package/skills/graphify/command-kilo.md +0 -15
- package/skills/graphify/dedup.py +0 -429
- package/skills/graphify/detect.py +0 -1379
- package/skills/graphify/diagnostics.py +0 -390
- package/skills/graphify/export.py +0 -1408
- package/skills/graphify/extract.py +0 -11570
- package/skills/graphify/global_graph.py +0 -159
- package/skills/graphify/google_workspace.py +0 -223
- package/skills/graphify/hooks.py +0 -457
- package/skills/graphify/ingest.py +0 -331
- package/skills/graphify/llm.py +0 -1896
- package/skills/graphify/manifest.py +0 -4
- package/skills/graphify/mcp_ingest.py +0 -392
- package/skills/graphify/multigraph_compat.py +0 -212
- package/skills/graphify/pg_introspect.py +0 -142
- package/skills/graphify/prs.py +0 -748
- package/skills/graphify/querylog.py +0 -70
- package/skills/graphify/report.py +0 -218
- package/skills/graphify/scip_ingest.py +0 -363
- package/skills/graphify/security.py +0 -336
- package/skills/graphify/semantic_cleanup.py +0 -319
- package/skills/graphify/serve.py +0 -1309
- package/skills/graphify/skill-aider.md +0 -1246
- package/skills/graphify/skill-amp.md +0 -613
- package/skills/graphify/skill-claw.md +0 -616
- package/skills/graphify/skill-codex.md +0 -613
- package/skills/graphify/skill-copilot.md +0 -616
- package/skills/graphify/skill-devin.md +0 -1372
- package/skills/graphify/skill-droid.md +0 -613
- package/skills/graphify/skill-kilo.md +0 -625
- package/skills/graphify/skill-kiro.md +0 -615
- package/skills/graphify/skill-opencode.md +0 -608
- package/skills/graphify/skill-pi.md +0 -615
- package/skills/graphify/skill-trae.md +0 -614
- package/skills/graphify/skill-vscode.md +0 -612
- package/skills/graphify/skill-windows.md +0 -651
- package/skills/graphify/skills/amp/references/add-watch.md +0 -56
- package/skills/graphify/skills/amp/references/exports.md +0 -71
- package/skills/graphify/skills/amp/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/amp/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/amp/references/hooks.md +0 -33
- package/skills/graphify/skills/amp/references/query.md +0 -249
- package/skills/graphify/skills/amp/references/transcribe.md +0 -48
- package/skills/graphify/skills/amp/references/update.md +0 -179
- package/skills/graphify/skills/claude/references/add-watch.md +0 -56
- package/skills/graphify/skills/claude/references/exports.md +0 -71
- package/skills/graphify/skills/claude/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/claude/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/claude/references/hooks.md +0 -33
- package/skills/graphify/skills/claude/references/query.md +0 -103
- package/skills/graphify/skills/claude/references/transcribe.md +0 -48
- package/skills/graphify/skills/claude/references/update.md +0 -179
- package/skills/graphify/skills/claw/references/add-watch.md +0 -56
- package/skills/graphify/skills/claw/references/exports.md +0 -71
- package/skills/graphify/skills/claw/references/extraction-spec.md +0 -29
- package/skills/graphify/skills/claw/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/claw/references/hooks.md +0 -33
- package/skills/graphify/skills/claw/references/query.md +0 -249
- package/skills/graphify/skills/claw/references/transcribe.md +0 -48
- package/skills/graphify/skills/claw/references/update.md +0 -179
- package/skills/graphify/skills/codex/references/add-watch.md +0 -56
- package/skills/graphify/skills/codex/references/exports.md +0 -71
- package/skills/graphify/skills/codex/references/extraction-spec.md +0 -29
- package/skills/graphify/skills/codex/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/codex/references/hooks.md +0 -33
- package/skills/graphify/skills/codex/references/query.md +0 -249
- package/skills/graphify/skills/codex/references/transcribe.md +0 -48
- package/skills/graphify/skills/codex/references/update.md +0 -179
- package/skills/graphify/skills/copilot/references/add-watch.md +0 -56
- package/skills/graphify/skills/copilot/references/exports.md +0 -71
- package/skills/graphify/skills/copilot/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/copilot/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/copilot/references/hooks.md +0 -33
- package/skills/graphify/skills/copilot/references/query.md +0 -249
- package/skills/graphify/skills/copilot/references/transcribe.md +0 -48
- package/skills/graphify/skills/copilot/references/update.md +0 -179
- package/skills/graphify/skills/droid/references/add-watch.md +0 -56
- package/skills/graphify/skills/droid/references/exports.md +0 -71
- package/skills/graphify/skills/droid/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/droid/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/droid/references/hooks.md +0 -33
- package/skills/graphify/skills/droid/references/query.md +0 -249
- package/skills/graphify/skills/droid/references/transcribe.md +0 -48
- package/skills/graphify/skills/droid/references/update.md +0 -179
- package/skills/graphify/skills/kilo/references/add-watch.md +0 -56
- package/skills/graphify/skills/kilo/references/exports.md +0 -71
- package/skills/graphify/skills/kilo/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/kilo/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/kilo/references/hooks.md +0 -33
- package/skills/graphify/skills/kilo/references/query.md +0 -249
- package/skills/graphify/skills/kilo/references/transcribe.md +0 -48
- package/skills/graphify/skills/kilo/references/update.md +0 -179
- package/skills/graphify/skills/kiro/references/add-watch.md +0 -56
- package/skills/graphify/skills/kiro/references/exports.md +0 -71
- package/skills/graphify/skills/kiro/references/extraction-spec.md +0 -29
- package/skills/graphify/skills/kiro/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/kiro/references/hooks.md +0 -33
- package/skills/graphify/skills/kiro/references/query.md +0 -249
- package/skills/graphify/skills/kiro/references/transcribe.md +0 -48
- package/skills/graphify/skills/kiro/references/update.md +0 -179
- package/skills/graphify/skills/opencode/references/add-watch.md +0 -56
- package/skills/graphify/skills/opencode/references/exports.md +0 -71
- package/skills/graphify/skills/opencode/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/opencode/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/opencode/references/hooks.md +0 -33
- package/skills/graphify/skills/opencode/references/query.md +0 -249
- package/skills/graphify/skills/opencode/references/transcribe.md +0 -48
- package/skills/graphify/skills/opencode/references/update.md +0 -179
- package/skills/graphify/skills/pi/references/add-watch.md +0 -56
- package/skills/graphify/skills/pi/references/exports.md +0 -71
- package/skills/graphify/skills/pi/references/extraction-spec.md +0 -29
- package/skills/graphify/skills/pi/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/pi/references/hooks.md +0 -33
- package/skills/graphify/skills/pi/references/query.md +0 -249
- package/skills/graphify/skills/pi/references/transcribe.md +0 -48
- package/skills/graphify/skills/pi/references/update.md +0 -179
- package/skills/graphify/skills/trae/references/add-watch.md +0 -56
- package/skills/graphify/skills/trae/references/exports.md +0 -71
- package/skills/graphify/skills/trae/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/trae/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/trae/references/hooks.md +0 -35
- package/skills/graphify/skills/trae/references/query.md +0 -249
- package/skills/graphify/skills/trae/references/transcribe.md +0 -48
- package/skills/graphify/skills/trae/references/update.md +0 -179
- package/skills/graphify/skills/vscode/references/add-watch.md +0 -56
- package/skills/graphify/skills/vscode/references/exports.md +0 -71
- package/skills/graphify/skills/vscode/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/vscode/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/vscode/references/hooks.md +0 -33
- package/skills/graphify/skills/vscode/references/query.md +0 -249
- package/skills/graphify/skills/vscode/references/transcribe.md +0 -48
- package/skills/graphify/skills/vscode/references/update.md +0 -179
- package/skills/graphify/skills/windows/references/add-watch.md +0 -56
- package/skills/graphify/skills/windows/references/exports.md +0 -71
- package/skills/graphify/skills/windows/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/windows/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/windows/references/hooks.md +0 -33
- package/skills/graphify/skills/windows/references/query.md +0 -249
- package/skills/graphify/skills/windows/references/transcribe.md +0 -48
- package/skills/graphify/skills/windows/references/update.md +0 -179
- package/skills/graphify/symbol_resolution.py +0 -538
- package/skills/graphify/transcribe.py +0 -184
- package/skills/graphify/tree_html.py +0 -582
- package/skills/graphify/validate.py +0 -72
- package/skills/graphify/watch.py +0 -898
- package/skills/graphify/wiki.py +0 -282
package/skills/graphify/hooks.py
DELETED
|
@@ -1,457 +0,0 @@
|
|
|
1
|
-
# git hook integration - install/uninstall graphify post-commit and post-checkout hooks
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
import configparser
|
|
4
|
-
import re
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
_HOOK_MARKER = "# graphify-hook-start"
|
|
9
|
-
_HOOK_MARKER_END = "# graphify-hook-end"
|
|
10
|
-
_CHECKOUT_MARKER = "# graphify-checkout-hook-start"
|
|
11
|
-
_CHECKOUT_MARKER_END = "# graphify-checkout-hook-end"
|
|
12
|
-
|
|
13
|
-
# __PINNED_PYTHON__ is replaced at install time with the absolute path of the
|
|
14
|
-
# Python interpreter that ran `graphify hook install`. For uv-tool and pipx
|
|
15
|
-
# installs the interpreter lives inside an isolated venv, so the launcher on
|
|
16
|
-
# PATH is the only entry point — and GUI git clients / CI runners often have a
|
|
17
|
-
# minimal PATH that omits ~/.local/bin. Pinning sys.executable at install time
|
|
18
|
-
# makes the hook work regardless of PATH at git-trigger time.
|
|
19
|
-
_PYTHON_DETECT = """\
|
|
20
|
-
# Detect the correct Python interpreter (handles uv tool, pipx, venv, system installs).
|
|
21
|
-
# _PINNED was recorded at hook-install time; tried first so the hook works even
|
|
22
|
-
# when the graphify launcher is not on PATH (common in GUI clients and CI).
|
|
23
|
-
GRAPHIFY_PYTHON=""
|
|
24
|
-
_PINNED='__PINNED_PYTHON__'
|
|
25
|
-
if [ -n "$_PINNED" ] && [ -x "$_PINNED" ] && "$_PINNED" -c "import graphify" 2>/dev/null; then
|
|
26
|
-
GRAPHIFY_PYTHON="$_PINNED"
|
|
27
|
-
fi
|
|
28
|
-
# Second probe: read graphify-out/.graphify_python (written by the skill and
|
|
29
|
-
# CLI; survives uv-tool reinstalls and is the same source the README documents).
|
|
30
|
-
if [ -z "$GRAPHIFY_PYTHON" ]; then
|
|
31
|
-
_GFY_PYTHON_FILE="graphify-out/.graphify_python"
|
|
32
|
-
if [ -f "$_GFY_PYTHON_FILE" ]; then
|
|
33
|
-
_FROM_FILE=$(cat "$_GFY_PYTHON_FILE" 2>/dev/null | tr -d '[:space:]')
|
|
34
|
-
case "$_FROM_FILE" in
|
|
35
|
-
*[!a-zA-Z0-9/_.@:\\-]*) _FROM_FILE="" ;; # allowlist (covers Windows paths)
|
|
36
|
-
esac
|
|
37
|
-
if [ -n "$_FROM_FILE" ] && [ -x "$_FROM_FILE" ] && "$_FROM_FILE" -c "import graphify" 2>/dev/null; then
|
|
38
|
-
GRAPHIFY_PYTHON="$_FROM_FILE"
|
|
39
|
-
fi
|
|
40
|
-
fi
|
|
41
|
-
fi
|
|
42
|
-
# Third probe: resolve via the graphify launcher on PATH (shebang probe).
|
|
43
|
-
if [ -z "$GRAPHIFY_PYTHON" ]; then
|
|
44
|
-
GRAPHIFY_BIN=$(command -v graphify 2>/dev/null)
|
|
45
|
-
if [ -n "$GRAPHIFY_BIN" ]; then
|
|
46
|
-
case "$GRAPHIFY_BIN" in
|
|
47
|
-
*.exe) _SHEBANG="" ;;
|
|
48
|
-
*) _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') ;;
|
|
49
|
-
esac
|
|
50
|
-
case "$_SHEBANG" in
|
|
51
|
-
*/env\\ *) GRAPHIFY_PYTHON="${_SHEBANG#*/env }" ;;
|
|
52
|
-
*) GRAPHIFY_PYTHON="$_SHEBANG" ;;
|
|
53
|
-
esac
|
|
54
|
-
# Allowlist: only keep characters valid in a filesystem path to prevent
|
|
55
|
-
# injection if the shebang contains shell metacharacters.
|
|
56
|
-
case "$GRAPHIFY_PYTHON" in
|
|
57
|
-
*[!a-zA-Z0-9/_.@-]*) GRAPHIFY_PYTHON="" ;;
|
|
58
|
-
esac
|
|
59
|
-
if [ -n "$GRAPHIFY_PYTHON" ] && ! "$GRAPHIFY_PYTHON" -c "import graphify" 2>/dev/null; then
|
|
60
|
-
GRAPHIFY_PYTHON=""
|
|
61
|
-
fi
|
|
62
|
-
fi
|
|
63
|
-
fi
|
|
64
|
-
# Last resort: try python3 / python (works for system/venv installs on PATH).
|
|
65
|
-
if [ -z "$GRAPHIFY_PYTHON" ]; then
|
|
66
|
-
if command -v python3 >/dev/null 2>&1 && python3 -c "import graphify" 2>/dev/null; then
|
|
67
|
-
GRAPHIFY_PYTHON="python3"
|
|
68
|
-
elif command -v python >/dev/null 2>&1 && python -c "import graphify" 2>/dev/null; then
|
|
69
|
-
GRAPHIFY_PYTHON="python"
|
|
70
|
-
else
|
|
71
|
-
echo "[graphify hook] could not locate a Python with graphify installed. Add the graphify bin dir to PATH or re-run 'graphify hook install' from the env where graphify lives." >&2
|
|
72
|
-
exit 0
|
|
73
|
-
fi
|
|
74
|
-
fi
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
# The Python that the rebuild runs, shared by both hooks. Embedded verbatim into
|
|
78
|
-
# the launcher below and re-executed in the detached child. Must not contain the
|
|
79
|
-
# double-quote, $, backtick or backslash characters: it is carried inside a
|
|
80
|
-
# shell double-quoted `-c "..."` argument (see _detached_launch).
|
|
81
|
-
_REBUILD_BODY_COMMIT = """\
|
|
82
|
-
import os, signal, sys
|
|
83
|
-
from pathlib import Path
|
|
84
|
-
|
|
85
|
-
changed_raw = os.environ.get('GRAPHIFY_CHANGED', '')
|
|
86
|
-
changed = [Path(f.strip()) for f in changed_raw.strip().splitlines() if f.strip()]
|
|
87
|
-
|
|
88
|
-
if not changed:
|
|
89
|
-
sys.exit(0)
|
|
90
|
-
|
|
91
|
-
print(f'[graphify hook] {len(changed)} file(s) changed - rebuilding graph...')
|
|
92
|
-
|
|
93
|
-
try:
|
|
94
|
-
from graphify.watch import _rebuild_code, _apply_resource_limits
|
|
95
|
-
_apply_resource_limits()
|
|
96
|
-
_timeout = int(os.environ.get('GRAPHIFY_REBUILD_TIMEOUT', '600'))
|
|
97
|
-
if _timeout > 0 and hasattr(signal, 'SIGALRM'):
|
|
98
|
-
signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(TimeoutError(f'graphify rebuild exceeded {_timeout}s')))
|
|
99
|
-
signal.alarm(_timeout)
|
|
100
|
-
_force = os.environ.get('GRAPHIFY_FORCE', '').lower() in ('1', 'true', 'yes')
|
|
101
|
-
_root = Path('.')
|
|
102
|
-
_saved = Path('graphify-out/.graphify_root')
|
|
103
|
-
if _saved.exists():
|
|
104
|
-
_txt = _saved.read_text(encoding='utf-8').strip()
|
|
105
|
-
if _txt:
|
|
106
|
-
_root = Path(_txt)
|
|
107
|
-
_rebuild_code(_root, changed_paths=changed, force=_force)
|
|
108
|
-
except TimeoutError as exc:
|
|
109
|
-
print(f'[graphify hook] {exc}')
|
|
110
|
-
sys.exit(1)
|
|
111
|
-
except Exception as exc:
|
|
112
|
-
print(f'[graphify hook] Rebuild failed: {exc}')
|
|
113
|
-
sys.exit(1)
|
|
114
|
-
"""
|
|
115
|
-
|
|
116
|
-
_REBUILD_BODY_CHECKOUT = """\
|
|
117
|
-
from graphify.watch import _rebuild_code, _apply_resource_limits
|
|
118
|
-
from pathlib import Path
|
|
119
|
-
import os, signal, sys
|
|
120
|
-
try:
|
|
121
|
-
_apply_resource_limits()
|
|
122
|
-
_timeout = int(os.environ.get('GRAPHIFY_REBUILD_TIMEOUT', '600'))
|
|
123
|
-
if _timeout > 0 and hasattr(signal, 'SIGALRM'):
|
|
124
|
-
signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(TimeoutError(f'graphify rebuild exceeded {_timeout}s')))
|
|
125
|
-
signal.alarm(_timeout)
|
|
126
|
-
_force = os.environ.get('GRAPHIFY_FORCE', '').lower() in ('1', 'true', 'yes')
|
|
127
|
-
# post-checkout: branch switch can touch arbitrary files; full rebuild path
|
|
128
|
-
# (no changed_paths) is correct here. The flock inside _rebuild_code still
|
|
129
|
-
# prevents pile-ups when commit + checkout fire back-to-back.
|
|
130
|
-
_root = Path('.')
|
|
131
|
-
_saved = Path('graphify-out/.graphify_root')
|
|
132
|
-
if _saved.exists():
|
|
133
|
-
_txt = _saved.read_text(encoding='utf-8').strip()
|
|
134
|
-
if _txt:
|
|
135
|
-
_root = Path(_txt)
|
|
136
|
-
_rebuild_code(_root, force=_force)
|
|
137
|
-
except TimeoutError as exc:
|
|
138
|
-
print(f'[graphify] {exc}')
|
|
139
|
-
sys.exit(1)
|
|
140
|
-
except Exception as exc:
|
|
141
|
-
print(f'[graphify] Rebuild failed: {exc}')
|
|
142
|
-
sys.exit(1)
|
|
143
|
-
"""
|
|
144
|
-
|
|
145
|
-
# Cross-platform detached-launch shim (#1161). The hooks used to background the
|
|
146
|
-
# rebuild with `nohup "$GRAPHIFY_PYTHON" -c "..." &`, but Git for Windows' bundled
|
|
147
|
-
# MSYS shell ships no nohup (nor setsid), so that line died with
|
|
148
|
-
# 'nohup: command not found' and the rebuild silently never ran — git commit/pull
|
|
149
|
-
# still returned 0, so the graph just went stale with no signal. graphify already
|
|
150
|
-
# requires Python, so we let Python do the detaching: a tiny outer process spawns
|
|
151
|
-
# the real rebuild fully detached and returns immediately, so the hook never
|
|
152
|
-
# blocks. POSIX uses start_new_session (the setsid equivalent); Windows uses
|
|
153
|
-
# DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, breaking away from any job object
|
|
154
|
-
# when allowed. This payload is carried inside a shell double-quoted -c argument,
|
|
155
|
-
# so it deliberately uses only single-quoted Python strings (no ", $, ` or \\).
|
|
156
|
-
_LAUNCHER_TEMPLATE = """\
|
|
157
|
-
import os, subprocess, sys
|
|
158
|
-
_src = '''
|
|
159
|
-
__REBUILD_BODY__
|
|
160
|
-
'''
|
|
161
|
-
_log = os.environ.get('GRAPHIFY_REBUILD_LOG') or os.path.join(os.path.expanduser('~'), '.cache', 'graphify-rebuild.log')
|
|
162
|
-
try:
|
|
163
|
-
os.makedirs(os.path.dirname(_log), exist_ok=True)
|
|
164
|
-
_out = open(_log, 'a', buffering=1, encoding='utf-8', errors='replace')
|
|
165
|
-
except OSError:
|
|
166
|
-
_out = subprocess.DEVNULL
|
|
167
|
-
_kw = dict(stdout=_out, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, cwd=os.getcwd(), close_fds=True)
|
|
168
|
-
_cmd = [sys.executable, '-c', _src]
|
|
169
|
-
if os.name == 'nt':
|
|
170
|
-
_flags = 0x00000008 | 0x00000200 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
|
|
171
|
-
try:
|
|
172
|
-
subprocess.Popen(_cmd, creationflags=_flags | 0x01000000, **_kw) # + CREATE_BREAKAWAY_FROM_JOB
|
|
173
|
-
except OSError:
|
|
174
|
-
subprocess.Popen(_cmd, creationflags=_flags, **_kw)
|
|
175
|
-
else:
|
|
176
|
-
subprocess.Popen(_cmd, start_new_session=True, **_kw)
|
|
177
|
-
"""
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def _detached_launch(rebuild_body: str) -> str:
|
|
181
|
-
"""Return a POSIX-sh line that runs ``rebuild_body`` as a detached background
|
|
182
|
-
Python process via ``$GRAPHIFY_PYTHON``.
|
|
183
|
-
|
|
184
|
-
Replaces the old ``nohup ... &`` form, which failed on Git for Windows'
|
|
185
|
-
shell (no nohup/setsid) and let the rebuild silently never run (#1161).
|
|
186
|
-
The launcher writes the child's output to ``$GRAPHIFY_REBUILD_LOG`` and
|
|
187
|
-
returns the instant the child is spawned, so the git hook never blocks.
|
|
188
|
-
"""
|
|
189
|
-
launcher = _LAUNCHER_TEMPLATE.replace("__REBUILD_BODY__", rebuild_body)
|
|
190
|
-
return '"$GRAPHIFY_PYTHON" -c "' + launcher + '"\n'
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
_HOOK_SCRIPT = """\
|
|
194
|
-
# graphify-hook-start
|
|
195
|
-
# Auto-rebuilds the knowledge graph after each commit (code files only, no LLM needed).
|
|
196
|
-
# Installed by: graphify hook install
|
|
197
|
-
|
|
198
|
-
# Deterministic clustering: networkx louvain iterates string-keyed sets whose
|
|
199
|
-
# order is randomized per-process by PYTHONHASHSEED, so community assignments
|
|
200
|
-
# churn run-to-run. Pinning it makes graphify-out reproducible.
|
|
201
|
-
export PYTHONHASHSEED=0
|
|
202
|
-
|
|
203
|
-
# Skip during rebase/merge/cherry-pick to avoid blocking --continue with unstaged changes
|
|
204
|
-
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
|
205
|
-
[ -d "$GIT_DIR/rebase-merge" ] && exit 0
|
|
206
|
-
[ -d "$GIT_DIR/rebase-apply" ] && exit 0
|
|
207
|
-
[ -f "$GIT_DIR/MERGE_HEAD" ] && exit 0
|
|
208
|
-
[ -f "$GIT_DIR/CHERRY_PICK_HEAD" ] && exit 0
|
|
209
|
-
|
|
210
|
-
[ "${GRAPHIFY_SKIP_HOOK:-0}" = "1" ] && exit 0
|
|
211
|
-
|
|
212
|
-
CHANGED=$(git diff --name-only HEAD~1 HEAD 2>/dev/null || git diff --name-only HEAD 2>/dev/null)
|
|
213
|
-
if [ -z "$CHANGED" ]; then
|
|
214
|
-
exit 0
|
|
215
|
-
fi
|
|
216
|
-
|
|
217
|
-
# Skip when only graphify-out/ artifacts changed (avoids rebuild loop when graph outputs are tracked in git)
|
|
218
|
-
_NON_GRAPH=$(echo "$CHANGED" | grep -v '^graphify-out/' || true)
|
|
219
|
-
if [ -z "$_NON_GRAPH" ]; then
|
|
220
|
-
exit 0
|
|
221
|
-
fi
|
|
222
|
-
|
|
223
|
-
""" + _PYTHON_DETECT + """
|
|
224
|
-
export GRAPHIFY_CHANGED="$CHANGED"
|
|
225
|
-
|
|
226
|
-
# Run the rebuild detached so git commit returns immediately. Full-repo rebuilds
|
|
227
|
-
# can take hours; blocking the post-commit hook stalls the shell. The Python
|
|
228
|
-
# launcher below detaches the child cross-platform, so it works on Git for
|
|
229
|
-
# Windows' shell too (which lacks the coreutils backgrounding tools) (#1161).
|
|
230
|
-
_GRAPHIFY_LOG="${HOME}/.cache/graphify-rebuild.log"
|
|
231
|
-
mkdir -p "$(dirname "$_GRAPHIFY_LOG")"
|
|
232
|
-
export GRAPHIFY_REBUILD_LOG="$_GRAPHIFY_LOG"
|
|
233
|
-
echo "[graphify hook] launching background rebuild (log: $_GRAPHIFY_LOG)"
|
|
234
|
-
""" + _detached_launch(_REBUILD_BODY_COMMIT) + """# graphify-hook-end
|
|
235
|
-
"""
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
_CHECKOUT_SCRIPT = """\
|
|
239
|
-
# graphify-checkout-hook-start
|
|
240
|
-
# Auto-rebuilds the knowledge graph (code only) when switching branches.
|
|
241
|
-
# Installed by: graphify hook install
|
|
242
|
-
|
|
243
|
-
# Deterministic clustering: networkx louvain iterates string-keyed sets whose
|
|
244
|
-
# order is randomized per-process by PYTHONHASHSEED, so community assignments
|
|
245
|
-
# churn run-to-run. Pinning it makes graphify-out reproducible.
|
|
246
|
-
export PYTHONHASHSEED=0
|
|
247
|
-
|
|
248
|
-
PREV_HEAD=$1
|
|
249
|
-
NEW_HEAD=$2
|
|
250
|
-
BRANCH_SWITCH=$3
|
|
251
|
-
|
|
252
|
-
# Only run on branch switches, not file checkouts
|
|
253
|
-
if [ "$BRANCH_SWITCH" != "1" ]; then
|
|
254
|
-
exit 0
|
|
255
|
-
fi
|
|
256
|
-
|
|
257
|
-
# Only run if graphify-out/ exists (graph has been built before)
|
|
258
|
-
if [ ! -d "graphify-out" ]; then
|
|
259
|
-
exit 0
|
|
260
|
-
fi
|
|
261
|
-
|
|
262
|
-
# Skip during rebase/merge/cherry-pick
|
|
263
|
-
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null)
|
|
264
|
-
[ -d "$GIT_DIR/rebase-merge" ] && exit 0
|
|
265
|
-
[ -d "$GIT_DIR/rebase-apply" ] && exit 0
|
|
266
|
-
[ -f "$GIT_DIR/MERGE_HEAD" ] && exit 0
|
|
267
|
-
[ -f "$GIT_DIR/CHERRY_PICK_HEAD" ] && exit 0
|
|
268
|
-
|
|
269
|
-
""" + _PYTHON_DETECT + """
|
|
270
|
-
_GRAPHIFY_LOG="${HOME}/.cache/graphify-rebuild.log"
|
|
271
|
-
mkdir -p "$(dirname "$_GRAPHIFY_LOG")"
|
|
272
|
-
export GRAPHIFY_REBUILD_LOG="$_GRAPHIFY_LOG"
|
|
273
|
-
echo "[graphify] Branch switched - launching background rebuild (log: $_GRAPHIFY_LOG)"
|
|
274
|
-
""" + _detached_launch(_REBUILD_BODY_CHECKOUT) + """# graphify-checkout-hook-end
|
|
275
|
-
"""
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def _git_root(path: Path) -> Path | None:
|
|
279
|
-
"""Walk up to find .git directory."""
|
|
280
|
-
current = path.resolve()
|
|
281
|
-
for parent in [current, *current.parents]:
|
|
282
|
-
if (parent / ".git").exists():
|
|
283
|
-
return parent
|
|
284
|
-
return None
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def _hooks_dir(root: Path) -> Path:
|
|
288
|
-
"""Return the git hooks directory, respecting core.hooksPath if set (e.g. Husky)."""
|
|
289
|
-
try:
|
|
290
|
-
cfg = configparser.RawConfigParser()
|
|
291
|
-
cfg.read(root / ".git" / "config", encoding="utf-8")
|
|
292
|
-
# configparser lowercases option names; git's hooksPath becomes hookspath
|
|
293
|
-
custom = cfg.get("core", "hookspath", fallback="").strip()
|
|
294
|
-
if custom:
|
|
295
|
-
p = Path(custom).expanduser()
|
|
296
|
-
if not p.is_absolute():
|
|
297
|
-
p = root / p
|
|
298
|
-
# Validate the resolved path stays within the repository root
|
|
299
|
-
# to prevent supply-chain attacks via malicious core.hooksPath values
|
|
300
|
-
try:
|
|
301
|
-
p.resolve().relative_to(root.resolve())
|
|
302
|
-
except ValueError:
|
|
303
|
-
pass # Path escapes repo root; fall through to default .git/hooks
|
|
304
|
-
else:
|
|
305
|
-
p.mkdir(parents=True, exist_ok=True)
|
|
306
|
-
return p
|
|
307
|
-
except (configparser.Error, OSError) as exc:
|
|
308
|
-
# Narrow the exception (PR747-NEW-2): a bare `except Exception: pass`
|
|
309
|
-
# was hiding tampering signals (corrupt .git/config, permission flips
|
|
310
|
-
# by another tool). Surface them on stderr instead of silently
|
|
311
|
-
# falling through to the default hooks directory.
|
|
312
|
-
print(
|
|
313
|
-
f"[graphify hooks] could not read core.hooksPath from "
|
|
314
|
-
f"{root / '.git' / 'config'}: {exc}",
|
|
315
|
-
file=sys.stderr,
|
|
316
|
-
)
|
|
317
|
-
# In a linked worktree .git is a file not a directory, so constructing
|
|
318
|
-
# root/.git/hooks directly fails. Ask git for the real hooks path instead.
|
|
319
|
-
# NOTE: do NOT pass --path-format=absolute — added in git 2.31; older git
|
|
320
|
-
# echoes it back as a literal argument, contaminating stdout and causing a
|
|
321
|
-
# phantom directory to be created (#907). git -C <root> already returns an
|
|
322
|
-
# absolute path for worktree/external-gitdir cases, and a path relative to
|
|
323
|
-
# <root> for normal repos — anchoring on root covers both.
|
|
324
|
-
import subprocess as _sp
|
|
325
|
-
try:
|
|
326
|
-
res = _sp.run(
|
|
327
|
-
["git", "-C", str(root), "rev-parse", "--git-path", "hooks"],
|
|
328
|
-
capture_output=True, text=True,
|
|
329
|
-
)
|
|
330
|
-
raw = res.stdout.strip()
|
|
331
|
-
# A valid hooks path can never contain newlines or NUL. Their presence
|
|
332
|
-
# means git echoed an unrecognised flag back (old git behaviour).
|
|
333
|
-
if res.returncode == 0 and raw and not any(c in raw for c in ("\n", "\r", "\x00")):
|
|
334
|
-
d = (root / raw).resolve()
|
|
335
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
336
|
-
return d
|
|
337
|
-
except (OSError, FileNotFoundError):
|
|
338
|
-
pass
|
|
339
|
-
d = root / ".git" / "hooks"
|
|
340
|
-
d.mkdir(parents=True, exist_ok=True)
|
|
341
|
-
return d
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def _install_hook(hooks_dir: Path, name: str, script: str, marker: str) -> str:
|
|
345
|
-
"""Install a single git hook, appending if an existing hook is present."""
|
|
346
|
-
hook_path = hooks_dir / name
|
|
347
|
-
if hook_path.exists():
|
|
348
|
-
content = hook_path.read_text(encoding="utf-8")
|
|
349
|
-
if marker in content:
|
|
350
|
-
return f"already installed at {hook_path}"
|
|
351
|
-
hook_path.write_text(content.rstrip() + "\n\n" + script, encoding="utf-8", newline="\n")
|
|
352
|
-
return f"appended to existing {name} hook at {hook_path}"
|
|
353
|
-
hook_path.write_text("#!/bin/sh\n" + script, encoding="utf-8", newline="\n")
|
|
354
|
-
hook_path.chmod(0o755)
|
|
355
|
-
return f"installed at {hook_path}"
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
def _uninstall_hook(hooks_dir: Path, name: str, marker: str, marker_end: str) -> str:
|
|
359
|
-
"""Remove graphify section from a git hook using start/end markers."""
|
|
360
|
-
hook_path = hooks_dir / name
|
|
361
|
-
if not hook_path.exists():
|
|
362
|
-
return f"no {name} hook found - nothing to remove."
|
|
363
|
-
content = hook_path.read_text(encoding="utf-8")
|
|
364
|
-
if marker not in content:
|
|
365
|
-
return f"graphify hook not found in {name} - nothing to remove."
|
|
366
|
-
new_content = re.sub(
|
|
367
|
-
rf"{re.escape(marker)}.*?{re.escape(marker_end)}\n?",
|
|
368
|
-
"",
|
|
369
|
-
content,
|
|
370
|
-
flags=re.DOTALL,
|
|
371
|
-
).strip()
|
|
372
|
-
if not new_content or new_content in ("#!/bin/bash", "#!/bin/sh"):
|
|
373
|
-
hook_path.unlink()
|
|
374
|
-
return f"removed {name} hook at {hook_path}"
|
|
375
|
-
hook_path.write_text(new_content + "\n", encoding="utf-8", newline="\n")
|
|
376
|
-
return f"graphify removed from {name} at {hook_path} (other hook content preserved)"
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def _user_hooks_dir(hooks_dir: Path) -> Path:
|
|
380
|
-
"""Return the user-editable hooks directory.
|
|
381
|
-
|
|
382
|
-
Husky 9 sets core.hooksPath to .husky/_ (wrapper scripts auto-generated by
|
|
383
|
-
Husky), while user-editable hooks live in the parent .husky/. Return the
|
|
384
|
-
parent when the resolved dir ends in '_' so install/status/uninstall target
|
|
385
|
-
the correct location (#987).
|
|
386
|
-
"""
|
|
387
|
-
if hooks_dir.name == "_":
|
|
388
|
-
return hooks_dir.parent
|
|
389
|
-
return hooks_dir
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
def install(path: Path = Path(".")) -> str:
|
|
393
|
-
"""Install graphify post-commit and post-checkout hooks in the nearest git repo."""
|
|
394
|
-
root = _git_root(path)
|
|
395
|
-
if root is None:
|
|
396
|
-
raise RuntimeError(f"No git repository found at or above {path.resolve()}")
|
|
397
|
-
|
|
398
|
-
hooks_dir = _user_hooks_dir(_hooks_dir(root))
|
|
399
|
-
|
|
400
|
-
# Pin the current interpreter so the hook works even when the graphify
|
|
401
|
-
# launcher is not on PATH at git-trigger time (uv tool / pipx isolation).
|
|
402
|
-
# sys.executable is the Python running this very install command, so it is
|
|
403
|
-
# always the correct isolated-venv interpreter. The placeholder is replaced
|
|
404
|
-
# in both scripts before writing; the allowlist in _PYTHON_DETECT strips any
|
|
405
|
-
# characters unsafe in a shell path, and import-verification catches a stale
|
|
406
|
-
# pinned path so it safely falls through to the dynamic detection.
|
|
407
|
-
# Apply the same allowlist used in _PYTHON_DETECT for all other probes.
|
|
408
|
-
# This rejects any character that is not a valid plain filesystem path
|
|
409
|
-
# character, preventing $(...), backtick, double-quote, semicolon, etc.
|
|
410
|
-
# from being injected into the generated shell scripts. The allowlist
|
|
411
|
-
# includes ':' and '\' so Windows paths (C:\...) are accepted.
|
|
412
|
-
import re as _re
|
|
413
|
-
_safe = sys.executable
|
|
414
|
-
if _re.search(r"[^a-zA-Z0-9/_.@:\\-]", _safe):
|
|
415
|
-
# Path contains characters outside the allowlist (spaces, quotes, etc.).
|
|
416
|
-
# Embed an empty string so the pinned probe is skipped and the hook
|
|
417
|
-
# falls through to the dynamic detection — safe degradation.
|
|
418
|
-
_safe = ""
|
|
419
|
-
pinned = _safe
|
|
420
|
-
hook = _HOOK_SCRIPT.replace("__PINNED_PYTHON__", pinned)
|
|
421
|
-
checkout = _CHECKOUT_SCRIPT.replace("__PINNED_PYTHON__", pinned)
|
|
422
|
-
|
|
423
|
-
commit_msg = _install_hook(hooks_dir, "post-commit", hook, _HOOK_MARKER)
|
|
424
|
-
checkout_msg = _install_hook(hooks_dir, "post-checkout", checkout, _CHECKOUT_MARKER)
|
|
425
|
-
|
|
426
|
-
return f"post-commit: {commit_msg}\npost-checkout: {checkout_msg}"
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
def uninstall(path: Path = Path(".")) -> str:
|
|
430
|
-
"""Remove graphify post-commit and post-checkout hooks."""
|
|
431
|
-
root = _git_root(path)
|
|
432
|
-
if root is None:
|
|
433
|
-
raise RuntimeError(f"No git repository found at or above {path.resolve()}")
|
|
434
|
-
|
|
435
|
-
hooks_dir = _user_hooks_dir(_hooks_dir(root))
|
|
436
|
-
commit_msg = _uninstall_hook(hooks_dir, "post-commit", _HOOK_MARKER, _HOOK_MARKER_END)
|
|
437
|
-
checkout_msg = _uninstall_hook(hooks_dir, "post-checkout", _CHECKOUT_MARKER, _CHECKOUT_MARKER_END)
|
|
438
|
-
|
|
439
|
-
return f"post-commit: {commit_msg}\npost-checkout: {checkout_msg}"
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
def status(path: Path = Path(".")) -> str:
|
|
443
|
-
"""Check if graphify hooks are installed."""
|
|
444
|
-
root = _git_root(path)
|
|
445
|
-
if root is None:
|
|
446
|
-
return "Not in a git repository."
|
|
447
|
-
hooks_dir = _user_hooks_dir(_hooks_dir(root))
|
|
448
|
-
|
|
449
|
-
def _check(name: str, marker: str) -> str:
|
|
450
|
-
p = hooks_dir / name
|
|
451
|
-
if not p.exists():
|
|
452
|
-
return "not installed"
|
|
453
|
-
return "installed" if marker in p.read_text(encoding="utf-8") else "not installed (hook exists but graphify not found)"
|
|
454
|
-
|
|
455
|
-
commit = _check("post-commit", _HOOK_MARKER)
|
|
456
|
-
checkout = _check("post-checkout", _CHECKOUT_MARKER)
|
|
457
|
-
return f"post-commit: {commit}\npost-checkout: {checkout}"
|