droidsuite 1.0.0-beta.8 → 1.0.0-beta.9

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/COMMANDS.md CHANGED
@@ -6,7 +6,7 @@ Run `droidsuite` with nothing after it for an interactive menu. Every command al
6
6
 
7
7
  | Command | What it does |
8
8
  |---|---|
9
- | [`setup`](#setup) | First-run setup: get adb ready for Wi-Fi debugging, add it to PATH, show next steps |
9
+ | [`setup`](#setup) | First-run setup: add the DroidSuite icon, get adb ready for Wi-Fi debugging, offer the AI agent skill, show next steps |
10
10
  | [`serve`](#serve) | Share APKs with your paired phones, push every new build to them, and swap files |
11
11
  | [`open`](#open) | Open the DroidSuite page, starting the server if it isn't running (what the desktop icon does) |
12
12
  | [`push`](#push) | Send any files to your paired phones (needs droidsuite serve running) |
@@ -22,21 +22,23 @@ Run `droidsuite` with nothing after it for an interactive menu. Every command al
22
22
  | [`debug path`](#debug-path) | Add adb to your PATH so typing "adb" works in new terminals |
23
23
  | [`adb`](#adb) | Run adb, even when it isn't on PATH |
24
24
  | [`shortcut`](#shortcut) | Add a "DroidSuite" icon that starts the server and opens its page |
25
- | [`skill`](#skill) | Teach AI coding agents (Claude Code and others) to use droidsuite, in every project |
25
+ | [`skill`](#skill) | Teach AI coding agents (Claude Code, Codex, Gemini CLI, Cursor and others) to use droidsuite, in every project |
26
26
  | [`help`](#help) | Show all commands, or the details of one |
27
27
 
28
28
  ## setup
29
29
 
30
- First-run setup: get adb ready for Wi-Fi debugging, add it to PATH, show next steps.
30
+ First-run setup: add the DroidSuite icon, get adb ready for Wi-Fi debugging, offer the AI agent skill, show next steps.
31
31
 
32
32
  ```
33
- droidsuite setup [--yes] [--skip-adb]
33
+ droidsuite setup [--yes] [--skip-adb] [--no-shortcut]
34
34
  ```
35
35
 
36
36
  | Option | Meaning |
37
37
  |---|---|
38
38
  | `-y, --yes` | Answer yes to every question (accepts the Platform-Tools license). |
39
39
  | `--skip-adb` | Don't check or install adb (only needed for Debug over Wi-Fi). |
40
+ | `--no-shortcut` | Don't add the DroidSuite icon. |
41
+ | `--shortcut` | Add the icon even with no terminal to answer in. The install scripts use this. |
40
42
 
41
43
  Examples:
42
44
 
@@ -44,7 +46,7 @@ Examples:
44
46
  droidsuite setup
45
47
  ```
46
48
 
47
- The install scripts run this for you. Asks before installing anything or changing PATH; with no terminal to answer in, it skips.
49
+ The install scripts run this for you. Have Node.js and nothing installed yet? npx droidsuite setup installs DroidSuite too. Asks before installing adb, changing PATH or adding the skill; with no terminal to answer in, it skips.
48
50
 
49
51
  ## serve
50
52
 
@@ -337,7 +339,7 @@ No terminal window: the icon opens the page in your browser, and the page does e
337
339
 
338
340
  ## skill
339
341
 
340
- Teach AI coding agents (Claude Code and others) to use droidsuite, in every project.
342
+ Teach AI coding agents (Claude Code, Codex, Gemini CLI, Cursor and others) to use droidsuite, in every project.
341
343
 
342
344
  ```
343
345
  droidsuite skill [--project | --dir <folder>] [--print]
@@ -345,19 +347,20 @@ droidsuite skill [--project | --dir <folder>] [--print]
345
347
 
346
348
  | Option | Meaning |
347
349
  |---|---|
348
- | `--project` | Install only into the current project (.claude/skills) instead of for every project on this computer. |
349
- | `--global` | Every project on this computer (~/.claude/skills). This is the default. |
350
- | `--dir <folder>` | Install into another skills folder, for agents that read skills from elsewhere. |
350
+ | `--project` | Install only into the current project instead of for every project on this computer. |
351
+ | `--global` | Every project on this computer. This is the default. |
352
+ | `--dir <folder>` | Install into another skills folder, for an agent that reads skills from somewhere else. |
351
353
  | `--print` | Only print where the bundled skill is. |
352
354
 
353
355
  Examples:
354
356
 
355
357
  ```bash
356
- droidsuite skill # every project on this computer: ~/.claude/skills/droidsuite
357
- droidsuite skill --project # only this project: .claude/skills/droidsuite (commit it to share)
358
+ droidsuite skill # every project on this computer, for every agent
359
+ droidsuite skill --project # only this project (commit it to share)
360
+ droidsuite skill --dir ~/.my-agent/skills
358
361
  ```
359
362
 
360
- The skill shows an agent how to install builds on your phone, read logcat and screenshots over Wi-Fi, and move files, and which steps need you (pairing, Allow, PIN). Run it again after updating droidsuite.
363
+ The installer offers this when it finds an AI agent on your computer. Always installs into .agents/skills and .claude/skills, and into the folders of Gemini CLI, Antigravity, Cursor, GitHub Copilot, Windsurf, Qwen Code and Amp when they are on this computer. The skill shows an agent how to install builds on your phone, read logcat and screenshots over Wi-Fi, and move files, and which steps need you (pairing, Allow, PIN). Run it again after updating droidsuite or adding an agent.
361
364
 
362
365
  ## help
363
366
 
package/README.md CHANGED
@@ -10,7 +10,7 @@ powershell -NoExit -ExecutionPolicy Bypass -Command "irm https://cdn.jsdelivr.ne
10
10
  # macOS / Linux
11
11
  curl -fsSL https://cdn.jsdelivr.net/npm/droidsuite/install.sh | sh
12
12
  # or, with Node.js 20+ already installed
13
- npm install -g droidsuite && droidsuite setup
13
+ npx droidsuite setup
14
14
  ```
15
15
 
16
16
  ```bash
@@ -28,7 +28,7 @@ droidsuite debug install # install adb (Google platform-tools) if you don't h
28
28
  droidsuite debug path # add adb to your PATH so typing adb works in new terminals
29
29
  droidsuite adb devices # run adb even when it isn't on PATH
30
30
  # http://localhost:8765 (while serve runs): see and use the phone's screen live
31
- droidsuite skill # teach AI coding agents (Claude Code…) to use all of the above, in every project
31
+ droidsuite skill # teach your AI coding agent (Claude Code, Codex, Gemini CLI…) to use all of the above
32
32
  ```
33
33
 
34
34
  Type `droidsuite` on its own for a menu. All commands and options: [COMMANDS.md](https://cdn.jsdelivr.net/npm/droidsuite/COMMANDS.md) or `droidsuite help <command>`.
Binary file
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
- import { spawn } from 'node:child_process';
3
+ import { spawn, spawnSync } from 'node:child_process';
4
4
  import { existsSync, mkdirSync, openSync, readFileSync } from 'node:fs';
5
5
  import { stat } from 'node:fs/promises';
6
6
  import { dirname, join, resolve, relative } from 'node:path';
@@ -15,7 +15,7 @@ import { advertise, discoverPhones } from '../src/mdns.mjs';
15
15
  import { findAdb } from '../src/adb.mjs';
16
16
  import { autoConnectAdb, DebugService } from '../src/debugservice.mjs';
17
17
  import { LICENSE_URL, toolsHome } from '../src/platformtools.mjs';
18
- import { runSetup } from '../src/setup.mjs';
18
+ import { isTemporaryRun, runSetup } from '../src/setup.mjs';
19
19
  import { CODE_HELP, connectAddress, newSetupSession, parseAddress, runSetup as runDebugSetup, SETUP_HELP, SETUP_STEPS, setupInput } from '../src/debug.mjs';
20
20
  import { hello, helloPaired, normalizeTarget, sendFile } from '../src/send.mjs';
21
21
  import { findServe, pushFile } from '../src/push.mjs';
@@ -25,7 +25,7 @@ import { commandHelp, findCommand, markdown, overview } from '../src/commands.mj
25
25
  import { runMenu } from '../src/menu.mjs';
26
26
  import { createShortcuts } from '../src/shortcut.mjs';
27
27
  import { openUrl } from '../src/browser.mjs';
28
- import { installSkill, SKILL_SOURCE } from '../src/skill.mjs';
28
+ import { hasAgents, installSkill, SKILL_SOURCE } from '../src/skill.mjs';
29
29
 
30
30
  const { version: VERSION } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
31
31
  const CLI_PATH = fileURLToPath(import.meta.url);
@@ -442,10 +442,40 @@ async function adbPassthrough(argv) {
442
442
  process.exitCode = await new Promise((ok) => child.on('close', (code) => ok(code ?? 1)));
443
443
  }
444
444
  /** `droidsuite setup`: first-run wizard (the install scripts run it). */
445
+ /**
446
+ * `npx droidsuite setup` runs from a temporary copy, and an icon made from it would break when npx cleans up.
447
+ * So it installs DroidSuite for good, then hands over to that copy. Returns false if the install failed.
448
+ */
449
+ function installThenSetup(argv) {
450
+ console.log('Installing DroidSuite on this computer...');
451
+ const onWindows = process.platform === 'win32';
452
+ const npm = (...args) => spawnSync('npm', args, { shell: onWindows, encoding: 'utf8', stdio: ['inherit', 'pipe', 'inherit'] });
453
+ const installed = npm('install', '--global', process.env.DROIDSUITE_PACKAGE ?? `droidsuite@${VERSION}`, '--no-fund', '--no-audit', '--loglevel=error');
454
+ const root = installed.status === 0 ? npm('root', '--global').stdout?.trim() : null;
455
+ if (!root) {
456
+ console.error(`${c.red('✗')} Couldn't install DroidSuite for this computer (often a permissions issue). See: https://docs.npmjs.com/resolving-eacces-permissions-errors-when-installing-packages-globally`);
457
+ return false;
458
+ }
459
+ const run = spawnSync(process.execPath, [join(root, 'droidsuite', 'bin', 'droidsuite.mjs'), 'setup', ...argv], { stdio: 'inherit' });
460
+ process.exitCode = run.status ?? 1;
461
+ return true;
462
+ }
463
+
445
464
  async function setup(argv) {
446
- const { values } = parseArgs({ args: argv, options: { yes: { type: 'boolean', short: 'y' }, 'skip-adb': { type: 'boolean' } } });
465
+ if (isTemporaryRun(CLI_PATH)) {
466
+ if (!installThenSetup(argv)) process.exitCode = 1;
467
+ return;
468
+ }
469
+ const { values } = parseArgs({ args: argv, options: { yes: { type: 'boolean', short: 'y' }, 'skip-adb': { type: 'boolean' }, shortcut: { type: 'boolean' }, 'no-shortcut': { type: 'boolean' } } });
470
+ // With no terminal (CI, a script) nobody asked for an icon, unless --yes or --shortcut says so.
471
+ const wantsIcon = !values['no-shortcut'] && (values.shortcut || values.yes || process.stdin.isTTY);
447
472
  const result = await runSetup({
448
473
  service: debugService,
474
+ skill: { present: hasAgents, install: installSkill },
475
+ shortcut: wantsIcon ? {
476
+ create: () => createShortcuts({ nodePath: process.execPath, cliPath: CLI_PATH, desktop: true }),
477
+ where: { win32: 'on your desktop and in the Start menu', darwin: 'on your desktop' }[process.platform] ?? 'in your applications menu',
478
+ } : undefined,
449
479
  say: (line) => console.log(line.startsWith('✓') ? c.green('✓') + line.slice(1) : line.startsWith('✗') ? c.red('✗') + line.slice(1) : line),
450
480
  confirm: async (q, def) => {
451
481
  // Nobody to ask (piped input, CI): never accept a license or change PATH on someone's behalf.
@@ -508,14 +538,15 @@ async function skill(argv) {
508
538
  // --global is the default now; still accepted so older instructions keep working.
509
539
  const { values } = parseArgs({ args: argv, options: { project: { type: 'boolean' }, global: { type: 'boolean' }, dir: { type: 'string' }, print: { type: 'boolean' } } });
510
540
  if (values.print) return console.log(SKILL_SOURCE);
511
- const { path, updated, alsoProject } = installSkill({ project: values.project, dir: values.dir });
512
- console.log(`${c.green('✓')} ${updated ? 'Updated' : 'Added'} the droidsuite skill: ${path}`);
513
- if (alsoProject) console.log(`${c.green('✓')} Also updated this project's older copy: ${alsoProject}`);
541
+ const { targets, updated, alsoProject } = installSkill({ project: values.project, dir: values.dir });
542
+ console.log(`${c.green('✓')} ${updated ? 'Updated' : 'Added'} the droidsuite skill:`);
543
+ for (const t of targets) console.log(` ${t.path}${t.agents ? c.dim(` (${t.agents})`) : ''}`);
544
+ for (const p of alsoProject) console.log(`${c.green('✓')} Also updated this project's older copy: ${p}`);
514
545
  console.log(c.dim(values.project
515
- ? ' Commit it, and every AI agent working on this project knows how to use your phone (Claude Code reads .claude/skills).'
546
+ ? ' Commit these, and every AI agent working on this project knows how to use your phone.'
516
547
  : values.dir
517
548
  ? ' Agents that read this folder can now install builds on the phone, read logcat, and move files.'
518
- : ' Claude Code now knows how to use your phone in every project on this computer. Only this project? droidsuite skill --project'));
549
+ : ' AI agents now know how to use your phone in every project on this computer. Added a new agent later? Run this again.\n Agent not listed? droidsuite skill --dir <its skills folder>'));
519
550
  }
520
551
 
521
552
  function help(argv) {
package/install.ps1 CHANGED
@@ -44,10 +44,8 @@
44
44
  $prefix = (& npm prefix --global).Trim()
45
45
  if (($env:Path -split ';') -notcontains $prefix) { $env:Path = "$env:Path;$prefix" }
46
46
 
47
- & droidsuite setup
48
-
49
- # One click from now on: the DroidSuite icon starts the server and opens its page.
50
- & droidsuite shortcut --desktop
47
+ # Also adds the icon: one click from now on, it starts the server and opens its page.
48
+ & droidsuite setup --shortcut
51
49
 
52
50
  # Open it now: the page comes up in the browser, the rest happens there.
53
51
  & droidsuite open
package/install.sh CHANGED
@@ -55,9 +55,8 @@ fi
55
55
 
56
56
  PATH="$PATH:$(npm prefix --global)/bin"
57
57
  if has_tty; then
58
- droidsuite setup < /dev/tty
59
- # One click from now on: macOS gets "DroidSuite" on the desktop, Linux in the applications menu.
60
- droidsuite shortcut || true
58
+ # Also adds the icon. One click from now on: macOS gets "DroidSuite" on the desktop, Linux in the applications menu.
59
+ droidsuite setup --shortcut < /dev/tty
61
60
  # Open it now: the page comes up in the browser, the rest happens there.
62
61
  if droidsuite open; then
63
62
  echo ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "droidsuite",
3
- "version": "1.0.0-beta.8",
3
+ "version": "1.0.0-beta.9",
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",
@@ -156,7 +156,8 @@ node <this-skill>/scripts/droidsuite-status.mjs --wait-build 120 --match debug
156
156
  in Receive mode, where the APK is kept). Sending the build again isn't needed.
157
157
  - An app installed by adb (Path A) is still updated by Path B; only that first silent update may ask once.
158
158
  - With Wireless debugging also connected (Path A), `serve` installs each build through adb itself: no first-install
159
- tap, and it works where Android's developer verification blocks apps from unregistered developers (adb is exempt).
159
+ tap (except on Xiaomi, Redmi and POCO, which ask on every adb install), and it works where Android's developer
160
+ verification blocks apps from unregistered developers (adb is exempt).
160
161
  The status output shows `"adb": "<serial>"` on the phone then. If installs stall on the phone in Brazil, Indonesia,
161
162
  Singapore or Thailand (or anywhere from 2027), set up Path A and keep `serve` running.
162
163
  - If adb refuses the install, `serve` falls back to the phone by itself and the person gets the install prompt:
@@ -197,7 +198,7 @@ node <this-skill>/scripts/droidsuite-status.mjs --wait-build 120 --match debug
197
198
  | `no phone with Wireless debugging found` | Wireless debugging is off, or another network | Ask to switch it on (Path A); then `droidsuite debug doctor` |
198
199
  | `Pairing didn't work: nothing answered at …` | Wrong IP address & Port typed for pairing | Ask for the line under the Wi-Fi pairing code |
199
200
  | `Paired, but the phone can't be found` | Network blocks discovery (mDNS) | Ask for the IP address & Port at the top of Wireless debugging; `droidsuite debug connect` |
200
- | `INSTALL_FAILED_USER_RESTRICTED` | The phone refused an adb install: locked, a prompt timed out, or Xiaomi's **Install via USB** is off | Ask them to unlock and approve; on Xiaomi, Redmi and POCO, to turn on **Developer options → Install via USB** |
201
+ | `INSTALL_FAILED_USER_RESTRICTED` | The phone refused an adb install: locked, a prompt timed out (Xiaomi gives about 10 seconds), or Xiaomi's **Install via USB** is off | Don't retry straight away. Say "Unlock the phone and be ready to tap **Install**", wait for their reply, then retry. On Xiaomi, Redmi and POCO, **Developer options → Install via USB** must be on, and each adb install still shows that prompt. If they'd rather not turn it on, or nobody is at the phone, use Path B: the phone app installs it, and the person confirms there |
201
202
  | Phone: `Wrong code` when pairing by typing | Old or mistyped code | Read `serve.typing.code` again from the status output |
202
203
  | Phone: `Needs Android X` on a build | Build's minSdk is above the phone's Android | Tell the person; it can't install there |
203
204
  | Phone shows **Can't reach** / builds never appear | Different Wi-Fi, VPN, or firewall blocking port 8765 | Check `lanAddresses` in the status output is the Wi-Fi IP; ask about guest Wi-Fi/VPN |
@@ -40,7 +40,7 @@ export function capabilities(sdk) {
40
40
  ? { id: 'files', label: 'Files', status: 'ok', note: 'Saved to Downloads/DroidSuite.', fix: null }
41
41
  : { id: 'files', label: 'Files', status: 'limited', note: "Saved in DroidSuite's own folder.", fix: 'Open them from the DroidSuite app.' },
42
42
  sdk >= NOTIFY_PERMISSION_SDK
43
- ? { id: 'notify', label: 'Notifications', status: 'ok', note: 'Needs permission.', fix: 'Allow notifications, or install prompts can\'t reach you.' }
43
+ ? { id: 'notify', label: 'Notifications', status: 'ok', note: 'Need your OK.', fix: 'Allow notifications when Android asks, or install prompts can\'t reach you.' }
44
44
  : { id: 'notify', label: 'Notifications', status: 'ok', note: 'On.', fix: null },
45
45
  ];
46
46
  }
package/src/commands.mjs CHANGED
@@ -5,14 +5,16 @@ import { DEFAULT_PORT } from './config.mjs';
5
5
  export const COMMANDS = [
6
6
  {
7
7
  name: 'setup',
8
- summary: 'First-run setup: get adb ready for Wi-Fi debugging, add it to PATH, show next steps',
9
- usage: 'droidsuite setup [--yes] [--skip-adb]',
8
+ summary: 'First-run setup: add the DroidSuite icon, get adb ready for Wi-Fi debugging, offer the AI agent skill, show next steps',
9
+ usage: 'droidsuite setup [--yes] [--skip-adb] [--no-shortcut]',
10
10
  options: [
11
11
  ['-y, --yes', 'Answer yes to every question (accepts the Platform-Tools license).'],
12
12
  ['--skip-adb', "Don't check or install adb (only needed for Debug over Wi-Fi)."],
13
+ ['--no-shortcut', "Don't add the DroidSuite icon."],
14
+ ['--shortcut', 'Add the icon even with no terminal to answer in. The install scripts use this.'],
13
15
  ],
14
16
  examples: ['droidsuite setup'],
15
- notes: 'The install scripts run this for you. Asks before installing anything or changing PATH; with no terminal to answer in, it skips.',
17
+ notes: 'The install scripts run this for you. Have Node.js and nothing installed yet? npx droidsuite setup installs DroidSuite too. Asks before installing adb, changing PATH or adding the skill; with no terminal to answer in, it skips.',
16
18
  },
17
19
  {
18
20
  name: 'serve',
@@ -159,16 +161,16 @@ export const COMMANDS = [
159
161
  },
160
162
  {
161
163
  name: 'skill',
162
- summary: 'Teach AI coding agents (Claude Code and others) to use droidsuite, in every project',
164
+ summary: 'Teach AI coding agents (Claude Code, Codex, Gemini CLI, Cursor and others) to use droidsuite, in every project',
163
165
  usage: 'droidsuite skill [--project | --dir <folder>] [--print]',
164
166
  options: [
165
- ['--project', 'Install only into the current project (.claude/skills) instead of for every project on this computer.'],
166
- ['--global', 'Every project on this computer (~/.claude/skills). This is the default.'],
167
- ['--dir <folder>', 'Install into another skills folder, for agents that read skills from elsewhere.'],
167
+ ['--project', 'Install only into the current project instead of for every project on this computer.'],
168
+ ['--global', 'Every project on this computer. This is the default.'],
169
+ ['--dir <folder>', 'Install into another skills folder, for an agent that reads skills from somewhere else.'],
168
170
  ['--print', 'Only print where the bundled skill is.'],
169
171
  ],
170
- examples: ['droidsuite skill # every project on this computer: ~/.claude/skills/droidsuite', 'droidsuite skill --project # only this project: .claude/skills/droidsuite (commit it to share)'],
171
- notes: 'The skill shows an agent how to install builds on your phone, read logcat and screenshots over Wi-Fi, and move files, and which steps need you (pairing, Allow, PIN). Run it again after updating droidsuite.',
172
+ examples: ['droidsuite skill # every project on this computer, for every agent', 'droidsuite skill --project # only this project (commit it to share)', 'droidsuite skill --dir ~/.my-agent/skills'],
173
+ notes: 'The installer offers this when it finds an AI agent on your computer. Always installs into .agents/skills and .claude/skills, and into the folders of Gemini CLI, Antigravity, Cursor, GitHub Copilot, Windsurf, Qwen Code and Amp when they are on this computer. The skill shows an agent how to install builds on your phone, read logcat and screenshots over Wi-Fi, and move files, and which steps need you (pairing, Allow, PIN). Run it again after updating droidsuite or adding an agent.',
172
174
  },
173
175
  {
174
176
  name: 'help',
package/src/debug.mjs CHANGED
@@ -414,7 +414,7 @@ export async function diagnose(deps, { phones = [], ownMdns = true, platform = p
414
414
  ];
415
415
  const verdict = (text, next = null) => ({ checks, verdict: text, next });
416
416
 
417
- if (wifi.some((d) => d.state === 'device')) return verdict('Connected. Builds install with no tap.');
417
+ if (wifi.some((d) => d.state === 'device')) return verdict('Connected. Builds install through adb. Xiaomi, Redmi and POCO phones show a prompt to tap each time.');
418
418
  const old = live.find((p) => p.sdk && p.sdk < WIRELESS_DEBUG_SDK);
419
419
  if (old && !seenIps.length && !live.some((p) => p.sdk >= WIRELESS_DEBUG_SDK)) {
420
420
  return verdict(`${old.name} has ${androidName(old.sdk)}. Wireless debugging needs Android 11.`, 'Plug in a USB cable once (USB debugging on), then run: droidsuite debug usb');
package/src/server.mjs CHANGED
@@ -265,6 +265,8 @@ export function createBridgeServer({
265
265
 
266
266
  const heartbeat = setInterval(() => {
267
267
  for (const res of [...phones.keys(), ...pages]) res.write(': ping\n\n');
268
+ // Notices Wireless debugging connected or dropped from outside (`droidsuite debug`, Android Studio): route and name follow.
269
+ for (const [res, phone] of phones) resolveAdb(res, phone).catch(() => {});
268
270
  pruneCodeFailures();
269
271
  if (Date.now() > typingExpiresAt) changeTypingCode('Typing code expired');
270
272
  }, 20000).unref();
package/src/setup.mjs CHANGED
@@ -3,12 +3,17 @@
3
3
  import { dirname } from 'node:path';
4
4
  import { LICENSE_URL, toolsHome } from './platformtools.mjs';
5
5
 
6
+ /** True when this copy of the program is a temporary one: `npx`, `pnpm dlx`. */
7
+ export const isTemporaryRun = (cliPath) => /[\\/](_npx|dlx-[^\\/]*)[\\/]/.test(cliPath);
8
+
6
9
  /**
7
10
  * @param {object} deps
8
11
  * @param {import('./debugservice.mjs').DebugService} deps.service
9
12
  * @param {(question: string, defaultYes: boolean) => Promise<boolean>} deps.confirm
10
13
  * @param {(line: string) => void} deps.say
11
14
  * @param {(ms: number) => Promise<void>} [deps.sleep]
15
+ * @param {{ present: () => boolean, install: () => { targets: object[], updated: boolean } }} [deps.skill]
16
+ * @param {{ create: () => Promise<unknown>, where: string }} [deps.shortcut] Left out when no icon is wanted.
12
17
  * @param {{ yes?: boolean, skipAdb?: boolean }} [opts]
13
18
  * Resolves to a summary: { adb: 'ready'|'installed'|'skipped'|'failed', path: 'ok'|'added'|'skipped'|'failed' }.
14
19
  */
@@ -16,7 +21,7 @@ export async function runSetup(deps, opts = {}) {
16
21
  const { service, say } = deps;
17
22
  const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
18
23
  const ask = (q, def) => (opts.yes ? Promise.resolve(true) : deps.confirm(q, def));
19
- const result = { adb: 'skipped', path: 'skipped' };
24
+ const result = { adb: 'skipped', path: 'skipped', skill: 'skipped', shortcut: 'skipped' };
20
25
 
21
26
  say('DroidSuite setup');
22
27
  say('');
@@ -76,13 +81,43 @@ export async function runSetup(deps, opts = {}) {
76
81
  }
77
82
  }
78
83
 
84
+ // Only for people who have an AI coding agent: nobody else needs these folders.
85
+ if (deps.skill?.present()) {
86
+ say('');
87
+ if (await ask('Found an AI coding agent on this computer. Let it use your phone? (adds the droidsuite skill to its folder)', true)) {
88
+ try {
89
+ const { targets, updated } = deps.skill.install();
90
+ say(`✓ ${updated ? 'Updated' : 'Added'} the droidsuite skill for your AI agents (${targets.length} folders).`);
91
+ result.skill = 'installed';
92
+ } catch (err) {
93
+ say(`✗ Couldn't add the skill: ${err.message}. Try again later with: droidsuite skill`);
94
+ result.skill = 'failed';
95
+ }
96
+ } else {
97
+ say(' Skipped. Add it later with: droidsuite skill');
98
+ }
99
+ }
100
+
101
+ // One click from now on. It only adds an icon, so unlike adb it doesn't ask.
102
+ if (deps.shortcut) {
103
+ say('');
104
+ try {
105
+ await deps.shortcut.create();
106
+ say(`✓ Added the DroidSuite icon: ${deps.shortcut.where}.`);
107
+ result.shortcut = 'added';
108
+ } catch (err) {
109
+ say(`✗ Couldn't add the icon: ${err.message}. Try again later with: droidsuite shortcut --desktop`);
110
+ result.shortcut = 'failed';
111
+ }
112
+ }
113
+
79
114
  say('');
80
115
  say('Next:');
81
- say(' 1. Open "DroidSuite" (the icon the installer added, or: droidsuite open). In an Android project: droidsuite serve');
116
+ say(` 1. Open "DroidSuite" (${result.shortcut === 'added' ? 'the icon' : 'run: droidsuite open'}). In an Android project: droidsuite serve`);
82
117
  say(' 2. No app on the phone yet? Open the address serve prints (http://<this computer>:8765/app)');
83
118
  say(' on the phone, or scan "Get the phone app" at http://localhost:8765.');
84
119
  say(' 3. In the app tap Pair and scan the pairing QR code. Turn on Live.');
85
120
  say(' Debug over Wi-Fi: open http://localhost:8765 while serve runs, or run "droidsuite debug setup".');
86
- say(' Use an AI coding agent such as Claude Code? Run "droidsuite skill" once, so it can install and check your app on the phone.');
121
+ if (result.skill !== 'installed') say(' Use an AI coding agent such as Claude Code, Codex or Gemini CLI? Run "droidsuite skill" once, so it can install and check your app on the phone.');
87
122
  return result;
88
123
  }
package/src/skill.mjs CHANGED
@@ -8,15 +8,48 @@ export const SKILL_NAME = 'droidsuite';
8
8
  const OLD_SKILL_NAME = 'apk-bridge';
9
9
  export const SKILL_SOURCE = fileURLToPath(new URL(`../skills/${SKILL_NAME}`, import.meta.url));
10
10
 
11
+ /**
12
+ * Every agent's skills folder, relative to the home folder (every project) or the project (--project).
13
+ * The first two always get a copy: most agents read one of them. Checked with the real programs:
14
+ * .agents/skills Codex (home and project), Antigravity and OpenCode (project)
15
+ * .claude/skills Claude Code, OpenCode
16
+ * .gemini/skills Antigravity (home)
17
+ * The rest come from each agent's documentation. They get a copy only when `when` already exists, so nobody ends up
18
+ * with folders for programs they don't have.
19
+ */
20
+ const AGENT_FOLDERS = [
21
+ { skills: ['.agents', 'skills'], agents: 'Codex, and agents that share this folder' },
22
+ { skills: ['.claude', 'skills'], agents: 'Claude Code, OpenCode' },
23
+ { when: ['.gemini'], skills: ['.gemini', 'skills'], agents: 'Gemini CLI, Antigravity' },
24
+ { when: ['.cursor'], skills: ['.cursor', 'skills'], agents: 'Cursor' },
25
+ { when: ['.copilot'], skills: ['.copilot', 'skills'], agents: 'GitHub Copilot' },
26
+ { when: ['.codeium', 'windsurf'], skills: ['.codeium', 'windsurf', 'skills'], agents: 'Windsurf' },
27
+ { when: ['.qwen'], skills: ['.qwen', 'skills'], agents: 'Qwen Code' },
28
+ { when: ['.config', 'agents'], skills: ['.config', 'agents', 'skills'], agents: 'Amp' },
29
+ ];
30
+
31
+ /** The folders to install into under [base], each with the agents that read it. */
32
+ function foldersUnder(base) {
33
+ return AGENT_FOLDERS
34
+ .filter((f) => !f.when || existsSync(join(base, ...f.when)))
35
+ .map((f) => ({ path: join(base, ...f.skills, SKILL_NAME), agents: f.agents }));
36
+ }
37
+
38
+ /** True when any AI coding agent has left its folder in [home], so setup only asks people who use one. */
39
+ export function hasAgents({ home = homedir() } = {}) {
40
+ const others = [['.codex'], ['.config', 'opencode']]; // these read the shared folders, and have no folder of their own above
41
+ return [...AGENT_FOLDERS.map((f) => f.when ?? [f.skills[0]]), ...others].some((p) => existsSync(join(home, ...p)));
42
+ }
43
+
11
44
  /**
12
45
  * Where the skill goes:
13
- * - default: `~/.claude/skills/droidsuite`, so agents know DroidSuite in every project on this computer
14
- * - project: `<cwd>/.claude/skills/droidsuite` (commit it so everyone working on the project has it)
46
+ * - default: every agent's folder in the home folder, so agents know DroidSuite in every project on this computer
47
+ * - project: the same folders in <cwd> (commit them so everyone working on the project has it)
15
48
  * - dir: `<dir>/droidsuite`, for agents that read skills from somewhere else
16
49
  */
17
- export function skillTarget({ project = false, dir = null, cwd = process.cwd(), home = homedir() } = {}) {
18
- if (dir) return join(resolve(cwd, dir), SKILL_NAME);
19
- return join(project ? cwd : home, '.claude', 'skills', SKILL_NAME);
50
+ export function skillTargets({ project = false, dir = null, cwd = process.cwd(), home = homedir() } = {}) {
51
+ if (dir) return [{ path: join(resolve(cwd, dir), SKILL_NAME), agents: null }];
52
+ return foldersUnder(project ? cwd : home);
20
53
  }
21
54
 
22
55
  function copySkill(source, path) {
@@ -33,21 +66,22 @@ function removeOldName(path) {
33
66
  }
34
67
 
35
68
  /**
36
- * Copies (or refreshes) the skill. Returns { path, updated, alsoProject }: updated when an older copy was replaced;
37
- * alsoProject when a global install also refreshed this project's own copy (an older `droidsuite skill` put one
38
- * there, and it would otherwise shadow the new global one in this project).
69
+ * Copies (or refreshes) the skill. Returns { targets, updated, alsoProject }: updated when an older copy was replaced;
70
+ * alsoProject lists this project's own copies that a global install also refreshed (one left by an older
71
+ * `droidsuite skill` would otherwise shadow the new global one in this project).
39
72
  */
40
73
  export function installSkill(options = {}, source = SKILL_SOURCE) {
41
- const path = skillTarget(options);
42
- const updated = existsSync(path);
43
- copySkill(source, path);
44
- let alsoProject = null;
74
+ const targets = skillTargets(options);
75
+ const updated = targets.some((t) => existsSync(t.path));
76
+ for (const t of targets) copySkill(source, t.path);
77
+ const alsoProject = [];
45
78
  if (!options.project && !options.dir) {
46
- const project = skillTarget({ ...options, project: true });
47
- if (project !== path && existsSync(project)) {
48
- copySkill(source, project);
49
- alsoProject = project;
79
+ const installed = new Set(targets.map((t) => t.path));
80
+ for (const t of skillTargets({ ...options, project: true })) {
81
+ if (installed.has(t.path) || !existsSync(t.path)) continue;
82
+ copySkill(source, t.path);
83
+ alsoProject.push(t.path);
50
84
  }
51
85
  }
52
- return { path, updated, alsoProject };
86
+ return { targets, updated, alsoProject };
53
87
  }
package/web/index.html CHANGED
@@ -473,7 +473,7 @@
473
473
  <div id="old-phones"></div>
474
474
  </div>
475
475
  </div>
476
- <div class="sub" id="agent-note" style="margin-top:18px">Use an AI coding agent such as Claude Code? Run <code class="mono">droidsuite skill</code> once in a terminal, so it can install and check your app on the phone.</div>
476
+ <div class="sub" id="agent-note" style="margin-top:18px">Use an AI coding agent such as Claude Code, Codex or Gemini CLI? Run <code class="mono">droidsuite skill</code> once in a terminal, so it can install and check your app on the phone.</div>
477
477
  </section>
478
478
  <p class="credit">DroidSuite · made by Mahmoud Helmy</p>
479
479
  </div>
@@ -496,7 +496,7 @@
496
496
  /** What this phone's Android version can't do (or needs), with the fix. */
497
497
  const caps = (p) => {
498
498
  const rows = (p.capabilities ?? []).filter((c) => c.status !== 'ok' || c.fix);
499
- return rows.length ? `<div class="caps">${rows.map((c) => `<div>${c.status === 'ok' ? '✓' : c.status === 'no' ? '✗' : '!'} <b>${esc(c.label)}:</b> ${esc(c.note)}${c.fix ? ` ${esc(c.fix)}` : ''}</div>`).join('')}</div>` : '';
499
+ return rows.length ? `<div class="caps">${rows.map((c) => `<div>${c.status === 'no' ? '✗' : '!'} <b>${esc(c.label)}:</b> ${esc(c.note)}${c.fix ? ` ${esc(c.fix)}` : ''}</div>`).join('')}</div>` : '';
500
500
  };
501
501
  /** "needs Android 12+" when a connected phone is older than the build's minSdk. */
502
502
  const tooNewFor = (b, phones) => {