zwischen 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/.zwischen.yml.example +2 -2
- data/CHANGELOG.md +31 -2
- data/DEVELOPMENT.md +22 -20
- data/README.md +7 -4
- data/TESTING.md +26 -0
- data/lib/zwischen/ai/analyzer.rb +5 -3
- data/lib/zwischen/cli.rb +15 -3
- data/lib/zwischen/git_diff.rb +19 -7
- data/lib/zwischen/hooks.rb +13 -0
- data/lib/zwischen/reporter/terminal.rb +2 -2
- data/lib/zwischen/setup.rb +6 -2
- data/lib/zwischen/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb7b4a9cea5d36d3f7fe7414f3034d5f3fc6eaa860cec2136b303e9bcea2877c
|
|
4
|
+
data.tar.gz: d45c2d6d9c962508996eca3a618fe54c3e0692dde54b6dad347f020d9251f346
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a02881f9e8f76961a655e5d054d91166c3f3842abb8e1ce9fa31252dc2cb742e1a4014699631dbf9116f06d5cc6ef347d54dec09c77313949a8d866b027c6646
|
|
7
|
+
data.tar.gz: bf3dd2d05ab9d21ba880f2d9990ced3d65d97ee5846ee2135d33372feedbd6a9a8c66a8f968e3750d268f475ddc1d1b090fdd1043a8567956c9072d42cbb8a01
|
data/.zwischen.yml.example
CHANGED
|
@@ -11,6 +11,7 @@ ai:
|
|
|
11
11
|
ollama:
|
|
12
12
|
model: llama3
|
|
13
13
|
url: http://localhost:11434
|
|
14
|
+
# timeout: 180 # seconds; raise for large local models that load slowly
|
|
14
15
|
|
|
15
16
|
openai:
|
|
16
17
|
model: gpt-4
|
|
@@ -35,11 +36,10 @@ scanners:
|
|
|
35
36
|
# enabled: true
|
|
36
37
|
# semgrep:
|
|
37
38
|
# enabled: true
|
|
38
|
-
# config: p/security-audit #
|
|
39
|
+
# config: p/security-audit,p/expressjs # Comma-separated open rulesets, no login required
|
|
39
40
|
# # Other options: p/secrets, p/owasp-top-ten, or path to custom ruleset
|
|
40
41
|
|
|
41
42
|
# Ignored Paths — findings in matching paths are dropped before reporting.
|
|
42
|
-
|
|
43
43
|
ignore:
|
|
44
44
|
- "**/vendor/**"
|
|
45
45
|
- "**/node_modules/**"
|
data/CHANGELOG.md
CHANGED
|
@@ -7,15 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.1.
|
|
10
|
+
## [0.1.1] - 2026-06-11
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `blocking.severity` is now honored by manual terminal scans — `critical`
|
|
14
|
+
and `none` previously behaved like `high` because the CLI never passed
|
|
15
|
+
config to the terminal reporter
|
|
16
|
+
- Hooks install where git actually executes them: `core.hooksPath`
|
|
17
|
+
(husky/pre-commit setups) and linked worktrees are now resolved via
|
|
18
|
+
`git rev-parse --git-path hooks` — previously the hook was written to
|
|
19
|
+
`.git/hooks` and silently never ran under `core.hooksPath`, and
|
|
20
|
+
worktrees skipped installation entirely
|
|
21
|
+
- `--format json` file paths are project-relative, matching terminal and
|
|
22
|
+
SARIF output
|
|
23
|
+
- AI response parsing strips markdown code fences, so small local models
|
|
24
|
+
that wrap JSON in ``` blocks still produce annotations
|
|
25
|
+
- npm/pip wrappers: `ignore:` globs are enforced, `--format json` emits
|
|
26
|
+
pure JSON with a `summary` key and relative paths, `--format sarif`
|
|
27
|
+
fails fast with a clear error, and pip `zwischen --version` no longer
|
|
28
|
+
crashes
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- `scan --changed` now includes staged and untracked files; pre-push
|
|
32
|
+
keeps committed-range semantics since only commits get pushed
|
|
33
|
+
|
|
34
|
+
## [0.1.0] - 2026-06-11
|
|
11
35
|
|
|
12
36
|
### Added
|
|
13
37
|
- Initial release
|
|
14
38
|
- Gitleaks and Semgrep scanner orchestration with auto-install
|
|
15
39
|
- AI-powered finding triage via Ollama, OpenAI, or Anthropic
|
|
16
40
|
- Pre-push git hook that blocks pushes introducing secrets or security issues
|
|
41
|
+
- `--changed` flag to scan only files changed since the default branch
|
|
42
|
+
- SARIF 2.1.0 output (`--format sarif`) for GitHub code scanning
|
|
43
|
+
- Composite GitHub Action (`uses: cjordan223/zwischen@main`)
|
|
44
|
+
- Ignore globs in `.zwischen.yml` enforced at the orchestrator level
|
|
17
45
|
- Project type detection (Next.js, React, Django, Rails, and more)
|
|
18
46
|
- npm (`zwischen`) and pip (`zwischen-cli`) wrapper packages
|
|
19
47
|
|
|
20
|
-
[Unreleased]: https://github.com/cjordan223/zwischen/compare/v0.1.
|
|
48
|
+
[Unreleased]: https://github.com/cjordan223/zwischen/compare/v0.1.1...HEAD
|
|
49
|
+
[0.1.1]: https://github.com/cjordan223/zwischen/compare/v0.1.0...v0.1.1
|
|
21
50
|
[0.1.0]: https://github.com/cjordan223/zwischen/releases/tag/v0.1.0
|
data/DEVELOPMENT.md
CHANGED
|
@@ -20,7 +20,7 @@ bin/zwischen
|
|
|
20
20
|
-> Scanner::Semgrep
|
|
21
21
|
-> Finding::Aggregator
|
|
22
22
|
-> AI::Analyzer, when enabled
|
|
23
|
-
-> Reporter::Terminal
|
|
23
|
+
-> Reporter::Terminal (or Reporter::Sarif for --format sarif)
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
`zwischen init` follows a separate path:
|
|
@@ -34,7 +34,7 @@ CLI#init
|
|
|
34
34
|
-> Config.init creates .zwischen.yml
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
`zwischen scan --pre-push` uses `GitDiff.changed_files`, passes those files to scanner adapters, filters findings to changed files again as a safety net, and only prints compact output for blocking findings.
|
|
37
|
+
`zwischen scan --pre-push` uses `GitDiff.changed_files`, passes those files to scanner adapters, filters findings to changed files again as a safety net, and only prints compact output for blocking findings. `zwischen scan --changed` applies the same changed-files scoping to a manual scan with full output.
|
|
38
38
|
|
|
39
39
|
## Config Contract
|
|
40
40
|
|
|
@@ -90,8 +90,8 @@ Package-wrapper parity changes:
|
|
|
90
90
|
## Known Iteration Points
|
|
91
91
|
|
|
92
92
|
- Ruby `Hooks.handle_existing_hook` has backup/append/skip logic, but `Setup#install_hook` currently backs up and replaces existing non-Zwischen hooks directly.
|
|
93
|
-
- Ruby config exposes `
|
|
94
|
-
- npm and pip wrappers do not yet match Ruby feature parity. They do not support `uninstall`, `--only`, Ruby's changed-file pre-push filtering, or the Ruby JSON summary shape.
|
|
93
|
+
- Ruby config exposes `severity.fail_on`, but blocking decisions use `blocking.severity`. (`ignore` globs are enforced by the orchestrator.)
|
|
94
|
+
- npm and pip wrappers do not yet match Ruby feature parity. They do not support `uninstall`, `--only`, `--changed`, `--format sarif`, Ruby's changed-file pre-push filtering, or the Ruby JSON summary shape.
|
|
95
95
|
- npm and pip wrappers default AI provider to Ollama, while Ruby defaults to Claude.
|
|
96
96
|
|
|
97
97
|
## Verification
|
|
@@ -110,12 +110,12 @@ Installed-gem workflow:
|
|
|
110
110
|
|
|
111
111
|
Then follow `TESTING.md` from a temporary directory outside this repository.
|
|
112
112
|
|
|
113
|
-
npm wrapper smoke checks:
|
|
113
|
+
npm wrapper smoke checks (mirrors CI):
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
cd packages/npm
|
|
117
|
-
npm test
|
|
118
117
|
node bin/zwischen.js --help
|
|
118
|
+
npm pack --dry-run
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
pip wrapper smoke checks:
|
|
@@ -134,21 +134,23 @@ version tag:
|
|
|
134
134
|
```bash
|
|
135
135
|
# bump lib/zwischen/version.rb, packages/npm/package.json,
|
|
136
136
|
# packages/pip/pyproject.toml, and CHANGELOG.md first
|
|
137
|
-
git tag
|
|
138
|
-
git push origin
|
|
137
|
+
git tag vX.Y.Z
|
|
138
|
+
git push origin vX.Y.Z
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
Registry setup (already configured as of v0.1.0; reference for new registries
|
|
142
|
+
or token rotation):
|
|
142
143
|
|
|
143
|
-
1. **RubyGems** —
|
|
144
|
-
<https://rubygems.org/profile/oidc/pending_trusted_publishers>:
|
|
144
|
+
1. **RubyGems** — trusted publisher on the `zwischen` gem:
|
|
145
145
|
repository `cjordan223/zwischen`, workflow `release.yml`, environment `release`.
|
|
146
|
-
2. **PyPI** —
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`
|
|
153
|
-
|
|
154
|
-
|
|
146
|
+
2. **PyPI** — trusted publisher on the `zwischen-cli` project, same
|
|
147
|
+
repository, workflow, and environment. (The bare `zwischen` name is taken
|
|
148
|
+
on PyPI by an unrelated package, so the distribution is `zwischen-cli`;
|
|
149
|
+
the installed command is still `zwischen`.)
|
|
150
|
+
3. **npm** — granular access token saved as the `NPM_TOKEN` repository
|
|
151
|
+
secret. Must have read/write on the `zwischen` package and **bypass 2FA**
|
|
152
|
+
enabled, or CI publishes fail with `EOTP`. When rotating, set the secret
|
|
153
|
+
via the interactive prompt (`gh secret set NPM_TOKEN`) — never pass the
|
|
154
|
+
token as a command argument.
|
|
155
|
+
4. **GitHub** — the `release` environment in repo settings, so OIDC claims
|
|
156
|
+
match the workflows.
|
data/README.md
CHANGED
|
@@ -67,8 +67,11 @@ model via Ollama.
|
|
|
67
67
|
|
|
68
68
|
Manual scans use AI when `--ai` is passed or config enables it. Pre-push
|
|
69
69
|
scans stay scanner-only unless `ai.pre_push_enabled: true` — blocking
|
|
70
|
-
decisions should be fast and deterministic.
|
|
71
|
-
|
|
70
|
+
decisions should be fast and deterministic. Annotation quality is
|
|
71
|
+
model-dependent: larger models give reliably structured, accurate triage,
|
|
72
|
+
while very small local models may fail to annotate. If AI is unavailable or
|
|
73
|
+
unparseable, scans always fall back to raw findings. The design rationale
|
|
74
|
+
is in [docs/design.md](docs/design.md).
|
|
72
75
|
|
|
73
76
|
## Commands
|
|
74
77
|
|
|
@@ -76,7 +79,7 @@ decisions should be fast and deterministic. The design rationale is in
|
|
|
76
79
|
| --- | --- |
|
|
77
80
|
| `zwischen init` | Installs/checks tools, creates config, installs pre-push hook (backs up an existing non-Zwischen hook). |
|
|
78
81
|
| `zwischen scan` | Runs enabled scanners, prints a terminal report. |
|
|
79
|
-
| `zwischen scan --changed` | Scans only files changed since the default branch. |
|
|
82
|
+
| `zwischen scan --changed` | Scans only files changed since the default branch, including staged and untracked files. |
|
|
80
83
|
| `zwischen scan --only secrets,sast` | Limits to Gitleaks (`secrets`) and/or Semgrep (`sast`). |
|
|
81
84
|
| `zwischen scan --ai <provider>` | Adds AI prioritization, fix suggestions, false-positive detection. |
|
|
82
85
|
| `zwischen scan --format json` | Machine-readable summary + findings. |
|
|
@@ -195,7 +198,7 @@ docs/ Design write-up, triage example, demo GIF
|
|
|
195
198
|
## Development
|
|
196
199
|
|
|
197
200
|
```bash
|
|
198
|
-
bundle exec rspec #
|
|
201
|
+
bundle exec rspec # 212 examples
|
|
199
202
|
./scripts/test_as_gem.sh # install and exercise as a real gem
|
|
200
203
|
```
|
|
201
204
|
|
data/TESTING.md
CHANGED
|
@@ -177,6 +177,30 @@ Expected:
|
|
|
177
177
|
- Pre-push mode only reports findings from files returned by `GitDiff.changed_files`.
|
|
178
178
|
- Uncommitted files outside that diff are not reported.
|
|
179
179
|
|
|
180
|
+
### Test 3.5: SARIF Output
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
zwischen scan --format sarif
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Expected:
|
|
187
|
+
|
|
188
|
+
- Prints valid SARIF 2.1.0 JSON (`version`, `runs[0].tool.driver.name == "Zwischen"`).
|
|
189
|
+
- File URIs are project-relative.
|
|
190
|
+
- Exit code still reflects configured blocking behavior.
|
|
191
|
+
- With no findings, prints an empty SARIF document and exits `0`.
|
|
192
|
+
|
|
193
|
+
### Test 3.6: Changed-Only Manual Scan
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
zwischen scan --changed
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Expected:
|
|
200
|
+
|
|
201
|
+
- Only files changed since the default branch are scanned and reported.
|
|
202
|
+
- Exits `0` silently when there are no changed files.
|
|
203
|
+
|
|
180
204
|
## Test Suite 4: Blocking Configuration
|
|
181
205
|
|
|
182
206
|
### Test 4.1: Default Blocking
|
|
@@ -347,6 +371,8 @@ Expected:
|
|
|
347
371
|
- Test 3.2:
|
|
348
372
|
- Test 3.3:
|
|
349
373
|
- Test 3.4:
|
|
374
|
+
- Test 3.5:
|
|
375
|
+
- Test 3.6:
|
|
350
376
|
- Test 4.1:
|
|
351
377
|
- Test 4.2:
|
|
352
378
|
- Test 4.3:
|
data/lib/zwischen/ai/analyzer.rb
CHANGED
|
@@ -80,9 +80,11 @@ module Zwischen
|
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def enhance_findings(findings, ai_response)
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
|
|
83
|
+
# Models (especially small local ones) often wrap JSON in markdown
|
|
84
|
+
# fences or prose; strip fences first, then extract the outermost
|
|
85
|
+
# JSON object.
|
|
86
|
+
cleaned = ai_response.to_s.gsub(/```(?:json)?/i, "")
|
|
87
|
+
json_match = cleaned.match(/\{[\s\S]*\}/m)
|
|
86
88
|
return findings unless json_match
|
|
87
89
|
|
|
88
90
|
ai_analysis = JSON.parse(json_match[0])
|
data/lib/zwischen/cli.rb
CHANGED
|
@@ -91,7 +91,7 @@ module Zwischen
|
|
|
91
91
|
|
|
92
92
|
changed_files = nil
|
|
93
93
|
if pre_push || options[:changed]
|
|
94
|
-
changed_files = GitDiff.changed_files
|
|
94
|
+
changed_files = GitDiff.changed_files(include_working_tree: !pre_push)
|
|
95
95
|
changed_files = changed_files.select do |path|
|
|
96
96
|
candidate = path
|
|
97
97
|
candidate = File.join(project[:root], candidate) unless Pathname.new(candidate).absolute?
|
|
@@ -173,7 +173,7 @@ module Zwischen
|
|
|
173
173
|
require "json"
|
|
174
174
|
puts JSON.pretty_generate({
|
|
175
175
|
summary: aggregated[:summary],
|
|
176
|
-
findings: aggregated[:findings].map(
|
|
176
|
+
findings: aggregated[:findings].map { |f| f.to_h.merge(file: relative_path(f.file, project[:root])) }
|
|
177
177
|
})
|
|
178
178
|
blocking_severity = config.blocking_severity
|
|
179
179
|
exit_code = aggregated[:findings].any? { |f| should_block?(f, blocking_severity, ai_enabled) } ? 1 : 0
|
|
@@ -187,7 +187,7 @@ module Zwischen
|
|
|
187
187
|
if pre_push
|
|
188
188
|
exit_code = Reporter::Terminal.report_compact(aggregated, config: config, ai_enabled: ai_enabled)
|
|
189
189
|
else
|
|
190
|
-
exit_code = Reporter::Terminal.report(aggregated, ai_enabled: ai_enabled)
|
|
190
|
+
exit_code = Reporter::Terminal.report(aggregated, config: config, ai_enabled: ai_enabled)
|
|
191
191
|
end
|
|
192
192
|
exit exit_code
|
|
193
193
|
end
|
|
@@ -206,6 +206,18 @@ module Zwischen
|
|
|
206
206
|
|
|
207
207
|
private
|
|
208
208
|
|
|
209
|
+
# Scanners may emit absolute (and symlink-resolved) paths; report
|
|
210
|
+
# machine-readable output relative to the project root like the
|
|
211
|
+
# terminal and SARIF reporters do.
|
|
212
|
+
def relative_path(path, project_root)
|
|
213
|
+
expanded = File.expand_path(path.to_s)
|
|
214
|
+
roots = [project_root, (File.realpath(project_root) rescue project_root)].uniq
|
|
215
|
+
roots.each do |root|
|
|
216
|
+
return expanded.delete_prefix("#{root}/") if expanded.start_with?("#{root}/")
|
|
217
|
+
end
|
|
218
|
+
path.to_s
|
|
219
|
+
end
|
|
220
|
+
|
|
209
221
|
def should_block?(finding, blocking_severity, ai_enabled)
|
|
210
222
|
return false if ai_enabled && finding.raw_data["ai_false_positive"]
|
|
211
223
|
|
data/lib/zwischen/git_diff.rb
CHANGED
|
@@ -17,19 +17,31 @@ module Zwischen
|
|
|
17
17
|
"HEAD"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# include_working_tree: also count staged and untracked files. Used by
|
|
21
|
+
# manual `scan --changed`; pre-push keeps committed-range semantics
|
|
22
|
+
# because only committed changes get pushed.
|
|
23
|
+
def self.changed_files(remote: nil, local: "HEAD", include_working_tree: false)
|
|
21
24
|
branch = remote || default_branch
|
|
22
25
|
remote_ref = "origin/#{branch}"
|
|
23
26
|
|
|
27
|
+
files = []
|
|
28
|
+
|
|
24
29
|
# Try remote diff first
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
committed = `git diff --name-only #{remote_ref}...#{local} 2>/dev/null`.strip.split("\n")
|
|
31
|
+
if $?.success? && !committed.empty?
|
|
32
|
+
files = committed
|
|
33
|
+
else
|
|
34
|
+
# Fallback: local diff
|
|
35
|
+
local_diff = `git diff --name-only HEAD@{1}...#{local} 2>/dev/null`.strip.split("\n")
|
|
36
|
+
files = local_diff if $?.success?
|
|
37
|
+
end
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
if include_working_tree
|
|
40
|
+
working = `git status --porcelain 2>/dev/null`.strip.split("\n").map { |l| l[3..]&.strip }.compact
|
|
41
|
+
files |= working if $?.success?
|
|
42
|
+
end
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
files.reject { |f| f.nil? || f.empty? }
|
|
33
45
|
rescue StandardError => e
|
|
34
46
|
warn "Failed to get changed files: #{e.message}" if ENV["DEBUG"]
|
|
35
47
|
[]
|
data/lib/zwischen/hooks.rb
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "open3"
|
|
4
5
|
|
|
5
6
|
module Zwischen
|
|
6
7
|
class Hooks
|
|
7
8
|
HOOK_MARKER = "Zwischen pre-push hook"
|
|
8
9
|
|
|
10
|
+
# Resolve the hooks directory through git itself so the hook lands where
|
|
11
|
+
# git will actually execute it — this respects core.hooksPath (husky,
|
|
12
|
+
# pre-commit framework, etc.) and linked worktrees, where .git is a file.
|
|
9
13
|
def self.hook_path(project_root = Dir.pwd)
|
|
14
|
+
stdout, _stderr, status = Open3.capture3(
|
|
15
|
+
"git", "rev-parse", "--git-path", "hooks", chdir: project_root
|
|
16
|
+
)
|
|
17
|
+
hooks_dir = status.success? ? stdout.strip : nil
|
|
18
|
+
hooks_dir = File.join(".git", "hooks") if hooks_dir.nil? || hooks_dir.empty?
|
|
19
|
+
hooks_dir = File.expand_path(hooks_dir, project_root)
|
|
20
|
+
|
|
21
|
+
File.join(hooks_dir, "pre-push")
|
|
22
|
+
rescue StandardError
|
|
10
23
|
File.join(project_root, ".git", "hooks", "pre-push")
|
|
11
24
|
end
|
|
12
25
|
|
|
@@ -22,8 +22,8 @@ module Zwischen
|
|
|
22
22
|
"info" => "ℹ️ INFO"
|
|
23
23
|
}.freeze
|
|
24
24
|
|
|
25
|
-
def self.report(aggregated_results, ai_enabled: false)
|
|
26
|
-
new(aggregated_results, ai_enabled: ai_enabled).report
|
|
25
|
+
def self.report(aggregated_results, config: nil, ai_enabled: false)
|
|
26
|
+
new(aggregated_results, ai_enabled: ai_enabled, config: config).report
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def self.report_compact(aggregated_results, config:, ai_enabled: false)
|
data/lib/zwischen/setup.rb
CHANGED
|
@@ -113,14 +113,18 @@ module Zwischen
|
|
|
113
113
|
|
|
114
114
|
def install_hook
|
|
115
115
|
project_root = Dir.pwd
|
|
116
|
-
git_dir = File.join(project_root, ".git")
|
|
117
116
|
|
|
118
|
-
|
|
117
|
+
# File check alone breaks linked worktrees, where .git is a file
|
|
118
|
+
unless File.exist?(File.join(project_root, ".git"))
|
|
119
119
|
@shell.say(" ⚠️ No .git directory found. Skipping hook installation.", :yellow)
|
|
120
120
|
return false
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
hook_path = Hooks.hook_path(project_root)
|
|
124
|
+
default_path = File.expand_path(File.join(project_root, ".git", "hooks", "pre-push"))
|
|
125
|
+
if File.expand_path(hook_path) != default_path
|
|
126
|
+
@shell.say(" ↳ Git hooks are redirected (core.hooksPath or worktree); installing to #{hook_path}", :yellow)
|
|
127
|
+
end
|
|
124
128
|
|
|
125
129
|
if File.exist?(hook_path)
|
|
126
130
|
if Hooks.zwischen_hook?(hook_path)
|
data/lib/zwischen/version.rb
CHANGED