@autifyhq/aximo-cli 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -13
- package/dist/client.d.ts +7 -1
- package/dist/client.js +21 -2
- package/dist/client.test.js +36 -0
- package/dist/commands/case/case.test.js +62 -0
- package/dist/commands/case/create.d.ts +1 -1
- package/dist/commands/case/create.js +4 -4
- package/dist/commands/case/index.js +1 -0
- package/dist/commands/case/update.d.ts +50 -0
- package/dist/commands/case/update.js +78 -0
- package/dist/commands/health/check.js +15 -3
- package/dist/commands/health/check.test.js +27 -3
- package/dist/commands/mobile/connect-core.d.ts +3 -1
- package/dist/commands/mobile/connect-core.js +34 -14
- package/dist/commands/mobile/connect.d.ts +5 -0
- package/dist/commands/mobile/connect.js +10 -4
- package/dist/commands/mobile/connect.test.js +7 -1
- package/dist/commands/mobile/doctor.d.ts +1 -0
- package/dist/commands/mobile/doctor.js +18 -2
- package/dist/commands/mobile/doctor.test.js +15 -1
- package/dist/commands/mobile/install.d.ts +1 -0
- package/dist/commands/mobile/install.js +14 -3
- package/dist/commands/mobile/list.d.ts +1 -0
- package/dist/commands/mobile/list.js +23 -3
- package/dist/commands/mobile/list.test.d.ts +1 -0
- package/dist/commands/mobile/list.test.js +24 -0
- package/dist/commands/mobile/output.d.ts +13 -0
- package/dist/commands/mobile/output.js +21 -0
- package/dist/commands/mobile/output.test.d.ts +1 -0
- package/dist/commands/mobile/output.test.js +54 -0
- package/dist/commands/mobile/register.d.ts +1 -0
- package/dist/commands/mobile/register.js +11 -3
- package/dist/commands/mobile/setup.d.ts +1 -0
- package/dist/commands/mobile/setup.js +7 -2
- package/dist/commands/mobile/signing-consent.d.ts +18 -0
- package/dist/commands/mobile/signing-consent.js +28 -0
- package/dist/commands/mobile/signing-consent.test.d.ts +1 -0
- package/dist/commands/mobile/signing-consent.test.js +50 -0
- package/dist/commands/mobile/unregister.d.ts +7 -3
- package/dist/commands/mobile/unregister.js +10 -4
- package/dist/commands/plan/index.js +1 -0
- package/dist/commands/plan/plan.test.js +61 -0
- package/dist/commands/plan/update.d.ts +42 -0
- package/dist/commands/plan/update.js +78 -0
- package/dist/commands/schema.js +1 -1
- package/dist/commands/session/save-as-case.js +3 -3
- package/dist/commands/trigger/plan/run.d.ts +6 -1
- package/dist/commands/trigger/plan/run.js +9 -3
- package/dist/commands/trigger/trigger.test.js +22 -3
- package/dist/commands/upload/put.d.ts +5 -0
- package/dist/commands/upload/put.js +44 -11
- package/dist/commands/upload/upload.test.js +82 -0
- package/dist/mobile-control/appium-controller.d.ts +4 -0
- package/dist/mobile-control/appium-controller.js +90 -2
- package/dist/mobile-control/appium-controller.test.js +156 -8
- package/dist/mobile-control/bundle-id-map.js +3 -0
- package/dist/mobile-control/controller.d.ts +3 -0
- package/dist/mobile-control/exec.d.ts +5 -1
- package/dist/mobile-control/exec.js +2 -2
- package/dist/mobile-control/ios-tunnel.d.ts +5 -2
- package/dist/mobile-control/ios-tunnel.js +25 -5
- package/dist/mobile-control/ios-tunnel.test.js +8 -3
- package/dist/mobile-control/ios-wda-preinstall.js +6 -3
- package/dist/mobile-control/ios-wda-preinstall.test.js +12 -8
- package/dist/mobile-control/tunnel.js +57 -1
- package/dist/mobile-control/tunnel.test.js +157 -0
- package/dist/mobile-control/webdriver.d.ts +7 -1
- package/dist/mobile-control/webdriver.js +12 -6
- package/dist/mutate.d.ts +1 -1
- package/dist/mutate.js +4 -2
- package/dist/types.d.ts +49 -4
- package/dist/validate.d.ts +6 -1
- package/dist/validate.js +46 -0
- package/dist/validate.test.js +39 -1
- package/package.json +1 -1
|
@@ -5,12 +5,13 @@ import { installUploadedApp } from "../../mobile-control/app-install.js";
|
|
|
5
5
|
import { pickFreePort, startAppiumServer } from "../../mobile-control/appium-server.js";
|
|
6
6
|
import { withBundleIdTranslation } from "../../mobile-control/bundle-id-map.js";
|
|
7
7
|
import { getIosDeviceInfo, iosMajorVersion } from "../../mobile-control/ios-devicectl.js";
|
|
8
|
-
import { ensureIosTunnel } from "../../mobile-control/ios-tunnel.js";
|
|
8
|
+
import { ensureIosTunnel, TUNNEL_STOP_COMMAND } from "../../mobile-control/ios-tunnel.js";
|
|
9
9
|
import { resolveIosSigning, resolveIosSigningAuto, } from "../../mobile-control/ios-signing.js";
|
|
10
10
|
import { resolveWdaCaching } from "../../mobile-control/ios-wda-cache.js";
|
|
11
11
|
import { resolvePreinstalledWdaStrategy } from "../../mobile-control/ios-wda-preinstall.js";
|
|
12
12
|
import { runControlTunnel } from "../../mobile-control/tunnel.js";
|
|
13
13
|
import { runMobileDoctor } from "./doctor.js";
|
|
14
|
+
import { confirmIosSigning } from "./signing-consent.js";
|
|
14
15
|
import { listMobileDeviceConfigs, readMobileDeviceConfig, } from "./paths.js";
|
|
15
16
|
export function getTunnelOrigin() {
|
|
16
17
|
return getBaseUrl().replace(/^http/, "ws");
|
|
@@ -28,9 +29,8 @@ export function resolveDeviceConfig(deviceId) {
|
|
|
28
29
|
return formatError("validation_error", 'No registered devices. Register one first: aximo mobile register --name "My Pixel" --platform android', false);
|
|
29
30
|
}
|
|
30
31
|
if (all.length > 1) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
.join(", ")}`, false);
|
|
32
|
+
const lines = all.map((c) => ` - ${c.name} (${c.platform}) — device-id: ${c.deviceId}`);
|
|
33
|
+
return formatError("validation_error", `Multiple devices are registered on this machine; pass --device-id to pick one:\n${lines.join("\n")}`, false, `Re-run with --device-id, e.g. --device-id ${all[0]?.deviceId}`);
|
|
34
34
|
}
|
|
35
35
|
const only = all[0];
|
|
36
36
|
if (!only) {
|
|
@@ -87,11 +87,34 @@ async function preflightIosDevice(udid) {
|
|
|
87
87
|
}
|
|
88
88
|
return iosMajorVersion(info.osVersion);
|
|
89
89
|
}
|
|
90
|
-
export async function connectWithConfig(config) {
|
|
90
|
+
export async function connectWithConfig(config, options) {
|
|
91
91
|
await assertPrerequisites(config.platform);
|
|
92
92
|
let iosMajor = null;
|
|
93
|
+
let tunnelRunning = false;
|
|
94
|
+
let signing;
|
|
93
95
|
if (config.platform === "ios") {
|
|
94
96
|
iosMajor = await preflightIosDevice(config.target);
|
|
97
|
+
// Resolve and confirm the signing identity before any signing side effects —
|
|
98
|
+
// the RemoteXPC tunnel (admin prompt), Appium, WDA install, and profile
|
|
99
|
+
// minting against the user's Apple Developer account all come after, so
|
|
100
|
+
// declining costs nothing.
|
|
101
|
+
signing = await resolveIosSigningAuto({ udid: config.target });
|
|
102
|
+
if (signing.teamId) {
|
|
103
|
+
const accepted = await confirmIosSigning({
|
|
104
|
+
signing,
|
|
105
|
+
...(options?.assumeYes !== undefined ? { assumeYes: options.assumeYes } : {}),
|
|
106
|
+
});
|
|
107
|
+
if (!accepted) {
|
|
108
|
+
console.error("Signing not accepted — not connecting. Re-run with --yes to skip this prompt, " +
|
|
109
|
+
"or set AXIMO_IOS_TEAM_ID to sign with a different team.");
|
|
110
|
+
process.exitCode = 1;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.error("No Apple signing identity found — physical-device WebDriverAgent signing will fail. " +
|
|
116
|
+
"Open Xcode → Settings → Accounts to add your Apple ID (see `aximo mobile doctor`).");
|
|
117
|
+
}
|
|
95
118
|
// Physical iPhones on iOS 17+ need the RemoteXPC tunnel before XCUITest can
|
|
96
119
|
// reach WebDriverAgent; start it (prompting for admin) unless one is running.
|
|
97
120
|
// Older devices use the classic lockdown path — no tunnel, no admin prompt.
|
|
@@ -100,6 +123,7 @@ export async function connectWithConfig(config) {
|
|
|
100
123
|
}
|
|
101
124
|
else {
|
|
102
125
|
const tunnel = await ensureIosTunnel();
|
|
126
|
+
tunnelRunning = tunnel.started || tunnel.reused;
|
|
103
127
|
if (tunnel.reused)
|
|
104
128
|
console.error("Reusing the running iOS device tunnel.");
|
|
105
129
|
}
|
|
@@ -113,15 +137,7 @@ export async function connectWithConfig(config) {
|
|
|
113
137
|
// installed profiles; env vars only override.
|
|
114
138
|
let iosSigning;
|
|
115
139
|
let wdaLocalPort;
|
|
116
|
-
if (config.platform === "ios") {
|
|
117
|
-
const signing = await resolveIosSigningAuto({ udid: config.target });
|
|
118
|
-
if (signing.teamId) {
|
|
119
|
-
console.error(`Signing as team ${signing.teamId} (WDA id ${signing.wdaBundleId}).`);
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
console.error("No Apple signing identity found — physical-device WebDriverAgent signing will fail. " +
|
|
123
|
-
"Open Xcode → Settings → Accounts to add your Apple ID (see `aximo mobile doctor`).");
|
|
124
|
-
}
|
|
140
|
+
if (config.platform === "ios" && signing) {
|
|
125
141
|
const preinstalled = await resolvePreinstalledWdaStrategy(signing, {
|
|
126
142
|
udid: config.target,
|
|
127
143
|
iosMajor,
|
|
@@ -182,5 +198,9 @@ export async function connectWithConfig(config) {
|
|
|
182
198
|
finally {
|
|
183
199
|
await controller.close();
|
|
184
200
|
await server.stop();
|
|
201
|
+
if (tunnelRunning) {
|
|
202
|
+
console.error("The iOS device tunnel is still running in the background (admin-started; the next " +
|
|
203
|
+
`connect won't prompt again). Stop it anytime with: ${TUNNEL_STOP_COMMAND}`);
|
|
204
|
+
}
|
|
185
205
|
}
|
|
186
206
|
}
|
|
@@ -5,5 +5,10 @@ declare const _default: import("citty").CommandDef<{
|
|
|
5
5
|
readonly type: "string";
|
|
6
6
|
readonly description: "Registered device id (required if multiple)";
|
|
7
7
|
};
|
|
8
|
+
readonly yes: {
|
|
9
|
+
readonly type: "boolean";
|
|
10
|
+
readonly alias: "y";
|
|
11
|
+
readonly description: "Skip the iOS code-signing confirmation prompt";
|
|
12
|
+
};
|
|
8
13
|
}>;
|
|
9
14
|
export default _default;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { mobileErrorHint } from "../../mobile-control/mobile-errors.js";
|
|
3
|
-
import { formatError
|
|
3
|
+
import { formatError } from "../../output.js";
|
|
4
4
|
import { connectWithConfig, resolveDeviceConfig } from "./connect-core.js";
|
|
5
|
+
import { printMobileResult } from "./output.js";
|
|
5
6
|
const IOS_PHYSICAL_WARNING = "Driving a physical iPhone goes through Appium's XCUITest driver, which builds and " +
|
|
6
7
|
"installs a WebDriverAgent runner on the device. The device must have Developer Mode " +
|
|
7
8
|
"enabled (Settings → Privacy & Security) and be trusted, and WDA needs an Apple Developer " +
|
|
@@ -16,22 +17,27 @@ export default defineCommand({
|
|
|
16
17
|
},
|
|
17
18
|
args: {
|
|
18
19
|
"device-id": { type: "string", description: "Registered device id (required if multiple)" },
|
|
20
|
+
yes: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
alias: "y",
|
|
23
|
+
description: "Skip the iOS code-signing confirmation prompt",
|
|
24
|
+
},
|
|
19
25
|
},
|
|
20
26
|
async run({ args }) {
|
|
21
27
|
const resolved = resolveDeviceConfig(args["device-id"]);
|
|
22
28
|
if (!resolved.ok) {
|
|
23
|
-
|
|
29
|
+
printMobileResult(resolved);
|
|
24
30
|
return;
|
|
25
31
|
}
|
|
26
32
|
const warning = iosPhysicalWarning(resolved.config);
|
|
27
33
|
if (warning)
|
|
28
34
|
console.error(warning);
|
|
29
35
|
try {
|
|
30
|
-
await connectWithConfig(resolved.config);
|
|
36
|
+
await connectWithConfig(resolved.config, { assumeYes: Boolean(args.yes) });
|
|
31
37
|
}
|
|
32
38
|
catch (error) {
|
|
33
39
|
const message = error instanceof Error ? error.message : String(error);
|
|
34
|
-
|
|
40
|
+
printMobileResult(formatError("api_error", message, true, mobileErrorHint(message, resolved.config.platform)));
|
|
35
41
|
}
|
|
36
42
|
},
|
|
37
43
|
});
|
|
@@ -61,7 +61,7 @@ describe("resolveDeviceConfig", () => {
|
|
|
61
61
|
if (result.ok)
|
|
62
62
|
expect(result.config.deviceId).toBe("only-1");
|
|
63
63
|
});
|
|
64
|
-
it("requires --device-id when multiple are registered", () => {
|
|
64
|
+
it("requires --device-id when multiple are registered, listing each candidate", () => {
|
|
65
65
|
for (const id of ["a", "b"]) {
|
|
66
66
|
writeMobileDeviceConfig({
|
|
67
67
|
deviceId: id,
|
|
@@ -73,6 +73,12 @@ describe("resolveDeviceConfig", () => {
|
|
|
73
73
|
}
|
|
74
74
|
const ambiguous = resolveDeviceConfig(undefined);
|
|
75
75
|
expect(ambiguous.ok).toBe(false);
|
|
76
|
+
if (!ambiguous.ok) {
|
|
77
|
+
expect(ambiguous.error.message).toContain("pass --device-id");
|
|
78
|
+
expect(ambiguous.error.message).toContain("device-id: a");
|
|
79
|
+
expect(ambiguous.error.message).toContain("device-id: b");
|
|
80
|
+
expect(ambiguous.error.hint).toContain("--device-id");
|
|
81
|
+
}
|
|
76
82
|
const picked = resolveDeviceConfig("b");
|
|
77
83
|
expect(picked.ok).toBe(true);
|
|
78
84
|
if (picked.ok)
|
|
@@ -22,6 +22,7 @@ export interface RunMobileDoctorOptions {
|
|
|
22
22
|
isTunnelHealthy?: (port: number) => Promise<boolean>;
|
|
23
23
|
}
|
|
24
24
|
export declare function runMobileDoctor(options?: RunMobileDoctorOptions): Promise<Envelope<MobileDoctorReport>>;
|
|
25
|
+
export declare function renderDoctorReport(data: MobileDoctorReport): string;
|
|
25
26
|
declare const _default: import("citty").CommandDef<{
|
|
26
27
|
readonly platform: {
|
|
27
28
|
readonly type: "string";
|
|
@@ -3,7 +3,8 @@ import { existsSync } from "node:fs";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { platform } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { formatSuccess
|
|
6
|
+
import { formatSuccess } from "../../output.js";
|
|
7
|
+
import { printMobileResult } from "./output.js";
|
|
7
8
|
import { importAppium } from "../../mobile-control/appium-import.js";
|
|
8
9
|
import { getAppiumHome, resolveAndroidSdk } from "../../mobile-control/appium-server.js";
|
|
9
10
|
import { resolveIosSigningAuto } from "../../mobile-control/ios-signing.js";
|
|
@@ -171,6 +172,21 @@ export async function runMobileDoctor(options = {}) {
|
|
|
171
172
|
summary[c.status]++;
|
|
172
173
|
return formatSuccess({ checks, summary });
|
|
173
174
|
}
|
|
175
|
+
const STATUS_MARK = {
|
|
176
|
+
pass: "✓",
|
|
177
|
+
warn: "!",
|
|
178
|
+
fail: "✗",
|
|
179
|
+
info: "i",
|
|
180
|
+
};
|
|
181
|
+
export function renderDoctorReport(data) {
|
|
182
|
+
const lines = data.checks.map((c) => {
|
|
183
|
+
const line = `${STATUS_MARK[c.status]} ${c.name}: ${c.message}`;
|
|
184
|
+
return c.hint ? `${line}\n ${c.hint}` : line;
|
|
185
|
+
});
|
|
186
|
+
const s = data.summary;
|
|
187
|
+
lines.push(`Summary: ${s.pass} pass, ${s.warn} warn, ${s.fail} fail, ${s.info} info`);
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
174
190
|
export default defineCommand({
|
|
175
191
|
meta: {
|
|
176
192
|
name: "doctor",
|
|
@@ -184,7 +200,7 @@ export default defineCommand({
|
|
|
184
200
|
if (args.platform === "ios" || args.platform === "android")
|
|
185
201
|
options.platform = args.platform;
|
|
186
202
|
const result = await runMobileDoctor(options);
|
|
187
|
-
|
|
203
|
+
printMobileResult(result, { render: renderDoctorReport });
|
|
188
204
|
if (result.ok && result.data.summary.fail > 0) {
|
|
189
205
|
process.exitCode = 1;
|
|
190
206
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { checkIosTunnel } from "./doctor.js";
|
|
2
|
+
import { checkIosTunnel, renderDoctorReport } from "./doctor.js";
|
|
3
3
|
describe("checkIosTunnel", () => {
|
|
4
4
|
it("passes when the RemoteXPC tunnel is reachable", async () => {
|
|
5
5
|
const check = await checkIosTunnel(async () => true);
|
|
@@ -12,3 +12,17 @@ describe("checkIosTunnel", () => {
|
|
|
12
12
|
expect(check.hint).toContain("aximo mobile connect");
|
|
13
13
|
});
|
|
14
14
|
});
|
|
15
|
+
describe("renderDoctorReport", () => {
|
|
16
|
+
it("renders one line per check with status marks, indented hints, and a summary", () => {
|
|
17
|
+
const text = renderDoctorReport({
|
|
18
|
+
checks: [
|
|
19
|
+
{ name: "appium", status: "pass", message: "bundled Appium found" },
|
|
20
|
+
{ name: "adb", status: "fail", message: "adb not found", hint: "install platform-tools" },
|
|
21
|
+
],
|
|
22
|
+
summary: { pass: 1, warn: 0, fail: 1, info: 0 },
|
|
23
|
+
});
|
|
24
|
+
expect(text).toContain("✓ appium: bundled Appium found");
|
|
25
|
+
expect(text).toContain("✗ adb: adb not found\n install platform-tools");
|
|
26
|
+
expect(text).toContain("Summary: 1 pass, 0 warn, 1 fail, 0 info");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -28,6 +28,7 @@ export interface MobileInstallOptions {
|
|
|
28
28
|
* signature doesn't already cover it (disable with AXIMO_IOS_NO_RESIGN).
|
|
29
29
|
*/
|
|
30
30
|
export declare function runMobileInstall(options: MobileInstallOptions, exec?: ExecFn, ensureInstallable?: typeof ensureInstallableIpa, installIos?: typeof installIosApp): Promise<Envelope<MobileInstallResult>>;
|
|
31
|
+
export declare function renderInstallResult(data: MobileInstallResult): string;
|
|
31
32
|
declare const _default: import("citty").CommandDef<{
|
|
32
33
|
readonly app: {
|
|
33
34
|
readonly type: "string";
|
|
@@ -7,7 +7,8 @@ import { execBinary } from "../../mobile-control/exec.js";
|
|
|
7
7
|
import { ensureInstallableIpa } from "../../mobile-control/ios-auto-resign.js";
|
|
8
8
|
import { installIosApp } from "../../mobile-control/ios-devicectl.js";
|
|
9
9
|
import { mobileErrorHint } from "../../mobile-control/mobile-errors.js";
|
|
10
|
-
import { formatError, formatSuccess
|
|
10
|
+
import { formatError, formatSuccess } from "../../output.js";
|
|
11
|
+
import { printMobileResult } from "./output.js";
|
|
11
12
|
import { parsePlatformArg } from "./register-core.js";
|
|
12
13
|
function inferPlatform(appPath) {
|
|
13
14
|
const ext = extname(appPath).toLowerCase();
|
|
@@ -85,6 +86,16 @@ export async function runMobileInstall(options, exec = execBinary, ensureInstall
|
|
|
85
86
|
return formatError("api_error", message, false, mobileErrorHint(message, platform));
|
|
86
87
|
}
|
|
87
88
|
}
|
|
89
|
+
export function renderInstallResult(data) {
|
|
90
|
+
const target = data.serial ?? data.udid ?? "device";
|
|
91
|
+
const lines = [`Installed ${data.app} on ${data.platform} device ${target}.`];
|
|
92
|
+
if (data.resigned) {
|
|
93
|
+
lines.push(`Re-signed for this device: ${data.originalBundleId} installed as ${data.installedBundleId}.`);
|
|
94
|
+
}
|
|
95
|
+
if (data.output)
|
|
96
|
+
lines.push(data.output);
|
|
97
|
+
return lines.join("\n");
|
|
98
|
+
}
|
|
88
99
|
export default defineCommand({
|
|
89
100
|
meta: {
|
|
90
101
|
name: "install",
|
|
@@ -97,11 +108,11 @@ export default defineCommand({
|
|
|
97
108
|
udid: { type: "string", description: "iOS device udid (defaults to the connected one)" },
|
|
98
109
|
},
|
|
99
110
|
async run({ args }) {
|
|
100
|
-
|
|
111
|
+
printMobileResult(await runMobileInstall({
|
|
101
112
|
app: args.app,
|
|
102
113
|
platform: args.platform,
|
|
103
114
|
serial: args.serial,
|
|
104
115
|
udid: args.udid,
|
|
105
|
-
}));
|
|
116
|
+
}), { render: renderInstallResult });
|
|
106
117
|
},
|
|
107
118
|
});
|
|
@@ -8,5 +8,6 @@ export interface MobileListResult {
|
|
|
8
8
|
warnings?: string[];
|
|
9
9
|
}
|
|
10
10
|
export declare function runMobileList(exec?: ExecFn): Promise<Envelope<MobileListResult>>;
|
|
11
|
+
export declare function renderMobileList(data: MobileListResult): string;
|
|
11
12
|
declare const _default: import("citty").CommandDef<{}>;
|
|
12
13
|
export default _default;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
|
-
import { formatSuccess
|
|
2
|
+
import { formatSuccess } from "../../output.js";
|
|
3
|
+
import { printMobileResult } from "./output.js";
|
|
3
4
|
import { listAndroidDevices, listIosPhysicalDevices, } from "../../mobile-control/devices.js";
|
|
4
5
|
export async function runMobileList(exec) {
|
|
5
6
|
const warnings = [];
|
|
@@ -24,13 +25,32 @@ export async function runMobileList(exec) {
|
|
|
24
25
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
25
26
|
});
|
|
26
27
|
}
|
|
28
|
+
export function renderMobileList(data) {
|
|
29
|
+
const lines = ["Android phones (adb):"];
|
|
30
|
+
if (data.androidDevices.length === 0)
|
|
31
|
+
lines.push(" (none connected)");
|
|
32
|
+
for (const d of data.androidDevices) {
|
|
33
|
+
const label = d.model ? `${d.model} — ` : "";
|
|
34
|
+
lines.push(` - ${label}serial: ${d.serial} (${d.state})`);
|
|
35
|
+
}
|
|
36
|
+
lines.push("iOS phones (devicectl):");
|
|
37
|
+
if (data.iosDevices.length === 0)
|
|
38
|
+
lines.push(" (none connected)");
|
|
39
|
+
for (const d of data.iosDevices) {
|
|
40
|
+
lines.push(` - ${d.name} — udid: ${d.udid}${d.state ? ` (${d.state})` : ""}`);
|
|
41
|
+
}
|
|
42
|
+
for (const w of data.warnings ?? [])
|
|
43
|
+
lines.push(`Warning: ${w}`);
|
|
44
|
+
lines.push('Next: aximo mobile register --name "My Phone" --platform android|ios');
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
27
47
|
export default defineCommand({
|
|
28
48
|
meta: {
|
|
29
49
|
name: "list",
|
|
30
|
-
description: "List physically-connected phones (Android via adb, iOS via
|
|
50
|
+
description: "List physically-connected phones (Android via adb, iOS via devicectl)",
|
|
31
51
|
},
|
|
32
52
|
args: {},
|
|
33
53
|
async run() {
|
|
34
|
-
|
|
54
|
+
printMobileResult(await runMobileList(), { render: renderMobileList });
|
|
35
55
|
},
|
|
36
56
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderMobileList } from "./list.js";
|
|
3
|
+
describe("renderMobileList", () => {
|
|
4
|
+
it("lists devices per platform with serial/udid and state", () => {
|
|
5
|
+
const text = renderMobileList({
|
|
6
|
+
androidDevices: [{ serial: "1A2B3C", state: "device", model: "Pixel_7" }],
|
|
7
|
+
iosDevices: [{ udid: "00008120-XYZ", name: "My iPhone", state: "Booted" }],
|
|
8
|
+
});
|
|
9
|
+
expect(text).toContain("Android phones (adb):");
|
|
10
|
+
expect(text).toContain(" - Pixel_7 — serial: 1A2B3C (device)");
|
|
11
|
+
expect(text).toContain("iOS phones (devicectl):");
|
|
12
|
+
expect(text).toContain(" - My iPhone — udid: 00008120-XYZ (Booted)");
|
|
13
|
+
expect(text).not.toContain("(none connected)");
|
|
14
|
+
});
|
|
15
|
+
it("marks empty platforms and surfaces discovery warnings", () => {
|
|
16
|
+
const text = renderMobileList({
|
|
17
|
+
androidDevices: [],
|
|
18
|
+
iosDevices: [],
|
|
19
|
+
warnings: ["ios (idb): idb not found"],
|
|
20
|
+
});
|
|
21
|
+
expect(text).toContain("(none connected)");
|
|
22
|
+
expect(text).toContain("Warning: ios (idb): idb not found");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Envelope } from "../../types.js";
|
|
2
|
+
export interface PrintMobileResultOptions<T> {
|
|
3
|
+
/** Human rendering of the success payload; falls back to pretty-printed data. */
|
|
4
|
+
render?: (data: T) => string;
|
|
5
|
+
/** Defaults to stdout being a TTY; tests override. */
|
|
6
|
+
interactive?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Mobile commands are usually run by a person at a terminal, where the raw JSON
|
|
10
|
+
* envelope is hard to read. Print human-readable text when stdout is a TTY, and
|
|
11
|
+
* keep the JSON envelope when piped — scripts and agents parse it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function printMobileResult<T>(result: Envelope<T>, options?: PrintMobileResultOptions<T>): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { printResult } from "../../output.js";
|
|
2
|
+
/**
|
|
3
|
+
* Mobile commands are usually run by a person at a terminal, where the raw JSON
|
|
4
|
+
* envelope is hard to read. Print human-readable text when stdout is a TTY, and
|
|
5
|
+
* keep the JSON envelope when piped — scripts and agents parse it.
|
|
6
|
+
*/
|
|
7
|
+
export function printMobileResult(result, options = {}) {
|
|
8
|
+
const interactive = options.interactive ?? process.stdout.isTTY === true;
|
|
9
|
+
if (!interactive) {
|
|
10
|
+
printResult(result);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (!result.ok) {
|
|
14
|
+
console.error(`Error: ${result.error.message}`);
|
|
15
|
+
if (result.error.hint)
|
|
16
|
+
console.error(`Hint: ${result.error.hint}`);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(options.render ? options.render(result.data) : JSON.stringify(result.data, null, 2));
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { formatError, formatSuccess } from "../../output.js";
|
|
3
|
+
import { printMobileResult } from "./output.js";
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.restoreAllMocks();
|
|
6
|
+
process.exitCode = undefined;
|
|
7
|
+
});
|
|
8
|
+
describe("printMobileResult", () => {
|
|
9
|
+
it("prints the JSON envelope when not interactive", () => {
|
|
10
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
11
|
+
printMobileResult(formatSuccess({ a: 1 }), { interactive: false });
|
|
12
|
+
expect(log).toHaveBeenCalledOnce();
|
|
13
|
+
expect(JSON.parse(log.mock.calls[0]?.[0])).toEqual({ ok: true, data: { a: 1 } });
|
|
14
|
+
});
|
|
15
|
+
it("prints the JSON envelope for errors when not interactive and sets the exit code", () => {
|
|
16
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
17
|
+
printMobileResult(formatError("validation_error", "boom", false), { interactive: false });
|
|
18
|
+
expect(JSON.parse(log.mock.calls[0]?.[0])).toMatchObject({
|
|
19
|
+
ok: false,
|
|
20
|
+
error: { message: "boom" },
|
|
21
|
+
});
|
|
22
|
+
expect(process.exitCode).toBe(1);
|
|
23
|
+
});
|
|
24
|
+
it("prints a human-readable error with hint to stderr when interactive", () => {
|
|
25
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
26
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
27
|
+
printMobileResult(formatError("validation_error", "boom", false, "try this"), {
|
|
28
|
+
interactive: true,
|
|
29
|
+
});
|
|
30
|
+
expect(log).not.toHaveBeenCalled();
|
|
31
|
+
expect(error).toHaveBeenCalledWith("Error: boom");
|
|
32
|
+
expect(error).toHaveBeenCalledWith("Hint: try this");
|
|
33
|
+
expect(process.exitCode).toBe(1);
|
|
34
|
+
});
|
|
35
|
+
it("omits the hint line when the error has none", () => {
|
|
36
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
37
|
+
printMobileResult(formatError("api_error", "boom", true), { interactive: true });
|
|
38
|
+
expect(error).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
it("renders success data with the provided renderer when interactive", () => {
|
|
41
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
42
|
+
printMobileResult(formatSuccess({ n: 2 }), {
|
|
43
|
+
interactive: true,
|
|
44
|
+
render: (d) => `n is ${d.n}`,
|
|
45
|
+
});
|
|
46
|
+
expect(log).toHaveBeenCalledWith("n is 2");
|
|
47
|
+
expect(process.exitCode).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
it("falls back to pretty-printed data without a renderer when interactive", () => {
|
|
50
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
51
|
+
printMobileResult(formatSuccess({ n: 2 }), { interactive: true });
|
|
52
|
+
expect(JSON.parse(log.mock.calls[0]?.[0])).toEqual({ n: 2 });
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -10,6 +10,7 @@ export interface MobileRegisterCmdOptions {
|
|
|
10
10
|
os?: string | undefined;
|
|
11
11
|
}
|
|
12
12
|
export declare function runMobileRegister(options: MobileRegisterCmdOptions, deps?: ClientDeps, exec?: ExecFn): Promise<Envelope<MobileRegisterResult>>;
|
|
13
|
+
export declare function renderRegisterResult(data: MobileRegisterResult): string;
|
|
13
14
|
declare const _default: import("citty").CommandDef<{
|
|
14
15
|
readonly name: {
|
|
15
16
|
readonly type: "string";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import {} from "../../client.js";
|
|
3
|
-
import { formatError
|
|
3
|
+
import { formatError } from "../../output.js";
|
|
4
4
|
import { resolveAndroidSerial, resolveIosUdid } from "../../mobile-control/devices.js";
|
|
5
|
+
import { printMobileResult } from "./output.js";
|
|
5
6
|
import { parsePlatformArg, registerMobileDevice, } from "./register-core.js";
|
|
6
7
|
export async function runMobileRegister(options, deps, exec) {
|
|
7
8
|
const platform = parsePlatformArg(options.platform);
|
|
@@ -29,6 +30,13 @@ export async function runMobileRegister(options, deps, exec) {
|
|
|
29
30
|
...(options.os ? { os: options.os } : {}),
|
|
30
31
|
}, deps);
|
|
31
32
|
}
|
|
33
|
+
export function renderRegisterResult(data) {
|
|
34
|
+
return [
|
|
35
|
+
`Registered "${data.name}" (${data.platform}, target ${data.target}) as device ${data.deviceId}.`,
|
|
36
|
+
`Config: ${data.configPath}`,
|
|
37
|
+
"Next: aximo mobile connect",
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
32
40
|
export default defineCommand({
|
|
33
41
|
meta: {
|
|
34
42
|
name: "register",
|
|
@@ -46,12 +54,12 @@ export default defineCommand({
|
|
|
46
54
|
os: { type: "string", description: "Optional OS version label" },
|
|
47
55
|
},
|
|
48
56
|
async run({ args }) {
|
|
49
|
-
|
|
57
|
+
printMobileResult(await runMobileRegister({
|
|
50
58
|
name: args.name,
|
|
51
59
|
platform: args.platform,
|
|
52
60
|
serial: args.serial,
|
|
53
61
|
udid: args.udid,
|
|
54
62
|
os: args.os,
|
|
55
|
-
}));
|
|
63
|
+
}), { render: renderRegisterResult });
|
|
56
64
|
},
|
|
57
65
|
});
|
|
@@ -18,6 +18,7 @@ export interface RunMobileSetupOptions {
|
|
|
18
18
|
}) => Promise<Envelope<MobileDoctorReport>>;
|
|
19
19
|
}
|
|
20
20
|
export declare function runMobileSetup(options?: RunMobileSetupOptions): Promise<Envelope<SetupResult>>;
|
|
21
|
+
export declare function renderSetupResult(data: SetupResult): string;
|
|
21
22
|
declare const _default: import("citty").CommandDef<{
|
|
22
23
|
readonly platform: {
|
|
23
24
|
readonly type: "string";
|
|
@@ -3,7 +3,8 @@ import { execFile } from "node:child_process";
|
|
|
3
3
|
import { existsSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { formatError, formatSuccess
|
|
6
|
+
import { formatError, formatSuccess } from "../../output.js";
|
|
7
|
+
import { printMobileResult } from "./output.js";
|
|
7
8
|
import { getAppiumHome } from "../../mobile-control/appium-server.js";
|
|
8
9
|
import { runMobileDoctor } from "./doctor.js";
|
|
9
10
|
const DRIVER_PACKAGE = {
|
|
@@ -82,6 +83,10 @@ export async function runMobileSetup(options = {}) {
|
|
|
82
83
|
printNextSteps(report.ok ? report.data.checks : []);
|
|
83
84
|
return formatSuccess({ appiumHome, drivers: results });
|
|
84
85
|
}
|
|
86
|
+
export function renderSetupResult(data) {
|
|
87
|
+
const lines = data.drivers.map((d) => ` - ${d.driver}: ${d.alreadyInstalled ? "already installed" : "installed"}`);
|
|
88
|
+
return [`Appium home: ${data.appiumHome}`, ...lines].join("\n");
|
|
89
|
+
}
|
|
85
90
|
export default defineCommand({
|
|
86
91
|
meta: {
|
|
87
92
|
name: "setup",
|
|
@@ -95,6 +100,6 @@ export default defineCommand({
|
|
|
95
100
|
if (args.platform === "ios" || args.platform === "android")
|
|
96
101
|
options.platform = args.platform;
|
|
97
102
|
const result = await runMobileSetup(options);
|
|
98
|
-
|
|
103
|
+
printMobileResult(result, { render: renderSetupResult });
|
|
99
104
|
},
|
|
100
105
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IosSigningConfig } from "../../mobile-control/ios-signing.js";
|
|
2
|
+
export interface ConfirmIosSigningOptions {
|
|
3
|
+
signing: IosSigningConfig;
|
|
4
|
+
/** Skip the prompt entirely (--yes). The notice still prints. */
|
|
5
|
+
assumeYes?: boolean;
|
|
6
|
+
/** Whether a human can answer; defaults to stdin being a TTY. */
|
|
7
|
+
interactive?: boolean;
|
|
8
|
+
ask?: (question: string) => Promise<string>;
|
|
9
|
+
log?: (message: string) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Informed consent before using the user's Apple signing identity: connecting
|
|
13
|
+
* can re-sign apps/WDA with it, register the device to the team's Apple
|
|
14
|
+
* Developer account, and create aximo App IDs in it. Defaults to yes (Enter
|
|
15
|
+
* proceeds); only an explicit n/no declines. Unattended connects (no TTY)
|
|
16
|
+
* proceed after printing the notice, so daemonized devices keep working.
|
|
17
|
+
*/
|
|
18
|
+
export declare function confirmIosSigning(options: ConfirmIosSigningOptions): Promise<boolean>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createInterface } from "node:readline/promises";
|
|
2
|
+
function defaultAsk(question) {
|
|
3
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
4
|
+
return rl.question(question).finally(() => rl.close());
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Informed consent before using the user's Apple signing identity: connecting
|
|
8
|
+
* can re-sign apps/WDA with it, register the device to the team's Apple
|
|
9
|
+
* Developer account, and create aximo App IDs in it. Defaults to yes (Enter
|
|
10
|
+
* proceeds); only an explicit n/no declines. Unattended connects (no TTY)
|
|
11
|
+
* proceed after printing the notice, so daemonized devices keep working.
|
|
12
|
+
*/
|
|
13
|
+
export async function confirmIosSigning(options) {
|
|
14
|
+
const { signing } = options;
|
|
15
|
+
const log = options.log ?? ((message) => console.error(message));
|
|
16
|
+
log(`Code signing: team ${signing.teamId} (WDA id ${signing.wdaBundleId}). Connecting may ` +
|
|
17
|
+
"re-sign apps and WebDriverAgent with this identity, register this device to the " +
|
|
18
|
+
"team's Apple Developer account, and create aximo App IDs in it. " +
|
|
19
|
+
"Set AXIMO_IOS_TEAM_ID to sign with a different team.");
|
|
20
|
+
if (options.assumeYes)
|
|
21
|
+
return true;
|
|
22
|
+
const interactive = options.interactive ?? process.stdin.isTTY === true;
|
|
23
|
+
if (!interactive)
|
|
24
|
+
return true;
|
|
25
|
+
const ask = options.ask ?? defaultAsk;
|
|
26
|
+
const answer = (await ask("Proceed with this signing identity? [Y/n] ")).trim().toLowerCase();
|
|
27
|
+
return answer !== "n" && answer !== "no";
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|