Merge pull request #2 from masa2146/re-skill
feat(clone-app): enhance HTTP request handling and update test cases …
This commit is contained in:
commit
d9594eb320
|
|
@ -2,14 +2,33 @@
|
|||
"""Best-effort check whether an iOS App Store equivalent exists, via the
|
||||
public iTunes Search API. There is no reliable Android-package → App-Store-ID
|
||||
mapping, so we search by term (app title) and return the top matches."""
|
||||
import sys, json, argparse, urllib.request, urllib.parse
|
||||
import sys, json, argparse, urllib.request, urllib.parse, ssl, subprocess, shutil
|
||||
|
||||
UA = "Mozilla/5.0"
|
||||
|
||||
def _http_get(url):
|
||||
"""GET a URL; on an SSL trust failure (macOS system Python ships without a
|
||||
CA bundle) fall back to the system `curl`. Stdlib-only, no pip."""
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return r.read().decode("utf-8", "replace")
|
||||
except urllib.error.URLError as e:
|
||||
if not isinstance(e.reason, ssl.SSLError):
|
||||
raise
|
||||
if not shutil.which("curl"):
|
||||
raise
|
||||
out = subprocess.run(
|
||||
["curl", "-sL", "--fail", "-A", UA, url],
|
||||
capture_output=True, timeout=60)
|
||||
if out.returncode != 0:
|
||||
raise
|
||||
return out.stdout.decode("utf-8", "replace")
|
||||
|
||||
def fetch(term):
|
||||
q = urllib.parse.urlencode({"term": term, "entity": "software", "limit": 5})
|
||||
url = f"https://itunes.apple.com/search?{q}"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.loads(r.read().decode("utf-8", "replace"))
|
||||
return json.loads(_http_get(url))
|
||||
|
||||
def shape(data):
|
||||
results = []
|
||||
|
|
|
|||
|
|
@ -11,11 +11,43 @@ else
|
|||
plugin_root="$(cd "$script_dir/../../.." && pwd)"
|
||||
fi
|
||||
|
||||
# Sibling RE plugin lives next to clone-app under plugins/
|
||||
re_scripts="$(cd "$plugin_root/.." 2>/dev/null && pwd)/android-reverse-engineering/skills/android-reverse-engineering/scripts"
|
||||
# Locate the RE plugin's scripts dir. The tail is always
|
||||
# .../android-reverse-engineering/skills/android-reverse-engineering/scripts,
|
||||
# but the parent layout differs by install:
|
||||
# - repo / dev checkout: plugins/android-reverse-engineering/skills/... (flat sibling)
|
||||
# - plugin cache: cache/<marketplace>/android-reverse-engineering/<version>/skills/...
|
||||
# so we probe a list of candidate globs and take the first match (highest
|
||||
# version when several exist).
|
||||
tail="skills/android-reverse-engineering/scripts"
|
||||
# `|| true`: under `set -e`, a bare `var=$(cmd)` whose cmd fails (e.g. the dir
|
||||
# doesn't exist) aborts the script. We want to fall through to the cache globs.
|
||||
parent="$(cd "$plugin_root/.." 2>/dev/null && pwd || true)"
|
||||
|
||||
if [[ ! -d "$re_scripts" ]]; then
|
||||
echo "ERROR: android-reverse-engineering scripts not found at: $re_scripts" >&2
|
||||
candidates=()
|
||||
# 1. flat sibling next to clone-app (repo/dev layout)
|
||||
[[ -n "$parent" ]] && candidates+=("$parent/android-reverse-engineering/$tail")
|
||||
# 2. versioned sibling under the same marketplace dir (cache layout)
|
||||
[[ -n "$parent" ]] && candidates+=("$parent"/android-reverse-engineering/*/"$tail")
|
||||
# 3. anywhere under the Claude plugin cache (any marketplace, any version)
|
||||
cache_root="${CLAUDE_PLUGIN_CACHE:-$HOME/.claude/plugins/cache}"
|
||||
candidates+=("$cache_root"/*/android-reverse-engineering/*/"$tail")
|
||||
candidates+=("$cache_root"/*/android-reverse-engineering/"$tail")
|
||||
|
||||
re_scripts=""
|
||||
for cand in "${candidates[@]}"; do
|
||||
# glob entries that didn't match stay literal (with '*'); skip those.
|
||||
[[ "$cand" == *"*"* ]] && continue
|
||||
[[ -d "$cand" ]] || continue
|
||||
# Prefer the highest version: keep the lexically-greatest matching path.
|
||||
if [[ -z "$re_scripts" || "$cand" > "$re_scripts" ]]; then
|
||||
re_scripts="$cand"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$re_scripts" || ! -d "$re_scripts" ]]; then
|
||||
echo "ERROR: android-reverse-engineering scripts not found. Looked for:" >&2
|
||||
echo " - $parent/android-reverse-engineering/$tail (sibling)" >&2
|
||||
echo " - $cache_root/*/android-reverse-engineering/*/$tail (cache)" >&2
|
||||
echo "Install it: /plugin install android-reverse-engineering@android-reverse-engineering-skill" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -4,24 +4,45 @@
|
|||
Primary source is the embedded ld+json SoftwareApplication block (stable).
|
||||
Falls back to light regex for installs/updated which aren't always in ld+json.
|
||||
"""
|
||||
import sys, json, re, argparse, urllib.request
|
||||
import sys, json, re, argparse, urllib.request, ssl, subprocess, shutil
|
||||
|
||||
KEYS = ["package", "title", "rating", "rating_count", "installs",
|
||||
"category", "developer", "updated", "source"]
|
||||
|
||||
UA = "Mozilla/5.0"
|
||||
|
||||
def _http_get(url):
|
||||
"""GET a URL as text. Try urllib first; on an SSL trust failure (common on
|
||||
macOS' system Python, which ships without a CA bundle) fall back to the
|
||||
system `curl`, which uses the OS trust store. Stdlib-only, no pip."""
|
||||
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return r.read().decode("utf-8", "replace")
|
||||
except urllib.error.URLError as e:
|
||||
if not isinstance(e.reason, ssl.SSLError):
|
||||
raise
|
||||
if not shutil.which("curl"):
|
||||
raise
|
||||
out = subprocess.run(
|
||||
["curl", "-sL", "--fail", "-A", UA, url],
|
||||
capture_output=True, timeout=60)
|
||||
if out.returncode != 0:
|
||||
raise
|
||||
return out.stdout.decode("utf-8", "replace")
|
||||
|
||||
def fetch(package):
|
||||
url = f"https://play.google.com/store/apps/details?id={package}&hl=en&gl=US"
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return r.read().decode("utf-8", "replace")
|
||||
return _http_get(url)
|
||||
|
||||
def parse(html, package):
|
||||
out = {k: None for k in KEYS}
|
||||
out["package"] = package
|
||||
out["source"] = "google-play"
|
||||
|
||||
# ld+json SoftwareApplication block
|
||||
for m in re.finditer(r'<script type="application/ld\+json">(.*?)</script>',
|
||||
# ld+json SoftwareApplication block. The <script> tag may carry extra
|
||||
# attributes (e.g. a CSP nonce="..."), so match any attributes up to '>'.
|
||||
for m in re.finditer(r'<script type="application/ld\+json"[^>]*>(.*?)</script>',
|
||||
html, re.DOTALL):
|
||||
try:
|
||||
data = json.loads(m.group(1).strip())
|
||||
|
|
@ -42,13 +63,19 @@ def parse(html, package):
|
|||
except Exception: pass
|
||||
break
|
||||
|
||||
# installs (e.g. "1,000,000+")
|
||||
m = re.search(r'([\d,]+\+)\s*<span>\s*Downloads', html)
|
||||
# installs (e.g. "1,000,000+"). Play wraps the count and the "Downloads"
|
||||
# label in their own elements; the label may be a <span> or a <div>.
|
||||
m = re.search(r'([\d,]+\+)\s*</[^>]+>\s*<[^>]*>\s*Downloads', html)
|
||||
if not m:
|
||||
m = re.search(r'([\d,]+\+)\s*<span>\s*Downloads', html)
|
||||
if m:
|
||||
out["installs"] = m.group(1)
|
||||
|
||||
# updated date (e.g. "Updated on<span>Jun 1, 2026</span>")
|
||||
m = re.search(r'Updated on\s*<span>([^<]+)</span>', html)
|
||||
# updated date (e.g. "Updated on</div><div ...>Jun 1, 2026</div>" — the
|
||||
# label and value live in adjacent elements that may be <span> or <div>).
|
||||
m = re.search(r'Updated on\s*</[^>]+>\s*<[^>]*>([^<]+)</[^>]+>', html)
|
||||
if not m:
|
||||
m = re.search(r'Updated on\s*<span>([^<]+)</span>', html)
|
||||
if m:
|
||||
out["updated"] = m.group(1).strip()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
<html><head><title>Example App - Apps on Google Play</title>
|
||||
<script type="application/ld+json">
|
||||
{"@type":"SoftwareApplication","name":"Example App","author":{"name":"Example Studio"},
|
||||
<script type="application/ld+json" nonce="5kflo12lNMmEYHKRtMGqHQ">
|
||||
{"@context":"https://schema.org","@type":"SoftwareApplication","name":"Example App","author":{"name":"Example Studio"},
|
||||
"aggregateRating":{"ratingValue":"4.3","ratingCount":"12873"},
|
||||
"applicationCategory":"GAME_PUZZLE"}
|
||||
</script>
|
||||
</head><body>
|
||||
<div>Updated on<span>Jun 1, 2026</span></div>
|
||||
<div>1,000,000+<span>Downloads</span></div>
|
||||
<div class="reAt0">Updated on</div><div class="xg1aie">Jun 1, 2026</div>
|
||||
<div class="ClM7O">1,000,000+</div><div class="g1rdde">Downloads</div>
|
||||
</body></html>
|
||||
|
|
|
|||
|
|
@ -12,10 +12,20 @@ check "ends with scripts dir" "android-reverse-engineering/skills/android-revers
|
|||
check "decompile.sh exists under resolved dir" "yes" \
|
||||
"$([[ -f "$out/decompile.sh" ]] && echo yes || echo no)"
|
||||
|
||||
# When CLAUDE_PLUGIN_ROOT points somewhere with no sibling RE plugin → exit 1
|
||||
# When the sibling is absent but the RE plugin lives in the Claude plugin cache
|
||||
# (the real install layout: cache/<marketplace>/android-reverse-engineering/<version>/...),
|
||||
# the resolver discovers it there. Point both env vars at temp dirs: the cache
|
||||
# var at a fake versioned layout, the root at a dir with no sibling.
|
||||
tmp="$(mktemp -d)"
|
||||
out2="$(CLAUDE_PLUGIN_ROOT="$tmp/clone-app" bash "$SCRIPT" 2>/dev/null)"; rc2=$?
|
||||
check "exit 1 when RE missing" "1" "$rc2"
|
||||
fake="$tmp/cache/some-marketplace/android-reverse-engineering/9.9.9/skills/android-reverse-engineering/scripts"
|
||||
mkdir -p "$fake"; : > "$fake/decompile.sh"
|
||||
out2="$(CLAUDE_PLUGIN_ROOT="$tmp/clone-app" CLAUDE_PLUGIN_CACHE="$tmp/cache" bash "$SCRIPT" 2>/dev/null)"; rc2=$?
|
||||
check "exit 0 when RE only in cache" "0" "$rc2"
|
||||
check "resolves the cached scripts dir" "$fake" "$out2"
|
||||
|
||||
# When neither a sibling nor any cache entry exists → exit 1
|
||||
out3="$(CLAUDE_PLUGIN_ROOT="$tmp/clone-app" CLAUDE_PLUGIN_CACHE="$tmp/empty" bash "$SCRIPT" 2>/dev/null)"; rc3=$?
|
||||
check "exit 1 when RE missing everywhere" "1" "$rc3"
|
||||
rm -rf "$tmp"
|
||||
|
||||
exit $fail
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ def main():
|
|||
check("developer", d["developer"] == "Example Studio")
|
||||
check("category", d["category"] == "GAME_PUZZLE")
|
||||
check("installs", d["installs"] == "1,000,000+")
|
||||
check("updated", d["updated"] == "Jun 1, 2026")
|
||||
check("source", d["source"] == "google-play")
|
||||
# all expected keys present even if null
|
||||
for k in ["package","title","rating","rating_count","installs","category","developer","updated","source"]:
|
||||
|
|
|
|||
Loading…
Reference in New Issue