@jonit-dev/night-watch-cli 1.8.8-beta.1 → 1.8.8-beta.11

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 (52) hide show
  1. package/dist/cli.js +981 -78
  2. package/dist/cli.js.map +1 -1
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +39 -6
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/commands/install.d.ts +8 -0
  7. package/dist/commands/install.d.ts.map +1 -1
  8. package/dist/commands/install.js +50 -0
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/merge.d.ts +26 -0
  11. package/dist/commands/merge.d.ts.map +1 -0
  12. package/dist/commands/merge.js +159 -0
  13. package/dist/commands/merge.js.map +1 -0
  14. package/dist/commands/qa.d.ts.map +1 -1
  15. package/dist/commands/qa.js +2 -0
  16. package/dist/commands/qa.js.map +1 -1
  17. package/dist/commands/queue.d.ts.map +1 -1
  18. package/dist/commands/queue.js +27 -4
  19. package/dist/commands/queue.js.map +1 -1
  20. package/dist/commands/resolve.d.ts +26 -0
  21. package/dist/commands/resolve.d.ts.map +1 -0
  22. package/dist/commands/resolve.js +186 -0
  23. package/dist/commands/resolve.js.map +1 -0
  24. package/dist/commands/review.d.ts +5 -1
  25. package/dist/commands/review.d.ts.map +1 -1
  26. package/dist/commands/review.js +18 -18
  27. package/dist/commands/review.js.map +1 -1
  28. package/dist/commands/slice.d.ts +1 -0
  29. package/dist/commands/slice.d.ts.map +1 -1
  30. package/dist/commands/slice.js +19 -19
  31. package/dist/commands/slice.js.map +1 -1
  32. package/dist/commands/summary.d.ts +14 -0
  33. package/dist/commands/summary.d.ts.map +1 -0
  34. package/dist/commands/summary.js +193 -0
  35. package/dist/commands/summary.js.map +1 -0
  36. package/dist/commands/uninstall.d.ts.map +1 -1
  37. package/dist/commands/uninstall.js +14 -2
  38. package/dist/commands/uninstall.js.map +1 -1
  39. package/dist/scripts/night-watch-helpers.sh +10 -1
  40. package/dist/scripts/night-watch-merger-cron.sh +321 -0
  41. package/dist/scripts/night-watch-pr-resolver-cron.sh +402 -0
  42. package/dist/scripts/night-watch-pr-reviewer-cron.sh +23 -142
  43. package/dist/scripts/night-watch-qa-cron.sh +30 -4
  44. package/dist/scripts/test-helpers.bats +45 -0
  45. package/dist/templates/night-watch-pr-reviewer.md +2 -1
  46. package/dist/templates/pr-reviewer.md +2 -1
  47. package/dist/templates/slicer.md +54 -64
  48. package/dist/web/assets/index-CPQbZ1BL.css +1 -0
  49. package/dist/web/assets/index-CiRJZI4z.js +386 -0
  50. package/dist/web/assets/index-ZE5lOeJp.js +386 -0
  51. package/dist/web/index.html +2 -2
  52. package/package.json +1 -1
@@ -0,0 +1,321 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Night Watch Merge Orchestrator Cron Runner
5
+ # Usage: night-watch-merger-cron.sh /path/to/project
6
+ #
7
+ # Scans all open PRs, filters eligible ones, and merges them in FIFO order
8
+ # (oldest PR first by creation date). Rebases remaining PRs after each merge.
9
+ #
10
+ # Required env vars (with defaults shown):
11
+ # NW_MERGER_MAX_RUNTIME=1800 - Maximum runtime in seconds (30 min)
12
+ # NW_MERGER_MERGE_METHOD=squash - Merge method: squash|merge|rebase
13
+ # NW_MERGER_MIN_REVIEW_SCORE=80 - Minimum review score threshold
14
+ # NW_MERGER_BRANCH_PATTERNS= - Comma-separated branch prefixes (empty = all)
15
+ # NW_MERGER_REBASE_BEFORE_MERGE=1 - Set to 1 to rebase before merging
16
+ # NW_MERGER_MAX_PRS_PER_RUN=0 - Max PRs to merge per run (0 = unlimited)
17
+ # NW_DRY_RUN=0 - Set to 1 for dry-run mode
18
+
19
+ PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
20
+ PROJECT_NAME=$(basename "${PROJECT_DIR}")
21
+ LOG_DIR="${PROJECT_DIR}/logs"
22
+ LOG_FILE="${LOG_DIR}/merger.log"
23
+ MAX_RUNTIME="${NW_MERGER_MAX_RUNTIME:-1800}"
24
+ MAX_LOG_SIZE="524288" # 512 KB
25
+ MERGE_METHOD="${NW_MERGER_MERGE_METHOD:-squash}"
26
+ MIN_REVIEW_SCORE="${NW_MERGER_MIN_REVIEW_SCORE:-80}"
27
+ REBASE_BEFORE_MERGE="${NW_MERGER_REBASE_BEFORE_MERGE:-1}"
28
+ MAX_PRS_PER_RUN="${NW_MERGER_MAX_PRS_PER_RUN:-0}"
29
+ BRANCH_PATTERNS_RAW="${NW_MERGER_BRANCH_PATTERNS:-}"
30
+ SCRIPT_START_TIME=$(date +%s)
31
+ DRY_RUN="${NW_DRY_RUN:-0}"
32
+ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
33
+ PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
34
+
35
+ # Normalize numeric settings
36
+ if ! [[ "${MAX_PRS_PER_RUN}" =~ ^[0-9]+$ ]]; then
37
+ MAX_PRS_PER_RUN="0"
38
+ fi
39
+ if ! [[ "${MIN_REVIEW_SCORE}" =~ ^[0-9]+$ ]]; then
40
+ MIN_REVIEW_SCORE="80"
41
+ fi
42
+ # Clamp merge method to valid values
43
+ case "${MERGE_METHOD}" in
44
+ squash|merge|rebase) ;;
45
+ *) MERGE_METHOD="squash" ;;
46
+ esac
47
+
48
+ mkdir -p "${LOG_DIR}"
49
+
50
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
51
+ # shellcheck source=night-watch-helpers.sh
52
+ source "${SCRIPT_DIR}/night-watch-helpers.sh"
53
+
54
+ PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
55
+ # NOTE: Lock file path must match mergerLockPath() in src/utils/status-data.ts
56
+ LOCK_FILE="/tmp/night-watch-merger-${PROJECT_RUNTIME_KEY}.lock"
57
+ SCRIPT_TYPE="merger"
58
+
59
+ MERGED_PRS=0
60
+ FAILED_PRS=0
61
+ MERGED_PR_LIST=""
62
+
63
+ emit_result() {
64
+ local status="${1:?status required}"
65
+ local details="${2:-}"
66
+ if [ -n "${details}" ]; then
67
+ echo "NIGHT_WATCH_RESULT:${status}|${details}"
68
+ else
69
+ echo "NIGHT_WATCH_RESULT:${status}"
70
+ fi
71
+ }
72
+
73
+ # ── Global Job Queue Gate ────────────────────────────────────────────────────
74
+ # Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
75
+ if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
76
+ if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
77
+ arm_global_queue_cleanup
78
+ else
79
+ claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
80
+ fi
81
+ fi
82
+ # ──────────────────────────────────────────────────────────────────────────────
83
+
84
+ # Check if branch matches configured patterns
85
+ matches_branch_patterns() {
86
+ local branch="${1}"
87
+ if [ -z "${BRANCH_PATTERNS_RAW}" ]; then
88
+ return 0 # No filter = match all
89
+ fi
90
+ IFS=',' read -ra patterns <<< "${BRANCH_PATTERNS_RAW}"
91
+ for pattern in "${patterns[@]}"; do
92
+ pattern="${pattern# }" # trim leading space
93
+ if [ -n "${pattern}" ] && [[ "${branch}" == "${pattern}"* ]]; then
94
+ return 0
95
+ fi
96
+ done
97
+ return 1
98
+ }
99
+
100
+ # Get review score from PR labels/comments
101
+ get_review_score() {
102
+ local pr_number="${1}"
103
+ # Look for review score comment from night-watch
104
+ local score
105
+ score=$(gh pr view "${pr_number}" --json comments \
106
+ --jq '[.comments[].body | select(test("review score|score:? [0-9]+/100"; "i")) | capture("(?i)score:? *(?<s>[0-9]+)/100") | .s] | last | tonumber // -1' \
107
+ 2>/dev/null || echo "-1")
108
+ echo "${score}"
109
+ }
110
+
111
+ # Check if CI is passing for a PR (all checks must be complete and none failing)
112
+ ci_passing() {
113
+ local pr_number="${1}"
114
+ local checks_json
115
+ checks_json=$(gh pr checks "${pr_number}" --json name,state,conclusion 2>/dev/null || echo "[]")
116
+ # Fail if any checks have explicit failures
117
+ local fail_count
118
+ fail_count=$(echo "${checks_json}" | jq '[.[] | select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "CANCELLED" or .state == "FAILURE")] | length' 2>/dev/null || echo "999")
119
+ if [ "${fail_count}" != "0" ]; then
120
+ return 1
121
+ fi
122
+ # Fail if any checks are still pending/in-progress (not yet concluded)
123
+ local pending_count
124
+ pending_count=$(echo "${checks_json}" | jq '[.[] | select(.state == "PENDING" or .state == "IN_PROGRESS" or (.conclusion == null and .state != "SUCCESS"))] | length' 2>/dev/null || echo "999")
125
+ if [ "${pending_count}" != "0" ]; then
126
+ return 1
127
+ fi
128
+ return 0
129
+ }
130
+
131
+ # Rebase a PR against its base branch
132
+ rebase_pr() {
133
+ local pr_number="${1}"
134
+ log "INFO: Rebasing PR #${pr_number} against base branch"
135
+ if [ "${DRY_RUN}" = "1" ]; then
136
+ log "INFO: [DRY RUN] Would rebase PR #${pr_number}"
137
+ return 0
138
+ fi
139
+ gh pr update-branch --rebase "${pr_number}" 2>/dev/null
140
+ return $?
141
+ }
142
+
143
+ log() {
144
+ echo "[$(date '+%Y-%m-%dT%H:%M:%S')] $*" | tee -a "${LOG_FILE}"
145
+ }
146
+
147
+ rotate_log() {
148
+ if [ -f "${LOG_FILE}" ] && [ "$(stat -c%s "${LOG_FILE}" 2>/dev/null || echo 0)" -ge "${MAX_LOG_SIZE}" ]; then
149
+ mv "${LOG_FILE}" "${LOG_FILE}.bak" 2>/dev/null || true
150
+ fi
151
+ }
152
+
153
+ # ── Log rotation ──────────────────────────────────────────────────────────────
154
+ rotate_log
155
+ # ─────────────────────────────────────────────────────────────────────────────
156
+
157
+ cd "${PROJECT_DIR}"
158
+
159
+ log "========================================"
160
+ log "RUN-START: merger invoked project=${PROJECT_DIR} dry_run=${DRY_RUN}"
161
+ log "CONFIG: merge_method=${MERGE_METHOD} min_review_score=${MIN_REVIEW_SCORE} rebase_before_merge=${REBASE_BEFORE_MERGE} max_prs=${MAX_PRS_PER_RUN} max_runtime=${MAX_RUNTIME}s branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
162
+ log "========================================"
163
+
164
+ if ! acquire_lock "${LOCK_FILE}"; then
165
+ emit_result "skip_locked"
166
+ exit 0
167
+ fi
168
+
169
+ # ── Dry-run mode ────────────────────────────────────────────────────────────
170
+ if [ "${DRY_RUN}" = "1" ]; then
171
+ echo "=== Dry Run: Merge Orchestrator ==="
172
+ echo "Merge Method: ${MERGE_METHOD}"
173
+ echo "Min Review Score: ${MIN_REVIEW_SCORE}/100"
174
+ echo "Rebase Before Merge: ${REBASE_BEFORE_MERGE}"
175
+ echo "Max PRs Per Run: ${MAX_PRS_PER_RUN} (0=unlimited)"
176
+ echo "Max Runtime: ${MAX_RUNTIME}s"
177
+ echo "Branch Patterns: ${BRANCH_PATTERNS_RAW:-<all>}"
178
+ log "INFO: Dry run mode — exiting without processing"
179
+ emit_result "skip_dry_run"
180
+ exit 0
181
+ fi
182
+
183
+ # Timeout watchdog
184
+ (
185
+ sleep "${MAX_RUNTIME}"
186
+ log "TIMEOUT: Merger exceeded ${MAX_RUNTIME}s, terminating"
187
+ kill -TERM $$ 2>/dev/null || true
188
+ ) &
189
+ WATCHDOG_PID=$!
190
+ trap 'kill ${WATCHDOG_PID} 2>/dev/null || true; rm -f "${LOCK_FILE}"' EXIT
191
+
192
+ # Discover open PRs sorted by creation date (oldest first = FIFO)
193
+ log "INFO: Scanning open PRs..."
194
+ PR_LIST_JSON=$(gh pr list --state open \
195
+ --json number,headRefName,createdAt,isDraft \
196
+ --jq 'sort_by(.createdAt)' \
197
+ 2>/dev/null || echo "[]")
198
+
199
+ PR_COUNT=$(echo "${PR_LIST_JSON}" | jq 'length')
200
+ log "INFO: Found ${PR_COUNT} open PRs"
201
+
202
+ if [ "${PR_COUNT}" = "0" ]; then
203
+ log "SKIP: No open PRs found. Exiting."
204
+ emit_result "skip_no_prs"
205
+ exit 0
206
+ fi
207
+
208
+ # Process each PR in FIFO order
209
+ PROCESSED=0
210
+ while IFS= read -r pr_json; do
211
+ pr_number=$(echo "${pr_json}" | jq -r '.number')
212
+ pr_branch=$(echo "${pr_json}" | jq -r '.headRefName')
213
+ is_draft=$(echo "${pr_json}" | jq -r '.isDraft')
214
+
215
+ # Skip drafts
216
+ if [ "${is_draft}" = "true" ]; then
217
+ log "INFO: PR #${pr_number} (${pr_branch}): Skipping draft"
218
+ continue
219
+ fi
220
+
221
+ # Check branch pattern filter
222
+ if ! matches_branch_patterns "${pr_branch}"; then
223
+ log "DEBUG: PR #${pr_number} (${pr_branch}): Branch pattern mismatch, skipping"
224
+ continue
225
+ fi
226
+
227
+ # Check CI status
228
+ if ! ci_passing "${pr_number}"; then
229
+ log "INFO: PR #${pr_number} (${pr_branch}): CI not passing, skipping"
230
+ continue
231
+ fi
232
+
233
+ # Check review score
234
+ if [ "${MIN_REVIEW_SCORE}" -gt "0" ]; then
235
+ score=$(get_review_score "${pr_number}")
236
+ if [ "${score}" -lt "0" ] || [ "${score}" -lt "${MIN_REVIEW_SCORE}" ]; then
237
+ log "INFO: PR #${pr_number} (${pr_branch}): Review score ${score} < ${MIN_REVIEW_SCORE} (or no score found), skipping"
238
+ continue
239
+ fi
240
+ fi
241
+
242
+ log "INFO: PR #${pr_number} (${pr_branch}): Eligible for merge"
243
+
244
+ # Rebase before merge if configured
245
+ if [ "${REBASE_BEFORE_MERGE}" = "1" ]; then
246
+ if ! rebase_pr "${pr_number}"; then
247
+ log "WARN: PR #${pr_number}: Rebase failed, skipping"
248
+ FAILED_PRS=$((FAILED_PRS + 1))
249
+ continue
250
+ fi
251
+ log "INFO: PR #${pr_number}: Rebase successful"
252
+
253
+ # Poll CI until all checks complete after rebase (up to 5 minutes)
254
+ local ci_max_wait=300
255
+ local ci_waited=0
256
+ local ci_poll=15
257
+ while [ "${ci_waited}" -lt "${ci_max_wait}" ]; do
258
+ sleep "${ci_poll}"
259
+ ci_waited=$((ci_waited + ci_poll))
260
+ if ci_passing "${pr_number}"; then
261
+ break
262
+ fi
263
+ log "INFO: PR #${pr_number}: Waiting for CI after rebase (${ci_waited}s/${ci_max_wait}s)..."
264
+ done
265
+ if ! ci_passing "${pr_number}"; then
266
+ log "INFO: PR #${pr_number}: CI not passing after rebase (waited ${ci_waited}s), skipping"
267
+ continue
268
+ fi
269
+ fi
270
+
271
+ # Merge the PR
272
+ log "INFO: Merging PR #${pr_number} with method: ${MERGE_METHOD}..."
273
+ if gh pr merge "${pr_number}" "--${MERGE_METHOD}" --delete-branch 2>&1 | tee -a "${LOG_FILE}"; then
274
+ log "INFO: PR #${pr_number}: Merged successfully"
275
+ MERGED_PRS=$((MERGED_PRS + 1))
276
+ MERGED_PR_LIST="${MERGED_PR_LIST}${pr_number},"
277
+
278
+ # Rebase remaining PRs after each successful merge
279
+ log "INFO: Rebasing remaining open PRs after merging #${pr_number}..."
280
+ REMAINING_JSON=$(gh pr list --state open \
281
+ --json number,headRefName \
282
+ 2>/dev/null || echo "[]")
283
+ while IFS= read -r remaining_pr; do
284
+ remaining_number=$(echo "${remaining_pr}" | jq -r '.number')
285
+ remaining_branch=$(echo "${remaining_pr}" | jq -r '.headRefName')
286
+ if [ "${remaining_number}" != "${pr_number}" ]; then
287
+ log "INFO: Rebasing remaining PR #${remaining_number} (${remaining_branch})"
288
+ gh pr update-branch --rebase "${remaining_number}" 2>/dev/null || \
289
+ log "WARN: PR #${remaining_number}: Rebase failed (continuing)"
290
+ fi
291
+ done < <(echo "${REMAINING_JSON}" | jq -c '.[]')
292
+ else
293
+ log "WARN: PR #${pr_number}: Merge failed"
294
+ FAILED_PRS=$((FAILED_PRS + 1))
295
+ fi
296
+
297
+ PROCESSED=$((PROCESSED + 1))
298
+
299
+ # Check max PRs per run limit
300
+ if [ "${MAX_PRS_PER_RUN}" -gt "0" ] && [ "${PROCESSED}" -ge "${MAX_PRS_PER_RUN}" ]; then
301
+ log "INFO: Reached max PRs per run limit (${MAX_PRS_PER_RUN}). Stopping."
302
+ break
303
+ fi
304
+
305
+ # Enforce global timeout
306
+ elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
307
+ if [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
308
+ log "WARN: Global timeout reached (${MAX_RUNTIME}s), stopping early"
309
+ break
310
+ fi
311
+ done < <(echo "${PR_LIST_JSON}" | jq -c '.[]')
312
+
313
+ # Trim trailing comma from PR list
314
+ MERGED_PR_LIST="${MERGED_PR_LIST%,}"
315
+
316
+ log "========================================"
317
+ log "RUN-END: merger complete merged=${MERGED_PRS} failed=${FAILED_PRS}"
318
+ log "========================================"
319
+
320
+ emit_result "success" "merged=${MERGED_PRS}|failed=${FAILED_PRS}|prs=${MERGED_PR_LIST}"
321
+ exit 0