shipwright-cli 1.9.0 → 2.0.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/.claude/hooks/post-tool-use.sh +12 -5
- package/README.md +114 -36
- package/completions/_shipwright +212 -32
- package/completions/shipwright.bash +97 -25
- package/docs/strategy/01-market-research.md +619 -0
- package/docs/strategy/02-mission-and-brand.md +587 -0
- package/docs/strategy/03-gtm-and-roadmap.md +759 -0
- package/docs/strategy/QUICK-START.txt +289 -0
- package/docs/strategy/README.md +172 -0
- package/package.json +4 -2
- package/scripts/sw +217 -2
- package/scripts/sw-activity.sh +500 -0
- package/scripts/sw-adaptive.sh +925 -0
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +613 -0
- package/scripts/sw-autonomous.sh +664 -0
- package/scripts/sw-changelog.sh +704 -0
- package/scripts/sw-checkpoint.sh +79 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +192 -7
- package/scripts/sw-code-review.sh +637 -0
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +605 -0
- package/scripts/sw-cost.sh +1 -1
- package/scripts/sw-daemon.sh +812 -138
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +540 -0
- package/scripts/sw-decompose.sh +539 -0
- package/scripts/sw-deps.sh +551 -0
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +412 -0
- package/scripts/sw-docs-agent.sh +539 -0
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +59 -1
- package/scripts/sw-dora.sh +615 -0
- package/scripts/sw-durable.sh +710 -0
- package/scripts/sw-e2e-orchestrator.sh +535 -0
- package/scripts/sw-eventbus.sh +393 -0
- package/scripts/sw-feedback.sh +471 -0
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +567 -0
- package/scripts/sw-fleet-viz.sh +404 -0
- package/scripts/sw-fleet.sh +8 -1
- package/scripts/sw-github-app.sh +596 -0
- package/scripts/sw-github-checks.sh +1 -1
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +569 -0
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +559 -0
- package/scripts/sw-incident.sh +617 -0
- package/scripts/sw-init.sh +88 -1
- package/scripts/sw-instrument.sh +699 -0
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +366 -31
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +507 -51
- package/scripts/sw-memory.sh +198 -3
- package/scripts/sw-mission-control.sh +487 -0
- package/scripts/sw-model-router.sh +545 -0
- package/scripts/sw-otel.sh +596 -0
- package/scripts/sw-oversight.sh +689 -0
- package/scripts/sw-pipeline-composer.sh +8 -8
- package/scripts/sw-pipeline-vitals.sh +1096 -0
- package/scripts/sw-pipeline.sh +2451 -180
- package/scripts/sw-pm.sh +693 -0
- package/scripts/sw-pr-lifecycle.sh +522 -0
- package/scripts/sw-predictive.sh +1 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +4 -3
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +5 -3
- package/scripts/sw-recruit.sh +573 -0
- package/scripts/sw-regression.sh +642 -0
- package/scripts/sw-release-manager.sh +736 -0
- package/scripts/sw-release.sh +706 -0
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +520 -0
- package/scripts/sw-retro.sh +691 -0
- package/scripts/sw-scale.sh +444 -0
- package/scripts/sw-security-audit.sh +505 -0
- package/scripts/sw-self-optimize.sh +109 -8
- package/scripts/sw-session.sh +31 -9
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +192 -1
- package/scripts/sw-strategic.sh +658 -0
- package/scripts/sw-stream.sh +450 -0
- package/scripts/sw-swarm.sh +583 -0
- package/scripts/sw-team-stages.sh +511 -0
- package/scripts/sw-templates.sh +1 -1
- package/scripts/sw-testgen.sh +515 -0
- package/scripts/sw-tmux-pipeline.sh +554 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +485 -0
- package/scripts/sw-tracker-github.sh +188 -0
- package/scripts/sw-tracker-jira.sh +172 -0
- package/scripts/sw-tracker-linear.sh +251 -0
- package/scripts/sw-tracker.sh +117 -2
- package/scripts/sw-triage.sh +603 -0
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +677 -0
- package/scripts/sw-webhook.sh +627 -0
- package/scripts/sw-widgets.sh +530 -0
- package/scripts/sw-worktree.sh +1 -1
- package/templates/pipelines/autonomous.json +8 -1
- package/templates/pipelines/cost-aware.json +21 -0
- package/templates/pipelines/deployed.json +40 -6
- package/templates/pipelines/enterprise.json +16 -2
- package/templates/pipelines/fast.json +19 -0
- package/templates/pipelines/full.json +16 -2
- package/templates/pipelines/hotfix.json +19 -0
- package/templates/pipelines/standard.json +19 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ Autonomous Code Review Agent — Clean Code & Architecture Analysis ║
|
|
4
|
+
# ║ Quality enforcement: code smells, SOLID, layer boundaries, complexity ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="2.0.0"
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|
|
14
|
+
CYAN='\033[38;2;0;212;255m' # #00d4ff — primary accent
|
|
15
|
+
PURPLE='\033[38;2;124;58;237m' # #7c3aed — secondary
|
|
16
|
+
BLUE='\033[38;2;0;102;255m' # #0066ff — tertiary
|
|
17
|
+
GREEN='\033[38;2;74;222;128m' # success
|
|
18
|
+
YELLOW='\033[38;2;250;204;21m' # warning
|
|
19
|
+
RED='\033[38;2;248;113;113m' # error
|
|
20
|
+
DIM='\033[2m'
|
|
21
|
+
BOLD='\033[1m'
|
|
22
|
+
RESET='\033[0m'
|
|
23
|
+
|
|
24
|
+
# ─── Cross-platform compatibility ──────────────────────────────────────────
|
|
25
|
+
# shellcheck source=lib/compat.sh
|
|
26
|
+
[[ -f "$SCRIPT_DIR/lib/compat.sh" ]] && source "$SCRIPT_DIR/lib/compat.sh"
|
|
27
|
+
|
|
28
|
+
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
29
|
+
info() { echo -e "${CYAN}${BOLD}▸${RESET} $*"; }
|
|
30
|
+
success() { echo -e "${GREEN}${BOLD}✓${RESET} $*"; }
|
|
31
|
+
warn() { echo -e "${YELLOW}${BOLD}⚠${RESET} $*"; }
|
|
32
|
+
error() { echo -e "${RED}${BOLD}✗${RESET} $*" >&2; }
|
|
33
|
+
|
|
34
|
+
emit_event() {
|
|
35
|
+
local type="$1"; shift
|
|
36
|
+
local entry="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$type\""
|
|
37
|
+
while [[ $# -gt 0 ]]; do
|
|
38
|
+
local key="${1%%=*}"; local val="${1#*=}"
|
|
39
|
+
val="${val//\"/\\\"}"
|
|
40
|
+
entry="$entry,\"${key}\":\"${val}\""
|
|
41
|
+
shift
|
|
42
|
+
done
|
|
43
|
+
entry="$entry}"
|
|
44
|
+
mkdir -p "$HOME/.shipwright"
|
|
45
|
+
echo "$entry" >> "$HOME/.shipwright/events.jsonl"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ─── Configuration ───────────────────────────────────────────────────────
|
|
49
|
+
REVIEW_CONFIG="${REPO_DIR}/.claude/code-review.json"
|
|
50
|
+
QUALITY_METRICS_FILE="${REPO_DIR}/.claude/pipeline-artifacts/quality-metrics.json"
|
|
51
|
+
TRENDS_FILE="${HOME}/.shipwright/code-review-trends.jsonl"
|
|
52
|
+
STRICTNESS="${STRICTNESS:-normal}" # relaxed, normal, strict
|
|
53
|
+
|
|
54
|
+
init_config() {
|
|
55
|
+
[[ -f "$REVIEW_CONFIG" ]] && return 0
|
|
56
|
+
mkdir -p "${REPO_DIR}/.claude"
|
|
57
|
+
cat > "$REVIEW_CONFIG" <<'EOF'
|
|
58
|
+
{
|
|
59
|
+
"strictness": "normal",
|
|
60
|
+
"ignore_patterns": ["test", "vendor", "node_modules", "dist", "build"],
|
|
61
|
+
"rules": {
|
|
62
|
+
"max_function_lines": 60,
|
|
63
|
+
"max_nesting_depth": 4,
|
|
64
|
+
"max_cyclomatic_complexity": 10,
|
|
65
|
+
"long_variable_name_chars": 25,
|
|
66
|
+
"magic_number_detection": true
|
|
67
|
+
},
|
|
68
|
+
"enabled_checks": [
|
|
69
|
+
"code_smells",
|
|
70
|
+
"solid_violations",
|
|
71
|
+
"architecture_boundaries",
|
|
72
|
+
"complexity_metrics",
|
|
73
|
+
"style_consistency",
|
|
74
|
+
"auto_fix_simple"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
EOF
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
load_config() {
|
|
81
|
+
if [[ -f "$REVIEW_CONFIG" ]]; then
|
|
82
|
+
STRICTNESS=$(jq -r '.strictness // "normal"' "$REVIEW_CONFIG" 2>/dev/null || echo "normal")
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# ─── Code Smell Detection ────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
detect_code_smells() {
|
|
89
|
+
local target_file="$1"
|
|
90
|
+
local issues=()
|
|
91
|
+
|
|
92
|
+
[[ ! -f "$target_file" ]] && return 0
|
|
93
|
+
|
|
94
|
+
local ext="${target_file##*.}"
|
|
95
|
+
[[ "$ext" != "sh" ]] && return 0
|
|
96
|
+
|
|
97
|
+
# Check 1: Long functions (>60 lines in bash)
|
|
98
|
+
local func_count
|
|
99
|
+
func_count=$(grep -c "^[a-zA-Z_][a-zA-Z0-9_]*().*{" "$target_file" 2>/dev/null || echo "0")
|
|
100
|
+
if [[ "$func_count" -gt 0 ]]; then
|
|
101
|
+
while IFS= read -r line; do
|
|
102
|
+
if [[ "$line" =~ ^[a-zA-Z_][a-zA-Z0-9_]*\(\).*\{ ]]; then
|
|
103
|
+
local func_name="${line%%(*}"
|
|
104
|
+
local start_line
|
|
105
|
+
start_line=$(grep -n "^${func_name}()" "$target_file" | head -1 | cut -d: -f1)
|
|
106
|
+
local end_line
|
|
107
|
+
end_line=$(awk "NR>$start_line && /^}/ {print NR; exit}" "$target_file")
|
|
108
|
+
local func_lines=$((end_line - start_line))
|
|
109
|
+
if [[ $func_lines -gt 60 ]]; then
|
|
110
|
+
issues+=("LONG_FUNCTION: $func_name at line $start_line ($func_lines lines)")
|
|
111
|
+
fi
|
|
112
|
+
fi
|
|
113
|
+
done < <(grep "^[a-zA-Z_][a-zA-Z0-9_]*().*{" "$target_file" 2>/dev/null || true)
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
# Check 2: Deep nesting (>4 levels)
|
|
117
|
+
local max_indent=0
|
|
118
|
+
while IFS= read -r line; do
|
|
119
|
+
local indent=0
|
|
120
|
+
while [[ "${line:0:1}" == " " || "${line:0:1}" == " " ]]; do
|
|
121
|
+
indent=$((indent + 1))
|
|
122
|
+
line="${line:1}"
|
|
123
|
+
done
|
|
124
|
+
[[ $indent -gt $max_indent ]] && max_indent=$indent
|
|
125
|
+
done < "$target_file"
|
|
126
|
+
local nesting_level=$((max_indent / 4))
|
|
127
|
+
if [[ $nesting_level -gt 4 ]]; then
|
|
128
|
+
issues+=("DEEP_NESTING: Maximum $nesting_level levels detected (threshold: 4)")
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# Check 3: Duplicate code patterns (repeated >3 times)
|
|
132
|
+
local dup_count=0
|
|
133
|
+
dup_count=$(grep -c '^\s*\(cd\|cd\|mkdir\|rm\|echo\)' "$target_file" 2>/dev/null || echo "0")
|
|
134
|
+
if [[ $dup_count -gt 3 ]]; then
|
|
135
|
+
issues+=("REPEATED_PATTERNS: Common operations appear $dup_count times (consider helper functions)")
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# Check 4: Magic numbers
|
|
139
|
+
if grep -qE '\s[0-9]{3,}\s' "$target_file" 2>/dev/null; then
|
|
140
|
+
issues+=("MAGIC_NUMBERS: Found numeric literals without clear purpose")
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Check 5: Poor naming (single letter variables in conditionals)
|
|
144
|
+
if grep -qE 'for\s+[a-z]\s+in|if.*\[\[\s*[a-z]\s*(==|-\w)' "$target_file" 2>/dev/null; then
|
|
145
|
+
issues+=("POOR_NAMING: Single-letter variables in conditionals")
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
for issue in "${issues[@]}"; do
|
|
149
|
+
echo "$issue"
|
|
150
|
+
done
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# ─── SOLID Violations ────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
check_solid_principles() {
|
|
156
|
+
local target_file="$1"
|
|
157
|
+
local violations=()
|
|
158
|
+
|
|
159
|
+
[[ ! -f "$target_file" ]] && return 0
|
|
160
|
+
|
|
161
|
+
local ext="${target_file##*.}"
|
|
162
|
+
[[ "$ext" != "sh" ]] && return 0
|
|
163
|
+
|
|
164
|
+
# Single Responsibility: Check if scripts do multiple unrelated things
|
|
165
|
+
local sourced_count
|
|
166
|
+
sourced_count=$(grep -c '^\s*source\|^\s*\.\s' "$target_file" 2>/dev/null || echo "0")
|
|
167
|
+
if [[ $sourced_count -gt 3 ]]; then
|
|
168
|
+
violations+=("SRP_VIOLATION: Script sources $sourced_count modules (too many responsibilities)")
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
# Open/Closed: Check for hardcoded config values
|
|
172
|
+
if grep -qE '^\s*(HARDCODED|MAGIC|CONFIG)=' "$target_file" 2>/dev/null; then
|
|
173
|
+
violations+=("OCP_VIOLATION: Hardcoded configuration values (use config files instead)")
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Liskov Substitution: Check for function parameter assumptions
|
|
177
|
+
if grep -qE 'if.*type\s+\$|if.*\[\[\s*-x' "$target_file" 2>/dev/null; then
|
|
178
|
+
violations+=("LSP_CONCERN: Possible type checking in function (breaks substitution)")
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
# Interface Segregation: Check if functions have too many parameters
|
|
182
|
+
while IFS= read -r line; do
|
|
183
|
+
if [[ "$line" =~ ^[a-zA-Z_][a-zA-Z0-9_]*\(\) ]]; then
|
|
184
|
+
local func_name="${line%%(*}"
|
|
185
|
+
local body
|
|
186
|
+
body=$(awk "/^${func_name}\\(\\)/,/^}/" "$target_file" | grep -c '\$[0-9]' || echo "0")
|
|
187
|
+
if [[ $body -gt 5 ]]; then
|
|
188
|
+
violations+=("ISP_VIOLATION: Function $func_name uses >5 parameters")
|
|
189
|
+
fi
|
|
190
|
+
fi
|
|
191
|
+
done < <(grep "^[a-zA-Z_][a-zA-Z0-9_]*().*{" "$target_file" 2>/dev/null || true)
|
|
192
|
+
|
|
193
|
+
for violation in "${violations[@]}"; do
|
|
194
|
+
echo "$violation"
|
|
195
|
+
done
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# ─── Architecture Boundary Checks ────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
check_architecture_boundaries() {
|
|
201
|
+
local target_file="$1"
|
|
202
|
+
local violations=()
|
|
203
|
+
|
|
204
|
+
[[ ! -f "$target_file" ]] && return 0
|
|
205
|
+
|
|
206
|
+
# Providers should not call each other directly
|
|
207
|
+
if [[ "$target_file" =~ sw-tracker-.*\.sh ]] || [[ "$target_file" =~ .*provider.*\.sh ]]; then
|
|
208
|
+
if grep -qE 'source.*provider|source.*tracker-' "$target_file" 2>/dev/null; then
|
|
209
|
+
violations+=("ARCH_VIOLATION: Provider sourcing another provider (use router pattern)")
|
|
210
|
+
fi
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# Non-router scripts shouldn't bypass the router
|
|
214
|
+
if [[ ! "$target_file" =~ /sw$ ]] && [[ ! "$target_file" =~ /sw-.*router.*\.sh ]]; then
|
|
215
|
+
if grep -qE 'exec\s+\$SCRIPT_DIR/sw-' "$target_file" 2>/dev/null; then
|
|
216
|
+
violations+=("ARCH_VIOLATION: Direct exec to script (use router pattern)")
|
|
217
|
+
fi
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# Tests shouldn't call production scripts
|
|
221
|
+
if [[ "$target_file" =~ -test\.sh$ ]]; then
|
|
222
|
+
if grep -qE 'source.*[^-test]\.sh' "$target_file" 2>/dev/null; then
|
|
223
|
+
violations+=("ARCH_VIOLATION: Test file sourcing production code directly")
|
|
224
|
+
fi
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
for violation in "${violations[@]}"; do
|
|
228
|
+
echo "$violation"
|
|
229
|
+
done
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# ─── Complexity Metrics ──────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
analyze_complexity() {
|
|
235
|
+
local target_file="$1"
|
|
236
|
+
|
|
237
|
+
[[ ! -f "$target_file" ]] && return 0
|
|
238
|
+
|
|
239
|
+
local ext="${target_file##*.}"
|
|
240
|
+
[[ "$ext" != "sh" ]] && return 0
|
|
241
|
+
|
|
242
|
+
local metrics="{\"file\":\"$target_file\",\"functions\":[]}"
|
|
243
|
+
|
|
244
|
+
while IFS= read -r line; do
|
|
245
|
+
if [[ "$line" =~ ^[a-zA-Z_][a-zA-Z0-9_]*\(\) ]]; then
|
|
246
|
+
local func_name="${line%%(*}"
|
|
247
|
+
local start_line
|
|
248
|
+
start_line=$(grep -n "^${func_name}()" "$target_file" | head -1 | cut -d: -f1)
|
|
249
|
+
local end_line
|
|
250
|
+
end_line=$(awk "NR>$start_line && /^}/ {print NR; exit}" "$target_file")
|
|
251
|
+
|
|
252
|
+
# Cyclomatic complexity: count decision points (if, elif, case, &&, ||)
|
|
253
|
+
local cc=1
|
|
254
|
+
cc=$((cc + $(sed -n "${start_line},${end_line}p" "$target_file" | grep -cE '\s(if|elif|case|&&|\|\|)' || echo 0)))
|
|
255
|
+
|
|
256
|
+
# Lines of code
|
|
257
|
+
local loc=$((end_line - start_line))
|
|
258
|
+
|
|
259
|
+
# Cognitive complexity: nested decision depth
|
|
260
|
+
local max_depth=0
|
|
261
|
+
local curr_depth=0
|
|
262
|
+
while IFS= read -r func_line; do
|
|
263
|
+
if [[ "$func_line" =~ \[\[ ]] || [[ "$func_line" =~ if.*then ]]; then
|
|
264
|
+
curr_depth=$((curr_depth + 1))
|
|
265
|
+
[[ $curr_depth -gt $max_depth ]] && max_depth=$curr_depth
|
|
266
|
+
fi
|
|
267
|
+
[[ "$func_line" =~ fi ]] && [[ $curr_depth -gt 0 ]] && curr_depth=$((curr_depth - 1))
|
|
268
|
+
done < <(sed -n "${start_line},${end_line}p" "$target_file")
|
|
269
|
+
|
|
270
|
+
metrics=$(echo "$metrics" | jq --arg fn "$func_name" --arg cc "$cc" --arg loc "$loc" --arg cd "$max_depth" \
|
|
271
|
+
'.functions += [{"name": $fn, "cyclomatic_complexity": $cc, "lines": $loc, "cognitive_complexity": $cd}]' 2>/dev/null || echo "$metrics")
|
|
272
|
+
fi
|
|
273
|
+
done < <(grep "^[a-zA-Z_][a-zA-Z0-9_]*().*{" "$target_file" 2>/dev/null || true)
|
|
274
|
+
|
|
275
|
+
echo "$metrics"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
# ─── Style Consistency ───────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
check_style_consistency() {
|
|
281
|
+
local target_file="$1"
|
|
282
|
+
local issues=()
|
|
283
|
+
|
|
284
|
+
[[ ! -f "$target_file" ]] && return 0
|
|
285
|
+
|
|
286
|
+
local ext="${target_file##*.}"
|
|
287
|
+
[[ "$ext" != "sh" ]] && return 0
|
|
288
|
+
|
|
289
|
+
# Check error handling consistency
|
|
290
|
+
local has_trap=false
|
|
291
|
+
local has_set_e=false
|
|
292
|
+
|
|
293
|
+
[[ $(grep -c 'trap.*ERR' "$target_file" 2>/dev/null || echo 0) -gt 0 ]] && has_trap=true
|
|
294
|
+
[[ $(grep -c 'set -e' "$target_file" 2>/dev/null || echo 0) -gt 0 ]] && has_set_e=true
|
|
295
|
+
|
|
296
|
+
if [[ "$has_set_e" == "true" ]] && [[ "$has_trap" == "false" ]]; then
|
|
297
|
+
issues+=("STYLE: Missing ERR trap despite 'set -e' (inconsistent error handling)")
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
# Check for inconsistent quote usage
|
|
301
|
+
local single_quotes
|
|
302
|
+
local double_quotes
|
|
303
|
+
single_quotes=$(grep -o "'" "$target_file" 2>/dev/null | wc -l || echo 0)
|
|
304
|
+
double_quotes=$(grep -o '"' "$target_file" 2>/dev/null | wc -l || echo 0)
|
|
305
|
+
if [[ $single_quotes -gt $((double_quotes * 3)) ]] || [[ $double_quotes -gt $((single_quotes * 3)) ]]; then
|
|
306
|
+
issues+=("STYLE: Inconsistent quote style (mix of single and double quotes)")
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
# Check for inconsistent spacing/indentation
|
|
310
|
+
local tab_count
|
|
311
|
+
local space_count
|
|
312
|
+
tab_count=$(grep -c $'^\t' "$target_file" 2>/dev/null || echo 0)
|
|
313
|
+
space_count=$(grep -c '^ ' "$target_file" 2>/dev/null || echo 0)
|
|
314
|
+
if [[ $tab_count -gt 0 ]] && [[ $space_count -gt 0 ]]; then
|
|
315
|
+
issues+=("STYLE: Mixed tabs and spaces")
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
# Check variable naming consistency
|
|
319
|
+
if grep -qE '\$\{[a-z]+_[a-z]+\}' "$target_file" && grep -qE '\$\{[A-Z]+\}' "$target_file"; then
|
|
320
|
+
if ! grep -qE '\$\{[A-Z]+_[A-Z]+\}' "$target_file"; then
|
|
321
|
+
issues+=("STYLE: Inconsistent variable naming (snake_case vs UPPERCASE)")
|
|
322
|
+
fi
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
for issue in "${issues[@]}"; do
|
|
326
|
+
echo "$issue"
|
|
327
|
+
done
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# ─── Auto-fix Simple Issues ─────────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
auto_fix() {
|
|
333
|
+
local target_file="$1"
|
|
334
|
+
local fixed=0
|
|
335
|
+
|
|
336
|
+
[[ ! -f "$target_file" ]] && return 0
|
|
337
|
+
|
|
338
|
+
local ext="${target_file##*.}"
|
|
339
|
+
[[ "$ext" != "sh" ]] && return 0
|
|
340
|
+
|
|
341
|
+
local backup="${target_file}.review-backup"
|
|
342
|
+
cp "$target_file" "$backup"
|
|
343
|
+
|
|
344
|
+
# Fix 1: Run shellcheck and capture warnings
|
|
345
|
+
if command -v shellcheck &>/dev/null; then
|
|
346
|
+
local shellcheck_fixes=0
|
|
347
|
+
local warnings_file
|
|
348
|
+
warnings_file=$(mktemp)
|
|
349
|
+
shellcheck -f json "$target_file" > "$warnings_file" 2>/dev/null || true
|
|
350
|
+
|
|
351
|
+
if [[ -s "$warnings_file" ]]; then
|
|
352
|
+
shellcheck_fixes=$(jq 'length' "$warnings_file" 2>/dev/null || echo "0")
|
|
353
|
+
info "shellcheck found $shellcheck_fixes issues in $target_file"
|
|
354
|
+
fixed=$((fixed + shellcheck_fixes))
|
|
355
|
+
fi
|
|
356
|
+
rm -f "$warnings_file"
|
|
357
|
+
fi
|
|
358
|
+
|
|
359
|
+
# Fix 2: Trailing whitespace
|
|
360
|
+
local trailing_ws
|
|
361
|
+
trailing_ws=$(grep -c '[[:space:]]$' "$target_file" 2>/dev/null || echo "0")
|
|
362
|
+
if [[ $trailing_ws -gt 0 ]]; then
|
|
363
|
+
sed -i '' 's/[[:space:]]*$//' "$target_file"
|
|
364
|
+
info "Removed $trailing_ws lines of trailing whitespace"
|
|
365
|
+
fixed=$((fixed + trailing_ws))
|
|
366
|
+
fi
|
|
367
|
+
|
|
368
|
+
# Fix 3: Ensure final newline
|
|
369
|
+
if [[ -n "$(tail -c1 "$target_file" 2>/dev/null)" ]]; then
|
|
370
|
+
echo "" >> "$target_file"
|
|
371
|
+
info "Added final newline"
|
|
372
|
+
fixed=$((fixed + 1))
|
|
373
|
+
fi
|
|
374
|
+
|
|
375
|
+
# Fix 4: Consistent spacing around operators (simple cases)
|
|
376
|
+
local spacing_fixes=0
|
|
377
|
+
spacing_fixes=$(grep -c '==' "$target_file" 2>/dev/null || echo "0")
|
|
378
|
+
if [[ $spacing_fixes -gt 0 ]]; then
|
|
379
|
+
info "Flagged $spacing_fixes operator spacing cases (manual review recommended)"
|
|
380
|
+
fi
|
|
381
|
+
|
|
382
|
+
echo "$fixed"
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
# ─── Review Subcommand ───────────────────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
review_changes() {
|
|
388
|
+
local pr_number="${1:-}"
|
|
389
|
+
local review_scope="staged"
|
|
390
|
+
|
|
391
|
+
if [[ -n "$pr_number" ]]; then
|
|
392
|
+
review_scope="pr:$pr_number"
|
|
393
|
+
fi
|
|
394
|
+
|
|
395
|
+
info "Reviewing code changes ($review_scope)..."
|
|
396
|
+
|
|
397
|
+
mkdir -p "${REPO_DIR}/.claude/pipeline-artifacts"
|
|
398
|
+
|
|
399
|
+
local review_output="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"scope\":\"$review_scope\",\"findings\":{}}"
|
|
400
|
+
local total_issues=0
|
|
401
|
+
|
|
402
|
+
# Get changed files
|
|
403
|
+
local changed_files=()
|
|
404
|
+
if [[ "$review_scope" == "staged" ]]; then
|
|
405
|
+
mapfile -t changed_files < <(cd "$REPO_DIR" && git diff --cached --name-only 2>/dev/null || true)
|
|
406
|
+
else
|
|
407
|
+
# For PR: get diff against main
|
|
408
|
+
mapfile -t changed_files < <(cd "$REPO_DIR" && git diff main...HEAD --name-only 2>/dev/null || true)
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
[[ ${#changed_files[@]} -eq 0 ]] && { success "No changes to review"; return 0; }
|
|
412
|
+
|
|
413
|
+
for file in "${changed_files[@]}"; do
|
|
414
|
+
local file_path="${REPO_DIR}/${file}"
|
|
415
|
+
[[ ! -f "$file_path" ]] && continue
|
|
416
|
+
|
|
417
|
+
info "Analyzing $file..."
|
|
418
|
+
|
|
419
|
+
local smells=()
|
|
420
|
+
local solids=()
|
|
421
|
+
local arch_issues=()
|
|
422
|
+
local style_issues=()
|
|
423
|
+
|
|
424
|
+
mapfile -t smells < <(detect_code_smells "$file_path")
|
|
425
|
+
mapfile -t solids < <(check_solid_principles "$file_path")
|
|
426
|
+
mapfile -t arch_issues < <(check_architecture_boundaries "$file_path")
|
|
427
|
+
mapfile -t style_issues < <(check_style_consistency "$file_path")
|
|
428
|
+
|
|
429
|
+
local file_issues=$((${#smells[@]} + ${#solids[@]} + ${#arch_issues[@]} + ${#style_issues[@]}))
|
|
430
|
+
total_issues=$((total_issues + file_issues))
|
|
431
|
+
|
|
432
|
+
if [[ $file_issues -gt 0 ]]; then
|
|
433
|
+
local file_summary="{\"code_smells\":"
|
|
434
|
+
file_summary+=$(printf '%s\n' "${smells[@]}" | jq -R . | jq -s . 2>/dev/null || echo "[]")
|
|
435
|
+
file_summary+=",\"solid_violations\":"
|
|
436
|
+
file_summary+=$(printf '%s\n' "${solids[@]}" | jq -R . | jq -s . 2>/dev/null || echo "[]")
|
|
437
|
+
file_summary+=",\"architecture_issues\":"
|
|
438
|
+
file_summary+=$(printf '%s\n' "${arch_issues[@]}" | jq -R . | jq -s . 2>/dev/null || echo "[]")
|
|
439
|
+
file_summary+=",\"style_issues\":"
|
|
440
|
+
file_summary+=$(printf '%s\n' "${style_issues[@]}" | jq -R . | jq -s . 2>/dev/null || echo "[]")
|
|
441
|
+
file_summary+="}"
|
|
442
|
+
|
|
443
|
+
review_output=$(echo "$review_output" | jq --arg fname "$file" --argjson summary "$file_summary" \
|
|
444
|
+
'.findings[$fname] = $summary' 2>/dev/null || echo "$review_output")
|
|
445
|
+
fi
|
|
446
|
+
done
|
|
447
|
+
|
|
448
|
+
review_output=$(echo "$review_output" | jq --arg tc "$total_issues" '.total_issues = $tc' 2>/dev/null || echo "$review_output")
|
|
449
|
+
|
|
450
|
+
echo "$review_output" | jq '.' 2>/dev/null || echo "$review_output"
|
|
451
|
+
|
|
452
|
+
mkdir -p "$(dirname "$QUALITY_METRICS_FILE")"
|
|
453
|
+
echo "$review_output" | jq '.' > "$QUALITY_METRICS_FILE" 2>/dev/null || true
|
|
454
|
+
|
|
455
|
+
emit_event "code_review.complete" "scope=$review_scope" "total_issues=$total_issues" "file_count=${#changed_files[@]}"
|
|
456
|
+
|
|
457
|
+
[[ $total_issues -gt 0 ]] && warn "Review found $total_issues issues"
|
|
458
|
+
[[ $total_issues -eq 0 ]] && success "No issues found"
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
# ─── Scan Subcommand ────────────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
scan_codebase() {
|
|
464
|
+
info "Running full codebase quality scan..."
|
|
465
|
+
|
|
466
|
+
local scan_output="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"files\":[]}"
|
|
467
|
+
local total_issues=0
|
|
468
|
+
|
|
469
|
+
find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | while read -r file; do
|
|
470
|
+
local file_rel="${file#$REPO_DIR/}"
|
|
471
|
+
local smells=0
|
|
472
|
+
local solids=0
|
|
473
|
+
local arch_issues=0
|
|
474
|
+
|
|
475
|
+
smells=$(detect_code_smells "$file" 2>/dev/null | wc -l || echo 0)
|
|
476
|
+
solids=$(check_solid_principles "$file" 2>/dev/null | wc -l || echo 0)
|
|
477
|
+
arch_issues=$(check_architecture_boundaries "$file" 2>/dev/null | wc -l || echo 0)
|
|
478
|
+
|
|
479
|
+
local file_issues=$((smells + solids + arch_issues))
|
|
480
|
+
total_issues=$((total_issues + file_issues))
|
|
481
|
+
|
|
482
|
+
if [[ $file_issues -gt 0 ]]; then
|
|
483
|
+
echo "$file_rel: $file_issues issues (smells: $smells, SOLID: $solids, arch: $arch_issues)"
|
|
484
|
+
fi
|
|
485
|
+
done
|
|
486
|
+
|
|
487
|
+
success "Scan complete: $total_issues total issues found"
|
|
488
|
+
emit_event "code_review.scan" "total_issues=$total_issues"
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
# ─── Complexity Subcommand ──────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
complexity_report() {
|
|
494
|
+
info "Analyzing code complexity metrics..."
|
|
495
|
+
|
|
496
|
+
local complexity_data="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"files\":[]}"
|
|
497
|
+
|
|
498
|
+
find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | while read -r file; do
|
|
499
|
+
local file_metrics
|
|
500
|
+
file_metrics=$(analyze_complexity "$file" 2>/dev/null || echo "{}")
|
|
501
|
+
complexity_data=$(echo "$complexity_data" | jq --argjson metrics "$file_metrics" '.files += [$metrics]' 2>/dev/null || echo "$complexity_data")
|
|
502
|
+
|
|
503
|
+
# Output per-file summary
|
|
504
|
+
echo "$file_metrics" | jq -r '.functions[] | "\(.name): CC=\(.cyclomatic_complexity), LOC=\(.lines), Cog=\(.cognitive_complexity)"' 2>/dev/null || true
|
|
505
|
+
done
|
|
506
|
+
|
|
507
|
+
success "Complexity analysis complete"
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
# ─── Trends Subcommand ──────────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
show_trends() {
|
|
513
|
+
[[ ! -f "$TRENDS_FILE" ]] && { info "No trend data available yet"; return 0; }
|
|
514
|
+
|
|
515
|
+
info "Code Quality Trends:"
|
|
516
|
+
echo ""
|
|
517
|
+
|
|
518
|
+
tail -10 "$TRENDS_FILE" | jq -r '.timestamp + ": " + (.total_issues | tostring) + " issues"' 2>/dev/null || cat "$TRENDS_FILE"
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
# ─── Config Subcommand ──────────────────────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
manage_config() {
|
|
524
|
+
local action="${1:-show}"
|
|
525
|
+
|
|
526
|
+
case "$action" in
|
|
527
|
+
show)
|
|
528
|
+
init_config
|
|
529
|
+
cat "$REVIEW_CONFIG" | jq '.' 2>/dev/null || cat "$REVIEW_CONFIG"
|
|
530
|
+
;;
|
|
531
|
+
set)
|
|
532
|
+
local key="$2" value="$3"
|
|
533
|
+
init_config
|
|
534
|
+
jq --arg k "$key" --arg v "$value" '.[$k] = $v' "$REVIEW_CONFIG" > "${REVIEW_CONFIG}.tmp"
|
|
535
|
+
mv "${REVIEW_CONFIG}.tmp" "$REVIEW_CONFIG"
|
|
536
|
+
success "Updated $key = $value"
|
|
537
|
+
;;
|
|
538
|
+
*)
|
|
539
|
+
error "Unknown config action: $action"
|
|
540
|
+
return 1
|
|
541
|
+
;;
|
|
542
|
+
esac
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# ─── Help ────────────────────────────────────────────────────────────────────
|
|
546
|
+
|
|
547
|
+
show_help() {
|
|
548
|
+
echo -e "${BOLD}Autonomous Code Review Agent${RESET} — Issue #76"
|
|
549
|
+
echo ""
|
|
550
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
551
|
+
echo -e " ${CYAN}sw code-review${RESET} <subcommand> [options]"
|
|
552
|
+
echo ""
|
|
553
|
+
echo -e "${BOLD}SUBCOMMANDS${RESET}"
|
|
554
|
+
echo -e " ${CYAN}review${RESET} [--pr N] Review staged changes (or specific PR)"
|
|
555
|
+
echo -e " ${CYAN}scan${RESET} Full codebase quality scan"
|
|
556
|
+
echo -e " ${CYAN}complexity${RESET} Show complexity metrics per function"
|
|
557
|
+
echo -e " ${CYAN}boundaries${RESET} Check architecture boundary violations"
|
|
558
|
+
echo -e " ${CYAN}fix${RESET} Auto-fix simple code quality issues"
|
|
559
|
+
echo -e " ${CYAN}trends${RESET} Show code quality trends over time"
|
|
560
|
+
echo -e " ${CYAN}config${RESET} [show|set K V] Manage review configuration"
|
|
561
|
+
echo -e " ${CYAN}help${RESET} Show this help message"
|
|
562
|
+
echo ""
|
|
563
|
+
echo -e "${BOLD}CHECKS${RESET}"
|
|
564
|
+
echo -e " • Code smells: long functions, deep nesting, duplication, magic numbers"
|
|
565
|
+
echo -e " • SOLID principles: SRP, OCP, LSP, ISP violations"
|
|
566
|
+
echo -e " • Architecture: layer boundaries, provider isolation"
|
|
567
|
+
echo -e " • Complexity: cyclomatic, cognitive, function length"
|
|
568
|
+
echo -e " • Style: consistency in error handling, quotes, indentation"
|
|
569
|
+
echo ""
|
|
570
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
571
|
+
echo -e " ${DIM}sw code-review review${RESET} # Review staged changes"
|
|
572
|
+
echo -e " ${DIM}sw code-review review --pr 42${RESET} # Review specific PR"
|
|
573
|
+
echo -e " ${DIM}sw code-review complexity${RESET} # Show complexity metrics"
|
|
574
|
+
echo -e " ${DIM}sw code-review fix${RESET} # Auto-fix simple issues"
|
|
575
|
+
echo ""
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
# ─── Main ────────────────────────────────────────────────────────────────────
|
|
579
|
+
|
|
580
|
+
main() {
|
|
581
|
+
local subcommand="${1:-help}"
|
|
582
|
+
|
|
583
|
+
init_config
|
|
584
|
+
load_config
|
|
585
|
+
|
|
586
|
+
case "$subcommand" in
|
|
587
|
+
review)
|
|
588
|
+
shift || true
|
|
589
|
+
local pr_opt=""
|
|
590
|
+
[[ "${1:-}" == "--pr" ]] && pr_opt="${2:-}"
|
|
591
|
+
review_changes "$pr_opt"
|
|
592
|
+
;;
|
|
593
|
+
scan)
|
|
594
|
+
scan_codebase
|
|
595
|
+
;;
|
|
596
|
+
complexity)
|
|
597
|
+
complexity_report
|
|
598
|
+
;;
|
|
599
|
+
boundaries)
|
|
600
|
+
info "Checking architecture boundaries..."
|
|
601
|
+
find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | while read -r file; do
|
|
602
|
+
check_architecture_boundaries "$file" 2>/dev/null | grep . && echo " in $file"
|
|
603
|
+
done
|
|
604
|
+
success "Architecture check complete"
|
|
605
|
+
;;
|
|
606
|
+
fix)
|
|
607
|
+
info "Auto-fixing simple issues..."
|
|
608
|
+
local fixed_count=0
|
|
609
|
+
find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | while read -r file; do
|
|
610
|
+
local fixes
|
|
611
|
+
fixes=$(auto_fix "$file" 2>/dev/null || echo "0")
|
|
612
|
+
[[ "$fixes" -gt 0 ]] && fixed_count=$((fixed_count + fixes))
|
|
613
|
+
done
|
|
614
|
+
success "Auto-fix complete: $fixed_count issues addressed"
|
|
615
|
+
emit_event "code_review.autofix" "issues_fixed=$fixed_count"
|
|
616
|
+
;;
|
|
617
|
+
trends)
|
|
618
|
+
show_trends
|
|
619
|
+
;;
|
|
620
|
+
config)
|
|
621
|
+
shift || true
|
|
622
|
+
manage_config "$@"
|
|
623
|
+
;;
|
|
624
|
+
help|--help|-h)
|
|
625
|
+
show_help
|
|
626
|
+
;;
|
|
627
|
+
*)
|
|
628
|
+
error "Unknown subcommand: $subcommand"
|
|
629
|
+
show_help
|
|
630
|
+
return 1
|
|
631
|
+
;;
|
|
632
|
+
esac
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
636
|
+
main "$@"
|
|
637
|
+
fi
|
package/scripts/sw-connect.sh
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
set -euo pipefail
|
|
9
9
|
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
10
10
|
|
|
11
|
-
VERSION="
|
|
11
|
+
VERSION="2.0.0"
|
|
12
12
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
13
|
|
|
14
14
|
# ─── Colors (matches Seth's tmux theme) ─────────────────────────────────────
|