@nickname4th/pura-cli 0.1.0 → 0.1.2
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/README.md +14 -12
- package/package.json +1 -1
- package/server/dist/adb.js +16 -1
- package/server/dist/cli.js +120 -3
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Open the Hub:
|
|
|
20
20
|
http://<hub-lan-ip>:8787
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
On each developer machine, connect an Agent:
|
|
23
|
+
On each developer machine, connect an Agent. After this command starts, the Hub page shows every authorized Android device attached to this machine:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
npx @nickname4th/pura-cli connect <hub-lan-ip>:8787 --name "Zhang San"
|
|
@@ -33,13 +33,7 @@ npm install -g @nickname4th/pura-cli
|
|
|
33
33
|
pura-cli connect <hub-lan-ip>:8787 --name "Zhang San" --background
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```bash
|
|
39
|
-
npx @nickname4th/pura-cli connect device --name "Zhang San Pixel 8" --owner "Zhang San" --note "login branch"
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Designers can now pick the published machine on the Hub homepage, open the live screen, and click on it with a mouse.
|
|
36
|
+
Open the Hub page, find the device under devices to publish, and publish it from the web UI. Designers can then pick the published machine on the Hub homepage, open the live screen, and click on it with a mouse.
|
|
43
37
|
|
|
44
38
|
## Project Site
|
|
45
39
|
|
|
@@ -105,13 +99,13 @@ pura-cli hub --host 0.0.0.0 --port 8787
|
|
|
105
99
|
|
|
106
100
|
## Developer Agent
|
|
107
101
|
|
|
108
|
-
Each developer connects their local Agent to the Hub:
|
|
102
|
+
Each developer connects their local Agent to the Hub. Once connected, the Hub web UI lists all authorized local Android devices, including devices that are not published yet:
|
|
109
103
|
|
|
110
104
|
```bash
|
|
111
105
|
pura-cli connect 192.168.100.128:8787 --name "Zhang San"
|
|
112
106
|
```
|
|
113
107
|
|
|
114
|
-
The Agent listens on `8788` by default and continuously reports local ADB devices to the Hub.
|
|
108
|
+
The Agent listens on `8788` by default and continuously reports local ADB devices to the Hub. Use the web UI to publish, rename, unpublish, and manage devices.
|
|
115
109
|
|
|
116
110
|
If the Hub cannot reach the auto-detected Agent URL, specify it:
|
|
117
111
|
|
|
@@ -140,7 +134,15 @@ Connect a phone over USB and confirm it is authorized:
|
|
|
140
134
|
adb devices -l
|
|
141
135
|
```
|
|
142
136
|
|
|
143
|
-
Then
|
|
137
|
+
Then run or install the Agent:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pura-cli connect 192.168.100.128:8787 --name "Zhang San" --background
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Open the Hub page and publish the device from the web UI.
|
|
144
|
+
|
|
145
|
+
The CLI also has a shortcut for scripts:
|
|
144
146
|
|
|
145
147
|
```bash
|
|
146
148
|
pura-cli connect device --name "Zhang San Pixel 8" --owner "Zhang San" --note "login branch"
|
|
@@ -211,7 +213,7 @@ Release flow:
|
|
|
211
213
|
1. Update `version` in `package.json`.
|
|
212
214
|
2. Run `npm run check`, `npm run build`, and `npm pack --dry-run`.
|
|
213
215
|
3. Push a tag like `v0.1.0`.
|
|
214
|
-
4. GitHub Actions publishes
|
|
216
|
+
4. GitHub Actions publishes `@nickname4th/pura-cli` to npm and `ghcr.io/liutianjie/pura` to GHCR.
|
|
215
217
|
|
|
216
218
|
The release workflow requires an `NPM_TOKEN` repository secret.
|
|
217
219
|
|
package/package.json
CHANGED
package/server/dist/adb.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import { promisify } from "node:util";
|
|
3
6
|
const execFileAsync = promisify(execFile);
|
|
4
|
-
const ADB =
|
|
7
|
+
const ADB = resolveAdbCommand();
|
|
5
8
|
const INCLUDE_TCP_DEVICES = process.env.INCLUDE_TCP_DEVICES === "true";
|
|
6
9
|
export function adbCommand(args) {
|
|
7
10
|
return {
|
|
@@ -151,6 +154,18 @@ export async function getDisplaySize(serial) {
|
|
|
151
154
|
function clamp(value, min, max) {
|
|
152
155
|
return Math.max(min, Math.min(max, value));
|
|
153
156
|
}
|
|
157
|
+
function resolveAdbCommand() {
|
|
158
|
+
if (process.env.ADB_PATH)
|
|
159
|
+
return process.env.ADB_PATH;
|
|
160
|
+
const sdkRoot = process.env.ANDROID_HOME ?? process.env.ANDROID_SDK_ROOT;
|
|
161
|
+
const candidates = [
|
|
162
|
+
sdkRoot ? path.join(sdkRoot, "platform-tools", "adb") : "",
|
|
163
|
+
path.join(os.homedir(), "Library", "Android", "sdk", "platform-tools", "adb"),
|
|
164
|
+
"/opt/homebrew/bin/adb",
|
|
165
|
+
"/usr/local/bin/adb"
|
|
166
|
+
].filter(Boolean);
|
|
167
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) ?? "adb";
|
|
168
|
+
}
|
|
154
169
|
const keyEvents = {
|
|
155
170
|
back: "KEYCODE_BACK",
|
|
156
171
|
home: "KEYCODE_HOME",
|
package/server/dist/cli.js
CHANGED
|
@@ -59,7 +59,7 @@ async function handleConnect() {
|
|
|
59
59
|
console.log(`pura-cli saved hub: ${hubUrl}`);
|
|
60
60
|
console.log(`agent URL announced to hub: ${publicUrl}`);
|
|
61
61
|
if (hasFlag("--background") || hasFlag("--install")) {
|
|
62
|
-
installLaunchAgent();
|
|
62
|
+
await installLaunchAgent();
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
console.log(`pura-cli starting agent: ${agentName} (${agentId})`);
|
|
@@ -77,7 +77,7 @@ async function handleConnect() {
|
|
|
77
77
|
}
|
|
78
78
|
async function handleAutoConnect() {
|
|
79
79
|
if (hasFlag("--install")) {
|
|
80
|
-
installLaunchAgent();
|
|
80
|
+
await installLaunchAgent();
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
83
|
if (hasFlag("--uninstall")) {
|
|
@@ -119,6 +119,7 @@ async function publishLocalDevice() {
|
|
|
119
119
|
process.exit(1);
|
|
120
120
|
}
|
|
121
121
|
console.log(`Published ${label} (${serial}) to ${config.hubUrl ?? "configured hub"}`);
|
|
122
|
+
await printPublicationVisibility(config, agentPort, serial);
|
|
122
123
|
}
|
|
123
124
|
function startSavedAgent(config) {
|
|
124
125
|
const port = readFlag("--port") ?? config.agentPort ?? "8788";
|
|
@@ -150,7 +151,7 @@ function startSavedAgent(config) {
|
|
|
150
151
|
DATA_DIR: dataDir
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
|
-
function installLaunchAgent() {
|
|
154
|
+
async function installLaunchAgent() {
|
|
154
155
|
const config = readConfig();
|
|
155
156
|
if (!config.hubUrl) {
|
|
156
157
|
console.error("No saved hub found. Run `pura-cli connect <hub-url> --name <name>` once first.");
|
|
@@ -174,6 +175,7 @@ function installLaunchAgent() {
|
|
|
174
175
|
if (cliPath.includes(`${path.sep}_npx${path.sep}`)) {
|
|
175
176
|
console.warn("This was installed from an npx cache path. For long-term use, install pura-cli globally and run the install command again.");
|
|
176
177
|
}
|
|
178
|
+
await printAgentVisibility(config);
|
|
177
179
|
}
|
|
178
180
|
function uninstallLaunchAgent() {
|
|
179
181
|
if (process.platform !== "darwin") {
|
|
@@ -202,6 +204,16 @@ function printLaunchAgentStatus() {
|
|
|
202
204
|
}
|
|
203
205
|
}
|
|
204
206
|
function makeLaunchAgentPlist(nodePath, cliPath) {
|
|
207
|
+
const environment = {
|
|
208
|
+
PATH: process.env.PATH,
|
|
209
|
+
ANDROID_HOME: process.env.ANDROID_HOME,
|
|
210
|
+
ANDROID_SDK_ROOT: process.env.ANDROID_SDK_ROOT,
|
|
211
|
+
ADB_PATH: findExecutable("adb")
|
|
212
|
+
};
|
|
213
|
+
const environmentEntries = Object.entries(environment)
|
|
214
|
+
.filter((entry) => Boolean(entry[1]))
|
|
215
|
+
.map(([key, value]) => ` <key>${escapeXml(key)}</key>\n <string>${escapeXml(value)}</string>`)
|
|
216
|
+
.join("\n");
|
|
205
217
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
206
218
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
207
219
|
<plist version="1.0">
|
|
@@ -218,6 +230,10 @@ function makeLaunchAgentPlist(nodePath, cliPath) {
|
|
|
218
230
|
<true/>
|
|
219
231
|
<key>KeepAlive</key>
|
|
220
232
|
<true/>
|
|
233
|
+
<key>EnvironmentVariables</key>
|
|
234
|
+
<dict>
|
|
235
|
+
${environmentEntries}
|
|
236
|
+
</dict>
|
|
221
237
|
<key>StandardOutPath</key>
|
|
222
238
|
<string>${escapeXml(path.join(os.homedir(), "Library", "Logs", "pura-agent.log"))}</string>
|
|
223
239
|
<key>StandardErrorPath</key>
|
|
@@ -254,6 +270,16 @@ function resolveAgentDataDir(value) {
|
|
|
254
270
|
return path.join(os.homedir(), ".pura", "agent-data");
|
|
255
271
|
return path.isAbsolute(value) ? value : path.resolve(value);
|
|
256
272
|
}
|
|
273
|
+
function findExecutable(name) {
|
|
274
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
275
|
+
if (!directory)
|
|
276
|
+
continue;
|
|
277
|
+
const candidate = path.join(directory, name);
|
|
278
|
+
if (fs.existsSync(candidate))
|
|
279
|
+
return candidate;
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
257
283
|
function startServer(env) {
|
|
258
284
|
const child = spawn(process.execPath, [new URL("./index.js", import.meta.url).pathname], {
|
|
259
285
|
stdio: "inherit",
|
|
@@ -264,6 +290,97 @@ function startServer(env) {
|
|
|
264
290
|
});
|
|
265
291
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
266
292
|
}
|
|
293
|
+
async function printAgentVisibility(config) {
|
|
294
|
+
const agentId = config.agentId;
|
|
295
|
+
const hubUrl = config.hubUrl;
|
|
296
|
+
const port = config.agentPort ?? "8788";
|
|
297
|
+
if (!agentId || !hubUrl)
|
|
298
|
+
return;
|
|
299
|
+
const result = await waitForAgentVisibility({ agentId, hubUrl, port });
|
|
300
|
+
if (result.visible) {
|
|
301
|
+
console.log(`Agent is online. Local devices: ${result.localDevices?.length ?? 0}. Devices visible on Hub: ${result.hubDevices?.length ?? 0}.`);
|
|
302
|
+
console.log(`Open ${hubUrl} to publish or control devices.`);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
console.warn("Agent was installed, but the Hub did not report the local device list yet.");
|
|
306
|
+
if (result.localError)
|
|
307
|
+
console.warn(`Local Agent check: ${result.localError}`);
|
|
308
|
+
if (result.hubError)
|
|
309
|
+
console.warn(`Hub check: ${result.hubError}`);
|
|
310
|
+
console.warn(`Check logs: tail -f ${path.join(os.homedir(), "Library", "Logs", "pura-agent.err.log")}`);
|
|
311
|
+
}
|
|
312
|
+
async function printPublicationVisibility(config, agentPort, serial) {
|
|
313
|
+
if (!config.hubUrl || !config.agentId) {
|
|
314
|
+
console.warn("Device metadata was saved locally, but no Hub connection is configured.");
|
|
315
|
+
console.warn("Run `pura-cli connect <hub-ip>:8787 --name <your-name> --background` first.");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const result = await waitForAgentVisibility({
|
|
319
|
+
agentId: config.agentId,
|
|
320
|
+
hubUrl: config.hubUrl,
|
|
321
|
+
port: agentPort,
|
|
322
|
+
serial
|
|
323
|
+
});
|
|
324
|
+
if (result.publishedVisible) {
|
|
325
|
+
console.log("Hub confirmed this device is visible and published.");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (result.deviceVisible) {
|
|
329
|
+
console.warn("Hub can see this device, but it has not received the published state yet. Refresh the page in a few seconds.");
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
console.warn("The device was published locally, but the Hub does not see it yet.");
|
|
333
|
+
console.warn(`Make sure the Agent is connected: pura-cli connect ${config.hubUrl} --name "${config.agentName ?? "your name"}" --background`);
|
|
334
|
+
}
|
|
335
|
+
async function waitForAgentVisibility(options) {
|
|
336
|
+
let localDevices;
|
|
337
|
+
let hubDevices;
|
|
338
|
+
let localError = "";
|
|
339
|
+
let hubError = "";
|
|
340
|
+
for (const delayMs of [250, 500, 750, 1000, 1500, 2500]) {
|
|
341
|
+
await sleep(delayMs);
|
|
342
|
+
try {
|
|
343
|
+
localDevices = (await fetchJson(`http://127.0.0.1:${options.port}/api/devices`)).devices;
|
|
344
|
+
localError = "";
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
localError = error instanceof Error ? error.message : String(error);
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
const allHubDevices = (await fetchJson(`${options.hubUrl}/api/devices`)).devices;
|
|
351
|
+
hubDevices = allHubDevices.filter((device) => device.agentId === options.agentId);
|
|
352
|
+
hubError = "";
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
hubError = error instanceof Error ? error.message : String(error);
|
|
356
|
+
}
|
|
357
|
+
const deviceVisible = options.serial ? Boolean(hubDevices?.some((device) => device.remoteSerial === options.serial)) : false;
|
|
358
|
+
const publishedVisible = options.serial
|
|
359
|
+
? Boolean(hubDevices?.some((device) => device.remoteSerial === options.serial && device.publication?.published))
|
|
360
|
+
: false;
|
|
361
|
+
if (publishedVisible || deviceVisible) {
|
|
362
|
+
return { visible: true, deviceVisible, publishedVisible, localDevices, hubDevices, localError, hubError };
|
|
363
|
+
}
|
|
364
|
+
if (!options.serial && localDevices && hubDevices && (localDevices.length === 0 || hubDevices.length > 0)) {
|
|
365
|
+
return { visible: true, deviceVisible, publishedVisible, localDevices, hubDevices, localError, hubError };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const deviceVisible = options.serial ? Boolean(hubDevices?.some((device) => device.remoteSerial === options.serial)) : false;
|
|
369
|
+
const publishedVisible = options.serial
|
|
370
|
+
? Boolean(hubDevices?.some((device) => device.remoteSerial === options.serial && device.publication?.published))
|
|
371
|
+
: false;
|
|
372
|
+
return { visible: false, deviceVisible, publishedVisible, localDevices, hubDevices, localError, hubError };
|
|
373
|
+
}
|
|
374
|
+
async function fetchJson(url) {
|
|
375
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1500) });
|
|
376
|
+
if (!response.ok) {
|
|
377
|
+
throw new Error(`${response.status} ${response.statusText}`);
|
|
378
|
+
}
|
|
379
|
+
return (await response.json());
|
|
380
|
+
}
|
|
381
|
+
function sleep(ms) {
|
|
382
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
383
|
+
}
|
|
267
384
|
function readFlag(name) {
|
|
268
385
|
const index = args.indexOf(name);
|
|
269
386
|
if (index === -1)
|