droidsuite 1.0.0-beta.10 → 1.0.0-beta.11

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.
Binary file
@@ -26,9 +26,11 @@ import { runMenu } from '../src/menu.mjs';
26
26
  import { createShortcuts } from '../src/shortcut.mjs';
27
27
  import { openUrl } from '../src/browser.mjs';
28
28
  import { hasAgents, installSkill, SKILL_SOURCE } from '../src/skill.mjs';
29
+ import { checkForUpdate, updateMessage } from '../src/updatecheck.mjs';
29
30
 
30
31
  const { version: VERSION } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
31
32
  const CLI_PATH = fileURLToPath(import.meta.url);
33
+ const updateAvailable = () => checkForUpdate({ current: VERSION, load: () => loadConfig().updateCheck, save: (updateCheck) => updateConfig({ updateCheck }) });
32
34
 
33
35
  const c = process.stdout.isTTY && !process.env.NO_COLOR
34
36
  ? { dim: (s) => `\x1b[2m${s}\x1b[22m`, bold: (s) => `\x1b[1m${s}\x1b[22m`, green: (s) => `\x1b[32m${s}\x1b[39m`, red: (s) => `\x1b[31m${s}\x1b[39m`, yellow: (s) => `\x1b[33m${s}\x1b[39m` }
@@ -80,7 +82,13 @@ async function serve(argv, { defaultFolder = true } = {}) {
80
82
  pushes.sweep(); // leftovers from a serve that was killed
81
83
  const installVia = values['install-via'] ?? 'auto';
82
84
  if (!['auto', 'app'].includes(installVia)) throw new Error('--install-via is auto (adb when Wireless debugging is connected, else the app) or app');
85
+ let newerVersion = null;
86
+ updateAvailable().then((latest) => { // in the background: serving never waits for npm
87
+ newerVersion = latest;
88
+ if (latest) console.log(c.dim(updateMessage(latest)));
89
+ });
83
90
  const bridge = createBridgeServer({
91
+ updateAvailable: () => newerVersion,
84
92
  index, config, hosts, debug: debugService, app, inbox, pushes, installVia,
85
93
  // Uses adb only if it's already here; serve never downloads it or accepts its license.
86
94
  adb: async () => (findAdb() ? debugService.adb() : null),
@@ -467,6 +475,8 @@ async function setup(argv) {
467
475
  if (!installThenSetup(argv)) process.exitCode = 1;
468
476
  return;
469
477
  }
478
+ const newer = await updateAvailable();
479
+ if (newer) console.log(`${updateMessage(newer)}\n`);
470
480
  const { values } = parseArgs({ args: argv, options: { yes: { type: 'boolean', short: 'y' }, 'skip-adb': { type: 'boolean' }, shortcut: { type: 'boolean' }, 'no-shortcut': { type: 'boolean' } } });
471
481
  // With no terminal (CI, a script) nobody asked for an icon, unless --yes or --shortcut says so.
472
482
  const wantsIcon = !values['no-shortcut'] && (values.shortcut || values.yes || process.stdin.isTTY);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "droidsuite",
3
- "version": "1.0.0-beta.10",
3
+ "version": "1.0.0-beta.11",
4
4
  "description": "Your Android phone on your computer, no cable: see and control its screen, type on it, move files and clipboard both ways, install builds and debug over Wi-Fi.",
5
5
  "keywords": [
6
6
  "android",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "scripts": {
36
36
  "start": "node bin/droidsuite.mjs serve",
37
- "test": "node --test \"test/*.test.mjs\""
37
+ "test": "node run-tests.mjs"
38
38
  },
39
39
  "dependencies": {
40
40
  "bonjour-service": "^1.3.0",
@@ -100,7 +100,9 @@ droidsuite adb -s <serial> exec-out screencap -p > screen.png
100
100
  ```
101
101
  - Read only the logs of the app you're testing, as above. Other apps' logs can hold the person's private data, and
102
102
  whatever you read goes into this conversation. No output from the crash command means this app hasn't crashed.
103
- - Find `applicationId` in `app/build.gradle(.kts)` or with `droidsuite adb shell pm list packages <part>`.
103
+ - Find `applicationId` in `app/build.gradle(.kts)`, or read `builds[].packageName` from the status script while `serve`
104
+ watches the build. Failing that, ask the person, or search with part of the app's name:
105
+ `droidsuite adb shell pm list packages <part>`. Never list every package: which apps someone has is private.
104
106
  - `INSTALL_FAILED_UPDATE_INCOMPATIBLE` = signed with another key: uninstalling deletes the app's data, so ask first,
105
107
  then `adb uninstall <applicationId>`.
106
108
  - Use `logcat -d` (dump and exit). A plain `logcat` streams forever and blocks you. To see only one launch, note the
package/src/server.mjs CHANGED
@@ -138,7 +138,7 @@ function parseRange(header, size) {
138
138
  */
139
139
  export function createBridgeServer({
140
140
  index, config, hosts, debug = null, app = null, inbox = null, pushes = null,
141
- installVia = 'auto', adb = null, autoConnect = null, log = () => {}, screen = null, rememberFolder = null, quit = null, nearby: nearbyOption = null,
141
+ installVia = 'auto', adb = null, autoConnect = null, log = () => {}, screen = null, rememberFolder = null, quit = null, updateAvailable = () => null, nearby: nearbyOption = null,
142
142
  }) {
143
143
  const phones = new Map(); // response -> phone record (see openPhone)
144
144
  let appSha = null; // the bundled phone app's sha256, computed on its first download
@@ -174,7 +174,7 @@ export function createBridgeServer({
174
174
  id: config.id, name: config.name, hosts, port, link: link(),
175
175
  qrSvg: await QRCode.toString(link(), { type: 'svg', margin: 1, errorCorrectionLevel: 'M' }),
176
176
  typing: { address: hosts[0] ? `${hosts[0]}:${port}` : null, code: typingCode, expiresAt: typingExpiresAt },
177
- roots: index.roots, builds: index.list(), phones: phoneList(),
177
+ roots: index.roots, builds: index.list(), phones: phoneList(), update: updateAvailable(),
178
178
  app: app ? { available: true, versionName: app.versionName ?? null, size: app.size, ...await appQr() } : { available: false },
179
179
  inbox: inbox ? { dir: inbox.dir, recent: inbox.recent.map(({ name, size, from, time }) => ({ name, size, from, time })) } : null,
180
180
  });
@@ -0,0 +1,55 @@
1
+ // Tells people when a newer DroidSuite is on npm. Asks npm at most once a day, sends nothing about the user, and never
2
+ // gets in the way: no network, or npm being slow, just means no notice. DROIDSUITE_NO_UPDATE_CHECK=1 turns it off.
3
+ const DAY = 24 * 3600_000;
4
+ const LATEST_URL = 'https://registry.npmjs.org/droidsuite/latest';
5
+
6
+ function parts(version) {
7
+ const [main, pre] = String(version).split('-');
8
+ return { main: main.split('.').map(Number), pre: pre ? pre.split('.') : null };
9
+ }
10
+
11
+ /** True when [candidate] is a later version than [current], counting prereleases: beta.10 comes after beta.9. */
12
+ export function isNewer(candidate, current) {
13
+ const a = parts(candidate);
14
+ const b = parts(current);
15
+ for (let i = 0; i < 3; i++) {
16
+ if ((a.main[i] ?? 0) !== (b.main[i] ?? 0)) return (a.main[i] ?? 0) > (b.main[i] ?? 0);
17
+ }
18
+ if (!a.pre || !b.pre) return !a.pre && !!b.pre; // a final release comes after its prereleases
19
+ for (let i = 0; i < Math.max(a.pre.length, b.pre.length); i++) {
20
+ const [x, y] = [a.pre[i], b.pre[i]];
21
+ if (x === undefined) return false;
22
+ if (y === undefined) return true;
23
+ if (x === y) continue;
24
+ const bothNumbers = /^\d+$/.test(x) && /^\d+$/.test(y);
25
+ return bothNumbers ? Number(x) > Number(y) : x > y;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ async function latestOnNpm() {
31
+ const res = await fetch(LATEST_URL, { signal: AbortSignal.timeout(8000) });
32
+ if (!res.ok) throw new Error(`npm answered ${res.status}`);
33
+ return (await res.json()).version;
34
+ }
35
+
36
+ /**
37
+ * The newer version available, or null. [load] and [save] keep `{ at, latest }` between runs.
38
+ * Never throws.
39
+ */
40
+ export async function checkForUpdate({ current, load, save, fetchLatest = latestOnNpm, now = Date.now(), env = process.env }) {
41
+ if (env.DROIDSUITE_NO_UPDATE_CHECK || env.CI) return null;
42
+ try {
43
+ const saved = load();
44
+ let latest = saved?.latest ?? null;
45
+ if (!saved?.at || now - saved.at >= DAY) {
46
+ latest = await fetchLatest();
47
+ save({ at: now, latest });
48
+ }
49
+ return latest && isNewer(latest, current) ? latest : null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ export const updateMessage = (latest) => `A newer DroidSuite is available (${latest}). To update, run: npx droidsuite@latest setup`;
package/web/index.html CHANGED
@@ -261,6 +261,7 @@
261
261
  </head>
262
262
  <body>
263
263
  <div class="wrap">
264
+ <div class="note" id="update-note" role="status" hidden>A newer DroidSuite is available (<span id="update-version"></span>). To update, run <code class="mono">npx droidsuite@latest setup</code> in a terminal.</div>
264
265
  <header>
265
266
  <div class="mark" aria-hidden="true"><svg viewBox="0 0 96 96"><path fill="#fff" fill-rule="evenodd" d="M23.5 16h7a6.5 6.5 0 0 1 6.5 6.5v51a6.5 6.5 0 0 1-6.5 6.5h-7a6.5 6.5 0 0 1-6.5-6.5v-51a6.5 6.5 0 0 1 6.5-6.5zM24.6 70.5a1.6 1.6 0 0 0 0 3.2h4.8a1.6 1.6 0 0 0 0-3.2z"/><path fill="none" stroke="#fff" stroke-width="8" stroke-linecap="round" d="M46 20a28 28 0 0 1 0 56"/><path fill="none" stroke="#E89B2F" stroke-width="8" stroke-linecap="round" d="M46 38a10 10 0 0 1 0 20"/></svg></div>
266
267
  <h1>DroidSuite</h1>
@@ -539,6 +540,8 @@
539
540
  let folderOpen = false; // the Builds folder field, opened with "Change"
540
541
  function render(s) {
541
542
  last = s;
543
+ $('update-note').hidden = !s.update;
544
+ $('update-version').textContent = s.update ?? '';
542
545
  renderBridge();
543
546
  if (!pairTouched) $('pair-card').open = !s.phones.length;
544
547
  $('serving').innerHTML = `${esc(s.name)} · serving on <span class="mono">${esc(s.hosts[0] ?? 'localhost')}:${s.port}</span>`;