@ebowwa/ios-devices 1.0.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/src/utils.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Command execution utilities
3
+ */
4
+
5
+ import { spawn } from 'child_process';
6
+ import { parse as parseArgs } from 'shell-quote';
7
+ import { CommandResultSchema, type CommandResult } from './types.js';
8
+
9
+ export interface ExecOptions {
10
+ timeout?: number;
11
+ jsonOutput?: boolean;
12
+ cwd?: string;
13
+ env?: Record<string, string>;
14
+ }
15
+
16
+ /**
17
+ * Execute a command and return structured result
18
+ */
19
+ export async function exec(
20
+ command: string,
21
+ args: string[] = [],
22
+ options: ExecOptions = {}
23
+ ): Promise<CommandResult> {
24
+ const { timeout = 60000, cwd, env } = options;
25
+
26
+ return new Promise((resolve) => {
27
+ const proc = spawn(command, args, {
28
+ cwd,
29
+ env: { ...process.env, ...env },
30
+ stdio: ['ignore', 'pipe', 'pipe'],
31
+ });
32
+
33
+ let stdout = '';
34
+ let stderr = '';
35
+
36
+ proc.stdout?.on('data', (data) => {
37
+ stdout += data.toString();
38
+ });
39
+
40
+ proc.stderr?.on('data', (data) => {
41
+ stderr += data.toString();
42
+ });
43
+
44
+ const timer = setTimeout(() => {
45
+ proc.kill('SIGTERM');
46
+ stderr += '\nCommand timed out';
47
+ }, timeout);
48
+
49
+ proc.on('close', (code) => {
50
+ clearTimeout(timer);
51
+ const result: CommandResult = {
52
+ success: code === 0,
53
+ stdout,
54
+ stderr,
55
+ exitCode: code ?? 1,
56
+ };
57
+
58
+ // Try to parse JSON if output looks like JSON
59
+ const trimmed = stdout.trim();
60
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
61
+ try {
62
+ result.json = JSON.parse(trimmed);
63
+ } catch {
64
+ // Not valid JSON
65
+ }
66
+ }
67
+
68
+ resolve(CommandResultSchema.parse(result));
69
+ });
70
+
71
+ proc.on('error', (err) => {
72
+ clearTimeout(timer);
73
+ resolve({
74
+ success: false,
75
+ stdout,
76
+ stderr: err.message,
77
+ exitCode: 1,
78
+ });
79
+ });
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Execute xcrun devicectl with JSON output
85
+ */
86
+ export async function execDeviceCtl(
87
+ subcommand: string[],
88
+ options: ExecOptions = {}
89
+ ): Promise<CommandResult> {
90
+ const args = ['devicectl', ...subcommand];
91
+
92
+ // Add JSON output for stable parsing
93
+ if (!args.includes('--json-output')) {
94
+ args.push('--json-output', '-');
95
+ }
96
+
97
+ const result = await exec('xcrun', args, options);
98
+
99
+ // devicectl writes JSON to stderr when using --json-output -
100
+ // but also check stdout
101
+ if (result.stderr && result.stderr.startsWith('{')) {
102
+ try {
103
+ result.json = JSON.parse(result.stderr);
104
+ } catch {
105
+ // Not valid JSON
106
+ }
107
+ }
108
+
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * Execute libimobiledevice tool
114
+ */
115
+ export async function execILDevice(
116
+ tool: string,
117
+ args: string[] = [],
118
+ options: ExecOptions = {}
119
+ ): Promise<CommandResult> {
120
+ return exec(tool, args, options);
121
+ }
122
+
123
+ /**
124
+ * Execute xcrun simctl
125
+ */
126
+ export async function execSimCtl(
127
+ subcommand: string[],
128
+ options: ExecOptions = {}
129
+ ): Promise<CommandResult> {
130
+ return exec('xcrun', ['simctl', ...subcommand], options);
131
+ }
132
+
133
+ /**
134
+ * Parse command string into args
135
+ */
136
+ export function parseCommand(command: string): string[] {
137
+ return parseArgs(command).filter((arg): arg is string => typeof arg === 'string');
138
+ }
139
+
140
+ /**
141
+ * Build device identifier argument
142
+ */
143
+ export function buildDeviceArg(identifier: string): string[] {
144
+ return ['--device', identifier];
145
+ }
146
+
147
+ /**
148
+ * Check if a command exists
149
+ */
150
+ export async function commandExists(command: string): Promise<boolean> {
151
+ const result = await exec('which', [command], { timeout: 5000 });
152
+ return result.success && result.stdout.trim().length > 0;
153
+ }
154
+
155
+ /**
156
+ * Check available tools
157
+ */
158
+ export async function checkAvailableTools(): Promise<{
159
+ devicectl: boolean;
160
+ simctl: boolean;
161
+ libimobiledevice: boolean;
162
+ }> {
163
+ const [devicectl, simctl, libimobiledevice] = await Promise.all([
164
+ commandExists('xcrun').then((exists) => exists && exec('xcrun', ['devicectl', '--version'], { timeout: 5000 }).then((r) => r.success)),
165
+ commandExists('xcrun').then((exists) => exists && exec('xcrun', ['simctl', 'help'], { timeout: 5000 }).then((r) => r.success)),
166
+ commandExists('idevicesyslog'),
167
+ ]);
168
+
169
+ return {
170
+ devicectl,
171
+ simctl,
172
+ libimobiledevice,
173
+ };
174
+ }