diff --git a/docs/superpowers/plans/2026-04-05-adb-wireless-keeper.md b/docs/superpowers/plans/2026-04-05-adb-wireless-keeper.md new file mode 100644 index 0000000..c3bc69f --- /dev/null +++ b/docs/superpowers/plans/2026-04-05-adb-wireless-keeper.md @@ -0,0 +1,787 @@ +# ADB Wireless Keeper 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:** 为 Boss 本机增加一套多 Android 设备无线 ADB 自动扫描与后台自动重连守护方案,并提供状态、安装和文档入口。 + +**Architecture:** 保持轻量本机运维路线,不改 Web 或 Android 业务链路。核心逻辑集中在一个可测试的 Node ESM 脚本里,外层再包一层 `zsh` 脚本和 `launchd` 常驻安装脚本;运行态写入本机 `data/` 日志和状态文件,不把运行态文件提交到仓库。 + +**Tech Stack:** Node.js ESM、`node:test`、`node:assert/strict`、`zsh`、macOS `launchd`、本机 `adb` + +--- + +## File Map + +### New tracked files + +- `scripts/adb-wireless-keeper.mjs` + - 核心守护逻辑与 CLI 入口。 + - 负责配置解析、状态读写、子网推断、候选过滤、ADB 重连、日志落盘、状态摘要。 +- `tests/adb-wireless-keeper.test.mjs` + - Node 侧单测,覆盖纯逻辑和一次扫描/重连编排。 +- `scripts/adb-wireless-keeper-once.sh` + - 单次执行入口,便于人工调试一轮扫描。 +- `scripts/adb-wireless-keeper-status.sh` + - 状态查看入口,读取状态文件并打印当前摘要。 +- `scripts/install-adb-wireless-keeper.sh` + - 安装并启动 `launchd` 常驻服务。 +- `scripts/uninstall-adb-wireless-keeper.sh` + - 卸载并停止 `launchd` 常驻服务。 +- `deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist` + - `launchd` 模板,带仓库根路径和配置占位符。 +- `config/adb-wireless-keeper.json` + - 默认配置文件,定义扫描周期、白名单和日志/状态路径。 +- `docs/superpowers/specs/2026-04-05-adb-wireless-keeper-design.md` + - 已存在 spec,本计划直接实现它。 + +### Modified tracked files + +- `README.md` + - 新增脚本用法、安装命令、限制说明。 +- `docs/architecture/current_runtime_and_deploy_status_cn.md` + - 把新的守护脚本和运行态约束写进当前运行时文档。 + +### Runtime-only files created by script, not committed + +- `data/adb-wireless-keeper-state.json` +- `data/adb-wireless-keeper.log.jsonl` + +--- + +### Task 1: 建立可测试的核心配置与过滤逻辑 + +**Files:** +- Create: `tests/adb-wireless-keeper.test.mjs` +- Create: `scripts/adb-wireless-keeper.mjs` + +- [ ] **Step 1: 写第一个失败测试,覆盖配置默认值、白名单匹配和退避规则** + +```js +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +import { + computeBackoffMs, + loadKeeperConfig, + shouldManageHost, +} from "../scripts/adb-wireless-keeper.mjs"; + +let runtimeRoot = ""; + +test.before(async () => { + runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-adb-keeper-")); +}); + +test.after(async () => { + if (runtimeRoot) { + await rm(runtimeRoot, { recursive: true, force: true }); + } +}); + +test("loadKeeperConfig fills defaults and preserves allowedDevices", async () => { + const configPath = path.join(runtimeRoot, "keeper.json"); + await writeFile( + configPath, + JSON.stringify({ + allowedDevices: [{ label: "boss-oppo-main", serialHint: "SER-001" }], + }), + "utf8", + ); + + const config = await loadKeeperConfig(configPath); + + assert.equal(config.scanIntervalSeconds, 30); + assert.equal(config.connectTimeoutMs, 4000); + assert.equal(config.allowedDevices.length, 1); + assert.equal(config.allowedDevices[0].label, "boss-oppo-main"); +}); + +test("shouldManageHost only admits whitelist or known-device hosts", () => { + const allowedDevices = [{ label: "boss-oppo-main", serialHint: "SER-001", ipHint: "192.168.31.18" }]; + const knownDevices = { + "boss-oppo-main": { + serial: "SER-001", + lastKnownIp: "192.168.31.18", + failureCount: 0, + backoffUntil: "", + }, + }; + + assert.equal( + shouldManageHost({ + ip: "192.168.31.18", + allowedDevices, + knownDevices, + }).allowed, + true, + ); + assert.equal( + shouldManageHost({ + ip: "192.168.31.99", + allowedDevices, + knownDevices, + }).allowed, + false, + ); +}); + +test("computeBackoffMs escalates after repeated failures", () => { + assert.equal(computeBackoffMs(1), 30_000); + assert.equal(computeBackoffMs(2), 120_000); + assert.equal(computeBackoffMs(4), 300_000); +}); +``` + +- [ ] **Step 2: 跑测试确认它先失败** + +Run: `node --test tests/adb-wireless-keeper.test.mjs` + +Expected: FAIL,报 `ERR_MODULE_NOT_FOUND` 或缺失导出,例如 `loadKeeperConfig is not exported`。 + +- [ ] **Step 3: 在核心脚本里补最小实现,让这些纯逻辑先可测** + +```js +#!/usr/bin/env node + +import { readFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +const DEFAULT_CONFIG = Object.freeze({ + subnets: [], + scanIntervalSeconds: 30, + connectTimeoutMs: 4000, + adbPath: "", + logFile: "data/adb-wireless-keeper.log.jsonl", + stateFile: "data/adb-wireless-keeper-state.json", + allowedDevices: [], +}); + +function normalizeAllowedDevice(raw = {}) { + return { + label: typeof raw.label === "string" ? raw.label.trim() : "", + serialHint: typeof raw.serialHint === "string" ? raw.serialHint.trim() : "", + ipHint: typeof raw.ipHint === "string" ? raw.ipHint.trim() : "", + }; +} + +export async function loadKeeperConfig(configPath) { + const resolvedPath = resolve(configPath); + const raw = JSON.parse(await readFile(resolvedPath, "utf8")); + return { + ...DEFAULT_CONFIG, + ...raw, + allowedDevices: Array.isArray(raw.allowedDevices) + ? raw.allowedDevices.map((item) => normalizeAllowedDevice(item)) + : [], + }; +} + +export function computeBackoffMs(failureCount) { + if (failureCount >= 4) return 300_000; + if (failureCount >= 2) return 120_000; + return 30_000; +} + +export function shouldManageHost({ ip, allowedDevices, knownDevices }) { + const allowedByConfig = allowedDevices.find((device) => device.ipHint === ip); + if (allowedByConfig) { + return { allowed: true, label: allowedByConfig.label, source: "config" }; + } + + const knownEntry = Object.entries(knownDevices ?? {}).find(([, value]) => value?.lastKnownIp === ip); + if (knownEntry) { + return { allowed: true, label: knownEntry[0], source: "known" }; + } + + return { allowed: false, label: "", source: "unknown" }; +} +``` + +- [ ] **Step 4: 再跑测试确认第一批逻辑通过** + +Run: `node --test tests/adb-wireless-keeper.test.mjs` + +Expected: PASS,3 个测试全部通过。 + +- [ ] **Step 5: 提交这一批纯逻辑与测试** + +```bash +git add tests/adb-wireless-keeper.test.mjs scripts/adb-wireless-keeper.mjs +git commit -m "feat: add adb wireless keeper core helpers" +``` + +--- + +### Task 2: 实现一次扫描与自动重连编排 + +**Files:** +- Modify: `tests/adb-wireless-keeper.test.mjs` +- Modify: `scripts/adb-wireless-keeper.mjs` + +- [ ] **Step 1: 先写失败测试,覆盖“已授权设备自动重连”和“未知设备跳过”** + +```js +import { + runKeeperOnce, +} from "../scripts/adb-wireless-keeper.mjs"; + +test("runKeeperOnce reconnects a known offline device discovered on the subnet", async () => { + const events = []; + const writes = []; + const now = new Date("2026-04-05T12:00:00.000Z"); + const config = { + subnets: ["192.168.31.0/24"], + scanIntervalSeconds: 30, + connectTimeoutMs: 4000, + allowedDevices: [{ label: "boss-oppo-main", serialHint: "SER-001", ipHint: "192.168.31.18" }], + }; + + const summary = await runKeeperOnce(config, { + now: () => now, + loadState: async () => ({ knownDevices: {} }), + saveState: async (state) => writes.push(state), + listConnectedDevices: async () => [], + scanSubnetHosts: async () => [{ ip: "192.168.31.18", port: 5555 }], + adbConnect: async (ip) => `connected to ${ip}:5555`, + appendLog: async (event) => events.push(event), + }); + + assert.equal(summary.connectAttempts, 1); + assert.equal(summary.connectSuccesses, 1); + assert.equal(events.some((item) => item.event === "connect_success"), true); + assert.equal(writes.length > 0, true); +}); + +test("runKeeperOnce skips unknown hosts and respects active backoff", async () => { + const events = []; + const now = new Date("2026-04-05T12:00:00.000Z"); + const config = { + subnets: ["192.168.31.0/24"], + scanIntervalSeconds: 30, + connectTimeoutMs: 4000, + allowedDevices: [{ label: "boss-oppo-main", serialHint: "SER-001", ipHint: "192.168.31.18" }], + }; + + const summary = await runKeeperOnce(config, { + now: () => now, + loadState: async () => ({ + knownDevices: { + "boss-oppo-main": { + serial: "SER-001", + lastKnownIp: "192.168.31.18", + failureCount: 2, + backoffUntil: "2026-04-05T12:01:00.000Z", + }, + }, + }), + saveState: async () => {}, + listConnectedDevices: async () => [], + scanSubnetHosts: async () => [ + { ip: "192.168.31.18", port: 5555 }, + { ip: "192.168.31.77", port: 5555 }, + ], + adbConnect: async () => { + throw new Error("should not connect during backoff"); + }, + appendLog: async (event) => events.push(event), + }); + + assert.equal(summary.connectAttempts, 0); + assert.equal(events.some((item) => item.event === "device_skipped_unknown"), true); + assert.equal(events.some((item) => item.event === "backoff_applied"), true); +}); +``` + +- [ ] **Step 2: 跑测试确认它们按预期失败** + +Run: `node --test tests/adb-wireless-keeper.test.mjs` + +Expected: FAIL,提示 `runKeeperOnce is not a function` 或摘要字段缺失。 + +- [ ] **Step 3: 给核心脚本补一次扫描/重连的最小编排实现** + +```js +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; + +function toIso(value) { + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); +} + +export async function loadState(statePath, fallback = { knownDevices: {} }) { + try { + return JSON.parse(await readFile(resolve(statePath), "utf8")); + } catch { + return fallback; + } +} + +export async function saveState(statePath, state) { + const resolved = resolve(statePath); + await mkdir(dirname(resolved), { recursive: true }); + await writeFile(resolved, `${JSON.stringify(state, null, 2)}\n`, "utf8"); +} + +export async function appendLog(logPath, payload) { + const resolved = resolve(logPath); + await mkdir(dirname(resolved), { recursive: true }); + const line = `${JSON.stringify(payload)}\n`; + await writeFile(resolved, line, { encoding: "utf8", flag: "a" }); +} + +function isInBackoff(deviceState, now) { + return Boolean(deviceState?.backoffUntil) && new Date(deviceState.backoffUntil).getTime() > now.getTime(); +} + +export async function runKeeperOnce(config, deps = {}) { + const now = deps.now ? deps.now() : new Date(); + const statePath = config.stateFile ?? DEFAULT_CONFIG.stateFile; + const logPath = config.logFile ?? DEFAULT_CONFIG.logFile; + const state = deps.loadState + ? await deps.loadState() + : await loadState(statePath); + const connected = deps.listConnectedDevices ? await deps.listConnectedDevices() : []; + const onlineIps = new Set(connected.map((item) => item.ip).filter(Boolean)); + const subnets = Array.isArray(config.subnets) && config.subnets.length > 0 ? config.subnets : []; + + let connectAttempts = 0; + let connectSuccesses = 0; + + for (const subnet of subnets) { + const hosts = deps.scanSubnetHosts ? await deps.scanSubnetHosts(subnet) : []; + for (const host of hosts) { + const match = shouldManageHost({ + ip: host.ip, + allowedDevices: config.allowedDevices, + knownDevices: state.knownDevices ?? {}, + }); + if (!match.allowed) { + await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "device_skipped_unknown", ip: host.ip, subnet }) : appendLog(logPath, { ts: toIso(now), event: "device_skipped_unknown", ip: host.ip, subnet })); + continue; + } + + const deviceState = state.knownDevices?.[match.label] ?? {}; + if (onlineIps.has(host.ip)) { + await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "device_already_online", ip: host.ip, subnet, label: match.label }) : appendLog(logPath, { ts: toIso(now), event: "device_already_online", ip: host.ip, subnet, label: match.label })); + continue; + } + if (isInBackoff(deviceState, now)) { + await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "backoff_applied", ip: host.ip, subnet, label: match.label }) : appendLog(logPath, { ts: toIso(now), event: "backoff_applied", ip: host.ip, subnet, label: match.label })); + continue; + } + + connectAttempts += 1; + try { + await (deps.adbConnect ? deps.adbConnect(host.ip) : Promise.resolve()); + connectSuccesses += 1; + state.knownDevices = state.knownDevices ?? {}; + state.knownDevices[match.label] = { + ...(state.knownDevices[match.label] ?? {}), + lastKnownIp: host.ip, + lastConnectedAt: toIso(now), + failureCount: 0, + backoffUntil: "", + }; + await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "connect_success", ip: host.ip, subnet, label: match.label }) : appendLog(logPath, { ts: toIso(now), event: "connect_success", ip: host.ip, subnet, label: match.label })); + } catch (error) { + const failureCount = Number(deviceState.failureCount ?? 0) + 1; + state.knownDevices = state.knownDevices ?? {}; + state.knownDevices[match.label] = { + ...(state.knownDevices[match.label] ?? {}), + lastKnownIp: host.ip, + lastFailureAt: toIso(now), + failureCount, + backoffUntil: toIso(now.getTime() + computeBackoffMs(failureCount)), + }; + await (deps.appendLog ? deps.appendLog({ ts: toIso(now), event: "connect_failed", ip: host.ip, subnet, label: match.label, reason: error instanceof Error ? error.message : String(error) }) : appendLog(logPath, { ts: toIso(now), event: "connect_failed", ip: host.ip, subnet, label: match.label, reason: error instanceof Error ? error.message : String(error) })); + } + } + } + + state.lastScanAt = toIso(now); + state.nextScanAt = toIso(now.getTime() + config.scanIntervalSeconds * 1000); + await (deps.saveState ? deps.saveState(state) : saveState(statePath, state)); + + return { + subnetCount: subnets.length, + connectAttempts, + connectSuccesses, + }; +} +``` + +- [ ] **Step 4: 再跑测试确认编排行为通过** + +Run: `node --test tests/adb-wireless-keeper.test.mjs` + +Expected: PASS,新增的编排测试通过,且原有纯逻辑测试不回退。 + +- [ ] **Step 5: 提交扫描与重连编排** + +```bash +git add tests/adb-wireless-keeper.test.mjs scripts/adb-wireless-keeper.mjs +git commit -m "feat: add adb wireless keeper reconnect loop" +``` + +--- + +### Task 3: 补 CLI 模式、壳脚本与 launchd 安装链 + +**Files:** +- Modify: `tests/adb-wireless-keeper.test.mjs` +- Modify: `scripts/adb-wireless-keeper.mjs` +- Create: `scripts/adb-wireless-keeper-once.sh` +- Create: `scripts/adb-wireless-keeper-status.sh` +- Create: `scripts/install-adb-wireless-keeper.sh` +- Create: `scripts/uninstall-adb-wireless-keeper.sh` +- Create: `deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist` +- Create: `config/adb-wireless-keeper.json` + +- [ ] **Step 1: 先写失败测试,覆盖状态摘要和 launchd 模板渲染** + +```js +import { + formatStatusSummary, + renderLaunchAgentPlist, +} from "../scripts/adb-wireless-keeper.mjs"; + +test("formatStatusSummary shows subnet, next scan and recent failures", () => { + const summary = formatStatusSummary({ + lastSubnet: "192.168.31.0/24", + nextScanAt: "2026-04-05T12:00:30.000Z", + knownDevices: { + "boss-oppo-main": { + lastKnownIp: "192.168.31.18", + failureCount: 1, + backoffUntil: "2026-04-05T12:01:00.000Z", + }, + }, + }); + + assert.match(summary, /192\\.168\\.31\\.0\\/24/); + assert.match(summary, /boss-oppo-main/); + assert.match(summary, /2026-04-05T12:00:30\\.000Z/); +}); + +test("renderLaunchAgentPlist injects repo root and config path", () => { + const rendered = renderLaunchAgentPlist({ + template: ` + + + ProgramArguments + + __BOSS_ROOT__ + __CONFIG_PATH__ + + +`, + rootDir: "/Users/kris/code/boss", + configPath: "/Users/kris/code/boss/config/adb-wireless-keeper.json", + }); + + assert.match(rendered, /\\/Users\\/kris\\/code\\/boss/); + assert.match(rendered, /adb-wireless-keeper\\.json/); +}); +``` + +- [ ] **Step 2: 跑测试确认先失败** + +Run: `node --test tests/adb-wireless-keeper.test.mjs` + +Expected: FAIL,提示缺失 `formatStatusSummary` 或 `renderLaunchAgentPlist`。 + +- [ ] **Step 3: 在 Node 脚本里补 CLI、状态摘要和 plist 渲染** + +```js +export function formatStatusSummary(state) { + const lines = [ + `lastSubnet=${state.lastSubnet ?? ""}`, + `lastScanAt=${state.lastScanAt ?? ""}`, + `nextScanAt=${state.nextScanAt ?? ""}`, + ]; + for (const [label, device] of Object.entries(state.knownDevices ?? {})) { + lines.push( + `${label} ip=${device.lastKnownIp ?? ""} failures=${device.failureCount ?? 0} backoffUntil=${device.backoffUntil ?? ""}`, + ); + } + return lines.join("\n"); +} + +export function renderLaunchAgentPlist({ template, rootDir, configPath }) { + return template + .replaceAll("__BOSS_ROOT__", rootDir) + .replaceAll("__CONFIG_PATH__", configPath); +} + +async function main(argv = process.argv.slice(2)) { + const mode = argv[0] ?? "once"; + const configPath = argv[1] ?? "config/adb-wireless-keeper.json"; + const config = await loadKeeperConfig(configPath); + if (mode === "status") { + const state = await loadState(config.stateFile); + process.stdout.write(`${formatStatusSummary(state)}\n`); + return; + } + if (mode === "once") { + const summary = await runKeeperOnce(config); + process.stdout.write(`${JSON.stringify(summary)}\n`); + return; + } + throw new Error(`UNKNOWN_MODE: ${mode}`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main().catch((error) => { + process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`); + process.exit(1); + }); +} +``` + +- [ ] **Step 4: 新增配置文件、launchd 模板和壳脚本** + +`config/adb-wireless-keeper.json` + +```json +{ + "subnets": [], + "scanIntervalSeconds": 30, + "connectTimeoutMs": 4000, + "adbPath": "", + "logFile": "data/adb-wireless-keeper.log.jsonl", + "stateFile": "data/adb-wireless-keeper-state.json", + "allowedDevices": [] +} +``` + +`deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist` + +```xml + + + + + Label + com.hyzq.boss.adb-wireless-keeper + ProgramArguments + + /bin/zsh + -lc + cd __BOSS_ROOT__ && node ./scripts/adb-wireless-keeper.mjs once __CONFIG_PATH__ + + RunAtLoad + + KeepAlive + + StandardOutPath + /tmp/boss-adb-wireless-keeper.out + StandardErrorPath + /tmp/boss-adb-wireless-keeper.err + + +``` + +`scripts/adb-wireless-keeper-once.sh` + +```sh +#!/bin/zsh +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CONFIG_PATH="${1:-$ROOT_DIR/config/adb-wireless-keeper.json}" + +cd "$ROOT_DIR" +exec node ./scripts/adb-wireless-keeper.mjs once "$CONFIG_PATH" +``` + +`scripts/adb-wireless-keeper-status.sh` + +```sh +#!/bin/zsh +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +CONFIG_PATH="${1:-$ROOT_DIR/config/adb-wireless-keeper.json}" + +cd "$ROOT_DIR" +exec node ./scripts/adb-wireless-keeper.mjs status "$CONFIG_PATH" +``` + +`scripts/install-adb-wireless-keeper.sh` + +```sh +#!/bin/zsh +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +PLIST_SOURCE="$ROOT_DIR/deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist" +PLIST_TARGET="$HOME/Library/LaunchAgents/com.hyzq.boss.adb-wireless-keeper.plist" +CONFIG_PATH="${1:-$ROOT_DIR/config/adb-wireless-keeper.json}" + +command -v adb >/dev/null +mkdir -p "$HOME/Library/LaunchAgents" +python3 - <<'PY' "$PLIST_SOURCE" "$PLIST_TARGET" "$ROOT_DIR" "$CONFIG_PATH" +from pathlib import Path +import sys + +source = Path(sys.argv[1]).read_text() +target = Path(sys.argv[2]) +target.write_text( + source.replace("__BOSS_ROOT__", sys.argv[3]).replace("__CONFIG_PATH__", sys.argv[4]) +) +PY +plutil -lint "$PLIST_TARGET" >/dev/null +launchctl unload "$PLIST_TARGET" >/dev/null 2>&1 || true +launchctl load "$PLIST_TARGET" +echo "Loaded $PLIST_TARGET" +``` + +`scripts/uninstall-adb-wireless-keeper.sh` + +```sh +#!/bin/zsh +set -euo pipefail + +PLIST_TARGET="$HOME/Library/LaunchAgents/com.hyzq.boss.adb-wireless-keeper.plist" +launchctl unload "$PLIST_TARGET" >/dev/null 2>&1 || true +rm -f "$PLIST_TARGET" +echo "Removed $PLIST_TARGET" +``` + +- [ ] **Step 5: 跑测试和模板校验** + +Run: + +```bash +node --test tests/adb-wireless-keeper.test.mjs +plutil -lint deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist +zsh ./scripts/adb-wireless-keeper-status.sh config/adb-wireless-keeper.json +``` + +Expected: +- Node tests PASS +- `plutil -lint` 输出 `OK` +- `status` 能打印 `lastSubnet=` / `nextScanAt=` 这类摘要字段,即使状态文件还不存在也不报错 + +- [ ] **Step 6: 提交 CLI 与守护安装链** + +```bash +git add \ + tests/adb-wireless-keeper.test.mjs \ + scripts/adb-wireless-keeper.mjs \ + scripts/adb-wireless-keeper-once.sh \ + scripts/adb-wireless-keeper-status.sh \ + scripts/install-adb-wireless-keeper.sh \ + scripts/uninstall-adb-wireless-keeper.sh \ + deployment/launchd/com.hyzq.boss.adb-wireless-keeper.plist \ + config/adb-wireless-keeper.json +git commit -m "feat: add adb wireless keeper launchd tooling" +``` + +--- + +### Task 4: 文档收口与整体验证 + +**Files:** +- Modify: `README.md` +- Modify: `docs/architecture/current_runtime_and_deploy_status_cn.md` +- Verify: `tests/adb-wireless-keeper.test.mjs` +- Verify: `scripts/adb-wireless-keeper-status.sh` + +- [ ] **Step 1: 把实际脚本名称和使用方式写进 README** + +在 `README.md` 的 Android 段落补实际脚本入口和限制,文本至少包含: + +```md +- Android ADB 无线保活脚本:`scripts/adb-wireless-keeper.mjs` +- 单次执行:`zsh ./scripts/adb-wireless-keeper-once.sh` +- 查看状态:`zsh ./scripts/adb-wireless-keeper-status.sh` +- 安装常驻守护:`zsh ./scripts/install-adb-wireless-keeper.sh` +- 卸载守护:`zsh ./scripts/uninstall-adb-wireless-keeper.sh` +- 该守护只能自动恢复“仍允许 TCP/IP ADB 的设备连接”,不能替 Android 永久保持无线调试开启 +``` + +- [ ] **Step 2: 把当前运行时文档改成和真实交付物一致** + +在 `docs/architecture/current_runtime_and_deploy_status_cn.md` 补一段实际运行说明,文本至少包含: + +```md +- 本机当前提供 `adb-wireless-keeper` 守护,用于多 Android 设备在同网段内自动扫描 `5555` 并对白名单/历史已授权设备执行 `adb connect` +- 守护配置文件位于 `config/adb-wireless-keeper.json` +- 运行态写入 `data/adb-wireless-keeper-state.json` 与 `data/adb-wireless-keeper.log.jsonl` +- 安装方式走 `launchd`,由 `scripts/install-adb-wireless-keeper.sh` 管理 +``` + +- [ ] **Step 3: 跑最终验证命令** + +Run: + +```bash +node --test tests/adb-wireless-keeper.test.mjs +npm run lint +npm run build +zsh ./scripts/adb-wireless-keeper-status.sh config/adb-wireless-keeper.json +``` + +Expected: +- `node --test` PASS +- `npm run lint` 0 errors +- `npm run build` success +- `status` 命令可运行并输出状态摘要 + +- [ ] **Step 4: 做一轮人工无线重连验收** + +Run: + +```bash +adb devices -l +adb disconnect 192.168.31.18:5555 +zsh ./scripts/adb-wireless-keeper-once.sh config/adb-wireless-keeper.json +adb devices -l +``` + +Expected: +- 目标设备在断开后被重新连接回 `192.168.31.18:5555` +- 如果设备当前已不允许 TCP/IP ADB,脚本写失败日志并进入退避,而不是无限刷 `adb connect` + +- [ ] **Step 5: 提交文档和最终收口** + +```bash +git add \ + README.md \ + docs/architecture/current_runtime_and_deploy_status_cn.md +git commit -m "docs: wire adb wireless keeper into runtime docs" +``` + +--- + +## Plan Self-Review + +### Spec coverage + +- 多设备:Task 2 的 `runKeeperOnce` 和 `knownDevices`/白名单逻辑覆盖。 +- 自动扫描同网段:Task 2 负责一次扫描编排,Task 4 做人工验收。 +- 后台常驻:Task 3 补 `launchd` 模板与安装脚本。 +- 状态/日志/退避:Task 2 和 Task 3 覆盖。 +- 文档入口:Task 4 覆盖。 + +### Placeholder scan + +- 本计划没有留下未决占位标记。 +- 每个代码步骤都给了实际文件路径、命令和最小代码。 + +### Type consistency + +- 核心导出统一使用: + - `loadKeeperConfig` + - `computeBackoffMs` + - `shouldManageHost` + - `loadState` + - `saveState` + - `appendLog` + - `runKeeperOnce` + - `formatStatusSummary` + - `renderLaunchAgentPlist`