app
Adom Chip Fetcher
Public Made by Adomby adom
Your whole parts library — manufacturer-grade chip CAD (symbol, footprint, 3D) one tap from your EDA tool.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
#!/bin/bash
# cf-ul-fast.sh — fast two-phase UL fetch with AI-vision handoff
#
# Phase 1 (pre-captcha): navigate → preset form → trusted-click I'm-not-a-robot
# → screenshot iframe + crop if image-grid escalates
# → output JSON: silent_pass OR image_grid info
#
# AI does vision step (only LLM round-trip).
#
# Phase 2 (post-captcha): click tiles → click Verify → atomic-eval Submit
# → poll Downloads → pull → distribute to family MPNs
# → output JSON: done with file paths
#
# This collapses the chained-LLM-call problem (~30s of AI latency between
# tile-pick and Submit-fire) into TWO calls with one vision step in between.
# All non-vision work runs in tight bash with sub-second internal latency.
#
# Usage:
# ./cf-ul-fast.sh --phase pre-captcha --url '<UL-URL>'
# ./cf-ul-fast.sh --phase post-captcha --tiles 4,9 \
# --mpns VLMS1500,VLMTG1500,VLMB1500,VLMO1500,VLMY1500
#
# State between phases lives at /tmp/cf-ul-fast-state.json.
set -u
SESSION="${CF_SESSION:-chip-fetcher}"
ACTIVITY="http://127.0.0.1:8786/api/activity"
STATE_FILE="/tmp/cf-ul-fast-state.json"
PHASE=""
URL=""
TILES=""
MPNS=""
GRID="3x3" # 3x3 (9 tiles) or 4x4 (16 tiles) — UL rotates these to throw off AI; AI sees grid in screenshot and passes to script
ROUND=1
while [[ $# -gt 0 ]]; do
case "$1" in
--phase) PHASE="$2"; shift 2 ;;
--url) URL="$2"; shift 2 ;;
--tiles) TILES="$2"; shift 2 ;;
--mpns) MPNS="$2"; shift 2 ;;
--grid) GRID="$2"; shift 2 ;;
--round) ROUND="$2"; shift 2 ;;
*) echo "{\"ok\":false,\"error\":\"unknown arg: $1\"}"; exit 1 ;;
esac
done
# Grid geometry (derived from iframe rect at click time, not hardcoded).
# bframe internal layout (% of iframe inner dims):
# - header (blue prompt bar): ~14% of height (top)
# - tile grid: middle, full width minus ~3% side margins
# - footer (refresh/audio/info + VERIFY): ~17% of height (bottom)
HEADER_PCT=0.14
FOOTER_PCT=0.17
SIDE_PCT=0.03
if [[ "$GRID" == "4x4" ]]; then
TILE_COLS=4; TILE_ROWS=4
else
TILE_COLS=3; TILE_ROWS=3
fi
post_act() { curl -s -X POST "$ACTIVITY" -H 'Content-Type: application/json' -d "$1" >/dev/null 2>&1 || true; }
clear_act() { curl -s -X POST "$ACTIVITY" -H 'Content-Type: application/json' -d '{}' >/dev/null 2>&1 || true; }
# ===== PHASE 1: pre-captcha =====
if [[ "$PHASE" == "pre-captcha" ]]; then
[[ -z "$URL" ]] && { echo '{"ok":false,"error":"--url required for pre-captcha phase"}'; exit 1; }
post_act '{"mpn":"_ul","stage":"navigating UL"}'
adom-desktop browser_navigate "{\"sessionId\":\"$SESSION\",\"url\":\"$URL\"}" >/dev/null 2>&1
sleep 5
# Optionally click "Choose CAD Formats & Download" — exists on vendor.ultralibrarian.com
# embedded URLs but NOT on app.ultralibrarian.com part-detail pages where the format
# selectors are inline. Skip the click if the button isn't on the page.
post_act '{"mpn":"_ul","stage":"opening Choose CAD Formats (if present)"}'
RECT=$(adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{const a=Array.from(document.querySelectorAll('a, button')).find(e=>/choose cad/i.test((e.textContent||'').trim())); if(!a)return null; const r=a.getBoundingClientRect(); return JSON.stringify({x:Math.round(r.x+r.width/2),y:Math.round(r.y+r.height/2)});})()\"}" 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); r=json.loads(d.get('result','{}') or '{}'); print(r.get('x',0), r.get('y',0))" 2>/dev/null)
CX=$(echo "$RECT" | awk '{print $1}')
CY=$(echo "$RECT" | awk '{print $2}')
if [[ -n "$CX" && "$CX" != "0" ]]; then
adom-desktop browser_input_dispatch "{\"sessionId\":\"$SESSION\",\"type\":\"click\",\"x\":$CX,\"y\":$CY}" >/dev/null 2>&1
sleep 3
fi
# On UL Pro pages, format selectors may be in collapsible category panels — expand them
# so the format-checkbox preset finds them.
adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{const cats=Array.from(document.querySelectorAll('a, button, [role=button], div')).filter(e=>{const t=(e.textContent||'').trim(); return /^(3D CAD Model|KiCAD|Altium|Autodesk|Fusion)\\s*[▶▼►▾]?\$/i.test(t);}); for(const c of cats){c.click();} return cats.length+' cats expanded';})()\"}" >/dev/null 2>&1
sleep 1
# Preset form (canonical four + T&C + mm)
post_act '{"mpn":"_ul","stage":"presetting form (STEP + KiCad v6+ + Altium + Fusion 360 + mm + T&C)"}'
adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{const cbs=Array.from(document.querySelectorAll('input[type=checkbox]')); const want=[/^STEP\$/i,/kicad v6/i,/^altium designer/i,/^fusion.?360/i,/agree|terms/i]; let n=0; for(const re of want){const c=cbs.find(c=>re.test((c.parentElement?.textContent||c.closest('label')?.textContent||'').trim())); if(c&&!c.checked){c.click();n++;}} const sels=Array.from(document.querySelectorAll('select')); const u=sels.find(s=>Array.from(s.options).some(o=>/metric|mm/i.test(o.textContent))); if(u){const m=Array.from(u.options).find(o=>/metric|mm/i.test(o.textContent)); u.value=m.value; u.dispatchEvent(new Event('change',{bubbles:true}));} return n+' set';})()\"}" >/dev/null 2>&1
sleep 1
# Install JS hooks: MutationObserver auto-submit + DOM cache. Once token populates,
# Submit fires within one event-loop tick (no 200ms polling latency).
post_act '{"mpn":"_ul","stage":"installing auto-submit hook"}'
adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{if(window.__cf)return 'already';window.__cf={submitted:false,submittedAt:0,tokenLen:0};const ta=document.querySelector('[name=g-recaptcha-response]');if(!ta)return 'no_token_textarea';const findSubmit=()=>Array.from(document.querySelectorAll('a, button, input[type=submit]')).find(e=>/^submit\$/i.test((e.textContent||e.value||'').trim()));const fire=()=>{const t=ta.value||'';if(t.length<=20||window.__cf.submitted)return;window.__cf.tokenLen=t.length;const btn=findSubmit();if(btn){btn.click();window.__cf.submitted=true;window.__cf.submittedAt=Date.now();}};const obs=new MutationObserver(fire);obs.observe(ta,{attributes:true,attributeFilter:['value'],characterData:true,subtree:true,childList:true});window.__cf.obs=obs;window.__cf.poll=setInterval(fire,100);return 'installed';})()\"}" >/dev/null 2>&1
# Mark a unix-second checkpoint BEFORE the captcha. desktop_watch_files
# (adom-desktop v1.5+) accepts unix seconds for `since` so we only see
# files written AFTER this checkpoint — no shell, no PowerShell.
post_act '{"mpn":"_ul","stage":"marking pre-captcha checkpoint"}'
BEFORE_TS=$(date +%s)
echo "$BEFORE_TS" > /tmp/cf-ul-fast-before.txt
# Trigger captcha — two paths:
# (a) vendor.ultralibrarian.com/<mfr>/embedded — anchor iframe "I'm not a robot"
# (b) app.ultralibrarian.com/details/... — "Download Now" button (invisible reCAPTCHA)
# Try (a) first; fall back to (b).
post_act '{"mpn":"_ul","stage":"triggering captcha (anchor or Download Now)"}'
IFR=$(adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{const f=document.querySelector('iframe[src*=anchor]') || document.querySelector('iframe[src*=recaptcha]'); if(!f)return null; const r=f.getBoundingClientRect(); if(r.width<10||r.height<10)return null; return JSON.stringify({x:r.x|0,y:r.y|0});})()\"}" 2>/dev/null \
| python3 -c "import sys,json; d=json.load(sys.stdin); r=json.loads(d.get('result','{}') or '{}'); print(r.get('x',0), r.get('y',0))" 2>/dev/null)
IX=$(echo $IFR | awk '{print $1}')
IY=$(echo $IFR | awk '{print $2}')
if [[ -n "$IX" && "$IX" != "0" ]]; then
# Path A — visible anchor checkbox (vendor.ultralibrarian.com embedded)
IX=$((IX + 30))
IY=$((IY + 40))
adom-desktop browser_input_dispatch "{\"sessionId\":\"$SESSION\",\"type\":\"move\",\"x\":$((IX-200)),\"y\":$((IY-100)),\"steps\":5}" >/dev/null 2>&1
sleep 0.4
adom-desktop browser_input_dispatch "{\"sessionId\":\"$SESSION\",\"type\":\"move\",\"x\":$IX,\"y\":$IY,\"steps\":15}" >/dev/null 2>&1
sleep 0.3
adom-desktop browser_input_dispatch "{\"sessionId\":\"$SESSION\",\"type\":\"click\",\"x\":$IX,\"y\":$IY}" >/dev/null 2>&1
sleep 4
else
# Path B — UL Pro page; click "Download Now" button which triggers invisible reCAPTCHA
post_act '{"mpn":"_ul","stage":"clicking Download Now (UL Pro flow)"}'
adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{const b=Array.from(document.querySelectorAll('button, a')).find(e=>/^Download\\s*Now\$/i.test((e.textContent||e.value||'').trim())); if(!b) return 'no btn'; b.click(); return 'clicked';})()\"}" >/dev/null 2>&1
sleep 5
fi
# Read state
STATE=$(adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{const t=(document.querySelector('[name=g-recaptcha-response]')?.value||'').length; const bf=document.querySelector('iframe[src*=bframe]'); const r=bf?bf.getBoundingClientRect():null; const visible=r&&r.y>0&&r.y<2000; return JSON.stringify({tokenLen:t,bframe:r?{x:r.x|0,y:r.y|0,w:r.width|0,h:r.height|0}:null,visible:visible,dpr:window.devicePixelRatio});})()\"}" 2>/dev/null \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('result',''))")
TOKLEN=$(echo "$STATE" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('tokenLen',0))")
if [[ "$TOKLEN" -gt 20 ]]; then
# Silent pass — Submit immediately, but defer to phase 2 (so AI sees the structured output)
cat > "$STATE_FILE" <<EOF
{"phase1_completed_at":"$(date -u +%FT%TZ)","silent_pass":true,"url":"$URL"}
EOF
cat <<EOF
{"ok":true,"stage":"silent_pass","_hint":"reCAPTCHA passed silently. Run: cf-ul-fast.sh --phase post-captcha --mpns <comma-list>"}
EOF
exit 0
fi
# Image grid escalated — screenshot + crop
post_act '{"mpn":"_ul","stage":"image grid escalated — cropping for AI vision"}'
adom-desktop browser_screenshot "{\"sessionId\":\"$SESSION\",\"path\":\"/tmp/cf-ul-fullpage.png\",\"maxWidth\":2000}" >/dev/null 2>&1
# browser_screenshot writes to its own conduit-screenshots dir; find latest
LATEST=$(ls -1t /tmp/conduit-screenshots/screenshot-*.png 2>/dev/null | head -1)
cp "$LATEST" /tmp/cf-ul-fullpage.png
# Crop bframe region using state from above
python3 << PYEOF > /tmp/cf-ul-bframe-meta.json
import json
from PIL import Image
state = json.loads("""$STATE""")
bf = state.get('bframe') or {}
dpr = state.get('dpr', 1.5)
img = Image.open('/tmp/cf-ul-fullpage.png')
sw, sh = img.size
x = int(bf.get('x',0) * dpr)
y = int(bf.get('y',0) * dpr)
w = int(bf.get('w',0) * dpr)
h = int(bf.get('h',0) * dpr)
crop = img.crop((x, y, min(x+w, sw), min(y+h, sh)))
crop.save('/tmp/cf-ul-bframe-crop.png')
crop_2x = crop.resize((crop.size[0]*2, crop.size[1]*2))
crop_2x.save('/tmp/cf-ul-bframe-crop-2x.png')
print(json.dumps({
"iframe_png": "/tmp/cf-ul-bframe-crop.png",
"iframe_png_2x": "/tmp/cf-ul-bframe-crop-2x.png",
"size": list(crop.size),
"size_2x": list(crop_2x.size),
"iframe_css_rect": bf,
"dpr": dpr
}))
PYEOF
META=$(cat /tmp/cf-ul-bframe-meta.json)
# Try to extract the prompt + grid size from the iframe DOM (best-effort — bframe is cross-origin)
# Most reCAPTCHA prompts surface as "Select all images with X" — for now we leave grid detection to AI vision.
# Save state for phase 2
cat > "$STATE_FILE" <<EOF
{"phase1_completed_at":"$(date -u +%FT%TZ)","silent_pass":false,"url":"$URL","iframe_meta":$META}
EOF
cat <<EOF
{"ok":true,"stage":"image_grid","round":1,"iframe_png":"/tmp/cf-ul-bframe-crop.png","iframe_png_2x":"/tmp/cf-ul-bframe-crop-2x.png","_hint":"Read iframe_png_2x. UL rotates captcha types — note (a) grid size (3x3 = 9 tiles, 4x4 = 16 tiles), (b) prompt text in the blue header, (c) whether it says 'Select all images with X' (one-shot) vs 'Click verify once there are none left' (iterative; may need multiple rounds). Then call: cf-ul-fast.sh --phase post-captcha --tiles <list> --mpns <list> --grid <3x3-or-4x4>. Tiles numbered left-to-right, top-to-bottom (1-9 for 3x3, 1-16 for 4x4)."}
EOF
exit 0
# ===== PHASE 2: post-captcha =====
elif [[ "$PHASE" == "post-captcha" ]]; then
[[ -z "$MPNS" ]] && { echo '{"ok":false,"error":"--mpns required for post-captcha phase"}'; exit 1; }
if [[ ! -f "$STATE_FILE" ]]; then
echo "{\"ok\":false,\"error\":\"no $STATE_FILE — run --phase pre-captcha first\"}"
exit 1
fi
SILENT=$(python3 -c "import json; print(json.load(open('$STATE_FILE')).get('silent_pass',False))")
IFRAME_RECT=$(python3 -c "import json; m=json.load(open('$STATE_FILE')).get('iframe_meta',{}).get('iframe_css_rect',{}); print(m.get('x',0),m.get('y',0),m.get('w',0),m.get('h',0))")
read IX IY IW IH <<< "$IFRAME_RECT"
if [[ "$SILENT" == "False" || "$SILENT" == "false" ]]; then
if [[ -z "$TILES" ]]; then
echo '{"ok":false,"error":"image_grid was active in phase 1 — --tiles is required"}'
exit 1
fi
# Click each tile — coords derived from CURRENT iframe rect (not hardcoded)
post_act "{\"mpn\":\"_ul\",\"stage\":\"AI vision round $ROUND: clicking tiles $TILES ($GRID)\"}"
# Compute grid geometry from iframe rect (CSS pixels)
GRID_X0=$(python3 -c "print(int($IX + $IW * $SIDE_PCT))")
GRID_Y0=$(python3 -c "print(int($IY + $IH * $HEADER_PCT))")
GRID_W=$(python3 -c "print(int($IW * (1 - 2 * $SIDE_PCT)))")
GRID_H=$(python3 -c "print(int($IH * (1 - $HEADER_PCT - $FOOTER_PCT)))")
TW=$(python3 -c "print($GRID_W / $TILE_COLS)")
TH=$(python3 -c "print($GRID_H / $TILE_ROWS)")
IFS=',' read -ra TILE_ARR <<< "$TILES"
for T in "${TILE_ARR[@]}"; do
ROW=$(( (T - 1) / TILE_COLS ))
COL=$(( (T - 1) % TILE_COLS ))
X=$(python3 -c "print(int($GRID_X0 + ($COL + 0.5) * $TW))")
Y=$(python3 -c "print(int($GRID_Y0 + ($ROW + 0.5) * $TH))")
adom-desktop browser_input_dispatch "{\"sessionId\":\"$SESSION\",\"type\":\"click\",\"x\":$X,\"y\":$Y}" >/dev/null 2>&1 &
done
wait
sleep 0.2
# Click Verify — bottom-right of iframe footer.
# Footer is the last $FOOTER_PCT of iframe height; VERIFY button sits ~75px in from right edge, ~40px above bottom (CSS).
VX=$((IX + IW - 75))
VY=$((IY + IH - 40))
post_act "{\"mpn\":\"_ul\",\"stage\":\"clicking Verify\"}"
adom-desktop browser_input_dispatch "{\"sessionId\":\"$SESSION\",\"type\":\"click\",\"x\":$VX,\"y\":$VY}" >/dev/null 2>&1
sleep 0.5
# Detect iterative-captcha escalation: bframe still visible + no token = new round needed
ROUND2=$(adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{const t=(document.querySelector('[name=g-recaptcha-response]')?.value||'').length; const bf=document.querySelector('iframe[src*=bframe]'); const r=bf?bf.getBoundingClientRect():null; const visible=r&&r.y>0&&r.y<2000; return JSON.stringify({tokenLen:t,bframe:r?{x:r.x|0,y:r.y|0,w:r.width|0,h:r.height|0}:null,visible:visible});})()\"}" 2>/dev/null \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('result',''))")
R2_TOK=$(echo "$ROUND2" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('tokenLen',0))")
R2_VIS=$(echo "$ROUND2" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('visible',False))")
if [[ "$R2_TOK" == "0" && ( "$R2_VIS" == "True" || "$R2_VIS" == "true" ) ]]; then
# Iterative captcha — new tiles appeared. Re-screenshot + output for AI vision again.
post_act '{"mpn":"_ul","stage":"iterative captcha — round 2 escalation, re-screenshotting"}'
adom-desktop browser_screenshot "{\"sessionId\":\"$SESSION\",\"path\":\"/tmp/cf-ul-fullpage.png\",\"maxWidth\":2000}" >/dev/null 2>&1
LATEST=$(ls -1t /tmp/conduit-screenshots/screenshot-*.png 2>/dev/null | head -1)
cp "$LATEST" /tmp/cf-ul-fullpage.png
python3 << PYEOF >/dev/null
import json
from PIL import Image
state = json.loads("""$ROUND2""")
bf = state.get('bframe') or {}
dpr = 1.5 # pup default
img = Image.open('/tmp/cf-ul-fullpage.png')
sw, sh = img.size
x = int(bf.get('x',0) * dpr); y = int(bf.get('y',0) * dpr)
w = int(bf.get('w',0) * dpr); h = int(bf.get('h',0) * dpr)
crop = img.crop((x, y, min(x+w, sw), min(y+h, sh)))
crop.save('/tmp/cf-ul-bframe-crop.png')
crop.resize((crop.size[0]*2, crop.size[1]*2)).save('/tmp/cf-ul-bframe-crop-2x.png')
PYEOF
NEXT_ROUND=$((ROUND + 1))
cat <<EOF
{"ok":true,"stage":"image_grid_round_${NEXT_ROUND}","round":${NEXT_ROUND},"iframe_png":"/tmp/cf-ul-bframe-crop.png","iframe_png_2x":"/tmp/cf-ul-bframe-crop-2x.png","_hint":"Iterative captcha round ${NEXT_ROUND}. UL rotates approaches — re-read iframe_png_2x carefully (grid may be 3x3 or 4x4, prompt may differ from round 1, type may switch between 'select all' and 'click verify once there are none left'). Call --phase post-captcha --tiles <new-list> --mpns <same-list> --grid <3x3-or-4x4> --round ${NEXT_ROUND}. State preserved."}
EOF
exit 0
fi
fi
# Atomic-eval poll for token + auto-Submit (90s budget, 200ms cadence — TIGHT)
post_act '{"mpn":"_ul","stage":"atomic-eval token watcher (Submit fires on token populate)"}'
SUBMITTED=""
for i in $(seq 1 450); do
sleep 0.2
RES=$(adom-desktop browser_eval "{\"sessionId\":\"$SESSION\",\"expr\":\"(()=>{const t=(document.querySelector('[name=g-recaptcha-response]')?.value||''); if(t.length>20){const a=Array.from(document.querySelectorAll('a, button, input[type=submit]')).find(e=>/^submit\$/i.test((e.textContent||e.value||'').trim())); if(a){a.click();return 'SUBMITTED_'+a.tagName;} return 'NO_BTN';} return 'WAIT';})()\"}" 2>/dev/null \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('result',''))")
case "$RES" in
SUBMITTED*) SUBMITTED="$RES"; break ;;
NO_BTN) echo "{\"ok\":false,\"error\":\"token populated but Submit btn not found\",\"attempt\":$i}"; exit 1 ;;
esac
done
if [[ -z "$SUBMITTED" ]]; then
echo '{"ok":false,"error":"timeout waiting for token — captcha may have failed or token expired"}'
exit 1
fi
# Watch Downloads for new ul_*.zip and pull it in one shot — no shell,
# no PowerShell, no user-approval dialog.
post_act '{"mpn":"_ul","stage":"watching Downloads for ul_*.zip"}'
BEFORE_TS=$(cat /tmp/cf-ul-fast-before.txt)
PULL=$(adom-desktop desktop_pull_glob "{\"path\":\"%USERPROFILE%\\\\Downloads\",\"glob\":\"ul_*.zip\",\"since\":$BEFORE_TS,\"wait\":true,\"timeoutMs\":60000,\"saveTo\":\"/home/adom/project/chip-fetcher/incoming/\"}" 2>/dev/null)
ZIP_PATH=$(echo "$PULL" | python3 -c "import sys,json; d=json.load(sys.stdin); f=(d.get('files') or [{}])[0]; print(f.get('path',''))")
if [[ -z "$ZIP_PATH" || ! -f "$ZIP_PATH" ]]; then
echo "{\"ok\":false,\"error\":\"timeout — no new ul_*.zip after Submit\",\"detail\":$(echo "$PULL" | python3 -c 'import sys,json; print(json.dumps(json.load(sys.stdin)))')}"
exit 1
fi
ZIP_NAME=$(basename "$ZIP_PATH")
# Extract once to a tmp dir
EXTRACT_DIR=$(mktemp -d)
unzip -q -o "$ZIP_PATH" -d "$EXTRACT_DIR"
# Distribute KiCad + Altium + Fusion + STEP to each MPN's library dir (family-distribution)
post_act '{"mpn":"_ul","stage":"distributing KiCad+Altium+Fusion+STEP to family MPNs"}'
IFS=',' read -ra MPN_ARR <<< "$MPNS"
DISTRIBUTED=""
for MPN in "${MPN_ARR[@]}"; do
LIB="library/$MPN"
mkdir -p "$LIB"
# KiCad: .kicad_sym + .kicad_mod (under KiCADv6/ in UL zip)
SYM=$(find "$EXTRACT_DIR" -name "*.kicad_sym" | head -1)
MOD=$(find "$EXTRACT_DIR" -name "*.kicad_mod" | head -1)
# Altium: .SchLib + .PcbLib (under Altium/<MPN>/ in UL zip)
ALT_SCH=$(find "$EXTRACT_DIR" -name "*.SchLib" | head -1)
ALT_PCB=$(find "$EXTRACT_DIR" -name "*.PcbLib" | head -1)
# Fusion 360: .lbr (under Fusion360/ in UL zip)
FUS_LBR=$(find "$EXTRACT_DIR" -name "*.lbr" | head -1)
# STEP (top-level OR inside Fusion360/ / Altium/)
STEP_F=$(find "$EXTRACT_DIR" \( -name "*.step" -o -name "*.stp" \) | head -1)
[[ -n "$SYM" ]] && cp "$SYM" "$LIB/$MPN.kicad_sym"
[[ -n "$MOD" ]] && cp "$MOD" "$LIB/$MPN.kicad_mod"
[[ -n "$ALT_SCH" ]] && cp "$ALT_SCH" "$LIB/$MPN.SchLib"
[[ -n "$ALT_PCB" ]] && cp "$ALT_PCB" "$LIB/$MPN.PcbLib"
[[ -n "$FUS_LBR" ]] && cp "$FUS_LBR" "$LIB/$MPN.lbr"
# Don't overwrite mfr-direct STEP if already imported
if [[ -n "$STEP_F" && ! -f "$LIB/$MPN.step" ]]; then
cp "$STEP_F" "$LIB/$MPN.step"
fi
DISTRIBUTED="$DISTRIBUTED \"$MPN\","
post_act "{\"mpn\":\"$MPN\",\"stage\":\"KiCad+Altium+Fusion imported (mfr-via-UL)\"}"
done
DISTRIBUTED="[${DISTRIBUTED%,}]"
# Update info.json per MPN with mfr-via-UL provenance for sym/mod
for MPN in "${MPN_ARR[@]}"; do
INFO="library/$MPN/info.json"
if [[ -f "$INFO" ]]; then
python3 -c "
import json
p = '$INFO'
d = json.load(open(p))
d['sym_source'] = 'mfr-via-UL'
d['sym_url'] = '$URL'
d['mod_source'] = 'mfr-via-UL'
d['mod_url'] = '$URL'
d['ul_zip'] = '$ZIP_NAME'
json.dump(d, open(p,'w'), indent=2)
"
fi
done
rm -rf "$EXTRACT_DIR"
clear_act
cat <<EOF
{"ok":true,"stage":"done","ul_zip":"$NEW","distributed_to":$DISTRIBUTED,"_hint":"sym+mod imported to library/<MPN>/. Refresh chip-fetcher dashboard to see the chips promoted from stub to bundled."}
EOF
exit 0
else
cat <<EOF
{"ok":false,"error":"--phase must be 'pre-captcha' or 'post-captcha'","_hint":"Phase 1: cf-ul-fast.sh --phase pre-captcha --url <UL-URL>. Phase 2: cf-ul-fast.sh --phase post-captcha --tiles 4,9 --mpns A,B,C"}
EOF
exit 1
fi