@oriro/orirocli 0.1.7 → 0.1.9

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.
Files changed (351) hide show
  1. package/ATTRIBUTION.md +8 -0
  2. package/LICENSE +21 -0
  3. package/dist/cli.js +35 -5
  4. package/package.json +1 -1
  5. package/skills/21stdev/SKILL.md +64 -0
  6. package/skills/graphify/SKILL.md +619 -0
  7. package/skills/graphify/__init__.py +28 -0
  8. package/skills/graphify/__main__.py +4582 -0
  9. package/skills/graphify/affected.py +154 -0
  10. package/skills/graphify/always_on/agents-md.md +12 -0
  11. package/skills/graphify/always_on/antigravity-rules.md +14 -0
  12. package/skills/graphify/always_on/claude-md.md +9 -0
  13. package/skills/graphify/always_on/gemini-md.md +9 -0
  14. package/skills/graphify/always_on/kiro-steering.md +5 -0
  15. package/skills/graphify/always_on/vscode-instructions.md +17 -0
  16. package/skills/graphify/analyze.py +724 -0
  17. package/skills/graphify/benchmark.py +155 -0
  18. package/skills/graphify/build.py +487 -0
  19. package/skills/graphify/cache.py +417 -0
  20. package/skills/graphify/callflow_html.py +2020 -0
  21. package/skills/graphify/cluster.py +272 -0
  22. package/skills/graphify/command-kilo.md +15 -0
  23. package/skills/graphify/dedup.py +429 -0
  24. package/skills/graphify/detect.py +1379 -0
  25. package/skills/graphify/diagnostics.py +390 -0
  26. package/skills/graphify/export.py +1408 -0
  27. package/skills/graphify/extract.py +11570 -0
  28. package/skills/graphify/global_graph.py +159 -0
  29. package/skills/graphify/google_workspace.py +223 -0
  30. package/skills/graphify/hooks.py +457 -0
  31. package/skills/graphify/ingest.py +331 -0
  32. package/skills/graphify/llm.py +1896 -0
  33. package/skills/graphify/manifest.py +4 -0
  34. package/skills/graphify/mcp_ingest.py +392 -0
  35. package/skills/graphify/multigraph_compat.py +212 -0
  36. package/skills/graphify/pg_introspect.py +142 -0
  37. package/skills/graphify/prs.py +748 -0
  38. package/skills/graphify/querylog.py +70 -0
  39. package/skills/graphify/report.py +218 -0
  40. package/skills/graphify/scip_ingest.py +363 -0
  41. package/skills/graphify/security.py +336 -0
  42. package/skills/graphify/semantic_cleanup.py +319 -0
  43. package/skills/graphify/serve.py +1309 -0
  44. package/skills/graphify/skill-aider.md +1246 -0
  45. package/skills/graphify/skill-amp.md +613 -0
  46. package/skills/graphify/skill-claw.md +616 -0
  47. package/skills/graphify/skill-codex.md +613 -0
  48. package/skills/graphify/skill-copilot.md +616 -0
  49. package/skills/graphify/skill-devin.md +1372 -0
  50. package/skills/graphify/skill-droid.md +613 -0
  51. package/skills/graphify/skill-kilo.md +625 -0
  52. package/skills/graphify/skill-kiro.md +615 -0
  53. package/skills/graphify/skill-opencode.md +608 -0
  54. package/skills/graphify/skill-pi.md +615 -0
  55. package/skills/graphify/skill-trae.md +614 -0
  56. package/skills/graphify/skill-vscode.md +612 -0
  57. package/skills/graphify/skill-windows.md +651 -0
  58. package/skills/graphify/skills/amp/references/add-watch.md +56 -0
  59. package/skills/graphify/skills/amp/references/exports.md +71 -0
  60. package/skills/graphify/skills/amp/references/extraction-spec.md +68 -0
  61. package/skills/graphify/skills/amp/references/github-and-merge.md +46 -0
  62. package/skills/graphify/skills/amp/references/hooks.md +33 -0
  63. package/skills/graphify/skills/amp/references/query.md +249 -0
  64. package/skills/graphify/skills/amp/references/transcribe.md +48 -0
  65. package/skills/graphify/skills/amp/references/update.md +179 -0
  66. package/skills/graphify/skills/claude/references/add-watch.md +56 -0
  67. package/skills/graphify/skills/claude/references/exports.md +71 -0
  68. package/skills/graphify/skills/claude/references/extraction-spec.md +68 -0
  69. package/skills/graphify/skills/claude/references/github-and-merge.md +46 -0
  70. package/skills/graphify/skills/claude/references/hooks.md +33 -0
  71. package/skills/graphify/skills/claude/references/query.md +103 -0
  72. package/skills/graphify/skills/claude/references/transcribe.md +48 -0
  73. package/skills/graphify/skills/claude/references/update.md +179 -0
  74. package/skills/graphify/skills/claw/references/add-watch.md +56 -0
  75. package/skills/graphify/skills/claw/references/exports.md +71 -0
  76. package/skills/graphify/skills/claw/references/extraction-spec.md +29 -0
  77. package/skills/graphify/skills/claw/references/github-and-merge.md +46 -0
  78. package/skills/graphify/skills/claw/references/hooks.md +33 -0
  79. package/skills/graphify/skills/claw/references/query.md +249 -0
  80. package/skills/graphify/skills/claw/references/transcribe.md +48 -0
  81. package/skills/graphify/skills/claw/references/update.md +179 -0
  82. package/skills/graphify/skills/codex/references/add-watch.md +56 -0
  83. package/skills/graphify/skills/codex/references/exports.md +71 -0
  84. package/skills/graphify/skills/codex/references/extraction-spec.md +29 -0
  85. package/skills/graphify/skills/codex/references/github-and-merge.md +46 -0
  86. package/skills/graphify/skills/codex/references/hooks.md +33 -0
  87. package/skills/graphify/skills/codex/references/query.md +249 -0
  88. package/skills/graphify/skills/codex/references/transcribe.md +48 -0
  89. package/skills/graphify/skills/codex/references/update.md +179 -0
  90. package/skills/graphify/skills/copilot/references/add-watch.md +56 -0
  91. package/skills/graphify/skills/copilot/references/exports.md +71 -0
  92. package/skills/graphify/skills/copilot/references/extraction-spec.md +68 -0
  93. package/skills/graphify/skills/copilot/references/github-and-merge.md +46 -0
  94. package/skills/graphify/skills/copilot/references/hooks.md +33 -0
  95. package/skills/graphify/skills/copilot/references/query.md +249 -0
  96. package/skills/graphify/skills/copilot/references/transcribe.md +48 -0
  97. package/skills/graphify/skills/copilot/references/update.md +179 -0
  98. package/skills/graphify/skills/droid/references/add-watch.md +56 -0
  99. package/skills/graphify/skills/droid/references/exports.md +71 -0
  100. package/skills/graphify/skills/droid/references/extraction-spec.md +68 -0
  101. package/skills/graphify/skills/droid/references/github-and-merge.md +46 -0
  102. package/skills/graphify/skills/droid/references/hooks.md +33 -0
  103. package/skills/graphify/skills/droid/references/query.md +249 -0
  104. package/skills/graphify/skills/droid/references/transcribe.md +48 -0
  105. package/skills/graphify/skills/droid/references/update.md +179 -0
  106. package/skills/graphify/skills/kilo/references/add-watch.md +56 -0
  107. package/skills/graphify/skills/kilo/references/exports.md +71 -0
  108. package/skills/graphify/skills/kilo/references/extraction-spec.md +68 -0
  109. package/skills/graphify/skills/kilo/references/github-and-merge.md +46 -0
  110. package/skills/graphify/skills/kilo/references/hooks.md +33 -0
  111. package/skills/graphify/skills/kilo/references/query.md +249 -0
  112. package/skills/graphify/skills/kilo/references/transcribe.md +48 -0
  113. package/skills/graphify/skills/kilo/references/update.md +179 -0
  114. package/skills/graphify/skills/kiro/references/add-watch.md +56 -0
  115. package/skills/graphify/skills/kiro/references/exports.md +71 -0
  116. package/skills/graphify/skills/kiro/references/extraction-spec.md +29 -0
  117. package/skills/graphify/skills/kiro/references/github-and-merge.md +46 -0
  118. package/skills/graphify/skills/kiro/references/hooks.md +33 -0
  119. package/skills/graphify/skills/kiro/references/query.md +249 -0
  120. package/skills/graphify/skills/kiro/references/transcribe.md +48 -0
  121. package/skills/graphify/skills/kiro/references/update.md +179 -0
  122. package/skills/graphify/skills/opencode/references/add-watch.md +56 -0
  123. package/skills/graphify/skills/opencode/references/exports.md +71 -0
  124. package/skills/graphify/skills/opencode/references/extraction-spec.md +68 -0
  125. package/skills/graphify/skills/opencode/references/github-and-merge.md +46 -0
  126. package/skills/graphify/skills/opencode/references/hooks.md +33 -0
  127. package/skills/graphify/skills/opencode/references/query.md +249 -0
  128. package/skills/graphify/skills/opencode/references/transcribe.md +48 -0
  129. package/skills/graphify/skills/opencode/references/update.md +179 -0
  130. package/skills/graphify/skills/pi/references/add-watch.md +56 -0
  131. package/skills/graphify/skills/pi/references/exports.md +71 -0
  132. package/skills/graphify/skills/pi/references/extraction-spec.md +29 -0
  133. package/skills/graphify/skills/pi/references/github-and-merge.md +46 -0
  134. package/skills/graphify/skills/pi/references/hooks.md +33 -0
  135. package/skills/graphify/skills/pi/references/query.md +249 -0
  136. package/skills/graphify/skills/pi/references/transcribe.md +48 -0
  137. package/skills/graphify/skills/pi/references/update.md +179 -0
  138. package/skills/graphify/skills/trae/references/add-watch.md +56 -0
  139. package/skills/graphify/skills/trae/references/exports.md +71 -0
  140. package/skills/graphify/skills/trae/references/extraction-spec.md +68 -0
  141. package/skills/graphify/skills/trae/references/github-and-merge.md +46 -0
  142. package/skills/graphify/skills/trae/references/hooks.md +35 -0
  143. package/skills/graphify/skills/trae/references/query.md +249 -0
  144. package/skills/graphify/skills/trae/references/transcribe.md +48 -0
  145. package/skills/graphify/skills/trae/references/update.md +179 -0
  146. package/skills/graphify/skills/vscode/references/add-watch.md +56 -0
  147. package/skills/graphify/skills/vscode/references/exports.md +71 -0
  148. package/skills/graphify/skills/vscode/references/extraction-spec.md +68 -0
  149. package/skills/graphify/skills/vscode/references/github-and-merge.md +46 -0
  150. package/skills/graphify/skills/vscode/references/hooks.md +33 -0
  151. package/skills/graphify/skills/vscode/references/query.md +249 -0
  152. package/skills/graphify/skills/vscode/references/transcribe.md +48 -0
  153. package/skills/graphify/skills/vscode/references/update.md +179 -0
  154. package/skills/graphify/skills/windows/references/add-watch.md +56 -0
  155. package/skills/graphify/skills/windows/references/exports.md +71 -0
  156. package/skills/graphify/skills/windows/references/extraction-spec.md +68 -0
  157. package/skills/graphify/skills/windows/references/github-and-merge.md +46 -0
  158. package/skills/graphify/skills/windows/references/hooks.md +33 -0
  159. package/skills/graphify/skills/windows/references/query.md +249 -0
  160. package/skills/graphify/skills/windows/references/transcribe.md +48 -0
  161. package/skills/graphify/skills/windows/references/update.md +179 -0
  162. package/skills/graphify/symbol_resolution.py +538 -0
  163. package/skills/graphify/transcribe.py +184 -0
  164. package/skills/graphify/tree_html.py +582 -0
  165. package/skills/graphify/validate.py +72 -0
  166. package/skills/graphify/watch.py +898 -0
  167. package/skills/graphify/wiki.py +282 -0
  168. package/skills/impeccable/SKILL.md +186 -0
  169. package/skills/impeccable/agents/impeccable_asset_producer.toml +92 -0
  170. package/skills/impeccable/agents/impeccable_manual_edit_applier.toml +95 -0
  171. package/skills/impeccable/agents/openai.yaml +4 -0
  172. package/skills/impeccable/reference/adapt.md +311 -0
  173. package/skills/impeccable/reference/animate.md +201 -0
  174. package/skills/impeccable/reference/audit.md +133 -0
  175. package/skills/impeccable/reference/bolder.md +113 -0
  176. package/skills/impeccable/reference/brand.md +108 -0
  177. package/skills/impeccable/reference/clarify.md +288 -0
  178. package/skills/impeccable/reference/codex.md +105 -0
  179. package/skills/impeccable/reference/colorize.md +257 -0
  180. package/skills/impeccable/reference/craft.md +123 -0
  181. package/skills/impeccable/reference/critique.md +790 -0
  182. package/skills/impeccable/reference/delight.md +302 -0
  183. package/skills/impeccable/reference/distill.md +111 -0
  184. package/skills/impeccable/reference/document.md +429 -0
  185. package/skills/impeccable/reference/extract.md +69 -0
  186. package/skills/impeccable/reference/harden.md +347 -0
  187. package/skills/impeccable/reference/init.md +172 -0
  188. package/skills/impeccable/reference/interaction-design.md +189 -0
  189. package/skills/impeccable/reference/layout.md +161 -0
  190. package/skills/impeccable/reference/live.md +720 -0
  191. package/skills/impeccable/reference/onboard.md +234 -0
  192. package/skills/impeccable/reference/optimize.md +258 -0
  193. package/skills/impeccable/reference/overdrive.md +130 -0
  194. package/skills/impeccable/reference/polish.md +241 -0
  195. package/skills/impeccable/reference/product.md +60 -0
  196. package/skills/impeccable/reference/quieter.md +99 -0
  197. package/skills/impeccable/reference/shape.md +165 -0
  198. package/skills/impeccable/reference/typeset.md +279 -0
  199. package/skills/impeccable/scripts/cleanup-deprecated.mjs +284 -0
  200. package/skills/impeccable/scripts/command-metadata.json +94 -0
  201. package/skills/impeccable/scripts/context-signals.mjs +225 -0
  202. package/skills/impeccable/scripts/context.mjs +266 -0
  203. package/skills/impeccable/scripts/critique-storage.mjs +242 -0
  204. package/skills/impeccable/scripts/design-parser.mjs +835 -0
  205. package/skills/impeccable/scripts/detect-csp.mjs +198 -0
  206. package/skills/impeccable/scripts/detect.mjs +21 -0
  207. package/skills/impeccable/scripts/detector/browser/injected/index.mjs +1733 -0
  208. package/skills/impeccable/scripts/detector/cli/main.mjs +244 -0
  209. package/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +4618 -0
  210. package/skills/impeccable/scripts/detector/detect-antipatterns.mjs +43 -0
  211. package/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +252 -0
  212. package/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +535 -0
  213. package/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +986 -0
  214. package/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +208 -0
  215. package/skills/impeccable/scripts/detector/engines/visual/screenshot-contrast.mjs +189 -0
  216. package/skills/impeccable/scripts/detector/findings.mjs +12 -0
  217. package/skills/impeccable/scripts/detector/node/file-system.mjs +198 -0
  218. package/skills/impeccable/scripts/detector/profile/profiler.mjs +166 -0
  219. package/skills/impeccable/scripts/detector/registry/antipatterns.mjs +419 -0
  220. package/skills/impeccable/scripts/detector/rules/checks.mjs +2384 -0
  221. package/skills/impeccable/scripts/detector/shared/color.mjs +124 -0
  222. package/skills/impeccable/scripts/detector/shared/constants.mjs +101 -0
  223. package/skills/impeccable/scripts/detector/shared/page.mjs +7 -0
  224. package/skills/impeccable/scripts/impeccable-paths.mjs +126 -0
  225. package/skills/impeccable/scripts/is-generated.mjs +69 -0
  226. package/skills/impeccable/scripts/live-accept.mjs +812 -0
  227. package/skills/impeccable/scripts/live-browser-session.js +123 -0
  228. package/skills/impeccable/scripts/live-browser.js +10295 -0
  229. package/skills/impeccable/scripts/live-commit-manual-edits.mjs +1241 -0
  230. package/skills/impeccable/scripts/live-complete.mjs +75 -0
  231. package/skills/impeccable/scripts/live-completion.mjs +19 -0
  232. package/skills/impeccable/scripts/live-copy-edit-agent.mjs +683 -0
  233. package/skills/impeccable/scripts/live-discard-manual-edits.mjs +51 -0
  234. package/skills/impeccable/scripts/live-event-validation.mjs +137 -0
  235. package/skills/impeccable/scripts/live-inject.mjs +557 -0
  236. package/skills/impeccable/scripts/live-insert-ui.mjs +458 -0
  237. package/skills/impeccable/scripts/live-insert.mjs +272 -0
  238. package/skills/impeccable/scripts/live-manual-edit-evidence.mjs +363 -0
  239. package/skills/impeccable/scripts/live-manual-edits-buffer.mjs +152 -0
  240. package/skills/impeccable/scripts/live-poll.mjs +379 -0
  241. package/skills/impeccable/scripts/live-resume.mjs +94 -0
  242. package/skills/impeccable/scripts/live-server.mjs +2326 -0
  243. package/skills/impeccable/scripts/live-session-store.mjs +289 -0
  244. package/skills/impeccable/scripts/live-status.mjs +61 -0
  245. package/skills/impeccable/scripts/live-svelte-component.mjs +826 -0
  246. package/skills/impeccable/scripts/live-sveltekit-adapter.mjs +274 -0
  247. package/skills/impeccable/scripts/live-ui-core.mjs +179 -0
  248. package/skills/impeccable/scripts/live-vocabulary.mjs +36 -0
  249. package/skills/impeccable/scripts/live-wrap.mjs +894 -0
  250. package/skills/impeccable/scripts/live.mjs +246 -0
  251. package/skills/impeccable/scripts/modern-screenshot.umd.js +14 -0
  252. package/skills/impeccable/scripts/palette.mjs +633 -0
  253. package/skills/impeccable/scripts/pin.mjs +214 -0
  254. package/skills/uipm-ui-styling/LICENSE.txt +202 -0
  255. package/skills/uipm-ui-styling/SKILL.md +328 -0
  256. package/skills/uipm-ui-styling/canvas-fonts/ArsenalSC-OFL.txt +93 -0
  257. package/skills/uipm-ui-styling/canvas-fonts/ArsenalSC-Regular.ttf +0 -0
  258. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-Bold.ttf +0 -0
  259. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-OFL.txt +93 -0
  260. package/skills/uipm-ui-styling/canvas-fonts/BigShoulders-Regular.ttf +0 -0
  261. package/skills/uipm-ui-styling/canvas-fonts/Boldonse-OFL.txt +93 -0
  262. package/skills/uipm-ui-styling/canvas-fonts/Boldonse-Regular.ttf +0 -0
  263. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-Bold.ttf +0 -0
  264. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-OFL.txt +93 -0
  265. package/skills/uipm-ui-styling/canvas-fonts/BricolageGrotesque-Regular.ttf +0 -0
  266. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Bold.ttf +0 -0
  267. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Italic.ttf +0 -0
  268. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-OFL.txt +93 -0
  269. package/skills/uipm-ui-styling/canvas-fonts/CrimsonPro-Regular.ttf +0 -0
  270. package/skills/uipm-ui-styling/canvas-fonts/DMMono-OFL.txt +93 -0
  271. package/skills/uipm-ui-styling/canvas-fonts/DMMono-Regular.ttf +0 -0
  272. package/skills/uipm-ui-styling/canvas-fonts/EricaOne-OFL.txt +94 -0
  273. package/skills/uipm-ui-styling/canvas-fonts/EricaOne-Regular.ttf +0 -0
  274. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-Bold.ttf +0 -0
  275. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-OFL.txt +93 -0
  276. package/skills/uipm-ui-styling/canvas-fonts/GeistMono-Regular.ttf +0 -0
  277. package/skills/uipm-ui-styling/canvas-fonts/Gloock-OFL.txt +93 -0
  278. package/skills/uipm-ui-styling/canvas-fonts/Gloock-Regular.ttf +0 -0
  279. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-Bold.ttf +0 -0
  280. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-OFL.txt +93 -0
  281. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexMono-Regular.ttf +0 -0
  282. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Bold.ttf +0 -0
  283. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-BoldItalic.ttf +0 -0
  284. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Italic.ttf +0 -0
  285. package/skills/uipm-ui-styling/canvas-fonts/IBMPlexSerif-Regular.ttf +0 -0
  286. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Bold.ttf +0 -0
  287. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-BoldItalic.ttf +0 -0
  288. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Italic.ttf +0 -0
  289. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-OFL.txt +93 -0
  290. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSans-Regular.ttf +0 -0
  291. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSerif-Italic.ttf +0 -0
  292. package/skills/uipm-ui-styling/canvas-fonts/InstrumentSerif-Regular.ttf +0 -0
  293. package/skills/uipm-ui-styling/canvas-fonts/Italiana-OFL.txt +93 -0
  294. package/skills/uipm-ui-styling/canvas-fonts/Italiana-Regular.ttf +0 -0
  295. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-Bold.ttf +0 -0
  296. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-OFL.txt +93 -0
  297. package/skills/uipm-ui-styling/canvas-fonts/JetBrainsMono-Regular.ttf +0 -0
  298. package/skills/uipm-ui-styling/canvas-fonts/Jura-Light.ttf +0 -0
  299. package/skills/uipm-ui-styling/canvas-fonts/Jura-Medium.ttf +0 -0
  300. package/skills/uipm-ui-styling/canvas-fonts/Jura-OFL.txt +93 -0
  301. package/skills/uipm-ui-styling/canvas-fonts/LibreBaskerville-OFL.txt +93 -0
  302. package/skills/uipm-ui-styling/canvas-fonts/LibreBaskerville-Regular.ttf +0 -0
  303. package/skills/uipm-ui-styling/canvas-fonts/Lora-Bold.ttf +0 -0
  304. package/skills/uipm-ui-styling/canvas-fonts/Lora-BoldItalic.ttf +0 -0
  305. package/skills/uipm-ui-styling/canvas-fonts/Lora-Italic.ttf +0 -0
  306. package/skills/uipm-ui-styling/canvas-fonts/Lora-OFL.txt +93 -0
  307. package/skills/uipm-ui-styling/canvas-fonts/Lora-Regular.ttf +0 -0
  308. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-Bold.ttf +0 -0
  309. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-OFL.txt +93 -0
  310. package/skills/uipm-ui-styling/canvas-fonts/NationalPark-Regular.ttf +0 -0
  311. package/skills/uipm-ui-styling/canvas-fonts/NothingYouCouldDo-OFL.txt +93 -0
  312. package/skills/uipm-ui-styling/canvas-fonts/NothingYouCouldDo-Regular.ttf +0 -0
  313. package/skills/uipm-ui-styling/canvas-fonts/Outfit-Bold.ttf +0 -0
  314. package/skills/uipm-ui-styling/canvas-fonts/Outfit-OFL.txt +93 -0
  315. package/skills/uipm-ui-styling/canvas-fonts/Outfit-Regular.ttf +0 -0
  316. package/skills/uipm-ui-styling/canvas-fonts/PixelifySans-Medium.ttf +0 -0
  317. package/skills/uipm-ui-styling/canvas-fonts/PixelifySans-OFL.txt +93 -0
  318. package/skills/uipm-ui-styling/canvas-fonts/PoiretOne-OFL.txt +93 -0
  319. package/skills/uipm-ui-styling/canvas-fonts/PoiretOne-Regular.ttf +0 -0
  320. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-Bold.ttf +0 -0
  321. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-OFL.txt +93 -0
  322. package/skills/uipm-ui-styling/canvas-fonts/RedHatMono-Regular.ttf +0 -0
  323. package/skills/uipm-ui-styling/canvas-fonts/Silkscreen-OFL.txt +93 -0
  324. package/skills/uipm-ui-styling/canvas-fonts/Silkscreen-Regular.ttf +0 -0
  325. package/skills/uipm-ui-styling/canvas-fonts/SmoochSans-Medium.ttf +0 -0
  326. package/skills/uipm-ui-styling/canvas-fonts/SmoochSans-OFL.txt +93 -0
  327. package/skills/uipm-ui-styling/canvas-fonts/Tektur-Medium.ttf +0 -0
  328. package/skills/uipm-ui-styling/canvas-fonts/Tektur-OFL.txt +93 -0
  329. package/skills/uipm-ui-styling/canvas-fonts/Tektur-Regular.ttf +0 -0
  330. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Bold.ttf +0 -0
  331. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-BoldItalic.ttf +0 -0
  332. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Italic.ttf +0 -0
  333. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-OFL.txt +93 -0
  334. package/skills/uipm-ui-styling/canvas-fonts/WorkSans-Regular.ttf +0 -0
  335. package/skills/uipm-ui-styling/canvas-fonts/YoungSerif-OFL.txt +93 -0
  336. package/skills/uipm-ui-styling/canvas-fonts/YoungSerif-Regular.ttf +0 -0
  337. package/skills/uipm-ui-styling/references/canvas-design-system.md +320 -0
  338. package/skills/uipm-ui-styling/references/shadcn-accessibility.md +471 -0
  339. package/skills/uipm-ui-styling/references/shadcn-components.md +424 -0
  340. package/skills/uipm-ui-styling/references/shadcn-theming.md +373 -0
  341. package/skills/uipm-ui-styling/references/tailwind-customization.md +483 -0
  342. package/skills/uipm-ui-styling/references/tailwind-responsive.md +382 -0
  343. package/skills/uipm-ui-styling/references/tailwind-utilities.md +455 -0
  344. package/skills/uipm-ui-styling/scripts/.coverage +0 -0
  345. package/skills/uipm-ui-styling/scripts/requirements.txt +17 -0
  346. package/skills/uipm-ui-styling/scripts/shadcn_add.py +292 -0
  347. package/skills/uipm-ui-styling/scripts/tailwind_config_gen.py +456 -0
  348. package/skills/uipm-ui-styling/scripts/tests/coverage-ui.json +1 -0
  349. package/skills/uipm-ui-styling/scripts/tests/requirements.txt +3 -0
  350. package/skills/uipm-ui-styling/scripts/tests/test_shadcn_add.py +266 -0
  351. package/skills/uipm-ui-styling/scripts/tests/test_tailwind_config_gen.py +336 -0
@@ -0,0 +1,457 @@
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}"