shimwrappercheck 0.2.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/AGENTS.md +21 -0
- package/README.md +286 -0
- package/bin/git +5 -0
- package/bin/shim +5 -0
- package/bin/shimwrappercheck +2 -0
- package/bin/supabase +5 -0
- package/dashboard/.next/cache/config.json +7 -0
- package/dashboard/.next/package.json +1 -0
- package/dashboard/.next/routes-manifest.json +1 -0
- package/dashboard/.next/trace +1 -0
- package/dashboard/README.md +32 -0
- package/dashboard/app/agents/page.tsx +88 -0
- package/dashboard/app/api/agents-md/route.ts +68 -0
- package/dashboard/app/api/config/route.ts +51 -0
- package/dashboard/app/api/run-checks/route.ts +54 -0
- package/dashboard/app/api/settings/route.ts +126 -0
- package/dashboard/app/api/status/route.ts +38 -0
- package/dashboard/app/config/page.tsx +77 -0
- package/dashboard/app/globals.css +20 -0
- package/dashboard/app/layout.tsx +23 -0
- package/dashboard/app/page.tsx +122 -0
- package/dashboard/app/settings/page.tsx +422 -0
- package/dashboard/components/Nav.tsx +33 -0
- package/dashboard/components/StatusCard.tsx +27 -0
- package/dashboard/lib/presets.ts +97 -0
- package/dashboard/lib/projectRoot.ts +25 -0
- package/dashboard/next.config.js +6 -0
- package/dashboard/package-lock.json +5307 -0
- package/dashboard/package.json +28 -0
- package/dashboard/postcss.config.js +6 -0
- package/dashboard/tailwind.config.js +14 -0
- package/dashboard/tsconfig.json +20 -0
- package/docs/SHIM_WRAPPER_CONCEPT.md +79 -0
- package/docs/WORKFLOW_AND_GAP_ANALYSIS.md +159 -0
- package/package.json +24 -0
- package/scripts/cli-checked.sh +307 -0
- package/scripts/cli.js +23 -0
- package/scripts/fetch-edge-logs.sh +96 -0
- package/scripts/git-checked.sh +194 -0
- package/scripts/init.js +341 -0
- package/scripts/install.js +303 -0
- package/scripts/ping-edge-health.sh +113 -0
- package/scripts/setup.js +55 -0
- package/scripts/supabase-checked.sh +330 -0
- package/templates/ai-code-review.sh +217 -0
- package/templates/git-pre-push +41 -0
- package/templates/husky-pre-push +46 -0
- package/templates/run-checks.sh +67 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shim wrapper for Supabase CLI: run checks, call real CLI, then run optional hooks.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
WRAPPER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
6
|
+
|
|
7
|
+
resolve_project_root() {
|
|
8
|
+
if [[ -n "${SHIM_PROJECT_ROOT:-}" ]]; then
|
|
9
|
+
echo "$SHIM_PROJECT_ROOT"
|
|
10
|
+
return
|
|
11
|
+
fi
|
|
12
|
+
if command -v git >/dev/null 2>&1; then
|
|
13
|
+
local root
|
|
14
|
+
root="$(git rev-parse --show-toplevel 2>/dev/null || true)"
|
|
15
|
+
if [[ -n "$root" ]]; then
|
|
16
|
+
echo "$root"
|
|
17
|
+
return
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
pwd
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
PROJECT_ROOT="$(resolve_project_root)"
|
|
24
|
+
cd "$PROJECT_ROOT"
|
|
25
|
+
|
|
26
|
+
CONFIG_FILE="${SHIM_CONFIG_FILE:-$PROJECT_ROOT/.shimwrappercheckrc}"
|
|
27
|
+
if [[ -f "$CONFIG_FILE" ]]; then
|
|
28
|
+
# shellcheck disable=SC1090
|
|
29
|
+
source "$CONFIG_FILE"
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
ARGS_IN=("$@")
|
|
33
|
+
ARGS_TEXT_RAW=" ${*:-} "
|
|
34
|
+
SUPABASE_ARGS=()
|
|
35
|
+
CHECKS_PASSTHROUGH=()
|
|
36
|
+
|
|
37
|
+
RUN_CHECKS=true
|
|
38
|
+
CHECKS_ONLY=false
|
|
39
|
+
RUN_HOOKS=true
|
|
40
|
+
RUN_PUSH=true
|
|
41
|
+
FORCE_FRONTEND=false
|
|
42
|
+
|
|
43
|
+
matches_command_list() {
|
|
44
|
+
local list="$1"
|
|
45
|
+
local text="$2"
|
|
46
|
+
|
|
47
|
+
list="$(echo "$list" | tr '[:upper:]' '[:lower:]')"
|
|
48
|
+
text="$(echo "$text" | tr '[:upper:]' '[:lower:]')"
|
|
49
|
+
|
|
50
|
+
if [[ -z "$list" ]] || [[ "$list" == "all" ]]; then
|
|
51
|
+
return 0
|
|
52
|
+
fi
|
|
53
|
+
if [[ "$list" == "none" ]]; then
|
|
54
|
+
return 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
IFS=',' read -r -a items <<< "$list"
|
|
58
|
+
for item in "${items[@]}"; do
|
|
59
|
+
item="$(echo "$item" | xargs)"
|
|
60
|
+
[[ -z "$item" ]] && continue
|
|
61
|
+
if [[ "$text" == *" $item "* ]]; then
|
|
62
|
+
return 0
|
|
63
|
+
fi
|
|
64
|
+
done
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for arg in "${ARGS_IN[@]}"; do
|
|
69
|
+
case "$arg" in
|
|
70
|
+
--no-checks) RUN_CHECKS=false ;;
|
|
71
|
+
--checks-only) CHECKS_ONLY=true ;;
|
|
72
|
+
--no-hooks) RUN_HOOKS=false ;;
|
|
73
|
+
--no-push) RUN_PUSH=false ;;
|
|
74
|
+
--with-frontend) FORCE_FRONTEND=true ;;
|
|
75
|
+
--no-ai-review) CHECKS_PASSTHROUGH+=("$arg") ;;
|
|
76
|
+
--ai-review) CHECKS_PASSTHROUGH+=("$arg") ;;
|
|
77
|
+
*) SUPABASE_ARGS+=("$arg") ;;
|
|
78
|
+
esac
|
|
79
|
+
done
|
|
80
|
+
|
|
81
|
+
[[ -n "${SHIM_DISABLE_CHECKS:-}" ]] && RUN_CHECKS=false
|
|
82
|
+
[[ -n "${SHIM_DISABLE_HOOKS:-}" ]] && RUN_HOOKS=false
|
|
83
|
+
if [[ -n "${SHIM_AUTO_PUSH:-}" ]]; then
|
|
84
|
+
case "${SHIM_AUTO_PUSH}" in
|
|
85
|
+
1|true|TRUE|yes|YES) RUN_PUSH=true ;;
|
|
86
|
+
0|false|FALSE|no|NO) RUN_PUSH=false ;;
|
|
87
|
+
esac
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if [[ "${#SUPABASE_ARGS[@]}" -eq 0 ]] && [[ "$CHECKS_ONLY" != true ]]; then
|
|
91
|
+
echo "No Supabase command provided. Usage: supabase [shim flags] <supabase args>" >&2
|
|
92
|
+
echo "Shim flags: --no-checks --checks-only --no-hooks --no-push --no-ai-review" >&2
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
ARGS_TEXT=" ${SUPABASE_ARGS[*]:-} "
|
|
97
|
+
if [[ "$CHECKS_ONLY" != true ]]; then
|
|
98
|
+
enforce_list="${SHIM_ENFORCE_COMMANDS:-all}"
|
|
99
|
+
if ! matches_command_list "$enforce_list" "$ARGS_TEXT"; then
|
|
100
|
+
RUN_CHECKS=false
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
resolve_checks_script() {
|
|
105
|
+
local script="${SHIM_CHECKS_SCRIPT:-}"
|
|
106
|
+
if [[ -n "$script" ]]; then
|
|
107
|
+
if [[ "$script" != /* ]]; then
|
|
108
|
+
script="$PROJECT_ROOT/$script"
|
|
109
|
+
fi
|
|
110
|
+
echo "$script"
|
|
111
|
+
return
|
|
112
|
+
fi
|
|
113
|
+
local candidates=("scripts/run-checks.sh" "scripts/shim-checks.sh")
|
|
114
|
+
for candidate in "${candidates[@]}"; do
|
|
115
|
+
if [[ -f "$PROJECT_ROOT/$candidate" ]]; then
|
|
116
|
+
echo "$PROJECT_ROOT/$candidate"
|
|
117
|
+
return
|
|
118
|
+
fi
|
|
119
|
+
done
|
|
120
|
+
echo ""
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if [[ "$RUN_CHECKS" = true ]]; then
|
|
124
|
+
run_frontend=false
|
|
125
|
+
run_backend=false
|
|
126
|
+
run_ai_review=true
|
|
127
|
+
|
|
128
|
+
changed_files=""
|
|
129
|
+
if command -v git >/dev/null 2>&1; then
|
|
130
|
+
unstaged=$(git diff --name-only --diff-filter=ACMR || true)
|
|
131
|
+
staged=$(git diff --name-only --cached --diff-filter=ACMR || true)
|
|
132
|
+
changed_files=$(printf "%s\n%s\n" "$unstaged" "$staged")
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
if [[ -n "$changed_files" ]]; then
|
|
136
|
+
echo "$changed_files" | grep -q '^src/' && run_frontend=true
|
|
137
|
+
echo "$changed_files" | grep -q '^supabase/functions/' && run_backend=true
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
ARGS_TEXT=" ${SUPABASE_ARGS[*]:-} "
|
|
141
|
+
if [[ "$ARGS_TEXT" == *" functions "* ]]; then
|
|
142
|
+
run_backend=true
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
if [[ "$FORCE_FRONTEND" = true ]] || [[ "$ARGS_TEXT_RAW" == *" --with-frontend "* ]]; then
|
|
146
|
+
run_frontend=true
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
if [[ "$ARGS_TEXT_RAW" == *" --no-ai-review "* ]]; then
|
|
150
|
+
run_ai_review=false
|
|
151
|
+
fi
|
|
152
|
+
if [[ -n "${SKIP_AI_REVIEW:-}" ]]; then
|
|
153
|
+
run_ai_review=false
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if [[ "$run_frontend" = true ]] || [[ "$run_backend" = true ]]; then
|
|
157
|
+
CHECKS_SCRIPT="$(resolve_checks_script)"
|
|
158
|
+
if [[ -n "$CHECKS_SCRIPT" ]]; then
|
|
159
|
+
CHECKS_ARGS=()
|
|
160
|
+
if [[ -n "${SHIM_CHECKS_ARGS:-}" ]]; then
|
|
161
|
+
read -r -a CHECKS_ARGS <<< "${SHIM_CHECKS_ARGS}"
|
|
162
|
+
fi
|
|
163
|
+
[[ "$run_frontend" = true ]] && CHECKS_ARGS+=(--frontend)
|
|
164
|
+
[[ "$run_backend" = true ]] && CHECKS_ARGS+=(--backend)
|
|
165
|
+
[[ "$run_ai_review" = false ]] && CHECKS_ARGS+=(--no-ai-review)
|
|
166
|
+
CHECKS_ARGS+=("${CHECKS_PASSTHROUGH[@]}")
|
|
167
|
+
bash "$CHECKS_SCRIPT" "${CHECKS_ARGS[@]}"
|
|
168
|
+
else
|
|
169
|
+
echo "Shim checks: no checks script found; skipping." >&2
|
|
170
|
+
fi
|
|
171
|
+
fi
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
if [[ "$CHECKS_ONLY" = true ]]; then
|
|
175
|
+
exit 0
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
REAL_BIN="${SHIM_SUPABASE_BIN:-${SUPABASE_REAL_BIN:-}}"
|
|
179
|
+
if [[ -z "$REAL_BIN" ]] && [[ -f "$HOME/.supabase-real-bin" ]]; then
|
|
180
|
+
REAL_BIN="$(cat "$HOME/.supabase-real-bin")"
|
|
181
|
+
fi
|
|
182
|
+
if [[ -z "$REAL_BIN" ]]; then
|
|
183
|
+
REAL_BIN="$(command -v supabase || true)"
|
|
184
|
+
fi
|
|
185
|
+
if [[ -n "$REAL_BIN" ]] && { [[ "$REAL_BIN" == *"node_modules"* ]] || [[ "$REAL_BIN" == "$WRAPPER_DIR"* ]]; }; then
|
|
186
|
+
REAL_BIN=""
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
retry_max=${SUPABASE_RETRY_MAX:-1}
|
|
190
|
+
if ! [[ "$retry_max" =~ ^[0-9]+$ ]]; then
|
|
191
|
+
retry_max=1
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
retry_backoffs="${SUPABASE_RETRY_BACKOFF_SECONDS:-5,15}"
|
|
195
|
+
IFS=',' read -r -a retry_backoff_list <<< "$retry_backoffs"
|
|
196
|
+
|
|
197
|
+
retry_extra_args=()
|
|
198
|
+
if [[ -n "${SUPABASE_RETRY_EXTRA_ARGS:-}" ]]; then
|
|
199
|
+
read -r -a retry_extra_args <<< "${SUPABASE_RETRY_EXTRA_ARGS}"
|
|
200
|
+
fi
|
|
201
|
+
|
|
202
|
+
run_supabase_cli() {
|
|
203
|
+
local retry_mode="${1:-false}"
|
|
204
|
+
local -a cmd_args=("${SUPABASE_ARGS[@]}")
|
|
205
|
+
|
|
206
|
+
if [[ "$retry_mode" == "true" ]] && [[ "${#retry_extra_args[@]}" -gt 0 ]]; then
|
|
207
|
+
cmd_args+=("${retry_extra_args[@]}")
|
|
208
|
+
fi
|
|
209
|
+
|
|
210
|
+
RUN_OUTPUT_FILE="$(mktemp)"
|
|
211
|
+
set +e
|
|
212
|
+
if [[ -z "$REAL_BIN" ]] || [[ ! -x "$REAL_BIN" ]]; then
|
|
213
|
+
# Use registry package to avoid recursion when shim is the local bin.
|
|
214
|
+
npx --yes --package supabase supabase "${cmd_args[@]}" 2>&1 | tee "$RUN_OUTPUT_FILE"
|
|
215
|
+
else
|
|
216
|
+
"$REAL_BIN" "${cmd_args[@]}" 2>&1 | tee "$RUN_OUTPUT_FILE"
|
|
217
|
+
fi
|
|
218
|
+
local rc=${PIPESTATUS[0]}
|
|
219
|
+
set -e
|
|
220
|
+
return $rc
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
is_network_error() {
|
|
224
|
+
local file="$1"
|
|
225
|
+
local pattern="(timed out|timeout|context deadline exceeded|connection (reset|refused|aborted|closed|lost)|network is unreachable|temporary failure in name resolution|no such host|tls handshake timeout|i/o timeout|EOF|ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENETUNREACH|EAI_AGAIN|ENOTFOUND|dial tcp)"
|
|
226
|
+
grep -E -i -q "$pattern" "$file"
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
attempt=1
|
|
230
|
+
max_attempts=$((retry_max + 1))
|
|
231
|
+
while true; do
|
|
232
|
+
if [[ "$attempt" -gt 1 ]]; then
|
|
233
|
+
echo "Supabase CLI retry stage ${attempt}/${max_attempts}..."
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
if run_supabase_cli "$([[ "$attempt" -gt 1 ]] && echo true || echo false)"; then
|
|
237
|
+
rm -f "$RUN_OUTPUT_FILE"
|
|
238
|
+
break
|
|
239
|
+
fi
|
|
240
|
+
|
|
241
|
+
rc=$?
|
|
242
|
+
if ! is_network_error "$RUN_OUTPUT_FILE"; then
|
|
243
|
+
rm -f "$RUN_OUTPUT_FILE"
|
|
244
|
+
exit $rc
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
rm -f "$RUN_OUTPUT_FILE"
|
|
248
|
+
|
|
249
|
+
if [[ "$attempt" -ge "$max_attempts" ]]; then
|
|
250
|
+
echo "Supabase CLI failed after ${attempt} attempt(s) due to network error."
|
|
251
|
+
exit $rc
|
|
252
|
+
fi
|
|
253
|
+
|
|
254
|
+
backoff_index=$((attempt - 1))
|
|
255
|
+
if [[ "${#retry_backoff_list[@]}" -gt 0 ]]; then
|
|
256
|
+
if [[ "$backoff_index" -ge "${#retry_backoff_list[@]}" ]]; then
|
|
257
|
+
backoff_seconds="${retry_backoff_list[-1]}"
|
|
258
|
+
else
|
|
259
|
+
backoff_seconds="${retry_backoff_list[$backoff_index]}"
|
|
260
|
+
fi
|
|
261
|
+
else
|
|
262
|
+
backoff_seconds=5
|
|
263
|
+
fi
|
|
264
|
+
|
|
265
|
+
if [[ ! "$backoff_seconds" =~ ^[0-9]+$ ]]; then
|
|
266
|
+
backoff_seconds=5
|
|
267
|
+
fi
|
|
268
|
+
|
|
269
|
+
echo "Network error detected. Retrying in ${backoff_seconds}s with extended wait."
|
|
270
|
+
sleep "$backoff_seconds"
|
|
271
|
+
attempt=$((attempt + 1))
|
|
272
|
+
done
|
|
273
|
+
|
|
274
|
+
should_run_hooks=false
|
|
275
|
+
ARGS_TEXT=" ${SUPABASE_ARGS[*]:-} "
|
|
276
|
+
hook_list="${SHIM_HOOK_COMMANDS:-functions,db,migration}"
|
|
277
|
+
if matches_command_list "$hook_list" "$ARGS_TEXT"; then
|
|
278
|
+
should_run_hooks=true
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
resolve_hook_script() {
|
|
282
|
+
local env_var="$1"
|
|
283
|
+
local name="$2"
|
|
284
|
+
local script=""
|
|
285
|
+
script="${!env_var:-}"
|
|
286
|
+
if [[ -n "$script" ]]; then
|
|
287
|
+
if [[ "$script" != /* ]]; then
|
|
288
|
+
script="$PROJECT_ROOT/$script"
|
|
289
|
+
fi
|
|
290
|
+
echo "$script"
|
|
291
|
+
return
|
|
292
|
+
fi
|
|
293
|
+
if [[ -f "$PROJECT_ROOT/scripts/$name" ]]; then
|
|
294
|
+
echo "$PROJECT_ROOT/scripts/$name"
|
|
295
|
+
return
|
|
296
|
+
fi
|
|
297
|
+
if [[ -f "$WRAPPER_DIR/scripts/$name" ]]; then
|
|
298
|
+
echo "$WRAPPER_DIR/scripts/$name"
|
|
299
|
+
return
|
|
300
|
+
fi
|
|
301
|
+
echo ""
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if [[ "$RUN_HOOKS" = true ]] && [[ "$should_run_hooks" = true ]]; then
|
|
305
|
+
PING_SCRIPT="$(resolve_hook_script SHIM_PING_SCRIPT ping-edge-health.sh)"
|
|
306
|
+
LOG_SCRIPT="$(resolve_hook_script SHIM_LOG_SCRIPT fetch-edge-logs.sh)"
|
|
307
|
+
|
|
308
|
+
if [[ -n "$PING_SCRIPT" ]]; then
|
|
309
|
+
SHIM_PROJECT_ROOT="$PROJECT_ROOT" bash "$PING_SCRIPT" "${SUPABASE_ARGS[@]}" || true
|
|
310
|
+
fi
|
|
311
|
+
if [[ -n "$LOG_SCRIPT" ]]; then
|
|
312
|
+
SHIM_PROJECT_ROOT="$PROJECT_ROOT" bash "$LOG_SCRIPT" "${SUPABASE_ARGS[@]}" || true
|
|
313
|
+
fi
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
if [[ "$RUN_PUSH" = true ]] && command -v git >/dev/null 2>&1; then
|
|
317
|
+
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
318
|
+
if git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then
|
|
319
|
+
ahead=$(git rev-list --count @{u}..HEAD)
|
|
320
|
+
if [[ "${ahead:-0}" -gt 0 ]]; then
|
|
321
|
+
echo "Pushing commits to remote..."
|
|
322
|
+
git push
|
|
323
|
+
else
|
|
324
|
+
echo "No commits to push."
|
|
325
|
+
fi
|
|
326
|
+
else
|
|
327
|
+
echo "No upstream configured; skipping git push."
|
|
328
|
+
fi
|
|
329
|
+
fi
|
|
330
|
+
fi
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# AI code review: Codex only (Cursor disabled). Called from run-checks.sh.
|
|
3
|
+
# Prompt: senior-dev, decades-of-expertise, best-code bar.
|
|
4
|
+
# Codex: codex in PATH; use session after codex login (ChatGPT account, no API key in terminal).
|
|
5
|
+
# Diff: staged + unstaged; if clean (e.g. pre-push), uses diff of commits being pushed (@{u}...HEAD or HEAD~1...HEAD).
|
|
6
|
+
# Runs codex exec --json to get token usage from turn.completed; prints Token usage for agent to report.
|
|
7
|
+
# Diff limited to ~50KB head + tail. Timeout 180s when timeout(1) available.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
11
|
+
cd "$ROOT_DIR"
|
|
12
|
+
|
|
13
|
+
DIFF_FILE=""
|
|
14
|
+
cleanup() {
|
|
15
|
+
[[ -n "$DIFF_FILE" ]] && [[ -f "$DIFF_FILE" ]] && rm -f "$DIFF_FILE"
|
|
16
|
+
}
|
|
17
|
+
trap cleanup EXIT
|
|
18
|
+
|
|
19
|
+
DIFF_FILE="$(mktemp)"
|
|
20
|
+
git diff --no-color >> "$DIFF_FILE" 2>/dev/null || true
|
|
21
|
+
git diff --cached --no-color >> "$DIFF_FILE" 2>/dev/null || true
|
|
22
|
+
|
|
23
|
+
# If working tree is clean (e.g. pre-push after commit), use diff of commits being pushed.
|
|
24
|
+
if [[ ! -s "$DIFF_FILE" ]] && command -v git >/dev/null 2>&1; then
|
|
25
|
+
RANGE=""
|
|
26
|
+
if git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then
|
|
27
|
+
RANGE="@{u}...HEAD"
|
|
28
|
+
elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
|
29
|
+
RANGE="HEAD~1...HEAD"
|
|
30
|
+
fi
|
|
31
|
+
if [[ -n "$RANGE" ]]; then
|
|
32
|
+
git diff --no-color "$RANGE" >> "$DIFF_FILE" 2>/dev/null || true
|
|
33
|
+
fi
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if [[ ! -s "$DIFF_FILE" ]]; then
|
|
37
|
+
echo "Skipping AI review: no staged, unstaged, or pushed changes." >&2
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Limit diff to first and last ~50KB to avoid token limits and timeouts
|
|
42
|
+
LIMIT_BYTES=51200
|
|
43
|
+
DIFF_LIMITED=""
|
|
44
|
+
if [[ $(wc -c < "$DIFF_FILE") -le $((LIMIT_BYTES * 2)) ]]; then
|
|
45
|
+
DIFF_LIMITED="$(cat "$DIFF_FILE")"
|
|
46
|
+
else
|
|
47
|
+
DIFF_LIMITED="$(head -c $LIMIT_BYTES "$DIFF_FILE")
|
|
48
|
+
...[truncated]...
|
|
49
|
+
$(tail -c $LIMIT_BYTES "$DIFF_FILE")"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
# Senior-dev review prompt: structured output with rating (1-100), positive, warnings, errors, recommendations.
|
|
53
|
+
# PASS only if rating >= 95 and no warnings and no errors.
|
|
54
|
+
PROMPT="You are a senior software engineer with decades of code review experience. Your goal is to ensure the highest possible code quality: production-ready, secure, maintainable, and aligned with project standards.
|
|
55
|
+
|
|
56
|
+
Review the following code diff with the rigor of a principal engineer. Evaluate:
|
|
57
|
+
|
|
58
|
+
1. Correctness and bugs: logic errors, edge cases, race conditions, off-by-one, null/undefined handling.
|
|
59
|
+
2. Security: injection, XSS, sensitive data exposure, auth/authz, input validation, dependency risks.
|
|
60
|
+
3. Performance: unnecessary work, N+1, memory leaks, blocking calls, inefficient algorithms or data structures.
|
|
61
|
+
4. Maintainability: clarity, naming, single responsibility, DRY, testability, documentation where needed.
|
|
62
|
+
5. Project compliance: AGENTS.md rules, existing patterns, lint/type discipline, error handling and logging.
|
|
63
|
+
6. Robustness: error paths, timeouts, retries, backward compatibility, defensive checks.
|
|
64
|
+
|
|
65
|
+
Reply with exactly the following structure (one label per line, use None when there are no warnings or errors). Do not modify any files or suggest edits in the response.
|
|
66
|
+
|
|
67
|
+
RATING: <1-100> (integer)
|
|
68
|
+
POSITIVE: <short points on what is good>
|
|
69
|
+
WARNINGS: <points or None>
|
|
70
|
+
ERRORS: <points or None>
|
|
71
|
+
RECOMMENDATIONS: <short recommendations>
|
|
72
|
+
VERDICT: PASS or VERDICT: FAIL
|
|
73
|
+
|
|
74
|
+
Rule: VERDICT must be PASS only if rating >= 95 and there are no warnings and no errors; otherwise VERDICT must be FAIL.
|
|
75
|
+
|
|
76
|
+
--- DIFF ---
|
|
77
|
+
$DIFF_LIMITED"
|
|
78
|
+
|
|
79
|
+
CODEX_JSON_FILE="$(mktemp)"
|
|
80
|
+
CODEX_LAST_MSG_FILE="$(mktemp)"
|
|
81
|
+
cleanup() {
|
|
82
|
+
[[ -n "$DIFF_FILE" ]] && [[ -f "$DIFF_FILE" ]] && rm -f "$DIFF_FILE"
|
|
83
|
+
[[ -n "$CODEX_JSON_FILE" ]] && [[ -f "$CODEX_JSON_FILE" ]] && rm -f "$CODEX_JSON_FILE"
|
|
84
|
+
[[ -n "$CODEX_LAST_MSG_FILE" ]] && [[ -f "$CODEX_LAST_MSG_FILE" ]] && rm -f "$CODEX_LAST_MSG_FILE"
|
|
85
|
+
}
|
|
86
|
+
trap cleanup EXIT
|
|
87
|
+
|
|
88
|
+
if ! command -v codex >/dev/null 2>&1; then
|
|
89
|
+
echo "Skipping AI review: Codex CLI not available (run codex login or install codex in PATH)." >&2
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
TIMEOUT_SEC=180
|
|
94
|
+
echo "Running Codex AI review..." >&2
|
|
95
|
+
|
|
96
|
+
# Run with --json to get turn.completed (token usage) and item.completed (assistant message). Use -o to get final message for PASS/FAIL.
|
|
97
|
+
CODEX_RC=0
|
|
98
|
+
if command -v timeout >/dev/null 2>&1; then
|
|
99
|
+
timeout "$TIMEOUT_SEC" codex exec --json -o "$CODEX_LAST_MSG_FILE" "$PROMPT" 2>/dev/null > "$CODEX_JSON_FILE" || CODEX_RC=$?
|
|
100
|
+
else
|
|
101
|
+
codex exec --json -o "$CODEX_LAST_MSG_FILE" "$PROMPT" 2>/dev/null > "$CODEX_JSON_FILE" || CODEX_RC=$?
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
if [[ $CODEX_RC -eq 124 ]] || [[ $CODEX_RC -eq 142 ]]; then
|
|
105
|
+
echo "Codex AI review timed out after ${TIMEOUT_SEC}s." >&2
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
if [[ $CODEX_RC -ne 0 ]]; then
|
|
110
|
+
echo "Codex AI review command failed (exit $CODEX_RC)." >&2
|
|
111
|
+
cat "$CODEX_JSON_FILE" 2>/dev/null | head -50 >&2
|
|
112
|
+
exit 1
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
# Parse JSONL: turn.completed has usage (input_tokens, output_tokens); last assistant_message is in item.completed.
|
|
116
|
+
INPUT_T=""
|
|
117
|
+
OUTPUT_T=""
|
|
118
|
+
RESULT_TEXT=""
|
|
119
|
+
if command -v jq >/dev/null 2>&1; then
|
|
120
|
+
while IFS= read -r line; do
|
|
121
|
+
[[ -z "$line" ]] && continue
|
|
122
|
+
type="$(echo "$line" | jq -r '.type // empty')"
|
|
123
|
+
if [[ "$type" == "turn.completed" ]]; then
|
|
124
|
+
INPUT_T="$(echo "$line" | jq -r '.usage.input_tokens // empty')"
|
|
125
|
+
OUTPUT_T="$(echo "$line" | jq -r '.usage.output_tokens // empty')"
|
|
126
|
+
fi
|
|
127
|
+
if [[ "$type" == "item.completed" ]]; then
|
|
128
|
+
item_type="$(echo "$line" | jq -r '.item.item_type // empty')"
|
|
129
|
+
if [[ "$item_type" == "assistant_message" ]]; then
|
|
130
|
+
RESULT_TEXT="$(echo "$line" | jq -r '.item.text // empty')"
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
done < "$CODEX_JSON_FILE"
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
# Fallback: use --output-last-message file for PASS/FAIL if we didn't get assistant_message from JSONL
|
|
137
|
+
if [[ -z "$RESULT_TEXT" ]] && [[ -s "$CODEX_LAST_MSG_FILE" ]]; then
|
|
138
|
+
RESULT_TEXT="$(cat "$CODEX_LAST_MSG_FILE")"
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
# Parse structured review: RATING, POSITIVE, WARNINGS, ERRORS, RECOMMENDATIONS. Fallback: rating=0, has_warnings/errors=1.
|
|
142
|
+
REVIEW_RATING=0
|
|
143
|
+
REVIEW_POSITIVE=""
|
|
144
|
+
REVIEW_WARNINGS=""
|
|
145
|
+
REVIEW_ERRORS=""
|
|
146
|
+
REVIEW_RECOMMENDATIONS=""
|
|
147
|
+
HAS_WARNINGS=1
|
|
148
|
+
HAS_ERRORS=1
|
|
149
|
+
|
|
150
|
+
if [[ -n "$RESULT_TEXT" ]]; then
|
|
151
|
+
REVIEW_RATING=$(echo "$RESULT_TEXT" | grep -iE '^RATING:[[:space:]]*[0-9]+' | head -1 | sed 's/.*:[[:space:]]*//' | tr -d '\r' | grep -oE '[0-9]+' | head -1)
|
|
152
|
+
[[ -z "$REVIEW_RATING" ]] && REVIEW_RATING=0
|
|
153
|
+
[[ "$REVIEW_RATING" -lt 0 ]] 2>/dev/null && REVIEW_RATING=0
|
|
154
|
+
[[ "$REVIEW_RATING" -gt 100 ]] 2>/dev/null && REVIEW_RATING=100
|
|
155
|
+
|
|
156
|
+
# Sections: content from label line (after colon) until next label (sed '$d' = drop last line, macOS-compatible)
|
|
157
|
+
REVIEW_POSITIVE=$(echo "$RESULT_TEXT" | sed -n '/^POSITIVE:/,/^WARNINGS:/p' | tail -n +1 | sed '$d' | sed 's/^POSITIVE:[[:space:]]*//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
158
|
+
REVIEW_WARNINGS=$(echo "$RESULT_TEXT" | sed -n '/^WARNINGS:/,/^ERRORS:/p' | tail -n +1 | sed '$d' | sed 's/^WARNINGS:[[:space:]]*//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
159
|
+
REVIEW_ERRORS=$(echo "$RESULT_TEXT" | sed -n '/^ERRORS:/,/^RECOMMENDATIONS:/p' | tail -n +1 | sed '$d' | sed 's/^ERRORS:[[:space:]]*//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
160
|
+
REVIEW_RECOMMENDATIONS=$(echo "$RESULT_TEXT" | sed -n '/^RECOMMENDATIONS:/,/^VERDICT:/p' | tail -n +1 | sed '$d' | sed 's/^RECOMMENDATIONS:[[:space:]]*//' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
161
|
+
|
|
162
|
+
# has_warnings/has_errors: 0 only if content is empty or "None" (case-insensitive)
|
|
163
|
+
WNORM=$(echo "$REVIEW_WARNINGS" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
164
|
+
ENORM=$(echo "$REVIEW_ERRORS" | tr -d '\r' | tr '[:upper:]' '[:lower:]' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
165
|
+
[[ -z "$WNORM" || "$WNORM" == "none" ]] && HAS_WARNINGS=0 || HAS_WARNINGS=1
|
|
166
|
+
[[ -z "$ENORM" || "$ENORM" == "none" ]] && HAS_ERRORS=0 || HAS_ERRORS=1
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
# Pass only if rating >= 95 and no warnings and no errors
|
|
170
|
+
PASS=0
|
|
171
|
+
if [[ "$REVIEW_RATING" -ge 95 ]] && [[ "$HAS_WARNINGS" -eq 0 ]] && [[ "$HAS_ERRORS" -eq 0 ]]; then
|
|
172
|
+
PASS=1
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Always print token usage
|
|
176
|
+
if [[ -n "$INPUT_T" && -n "$OUTPUT_T" ]]; then
|
|
177
|
+
TOTAL=$((INPUT_T + OUTPUT_T))
|
|
178
|
+
echo "Token usage: ${INPUT_T} input, ${OUTPUT_T} output (total ${TOTAL})" >&2
|
|
179
|
+
else
|
|
180
|
+
echo "Token usage: not reported by Codex CLI" >&2
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# Save review to .shimwrapper/reviews/ as markdown (always, pass or fail)
|
|
184
|
+
REVIEWS_DIR="$ROOT_DIR/.shimwrapper/reviews"
|
|
185
|
+
mkdir -p "$REVIEWS_DIR"
|
|
186
|
+
REVIEW_FILE="$REVIEWS_DIR/review-$(date +%Y%m%d-%H%M%S).md"
|
|
187
|
+
BRANCH=""
|
|
188
|
+
[[ -n "${GIT_BRANCH:-}" ]] && BRANCH="$GIT_BRANCH" || BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")"
|
|
189
|
+
{
|
|
190
|
+
echo "# AI Code Review — $(date -Iseconds 2>/dev/null || date '+%Y-%m-%dT%H:%M:%S%z')"
|
|
191
|
+
echo ""
|
|
192
|
+
echo "- **Branch:** $BRANCH"
|
|
193
|
+
echo "- **Verdict:** $([ "$PASS" -eq 1 ] && echo "PASS" || echo "FAIL")"
|
|
194
|
+
echo "- **Rating:** ${REVIEW_RATING}%"
|
|
195
|
+
echo "- **Tokens:** ${INPUT_T:-?} input, ${OUTPUT_T:-?} output"
|
|
196
|
+
echo ""
|
|
197
|
+
echo "## Structured review"
|
|
198
|
+
echo ""
|
|
199
|
+
echo '```'
|
|
200
|
+
[[ -n "$RESULT_TEXT" ]] && echo "$RESULT_TEXT" || echo "(no review text)"
|
|
201
|
+
echo '```'
|
|
202
|
+
} >> "$REVIEW_FILE"
|
|
203
|
+
echo "Review saved: $REVIEW_FILE" >&2
|
|
204
|
+
|
|
205
|
+
# Always print review result and full structured review
|
|
206
|
+
if [[ $PASS -eq 1 ]]; then
|
|
207
|
+
echo "Codex AI review: PASS" >&2
|
|
208
|
+
else
|
|
209
|
+
echo "Codex AI review: FAIL" >&2
|
|
210
|
+
fi
|
|
211
|
+
echo "Rating: ${REVIEW_RATING}%" >&2
|
|
212
|
+
echo "Positive: ${REVIEW_POSITIVE:-"(none)"}" >&2
|
|
213
|
+
echo "Warnings: ${REVIEW_WARNINGS:-"(none)"}" >&2
|
|
214
|
+
echo "Errors: ${REVIEW_ERRORS:-"(none)"}" >&2
|
|
215
|
+
echo "Recommendations: ${REVIEW_RECOMMENDATIONS:-"(none)"}" >&2
|
|
216
|
+
|
|
217
|
+
[[ $PASS -eq 1 ]] && exit 0 || exit 1
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
if git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then
|
|
7
|
+
RANGE="@{u}...HEAD"
|
|
8
|
+
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR "$RANGE")
|
|
9
|
+
else
|
|
10
|
+
if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
|
11
|
+
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR HEAD~1...HEAD)
|
|
12
|
+
else
|
|
13
|
+
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR --root HEAD)
|
|
14
|
+
fi
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if [[ -z "$CHANGED_FILES" ]]; then
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
run_frontend=false
|
|
22
|
+
run_backend=false
|
|
23
|
+
|
|
24
|
+
printf "%s\n" "$CHANGED_FILES" | grep -q '^src/' && run_frontend=true
|
|
25
|
+
printf "%s\n" "$CHANGED_FILES" | grep -q '^supabase/functions/' && run_backend=true
|
|
26
|
+
|
|
27
|
+
if [[ "$run_frontend" = false ]] && [[ "$run_backend" = false ]]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
CHECK_ARGS=()
|
|
32
|
+
[[ "$run_frontend" = true ]] && CHECK_ARGS+=(--frontend)
|
|
33
|
+
[[ "$run_backend" = true ]] && CHECK_ARGS+=(--backend)
|
|
34
|
+
[[ -n "${SKIP_AI_REVIEW:-}" ]] && CHECK_ARGS+=(--no-ai-review)
|
|
35
|
+
|
|
36
|
+
if [[ -x "$ROOT_DIR/scripts/run-checks.sh" ]]; then
|
|
37
|
+
bash "$ROOT_DIR/scripts/run-checks.sh" "${CHECK_ARGS[@]}"
|
|
38
|
+
else
|
|
39
|
+
echo "run-checks.sh not found. Create scripts/run-checks.sh or update this hook." >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Same checks as supabase-checked wrapper (scripts/run-checks.sh).
|
|
3
|
+
# Runs on every git push to GitHub so checks are always enforced.
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
if git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then
|
|
7
|
+
RANGE="@{u}...HEAD"
|
|
8
|
+
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR "$RANGE")
|
|
9
|
+
else
|
|
10
|
+
if git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
|
11
|
+
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR HEAD~1...HEAD)
|
|
12
|
+
else
|
|
13
|
+
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR --root HEAD)
|
|
14
|
+
fi
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if [ -z "$CHANGED_FILES" ]; then
|
|
18
|
+
exit 0
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
run_frontend=false
|
|
22
|
+
run_backend=false
|
|
23
|
+
|
|
24
|
+
printf "%s\n" "$CHANGED_FILES" | grep -q '^src/' && run_frontend=true
|
|
25
|
+
printf "%s\n" "$CHANGED_FILES" | grep -q '^supabase/functions/' && run_backend=true
|
|
26
|
+
|
|
27
|
+
if [ "$run_frontend" = false ] && [ "$run_backend" = false ]; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# On push: AI review is mandatory (PASS only if rating >= 95% and no warnings/errors). Do not allow skip.
|
|
32
|
+
CHECK_ARGS=""
|
|
33
|
+
[ "$run_frontend" = true ] && CHECK_ARGS="--frontend"
|
|
34
|
+
[ "$run_backend" = true ] && CHECK_ARGS="${CHECK_ARGS:+$CHECK_ARGS }--backend"
|
|
35
|
+
# Unset so run-checks.sh always runs the AI review; push is only allowed when review passes (95%+).
|
|
36
|
+
unset SKIP_AI_REVIEW
|
|
37
|
+
bash scripts/run-checks.sh $CHECK_ARGS
|
|
38
|
+
|
|
39
|
+
# Update README (e.g. "Last updated" line) so it stays in sync on push
|
|
40
|
+
if [ -f scripts/update-readme-on-push.sh ]; then
|
|
41
|
+
bash scripts/update-readme-on-push.sh
|
|
42
|
+
if ! git diff --quiet README.md 2>/dev/null; then
|
|
43
|
+
git add README.md
|
|
44
|
+
git commit -m "docs: update README (auto on push)"
|
|
45
|
+
fi
|
|
46
|
+
fi
|