@pixelkit-labs/cli 1.4.3
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/LICENSE +21 -0
- package/README.md +64 -0
- package/build/index.d.ts +14 -0
- package/build/index.js +430 -0
- package/index.ts +515 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Traves Theberge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @pixelkit-labs/cli
|
|
2
|
+
|
|
3
|
+
Command-line diagnostics for [PixelKit](https://www.npmjs.com/package/pixelkit). Its only command,
|
|
4
|
+
`pixelkit doctor`, answers "why is everything showing —?" before you file an issue.
|
|
5
|
+
|
|
6
|
+
PixelKit hooks report `source: 'unavailable'` and render an em dash when a reading cannot be taken
|
|
7
|
+
on real hardware. That is correct behaviour, but on the wrong device, without a development build,
|
|
8
|
+
or without the native packages installed, every hook looks that way at once. `doctor` runs the
|
|
9
|
+
checks a maintainer would run by hand.
|
|
10
|
+
|
|
11
|
+
**Zero third-party dependencies.** It uses only Node's `child_process` and `util`.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @pixelkit-labs/cli doctor
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
or, installed as a dev dependency:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install --save-dev @pixelkit-labs/cli
|
|
21
|
+
npx pixelkit doctor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it checks
|
|
25
|
+
|
|
26
|
+
1. **adb on PATH and exactly one device connected** (`adb devices`). If several devices are
|
|
27
|
+
connected, they are listed and you are told to pass `--serial`.
|
|
28
|
+
2. **The connected device's identity** — manufacturer, model, Android release, SDK int
|
|
29
|
+
(`adb shell getprop`). States plainly whether it is a Pixel, because most hooks report
|
|
30
|
+
`unavailable` otherwise.
|
|
31
|
+
3. **Whether a PixelKit-based development build is installed** (`adb shell pm path <package>`),
|
|
32
|
+
keyed off `--package` (default `com.pixelkit.sdk`). Expo Go can never satisfy this: the Kotlin
|
|
33
|
+
Expo Modules (`@pixelkit-labs/native`, `@pixelkit-labs/mlkit`) must be compiled in.
|
|
34
|
+
4. **Whether `@pixelkit-labs/native` and `@pixelkit-labs/mlkit` resolve from the current project**
|
|
35
|
+
(`require.resolve` from the working directory), and which hook groups are therefore available.
|
|
36
|
+
A missing `@pixelkit-labs/mlkit` is reported as informational, not a failure — it is the opt-in ML
|
|
37
|
+
Kit package.
|
|
38
|
+
5. **Whether AICore is present on the device** (`adb shell pm list packages`, matched for
|
|
39
|
+
"aicore"), which Gemini Nano needs. Not-applicable on a non-Pixel.
|
|
40
|
+
6. **Whether `adb reverse tcp:8081 tcp:8081` is set**, so a development client can reach Metro on
|
|
41
|
+
localhost.
|
|
42
|
+
|
|
43
|
+
Each check is reported as `PASS`, `FAIL`, `N/A`, or `????` ("could not be determined" — the check
|
|
44
|
+
itself could not be run, for example because adb is missing or the device went offline).
|
|
45
|
+
`doctor` never guesses a result: an inconclusive check is reported as such, the same discipline
|
|
46
|
+
PixelKit hooks use for `source: 'unavailable'`.
|
|
47
|
+
|
|
48
|
+
## Options
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
pixelkit doctor [--package <id>] [--serial <serial>]
|
|
52
|
+
|
|
53
|
+
--package <id> Application id of the installed PixelKit development build
|
|
54
|
+
(default: com.pixelkit.sdk)
|
|
55
|
+
--serial <id> adb serial to target when more than one device is connected
|
|
56
|
+
-h, --help Show help
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Exit code
|
|
60
|
+
|
|
61
|
+
`0` when every check passed or was not applicable. `1` when any check failed, or could not be
|
|
62
|
+
determined at all.
|
|
63
|
+
|
|
64
|
+
MIT
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file packages/cli/index.ts
|
|
4
|
+
* @description `pixelkit doctor`: the first and only command of `@pixelkit-labs/cli`. PixelKit hooks
|
|
5
|
+
* report `source: 'unavailable'` and render an em dash when a reading cannot be taken on real
|
|
6
|
+
* hardware; on the wrong device, without a development build, or without the native packages
|
|
7
|
+
* installed, every hook looks that way at once. `doctor` runs the checks a maintainer would run
|
|
8
|
+
* by hand — adb on PATH, the connected device's identity, the installed development build,
|
|
9
|
+
* whether `@pixelkit-labs/native` and `@pixelkit-labs/mlkit` resolve from the current project, AICore, and
|
|
10
|
+
* the Metro port forward — and reports each as pass, fail, not-applicable, or "could not
|
|
11
|
+
* determine" when the check itself could not be run. No dependency other than Node's
|
|
12
|
+
* `child_process` and `util`; nothing here is guessed.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
package/build/index.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* @file packages/cli/index.ts
|
|
5
|
+
* @description `pixelkit doctor`: the first and only command of `@pixelkit-labs/cli`. PixelKit hooks
|
|
6
|
+
* report `source: 'unavailable'` and render an em dash when a reading cannot be taken on real
|
|
7
|
+
* hardware; on the wrong device, without a development build, or without the native packages
|
|
8
|
+
* installed, every hook looks that way at once. `doctor` runs the checks a maintainer would run
|
|
9
|
+
* by hand — adb on PATH, the connected device's identity, the installed development build,
|
|
10
|
+
* whether `@pixelkit-labs/native` and `@pixelkit-labs/mlkit` resolve from the current project, AICore, and
|
|
11
|
+
* the Metro port forward — and reports each as pass, fail, not-applicable, or "could not
|
|
12
|
+
* determine" when the check itself could not be run. No dependency other than Node's
|
|
13
|
+
* `child_process` and `util`; nothing here is guessed.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
const child_process_1 = require("child_process");
|
|
17
|
+
const util_1 = require("util");
|
|
18
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
19
|
+
const ANSI = {
|
|
20
|
+
reset: '\x1b[0m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
22
|
+
dim: '\x1b[2m',
|
|
23
|
+
green: '\x1b[32m',
|
|
24
|
+
red: '\x1b[31m',
|
|
25
|
+
yellow: '\x1b[33m',
|
|
26
|
+
};
|
|
27
|
+
const STATUS_LABEL = {
|
|
28
|
+
pass: `${ANSI.green}PASS${ANSI.reset}`,
|
|
29
|
+
fail: `${ANSI.red}FAIL${ANSI.reset}`,
|
|
30
|
+
'n/a': `${ANSI.dim}N/A ${ANSI.reset}`,
|
|
31
|
+
unknown: `${ANSI.yellow}UNKN${ANSI.reset}`,
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Runs `adb <args>` via Node's `child_process.execFile` (promisified with `util.promisify`) and
|
|
35
|
+
* returns its stdout, or a description of why the command could not be run. Never throws: every
|
|
36
|
+
* failure mode (adb absent, device offline, timeout) becomes a value the caller reports.
|
|
37
|
+
*/
|
|
38
|
+
async function runAdb(args, timeoutMs = 8000) {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execFileAsync('adb', args, {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
timeout: timeoutMs,
|
|
43
|
+
windowsHide: true,
|
|
44
|
+
});
|
|
45
|
+
return { ok: true, stdout };
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const e = err;
|
|
49
|
+
if (e.code === 'ENOENT')
|
|
50
|
+
return { ok: false, error: 'adb is not on PATH' };
|
|
51
|
+
if (e.killed || e.signal) {
|
|
52
|
+
return { ok: false, error: `adb ${args.join(' ')} timed out after ${timeoutMs}ms` };
|
|
53
|
+
}
|
|
54
|
+
const stderr = (e.stderr || '').toString().trim();
|
|
55
|
+
return { ok: false, error: stderr || e.message || String(err) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Parses `adb devices` output: a header line, then `<serial>\t<state>` per connected device. */
|
|
59
|
+
function parseAdbDevices(stdout) {
|
|
60
|
+
return stdout
|
|
61
|
+
.split(/\r?\n/)
|
|
62
|
+
.slice(1)
|
|
63
|
+
.map((l) => l.trim())
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map((l) => {
|
|
66
|
+
const [serial, state] = l.split(/\s+/);
|
|
67
|
+
return { serial, state };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check 1: adb is on PATH and exactly one device is connected (`adb devices`). When a `--serial`
|
|
72
|
+
* flag was given it is used to disambiguate instead of requiring exactly one device.
|
|
73
|
+
*/
|
|
74
|
+
async function checkAdbAndDevice(explicitSerial) {
|
|
75
|
+
const version = await runAdb(['version']);
|
|
76
|
+
if (!version.ok) {
|
|
77
|
+
return {
|
|
78
|
+
result: {
|
|
79
|
+
name: 'adb on PATH',
|
|
80
|
+
status: 'fail',
|
|
81
|
+
reason: version.error,
|
|
82
|
+
fix: 'Install Android platform-tools and add it to PATH: https://developer.android.com/tools/releases/platform-tools',
|
|
83
|
+
},
|
|
84
|
+
serial: null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const devicesRes = await runAdb(['devices']);
|
|
88
|
+
if (!devicesRes.ok) {
|
|
89
|
+
return {
|
|
90
|
+
result: {
|
|
91
|
+
name: 'adb device',
|
|
92
|
+
status: 'unknown',
|
|
93
|
+
reason: `adb is on PATH but "adb devices" could not be run: ${devicesRes.error}`,
|
|
94
|
+
},
|
|
95
|
+
serial: null,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const devices = parseAdbDevices(devicesRes.stdout);
|
|
99
|
+
const ready = devices.filter((d) => d.state === 'device');
|
|
100
|
+
if (explicitSerial) {
|
|
101
|
+
const match = ready.find((d) => d.serial === explicitSerial);
|
|
102
|
+
if (!match) {
|
|
103
|
+
const seen = devices.length ? devices.map((d) => `${d.serial} (${d.state})`).join(', ') : 'none';
|
|
104
|
+
return {
|
|
105
|
+
result: {
|
|
106
|
+
name: 'adb device',
|
|
107
|
+
status: 'fail',
|
|
108
|
+
reason: `--serial ${explicitSerial} is not connected in "device" state. Connected: ${seen}.`,
|
|
109
|
+
fix: 'Run `adb devices` and pass one of the listed serials.',
|
|
110
|
+
},
|
|
111
|
+
serial: null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
result: { name: 'adb device', status: 'pass', reason: `targeting ${explicitSerial} (--serial)` },
|
|
116
|
+
serial: explicitSerial,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (devices.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
result: {
|
|
122
|
+
name: 'adb device',
|
|
123
|
+
status: 'fail',
|
|
124
|
+
reason: 'no device connected.',
|
|
125
|
+
fix: 'Connect the Pixel over USB (accept the RSA prompt) or `adb connect <ip>:<port>` for wireless adb, then re-run.',
|
|
126
|
+
},
|
|
127
|
+
serial: null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (ready.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
result: {
|
|
133
|
+
name: 'adb device',
|
|
134
|
+
status: 'fail',
|
|
135
|
+
reason: `${devices.length} device(s) present, none in "device" state: ${devices
|
|
136
|
+
.map((d) => `${d.serial} (${d.state})`)
|
|
137
|
+
.join(', ')}.`,
|
|
138
|
+
fix: 'unauthorized: accept the USB debugging prompt on the device screen. offline: reconnect the cable or re-run `adb connect`.',
|
|
139
|
+
},
|
|
140
|
+
serial: null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (ready.length > 1) {
|
|
144
|
+
return {
|
|
145
|
+
result: {
|
|
146
|
+
name: 'adb device',
|
|
147
|
+
status: 'fail',
|
|
148
|
+
reason: `${ready.length} devices connected: ${ready.map((d) => d.serial).join(', ')}.`,
|
|
149
|
+
fix: 'Re-run with --serial <serial> to target one of them.',
|
|
150
|
+
},
|
|
151
|
+
serial: null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
result: { name: 'adb device', status: 'pass', reason: `${ready[0].serial} connected` },
|
|
156
|
+
serial: ready[0].serial,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Check 2: the connected device's manufacturer, model, Android release and SDK int, read with
|
|
161
|
+
* `adb shell getprop`. Most PixelKit hooks target Pixel-only APIs, so this states plainly whether
|
|
162
|
+
* the device is a Pixel rather than leaving that to be inferred from a wall of em dashes.
|
|
163
|
+
*/
|
|
164
|
+
async function checkDeviceIdentity(serial) {
|
|
165
|
+
if (!serial) {
|
|
166
|
+
return {
|
|
167
|
+
result: {
|
|
168
|
+
name: 'Device identity',
|
|
169
|
+
status: 'unknown',
|
|
170
|
+
reason: 'skipped: no single adb device selected (see "adb device" above).',
|
|
171
|
+
},
|
|
172
|
+
manufacturer: null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const props = [
|
|
176
|
+
'ro.product.manufacturer',
|
|
177
|
+
'ro.product.model',
|
|
178
|
+
'ro.build.version.release',
|
|
179
|
+
'ro.build.version.sdk',
|
|
180
|
+
];
|
|
181
|
+
const values = {};
|
|
182
|
+
for (const prop of props) {
|
|
183
|
+
const res = await runAdb(['-s', serial, 'shell', 'getprop', prop]);
|
|
184
|
+
if (!res.ok) {
|
|
185
|
+
return {
|
|
186
|
+
result: { name: 'Device identity', status: 'unknown', reason: `could not read ${prop}: ${res.error}` },
|
|
187
|
+
manufacturer: null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
values[prop] = res.stdout.trim();
|
|
191
|
+
}
|
|
192
|
+
const manufacturer = values['ro.product.manufacturer'];
|
|
193
|
+
const model = values['ro.product.model'];
|
|
194
|
+
const release = values['ro.build.version.release'];
|
|
195
|
+
const sdk = values['ro.build.version.sdk'];
|
|
196
|
+
const summary = `${manufacturer} ${model}, Android ${release} (SDK ${sdk})`;
|
|
197
|
+
const isPixel = manufacturer.toLowerCase() === 'google';
|
|
198
|
+
if (!isPixel) {
|
|
199
|
+
return {
|
|
200
|
+
result: {
|
|
201
|
+
name: 'Device identity',
|
|
202
|
+
status: 'fail',
|
|
203
|
+
reason: `${summary} — not a Pixel. Most PixelKit hooks are written against Pixel-only APIs and will report source: "unavailable" here.`,
|
|
204
|
+
},
|
|
205
|
+
manufacturer,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { result: { name: 'Device identity', status: 'pass', reason: summary }, manufacturer };
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Check 3: whether a PixelKit-based development build is installed (`adb shell pm path
|
|
212
|
+
* <package>`), keyed off `--package` (default `com.pixelkit.sdk`). Expo Go can never satisfy this
|
|
213
|
+
* check: `@pixelkit-labs/native` and `@pixelkit-labs/mlkit` are Kotlin Expo Modules that must be compiled
|
|
214
|
+
* into a development build, so that limitation is stated regardless of the result.
|
|
215
|
+
*/
|
|
216
|
+
async function checkDevBuild(serial, packageId) {
|
|
217
|
+
if (!serial) {
|
|
218
|
+
return { name: 'PixelKit development build', status: 'unknown', reason: 'skipped: no single adb device selected.' };
|
|
219
|
+
}
|
|
220
|
+
const res = await runAdb(['-s', serial, 'shell', 'pm', 'path', packageId]);
|
|
221
|
+
const installed = res.ok && /^package:/m.test(res.stdout.trim());
|
|
222
|
+
const goNote = 'Expo Go can never run PixelKit: @pixelkit-labs/native and @pixelkit-labs/mlkit are compiled Kotlin Expo Modules, so a development build is required.';
|
|
223
|
+
if (installed) {
|
|
224
|
+
return {
|
|
225
|
+
name: 'PixelKit development build',
|
|
226
|
+
status: 'pass',
|
|
227
|
+
reason: `${packageId} is installed on ${serial}. ${goNote}`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
name: 'PixelKit development build',
|
|
232
|
+
status: 'fail',
|
|
233
|
+
reason: `${packageId} is not installed on ${serial}. ${goNote}`,
|
|
234
|
+
fix: `Build a development client and install it — npx expo run:android (or eas build --profile development), then adb -s ${serial} install -r -g <path-to-apk>. Pass --package <id> if your app id differs.`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Check 4: whether `@pixelkit-labs/native` and `@pixelkit-labs/mlkit` resolve from the current project
|
|
239
|
+
* (`require.resolve` from `process.cwd()`, walking the real node_modules chain — nothing is
|
|
240
|
+
* assumed from package.json alone). `@pixelkit-labs/mlkit` is opt-in, so its absence is reported as
|
|
241
|
+
* informational (`n/a`), never a failure.
|
|
242
|
+
*/
|
|
243
|
+
function resolveFrom(name, cwd) {
|
|
244
|
+
try {
|
|
245
|
+
const path = require.resolve(name, { paths: [cwd] });
|
|
246
|
+
return { ok: true, path };
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
return { ok: false, error: err.message };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function checkNativeModules(cwd) {
|
|
253
|
+
const native = resolveFrom('@pixelkit-labs/native', cwd);
|
|
254
|
+
const mlkit = resolveFrom('@pixelkit-labs/mlkit', cwd);
|
|
255
|
+
const results = [];
|
|
256
|
+
if (native.ok) {
|
|
257
|
+
results.push({
|
|
258
|
+
name: '@pixelkit-labs/native resolvable',
|
|
259
|
+
status: 'pass',
|
|
260
|
+
reason: `resolved from ${native.path}. Silicon (SoC, CPU, memory, thermal, GPU), display, torch and haptics hooks are available.`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
results.push({
|
|
265
|
+
name: '@pixelkit-labs/native resolvable',
|
|
266
|
+
status: 'fail',
|
|
267
|
+
reason: `not resolvable from ${cwd}. Silicon, display, torch and haptics hooks will report source: "unavailable".`,
|
|
268
|
+
fix: 'npm install @pixelkit-labs/native (or install `@pixelkit-labs/sdk`, which depends on it directly).',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
if (mlkit.ok) {
|
|
272
|
+
results.push({
|
|
273
|
+
name: '@pixelkit-labs/mlkit resolvable',
|
|
274
|
+
status: 'pass',
|
|
275
|
+
reason: `resolved from ${mlkit.path}. Gemini Nano / on-device ML Kit hooks are available.`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
results.push({
|
|
280
|
+
name: '@pixelkit-labs/mlkit resolvable',
|
|
281
|
+
status: 'n/a',
|
|
282
|
+
reason: `not resolvable from ${cwd}. This is opt-in, not a failure — Gemini Nano / on-device ML Kit hooks report source: "unavailable" without it.`,
|
|
283
|
+
fix: 'npm install @pixelkit-labs/mlkit to enable on-device ML Kit hooks.',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Check 5: whether AICore is present (`adb shell pm list packages`, matched for "aicore"), which
|
|
290
|
+
* Gemini Nano needs. Not-applicable on a device that Check 2 already found is not a Pixel.
|
|
291
|
+
*/
|
|
292
|
+
async function checkAiCore(serial, manufacturer) {
|
|
293
|
+
if (manufacturer && manufacturer.toLowerCase() !== 'google') {
|
|
294
|
+
return { name: 'AICore', status: 'n/a', reason: `device manufacturer is ${manufacturer}, not Google; AICore is a Pixel component.` };
|
|
295
|
+
}
|
|
296
|
+
if (!serial) {
|
|
297
|
+
return { name: 'AICore', status: 'unknown', reason: 'skipped: no single adb device selected.' };
|
|
298
|
+
}
|
|
299
|
+
const res = await runAdb(['-s', serial, 'shell', 'pm', 'list', 'packages']);
|
|
300
|
+
if (!res.ok) {
|
|
301
|
+
return { name: 'AICore', status: 'unknown', reason: `could not list packages: ${res.error}` };
|
|
302
|
+
}
|
|
303
|
+
const present = res.stdout.toLowerCase().includes('aicore');
|
|
304
|
+
if (present) {
|
|
305
|
+
return { name: 'AICore', status: 'pass', reason: 'a package matching "aicore" is installed. Gemini Nano can run through @pixelkit-labs/mlkit.' };
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
name: 'AICore',
|
|
309
|
+
status: 'fail',
|
|
310
|
+
reason: 'no package matching "aicore" is installed. useGeminiNano and other on-device Gemini hooks will report source: "unavailable".',
|
|
311
|
+
fix: 'AICore ships through Google Play services on supported Pixels; update Play services and the Play Store, then reboot.',
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Check 6: whether `adb reverse tcp:8081 tcp:8081` is set (`adb reverse --list`), so a
|
|
316
|
+
* development client can reach Metro on localhost.
|
|
317
|
+
*/
|
|
318
|
+
async function checkAdbReverse(serial) {
|
|
319
|
+
if (!serial) {
|
|
320
|
+
return { name: 'adb reverse tcp:8081', status: 'unknown', reason: 'skipped: no single adb device selected.' };
|
|
321
|
+
}
|
|
322
|
+
const res = await runAdb(['-s', serial, 'reverse', '--list']);
|
|
323
|
+
if (!res.ok) {
|
|
324
|
+
return { name: 'adb reverse tcp:8081', status: 'unknown', reason: `could not list adb reverse rules: ${res.error}` };
|
|
325
|
+
}
|
|
326
|
+
const set = res.stdout.split(/\r?\n/).some((l) => /tcp:8081\s+tcp:8081/.test(l));
|
|
327
|
+
if (set) {
|
|
328
|
+
return { name: 'adb reverse tcp:8081', status: 'pass', reason: 'set: the dev client can reach Metro at localhost:8081.' };
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
name: 'adb reverse tcp:8081',
|
|
332
|
+
status: 'fail',
|
|
333
|
+
reason: 'not set. A development build looking for Metro on localhost:8081 will not find it.',
|
|
334
|
+
fix: `adb -s ${serial} reverse tcp:8081 tcp:8081`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function parseArgs(argv) {
|
|
338
|
+
const flags = { packageId: 'com.pixelkit.sdk', help: false };
|
|
339
|
+
let command;
|
|
340
|
+
for (let i = 0; i < argv.length; i++) {
|
|
341
|
+
const arg = argv[i];
|
|
342
|
+
if (arg === '--package') {
|
|
343
|
+
flags.packageId = argv[++i] ?? flags.packageId;
|
|
344
|
+
}
|
|
345
|
+
else if (arg.startsWith('--package=')) {
|
|
346
|
+
flags.packageId = arg.slice('--package='.length);
|
|
347
|
+
}
|
|
348
|
+
else if (arg === '--serial') {
|
|
349
|
+
flags.serial = argv[++i];
|
|
350
|
+
}
|
|
351
|
+
else if (arg.startsWith('--serial=')) {
|
|
352
|
+
flags.serial = arg.slice('--serial='.length);
|
|
353
|
+
}
|
|
354
|
+
else if (arg === '--help' || arg === '-h') {
|
|
355
|
+
flags.help = true;
|
|
356
|
+
}
|
|
357
|
+
else if (command === undefined) {
|
|
358
|
+
command = arg;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return { command, flags };
|
|
362
|
+
}
|
|
363
|
+
function printUsage() {
|
|
364
|
+
console.log(`${ANSI.bold}pixelkit doctor${ANSI.reset} - diagnose why PixelKit hooks might report source: "unavailable"
|
|
365
|
+
|
|
366
|
+
Usage:
|
|
367
|
+
pixelkit doctor [--package <id>] [--serial <serial>]
|
|
368
|
+
|
|
369
|
+
Options:
|
|
370
|
+
--package <id> Application id of the installed PixelKit development build (default: com.pixelkit.sdk)
|
|
371
|
+
--serial <id> adb serial to target when more than one device is connected
|
|
372
|
+
-h, --help Show this help
|
|
373
|
+
`);
|
|
374
|
+
}
|
|
375
|
+
function printResult(r) {
|
|
376
|
+
console.log(`[${STATUS_LABEL[r.status]}] ${ANSI.bold}${r.name}${ANSI.reset} - ${r.reason}`);
|
|
377
|
+
if (r.fix)
|
|
378
|
+
console.log(`${ANSI.dim} fix: ${r.fix}${ANSI.reset}`);
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* Runs all six checks in order and prints them. Returns the process exit code: 0 only when every
|
|
382
|
+
* check passed or was not applicable; 1 when any check failed or — per the "never fabricate a
|
|
383
|
+
* result" rule — could not be determined at all.
|
|
384
|
+
*/
|
|
385
|
+
async function doctor(flags) {
|
|
386
|
+
console.log(`${ANSI.bold}PixelKit doctor${ANSI.reset}\n`);
|
|
387
|
+
const results = [];
|
|
388
|
+
const step1 = await checkAdbAndDevice(flags.serial);
|
|
389
|
+
results.push(step1.result);
|
|
390
|
+
const serial = step1.serial;
|
|
391
|
+
const step2 = await checkDeviceIdentity(serial);
|
|
392
|
+
results.push(step2.result);
|
|
393
|
+
const manufacturer = step2.manufacturer;
|
|
394
|
+
results.push(await checkDevBuild(serial, flags.packageId));
|
|
395
|
+
results.push(...checkNativeModules(process.cwd()));
|
|
396
|
+
results.push(await checkAiCore(serial, manufacturer));
|
|
397
|
+
results.push(await checkAdbReverse(serial));
|
|
398
|
+
for (const r of results)
|
|
399
|
+
printResult(r);
|
|
400
|
+
const failed = results.filter((r) => r.status === 'fail');
|
|
401
|
+
const unresolved = results.filter((r) => r.status === 'unknown');
|
|
402
|
+
const passed = results.filter((r) => r.status === 'pass');
|
|
403
|
+
const notApplicable = results.filter((r) => r.status === 'n/a');
|
|
404
|
+
console.log(`\n${passed.length} passed, ${failed.length} failed, ${unresolved.length} could not be determined, ${notApplicable.length} not applicable.`);
|
|
405
|
+
return failed.length > 0 || unresolved.length > 0 ? 1 : 0;
|
|
406
|
+
}
|
|
407
|
+
async function main() {
|
|
408
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
409
|
+
if (flags.help) {
|
|
410
|
+
printUsage();
|
|
411
|
+
process.exitCode = 0;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (!command) {
|
|
415
|
+
printUsage();
|
|
416
|
+
process.exitCode = 1;
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (command !== 'doctor') {
|
|
420
|
+
console.error(`Unknown command "${command}". The only command is "doctor".\n`);
|
|
421
|
+
printUsage();
|
|
422
|
+
process.exitCode = 1;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
process.exitCode = await doctor(flags);
|
|
426
|
+
}
|
|
427
|
+
main().catch((err) => {
|
|
428
|
+
console.error(`pixelkit doctor crashed: ${err.stack || String(err)}`);
|
|
429
|
+
process.exitCode = 1;
|
|
430
|
+
});
|
package/index.ts
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file packages/cli/index.ts
|
|
4
|
+
* @description `pixelkit doctor`: the first and only command of `@pixelkit-labs/cli`. PixelKit hooks
|
|
5
|
+
* report `source: 'unavailable'` and render an em dash when a reading cannot be taken on real
|
|
6
|
+
* hardware; on the wrong device, without a development build, or without the native packages
|
|
7
|
+
* installed, every hook looks that way at once. `doctor` runs the checks a maintainer would run
|
|
8
|
+
* by hand — adb on PATH, the connected device's identity, the installed development build,
|
|
9
|
+
* whether `@pixelkit-labs/native` and `@pixelkit-labs/mlkit` resolve from the current project, AICore, and
|
|
10
|
+
* the Metro port forward — and reports each as pass, fail, not-applicable, or "could not
|
|
11
|
+
* determine" when the check itself could not be run. No dependency other than Node's
|
|
12
|
+
* `child_process` and `util`; nothing here is guessed.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFile } from 'child_process';
|
|
16
|
+
import { promisify } from 'util';
|
|
17
|
+
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `pass` / `fail` / `n/a` are the three outcomes a check can reach. `unknown` is a fourth,
|
|
22
|
+
* deliberate state for when the check itself could not be run (adb missing, device offline,
|
|
23
|
+
* a transient adb error) — the same discipline as a hook's `source: 'unavailable'`: a result
|
|
24
|
+
* that cannot be verified is reported as such, never guessed into a pass.
|
|
25
|
+
*/
|
|
26
|
+
type Status = 'pass' | 'fail' | 'n/a' | 'unknown';
|
|
27
|
+
|
|
28
|
+
interface CheckResult {
|
|
29
|
+
name: string;
|
|
30
|
+
status: Status;
|
|
31
|
+
reason: string;
|
|
32
|
+
fix?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ANSI = {
|
|
36
|
+
reset: '\x1b[0m',
|
|
37
|
+
bold: '\x1b[1m',
|
|
38
|
+
dim: '\x1b[2m',
|
|
39
|
+
green: '\x1b[32m',
|
|
40
|
+
red: '\x1b[31m',
|
|
41
|
+
yellow: '\x1b[33m',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const STATUS_LABEL: Record<Status, string> = {
|
|
45
|
+
pass: `${ANSI.green}PASS${ANSI.reset}`,
|
|
46
|
+
fail: `${ANSI.red}FAIL${ANSI.reset}`,
|
|
47
|
+
'n/a': `${ANSI.dim}N/A ${ANSI.reset}`,
|
|
48
|
+
unknown: `${ANSI.yellow}UNKN${ANSI.reset}`,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type AdbError = NodeJS.ErrnoException & {
|
|
52
|
+
stdout?: string;
|
|
53
|
+
stderr?: string;
|
|
54
|
+
killed?: boolean;
|
|
55
|
+
signal?: NodeJS.Signals | null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Runs `adb <args>` via Node's `child_process.execFile` (promisified with `util.promisify`) and
|
|
60
|
+
* returns its stdout, or a description of why the command could not be run. Never throws: every
|
|
61
|
+
* failure mode (adb absent, device offline, timeout) becomes a value the caller reports.
|
|
62
|
+
*/
|
|
63
|
+
async function runAdb(
|
|
64
|
+
args: string[],
|
|
65
|
+
timeoutMs = 8000
|
|
66
|
+
): Promise<{ ok: true; stdout: string } | { ok: false; error: string }> {
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await execFileAsync('adb', args, {
|
|
69
|
+
encoding: 'utf8',
|
|
70
|
+
timeout: timeoutMs,
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
});
|
|
73
|
+
return { ok: true, stdout };
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const e = err as AdbError;
|
|
76
|
+
if (e.code === 'ENOENT') return { ok: false, error: 'adb is not on PATH' };
|
|
77
|
+
if (e.killed || e.signal) {
|
|
78
|
+
return { ok: false, error: `adb ${args.join(' ')} timed out after ${timeoutMs}ms` };
|
|
79
|
+
}
|
|
80
|
+
const stderr = (e.stderr || '').toString().trim();
|
|
81
|
+
return { ok: false, error: stderr || e.message || String(err) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface DeviceLine {
|
|
86
|
+
serial: string;
|
|
87
|
+
state: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Parses `adb devices` output: a header line, then `<serial>\t<state>` per connected device. */
|
|
91
|
+
function parseAdbDevices(stdout: string): DeviceLine[] {
|
|
92
|
+
return stdout
|
|
93
|
+
.split(/\r?\n/)
|
|
94
|
+
.slice(1)
|
|
95
|
+
.map((l) => l.trim())
|
|
96
|
+
.filter(Boolean)
|
|
97
|
+
.map((l) => {
|
|
98
|
+
const [serial, state] = l.split(/\s+/);
|
|
99
|
+
return { serial, state };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check 1: adb is on PATH and exactly one device is connected (`adb devices`). When a `--serial`
|
|
105
|
+
* flag was given it is used to disambiguate instead of requiring exactly one device.
|
|
106
|
+
*/
|
|
107
|
+
async function checkAdbAndDevice(
|
|
108
|
+
explicitSerial: string | undefined
|
|
109
|
+
): Promise<{ result: CheckResult; serial: string | null }> {
|
|
110
|
+
const version = await runAdb(['version']);
|
|
111
|
+
if (!version.ok) {
|
|
112
|
+
return {
|
|
113
|
+
result: {
|
|
114
|
+
name: 'adb on PATH',
|
|
115
|
+
status: 'fail',
|
|
116
|
+
reason: version.error,
|
|
117
|
+
fix: 'Install Android platform-tools and add it to PATH: https://developer.android.com/tools/releases/platform-tools',
|
|
118
|
+
},
|
|
119
|
+
serial: null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const devicesRes = await runAdb(['devices']);
|
|
124
|
+
if (!devicesRes.ok) {
|
|
125
|
+
return {
|
|
126
|
+
result: {
|
|
127
|
+
name: 'adb device',
|
|
128
|
+
status: 'unknown',
|
|
129
|
+
reason: `adb is on PATH but "adb devices" could not be run: ${devicesRes.error}`,
|
|
130
|
+
},
|
|
131
|
+
serial: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const devices = parseAdbDevices(devicesRes.stdout);
|
|
136
|
+
const ready = devices.filter((d) => d.state === 'device');
|
|
137
|
+
|
|
138
|
+
if (explicitSerial) {
|
|
139
|
+
const match = ready.find((d) => d.serial === explicitSerial);
|
|
140
|
+
if (!match) {
|
|
141
|
+
const seen = devices.length ? devices.map((d) => `${d.serial} (${d.state})`).join(', ') : 'none';
|
|
142
|
+
return {
|
|
143
|
+
result: {
|
|
144
|
+
name: 'adb device',
|
|
145
|
+
status: 'fail',
|
|
146
|
+
reason: `--serial ${explicitSerial} is not connected in "device" state. Connected: ${seen}.`,
|
|
147
|
+
fix: 'Run `adb devices` and pass one of the listed serials.',
|
|
148
|
+
},
|
|
149
|
+
serial: null,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
result: { name: 'adb device', status: 'pass', reason: `targeting ${explicitSerial} (--serial)` },
|
|
154
|
+
serial: explicitSerial,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (devices.length === 0) {
|
|
159
|
+
return {
|
|
160
|
+
result: {
|
|
161
|
+
name: 'adb device',
|
|
162
|
+
status: 'fail',
|
|
163
|
+
reason: 'no device connected.',
|
|
164
|
+
fix: 'Connect the Pixel over USB (accept the RSA prompt) or `adb connect <ip>:<port>` for wireless adb, then re-run.',
|
|
165
|
+
},
|
|
166
|
+
serial: null,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (ready.length === 0) {
|
|
171
|
+
return {
|
|
172
|
+
result: {
|
|
173
|
+
name: 'adb device',
|
|
174
|
+
status: 'fail',
|
|
175
|
+
reason: `${devices.length} device(s) present, none in "device" state: ${devices
|
|
176
|
+
.map((d) => `${d.serial} (${d.state})`)
|
|
177
|
+
.join(', ')}.`,
|
|
178
|
+
fix: 'unauthorized: accept the USB debugging prompt on the device screen. offline: reconnect the cable or re-run `adb connect`.',
|
|
179
|
+
},
|
|
180
|
+
serial: null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (ready.length > 1) {
|
|
185
|
+
return {
|
|
186
|
+
result: {
|
|
187
|
+
name: 'adb device',
|
|
188
|
+
status: 'fail',
|
|
189
|
+
reason: `${ready.length} devices connected: ${ready.map((d) => d.serial).join(', ')}.`,
|
|
190
|
+
fix: 'Re-run with --serial <serial> to target one of them.',
|
|
191
|
+
},
|
|
192
|
+
serial: null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
result: { name: 'adb device', status: 'pass', reason: `${ready[0].serial} connected` },
|
|
198
|
+
serial: ready[0].serial,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Check 2: the connected device's manufacturer, model, Android release and SDK int, read with
|
|
204
|
+
* `adb shell getprop`. Most PixelKit hooks target Pixel-only APIs, so this states plainly whether
|
|
205
|
+
* the device is a Pixel rather than leaving that to be inferred from a wall of em dashes.
|
|
206
|
+
*/
|
|
207
|
+
async function checkDeviceIdentity(
|
|
208
|
+
serial: string | null
|
|
209
|
+
): Promise<{ result: CheckResult; manufacturer: string | null }> {
|
|
210
|
+
if (!serial) {
|
|
211
|
+
return {
|
|
212
|
+
result: {
|
|
213
|
+
name: 'Device identity',
|
|
214
|
+
status: 'unknown',
|
|
215
|
+
reason: 'skipped: no single adb device selected (see "adb device" above).',
|
|
216
|
+
},
|
|
217
|
+
manufacturer: null,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const props = [
|
|
222
|
+
'ro.product.manufacturer',
|
|
223
|
+
'ro.product.model',
|
|
224
|
+
'ro.build.version.release',
|
|
225
|
+
'ro.build.version.sdk',
|
|
226
|
+
];
|
|
227
|
+
const values: Record<string, string> = {};
|
|
228
|
+
for (const prop of props) {
|
|
229
|
+
const res = await runAdb(['-s', serial, 'shell', 'getprop', prop]);
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
return {
|
|
232
|
+
result: { name: 'Device identity', status: 'unknown', reason: `could not read ${prop}: ${res.error}` },
|
|
233
|
+
manufacturer: null,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
values[prop] = res.stdout.trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const manufacturer = values['ro.product.manufacturer'];
|
|
240
|
+
const model = values['ro.product.model'];
|
|
241
|
+
const release = values['ro.build.version.release'];
|
|
242
|
+
const sdk = values['ro.build.version.sdk'];
|
|
243
|
+
const summary = `${manufacturer} ${model}, Android ${release} (SDK ${sdk})`;
|
|
244
|
+
const isPixel = manufacturer.toLowerCase() === 'google';
|
|
245
|
+
|
|
246
|
+
if (!isPixel) {
|
|
247
|
+
return {
|
|
248
|
+
result: {
|
|
249
|
+
name: 'Device identity',
|
|
250
|
+
status: 'fail',
|
|
251
|
+
reason: `${summary} — not a Pixel. Most PixelKit hooks are written against Pixel-only APIs and will report source: "unavailable" here.`,
|
|
252
|
+
},
|
|
253
|
+
manufacturer,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { result: { name: 'Device identity', status: 'pass', reason: summary }, manufacturer };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check 3: whether a PixelKit-based development build is installed (`adb shell pm path
|
|
262
|
+
* <package>`), keyed off `--package` (default `com.pixelkit.sdk`). Expo Go can never satisfy this
|
|
263
|
+
* check: `@pixelkit-labs/native` and `@pixelkit-labs/mlkit` are Kotlin Expo Modules that must be compiled
|
|
264
|
+
* into a development build, so that limitation is stated regardless of the result.
|
|
265
|
+
*/
|
|
266
|
+
async function checkDevBuild(serial: string | null, packageId: string): Promise<CheckResult> {
|
|
267
|
+
if (!serial) {
|
|
268
|
+
return { name: 'PixelKit development build', status: 'unknown', reason: 'skipped: no single adb device selected.' };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const res = await runAdb(['-s', serial, 'shell', 'pm', 'path', packageId]);
|
|
272
|
+
const installed = res.ok && /^package:/m.test(res.stdout.trim());
|
|
273
|
+
const goNote =
|
|
274
|
+
'Expo Go can never run PixelKit: @pixelkit-labs/native and @pixelkit-labs/mlkit are compiled Kotlin Expo Modules, so a development build is required.';
|
|
275
|
+
|
|
276
|
+
if (installed) {
|
|
277
|
+
return {
|
|
278
|
+
name: 'PixelKit development build',
|
|
279
|
+
status: 'pass',
|
|
280
|
+
reason: `${packageId} is installed on ${serial}. ${goNote}`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
name: 'PixelKit development build',
|
|
286
|
+
status: 'fail',
|
|
287
|
+
reason: `${packageId} is not installed on ${serial}. ${goNote}`,
|
|
288
|
+
fix: `Build a development client and install it — npx expo run:android (or eas build --profile development), then adb -s ${serial} install -r -g <path-to-apk>. Pass --package <id> if your app id differs.`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Check 4: whether `@pixelkit-labs/native` and `@pixelkit-labs/mlkit` resolve from the current project
|
|
294
|
+
* (`require.resolve` from `process.cwd()`, walking the real node_modules chain — nothing is
|
|
295
|
+
* assumed from package.json alone). `@pixelkit-labs/mlkit` is opt-in, so its absence is reported as
|
|
296
|
+
* informational (`n/a`), never a failure.
|
|
297
|
+
*/
|
|
298
|
+
function resolveFrom(name: string, cwd: string): { ok: true; path: string } | { ok: false; error: string } {
|
|
299
|
+
try {
|
|
300
|
+
const path = require.resolve(name, { paths: [cwd] });
|
|
301
|
+
return { ok: true, path };
|
|
302
|
+
} catch (err) {
|
|
303
|
+
return { ok: false, error: (err as Error).message };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function checkNativeModules(cwd: string): CheckResult[] {
|
|
308
|
+
const native = resolveFrom('@pixelkit-labs/native', cwd);
|
|
309
|
+
const mlkit = resolveFrom('@pixelkit-labs/mlkit', cwd);
|
|
310
|
+
const results: CheckResult[] = [];
|
|
311
|
+
|
|
312
|
+
if (native.ok) {
|
|
313
|
+
results.push({
|
|
314
|
+
name: '@pixelkit-labs/native resolvable',
|
|
315
|
+
status: 'pass',
|
|
316
|
+
reason: `resolved from ${native.path}. Silicon (SoC, CPU, memory, thermal, GPU), display, torch and haptics hooks are available.`,
|
|
317
|
+
});
|
|
318
|
+
} else {
|
|
319
|
+
results.push({
|
|
320
|
+
name: '@pixelkit-labs/native resolvable',
|
|
321
|
+
status: 'fail',
|
|
322
|
+
reason: `not resolvable from ${cwd}. Silicon, display, torch and haptics hooks will report source: "unavailable".`,
|
|
323
|
+
fix: 'npm install @pixelkit-labs/native (or install `@pixelkit-labs/sdk`, which depends on it directly).',
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (mlkit.ok) {
|
|
328
|
+
results.push({
|
|
329
|
+
name: '@pixelkit-labs/mlkit resolvable',
|
|
330
|
+
status: 'pass',
|
|
331
|
+
reason: `resolved from ${mlkit.path}. Gemini Nano / on-device ML Kit hooks are available.`,
|
|
332
|
+
});
|
|
333
|
+
} else {
|
|
334
|
+
results.push({
|
|
335
|
+
name: '@pixelkit-labs/mlkit resolvable',
|
|
336
|
+
status: 'n/a',
|
|
337
|
+
reason: `not resolvable from ${cwd}. This is opt-in, not a failure — Gemini Nano / on-device ML Kit hooks report source: "unavailable" without it.`,
|
|
338
|
+
fix: 'npm install @pixelkit-labs/mlkit to enable on-device ML Kit hooks.',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return results;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Check 5: whether AICore is present (`adb shell pm list packages`, matched for "aicore"), which
|
|
347
|
+
* Gemini Nano needs. Not-applicable on a device that Check 2 already found is not a Pixel.
|
|
348
|
+
*/
|
|
349
|
+
async function checkAiCore(serial: string | null, manufacturer: string | null): Promise<CheckResult> {
|
|
350
|
+
if (manufacturer && manufacturer.toLowerCase() !== 'google') {
|
|
351
|
+
return { name: 'AICore', status: 'n/a', reason: `device manufacturer is ${manufacturer}, not Google; AICore is a Pixel component.` };
|
|
352
|
+
}
|
|
353
|
+
if (!serial) {
|
|
354
|
+
return { name: 'AICore', status: 'unknown', reason: 'skipped: no single adb device selected.' };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const res = await runAdb(['-s', serial, 'shell', 'pm', 'list', 'packages']);
|
|
358
|
+
if (!res.ok) {
|
|
359
|
+
return { name: 'AICore', status: 'unknown', reason: `could not list packages: ${res.error}` };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const present = res.stdout.toLowerCase().includes('aicore');
|
|
363
|
+
if (present) {
|
|
364
|
+
return { name: 'AICore', status: 'pass', reason: 'a package matching "aicore" is installed. Gemini Nano can run through @pixelkit-labs/mlkit.' };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
name: 'AICore',
|
|
369
|
+
status: 'fail',
|
|
370
|
+
reason: 'no package matching "aicore" is installed. useGeminiNano and other on-device Gemini hooks will report source: "unavailable".',
|
|
371
|
+
fix: 'AICore ships through Google Play services on supported Pixels; update Play services and the Play Store, then reboot.',
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Check 6: whether `adb reverse tcp:8081 tcp:8081` is set (`adb reverse --list`), so a
|
|
377
|
+
* development client can reach Metro on localhost.
|
|
378
|
+
*/
|
|
379
|
+
async function checkAdbReverse(serial: string | null): Promise<CheckResult> {
|
|
380
|
+
if (!serial) {
|
|
381
|
+
return { name: 'adb reverse tcp:8081', status: 'unknown', reason: 'skipped: no single adb device selected.' };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const res = await runAdb(['-s', serial, 'reverse', '--list']);
|
|
385
|
+
if (!res.ok) {
|
|
386
|
+
return { name: 'adb reverse tcp:8081', status: 'unknown', reason: `could not list adb reverse rules: ${res.error}` };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const set = res.stdout.split(/\r?\n/).some((l) => /tcp:8081\s+tcp:8081/.test(l));
|
|
390
|
+
if (set) {
|
|
391
|
+
return { name: 'adb reverse tcp:8081', status: 'pass', reason: 'set: the dev client can reach Metro at localhost:8081.' };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
name: 'adb reverse tcp:8081',
|
|
396
|
+
status: 'fail',
|
|
397
|
+
reason: 'not set. A development build looking for Metro on localhost:8081 will not find it.',
|
|
398
|
+
fix: `adb -s ${serial} reverse tcp:8081 tcp:8081`,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
interface Flags {
|
|
403
|
+
packageId: string;
|
|
404
|
+
serial?: string;
|
|
405
|
+
help: boolean;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function parseArgs(argv: string[]): { command: string | undefined; flags: Flags } {
|
|
409
|
+
const flags: Flags = { packageId: 'com.pixelkit.sdk', help: false };
|
|
410
|
+
let command: string | undefined;
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < argv.length; i++) {
|
|
413
|
+
const arg = argv[i];
|
|
414
|
+
if (arg === '--package') {
|
|
415
|
+
flags.packageId = argv[++i] ?? flags.packageId;
|
|
416
|
+
} else if (arg.startsWith('--package=')) {
|
|
417
|
+
flags.packageId = arg.slice('--package='.length);
|
|
418
|
+
} else if (arg === '--serial') {
|
|
419
|
+
flags.serial = argv[++i];
|
|
420
|
+
} else if (arg.startsWith('--serial=')) {
|
|
421
|
+
flags.serial = arg.slice('--serial='.length);
|
|
422
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
423
|
+
flags.help = true;
|
|
424
|
+
} else if (command === undefined) {
|
|
425
|
+
command = arg;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return { command, flags };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function printUsage(): void {
|
|
433
|
+
console.log(`${ANSI.bold}pixelkit doctor${ANSI.reset} - diagnose why PixelKit hooks might report source: "unavailable"
|
|
434
|
+
|
|
435
|
+
Usage:
|
|
436
|
+
pixelkit doctor [--package <id>] [--serial <serial>]
|
|
437
|
+
|
|
438
|
+
Options:
|
|
439
|
+
--package <id> Application id of the installed PixelKit development build (default: com.pixelkit.sdk)
|
|
440
|
+
--serial <id> adb serial to target when more than one device is connected
|
|
441
|
+
-h, --help Show this help
|
|
442
|
+
`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function printResult(r: CheckResult): void {
|
|
446
|
+
console.log(`[${STATUS_LABEL[r.status]}] ${ANSI.bold}${r.name}${ANSI.reset} - ${r.reason}`);
|
|
447
|
+
if (r.fix) console.log(`${ANSI.dim} fix: ${r.fix}${ANSI.reset}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Runs all six checks in order and prints them. Returns the process exit code: 0 only when every
|
|
452
|
+
* check passed or was not applicable; 1 when any check failed or — per the "never fabricate a
|
|
453
|
+
* result" rule — could not be determined at all.
|
|
454
|
+
*/
|
|
455
|
+
async function doctor(flags: Flags): Promise<number> {
|
|
456
|
+
console.log(`${ANSI.bold}PixelKit doctor${ANSI.reset}\n`);
|
|
457
|
+
|
|
458
|
+
const results: CheckResult[] = [];
|
|
459
|
+
|
|
460
|
+
const step1 = await checkAdbAndDevice(flags.serial);
|
|
461
|
+
results.push(step1.result);
|
|
462
|
+
const serial = step1.serial;
|
|
463
|
+
|
|
464
|
+
const step2 = await checkDeviceIdentity(serial);
|
|
465
|
+
results.push(step2.result);
|
|
466
|
+
const manufacturer = step2.manufacturer;
|
|
467
|
+
|
|
468
|
+
results.push(await checkDevBuild(serial, flags.packageId));
|
|
469
|
+
results.push(...checkNativeModules(process.cwd()));
|
|
470
|
+
results.push(await checkAiCore(serial, manufacturer));
|
|
471
|
+
results.push(await checkAdbReverse(serial));
|
|
472
|
+
|
|
473
|
+
for (const r of results) printResult(r);
|
|
474
|
+
|
|
475
|
+
const failed = results.filter((r) => r.status === 'fail');
|
|
476
|
+
const unresolved = results.filter((r) => r.status === 'unknown');
|
|
477
|
+
const passed = results.filter((r) => r.status === 'pass');
|
|
478
|
+
const notApplicable = results.filter((r) => r.status === 'n/a');
|
|
479
|
+
|
|
480
|
+
console.log(
|
|
481
|
+
`\n${passed.length} passed, ${failed.length} failed, ${unresolved.length} could not be determined, ${notApplicable.length} not applicable.`
|
|
482
|
+
);
|
|
483
|
+
|
|
484
|
+
return failed.length > 0 || unresolved.length > 0 ? 1 : 0;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function main(): Promise<void> {
|
|
488
|
+
const { command, flags } = parseArgs(process.argv.slice(2));
|
|
489
|
+
|
|
490
|
+
if (flags.help) {
|
|
491
|
+
printUsage();
|
|
492
|
+
process.exitCode = 0;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (!command) {
|
|
497
|
+
printUsage();
|
|
498
|
+
process.exitCode = 1;
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (command !== 'doctor') {
|
|
503
|
+
console.error(`Unknown command "${command}". The only command is "doctor".\n`);
|
|
504
|
+
printUsage();
|
|
505
|
+
process.exitCode = 1;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
process.exitCode = await doctor(flags);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
main().catch((err) => {
|
|
513
|
+
console.error(`pixelkit doctor crashed: ${(err as Error).stack || String(err)}`);
|
|
514
|
+
process.exitCode = 1;
|
|
515
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pixelkit-labs/cli",
|
|
3
|
+
"version": "1.4.3",
|
|
4
|
+
"description": "pixelkit doctor: tells you why a PixelKit hardware reading is unavailable instead of leaving you guessing.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pixelkit",
|
|
7
|
+
"expo",
|
|
8
|
+
"react-native",
|
|
9
|
+
"android",
|
|
10
|
+
"pixel",
|
|
11
|
+
"cli",
|
|
12
|
+
"doctor",
|
|
13
|
+
"adb"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Traves Theberge",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/PixelKit-Labs/pixelkit-cli.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/PixelKit-Labs/pixelkit-cli#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/PixelKit-Labs/pixelkit-cli/issues"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"pixelkit": "./build/index.js"
|
|
27
|
+
},
|
|
28
|
+
"main": "build/index.js",
|
|
29
|
+
"types": "build/index.d.ts",
|
|
30
|
+
"files": [
|
|
31
|
+
"build",
|
|
32
|
+
"index.ts",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"prepublishOnly": "npm run build"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"typescript": "~6.0.3",
|
|
48
|
+
"@types/node": "^22.10.2"
|
|
49
|
+
}
|
|
50
|
+
}
|