From ec96bbd7d4924f2832d56513f830671ff1310a06 Mon Sep 17 00:00:00 2001 From: "fatih.bulut" Date: Thu, 25 Jun 2026 02:01:47 +0300 Subject: [PATCH] =?UTF-8?q?docs(clone-app):=20implementation=20plan=20?= =?UTF-8?q?=E2=80=94=20design=20capture,=20build=20spec,=20Unity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...clone-app-design-capture-and-build-spec.md | 1196 +++++++++++++++++ 1 file changed, 1196 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-25-clone-app-design-capture-and-build-spec.md diff --git a/docs/superpowers/plans/2026-06-25-clone-app-design-capture-and-build-spec.md b/docs/superpowers/plans/2026-06-25-clone-app-design-capture-and-build-spec.md new file mode 100644 index 0000000..5db8c97 --- /dev/null +++ b/docs/superpowers/plans/2026-06-25-clone-app-design-capture-and-build-spec.md @@ -0,0 +1,1196 @@ +# clone-app: Design Capture + Standalone Build Spec (+ Unity/Game Support) — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make `clone-app` capture the target app's design system + screenshots + (for Unity games) C# type model and game assets, and emit one standalone `clone-build-spec.md` so a fresh session can build a pixel-perfect, production-ready clone. + +**Architecture:** Add three helper scripts (`extract-design.py`, `detect-unity.sh`, plus two thin .NET-tool wrappers `il2cpp-dump.sh` / `unity-assets.sh`), extend `scrape-play-store.py` to emit screenshots, add three reference rubrics, update two existing references, and extend `SKILL.md` with design-extraction + Unity branches in Phase 2, screenshot download in Phase 3, and a new Phase 8 that assembles the build spec before invoking `writing-plans`. Design/Unity extraction runs inside the existing isolated Phase-2 RE subagent so decompiled sources/assets never flood the orchestrator context. + +**Tech Stack:** bash 4+ (`#!/usr/bin/env bash`, run with `bash `), Python 3 stdlib-only (`xml.etree.ElementTree`, `json`, `re`, `urllib`, `zipfile`), external .NET tools driven via wrappers (Il2CppInspectorRedux, AssetRipper, ILSpy `ilspycmd`). + +## Global Constraints + +- `plugins/android-reverse-engineering/` is vendored upstream — MUST stay byte-identical. `git status --porcelain plugins/android-reverse-engineering/` MUST print nothing. All new logic lives under `plugins/clone-app/`. +- Python is stdlib-only: no pip, no virtualenv. +- Scripts use `#!/usr/bin/env bash`; bash tests use `set -uo pipefail` (not `-e`) and aggregate failures into a `fail` var so every assertion runs. +- Python scrapers/parsers are tested offline against `tests/fixtures/` via file flags — never hitting the network. +- Working dir is `./work//` relative to the user's cwd, never inside the plugin. +- Effort is measured in "AI Sprints", never calendar time. +- Conventional Commits scoped to the plugin: `feat(clone-app): …`, `test(clone-app): …`, `docs(clone-app): …`. +- External .NET tools are NOT bundled; scripts detect them and degrade gracefully with install guidance when absent. +- New `test-*.sh` / `test-*.py` files under `tests/` are auto-discovered by `tests/run-all.sh` (it globs) — no `run-all.sh` edit needed. + +--- + +### Task 1: `extract-design.py` — design tokens from decompiled resources + +**Files:** +- Create: `plugins/clone-app/skills/clone-app/scripts/extract-design.py` +- Create: `plugins/clone-app/tests/fixtures/design-sample/res/values/colors.xml` +- Create: `plugins/clone-app/tests/fixtures/design-sample/res/values/dimens.xml` +- Create: `plugins/clone-app/tests/fixtures/design-sample/res/values/themes.xml` +- Create: `plugins/clone-app/tests/fixtures/design-sample/res/layout/activity_main.xml` +- Create: `plugins/clone-app/tests/fixtures/design-sample/res/font/inter_regular.ttf` +- Test: `plugins/clone-app/tests/test-extract-design.py` + +**Interfaces:** +- Consumes: nothing (first task). +- Produces: CLI `python3 extract-design.py [--framework F] [--out tokens.json] [--digest digest.md]`. Writes a `design-tokens.json` object (also printed to stdout when `--out` omitted) with top-level keys: `package` (always `null` here — caller fills), `source` (`"apk-resources"`), `framework`, `colors`, `dimens`, `typography`, `shapes`, `theme`, `icon`, `layouts`. Each of `colors/dimens/typography/shapes/theme/icon/layouts` is an object `{ "values": …, "confidence": "high"|"med"|"low" }`. + +- [ ] **Step 1: Write the failing test** + +Create `plugins/clone-app/tests/test-extract-design.py`: + +```python +#!/usr/bin/env python3 +import json, subprocess, sys, os + +HERE = os.path.dirname(os.path.abspath(__file__)) +SCRIPT = os.path.join(HERE, "..", "skills", "clone-app", "scripts", "extract-design.py") +ROOT = os.path.join(HERE, "fixtures", "design-sample") + +def run(): + out = subprocess.check_output([sys.executable, SCRIPT, ROOT]) + return json.loads(out) + +def main(): + d = run() + fails = [] + def check(name, cond): + print(f"{'PASS' if cond else 'FAIL'}: {name}") + if not cond: fails.append(name) + check("source", d["source"] == "apk-resources") + check("colors parsed", d["colors"]["values"].get("colorPrimary") == "#FF6200EE") + check("colors accent", d["colors"]["values"].get("colorAccent") == "#FF03DAC5") + check("colors confidence high", d["colors"]["confidence"] == "high") + check("dimens parsed", d["dimens"]["values"].get("spacing_small") == "8dp") + check("text size dimen", d["dimens"]["values"].get("text_size_body") == "14sp") + check("theme parent", d["theme"]["values"].get("parent") == "Theme.Material3.DayNight") + check("theme is_dark flag present", "is_dark" in d["theme"]["values"]) + check("fonts list", "inter_regular.ttf" in d["typography"]["values"]["fonts"]) + check("layout count", d["layouts"]["values"]["count"] == 1) + for k in ["package","source","framework","colors","dimens","typography","shapes","theme","icon","layouts"]: + check(f"key present: {k}", k in d) + sys.exit(1 if fails else 0) + +main() +``` + +- [ ] **Step 2: Create the fixtures** + +`plugins/clone-app/tests/fixtures/design-sample/res/values/colors.xml`: + +```xml + + + #FF6200EE + #FF03DAC5 + +``` + +`plugins/clone-app/tests/fixtures/design-sample/res/values/dimens.xml`: + +```xml + + + 8dp + 14sp + +``` + +`plugins/clone-app/tests/fixtures/design-sample/res/values/themes.xml`: + +```xml + + + + +``` + +`plugins/clone-app/tests/fixtures/design-sample/res/layout/activity_main.xml`: + +```xml + + +``` + +`plugins/clone-app/tests/fixtures/design-sample/res/font/inter_regular.ttf` — create as an empty placeholder file (content irrelevant; only the filename is inventoried): + +```bash +mkdir -p plugins/clone-app/tests/fixtures/design-sample/res/font +: > plugins/clone-app/tests/fixtures/design-sample/res/font/inter_regular.ttf +``` + +- [ ] **Step 3: Run test to verify it fails** + +Run: `python3 plugins/clone-app/tests/test-extract-design.py` +Expected: FAIL — `extract-design.py` does not exist (subprocess raises / non-zero). + +- [ ] **Step 4: Write the implementation** + +Create `plugins/clone-app/skills/clone-app/scripts/extract-design.py`: + +```python +#!/usr/bin/env python3 +"""Extract a design-token digest from a decompiled APK resource tree. + +Reads res/values/{colors,dimens,styles,themes}.xml, the res/font/ dir, the +drawable*/mipmap* inventory, and counts res/layout/*.xml. Stdlib-only. + +Emits a design-tokens.json object on stdout (or --out FILE) and, with --digest +FILE, a markdown design summary. Framework-aware: native apps have a rich XML +layer (confidence high); Compose keeps tokens in res/values but no layouts +(med); Flutter/React Native keep almost nothing in Android res (low) so the +caller leans on screenshots. Pass --framework to override the auto guess. +""" +import sys, os, json, re, argparse, glob +import xml.etree.ElementTree as ET + +def _find_res_dirs(root): + """All res/ dirs under root that contain a values/ subdir (jadx puts these + under /resources/res; apktool under /res — search either).""" + hits = [] + for p in glob.glob(os.path.join(root, "**", "res"), recursive=True): + if os.path.isdir(os.path.join(p, "values")) or glob.glob(os.path.join(p, "values*")): + hits.append(p) + return sorted(set(hits)) + +def _iter_value_files(res_dirs, basename): + for res in res_dirs: + for vdir in glob.glob(os.path.join(res, "values*")): + f = os.path.join(vdir, basename) + if os.path.isfile(f): + yield f + +def _parse_named(res_dirs, basename, tag): + """Collect value entries from every values*/basename.""" + out = {} + for f in _iter_value_files(res_dirs, basename): + try: + tree = ET.parse(f) + except ET.ParseError: + continue + for el in tree.getroot().iter(tag): + name = el.get("name") + if name and (el.text is not None): + out[name] = el.text.strip() + return out + +def _parse_themes(res_dirs): + """First