# Dev Environment & Debug Playbook How to stand up a working loop against **EasyEDA Pro** and iterate on the connector fast. Covers both the **desktop client** and the **web editor** (`pro.lceda.cn/editor`), plus a **hot-reload** trick that skips the uninstall→re-import dance entirely. Companion docs: [connector-contract.md](connector-contract.md) (the wire protocol & runtime constraints) and [protocol.md](protocol.md) (message envelopes). This file is the operational "how do I run it" guide. ## The AI-agent automation testing loop (what this whole setup is for) The pieces below combine into one repeatable loop an AI agent (or you) runs to develop + regression-test the connector against a LIVE editor, hands-free: 1. **Bootstrap the live environment** — with a browser-control tool (chrome-devtools MCP) the agent opens the web editor, opens a project by `#id=`, and waits for the connector to attach — no manual clicks. The agent-facing SOP (exact steps, retries, pitfalls) is [`skills/easyeda-agent/references/environment-setup.md`](../skills/easyeda-agent/references/environment-setup.md); §1–4 below are the manual equivalent. 2. **Iterate the connector fast** — edit `extension/src`, `make eext`, then **hot-reload** into the running editor via IndexedDB (§5) — no uninstall / re-import / restart. Go/daemon changes hot-reload via `air` (`make dev`). 3. **Verify against real geometry, not tests alone** — drive the typed actions against the live board; judge by data (`sch/pcb list`, `drc`, `check`, `layout-lint`), never by a screenshot alone (stale/blank-frame traps, Core Rule 7 in the skill). "Builds + unit tests pass" ≠ "works live" — several bugs this project shipped only surfaced on a real board. 4. **Probe → file → fix → merge → re-verify** — run a real design task as the probe (the fixed regression case is [`esp32MiniRequire.md`](../esp32MiniRequire.md); the copy-a-golden-board harness is `training/copy-check.py`), file each gap as a GitHub issue labeled `ready-for-agent`, let ClawFlow implement + open a PR, then **you merge and live-verify** (the operator can't do runtime acceptance — see the advisory-loop section at the bottom). The rolling gap/roadmap ledger is [`optimization-loop.md`](optimization-loop.md). ## TL;DR loop ``` make dev # daemon with air hot-reload (rebuilds on .go change) # open EasyEDA (desktop or web), open a project, import the .eext once, # enable "Allow external interaction" easyeda daemon health # windows[] should list your connected editor easyeda project info --project # first typed action round-trip ``` ## Prerequisites - Go toolchain + `make` (see repo root `Makefile`). - `air` for daemon hot-reload: `go install github.com/air-verse/air@latest`. - Node ≥ 20 for building the connector `.eext` (`make eext`); run `npm install` in `extension/` once (brings `ws`, used by the hot-reload server). - EasyEDA Pro: desktop client **or** the web editor `https://pro.lceda.cn/editor`. - For the hands-free agent loop (optional): a **chrome-devtools MCP** server (drives the web editor + runs the hot-reload inject) and the **`gh` CLI** (the probe → issue → PR → merge cycle). Without them the manual §1–5 flow still works. ## 1. Start the daemon ```bash make dev # air live-reload; leave running in a terminal # or one-shot: ./bin/easyeda daemon start & ./bin/easyeda daemon health # status=found; windows[] empty until a connector attaches ``` The daemon listens on `127.0.0.1:60832-60841` (`0xEDA0`-`0xEDA9`) and speaks the handshake in [connector-contract.md](connector-contract.md). ## 2. Open a project (this step is manual) The connector activates via `onStartupFinished`, which only fires once a project is open in the editor. **The CLI cannot open a not-yet-open project** — that first open is always done in the editor UI: - Desktop: File → Open, or pick a recent project. - Web: double-click a project in the list, or navigate to `https://pro.lceda.cn/editor#id=`. Once open, document switching *is* scriptable: ```bash easyeda doc ls --project # list pages/PCBs, ★ = active easyeda doc switch --project ``` ## 3. Import the connector + grant permission (first time per install) 1. **高级(A) → 扩展管理器(E)… → 导入** and pick `dist/easyeda-agent-connector.eext`. 2. A security prompt appears: external-interaction permission is **disabled by default**. Confirm it. 3. Enable the permission: in the connector's detail page, open the **配置 (Config)** tab and tick **允许外部交互 (Allow external interaction)**. Without it, every `eda.sys_WebSocket` call throws (see connector-contract.md). After enabling, the connector's watchdog reconnects within a few seconds — no manual Reconnect needed. ## 4. Confirm the round-trip ```bash easyeda daemon health # windows[] now lists connectorVersion / easyedaVersion / context easyeda project info --project # structured data flows back easyeda notify --project --message "hi" --type success # visible toast in the editor ``` Prefer `--project ` over `--window `: it routes by project and survives windowId churn across reconnects. ## 5. Hot-reload the connector (skip uninstall → re-import) Re-importing a `.eext` is slow and error-prone: EasyEDA dedups by UUID, so you must uninstall the old one first (a version bump alone silently fails), and an already-open window keeps running the OLD code until reloaded. There's a much faster path once you know where EasyEDA keeps installed extensions. ### Where extensions live: IndexedDB EasyEDA Pro stores every installed extension in the browser's **IndexedDB**, not on disk. This is true for both the web editor and the Electron desktop client (the desktop app is a Chromium webview). - **Database**: `User__v6` (the `teamUuid` is the one returned by `easyeda project info`). - **Object stores**: - `extensionsIndex` — one record per extension, keyed by `uuid`. Fields: `config` (the parsed `extension.json`), `isEnable`, `isAllowExternalInteractions` (yes — the permission is just this boolean), `fileIndex`, `fileSize`. - `extensionsObjectStorage` — one record per file, key = `|`, with `source` being a `File` object. The connector's only executable file is `|dist/index.js`. - `extensionsUserConfig`, `standaloneScript`. ### The trick Overwrite the `|dist/index.js` record with a freshly-built bundle, bump `config.version` in `extensionsIndex`, then reload the page. EasyEDA re-reads the extension from IndexedDB on load, so the new code runs — **no uninstall, no re-import, no file dialogs**. ### Transport constraint The editor is HTTPS, so `fetch('http://127.0.0.1/...')` is blocked as mixed content. But `ws://127.0.0.1` is allowed (that's exactly how the connector reaches the daemon). So move the new bundle over a **local WebSocket**, not HTTP. ### Steps Two committed scripts do this end to end — no external tooling needed: `extension/scripts/hot-reload-server.mjs` (the WS file server) and `extension/scripts/hot-reload-inject.js` (the browser-side IndexedDB writer). ```bash # 0. One-time: install the ws devDependency the server uses. (cd extension && npm install) # 1. Rebuild the connector after editing its source (bumps the patch version). make eext # recompiles extension/dist/index.js # 2. Serve the fresh bundle over a local WebSocket (one-shot; exits after serving). node extension/scripts/hot-reload-server.mjs & # ws://127.0.0.1:8790 # 3. In the EDITOR PAGE, run hot-reload-inject.js — paste into the browser console, # or (agent-driven) pass its body to a chrome-devtools MCP evaluate_script call. # Fill in TEAM (teamUuid, from `easyeda project info`), UUID + VERSION (from # extension/extension.json). It pulls the bundle over ws://, overwrites the # connector's |dist/index.js record + bumps config.version, then reloads. # 4. Verify the new code is live. easyeda daemon health # connectorVersion shows the new version ``` If the editor has unsaved/open library documents, Chrome may show “重新加载此网站?” after step 3. Confirm **重新加载** in the browser; until that dialog is handled the old connector's `debug.exec_js` request can remain at the head of its FIFO and later typed actions will correctly refuse with `CONNECTOR_QUEUE_BLOCKED`. After reload, wait for a new `windowId`/`connectedAt` in `easyeda daemon health` before issuing a write. The inject script writes the IndexedDB records described above; the server reads `extension/dist/index.js` + the version from `extension.json`. Both take flags (`--port`, `--bundle`; `--keep` to stay resident) — see the file headers. `connectorVersion` in `daemon health` is compiled into `index.js` (`CONNECTOR_VERSION`), so a changed value is proof the new bundle is running. For a same-version development build, `make connector` is sufficient; do not infer loaded code from the version alone. Compare the stored bundle hash with `extension/dist/index.js`, confirm a new window ID / connectedAt after reload, and run the changed action through the ordinary CLI. The inject script also works via `easyeda --project ceshi debug exec` in the in-app browser (verified 2026-09-08): this avoids browser-control timeouts and manual reinstall. Save open schematic and PCB documents first. Prefer this committed workflow before asking the user to uninstall/import the connector. ### Minimal WS protocol (if you build your own transport) Server is request/response over a JSON WebSocket: - Client → `{"action":"getUpdatedFileTree","fileList":[]}` Server → `{"action":"getUpdatedFileTree_Response","fileList":[{path,id,hash,status},...]}` (empty `fileList` makes the server mark everything as `add`.) - Client → `{"action":"updateFile","file":{"path":"index.js","id":..,"hash":..}}` Server → `{"action":"updateFile_Response","success":true,"file":{path,hash,size,mtime,content}}` where `content` is base64. The IndexedDB key uses the connector's install path (`dist/index.js`), while the server's `dir` root maps to that dir's contents — so a server file `index.js` lands at IndexedDB key `|dist/index.js`. Mind that offset. ## Troubleshooting - **`daemon health` shows `windows: []`** — the connector isn't attached. Check: project actually open? permission "允许外部交互" enabled? For a freshly-imported connector in an already-open window, reload the page to trigger `onStartupFinished`. - **Permission toggle is hard to find** — it's not on the extension card; it's in the connector's **detail page → 配置 (Config)** tab. - **Uninstall confirm dialog is flaky to automate** — ticking the "I understand" checkbox re-renders the confirm button. Prefer the hot-reload path (section 5), which never uninstalls. - **`fetch` to the daemon/localhost fails in the editor** — expected; HTTPS→HTTP mixed content is blocked. Use `ws://127.0.0.1` (the connector and the hot-reload transport both rely on this being allowed). - **Automating the web editor via chrome-devtools**: it uses a dedicated Chrome profile. If it errors with "browser already running", a stale Chrome is holding that profile — kill the process using that `--user-data-dir` and let the tooling relaunch. The extension install persists in the profile, so you don't need to re-import after the relaunch. - **Network blocks a git host** (e.g. a corporate gateway blocking a mirror) — clone from a machine with open egress (a cloud VM) and copy the tree back. - **Connector port-scans forever (`WebSocket ... closed before ... established` on every port 60832–60841), `daemon health` shows a daemon but `windows: 0`** — usually TWO daemons fighting over the port: `air` restarted the daemon after a `.go` edit but a previous instance lingered, so `/health` answers on the port while the other instance's `/eda` WebSocket never completes the handshake. Diagnose + fix: ```bash pgrep -fl "easyeda daemon" # >1 line = orphans fighting lsof -iTCP:60832 -sTCP:LISTEN -n # which PID owns the port pkill -f "easyeda daemon"; sleep 2 # kill all, then start ONE clean: nohup ./bin/easyeda daemon start > /tmp/easyeda-daemon.log 2>&1 & ``` Then reload the editor page so the connector re-handshakes. (If you run the daemon under `make dev`/air, prefer letting air own it — but after a messy restart, a single clean `daemon start` is the reliable reset.) ## Notes - The connector persists in the browser/profile's IndexedDB, so it survives restarts of the editor. Wiping site data removes it. - The hot-reload approach touches EasyEDA-internal IndexedDB structures that are **not an official, stable API** — treat it as a dev-only convenience and re-verify the store names/keys if EasyEDA bumps its schema version (`_v6` today). ## Native net-label compatibility (verified 2026-09-05) EasyEDA's official `eda.sch_PrimitiveAttribute.createNetLabel(x, y, net)` API is documented as a **BETA** API and is marked **"ADD since EDA v4"**. The arguments are simply the schematic X/Y coordinate and a non-empty net name; there is no alternate direction/rotation argument. The connector's `net_label` implementation passes that signature correctly. Do not infer support from the editor UI alone: manual placement of a net label may work while the typed connector call hangs. In EasyEDA 3.2.186, a live `sch connect --kind net_label` probe timed out while awaiting the native promise and rolled back the stub wire, even with connector 1.3.1. The same page could still be made electrically correct by placing EN as a net label in the UI; the connector readback exposed the EN net name but did not enumerate the native label as `componentType: "netlabel"`. When diagnosing this class of issue, follow this order: 1. Run `easyeda health` and record the exact EasyEDA and connector versions. 2. Read the official API page and check its "ADD since"/BETA note before changing arguments. 3. Run a one-pin live probe, then immediately `sch read`/`sch check`; never assume a timeout means nothing landed. 4. If the native promise does not settle, restore the previous connection, save, and report a runtime-version compatibility gap. Upgrade EasyEDA to a runtime that supports EDA v4 before retrying automation. ## Advisory loop: what the implement operator can and cannot do ClawFlow's implement operator runs `claude -p` in a **headless worktree** — it has the repo source but **no live EasyEDA editor, no connected connector, no running daemon with an attached window**. That draws a hard line: | Task type | Operator? | Examples | |---|---|---| | **Code / doc change** | ✅ yes | new `pcb check` rule, `sch rebind-*`, netlist cross-check | | **Runtime acceptance** | ❌ no | end-to-end board bring-up, any `pcb drc passed:true` gate | **Why runtime acceptance can't go through the operator**: its pass criteria (`pcb drc passed`, `pcb check` 0 ERROR, BOM saved) are **runtime products** that only exist by driving a live editor through the S0–S6 / P0–P10 flow. No source edit produces them, and the headless worktree shows `windows: []` — every flow command returns `NO_CONNECTOR`. A correct operator run on such an issue **fails and says so** (it must not fabricate a "DRC passed" PR — that violates the "never feed a pre-cooked answer" rule). **The advisory loop, done right:** - Code/doc issues → label `ready-for-agent` → operator evaluates, implements, opens a PR, auto-merges → **you re-verify** with the editor loop (build + tests for code correctness, then run the new command against a live board). - Runtime-acceptance issues (e.g. the ESP32 end-to-end regressions) → **do NOT** label `ready-for-agent`. Mark them as interactive/manual and run them from a session that has chrome-devtools MCP + a live daemon + a connected editor. Verified 2026-07-06: issues #44 (rebind actions) and #45 (netlist cross-check) went code → operator → PR → auto-merge → re-verified in an editor session; issue #42 (ESP32 end-to-end) was correctly `agent-failed` by the operator with a precise "no attached editor" explanation.