javi-forge 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/README.md +45 -1
  2. package/ci-local/README.md +56 -11
  3. package/ci-local/ci-local.ps1 +445 -0
  4. package/ci-local/ci-local.sh +52 -9
  5. package/ci-local/docker/python.Dockerfile +7 -0
  6. package/ci-local/hooks/commit-msg +120 -44
  7. package/ci-local/hooks/commit-msg.test.sh +286 -0
  8. package/ci-local/hooks/pre-commit +9 -1
  9. package/ci-local/hooks/pre-push +13 -2
  10. package/ci-local/install.ps1 +367 -0
  11. package/ci-local/install.sh +151 -5
  12. package/ci-local/uninstall.ps1 +85 -0
  13. package/ci-local/uninstall.sh +95 -0
  14. package/dist/cli/dispatch/ci.d.ts +12 -0
  15. package/dist/cli/dispatch/ci.js +38 -0
  16. package/dist/cli/dispatch/security.d.ts +11 -0
  17. package/dist/cli/dispatch/security.js +68 -0
  18. package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
  19. package/dist/cli/dispatch/simple-renderers.js +65 -0
  20. package/dist/cli/dispatch/skill-publish.d.ts +15 -0
  21. package/dist/cli/dispatch/skill-publish.js +44 -0
  22. package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
  23. package/dist/cli/dispatch/skills-cmd.js +93 -0
  24. package/dist/cli/dispatch/tdd.d.ts +11 -0
  25. package/dist/cli/dispatch/tdd.js +45 -0
  26. package/dist/cli/dispatch/types.d.ts +29 -0
  27. package/dist/cli/dispatch/types.js +2 -0
  28. package/dist/cli/dispatch/workflow.d.ts +11 -0
  29. package/dist/cli/dispatch/workflow.js +67 -0
  30. package/dist/cli/help.d.ts +127 -0
  31. package/dist/cli/help.js +138 -0
  32. package/dist/cli/runtime.d.ts +16 -0
  33. package/dist/cli/runtime.js +38 -0
  34. package/dist/cli/validators.d.ts +17 -0
  35. package/dist/cli/validators.js +35 -0
  36. package/dist/commands/analyze.d.ts +1 -1
  37. package/dist/commands/analyze.js +14 -16
  38. package/dist/commands/ci.d.ts +50 -3
  39. package/dist/commands/ci.js +537 -182
  40. package/dist/commands/doctor.d.ts +2 -2
  41. package/dist/commands/doctor.js +112 -62
  42. package/dist/commands/init/report.d.ts +13 -0
  43. package/dist/commands/init/report.js +13 -0
  44. package/dist/commands/init/steps/agent-skills.d.ts +13 -0
  45. package/dist/commands/init/steps/agent-skills.js +45 -0
  46. package/dist/commands/init/steps/ai-sync.d.ts +15 -0
  47. package/dist/commands/init/steps/ai-sync.js +62 -0
  48. package/dist/commands/init/steps/ci.d.ts +24 -0
  49. package/dist/commands/init/steps/ci.js +74 -0
  50. package/dist/commands/init/steps/claude-md.d.ts +14 -0
  51. package/dist/commands/init/steps/claude-md.js +54 -0
  52. package/dist/commands/init/steps/code-graph.d.ts +16 -0
  53. package/dist/commands/init/steps/code-graph.js +68 -0
  54. package/dist/commands/init/steps/context-dir.d.ts +13 -0
  55. package/dist/commands/init/steps/context-dir.js +47 -0
  56. package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
  57. package/dist/commands/init/steps/docker-deploy.js +60 -0
  58. package/dist/commands/init/steps/ghagga.d.ts +15 -0
  59. package/dist/commands/init/steps/ghagga.js +58 -0
  60. package/dist/commands/init/steps/git.d.ts +25 -0
  61. package/dist/commands/init/steps/git.js +81 -0
  62. package/dist/commands/init/steps/gitignore.d.ts +13 -0
  63. package/dist/commands/init/steps/gitignore.js +39 -0
  64. package/dist/commands/init/steps/local-ai.d.ts +14 -0
  65. package/dist/commands/init/steps/local-ai.js +53 -0
  66. package/dist/commands/init/steps/manifest.d.ts +13 -0
  67. package/dist/commands/init/steps/manifest.js +58 -0
  68. package/dist/commands/init/steps/memory.d.ts +15 -0
  69. package/dist/commands/init/steps/memory.js +60 -0
  70. package/dist/commands/init/steps/mock.d.ts +13 -0
  71. package/dist/commands/init/steps/mock.js +64 -0
  72. package/dist/commands/init/steps/sdd.d.ts +12 -0
  73. package/dist/commands/init/steps/sdd.js +40 -0
  74. package/dist/commands/init/steps/security.d.ts +28 -0
  75. package/dist/commands/init/steps/security.js +103 -0
  76. package/dist/commands/init/types.d.ts +30 -0
  77. package/dist/commands/init/types.js +2 -0
  78. package/dist/commands/init.d.ts +5 -4
  79. package/dist/commands/init.js +42 -387
  80. package/dist/commands/llmstxt.d.ts +1 -1
  81. package/dist/commands/llmstxt.js +36 -34
  82. package/dist/commands/plugin.d.ts +10 -1
  83. package/dist/commands/plugin.js +92 -47
  84. package/dist/commands/security.d.ts +11 -5
  85. package/dist/commands/security.js +236 -77
  86. package/dist/commands/skills/analysis.d.ts +19 -0
  87. package/dist/commands/skills/analysis.js +211 -0
  88. package/dist/commands/skills/benchmark.d.ts +6 -0
  89. package/dist/commands/skills/benchmark.js +82 -0
  90. package/dist/commands/skills/constants.d.ts +10 -0
  91. package/dist/commands/skills/constants.js +27 -0
  92. package/dist/commands/skills/directives.d.ts +21 -0
  93. package/dist/commands/skills/directives.js +101 -0
  94. package/dist/commands/skills/parsing.d.ts +16 -0
  95. package/dist/commands/skills/parsing.js +108 -0
  96. package/dist/commands/skills/rules.d.ts +5 -0
  97. package/dist/commands/skills/rules.js +32 -0
  98. package/dist/commands/skills/scoring.d.ts +47 -0
  99. package/dist/commands/skills/scoring.js +306 -0
  100. package/dist/commands/skills.d.ts +8 -69
  101. package/dist/commands/skills.js +9 -490
  102. package/dist/commands/tdd-pipeline.d.ts +17 -0
  103. package/dist/commands/tdd-pipeline.js +144 -0
  104. package/dist/commands/tdd.d.ts +1 -1
  105. package/dist/commands/tdd.js +21 -18
  106. package/dist/commands/workflow.d.ts +23 -0
  107. package/dist/commands/workflow.js +116 -0
  108. package/dist/constants.d.ts +15 -1
  109. package/dist/constants.js +161 -122
  110. package/dist/index.js +43 -280
  111. package/dist/lib/agent-skills.d.ts +36 -1
  112. package/dist/lib/agent-skills.js +168 -19
  113. package/dist/lib/atlassian-mcp.d.ts +42 -0
  114. package/dist/lib/atlassian-mcp.js +98 -0
  115. package/dist/lib/auto-skill-install.d.ts +37 -0
  116. package/dist/lib/auto-skill-install.js +91 -0
  117. package/dist/lib/auto-wire.d.ts +20 -0
  118. package/dist/lib/auto-wire.js +240 -0
  119. package/dist/lib/ci-config.d.ts +65 -0
  120. package/dist/lib/ci-config.js +297 -0
  121. package/dist/lib/claudemd.d.ts +13 -1
  122. package/dist/lib/claudemd.js +174 -24
  123. package/dist/lib/codex-export.d.ts +1 -1
  124. package/dist/lib/codex-export.js +26 -32
  125. package/dist/lib/common.d.ts +1 -1
  126. package/dist/lib/common.js +52 -44
  127. package/dist/lib/context.d.ts +17 -2
  128. package/dist/lib/context.js +142 -13
  129. package/dist/lib/crash-recovery.d.ts +34 -0
  130. package/dist/lib/crash-recovery.js +123 -0
  131. package/dist/lib/docker.d.ts +23 -2
  132. package/dist/lib/docker.js +168 -131
  133. package/dist/lib/exec.d.ts +10 -0
  134. package/dist/lib/exec.js +11 -0
  135. package/dist/lib/frontmatter.d.ts +1 -1
  136. package/dist/lib/frontmatter.js +29 -15
  137. package/dist/lib/harness-audit.d.ts +35 -0
  138. package/dist/lib/harness-audit.js +277 -0
  139. package/dist/lib/parallel-batch.d.ts +42 -0
  140. package/dist/lib/parallel-batch.js +88 -0
  141. package/dist/lib/plugin.d.ts +9 -3
  142. package/dist/lib/plugin.js +127 -70
  143. package/dist/lib/secret-scanner.d.ts +30 -0
  144. package/dist/lib/secret-scanner.js +274 -0
  145. package/dist/lib/security-analysis.d.ts +74 -0
  146. package/dist/lib/security-analysis.js +489 -0
  147. package/dist/lib/skill-publish.d.ts +40 -0
  148. package/dist/lib/skill-publish.js +164 -0
  149. package/dist/lib/skill-scanner.d.ts +63 -0
  150. package/dist/lib/skill-scanner.js +383 -0
  151. package/dist/lib/stack-detector.d.ts +38 -0
  152. package/dist/lib/stack-detector.js +207 -0
  153. package/dist/lib/supply-chain.d.ts +23 -0
  154. package/dist/lib/supply-chain.js +126 -0
  155. package/dist/lib/team-presets.d.ts +53 -0
  156. package/dist/lib/team-presets.js +201 -0
  157. package/dist/lib/template.d.ts +16 -1
  158. package/dist/lib/template.js +46 -17
  159. package/dist/lib/workflow/discovery.d.ts +27 -0
  160. package/dist/lib/workflow/discovery.js +85 -0
  161. package/dist/lib/workflow/index.d.ts +5 -0
  162. package/dist/lib/workflow/index.js +5 -0
  163. package/dist/lib/workflow/parser.d.ts +16 -0
  164. package/dist/lib/workflow/parser.js +198 -0
  165. package/dist/lib/workflow/renderer.d.ts +9 -0
  166. package/dist/lib/workflow/renderer.js +152 -0
  167. package/dist/lib/workflow/validator.d.ts +10 -0
  168. package/dist/lib/workflow/validator.js +189 -0
  169. package/dist/tasks/index.d.ts +4 -0
  170. package/dist/tasks/index.js +4 -0
  171. package/dist/tasks/scaffold-tasks.d.ts +3 -0
  172. package/dist/tasks/scaffold-tasks.js +14 -0
  173. package/dist/tasks/task-id.d.ts +30 -0
  174. package/dist/tasks/task-id.js +59 -0
  175. package/dist/tasks/task-tracker.d.ts +15 -0
  176. package/dist/tasks/task-tracker.js +81 -0
  177. package/dist/types/index.d.ts +134 -6
  178. package/dist/types/index.js +11 -1
  179. package/dist/ui/AnalyzeUI.d.ts +1 -1
  180. package/dist/ui/AnalyzeUI.js +38 -39
  181. package/dist/ui/App.d.ts +5 -3
  182. package/dist/ui/App.js +86 -46
  183. package/dist/ui/AutoSkills.d.ts +9 -0
  184. package/dist/ui/AutoSkills.js +124 -0
  185. package/dist/ui/CI.d.ts +2 -2
  186. package/dist/ui/CI.js +24 -26
  187. package/dist/ui/CIContext.d.ts +1 -1
  188. package/dist/ui/CIContext.js +2 -2
  189. package/dist/ui/CISelector.d.ts +2 -2
  190. package/dist/ui/CISelector.js +23 -15
  191. package/dist/ui/Doctor.d.ts +1 -1
  192. package/dist/ui/Doctor.js +35 -29
  193. package/dist/ui/Header.d.ts +1 -1
  194. package/dist/ui/Header.js +14 -14
  195. package/dist/ui/HookProfileSelector.d.ts +9 -0
  196. package/dist/ui/HookProfileSelector.js +54 -0
  197. package/dist/ui/LlmsTxt.d.ts +1 -1
  198. package/dist/ui/LlmsTxt.js +31 -22
  199. package/dist/ui/MemorySelector.d.ts +2 -2
  200. package/dist/ui/MemorySelector.js +28 -16
  201. package/dist/ui/NameInput.d.ts +1 -1
  202. package/dist/ui/NameInput.js +21 -21
  203. package/dist/ui/OptionSelector.d.ts +6 -2
  204. package/dist/ui/OptionSelector.js +83 -32
  205. package/dist/ui/Plugin.d.ts +4 -3
  206. package/dist/ui/Plugin.js +78 -35
  207. package/dist/ui/Progress.d.ts +3 -3
  208. package/dist/ui/Progress.js +23 -22
  209. package/dist/ui/Skills.d.ts +2 -2
  210. package/dist/ui/Skills.js +61 -32
  211. package/dist/ui/StackSelector.d.ts +2 -2
  212. package/dist/ui/StackSelector.js +27 -17
  213. package/dist/ui/Summary.d.ts +3 -3
  214. package/dist/ui/Summary.js +60 -50
  215. package/dist/ui/Welcome.d.ts +1 -1
  216. package/dist/ui/Welcome.js +15 -16
  217. package/dist/ui/theme.d.ts +1 -1
  218. package/dist/ui/theme.js +6 -6
  219. package/lib/common.psm1 +167 -0
  220. package/lib/common.sh +30 -0
  221. package/package.json +46 -24
  222. package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
  223. package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
  224. package/templates/common/repoforge/repoforge.yaml +34 -0
  225. package/templates/github/deploy-docker-zero-downtime.yml +140 -0
  226. package/templates/github/repoforge-graph.yml +45 -0
  227. package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
  228. package/templates/local-ai/.env.example +17 -0
  229. package/templates/local-ai/docker-compose.yml +95 -0
  230. package/templates/security-hooks/claude-settings-security.json +30 -0
  231. package/templates/security-hooks/commit-msg-signing +29 -0
  232. package/templates/security-hooks/pre-commit-permissions +74 -0
  233. package/templates/security-hooks/pre-commit-secrets +74 -0
  234. package/templates/security-hooks/pre-push-branch-protection +62 -0
  235. package/templates/security-hooks/pre-push-deps +83 -0
  236. package/templates/security-hooks/pre-push-signing +67 -0
  237. package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
  238. package/templates/workflows/ci-pipeline.dot +15 -0
  239. package/templates/workflows/feature-flow.dot +21 -0
  240. package/templates/workflows/release.dot +16 -0
  241. package/dist/__integration__/helpers.d.ts +0 -20
  242. package/dist/__integration__/helpers.d.ts.map +0 -1
  243. package/dist/__integration__/helpers.js +0 -31
  244. package/dist/__integration__/helpers.js.map +0 -1
  245. package/dist/commands/analyze.d.ts.map +0 -1
  246. package/dist/commands/analyze.js.map +0 -1
  247. package/dist/commands/ci.d.ts.map +0 -1
  248. package/dist/commands/ci.js.map +0 -1
  249. package/dist/commands/doctor.d.ts.map +0 -1
  250. package/dist/commands/doctor.js.map +0 -1
  251. package/dist/commands/init.d.ts.map +0 -1
  252. package/dist/commands/init.js.map +0 -1
  253. package/dist/commands/llmstxt.d.ts.map +0 -1
  254. package/dist/commands/llmstxt.js.map +0 -1
  255. package/dist/commands/plugin.d.ts.map +0 -1
  256. package/dist/commands/plugin.js.map +0 -1
  257. package/dist/commands/security.d.ts.map +0 -1
  258. package/dist/commands/security.js.map +0 -1
  259. package/dist/commands/skills.d.ts.map +0 -1
  260. package/dist/commands/skills.js.map +0 -1
  261. package/dist/commands/tdd.d.ts.map +0 -1
  262. package/dist/commands/tdd.js.map +0 -1
  263. package/dist/constants.d.ts.map +0 -1
  264. package/dist/constants.js.map +0 -1
  265. package/dist/index.d.ts.map +0 -1
  266. package/dist/index.js.map +0 -1
  267. package/dist/lib/agent-skills.d.ts.map +0 -1
  268. package/dist/lib/agent-skills.js.map +0 -1
  269. package/dist/lib/claudemd.d.ts.map +0 -1
  270. package/dist/lib/claudemd.js.map +0 -1
  271. package/dist/lib/codex-export.d.ts.map +0 -1
  272. package/dist/lib/codex-export.js.map +0 -1
  273. package/dist/lib/common.d.ts.map +0 -1
  274. package/dist/lib/common.js.map +0 -1
  275. package/dist/lib/context.d.ts.map +0 -1
  276. package/dist/lib/context.js.map +0 -1
  277. package/dist/lib/docker.d.ts.map +0 -1
  278. package/dist/lib/docker.js.map +0 -1
  279. package/dist/lib/frontmatter.d.ts.map +0 -1
  280. package/dist/lib/frontmatter.js.map +0 -1
  281. package/dist/lib/plugin.d.ts.map +0 -1
  282. package/dist/lib/plugin.js.map +0 -1
  283. package/dist/lib/template.d.ts.map +0 -1
  284. package/dist/lib/template.js.map +0 -1
  285. package/dist/types/index.d.ts.map +0 -1
  286. package/dist/types/index.js.map +0 -1
  287. package/dist/ui/AnalyzeUI.d.ts.map +0 -1
  288. package/dist/ui/AnalyzeUI.js.map +0 -1
  289. package/dist/ui/App.d.ts.map +0 -1
  290. package/dist/ui/App.js.map +0 -1
  291. package/dist/ui/CI.d.ts.map +0 -1
  292. package/dist/ui/CI.js.map +0 -1
  293. package/dist/ui/CIContext.d.ts.map +0 -1
  294. package/dist/ui/CIContext.js.map +0 -1
  295. package/dist/ui/CISelector.d.ts.map +0 -1
  296. package/dist/ui/CISelector.js.map +0 -1
  297. package/dist/ui/Doctor.d.ts.map +0 -1
  298. package/dist/ui/Doctor.js.map +0 -1
  299. package/dist/ui/Header.d.ts.map +0 -1
  300. package/dist/ui/Header.js.map +0 -1
  301. package/dist/ui/LlmsTxt.d.ts.map +0 -1
  302. package/dist/ui/LlmsTxt.js.map +0 -1
  303. package/dist/ui/MemorySelector.d.ts.map +0 -1
  304. package/dist/ui/MemorySelector.js.map +0 -1
  305. package/dist/ui/NameInput.d.ts.map +0 -1
  306. package/dist/ui/NameInput.js.map +0 -1
  307. package/dist/ui/OptionSelector.d.ts.map +0 -1
  308. package/dist/ui/OptionSelector.js.map +0 -1
  309. package/dist/ui/Plugin.d.ts.map +0 -1
  310. package/dist/ui/Plugin.js.map +0 -1
  311. package/dist/ui/Progress.d.ts.map +0 -1
  312. package/dist/ui/Progress.js.map +0 -1
  313. package/dist/ui/Skills.d.ts.map +0 -1
  314. package/dist/ui/Skills.js.map +0 -1
  315. package/dist/ui/StackSelector.d.ts.map +0 -1
  316. package/dist/ui/StackSelector.js.map +0 -1
  317. package/dist/ui/Summary.d.ts.map +0 -1
  318. package/dist/ui/Summary.js.map +0 -1
  319. package/dist/ui/Welcome.d.ts.map +0 -1
  320. package/dist/ui/Welcome.js.map +0 -1
  321. package/dist/ui/theme.d.ts.map +0 -1
  322. package/dist/ui/theme.js.map +0 -1
@@ -4,15 +4,19 @@
4
4
  # =============================================================================
5
5
  # Requires: npm install -g javi-forge
6
6
  # To skip: git push --no-verify
7
+ # Target: ~3 minutes when Docker is running. Elapsed time logged below.
7
8
  # =============================================================================
8
9
 
9
10
  set -e
10
11
 
12
+ _start=$SECONDS
13
+
11
14
  if docker info &>/dev/null; then
12
15
  echo "PRE-PUSH: Running full CI simulation..."
13
16
  javi-forge ci || {
17
+ _elapsed=$((SECONDS - _start))
14
18
  echo ""
15
- echo "CI FAILED — Push aborted. Fix the issues above before pushing."
19
+ echo "CI FAILED after ${_elapsed}s — Push aborted. Fix the issues above before pushing."
16
20
  echo "To skip: git push --no-verify"
17
21
  exit 1
18
22
  }
@@ -20,9 +24,16 @@ else
20
24
  echo "PRE-PUSH: ⚠ Docker not running — running quick checks only (lint + typecheck)"
21
25
  echo " Start Docker for full CI (security scan, Docker tests)"
22
26
  javi-forge ci --quick --no-docker --no-security --no-ci-ghagga || {
27
+ _elapsed=$((SECONDS - _start))
23
28
  echo ""
24
- echo "Quick checks FAILED — Push aborted. Fix the issues above before pushing."
29
+ echo "Quick checks FAILED after ${_elapsed}s — Push aborted. Fix the issues above before pushing."
25
30
  echo "To skip: git push --no-verify"
26
31
  exit 1
27
32
  }
28
33
  fi
34
+
35
+ _elapsed=$((SECONDS - _start))
36
+ echo "PRE-PUSH: ${_elapsed}s (target: 180s)"
37
+ if [ "$_elapsed" -gt 300 ]; then
38
+ echo " WARNING: pre-push took ${_elapsed}s, well over the 180s target."
39
+ fi
@@ -0,0 +1,367 @@
1
+ # =============================================================================
2
+ # CI-LOCAL: Installation Script (PowerShell)
3
+ # =============================================================================
4
+ # Mirror of ci-local/install.sh for Windows / PowerShell users.
5
+ #
6
+ # Usage:
7
+ # .\install.ps1
8
+ #
9
+ # Behaviour MUST match install.sh. Diff against the bash version on every
10
+ # change. Test on real Windows + WSL + macOS pwsh before tagging a release.
11
+ # =============================================================================
12
+
13
+ #Requires -Version 7.2
14
+ [CmdletBinding()]
15
+ param()
16
+
17
+ # Defense-in-depth: #Requires is parsed by some hosts AFTER the script body.
18
+ # 7.2 is the minimum because FileSystemInfo.ResolveLinkTarget($true) is .NET 6+,
19
+ # which shipped with pwsh 7.2. Earlier pwsh 7.x runs on .NET 5 and the method
20
+ # is absent — the symlink check would silently fall back to no-op.
21
+ if ($PSVersionTable.PSEdition -ne 'Core' -or $PSVersionTable.PSVersion -lt [Version]'7.2') {
22
+ Write-Host 'ERROR: ci-local install.ps1 requires PowerShell 7.2+ (pwsh).' -ForegroundColor Red
23
+ Write-Host "Detected: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)" -ForegroundColor Yellow
24
+ Write-Host 'Install: https://github.com/PowerShell/PowerShell/releases' -ForegroundColor Cyan
25
+ exit 1
26
+ }
27
+
28
+ Set-StrictMode -Version Latest
29
+ $ErrorActionPreference = 'Stop'
30
+
31
+ $ScriptDir = Split-Path -Parent $PSCommandPath
32
+ $ProjectDir = Split-Path -Parent $ScriptDir
33
+
34
+ # ─── Symlink-safe path resolver ────────────────────────────────────
35
+ # Defined inline (NOT in lib/common.psm1) because we need it BEFORE the
36
+ # module is loaded — Import-Module on a hostile symlink would already
37
+ # execute the attacker's code. Audit round 4 PoC verified.
38
+ # Fails closed: any exception aborts the script.
39
+ #
40
+ # DEFENSE — alias / function shadowing (round 5):
41
+ # PowerShell command resolution is Alias > Function > Cmdlet. A
42
+ # pre-existing GLOBAL alias of the same name would beat our local
43
+ # function. Remove any prior binding before defining, and verify the
44
+ # resolved Get-Command points to OUR script file.
45
+ Remove-Item Alias:Resolve-RealPath -Force -ErrorAction SilentlyContinue
46
+ Remove-Item Function:Resolve-RealPath -Force -ErrorAction SilentlyContinue
47
+ function Resolve-RealPath([string]$P) {
48
+ if ([string]::IsNullOrEmpty($P)) {
49
+ throw "Resolve-RealPath: empty path"
50
+ }
51
+ $item = Get-Item -LiteralPath $P -Force -ErrorAction Stop
52
+ $target = $item.ResolveLinkTarget($true)
53
+ if ($target) {
54
+ # ResolveLinkTarget($true) is platform-inconsistent for dangling
55
+ # links: Linux throws on the call above, but Windows returns a
56
+ # FileSystemInfo whose .Exists is false. Treat both the same.
57
+ if (-not $target.Exists) {
58
+ throw "Resolve-RealPath: dangling symlink target $($target.FullName)"
59
+ }
60
+ return $target.FullName
61
+ }
62
+ return $item.FullName
63
+ }
64
+ $_rr = Get-Command Resolve-RealPath -ErrorAction SilentlyContinue
65
+ if (-not $_rr -or $_rr.CommandType -ne 'Function' -or $_rr.ScriptBlock.File -ne $PSCommandPath) {
66
+ Write-Host 'ERROR: Resolve-RealPath is shadowed by an external command' -ForegroundColor Red
67
+ if ($_rr) {
68
+ Write-Host " Resolved to: $($_rr.CommandType) at $($_rr.ScriptBlock.File)" -ForegroundColor Yellow
69
+ }
70
+ exit 1
71
+ }
72
+ Remove-Variable _rr
73
+
74
+ # Find the shared library (mirrors install.sh's source order).
75
+ $libPath = Join-Path $ScriptDir '..' 'lib' 'common.psm1'
76
+ if (-not (Test-Path -LiteralPath $libPath -PathType Leaf)) {
77
+ Write-Host "ERROR: lib/common.psm1 not found at $libPath" -ForegroundColor Red
78
+ Write-Host "Copy lib/ alongside ci-local/ (see README)." -ForegroundColor Yellow
79
+ exit 1
80
+ }
81
+
82
+ # SECURITY: validate the module BEFORE importing. A hostile symlink runs
83
+ # attacker code at user privilege as soon as Import-Module evaluates it.
84
+ # Round 4 (2026-05-17) PoC verified the same hijack on install.sh side.
85
+ try {
86
+ $libReal = Resolve-RealPath $libPath
87
+ $projectReal = Resolve-RealPath $ProjectDir
88
+ } catch {
89
+ Write-Host "ERROR: could not resolve lib/common.psm1 or project root: $($_.Exception.Message)" -ForegroundColor Red
90
+ exit 1
91
+ }
92
+ $sep = [System.IO.Path]::DirectorySeparatorChar
93
+ if (-not ($libReal.Equals($projectReal, [StringComparison]::OrdinalIgnoreCase) -or
94
+ $libReal.StartsWith($projectReal + $sep, [StringComparison]::OrdinalIgnoreCase))) {
95
+ Write-Host 'ERROR: lib/common.psm1 resolves outside the project root' -ForegroundColor Red
96
+ Write-Host " LIB resolved: $libReal" -ForegroundColor Yellow
97
+ Write-Host " PROJECT resolved: $projectReal" -ForegroundColor Yellow
98
+ Write-Host 'Refusing to import. Investigate symlinks under lib/.' -ForegroundColor Yellow
99
+ exit 1
100
+ }
101
+
102
+ Import-Module $libPath -Force
103
+
104
+ Write-Host '=== CI-LOCAL Installation ===' -ForegroundColor Cyan
105
+
106
+ Push-Location -LiteralPath $ProjectDir
107
+ try {
108
+
109
+ # ─── Verify javi-forge CLI is available ────────────────────────────
110
+ # The git hooks (pre-commit, pre-push) invoke `javi-forge ci ...`.
111
+ # Catch a missing CLI BEFORE configuring hooks so the user gets a
112
+ # clear error instead of a silent "command not found" on first commit.
113
+ if (-not (Test-CommandExists -Name 'javi-forge')) {
114
+ Write-Host 'ERROR: javi-forge CLI not found in PATH' -ForegroundColor Red
115
+ Write-Host ''
116
+ Write-Host 'The git hooks (pre-commit, pre-push) require the javi-forge CLI.'
117
+ Write-Host 'Install it globally:'
118
+ Write-Host ' npm install -g javi-forge' -ForegroundColor Cyan
119
+ Write-Host ''
120
+ Write-Host 'Or, if you are developing javi-forge itself, link the workspace:'
121
+ Write-Host ' pnpm link --global' -ForegroundColor Cyan
122
+ Write-Host ''
123
+ exit 1
124
+ }
125
+
126
+ # Resolve the binary path. Get-Command returns an ApplicationInfo whose
127
+ # .Source is the absolute path. Resolve any symlink chain via .Target
128
+ # (PowerShell 6+ exposes symlink Target on FileInfo).
129
+ $jfCmd = Get-Command -Name 'javi-forge'
130
+ $jfPath = $jfCmd.Source
131
+ $jfReal = $jfPath
132
+ try {
133
+ $jfItem = Get-Item -LiteralPath $jfPath -Force -ErrorAction Stop
134
+ if ($jfItem.LinkType -eq 'SymbolicLink' -and $jfItem.Target) {
135
+ $target = $jfItem.Target
136
+ if ($target -is [array]) { $target = $target[0] }
137
+ if (-not [System.IO.Path]::IsPathRooted($target)) {
138
+ $target = Join-Path (Split-Path -Parent $jfPath) $target
139
+ }
140
+ $jfReal = [System.IO.Path]::GetFullPath($target)
141
+ }
142
+ } catch {
143
+ # If Get-Item fails (e.g. on platforms without symlink support), keep $jfPath.
144
+ }
145
+
146
+ $jfVersionOk = $true
147
+ $jfVersionOutput = ''
148
+ try {
149
+ $jfVersionOutput = (& javi-forge --version 2>&1) -join "`n"
150
+ if ($LASTEXITCODE -ne 0) { $jfVersionOk = $false }
151
+ } catch {
152
+ $jfVersionOk = $false
153
+ $jfVersionOutput = $_.Exception.Message
154
+ }
155
+
156
+ if ($jfVersionOk) {
157
+ if ($jfPath -eq $jfReal) {
158
+ Write-Host "javi-forge: $jfVersionOutput ($jfPath)" -ForegroundColor Green
159
+ } else {
160
+ Write-Host "javi-forge: $jfVersionOutput ($jfPath -> $jfReal)" -ForegroundColor Green
161
+ }
162
+ } else {
163
+ Write-Host "ERROR: javi-forge found at $jfPath but '--version' failed" -ForegroundColor Red
164
+ Write-Host "Output: $jfVersionOutput" -ForegroundColor Yellow
165
+ Write-Host 'Reinstall: npm install -g javi-forge' -ForegroundColor Cyan
166
+ exit 1
167
+ }
168
+
169
+ # Reject UNC / remote paths outright — a javi-forge symlinked to
170
+ # \\attacker\share passes the writable-cache check because none of the
171
+ # prefixes match. Two forms to handle:
172
+ # \\server\share\path → UNC (reject)
173
+ # \\?\UNC\server\share\path → extended-length UNC (reject)
174
+ # \\?\C:\very\long\path → extended-length LOCAL (allow)
175
+ if ($jfReal -like '\\\\?\\UNC\\*' -or
176
+ ($jfReal -like '\\\\*' -and -not ($jfReal -like '\\\\?\\*'))) {
177
+ Write-Host "ERROR: javi-forge resolves to a UNC / network path ($jfReal)." -ForegroundColor Red
178
+ Write-Host 'Refusing to wire hooks against a remote binary.' -ForegroundColor Yellow
179
+ exit 1
180
+ }
181
+
182
+ # Warn if the resolved path is in a writable / cache / temp location.
183
+ # Build the candidate list defensively: an empty env var would produce
184
+ # something like Join-Path '' 'npm-cache' = 'npm-cache', which then
185
+ # matches every path containing that substring (Opus-alt PoC).
186
+ function Test-IsAbsolutePath([string]$P) {
187
+ if ([string]::IsNullOrWhiteSpace($P)) { return $false }
188
+ try { return [System.IO.Path]::IsPathRooted($P) } catch { return $false }
189
+ }
190
+
191
+ $tempPaths = @('/tmp/', '/var/tmp/', '/dev/shm/')
192
+ foreach ($candidate in @(
193
+ (Join-Path $HOME '.cache'),
194
+ (Join-Path $HOME '.npm/_npx'),
195
+ (Join-Path $HOME '.pnpm-store')
196
+ )) {
197
+ if (Test-IsAbsolutePath $candidate) { $tempPaths += $candidate }
198
+ }
199
+ if (Test-IsAbsolutePath $env:TEMP) { $tempPaths += $env:TEMP }
200
+ if (Test-IsAbsolutePath $env:LOCALAPPDATA) { $tempPaths += (Join-Path $env:LOCALAPPDATA 'npm-cache') }
201
+ if (Test-IsAbsolutePath $env:APPDATA) { $tempPaths += (Join-Path $env:APPDATA 'npm') }
202
+
203
+ foreach ($p in $tempPaths) {
204
+ if ([string]::IsNullOrWhiteSpace($p)) { continue }
205
+ if ($jfReal.StartsWith($p, [StringComparison]::OrdinalIgnoreCase)) {
206
+ Write-Host "WARNING: javi-forge resolves to a writable/cache directory ($jfReal)." -ForegroundColor Yellow
207
+ Write-Host 'Caches can be overwritten by package installs — prefer a stable global location.' -ForegroundColor Yellow
208
+ break
209
+ }
210
+ }
211
+
212
+ # ─── Configure git hooks ──────────────────────────────────────────
213
+ Write-Host '[1/2] Configuring git hooks...' -ForegroundColor Yellow
214
+
215
+ # Detect competing hook managers BEFORE we touch core.hooksPath.
216
+ if (Test-Path -LiteralPath '.husky' -PathType Container) {
217
+ Write-Host 'WARNING: husky detected (.husky/). ci-local will override its hooks via core.hooksPath.' -ForegroundColor Yellow
218
+ Write-Host "If you want to keep husky: 'pnpm remove husky' first, OR back out of this install." -ForegroundColor Yellow
219
+ }
220
+ if ((Test-Path -LiteralPath 'lefthook.yml' -PathType Leaf) -or
221
+ (Test-Path -LiteralPath 'lefthook.yaml' -PathType Leaf)) {
222
+ Write-Host 'WARNING: lefthook detected. ci-local will override its hooks via core.hooksPath.' -ForegroundColor Yellow
223
+ }
224
+
225
+ # Backup existing .git/hooks/ files BEFORE we redirect hooksPath.
226
+ $existingHooks = (& git config --get core.hooksPath 2>$null) -join ''
227
+ if ($existingHooks -and $existingHooks -ne 'ci-local/hooks' -and $existingHooks -ne '.ci-local/hooks') {
228
+ Write-Host "WARNING: existing core.hooksPath was $existingHooks. It will be replaced." -ForegroundColor Yellow
229
+ }
230
+ foreach ($hook in 'pre-commit', 'commit-msg', 'pre-push') {
231
+ $hookPath = ".git/hooks/$hook"
232
+ $bakPath = "$hookPath.bak"
233
+ if ((Test-Path -LiteralPath $hookPath -PathType Leaf) -and
234
+ (-not (Test-Path -LiteralPath $bakPath -PathType Leaf))) {
235
+ $size = (Get-Item -LiteralPath $hookPath).Length
236
+ if ($size -gt 0) {
237
+ Copy-Item -LiteralPath $hookPath -Destination $bakPath -Force
238
+ Write-Host " Backed up existing $hookPath -> $bakPath" -ForegroundColor Yellow
239
+ }
240
+ }
241
+ }
242
+
243
+
244
+ # Compute hooksPath RELATIVE to the project root so the same install.ps1
245
+ # works whether the dir is "ci-local" (dev checkout) or ".ci-local"
246
+ # (user's project after copying via install instructions).
247
+ $hooksDir = Join-Path $ScriptDir 'hooks'
248
+
249
+ # SECURITY: reject hooksPath that escapes the project root. If
250
+ # ci-local/hooks is a symlink/junction pointing outside the project,
251
+ # GetRelativePath returns "../../../somewhere/evil" and every future
252
+ # commit executes whatever lives there. Audit round 3 caught this.
253
+ #
254
+ # Resolve-RealPath is defined at top level (BEFORE Import-Module) so we
255
+ # can reuse it here for the hooks-dir check. It fails closed — any
256
+ # exception aborts instead of falling back to GetFullPath which does
257
+ # not follow symlinks (the original fallback opened a hole flagged by
258
+ # audit round 4).
259
+ try {
260
+ $hooksAbs = Resolve-RealPath $hooksDir
261
+ $projectAbs = Resolve-RealPath $ProjectDir
262
+ } catch {
263
+ Write-Host "ERROR: could not resolve hooks dir or project root: $($_.Exception.Message)" -ForegroundColor Red
264
+ exit 1
265
+ }
266
+ if (-not ($hooksAbs.Equals($projectAbs, [StringComparison]::OrdinalIgnoreCase) -or
267
+ $hooksAbs.StartsWith($projectAbs + $sep, [StringComparison]::OrdinalIgnoreCase))) {
268
+ Write-Host 'ERROR: hooks directory resolves outside the project root' -ForegroundColor Red
269
+ Write-Host " HOOKS_DIR resolved to: $hooksAbs" -ForegroundColor Yellow
270
+ Write-Host " PROJECT_DIR: $projectAbs" -ForegroundColor Yellow
271
+ Write-Host 'Refusing to set core.hooksPath. Investigate symlinks/junctions under ci-local/.' -ForegroundColor Yellow
272
+ exit 1
273
+ }
274
+
275
+ try {
276
+ $hooksRel = [System.IO.Path]::GetRelativePath($projectAbs, $hooksAbs)
277
+ } catch {
278
+ $hooksRel = $hooksDir
279
+ }
280
+ # Normalize separators to forward slashes (git config uses POSIX paths).
281
+ $hooksRel = $hooksRel -replace '\\', '/'
282
+
283
+ & git config core.hooksPath $hooksRel
284
+ if ($LASTEXITCODE -ne 0) {
285
+ Write-Host 'ERROR: failed to set core.hooksPath. Are you inside a git repo?' -ForegroundColor Red
286
+ exit 1
287
+ }
288
+ Write-Host "hooksPath = $hooksRel" -ForegroundColor Green
289
+
290
+ # Make hooks executable. Different OS, different model:
291
+ # - macOS / Linux / WSL: chmod 0755 — POSIX bits, what git expects.
292
+ # - Windows native NTFS: chmod is a no-op; lock the ACL via icacls so
293
+ # only the current user can rewrite the hooks between commits.
294
+ if (Test-CommandExists -Name 'chmod') {
295
+ Get-ChildItem -LiteralPath $hooksDir -File -ErrorAction SilentlyContinue |
296
+ ForEach-Object { & chmod 0755 $_.FullName }
297
+ Get-ChildItem -LiteralPath $ScriptDir -Filter '*.sh' -File -ErrorAction SilentlyContinue |
298
+ ForEach-Object { & chmod 0755 $_.FullName }
299
+ }
300
+ if ($IsWindows -and (Test-CommandExists -Name 'icacls')) {
301
+ # Restrict hooks dir to current user (F = full, OI/CI = inherit to
302
+ # files/subfolders). /inheritance:r drops any permissive parent ACL.
303
+ #
304
+ # Use the current user's SID rather than $env:USERNAME — usernames
305
+ # with spaces ("John Doe") or domain prefixes ("CONTOSO\jdoe")
306
+ # break the SDDL principal syntax; SIDs are unambiguous. Falls
307
+ # back to USERNAME only if the SID lookup fails.
308
+ $principal = $null
309
+ try {
310
+ $principal = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value
311
+ } catch {
312
+ $principal = $env:USERNAME
313
+ }
314
+ if (-not $principal -or $principal -notmatch '^[\w\-\\:]+$') {
315
+ Write-Host 'WARNING: could not derive a safe principal for icacls; skipping ACL hardening.' -ForegroundColor Yellow
316
+ } else {
317
+ try {
318
+ & icacls $hooksDir /inheritance:r /grant:r "${principal}:(OI)(CI)F" *> $null
319
+ if ($LASTEXITCODE -ne 0) {
320
+ Write-Host "WARNING: icacls on $hooksDir returned $LASTEXITCODE" -ForegroundColor Yellow
321
+ }
322
+ } catch {
323
+ Write-Host "WARNING: icacls failed: $($_.Exception.Message)" -ForegroundColor Yellow
324
+ }
325
+ }
326
+ }
327
+ Write-Host 'Done' -ForegroundColor Green
328
+
329
+ # ─── Dependency check ─────────────────────────────────────────────
330
+ Write-Host '[2/2] Checking dependencies...' -ForegroundColor Yellow
331
+
332
+ $dockerOk = $false
333
+ if (Test-CommandExists -Name 'docker') {
334
+ try {
335
+ & docker info *> $null
336
+ if ($LASTEXITCODE -eq 0) { $dockerOk = $true }
337
+ } catch { }
338
+ }
339
+ if ($dockerOk) {
340
+ Write-Host 'Docker: available' -ForegroundColor Green
341
+ } else {
342
+ Write-Host 'Docker: not running (required for pre-push CI)' -ForegroundColor Yellow
343
+ }
344
+
345
+ if (Test-CommandExists -Name 'semgrep') {
346
+ Write-Host 'Semgrep: installed (native)' -ForegroundColor Green
347
+ } elseif ($dockerOk) {
348
+ Write-Host 'Semgrep: available via Docker (returntocorp/semgrep)' -ForegroundColor Green
349
+ } else {
350
+ Write-Host 'Semgrep: not available (install semgrep or Docker)' -ForegroundColor Yellow
351
+ }
352
+
353
+ Write-Host ''
354
+ Write-Host 'Setup complete!' -ForegroundColor Green
355
+ Write-Host ''
356
+ Write-Host 'Hooks enabled:'
357
+ Write-Host ' - pre-commit: AI check + lint + security'
358
+ Write-Host ' - commit-msg: Block AI attribution (best-effort)'
359
+ Write-Host ' - pre-push: CI simulation in Docker'
360
+ Write-Host ''
361
+ Write-Host 'NOTE: git hooks are bash scripts. On Windows they run via the'
362
+ Write-Host 'MSYS2 bash bundled with Git for Windows. WSL is also supported.'
363
+ Write-Host ''
364
+
365
+ } finally {
366
+ Pop-Location
367
+ }
@@ -8,18 +8,164 @@ set -e
8
8
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
9
9
  PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
10
10
 
11
- # Source shared library for colors
12
- source "$SCRIPT_DIR/../lib/common.sh"
11
+ # ─── Required tools (fail-closed) ──────────────────────────────────
12
+ # realpath is load-bearing for the symlink-traversal checks below. If it's
13
+ # missing, the fallback used to silently return unresolved paths, which let
14
+ # a hostile symlink slip past. Audit round 4 (2026-05-17) PoC verified.
15
+ if ! command -v realpath >/dev/null 2>&1; then
16
+ printf 'ERROR: realpath is required for ci-local install (symlink checks).\n' >&2
17
+ printf ' Linux/WSL: install coreutils\n' >&2
18
+ printf ' macOS: brew install coreutils, then add gnubin to PATH\n' >&2
19
+ exit 1
20
+ fi
21
+
22
+ # ─── SECURITY: validate lib/common.sh BEFORE sourcing it ───────────
23
+ # Sourcing a hostile symlink runs attacker code at user privilege BEFORE
24
+ # any traversal check could fire. Round 4 PoC verified this on bash and
25
+ # PowerShell. Resolve the lib path and abort if it escapes the project.
26
+ LIB_FILE="$SCRIPT_DIR/../lib/common.sh"
27
+ if [ ! -f "$LIB_FILE" ]; then
28
+ printf 'ERROR: lib/common.sh not found at %s\n' "$LIB_FILE" >&2
29
+ exit 1
30
+ fi
31
+ # realpath was hard-required above — failures here are unexpected, so let
32
+ # set -e propagate them instead of swallowing with `|| true`.
33
+ LIB_REAL=$(realpath "$LIB_FILE")
34
+ PROJECT_REAL=$(realpath "$PROJECT_DIR")
35
+ case "$LIB_REAL" in
36
+ "$PROJECT_REAL"|"$PROJECT_REAL"/*) ;;
37
+ *)
38
+ printf 'ERROR: lib/common.sh resolves outside the project root:\n' >&2
39
+ printf ' LIB resolved: %s\n' "$LIB_REAL" >&2
40
+ printf ' PROJECT resolved: %s\n' "$PROJECT_REAL" >&2
41
+ printf 'Refusing to source. Investigate symlinks under lib/.\n' >&2
42
+ exit 1
43
+ ;;
44
+ esac
45
+
46
+ # Source the RESOLVED path, not the original. Round 5 flagged a TOCTOU:
47
+ # between the realpath check and the source, an attacker could swap the
48
+ # symlink target. Sourcing $LIB_REAL eliminates that window.
49
+ source "$LIB_REAL"
13
50
 
14
51
  echo -e "${CYAN}=== CI-LOCAL Installation ===${NC}"
15
52
 
16
53
  cd "$PROJECT_DIR"
17
54
 
55
+ # 0. Verify javi-forge CLI is available (required by hooks)
56
+ # The git hooks (pre-commit, pre-push) invoke `javi-forge ci ...`.
57
+ # If the CLI is missing, hooks fail silently with "command not found".
58
+ # Catch this BEFORE configuring hooks so the user gets a clear error.
59
+ if ! command -v javi-forge >/dev/null 2>&1; then
60
+ echo -e "${RED}ERROR: javi-forge CLI not found in PATH${NC}"
61
+ echo -e ""
62
+ echo -e "The git hooks (pre-commit, pre-push) require the javi-forge CLI."
63
+ echo -e "Install it globally:"
64
+ echo -e " ${CYAN}npm install -g javi-forge${NC}"
65
+ echo -e ""
66
+ echo -e "Or, if you are developing javi-forge itself, link the workspace:"
67
+ echo -e " ${CYAN}pnpm link --global${NC}"
68
+ echo -e ""
69
+ exit 1
70
+ fi
71
+
72
+ # Log resolved path. The hooks will trust whatever PATH gives them at commit
73
+ # time — surface the binary location so the user catches a hijack early.
74
+ # readlink -f resolves any symlink chain so a symlink trick (~/.local/bin
75
+ # pointing to /tmp/evil) doesn't escape the writable-path warning below.
76
+ JAVI_FORGE_PATH=$(command -v javi-forge)
77
+ JAVI_FORGE_REAL=$(readlink -f "$JAVI_FORGE_PATH" 2>/dev/null || printf "%s" "$JAVI_FORGE_PATH")
78
+ JAVI_FORGE_VERSION_OUTPUT=$(javi-forge --version 2>&1) && JAVI_FORGE_VERSION_OK=1 || JAVI_FORGE_VERSION_OK=0
79
+
80
+ if [ "$JAVI_FORGE_VERSION_OK" -eq 1 ]; then
81
+ if [ "$JAVI_FORGE_PATH" = "$JAVI_FORGE_REAL" ]; then
82
+ echo -e "${GREEN}javi-forge: ${JAVI_FORGE_VERSION_OUTPUT} (${JAVI_FORGE_PATH})${NC}"
83
+ else
84
+ echo -e "${GREEN}javi-forge: ${JAVI_FORGE_VERSION_OUTPUT} (${JAVI_FORGE_PATH} -> ${JAVI_FORGE_REAL})${NC}"
85
+ fi
86
+ else
87
+ echo -e "${RED}ERROR: javi-forge found at ${JAVI_FORGE_PATH} but '--version' failed${NC}"
88
+ echo -e "${YELLOW}Output: ${JAVI_FORGE_VERSION_OUTPUT}${NC}"
89
+ echo -e "Reinstall: ${CYAN}npm install -g javi-forge${NC}"
90
+ exit 1
91
+ fi
92
+
93
+ # Warn if the RESOLVED (post-symlink) path is in a writable-by-many or temp
94
+ # location. Audit (2026-05-17) flagged additional locations beyond /tmp:
95
+ # user-controlled caches and globals can be hijacked by a malicious package.
96
+ case "$JAVI_FORGE_REAL" in
97
+ /tmp/*|/var/tmp/*|/dev/shm/*)
98
+ echo -e "${YELLOW}WARNING: javi-forge resolves to a temp directory (${JAVI_FORGE_REAL}).${NC}"
99
+ echo -e "${YELLOW}This is unusual and may indicate a compromised install.${NC}"
100
+ ;;
101
+ "$HOME"/.cache/*|"$HOME"/.npm/_npx/*|"$HOME"/.pnpm-store/*)
102
+ echo -e "${YELLOW}WARNING: javi-forge resolves to a cache directory (${JAVI_FORGE_REAL}).${NC}"
103
+ echo -e "${YELLOW}Caches can be overwritten by package installs — prefer a stable global location.${NC}"
104
+ ;;
105
+ esac
106
+
18
107
  # 1. Configurar git hooks
19
108
  echo -e "${YELLOW}[1/2] Configuring git hooks...${NC}"
20
- git config core.hooksPath .ci-local/hooks
21
- chmod +x "$SCRIPT_DIR/hooks/"* 2>/dev/null || true
22
- chmod +x "$SCRIPT_DIR/"*.sh 2>/dev/null || true
109
+
110
+ # Detect competing hook managers BEFORE we touch core.hooksPath. Setting
111
+ # our path would silently override theirs and break expected behaviour.
112
+ if [ -d ".husky" ]; then
113
+ echo -e "${YELLOW}WARNING: husky detected (.husky/). ci-local will override its hooks via core.hooksPath.${NC}"
114
+ echo -e "${YELLOW}If you want to keep husky: 'pnpm remove husky' first, OR back out of this install.${NC}"
115
+ fi
116
+ if [ -f "lefthook.yml" ] || [ -f "lefthook.yaml" ]; then
117
+ echo -e "${YELLOW}WARNING: lefthook detected. ci-local will override its hooks via core.hooksPath.${NC}"
118
+ fi
119
+
120
+ # Backup any existing .git/hooks/ files BEFORE we redirect hooksPath. The
121
+ # user can restore them via './uninstall.sh --restore-backups'. We move
122
+ # instead of copy so a future install does not double-back-up.
123
+ EXISTING_HOOKSPATH=$(git config --get core.hooksPath 2>/dev/null || echo "")
124
+ if [ -n "$EXISTING_HOOKSPATH" ] && [ "$EXISTING_HOOKSPATH" != "ci-local/hooks" ] && [ "$EXISTING_HOOKSPATH" != ".ci-local/hooks" ]; then
125
+ echo -e "${YELLOW}WARNING: existing core.hooksPath was ${EXISTING_HOOKSPATH}.${NC}"
126
+ echo -e "${YELLOW}It will be replaced. The previous value can be restored manually.${NC}"
127
+ fi
128
+ for hook in pre-commit commit-msg pre-push; do
129
+ if [ -f ".git/hooks/$hook" ] && [ -s ".git/hooks/$hook" ] && [ ! -f ".git/hooks/$hook.bak" ]; then
130
+ cp ".git/hooks/$hook" ".git/hooks/$hook.bak"
131
+ echo -e "${YELLOW} Backed up existing .git/hooks/$hook → .git/hooks/$hook.bak${NC}"
132
+ fi
133
+ done
134
+
135
+ # Compute hooksPath relative to the project root so this works whether the
136
+ # directory is "ci-local" (development checkout) or ".ci-local" (user's
137
+ # project, after copying via the install instructions).
138
+ HOOKS_DIR="$SCRIPT_DIR/hooks"
139
+
140
+ # SECURITY: reject hooksPath that escapes the project root. If $SCRIPT_DIR or
141
+ # $SCRIPT_DIR/hooks is a symlink pointing outside the project, realpath
142
+ # resolves through it and emits "../../tmp/evil/hooks". Setting that as
143
+ # core.hooksPath means every future commit executes whatever lives there.
144
+ # realpath is guaranteed above so no fallback is needed.
145
+ HOOKS_ABS=$(realpath "$HOOKS_DIR")
146
+ case "$HOOKS_ABS" in
147
+ "$PROJECT_REAL"|"$PROJECT_REAL"/*) ;;
148
+ *)
149
+ echo -e "${RED}ERROR: hooks directory resolves outside the project root${NC}"
150
+ echo -e "${YELLOW} HOOKS_DIR resolved to: $HOOKS_ABS${NC}"
151
+ echo -e "${YELLOW} PROJECT_DIR: $PROJECT_REAL${NC}"
152
+ echo -e "${YELLOW}Refusing to set core.hooksPath. Investigate symlinks under ci-local/.${NC}"
153
+ exit 1
154
+ ;;
155
+ esac
156
+ HOOKS_REL=$(realpath --relative-to="$PROJECT_DIR" "$HOOKS_DIR")
157
+
158
+ git config core.hooksPath "$HOOKS_REL"
159
+ echo -e "${GREEN}hooksPath = $HOOKS_REL${NC}"
160
+ # Explicit 0755 (and only when files exist) — owner rwx, group/others rx.
161
+ # Avoids group-writable hooks AND avoids passing a literal "*" to chmod
162
+ # when the glob expands to nothing.
163
+ shopt -s nullglob
164
+ hook_files=("$HOOKS_DIR"/* "$SCRIPT_DIR"/*.sh)
165
+ if [ ${#hook_files[@]} -gt 0 ]; then
166
+ chmod 0755 "${hook_files[@]}"
167
+ fi
168
+ shopt -u nullglob
23
169
  echo -e "${GREEN}Done${NC}"
24
170
 
25
171
  # 2. Verificar dependencias
@@ -0,0 +1,85 @@
1
+ # =============================================================================
2
+ # CI-LOCAL: Uninstall Script (PowerShell)
3
+ # =============================================================================
4
+ # Mirror of ci-local/uninstall.sh for Windows / PowerShell users.
5
+ #
6
+ # Usage:
7
+ # .\uninstall.ps1 # only unset core.hooksPath
8
+ # .\uninstall.ps1 -RestoreBackups # also restore .bak hooks
9
+ # .\uninstall.ps1 -Purge # also rm -rf ci-local/ + lib/
10
+ # =============================================================================
11
+
12
+ #Requires -Version 7.2
13
+ [CmdletBinding()]
14
+ param(
15
+ [switch]$RestoreBackups,
16
+ [switch]$Purge
17
+ )
18
+
19
+ Set-StrictMode -Version Latest
20
+ $ErrorActionPreference = 'Stop'
21
+
22
+ $ScriptDir = Split-Path -Parent $PSCommandPath
23
+ $ProjectDir = Split-Path -Parent $ScriptDir
24
+
25
+ Push-Location -LiteralPath $ProjectDir
26
+ try {
27
+
28
+ Write-Host '=== CI-LOCAL Uninstall ===' -ForegroundColor Cyan
29
+
30
+ # 1. Unset core.hooksPath if set
31
+ Write-Host '[1/3] Removing core.hooksPath...' -ForegroundColor Yellow
32
+ $current = (& git config --get core.hooksPath 2>$null) -join ''
33
+ if ($current) {
34
+ & git config --unset core.hooksPath
35
+ Write-Host "Removed hooksPath = $current" -ForegroundColor Green
36
+ } else {
37
+ Write-Host 'No core.hooksPath was set; nothing to remove' -ForegroundColor Yellow
38
+ }
39
+
40
+ # 2. Restore .bak hooks if requested
41
+ Write-Host '[2/3] Restoring hook backups...' -ForegroundColor Yellow
42
+ if ($RestoreBackups) {
43
+ $restored = 0
44
+ foreach ($hook in 'pre-commit', 'commit-msg', 'pre-push') {
45
+ $bak = ".git/hooks/$hook.bak"
46
+ if (Test-Path -LiteralPath $bak -PathType Leaf) {
47
+ Move-Item -LiteralPath $bak -Destination ".git/hooks/$hook" -Force
48
+ Write-Host "Restored .git/hooks/$hook" -ForegroundColor Green
49
+ $restored++
50
+ }
51
+ }
52
+ if ($restored -eq 0) {
53
+ Write-Host 'No .bak hooks found to restore' -ForegroundColor Yellow
54
+ }
55
+ } else {
56
+ Write-Host 'Skipped (pass -RestoreBackups to enable)' -ForegroundColor Yellow
57
+ }
58
+
59
+ # 3. Purge ci-local/ if requested
60
+ Write-Host '[3/3] Purging ci-local/ directory...' -ForegroundColor Yellow
61
+ if ($Purge) {
62
+ if (Test-Path -LiteralPath '.ci-local' -PathType Container) {
63
+ Remove-Item -LiteralPath '.ci-local' -Recurse -Force
64
+ Write-Host 'Removed .ci-local/' -ForegroundColor Green
65
+ }
66
+ # Refuse to delete our own working directory.
67
+ if (Test-Path -LiteralPath 'ci-local' -PathType Container) {
68
+ $thisScriptDir = (Get-Item -LiteralPath $ScriptDir).FullName
69
+ $ciLocalDir = (Get-Item -LiteralPath (Join-Path $ProjectDir 'ci-local')).FullName
70
+ if ($thisScriptDir -eq $ciLocalDir) {
71
+ Write-Host 'WARNING: ci-local/ contains THIS uninstall script.' -ForegroundColor Yellow
72
+ Write-Host 'Refusing to delete it. Remove manually after this script exits.' -ForegroundColor Yellow
73
+ }
74
+ }
75
+ } else {
76
+ Write-Host 'Skipped (pass -Purge to delete directories)' -ForegroundColor Yellow
77
+ }
78
+
79
+ Write-Host ''
80
+ Write-Host 'Uninstall complete!' -ForegroundColor Green
81
+ Write-Host ''
82
+
83
+ } finally {
84
+ Pop-Location
85
+ }