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.
- package/README.md +45 -1
- package/ci-local/README.md +56 -11
- package/ci-local/ci-local.ps1 +445 -0
- package/ci-local/ci-local.sh +52 -9
- package/ci-local/docker/python.Dockerfile +7 -0
- package/ci-local/hooks/commit-msg +120 -44
- package/ci-local/hooks/commit-msg.test.sh +286 -0
- package/ci-local/hooks/pre-commit +9 -1
- package/ci-local/hooks/pre-push +13 -2
- package/ci-local/install.ps1 +367 -0
- package/ci-local/install.sh +151 -5
- package/ci-local/uninstall.ps1 +85 -0
- package/ci-local/uninstall.sh +95 -0
- package/dist/cli/dispatch/ci.d.ts +12 -0
- package/dist/cli/dispatch/ci.js +38 -0
- package/dist/cli/dispatch/security.d.ts +11 -0
- package/dist/cli/dispatch/security.js +68 -0
- package/dist/cli/dispatch/simple-renderers.d.ts +16 -0
- package/dist/cli/dispatch/simple-renderers.js +65 -0
- package/dist/cli/dispatch/skill-publish.d.ts +15 -0
- package/dist/cli/dispatch/skill-publish.js +44 -0
- package/dist/cli/dispatch/skills-cmd.d.ts +13 -0
- package/dist/cli/dispatch/skills-cmd.js +93 -0
- package/dist/cli/dispatch/tdd.d.ts +11 -0
- package/dist/cli/dispatch/tdd.js +45 -0
- package/dist/cli/dispatch/types.d.ts +29 -0
- package/dist/cli/dispatch/types.js +2 -0
- package/dist/cli/dispatch/workflow.d.ts +11 -0
- package/dist/cli/dispatch/workflow.js +67 -0
- package/dist/cli/help.d.ts +127 -0
- package/dist/cli/help.js +138 -0
- package/dist/cli/runtime.d.ts +16 -0
- package/dist/cli/runtime.js +38 -0
- package/dist/cli/validators.d.ts +17 -0
- package/dist/cli/validators.js +35 -0
- package/dist/commands/analyze.d.ts +1 -1
- package/dist/commands/analyze.js +14 -16
- package/dist/commands/ci.d.ts +50 -3
- package/dist/commands/ci.js +537 -182
- package/dist/commands/doctor.d.ts +2 -2
- package/dist/commands/doctor.js +112 -62
- package/dist/commands/init/report.d.ts +13 -0
- package/dist/commands/init/report.js +13 -0
- package/dist/commands/init/steps/agent-skills.d.ts +13 -0
- package/dist/commands/init/steps/agent-skills.js +45 -0
- package/dist/commands/init/steps/ai-sync.d.ts +15 -0
- package/dist/commands/init/steps/ai-sync.js +62 -0
- package/dist/commands/init/steps/ci.d.ts +24 -0
- package/dist/commands/init/steps/ci.js +74 -0
- package/dist/commands/init/steps/claude-md.d.ts +14 -0
- package/dist/commands/init/steps/claude-md.js +54 -0
- package/dist/commands/init/steps/code-graph.d.ts +16 -0
- package/dist/commands/init/steps/code-graph.js +68 -0
- package/dist/commands/init/steps/context-dir.d.ts +13 -0
- package/dist/commands/init/steps/context-dir.js +47 -0
- package/dist/commands/init/steps/docker-deploy.d.ts +16 -0
- package/dist/commands/init/steps/docker-deploy.js +60 -0
- package/dist/commands/init/steps/ghagga.d.ts +15 -0
- package/dist/commands/init/steps/ghagga.js +58 -0
- package/dist/commands/init/steps/git.d.ts +25 -0
- package/dist/commands/init/steps/git.js +81 -0
- package/dist/commands/init/steps/gitignore.d.ts +13 -0
- package/dist/commands/init/steps/gitignore.js +39 -0
- package/dist/commands/init/steps/local-ai.d.ts +14 -0
- package/dist/commands/init/steps/local-ai.js +53 -0
- package/dist/commands/init/steps/manifest.d.ts +13 -0
- package/dist/commands/init/steps/manifest.js +58 -0
- package/dist/commands/init/steps/memory.d.ts +15 -0
- package/dist/commands/init/steps/memory.js +60 -0
- package/dist/commands/init/steps/mock.d.ts +13 -0
- package/dist/commands/init/steps/mock.js +64 -0
- package/dist/commands/init/steps/sdd.d.ts +12 -0
- package/dist/commands/init/steps/sdd.js +40 -0
- package/dist/commands/init/steps/security.d.ts +28 -0
- package/dist/commands/init/steps/security.js +103 -0
- package/dist/commands/init/types.d.ts +30 -0
- package/dist/commands/init/types.js +2 -0
- package/dist/commands/init.d.ts +5 -4
- package/dist/commands/init.js +42 -387
- package/dist/commands/llmstxt.d.ts +1 -1
- package/dist/commands/llmstxt.js +36 -34
- package/dist/commands/plugin.d.ts +10 -1
- package/dist/commands/plugin.js +92 -47
- package/dist/commands/security.d.ts +11 -5
- package/dist/commands/security.js +236 -77
- package/dist/commands/skills/analysis.d.ts +19 -0
- package/dist/commands/skills/analysis.js +211 -0
- package/dist/commands/skills/benchmark.d.ts +6 -0
- package/dist/commands/skills/benchmark.js +82 -0
- package/dist/commands/skills/constants.d.ts +10 -0
- package/dist/commands/skills/constants.js +27 -0
- package/dist/commands/skills/directives.d.ts +21 -0
- package/dist/commands/skills/directives.js +101 -0
- package/dist/commands/skills/parsing.d.ts +16 -0
- package/dist/commands/skills/parsing.js +108 -0
- package/dist/commands/skills/rules.d.ts +5 -0
- package/dist/commands/skills/rules.js +32 -0
- package/dist/commands/skills/scoring.d.ts +47 -0
- package/dist/commands/skills/scoring.js +306 -0
- package/dist/commands/skills.d.ts +8 -69
- package/dist/commands/skills.js +9 -490
- package/dist/commands/tdd-pipeline.d.ts +17 -0
- package/dist/commands/tdd-pipeline.js +144 -0
- package/dist/commands/tdd.d.ts +1 -1
- package/dist/commands/tdd.js +21 -18
- package/dist/commands/workflow.d.ts +23 -0
- package/dist/commands/workflow.js +116 -0
- package/dist/constants.d.ts +15 -1
- package/dist/constants.js +161 -122
- package/dist/index.js +43 -280
- package/dist/lib/agent-skills.d.ts +36 -1
- package/dist/lib/agent-skills.js +168 -19
- package/dist/lib/atlassian-mcp.d.ts +42 -0
- package/dist/lib/atlassian-mcp.js +98 -0
- package/dist/lib/auto-skill-install.d.ts +37 -0
- package/dist/lib/auto-skill-install.js +91 -0
- package/dist/lib/auto-wire.d.ts +20 -0
- package/dist/lib/auto-wire.js +240 -0
- package/dist/lib/ci-config.d.ts +65 -0
- package/dist/lib/ci-config.js +297 -0
- package/dist/lib/claudemd.d.ts +13 -1
- package/dist/lib/claudemd.js +174 -24
- package/dist/lib/codex-export.d.ts +1 -1
- package/dist/lib/codex-export.js +26 -32
- package/dist/lib/common.d.ts +1 -1
- package/dist/lib/common.js +52 -44
- package/dist/lib/context.d.ts +17 -2
- package/dist/lib/context.js +142 -13
- package/dist/lib/crash-recovery.d.ts +34 -0
- package/dist/lib/crash-recovery.js +123 -0
- package/dist/lib/docker.d.ts +23 -2
- package/dist/lib/docker.js +168 -131
- package/dist/lib/exec.d.ts +10 -0
- package/dist/lib/exec.js +11 -0
- package/dist/lib/frontmatter.d.ts +1 -1
- package/dist/lib/frontmatter.js +29 -15
- package/dist/lib/harness-audit.d.ts +35 -0
- package/dist/lib/harness-audit.js +277 -0
- package/dist/lib/parallel-batch.d.ts +42 -0
- package/dist/lib/parallel-batch.js +88 -0
- package/dist/lib/plugin.d.ts +9 -3
- package/dist/lib/plugin.js +127 -70
- package/dist/lib/secret-scanner.d.ts +30 -0
- package/dist/lib/secret-scanner.js +274 -0
- package/dist/lib/security-analysis.d.ts +74 -0
- package/dist/lib/security-analysis.js +489 -0
- package/dist/lib/skill-publish.d.ts +40 -0
- package/dist/lib/skill-publish.js +164 -0
- package/dist/lib/skill-scanner.d.ts +63 -0
- package/dist/lib/skill-scanner.js +383 -0
- package/dist/lib/stack-detector.d.ts +38 -0
- package/dist/lib/stack-detector.js +207 -0
- package/dist/lib/supply-chain.d.ts +23 -0
- package/dist/lib/supply-chain.js +126 -0
- package/dist/lib/team-presets.d.ts +53 -0
- package/dist/lib/team-presets.js +201 -0
- package/dist/lib/template.d.ts +16 -1
- package/dist/lib/template.js +46 -17
- package/dist/lib/workflow/discovery.d.ts +27 -0
- package/dist/lib/workflow/discovery.js +85 -0
- package/dist/lib/workflow/index.d.ts +5 -0
- package/dist/lib/workflow/index.js +5 -0
- package/dist/lib/workflow/parser.d.ts +16 -0
- package/dist/lib/workflow/parser.js +198 -0
- package/dist/lib/workflow/renderer.d.ts +9 -0
- package/dist/lib/workflow/renderer.js +152 -0
- package/dist/lib/workflow/validator.d.ts +10 -0
- package/dist/lib/workflow/validator.js +189 -0
- package/dist/tasks/index.d.ts +4 -0
- package/dist/tasks/index.js +4 -0
- package/dist/tasks/scaffold-tasks.d.ts +3 -0
- package/dist/tasks/scaffold-tasks.js +14 -0
- package/dist/tasks/task-id.d.ts +30 -0
- package/dist/tasks/task-id.js +59 -0
- package/dist/tasks/task-tracker.d.ts +15 -0
- package/dist/tasks/task-tracker.js +81 -0
- package/dist/types/index.d.ts +134 -6
- package/dist/types/index.js +11 -1
- package/dist/ui/AnalyzeUI.d.ts +1 -1
- package/dist/ui/AnalyzeUI.js +38 -39
- package/dist/ui/App.d.ts +5 -3
- package/dist/ui/App.js +86 -46
- package/dist/ui/AutoSkills.d.ts +9 -0
- package/dist/ui/AutoSkills.js +124 -0
- package/dist/ui/CI.d.ts +2 -2
- package/dist/ui/CI.js +24 -26
- package/dist/ui/CIContext.d.ts +1 -1
- package/dist/ui/CIContext.js +2 -2
- package/dist/ui/CISelector.d.ts +2 -2
- package/dist/ui/CISelector.js +23 -15
- package/dist/ui/Doctor.d.ts +1 -1
- package/dist/ui/Doctor.js +35 -29
- package/dist/ui/Header.d.ts +1 -1
- package/dist/ui/Header.js +14 -14
- package/dist/ui/HookProfileSelector.d.ts +9 -0
- package/dist/ui/HookProfileSelector.js +54 -0
- package/dist/ui/LlmsTxt.d.ts +1 -1
- package/dist/ui/LlmsTxt.js +31 -22
- package/dist/ui/MemorySelector.d.ts +2 -2
- package/dist/ui/MemorySelector.js +28 -16
- package/dist/ui/NameInput.d.ts +1 -1
- package/dist/ui/NameInput.js +21 -21
- package/dist/ui/OptionSelector.d.ts +6 -2
- package/dist/ui/OptionSelector.js +83 -32
- package/dist/ui/Plugin.d.ts +4 -3
- package/dist/ui/Plugin.js +78 -35
- package/dist/ui/Progress.d.ts +3 -3
- package/dist/ui/Progress.js +23 -22
- package/dist/ui/Skills.d.ts +2 -2
- package/dist/ui/Skills.js +61 -32
- package/dist/ui/StackSelector.d.ts +2 -2
- package/dist/ui/StackSelector.js +27 -17
- package/dist/ui/Summary.d.ts +3 -3
- package/dist/ui/Summary.js +60 -50
- package/dist/ui/Welcome.d.ts +1 -1
- package/dist/ui/Welcome.js +15 -16
- package/dist/ui/theme.d.ts +1 -1
- package/dist/ui/theme.js +6 -6
- package/lib/common.psm1 +167 -0
- package/lib/common.sh +30 -0
- package/package.json +46 -24
- package/templates/common/atlassian/mcp-atlassian-snippet.json +16 -0
- package/templates/common/repoforge/mcp-repoforge-snippet.json +11 -0
- package/templates/common/repoforge/repoforge.yaml +34 -0
- package/templates/github/deploy-docker-zero-downtime.yml +140 -0
- package/templates/github/repoforge-graph.yml +45 -0
- package/templates/gitlab/deploy-docker-zero-downtime.yml +57 -0
- package/templates/local-ai/.env.example +17 -0
- package/templates/local-ai/docker-compose.yml +95 -0
- package/templates/security-hooks/claude-settings-security.json +30 -0
- package/templates/security-hooks/commit-msg-signing +29 -0
- package/templates/security-hooks/pre-commit-permissions +74 -0
- package/templates/security-hooks/pre-commit-secrets +74 -0
- package/templates/security-hooks/pre-push-branch-protection +62 -0
- package/templates/security-hooks/pre-push-deps +83 -0
- package/templates/security-hooks/pre-push-signing +67 -0
- package/templates/woodpecker/deploy-docker-zero-downtime.yml +50 -0
- package/templates/workflows/ci-pipeline.dot +15 -0
- package/templates/workflows/feature-flow.dot +21 -0
- package/templates/workflows/release.dot +16 -0
- package/dist/__integration__/helpers.d.ts +0 -20
- package/dist/__integration__/helpers.d.ts.map +0 -1
- package/dist/__integration__/helpers.js +0 -31
- package/dist/__integration__/helpers.js.map +0 -1
- package/dist/commands/analyze.d.ts.map +0 -1
- package/dist/commands/analyze.js.map +0 -1
- package/dist/commands/ci.d.ts.map +0 -1
- package/dist/commands/ci.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/llmstxt.d.ts.map +0 -1
- package/dist/commands/llmstxt.js.map +0 -1
- package/dist/commands/plugin.d.ts.map +0 -1
- package/dist/commands/plugin.js.map +0 -1
- package/dist/commands/security.d.ts.map +0 -1
- package/dist/commands/security.js.map +0 -1
- package/dist/commands/skills.d.ts.map +0 -1
- package/dist/commands/skills.js.map +0 -1
- package/dist/commands/tdd.d.ts.map +0 -1
- package/dist/commands/tdd.js.map +0 -1
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lib/agent-skills.d.ts.map +0 -1
- package/dist/lib/agent-skills.js.map +0 -1
- package/dist/lib/claudemd.d.ts.map +0 -1
- package/dist/lib/claudemd.js.map +0 -1
- package/dist/lib/codex-export.d.ts.map +0 -1
- package/dist/lib/codex-export.js.map +0 -1
- package/dist/lib/common.d.ts.map +0 -1
- package/dist/lib/common.js.map +0 -1
- package/dist/lib/context.d.ts.map +0 -1
- package/dist/lib/context.js.map +0 -1
- package/dist/lib/docker.d.ts.map +0 -1
- package/dist/lib/docker.js.map +0 -1
- package/dist/lib/frontmatter.d.ts.map +0 -1
- package/dist/lib/frontmatter.js.map +0 -1
- package/dist/lib/plugin.d.ts.map +0 -1
- package/dist/lib/plugin.js.map +0 -1
- package/dist/lib/template.d.ts.map +0 -1
- package/dist/lib/template.js.map +0 -1
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/ui/AnalyzeUI.d.ts.map +0 -1
- package/dist/ui/AnalyzeUI.js.map +0 -1
- package/dist/ui/App.d.ts.map +0 -1
- package/dist/ui/App.js.map +0 -1
- package/dist/ui/CI.d.ts.map +0 -1
- package/dist/ui/CI.js.map +0 -1
- package/dist/ui/CIContext.d.ts.map +0 -1
- package/dist/ui/CIContext.js.map +0 -1
- package/dist/ui/CISelector.d.ts.map +0 -1
- package/dist/ui/CISelector.js.map +0 -1
- package/dist/ui/Doctor.d.ts.map +0 -1
- package/dist/ui/Doctor.js.map +0 -1
- package/dist/ui/Header.d.ts.map +0 -1
- package/dist/ui/Header.js.map +0 -1
- package/dist/ui/LlmsTxt.d.ts.map +0 -1
- package/dist/ui/LlmsTxt.js.map +0 -1
- package/dist/ui/MemorySelector.d.ts.map +0 -1
- package/dist/ui/MemorySelector.js.map +0 -1
- package/dist/ui/NameInput.d.ts.map +0 -1
- package/dist/ui/NameInput.js.map +0 -1
- package/dist/ui/OptionSelector.d.ts.map +0 -1
- package/dist/ui/OptionSelector.js.map +0 -1
- package/dist/ui/Plugin.d.ts.map +0 -1
- package/dist/ui/Plugin.js.map +0 -1
- package/dist/ui/Progress.d.ts.map +0 -1
- package/dist/ui/Progress.js.map +0 -1
- package/dist/ui/Skills.d.ts.map +0 -1
- package/dist/ui/Skills.js.map +0 -1
- package/dist/ui/StackSelector.d.ts.map +0 -1
- package/dist/ui/StackSelector.js.map +0 -1
- package/dist/ui/Summary.d.ts.map +0 -1
- package/dist/ui/Summary.js.map +0 -1
- package/dist/ui/Welcome.d.ts.map +0 -1
- package/dist/ui/Welcome.js.map +0 -1
- package/dist/ui/theme.d.ts.map +0 -1
- package/dist/ui/theme.js.map +0 -1
package/ci-local/hooks/pre-push
CHANGED
|
@@ -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
|
+
}
|
package/ci-local/install.sh
CHANGED
|
@@ -8,18 +8,164 @@ set -e
|
|
|
8
8
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
9
9
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
10
10
|
|
|
11
|
-
#
|
|
12
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|