farm-orchestrator 1.0.1
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 +276 -0
- package/dist/src/bundle.js +1 -0
- package/dist/src/scripts/db-utils.d.ts +2 -0
- package/dist/src/scripts/db-utils.js +191 -0
- package/dist/src/scripts/prepare-wda.d.ts +16 -0
- package/dist/src/scripts/prepare-wda.js +609 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +115 -0
|
@@ -0,0 +1,609 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WebDriverAgent (WDA) Preparation Script
|
|
4
|
+
*
|
|
5
|
+
* This script builds, signs, and packages WebDriverAgent for iOS devices.
|
|
6
|
+
* It can be run as a CLI command: `hub prepare-wda`
|
|
7
|
+
*
|
|
8
|
+
* Options:
|
|
9
|
+
* --mobile-provisioning-file, -m Path to the mobile provisioning file
|
|
10
|
+
* --wda-project-path, -p Path to WebDriverAgent xcode project
|
|
11
|
+
* --platform Platform type: ios, tvos, or both (default: ios)
|
|
12
|
+
*/
|
|
13
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
16
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
17
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
18
|
+
}
|
|
19
|
+
Object.defineProperty(o, k2, desc);
|
|
20
|
+
}) : (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
o[k2] = m[k];
|
|
23
|
+
}));
|
|
24
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
25
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
26
|
+
}) : function(o, v) {
|
|
27
|
+
o["default"] = v;
|
|
28
|
+
});
|
|
29
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
30
|
+
var ownKeys = function(o) {
|
|
31
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
32
|
+
var ar = [];
|
|
33
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
34
|
+
return ar;
|
|
35
|
+
};
|
|
36
|
+
return ownKeys(o);
|
|
37
|
+
};
|
|
38
|
+
return function (mod) {
|
|
39
|
+
if (mod && mod.__esModule) return mod;
|
|
40
|
+
var result = {};
|
|
41
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
42
|
+
__setModuleDefault(result, mod);
|
|
43
|
+
return result;
|
|
44
|
+
};
|
|
45
|
+
})();
|
|
46
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
47
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
48
|
+
};
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.prepareWda = prepareWda;
|
|
51
|
+
const child_process_1 = require("child_process");
|
|
52
|
+
const fs_1 = __importDefault(require("fs"));
|
|
53
|
+
const http_1 = __importDefault(require("http"));
|
|
54
|
+
const https_1 = __importDefault(require("https"));
|
|
55
|
+
const os_1 = __importDefault(require("os"));
|
|
56
|
+
const path_1 = __importDefault(require("path"));
|
|
57
|
+
const util_1 = __importDefault(require("util"));
|
|
58
|
+
const hub_config_1 = require("../config/hub.config");
|
|
59
|
+
const execAsync = util_1.default.promisify(child_process_1.exec);
|
|
60
|
+
const WDA_BUILD_PATH = '/appium_wda_ios/Build/Products/Debug-iphoneos';
|
|
61
|
+
let bundleIdName = null;
|
|
62
|
+
let freeBundleID = null;
|
|
63
|
+
let isFreeAccount = false;
|
|
64
|
+
let customOutputFileName = null;
|
|
65
|
+
let generatedIpaPath = null;
|
|
66
|
+
/**
|
|
67
|
+
* Get Xcode major version
|
|
68
|
+
*/
|
|
69
|
+
async function getXcodeMajorVersion() {
|
|
70
|
+
const { stdout } = await execAsync('xcodebuild -version');
|
|
71
|
+
const match = stdout.match(/Xcode (\d+)\./);
|
|
72
|
+
if (!match) {
|
|
73
|
+
throw new Error('Unable to determine Xcode version');
|
|
74
|
+
}
|
|
75
|
+
return parseInt(match[1], 10);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get provisioning profile path based on Xcode version
|
|
79
|
+
*/
|
|
80
|
+
async function getProvisioningProfilePath() {
|
|
81
|
+
const xcodeVersion = await getXcodeMajorVersion();
|
|
82
|
+
if (xcodeVersion <= 15) {
|
|
83
|
+
return path_1.default.join(os_1.default.homedir(), 'Library/MobileDevice/Provisioning Profiles');
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return path_1.default.join(os_1.default.homedir(), 'Library/Developer/Xcode/UserData/Provisioning Profiles');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Load required modules dynamically
|
|
91
|
+
*/
|
|
92
|
+
async function loadDependencies() {
|
|
93
|
+
try {
|
|
94
|
+
// Dynamic imports to handle optional dependencies
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
96
|
+
const Applesign = require('applesign');
|
|
97
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
98
|
+
const archiver = require('archiver');
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
100
|
+
const { provision } = require('ios-mobileprovision-finder');
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
102
|
+
const Listr = require('listr');
|
|
103
|
+
const { Observable } = await Promise.resolve().then(() => __importStar(require('rxjs')));
|
|
104
|
+
const { select, input } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
105
|
+
return { Applesign, archiver, provision, Listr, Observable, select, input };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
console.error('❌ Missing required dependencies for prepare-wda command.');
|
|
109
|
+
console.error('Please install them:');
|
|
110
|
+
console.error(' npm install applesign archiver ios-mobileprovision-finder listr rxjs @inquirer/prompts');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get mobile provisioning file (interactive selection if not provided)
|
|
116
|
+
*/
|
|
117
|
+
async function getMobileProvisioningFile(provision, select, mobileProvisioningFile) {
|
|
118
|
+
if (mobileProvisioningFile) {
|
|
119
|
+
if (!fs_1.default.existsSync(mobileProvisioningFile) || !fs_1.default.statSync(mobileProvisioningFile).isFile()) {
|
|
120
|
+
throw new Error(`Mobile provisioning file ${mobileProvisioningFile} does not exist`);
|
|
121
|
+
}
|
|
122
|
+
return mobileProvisioningFile;
|
|
123
|
+
}
|
|
124
|
+
const provisionFileDir = await getProvisioningProfilePath();
|
|
125
|
+
if (!fs_1.default.existsSync(provisionFileDir)) {
|
|
126
|
+
throw new Error(`Provisioning directory does not exist: ${provisionFileDir}`);
|
|
127
|
+
}
|
|
128
|
+
const files = fs_1.default
|
|
129
|
+
.readdirSync(provisionFileDir, { encoding: 'utf8' })
|
|
130
|
+
.filter(file => file.endsWith('.mobileprovision'));
|
|
131
|
+
const provisioningFiles = files.map(file => {
|
|
132
|
+
const fullPath = path_1.default.join(provisionFileDir, file);
|
|
133
|
+
const mp = provision.readFromFile(fullPath);
|
|
134
|
+
return { ...mp, _filePath: fullPath };
|
|
135
|
+
});
|
|
136
|
+
if (!provisioningFiles || !provisioningFiles.length) {
|
|
137
|
+
throw new Error('No mobileprovision file found on the machine');
|
|
138
|
+
}
|
|
139
|
+
const prompt = await select({
|
|
140
|
+
message: 'Select the mobileprovision to use for signing',
|
|
141
|
+
choices: provisioningFiles.map(file => ({
|
|
142
|
+
value: file.UUID,
|
|
143
|
+
name: `${file.Name.split(':')[1] || file.Name} (Team: ${file.TeamName}) (${file.UUID})`,
|
|
144
|
+
})),
|
|
145
|
+
});
|
|
146
|
+
isFreeAccount = await select({
|
|
147
|
+
message: 'Is this a free or enterprise account provisioning profile?',
|
|
148
|
+
choices: [
|
|
149
|
+
{ value: true, name: 'Free Account' },
|
|
150
|
+
{ value: false, name: 'Enterprise Account' },
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
if (isFreeAccount) {
|
|
154
|
+
bundleIdName = provisioningFiles.map(file => ({
|
|
155
|
+
uuid: file.UUID,
|
|
156
|
+
name: `${file.Name.split(':')[1] || file.Name}`,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
freeBundleID = bundleIdName?.find(d => d.uuid === prompt) || null;
|
|
160
|
+
return path_1.default.join(await getProvisioningProfilePath(), `${prompt}.mobileprovision`);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Get WebDriverAgent project path
|
|
164
|
+
*/
|
|
165
|
+
async function getWdaProject(wdaProjectPath) {
|
|
166
|
+
if (wdaProjectPath) {
|
|
167
|
+
if (!fs_1.default.existsSync(wdaProjectPath) || !fs_1.default.statSync(wdaProjectPath).isDirectory()) {
|
|
168
|
+
throw new Error(`Unable to find webdriver agent project in path ${wdaProjectPath}`);
|
|
169
|
+
}
|
|
170
|
+
return wdaProjectPath;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const { stdout } = await execAsync('find $HOME/.appium -name WebDriverAgent.xcodeproj');
|
|
174
|
+
const projectPath = stdout.trim().split('\n')[0];
|
|
175
|
+
if (!projectPath) {
|
|
176
|
+
throw new Error('WebDriverAgent.xcodeproj not found');
|
|
177
|
+
}
|
|
178
|
+
return path_1.default.dirname(projectPath);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
throw new Error('Unable to find WebDriverAgent project');
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Build WebDriverAgent
|
|
186
|
+
*/
|
|
187
|
+
async function buildWebDriverAgent(projectDir, logger, _bundleId) {
|
|
188
|
+
try {
|
|
189
|
+
let buildCommand = 'xcodebuild clean build-for-testing -project WebDriverAgent.xcodeproj -derivedDataPath appium_wda_ios -scheme WebDriverAgentRunner -destination generic/platform=iOS CODE_SIGNING_ALLOWED=NO';
|
|
190
|
+
if (isFreeAccount && freeBundleID) {
|
|
191
|
+
buildCommand += ` PRODUCT_BUNDLE_IDENTIFIER=${freeBundleID.name.replace(/^\s+|\s+$/g, '')}`;
|
|
192
|
+
}
|
|
193
|
+
logger(buildCommand);
|
|
194
|
+
await execAsync(buildCommand, { cwd: projectDir, maxBuffer: 1024 * 1024 * 100 });
|
|
195
|
+
return `${projectDir}/${WDA_BUILD_PATH}/WebDriverAgentRunner-Runner.app`;
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
console.error(error);
|
|
199
|
+
throw new Error(`❌ Error building WebDriverAgent: ${error?.message}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Zip payload directory
|
|
204
|
+
*/
|
|
205
|
+
async function zipPayloadDirectory(archiver, outputZipPath, folderPath, observer) {
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const output = fs_1.default.createWriteStream(outputZipPath);
|
|
208
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
209
|
+
output.on('close', () => {
|
|
210
|
+
observer.next(`Zipped ${archive.pointer()} total bytes`);
|
|
211
|
+
observer.next(`Archive has been written to ${outputZipPath}`);
|
|
212
|
+
resolve();
|
|
213
|
+
});
|
|
214
|
+
archive.on('error', (err) => {
|
|
215
|
+
reject(err);
|
|
216
|
+
});
|
|
217
|
+
archive.pipe(output);
|
|
218
|
+
archive.directory(folderPath, 'Payload');
|
|
219
|
+
archive.finalize();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Store WDA IPA in hub storage
|
|
224
|
+
*/
|
|
225
|
+
function storeWdaInHub(ipaPath, filename) {
|
|
226
|
+
const config = (0, hub_config_1.getMergedConfig)();
|
|
227
|
+
const wdaStoragePath = config.wdaStoragePath || path_1.default.join(process.cwd(), '.cache', 'wda');
|
|
228
|
+
// Ensure storage directory exists
|
|
229
|
+
if (!fs_1.default.existsSync(wdaStoragePath)) {
|
|
230
|
+
fs_1.default.mkdirSync(wdaStoragePath, { recursive: true });
|
|
231
|
+
}
|
|
232
|
+
const targetFilename = filename.endsWith('.ipa') ? filename : `${filename}.ipa`;
|
|
233
|
+
const targetPath = path_1.default.join(wdaStoragePath, targetFilename);
|
|
234
|
+
// Copy the file to hub storage
|
|
235
|
+
fs_1.default.copyFileSync(ipaPath, targetPath);
|
|
236
|
+
return targetPath;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Check if hub server is running
|
|
240
|
+
*/
|
|
241
|
+
async function isHubRunning(hubUrl) {
|
|
242
|
+
return new Promise(resolve => {
|
|
243
|
+
const url = new URL('/health', hubUrl);
|
|
244
|
+
const client = url.protocol === 'https:' ? https_1.default : http_1.default;
|
|
245
|
+
const req = client.get(url.href, { timeout: 3000 }, res => {
|
|
246
|
+
resolve(res.statusCode === 200);
|
|
247
|
+
});
|
|
248
|
+
req.on('error', () => resolve(false));
|
|
249
|
+
req.on('timeout', () => {
|
|
250
|
+
req.destroy();
|
|
251
|
+
resolve(false);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get online nodes from hub
|
|
257
|
+
*/
|
|
258
|
+
async function getOnlineNodes(hubUrl, authHeader) {
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
const url = new URL('/api/nodes', hubUrl);
|
|
261
|
+
const client = url.protocol === 'https:' ? https_1.default : http_1.default;
|
|
262
|
+
const req = client.get(url.href, {
|
|
263
|
+
headers: {
|
|
264
|
+
Authorization: authHeader,
|
|
265
|
+
'Content-Type': 'application/json',
|
|
266
|
+
},
|
|
267
|
+
timeout: 10000,
|
|
268
|
+
}, res => {
|
|
269
|
+
let data = '';
|
|
270
|
+
res.on('data', chunk => (data += chunk));
|
|
271
|
+
res.on('end', () => {
|
|
272
|
+
try {
|
|
273
|
+
const response = JSON.parse(data);
|
|
274
|
+
const nodes = (response.data || response || []).filter((n) => n.isOnline === true);
|
|
275
|
+
resolve(nodes.map((n) => ({
|
|
276
|
+
id: n.id,
|
|
277
|
+
name: n.name,
|
|
278
|
+
})));
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
resolve([]);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
req.on('error', err => reject(err));
|
|
286
|
+
req.on('timeout', () => {
|
|
287
|
+
req.destroy();
|
|
288
|
+
reject(new Error('Request timeout'));
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Deploy WDA to a node via hub API
|
|
294
|
+
*/
|
|
295
|
+
async function deployWdaToNode(hubUrl, authHeader, nodeId, filename) {
|
|
296
|
+
return new Promise(resolve => {
|
|
297
|
+
const url = new URL('/api/wda/deploy', hubUrl);
|
|
298
|
+
const client = url.protocol === 'https:' ? https_1.default : http_1.default;
|
|
299
|
+
const postData = JSON.stringify({ nodeId, filename });
|
|
300
|
+
const req = client.request(url.href, {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: {
|
|
303
|
+
Authorization: authHeader,
|
|
304
|
+
'Content-Type': 'application/json',
|
|
305
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
306
|
+
},
|
|
307
|
+
timeout: 300000, // 5 minutes for large file transfer
|
|
308
|
+
}, res => {
|
|
309
|
+
let data = '';
|
|
310
|
+
res.on('data', chunk => (data += chunk));
|
|
311
|
+
res.on('end', () => {
|
|
312
|
+
if (res.statusCode !== 200) {
|
|
313
|
+
// Log error responses for debugging
|
|
314
|
+
console.log(`\n Debug: HTTP ${res.statusCode} - ${data.substring(0, 200)}`);
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
const response = JSON.parse(data);
|
|
318
|
+
resolve({
|
|
319
|
+
success: res.statusCode === 200,
|
|
320
|
+
message: response.message || response.error || `HTTP ${res.statusCode}`,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
resolve({
|
|
325
|
+
success: false,
|
|
326
|
+
message: `Failed to parse response (HTTP ${res.statusCode})`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
req.on('error', err => {
|
|
332
|
+
console.log(`\n Debug: Request error - ${err.message}`);
|
|
333
|
+
resolve({ success: false, message: err.message });
|
|
334
|
+
});
|
|
335
|
+
req.on('timeout', () => {
|
|
336
|
+
console.log('\n Debug: Request timeout');
|
|
337
|
+
req.destroy();
|
|
338
|
+
resolve({ success: false, message: 'Request timeout' });
|
|
339
|
+
});
|
|
340
|
+
req.write(postData);
|
|
341
|
+
req.end();
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Main function to prepare WDA
|
|
346
|
+
*/
|
|
347
|
+
async function prepareWda(options) {
|
|
348
|
+
console.log('🍎 Starting WebDriverAgent preparation...\n');
|
|
349
|
+
const { Applesign, archiver, provision, Listr, Observable, select, input } = await loadDependencies();
|
|
350
|
+
const mobileProvisioningFile = await getMobileProvisioningFile(provision, select, options.mobileProvisioningFile);
|
|
351
|
+
const platform = options.platform || 'ios';
|
|
352
|
+
// Ask user for custom output filename
|
|
353
|
+
const defaultFileName = platform === 'tvos' ? 'wda-resign_tvos' : 'wda-resign';
|
|
354
|
+
customOutputFileName = await input({
|
|
355
|
+
message: 'Enter output filename (without .ipa extension):',
|
|
356
|
+
default: defaultFileName,
|
|
357
|
+
});
|
|
358
|
+
const tasks = new Listr([
|
|
359
|
+
{
|
|
360
|
+
title: '🔍 Searching for WebDriverAgent.xcodeproj',
|
|
361
|
+
task: async (context, task) => {
|
|
362
|
+
context.wdaProjectPath = await getWdaProject(options.wdaProjectPath);
|
|
363
|
+
task.title = `Found WebDriverAgent.xcodeproj at: ${context.wdaProjectPath}`;
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
title: '🏗️ Building WebDriverAgent',
|
|
368
|
+
task: (context, task) => {
|
|
369
|
+
return new Observable((observer) => {
|
|
370
|
+
buildWebDriverAgent(context.wdaProjectPath, observer.next.bind(observer))
|
|
371
|
+
.then(wdaAppPath => {
|
|
372
|
+
context.wdaAppPath = wdaAppPath;
|
|
373
|
+
task.title = 'Successfully built WebDriverAgent';
|
|
374
|
+
observer.complete();
|
|
375
|
+
})
|
|
376
|
+
.catch((err) => {
|
|
377
|
+
observer.error(err);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
title: '📦 Preparing WebDriverAgent IPA',
|
|
384
|
+
task: (context) => {
|
|
385
|
+
return new Observable((observer) => {
|
|
386
|
+
const wdaBuildPath = path_1.default.join(context.wdaProjectPath, WDA_BUILD_PATH);
|
|
387
|
+
const payloadDirectory = path_1.default.join(wdaBuildPath, 'Payload');
|
|
388
|
+
observer.next('Removing framework directory');
|
|
389
|
+
const frameworksPath = `${context.wdaAppPath}/Frameworks`;
|
|
390
|
+
if (fs_1.default.existsSync(frameworksPath)) {
|
|
391
|
+
fs_1.default.readdirSync(frameworksPath).forEach(f => fs_1.default.rmSync(`${frameworksPath}/${f}`, { recursive: true }));
|
|
392
|
+
}
|
|
393
|
+
const infoPlistPath = path_1.default.join(context.wdaAppPath, 'Info.plist');
|
|
394
|
+
const bundleId = freeBundleID?.name.replace(/^\s+|\s+$/g, '') || '';
|
|
395
|
+
// Read and update Info.plist
|
|
396
|
+
if (fs_1.default.existsSync(infoPlistPath) && bundleId) {
|
|
397
|
+
let infoPlistContent = fs_1.default.readFileSync(infoPlistPath, 'utf8');
|
|
398
|
+
infoPlistContent = infoPlistContent.replace(/<key>CFBundleIdentifier<\/key>\n\s*<string>(.*?)<\/string>/, `<key>CFBundleIdentifier</key>\n<string>${bundleId}</string>`);
|
|
399
|
+
fs_1.default.writeFileSync(infoPlistPath, infoPlistContent, 'utf8');
|
|
400
|
+
}
|
|
401
|
+
observer.next('Creating Payload directory');
|
|
402
|
+
execAsync(`mkdir -p ${payloadDirectory}`)
|
|
403
|
+
.then(() => {
|
|
404
|
+
observer.next('Payload directory created successfully');
|
|
405
|
+
})
|
|
406
|
+
.then(() => {
|
|
407
|
+
observer.next('🚚 Moving .app file to Payload directory...');
|
|
408
|
+
return execAsync(`mv ${context.wdaAppPath} ${payloadDirectory}`);
|
|
409
|
+
})
|
|
410
|
+
.then(() => {
|
|
411
|
+
observer.next('Packing Payload directory...');
|
|
412
|
+
return zipPayloadDirectory(archiver, `${wdaBuildPath}/wda-resign.zip`, payloadDirectory, observer);
|
|
413
|
+
})
|
|
414
|
+
.then(() => observer.complete())
|
|
415
|
+
.catch((err) => {
|
|
416
|
+
observer.error(err);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
title: '✍️ Signing WebDriverAgent IPA',
|
|
423
|
+
task: async (context, task) => {
|
|
424
|
+
const wdaBuildPath = path_1.default.join(context.wdaProjectPath, WDA_BUILD_PATH);
|
|
425
|
+
if (platform === 'both') {
|
|
426
|
+
const platforms = ['ios', 'tvos'];
|
|
427
|
+
const results = [];
|
|
428
|
+
for (const p of platforms) {
|
|
429
|
+
const wdaFileName = p === 'tvos' ? `${customOutputFileName}_tvos.ipa` : `${customOutputFileName}.ipa`;
|
|
430
|
+
const ipaPath = `${wdaBuildPath}/${wdaFileName}`;
|
|
431
|
+
const appleOptions = {
|
|
432
|
+
mobileprovision: mobileProvisioningFile,
|
|
433
|
+
outfile: ipaPath,
|
|
434
|
+
};
|
|
435
|
+
if (freeBundleID) {
|
|
436
|
+
appleOptions.bundleId = freeBundleID.name.replace(/^\s+|\s+$/g, '');
|
|
437
|
+
}
|
|
438
|
+
const as = new Applesign(appleOptions);
|
|
439
|
+
await as.signIPA(path_1.default.join(wdaBuildPath, 'wda-resign.zip'));
|
|
440
|
+
results.push(`${p}: ${ipaPath}`);
|
|
441
|
+
// Store the iOS IPA path for deployment
|
|
442
|
+
if (p === 'ios') {
|
|
443
|
+
generatedIpaPath = ipaPath;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
task.title = `Successfully signed WebDriverAgent files for both platforms: ${results.join(', ')}`;
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
const wdaFileName = `${customOutputFileName}.ipa`;
|
|
450
|
+
const ipaPath = `${wdaBuildPath}/${wdaFileName}`;
|
|
451
|
+
const appleOptions = {
|
|
452
|
+
mobileprovision: mobileProvisioningFile,
|
|
453
|
+
outfile: ipaPath,
|
|
454
|
+
};
|
|
455
|
+
if (freeBundleID) {
|
|
456
|
+
appleOptions.bundleId = freeBundleID.name.replace(/^\s+|\s+$/g, '');
|
|
457
|
+
}
|
|
458
|
+
const as = new Applesign(appleOptions);
|
|
459
|
+
await as.signIPA(path_1.default.join(wdaBuildPath, 'wda-resign.zip'));
|
|
460
|
+
generatedIpaPath = ipaPath;
|
|
461
|
+
task.title = `Successfully signed WebDriverAgent file for ${platform}: ${ipaPath}`;
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
title: '💾 Storing WDA in hub',
|
|
467
|
+
task: (_context, task) => {
|
|
468
|
+
if (!generatedIpaPath) {
|
|
469
|
+
throw new Error('No IPA file generated');
|
|
470
|
+
}
|
|
471
|
+
if (!fs_1.default.existsSync(generatedIpaPath)) {
|
|
472
|
+
throw new Error(`Generated IPA file not found: ${generatedIpaPath}`);
|
|
473
|
+
}
|
|
474
|
+
const filename = `${customOutputFileName}.ipa`;
|
|
475
|
+
const storedPath = storeWdaInHub(generatedIpaPath, filename);
|
|
476
|
+
if (!fs_1.default.existsSync(storedPath)) {
|
|
477
|
+
throw new Error(`Failed to store WDA file: ${storedPath}`);
|
|
478
|
+
}
|
|
479
|
+
task.title = `Stored WDA in hub: ${storedPath}`;
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
], { exitOnError: true });
|
|
483
|
+
await tasks.run();
|
|
484
|
+
console.log('\n✅ WebDriverAgent preparation complete!\n');
|
|
485
|
+
// Track if deployment was completed successfully
|
|
486
|
+
let deploymentCompleted = false;
|
|
487
|
+
// Offer to deploy to nodes if hub is running
|
|
488
|
+
const hubUrl = process.env.HUB_URL || 'http://localhost:3000';
|
|
489
|
+
const hubRunning = await isHubRunning(hubUrl);
|
|
490
|
+
if (hubRunning) {
|
|
491
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
492
|
+
console.log('🌐 Hub server detected! Deploy WDA to remote nodes?');
|
|
493
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
494
|
+
const shouldDeploy = await select({
|
|
495
|
+
message: 'Would you like to deploy WDA to remote nodes?',
|
|
496
|
+
choices: [
|
|
497
|
+
{ value: true, name: 'Yes, deploy to nodes' },
|
|
498
|
+
{ value: false, name: 'No, skip deployment' },
|
|
499
|
+
],
|
|
500
|
+
});
|
|
501
|
+
if (shouldDeploy) {
|
|
502
|
+
// Get credentials from user (Basic Auth with access key and token)
|
|
503
|
+
console.log('📝 Enter your hub credentials (same as used in node.config.json)\n');
|
|
504
|
+
const accessKey = await input({
|
|
505
|
+
message: 'Enter your Access Key:',
|
|
506
|
+
});
|
|
507
|
+
const secretToken = await input({
|
|
508
|
+
message: 'Enter your Token:',
|
|
509
|
+
});
|
|
510
|
+
if (accessKey && secretToken) {
|
|
511
|
+
// Create Basic Auth header
|
|
512
|
+
const basicAuth = Buffer.from(`${accessKey}:${secretToken}`).toString('base64');
|
|
513
|
+
const authHeader = `Basic ${basicAuth}`;
|
|
514
|
+
try {
|
|
515
|
+
const nodes = await getOnlineNodes(hubUrl, authHeader);
|
|
516
|
+
if (nodes.length === 0) {
|
|
517
|
+
console.log('\n⚠️ No online nodes found. Skipping deployment.\n');
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
// Let user select nodes
|
|
521
|
+
const { checkbox } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
522
|
+
const selectedNodeIds = await checkbox({
|
|
523
|
+
message: 'Select nodes to deploy WDA to:',
|
|
524
|
+
choices: nodes.map(n => ({
|
|
525
|
+
value: n.id,
|
|
526
|
+
name: `${n.name} (${n.id.substring(0, 8)}...)`,
|
|
527
|
+
})),
|
|
528
|
+
});
|
|
529
|
+
console.log(`\nℹ️ Selected ${selectedNodeIds.length} node(s)`);
|
|
530
|
+
if (selectedNodeIds.length > 0) {
|
|
531
|
+
console.log('\n📤 Deploying WDA to selected nodes...');
|
|
532
|
+
const filename = `${customOutputFileName}.ipa`;
|
|
533
|
+
console.log(` WDA file: ${filename}`);
|
|
534
|
+
console.log(` Hub URL: ${hubUrl}\n`);
|
|
535
|
+
let successCount = 0;
|
|
536
|
+
for (const nodeId of selectedNodeIds) {
|
|
537
|
+
const node = nodes.find(n => n.id === nodeId);
|
|
538
|
+
process.stdout.write(` Deploying to ${node?.name || nodeId}... `);
|
|
539
|
+
try {
|
|
540
|
+
const result = await deployWdaToNode(hubUrl, authHeader, nodeId, filename);
|
|
541
|
+
if (result.success) {
|
|
542
|
+
console.log('✅');
|
|
543
|
+
successCount++;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
console.log(`❌ ${result.message}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
catch (deployError) {
|
|
550
|
+
console.log(`❌ ${deployError.message}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (successCount > 0) {
|
|
554
|
+
deploymentCompleted = true;
|
|
555
|
+
}
|
|
556
|
+
console.log('');
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
console.log('ℹ️ No nodes selected, skipping deployment.\n');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch (err) {
|
|
564
|
+
console.log(`\n⚠️ Failed to fetch nodes: ${err.message}\n`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
// Print post-installation instructions
|
|
570
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
571
|
+
if (deploymentCompleted) {
|
|
572
|
+
// If deployment was successful, only show trust instructions
|
|
573
|
+
console.log('📱 Next Step: Trust the WDA Certificate on your Device');
|
|
574
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
575
|
+
console.log('✅ WDA has been automatically installed on your devices!\n');
|
|
576
|
+
console.log('⚠️ IMPORTANT: Trust the developer certificate on your iOS device(s):');
|
|
577
|
+
console.log(' 1. On your iOS device, go to: Settings → General → VPN & Device Management');
|
|
578
|
+
console.log(' 2. Under "Developer App", tap on your developer/team name');
|
|
579
|
+
console.log(' 3. Tap "Trust <Developer Name>" and confirm\n');
|
|
580
|
+
console.log('📱 Verify WDA is installed:');
|
|
581
|
+
console.log(' • Look for "WebDriverAgentRunner" app on your device');
|
|
582
|
+
console.log(' • The app icon may be blank/white - this is normal\n');
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
// If deployment was skipped/failed, show full manual installation instructions
|
|
586
|
+
console.log('📱 Next Steps: Install and Trust WebDriverAgent on your device');
|
|
587
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
588
|
+
console.log('1️⃣ Install the signed WDA IPA on your device:');
|
|
589
|
+
console.log(' Using Xcode:');
|
|
590
|
+
console.log(' • Open Xcode → Window → Devices and Simulators');
|
|
591
|
+
console.log(' • Select your device → Click "+" under "Installed Apps"');
|
|
592
|
+
console.log(' • Select the generated .ipa file\n');
|
|
593
|
+
console.log(' Using xcrun devicectl (recommended):');
|
|
594
|
+
console.log(' $ xcrun devicectl device install app --device <udid> <path-to-wda.ipa>\n');
|
|
595
|
+
console.log(' Using command line (legacy tools):');
|
|
596
|
+
console.log(' $ ideviceinstaller -i <path-to-wda.ipa>');
|
|
597
|
+
console.log(' $ ios-deploy --bundle <path-to-wda.ipa>\n');
|
|
598
|
+
console.log('2️⃣ Trust the developer certificate on your iOS device:');
|
|
599
|
+
console.log(' • Go to Settings → General → VPN & Device Management');
|
|
600
|
+
console.log(' • Under "Developer App", tap on your developer/team name');
|
|
601
|
+
console.log(' • Tap "Trust <Developer Name>" and confirm\n');
|
|
602
|
+
console.log('3️⃣ Verify WDA is installed:');
|
|
603
|
+
console.log(' • Look for "WebDriverAgentRunner" app on your device');
|
|
604
|
+
console.log(' • The app icon may be blank/white - this is normal\n');
|
|
605
|
+
}
|
|
606
|
+
console.log('💡 Tip: For free Apple Developer accounts, the certificate expires');
|
|
607
|
+
console.log(' after 7 days. You will need to re-sign and reinstall WDA.\n');
|
|
608
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
609
|
+
}
|