@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 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
- Publish the local Android device:
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 publish it:
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 `pura-cli` to npm and `ghcr.io/liutianjie/pura` to GHCR.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nickname4th/pura-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "LAN Android device mirroring hub and developer CLI for distributed teams.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 = process.env.ADB_PATH ?? "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",
@@ -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)