budexp 0.1.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,377 @@
1
+ const { execFileSync, execSync } = require('child_process');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+ const logger = require('./logger');
6
+
7
+ function askQuestion(question) {
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ });
12
+
13
+ return new Promise((resolve) => {
14
+ rl.question(question, (answer) => {
15
+ rl.close();
16
+ resolve(answer.trim().toLowerCase());
17
+ });
18
+ });
19
+ }
20
+
21
+ function isValidBundleId(bundleId) {
22
+ return /^[a-zA-Z][a-zA-Z0-9_]*(\.[a-zA-Z][a-zA-Z0-9_]*)+$/.test(bundleId);
23
+ }
24
+
25
+ function removeFilesByPrefix(directory, prefixes) {
26
+ if (!directory || !fs.existsSync(directory)) return;
27
+
28
+ let entries = [];
29
+ try {
30
+ entries = fs.readdirSync(directory);
31
+ } catch (e) {
32
+ return;
33
+ }
34
+
35
+ for (const entry of entries) {
36
+ if (!prefixes.some((prefix) => entry.startsWith(prefix))) continue;
37
+
38
+ try {
39
+ fs.removeSync(path.join(directory, entry));
40
+ } catch (e) {
41
+ // Ignore individual cache removal errors.
42
+ }
43
+ }
44
+ }
45
+
46
+ function tryExecFile(command, args, options = {}) {
47
+ try {
48
+ execFileSync(command, args, options);
49
+ return true;
50
+ } catch (e) {
51
+ return false;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Kill running applications
57
+ */
58
+ async function killRunningApps(bundleId, platform) {
59
+ logger.step('Checking for running applications...');
60
+
61
+ if (platform === 'android' || platform === 'all') {
62
+ try {
63
+ if (commandExists('adb')) {
64
+ const devices = execFileSync('adb', ['devices'], { encoding: 'utf8' });
65
+ const deviceCount = (devices.match(/device$/gm) || []).length;
66
+
67
+ if (deviceCount > 0 && bundleId) {
68
+ try {
69
+ if (isValidBundleId(bundleId)) {
70
+ execFileSync('adb', ['shell', 'am', 'force-stop', bundleId], { stdio: 'ignore' });
71
+ logger.success('Stopped Android app');
72
+ } else {
73
+ logger.warning('Invalid bundle identifier, skipping Android app kill');
74
+ }
75
+ } catch (e) {
76
+ logger.warning('App might not be running on device');
77
+ }
78
+ }
79
+ }
80
+ } catch (e) {
81
+ logger.warning('ADB not available, skipping Android app kill');
82
+ }
83
+ }
84
+
85
+ // Kill Metro bundler if running
86
+ try {
87
+ if (process.platform === 'darwin') {
88
+ tryExecFile('pkill', ['-f', 'react-native'], { stdio: 'ignore' });
89
+ tryExecFile('pkill', ['-f', 'metro'], { stdio: 'ignore' });
90
+ } else if (process.platform === 'win32') {
91
+ execSync('taskkill /F /IM node.exe /FI "WINDOWTITLE eq *metro*" 2>nul || true', {
92
+ stdio: 'ignore',
93
+ });
94
+ } else {
95
+ tryExecFile('pkill', ['-f', 'react-native'], { stdio: 'ignore' });
96
+ tryExecFile('pkill', ['-f', 'metro'], { stdio: 'ignore' });
97
+ }
98
+ logger.success('Killed running Metro/React Native processes');
99
+ } catch (e) {
100
+ // Ignore errors
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Clean watchman cache
106
+ */
107
+ async function cleanWatchman() {
108
+ logger.step('Cleaning watchman cache...');
109
+ try {
110
+ if (commandExists('watchman')) {
111
+ execFileSync('watchman', ['watch-del-all'], { stdio: 'ignore' });
112
+ logger.success('Watchman cache cleared');
113
+ } else {
114
+ logger.warning('Watchman not found, skipping...');
115
+ }
116
+ } catch (e) {
117
+ logger.warning('Could not clean watchman cache');
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Clean Metro and Expo caches
123
+ */
124
+ async function cleanCaches() {
125
+ logger.step('Clearing Metro bundler and Expo caches...');
126
+
127
+ const tmpDir =
128
+ process.env.TMPDIR ||
129
+ process.env.TMP ||
130
+ (process.platform === 'win32' ? process.env.TEMP : '/tmp');
131
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
132
+
133
+ removeFilesByPrefix(tmpDir, ['metro-', 'haste-map-', 'react-', 'react-native-packager-cache-']);
134
+
135
+ // Clear Expo cache in home directory
136
+ const expoCachePath = path.join(homeDir, '.expo', 'metro-cache');
137
+ if (fs.existsSync(expoCachePath)) {
138
+ try {
139
+ fs.removeSync(expoCachePath);
140
+ } catch (e) {
141
+ // Ignore errors
142
+ }
143
+ }
144
+
145
+ // Clear .expo in project directory
146
+ if (fs.existsSync('.expo')) {
147
+ try {
148
+ fs.removeSync('.expo');
149
+ } catch (e) {
150
+ // Ignore errors
151
+ }
152
+ }
153
+
154
+ logger.success('All Metro and Expo caches cleared');
155
+ }
156
+
157
+ /**
158
+ * Delete native folders
159
+ */
160
+ async function deleteNativeFolders(platform) {
161
+ logger.step('Removing native folders...');
162
+
163
+ const foldersToRemove = [];
164
+
165
+ if (platform === 'android' || platform === 'all') {
166
+ foldersToRemove.push('android');
167
+ }
168
+
169
+ if (platform === 'ios' || platform === 'all') {
170
+ foldersToRemove.push('ios');
171
+ }
172
+
173
+ for (const folder of foldersToRemove) {
174
+ if (fs.existsSync(folder)) {
175
+ fs.removeSync(folder);
176
+ logger.success(`Removed ${folder}/ folder`);
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Clean node_modules and optionally lock files
183
+ */
184
+ async function cleanDependencies() {
185
+ logger.step('Removing node_modules...');
186
+
187
+ if (fs.existsSync('node_modules')) {
188
+ try {
189
+ fs.removeSync('node_modules');
190
+ // Verify it was removed
191
+ if (!fs.existsSync('node_modules')) {
192
+ logger.success('Removed node_modules');
193
+ } else {
194
+ throw new Error('node_modules still exists after removal attempt');
195
+ }
196
+ } catch (e) {
197
+ // Fallback: try fs-extra
198
+ logger.warning('Shell removal failed, trying alternative method...');
199
+ try {
200
+ fs.removeSync('node_modules');
201
+ if (!fs.existsSync('node_modules')) {
202
+ logger.success('Removed node_modules');
203
+ } else {
204
+ throw new Error('node_modules still exists');
205
+ }
206
+ } catch (e2) {
207
+ logger.error('Could not remove node_modules. This might be due to:');
208
+ logger.error(' - Files are locked by another process');
209
+ logger.error(' - Permission issues');
210
+ logger.error(' - Directory is too large');
211
+ logger.warning('Please try removing node_modules manually: rm -rf node_modules');
212
+ // Don't throw - allow the process to continue
213
+ logger.warning('Continuing with other cleanup operations...');
214
+ }
215
+ }
216
+ }
217
+
218
+ const lockFiles = ['package-lock.json', 'yarn.lock', 'bun.lockb', 'pnpm-lock.yaml'];
219
+ const existingLocks = lockFiles.filter((lockFile) => fs.existsSync(lockFile));
220
+
221
+ if (existingLocks.length > 0) {
222
+ logger.warning('Lock files detected:');
223
+ existingLocks.forEach((file) => logger.info(` - ${file}`));
224
+ logger.info('');
225
+ logger.info('Keeping lock files preserves deterministic installs and reduces version drift.');
226
+ logger.info(
227
+ 'Removing lock files can install newer transitive versions and potentially resolve stale lock issues.'
228
+ );
229
+ logger.info('');
230
+
231
+ const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
232
+ if (isInteractive) {
233
+ const answer = await askQuestion('Do you want to delete lock files? (y/N): ');
234
+ if (answer === 'y' || answer === 'yes') {
235
+ for (const file of existingLocks) {
236
+ try {
237
+ fs.removeSync(file);
238
+ logger.success(`Removed ${file}`);
239
+ } catch (e) {
240
+ logger.warning(`Could not remove ${file}`);
241
+ }
242
+ }
243
+ } else {
244
+ logger.info('Keeping lock files for deterministic installs.');
245
+ }
246
+ } else {
247
+ logger.info('Non-interactive shell detected. Keeping lock files by default.');
248
+ }
249
+ }
250
+
251
+ if (fs.existsSync('.expo')) {
252
+ try {
253
+ fs.removeSync('.expo');
254
+ logger.success('Removed .expo cache');
255
+ } catch (e) {
256
+ logger.warning('Could not remove .expo cache');
257
+ }
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Reinstall dependencies
263
+ */
264
+ async function reinstallDependencies() {
265
+ logger.step('Reinstalling dependencies...');
266
+
267
+ // Detect package manager
268
+ let packageManager = 'npm';
269
+ if (commandExists('bun')) {
270
+ packageManager = 'bun';
271
+ } else if (commandExists('yarn')) {
272
+ packageManager = 'yarn';
273
+ } else if (commandExists('pnpm')) {
274
+ packageManager = 'pnpm';
275
+ }
276
+
277
+ try {
278
+ execFileSync(packageManager, ['install'], { stdio: 'inherit' });
279
+ logger.success('Dependencies installed');
280
+ } catch (e) {
281
+ logger.error('Failed to install dependencies');
282
+ throw e;
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Rebuild native code
288
+ */
289
+ async function rebuildNative(platform) {
290
+ logger.step('Rebuilding native code from scratch...');
291
+
292
+ try {
293
+ if (platform === 'all') {
294
+ execFileSync('npx', ['expo', 'prebuild', '--clean'], { stdio: 'inherit' });
295
+ } else {
296
+ execFileSync('npx', ['expo', 'prebuild', '--clean', '--platform', platform], {
297
+ stdio: 'inherit',
298
+ });
299
+ }
300
+ logger.success('Native code rebuilt');
301
+ } catch (e) {
302
+ logger.error('Failed to rebuild native code');
303
+ throw e;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Check if command exists
309
+ */
310
+ function commandExists(command) {
311
+ try {
312
+ const lookupCommand = process.platform === 'win32' ? 'where' : 'which';
313
+ execFileSync(lookupCommand, [command], { stdio: 'ignore' });
314
+ return true;
315
+ } catch (e) {
316
+ return false;
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Get bundle ID from app config
322
+ */
323
+ async function getBundleId() {
324
+ try {
325
+ // Try app.json first
326
+ if (fs.existsSync('app.json')) {
327
+ const config = fs.readJsonSync('app.json');
328
+ return config.expo?.android?.package || config.expo?.ios?.bundleIdentifier || null;
329
+ }
330
+
331
+ // Try app.config.js
332
+ if (fs.existsSync('app.config.js')) {
333
+ try {
334
+ // Use require to load the config (works for both .js and .ts)
335
+ delete require.cache[require.resolve(path.join(process.cwd(), 'app.config.js'))];
336
+ const config = require(path.join(process.cwd(), 'app.config.js'));
337
+ const expoConfig = config.default || config;
338
+ return (
339
+ expoConfig?.expo?.android?.package || expoConfig?.expo?.ios?.bundleIdentifier || null
340
+ );
341
+ } catch (e) {
342
+ // If require fails, return null
343
+ return null;
344
+ }
345
+ }
346
+
347
+ // Try app.config.ts
348
+ if (fs.existsSync('app.config.ts')) {
349
+ try {
350
+ // TypeScript configs need to be compiled, but we can try
351
+ delete require.cache[require.resolve(path.join(process.cwd(), 'app.config.ts'))];
352
+ const config = require(path.join(process.cwd(), 'app.config.ts'));
353
+ const expoConfig = config.default || config;
354
+ return (
355
+ expoConfig?.expo?.android?.package || expoConfig?.expo?.ios?.bundleIdentifier || null
356
+ );
357
+ } catch (e) {
358
+ return null;
359
+ }
360
+ }
361
+
362
+ return null;
363
+ } catch (e) {
364
+ return null;
365
+ }
366
+ }
367
+
368
+ module.exports = {
369
+ killRunningApps,
370
+ cleanWatchman,
371
+ cleanCaches,
372
+ deleteNativeFolders,
373
+ cleanDependencies,
374
+ reinstallDependencies,
375
+ rebuildNative,
376
+ getBundleId,
377
+ };
@@ -0,0 +1,198 @@
1
+ // ============================================
2
+ // FILE: src/utils/eas.js (Enhanced)
3
+ // ============================================
4
+ const { execFileSync } = require('child_process');
5
+ const logger = require('./logger');
6
+
7
+ function validatePlatform(platform) {
8
+ if (!['android', 'ios'].includes(platform)) {
9
+ throw new Error(`Invalid platform: ${platform}`);
10
+ }
11
+ }
12
+
13
+ function validateBuildId(buildId) {
14
+ if (typeof buildId !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(buildId)) {
15
+ throw new Error('Invalid build ID. Use only letters, numbers, underscores, and hyphens.');
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Check EAS login status
21
+ */
22
+ async function checkEASLogin() {
23
+ logger.step('Checking EAS login status...');
24
+
25
+ try {
26
+ const output = execFileSync('eas', ['whoami'], { encoding: 'utf8', stdio: 'pipe' });
27
+ const email = output.trim();
28
+
29
+ if (email && !email.includes('error') && !email.includes('not logged')) {
30
+ logger.success(`Logged in as: ${email}`);
31
+ return {
32
+ loggedIn: true,
33
+ email,
34
+ };
35
+ }
36
+ } catch (e) {
37
+ // Not logged in or error
38
+ }
39
+
40
+ logger.warning('Not logged in to EAS');
41
+ return {
42
+ loggedIn: false,
43
+ email: null,
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Get EAS configuration
49
+ */
50
+ async function getEASConfig() {
51
+ logger.step('Reading EAS configuration...');
52
+
53
+ try {
54
+ const fs = require('fs-extra');
55
+ const path = require('path');
56
+
57
+ const easConfigPath = path.join(process.cwd(), 'eas.json');
58
+
59
+ if (!fs.existsSync(easConfigPath)) {
60
+ logger.warning('eas.json not found');
61
+ return null;
62
+ }
63
+
64
+ const config = fs.readJsonSync(easConfigPath);
65
+ logger.success('EAS configuration loaded');
66
+
67
+ return config;
68
+ } catch (e) {
69
+ logger.error('Failed to read EAS configuration');
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Display EAS status information
76
+ */
77
+ async function displayEASStatus() {
78
+ const loginStatus = await checkEASLogin();
79
+ const config = await getEASConfig();
80
+
81
+ console.log('\n' + '='.repeat(50));
82
+ console.log('EAS Status');
83
+ console.log('='.repeat(50));
84
+
85
+ console.log(`\nLogin Status: ${loginStatus.loggedIn ? '✔ Logged in' : '✖ Not logged in'}`);
86
+ if (loginStatus.email) {
87
+ console.log(`Email: ${loginStatus.email}`);
88
+ }
89
+
90
+ if (config) {
91
+ console.log('\nBuild Profiles:');
92
+ const profiles = Object.keys(config.build || {});
93
+ if (profiles.length > 0) {
94
+ profiles.forEach((profile) => {
95
+ console.log(` - ${profile}`);
96
+ const profileConfig = config.build[profile];
97
+ if (profileConfig.platform) {
98
+ console.log(
99
+ ` Platform: ${Array.isArray(profileConfig.platform) ? profileConfig.platform.join(', ') : profileConfig.platform}`
100
+ );
101
+ }
102
+ });
103
+ } else {
104
+ console.log(' No build profiles found');
105
+ }
106
+
107
+ if (config.submit) {
108
+ console.log('\nSubmit Configuration:');
109
+ Object.keys(config.submit).forEach((platform) => {
110
+ console.log(` ${platform}: configured`);
111
+ });
112
+ }
113
+ } else {
114
+ console.log('\nNo EAS configuration found');
115
+ }
116
+
117
+ console.log('\n' + '='.repeat(50) + '\n');
118
+
119
+ return {
120
+ loginStatus,
121
+ config,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * List recent EAS builds
127
+ */
128
+ async function listEASBuilds() {
129
+ logger.step('Fetching recent EAS builds...');
130
+
131
+ try {
132
+ console.log('');
133
+ execFileSync('eas', ['build:list', '--limit', '20'], { stdio: 'inherit' });
134
+ console.log('');
135
+ } catch (e) {
136
+ logger.error('Failed to fetch builds. Make sure you are logged in to EAS.');
137
+ logger.info('Run: eas login');
138
+ }
139
+ }
140
+
141
+ /**
142
+ * View specific EAS build details
143
+ */
144
+ async function viewEASBuild(buildId) {
145
+ logger.step(`Fetching build details for: ${buildId}`);
146
+
147
+ try {
148
+ validateBuildId(buildId);
149
+ console.log('');
150
+ execFileSync('eas', ['build:view', buildId], { stdio: 'inherit' });
151
+ console.log('');
152
+ } catch (e) {
153
+ logger.error(`Failed to fetch build ${buildId}`);
154
+ logger.info('Make sure the build ID is correct and you have access to it.');
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Display EAS project information
160
+ */
161
+ async function displayEASProjectInfo() {
162
+ logger.step('Fetching EAS project information...');
163
+
164
+ try {
165
+ console.log('');
166
+ execFileSync('eas', ['project:info'], { stdio: 'inherit' });
167
+ console.log('');
168
+ } catch (e) {
169
+ logger.error('Failed to fetch project info');
170
+ logger.info('Make sure you are logged in and the project is configured.');
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Submit build to EAS
176
+ */
177
+ async function submitToEAS(platform) {
178
+ logger.step(`Submitting ${platform} build to EAS...`);
179
+
180
+ try {
181
+ validatePlatform(platform);
182
+ execFileSync('eas', ['submit', '--platform', platform], { stdio: 'inherit' });
183
+ logger.success('Build submitted successfully');
184
+ } catch (e) {
185
+ logger.error('Failed to submit build to EAS');
186
+ throw e;
187
+ }
188
+ }
189
+
190
+ module.exports = {
191
+ checkEASLogin,
192
+ getEASConfig,
193
+ displayEASStatus,
194
+ listEASBuilds,
195
+ viewEASBuild,
196
+ displayEASProjectInfo,
197
+ submitToEAS,
198
+ };