appshot-cli 0.8.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.
- package/README.md +96 -0
- package/dist/cli.js +18 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/device.d.ts +3 -0
- package/dist/commands/device.d.ts.map +1 -0
- package/dist/commands/device.js +414 -0
- package/dist/commands/device.js.map +1 -0
- package/dist/commands/unwatch.d.ts +3 -0
- package/dist/commands/unwatch.d.ts.map +1 -0
- package/dist/commands/unwatch.js +53 -0
- package/dist/commands/unwatch.js.map +1 -0
- package/dist/commands/watch-status.d.ts +3 -0
- package/dist/commands/watch-status.d.ts.map +1 -0
- package/dist/commands/watch-status.js +116 -0
- package/dist/commands/watch-status.js.map +1 -0
- package/dist/commands/watch.d.ts +3 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +322 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/core/compose.d.ts.map +1 -1
- package/dist/core/compose.js +4 -0
- package/dist/core/compose.js.map +1 -1
- package/dist/services/compose-bridge.d.ts +47 -0
- package/dist/services/compose-bridge.d.ts.map +1 -0
- package/dist/services/compose-bridge.js +222 -0
- package/dist/services/compose-bridge.js.map +1 -0
- package/dist/services/device-manager.d.ts +14 -0
- package/dist/services/device-manager.d.ts.map +1 -0
- package/dist/services/device-manager.js +244 -0
- package/dist/services/device-manager.js.map +1 -0
- package/dist/services/doctor.d.ts +1 -0
- package/dist/services/doctor.d.ts.map +1 -1
- package/dist/services/doctor.js +94 -2
- package/dist/services/doctor.js.map +1 -1
- package/dist/services/processing-queue.d.ts +32 -0
- package/dist/services/processing-queue.d.ts.map +1 -0
- package/dist/services/processing-queue.js +150 -0
- package/dist/services/processing-queue.js.map +1 -0
- package/dist/services/screenshot-router.d.ts +31 -0
- package/dist/services/screenshot-router.d.ts.map +1 -0
- package/dist/services/screenshot-router.js +227 -0
- package/dist/services/screenshot-router.js.map +1 -0
- package/dist/services/system-requirements.d.ts +26 -0
- package/dist/services/system-requirements.d.ts.map +1 -0
- package/dist/services/system-requirements.js +189 -0
- package/dist/services/system-requirements.js.map +1 -0
- package/dist/services/watch-service.d.ts +39 -0
- package/dist/services/watch-service.d.ts.map +1 -0
- package/dist/services/watch-service.js +293 -0
- package/dist/services/watch-service.js.map +1 -0
- package/dist/types/device.d.ts +52 -0
- package/dist/types/device.d.ts.map +1 -0
- package/dist/types/device.js +2 -0
- package/dist/types/device.js.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/pid-manager.d.ts +11 -0
- package/dist/utils/pid-manager.d.ts.map +1 -0
- package/dist/utils/pid-manager.js +76 -0
- package/dist/utils/pid-manager.js.map +1 -0
- 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
|