[{"data":1,"prerenderedAt":7584},["ShallowReactive",2],{"page-\u002Fbash\u002F15-exercises-and-projects":3},{"id":4,"title":5,"body":6,"description":7577,"extension":7578,"meta":7579,"navigation":66,"path":7580,"seo":7581,"stem":7582,"__hash__":7583},"content\u002Fbash\u002F15-exercises-and-projects.md","Bash 15 — Production Projects: Backup, Deploy, Monitor & Capstone",{"type":7,"value":8,"toc":7564},"minimark",[9,14,19,1507,1511,2055,2059,2433,2444,2448,3106,3110,3610,3614,4519,4523,6919,6923,7200,7204,7284,7288,7380,7384,7388,7475,7492,7560],[10,11,13],"h1",{"id":12},"_15-production-projects-backup-deploy-monitor-capstone","15 — Production Projects: Backup, Deploy, Monitor & Capstone",[15,16,18],"h2",{"id":17},"project-1-production-backup-script","Project 1 — Production Backup Script",[20,21,23],"code-wrapper",{"language":22},"bash",[24,25,29],"pre",{"className":26,"code":27,"language":22,"meta":28,"style":28},"language-bash shiki shiki-themes github-light github-dark","#!\u002Fusr\u002Fbin\u002Fenv bash\n# ── Timestamped backup with retention, verification, and dry-run ──\nset -Eeuo pipefail\n\nreadonly SCRIPT_NAME=$(basename \"${BASH_SOURCE[0]}\")\nreadonly TS=$(date +%Y%m%d_%H%M%S)\n\n# ── Config ──\nRETENTION=5\nDRY_RUN=false\nCOMPRESS=true\nVERBOSE=false\n\nusage() {\n    cat \u003C\u003CEOF\nUsage: $SCRIPT_NAME [OPTIONS] \u003Csource> \u003Cdestination>\n\nOptions:\n  -h, --help           Show this help\n  -n, --dry-run        Show what would be done (no execution)\n  -k, --keep N         Keep last N backups (default: $RETENTION)\n  -z, --gzip           Compress backup (default: on)\n  -v, --verbose        Verbose output\n\nExamples:\n  $SCRIPT_NAME \u002Fdata \u002Fbackups\n  $SCRIPT_NAME -n -k 10 \u002Fdata \u002Fbackups\nEOF\n}\n\nlog() { printf '[%s] %s\\n' \"$(date -Iseconds)\" \"$*\" >&2; }\ndie() { log \"FATAL: $*\"; exit 1; }\n\n# ── Parse args ──\nwhile [[ $# -gt 0 ]]; do\n    case \"$1\" in\n        -h|--help)    usage; exit 0 ;;\n        -n|--dry-run) DRY_RUN=true; shift ;;\n        -k|--keep)    RETENTION=\"${2:?--keep requires a number}\"; shift 2 ;;\n        -z|--gzip)    COMPRESS=true; shift ;;\n        --no-gzip)    COMPRESS=false; shift ;;\n        -v|--verbose) VERBOSE=true; shift ;;\n        -*)           die \"unknown option: $1\" ;;\n        *)            break ;;\n    esac\ndone\n\n[[ $# -ge 2 ]] || { usage; exit 1; }\nreadonly SRC=\"$1\"\nreadonly DST=\"$2\"\n\n# ── Validate ──\n[[ -d \"$SRC\" ]] || die \"source not a directory: $SRC\"\n[[ -d \"$DST\" ]] || mkdir -p \"$DST\" || die \"can't create destination: $DST\"\n\n# ── Cleanup trap ──\ntmpfile=\"\"\ncleanup() {\n    local code=$?\n    [[ -n \"$tmpfile\" && -f \"$tmpfile\" ]] && rm -f \"$tmpfile\"\n    exit \"$code\"\n}\ntrap cleanup EXIT\n\n# ── Run wrapper (dry-run aware) ──\nrun() {\n    if $DRY_RUN; then\n        log \"DRY RUN: $*\"\n    else\n        $VERBOSE && log \"executing: $*\"\n        \"$@\"\n    fi\n}\n\n# ── Create backup ──\nbackup_file=\"$DST\u002Fbackup_${TS}.tar.$([[ $COMPRESS == true ]] && echo gz || echo tar)\"\nlog \"backing up '$SRC' to '$backup_file'\"\n\nif $COMPRESS; then\n    run tar -czf \"$backup_file\" -C \"$(dirname \"$SRC\")\" \"$(basename \"$SRC\")\"\nelse\n    run tar -cf \"$backup_file\" -C \"$(dirname \"$SRC\")\" \"$(basename \"$SRC\")\"\nfi\n\n# ── Verify backup (only if not dry-run) ──\nif ! $DRY_RUN; then\n    log \"verifying backup...\"\n    if $COMPRESS; then\n        gzip -t \"$backup_file\" 2>\u002Fdev\u002Fnull || die \"backup verification failed (corrupt gzip)\"\n    else\n        tar -tf \"$backup_file\" &>\u002Fdev\u002Fnull || die \"backup verification failed (corrupt tar)\"\n    fi\n    log \"verification passed\"\nfi\n\n# ── Cleanup old backups ──\nlog \"retention: keeping last $RETENTION backups\"\nmapfile -t backups \u003C \u003C(ls -1 \"$DST\"\u002Fbackup_*.tar.* 2>\u002Fdev\u002Fnull | sort -r)\nif (( ${#backups[@]} > RETENTION )); then\n    for old in \"${backups[@]:RETENTION}\"; do\n        log \"removing old backup: $old\"\n        run rm -f \"$old\"\n    done\nfi\n\nlog \"done\"\n","",[30,31,32,41,47,61,68,101,121,126,132,143,154,165,175,180,189,201,213,218,224,230,236,247,253,259,264,270,279,287,292,298,303,344,372,377,383,407,423,451,479,530,556,577,603,626,637,643,649,654,686,703,720,725,731,760,802,807,813,824,832,846,888,901,906,918,923,929,937,949,962,968,983,994,1000,1005,1010,1016,1062,1081,1086,1097,1141,1147,1185,1191,1196,1202,1214,1223,1232,1260,1265,1296,1301,1309,1314,1319,1325,1338,1389,1416,1453,1466,1483,1489,1494,1499],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"sdCPZ","#!\u002Fusr\u002Fbin\u002Fenv bash\n",[33,42,44],{"class":35,"line":43},2,[33,45,46],{"class":39},"# ── Timestamped backup with retention, verification, and dry-run ──\n",[33,48,50,54,57],{"class":35,"line":49},3,[33,51,53],{"class":52},"snvgF","set",[33,55,56],{"class":52}," -Eeuo",[33,58,60],{"class":59},"sJ6F3"," pipefail\n",[33,62,64],{"class":35,"line":63},4,[33,65,67],{"emptyLinePlaceholder":66},true,"\n",[33,69,71,75,79,82,85,89,92,95,98],{"class":35,"line":70},5,[33,72,74],{"class":73},"svdQ7","readonly",[33,76,78],{"class":77},"ssxIu"," SCRIPT_NAME",[33,80,81],{"class":73},"=",[33,83,84],{"class":77},"$(",[33,86,88],{"class":87},"sIsaT","basename",[33,90,91],{"class":59}," \"${",[33,93,94],{"class":77},"BASH_SOURCE",[33,96,97],{"class":59},"[0]}\"",[33,99,100],{"class":77},")\n",[33,102,104,106,109,111,113,116,119],{"class":35,"line":103},6,[33,105,74],{"class":73},[33,107,108],{"class":77}," TS",[33,110,81],{"class":73},[33,112,84],{"class":77},[33,114,115],{"class":87},"date",[33,117,118],{"class":59}," +%Y%m%d_%H%M%S",[33,120,100],{"class":77},[33,122,124],{"class":35,"line":123},7,[33,125,67],{"emptyLinePlaceholder":66},[33,127,129],{"class":35,"line":128},8,[33,130,131],{"class":39},"# ── Config ──\n",[33,133,135,138,140],{"class":35,"line":134},9,[33,136,137],{"class":77},"RETENTION",[33,139,81],{"class":73},[33,141,142],{"class":59},"5\n",[33,144,146,149,151],{"class":35,"line":145},10,[33,147,148],{"class":77},"DRY_RUN",[33,150,81],{"class":73},[33,152,153],{"class":59},"false\n",[33,155,157,160,162],{"class":35,"line":156},11,[33,158,159],{"class":77},"COMPRESS",[33,161,81],{"class":73},[33,163,164],{"class":59},"true\n",[33,166,168,171,173],{"class":35,"line":167},12,[33,169,170],{"class":77},"VERBOSE",[33,172,81],{"class":73},[33,174,153],{"class":59},[33,176,178],{"class":35,"line":177},13,[33,179,67],{"emptyLinePlaceholder":66},[33,181,183,186],{"class":35,"line":182},14,[33,184,185],{"class":87},"usage",[33,187,188],{"class":77},"() {\n",[33,190,192,195,198],{"class":35,"line":191},15,[33,193,194],{"class":87},"    cat",[33,196,197],{"class":73}," \u003C\u003C",[33,199,200],{"class":59},"EOF\n",[33,202,204,207,210],{"class":35,"line":203},16,[33,205,206],{"class":59},"Usage: ",[33,208,209],{"class":77},"$SCRIPT_NAME",[33,211,212],{"class":59}," [OPTIONS] \u003Csource> \u003Cdestination>\n",[33,214,216],{"class":35,"line":215},17,[33,217,67],{"emptyLinePlaceholder":66},[33,219,221],{"class":35,"line":220},18,[33,222,223],{"class":59},"Options:\n",[33,225,227],{"class":35,"line":226},19,[33,228,229],{"class":59},"  -h, --help           Show this help\n",[33,231,233],{"class":35,"line":232},20,[33,234,235],{"class":59},"  -n, --dry-run        Show what would be done (no execution)\n",[33,237,239,242,245],{"class":35,"line":238},21,[33,240,241],{"class":59},"  -k, --keep N         Keep last N backups (default: ",[33,243,244],{"class":77},"$RETENTION",[33,246,100],{"class":59},[33,248,250],{"class":35,"line":249},22,[33,251,252],{"class":59},"  -z, --gzip           Compress backup (default: on)\n",[33,254,256],{"class":35,"line":255},23,[33,257,258],{"class":59},"  -v, --verbose        Verbose output\n",[33,260,262],{"class":35,"line":261},24,[33,263,67],{"emptyLinePlaceholder":66},[33,265,267],{"class":35,"line":266},25,[33,268,269],{"class":59},"Examples:\n",[33,271,273,276],{"class":35,"line":272},26,[33,274,275],{"class":77},"  $SCRIPT_NAME",[33,277,278],{"class":59}," \u002Fdata \u002Fbackups\n",[33,280,282,284],{"class":35,"line":281},27,[33,283,275],{"class":77},[33,285,286],{"class":59}," -n -k 10 \u002Fdata \u002Fbackups\n",[33,288,290],{"class":35,"line":289},28,[33,291,200],{"class":59},[33,293,295],{"class":35,"line":294},29,[33,296,297],{"class":77},"}\n",[33,299,301],{"class":35,"line":300},30,[33,302,67],{"emptyLinePlaceholder":66},[33,304,306,309,312,315,318,321,323,326,329,332,335,338,341],{"class":35,"line":305},31,[33,307,308],{"class":87},"log",[33,310,311],{"class":77},"() { ",[33,313,314],{"class":52},"printf",[33,316,317],{"class":59}," '[%s] %s\\n'",[33,319,320],{"class":59}," \"$(",[33,322,115],{"class":87},[33,324,325],{"class":52}," -Iseconds",[33,327,328],{"class":59},")\"",[33,330,331],{"class":59}," \"",[33,333,334],{"class":52},"$*",[33,336,337],{"class":59},"\"",[33,339,340],{"class":73}," >&2",[33,342,343],{"class":77},"; }\n",[33,345,347,350,352,354,357,359,361,364,367,370],{"class":35,"line":346},32,[33,348,349],{"class":87},"die",[33,351,311],{"class":77},[33,353,308],{"class":87},[33,355,356],{"class":59}," \"FATAL: ",[33,358,334],{"class":52},[33,360,337],{"class":59},[33,362,363],{"class":77},"; ",[33,365,366],{"class":52},"exit",[33,368,369],{"class":52}," 1",[33,371,343],{"class":77},[33,373,375],{"class":35,"line":374},33,[33,376,67],{"emptyLinePlaceholder":66},[33,378,380],{"class":35,"line":379},34,[33,381,382],{"class":39},"# ── Parse args ──\n",[33,384,386,389,392,395,398,401,404],{"class":35,"line":385},35,[33,387,388],{"class":73},"while",[33,390,391],{"class":77}," [[ ",[33,393,394],{"class":52},"$#",[33,396,397],{"class":73}," -gt",[33,399,400],{"class":52}," 0",[33,402,403],{"class":77}," ]]; ",[33,405,406],{"class":73},"do\n",[33,408,410,413,415,418,420],{"class":35,"line":409},36,[33,411,412],{"class":73},"    case",[33,414,331],{"class":59},[33,416,417],{"class":52},"$1",[33,419,337],{"class":59},[33,421,422],{"class":73}," in\n",[33,424,426,430,433,436,439,442,444,446,448],{"class":35,"line":425},37,[33,427,429],{"class":428},"svAP2","        -h",[33,431,432],{"class":73},"|",[33,434,435],{"class":428},"--help",[33,437,438],{"class":73},")",[33,440,441],{"class":87},"    usage",[33,443,363],{"class":77},[33,445,366],{"class":52},[33,447,400],{"class":52},[33,449,450],{"class":77}," ;;\n",[33,452,454,457,459,462,464,467,469,472,474,477],{"class":35,"line":453},38,[33,455,456],{"class":428},"        -n",[33,458,432],{"class":73},[33,460,461],{"class":428},"--dry-run",[33,463,438],{"class":73},[33,465,466],{"class":77}," DRY_RUN",[33,468,81],{"class":73},[33,470,471],{"class":59},"true",[33,473,363],{"class":77},[33,475,476],{"class":52},"shift",[33,478,450],{"class":77},[33,480,482,485,487,490,492,495,497,499,502,505,507,510,513,516,519,521,523,525,528],{"class":35,"line":481},39,[33,483,484],{"class":428},"        -k",[33,486,432],{"class":73},[33,488,489],{"class":428},"--keep",[33,491,438],{"class":73},[33,493,494],{"class":77},"    RETENTION",[33,496,81],{"class":73},[33,498,337],{"class":59},[33,500,501],{"class":52},"${2",[33,503,504],{"class":73},":?",[33,506,489],{"class":77},[33,508,509],{"class":77}," requires",[33,511,512],{"class":77}," a",[33,514,515],{"class":77}," number",[33,517,518],{"class":52},"}",[33,520,337],{"class":59},[33,522,363],{"class":77},[33,524,476],{"class":52},[33,526,527],{"class":52}," 2",[33,529,450],{"class":77},[33,531,533,536,538,541,543,546,548,550,552,554],{"class":35,"line":532},40,[33,534,535],{"class":428},"        -z",[33,537,432],{"class":73},[33,539,540],{"class":428},"--gzip",[33,542,438],{"class":73},[33,544,545],{"class":77},"    COMPRESS",[33,547,81],{"class":73},[33,549,471],{"class":59},[33,551,363],{"class":77},[33,553,476],{"class":52},[33,555,450],{"class":77},[33,557,559,562,564,566,568,571,573,575],{"class":35,"line":558},41,[33,560,561],{"class":428},"        --no-gzip",[33,563,438],{"class":73},[33,565,545],{"class":77},[33,567,81],{"class":73},[33,569,570],{"class":59},"false",[33,572,363],{"class":77},[33,574,476],{"class":52},[33,576,450],{"class":77},[33,578,580,583,585,588,590,593,595,597,599,601],{"class":35,"line":579},42,[33,581,582],{"class":428},"        -v",[33,584,432],{"class":73},[33,586,587],{"class":428},"--verbose",[33,589,438],{"class":73},[33,591,592],{"class":77}," VERBOSE",[33,594,81],{"class":73},[33,596,471],{"class":59},[33,598,363],{"class":77},[33,600,476],{"class":52},[33,602,450],{"class":77},[33,604,606,609,612,614,617,620,622,624],{"class":35,"line":605},43,[33,607,608],{"class":428},"        -",[33,610,611],{"class":73},"*",[33,613,438],{"class":73},[33,615,616],{"class":87},"           die",[33,618,619],{"class":59}," \"unknown option: ",[33,621,417],{"class":52},[33,623,337],{"class":59},[33,625,450],{"class":77},[33,627,629,632,635],{"class":35,"line":628},44,[33,630,631],{"class":73},"        *)",[33,633,634],{"class":73},"            break",[33,636,450],{"class":77},[33,638,640],{"class":35,"line":639},45,[33,641,642],{"class":73},"    esac\n",[33,644,646],{"class":35,"line":645},46,[33,647,648],{"class":73},"done\n",[33,650,652],{"class":35,"line":651},47,[33,653,67],{"emptyLinePlaceholder":66},[33,655,657,660,662,665,667,670,673,676,678,680,682,684],{"class":35,"line":656},48,[33,658,659],{"class":77},"[[ ",[33,661,394],{"class":52},[33,663,664],{"class":73}," -ge",[33,666,527],{"class":52},[33,668,669],{"class":77}," ]] ",[33,671,672],{"class":73},"||",[33,674,675],{"class":77}," { ",[33,677,185],{"class":87},[33,679,363],{"class":77},[33,681,366],{"class":52},[33,683,369],{"class":52},[33,685,343],{"class":77},[33,687,689,691,694,696,698,700],{"class":35,"line":688},49,[33,690,74],{"class":73},[33,692,693],{"class":77}," SRC",[33,695,81],{"class":73},[33,697,337],{"class":59},[33,699,417],{"class":52},[33,701,702],{"class":59},"\"\n",[33,704,706,708,711,713,715,718],{"class":35,"line":705},50,[33,707,74],{"class":73},[33,709,710],{"class":77}," DST",[33,712,81],{"class":73},[33,714,337],{"class":59},[33,716,717],{"class":52},"$2",[33,719,702],{"class":59},[33,721,723],{"class":35,"line":722},51,[33,724,67],{"emptyLinePlaceholder":66},[33,726,728],{"class":35,"line":727},52,[33,729,730],{"class":39},"# ── Validate ──\n",[33,732,734,736,739,741,744,746,748,750,753,756,758],{"class":35,"line":733},53,[33,735,659],{"class":77},[33,737,738],{"class":73},"-d",[33,740,331],{"class":59},[33,742,743],{"class":77},"$SRC",[33,745,337],{"class":59},[33,747,669],{"class":77},[33,749,672],{"class":73},[33,751,752],{"class":87}," die",[33,754,755],{"class":59}," \"source not a directory: ",[33,757,743],{"class":77},[33,759,702],{"class":59},[33,761,763,765,767,769,772,774,776,778,781,784,786,788,790,793,795,798,800],{"class":35,"line":762},54,[33,764,659],{"class":77},[33,766,738],{"class":73},[33,768,331],{"class":59},[33,770,771],{"class":77},"$DST",[33,773,337],{"class":59},[33,775,669],{"class":77},[33,777,672],{"class":73},[33,779,780],{"class":87}," mkdir",[33,782,783],{"class":52}," -p",[33,785,331],{"class":59},[33,787,771],{"class":77},[33,789,337],{"class":59},[33,791,792],{"class":73}," ||",[33,794,752],{"class":87},[33,796,797],{"class":59}," \"can't create destination: ",[33,799,771],{"class":77},[33,801,702],{"class":59},[33,803,805],{"class":35,"line":804},55,[33,806,67],{"emptyLinePlaceholder":66},[33,808,810],{"class":35,"line":809},56,[33,811,812],{"class":39},"# ── Cleanup trap ──\n",[33,814,816,819,821],{"class":35,"line":815},57,[33,817,818],{"class":77},"tmpfile",[33,820,81],{"class":73},[33,822,823],{"class":59},"\"\"\n",[33,825,827,830],{"class":35,"line":826},58,[33,828,829],{"class":87},"cleanup",[33,831,188],{"class":77},[33,833,835,838,841,843],{"class":35,"line":834},59,[33,836,837],{"class":73},"    local",[33,839,840],{"class":77}," code",[33,842,81],{"class":73},[33,844,845],{"class":52},"$?\n",[33,847,849,852,855,857,860,862,865,868,870,872,874,877,880,882,884,886],{"class":35,"line":848},60,[33,850,851],{"class":77},"    [[ ",[33,853,854],{"class":73},"-n",[33,856,331],{"class":59},[33,858,859],{"class":77},"$tmpfile",[33,861,337],{"class":59},[33,863,864],{"class":73}," &&",[33,866,867],{"class":73}," -f",[33,869,331],{"class":59},[33,871,859],{"class":77},[33,873,337],{"class":59},[33,875,876],{"class":77}," ]] && ",[33,878,879],{"class":87},"rm",[33,881,867],{"class":52},[33,883,331],{"class":59},[33,885,859],{"class":77},[33,887,702],{"class":59},[33,889,891,894,896,899],{"class":35,"line":890},61,[33,892,893],{"class":52},"    exit",[33,895,331],{"class":59},[33,897,898],{"class":77},"$code",[33,900,702],{"class":59},[33,902,904],{"class":35,"line":903},62,[33,905,297],{"class":77},[33,907,909,912,915],{"class":35,"line":908},63,[33,910,911],{"class":52},"trap",[33,913,914],{"class":59}," cleanup",[33,916,917],{"class":59}," EXIT\n",[33,919,921],{"class":35,"line":920},64,[33,922,67],{"emptyLinePlaceholder":66},[33,924,926],{"class":35,"line":925},65,[33,927,928],{"class":39},"# ── Run wrapper (dry-run aware) ──\n",[33,930,932,935],{"class":35,"line":931},66,[33,933,934],{"class":87},"run",[33,936,188],{"class":77},[33,938,940,943,946],{"class":35,"line":939},67,[33,941,942],{"class":73},"    if",[33,944,945],{"class":77}," $DRY_RUN; ",[33,947,948],{"class":73},"then\n",[33,950,952,955,958,960],{"class":35,"line":951},68,[33,953,954],{"class":87},"        log",[33,956,957],{"class":59}," \"DRY RUN: ",[33,959,334],{"class":52},[33,961,702],{"class":59},[33,963,965],{"class":35,"line":964},69,[33,966,967],{"class":73},"    else\n",[33,969,971,974,976,979,981],{"class":35,"line":970},70,[33,972,973],{"class":77},"        $VERBOSE && ",[33,975,308],{"class":87},[33,977,978],{"class":59}," \"executing: ",[33,980,334],{"class":52},[33,982,702],{"class":59},[33,984,986,989,992],{"class":35,"line":985},71,[33,987,988],{"class":87},"        \"",[33,990,991],{"class":52},"$@",[33,993,702],{"class":87},[33,995,997],{"class":35,"line":996},72,[33,998,999],{"class":73},"    fi\n",[33,1001,1003],{"class":35,"line":1002},73,[33,1004,297],{"class":77},[33,1006,1008],{"class":35,"line":1007},74,[33,1009,67],{"emptyLinePlaceholder":66},[33,1011,1013],{"class":35,"line":1012},75,[33,1014,1015],{"class":39},"# ── Create backup ──\n",[33,1017,1019,1022,1024,1026,1028,1031,1034,1037,1040,1043,1046,1048,1051,1054,1056,1059],{"class":35,"line":1018},76,[33,1020,1021],{"class":77},"backup_file",[33,1023,81],{"class":73},[33,1025,337],{"class":59},[33,1027,771],{"class":77},[33,1029,1030],{"class":59},"\u002Fbackup_${",[33,1032,1033],{"class":77},"TS",[33,1035,1036],{"class":59},"}.tar.$([[ ",[33,1038,1039],{"class":77},"$COMPRESS",[33,1041,1042],{"class":73}," ==",[33,1044,1045],{"class":52}," true",[33,1047,876],{"class":59},[33,1049,1050],{"class":52},"echo",[33,1052,1053],{"class":59}," gz ",[33,1055,672],{"class":73},[33,1057,1058],{"class":52}," echo",[33,1060,1061],{"class":59}," tar)\"\n",[33,1063,1065,1067,1070,1072,1075,1078],{"class":35,"line":1064},77,[33,1066,308],{"class":87},[33,1068,1069],{"class":59}," \"backing up '",[33,1071,743],{"class":77},[33,1073,1074],{"class":59},"' to '",[33,1076,1077],{"class":77},"$backup_file",[33,1079,1080],{"class":59},"'\"\n",[33,1082,1084],{"class":35,"line":1083},78,[33,1085,67],{"emptyLinePlaceholder":66},[33,1087,1089,1092,1095],{"class":35,"line":1088},79,[33,1090,1091],{"class":73},"if",[33,1093,1094],{"class":77}," $COMPRESS; ",[33,1096,948],{"class":73},[33,1098,1100,1103,1106,1109,1111,1113,1115,1118,1120,1123,1125,1127,1130,1132,1134,1136,1138],{"class":35,"line":1099},80,[33,1101,1102],{"class":87},"    run",[33,1104,1105],{"class":59}," tar",[33,1107,1108],{"class":52}," -czf",[33,1110,331],{"class":59},[33,1112,1077],{"class":77},[33,1114,337],{"class":59},[33,1116,1117],{"class":52}," -C",[33,1119,320],{"class":59},[33,1121,1122],{"class":87},"dirname",[33,1124,331],{"class":59},[33,1126,743],{"class":77},[33,1128,1129],{"class":59},"\")\"",[33,1131,320],{"class":59},[33,1133,88],{"class":87},[33,1135,331],{"class":59},[33,1137,743],{"class":77},[33,1139,1140],{"class":59},"\")\"\n",[33,1142,1144],{"class":35,"line":1143},81,[33,1145,1146],{"class":73},"else\n",[33,1148,1150,1152,1154,1157,1159,1161,1163,1165,1167,1169,1171,1173,1175,1177,1179,1181,1183],{"class":35,"line":1149},82,[33,1151,1102],{"class":87},[33,1153,1105],{"class":59},[33,1155,1156],{"class":52}," -cf",[33,1158,331],{"class":59},[33,1160,1077],{"class":77},[33,1162,337],{"class":59},[33,1164,1117],{"class":52},[33,1166,320],{"class":59},[33,1168,1122],{"class":87},[33,1170,331],{"class":59},[33,1172,743],{"class":77},[33,1174,1129],{"class":59},[33,1176,320],{"class":59},[33,1178,88],{"class":87},[33,1180,331],{"class":59},[33,1182,743],{"class":77},[33,1184,1140],{"class":59},[33,1186,1188],{"class":35,"line":1187},83,[33,1189,1190],{"class":73},"fi\n",[33,1192,1194],{"class":35,"line":1193},84,[33,1195,67],{"emptyLinePlaceholder":66},[33,1197,1199],{"class":35,"line":1198},85,[33,1200,1201],{"class":39},"# ── Verify backup (only if not dry-run) ──\n",[33,1203,1205,1207,1210,1212],{"class":35,"line":1204},86,[33,1206,1091],{"class":73},[33,1208,1209],{"class":73}," !",[33,1211,945],{"class":77},[33,1213,948],{"class":73},[33,1215,1217,1220],{"class":35,"line":1216},87,[33,1218,1219],{"class":87},"    log",[33,1221,1222],{"class":59}," \"verifying backup...\"\n",[33,1224,1226,1228,1230],{"class":35,"line":1225},88,[33,1227,942],{"class":73},[33,1229,1094],{"class":77},[33,1231,948],{"class":73},[33,1233,1235,1238,1241,1243,1245,1247,1250,1253,1255,1257],{"class":35,"line":1234},89,[33,1236,1237],{"class":87},"        gzip",[33,1239,1240],{"class":52}," -t",[33,1242,331],{"class":59},[33,1244,1077],{"class":77},[33,1246,337],{"class":59},[33,1248,1249],{"class":73}," 2>",[33,1251,1252],{"class":59},"\u002Fdev\u002Fnull",[33,1254,792],{"class":73},[33,1256,752],{"class":87},[33,1258,1259],{"class":59}," \"backup verification failed (corrupt gzip)\"\n",[33,1261,1263],{"class":35,"line":1262},90,[33,1264,967],{"class":73},[33,1266,1268,1271,1274,1276,1278,1280,1283,1286,1289,1291,1293],{"class":35,"line":1267},91,[33,1269,1270],{"class":87},"        tar",[33,1272,1273],{"class":52}," -tf",[33,1275,331],{"class":59},[33,1277,1077],{"class":77},[33,1279,337],{"class":59},[33,1281,1282],{"class":77}," &",[33,1284,1285],{"class":73},">",[33,1287,1288],{"class":77},"\u002Fdev\u002Fnull ",[33,1290,672],{"class":73},[33,1292,752],{"class":87},[33,1294,1295],{"class":59}," \"backup verification failed (corrupt tar)\"\n",[33,1297,1299],{"class":35,"line":1298},92,[33,1300,999],{"class":73},[33,1302,1304,1306],{"class":35,"line":1303},93,[33,1305,1219],{"class":87},[33,1307,1308],{"class":59}," \"verification passed\"\n",[33,1310,1312],{"class":35,"line":1311},94,[33,1313,1190],{"class":73},[33,1315,1317],{"class":35,"line":1316},95,[33,1318,67],{"emptyLinePlaceholder":66},[33,1320,1322],{"class":35,"line":1321},96,[33,1323,1324],{"class":39},"# ── Cleanup old backups ──\n",[33,1326,1328,1330,1333,1335],{"class":35,"line":1327},97,[33,1329,308],{"class":87},[33,1331,1332],{"class":59}," \"retention: keeping last ",[33,1334,244],{"class":77},[33,1336,1337],{"class":59}," backups\"\n",[33,1339,1341,1344,1346,1349,1352,1355,1358,1361,1363,1365,1368,1370,1373,1375,1377,1379,1381,1384,1387],{"class":35,"line":1340},98,[33,1342,1343],{"class":87},"mapfile",[33,1345,1240],{"class":52},[33,1347,1348],{"class":59}," backups",[33,1350,1351],{"class":73}," \u003C",[33,1353,1354],{"class":59}," \u003C(",[33,1356,1357],{"class":87},"ls",[33,1359,1360],{"class":52}," -1",[33,1362,331],{"class":59},[33,1364,771],{"class":77},[33,1366,1367],{"class":59},"\"\u002Fbackup_",[33,1369,611],{"class":52},[33,1371,1372],{"class":59},".tar.",[33,1374,611],{"class":52},[33,1376,1249],{"class":73},[33,1378,1288],{"class":59},[33,1380,432],{"class":73},[33,1382,1383],{"class":87}," sort",[33,1385,1386],{"class":52}," -r",[33,1388,100],{"class":59},[33,1390,1392,1394,1397,1400,1403,1406,1409,1411,1414],{"class":35,"line":1391},99,[33,1393,1091],{"class":73},[33,1395,1396],{"class":77}," (( ${",[33,1398,1399],{"class":73},"#",[33,1401,1402],{"class":77},"backups[",[33,1404,1405],{"class":73},"@",[33,1407,1408],{"class":77},"]} ",[33,1410,1285],{"class":73},[33,1412,1413],{"class":77}," RETENTION )); ",[33,1415,948],{"class":73},[33,1417,1419,1422,1425,1428,1430,1433,1436,1438,1441,1444,1446,1449,1451],{"class":35,"line":1418},100,[33,1420,1421],{"class":73},"    for",[33,1423,1424],{"class":77}," old ",[33,1426,1427],{"class":73},"in",[33,1429,91],{"class":59},[33,1431,1432],{"class":77},"backups",[33,1434,1435],{"class":59},"[",[33,1437,1405],{"class":73},[33,1439,1440],{"class":59},"]",[33,1442,1443],{"class":73},":",[33,1445,137],{"class":77},[33,1447,1448],{"class":59},"}\"",[33,1450,363],{"class":77},[33,1452,406],{"class":73},[33,1454,1456,1458,1461,1464],{"class":35,"line":1455},101,[33,1457,954],{"class":87},[33,1459,1460],{"class":59}," \"removing old backup: ",[33,1462,1463],{"class":77},"$old",[33,1465,702],{"class":59},[33,1467,1469,1472,1475,1477,1479,1481],{"class":35,"line":1468},102,[33,1470,1471],{"class":87},"        run",[33,1473,1474],{"class":59}," rm",[33,1476,867],{"class":52},[33,1478,331],{"class":59},[33,1480,1463],{"class":77},[33,1482,702],{"class":59},[33,1484,1486],{"class":35,"line":1485},103,[33,1487,1488],{"class":73},"    done\n",[33,1490,1492],{"class":35,"line":1491},104,[33,1493,1190],{"class":73},[33,1495,1497],{"class":35,"line":1496},105,[33,1498,67],{"emptyLinePlaceholder":66},[33,1500,1502,1504],{"class":35,"line":1501},106,[33,1503,308],{"class":87},[33,1505,1506],{"class":59}," \"done\"\n",[15,1508,1510],{"id":1509},"project-2-log-analyzer-with-summary-report","Project 2 — Log Analyzer with Summary Report",[20,1512,1513],{"language":22},[24,1514,1516],{"className":26,"code":1515,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# ── Analyze an access log: top IPs, URLs, status codes, busiest hour ──\nset -euo pipefail\n\nreadonly LOG=\"${1:-access.log}\"\n\n[[ -f \"$LOG\" ]] || { echo \"log not found: $LOG\" >&2; exit 1; }\n\n# ── Summary report ──\nreport() {\n    local section=$1; shift\n    printf '\\n=== %s ===\\n' \"$section\"\n    \"$@\"\n}\n\n# ── Top 10 client IPs ──\nreport \"Top 10 IPs\" bash -c \"awk '{print \\$1}' \\\"\\$1\\\" | sort | uniq -c | sort -rn | head -10\" _ \"$LOG\"\n\n# ── Top 10 URLs ──\nreport \"Top 10 URLs\" bash -c \"awk '{print \\$7}' \\\"\\$1\\\" | sort | uniq -c | sort -rn | head -10\" _ \"$LOG\"\n\n# ── Status code distribution ──\nreport \"Status Codes\" bash -c \"awk '{print \\$9}' \\\"\\$1\\\" | sort | uniq -c | sort -rn\" _ \"$LOG\"\n\n# ── Busiest hour ──\nreport \"Busiest Hour\" bash -c \"awk '{print \\$4}' \\\"\\$1\\\" | cut -d: -f2 | sort | uniq -c | sort -rn | head -1\" _ \"$LOG\"\n\n# ── Total requests ──\nprintf '\\n=== Total Requests ===\\n'\nwc -l \u003C \"$LOG\"\n\n# ── Error rate (4xx + 5xx) ──\nprintf '\\n=== Error Rate ===\\n'\ntotal=$(wc -l \u003C \"$LOG\")\nerrors=$(awk '$9 >= 400' \"$LOG\" | wc -l)\nif ((total > 0)); then\n    awk -v e=\"$errors\" -v t=\"$total\" 'BEGIN { printf \"%.2f%%\\n\", (e\u002Ft)*100 }'\nfi\n\n# ── Top error URLs ──\nreport \"Top Error URLs (4xx\u002F5xx)\" bash -c \"awk '\\$9 >= 400 {print \\$7}' \\\"\\$1\\\" | sort | uniq -c | sort -rn | head -10\" _ \"$LOG\"\n",[30,1517,1518,1522,1527,1536,1540,1569,1573,1612,1616,1621,1628,1645,1660,1669,1673,1677,1682,1725,1729,1734,1768,1772,1777,1812,1816,1821,1856,1860,1865,1872,1888,1892,1897,1904,1927,1958,1974,2003,2007,2011,2016],{"__ignoreMap":28},[33,1519,1520],{"class":35,"line":36},[33,1521,40],{"class":39},[33,1523,1524],{"class":35,"line":43},[33,1525,1526],{"class":39},"# ── Analyze an access log: top IPs, URLs, status codes, busiest hour ──\n",[33,1528,1529,1531,1534],{"class":35,"line":49},[33,1530,53],{"class":52},[33,1532,1533],{"class":52}," -euo",[33,1535,60],{"class":59},[33,1537,1538],{"class":35,"line":63},[33,1539,67],{"emptyLinePlaceholder":66},[33,1541,1542,1544,1547,1549,1551,1554,1557,1560,1563,1565,1567],{"class":35,"line":70},[33,1543,74],{"class":73},[33,1545,1546],{"class":77}," LOG",[33,1548,81],{"class":73},[33,1550,337],{"class":59},[33,1552,1553],{"class":52},"${1",[33,1555,1556],{"class":73},":-",[33,1558,1559],{"class":77},"access",[33,1561,1562],{"class":59},".",[33,1564,308],{"class":77},[33,1566,518],{"class":52},[33,1568,702],{"class":59},[33,1570,1571],{"class":35,"line":103},[33,1572,67],{"emptyLinePlaceholder":66},[33,1574,1575,1577,1580,1582,1585,1587,1589,1591,1593,1595,1598,1600,1602,1604,1606,1608,1610],{"class":35,"line":123},[33,1576,659],{"class":77},[33,1578,1579],{"class":73},"-f",[33,1581,331],{"class":59},[33,1583,1584],{"class":77},"$LOG",[33,1586,337],{"class":59},[33,1588,669],{"class":77},[33,1590,672],{"class":73},[33,1592,675],{"class":77},[33,1594,1050],{"class":52},[33,1596,1597],{"class":59}," \"log not found: ",[33,1599,1584],{"class":77},[33,1601,337],{"class":59},[33,1603,340],{"class":73},[33,1605,363],{"class":77},[33,1607,366],{"class":52},[33,1609,369],{"class":52},[33,1611,343],{"class":77},[33,1613,1614],{"class":35,"line":128},[33,1615,67],{"emptyLinePlaceholder":66},[33,1617,1618],{"class":35,"line":134},[33,1619,1620],{"class":39},"# ── Summary report ──\n",[33,1622,1623,1626],{"class":35,"line":145},[33,1624,1625],{"class":87},"report",[33,1627,188],{"class":77},[33,1629,1630,1632,1635,1637,1640,1642],{"class":35,"line":156},[33,1631,837],{"class":73},[33,1633,1634],{"class":77}," section",[33,1636,81],{"class":73},[33,1638,417],{"class":1639},"sCrzJ",[33,1641,363],{"class":77},[33,1643,1644],{"class":52},"shift\n",[33,1646,1647,1650,1653,1655,1658],{"class":35,"line":167},[33,1648,1649],{"class":52},"    printf",[33,1651,1652],{"class":59}," '\\n=== %s ===\\n'",[33,1654,331],{"class":59},[33,1656,1657],{"class":77},"$section",[33,1659,702],{"class":59},[33,1661,1662,1665,1667],{"class":35,"line":177},[33,1663,1664],{"class":87},"    \"",[33,1666,991],{"class":52},[33,1668,702],{"class":87},[33,1670,1671],{"class":35,"line":182},[33,1672,297],{"class":77},[33,1674,1675],{"class":35,"line":191},[33,1676,67],{"emptyLinePlaceholder":66},[33,1678,1679],{"class":35,"line":203},[33,1680,1681],{"class":39},"# ── Top 10 client IPs ──\n",[33,1683,1684,1686,1689,1692,1695,1698,1701,1704,1707,1710,1713,1716,1719,1721,1723],{"class":35,"line":215},[33,1685,1625],{"class":87},[33,1687,1688],{"class":59}," \"Top 10 IPs\"",[33,1690,1691],{"class":59}," bash",[33,1693,1694],{"class":52}," -c",[33,1696,1697],{"class":59}," \"awk '{print ",[33,1699,1700],{"class":52},"\\$",[33,1702,1703],{"class":59},"1}' ",[33,1705,1706],{"class":52},"\\\"\\$",[33,1708,1709],{"class":59},"1",[33,1711,1712],{"class":52},"\\\"",[33,1714,1715],{"class":59}," | sort | uniq -c | sort -rn | head -10\"",[33,1717,1718],{"class":59}," _",[33,1720,331],{"class":59},[33,1722,1584],{"class":77},[33,1724,702],{"class":59},[33,1726,1727],{"class":35,"line":220},[33,1728,67],{"emptyLinePlaceholder":66},[33,1730,1731],{"class":35,"line":226},[33,1732,1733],{"class":39},"# ── Top 10 URLs ──\n",[33,1735,1736,1738,1741,1743,1745,1747,1749,1752,1754,1756,1758,1760,1762,1764,1766],{"class":35,"line":232},[33,1737,1625],{"class":87},[33,1739,1740],{"class":59}," \"Top 10 URLs\"",[33,1742,1691],{"class":59},[33,1744,1694],{"class":52},[33,1746,1697],{"class":59},[33,1748,1700],{"class":52},[33,1750,1751],{"class":59},"7}' ",[33,1753,1706],{"class":52},[33,1755,1709],{"class":59},[33,1757,1712],{"class":52},[33,1759,1715],{"class":59},[33,1761,1718],{"class":59},[33,1763,331],{"class":59},[33,1765,1584],{"class":77},[33,1767,702],{"class":59},[33,1769,1770],{"class":35,"line":238},[33,1771,67],{"emptyLinePlaceholder":66},[33,1773,1774],{"class":35,"line":249},[33,1775,1776],{"class":39},"# ── Status code distribution ──\n",[33,1778,1779,1781,1784,1786,1788,1790,1792,1795,1797,1799,1801,1804,1806,1808,1810],{"class":35,"line":255},[33,1780,1625],{"class":87},[33,1782,1783],{"class":59}," \"Status Codes\"",[33,1785,1691],{"class":59},[33,1787,1694],{"class":52},[33,1789,1697],{"class":59},[33,1791,1700],{"class":52},[33,1793,1794],{"class":59},"9}' ",[33,1796,1706],{"class":52},[33,1798,1709],{"class":59},[33,1800,1712],{"class":52},[33,1802,1803],{"class":59}," | sort | uniq -c | sort -rn\"",[33,1805,1718],{"class":59},[33,1807,331],{"class":59},[33,1809,1584],{"class":77},[33,1811,702],{"class":59},[33,1813,1814],{"class":35,"line":261},[33,1815,67],{"emptyLinePlaceholder":66},[33,1817,1818],{"class":35,"line":266},[33,1819,1820],{"class":39},"# ── Busiest hour ──\n",[33,1822,1823,1825,1828,1830,1832,1834,1836,1839,1841,1843,1845,1848,1850,1852,1854],{"class":35,"line":272},[33,1824,1625],{"class":87},[33,1826,1827],{"class":59}," \"Busiest Hour\"",[33,1829,1691],{"class":59},[33,1831,1694],{"class":52},[33,1833,1697],{"class":59},[33,1835,1700],{"class":52},[33,1837,1838],{"class":59},"4}' ",[33,1840,1706],{"class":52},[33,1842,1709],{"class":59},[33,1844,1712],{"class":52},[33,1846,1847],{"class":59}," | cut -d: -f2 | sort | uniq -c | sort -rn | head -1\"",[33,1849,1718],{"class":59},[33,1851,331],{"class":59},[33,1853,1584],{"class":77},[33,1855,702],{"class":59},[33,1857,1858],{"class":35,"line":281},[33,1859,67],{"emptyLinePlaceholder":66},[33,1861,1862],{"class":35,"line":289},[33,1863,1864],{"class":39},"# ── Total requests ──\n",[33,1866,1867,1869],{"class":35,"line":294},[33,1868,314],{"class":52},[33,1870,1871],{"class":59}," '\\n=== Total Requests ===\\n'\n",[33,1873,1874,1877,1880,1882,1884,1886],{"class":35,"line":300},[33,1875,1876],{"class":87},"wc",[33,1878,1879],{"class":52}," -l",[33,1881,1351],{"class":73},[33,1883,331],{"class":59},[33,1885,1584],{"class":77},[33,1887,702],{"class":59},[33,1889,1890],{"class":35,"line":305},[33,1891,67],{"emptyLinePlaceholder":66},[33,1893,1894],{"class":35,"line":346},[33,1895,1896],{"class":39},"# ── Error rate (4xx + 5xx) ──\n",[33,1898,1899,1901],{"class":35,"line":374},[33,1900,314],{"class":52},[33,1902,1903],{"class":59}," '\\n=== Error Rate ===\\n'\n",[33,1905,1906,1909,1911,1913,1915,1917,1919,1921,1923,1925],{"class":35,"line":379},[33,1907,1908],{"class":77},"total",[33,1910,81],{"class":73},[33,1912,84],{"class":77},[33,1914,1876],{"class":87},[33,1916,1879],{"class":52},[33,1918,1351],{"class":73},[33,1920,331],{"class":59},[33,1922,1584],{"class":77},[33,1924,337],{"class":59},[33,1926,100],{"class":77},[33,1928,1929,1932,1934,1936,1939,1942,1944,1946,1948,1951,1954,1956],{"class":35,"line":385},[33,1930,1931],{"class":77},"errors",[33,1933,81],{"class":73},[33,1935,84],{"class":77},[33,1937,1938],{"class":87},"awk",[33,1940,1941],{"class":59}," '$9 >= 400'",[33,1943,331],{"class":59},[33,1945,1584],{"class":77},[33,1947,337],{"class":59},[33,1949,1950],{"class":73}," |",[33,1952,1953],{"class":87}," wc",[33,1955,1879],{"class":52},[33,1957,100],{"class":77},[33,1959,1960,1962,1965,1967,1969,1972],{"class":35,"line":409},[33,1961,1091],{"class":73},[33,1963,1964],{"class":77}," ((total ",[33,1966,1285],{"class":73},[33,1968,400],{"class":52},[33,1970,1971],{"class":77},")); ",[33,1973,948],{"class":73},[33,1975,1976,1979,1982,1985,1988,1990,1992,1995,1998,2000],{"class":35,"line":425},[33,1977,1978],{"class":87},"    awk",[33,1980,1981],{"class":52}," -v",[33,1983,1984],{"class":59}," e=\"",[33,1986,1987],{"class":77},"$errors",[33,1989,337],{"class":59},[33,1991,1981],{"class":52},[33,1993,1994],{"class":59}," t=\"",[33,1996,1997],{"class":77},"$total",[33,1999,337],{"class":59},[33,2001,2002],{"class":59}," 'BEGIN { printf \"%.2f%%\\n\", (e\u002Ft)*100 }'\n",[33,2004,2005],{"class":35,"line":453},[33,2006,1190],{"class":73},[33,2008,2009],{"class":35,"line":481},[33,2010,67],{"emptyLinePlaceholder":66},[33,2012,2013],{"class":35,"line":532},[33,2014,2015],{"class":39},"# ── Top error URLs ──\n",[33,2017,2018,2020,2023,2025,2027,2030,2032,2035,2037,2039,2041,2043,2045,2047,2049,2051,2053],{"class":35,"line":558},[33,2019,1625],{"class":87},[33,2021,2022],{"class":59}," \"Top Error URLs (4xx\u002F5xx)\"",[33,2024,1691],{"class":59},[33,2026,1694],{"class":52},[33,2028,2029],{"class":59}," \"awk '",[33,2031,1700],{"class":52},[33,2033,2034],{"class":59},"9 >= 400 {print ",[33,2036,1700],{"class":52},[33,2038,1751],{"class":59},[33,2040,1706],{"class":52},[33,2042,1709],{"class":59},[33,2044,1712],{"class":52},[33,2046,1715],{"class":59},[33,2048,1718],{"class":59},[33,2050,331],{"class":59},[33,2052,1584],{"class":77},[33,2054,702],{"class":59},[15,2056,2058],{"id":2057},"project-3-git-pre-commit-hook","Project 3 — Git Pre-Commit Hook",[20,2060,2061],{"language":22},[24,2062,2064],{"className":26,"code":2063,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# ── .git\u002Fhooks\u002Fpre-commit: run ShellCheck and tests before commit ──\nset -euo pipefail\n\nlog() { printf '[pre-commit] %s\\n' \"$*\" >&2; }\n\n# ── Get staged .sh files ──\nmapfile -t staged_sh \u003C \u003C(git diff --cached --name-only --diff-filter=ACM | grep '\\.sh$' || true)\n\nif (( ${#staged_sh[@]} > 0 )); then\n    log \"running ShellCheck on ${#staged_sh[@]} file(s)...\"\n    # Use xargs for batch processing\n    printf '%s\\n' \"${staged_sh[@]}\" | xargs shellcheck\n    log \"ShellCheck passed\"\nfi\n\n# ── Run bats tests if they exist ──\nif [[ -d test ]] && ls test\u002F*.bats &>\u002Fdev\u002Fnull; then\n    log \"running bats tests...\"\n    bats test\u002F\n    log \"tests passed\"\nfi\n\n# ── Run ShellCheck on staged .bats files too ──\nmapfile -t staged_bats \u003C \u003C(git diff --cached --name-only --diff-filter=ACM | grep '\\.bats$' || true)\nif (( ${#staged_bats[@]} > 0 )); then\n    log \"running ShellCheck on .bats files...\"\n    printf '%s\\n' \"${staged_bats[@]}\" | xargs shellcheck --shell bash\nfi\n\nlog \"all pre-commit checks passed\"\n",[30,2065,2066,2070,2075,2083,2087,2108,2112,2117,2159,2163,2187,2206,2211,2237,2244,2248,2252,2257,2287,2294,2302,2309,2313,2317,2322,2358,2381,2388,2418,2422,2426],{"__ignoreMap":28},[33,2067,2068],{"class":35,"line":36},[33,2069,40],{"class":39},[33,2071,2072],{"class":35,"line":43},[33,2073,2074],{"class":39},"# ── .git\u002Fhooks\u002Fpre-commit: run ShellCheck and tests before commit ──\n",[33,2076,2077,2079,2081],{"class":35,"line":49},[33,2078,53],{"class":52},[33,2080,1533],{"class":52},[33,2082,60],{"class":59},[33,2084,2085],{"class":35,"line":63},[33,2086,67],{"emptyLinePlaceholder":66},[33,2088,2089,2091,2093,2095,2098,2100,2102,2104,2106],{"class":35,"line":70},[33,2090,308],{"class":87},[33,2092,311],{"class":77},[33,2094,314],{"class":52},[33,2096,2097],{"class":59}," '[pre-commit] %s\\n'",[33,2099,331],{"class":59},[33,2101,334],{"class":52},[33,2103,337],{"class":59},[33,2105,340],{"class":73},[33,2107,343],{"class":77},[33,2109,2110],{"class":35,"line":103},[33,2111,67],{"emptyLinePlaceholder":66},[33,2113,2114],{"class":35,"line":123},[33,2115,2116],{"class":39},"# ── Get staged .sh files ──\n",[33,2118,2119,2121,2123,2126,2128,2130,2133,2136,2139,2142,2145,2147,2150,2153,2155,2157],{"class":35,"line":128},[33,2120,1343],{"class":87},[33,2122,1240],{"class":52},[33,2124,2125],{"class":59}," staged_sh",[33,2127,1351],{"class":73},[33,2129,1354],{"class":59},[33,2131,2132],{"class":87},"git",[33,2134,2135],{"class":59}," diff ",[33,2137,2138],{"class":52},"--cached",[33,2140,2141],{"class":52}," --name-only",[33,2143,2144],{"class":52}," --diff-filter=ACM",[33,2146,1950],{"class":73},[33,2148,2149],{"class":87}," grep",[33,2151,2152],{"class":59}," '\\.sh$' ",[33,2154,672],{"class":73},[33,2156,1045],{"class":52},[33,2158,100],{"class":59},[33,2160,2161],{"class":35,"line":134},[33,2162,67],{"emptyLinePlaceholder":66},[33,2164,2165,2167,2169,2171,2174,2176,2178,2180,2182,2185],{"class":35,"line":145},[33,2166,1091],{"class":73},[33,2168,1396],{"class":77},[33,2170,1399],{"class":73},[33,2172,2173],{"class":77},"staged_sh[",[33,2175,1405],{"class":73},[33,2177,1408],{"class":77},[33,2179,1285],{"class":73},[33,2181,400],{"class":52},[33,2183,2184],{"class":77}," )); ",[33,2186,948],{"class":73},[33,2188,2189,2191,2194,2196,2199,2201,2203],{"class":35,"line":156},[33,2190,1219],{"class":87},[33,2192,2193],{"class":59}," \"running ShellCheck on ${",[33,2195,1399],{"class":73},[33,2197,2198],{"class":77},"staged_sh",[33,2200,1435],{"class":59},[33,2202,1405],{"class":73},[33,2204,2205],{"class":59},"]} file(s)...\"\n",[33,2207,2208],{"class":35,"line":167},[33,2209,2210],{"class":39},"    # Use xargs for batch processing\n",[33,2212,2213,2215,2218,2220,2222,2224,2226,2229,2231,2234],{"class":35,"line":177},[33,2214,1649],{"class":52},[33,2216,2217],{"class":59}," '%s\\n'",[33,2219,91],{"class":59},[33,2221,2198],{"class":77},[33,2223,1435],{"class":59},[33,2225,1405],{"class":73},[33,2227,2228],{"class":59},"]}\"",[33,2230,1950],{"class":73},[33,2232,2233],{"class":87}," xargs",[33,2235,2236],{"class":59}," shellcheck\n",[33,2238,2239,2241],{"class":35,"line":182},[33,2240,1219],{"class":87},[33,2242,2243],{"class":59}," \"ShellCheck passed\"\n",[33,2245,2246],{"class":35,"line":191},[33,2247,1190],{"class":73},[33,2249,2250],{"class":35,"line":203},[33,2251,67],{"emptyLinePlaceholder":66},[33,2253,2254],{"class":35,"line":215},[33,2255,2256],{"class":39},"# ── Run bats tests if they exist ──\n",[33,2258,2259,2261,2263,2265,2268,2270,2273,2275,2278,2280,2282,2285],{"class":35,"line":220},[33,2260,1091],{"class":73},[33,2262,391],{"class":77},[33,2264,738],{"class":73},[33,2266,2267],{"class":77}," test ]] && ",[33,2269,1357],{"class":87},[33,2271,2272],{"class":59}," test\u002F",[33,2274,611],{"class":52},[33,2276,2277],{"class":59},".bats",[33,2279,1282],{"class":77},[33,2281,1285],{"class":73},[33,2283,2284],{"class":77},"\u002Fdev\u002Fnull; ",[33,2286,948],{"class":73},[33,2288,2289,2291],{"class":35,"line":226},[33,2290,1219],{"class":87},[33,2292,2293],{"class":59}," \"running bats tests...\"\n",[33,2295,2296,2299],{"class":35,"line":232},[33,2297,2298],{"class":87},"    bats",[33,2300,2301],{"class":59}," test\u002F\n",[33,2303,2304,2306],{"class":35,"line":238},[33,2305,1219],{"class":87},[33,2307,2308],{"class":59}," \"tests passed\"\n",[33,2310,2311],{"class":35,"line":249},[33,2312,1190],{"class":73},[33,2314,2315],{"class":35,"line":255},[33,2316,67],{"emptyLinePlaceholder":66},[33,2318,2319],{"class":35,"line":261},[33,2320,2321],{"class":39},"# ── Run ShellCheck on staged .bats files too ──\n",[33,2323,2324,2326,2328,2331,2333,2335,2337,2339,2341,2343,2345,2347,2349,2352,2354,2356],{"class":35,"line":266},[33,2325,1343],{"class":87},[33,2327,1240],{"class":52},[33,2329,2330],{"class":59}," staged_bats",[33,2332,1351],{"class":73},[33,2334,1354],{"class":59},[33,2336,2132],{"class":87},[33,2338,2135],{"class":59},[33,2340,2138],{"class":52},[33,2342,2141],{"class":52},[33,2344,2144],{"class":52},[33,2346,1950],{"class":73},[33,2348,2149],{"class":87},[33,2350,2351],{"class":59}," '\\.bats$' ",[33,2353,672],{"class":73},[33,2355,1045],{"class":52},[33,2357,100],{"class":59},[33,2359,2360,2362,2364,2366,2369,2371,2373,2375,2377,2379],{"class":35,"line":272},[33,2361,1091],{"class":73},[33,2363,1396],{"class":77},[33,2365,1399],{"class":73},[33,2367,2368],{"class":77},"staged_bats[",[33,2370,1405],{"class":73},[33,2372,1408],{"class":77},[33,2374,1285],{"class":73},[33,2376,400],{"class":52},[33,2378,2184],{"class":77},[33,2380,948],{"class":73},[33,2382,2383,2385],{"class":35,"line":281},[33,2384,1219],{"class":87},[33,2386,2387],{"class":59}," \"running ShellCheck on .bats files...\"\n",[33,2389,2390,2392,2394,2396,2399,2401,2403,2405,2407,2409,2412,2415],{"class":35,"line":289},[33,2391,1649],{"class":52},[33,2393,2217],{"class":59},[33,2395,91],{"class":59},[33,2397,2398],{"class":77},"staged_bats",[33,2400,1435],{"class":59},[33,2402,1405],{"class":73},[33,2404,2228],{"class":59},[33,2406,1950],{"class":73},[33,2408,2233],{"class":87},[33,2410,2411],{"class":59}," shellcheck",[33,2413,2414],{"class":52}," --shell",[33,2416,2417],{"class":59}," bash\n",[33,2419,2420],{"class":35,"line":294},[33,2421,1190],{"class":73},[33,2423,2424],{"class":35,"line":300},[33,2425,67],{"emptyLinePlaceholder":66},[33,2427,2428,2430],{"class":35,"line":305},[33,2429,308],{"class":87},[33,2431,2432],{"class":59}," \"all pre-commit checks passed\"\n",[20,2434,2435],{"language":22},[24,2436,2438],{"className":26,"code":2437,"language":22,"meta":28,"style":28},"# Install: chmod +x .git\u002Fhooks\u002Fpre-commit\n",[30,2439,2440],{"__ignoreMap":28},[33,2441,2442],{"class":35,"line":36},[33,2443,2437],{"class":39},[15,2445,2447],{"id":2446},"project-4-process-supervisor-with-backoff","Project 4 — Process Supervisor with Backoff",[20,2449,2450],{"language":22},[24,2451,2453],{"className":26,"code":2452,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# ── Monitor a process, restart on crash with exponential backoff ──\nset -uo pipefail  # no set -e (we handle errors manually for restart logic)\n\nlog() { printf '[%s] %s\\n' \"$(date -Iseconds)\" \"$*\" >&2; }\ndie() { log \"FATAL: $*\"; exit 1; }\n\n[[ $# -ge 1 ]] || die \"usage: $0 \u003Ccommand> [args...]\"\n\nchild_pid=0\nbackoff=1\nmax_backoff=60\nmax_restarts=0  # 0 = unlimited\nrestart_count=0\n\ncleanup() {\n    local code=$?\n    if ((child_pid > 0)) && kill -0 \"$child_pid\" 2>\u002Fdev\u002Fnull; then\n        log \"stopping child (PID $child_pid)...\"\n        kill -TERM \"$child_pid\" 2>\u002Fdev\u002Fnull\n        # Wait up to 10s for graceful shutdown\n        for i in {1..10}; do\n            kill -0 \"$child_pid\" 2>\u002Fdev\u002Fnull || break\n            sleep 1\n        done\n        kill -KILL \"$child_pid\" 2>\u002Fdev\u002Fnull  # force if still alive\n        wait \"$child_pid\" 2>\u002Fdev\u002Fnull\n    fi\n    exit \"$code\"\n}\ntrap cleanup INT TERM EXIT\n\nlog \"supervisor started: $*\"\n\nwhile true; do\n    log \"starting: $*\"\n    \"$@\" &\n    child_pid=$!\n\n    # Wait for the child to exit\n    if wait \"$child_pid\"; then\n        log \"child exited normally\"\n        break  # clean exit — stop supervisor\n    fi\n\n    exit_code=$?\n    log \"child crashed (exit $exit_code)\"\n\n    # Check restart limit\n    ((restart_count++))\n    if ((max_restarts > 0 && restart_count >= max_restarts)); then\n        die \"max restarts ($max_restarts) reached\"\n    fi\n\n    log \"restarting in ${backoff}s (attempt $restart_count)...\"\n    sleep \"$backoff\"\n\n    # Exponential backoff: 1, 2, 4, 8, 16, 32, 60, 60, ...\n    ((backoff *= 2))\n    ((backoff > max_backoff)) && backoff=$max_backoff\ndone\n\nlog \"supervisor stopped\"\n",[30,2454,2455,2459,2464,2477,2481,2509,2531,2535,2560,2564,2574,2584,2594,2607,2616,2620,2626,2636,2671,2683,2702,2707,2727,2749,2757,2762,2782,2797,2801,2811,2815,2829,2833,2844,2848,2858,2869,2880,2890,2894,2899,2916,2923,2931,2935,2939,2948,2961,2965,2970,2981,3005,3019,3023,3027,3044,3056,3060,3065,3077,3091,3095,3099],{"__ignoreMap":28},[33,2456,2457],{"class":35,"line":36},[33,2458,40],{"class":39},[33,2460,2461],{"class":35,"line":43},[33,2462,2463],{"class":39},"# ── Monitor a process, restart on crash with exponential backoff ──\n",[33,2465,2466,2468,2471,2474],{"class":35,"line":49},[33,2467,53],{"class":52},[33,2469,2470],{"class":52}," -uo",[33,2472,2473],{"class":59}," pipefail",[33,2475,2476],{"class":39},"  # no set -e (we handle errors manually for restart logic)\n",[33,2478,2479],{"class":35,"line":63},[33,2480,67],{"emptyLinePlaceholder":66},[33,2482,2483,2485,2487,2489,2491,2493,2495,2497,2499,2501,2503,2505,2507],{"class":35,"line":70},[33,2484,308],{"class":87},[33,2486,311],{"class":77},[33,2488,314],{"class":52},[33,2490,317],{"class":59},[33,2492,320],{"class":59},[33,2494,115],{"class":87},[33,2496,325],{"class":52},[33,2498,328],{"class":59},[33,2500,331],{"class":59},[33,2502,334],{"class":52},[33,2504,337],{"class":59},[33,2506,340],{"class":73},[33,2508,343],{"class":77},[33,2510,2511,2513,2515,2517,2519,2521,2523,2525,2527,2529],{"class":35,"line":103},[33,2512,349],{"class":87},[33,2514,311],{"class":77},[33,2516,308],{"class":87},[33,2518,356],{"class":59},[33,2520,334],{"class":52},[33,2522,337],{"class":59},[33,2524,363],{"class":77},[33,2526,366],{"class":52},[33,2528,369],{"class":52},[33,2530,343],{"class":77},[33,2532,2533],{"class":35,"line":123},[33,2534,67],{"emptyLinePlaceholder":66},[33,2536,2537,2539,2541,2543,2545,2547,2549,2551,2554,2557],{"class":35,"line":128},[33,2538,659],{"class":77},[33,2540,394],{"class":52},[33,2542,664],{"class":73},[33,2544,369],{"class":52},[33,2546,669],{"class":77},[33,2548,672],{"class":73},[33,2550,752],{"class":87},[33,2552,2553],{"class":59}," \"usage: ",[33,2555,2556],{"class":52},"$0",[33,2558,2559],{"class":59}," \u003Ccommand> [args...]\"\n",[33,2561,2562],{"class":35,"line":134},[33,2563,67],{"emptyLinePlaceholder":66},[33,2565,2566,2569,2571],{"class":35,"line":145},[33,2567,2568],{"class":77},"child_pid",[33,2570,81],{"class":73},[33,2572,2573],{"class":59},"0\n",[33,2575,2576,2579,2581],{"class":35,"line":156},[33,2577,2578],{"class":77},"backoff",[33,2580,81],{"class":73},[33,2582,2583],{"class":59},"1\n",[33,2585,2586,2589,2591],{"class":35,"line":167},[33,2587,2588],{"class":77},"max_backoff",[33,2590,81],{"class":73},[33,2592,2593],{"class":59},"60\n",[33,2595,2596,2599,2601,2604],{"class":35,"line":177},[33,2597,2598],{"class":77},"max_restarts",[33,2600,81],{"class":73},[33,2602,2603],{"class":59},"0",[33,2605,2606],{"class":39},"  # 0 = unlimited\n",[33,2608,2609,2612,2614],{"class":35,"line":182},[33,2610,2611],{"class":77},"restart_count",[33,2613,81],{"class":73},[33,2615,2573],{"class":59},[33,2617,2618],{"class":35,"line":191},[33,2619,67],{"emptyLinePlaceholder":66},[33,2621,2622,2624],{"class":35,"line":203},[33,2623,829],{"class":87},[33,2625,188],{"class":77},[33,2627,2628,2630,2632,2634],{"class":35,"line":215},[33,2629,837],{"class":73},[33,2631,840],{"class":77},[33,2633,81],{"class":73},[33,2635,845],{"class":52},[33,2637,2638,2640,2643,2645,2647,2650,2653,2656,2658,2661,2663,2665,2667,2669],{"class":35,"line":220},[33,2639,942],{"class":73},[33,2641,2642],{"class":77}," ((child_pid ",[33,2644,1285],{"class":73},[33,2646,400],{"class":52},[33,2648,2649],{"class":77},")) && ",[33,2651,2652],{"class":52},"kill",[33,2654,2655],{"class":52}," -0",[33,2657,331],{"class":59},[33,2659,2660],{"class":77},"$child_pid",[33,2662,337],{"class":59},[33,2664,1249],{"class":73},[33,2666,1252],{"class":59},[33,2668,363],{"class":77},[33,2670,948],{"class":73},[33,2672,2673,2675,2678,2680],{"class":35,"line":226},[33,2674,954],{"class":87},[33,2676,2677],{"class":59}," \"stopping child (PID ",[33,2679,2660],{"class":77},[33,2681,2682],{"class":59},")...\"\n",[33,2684,2685,2688,2691,2693,2695,2697,2699],{"class":35,"line":232},[33,2686,2687],{"class":52},"        kill",[33,2689,2690],{"class":52}," -TERM",[33,2692,331],{"class":59},[33,2694,2660],{"class":77},[33,2696,337],{"class":59},[33,2698,1249],{"class":73},[33,2700,2701],{"class":59},"\u002Fdev\u002Fnull\n",[33,2703,2704],{"class":35,"line":238},[33,2705,2706],{"class":39},"        # Wait up to 10s for graceful shutdown\n",[33,2708,2709,2712,2715,2717,2720,2723,2725],{"class":35,"line":249},[33,2710,2711],{"class":73},"        for",[33,2713,2714],{"class":77}," i ",[33,2716,1427],{"class":73},[33,2718,2719],{"class":77}," {",[33,2721,2722],{"class":87},"1..10}",[33,2724,363],{"class":77},[33,2726,406],{"class":73},[33,2728,2729,2732,2734,2736,2738,2740,2742,2744,2746],{"class":35,"line":255},[33,2730,2731],{"class":52},"            kill",[33,2733,2655],{"class":52},[33,2735,331],{"class":59},[33,2737,2660],{"class":77},[33,2739,337],{"class":59},[33,2741,1249],{"class":73},[33,2743,1252],{"class":59},[33,2745,792],{"class":73},[33,2747,2748],{"class":73}," break\n",[33,2750,2751,2754],{"class":35,"line":261},[33,2752,2753],{"class":87},"            sleep",[33,2755,2756],{"class":52}," 1\n",[33,2758,2759],{"class":35,"line":266},[33,2760,2761],{"class":73},"        done\n",[33,2763,2764,2766,2769,2771,2773,2775,2777,2779],{"class":35,"line":272},[33,2765,2687],{"class":52},[33,2767,2768],{"class":52}," -KILL",[33,2770,331],{"class":59},[33,2772,2660],{"class":77},[33,2774,337],{"class":59},[33,2776,1249],{"class":73},[33,2778,1252],{"class":59},[33,2780,2781],{"class":39},"  # force if still alive\n",[33,2783,2784,2787,2789,2791,2793,2795],{"class":35,"line":281},[33,2785,2786],{"class":52},"        wait",[33,2788,331],{"class":59},[33,2790,2660],{"class":77},[33,2792,337],{"class":59},[33,2794,1249],{"class":73},[33,2796,2701],{"class":59},[33,2798,2799],{"class":35,"line":289},[33,2800,999],{"class":73},[33,2802,2803,2805,2807,2809],{"class":35,"line":294},[33,2804,893],{"class":52},[33,2806,331],{"class":59},[33,2808,898],{"class":77},[33,2810,702],{"class":59},[33,2812,2813],{"class":35,"line":300},[33,2814,297],{"class":77},[33,2816,2817,2819,2821,2824,2827],{"class":35,"line":305},[33,2818,911],{"class":52},[33,2820,914],{"class":59},[33,2822,2823],{"class":59}," INT",[33,2825,2826],{"class":59}," TERM",[33,2828,917],{"class":59},[33,2830,2831],{"class":35,"line":346},[33,2832,67],{"emptyLinePlaceholder":66},[33,2834,2835,2837,2840,2842],{"class":35,"line":374},[33,2836,308],{"class":87},[33,2838,2839],{"class":59}," \"supervisor started: ",[33,2841,334],{"class":52},[33,2843,702],{"class":59},[33,2845,2846],{"class":35,"line":379},[33,2847,67],{"emptyLinePlaceholder":66},[33,2849,2850,2852,2854,2856],{"class":35,"line":385},[33,2851,388],{"class":73},[33,2853,1045],{"class":52},[33,2855,363],{"class":77},[33,2857,406],{"class":73},[33,2859,2860,2862,2865,2867],{"class":35,"line":409},[33,2861,1219],{"class":87},[33,2863,2864],{"class":59}," \"starting: ",[33,2866,334],{"class":52},[33,2868,702],{"class":59},[33,2870,2871,2873,2875,2877],{"class":35,"line":425},[33,2872,1664],{"class":87},[33,2874,991],{"class":52},[33,2876,337],{"class":87},[33,2878,2879],{"class":77}," &\n",[33,2881,2882,2885,2887],{"class":35,"line":453},[33,2883,2884],{"class":77},"    child_pid",[33,2886,81],{"class":73},[33,2888,2889],{"class":52},"$!\n",[33,2891,2892],{"class":35,"line":481},[33,2893,67],{"emptyLinePlaceholder":66},[33,2895,2896],{"class":35,"line":532},[33,2897,2898],{"class":39},"    # Wait for the child to exit\n",[33,2900,2901,2903,2906,2908,2910,2912,2914],{"class":35,"line":558},[33,2902,942],{"class":73},[33,2904,2905],{"class":52}," wait",[33,2907,331],{"class":59},[33,2909,2660],{"class":77},[33,2911,337],{"class":59},[33,2913,363],{"class":77},[33,2915,948],{"class":73},[33,2917,2918,2920],{"class":35,"line":579},[33,2919,954],{"class":87},[33,2921,2922],{"class":59}," \"child exited normally\"\n",[33,2924,2925,2928],{"class":35,"line":605},[33,2926,2927],{"class":73},"        break",[33,2929,2930],{"class":39},"  # clean exit — stop supervisor\n",[33,2932,2933],{"class":35,"line":628},[33,2934,999],{"class":73},[33,2936,2937],{"class":35,"line":639},[33,2938,67],{"emptyLinePlaceholder":66},[33,2940,2941,2944,2946],{"class":35,"line":645},[33,2942,2943],{"class":77},"    exit_code",[33,2945,81],{"class":73},[33,2947,845],{"class":52},[33,2949,2950,2952,2955,2958],{"class":35,"line":651},[33,2951,1219],{"class":87},[33,2953,2954],{"class":59}," \"child crashed (exit ",[33,2956,2957],{"class":77},"$exit_code",[33,2959,2960],{"class":59},")\"\n",[33,2962,2963],{"class":35,"line":656},[33,2964,67],{"emptyLinePlaceholder":66},[33,2966,2967],{"class":35,"line":688},[33,2968,2969],{"class":39},"    # Check restart limit\n",[33,2971,2972,2975,2978],{"class":35,"line":705},[33,2973,2974],{"class":77},"    ((restart_count",[33,2976,2977],{"class":73},"++",[33,2979,2980],{"class":77},"))\n",[33,2982,2983,2985,2988,2990,2992,2994,2997,3000,3003],{"class":35,"line":722},[33,2984,942],{"class":73},[33,2986,2987],{"class":77}," ((max_restarts ",[33,2989,1285],{"class":73},[33,2991,400],{"class":52},[33,2993,864],{"class":73},[33,2995,2996],{"class":77}," restart_count ",[33,2998,2999],{"class":73},">=",[33,3001,3002],{"class":77}," max_restarts)); ",[33,3004,948],{"class":73},[33,3006,3007,3010,3013,3016],{"class":35,"line":727},[33,3008,3009],{"class":87},"        die",[33,3011,3012],{"class":59}," \"max restarts (",[33,3014,3015],{"class":77},"$max_restarts",[33,3017,3018],{"class":59},") reached\"\n",[33,3020,3021],{"class":35,"line":733},[33,3022,999],{"class":73},[33,3024,3025],{"class":35,"line":762},[33,3026,67],{"emptyLinePlaceholder":66},[33,3028,3029,3031,3034,3036,3039,3042],{"class":35,"line":804},[33,3030,1219],{"class":87},[33,3032,3033],{"class":59}," \"restarting in ${",[33,3035,2578],{"class":77},[33,3037,3038],{"class":59},"}s (attempt ",[33,3040,3041],{"class":77},"$restart_count",[33,3043,2682],{"class":59},[33,3045,3046,3049,3051,3054],{"class":35,"line":809},[33,3047,3048],{"class":87},"    sleep",[33,3050,331],{"class":59},[33,3052,3053],{"class":77},"$backoff",[33,3055,702],{"class":59},[33,3057,3058],{"class":35,"line":815},[33,3059,67],{"emptyLinePlaceholder":66},[33,3061,3062],{"class":35,"line":826},[33,3063,3064],{"class":39},"    # Exponential backoff: 1, 2, 4, 8, 16, 32, 60, 60, ...\n",[33,3066,3067,3070,3073,3075],{"class":35,"line":834},[33,3068,3069],{"class":77},"    ((backoff ",[33,3071,3072],{"class":73},"*=",[33,3074,527],{"class":52},[33,3076,2980],{"class":77},[33,3078,3079,3081,3083,3086,3088],{"class":35,"line":848},[33,3080,3069],{"class":77},[33,3082,1285],{"class":73},[33,3084,3085],{"class":77}," max_backoff)) && backoff",[33,3087,81],{"class":73},[33,3089,3090],{"class":77},"$max_backoff\n",[33,3092,3093],{"class":35,"line":890},[33,3094,648],{"class":73},[33,3096,3097],{"class":35,"line":903},[33,3098,67],{"emptyLinePlaceholder":66},[33,3100,3101,3103],{"class":35,"line":908},[33,3102,308],{"class":87},[33,3104,3105],{"class":59}," \"supervisor stopped\"\n",[15,3107,3109],{"id":3108},"project-5-parallel-batch-image-resizer","Project 5 — Parallel Batch Image Resizer",[20,3111,3112],{"language":22},[24,3113,3115],{"className":26,"code":3114,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# ── Resize images in parallel with xargs -P ──\nset -euo pipefail\n\nsize=\"${1:?usage: $0 \u003Csize> [directory]}\"\ndir=\"${2:-.}\"\n\ncommand -v convert &>\u002Fdev\u002Fnull || { echo \"ImageMagick not found\" >&2; exit 1; }\n[[ -d \"$dir\" ]] || { echo \"not a directory: $dir\" >&2; exit 1; }\n\n# ── Resize function (exported for xargs) ──\nresize_one() {\n    local file=$1 size=$2\n    local base=\"${file%.*}\"\n    local ext=\"${file##*.}\"\n    local out=\"${base}_${size}.${ext}\"\n    convert \"$file\" -resize \"$size\" \"$out\"\n    printf '%s → %s\\n' \"$file\" \"$out\"\n}\nexport -f resize_one\n\n# ── Find images and resize in parallel (4 at a time) ──\ncount=0\nwhile IFS= read -r -d '' file; do\n    resize_one \"$file\" \"$size\" &\n    ((count++))\n    # Limit to 4 parallel jobs\n    ((count % 4 == 0)) && wait\ndone \u003C \u003C(find \"$dir\" -maxdepth 1 -type f \\( -iname \"*.jpg\" -o -iname \"*.png\" \\) -print0)\nwait  # wait for remaining jobs\n\nprintf 'resized %d images\\n' \"$count\"\n",[30,3116,3117,3121,3126,3134,3138,3169,3188,3192,3227,3265,3269,3274,3281,3300,3325,3344,3371,3400,3419,3423,3433,3437,3442,3451,3477,3496,3505,3510,3529,3584,3592,3596],{"__ignoreMap":28},[33,3118,3119],{"class":35,"line":36},[33,3120,40],{"class":39},[33,3122,3123],{"class":35,"line":43},[33,3124,3125],{"class":39},"# ── Resize images in parallel with xargs -P ──\n",[33,3127,3128,3130,3132],{"class":35,"line":49},[33,3129,53],{"class":52},[33,3131,1533],{"class":52},[33,3133,60],{"class":59},[33,3135,3136],{"class":35,"line":63},[33,3137,67],{"emptyLinePlaceholder":66},[33,3139,3140,3143,3145,3147,3149,3151,3153,3155,3158,3160,3162,3165,3167],{"class":35,"line":70},[33,3141,3142],{"class":77},"size",[33,3144,81],{"class":73},[33,3146,337],{"class":59},[33,3148,1553],{"class":52},[33,3150,504],{"class":73},[33,3152,185],{"class":77},[33,3154,1443],{"class":73},[33,3156,3157],{"class":52}," $0",[33,3159,1351],{"class":59},[33,3161,3142],{"class":77},[33,3163,3164],{"class":59},"> [directory]",[33,3166,518],{"class":52},[33,3168,702],{"class":59},[33,3170,3171,3174,3176,3178,3180,3182,3184,3186],{"class":35,"line":103},[33,3172,3173],{"class":77},"dir",[33,3175,81],{"class":73},[33,3177,337],{"class":59},[33,3179,501],{"class":52},[33,3181,1556],{"class":73},[33,3183,1562],{"class":59},[33,3185,518],{"class":52},[33,3187,702],{"class":59},[33,3189,3190],{"class":35,"line":123},[33,3191,67],{"emptyLinePlaceholder":66},[33,3193,3194,3197,3199,3202,3204,3206,3208,3210,3212,3214,3217,3219,3221,3223,3225],{"class":35,"line":128},[33,3195,3196],{"class":52},"command",[33,3198,1981],{"class":52},[33,3200,3201],{"class":59}," convert",[33,3203,1282],{"class":77},[33,3205,1285],{"class":73},[33,3207,1288],{"class":77},[33,3209,672],{"class":73},[33,3211,675],{"class":77},[33,3213,1050],{"class":52},[33,3215,3216],{"class":59}," \"ImageMagick not found\"",[33,3218,340],{"class":73},[33,3220,363],{"class":77},[33,3222,366],{"class":52},[33,3224,369],{"class":52},[33,3226,343],{"class":77},[33,3228,3229,3231,3233,3235,3238,3240,3242,3244,3246,3248,3251,3253,3255,3257,3259,3261,3263],{"class":35,"line":134},[33,3230,659],{"class":77},[33,3232,738],{"class":73},[33,3234,331],{"class":59},[33,3236,3237],{"class":77},"$dir",[33,3239,337],{"class":59},[33,3241,669],{"class":77},[33,3243,672],{"class":73},[33,3245,675],{"class":77},[33,3247,1050],{"class":52},[33,3249,3250],{"class":59}," \"not a directory: ",[33,3252,3237],{"class":77},[33,3254,337],{"class":59},[33,3256,340],{"class":73},[33,3258,363],{"class":77},[33,3260,366],{"class":52},[33,3262,369],{"class":52},[33,3264,343],{"class":77},[33,3266,3267],{"class":35,"line":145},[33,3268,67],{"emptyLinePlaceholder":66},[33,3270,3271],{"class":35,"line":156},[33,3272,3273],{"class":39},"# ── Resize function (exported for xargs) ──\n",[33,3275,3276,3279],{"class":35,"line":167},[33,3277,3278],{"class":87},"resize_one",[33,3280,188],{"class":77},[33,3282,3283,3285,3288,3290,3292,3295,3297],{"class":35,"line":177},[33,3284,837],{"class":73},[33,3286,3287],{"class":77}," file",[33,3289,81],{"class":73},[33,3291,417],{"class":1639},[33,3293,3294],{"class":77}," size",[33,3296,81],{"class":73},[33,3298,3299],{"class":1639},"$2\n",[33,3301,3302,3304,3307,3309,3312,3315,3318,3320,3322],{"class":35,"line":182},[33,3303,837],{"class":73},[33,3305,3306],{"class":77}," base",[33,3308,81],{"class":73},[33,3310,3311],{"class":59},"\"${",[33,3313,3314],{"class":77},"file",[33,3316,3317],{"class":73},"%",[33,3319,1562],{"class":59},[33,3321,611],{"class":73},[33,3323,3324],{"class":59},"}\"\n",[33,3326,3327,3329,3332,3334,3336,3338,3341],{"class":35,"line":191},[33,3328,837],{"class":73},[33,3330,3331],{"class":77}," ext",[33,3333,81],{"class":73},[33,3335,3311],{"class":59},[33,3337,3314],{"class":77},[33,3339,3340],{"class":73},"##*",[33,3342,3343],{"class":59},".}\"\n",[33,3345,3346,3348,3351,3353,3355,3358,3361,3363,3366,3369],{"class":35,"line":203},[33,3347,837],{"class":73},[33,3349,3350],{"class":77}," out",[33,3352,81],{"class":73},[33,3354,3311],{"class":59},[33,3356,3357],{"class":77},"base",[33,3359,3360],{"class":59},"}_${",[33,3362,3142],{"class":77},[33,3364,3365],{"class":59},"}.${",[33,3367,3368],{"class":77},"ext",[33,3370,3324],{"class":59},[33,3372,3373,3376,3378,3381,3383,3386,3388,3391,3393,3395,3398],{"class":35,"line":215},[33,3374,3375],{"class":87},"    convert",[33,3377,331],{"class":59},[33,3379,3380],{"class":77},"$file",[33,3382,337],{"class":59},[33,3384,3385],{"class":52}," -resize",[33,3387,331],{"class":59},[33,3389,3390],{"class":77},"$size",[33,3392,337],{"class":59},[33,3394,331],{"class":59},[33,3396,3397],{"class":77},"$out",[33,3399,702],{"class":59},[33,3401,3402,3404,3407,3409,3411,3413,3415,3417],{"class":35,"line":220},[33,3403,1649],{"class":52},[33,3405,3406],{"class":59}," '%s → %s\\n'",[33,3408,331],{"class":59},[33,3410,3380],{"class":77},[33,3412,337],{"class":59},[33,3414,331],{"class":59},[33,3416,3397],{"class":77},[33,3418,702],{"class":59},[33,3420,3421],{"class":35,"line":226},[33,3422,297],{"class":77},[33,3424,3425,3428,3430],{"class":35,"line":232},[33,3426,3427],{"class":73},"export",[33,3429,867],{"class":52},[33,3431,3432],{"class":77}," resize_one\n",[33,3434,3435],{"class":35,"line":238},[33,3436,67],{"emptyLinePlaceholder":66},[33,3438,3439],{"class":35,"line":249},[33,3440,3441],{"class":39},"# ── Find images and resize in parallel (4 at a time) ──\n",[33,3443,3444,3447,3449],{"class":35,"line":255},[33,3445,3446],{"class":77},"count",[33,3448,81],{"class":73},[33,3450,2573],{"class":59},[33,3452,3453,3455,3458,3460,3463,3465,3468,3471,3473,3475],{"class":35,"line":261},[33,3454,388],{"class":73},[33,3456,3457],{"class":77}," IFS",[33,3459,81],{"class":73},[33,3461,3462],{"class":52}," read",[33,3464,1386],{"class":52},[33,3466,3467],{"class":52}," -d",[33,3469,3470],{"class":59}," ''",[33,3472,3287],{"class":59},[33,3474,363],{"class":77},[33,3476,406],{"class":73},[33,3478,3479,3482,3484,3486,3488,3490,3492,3494],{"class":35,"line":266},[33,3480,3481],{"class":87},"    resize_one",[33,3483,331],{"class":59},[33,3485,3380],{"class":77},[33,3487,337],{"class":59},[33,3489,331],{"class":59},[33,3491,3390],{"class":77},[33,3493,337],{"class":59},[33,3495,2879],{"class":77},[33,3497,3498,3501,3503],{"class":35,"line":272},[33,3499,3500],{"class":77},"    ((count",[33,3502,2977],{"class":73},[33,3504,2980],{"class":77},[33,3506,3507],{"class":35,"line":281},[33,3508,3509],{"class":39},"    # Limit to 4 parallel jobs\n",[33,3511,3512,3515,3517,3520,3522,3524,3526],{"class":35,"line":289},[33,3513,3514],{"class":77},"    ((count ",[33,3516,3317],{"class":73},[33,3518,3519],{"class":52}," 4",[33,3521,1042],{"class":73},[33,3523,400],{"class":52},[33,3525,2649],{"class":77},[33,3527,3528],{"class":52},"wait\n",[33,3530,3531,3534,3536,3538,3541,3543,3545,3548,3551,3553,3556,3559,3562,3565,3568,3571,3573,3576,3579,3582],{"class":35,"line":294},[33,3532,3533],{"class":73},"done",[33,3535,1351],{"class":73},[33,3537,1354],{"class":59},[33,3539,3540],{"class":87},"find",[33,3542,331],{"class":59},[33,3544,3237],{"class":77},[33,3546,3547],{"class":59},"\" ",[33,3549,3550],{"class":52},"-maxdepth",[33,3552,369],{"class":52},[33,3554,3555],{"class":52}," -type",[33,3557,3558],{"class":59}," f ",[33,3560,3561],{"class":52},"\\(",[33,3563,3564],{"class":52}," -iname",[33,3566,3567],{"class":59}," \"*.jpg\" ",[33,3569,3570],{"class":52},"-o",[33,3572,3564],{"class":52},[33,3574,3575],{"class":59}," \"*.png\" ",[33,3577,3578],{"class":52},"\\)",[33,3580,3581],{"class":52}," -print0",[33,3583,100],{"class":59},[33,3585,3586,3589],{"class":35,"line":300},[33,3587,3588],{"class":52},"wait",[33,3590,3591],{"class":39},"  # wait for remaining jobs\n",[33,3593,3594],{"class":35,"line":305},[33,3595,67],{"emptyLinePlaceholder":66},[33,3597,3598,3600,3603,3605,3608],{"class":35,"line":346},[33,3599,314],{"class":52},[33,3601,3602],{"class":59}," 'resized %d images\\n'",[33,3604,331],{"class":59},[33,3606,3607],{"class":77},"$count",[33,3609,702],{"class":59},[15,3611,3613],{"id":3612},"project-6-dotfile-manager","Project 6 — Dotfile Manager",[20,3615,3616],{"language":22},[24,3617,3619],{"className":26,"code":3618,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# ── Symlink dotfiles from repo to $HOME with backup and status ──\nset -euo pipefail\n\nreadonly SCRIPT_DIR=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\nreadonly DOTFILES=(\"bashrc\" \"vimrc\" \"gitconfig\" \"tmux.conf\" \"zshrc\")\n\nusage() {\n    cat \u003C\u003CEOF\nUsage: $(basename \"$0\") \u003Ccommand>\n\nCommands:\n  link     Symlink dotfiles to $HOME (backups existing files)\n  unlink   Remove symlinks from $HOME\n  status   Show link status\nEOF\n}\n\nlog() { printf '%s\\n' \"$*\"; }\n\n# ── Link dotfiles ──\nlink() {\n    for dot in \"${DOTFILES[@]}\"; do\n        local src=\"$SCRIPT_DIR\u002F$dot\"\n        local dst=\"$HOME\u002F.$dot\"\n\n        [[ -f \"$src\" ]] || continue  # skip if source doesn't exist\n\n        # Backup existing file (if it's not already a symlink)\n        if [[ -e \"$dst\" && ! -L \"$dst\" ]]; then\n            local backup=\"$dst.backup.$(date +%s)\"\n            mv \"$dst\" \"$backup\"\n            log \"backed up existing .$dot → $backup\"\n        fi\n\n        # Create symlink (force — replaces existing symlink)\n        ln -sf \"$src\" \"$dst\"\n        log \"linked .$dot → $src\"\n    done\n}\n\n# ── Unlink dotfiles ──\nunlink() {\n    for dot in \"${DOTFILES[@]}\"; do\n        local dst=\"$HOME\u002F.$dot\"\n        if [[ -L \"$dst\" ]]; then\n            rm \"$dst\"\n            log \"unlinked .$dot\"\n        fi\n    done\n}\n\n# ── Show status ──\nstatus() {\n    for dot in \"${DOTFILES[@]}\"; do\n        local src=\"$SCRIPT_DIR\u002F$dot\"\n        local dst=\"$HOME\u002F.$dot\"\n        if [[ -L \"$dst\" ]]; then\n            local target\n            target=$(readlink \"$dst\")\n            if [[ \"$target\" == \"$src\" ]]; then\n                log \"  ✓ .$dot → $target\"\n            else\n                log \"  ⚠ .$dot → $target (wrong target!)\"\n            fi\n        elif [[ -e \"$dst\" ]]; then\n            log \"  ! .$dot exists (not a symlink)\"\n        elif [[ -f \"$src\" ]]; then\n            log \"  ✗ .$dot missing (not linked)\"\n        fi\n    done\n}\n\n# ── Dispatch ──\ncase \"${1:-}\" in\n    link)   link ;;\n    unlink) unlink ;;\n    status) status ;;\n    *)      usage; exit 1 ;;\nesac\n",[30,3620,3621,3625,3630,3638,3642,3675,3704,3708,3714,3722,3736,3740,3745,3756,3764,3769,3773,3777,3781,3799,3803,3808,3815,3839,3862,3882,3886,3910,3914,3919,3953,3975,3993,4010,4015,4019,4024,4044,4059,4063,4067,4071,4076,4083,4105,4123,4142,4153,4164,4168,4172,4176,4180,4185,4192,4214,4232,4250,4268,4275,4295,4321,4337,4342,4358,4363,4382,4394,4412,4424,4428,4432,4436,4440,4445,4462,4474,4486,4498,4514],{"__ignoreMap":28},[33,3622,3623],{"class":35,"line":36},[33,3624,40],{"class":39},[33,3626,3627],{"class":35,"line":43},[33,3628,3629],{"class":39},"# ── Symlink dotfiles from repo to $HOME with backup and status ──\n",[33,3631,3632,3634,3636],{"class":35,"line":49},[33,3633,53],{"class":52},[33,3635,1533],{"class":52},[33,3637,60],{"class":59},[33,3639,3640],{"class":35,"line":63},[33,3641,67],{"emptyLinePlaceholder":66},[33,3643,3644,3646,3649,3651,3653,3656,3658,3660,3662,3664,3667,3670,3673],{"class":35,"line":70},[33,3645,74],{"class":73},[33,3647,3648],{"class":77}," SCRIPT_DIR",[33,3650,81],{"class":73},[33,3652,84],{"class":77},[33,3654,3655],{"class":52},"cd",[33,3657,320],{"class":59},[33,3659,1122],{"class":87},[33,3661,91],{"class":59},[33,3663,94],{"class":77},[33,3665,3666],{"class":59},"[0]}\")\"",[33,3668,3669],{"class":77}," && ",[33,3671,3672],{"class":52},"pwd",[33,3674,100],{"class":77},[33,3676,3677,3679,3682,3684,3687,3690,3693,3696,3699,3702],{"class":35,"line":103},[33,3678,74],{"class":73},[33,3680,3681],{"class":77}," DOTFILES",[33,3683,81],{"class":73},[33,3685,3686],{"class":77},"(",[33,3688,3689],{"class":59},"\"bashrc\"",[33,3691,3692],{"class":59}," \"vimrc\"",[33,3694,3695],{"class":59}," \"gitconfig\"",[33,3697,3698],{"class":59}," \"tmux.conf\"",[33,3700,3701],{"class":59}," \"zshrc\"",[33,3703,100],{"class":77},[33,3705,3706],{"class":35,"line":123},[33,3707,67],{"emptyLinePlaceholder":66},[33,3709,3710,3712],{"class":35,"line":128},[33,3711,185],{"class":87},[33,3713,188],{"class":77},[33,3715,3716,3718,3720],{"class":35,"line":134},[33,3717,194],{"class":87},[33,3719,197],{"class":73},[33,3721,200],{"class":59},[33,3723,3724,3727,3729,3731,3733],{"class":35,"line":145},[33,3725,3726],{"class":59},"Usage: $(",[33,3728,88],{"class":87},[33,3730,331],{"class":59},[33,3732,2556],{"class":52},[33,3734,3735],{"class":59},"\") \u003Ccommand>\n",[33,3737,3738],{"class":35,"line":156},[33,3739,67],{"emptyLinePlaceholder":66},[33,3741,3742],{"class":35,"line":167},[33,3743,3744],{"class":59},"Commands:\n",[33,3746,3747,3750,3753],{"class":35,"line":177},[33,3748,3749],{"class":59},"  link     Symlink dotfiles to ",[33,3751,3752],{"class":77},"$HOME",[33,3754,3755],{"class":59}," (backups existing files)\n",[33,3757,3758,3761],{"class":35,"line":182},[33,3759,3760],{"class":59},"  unlink   Remove symlinks from ",[33,3762,3763],{"class":77},"$HOME\n",[33,3765,3766],{"class":35,"line":191},[33,3767,3768],{"class":59},"  status   Show link status\n",[33,3770,3771],{"class":35,"line":203},[33,3772,200],{"class":59},[33,3774,3775],{"class":35,"line":215},[33,3776,297],{"class":77},[33,3778,3779],{"class":35,"line":220},[33,3780,67],{"emptyLinePlaceholder":66},[33,3782,3783,3785,3787,3789,3791,3793,3795,3797],{"class":35,"line":226},[33,3784,308],{"class":87},[33,3786,311],{"class":77},[33,3788,314],{"class":52},[33,3790,2217],{"class":59},[33,3792,331],{"class":59},[33,3794,334],{"class":52},[33,3796,337],{"class":59},[33,3798,343],{"class":77},[33,3800,3801],{"class":35,"line":232},[33,3802,67],{"emptyLinePlaceholder":66},[33,3804,3805],{"class":35,"line":238},[33,3806,3807],{"class":39},"# ── Link dotfiles ──\n",[33,3809,3810,3813],{"class":35,"line":249},[33,3811,3812],{"class":87},"link",[33,3814,188],{"class":77},[33,3816,3817,3819,3822,3824,3826,3829,3831,3833,3835,3837],{"class":35,"line":255},[33,3818,1421],{"class":73},[33,3820,3821],{"class":77}," dot ",[33,3823,1427],{"class":73},[33,3825,91],{"class":59},[33,3827,3828],{"class":77},"DOTFILES",[33,3830,1435],{"class":59},[33,3832,1405],{"class":73},[33,3834,2228],{"class":59},[33,3836,363],{"class":77},[33,3838,406],{"class":73},[33,3840,3841,3844,3847,3849,3851,3854,3857,3860],{"class":35,"line":261},[33,3842,3843],{"class":73},"        local",[33,3845,3846],{"class":77}," src",[33,3848,81],{"class":73},[33,3850,337],{"class":59},[33,3852,3853],{"class":77},"$SCRIPT_DIR",[33,3855,3856],{"class":59},"\u002F",[33,3858,3859],{"class":77},"$dot",[33,3861,702],{"class":59},[33,3863,3864,3866,3869,3871,3873,3875,3878,3880],{"class":35,"line":266},[33,3865,3843],{"class":73},[33,3867,3868],{"class":77}," dst",[33,3870,81],{"class":73},[33,3872,337],{"class":59},[33,3874,3752],{"class":77},[33,3876,3877],{"class":59},"\u002F.",[33,3879,3859],{"class":77},[33,3881,702],{"class":59},[33,3883,3884],{"class":35,"line":272},[33,3885,67],{"emptyLinePlaceholder":66},[33,3887,3888,3891,3893,3895,3898,3900,3902,3904,3907],{"class":35,"line":281},[33,3889,3890],{"class":77},"        [[ ",[33,3892,1579],{"class":73},[33,3894,331],{"class":59},[33,3896,3897],{"class":77},"$src",[33,3899,337],{"class":59},[33,3901,669],{"class":77},[33,3903,672],{"class":73},[33,3905,3906],{"class":73}," continue",[33,3908,3909],{"class":39},"  # skip if source doesn't exist\n",[33,3911,3912],{"class":35,"line":289},[33,3913,67],{"emptyLinePlaceholder":66},[33,3915,3916],{"class":35,"line":294},[33,3917,3918],{"class":39},"        # Backup existing file (if it's not already a symlink)\n",[33,3920,3921,3924,3926,3929,3931,3934,3936,3938,3940,3943,3945,3947,3949,3951],{"class":35,"line":300},[33,3922,3923],{"class":73},"        if",[33,3925,391],{"class":77},[33,3927,3928],{"class":73},"-e",[33,3930,331],{"class":59},[33,3932,3933],{"class":77},"$dst",[33,3935,337],{"class":59},[33,3937,864],{"class":73},[33,3939,1209],{"class":73},[33,3941,3942],{"class":73}," -L",[33,3944,331],{"class":59},[33,3946,3933],{"class":77},[33,3948,337],{"class":59},[33,3950,403],{"class":77},[33,3952,948],{"class":73},[33,3954,3955,3958,3961,3963,3965,3967,3970,3972],{"class":35,"line":305},[33,3956,3957],{"class":73},"            local",[33,3959,3960],{"class":77}," backup",[33,3962,81],{"class":73},[33,3964,337],{"class":59},[33,3966,3933],{"class":77},[33,3968,3969],{"class":59},".backup.$(",[33,3971,115],{"class":87},[33,3973,3974],{"class":59}," +%s)\"\n",[33,3976,3977,3980,3982,3984,3986,3988,3991],{"class":35,"line":346},[33,3978,3979],{"class":87},"            mv",[33,3981,331],{"class":59},[33,3983,3933],{"class":77},[33,3985,337],{"class":59},[33,3987,331],{"class":59},[33,3989,3990],{"class":77},"$backup",[33,3992,702],{"class":59},[33,3994,3995,3998,4001,4003,4006,4008],{"class":35,"line":374},[33,3996,3997],{"class":87},"            log",[33,3999,4000],{"class":59}," \"backed up existing .",[33,4002,3859],{"class":77},[33,4004,4005],{"class":59}," → ",[33,4007,3990],{"class":77},[33,4009,702],{"class":59},[33,4011,4012],{"class":35,"line":379},[33,4013,4014],{"class":73},"        fi\n",[33,4016,4017],{"class":35,"line":385},[33,4018,67],{"emptyLinePlaceholder":66},[33,4020,4021],{"class":35,"line":409},[33,4022,4023],{"class":39},"        # Create symlink (force — replaces existing symlink)\n",[33,4025,4026,4029,4032,4034,4036,4038,4040,4042],{"class":35,"line":425},[33,4027,4028],{"class":87},"        ln",[33,4030,4031],{"class":52}," -sf",[33,4033,331],{"class":59},[33,4035,3897],{"class":77},[33,4037,337],{"class":59},[33,4039,331],{"class":59},[33,4041,3933],{"class":77},[33,4043,702],{"class":59},[33,4045,4046,4048,4051,4053,4055,4057],{"class":35,"line":453},[33,4047,954],{"class":87},[33,4049,4050],{"class":59}," \"linked .",[33,4052,3859],{"class":77},[33,4054,4005],{"class":59},[33,4056,3897],{"class":77},[33,4058,702],{"class":59},[33,4060,4061],{"class":35,"line":481},[33,4062,1488],{"class":73},[33,4064,4065],{"class":35,"line":532},[33,4066,297],{"class":77},[33,4068,4069],{"class":35,"line":558},[33,4070,67],{"emptyLinePlaceholder":66},[33,4072,4073],{"class":35,"line":579},[33,4074,4075],{"class":39},"# ── Unlink dotfiles ──\n",[33,4077,4078,4081],{"class":35,"line":605},[33,4079,4080],{"class":87},"unlink",[33,4082,188],{"class":77},[33,4084,4085,4087,4089,4091,4093,4095,4097,4099,4101,4103],{"class":35,"line":628},[33,4086,1421],{"class":73},[33,4088,3821],{"class":77},[33,4090,1427],{"class":73},[33,4092,91],{"class":59},[33,4094,3828],{"class":77},[33,4096,1435],{"class":59},[33,4098,1405],{"class":73},[33,4100,2228],{"class":59},[33,4102,363],{"class":77},[33,4104,406],{"class":73},[33,4106,4107,4109,4111,4113,4115,4117,4119,4121],{"class":35,"line":639},[33,4108,3843],{"class":73},[33,4110,3868],{"class":77},[33,4112,81],{"class":73},[33,4114,337],{"class":59},[33,4116,3752],{"class":77},[33,4118,3877],{"class":59},[33,4120,3859],{"class":77},[33,4122,702],{"class":59},[33,4124,4125,4127,4129,4132,4134,4136,4138,4140],{"class":35,"line":645},[33,4126,3923],{"class":73},[33,4128,391],{"class":77},[33,4130,4131],{"class":73},"-L",[33,4133,331],{"class":59},[33,4135,3933],{"class":77},[33,4137,337],{"class":59},[33,4139,403],{"class":77},[33,4141,948],{"class":73},[33,4143,4144,4147,4149,4151],{"class":35,"line":651},[33,4145,4146],{"class":87},"            rm",[33,4148,331],{"class":59},[33,4150,3933],{"class":77},[33,4152,702],{"class":59},[33,4154,4155,4157,4160,4162],{"class":35,"line":656},[33,4156,3997],{"class":87},[33,4158,4159],{"class":59}," \"unlinked .",[33,4161,3859],{"class":77},[33,4163,702],{"class":59},[33,4165,4166],{"class":35,"line":688},[33,4167,4014],{"class":73},[33,4169,4170],{"class":35,"line":705},[33,4171,1488],{"class":73},[33,4173,4174],{"class":35,"line":722},[33,4175,297],{"class":77},[33,4177,4178],{"class":35,"line":727},[33,4179,67],{"emptyLinePlaceholder":66},[33,4181,4182],{"class":35,"line":733},[33,4183,4184],{"class":39},"# ── Show status ──\n",[33,4186,4187,4190],{"class":35,"line":762},[33,4188,4189],{"class":87},"status",[33,4191,188],{"class":77},[33,4193,4194,4196,4198,4200,4202,4204,4206,4208,4210,4212],{"class":35,"line":804},[33,4195,1421],{"class":73},[33,4197,3821],{"class":77},[33,4199,1427],{"class":73},[33,4201,91],{"class":59},[33,4203,3828],{"class":77},[33,4205,1435],{"class":59},[33,4207,1405],{"class":73},[33,4209,2228],{"class":59},[33,4211,363],{"class":77},[33,4213,406],{"class":73},[33,4215,4216,4218,4220,4222,4224,4226,4228,4230],{"class":35,"line":809},[33,4217,3843],{"class":73},[33,4219,3846],{"class":77},[33,4221,81],{"class":73},[33,4223,337],{"class":59},[33,4225,3853],{"class":77},[33,4227,3856],{"class":59},[33,4229,3859],{"class":77},[33,4231,702],{"class":59},[33,4233,4234,4236,4238,4240,4242,4244,4246,4248],{"class":35,"line":815},[33,4235,3843],{"class":73},[33,4237,3868],{"class":77},[33,4239,81],{"class":73},[33,4241,337],{"class":59},[33,4243,3752],{"class":77},[33,4245,3877],{"class":59},[33,4247,3859],{"class":77},[33,4249,702],{"class":59},[33,4251,4252,4254,4256,4258,4260,4262,4264,4266],{"class":35,"line":826},[33,4253,3923],{"class":73},[33,4255,391],{"class":77},[33,4257,4131],{"class":73},[33,4259,331],{"class":59},[33,4261,3933],{"class":77},[33,4263,337],{"class":59},[33,4265,403],{"class":77},[33,4267,948],{"class":73},[33,4269,4270,4272],{"class":35,"line":834},[33,4271,3957],{"class":73},[33,4273,4274],{"class":77}," target\n",[33,4276,4277,4280,4282,4284,4287,4289,4291,4293],{"class":35,"line":848},[33,4278,4279],{"class":77},"            target",[33,4281,81],{"class":73},[33,4283,84],{"class":77},[33,4285,4286],{"class":87},"readlink",[33,4288,331],{"class":59},[33,4290,3933],{"class":77},[33,4292,337],{"class":59},[33,4294,100],{"class":77},[33,4296,4297,4300,4302,4304,4307,4309,4311,4313,4315,4317,4319],{"class":35,"line":890},[33,4298,4299],{"class":73},"            if",[33,4301,391],{"class":77},[33,4303,337],{"class":59},[33,4305,4306],{"class":77},"$target",[33,4308,337],{"class":59},[33,4310,1042],{"class":73},[33,4312,331],{"class":59},[33,4314,3897],{"class":77},[33,4316,337],{"class":59},[33,4318,403],{"class":77},[33,4320,948],{"class":73},[33,4322,4323,4326,4329,4331,4333,4335],{"class":35,"line":903},[33,4324,4325],{"class":87},"                log",[33,4327,4328],{"class":59}," \"  ✓ .",[33,4330,3859],{"class":77},[33,4332,4005],{"class":59},[33,4334,4306],{"class":77},[33,4336,702],{"class":59},[33,4338,4339],{"class":35,"line":908},[33,4340,4341],{"class":73},"            else\n",[33,4343,4344,4346,4349,4351,4353,4355],{"class":35,"line":920},[33,4345,4325],{"class":87},[33,4347,4348],{"class":59}," \"  ⚠ .",[33,4350,3859],{"class":77},[33,4352,4005],{"class":59},[33,4354,4306],{"class":77},[33,4356,4357],{"class":59}," (wrong target!)\"\n",[33,4359,4360],{"class":35,"line":925},[33,4361,4362],{"class":73},"            fi\n",[33,4364,4365,4368,4370,4372,4374,4376,4378,4380],{"class":35,"line":931},[33,4366,4367],{"class":73},"        elif",[33,4369,391],{"class":77},[33,4371,3928],{"class":73},[33,4373,331],{"class":59},[33,4375,3933],{"class":77},[33,4377,337],{"class":59},[33,4379,403],{"class":77},[33,4381,948],{"class":73},[33,4383,4384,4386,4389,4391],{"class":35,"line":939},[33,4385,3997],{"class":87},[33,4387,4388],{"class":59}," \"  ! .",[33,4390,3859],{"class":77},[33,4392,4393],{"class":59}," exists (not a symlink)\"\n",[33,4395,4396,4398,4400,4402,4404,4406,4408,4410],{"class":35,"line":951},[33,4397,4367],{"class":73},[33,4399,391],{"class":77},[33,4401,1579],{"class":73},[33,4403,331],{"class":59},[33,4405,3897],{"class":77},[33,4407,337],{"class":59},[33,4409,403],{"class":77},[33,4411,948],{"class":73},[33,4413,4414,4416,4419,4421],{"class":35,"line":964},[33,4415,3997],{"class":87},[33,4417,4418],{"class":59}," \"  ✗ .",[33,4420,3859],{"class":77},[33,4422,4423],{"class":59}," missing (not linked)\"\n",[33,4425,4426],{"class":35,"line":970},[33,4427,4014],{"class":73},[33,4429,4430],{"class":35,"line":985},[33,4431,1488],{"class":73},[33,4433,4434],{"class":35,"line":996},[33,4435,297],{"class":77},[33,4437,4438],{"class":35,"line":1002},[33,4439,67],{"emptyLinePlaceholder":66},[33,4441,4442],{"class":35,"line":1007},[33,4443,4444],{"class":39},"# ── Dispatch ──\n",[33,4446,4447,4450,4452,4454,4456,4458,4460],{"class":35,"line":1012},[33,4448,4449],{"class":73},"case",[33,4451,331],{"class":59},[33,4453,1553],{"class":52},[33,4455,1556],{"class":73},[33,4457,518],{"class":52},[33,4459,337],{"class":59},[33,4461,422],{"class":73},[33,4463,4464,4467,4469,4472],{"class":35,"line":1018},[33,4465,4466],{"class":428},"    link",[33,4468,438],{"class":73},[33,4470,4471],{"class":87},"   link",[33,4473,450],{"class":77},[33,4475,4476,4479,4481,4484],{"class":35,"line":1064},[33,4477,4478],{"class":428},"    unlink",[33,4480,438],{"class":73},[33,4482,4483],{"class":87}," unlink",[33,4485,450],{"class":77},[33,4487,4488,4491,4493,4496],{"class":35,"line":1083},[33,4489,4490],{"class":428},"    status",[33,4492,438],{"class":73},[33,4494,4495],{"class":87}," status",[33,4497,450],{"class":77},[33,4499,4500,4503,4506,4508,4510,4512],{"class":35,"line":1088},[33,4501,4502],{"class":73},"    *)",[33,4504,4505],{"class":87},"      usage",[33,4507,363],{"class":77},[33,4509,366],{"class":52},[33,4511,369],{"class":52},[33,4513,450],{"class":77},[33,4515,4516],{"class":35,"line":1099},[33,4517,4518],{"class":73},"esac\n",[15,4520,4522],{"id":4521},"project-7-capstone-deployment-script","Project 7 — Capstone: Deployment Script",[20,4524,4525],{"language":22},[24,4526,4528],{"className":26,"code":4527,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bash\n# ── Production deployment script: build, test, deploy, health check, rollback ──\nset -Eeuo pipefail\n\nreadonly SCRIPT_NAME=$(basename \"${BASH_SOURCE[0]}\")\nreadonly VERSION=\"2.0.0\"\n\n# ── Config (per-environment) ──\ndeclare -A ENVS=(\n    [staging_host]=\"staging.example.com\"\n    [staging_path]=\"\u002Fvar\u002Fwww\u002Fstaging\"\n    [staging_service]=\"myapp-staging\"\n    [prod_host]=\"prod.example.com\"\n    [prod_path]=\"\u002Fvar\u002Fwww\u002Fprod\"\n    [prod_service]=\"myapp-prod\"\n)\nRELEASES_TO_KEEP=5\nDRY_RUN=false\nROLLBACK=false\nHEALTH_RETRIES=5\nHEALTH_DELAY=5\n\nlog() { printf '[%s] %s\\n' \"$(date -Iseconds)\" \"$*\" >&2; }\ndie() { log \"FATAL: $*\"; exit 1; }\n\nusage() {\n    cat \u003C\u003CEOF\n$SCRIPT_NAME v$VERSION\n\nUsage: $SCRIPT_NAME \u003Cenv> [OPTIONS]\n\nArguments:\n  env         Target environment (staging, prod)\n\nOptions:\n  -h, --help       Show this help\n  -n, --dry-run    Show what would be done (no execution)\n  --rollback       Rollback to previous release\n  -k, --keep N     Keep last N releases (default: $RELEASES_TO_KEEP)\n\nExamples:\n  $SCRIPT_NAME staging\n  $SCRIPT_NAME prod --dry-run\n  $SCRIPT_NAME prod --rollback\nEOF\n}\n\n# ── Parse args ──\n[[ $# -ge 1 ]] || { usage; exit 1; }\nENV=\"$1\"; shift\ncase \"$ENV\" in\n    staging|prod) ;;\n    -h|--help) usage; exit 0 ;;\n    *) die \"invalid env: $ENV (use: staging, prod)\"; ;;\nesac\n\nwhile [[ $# -gt 0 ]]; do\n    case \"$1\" in\n        -n|--dry-run)  DRY_RUN=true; shift ;;\n        --rollback)   ROLLBACK=true; shift ;;\n        -k|--keep)    RELEASES_TO_KEEP=\"${2:?--keep requires N}\"; shift 2 ;;\n        -h|--help)    usage; exit 0 ;;\n        *)            die \"unknown option: $1\" ;;\n    esac\ndone\n\nreadonly ENV DRY_RUN ROLLBACK RELEASES_TO_KEEP\n\n# ── Dependencies ──\nrequire() { command -v \"$1\" &>\u002Fdev\u002Fnull || die \"missing: $1\"; }\nrequire rsync\nrequire ssh\nrequire curl\n\n# ── Run wrapper ──\nrun() {\n    if $DRY_RUN; then\n        log \"DRY RUN: $*\"\n    else\n        \"$@\"\n    fi\n}\n\n# ── Config lookup ──\nget_config() { echo \"${ENVS[${ENV}_$1]:-}\"; }\n\nreadonly HOST=$(get_config host)\nreadonly REMOTE_PATH=$(get_config path)\nreadonly SERVICE=$(get_config service)\n\n[[ -n \"$HOST\" ]] || die \"no host configured for $ENV\"\n[[ -n \"$REMOTE_PATH\" ]] || die \"no path configured for $ENV\"\n[[ -n \"$SERVICE\" ]] || die \"no service configured for $ENV\"\n\n# ── Pre-deploy checks ──\npre_check() {\n    log \"pre-deploy checks for $ENV\"\n\n    # For prod: require branch to be main\n    if [[ \"$ENV\" == \"prod\" ]]; then\n        local branch\n        branch=$(git rev-parse --abbrev-ref HEAD)\n        [[ \"$branch\" == \"main\" ]] || die \"prod deploy requires main branch (current: $branch)\"\n    fi\n\n    # Check for uncommitted changes\n    if ! $DRY_RUN; then\n        [[ -z \"$(git status --porcelain)\" ]] || die \"uncommitted changes — commit or stash first\"\n    fi\n\n    log \"pre-checks passed\"\n}\n\n# ── Build and test ──\nbuild_and_test() {\n    log \"building...\"\n\n    # Run tests in parallel with bats (if test dir exists)\n    if [[ -d test ]] && ls test\u002F*.bats &>\u002Fdev\u002Fnull; then\n        log \"running tests...\"\n        run bats test\u002F || die \"tests failed\"\n    fi\n\n    # Build (npm, make, etc.)\n    run npm run build || die \"build failed\"\n    log \"build complete\"\n}\n\n# ── Deploy ──\ndeploy() {\n    local ts=$(date +%Y%m%d_%H%M%S)\n    local release_dir=\"$REMOTE_PATH\u002Freleases\u002F$ts\"\n\n    log \"deploying to $HOST:$release_dir\"\n\n    # Create release directory on remote\n    run ssh \"$HOST\" \"mkdir -p '$release_dir'\"\n\n    # Rsync build output to the new release directory\n    log \"syncing files...\"\n    run rsync -az --delete dist\u002F \"$HOST:$release_dir\u002F\"\n\n    # Symlink current → new release (atomic switch)\n    log \"switching current symlink...\"\n    run ssh \"$HOST\" \"ln -sfn '$release_dir' '$REMOTE_PATH\u002Fcurrent'\"\n\n    # Restart the service\n    log \"restarting $SERVICE...\"\n    run ssh \"$HOST\" \"sudo systemctl restart '$SERVICE'\"\n\n    # Cleanup old releases\n    cleanup_old_releases\n\n    log \"deployed: $ts\"\n    echo \"$ts\"  # return the release timestamp\n}\n\n# ── Health check with retries ──\nhealth_check() {\n    local url=\"https:\u002F\u002F$HOST\u002Fhealth\"\n    local attempt=1\n\n    while ((attempt \u003C= HEALTH_RETRIES)); do\n        log \"health check $attempt\u002F$HEALTH_RETRIES: $url\"\n\n        if curl -sf --max-time 5 \"$url\" &>\u002Fdev\u002Fnull; then\n            log \"health check passed\"\n            return 0\n        fi\n\n        log \"health check failed (attempt $attempt)\"\n        ((attempt++))\n        sleep \"$HEALTH_DELAY\"\n    done\n\n    die \"health check failed after $HEALTH_RETRIES attempts\"\n}\n\n# ── Rollback ──\nrollback() {\n    log \"rolling back to previous release...\"\n\n    # Get the list of releases (sorted by name = by timestamp)\n    local releases\n    mapfile -t releases \u003C \u003C(ssh \"$HOST\" \"ls -1 '$REMOTE_PATH\u002Freleases\u002F' 2>\u002Fdev\u002Fnull | sort -r\" || true)\n\n    if (( ${#releases[@]} \u003C 2 )); then\n        die \"no previous release to rollback to (only ${#releases[@]} release(s))\"\n    fi\n\n    local previous=\"${releases[1]}\"  # second-newest\n    log \"rolling back to: $previous\"\n\n    run ssh \"$HOST\" \"ln -sfn '$REMOTE_PATH\u002Freleases\u002F$previous' '$REMOTE_PATH\u002Fcurrent'\"\n    run ssh \"$HOST\" \"sudo systemctl restart '$SERVICE'\"\n\n    log \"rollback complete (now on $previous)\"\n}\n\n# ── Cleanup old releases ──\ncleanup_old_releases() {\n    log \"cleaning up old releases (keeping $RELEASES_TO_KEEP)...\"\n\n    local releases\n    mapfile -t releases \u003C \u003C(ssh \"$HOST\" \"ls -1 '$REMOTE_PATH\u002Freleases\u002F' 2>\u002Fdev\u002Fnull | sort -r\" || true)\n\n    if (( ${#releases[@]} > RELEASES_TO_KEEP )); then\n        for old in \"${releases[@]:RELEASES_TO_KEEP}\"; do\n            log \"removing old release: $old\"\n            run ssh \"$HOST\" \"rm -rf '$REMOTE_PATH\u002Freleases\u002F$old'\"\n        done\n    fi\n}\n\n# ── Main ──\nmain() {\n    log \"=== $SCRIPT_NAME v$VERSION → $ENV ===\"\n\n    pre_check\n\n    if $ROLLBACK; then\n        rollback\n    else\n        build_and_test\n        deploy\n        health_check\n    fi\n\n    log \"=== deployment complete ===\"\n}\n\nmain \"$@\"\n",[30,4529,4530,4534,4539,4547,4551,4571,4583,4587,4592,4608,4622,4634,4646,4658,4670,4682,4686,4695,4703,4712,4721,4730,4734,4762,4784,4788,4794,4802,4812,4816,4825,4829,4834,4839,4843,4847,4852,4857,4862,4872,4876,4880,4887,4894,4901,4905,4909,4913,4917,4943,4960,4973,4987,5009,5026,5030,5034,5050,5062,5085,5105,5145,5165,5180,5184,5188,5192,5199,5203,5208,5244,5251,5258,5265,5269,5274,5280,5288,5298,5302,5310,5314,5318,5322,5327,5359,5363,5381,5399,5417,5421,5447,5473,5499,5503,5508,5515,5526,5530,5535,5556,5563,5585,5614,5618,5622,5627,5638,5667,5672,5677,5685,5690,5695,5701,5709,5717,5722,5728,5755,5763,5780,5785,5790,5796,5817,5825,5830,5835,5841,5849,5867,5889,5894,5911,5916,5922,5943,5948,5954,5962,5990,5995,6001,6009,6035,6040,6046,6059,6079,6084,6090,6096,6101,6113,6128,6133,6138,6144,6152,6170,6182,6187,6204,6228,6233,6263,6271,6280,6285,6290,6302,6312,6325,6330,6335,6349,6354,6359,6365,6373,6381,6386,6392,6400,6436,6441,6466,6486,6491,6496,6516,6529,6534,6561,6580,6585,6597,6602,6607,6613,6621,6633,6638,6645,6676,6681,6703,6732,6744,6769,6774,6779,6784,6789,6795,6803,6825,6830,6836,6841,6851,6857,6862,6868,6874,6880,6885,6890,6898,6903,6908],{"__ignoreMap":28},[33,4531,4532],{"class":35,"line":36},[33,4533,40],{"class":39},[33,4535,4536],{"class":35,"line":43},[33,4537,4538],{"class":39},"# ── Production deployment script: build, test, deploy, health check, rollback ──\n",[33,4540,4541,4543,4545],{"class":35,"line":49},[33,4542,53],{"class":52},[33,4544,56],{"class":52},[33,4546,60],{"class":59},[33,4548,4549],{"class":35,"line":63},[33,4550,67],{"emptyLinePlaceholder":66},[33,4552,4553,4555,4557,4559,4561,4563,4565,4567,4569],{"class":35,"line":70},[33,4554,74],{"class":73},[33,4556,78],{"class":77},[33,4558,81],{"class":73},[33,4560,84],{"class":77},[33,4562,88],{"class":87},[33,4564,91],{"class":59},[33,4566,94],{"class":77},[33,4568,97],{"class":59},[33,4570,100],{"class":77},[33,4572,4573,4575,4578,4580],{"class":35,"line":103},[33,4574,74],{"class":73},[33,4576,4577],{"class":77}," VERSION",[33,4579,81],{"class":73},[33,4581,4582],{"class":59},"\"2.0.0\"\n",[33,4584,4585],{"class":35,"line":123},[33,4586,67],{"emptyLinePlaceholder":66},[33,4588,4589],{"class":35,"line":128},[33,4590,4591],{"class":39},"# ── Config (per-environment) ──\n",[33,4593,4594,4597,4600,4603,4605],{"class":35,"line":134},[33,4595,4596],{"class":73},"declare",[33,4598,4599],{"class":52}," -A",[33,4601,4602],{"class":77}," ENVS",[33,4604,81],{"class":73},[33,4606,4607],{"class":77},"(\n",[33,4609,4610,4613,4616,4619],{"class":35,"line":145},[33,4611,4612],{"class":77},"    [",[33,4614,4615],{"class":87},"staging_host",[33,4617,4618],{"class":77},"]=",[33,4620,4621],{"class":59},"\"staging.example.com\"\n",[33,4623,4624,4626,4629,4631],{"class":35,"line":156},[33,4625,4612],{"class":77},[33,4627,4628],{"class":87},"staging_path",[33,4630,4618],{"class":77},[33,4632,4633],{"class":59},"\"\u002Fvar\u002Fwww\u002Fstaging\"\n",[33,4635,4636,4638,4641,4643],{"class":35,"line":167},[33,4637,4612],{"class":77},[33,4639,4640],{"class":87},"staging_service",[33,4642,4618],{"class":77},[33,4644,4645],{"class":59},"\"myapp-staging\"\n",[33,4647,4648,4650,4653,4655],{"class":35,"line":177},[33,4649,4612],{"class":77},[33,4651,4652],{"class":87},"prod_host",[33,4654,4618],{"class":77},[33,4656,4657],{"class":59},"\"prod.example.com\"\n",[33,4659,4660,4662,4665,4667],{"class":35,"line":182},[33,4661,4612],{"class":77},[33,4663,4664],{"class":87},"prod_path",[33,4666,4618],{"class":77},[33,4668,4669],{"class":59},"\"\u002Fvar\u002Fwww\u002Fprod\"\n",[33,4671,4672,4674,4677,4679],{"class":35,"line":191},[33,4673,4612],{"class":77},[33,4675,4676],{"class":87},"prod_service",[33,4678,4618],{"class":77},[33,4680,4681],{"class":59},"\"myapp-prod\"\n",[33,4683,4684],{"class":35,"line":203},[33,4685,100],{"class":77},[33,4687,4688,4691,4693],{"class":35,"line":215},[33,4689,4690],{"class":77},"RELEASES_TO_KEEP",[33,4692,81],{"class":73},[33,4694,142],{"class":59},[33,4696,4697,4699,4701],{"class":35,"line":220},[33,4698,148],{"class":77},[33,4700,81],{"class":73},[33,4702,153],{"class":59},[33,4704,4705,4708,4710],{"class":35,"line":226},[33,4706,4707],{"class":77},"ROLLBACK",[33,4709,81],{"class":73},[33,4711,153],{"class":59},[33,4713,4714,4717,4719],{"class":35,"line":232},[33,4715,4716],{"class":77},"HEALTH_RETRIES",[33,4718,81],{"class":73},[33,4720,142],{"class":59},[33,4722,4723,4726,4728],{"class":35,"line":238},[33,4724,4725],{"class":77},"HEALTH_DELAY",[33,4727,81],{"class":73},[33,4729,142],{"class":59},[33,4731,4732],{"class":35,"line":249},[33,4733,67],{"emptyLinePlaceholder":66},[33,4735,4736,4738,4740,4742,4744,4746,4748,4750,4752,4754,4756,4758,4760],{"class":35,"line":255},[33,4737,308],{"class":87},[33,4739,311],{"class":77},[33,4741,314],{"class":52},[33,4743,317],{"class":59},[33,4745,320],{"class":59},[33,4747,115],{"class":87},[33,4749,325],{"class":52},[33,4751,328],{"class":59},[33,4753,331],{"class":59},[33,4755,334],{"class":52},[33,4757,337],{"class":59},[33,4759,340],{"class":73},[33,4761,343],{"class":77},[33,4763,4764,4766,4768,4770,4772,4774,4776,4778,4780,4782],{"class":35,"line":261},[33,4765,349],{"class":87},[33,4767,311],{"class":77},[33,4769,308],{"class":87},[33,4771,356],{"class":59},[33,4773,334],{"class":52},[33,4775,337],{"class":59},[33,4777,363],{"class":77},[33,4779,366],{"class":52},[33,4781,369],{"class":52},[33,4783,343],{"class":77},[33,4785,4786],{"class":35,"line":266},[33,4787,67],{"emptyLinePlaceholder":66},[33,4789,4790,4792],{"class":35,"line":272},[33,4791,185],{"class":87},[33,4793,188],{"class":77},[33,4795,4796,4798,4800],{"class":35,"line":281},[33,4797,194],{"class":87},[33,4799,197],{"class":73},[33,4801,200],{"class":59},[33,4803,4804,4806,4809],{"class":35,"line":289},[33,4805,209],{"class":77},[33,4807,4808],{"class":59}," v",[33,4810,4811],{"class":77},"$VERSION\n",[33,4813,4814],{"class":35,"line":294},[33,4815,67],{"emptyLinePlaceholder":66},[33,4817,4818,4820,4822],{"class":35,"line":300},[33,4819,206],{"class":59},[33,4821,209],{"class":77},[33,4823,4824],{"class":59}," \u003Cenv> [OPTIONS]\n",[33,4826,4827],{"class":35,"line":305},[33,4828,67],{"emptyLinePlaceholder":66},[33,4830,4831],{"class":35,"line":346},[33,4832,4833],{"class":59},"Arguments:\n",[33,4835,4836],{"class":35,"line":374},[33,4837,4838],{"class":59},"  env         Target environment (staging, prod)\n",[33,4840,4841],{"class":35,"line":379},[33,4842,67],{"emptyLinePlaceholder":66},[33,4844,4845],{"class":35,"line":385},[33,4846,223],{"class":59},[33,4848,4849],{"class":35,"line":409},[33,4850,4851],{"class":59},"  -h, --help       Show this help\n",[33,4853,4854],{"class":35,"line":425},[33,4855,4856],{"class":59},"  -n, --dry-run    Show what would be done (no execution)\n",[33,4858,4859],{"class":35,"line":453},[33,4860,4861],{"class":59},"  --rollback       Rollback to previous release\n",[33,4863,4864,4867,4870],{"class":35,"line":481},[33,4865,4866],{"class":59},"  -k, --keep N     Keep last N releases (default: ",[33,4868,4869],{"class":77},"$RELEASES_TO_KEEP",[33,4871,100],{"class":59},[33,4873,4874],{"class":35,"line":532},[33,4875,67],{"emptyLinePlaceholder":66},[33,4877,4878],{"class":35,"line":558},[33,4879,269],{"class":59},[33,4881,4882,4884],{"class":35,"line":579},[33,4883,275],{"class":77},[33,4885,4886],{"class":59}," staging\n",[33,4888,4889,4891],{"class":35,"line":605},[33,4890,275],{"class":77},[33,4892,4893],{"class":59}," prod --dry-run\n",[33,4895,4896,4898],{"class":35,"line":628},[33,4897,275],{"class":77},[33,4899,4900],{"class":59}," prod --rollback\n",[33,4902,4903],{"class":35,"line":639},[33,4904,200],{"class":59},[33,4906,4907],{"class":35,"line":645},[33,4908,297],{"class":77},[33,4910,4911],{"class":35,"line":651},[33,4912,67],{"emptyLinePlaceholder":66},[33,4914,4915],{"class":35,"line":656},[33,4916,382],{"class":39},[33,4918,4919,4921,4923,4925,4927,4929,4931,4933,4935,4937,4939,4941],{"class":35,"line":688},[33,4920,659],{"class":77},[33,4922,394],{"class":52},[33,4924,664],{"class":73},[33,4926,369],{"class":52},[33,4928,669],{"class":77},[33,4930,672],{"class":73},[33,4932,675],{"class":77},[33,4934,185],{"class":87},[33,4936,363],{"class":77},[33,4938,366],{"class":52},[33,4940,369],{"class":52},[33,4942,343],{"class":77},[33,4944,4945,4948,4950,4952,4954,4956,4958],{"class":35,"line":705},[33,4946,4947],{"class":77},"ENV",[33,4949,81],{"class":73},[33,4951,337],{"class":59},[33,4953,417],{"class":52},[33,4955,337],{"class":59},[33,4957,363],{"class":77},[33,4959,1644],{"class":52},[33,4961,4962,4964,4966,4969,4971],{"class":35,"line":722},[33,4963,4449],{"class":73},[33,4965,331],{"class":59},[33,4967,4968],{"class":77},"$ENV",[33,4970,337],{"class":59},[33,4972,422],{"class":73},[33,4974,4975,4978,4980,4983,4985],{"class":35,"line":727},[33,4976,4977],{"class":428},"    staging",[33,4979,432],{"class":73},[33,4981,4982],{"class":428},"prod",[33,4984,438],{"class":73},[33,4986,450],{"class":77},[33,4988,4989,4992,4994,4996,4998,5001,5003,5005,5007],{"class":35,"line":733},[33,4990,4991],{"class":428},"    -h",[33,4993,432],{"class":73},[33,4995,435],{"class":428},[33,4997,438],{"class":73},[33,4999,5000],{"class":87}," usage",[33,5002,363],{"class":77},[33,5004,366],{"class":52},[33,5006,400],{"class":52},[33,5008,450],{"class":77},[33,5010,5011,5013,5015,5018,5020,5023],{"class":35,"line":762},[33,5012,4502],{"class":73},[33,5014,752],{"class":87},[33,5016,5017],{"class":59}," \"invalid env: ",[33,5019,4968],{"class":77},[33,5021,5022],{"class":59}," (use: staging, prod)\"",[33,5024,5025],{"class":77},"; ;;\n",[33,5027,5028],{"class":35,"line":804},[33,5029,4518],{"class":73},[33,5031,5032],{"class":35,"line":809},[33,5033,67],{"emptyLinePlaceholder":66},[33,5035,5036,5038,5040,5042,5044,5046,5048],{"class":35,"line":815},[33,5037,388],{"class":73},[33,5039,391],{"class":77},[33,5041,394],{"class":52},[33,5043,397],{"class":73},[33,5045,400],{"class":52},[33,5047,403],{"class":77},[33,5049,406],{"class":73},[33,5051,5052,5054,5056,5058,5060],{"class":35,"line":826},[33,5053,412],{"class":73},[33,5055,331],{"class":59},[33,5057,417],{"class":52},[33,5059,337],{"class":59},[33,5061,422],{"class":73},[33,5063,5064,5066,5068,5070,5072,5075,5077,5079,5081,5083],{"class":35,"line":834},[33,5065,456],{"class":428},[33,5067,432],{"class":73},[33,5069,461],{"class":428},[33,5071,438],{"class":73},[33,5073,5074],{"class":77},"  DRY_RUN",[33,5076,81],{"class":73},[33,5078,471],{"class":59},[33,5080,363],{"class":77},[33,5082,476],{"class":52},[33,5084,450],{"class":77},[33,5086,5087,5090,5092,5095,5097,5099,5101,5103],{"class":35,"line":848},[33,5088,5089],{"class":428},"        --rollback",[33,5091,438],{"class":73},[33,5093,5094],{"class":77},"   ROLLBACK",[33,5096,81],{"class":73},[33,5098,471],{"class":59},[33,5100,363],{"class":77},[33,5102,476],{"class":52},[33,5104,450],{"class":77},[33,5106,5107,5109,5111,5113,5115,5118,5120,5122,5124,5126,5128,5130,5133,5135,5137,5139,5141,5143],{"class":35,"line":890},[33,5108,484],{"class":428},[33,5110,432],{"class":73},[33,5112,489],{"class":428},[33,5114,438],{"class":73},[33,5116,5117],{"class":77},"    RELEASES_TO_KEEP",[33,5119,81],{"class":73},[33,5121,337],{"class":59},[33,5123,501],{"class":52},[33,5125,504],{"class":73},[33,5127,489],{"class":77},[33,5129,509],{"class":77},[33,5131,5132],{"class":77}," N",[33,5134,518],{"class":52},[33,5136,337],{"class":59},[33,5138,363],{"class":77},[33,5140,476],{"class":52},[33,5142,527],{"class":52},[33,5144,450],{"class":77},[33,5146,5147,5149,5151,5153,5155,5157,5159,5161,5163],{"class":35,"line":903},[33,5148,429],{"class":428},[33,5150,432],{"class":73},[33,5152,435],{"class":428},[33,5154,438],{"class":73},[33,5156,441],{"class":87},[33,5158,363],{"class":77},[33,5160,366],{"class":52},[33,5162,400],{"class":52},[33,5164,450],{"class":77},[33,5166,5167,5169,5172,5174,5176,5178],{"class":35,"line":908},[33,5168,631],{"class":73},[33,5170,5171],{"class":87},"            die",[33,5173,619],{"class":59},[33,5175,417],{"class":52},[33,5177,337],{"class":59},[33,5179,450],{"class":77},[33,5181,5182],{"class":35,"line":920},[33,5183,642],{"class":73},[33,5185,5186],{"class":35,"line":925},[33,5187,648],{"class":73},[33,5189,5190],{"class":35,"line":931},[33,5191,67],{"emptyLinePlaceholder":66},[33,5193,5194,5196],{"class":35,"line":939},[33,5195,74],{"class":73},[33,5197,5198],{"class":77}," ENV DRY_RUN ROLLBACK RELEASES_TO_KEEP\n",[33,5200,5201],{"class":35,"line":951},[33,5202,67],{"emptyLinePlaceholder":66},[33,5204,5205],{"class":35,"line":964},[33,5206,5207],{"class":39},"# ── Dependencies ──\n",[33,5209,5210,5213,5215,5217,5219,5221,5223,5225,5227,5229,5231,5233,5235,5238,5240,5242],{"class":35,"line":970},[33,5211,5212],{"class":87},"require",[33,5214,311],{"class":77},[33,5216,3196],{"class":52},[33,5218,1981],{"class":52},[33,5220,331],{"class":59},[33,5222,417],{"class":52},[33,5224,337],{"class":59},[33,5226,1282],{"class":77},[33,5228,1285],{"class":73},[33,5230,1288],{"class":77},[33,5232,672],{"class":73},[33,5234,752],{"class":87},[33,5236,5237],{"class":59}," \"missing: ",[33,5239,417],{"class":52},[33,5241,337],{"class":59},[33,5243,343],{"class":77},[33,5245,5246,5248],{"class":35,"line":985},[33,5247,5212],{"class":87},[33,5249,5250],{"class":59}," rsync\n",[33,5252,5253,5255],{"class":35,"line":996},[33,5254,5212],{"class":87},[33,5256,5257],{"class":59}," ssh\n",[33,5259,5260,5262],{"class":35,"line":1002},[33,5261,5212],{"class":87},[33,5263,5264],{"class":59}," curl\n",[33,5266,5267],{"class":35,"line":1007},[33,5268,67],{"emptyLinePlaceholder":66},[33,5270,5271],{"class":35,"line":1012},[33,5272,5273],{"class":39},"# ── Run wrapper ──\n",[33,5275,5276,5278],{"class":35,"line":1018},[33,5277,934],{"class":87},[33,5279,188],{"class":77},[33,5281,5282,5284,5286],{"class":35,"line":1064},[33,5283,942],{"class":73},[33,5285,945],{"class":77},[33,5287,948],{"class":73},[33,5289,5290,5292,5294,5296],{"class":35,"line":1083},[33,5291,954],{"class":87},[33,5293,957],{"class":59},[33,5295,334],{"class":52},[33,5297,702],{"class":59},[33,5299,5300],{"class":35,"line":1088},[33,5301,967],{"class":73},[33,5303,5304,5306,5308],{"class":35,"line":1099},[33,5305,988],{"class":87},[33,5307,991],{"class":52},[33,5309,702],{"class":87},[33,5311,5312],{"class":35,"line":1143},[33,5313,999],{"class":73},[33,5315,5316],{"class":35,"line":1149},[33,5317,297],{"class":77},[33,5319,5320],{"class":35,"line":1187},[33,5321,67],{"emptyLinePlaceholder":66},[33,5323,5324],{"class":35,"line":1193},[33,5325,5326],{"class":39},"# ── Config lookup ──\n",[33,5328,5329,5332,5334,5336,5338,5341,5344,5346,5349,5351,5353,5355,5357],{"class":35,"line":1198},[33,5330,5331],{"class":87},"get_config",[33,5333,311],{"class":77},[33,5335,1050],{"class":52},[33,5337,91],{"class":59},[33,5339,5340],{"class":77},"ENVS",[33,5342,5343],{"class":59},"[${",[33,5345,4947],{"class":77},[33,5347,5348],{"class":59},"}_",[33,5350,417],{"class":52},[33,5352,1440],{"class":59},[33,5354,1556],{"class":73},[33,5356,1448],{"class":59},[33,5358,343],{"class":77},[33,5360,5361],{"class":35,"line":1204},[33,5362,67],{"emptyLinePlaceholder":66},[33,5364,5365,5367,5370,5372,5374,5376,5379],{"class":35,"line":1216},[33,5366,74],{"class":73},[33,5368,5369],{"class":77}," HOST",[33,5371,81],{"class":73},[33,5373,84],{"class":77},[33,5375,5331],{"class":87},[33,5377,5378],{"class":59}," host",[33,5380,100],{"class":77},[33,5382,5383,5385,5388,5390,5392,5394,5397],{"class":35,"line":1225},[33,5384,74],{"class":73},[33,5386,5387],{"class":77}," REMOTE_PATH",[33,5389,81],{"class":73},[33,5391,84],{"class":77},[33,5393,5331],{"class":87},[33,5395,5396],{"class":59}," path",[33,5398,100],{"class":77},[33,5400,5401,5403,5406,5408,5410,5412,5415],{"class":35,"line":1234},[33,5402,74],{"class":73},[33,5404,5405],{"class":77}," SERVICE",[33,5407,81],{"class":73},[33,5409,84],{"class":77},[33,5411,5331],{"class":87},[33,5413,5414],{"class":59}," service",[33,5416,100],{"class":77},[33,5418,5419],{"class":35,"line":1262},[33,5420,67],{"emptyLinePlaceholder":66},[33,5422,5423,5425,5427,5429,5432,5434,5436,5438,5440,5443,5445],{"class":35,"line":1267},[33,5424,659],{"class":77},[33,5426,854],{"class":73},[33,5428,331],{"class":59},[33,5430,5431],{"class":77},"$HOST",[33,5433,337],{"class":59},[33,5435,669],{"class":77},[33,5437,672],{"class":73},[33,5439,752],{"class":87},[33,5441,5442],{"class":59}," \"no host configured for ",[33,5444,4968],{"class":77},[33,5446,702],{"class":59},[33,5448,5449,5451,5453,5455,5458,5460,5462,5464,5466,5469,5471],{"class":35,"line":1298},[33,5450,659],{"class":77},[33,5452,854],{"class":73},[33,5454,331],{"class":59},[33,5456,5457],{"class":77},"$REMOTE_PATH",[33,5459,337],{"class":59},[33,5461,669],{"class":77},[33,5463,672],{"class":73},[33,5465,752],{"class":87},[33,5467,5468],{"class":59}," \"no path configured for ",[33,5470,4968],{"class":77},[33,5472,702],{"class":59},[33,5474,5475,5477,5479,5481,5484,5486,5488,5490,5492,5495,5497],{"class":35,"line":1303},[33,5476,659],{"class":77},[33,5478,854],{"class":73},[33,5480,331],{"class":59},[33,5482,5483],{"class":77},"$SERVICE",[33,5485,337],{"class":59},[33,5487,669],{"class":77},[33,5489,672],{"class":73},[33,5491,752],{"class":87},[33,5493,5494],{"class":59}," \"no service configured for ",[33,5496,4968],{"class":77},[33,5498,702],{"class":59},[33,5500,5501],{"class":35,"line":1311},[33,5502,67],{"emptyLinePlaceholder":66},[33,5504,5505],{"class":35,"line":1316},[33,5506,5507],{"class":39},"# ── Pre-deploy checks ──\n",[33,5509,5510,5513],{"class":35,"line":1321},[33,5511,5512],{"class":87},"pre_check",[33,5514,188],{"class":77},[33,5516,5517,5519,5522,5524],{"class":35,"line":1327},[33,5518,1219],{"class":87},[33,5520,5521],{"class":59}," \"pre-deploy checks for ",[33,5523,4968],{"class":77},[33,5525,702],{"class":59},[33,5527,5528],{"class":35,"line":1340},[33,5529,67],{"emptyLinePlaceholder":66},[33,5531,5532],{"class":35,"line":1391},[33,5533,5534],{"class":39},"    # For prod: require branch to be main\n",[33,5536,5537,5539,5541,5543,5545,5547,5549,5552,5554],{"class":35,"line":1418},[33,5538,942],{"class":73},[33,5540,391],{"class":77},[33,5542,337],{"class":59},[33,5544,4968],{"class":77},[33,5546,337],{"class":59},[33,5548,1042],{"class":73},[33,5550,5551],{"class":59}," \"prod\"",[33,5553,403],{"class":77},[33,5555,948],{"class":73},[33,5557,5558,5560],{"class":35,"line":1455},[33,5559,3843],{"class":73},[33,5561,5562],{"class":77}," branch\n",[33,5564,5565,5568,5570,5572,5574,5577,5580,5583],{"class":35,"line":1468},[33,5566,5567],{"class":77},"        branch",[33,5569,81],{"class":73},[33,5571,84],{"class":77},[33,5573,2132],{"class":87},[33,5575,5576],{"class":59}," rev-parse",[33,5578,5579],{"class":52}," --abbrev-ref",[33,5581,5582],{"class":59}," HEAD",[33,5584,100],{"class":77},[33,5586,5587,5589,5591,5594,5596,5598,5601,5603,5605,5607,5610,5612],{"class":35,"line":1485},[33,5588,3890],{"class":77},[33,5590,337],{"class":59},[33,5592,5593],{"class":77},"$branch",[33,5595,337],{"class":59},[33,5597,1042],{"class":73},[33,5599,5600],{"class":59}," \"main\"",[33,5602,669],{"class":77},[33,5604,672],{"class":73},[33,5606,752],{"class":87},[33,5608,5609],{"class":59}," \"prod deploy requires main branch (current: ",[33,5611,5593],{"class":77},[33,5613,2960],{"class":59},[33,5615,5616],{"class":35,"line":1491},[33,5617,999],{"class":73},[33,5619,5620],{"class":35,"line":1496},[33,5621,67],{"emptyLinePlaceholder":66},[33,5623,5624],{"class":35,"line":1501},[33,5625,5626],{"class":39},"    # Check for uncommitted changes\n",[33,5628,5630,5632,5634,5636],{"class":35,"line":5629},107,[33,5631,942],{"class":73},[33,5633,1209],{"class":73},[33,5635,945],{"class":77},[33,5637,948],{"class":73},[33,5639,5641,5643,5646,5648,5650,5653,5656,5658,5660,5662,5664],{"class":35,"line":5640},108,[33,5642,3890],{"class":77},[33,5644,5645],{"class":73},"-z",[33,5647,320],{"class":59},[33,5649,2132],{"class":87},[33,5651,5652],{"class":59}," status ",[33,5654,5655],{"class":52},"--porcelain",[33,5657,328],{"class":59},[33,5659,669],{"class":77},[33,5661,672],{"class":73},[33,5663,752],{"class":87},[33,5665,5666],{"class":59}," \"uncommitted changes — commit or stash first\"\n",[33,5668,5670],{"class":35,"line":5669},109,[33,5671,999],{"class":73},[33,5673,5675],{"class":35,"line":5674},110,[33,5676,67],{"emptyLinePlaceholder":66},[33,5678,5680,5682],{"class":35,"line":5679},111,[33,5681,1219],{"class":87},[33,5683,5684],{"class":59}," \"pre-checks passed\"\n",[33,5686,5688],{"class":35,"line":5687},112,[33,5689,297],{"class":77},[33,5691,5693],{"class":35,"line":5692},113,[33,5694,67],{"emptyLinePlaceholder":66},[33,5696,5698],{"class":35,"line":5697},114,[33,5699,5700],{"class":39},"# ── Build and test ──\n",[33,5702,5704,5707],{"class":35,"line":5703},115,[33,5705,5706],{"class":87},"build_and_test",[33,5708,188],{"class":77},[33,5710,5712,5714],{"class":35,"line":5711},116,[33,5713,1219],{"class":87},[33,5715,5716],{"class":59}," \"building...\"\n",[33,5718,5720],{"class":35,"line":5719},117,[33,5721,67],{"emptyLinePlaceholder":66},[33,5723,5725],{"class":35,"line":5724},118,[33,5726,5727],{"class":39},"    # Run tests in parallel with bats (if test dir exists)\n",[33,5729,5731,5733,5735,5737,5739,5741,5743,5745,5747,5749,5751,5753],{"class":35,"line":5730},119,[33,5732,942],{"class":73},[33,5734,391],{"class":77},[33,5736,738],{"class":73},[33,5738,2267],{"class":77},[33,5740,1357],{"class":87},[33,5742,2272],{"class":59},[33,5744,611],{"class":52},[33,5746,2277],{"class":59},[33,5748,1282],{"class":77},[33,5750,1285],{"class":73},[33,5752,2284],{"class":77},[33,5754,948],{"class":73},[33,5756,5758,5760],{"class":35,"line":5757},120,[33,5759,954],{"class":87},[33,5761,5762],{"class":59}," \"running tests...\"\n",[33,5764,5766,5768,5771,5773,5775,5777],{"class":35,"line":5765},121,[33,5767,1471],{"class":87},[33,5769,5770],{"class":59}," bats",[33,5772,2272],{"class":59},[33,5774,792],{"class":73},[33,5776,752],{"class":87},[33,5778,5779],{"class":59}," \"tests failed\"\n",[33,5781,5783],{"class":35,"line":5782},122,[33,5784,999],{"class":73},[33,5786,5788],{"class":35,"line":5787},123,[33,5789,67],{"emptyLinePlaceholder":66},[33,5791,5793],{"class":35,"line":5792},124,[33,5794,5795],{"class":39},"    # Build (npm, make, etc.)\n",[33,5797,5799,5801,5804,5807,5810,5812,5814],{"class":35,"line":5798},125,[33,5800,1102],{"class":87},[33,5802,5803],{"class":59}," npm",[33,5805,5806],{"class":59}," run",[33,5808,5809],{"class":59}," build",[33,5811,792],{"class":73},[33,5813,752],{"class":87},[33,5815,5816],{"class":59}," \"build failed\"\n",[33,5818,5820,5822],{"class":35,"line":5819},126,[33,5821,1219],{"class":87},[33,5823,5824],{"class":59}," \"build complete\"\n",[33,5826,5828],{"class":35,"line":5827},127,[33,5829,297],{"class":77},[33,5831,5833],{"class":35,"line":5832},128,[33,5834,67],{"emptyLinePlaceholder":66},[33,5836,5838],{"class":35,"line":5837},129,[33,5839,5840],{"class":39},"# ── Deploy ──\n",[33,5842,5844,5847],{"class":35,"line":5843},130,[33,5845,5846],{"class":87},"deploy",[33,5848,188],{"class":77},[33,5850,5852,5854,5857,5859,5861,5863,5865],{"class":35,"line":5851},131,[33,5853,837],{"class":73},[33,5855,5856],{"class":77}," ts",[33,5858,81],{"class":73},[33,5860,84],{"class":77},[33,5862,115],{"class":87},[33,5864,118],{"class":59},[33,5866,100],{"class":77},[33,5868,5870,5872,5875,5877,5879,5881,5884,5887],{"class":35,"line":5869},132,[33,5871,837],{"class":73},[33,5873,5874],{"class":77}," release_dir",[33,5876,81],{"class":73},[33,5878,337],{"class":59},[33,5880,5457],{"class":77},[33,5882,5883],{"class":59},"\u002Freleases\u002F",[33,5885,5886],{"class":77},"$ts",[33,5888,702],{"class":59},[33,5890,5892],{"class":35,"line":5891},133,[33,5893,67],{"emptyLinePlaceholder":66},[33,5895,5897,5899,5902,5904,5906,5909],{"class":35,"line":5896},134,[33,5898,1219],{"class":87},[33,5900,5901],{"class":59}," \"deploying to ",[33,5903,5431],{"class":77},[33,5905,1443],{"class":59},[33,5907,5908],{"class":77},"$release_dir",[33,5910,702],{"class":59},[33,5912,5914],{"class":35,"line":5913},135,[33,5915,67],{"emptyLinePlaceholder":66},[33,5917,5919],{"class":35,"line":5918},136,[33,5920,5921],{"class":39},"    # Create release directory on remote\n",[33,5923,5925,5927,5930,5932,5934,5936,5939,5941],{"class":35,"line":5924},137,[33,5926,1102],{"class":87},[33,5928,5929],{"class":59}," ssh",[33,5931,331],{"class":59},[33,5933,5431],{"class":77},[33,5935,337],{"class":59},[33,5937,5938],{"class":59}," \"mkdir -p '",[33,5940,5908],{"class":77},[33,5942,1080],{"class":59},[33,5944,5946],{"class":35,"line":5945},138,[33,5947,67],{"emptyLinePlaceholder":66},[33,5949,5951],{"class":35,"line":5950},139,[33,5952,5953],{"class":39},"    # Rsync build output to the new release directory\n",[33,5955,5957,5959],{"class":35,"line":5956},140,[33,5958,1219],{"class":87},[33,5960,5961],{"class":59}," \"syncing files...\"\n",[33,5963,5965,5967,5970,5973,5976,5979,5981,5983,5985,5987],{"class":35,"line":5964},141,[33,5966,1102],{"class":87},[33,5968,5969],{"class":59}," rsync",[33,5971,5972],{"class":52}," -az",[33,5974,5975],{"class":52}," --delete",[33,5977,5978],{"class":59}," dist\u002F",[33,5980,331],{"class":59},[33,5982,5431],{"class":77},[33,5984,1443],{"class":59},[33,5986,5908],{"class":77},[33,5988,5989],{"class":59},"\u002F\"\n",[33,5991,5993],{"class":35,"line":5992},142,[33,5994,67],{"emptyLinePlaceholder":66},[33,5996,5998],{"class":35,"line":5997},143,[33,5999,6000],{"class":39},"    # Symlink current → new release (atomic switch)\n",[33,6002,6004,6006],{"class":35,"line":6003},144,[33,6005,1219],{"class":87},[33,6007,6008],{"class":59}," \"switching current symlink...\"\n",[33,6010,6012,6014,6016,6018,6020,6022,6025,6027,6030,6032],{"class":35,"line":6011},145,[33,6013,1102],{"class":87},[33,6015,5929],{"class":59},[33,6017,331],{"class":59},[33,6019,5431],{"class":77},[33,6021,337],{"class":59},[33,6023,6024],{"class":59}," \"ln -sfn '",[33,6026,5908],{"class":77},[33,6028,6029],{"class":59},"' '",[33,6031,5457],{"class":77},[33,6033,6034],{"class":59},"\u002Fcurrent'\"\n",[33,6036,6038],{"class":35,"line":6037},146,[33,6039,67],{"emptyLinePlaceholder":66},[33,6041,6043],{"class":35,"line":6042},147,[33,6044,6045],{"class":39},"    # Restart the service\n",[33,6047,6049,6051,6054,6056],{"class":35,"line":6048},148,[33,6050,1219],{"class":87},[33,6052,6053],{"class":59}," \"restarting ",[33,6055,5483],{"class":77},[33,6057,6058],{"class":59},"...\"\n",[33,6060,6062,6064,6066,6068,6070,6072,6075,6077],{"class":35,"line":6061},149,[33,6063,1102],{"class":87},[33,6065,5929],{"class":59},[33,6067,331],{"class":59},[33,6069,5431],{"class":77},[33,6071,337],{"class":59},[33,6073,6074],{"class":59}," \"sudo systemctl restart '",[33,6076,5483],{"class":77},[33,6078,1080],{"class":59},[33,6080,6082],{"class":35,"line":6081},150,[33,6083,67],{"emptyLinePlaceholder":66},[33,6085,6087],{"class":35,"line":6086},151,[33,6088,6089],{"class":39},"    # Cleanup old releases\n",[33,6091,6093],{"class":35,"line":6092},152,[33,6094,6095],{"class":87},"    cleanup_old_releases\n",[33,6097,6099],{"class":35,"line":6098},153,[33,6100,67],{"emptyLinePlaceholder":66},[33,6102,6104,6106,6109,6111],{"class":35,"line":6103},154,[33,6105,1219],{"class":87},[33,6107,6108],{"class":59}," \"deployed: ",[33,6110,5886],{"class":77},[33,6112,702],{"class":59},[33,6114,6116,6119,6121,6123,6125],{"class":35,"line":6115},155,[33,6117,6118],{"class":52},"    echo",[33,6120,331],{"class":59},[33,6122,5886],{"class":77},[33,6124,337],{"class":59},[33,6126,6127],{"class":39},"  # return the release timestamp\n",[33,6129,6131],{"class":35,"line":6130},156,[33,6132,297],{"class":77},[33,6134,6136],{"class":35,"line":6135},157,[33,6137,67],{"emptyLinePlaceholder":66},[33,6139,6141],{"class":35,"line":6140},158,[33,6142,6143],{"class":39},"# ── Health check with retries ──\n",[33,6145,6147,6150],{"class":35,"line":6146},159,[33,6148,6149],{"class":87},"health_check",[33,6151,188],{"class":77},[33,6153,6155,6157,6160,6162,6165,6167],{"class":35,"line":6154},160,[33,6156,837],{"class":73},[33,6158,6159],{"class":77}," url",[33,6161,81],{"class":73},[33,6163,6164],{"class":59},"\"https:\u002F\u002F",[33,6166,5431],{"class":77},[33,6168,6169],{"class":59},"\u002Fhealth\"\n",[33,6171,6173,6175,6178,6180],{"class":35,"line":6172},161,[33,6174,837],{"class":73},[33,6176,6177],{"class":77}," attempt",[33,6179,81],{"class":73},[33,6181,2583],{"class":52},[33,6183,6185],{"class":35,"line":6184},162,[33,6186,67],{"emptyLinePlaceholder":66},[33,6188,6190,6193,6196,6199,6202],{"class":35,"line":6189},163,[33,6191,6192],{"class":73},"    while",[33,6194,6195],{"class":77}," ((attempt ",[33,6197,6198],{"class":73},"\u003C=",[33,6200,6201],{"class":77}," HEALTH_RETRIES)); ",[33,6203,406],{"class":73},[33,6205,6207,6209,6212,6215,6217,6220,6223,6226],{"class":35,"line":6206},164,[33,6208,954],{"class":87},[33,6210,6211],{"class":59}," \"health check ",[33,6213,6214],{"class":77},"$attempt",[33,6216,3856],{"class":59},[33,6218,6219],{"class":77},"$HEALTH_RETRIES",[33,6221,6222],{"class":59},": ",[33,6224,6225],{"class":77},"$url",[33,6227,702],{"class":59},[33,6229,6231],{"class":35,"line":6230},165,[33,6232,67],{"emptyLinePlaceholder":66},[33,6234,6236,6238,6241,6243,6246,6249,6251,6253,6255,6257,6259,6261],{"class":35,"line":6235},166,[33,6237,3923],{"class":73},[33,6239,6240],{"class":87}," curl",[33,6242,4031],{"class":52},[33,6244,6245],{"class":52}," --max-time",[33,6247,6248],{"class":52}," 5",[33,6250,331],{"class":59},[33,6252,6225],{"class":77},[33,6254,337],{"class":59},[33,6256,1282],{"class":77},[33,6258,1285],{"class":73},[33,6260,2284],{"class":77},[33,6262,948],{"class":73},[33,6264,6266,6268],{"class":35,"line":6265},167,[33,6267,3997],{"class":87},[33,6269,6270],{"class":59}," \"health check passed\"\n",[33,6272,6274,6277],{"class":35,"line":6273},168,[33,6275,6276],{"class":73},"            return",[33,6278,6279],{"class":52}," 0\n",[33,6281,6283],{"class":35,"line":6282},169,[33,6284,4014],{"class":73},[33,6286,6288],{"class":35,"line":6287},170,[33,6289,67],{"emptyLinePlaceholder":66},[33,6291,6293,6295,6298,6300],{"class":35,"line":6292},171,[33,6294,954],{"class":87},[33,6296,6297],{"class":59}," \"health check failed (attempt ",[33,6299,6214],{"class":77},[33,6301,2960],{"class":59},[33,6303,6305,6308,6310],{"class":35,"line":6304},172,[33,6306,6307],{"class":77},"        ((attempt",[33,6309,2977],{"class":73},[33,6311,2980],{"class":77},[33,6313,6315,6318,6320,6323],{"class":35,"line":6314},173,[33,6316,6317],{"class":87},"        sleep",[33,6319,331],{"class":59},[33,6321,6322],{"class":77},"$HEALTH_DELAY",[33,6324,702],{"class":59},[33,6326,6328],{"class":35,"line":6327},174,[33,6329,1488],{"class":73},[33,6331,6333],{"class":35,"line":6332},175,[33,6334,67],{"emptyLinePlaceholder":66},[33,6336,6338,6341,6344,6346],{"class":35,"line":6337},176,[33,6339,6340],{"class":87},"    die",[33,6342,6343],{"class":59}," \"health check failed after ",[33,6345,6219],{"class":77},[33,6347,6348],{"class":59}," attempts\"\n",[33,6350,6352],{"class":35,"line":6351},177,[33,6353,297],{"class":77},[33,6355,6357],{"class":35,"line":6356},178,[33,6358,67],{"emptyLinePlaceholder":66},[33,6360,6362],{"class":35,"line":6361},179,[33,6363,6364],{"class":39},"# ── Rollback ──\n",[33,6366,6368,6371],{"class":35,"line":6367},180,[33,6369,6370],{"class":87},"rollback",[33,6372,188],{"class":77},[33,6374,6376,6378],{"class":35,"line":6375},181,[33,6377,1219],{"class":87},[33,6379,6380],{"class":59}," \"rolling back to previous release...\"\n",[33,6382,6384],{"class":35,"line":6383},182,[33,6385,67],{"emptyLinePlaceholder":66},[33,6387,6389],{"class":35,"line":6388},183,[33,6390,6391],{"class":39},"    # Get the list of releases (sorted by name = by timestamp)\n",[33,6393,6395,6397],{"class":35,"line":6394},184,[33,6396,837],{"class":73},[33,6398,6399],{"class":77}," releases\n",[33,6401,6403,6406,6408,6411,6413,6415,6418,6420,6422,6425,6427,6430,6432,6434],{"class":35,"line":6402},185,[33,6404,6405],{"class":87},"    mapfile",[33,6407,1240],{"class":52},[33,6409,6410],{"class":59}," releases",[33,6412,1351],{"class":73},[33,6414,1354],{"class":59},[33,6416,6417],{"class":87},"ssh",[33,6419,331],{"class":59},[33,6421,5431],{"class":77},[33,6423,6424],{"class":59},"\" \"ls -1 '",[33,6426,5457],{"class":77},[33,6428,6429],{"class":59},"\u002Freleases\u002F' 2>\u002Fdev\u002Fnull | sort -r\" ",[33,6431,672],{"class":73},[33,6433,1045],{"class":52},[33,6435,100],{"class":59},[33,6437,6439],{"class":35,"line":6438},186,[33,6440,67],{"emptyLinePlaceholder":66},[33,6442,6444,6446,6448,6450,6453,6455,6457,6460,6462,6464],{"class":35,"line":6443},187,[33,6445,942],{"class":73},[33,6447,1396],{"class":77},[33,6449,1399],{"class":73},[33,6451,6452],{"class":77},"releases[",[33,6454,1405],{"class":73},[33,6456,1408],{"class":77},[33,6458,6459],{"class":73},"\u003C",[33,6461,527],{"class":52},[33,6463,2184],{"class":77},[33,6465,948],{"class":73},[33,6467,6469,6471,6474,6476,6479,6481,6483],{"class":35,"line":6468},188,[33,6470,3009],{"class":87},[33,6472,6473],{"class":59}," \"no previous release to rollback to (only ${",[33,6475,1399],{"class":73},[33,6477,6478],{"class":77},"releases",[33,6480,1435],{"class":59},[33,6482,1405],{"class":73},[33,6484,6485],{"class":59},"]} release(s))\"\n",[33,6487,6489],{"class":35,"line":6488},189,[33,6490,999],{"class":73},[33,6492,6494],{"class":35,"line":6493},190,[33,6495,67],{"emptyLinePlaceholder":66},[33,6497,6499,6501,6504,6506,6508,6510,6513],{"class":35,"line":6498},191,[33,6500,837],{"class":73},[33,6502,6503],{"class":77}," previous",[33,6505,81],{"class":73},[33,6507,3311],{"class":59},[33,6509,6478],{"class":77},[33,6511,6512],{"class":59},"[1]}\"",[33,6514,6515],{"class":39},"  # second-newest\n",[33,6517,6519,6521,6524,6527],{"class":35,"line":6518},192,[33,6520,1219],{"class":87},[33,6522,6523],{"class":59}," \"rolling back to: ",[33,6525,6526],{"class":77},"$previous",[33,6528,702],{"class":59},[33,6530,6532],{"class":35,"line":6531},193,[33,6533,67],{"emptyLinePlaceholder":66},[33,6535,6537,6539,6541,6543,6545,6547,6549,6551,6553,6555,6557,6559],{"class":35,"line":6536},194,[33,6538,1102],{"class":87},[33,6540,5929],{"class":59},[33,6542,331],{"class":59},[33,6544,5431],{"class":77},[33,6546,337],{"class":59},[33,6548,6024],{"class":59},[33,6550,5457],{"class":77},[33,6552,5883],{"class":59},[33,6554,6526],{"class":77},[33,6556,6029],{"class":59},[33,6558,5457],{"class":77},[33,6560,6034],{"class":59},[33,6562,6564,6566,6568,6570,6572,6574,6576,6578],{"class":35,"line":6563},195,[33,6565,1102],{"class":87},[33,6567,5929],{"class":59},[33,6569,331],{"class":59},[33,6571,5431],{"class":77},[33,6573,337],{"class":59},[33,6575,6074],{"class":59},[33,6577,5483],{"class":77},[33,6579,1080],{"class":59},[33,6581,6583],{"class":35,"line":6582},196,[33,6584,67],{"emptyLinePlaceholder":66},[33,6586,6588,6590,6593,6595],{"class":35,"line":6587},197,[33,6589,1219],{"class":87},[33,6591,6592],{"class":59}," \"rollback complete (now on ",[33,6594,6526],{"class":77},[33,6596,2960],{"class":59},[33,6598,6600],{"class":35,"line":6599},198,[33,6601,297],{"class":77},[33,6603,6605],{"class":35,"line":6604},199,[33,6606,67],{"emptyLinePlaceholder":66},[33,6608,6610],{"class":35,"line":6609},200,[33,6611,6612],{"class":39},"# ── Cleanup old releases ──\n",[33,6614,6616,6619],{"class":35,"line":6615},201,[33,6617,6618],{"class":87},"cleanup_old_releases",[33,6620,188],{"class":77},[33,6622,6624,6626,6629,6631],{"class":35,"line":6623},202,[33,6625,1219],{"class":87},[33,6627,6628],{"class":59}," \"cleaning up old releases (keeping ",[33,6630,4869],{"class":77},[33,6632,2682],{"class":59},[33,6634,6636],{"class":35,"line":6635},203,[33,6637,67],{"emptyLinePlaceholder":66},[33,6639,6641,6643],{"class":35,"line":6640},204,[33,6642,837],{"class":73},[33,6644,6399],{"class":77},[33,6646,6648,6650,6652,6654,6656,6658,6660,6662,6664,6666,6668,6670,6672,6674],{"class":35,"line":6647},205,[33,6649,6405],{"class":87},[33,6651,1240],{"class":52},[33,6653,6410],{"class":59},[33,6655,1351],{"class":73},[33,6657,1354],{"class":59},[33,6659,6417],{"class":87},[33,6661,331],{"class":59},[33,6663,5431],{"class":77},[33,6665,6424],{"class":59},[33,6667,5457],{"class":77},[33,6669,6429],{"class":59},[33,6671,672],{"class":73},[33,6673,1045],{"class":52},[33,6675,100],{"class":59},[33,6677,6679],{"class":35,"line":6678},206,[33,6680,67],{"emptyLinePlaceholder":66},[33,6682,6684,6686,6688,6690,6692,6694,6696,6698,6701],{"class":35,"line":6683},207,[33,6685,942],{"class":73},[33,6687,1396],{"class":77},[33,6689,1399],{"class":73},[33,6691,6452],{"class":77},[33,6693,1405],{"class":73},[33,6695,1408],{"class":77},[33,6697,1285],{"class":73},[33,6699,6700],{"class":77}," RELEASES_TO_KEEP )); ",[33,6702,948],{"class":73},[33,6704,6706,6708,6710,6712,6714,6716,6718,6720,6722,6724,6726,6728,6730],{"class":35,"line":6705},208,[33,6707,2711],{"class":73},[33,6709,1424],{"class":77},[33,6711,1427],{"class":73},[33,6713,91],{"class":59},[33,6715,6478],{"class":77},[33,6717,1435],{"class":59},[33,6719,1405],{"class":73},[33,6721,1440],{"class":59},[33,6723,1443],{"class":73},[33,6725,4690],{"class":77},[33,6727,1448],{"class":59},[33,6729,363],{"class":77},[33,6731,406],{"class":73},[33,6733,6735,6737,6740,6742],{"class":35,"line":6734},209,[33,6736,3997],{"class":87},[33,6738,6739],{"class":59}," \"removing old release: ",[33,6741,1463],{"class":77},[33,6743,702],{"class":59},[33,6745,6747,6750,6752,6754,6756,6758,6761,6763,6765,6767],{"class":35,"line":6746},210,[33,6748,6749],{"class":87},"            run",[33,6751,5929],{"class":59},[33,6753,331],{"class":59},[33,6755,5431],{"class":77},[33,6757,337],{"class":59},[33,6759,6760],{"class":59}," \"rm -rf '",[33,6762,5457],{"class":77},[33,6764,5883],{"class":59},[33,6766,1463],{"class":77},[33,6768,1080],{"class":59},[33,6770,6772],{"class":35,"line":6771},211,[33,6773,2761],{"class":73},[33,6775,6777],{"class":35,"line":6776},212,[33,6778,999],{"class":73},[33,6780,6782],{"class":35,"line":6781},213,[33,6783,297],{"class":77},[33,6785,6787],{"class":35,"line":6786},214,[33,6788,67],{"emptyLinePlaceholder":66},[33,6790,6792],{"class":35,"line":6791},215,[33,6793,6794],{"class":39},"# ── Main ──\n",[33,6796,6798,6801],{"class":35,"line":6797},216,[33,6799,6800],{"class":87},"main",[33,6802,188],{"class":77},[33,6804,6806,6808,6811,6813,6815,6818,6820,6822],{"class":35,"line":6805},217,[33,6807,1219],{"class":87},[33,6809,6810],{"class":59}," \"=== ",[33,6812,209],{"class":77},[33,6814,4808],{"class":59},[33,6816,6817],{"class":77},"$VERSION",[33,6819,4005],{"class":59},[33,6821,4968],{"class":77},[33,6823,6824],{"class":59}," ===\"\n",[33,6826,6828],{"class":35,"line":6827},218,[33,6829,67],{"emptyLinePlaceholder":66},[33,6831,6833],{"class":35,"line":6832},219,[33,6834,6835],{"class":87},"    pre_check\n",[33,6837,6839],{"class":35,"line":6838},220,[33,6840,67],{"emptyLinePlaceholder":66},[33,6842,6844,6846,6849],{"class":35,"line":6843},221,[33,6845,942],{"class":73},[33,6847,6848],{"class":77}," $ROLLBACK; ",[33,6850,948],{"class":73},[33,6852,6854],{"class":35,"line":6853},222,[33,6855,6856],{"class":87},"        rollback\n",[33,6858,6860],{"class":35,"line":6859},223,[33,6861,967],{"class":73},[33,6863,6865],{"class":35,"line":6864},224,[33,6866,6867],{"class":87},"        build_and_test\n",[33,6869,6871],{"class":35,"line":6870},225,[33,6872,6873],{"class":87},"        deploy\n",[33,6875,6877],{"class":35,"line":6876},226,[33,6878,6879],{"class":87},"        health_check\n",[33,6881,6883],{"class":35,"line":6882},227,[33,6884,999],{"class":73},[33,6886,6888],{"class":35,"line":6887},228,[33,6889,67],{"emptyLinePlaceholder":66},[33,6891,6893,6895],{"class":35,"line":6892},229,[33,6894,1219],{"class":87},[33,6896,6897],{"class":59}," \"=== deployment complete ===\"\n",[33,6899,6901],{"class":35,"line":6900},230,[33,6902,297],{"class":77},[33,6904,6906],{"class":35,"line":6905},231,[33,6907,67],{"emptyLinePlaceholder":66},[33,6909,6911,6913,6915,6917],{"class":35,"line":6910},232,[33,6912,6800],{"class":87},[33,6914,331],{"class":59},[33,6916,991],{"class":52},[33,6918,702],{"class":59},[15,6920,6922],{"id":6921},"production-checklist","Production Checklist",[20,6924,6926],{"language":6925},"markdown",[24,6927,6930],{"className":6928,"code":6929,"language":6925,"meta":28,"style":28},"language-markdown shiki shiki-themes github-light github-dark","- [ ] `#!\u002Fusr\u002Fbin\u002Fenv bash` shebang (portable)\n- [ ] `set -Eeuo pipefail` strict mode (-E for ERR trap inheritance)\n- [ ] `trap cleanup EXIT` for temp file cleanup\n- [ ] `trap err_handler ERR` for error logging\n- [ ] All variables quoted: `\"$var\"`, `\"${arr[@]}\"`\n- [ ] `local` for all function variables\n- [ ] `readonly` for constants\n- [ ] `main \"$@\"` entry point (functions defined before call)\n- [ ] `usage` function and arg validation\n- [ ] Errors to stderr (`>&2`), data to stdout\n- [ ] Meaningful exit codes (0=success, 1=failure, 2=usage)\n- [ ] `mktemp` for temp files (not predictable names)\n- [ ] Idempotent operations (`mkdir -p`, `[[ -e ]] || create`)\n- [ ] No `eval` on untrusted input\n- [ ] `find -print0 | while IFS= read -r -d ''` for filenames\n- [ ] ShellCheck clean (`shellcheck script.sh`)\n- [ ] `bats` tests for logic functions\n- [ ] `--dry-run` for dangerous operations\n- [ ] Dependency checks (`command -v`)\n- [ ] Graceful shutdown (`trap INT TERM`)\n- [ ] Health checks with retries\n- [ ] Atomic operations (temp file + `mv` for writes)\n",[30,6931,6932,6946,6958,6970,6982,6998,7010,7022,7034,7046,7059,7066,7078,7095,7108,7120,7132,7144,7156,7168,7180,7187],{"__ignoreMap":28},[33,6933,6934,6937,6940,6943],{"class":35,"line":36},[33,6935,6936],{"class":1639},"-",[33,6938,6939],{"class":77}," [ ] ",[33,6941,6942],{"class":52},"`#!\u002Fusr\u002Fbin\u002Fenv bash`",[33,6944,6945],{"class":77}," shebang (portable)\n",[33,6947,6948,6950,6952,6955],{"class":35,"line":43},[33,6949,6936],{"class":1639},[33,6951,6939],{"class":77},[33,6953,6954],{"class":52},"`set -Eeuo pipefail`",[33,6956,6957],{"class":77}," strict mode (-E for ERR trap inheritance)\n",[33,6959,6960,6962,6964,6967],{"class":35,"line":49},[33,6961,6936],{"class":1639},[33,6963,6939],{"class":77},[33,6965,6966],{"class":52},"`trap cleanup EXIT`",[33,6968,6969],{"class":77}," for temp file cleanup\n",[33,6971,6972,6974,6976,6979],{"class":35,"line":63},[33,6973,6936],{"class":1639},[33,6975,6939],{"class":77},[33,6977,6978],{"class":52},"`trap err_handler ERR`",[33,6980,6981],{"class":77}," for error logging\n",[33,6983,6984,6986,6989,6992,6995],{"class":35,"line":70},[33,6985,6936],{"class":1639},[33,6987,6988],{"class":77}," [ ] All variables quoted: ",[33,6990,6991],{"class":52},"`\"$var\"`",[33,6993,6994],{"class":77},", ",[33,6996,6997],{"class":52},"`\"${arr[@]}\"`\n",[33,6999,7000,7002,7004,7007],{"class":35,"line":103},[33,7001,6936],{"class":1639},[33,7003,6939],{"class":77},[33,7005,7006],{"class":52},"`local`",[33,7008,7009],{"class":77}," for all function variables\n",[33,7011,7012,7014,7016,7019],{"class":35,"line":123},[33,7013,6936],{"class":1639},[33,7015,6939],{"class":77},[33,7017,7018],{"class":52},"`readonly`",[33,7020,7021],{"class":77}," for constants\n",[33,7023,7024,7026,7028,7031],{"class":35,"line":128},[33,7025,6936],{"class":1639},[33,7027,6939],{"class":77},[33,7029,7030],{"class":52},"`main \"$@\"`",[33,7032,7033],{"class":77}," entry point (functions defined before call)\n",[33,7035,7036,7038,7040,7043],{"class":35,"line":134},[33,7037,6936],{"class":1639},[33,7039,6939],{"class":77},[33,7041,7042],{"class":52},"`usage`",[33,7044,7045],{"class":77}," function and arg validation\n",[33,7047,7048,7050,7053,7056],{"class":35,"line":145},[33,7049,6936],{"class":1639},[33,7051,7052],{"class":77}," [ ] Errors to stderr (",[33,7054,7055],{"class":52},"`>&2`",[33,7057,7058],{"class":77},"), data to stdout\n",[33,7060,7061,7063],{"class":35,"line":156},[33,7062,6936],{"class":1639},[33,7064,7065],{"class":77}," [ ] Meaningful exit codes (0=success, 1=failure, 2=usage)\n",[33,7067,7068,7070,7072,7075],{"class":35,"line":167},[33,7069,6936],{"class":1639},[33,7071,6939],{"class":77},[33,7073,7074],{"class":52},"`mktemp`",[33,7076,7077],{"class":77}," for temp files (not predictable names)\n",[33,7079,7080,7082,7085,7088,7090,7093],{"class":35,"line":177},[33,7081,6936],{"class":1639},[33,7083,7084],{"class":77}," [ ] Idempotent operations (",[33,7086,7087],{"class":52},"`mkdir -p`",[33,7089,6994],{"class":77},[33,7091,7092],{"class":52},"`[[ -e ]] || create`",[33,7094,100],{"class":77},[33,7096,7097,7099,7102,7105],{"class":35,"line":182},[33,7098,6936],{"class":1639},[33,7100,7101],{"class":77}," [ ] No ",[33,7103,7104],{"class":52},"`eval`",[33,7106,7107],{"class":77}," on untrusted input\n",[33,7109,7110,7112,7114,7117],{"class":35,"line":191},[33,7111,6936],{"class":1639},[33,7113,6939],{"class":77},[33,7115,7116],{"class":52},"`find -print0 | while IFS= read -r -d ''`",[33,7118,7119],{"class":77}," for filenames\n",[33,7121,7122,7124,7127,7130],{"class":35,"line":203},[33,7123,6936],{"class":1639},[33,7125,7126],{"class":77}," [ ] ShellCheck clean (",[33,7128,7129],{"class":52},"`shellcheck script.sh`",[33,7131,100],{"class":77},[33,7133,7134,7136,7138,7141],{"class":35,"line":215},[33,7135,6936],{"class":1639},[33,7137,6939],{"class":77},[33,7139,7140],{"class":52},"`bats`",[33,7142,7143],{"class":77}," tests for logic functions\n",[33,7145,7146,7148,7150,7153],{"class":35,"line":220},[33,7147,6936],{"class":1639},[33,7149,6939],{"class":77},[33,7151,7152],{"class":52},"`--dry-run`",[33,7154,7155],{"class":77}," for dangerous operations\n",[33,7157,7158,7160,7163,7166],{"class":35,"line":226},[33,7159,6936],{"class":1639},[33,7161,7162],{"class":77}," [ ] Dependency checks (",[33,7164,7165],{"class":52},"`command -v`",[33,7167,100],{"class":77},[33,7169,7170,7172,7175,7178],{"class":35,"line":232},[33,7171,6936],{"class":1639},[33,7173,7174],{"class":77}," [ ] Graceful shutdown (",[33,7176,7177],{"class":52},"`trap INT TERM`",[33,7179,100],{"class":77},[33,7181,7182,7184],{"class":35,"line":238},[33,7183,6936],{"class":1639},[33,7185,7186],{"class":77}," [ ] Health checks with retries\n",[33,7188,7189,7191,7194,7197],{"class":35,"line":249},[33,7190,6936],{"class":1639},[33,7192,7193],{"class":77}," [ ] Atomic operations (temp file + ",[33,7195,7196],{"class":52},"`mv`",[33,7198,7199],{"class":77}," for writes)\n",[15,7201,7203],{"id":7202},"tips-tricks","💡 Tips & Tricks",[7205,7206,7207,7223,7236,7254,7271],"ul",{},[7208,7209,7210,7214,7215,7218,7219,7222],"li",{},[7211,7212,7213],"strong",{},"Debug",": run every script in this chapter through ",[30,7216,7217],{},"shellcheck"," before trusting it — Project 3's pre-commit hook exists precisely because ShellCheck catches unquoted expansions and word-splitting bugs that only surface on a machine with different ",[30,7220,7221],{},"$IFS"," or filenames containing spaces.",[7208,7224,7225,6222,7228,7231,7232,7235],{},[7211,7226,7227],{},"Idiom",[30,7229,7230],{},"run()"," wrapper functions (seen in Projects 1 and 7) that gate every side-effecting command behind ",[30,7233,7234],{},"$DRY_RUN"," are the cheapest insurance you can add to a deploy\u002Fbackup script — retrofit one into any existing script before it touches production data for the first time.",[7208,7237,7238,7241,7242,7245,7246,7249,7250,7253],{},[7211,7239,7240],{},"Performance",": Project 5's ",[30,7243,7244],{},"((count % 4 == 0)) && wait"," pattern throttles parallelism without a job-control library — for heavier fan-out, ",[30,7247,7248],{},"xargs -P N"," or GNU ",[30,7251,7252],{},"parallel"," do the same job with less bookkeeping and built-in output ordering.",[7208,7255,7256,6222,7259,7262,7263,7267,7268,7270],{},[7211,7257,7258],{},"Safety",[30,7260,7261],{},"trap cleanup EXIT"," (Projects 1 and 4) fires on ",[7264,7265,7266],"em",{},"any"," exit path — normal, ",[30,7269,349],{},", or a signal — so it's the one place to guarantee temp files are removed and child processes are reaped, instead of duplicating cleanup at every early return.",[7208,7272,7273,7275,7276,7279,7280,7283],{},[7211,7274,7213],{},": when a supervised child (Project 4) misbehaves, ",[30,7277,7278],{},"kill -0 \"$pid\""," is the idiomatic \"is this PID still alive\" check — it sends no signal, only tests permission and existence, and is far cheaper than parsing ",[30,7281,7282],{},"ps"," output.",[15,7285,7287],{"id":7286},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[7205,7289,7290,7329,7346,7363],{},[7208,7291,7292,7298,7299,6994,7301,7304,7305,6994,7307,6994,7309,3856,7312,7314,7315,7318,7319,7321,7322,7324,7325,7328],{},[7211,7293,7294,7297],{},[30,7295,7296],{},"set -e"," does not fire inside a conditional",": in Project 7's ",[30,7300,5706],{},[30,7302,7303],{},"run npm run build || die \"build failed\""," works because the failure is caught explicitly — but a bare failing command inside ",[30,7306,1091],{},[30,7308,388],{},[30,7310,7311],{},"&&",[30,7313,672],{},", or a pipeline (without ",[30,7316,7317],{},"pipefail",") silently continues under ",[30,7320,7296],{},", which is why every project here pairs ",[30,7323,7296],{}," with explicit ",[30,7326,7327],{},"|| die"," at the calls that matter most.",[7208,7330,7331,7337,7338,7341,7342,7345],{},[7211,7332,7333,7336],{},[30,7334,7335],{},"mapfile -t releases \u003C \u003C(ssh ... || true)"," can silently mask a real SSH failure",": Project 7's rollback trailing ",[30,7339,7340],{},"|| true"," exists to tolerate \"no releases directory yet,\" but it equally swallows a genuine SSH connection failure — the subsequent ",[30,7343,7344],{},"(( ${#releases[@]} \u003C 2 ))"," check catches the empty case either way, but the error message (\"no previous release\") would mislead you if the real cause was a dead host.",[7208,7347,7348,6222,7351,7354,7355,7358,7359,7362],{},[7211,7349,7350],{},"Backgrounded jobs in Project 5 inherit the parent's file descriptors",[30,7352,7353],{},"resize_one \"$file\" \"$size\" &"," forked in a loop reading from ",[30,7356,7357],{},"find ... -print0"," means every background job also holds the same read end of that process substitution pipe open — usually harmless here, but it's the same mechanism that causes classic \"background jobs reading stdin steal each other's input\" bugs when the loop reads from stdin instead of a ",[30,7360,7361],{},"-print0"," fd.",[7208,7364,7365,7375,7376,7379],{},[7211,7366,7367,7370,7371,7374],{},[30,7368,7369],{},"${releases[1]}"," in Project 7 assumes ",[30,7372,7373],{},"sort -r"," gives lexicographic order matching chronological order",": this only holds because the release directory names are ",[30,7377,7378],{},"YYYYMMDD_HHMMSS"," timestamps — zero-padded, fixed-width, sortable as strings. Swap in an unpadded or non-ISO timestamp format and \"previous release\" silently picks the wrong one.",[15,7381,7383],{"id":7382},"quick-quiz","🧠 Quick Quiz",[7385,7386,7387],"p",{},"Project 1's cleanup trap is:",[20,7389,7390],{"language":22},[24,7391,7393],{"className":26,"code":7392,"language":22,"meta":28,"style":28},"tmpfile=\"\"\ncleanup() {\n    local code=$?\n    [[ -n \"$tmpfile\" && -f \"$tmpfile\" ]] && rm -f \"$tmpfile\"\n    exit \"$code\"\n}\ntrap cleanup EXIT\n",[30,7394,7395,7403,7409,7419,7453,7463,7467],{"__ignoreMap":28},[33,7396,7397,7399,7401],{"class":35,"line":36},[33,7398,818],{"class":77},[33,7400,81],{"class":73},[33,7402,823],{"class":59},[33,7404,7405,7407],{"class":35,"line":43},[33,7406,829],{"class":87},[33,7408,188],{"class":77},[33,7410,7411,7413,7415,7417],{"class":35,"line":49},[33,7412,837],{"class":73},[33,7414,840],{"class":77},[33,7416,81],{"class":73},[33,7418,845],{"class":52},[33,7420,7421,7423,7425,7427,7429,7431,7433,7435,7437,7439,7441,7443,7445,7447,7449,7451],{"class":35,"line":63},[33,7422,851],{"class":77},[33,7424,854],{"class":73},[33,7426,331],{"class":59},[33,7428,859],{"class":77},[33,7430,337],{"class":59},[33,7432,864],{"class":73},[33,7434,867],{"class":73},[33,7436,331],{"class":59},[33,7438,859],{"class":77},[33,7440,337],{"class":59},[33,7442,876],{"class":77},[33,7444,879],{"class":87},[33,7446,867],{"class":52},[33,7448,331],{"class":59},[33,7450,859],{"class":77},[33,7452,702],{"class":59},[33,7454,7455,7457,7459,7461],{"class":35,"line":70},[33,7456,893],{"class":52},[33,7458,331],{"class":59},[33,7460,898],{"class":77},[33,7462,702],{"class":59},[33,7464,7465],{"class":35,"line":103},[33,7466,297],{"class":77},[33,7468,7469,7471,7473],{"class":35,"line":123},[33,7470,911],{"class":52},[33,7472,914],{"class":59},[33,7474,917],{"class":59},[7385,7476,7477,7478,7480,7481,7484,7485,7488,7489,7491],{},"If the script's main body never assigns anything to ",[30,7479,818],{},", and later fails with ",[30,7482,7483],{},"die \"backup verification failed\""," (which calls ",[30,7486,7487],{},"exit 1","), what does ",[30,7490,829],{}," do, and what exit code does the script report?",[7493,7494,7495,7499],"details",{},[7496,7497,7498],"summary",{},"Answer",[7385,7500,7501,7503,7504,7506,7507,7510,7511,7513,7514,7516,7517,7519,7520,7523,7524,7526,7527,7530,7531,7533,7534,7536,7537,7539,7540,7543,7544,7547,7548,7550,7551,7553,7554,7556,7557,7559],{},[30,7502,829],{}," runs (EXIT traps fire even when triggered by an explicit ",[30,7505,366],{},"), reads ",[30,7508,7509],{},"code=$?"," — which captures ",[30,7512,1709],{}," from ",[30,7515,349],{},"'s ",[30,7518,7487],{},", since ",[30,7521,7522],{},"$?"," is evaluated as the very first statement inside ",[30,7525,829],{},", before anything else changes it — skips the ",[30,7528,7529],{},"rm -f"," because ",[30,7532,818],{}," is empty, and re-exits with ",[30,7535,30],{},", i.e. ",[30,7538,1709],{},". The script correctly reports failure. The subtle part: if ",[30,7541,7542],{},"local code=$?"," weren't the ",[7264,7545,7546],{},"first"," line of ",[30,7549,829],{},", any earlier command in the trap (even a no-op comparison) could overwrite ",[30,7552,7522],{}," before it's captured, and the script would exit ",[30,7555,2603],{}," despite having failed — this is why every trap handler in this chapter captures ",[30,7558,7522],{}," immediately.",[7561,7562,7563],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .svAP2, html code.shiki .svAP2{--shiki-default:#032F62;--shiki-github-dark:#DBEDFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}",{"title":28,"searchDepth":43,"depth":43,"links":7565},[7566,7567,7568,7569,7570,7571,7572,7573,7574,7575,7576],{"id":17,"depth":43,"text":18},{"id":1509,"depth":43,"text":1510},{"id":2057,"depth":43,"text":2058},{"id":2446,"depth":43,"text":2447},{"id":3108,"depth":43,"text":3109},{"id":3612,"depth":43,"text":3613},{"id":4521,"depth":43,"text":4522},{"id":6921,"depth":43,"text":6922},{"id":7202,"depth":43,"text":7203},{"id":7286,"depth":43,"text":7287},{"id":7382,"depth":43,"text":7383},"Seven production-grade Bash projects from backup scripts to a capstone deployment system. Each project demonstrates real-world patterns: strict mode, traps, error handling, parallelism, retries, health checks, and testing. Code-first reference for senior engineers.","md",{},"\u002Fbash\u002F15-exercises-and-projects",{"title":5,"description":7577},"bash\u002F15-exercises-and-projects","zWiobL65kffZra3jtXJq7p32Up4Dj97R-GiuIMmz89I",1789924644818]