Skip to content

Commit 1686247

Browse files
committed
1.0.0-gamma.7: source images, preview isolation and diagnostics (from 30636a1)
1 parent 42b5540 commit 1686247

36 files changed

Lines changed: 1744 additions & 120 deletions

‎.claude-plugin/marketplace.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"source": "./",
1515
"displayName": "html2wp — HTML to WordPress",
1616
"description": "BETA. Convert a static HTML site into a standalone WordPress block theme, verified against the original at three viewport widths — and reviewed by you before handover. The conversion runs through a verified Astro 5 build, so the Astro project is a real output too — compared pixel by pixel against the source before anything WordPress-shaped is generated.",
17-
"version": "1.0.0-gamma.6",
17+
"version": "1.0.0-gamma.7",
1818
"author": {
1919
"name": "BELNEM s.r.o.",
2020
"email": "hello@html2wp.dev",

‎.claude-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "html2wp",
33
"displayName": "html2wp — HTML to WordPress",
44
"description": "BETA. Convert a static HTML site — or anything that builds to one (Lovable, Bolt, v0, Next export) — into a standalone WordPress block theme: verified pixel by pixel, pages, blog, forms, menus, SEO and redirects, with no plugin required and verification gates at every stage. Two modes: Full (every gate repaired until green, then your own page-by-page review) and Flash (every stage once, no repair loop, the gates reported). The gates catch most fidelity drift, not all of it.",
5-
"version": "1.0.0-gamma.6",
5+
"version": "1.0.0-gamma.7",
66
"author": {
77
"name": "BELNEM s.r.o.",
88
"email": "hello@html2wp.dev",

‎.codex-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "html2wp",
3-
"version": "1.0.0-gamma.6",
3+
"version": "1.0.0-gamma.7",
44
"description": "Convert static HTML or buildable web projects into verified, editable WordPress block themes.",
55
"author": {
66
"name": "BELNEM s.r.o.",

‎VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.0-gamma.6
1+
1.0.0-gamma.7

‎skills/html2wp/SKILL.md‎

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,7 +632,7 @@ container has no `apply_patch` command.
632632
| mode | `progress.sh mode flash` | — | — |
633633
| -4 | `whats-here.sh {workspace}` (a finished or part-finished workspace: follow its route — a Continue resumes, never restarts); `allowance.sh` (print its line verbatim); `detect-project.py <project> --out {workspace}/detect.json` | — | kind `none`; a site over the page allowance (say it, with the service's words) |
634634
| -3 | `$S/check-prereqs.sh` — in the app exactly `/opt/html2wp/skills/html2wp/assets/scripts/check-prereqs.sh` | — | a missing tool (it prints what to install) |
635-
| -1 | by kind — `static-html`: `rsync -a --exclude .git --exclude node_modules --exclude .html2wp <root>/ {input}/` — `<root>` is detect.json's `root`, never `<project>` itself (a site nested in a folder, `<project>/<name>/index.html`, is taken from that folder); `web-app`: `prerender-spa.py --project <project> --out {input} --no-verify --flash` (a group of pages — a route table's `/product/:id`, or pages sharing a path with one varying last segment like a TanStack `/blog/<slug>` — is recorded once, on its first page, and each other page shares that recording only when its controls are the same; the pages are captured in `--jobs` browsers, entrance motion is waited for at most 6 s; it says "N of M" as it goes); `static-site`: `static-site.py --project <project> --out {input}` (exit 3 → `prerender-spa.py … --no-verify --flash` instead, once); `html2wp-astro`: `detect-project.py <project> --prepare {workspace}` | — | no pages written (exit 2; 1 from static-site.py) |
635+
| -1 | by kind — `static-html`: `rsync -a --exclude .git --exclude node_modules --exclude .html2wp <root>/ {input}/`, then `prepare-source-assets.py --input {input} --project <root>` — `<root>` is detect.json's `root`, never `<project>` itself (a site nested in a folder, `<project>/<name>/index.html`, is taken from that folder); `web-app`: `prerender-spa.py --project <project> --out {input} --no-verify --flash` (a group of pages — a route table's `/product/:id`, or pages sharing a path with one varying last segment like a TanStack `/blog/<slug>` — is recorded once, on its first page, and each other page shares that recording only when its controls are the same; the pages are captured in `--jobs` browsers, entrance motion is waited for at most 6 s; it says "N of M" as it goes); `static-site`: `static-site.py --project <project> --out {input}` (exit 3 → `prerender-spa.py … --no-verify --flash` instead, once); `html2wp-astro`: `detect-project.py <project> --prepare {workspace}` | — | no pages written (exit 2; 1 from static-site.py) |
636636
| 0 | `cp -a {input} {workspace}/input-untouched`; `analyze-input.mjs {input} --out={workspace}/analysis.json`; `flash-manifest.py --analysis {workspace}/analysis.json --input {input} --workspace {workspace}`; decide (below); `check-manifest.py --manifest={workspace}/conversion-manifest.json --flash` | — | analyze exit 2 (refusal, key collision); check-manifest still red after its lever |
637637
| -1b | a shop: `capture-commerce-specimen.py --dist {input} --out {workspace}/style-specimens`; no shop: `skip` (it comes after stage 0 because stage 0 decides the shop) | a nonzero exit | — |
638638
| 0.5 | `optimize-images.py --input {input} --remote --apply --out {workspace}/optimize-images-report.json` (`skip` for `html2wp-astro`) | a nonzero exit | — |
@@ -1283,6 +1283,39 @@ prints `https://example.com` as an example) is reported as an uncaptured
12831283
address. Separating prose URLs from asset references is not something a
12841284
regex can do reliably, and suppressing the class would hide real misses.
12851285

1286+
## Source images — recover before the baseline (Flash, Full and Astro)
1287+
1288+
The plugin owns source-image recovery in HTML and standalone Astro conversions.
1289+
Restored images go into Astro `public/` and the built `dist/`; the Astro ZIP
1290+
includes both, so the next build keeps the image. `prerender-spa.py` and `static-site.py`
1291+
run it automatically on an isolated build copy **before** capture/flattening.
1292+
`detect-project.py --prepare` also carries recovered images into an imported
1293+
Astro project's `public/`, so the next build keeps them. For **static HTML**,
1294+
first copy the site into `{input}`, then run this **before** `input-untouched`:
1295+
1296+
```bash
1297+
python3 assets/scripts/prepare-source-assets.py --input {input} --project <source-root>
1298+
```
1299+
1300+
The helper reads used `*.asset.json` descriptors and missing local image
1301+
references in HTML/CSS. It restores the image at its original local URL from
1302+
an exact, unambiguous local asset, or from a **known original HTTPS origin**.
1303+
Pass `--asset-origin https://original.example` to the preparation command, or
1304+
set `H2WP_ASSET_ORIGIN` (also used by imported Astro preparation). Use only an
1305+
origin provided by the owner or verified project context; never guess one
1306+
from a project name, repository URL, R2 key, canonical or social link. A
1307+
relative `/__l5e/assets-v1/...` path alone does not identify its origin.
1308+
Absolute hotlinks continue through stage 0.5's `--remote` localizer.
1309+
1310+
Read `source-assets-report.json`: recovered files include source, SHA-256 and
1311+
size; unresolved entries name the missing images and reasons. Missing source
1312+
images are **content failures**, even when pixels agree because the image is
1313+
missing on both sides. Report them prominently in the hand-over; do not say
1314+
all checks passed or silently replace them with unrelated stock images.
1315+
An unresolved image does not withhold an otherwise valid ZIP. If no original
1316+
file or accessible origin exists, state exactly what source is needed.
1317+
`--gates-only` never downloads images or changes either comparison input.
1318+
12861319
## Stage -1 — SPA → static HTML (only when the input is an app)
12871320

12881321
```
@@ -1478,7 +1511,7 @@ structure AND zero stylesheet overlap). **You decide**, and write
14781511
later stage reads, local and remote alike.
14791512

14801513
```
1481-
cp -a <input-dir> {workspace}/input-untouched # U — ALWAYS, before anything else touches the input
1514+
cp -a <input-dir> {workspace}/input-untouched # U — after source-image recovery, before optimization
14821515
```
14831516

14841517
**Take the untouched copy on every conversion, before stage 0.5.** Stages 0.5,
@@ -2544,6 +2577,23 @@ assets/scripts/test-env.sh clone <slug> <copy> # second WP, byte-copy of <slu
25442577
assets/scripts/test-env.sh down <slug> # tear down when done
25452578
```
25462579

2580+
The user's preview keeps Visual Edit active. `install-theme.py` reuses an
2581+
active editor, activates an installed inactive Lite through the admin UI, or
2582+
uploads and activates the supplied ZIP when absent. `ensure-preview-editor.py
2583+
--env <state>` restores an installed editor during preview lifecycle actions,
2584+
verifies its active state, and refuses unproven ownership. An active Pro editor
2585+
is preserved. Do not deactivate Visual Edit for comparison screenshots: those
2586+
captures use an anonymous browser context. An absent editor or activation
2587+
failure must be reported, never described as active.
2588+
2589+
Preview ownership belongs to the canonical `H2WP_WORKSPACE` (or the current
2590+
working directory when unset) **and** `H2WP_CONTAINER`, never the theme slug
2591+
alone. State and the default manifest are read from that workspace even when
2592+
called from its parent. Use these helpers; never delete previews by slug or
2593+
run a global Docker prune. They refuse foreign ownership and preserve legacy
2594+
orphans whose owner cannot be proven. Keep the delivered preview running in
2595+
app mode; an exported ZIP survives independently of its WordPress containers.
2596+
25472597
`up` runs on a UNIQUE `docker compose` project (`h2wp-<slug>-<runid>`), finds
25482598
its own free host port (real allocation via Docker, not an assumed `8082`),
25492599
installs wp-cli and WordPress itself (the base `wordpress:latest` image ships
@@ -3503,6 +3553,7 @@ assets/scripts/mirror-live.py stage -2 (live URL → dist-shaped mirror +
35033553
assets/scripts/prerender-spa.py stage -1 (SPA → flat HTML + gates -1/-1b;
35043554
records disclosures/scroll state, emits
35053555
assets/spa-runtime.js)
3556+
assets/scripts/prepare-source-assets.py recover missing local source images on a working copy
35063557
assets/scripts/static-site.py stage -1 for a static-site generator
35073558
(Astro): builds it sandboxed and takes its
35083559
own pages, no browser; exit 3 = use
@@ -3657,3 +3708,18 @@ assets/scripts/lib/ shared helpers the scripts import
36573708
stamping, the decorative lead a header part
36583709
adopts) — read them before changing a caller
36593710
```
3711+
3712+
3713+
### User-requested comparison refresh and execution metadata
3714+
3715+
`visual-compare.py {workspace} --page-key <manifest-key>` captures only that
3716+
page at the requested widths, then replaces its row in the existing comparison
3717+
index. Other pages remain intact. A failed requested capture retains the old
3718+
index. Omit the option to capture all pages. The original input/manifest is
3719+
never rewritten by this operation.
3720+
3721+
The desktop app records actual accepted model selections and turn times in
3722+
`{workspace}/../.app/model-history.json`. `write-result.py` includes the recorded
3723+
models, reasoning effort, elapsed time including pauses, and measured agent
3724+
turn time in result.json, exported Markdown and PDF. Missing history is unknown,
3725+
not guessed. Optional metadata must never block delivery.

‎skills/html2wp/VERSION‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.0.0-gamma.6
1+
1.0.0-gamma.7

‎skills/html2wp/assets/scripts/cleanup.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ keep() { # a name is kept if it matches anything in the keep list
128128
esac
129129
[ "$MINIMAL" = "1" ] && return 1
130130
case "$1" in
131-
conversion-manifest.json|astro-report.json|FINDINGS.md|visual-review) return 0 ;;
131+
conversion-manifest.json|astro-report.json|source-assets-report.json|FINDINGS.md|visual-review) return 0 ;;
132132
block-plan|.gutenberg|gutenberg-routes.json) return 0 ;;
133133
theme|theme-patches|theme-patches-*) return 0 ;; # editable output and replay/rollback history
134134
result.json|progress.json|changes.json|.theme-applied.json|out) return 0 ;;

‎skills/html2wp/assets/scripts/compare-pages.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
ap.add_argument("--out", default="")
4343
ap.add_argument("--width", type=int, default=1440)
4444
ap.add_argument("--jobs", type=int, default=1, help="pages captured at once, one Chromium each (default 1)")
45+
ap.add_argument("--page-key", default="", help="capture only this manifest page key")
4546
ap.add_argument("--_pages", default="", help=argparse.SUPPRESS)
4647
ap.add_argument("--_partial", default="", help=argparse.SUPPRESS)
4748
args = ap.parse_args()
@@ -330,7 +331,9 @@ def v2_url(entry):
330331
return got
331332

332333

333-
EVERY = range(len(MF["pages"]))
334+
EVERY = [i for i, p in enumerate(MF["pages"]) if not args.page_key or p.get("key") == args.page_key]
335+
if args.page_key and not EVERY:
336+
raise SystemExit("Unknown comparison page key")
334337

335338
if args._partial:
336339
# A worker of a --jobs run: capture its share, hand it back, write nothing else.
@@ -347,7 +350,7 @@ def v2_url(entry):
347350
# Self-spawned rather than multiprocessing: this file has no __main__
348351
# guard. Shares are dealt round-robin; the manifest is assembled below
349352
# in page order, whatever order the workers finish in.
350-
n = min(args.jobs, len(MF["pages"]))
353+
n = min(args.jobs, len(EVERY))
351354
tmp = Path(tempfile.mkdtemp(prefix="compare-pages-"))
352355
workers = []
353356
for k in range(n):

‎skills/html2wp/assets/scripts/detect-project.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,18 @@ def prepare_astro_export(root, workspace):
166166
shutil.rmtree(dest)
167167
shutil.copytree(src, dest, symlinks=False,
168168
ignore=shutil.ignore_patterns("node_modules", ".git", "__MACOSX", ".DS_Store", "._*"))
169+
sys.path.insert(0, str(Path(__file__).resolve().parent / 'lib'))
170+
from source_assets import recover, safe_target
171+
from urllib.parse import quote
172+
assets = recover(workspace / 'static-src', root,
173+
report_path=workspace / 'source-assets-report.json')
174+
for entry in assets['recovered']:
175+
# Keep the restored file through the imported Astro project's next build.
176+
for output in ('public', 'dist'):
177+
target, _ = safe_target(workspace / 'astro-project' / output, '/' + quote(entry['file'], safe='/'))
178+
target.parent.mkdir(parents=True, exist_ok=True)
179+
if not target.exists():
180+
shutil.copyfile(workspace / 'static-src' / entry['file'], target)
169181
shutil.copyfile(root / ASTRO_REPORT, workspace / "astro-report.json")
170182
carried = workspace / "astro-project/.html2wp"
171183
if carried.exists():
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python3
2+
"""Restore an installed editor in this workspace's own preview. No download."""
3+
import argparse
4+
import json
5+
import os
6+
from pathlib import Path
7+
import sys
8+
sys.path.insert(0, str(Path(__file__).resolve().parent / 'lib'))
9+
from preview_owner import owned
10+
from preview_editor import ensure
11+
12+
13+
def main():
14+
ap = argparse.ArgumentParser(description=__doc__)
15+
ap.add_argument('--env', required=True)
16+
args = ap.parse_args()
17+
state = Path(args.env).absolute()
18+
workspace = Path(os.environ.get('H2WP_WORKSPACE') or state.parent).resolve()
19+
data = json.loads(state.read_text())
20+
if not owned(workspace, os.environ.get('H2WP_CONTAINER', ''), data.get('project', ''), state):
21+
raise RuntimeError('Visual Edit: preview ownership could not be verified')
22+
result = ensure(data['wpContainer'])
23+
print(json.dumps(result))
24+
return 0
25+
26+
if __name__ == '__main__':
27+
try:
28+
sys.exit(main())
29+
except Exception as error:
30+
print(str(error), file=sys.stderr)
31+
sys.exit(1)

0 commit comments

Comments
 (0)