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.
Files changed (48) hide show
  1. package/AGENTS.md +21 -0
  2. package/README.md +286 -0
  3. package/bin/git +5 -0
  4. package/bin/shim +5 -0
  5. package/bin/shimwrappercheck +2 -0
  6. package/bin/supabase +5 -0
  7. package/dashboard/.next/cache/config.json +7 -0
  8. package/dashboard/.next/package.json +1 -0
  9. package/dashboard/.next/routes-manifest.json +1 -0
  10. package/dashboard/.next/trace +1 -0
  11. package/dashboard/README.md +32 -0
  12. package/dashboard/app/agents/page.tsx +88 -0
  13. package/dashboard/app/api/agents-md/route.ts +68 -0
  14. package/dashboard/app/api/config/route.ts +51 -0
  15. package/dashboard/app/api/run-checks/route.ts +54 -0
  16. package/dashboard/app/api/settings/route.ts +126 -0
  17. package/dashboard/app/api/status/route.ts +38 -0
  18. package/dashboard/app/config/page.tsx +77 -0
  19. package/dashboard/app/globals.css +20 -0
  20. package/dashboard/app/layout.tsx +23 -0
  21. package/dashboard/app/page.tsx +122 -0
  22. package/dashboard/app/settings/page.tsx +422 -0
  23. package/dashboard/components/Nav.tsx +33 -0
  24. package/dashboard/components/StatusCard.tsx +27 -0
  25. package/dashboard/lib/presets.ts +97 -0
  26. package/dashboard/lib/projectRoot.ts +25 -0
  27. package/dashboard/next.config.js +6 -0
  28. package/dashboard/package-lock.json +5307 -0
  29. package/dashboard/package.json +28 -0
  30. package/dashboard/postcss.config.js +6 -0
  31. package/dashboard/tailwind.config.js +14 -0
  32. package/dashboard/tsconfig.json +20 -0
  33. package/docs/SHIM_WRAPPER_CONCEPT.md +79 -0
  34. package/docs/WORKFLOW_AND_GAP_ANALYSIS.md +159 -0
  35. package/package.json +24 -0
  36. package/scripts/cli-checked.sh +307 -0
  37. package/scripts/cli.js +23 -0
  38. package/scripts/fetch-edge-logs.sh +96 -0
  39. package/scripts/git-checked.sh +194 -0
  40. package/scripts/init.js +341 -0
  41. package/scripts/install.js +303 -0
  42. package/scripts/ping-edge-health.sh +113 -0
  43. package/scripts/setup.js +55 -0
  44. package/scripts/supabase-checked.sh +330 -0
  45. package/templates/ai-code-review.sh +217 -0
  46. package/templates/git-pre-push +41 -0
  47. package/templates/husky-pre-push +46 -0
  48. 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