hostpad 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/bridge.js +1 -1
- package/lib/tools.js +6 -1
- package/package.json +8 -1
- package/server.js +7 -13
package/lib/bridge.js
CHANGED
|
@@ -108,7 +108,7 @@ function createBridge({
|
|
|
108
108
|
const pairId = crypto.randomUUID();
|
|
109
109
|
const code = codeStore.issue(pairId, pairCode);
|
|
110
110
|
conn.pair = { pairId };
|
|
111
|
-
sendTo(conn, { type: 'pair-challenge', pairId, code });
|
|
111
|
+
sendTo(conn, { type: 'pair-challenge', pairId, code, codeTtlMs });
|
|
112
112
|
conn.pairTimer = setTimeout(() => {
|
|
113
113
|
conn.pairTimer = null;
|
|
114
114
|
if (conn.pair?.pairId === pairId) startPairing(conn);
|
package/lib/tools.js
CHANGED
|
@@ -47,14 +47,18 @@ function createToolStore(toolsDir) {
|
|
|
47
47
|
const toolDir = (id) => path.join(toolsDir, id);
|
|
48
48
|
|
|
49
49
|
function readRaw(id) {
|
|
50
|
+
// id 来自 MCP 输入(AI 可控),读取/删除入口必须先过白名单,防 ../ 穿越
|
|
51
|
+
if (typeof id !== 'string' || !ID_RE.test(id)) return null;
|
|
50
52
|
const dir = toolDir(id);
|
|
51
53
|
if (!fs.existsSync(path.join(dir, 'manifest.json'))) return null;
|
|
52
54
|
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
|
|
53
55
|
const files = [];
|
|
56
|
+
const root = path.resolve(dir);
|
|
54
57
|
const walk = (rel) => {
|
|
55
58
|
for (const name of fs.readdirSync(path.join(dir, rel))) {
|
|
56
59
|
const relPath = rel ? `${rel}/${name}` : name;
|
|
57
|
-
const abs = path.
|
|
60
|
+
const abs = path.resolve(dir, relPath);
|
|
61
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) continue; // 深度防御:symlink 等逃逸
|
|
58
62
|
if (fs.statSync(abs).isDirectory()) { walk(relPath); continue; }
|
|
59
63
|
if (relPath === 'manifest.json') continue;
|
|
60
64
|
files.push({ path: relPath, content: fs.readFileSync(abs, 'utf8') });
|
|
@@ -120,6 +124,7 @@ function createToolStore(toolsDir) {
|
|
|
120
124
|
},
|
|
121
125
|
|
|
122
126
|
remove(id) {
|
|
127
|
+
if (typeof id !== 'string' || !ID_RE.test(id)) throw new Error(`非法工具 id: ${id}`);
|
|
123
128
|
fs.rmSync(toolDir(id), { recursive: true, force: true });
|
|
124
129
|
},
|
|
125
130
|
};
|
package/package.json
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hostpad",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "HostPad 的 MCP 服务器 + iPhone 同步桥:AI agent 经 MCP 管理与调试 iOS 工具(写→推→读日志闭环)",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Huang-Yangkai/hostpad.git"
|
|
9
|
+
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/Huang-Yangkai/hostpad/issues"
|
|
12
|
+
},
|
|
6
13
|
"bin": {
|
|
7
14
|
"hostpad": "./bin/hostpad.js",
|
|
8
15
|
"hostpad-dev": "./server.js"
|
package/server.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
//
|
|
10
10
|
// 正式的 agent 入口是 bin/hostpad.js(MCP 服务器,`npx hostpad`)。
|
|
11
11
|
'use strict';
|
|
12
|
-
const fs = require('node:fs');
|
|
13
12
|
const path = require('node:path');
|
|
14
13
|
const readline = require('node:readline');
|
|
15
14
|
const { createBridge } = require('./lib/bridge.js');
|
|
@@ -75,17 +74,12 @@ bridge.start().then(() => {
|
|
|
75
74
|
function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
|
|
76
75
|
|
|
77
76
|
async function runSoak() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
85
|
-
if (!dir) { console.log('[soak] 没有含 SOAK_MARKER 的工具,无法压测'); process.exit(2); }
|
|
86
|
-
const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
|
|
87
|
-
const entryPath = path.join(dir, manifest.entry);
|
|
88
|
-
const template = fs.readFileSync(entryPath, 'utf8');
|
|
77
|
+
// 目标工具:entry 含 SOAK_MARKER;每轮 reload 强推触发 App 热重载(走 bridge 原生推送,不直接碰文件系统)
|
|
78
|
+
const tool = bridge.store.list()
|
|
79
|
+
.map((id) => bridge.store.read(id))
|
|
80
|
+
.find((t) => t?.files.some((f) => f.path === t.manifest.entry && f.content.includes('SOAK_MARKER')));
|
|
81
|
+
if (!tool) { console.log('[soak] 没有含 SOAK_MARKER 的工具,无法压测'); process.exit(2); }
|
|
82
|
+
const { manifest } = tool;
|
|
89
83
|
console.log(`[soak] tool=${manifest.id} cycles=${SOAK_N} interval=${SOAK_INTERVAL}ms timeout=${SOAK_TIMEOUT}ms`);
|
|
90
84
|
console.log('[soak] 等待 App 连接…');
|
|
91
85
|
while (bridge.listDevices().filter((d) => d.authed).length === 0) await sleep(200);
|
|
@@ -93,7 +87,7 @@ async function runSoak() {
|
|
|
93
87
|
|
|
94
88
|
const results = [];
|
|
95
89
|
for (let i = 1; i <= SOAK_N; i++) {
|
|
96
|
-
|
|
90
|
+
bridge.reload(manifest.id); // force 重推(无变化也推)→ App 热重载
|
|
97
91
|
const r = await bridge.waitForLoaded(manifest.id, SOAK_TIMEOUT);
|
|
98
92
|
results.push(r);
|
|
99
93
|
console.log(`[soak] ${i}/${SOAK_N} ${r.ok ? `${r.ms}ms` : `FAIL(>${SOAK_TIMEOUT}ms)`}`);
|