appium-ios-device 2.1.0 → 2.2.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.
@@ -0,0 +1,447 @@
1
+ import plistlib from 'bplist-parser';
2
+ import bplistCreate from 'bplist-creator';
3
+ import {parse as uuidParse, stringify as uuidStringify} from 'uuid';
4
+ import _ from 'lodash';
5
+
6
+ const NSKEYED_ARCHIVE_VERSION = 100_000;
7
+ const NULL_UID = new plistlib.UID(0);
8
+ const CYCLE_TOKEN = 'CycleToken';
9
+ const PRIMITIVE_TYPES = ['Number', 'String', 'Boolean', 'UID', 'Buffer'];
10
+ const NON_ENCODABLE_TYPES = ['number', 'boolean'];
11
+ const NSKEYEDARCHIVER = 'NSKeyedArchiver';
12
+ const UNIX2APPLE_TIMESTAMP_SECOND = 978307200;
13
+
14
+
15
+ class ArchivedObject {
16
+ /**
17
+ * Stateful wrapper around Archive for an object being archived.
18
+ * @param {Object} object
19
+ * @param {Unarchive} unarchiver
20
+ * @constructor
21
+ */
22
+ constructor (object, unarchiver) {
23
+ this.object = object;
24
+ this._unarchiver = unarchiver;
25
+ }
26
+
27
+ decodeIndex (index) {
28
+ return this._unarchiver.decodeObject(index);
29
+ }
30
+
31
+ decode (key) {
32
+ return this._unarchiver.decodeKey(this.object, key);
33
+ }
34
+ }
35
+
36
+ class ArchivingObject {
37
+ /**
38
+ * Stateful wrapper around Unarchive for an archived object
39
+ * @param {Object} object
40
+ * @param {Archive} archiver
41
+ * @constructor
42
+ */
43
+ constructor (object, archiver) {
44
+ this._archiveObj = object;
45
+ this._archiver = archiver;
46
+ }
47
+
48
+ encode (key, val) {
49
+ this._archiveObj[key] = this._archiver.encode(val);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * This class must be inherited when creating an archive/unarchive subclass
55
+ * And you need to call `updateNSKeyedArchiveClass` add subclass to archive/unarchive object
56
+ */
57
+ class BaseArchiveHandler {
58
+ /**
59
+ * @param {ArchivedObject} archive
60
+ */
61
+ // eslint-disable-next-line no-unused-vars
62
+ decodeArchive (archive) {
63
+ throw new Error(`Did not know how to decode the object`);
64
+ }
65
+
66
+ /**
67
+ * @param {Object} obj an instance of this class
68
+ * @param {ArchivingObject} archive
69
+ */
70
+ // eslint-disable-next-line no-unused-vars
71
+ encodeArchive (obj, archive) {
72
+ throw new Error(`Did not know how to encode the object`);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * "Delegate for packing/unpacking NS(Mutable)Dictionary objects"
78
+ */
79
+ class DictArchive extends BaseArchiveHandler {
80
+ decodeArchive (archive) {
81
+ const keyUids = archive.decode('NS.keys');
82
+ const valUids = archive.decode('NS.objects');
83
+ const d = {};
84
+ for (let i = 0; i < keyUids.length ; i++) {
85
+ const key = archive.decodeIndex(keyUids[i]);
86
+ d[key] = archive.decodeIndex(valUids[i]);
87
+ }
88
+ return d;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Delegate for packing/unpacking NS(Mutable)Array objects
94
+ */
95
+ class ListArchive extends BaseArchiveHandler {
96
+ decodeArchive (archive) {
97
+ const uids = archive.decode('NS.objects');
98
+ return uids.map(archive.decodeIndex.bind(archive));
99
+ }
100
+ }
101
+
102
+ class DTTapMessagePlist extends BaseArchiveHandler {
103
+ decodeArchive (archive) {
104
+ return archive.decode('DTTapMessagePlist');
105
+ }
106
+ }
107
+
108
+ class NSError extends BaseArchiveHandler {
109
+ decodeArchive (archive) {
110
+ return {
111
+ '$class': 'NSError',
112
+ 'domain': archive.decode('NSDomain'),
113
+ 'userinfo': archive.decode('NSUserInfo'),
114
+ 'code': archive.decode('NSCode')
115
+ };
116
+ }
117
+ }
118
+
119
+ class NSException extends BaseArchiveHandler {
120
+ decodeArchive (archive) {
121
+ return {
122
+ '$class': 'NSException',
123
+ 'reason': archive.decode('NS.reason'),
124
+ 'userinfo': archive.decode('userinfo'),
125
+ 'name': archive.decode('NS.name')
126
+ };
127
+ }
128
+ }
129
+
130
+ class NSURL extends BaseArchiveHandler {
131
+ /**
132
+ * @param {*} base
133
+ * @param {string} relative usually ios device relative path e.g: file://xx/
134
+ */
135
+ constructor (base, relative) {
136
+ super();
137
+ this._base = base;
138
+ this._relative = relative;
139
+ }
140
+
141
+ decodeArchive (archive) {
142
+ return {$class: 'NSURL', base: archive.decode('NS.base'), relative: archive.decode('NS.relative')};
143
+ }
144
+
145
+ encodeArchive (obj, archive) {
146
+ archive.encode('NS.base', obj._base);
147
+ archive.encode('NS.relative', obj._relative);
148
+ }
149
+ }
150
+
151
+ class NSDate extends BaseArchiveHandler {
152
+ /**
153
+ * @param {number} data timestamp in seconds
154
+ */
155
+ constructor (data) {
156
+ super();
157
+ this._data = data;
158
+ }
159
+
160
+ decodeArchive (archive) {
161
+ return UNIX2APPLE_TIMESTAMP_SECOND + archive.decode('NS.time');
162
+ }
163
+
164
+ encodeArchive (obj, archive) {
165
+ archive.encode('NS.time', obj._data - UNIX2APPLE_TIMESTAMP_SECOND);
166
+ }
167
+ }
168
+
169
+ class NSMutableString extends BaseArchiveHandler {
170
+ decodeArchive (archive) {
171
+ return archive.decode('NS.string');
172
+ }
173
+ }
174
+
175
+ class NSMutableData extends BaseArchiveHandler {
176
+ decodeArchive (archive) {
177
+ return archive.decode('NS.data');
178
+ }
179
+ }
180
+
181
+ class NSUUID extends BaseArchiveHandler {
182
+ /**
183
+ * @param {string} data uuid format data e.g:00000000-0000-0000-0000-000000000000
184
+ */
185
+ constructor (data) {
186
+ super();
187
+ this._data = data;
188
+ }
189
+
190
+ decodeArchive (archive) {
191
+ return uuidStringify(archive.decode('NS.uuidbytes'));
192
+ }
193
+
194
+ encodeArchive (obj, archive) {
195
+ archive._archiveObj['NS.uuidbytes'] = Buffer.from(uuidParse(obj._data).buffer);
196
+ }
197
+ }
198
+
199
+ class XCTCapabilities extends BaseArchiveHandler {
200
+ decodeArchive (archive) {
201
+ return archive.decode('capabilities-dictionary');
202
+ }
203
+ }
204
+
205
+ class NSNull extends BaseArchiveHandler {
206
+ decodeArchive () {
207
+ return null;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * decode and encode Archive of currently known data formats
213
+ */
214
+ const UNARCHIVE_CLASS_MAP = {
215
+ DTTapMessagePlist,
216
+ DTSysmonTapMessage: DTTapMessagePlist,
217
+ DTTapHeartbeatMessage: DTTapMessagePlist,
218
+ DTTapMessageArchive: DTTapMessagePlist,
219
+ DTKTraceTapMessage: DTTapMessagePlist,
220
+ ErrorArchive: NSError,
221
+ ExceptionArchive: NSException,
222
+ NSDictionary: DictArchive,
223
+ NSMutableDictionary: DictArchive,
224
+ NSArray: ListArchive,
225
+ NSMutableArray: ListArchive,
226
+ NSMutableSet: ListArchive,
227
+ NSSet: ListArchive,
228
+ NSDate,
229
+ NSError,
230
+ NSException,
231
+ NSMutableString,
232
+ NSMutableData,
233
+ NSNull,
234
+ NSUUID,
235
+ NSURL,
236
+ XCTCapabilities
237
+ };
238
+
239
+ /**
240
+ * Capable of unpacking an archived object tree in the NSKeyedArchive format.
241
+ * Apple's implementation can be found here:
242
+ * https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/NSKeyedUnarchiver.swift
243
+ */
244
+ class Unarchive {
245
+ constructor (inputBytes) {
246
+ this.input = inputBytes;
247
+ this.unpackedUids = {};
248
+ this.topUID = NULL_UID;
249
+ this.objects = [];
250
+ }
251
+
252
+ unpackArchiveHeader () {
253
+ const plist = plistlib.parseBuffer(this.input)[0];
254
+ if (plist.$archiver !== NSKEYEDARCHIVER) {
255
+ throw new Error(`unsupported encoder: ${plist.$archiver}`);
256
+ }
257
+ if (plist.$version !== NSKEYED_ARCHIVE_VERSION) {
258
+ throw new Error(`expected ${NSKEYED_ARCHIVE_VERSION}, got ${plist.$version }`);
259
+ }
260
+ const top = plist.$top;
261
+ const topUID = top.root;
262
+ if (!topUID) {
263
+ throw new Error(`top object did not have a UID! dump: ${JSON.stringify(top)}`);
264
+ }
265
+ this.topUID = topUID;
266
+ this.objects = plist.$objects;
267
+ }
268
+
269
+ /**
270
+ * use the UNARCHIVE_CLASS_MAP to find the unarchiving delegate of a uid
271
+ */
272
+ classForUid (index) {
273
+ const meta = this.objects[index.UID];
274
+ const name = meta.$classname;
275
+ const klass = UNARCHIVE_CLASS_MAP[name];
276
+ if (!klass) {
277
+ throw new Error(`no mapping for ${name} in UNARCHIVE_CLASS_MAP`);
278
+ }
279
+ return klass;
280
+ }
281
+
282
+ decodeKey (obj, key) {
283
+ const val = obj[key];
284
+ return _.isNil(val?.UID) ? val : this.decodeObject(val);
285
+ }
286
+
287
+ decodeObject (index) {
288
+ if (index === NULL_UID) {
289
+ return null;
290
+ }
291
+ const obj = this.unpackedUids[index];
292
+ if (obj === CYCLE_TOKEN) {
293
+ throw new Error(`archive has a cycle with ${index}`);
294
+ }
295
+ if (!_.isUndefined(obj)) {
296
+ return obj;
297
+ }
298
+ const rawObj = this.objects[index.UID];
299
+ this.unpackedUids[index.UID] = CYCLE_TOKEN;
300
+
301
+ if (!rawObj?.$class) {
302
+ this.unpackedUids[index.UID] = obj;
303
+ return rawObj;
304
+ }
305
+ const klass = this.classForUid(rawObj.$class);
306
+ const klassObj = new klass().decodeArchive(new ArchivedObject(rawObj, this));
307
+ this.unpackedUids[index.UID] = klassObj;
308
+ return klassObj;
309
+ }
310
+
311
+ toObject () {
312
+ this.unpackArchiveHeader();
313
+ return this.decodeObject(this.topUID);
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Capable of packing an object tree into the NSKeyedArchive format.
319
+ * Apple's implementation can be found here:
320
+ * https://github.com/apple/swift-corelibs-foundation/blob/main/Sources/Foundation/NSKeyedArchiver.swift
321
+ */
322
+ class Archive {
323
+ constructor (inputObject) {
324
+ this.input = inputObject;
325
+ this.classMap = {};
326
+ this.objects = ['$null']; // objects that go directly into the archive, always start with $null
327
+ }
328
+
329
+ uidForArchiver (archiver) {
330
+ let val = this.classMap[archiver];
331
+ if (val) {
332
+ return new plistlib.UID(val);
333
+ }
334
+ this.classMap[archiver] = this.objects.length;
335
+ val = new plistlib.UID(this.objects.length);
336
+ this.objects.push({
337
+ '$classes': [archiver],
338
+ '$classname': archiver
339
+ });
340
+ return val;
341
+ }
342
+
343
+ archive (obj) {
344
+ if (_.isUndefined(obj) || _.isNull(obj)) {
345
+ return NULL_UID;
346
+ }
347
+ const index = new plistlib.UID(this.objects.length);
348
+ if (PRIMITIVE_TYPES.includes(obj.constructor.name)) {
349
+ this.objects.push(obj);
350
+ return index;
351
+ }
352
+ const archiveObj = {};
353
+ this.objects.push(archiveObj);
354
+ this.encodeTopLevel(obj, archiveObj);
355
+ return index;
356
+ }
357
+
358
+ encode (val) {
359
+ if (NON_ENCODABLE_TYPES.includes(typeof val)) {
360
+ return val;
361
+ }
362
+ return this.archive(val);
363
+ }
364
+
365
+ encodeTopLevel (obj, archiveObj) {
366
+ if (obj instanceof Array) {
367
+ return this.encodeArray(obj, archiveObj);
368
+ } else if (obj instanceof Set) {
369
+ return this.encodeSet(obj, archiveObj);
370
+ } else if (obj instanceof Object) {
371
+ const objName = obj.constructor.name;
372
+ // Only special class instance are useful, such as NSURL, NSUUID, NSDate.
373
+ // And this class must also have the encodeArchive method
374
+ if (objName in UNARCHIVE_CLASS_MAP) {
375
+ archiveObj.$class = this.uidForArchiver(objName);
376
+ obj.encodeArchive(obj, new ArchivingObject(archiveObj, this));
377
+ } else {
378
+ return this.encodeDict(obj, archiveObj);
379
+ }
380
+ } else {
381
+ throw Error(`Unable to encode types: ${typeof obj}`);
382
+ }
383
+ }
384
+
385
+ encodeArray (objs, archiveObj) {
386
+ archiveObj.$class = this.uidForArchiver('NSArray');
387
+ archiveObj['NS.objects'] = objs.map(this.archive.bind(this));
388
+ }
389
+
390
+ encodeSet (objs, archiveObj) {
391
+ archiveObj.$class = this.uidForArchiver('NSSet');
392
+ archiveObj['NS.objects'] = objs.map(this.archive.bind(this));
393
+ }
394
+
395
+ encodeDict (obj, archiveObj) {
396
+ archiveObj.$class = this.uidForArchiver('NSDictionary');
397
+ archiveObj['NS.keys'] = _.keys(obj).map(this.archive.bind(this));
398
+ archiveObj['NS.objects'] = _.values(obj).map(this.archive.bind(this));
399
+ }
400
+
401
+ toBytes () {
402
+ if (this.objects.length === 1) {
403
+ this.archive(this.input);
404
+ }
405
+ const d = {
406
+ '$version': NSKEYED_ARCHIVE_VERSION,
407
+ '$archiver': NSKEYEDARCHIVER,
408
+ '$top': {'root': new plistlib.UID(1)},
409
+ '$objects': this.objects
410
+ };
411
+ return bplistCreate(d);
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Creates NSKeyed Buffer from an object
417
+ * @param {Object} inputObject
418
+ * @returns {Buffer} NSKeyed Buffer
419
+ */
420
+ function archive (inputObject) {
421
+ return new Archive(inputObject).toBytes();
422
+ }
423
+
424
+ /**
425
+ * Parses NSKeyed Buffer into JS Object
426
+ * @param {Buffer} inputBytes NSKeyed Buffer
427
+ * @returns {Object} JS Object
428
+ */
429
+ function unarchive (inputBytes) {
430
+ return new Unarchive(inputBytes).toObject();
431
+ }
432
+
433
+ /**
434
+ * Update unknown NSKeyedArchive types for packing/unpacking
435
+ * @param {String} name packing/unpacking key name
436
+ * @param {BaseArchiveHandler} subClass inherit from BaseArchiveHandler class
437
+ */
438
+ function updateNSKeyedArchiveClass (name, subClass) {
439
+ if (!_.isFunction(subClass.prototype?.decodeArchive) && !_.isFunction(subClass.prototype?.encodeArchive)) {
440
+ throw Error('subClass must have decodeArchive or encodeArchive methods');
441
+ }
442
+ if (!(name in UNARCHIVE_CLASS_MAP)) {
443
+ UNARCHIVE_CLASS_MAP[name] = subClass;
444
+ }
445
+ }
446
+
447
+ export { updateNSKeyedArchiveClass, BaseArchiveHandler, NSURL, NSUUID, NSDate, unarchive, archive};
package/lib/services.js CHANGED
@@ -6,10 +6,12 @@ import { InstallationProxyService, INSTALLATION_PROXY_SERVICE_NAME } from './ins
6
6
  import { AfcService, AFC_SERVICE_NAME } from './afc';
7
7
  import { NotificationProxyService, NOTIFICATION_PROXY_SERVICE_NAME } from './notification-proxy';
8
8
  import { HouseArrestService, HOUSE_ARREST_SERVICE_NAME } from './house-arrest';
9
+ import { InstrumentService, INSTRUMENT_SERVICE_NAME_VERSION_14, INSTRUMENT_SERVICE_NAME} from './instrument';
9
10
  import PlistService from './plist-service';
10
11
  import semver from 'semver';
11
12
 
12
13
  const CRASH_LOG_SERVICE_NAME = 'com.apple.crashreportcopymobile';
14
+ const INSTRUMENT_HANDSHAKE_VERSION = 14;
13
15
 
14
16
  async function startSyslogService (udid, opts = {}) {
15
17
  const socket = await startService(udid, SYSLOG_SERVICE_NAME, opts.socket);
@@ -80,12 +82,21 @@ async function startHouseArrestService (udid, opts = {}) {
80
82
  return new HouseArrestService(socket);
81
83
  }
82
84
 
83
- async function startService (udid, serviceName, socket) {
85
+ async function startInstrumentService (udid, opts = {}) {
86
+ const osVersion = opts.osVersion || await getOSVersion(udid, opts.socket);
87
+ return new InstrumentService(
88
+ parseInt(osVersion.split('.')[0], 10) < INSTRUMENT_HANDSHAKE_VERSION
89
+ ? await startService(udid, INSTRUMENT_SERVICE_NAME, opts.socket, true)
90
+ : await startService(udid, INSTRUMENT_SERVICE_NAME_VERSION_14, opts.socket)
91
+ );
92
+ }
93
+
94
+ async function startService (udid, serviceName, socket, handshakeOnly = false) {
84
95
  const lockdown = await startLockdownSession(udid, socket);
85
96
  try {
86
97
  const service = await lockdown.startService(serviceName);
87
98
  if (service.EnableServiceSSL) {
88
- return await connectPortSSL(udid, service.Port, socket);
99
+ return await connectPortSSL(udid, service.Port, socket, handshakeOnly);
89
100
  } else {
90
101
  return await connectPort(udid, service.Port, socket);
91
102
  }
@@ -98,5 +109,5 @@ export {
98
109
  startSyslogService, startWebInspectorService,
99
110
  startInstallationProxyService, startSimulateLocationService,
100
111
  startAfcService, startCrashLogService, startNotificationProxyService,
101
- startHouseArrestService
112
+ startHouseArrestService, startInstrumentService
102
113
  };
package/lib/ssl-helper.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import tls from 'tls';
2
-
2
+ import net from 'net';
3
+ import B from 'bluebird';
3
4
 
4
5
  const TLS_VERSION = 'TLSv1_method';
6
+ const HANDSHAKE_TIMEOUT_MS = 10000;
5
7
 
6
8
  function upgradeToSSL (socket, key, cert) {
7
9
  return new tls.TLSSocket(socket, {
@@ -13,4 +15,43 @@ function upgradeToSSL (socket, key, cert) {
13
15
  })
14
16
  });
15
17
  }
16
- export { upgradeToSSL };
18
+
19
+ /**
20
+ * After the ssl protocol is successfully handshake, close the ssl protocol channel and use text transmission
21
+ * @param socket
22
+ * @param key
23
+ * @param cert
24
+ * @returns {Promise<Socket>} Duplicate the input socket
25
+ */
26
+ async function enableSSLHandshakeOnly (socket, key, cert) {
27
+ const sslSocket = tls.connect({
28
+ socket,
29
+ secureContext: tls.createSecureContext({
30
+ key,
31
+ cert,
32
+ secureProtocol: TLS_VERSION
33
+ }),
34
+ rejectUnauthorized: false,
35
+ });
36
+
37
+ // stop receiving data after successful handshake
38
+ await new B((resolve, reject) => {
39
+ const timeoutHandler = setTimeout(() => {
40
+ if (!sslSocket.destroyed) {
41
+ sslSocket.end();
42
+ }
43
+ return reject(new Error('ssl handshake error'));
44
+ }, HANDSHAKE_TIMEOUT_MS);
45
+
46
+ sslSocket.once('secureConnect', () => {
47
+ clearTimeout(timeoutHandler);
48
+ sslSocket._handle.readStop();
49
+ return resolve();
50
+ });
51
+ });
52
+ // Duplicate the socket. Return a new socket object connected to the same system resource
53
+ return net.Socket({fd: socket._handle.fd});
54
+ }
55
+
56
+
57
+ export { upgradeToSSL, enableSSLHandshakeOnly };
package/lib/utilities.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import Usbmux, { getDefaultSocket } from './usbmux';
2
- import { upgradeToSSL } from './ssl-helper';
2
+ import { upgradeToSSL, enableSSLHandshakeOnly } from './ssl-helper';
3
3
  import _ from 'lodash';
4
4
  import log from './logger';
5
5
 
@@ -183,9 +183,10 @@ async function startLockdownSession (udid, socket = null) {
183
183
  * @param {string} udid Device UDID
184
184
  * @param {number} port Port to connect
185
185
  * @param {?net.Socket} socket the socket of usbmuxd. It will default to /var/run/usbmuxd if it is not passed
186
+ * @param {boolean} handshakeOnly only handshake and return the initial socket
186
187
  * @returns {tls.TLSSocket|Object} The socket or the object returned in the callback if the callback function exists
187
188
  */
188
- async function connectPortSSL (udid, port, socket = null) {
189
+ async function connectPortSSL (udid, port, socket = null, handshakeOnly = false) {
189
190
  const usbmux = new Usbmux(socket || await getDefaultSocket());
190
191
  try {
191
192
  const device = await usbmux.findDevice(udid);
@@ -197,7 +198,9 @@ async function connectPortSSL (udid, port, socket = null) {
197
198
  throw new Error(`Could not find a pair record for device '${udid}'. Please first pair with the device`);
198
199
  }
199
200
  const socket = await usbmux.connect(device.Properties.DeviceID, port, undefined);
200
- return upgradeToSSL(socket, pairRecord.HostPrivateKey, pairRecord.HostCertificate);
201
+ return handshakeOnly ?
202
+ await enableSSLHandshakeOnly(socket, pairRecord.HostPrivateKey, pairRecord.HostCertificate) :
203
+ upgradeToSSL(socket, pairRecord.HostPrivateKey, pairRecord.HostCertificate);
201
204
  } catch (e) {
202
205
  usbmux.close();
203
206
  throw e;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "keywords": [
5
5
  "appium"
6
6
  ],
7
- "version": "2.1.0",
7
+ "version": "2.2.0",
8
8
  "author": "appium",
9
9
  "license": "Apache-2.0",
10
10
  "repository": {
@@ -31,10 +31,14 @@
31
31
  "dependencies": {
32
32
  "@appium/support": "^2.55.3",
33
33
  "@babel/runtime": "^7.0.0",
34
+ "asyncbox": "^2.9.2",
34
35
  "bluebird": "^3.1.1",
36
+ "bplist-creator": "^0.x",
37
+ "bplist-parser": "^0.x",
35
38
  "lodash": "^4.17.15",
36
39
  "semver": "^7.0.0",
37
- "source-map-support": "^0.x"
40
+ "source-map-support": "^0.x",
41
+ "uuid": "^8.3.2"
38
42
  },
39
43
  "scripts": {
40
44
  "build": "gulp transpile",
@@ -54,9 +58,11 @@
54
58
  "devDependencies": {
55
59
  "@appium/gulp-plugins": "^6.0.0",
56
60
  "@appium/eslint-config-appium": "^5.0.0",
61
+ "@semantic-release/git": "^10.0.1",
57
62
  "chai": "^4.1.2",
58
63
  "chai-as-promised": "^7.1.1",
59
64
  "gulp": "^4.0.0",
60
- "pre-commit": "^1.1.3"
65
+ "pre-commit": "^1.1.3",
66
+ "semantic-release": "^19.0.2"
61
67
  }
62
68
  }