@jonit-dev/night-watch-cli 1.8.8-beta.0 → 1.8.8-beta.10
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/dist/cli.js +640 -24
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +38 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +25 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/qa.d.ts.map +1 -1
- package/dist/commands/qa.js +5 -0
- package/dist/commands/qa.js.map +1 -1
- package/dist/commands/queue.d.ts.map +1 -1
- package/dist/commands/queue.js +27 -4
- package/dist/commands/queue.js.map +1 -1
- package/dist/commands/resolve.d.ts +26 -0
- package/dist/commands/resolve.d.ts.map +1 -0
- package/dist/commands/resolve.js +186 -0
- package/dist/commands/resolve.js.map +1 -0
- package/dist/commands/review.d.ts +5 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +18 -5
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/summary.d.ts +14 -0
- package/dist/commands/summary.d.ts.map +1 -0
- package/dist/commands/summary.js +193 -0
- package/dist/commands/summary.js.map +1 -0
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +14 -2
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/scripts/night-watch-helpers.sh +10 -1
- package/dist/scripts/night-watch-pr-resolver-cron.sh +402 -0
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +22 -5
- package/dist/scripts/night-watch-qa-cron.sh +107 -38
- package/dist/scripts/test-helpers.bats +45 -0
- package/dist/templates/night-watch-pr-reviewer.md +2 -1
- package/dist/templates/pr-reviewer.md +2 -1
- package/package.json +1 -1
|
@@ -25,6 +25,7 @@ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
|
25
25
|
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
26
26
|
BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
|
|
27
27
|
SKIP_LABEL="${NW_QA_SKIP_LABEL:-skip-qa}"
|
|
28
|
+
VALIDATED_LABEL="${NW_QA_VALIDATED_LABEL:-e2e-validated}"
|
|
28
29
|
QA_ARTIFACTS="${NW_QA_ARTIFACTS:-both}"
|
|
29
30
|
QA_AUTO_INSTALL_PLAYWRIGHT="${NW_QA_AUTO_INSTALL_PLAYWRIGHT:-1}"
|
|
30
31
|
SCRIPT_START_TIME=$(date +%s)
|
|
@@ -55,6 +56,16 @@ emit_result() {
|
|
|
55
56
|
fi
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
LABEL_ENSURED=0
|
|
60
|
+
ensure_validated_label() {
|
|
61
|
+
if [ "${LABEL_ENSURED}" -eq 1 ]; then return 0; fi
|
|
62
|
+
gh label create "${VALIDATED_LABEL}" \
|
|
63
|
+
--description "PR acceptance requirements validated by e2e/integration tests" \
|
|
64
|
+
--color "0e8a16" \
|
|
65
|
+
--force 2>/dev/null || true
|
|
66
|
+
LABEL_ENSURED=1
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
# ── Global Job Queue Gate ────────────────────────────────────────────────────
|
|
59
70
|
# Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
|
|
60
71
|
if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
|
|
@@ -330,8 +341,8 @@ validate_qa_evidence() {
|
|
|
330
341
|
fi
|
|
331
342
|
|
|
332
343
|
if ! pr_has_qa_generated_files "${pr_number}"; then
|
|
333
|
-
log "
|
|
334
|
-
return
|
|
344
|
+
log "WARN-QA-EVIDENCE: PR #${pr_number} has QA marker comment but no qa-artifacts/ or tests/*/qa/ files"
|
|
345
|
+
return 2
|
|
335
346
|
fi
|
|
336
347
|
|
|
337
348
|
if [ "${QA_ARTIFACTS}" = "screenshot" ] || [ "${QA_ARTIFACTS}" = "both" ]; then
|
|
@@ -487,13 +498,16 @@ fi
|
|
|
487
498
|
EXIT_CODE=0
|
|
488
499
|
PROCESSED_PRS_CSV=""
|
|
489
500
|
PASSING_PRS_CSV=""
|
|
501
|
+
VALIDATED_PRS_CSV=""
|
|
490
502
|
ISSUES_FOUND_PRS_CSV=""
|
|
491
503
|
NO_TESTS_PRS_CSV=""
|
|
492
504
|
UNCLASSIFIED_PRS_CSV=""
|
|
505
|
+
WARNING_PRS_CSV=""
|
|
493
506
|
FAILED_AUTOMATION_PRS_CSV=""
|
|
494
507
|
FAILED_PR=""
|
|
495
508
|
FAILED_REASON="unknown"
|
|
496
509
|
QA_SCREENSHOT_SUMMARY=""
|
|
510
|
+
QA_WARNING_SUMMARY=""
|
|
497
511
|
|
|
498
512
|
# Process each PR that needs QA
|
|
499
513
|
for pr_ref in ${PRS_NEEDING_QA}; do
|
|
@@ -595,34 +609,56 @@ for pr_ref in ${PRS_NEEDING_QA}; do
|
|
|
595
609
|
FAILED_PR="#${pr_num}"
|
|
596
610
|
FAILED_REASON="invalid_provider_output"
|
|
597
611
|
EXIT_CODE=1
|
|
598
|
-
elif ! validate_qa_evidence "${pr_num}"; then
|
|
599
|
-
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
600
|
-
FAILED_PR="#${pr_num}"
|
|
601
|
-
FAILED_REASON="qa_evidence_validation_failed"
|
|
602
|
-
EXIT_CODE=1
|
|
603
612
|
else
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
613
|
+
if validate_qa_evidence "${pr_num}"; then
|
|
614
|
+
QA_EVIDENCE_STATUS=0
|
|
615
|
+
else
|
|
616
|
+
QA_EVIDENCE_STATUS=$?
|
|
617
|
+
fi
|
|
618
|
+
if [ ${QA_EVIDENCE_STATUS} -eq 2 ]; then
|
|
619
|
+
WARNING_PRS_CSV=$(append_csv "${WARNING_PRS_CSV}" "#${pr_num}")
|
|
620
|
+
QA_WARNING_SUMMARY="${QA_WARNING_SUMMARY}${QA_WARNING_SUMMARY:+$'\n'}#${pr_num}: no qa-artifacts/ or tests/*/qa/ files"
|
|
621
|
+
log "QA: PR #${pr_num} — provider completed with warning-only QA evidence"
|
|
622
|
+
elif [ ${QA_EVIDENCE_STATUS} -ne 0 ]; then
|
|
623
|
+
FAILED_AUTOMATION_PRS_CSV=$(append_csv "${FAILED_AUTOMATION_PRS_CSV}" "#${pr_num}")
|
|
624
|
+
FAILED_PR="#${pr_num}"
|
|
625
|
+
FAILED_REASON="qa_evidence_validation_failed"
|
|
626
|
+
EXIT_CODE=1
|
|
627
|
+
else
|
|
628
|
+
QA_OUTCOME=$(classify_qa_comment_outcome "${pr_num}")
|
|
629
|
+
case "${QA_OUTCOME}" in
|
|
630
|
+
passing)
|
|
631
|
+
PASSING_PRS_CSV=$(append_csv "${PASSING_PRS_CSV}" "#${pr_num}")
|
|
632
|
+
# Apply e2e-validated label
|
|
633
|
+
ensure_validated_label
|
|
634
|
+
gh pr edit "${pr_num}" --add-label "${VALIDATED_LABEL}" 2>/dev/null || true
|
|
635
|
+
VALIDATED_PRS_CSV=$(append_csv "${VALIDATED_PRS_CSV}" "#${pr_num}")
|
|
636
|
+
log "QA: PR #${pr_num} — added '${VALIDATED_LABEL}' label (tests passing)"
|
|
637
|
+
;;
|
|
638
|
+
issues_found)
|
|
639
|
+
ISSUES_FOUND_PRS_CSV=$(append_csv "${ISSUES_FOUND_PRS_CSV}" "#${pr_num}")
|
|
640
|
+
# Remove e2e-validated label if present
|
|
641
|
+
gh pr edit "${pr_num}" --remove-label "${VALIDATED_LABEL}" 2>/dev/null || true
|
|
642
|
+
log "QA: PR #${pr_num} — removed '${VALIDATED_LABEL}' label (issues found)"
|
|
643
|
+
;;
|
|
644
|
+
no_tests_needed)
|
|
645
|
+
NO_TESTS_PRS_CSV=$(append_csv "${NO_TESTS_PRS_CSV}" "#${pr_num}")
|
|
646
|
+
# Remove e2e-validated label — no tests doesn't prove acceptance
|
|
647
|
+
gh pr edit "${pr_num}" --remove-label "${VALIDATED_LABEL}" 2>/dev/null || true
|
|
648
|
+
log "QA: PR #${pr_num} — removed '${VALIDATED_LABEL}' label (no tests needed)"
|
|
649
|
+
;;
|
|
650
|
+
*)
|
|
651
|
+
UNCLASSIFIED_PRS_CSV=$(append_csv "${UNCLASSIFIED_PRS_CSV}" "#${pr_num}")
|
|
652
|
+
;;
|
|
653
|
+
esac
|
|
654
|
+
|
|
655
|
+
PR_FIRST_SCREENSHOT=$(get_qa_screenshot_links "${pr_num}" | head -n 1 || true)
|
|
656
|
+
if [ -n "${PR_FIRST_SCREENSHOT}" ]; then
|
|
657
|
+
QA_SCREENSHOT_SUMMARY="${QA_SCREENSHOT_SUMMARY}${QA_SCREENSHOT_SUMMARY:+$'\n'}#${pr_num}: ${PR_FIRST_SCREENSHOT}"
|
|
658
|
+
fi
|
|
659
|
+
|
|
660
|
+
log "QA: PR #${pr_num} — provider completed with verifiable QA evidence"
|
|
623
661
|
fi
|
|
624
|
-
|
|
625
|
-
log "QA: PR #${pr_num} — provider completed with verifiable QA evidence"
|
|
626
662
|
fi
|
|
627
663
|
fi
|
|
628
664
|
|
|
@@ -633,9 +669,11 @@ cleanup_worktrees "${PROJECT_DIR}"
|
|
|
633
669
|
|
|
634
670
|
FINAL_PROCESSED_PRS_CSV="${PROCESSED_PRS_CSV:-${PRS_NEEDING_QA_CSV}}"
|
|
635
671
|
PASSING_PRS_SUMMARY=$(csv_or_none "${PASSING_PRS_CSV}")
|
|
672
|
+
VALIDATED_PRS_SUMMARY=$(csv_or_none "${VALIDATED_PRS_CSV}")
|
|
636
673
|
ISSUES_FOUND_PRS_SUMMARY=$(csv_or_none "${ISSUES_FOUND_PRS_CSV}")
|
|
637
674
|
NO_TESTS_PRS_SUMMARY=$(csv_or_none "${NO_TESTS_PRS_CSV}")
|
|
638
675
|
UNCLASSIFIED_PRS_SUMMARY=$(csv_or_none "${UNCLASSIFIED_PRS_CSV}")
|
|
676
|
+
WARNING_PRS_SUMMARY=$(csv_or_none "${WARNING_PRS_CSV}")
|
|
639
677
|
FAILED_AUTOMATION_PRS_SUMMARY=$(csv_or_none "${FAILED_AUTOMATION_PRS_CSV}")
|
|
640
678
|
FAILED_PR_SUMMARY=$(csv_or_none "${FAILED_PR}")
|
|
641
679
|
|
|
@@ -643,25 +681,56 @@ QA_TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START_TIME ))
|
|
|
643
681
|
log "OUTCOME: exit_code=${EXIT_CODE} total_elapsed=${QA_TOTAL_ELAPSED}s processed_prs=${FINAL_PROCESSED_PRS_CSV:-none}"
|
|
644
682
|
|
|
645
683
|
if [ ${EXIT_CODE} -eq 0 ]; then
|
|
646
|
-
|
|
647
|
-
|
|
684
|
+
if [ -n "${WARNING_PRS_CSV}" ]; then
|
|
685
|
+
log "DONE-WARN: QA runner completed with warnings"
|
|
686
|
+
TELEGRAM_WARNING_BODY="Project: ${PROJECT_NAME}
|
|
648
687
|
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
649
688
|
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
650
689
|
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
651
690
|
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
691
|
+
E2E validated: ${VALIDATED_PRS_SUMMARY}
|
|
652
692
|
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
653
693
|
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
654
|
-
Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}
|
|
655
|
-
|
|
656
|
-
|
|
694
|
+
Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}
|
|
695
|
+
Warnings: ${WARNING_PRS_SUMMARY}"
|
|
696
|
+
if [ -n "${QA_WARNING_SUMMARY}" ]; then
|
|
697
|
+
TELEGRAM_WARNING_BODY="${TELEGRAM_WARNING_BODY}
|
|
698
|
+
Warning details:
|
|
699
|
+
${QA_WARNING_SUMMARY}"
|
|
700
|
+
fi
|
|
701
|
+
if [ -n "${QA_SCREENSHOT_SUMMARY}" ]; then
|
|
702
|
+
TELEGRAM_WARNING_BODY="${TELEGRAM_WARNING_BODY}
|
|
657
703
|
Screenshot links:
|
|
658
704
|
${QA_SCREENSHOT_SUMMARY}"
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
705
|
+
fi
|
|
706
|
+
send_telegram_status_message "🧪 Night Watch QA: warning" "${TELEGRAM_WARNING_BODY}"
|
|
707
|
+
if [ -n "${REPO}" ]; then
|
|
708
|
+
emit_result "warning_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|warnings=${WARNING_PRS_SUMMARY}|repo=${REPO}"
|
|
709
|
+
else
|
|
710
|
+
emit_result "warning_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|warnings=${WARNING_PRS_SUMMARY}"
|
|
711
|
+
fi
|
|
663
712
|
else
|
|
664
|
-
|
|
713
|
+
log "DONE: QA runner completed successfully"
|
|
714
|
+
TELEGRAM_SUCCESS_BODY="Project: ${PROJECT_NAME}
|
|
715
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
716
|
+
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
717
|
+
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
718
|
+
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
719
|
+
E2E validated: ${VALIDATED_PRS_SUMMARY}
|
|
720
|
+
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
721
|
+
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
722
|
+
Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}"
|
|
723
|
+
if [ -n "${QA_SCREENSHOT_SUMMARY}" ]; then
|
|
724
|
+
TELEGRAM_SUCCESS_BODY="${TELEGRAM_SUCCESS_BODY}
|
|
725
|
+
Screenshot links:
|
|
726
|
+
${QA_SCREENSHOT_SUMMARY}"
|
|
727
|
+
fi
|
|
728
|
+
send_telegram_status_message "🧪 Night Watch QA: completed" "${TELEGRAM_SUCCESS_BODY}"
|
|
729
|
+
if [ -n "${REPO}" ]; then
|
|
730
|
+
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|repo=${REPO}"
|
|
731
|
+
else
|
|
732
|
+
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}"
|
|
733
|
+
fi
|
|
665
734
|
fi
|
|
666
735
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
667
736
|
log "TIMEOUT: QA runner killed after ${MAX_RUNTIME}s"
|
|
@@ -75,3 +75,48 @@ teardown() {
|
|
|
75
75
|
|
|
76
76
|
[ "${result}" = "02-test-prd.md" ]
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
# ── pr-resolver lock acquisition ─────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
@test "pr-resolver lock acquisition: acquire_lock succeeds when no lock exists" {
|
|
82
|
+
local test_lock="/tmp/nw-test-resolver-$$.lock"
|
|
83
|
+
|
|
84
|
+
# Ensure clean state
|
|
85
|
+
rm -f "${test_lock}"
|
|
86
|
+
|
|
87
|
+
run acquire_lock "${test_lock}"
|
|
88
|
+
[ "$status" -eq 0 ]
|
|
89
|
+
[ -f "${test_lock}" ]
|
|
90
|
+
|
|
91
|
+
# PID written to lock file must be the current test process
|
|
92
|
+
local lock_pid
|
|
93
|
+
lock_pid=$(cat "${test_lock}")
|
|
94
|
+
[ -n "${lock_pid}" ]
|
|
95
|
+
|
|
96
|
+
rm -f "${test_lock}"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@test "pr-resolver lock acquisition: acquire_lock fails when active lock exists" {
|
|
100
|
+
local test_lock="/tmp/nw-test-resolver-active-$$.lock"
|
|
101
|
+
|
|
102
|
+
# Write current PID as an active lock holder
|
|
103
|
+
echo $$ > "${test_lock}"
|
|
104
|
+
|
|
105
|
+
run acquire_lock "${test_lock}"
|
|
106
|
+
[ "$status" -eq 1 ]
|
|
107
|
+
|
|
108
|
+
rm -f "${test_lock}"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@test "pr-resolver lock acquisition: acquire_lock removes stale lock and succeeds" {
|
|
112
|
+
local test_lock="/tmp/nw-test-resolver-stale-$$.lock"
|
|
113
|
+
|
|
114
|
+
# Write a PID that does not exist (use a very high number unlikely to be running)
|
|
115
|
+
echo "999999999" > "${test_lock}"
|
|
116
|
+
|
|
117
|
+
run acquire_lock "${test_lock}"
|
|
118
|
+
[ "$status" -eq 0 ]
|
|
119
|
+
[ -f "${test_lock}" ]
|
|
120
|
+
|
|
121
|
+
rm -f "${test_lock}"
|
|
122
|
+
}
|
|
@@ -21,7 +21,8 @@ If current PR code or review feedback conflicts with the PRD context, call out t
|
|
|
21
21
|
## Important: Early Exit
|
|
22
22
|
|
|
23
23
|
- If there are **no open PRs** on `night-watch/` or `feat/` branches, **stop immediately** and report "No PRs to review."
|
|
24
|
-
- If all open PRs have **no merge conflicts**, **passing CI**, and **review score >= 80
|
|
24
|
+
- If all open PRs have **no merge conflicts**, **passing CI**, and **review score >= 80**, **stop immediately** and report "All PRs are in good shape."
|
|
25
|
+
- If a PR has no review score yet, it needs a first review — do NOT skip it.
|
|
25
26
|
- Do **NOT** loop or retry. Process each PR **once** per run. After processing all PRs, stop.
|
|
26
27
|
- Do **NOT** re-check PRs after pushing fixes -- the CI will re-run automatically on the next push.
|
|
27
28
|
|
|
@@ -21,7 +21,8 @@ If current PR code or review feedback conflicts with the PRD context, call out t
|
|
|
21
21
|
## Important: Early Exit
|
|
22
22
|
|
|
23
23
|
- If there are **no open PRs** on `night-watch/` or `feat/` branches, **stop immediately** and report "No PRs to review."
|
|
24
|
-
- If all open PRs have **no merge conflicts**, **passing CI**, and **review score >= 80
|
|
24
|
+
- If all open PRs have **no merge conflicts**, **passing CI**, and **review score >= 80**, **stop immediately** and report "All PRs are in good shape."
|
|
25
|
+
- If a PR has no review score yet, it needs a first review — do NOT skip it.
|
|
25
26
|
- Do **NOT** loop or retry. Process each PR **once** per run. After processing all PRs, stop.
|
|
26
27
|
- Do **NOT** re-check PRs after pushing fixes -- the CI will re-run automatically on the next push.
|
|
27
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jonit-dev/night-watch-cli",
|
|
3
|
-
"version": "1.8.8-beta.
|
|
3
|
+
"version": "1.8.8-beta.10",
|
|
4
4
|
"description": "AI agent that implements your specs, opens PRs, and reviews code overnight. Queue GitHub issues or PRDs, wake up to pull requests.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|