Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

[*.md]
trim_trailing_whitespace = false
2 changes: 1 addition & 1 deletion .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ jobs:
- run: npm run lint
name: Run linter
- run: npm run format:check
name: Run Prettier check
name: Run format check
- run: npm run test
name: Run unit tests
37 changes: 0 additions & 37 deletions .releaserc

This file was deleted.

15 changes: 0 additions & 15 deletions eslint.config.mjs

This file was deleted.

18 changes: 7 additions & 11 deletions lib/devicectl.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {exec, SubProcess} from 'teen_process';
import {log as logger} from '@appium/logger';
import type {DevicectlOptions, ExecuteOptions, ExecuteResult} from './types.js';
import * as processMixins from './mixins/process.js';
import * as infoMixins from './mixins/info.js';
import {exec, SubProcess} from 'teen_process';

import * as copyMixins from './mixins/copy.js';
import * as infoMixins from './mixins/info.js';
import * as listMixins from './mixins/list.js';
import * as processMixins from './mixins/process.js';
import type {DevicectlOptions, ExecuteOptions, ExecuteResult} from './types.js';

const XCRUN = 'xcrun';
const LOG_TAG = 'Devicectl';
Expand Down Expand Up @@ -55,10 +56,7 @@ export class Devicectl {
* @param opts - Execution options
* @returns Promise that resolves to the command result
*/
async execute<T extends ExecuteOptions>(
subcommand: string[],
opts?: T,
): Promise<ExecuteResult<T>> {
async execute<T extends ExecuteOptions>(subcommand: string[], opts?: T): Promise<ExecuteResult<T>> {
const {
logStdout = false,
asynchronous = false,
Expand All @@ -72,9 +70,7 @@ export class Devicectl {
const finalArgs = ['devicectl', ...subcommand, ...(noDevice ? [] : ['--device', this.udid])];

if (subcommandOptions && subcommandOptions.length > 0) {
finalArgs.push(
...(Array.isArray(subcommandOptions) ? subcommandOptions : [subcommandOptions]),
);
finalArgs.push(...(Array.isArray(subcommandOptions) ? subcommandOptions : [subcommandOptions]));
}

if (asJson) {
Expand Down
9 changes: 2 additions & 7 deletions lib/mixins/copy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ListFilesOptions, PullFileOptions} from '../types.js';
import type {Devicectl} from '../devicectl.js';
import type {ListFilesOptions, PullFileOptions} from '../types.js';

/**
* Lists files at a specified path on the device
Expand Down Expand Up @@ -30,12 +30,7 @@ export async function listFiles(
/**
* Pulls a file from the specified path on the device to a local file system
*/
export async function pullFile(
this: Devicectl,
from: string,
to: string,
opts: PullFileOptions,
): Promise<string> {
export async function pullFile(this: Devicectl, from: string, to: string, opts: PullFileOptions): Promise<string> {
const subcommandOptions = [
'--domain-type',
opts.domainType,
Expand Down
2 changes: 1 addition & 1 deletion lib/mixins/info.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {AppInfo, ProcessInfo} from '../types.js';
import type {Devicectl} from '../devicectl.js';
import type {AppInfo, ProcessInfo} from '../types.js';

/**
* Retrieves the list of installed apps from the device
Expand Down
2 changes: 1 addition & 1 deletion lib/mixins/list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {DeviceInfo} from '../types.js';
import type {Devicectl} from '../devicectl.js';
import type {DeviceInfo} from '../types.js';

/**
* Retrieves the list of connected device infos.
Expand Down
17 changes: 4 additions & 13 deletions lib/mixins/process.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {LaunchAppOptions, ProcessInfo, TerminateAppOptions} from '../types.js';
import type {Devicectl} from '../devicectl.js';
import type {LaunchAppOptions, ProcessInfo, TerminateAppOptions} from '../types.js';

/**
* Simulates memory warning for the process with the given PID
Expand Down Expand Up @@ -28,11 +28,7 @@ export async function sendSignalToProcess(
* This method is over devicectl command, this it may take additional seconds to launch the app.
* Please use via WDA or via appium-ios-device as primary method to launch app if possible.
*/
export async function launchApp(
this: Devicectl,
bundleId: string,
opts: LaunchAppOptions = {},
): Promise<void> {
export async function launchApp(this: Devicectl, bundleId: string, opts: LaunchAppOptions = {}): Promise<void> {
const {env, terminateExisting = false} = opts;

const subcommandOptions: string[] = [];
Expand All @@ -44,9 +40,7 @@ export async function launchApp(
if (env && Object.keys(env).length > 0) {
subcommandOptions.push(
'--environment-variables',
JSON.stringify(
Object.fromEntries(Object.entries(env).map(([key, value]) => [key, String(value)])),
),
JSON.stringify(Object.fromEntries(Object.entries(env).map(([key, value]) => [key, String(value)]))),
);
}

Expand Down Expand Up @@ -112,10 +106,7 @@ export function escapeProcessFilterValue(value: string): string {
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

async function listProcessesForAppPath(
devicectl: Devicectl,
appPath: string,
): Promise<ProcessInfo[]> {
async function listProcessesForAppPath(devicectl: Devicectl, appPath: string): Promise<ProcessInfo[]> {
const filter = `executable.path BEGINSWITH "${escapeProcessFilterValue(appPath)}"`;
const {stdout} = await devicectl.execute(['device', 'info', 'processes'], {
subcommandOptions: ['--filter', filter],
Expand Down
6 changes: 6 additions & 0 deletions oxfmt.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import appiumConfig, {defineConfig, ignorePatterns} from '@appium/oxc-config/oxfmt';

export default defineConfig({
...appiumConfig,
ignorePatterns: [...ignorePatterns],
});
6 changes: 6 additions & 0 deletions oxlint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import appiumConfig, {defineConfig, ignorePatterns} from '@appium/oxc-config/oxlint';

export default defineConfig({
extends: [appiumConfig],
ignorePatterns: [...ignorePatterns],
});
72 changes: 31 additions & 41 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,23 @@
{
"name": "node-devicectl",
"version": "2.0.1",
"description": "Node.js wrapper around Apple's devicectl tool",
"tags": [
"keywords": [
"apple",
"ios",
"devicectl",
"device",
"devicectl",
"ios",
"xcode"
],
"version": "2.0.1",
"author": "Appium Contributors",
"bugs": {
"url": "https://github.com/appium/node-devicectl/issues"
},
"license": "Apache-2.0",
"author": "Appium Contributors",
"repository": {
"type": "git",
"url": "https://github.com/appium/node-devicectl.git"
},
"bugs": {
"url": "https://github.com/appium/node-devicectl/issues"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
"npm": ">=10"
},
"main": "./build/lib/index.js",
"types": "./build/lib/index.d.ts",
"bin": {},
"directories": {
"lib": "./lib"
Expand All @@ -33,46 +27,42 @@
"build/lib",
"CHANGELOG.md"
],
"dependencies": {
"@appium/logger": "^2.0.0-rc.1",
"teen_process": "^4.1.0"
"type": "module",
"main": "./build/lib/index.js",
"types": "./build/lib/index.d.ts",
"exports": {
".": {
"types": "./build/lib/index.d.ts",
"import": "./build/lib/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"build": "tsc -b",
"clean": "npm run build -- --clean",
"rebuild": "npm run clean; npm run build",
"dev": "npm run build -- --watch",
"lint": "eslint .",
"lint:fix": "npm run lint -- --fix",
"format": "prettier -w ./lib ./test",
"format:check": "prettier --check ./lib ./test",
"lint": "oxlint -c oxlint.config.mjs .",
"lint:fix": "oxlint -c oxlint.config.mjs --fix .",
"format": "oxfmt -c oxfmt.config.mjs .",
"format:check": "oxfmt -c oxfmt.config.mjs --check .",
"prepare": "npm run build",
"test": "node --enable-source-maps --test --test-timeout=60000 \"./build/test/unit/**/*.spec.js\"",
"e2e-test": "node --enable-source-maps --test --test-force-exit --test-concurrency=1 --test-timeout=300000 \"./build/test/e2e/**/*.spec.js\""
},
"prettier": {
"bracketSpacing": false,
"printWidth": 100,
"singleQuote": true
"dependencies": {
"@appium/logger": "^2.0.0-rc.1",
"teen_process": "^4.1.0"
},
"devDependencies": {
"@appium/eslint-config-appium-ts": "^3.0.1",
"@appium/tsconfig": "^1.0.0-rc.1",
"@appium/oxc-config": "^1.1.0",
"@appium/semantic-release-config": "^1.1.0",
"@appium/tsconfig": "^1.2.0",
"@appium/types": "^1.0.0-rc.1",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/git": "^10.0.1",
"@types/node": "^26.1.0",
"conventional-changelog-conventionalcommits": "^9.0.0",
"prettier": "^3.0.0",
"semantic-release": "^25.0.2",
"typescript": "^6.0.2"
"@types/node": "^26.1.0"
},
"type": "module",
"exports": {
".": {
"types": "./build/lib/index.d.ts",
"import": "./build/lib/index.js"
},
"./package.json": "./package.json"
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
"npm": ">=10"
}
}
3 changes: 3 additions & 0 deletions release.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import releaseConfig from '@appium/semantic-release-config';

export default releaseConfig();
3 changes: 2 additions & 1 deletion test/e2e/sudo-execution.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import assert from 'node:assert/strict';
import {Devicectl} from '../../lib/devicectl.js';
import {describe, it, type TestContext} from 'node:test';

import {Devicectl} from '../../lib/devicectl.js';

describe('manual sudo execution e2e', function () {
it('runs devicectl as original non-root user under sudo', async function (ctx: TestContext) {
if (
Expand Down
23 changes: 6 additions & 17 deletions test/unit/devicectl.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import assert from 'node:assert/strict';
import {describe, it, beforeEach} from 'node:test';

import {Devicectl} from '../../lib/devicectl.js';
import {appUrlToFilesystemPath, escapeProcessFilterValue} from '../../lib/mixins/process.js';
import {describe, it, beforeEach} from 'node:test';

describe('Devicectl', function () {
let devicectl: Devicectl;
Expand All @@ -24,10 +25,7 @@ describe('Devicectl', function () {
describe('sudo behavior', function () {
it('should cache sudo user identity when available', function () {
const localDevicectl = new Devicectl('test-device-udid');
assert.strictEqual(
(localDevicectl as any).sudoUser === null || !!(localDevicectl as any).sudoUser,
true,
);
assert.strictEqual((localDevicectl as any).sudoUser === null || !!(localDevicectl as any).sudoUser, true);
});

it('should keep constructor default for runAsNonRootWhenSudo behavior', function () {
Expand Down Expand Up @@ -100,10 +98,7 @@ describe('Devicectl', function () {
});

it('should strip both the file:// prefix and trailing slash', function () {
assert.strictEqual(
appUrlToFilesystemPath('file:///private/var/App.app/'),
'/private/var/App.app',
);
assert.strictEqual(appUrlToFilesystemPath('file:///private/var/App.app/'), '/private/var/App.app');
});

it('should leave paths without a file:// prefix or trailing slash unchanged', function () {
Expand All @@ -117,21 +112,15 @@ describe('Devicectl', function () {
});

it('should escape backslashes', function () {
assert.strictEqual(
escapeProcessFilterValue(String.raw`path\with\slashes`),
String.raw`path\\with\\slashes`,
);
assert.strictEqual(escapeProcessFilterValue(String.raw`path\with\slashes`), String.raw`path\\with\\slashes`);
});

it('should escape double quotes', function () {
assert.strictEqual(escapeProcessFilterValue('path"with"quotes'), 'path\\"with\\"quotes');
});

it('should escape backslashes and double quotes together', function () {
assert.strictEqual(
escapeProcessFilterValue(String.raw`path\"mixed`),
String.raw`path\\\"mixed`,
);
assert.strictEqual(escapeProcessFilterValue(String.raw`path\"mixed`), String.raw`path\\\"mixed`);
});
});
});
Expand Down