appshot-cli 0.7.0 → 0.8.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.
Files changed (70) hide show
  1. package/README.md +244 -5
  2. package/dist/cli.js +23 -4
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/device.d.ts +3 -0
  5. package/dist/commands/device.d.ts.map +1 -0
  6. package/dist/commands/device.js +414 -0
  7. package/dist/commands/device.js.map +1 -0
  8. package/dist/commands/frame.d.ts +3 -0
  9. package/dist/commands/frame.d.ts.map +1 -0
  10. package/dist/commands/frame.js +251 -0
  11. package/dist/commands/frame.js.map +1 -0
  12. package/dist/commands/unwatch.d.ts +3 -0
  13. package/dist/commands/unwatch.d.ts.map +1 -0
  14. package/dist/commands/unwatch.js +53 -0
  15. package/dist/commands/unwatch.js.map +1 -0
  16. package/dist/commands/watch-status.d.ts +3 -0
  17. package/dist/commands/watch-status.d.ts.map +1 -0
  18. package/dist/commands/watch-status.js +116 -0
  19. package/dist/commands/watch-status.js.map +1 -0
  20. package/dist/commands/watch.d.ts +3 -0
  21. package/dist/commands/watch.d.ts.map +1 -0
  22. package/dist/commands/watch.js +322 -0
  23. package/dist/commands/watch.js.map +1 -0
  24. package/dist/core/compose.d.ts +25 -0
  25. package/dist/core/compose.d.ts.map +1 -1
  26. package/dist/core/compose.js +82 -0
  27. package/dist/core/compose.js.map +1 -1
  28. package/dist/core/devices.d.ts +5 -0
  29. package/dist/core/devices.d.ts.map +1 -1
  30. package/dist/core/devices.js +43 -1
  31. package/dist/core/devices.js.map +1 -1
  32. package/dist/services/compose-bridge.d.ts +47 -0
  33. package/dist/services/compose-bridge.d.ts.map +1 -0
  34. package/dist/services/compose-bridge.js +222 -0
  35. package/dist/services/compose-bridge.js.map +1 -0
  36. package/dist/services/device-manager.d.ts +14 -0
  37. package/dist/services/device-manager.d.ts.map +1 -0
  38. package/dist/services/device-manager.js +244 -0
  39. package/dist/services/device-manager.js.map +1 -0
  40. package/dist/services/doctor.d.ts +1 -0
  41. package/dist/services/doctor.d.ts.map +1 -1
  42. package/dist/services/doctor.js +94 -2
  43. package/dist/services/doctor.js.map +1 -1
  44. package/dist/services/processing-queue.d.ts +32 -0
  45. package/dist/services/processing-queue.d.ts.map +1 -0
  46. package/dist/services/processing-queue.js +150 -0
  47. package/dist/services/processing-queue.js.map +1 -0
  48. package/dist/services/screenshot-router.d.ts +31 -0
  49. package/dist/services/screenshot-router.d.ts.map +1 -0
  50. package/dist/services/screenshot-router.js +227 -0
  51. package/dist/services/screenshot-router.js.map +1 -0
  52. package/dist/services/system-requirements.d.ts +26 -0
  53. package/dist/services/system-requirements.d.ts.map +1 -0
  54. package/dist/services/system-requirements.js +189 -0
  55. package/dist/services/system-requirements.js.map +1 -0
  56. package/dist/services/watch-service.d.ts +39 -0
  57. package/dist/services/watch-service.d.ts.map +1 -0
  58. package/dist/services/watch-service.js +293 -0
  59. package/dist/services/watch-service.js.map +1 -0
  60. package/dist/types/device.d.ts +52 -0
  61. package/dist/types/device.d.ts.map +1 -0
  62. package/dist/types/device.js +2 -0
  63. package/dist/types/device.js.map +1 -0
  64. package/dist/types.d.ts +9 -0
  65. package/dist/types.d.ts.map +1 -1
  66. package/dist/utils/pid-manager.d.ts +11 -0
  67. package/dist/utils/pid-manager.d.ts.map +1 -0
  68. package/dist/utils/pid-manager.js +76 -0
  69. package/dist/utils/pid-manager.js.map +1 -0
  70. package/package.json +3 -3
@@ -0,0 +1,150 @@
1
+ import { createHash } from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import pc from 'picocolors';
5
+ export class ProcessingQueue {
6
+ queue = [];
7
+ processing = false;
8
+ processed = new Set(); // Track by hash
9
+ stats = {
10
+ pending: 0,
11
+ processed: 0,
12
+ failed: 0,
13
+ duplicates: 0
14
+ };
15
+ hashCacheFile = '.appshot/processed/hashes.json';
16
+ processor;
17
+ constructor(processor) {
18
+ this.processor = processor;
19
+ this.loadHashCache();
20
+ }
21
+ async add(filepath) {
22
+ // Check if file exists
23
+ try {
24
+ await fs.stat(filepath);
25
+ }
26
+ catch {
27
+ console.warn(pc.yellow(`⚠️ File not found: ${filepath}`));
28
+ return false;
29
+ }
30
+ // Calculate hash
31
+ const hash = await this.hashFile(filepath);
32
+ // Check for duplicates
33
+ if (this.processed.has(hash)) {
34
+ console.log(pc.dim(`⏭️ Skipping duplicate: ${path.basename(filepath)}`));
35
+ this.stats.duplicates++;
36
+ return false;
37
+ }
38
+ // Add to queue
39
+ this.queue.push({
40
+ filepath,
41
+ hash,
42
+ addedAt: new Date(),
43
+ attempts: 0
44
+ });
45
+ this.stats.pending++;
46
+ // Start processing if not already running
47
+ if (!this.processing) {
48
+ this.processNext();
49
+ }
50
+ return true;
51
+ }
52
+ async processNext() {
53
+ if (this.queue.length === 0) {
54
+ this.processing = false;
55
+ await this.saveHashCache();
56
+ return;
57
+ }
58
+ this.processing = true;
59
+ const item = this.queue.shift();
60
+ this.stats.pending--;
61
+ try {
62
+ item.attempts++;
63
+ if (this.processor) {
64
+ await this.processor(item.filepath);
65
+ }
66
+ // Mark as processed
67
+ if (item.hash) {
68
+ this.processed.add(item.hash);
69
+ }
70
+ this.stats.processed++;
71
+ }
72
+ catch (error) {
73
+ console.error(pc.red(`❌ Failed to process ${path.basename(item.filepath)}:`), error);
74
+ // Retry logic
75
+ if (item.attempts < 3) {
76
+ console.log(pc.yellow(` Retrying... (attempt ${item.attempts + 1}/3)`));
77
+ this.queue.push(item);
78
+ this.stats.pending++;
79
+ }
80
+ else {
81
+ console.error(pc.red(' Giving up after 3 attempts'));
82
+ this.stats.failed++;
83
+ }
84
+ }
85
+ // Process next item
86
+ setTimeout(() => this.processNext(), 100);
87
+ }
88
+ async hashFile(filepath) {
89
+ try {
90
+ const buffer = await fs.readFile(filepath);
91
+ return createHash('md5').update(buffer).digest('hex');
92
+ }
93
+ catch {
94
+ // If can't read file, use filepath + size + mtime as hash
95
+ const stat = await fs.stat(filepath);
96
+ return createHash('md5')
97
+ .update(`${filepath}-${stat.size}-${stat.mtime.getTime()}`)
98
+ .digest('hex');
99
+ }
100
+ }
101
+ async isDuplicate(filepath) {
102
+ const hash = await this.hashFile(filepath);
103
+ return this.processed.has(hash);
104
+ }
105
+ async flush() {
106
+ // Process all remaining items immediately
107
+ while (this.queue.length > 0) {
108
+ await new Promise(resolve => setTimeout(resolve, 50));
109
+ }
110
+ // Wait for current processing to complete
111
+ while (this.processing) {
112
+ await new Promise(resolve => setTimeout(resolve, 50));
113
+ }
114
+ await this.saveHashCache();
115
+ }
116
+ async loadHashCache() {
117
+ try {
118
+ const content = await fs.readFile(this.hashCacheFile, 'utf8');
119
+ const hashes = JSON.parse(content);
120
+ hashes.forEach(hash => this.processed.add(hash));
121
+ }
122
+ catch {
123
+ // File doesn't exist or is invalid, start fresh
124
+ }
125
+ }
126
+ async saveHashCache() {
127
+ try {
128
+ const dir = path.dirname(this.hashCacheFile);
129
+ await fs.mkdir(dir, { recursive: true });
130
+ // Keep only last 1000 hashes to prevent unbounded growth
131
+ const hashes = Array.from(this.processed).slice(-1000);
132
+ await fs.writeFile(this.hashCacheFile, JSON.stringify(hashes, null, 2));
133
+ }
134
+ catch (error) {
135
+ console.warn(pc.yellow('⚠️ Failed to save hash cache:'), error);
136
+ }
137
+ }
138
+ getStats() {
139
+ return { ...this.stats };
140
+ }
141
+ getPendingCount() {
142
+ return this.queue.length;
143
+ }
144
+ clear() {
145
+ this.queue = [];
146
+ this.stats.pending = 0;
147
+ this.processing = false;
148
+ }
149
+ }
150
+ //# sourceMappingURL=processing-queue.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"processing-queue.js","sourceRoot":"","sources":["../../src/services/processing-queue.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import { UnifiedDevice, DeviceCategory } from '../types/device.js';
2
+ export interface RoutingOptions {
3
+ strategy: 'smart' | 'manual' | 'strict';
4
+ deleteOriginal: boolean;
5
+ filenamePattern: string;
6
+ overwrite: boolean;
7
+ }
8
+ export interface RouteResult {
9
+ sourcePath: string;
10
+ targetPath: string;
11
+ category: DeviceCategory;
12
+ filename: string;
13
+ }
14
+ export declare class ScreenshotRouter {
15
+ private options;
16
+ private projectRoot;
17
+ private fileCounter;
18
+ constructor(options?: RoutingOptions);
19
+ routeScreenshot(device: UnifiedDevice, screenshotPath: string, screenName?: string): Promise<RouteResult>;
20
+ moveScreenshot(result: RouteResult): Promise<void>;
21
+ routeAndMove(device: UnifiedDevice, screenshotPath: string, screenName?: string): Promise<string>;
22
+ private getProjectDirectory;
23
+ private generateFilename;
24
+ private expandPattern;
25
+ private generateUniqueFilename;
26
+ private fileExists;
27
+ detectCategoryFromDimensions(width: number, height: number): DeviceCategory;
28
+ setProjectRoot(root: string): Promise<void>;
29
+ }
30
+ export declare const screenshotRouter: ScreenshotRouter;
31
+ //# sourceMappingURL=screenshot-router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-router.d.ts","sourceRoot":"","sources":["../../src/services/screenshot-router.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEnE,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACxC,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,gBAAgB;IAIf,OAAO,CAAC,OAAO;IAH3B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,WAAW,CAAkC;gBAEjC,OAAO,GAAE,cAK5B;IAEK,eAAe,CACnB,MAAM,EAAE,aAAa,EACrB,cAAc,EAAE,MAAM,EACtB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,WAAW,CAAC;IA0CjB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBlD,YAAY,CAChB,MAAM,EAAE,aAAa,EACrB,cAAc,EAAE,MAAM,EACtB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC;YAMJ,mBAAmB;YAoEnB,gBAAgB;IAgC9B,OAAO,CAAC,aAAa;YAeP,sBAAsB;YAetB,UAAU;IASxB,4BAA4B,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,cAAc;IA+CrE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGlD;AAED,eAAO,MAAM,gBAAgB,kBAAyB,CAAC"}
@@ -0,0 +1,227 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import pc from 'picocolors';
4
+ export class ScreenshotRouter {
5
+ options;
6
+ projectRoot = null;
7
+ fileCounter = new Map();
8
+ constructor(options = {
9
+ strategy: 'smart',
10
+ deleteOriginal: false,
11
+ filenamePattern: '{screen}-{counter}.png',
12
+ overwrite: false
13
+ }) {
14
+ this.options = options;
15
+ }
16
+ async routeScreenshot(device, screenshotPath, screenName) {
17
+ // 1. Determine category
18
+ const category = device.category;
19
+ // 2. Find or create project directory
20
+ const projectDir = await this.getProjectDirectory();
21
+ // 3. Build target directory path
22
+ const targetDir = path.join(projectDir, 'screenshots', category);
23
+ // 4. Ensure directory exists
24
+ await fs.mkdir(targetDir, { recursive: true });
25
+ // 5. Generate filename
26
+ const filename = await this.generateFilename(device, targetDir, screenName);
27
+ // 6. Build full target path
28
+ const targetPath = path.join(targetDir, filename);
29
+ // 7. Check if file exists and handle accordingly
30
+ if (!this.options.overwrite && await this.fileExists(targetPath)) {
31
+ const newFilename = await this.generateUniqueFilename(targetDir, filename);
32
+ const newTargetPath = path.join(targetDir, newFilename);
33
+ console.log(pc.yellow(`⚠️ File exists, saving as: ${newFilename}`));
34
+ return {
35
+ sourcePath: screenshotPath,
36
+ targetPath: newTargetPath,
37
+ category,
38
+ filename: newFilename
39
+ };
40
+ }
41
+ return {
42
+ sourcePath: screenshotPath,
43
+ targetPath,
44
+ category,
45
+ filename
46
+ };
47
+ }
48
+ async moveScreenshot(result) {
49
+ try {
50
+ // Copy file to target
51
+ await fs.copyFile(result.sourcePath, result.targetPath);
52
+ console.log(pc.green(`✅ Saved to: ${result.targetPath}`));
53
+ // Delete original if configured
54
+ if (this.options.deleteOriginal) {
55
+ await fs.unlink(result.sourcePath);
56
+ console.log(pc.dim(` Removed original: ${result.sourcePath}`));
57
+ }
58
+ }
59
+ catch (error) {
60
+ console.error(pc.red(`❌ Failed to move screenshot: ${error}`));
61
+ throw error;
62
+ }
63
+ }
64
+ async routeAndMove(device, screenshotPath, screenName) {
65
+ const result = await this.routeScreenshot(device, screenshotPath, screenName);
66
+ await this.moveScreenshot(result);
67
+ return result.targetPath;
68
+ }
69
+ async getProjectDirectory() {
70
+ if (this.projectRoot) {
71
+ return this.projectRoot;
72
+ }
73
+ // Strategy 1: Look for .appshot directory
74
+ const cwd = process.cwd();
75
+ let currentDir = cwd;
76
+ while (currentDir !== '/') {
77
+ const appshotDir = path.join(currentDir, '.appshot');
78
+ try {
79
+ const stat = await fs.stat(appshotDir);
80
+ if (stat.isDirectory()) {
81
+ this.projectRoot = currentDir;
82
+ return currentDir;
83
+ }
84
+ }
85
+ catch {
86
+ // Directory doesn't exist, continue searching
87
+ }
88
+ currentDir = path.dirname(currentDir);
89
+ }
90
+ // Strategy 2: Look for package.json or Xcode project
91
+ currentDir = cwd;
92
+ while (currentDir !== '/') {
93
+ const indicators = [
94
+ 'package.json',
95
+ '*.xcodeproj',
96
+ '*.xcworkspace',
97
+ 'pubspec.yaml', // Flutter
98
+ 'app.json' // React Native
99
+ ];
100
+ for (const indicator of indicators) {
101
+ try {
102
+ if (indicator.includes('*')) {
103
+ // Handle glob patterns
104
+ const files = await fs.readdir(currentDir);
105
+ const pattern = indicator.replace('*', '');
106
+ if (files.some(f => f.endsWith(pattern))) {
107
+ this.projectRoot = currentDir;
108
+ return currentDir;
109
+ }
110
+ }
111
+ else {
112
+ // Check for specific file
113
+ await fs.stat(path.join(currentDir, indicator));
114
+ this.projectRoot = currentDir;
115
+ return currentDir;
116
+ }
117
+ }
118
+ catch {
119
+ // File doesn't exist
120
+ }
121
+ }
122
+ currentDir = path.dirname(currentDir);
123
+ }
124
+ // Strategy 3: Use current working directory
125
+ console.log(pc.yellow('⚠️ No project detected, using current directory'));
126
+ this.projectRoot = cwd;
127
+ return cwd;
128
+ }
129
+ async generateFilename(device, targetDir, screenName) {
130
+ const pattern = this.options.filenamePattern;
131
+ // Get or initialize counter for this directory
132
+ const counterKey = targetDir;
133
+ let counter = this.fileCounter.get(counterKey) || 1;
134
+ // Find next available counter
135
+ while (true) {
136
+ const filename = this.expandPattern(pattern, {
137
+ screen: screenName || 'screenshot',
138
+ counter: counter.toString().padStart(3, '0'),
139
+ device: device.name.replace(/[^a-zA-Z0-9]/g, '-'),
140
+ category: device.category,
141
+ timestamp: new Date().toISOString().replace(/[:.]/g, '-')
142
+ });
143
+ const fullPath = path.join(targetDir, filename);
144
+ if (!await this.fileExists(fullPath) || this.options.overwrite) {
145
+ this.fileCounter.set(counterKey, counter + 1);
146
+ return filename;
147
+ }
148
+ counter++;
149
+ }
150
+ }
151
+ expandPattern(pattern, values) {
152
+ let result = pattern;
153
+ for (const [key, value] of Object.entries(values)) {
154
+ result = result.replace(`{${key}}`, value);
155
+ }
156
+ // Ensure .png extension
157
+ if (!result.endsWith('.png')) {
158
+ result += '.png';
159
+ }
160
+ return result;
161
+ }
162
+ async generateUniqueFilename(directory, baseFilename) {
163
+ const ext = path.extname(baseFilename);
164
+ const base = path.basename(baseFilename, ext);
165
+ let counter = 1;
166
+ let newFilename;
167
+ do {
168
+ newFilename = `${base}-${counter}${ext}`;
169
+ counter++;
170
+ } while (await this.fileExists(path.join(directory, newFilename)));
171
+ return newFilename;
172
+ }
173
+ async fileExists(filePath) {
174
+ try {
175
+ await fs.stat(filePath);
176
+ return true;
177
+ }
178
+ catch {
179
+ return false;
180
+ }
181
+ }
182
+ detectCategoryFromDimensions(width, height) {
183
+ // Portrait orientation
184
+ if (height > width) {
185
+ // iPhone dimensions
186
+ if (width >= 750 && width <= 1320 && height >= 1334 && height <= 2868) {
187
+ return 'iphone';
188
+ }
189
+ // iPad dimensions
190
+ if (width >= 1488 && width <= 2064 && height >= 2266 && height <= 2752) {
191
+ return 'ipad';
192
+ }
193
+ // Watch dimensions
194
+ if (width >= 352 && width <= 410 && height >= 430 && height <= 502) {
195
+ return 'watch';
196
+ }
197
+ }
198
+ else {
199
+ // Landscape orientation - swap width/height checks
200
+ if (height >= 750 && height <= 1320 && width >= 1334 && width <= 2868) {
201
+ return 'iphone';
202
+ }
203
+ if (height >= 1488 && height <= 2064 && width >= 2266 && width <= 2752) {
204
+ return 'ipad';
205
+ }
206
+ }
207
+ // Mac dimensions (16:10 aspect ratio)
208
+ if (width >= 2560 && height >= 1600) {
209
+ return 'mac';
210
+ }
211
+ // Vision Pro
212
+ if (width === 3840 && height === 2160) {
213
+ return 'vision';
214
+ }
215
+ // Apple TV
216
+ if ((width === 1920 && height === 1080) || (width === 3840 && height === 2160)) {
217
+ return 'tv';
218
+ }
219
+ // Default to iPhone if unclear
220
+ return 'iphone';
221
+ }
222
+ async setProjectRoot(root) {
223
+ this.projectRoot = root;
224
+ }
225
+ }
226
+ export const screenshotRouter = new ScreenshotRouter();
227
+ //# sourceMappingURL=screenshot-router.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screenshot-router.js","sourceRoot":"","sources":["../../src/services/screenshot-router.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ export interface SystemCheck {
2
+ success: boolean;
3
+ error?: string;
4
+ fix?: string;
5
+ checks?: {
6
+ xcrun: boolean;
7
+ simctl: boolean;
8
+ devicectl: boolean;
9
+ xcodebuild: boolean;
10
+ xcodeVersion: string | null;
11
+ commandLineTools: boolean;
12
+ };
13
+ }
14
+ export interface OptionalTools {
15
+ libimobiledevice: boolean;
16
+ iosDeploy: boolean;
17
+ }
18
+ export declare class SystemRequirements {
19
+ private isMacOS;
20
+ checkXcodeTools(): Promise<SystemCheck>;
21
+ checkOptionalTools(): Promise<OptionalTools>;
22
+ printDiagnostics(): Promise<void>;
23
+ ensureRequirements(): Promise<boolean>;
24
+ }
25
+ export declare const systemRequirements: SystemRequirements;
26
+ //# sourceMappingURL=system-requirements.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"system-requirements.d.ts","sourceRoot":"","sources":["../../src/services/system-requirements.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE;QACP,KAAK,EAAE,OAAO,CAAC;QACf,MAAM,EAAE,OAAO,CAAC;QAChB,SAAS,EAAE,OAAO,CAAC;QACnB,UAAU,EAAE,OAAO,CAAC;QACpB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;QAC5B,gBAAgB,EAAE,OAAO,CAAC;KAC3B,CAAC;CACH;AAED,MAAM,WAAW,aAAa;IAC5B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,OAAO,CAA2B;IAEpC,eAAe,IAAI,OAAO,CAAC,WAAW,CAAC;IAwGvC,kBAAkB,IAAI,OAAO,CAAC,aAAa,CAAC;IA8B5C,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAqCjC,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAC;CA4B7C;AAED,eAAO,MAAM,kBAAkB,oBAA2B,CAAC"}
@@ -0,0 +1,189 @@
1
+ import { exec as execCallback } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { platform } from 'os';
4
+ import pc from 'picocolors';
5
+ const exec = promisify(execCallback);
6
+ export class SystemRequirements {
7
+ isMacOS = platform() === 'darwin';
8
+ async checkXcodeTools() {
9
+ // Only available on macOS
10
+ if (!this.isMacOS) {
11
+ return {
12
+ success: false,
13
+ error: 'Device features are only available on macOS',
14
+ fix: 'Use a Mac to capture screenshots from iOS devices and simulators'
15
+ };
16
+ }
17
+ const checks = {
18
+ xcrun: false,
19
+ simctl: false,
20
+ devicectl: false,
21
+ xcodebuild: false,
22
+ xcodeVersion: null,
23
+ commandLineTools: false
24
+ };
25
+ // Check for Xcode Command Line Tools
26
+ try {
27
+ const { stdout } = await exec('xcode-select -p');
28
+ if (stdout.trim()) {
29
+ checks.commandLineTools = true;
30
+ }
31
+ }
32
+ catch {
33
+ return {
34
+ success: false,
35
+ error: 'Xcode Command Line Tools not installed',
36
+ fix: 'Run: xcode-select --install'
37
+ };
38
+ }
39
+ // Check for xcrun
40
+ try {
41
+ await exec('xcrun --version');
42
+ checks.xcrun = true;
43
+ }
44
+ catch {
45
+ return {
46
+ success: false,
47
+ error: 'xcrun not available',
48
+ fix: 'Ensure Xcode or Command Line Tools are properly installed'
49
+ };
50
+ }
51
+ // Check for simctl (simulator control)
52
+ try {
53
+ await exec('xcrun simctl help');
54
+ checks.simctl = true;
55
+ }
56
+ catch {
57
+ return {
58
+ success: false,
59
+ error: 'simctl not available - simulator features will not work',
60
+ fix: 'Install Xcode from the Mac App Store for full simulator support'
61
+ };
62
+ }
63
+ // Check for devicectl (physical device control, Xcode 15+)
64
+ try {
65
+ await exec('xcrun devicectl --version 2>/dev/null');
66
+ checks.devicectl = true;
67
+ }
68
+ catch {
69
+ // Not fatal - can fallback to libimobiledevice
70
+ console.warn(pc.yellow('⚠️ devicectl not available (requires Xcode 15+)'));
71
+ console.warn(pc.dim(' Physical device support may be limited'));
72
+ }
73
+ // Check for xcodebuild
74
+ try {
75
+ await exec('xcodebuild -version');
76
+ checks.xcodebuild = true;
77
+ }
78
+ catch {
79
+ // Not fatal but limits functionality
80
+ console.warn(pc.yellow('⚠️ xcodebuild not available'));
81
+ }
82
+ // Check Xcode version if available
83
+ if (checks.xcodebuild) {
84
+ try {
85
+ const { stdout } = await exec('xcodebuild -version');
86
+ const match = stdout.match(/Xcode (\d+\.\d+)/);
87
+ if (match) {
88
+ checks.xcodeVersion = match[1];
89
+ const version = parseFloat(match[1]);
90
+ if (version < 14.0) {
91
+ return {
92
+ success: false,
93
+ error: `Xcode ${checks.xcodeVersion} is too old`,
94
+ fix: 'Update to Xcode 14.0 or later from the Mac App Store'
95
+ };
96
+ }
97
+ }
98
+ }
99
+ catch {
100
+ // Ignore version check errors
101
+ }
102
+ }
103
+ return {
104
+ success: true,
105
+ checks
106
+ };
107
+ }
108
+ async checkOptionalTools() {
109
+ const tools = {
110
+ libimobiledevice: false,
111
+ iosDeploy: false
112
+ };
113
+ // Only check on macOS
114
+ if (!this.isMacOS) {
115
+ return tools;
116
+ }
117
+ // Check for libimobiledevice (alternative for physical devices)
118
+ try {
119
+ await exec('which idevicescreenshot');
120
+ tools.libimobiledevice = true;
121
+ }
122
+ catch {
123
+ // Tool not installed - this is optional
124
+ }
125
+ // Check for ios-deploy
126
+ try {
127
+ await exec('which ios-deploy');
128
+ tools.iosDeploy = true;
129
+ }
130
+ catch {
131
+ // Tool not installed - this is optional
132
+ }
133
+ return tools;
134
+ }
135
+ async printDiagnostics() {
136
+ console.log(pc.bold('\n📱 Xcode Tools Check:\n'));
137
+ const xcodeCheck = await this.checkXcodeTools();
138
+ if (xcodeCheck.success && xcodeCheck.checks) {
139
+ console.log(' ' + (xcodeCheck.checks.commandLineTools ? pc.green('✅') : pc.red('❌')) + ' Xcode Command Line Tools');
140
+ console.log(' ' + (xcodeCheck.checks.xcrun ? pc.green('✅') : pc.red('❌')) + ' xcrun');
141
+ console.log(' ' + (xcodeCheck.checks.simctl ? pc.green('✅') : pc.red('❌')) + ' simctl (simulator control)');
142
+ console.log(' ' + (xcodeCheck.checks.devicectl ? pc.green('✅') : pc.yellow('⚠️')) + ' devicectl (physical devices, Xcode 15+)');
143
+ console.log(' ' + (xcodeCheck.checks.xcodebuild ? pc.green('✅') : pc.yellow('⚠️')) + ' xcodebuild');
144
+ if (xcodeCheck.checks.xcodeVersion) {
145
+ console.log(` ${pc.green('✅')} Xcode version: ${xcodeCheck.checks.xcodeVersion}`);
146
+ }
147
+ }
148
+ else {
149
+ console.log(pc.red(` ❌ ${xcodeCheck.error}`));
150
+ console.log(pc.cyan(` Fix: ${xcodeCheck.fix}`));
151
+ }
152
+ console.log(pc.bold('\n📦 Optional Tools:\n'));
153
+ const optional = await this.checkOptionalTools();
154
+ console.log(' ' + (optional.libimobiledevice ? pc.green('✅') : pc.yellow('⚠️')) + ' libimobiledevice');
155
+ if (!optional.libimobiledevice) {
156
+ console.log(pc.dim(' Install: brew install libimobiledevice'));
157
+ console.log(pc.dim(' Provides: idevicescreenshot for physical devices'));
158
+ }
159
+ console.log(' ' + (optional.iosDeploy ? pc.green('✅') : pc.yellow('⚠️')) + ' ios-deploy');
160
+ if (!optional.iosDeploy) {
161
+ console.log(pc.dim(' Install: npm install -g ios-deploy'));
162
+ console.log(pc.dim(' Provides: App deployment to physical devices'));
163
+ }
164
+ }
165
+ async ensureRequirements() {
166
+ const check = await this.checkXcodeTools();
167
+ if (!check.success) {
168
+ console.error(pc.red('\n❌ System requirements not met'));
169
+ console.error(pc.yellow(`\nError: ${check.error}`));
170
+ console.error(pc.cyan(`\nHow to fix:\n ${check.fix}`));
171
+ if (check.error?.includes('Command Line Tools')) {
172
+ console.error(pc.dim('\nAfter installation, you may need to:'));
173
+ console.error(pc.dim(' 1. Restart your terminal'));
174
+ console.error(pc.dim(' 2. Accept the license: sudo xcodebuild -license accept'));
175
+ }
176
+ return false;
177
+ }
178
+ // Check for optional tools and provide helpful messages
179
+ const optional = await this.checkOptionalTools();
180
+ if (!optional.libimobiledevice && (!check.checks?.devicectl)) {
181
+ console.warn(pc.yellow('\n⚠️ Limited physical device support'));
182
+ console.warn(pc.dim(' For physical device screenshots, install:'));
183
+ console.warn(pc.cyan(' brew install libimobiledevice'));
184
+ }
185
+ return true;
186
+ }
187
+ }
188
+ export const systemRequirements = new SystemRequirements();
189
+ //# sourceMappingURL=system-requirements.js.map