用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/EvanBacon/apple-health --skill expo-devtools-cli命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
基于 SOC 职业分类
| name | expo-devtools-cli |
| description | Building Expo DevTools Plugins with CLI Interfaces for interacting with running Expo apps using agents. |
Build CLI tools that communicate with running Expo apps via the DevTools plugin system.
┌─────────────────┐ WebSocket ┌─────────────────┐
│ CLI Client │◄──────────────────►│ Expo Dev Server │
│ (Bun + Stricli)│ │ (Metro) │
└─────────────────┘ └────────┬────────┘
│
┌────────▼────────┐
│ React Native │
│ App + Hook │
└─────────────────┘
| Component | Technology | Why |
|---|---|---|
| Runtime | Bun | Fast startup, native TypeScript, built-in WebSocket |
| CLI Framework | @stricli/core | Type-safe, lazy loading, tree-shakeable |
| App Hook | expo/devtools | useDevToolsPluginClient for app-side connection |
| Protocol | JSON over WebSocket | Simple, debuggable with standard tools |
cli/
├── index.ts # Entry point with shebang
├── app.ts # Stricli app definition with routes
├── client.ts # WebSocket client for devtools
├── types.ts # Shared TypeScript types
├── formatters.ts # Output formatting (table, JSON)
└── commands/
├── query.ts # Read commands
├── write.ts # Write commands
└── status.ts # Status/health commands
src/devtools/
└── useMyPluginDevTools.ts # App-side message handler hook
Add devtools config to expo-module.config.json:
{
"name": "MyModule",
"platforms": ["ios", "android"],
"devtools": {
"name": "My Plugin",
"id": "my-plugin"
}
}
// src/devtools/useMyPluginDevTools.ts
import { useEffect } from "react";
import { useDevToolsPluginClient } from "expo/devtools";
interface PluginMessage {
id: string;
type: string;
payload: Record<string, unknown>;
}
export function useMyPluginDevTools() {
const client = useDevToolsPluginClient("my-plugin"); // Must match devtools.id
useEffect(() => {
if (!client) return;
const handleMessage = (data: PluginMessage) => {
const { id, type, payload } = data;
const sendResult = (result: unknown) => {
client.sendMessage("result", { id, type: "result", data: result });
};
const sendError = (error: Error) => {
client.(, {
id,
: ,
: error.,
});
};
( () => {
{
() {
:
data = (payload. );
(data);
;
:
( ());
}
} (error) {
(error );
}
})();
};
subscription = client.(
,
{
(msg );
}
);
{
subscription?.?.();
};
}, [client]);
}
// cli/client.ts
const DEFAULT_PORT = 8081;
const REQUEST_TIMEOUT = 30000;
const PROTOCOL_VERSION = 1;
export class PluginClient {
private ws: WebSocket | null = null;
private pending = new Map<string, { resolve: Function; reject: Function }>();
private connected = false;
private browserClientId = Date.now().toString();
private pluginName = "my-plugin"; // Must match devtools.id
async connect(port = DEFAULT_PORT): Promise<void> {
if (this.connected) return;
return new Promise((resolve, reject) => {
// IMPORTANT: Use the broadcast endpoint
const url = ;
. = (url);
timeout = ( {
( ());
}, );
..(, {
(timeout);
. = ;
.();
();
});
..(, {
(timeout);
( ());
});
..(, {
. = ;
});
..(, {
.(event.);
});
});
}
(): {
handshake = {
: ,
: .,
: ,
: .,
: ,
};
.?.(.(handshake));
}
(: | ): {
( data === ) {
{
parsed = .(data);
(parsed.) ;
(parsed.) {
.(parsed);
}
} {
}
}
}
(: { : ; : }): {
{ messageKey, payload } = msg;
(messageKey. !== .) ;
(messageKey. === || messageKey. === ) {
response = payload {
: ;
?: ;
?: ;
};
pending = ..(response.);
(!pending) ;
..(response.);
(messageKey. === || response.) {
pending.( (response. ?? ));
} {
pending.(response.);
}
}
}
send<T>(: , : ): <T> {
(!. || !.) {
();
}
id = crypto.();
( {
..(id, { resolve, reject });
msg = {
: { : ., : },
: { id, , payload },
};
.!.(.(msg));
( {
(..(id)) {
..(id);
( ());
}
}, );
});
}
(): <> {
.?.();
. = ;
. = ;
}
}
// cli/index.ts
#!/usr/bin/env bun
import { run } from "@stricli/core";
import { app } from "./app";
await run(app, process.argv.slice(2), { process });
// cli/app.ts
import { buildApplication, buildRouteMap } from "@stricli/core";
const routes = buildRouteMap({
routes: {
status: () => import("./commands/status").then((m) => m.default),
query: () => import("./commands/query").then((m) => m.default),
},
});
export const app = buildApplication(routes, {
name: "my-cli",
versionInfo: { currentVersion: "1.0.0" },
});
{
"bin": {
"my-cli": "cli/index.ts"
},
"scripts": {
"cli": "bun cli/index.ts"
},
"dependencies": {
"@stricli/core": "^1.1.0"
}
}
Problem: Messages sent as ArrayBuffer are silently ignored.
// WRONG - Will not work
const encoder = new TextEncoder();
this.ws.send(encoder.encode(JSON.stringify(msg)).buffer);
// CORRECT - Send as JSON string
this.ws.send(JSON.stringify(msg));
Debugging: Use websocat to test the WebSocket:
websocat -v ws://localhost:8081/expo-dev-plugins/broadcast
Problem: Using /message or other endpoints won't work.
// WRONG
const url = `ws://localhost:${port}/message`;
// CORRECT - Must use broadcast endpoint
const url = `ws://localhost:${port}/expo-dev-plugins/broadcast`;
Debugging: Use curl to verify WebSocket upgrade:
curl -v -H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Key: test" -H "Sec-WebSocket-Version: 13" \
http://localhost:8081/expo-dev-plugins/broadcast
Problem: Connection appears to work but messages aren't routed.
// WRONG - Missing required fields
const handshake = { pluginName: "my-plugin" };
// CORRECT - All fields required
const handshake = {
protocolVersion: 1, // Must be 1
pluginName: "my-plugin",
method: "handshake",
browserClientId: "unique-id",
__isHandshakeMessages: true, // Critical flag
};
Problem: terminateBrowserClient messages with warning about incompatible clients.
// WRONG
protocolVersion: 2;
// CORRECT - Use version 1
protocolVersion: 1;
Problem: Messages sent but never received by app.
The pluginName must match exactly across:
expo-module.config.json → devtools.iduseDevToolsPluginClient("my-plugin")this.pluginName = "my-plugin"Problem: Hook logs "connected" but messages timeout.
Check that useDevToolsPluginClient is imported from the correct package:
// CORRECT
import { useDevToolsPluginClient } from "expo/devtools";
// WRONG - different package
import { useDevToolsPluginClient } from "@expo/devtools-plugin-client";
Problem: App receives connection but not messages.
The addMessageListener method name must match the messageKey.method from CLI:
// CLI sends with method: "message"
const msg = {
messageKey: { pluginName: "my-plugin", method: "message" },
payload: { id, type, payload },
};
// App listens for "message"
client.addMessageListener("message", handler);
# Listen to all broadcasts
websocat --no-close -v ws://localhost:8081/expo-dev-plugins/broadcast
# Send test handshake
echo '{"protocolVersion":1,"pluginName":"my-plugin","method":"handshake","browserClientId":"test","__isHandshakeMessages":true}' | \
websocat ws://localhost:8081/expo-dev-plugins/broadcast
bunx xcobra expo console --json | grep -i "my-plugin\|devtools"
Add temporary logging to the hook:
useEffect(() => {
console.log("[DevTools] client:", client ? "connected" : "null");
if (!client) return;
console.log("[DevTools] Setting up listener");
// ...
}, [client]);
// Minimal test script
const ws = new WebSocket("ws://localhost:8081/expo-dev-plugins/broadcast");
ws.onopen = () => {
console.log("Connected");
ws.send(
JSON.stringify({
protocolVersion: 1,
pluginName: "my-plugin",
method: "handshake",
browserClientId: "test",
__isHandshakeMessages: true,
})
);
};
ws.onmessage = (e) => console.log("Received:", e.data);
yarn expo run:ios or have simulator running with Expo Gohttp://localhost:8081 respondsbun cli/index.ts statusSee the HealthKit CLI in this repo:
cli/ - Full CLI implementationsrc/dev-tools/useHealthKitDevTools.ts - App-side hookexample/App.tsx - Hook usage in app