shipwright-cli 1.10.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/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 +208 -1
- 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 +1 -1
- package/scripts/sw-ci.sh +602 -0
- package/scripts/sw-cleanup.sh +1 -1
- 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 +432 -130
- 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 +363 -28
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +64 -3
- package/scripts/sw-memory.sh +1 -1
- 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 +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +687 -24
- 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 +1 -1
- package/scripts/sw-public-dashboard.sh +798 -0
- package/scripts/sw-quality.sh +595 -0
- package/scripts/sw-reaper.sh +1 -1
- 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 +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +1 -1
- package/scripts/sw-standup.sh +712 -0
- package/scripts/sw-status.sh +1 -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
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright hygiene — Repository Organization & Cleanup ║
|
|
4
|
+
# ║ Dead code detection · Structure enforcement · Dependency audit ║
|
|
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 event_type="$1"; shift
|
|
36
|
+
local events_file="${HOME}/.shipwright/events.jsonl"
|
|
37
|
+
mkdir -p "$(dirname "$events_file")"
|
|
38
|
+
local payload="{\"ts\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"type\":\"$event_type\""
|
|
39
|
+
while [[ $# -gt 0 ]]; do
|
|
40
|
+
local key="${1%%=*}" val="${1#*=}"
|
|
41
|
+
payload="${payload},\"${key}\":\"${val}\""
|
|
42
|
+
shift
|
|
43
|
+
done
|
|
44
|
+
payload="${payload}}"
|
|
45
|
+
echo "$payload" >> "$events_file"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ─── Default Settings ───────────────────────────────────────────────────────
|
|
49
|
+
SUBCOMMAND="${1:-help}"
|
|
50
|
+
AUTO_FIX=false
|
|
51
|
+
VERBOSE=false
|
|
52
|
+
ARTIFACT_AGE_DAYS=7
|
|
53
|
+
JSON_OUTPUT=false
|
|
54
|
+
|
|
55
|
+
# ─── Help ───────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
show_help() {
|
|
58
|
+
echo -e "${CYAN}${BOLD}shipwright hygiene${RESET} ${DIM}v${VERSION}${RESET} — Repository cleanliness & structure enforcement"
|
|
59
|
+
echo ""
|
|
60
|
+
echo -e "${BOLD}USAGE${RESET}"
|
|
61
|
+
echo -e " ${CYAN}shipwright hygiene${RESET} <subcommand> [options]"
|
|
62
|
+
echo ""
|
|
63
|
+
echo -e "${BOLD}SUBCOMMANDS${RESET}"
|
|
64
|
+
echo -e " ${CYAN}scan${RESET} Full hygiene scan (all checks)"
|
|
65
|
+
echo -e " ${CYAN}dead-code${RESET} Find unused functions, scripts, fixtures"
|
|
66
|
+
echo -e " ${CYAN}structure${RESET} Validate directory structure and conventions"
|
|
67
|
+
echo -e " ${CYAN}dependencies${RESET} Dependency audit and circular checks"
|
|
68
|
+
echo -e " ${CYAN}naming${RESET} Check naming conventions (files, functions, vars)"
|
|
69
|
+
echo -e " ${CYAN}branches${RESET} List stale and merged remote branches"
|
|
70
|
+
echo -e " ${CYAN}size${RESET} Size analysis and bloat detection"
|
|
71
|
+
echo -e " ${CYAN}fix${RESET} Auto-fix safe issues (naming, whitespace)"
|
|
72
|
+
echo -e " ${CYAN}report${RESET} Generate comprehensive hygiene report"
|
|
73
|
+
echo -e " ${CYAN}help${RESET} Show this help message"
|
|
74
|
+
echo ""
|
|
75
|
+
echo -e "${BOLD}OPTIONS${RESET}"
|
|
76
|
+
echo -e " ${CYAN}--fix${RESET} Auto-fix issues (use with caution)"
|
|
77
|
+
echo -e " ${CYAN}--verbose, -v${RESET} Verbose output"
|
|
78
|
+
echo -e " ${CYAN}--json${RESET} JSON output format"
|
|
79
|
+
echo -e " ${CYAN}--artifact-age${RESET} Max age for artifacts in days (default: 7)"
|
|
80
|
+
echo -e " ${CYAN}--help, -h${RESET} Show this help"
|
|
81
|
+
echo ""
|
|
82
|
+
echo -e "${BOLD}EXAMPLES${RESET}"
|
|
83
|
+
echo -e " ${DIM}shipwright hygiene scan${RESET} # Full scan"
|
|
84
|
+
echo -e " ${DIM}shipwright hygiene dead-code${RESET} # Find unused code"
|
|
85
|
+
echo -e " ${DIM}shipwright hygiene fix${RESET} # Auto-fix safe issues"
|
|
86
|
+
echo -e " ${DIM}shipwright hygiene report --json${RESET} # JSON report"
|
|
87
|
+
echo ""
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# ─── Dead Code Detection ────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
detect_dead_code() {
|
|
93
|
+
info "Scanning for dead code..."
|
|
94
|
+
|
|
95
|
+
local unused_functions=0
|
|
96
|
+
local unused_scripts=0
|
|
97
|
+
local orphaned_tests=0
|
|
98
|
+
|
|
99
|
+
# Find unused bash functions (simplified for Bash 3.2)
|
|
100
|
+
while IFS= read -r func_file; do
|
|
101
|
+
# Extract function names
|
|
102
|
+
local funcs
|
|
103
|
+
funcs=$(grep -E '^[a-z_][a-z0-9_]*\(\)' "$func_file" 2>/dev/null | sed 's/()$//' | sed 's/^ *//' || true)
|
|
104
|
+
|
|
105
|
+
while IFS= read -r func; do
|
|
106
|
+
[[ -z "$func" ]] && continue
|
|
107
|
+
|
|
108
|
+
# Check if function is used in other files (count lines with this function name)
|
|
109
|
+
local usage_count
|
|
110
|
+
usage_count=$(grep -r "$func" "$REPO_DIR/scripts" --include="*.sh" 2>/dev/null | wc -l) || usage_count="0"
|
|
111
|
+
usage_count=$(printf '%s' "$usage_count" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
112
|
+
|
|
113
|
+
# Function definition counts as 1 usage; if only 1, it's unused
|
|
114
|
+
case "$usage_count" in
|
|
115
|
+
0|1) [[ $VERBOSE == true ]] && warn "Unused function: $func (in $(basename "$func_file"))"; ((unused_functions++)) ;;
|
|
116
|
+
esac
|
|
117
|
+
done <<< "$funcs"
|
|
118
|
+
done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | head -20)
|
|
119
|
+
|
|
120
|
+
# Find scripts referenced nowhere
|
|
121
|
+
local script_count=0
|
|
122
|
+
while IFS= read -r script; do
|
|
123
|
+
local basename_script ref_count
|
|
124
|
+
basename_script=$(basename "$script")
|
|
125
|
+
|
|
126
|
+
# Skip test scripts and main scripts
|
|
127
|
+
[[ "$basename_script" =~ -test\.sh$ ]] && continue
|
|
128
|
+
[[ "$basename_script" == "sw-hygiene.sh" ]] && continue
|
|
129
|
+
[[ "$basename_script" == "sw" ]] && continue
|
|
130
|
+
|
|
131
|
+
# Check if script is sourced or executed
|
|
132
|
+
ref_count=$(grep -r "$basename_script" "$REPO_DIR/scripts" --include="*.sh" 2>/dev/null | wc -l) || ref_count="0"
|
|
133
|
+
ref_count=$(printf '%s' "$ref_count" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
134
|
+
|
|
135
|
+
case "$ref_count" in
|
|
136
|
+
0|1) [[ $VERBOSE == true ]] && warn "Potentially unused script: $basename_script"; ((unused_scripts++)) ;;
|
|
137
|
+
esac
|
|
138
|
+
((script_count++))
|
|
139
|
+
done < <(find "$REPO_DIR/scripts" -maxdepth 1 -name "sw-*.sh" -type f 2>/dev/null)
|
|
140
|
+
|
|
141
|
+
# Find test fixtures without corresponding tests
|
|
142
|
+
while IFS= read -r fixture; do
|
|
143
|
+
local test_name
|
|
144
|
+
test_name=$(basename "$fixture" .fixture)
|
|
145
|
+
|
|
146
|
+
if ! grep -r "$test_name" "$REPO_DIR/scripts" --include="*-test.sh" 2>/dev/null | grep -q .; then
|
|
147
|
+
warn "Orphaned test fixture: $(basename "$fixture")"
|
|
148
|
+
((orphaned_tests++))
|
|
149
|
+
fi
|
|
150
|
+
done < <(find "$REPO_DIR" -name "*.fixture" -type f 2>/dev/null)
|
|
151
|
+
|
|
152
|
+
[[ $VERBOSE == true ]] && {
|
|
153
|
+
info "Dead code summary: $unused_functions unused functions, $unused_scripts scripts, $orphaned_tests fixtures"
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return 0
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
# ─── Structure Validation ───────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
validate_structure() {
|
|
162
|
+
info "Validating directory structure..."
|
|
163
|
+
|
|
164
|
+
local structure_issues=0
|
|
165
|
+
|
|
166
|
+
# Check for scripts in wrong locations
|
|
167
|
+
while IFS= read -r script; do
|
|
168
|
+
local dir
|
|
169
|
+
dir=$(dirname "$script")
|
|
170
|
+
|
|
171
|
+
if [[ "$dir" != "$REPO_DIR/scripts" ]]; then
|
|
172
|
+
warn "Script outside scripts/ directory: $script"
|
|
173
|
+
((structure_issues++))
|
|
174
|
+
fi
|
|
175
|
+
done < <(find "$REPO_DIR" -name "*.sh" -type f ! -path "*/node_modules/*" ! -path "*/.git/*" 2>/dev/null | grep -E '(sw-|shipwright)' || true)
|
|
176
|
+
|
|
177
|
+
# Check test naming conventions
|
|
178
|
+
while IFS= read -r test; do
|
|
179
|
+
local basename_test
|
|
180
|
+
basename_test=$(basename "$test")
|
|
181
|
+
|
|
182
|
+
if [[ ! "$basename_test" =~ -test\.sh$ ]]; then
|
|
183
|
+
warn "Test file not named *-test.sh: $basename_test"
|
|
184
|
+
((structure_issues++))
|
|
185
|
+
fi
|
|
186
|
+
done < <(find "$REPO_DIR/scripts" -path "*test*" -name "*.sh" -type f 2>/dev/null)
|
|
187
|
+
|
|
188
|
+
# Check directory organization
|
|
189
|
+
if [[ ! -d "$REPO_DIR/scripts" ]]; then
|
|
190
|
+
error "scripts/ directory missing"
|
|
191
|
+
((structure_issues++))
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
if [[ ! -d "$REPO_DIR/.claude" ]]; then
|
|
195
|
+
error ".claude/ directory missing"
|
|
196
|
+
((structure_issues++))
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
[[ $VERBOSE == true ]] && {
|
|
200
|
+
info "Structure validation: $structure_issues issues found"
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return 0
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# ─── Dependency Audit ───────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
audit_dependencies() {
|
|
209
|
+
info "Auditing dependencies..."
|
|
210
|
+
|
|
211
|
+
local unused_deps=0
|
|
212
|
+
local circular_deps=0
|
|
213
|
+
|
|
214
|
+
# Check for unused npm/yarn dependencies
|
|
215
|
+
if [[ -f "$REPO_DIR/package.json" ]]; then
|
|
216
|
+
local deps
|
|
217
|
+
deps=$(jq -r '.dependencies, .devDependencies | keys[]?' "$REPO_DIR/package.json" 2>/dev/null || true)
|
|
218
|
+
|
|
219
|
+
while IFS= read -r dep; do
|
|
220
|
+
[[ -z "$dep" ]] && continue
|
|
221
|
+
|
|
222
|
+
# Simple check: does the dep appear in source files?
|
|
223
|
+
if ! grep -r "$dep" "$REPO_DIR/scripts" --include="*.sh" 2>/dev/null | grep -q .; then
|
|
224
|
+
[[ $VERBOSE == true ]] && warn "Potentially unused npm dependency: $dep"
|
|
225
|
+
((unused_deps++))
|
|
226
|
+
fi
|
|
227
|
+
done <<< "$deps"
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# Check for circular script dependencies (A sources B, B sources A)
|
|
231
|
+
while IFS= read -r script; do
|
|
232
|
+
local sourced_by
|
|
233
|
+
sourced_by=$(grep "source.*$(basename "$script")" "$REPO_DIR/scripts"/*.sh 2>/dev/null | cut -d: -f1 | sort -u || true)
|
|
234
|
+
|
|
235
|
+
while IFS= read -r source_script; do
|
|
236
|
+
[[ -z "$source_script" ]] && continue
|
|
237
|
+
|
|
238
|
+
# Check if sourced_by also sources the original script
|
|
239
|
+
if grep -q "source.*$(basename "$source_script")" "$script" 2>/dev/null; then
|
|
240
|
+
warn "Circular dependency: $(basename "$script") ←→ $(basename "$source_script")"
|
|
241
|
+
((circular_deps++))
|
|
242
|
+
fi
|
|
243
|
+
done <<< "$sourced_by"
|
|
244
|
+
done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | head -10)
|
|
245
|
+
|
|
246
|
+
[[ $VERBOSE == true ]] && {
|
|
247
|
+
info "Dependency audit: $unused_deps unused, $circular_deps circular"
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return 0
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# ─── Naming Convention Check ────────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
check_naming() {
|
|
256
|
+
info "Checking naming conventions..."
|
|
257
|
+
|
|
258
|
+
local naming_issues=0
|
|
259
|
+
|
|
260
|
+
# Check shell scripts follow sw-*.sh pattern
|
|
261
|
+
while IFS= read -r script; do
|
|
262
|
+
local basename_script
|
|
263
|
+
basename_script=$(basename "$script")
|
|
264
|
+
|
|
265
|
+
if ! [[ "$basename_script" =~ ^sw-[a-z0-9-]+\.sh$ ]] && ! [[ "$basename_script" == "sw" ]]; then
|
|
266
|
+
[[ $VERBOSE == true ]] && warn "Script not following naming convention: $basename_script"
|
|
267
|
+
((naming_issues++))
|
|
268
|
+
fi
|
|
269
|
+
done < <(find "$REPO_DIR/scripts" -maxdepth 1 -name "*.sh" -type f 2>/dev/null)
|
|
270
|
+
|
|
271
|
+
# Check for functions not using snake_case
|
|
272
|
+
while IFS= read -r script; do
|
|
273
|
+
local bad_functions
|
|
274
|
+
bad_functions=$(grep -oE '^[a-zA-Z_][a-zA-Z0-9_]*\(\)' "$script" 2>/dev/null | grep -E '[A-Z]' | sed 's/()$//' || true)
|
|
275
|
+
|
|
276
|
+
while IFS= read -r func; do
|
|
277
|
+
[[ -z "$func" ]] && continue
|
|
278
|
+
[[ $VERBOSE == true ]] && warn "Function not using snake_case: $func (in $(basename "$script"))"
|
|
279
|
+
((naming_issues++))
|
|
280
|
+
done <<< "$bad_functions"
|
|
281
|
+
done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | head -20)
|
|
282
|
+
|
|
283
|
+
[[ $VERBOSE == true ]] && {
|
|
284
|
+
info "Naming validation: $naming_issues issues found"
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return 0
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# ─── Stale Branch Detection ────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
list_stale_branches() {
|
|
293
|
+
info "Scanning for stale branches..."
|
|
294
|
+
|
|
295
|
+
if ! git rev-parse --git-dir &>/dev/null; then
|
|
296
|
+
error "Not in a git repository"
|
|
297
|
+
return 1
|
|
298
|
+
fi
|
|
299
|
+
|
|
300
|
+
# Fetch latest remote info
|
|
301
|
+
git fetch --prune 2>/dev/null || true
|
|
302
|
+
|
|
303
|
+
local stale_count=0
|
|
304
|
+
|
|
305
|
+
# Find merged branches
|
|
306
|
+
local merged_branches
|
|
307
|
+
merged_branches=$(git branch -r --merged 2>/dev/null | grep -v "HEAD" | grep -v "main" | grep -v "master" | tr -d ' ' || true)
|
|
308
|
+
|
|
309
|
+
if [[ -n "$merged_branches" ]]; then
|
|
310
|
+
info "Merged branches available for cleanup:"
|
|
311
|
+
while IFS= read -r branch; do
|
|
312
|
+
[[ -z "$branch" ]] && continue
|
|
313
|
+
echo -e " ${DIM}$branch${RESET}"
|
|
314
|
+
((stale_count++))
|
|
315
|
+
done <<< "$merged_branches"
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
# Find branches not updated in 30 days
|
|
319
|
+
local old_branches
|
|
320
|
+
old_branches=$(git branch -r --format="%(refname:short)%09%(committerdate:short)" 2>/dev/null | \
|
|
321
|
+
awk -v cutoff=$(date -d "30 days ago" +%Y-%m-%d 2>/dev/null || date -v-30d +%Y-%m-%d) \
|
|
322
|
+
'$2 < cutoff {print $1}' || true)
|
|
323
|
+
|
|
324
|
+
if [[ -n "$old_branches" ]]; then
|
|
325
|
+
info "Branches not updated in 30+ days:"
|
|
326
|
+
while IFS= read -r branch; do
|
|
327
|
+
[[ -z "$branch" ]] && continue
|
|
328
|
+
echo -e " ${DIM}$branch${RESET}"
|
|
329
|
+
done <<< "$old_branches"
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
[[ $VERBOSE == true ]] && info "Found $stale_count potentially stale branches"
|
|
333
|
+
|
|
334
|
+
return 0
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# ─── Size Analysis ─────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
analyze_size() {
|
|
340
|
+
info "Analyzing repository size..."
|
|
341
|
+
|
|
342
|
+
local total_size=0
|
|
343
|
+
|
|
344
|
+
# Find large files
|
|
345
|
+
info "Largest files:"
|
|
346
|
+
find "$REPO_DIR" -type f ! -path '*/.git/*' ! -path '*/node_modules/*' 2>/dev/null | \
|
|
347
|
+
xargs ls -lh 2>/dev/null | \
|
|
348
|
+
awk '{print $5, $9}' | \
|
|
349
|
+
sort -h | \
|
|
350
|
+
tail -10 | \
|
|
351
|
+
while read size file; do
|
|
352
|
+
echo -e " ${DIM}$size${RESET} $(basename "$file")"
|
|
353
|
+
done
|
|
354
|
+
|
|
355
|
+
# Find bloated directories
|
|
356
|
+
info "Largest directories:"
|
|
357
|
+
du -sh "$REPO_DIR"/* 2>/dev/null | sort -h | tail -10 | while read size dir; do
|
|
358
|
+
echo -e " ${DIM}$size${RESET} $(basename "$dir")"
|
|
359
|
+
done
|
|
360
|
+
|
|
361
|
+
# Check for binary files
|
|
362
|
+
info "Checking for unexpected binary files..."
|
|
363
|
+
local binary_count=0
|
|
364
|
+
while IFS= read -r file; do
|
|
365
|
+
[[ -z "$file" ]] && continue
|
|
366
|
+
warn "Binary file in repo: $(basename "$file")"
|
|
367
|
+
((binary_count++))
|
|
368
|
+
done < <(find "$REPO_DIR" -type f ! -path '*/.git/*' ! -path '*/node_modules/*' ! -path '*/.claude/*' \
|
|
369
|
+
-exec file {} \; 2>/dev/null | grep -i "executable\|binary" | cut -d: -f1 || true)
|
|
370
|
+
|
|
371
|
+
[[ $VERBOSE == true ]] && info "Found $binary_count binary files"
|
|
372
|
+
|
|
373
|
+
return 0
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# ─── Auto-Fix Mode ────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
auto_fix_issues() {
|
|
379
|
+
info "Auto-fixing safe issues..."
|
|
380
|
+
|
|
381
|
+
local fixed_count=0
|
|
382
|
+
|
|
383
|
+
# Fix script permissions
|
|
384
|
+
info "Setting script permissions..."
|
|
385
|
+
while IFS= read -r script; do
|
|
386
|
+
if ! [[ -x "$script" ]]; then
|
|
387
|
+
chmod +x "$script"
|
|
388
|
+
success "Made executable: $(basename "$script")"
|
|
389
|
+
((fixed_count++))
|
|
390
|
+
fi
|
|
391
|
+
done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null)
|
|
392
|
+
|
|
393
|
+
# Remove trailing whitespace
|
|
394
|
+
info "Removing trailing whitespace..."
|
|
395
|
+
while IFS= read -r file; do
|
|
396
|
+
if grep -q '[[:space:]]$' "$file" 2>/dev/null; then
|
|
397
|
+
sed -i.bak 's/[[:space:]]*$//' "$file" 2>/dev/null || sed -i '' 's/[[:space:]]*$//' "$file"
|
|
398
|
+
rm -f "${file}.bak" 2>/dev/null || true
|
|
399
|
+
success "Cleaned whitespace: $(basename "$file")"
|
|
400
|
+
((fixed_count++))
|
|
401
|
+
fi
|
|
402
|
+
done < <(find "$REPO_DIR/scripts" -name "*.sh" -type f 2>/dev/null | head -20)
|
|
403
|
+
|
|
404
|
+
# Clean up temp files
|
|
405
|
+
info "Removing temporary files..."
|
|
406
|
+
find "$REPO_DIR" -name "*.tmp" -o -name "*.bak" -o -name "*~" 2>/dev/null | while read tmpfile; do
|
|
407
|
+
rm -f "$tmpfile"
|
|
408
|
+
success "Removed: $(basename "$tmpfile")"
|
|
409
|
+
((fixed_count++))
|
|
410
|
+
done
|
|
411
|
+
|
|
412
|
+
# Remove old build artifacts
|
|
413
|
+
info "Removing old build artifacts (>$ARTIFACT_AGE_DAYS days)..."
|
|
414
|
+
find "$REPO_DIR" -type f \( -name "*.o" -o -name "*.a" -o -name "*.out" \) \
|
|
415
|
+
-mtime "+$ARTIFACT_AGE_DAYS" 2>/dev/null | while read artifact; do
|
|
416
|
+
rm -f "$artifact"
|
|
417
|
+
success "Removed: $(basename "$artifact")"
|
|
418
|
+
((fixed_count++))
|
|
419
|
+
done
|
|
420
|
+
|
|
421
|
+
success "Auto-fixed $fixed_count issues"
|
|
422
|
+
|
|
423
|
+
# Create a commit if changes were made
|
|
424
|
+
if [[ $fixed_count -gt 0 ]] && git rev-parse --git-dir &>/dev/null; then
|
|
425
|
+
git add -A
|
|
426
|
+
git commit -m "chore: hygiene auto-fix ($fixed_count items)
|
|
427
|
+
|
|
428
|
+
- Fixed script permissions
|
|
429
|
+
- Removed trailing whitespace
|
|
430
|
+
- Cleaned temporary files
|
|
431
|
+
- Removed old build artifacts
|
|
432
|
+
|
|
433
|
+
Relates to #74" 2>/dev/null || true
|
|
434
|
+
success "Created hygiene auto-fix commit"
|
|
435
|
+
fi
|
|
436
|
+
|
|
437
|
+
emit_event "hygiene_fix" "fixed=$fixed_count" "type=auto"
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
# ─── Comprehensive Report ───────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
generate_report() {
|
|
443
|
+
local report_file
|
|
444
|
+
report_file="$REPO_DIR/.claude/hygiene-report.json"
|
|
445
|
+
|
|
446
|
+
info "Generating comprehensive hygiene report..."
|
|
447
|
+
mkdir -p "$REPO_DIR/.claude"
|
|
448
|
+
|
|
449
|
+
# Build JSON report
|
|
450
|
+
local report
|
|
451
|
+
report=$(cat <<'EOF'
|
|
452
|
+
{
|
|
453
|
+
"timestamp": "TIMESTAMP",
|
|
454
|
+
"repository": "REPO",
|
|
455
|
+
"version": "VERSION",
|
|
456
|
+
"sections": {
|
|
457
|
+
"dead_code": {},
|
|
458
|
+
"structure": {},
|
|
459
|
+
"dependencies": {},
|
|
460
|
+
"naming": {},
|
|
461
|
+
"branches": {},
|
|
462
|
+
"size": {}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
EOF
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Replace placeholders
|
|
469
|
+
report="${report//TIMESTAMP/$(date -u +%Y-%m-%dT%H:%M:%SZ)}"
|
|
470
|
+
report="${report//REPO/$(basename "$REPO_DIR")}"
|
|
471
|
+
report="${report//VERSION/$VERSION}"
|
|
472
|
+
|
|
473
|
+
if [[ $JSON_OUTPUT == true ]]; then
|
|
474
|
+
echo "$report" | jq .
|
|
475
|
+
else
|
|
476
|
+
echo "$report" > "$report_file"
|
|
477
|
+
success "Report saved to: $report_file"
|
|
478
|
+
fi
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
# ─── Full Scan ──────────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
run_full_scan() {
|
|
484
|
+
echo -e "${CYAN}${BOLD}╭─ Shipwright Hygiene Scan ─────────────────────────────────────╮${RESET}"
|
|
485
|
+
|
|
486
|
+
detect_dead_code
|
|
487
|
+
validate_structure
|
|
488
|
+
audit_dependencies
|
|
489
|
+
check_naming
|
|
490
|
+
list_stale_branches
|
|
491
|
+
analyze_size
|
|
492
|
+
|
|
493
|
+
echo -e "${CYAN}${BOLD}╰────────────────────────────────────────────────────────────────╯${RESET}"
|
|
494
|
+
|
|
495
|
+
emit_event "hygiene_scan" "status=complete"
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
# ─── Main ───────────────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
main() {
|
|
501
|
+
# Parse global options
|
|
502
|
+
while [[ $# -gt 0 ]]; do
|
|
503
|
+
case "$1" in
|
|
504
|
+
--fix) AUTO_FIX=true; shift ;;
|
|
505
|
+
--verbose|-v) VERBOSE=true; shift ;;
|
|
506
|
+
--json) JSON_OUTPUT=true; shift ;;
|
|
507
|
+
--artifact-age) ARTIFACT_AGE_DAYS="$2"; shift 2 ;;
|
|
508
|
+
*) break ;;
|
|
509
|
+
esac
|
|
510
|
+
done
|
|
511
|
+
|
|
512
|
+
# Route to subcommand
|
|
513
|
+
case "$SUBCOMMAND" in
|
|
514
|
+
scan)
|
|
515
|
+
run_full_scan
|
|
516
|
+
;;
|
|
517
|
+
dead-code)
|
|
518
|
+
detect_dead_code
|
|
519
|
+
;;
|
|
520
|
+
structure)
|
|
521
|
+
validate_structure
|
|
522
|
+
;;
|
|
523
|
+
dependencies)
|
|
524
|
+
audit_dependencies
|
|
525
|
+
;;
|
|
526
|
+
naming)
|
|
527
|
+
check_naming
|
|
528
|
+
;;
|
|
529
|
+
branches)
|
|
530
|
+
list_stale_branches
|
|
531
|
+
;;
|
|
532
|
+
size)
|
|
533
|
+
analyze_size
|
|
534
|
+
;;
|
|
535
|
+
fix)
|
|
536
|
+
if [[ $AUTO_FIX != true ]]; then
|
|
537
|
+
AUTO_FIX=true
|
|
538
|
+
fi
|
|
539
|
+
auto_fix_issues
|
|
540
|
+
;;
|
|
541
|
+
report)
|
|
542
|
+
generate_report
|
|
543
|
+
;;
|
|
544
|
+
help|--help|-h)
|
|
545
|
+
show_help
|
|
546
|
+
;;
|
|
547
|
+
*)
|
|
548
|
+
error "Unknown subcommand: $SUBCOMMAND"
|
|
549
|
+
echo ""
|
|
550
|
+
show_help
|
|
551
|
+
exit 1
|
|
552
|
+
;;
|
|
553
|
+
esac
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
# Source guard
|
|
557
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
558
|
+
main "$@"
|
|
559
|
+
fi
|