From 1efaecfa28a1fce8393362c7ecb85a85d223b8d8 Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Wed, 23 Jul 2025 12:52:51 +0300 Subject: [PATCH 01/11] feat: create instabug CLI with --help flag --- bin/instabug.dart | 14 ++++++++++++++ pubspec.yaml | 3 +++ 2 files changed, 17 insertions(+) create mode 100755 bin/instabug.dart diff --git a/bin/instabug.dart b/bin/instabug.dart new file mode 100755 index 000000000..1451b0e3a --- /dev/null +++ b/bin/instabug.dart @@ -0,0 +1,14 @@ +#!/usr/bin/env dart + +import 'package:args/args.dart'; + +void main(List args) { + final parser = ArgParser()..addFlag('help', abbr: 'h'); + final result = parser.parse(args); + if (result['help'] as bool) { + print('Usage: instabug [options]'); + print(parser.usage); + return; + } + // …call into your SDK’s API… +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 86884fb71..2d5fa7b38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,3 +37,6 @@ flutter: environment: sdk: ">=2.14.0 <4.0.0" flutter: ">=1.17.0" + +executables: + instabug: instabug From cdc288f97720dc65e94ed9853ecaefe1cd2567f6 Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Wed, 23 Jul 2025 19:55:30 +0300 Subject: [PATCH 02/11] feat: add upload command to instabug CLI - Introduced a new command `upload-so-files` for uploading .so files with architecture and API key options. - Enhanced the main CLI to support command registration and help functionality. - Updated dependencies in `pubspec.yaml` for `args` and `http`. --- bin/commands/upload_so_files.dart | 140 ++++++++++++++++++++++++++++++ bin/instabug.dart | 77 ++++++++++++++-- example/pubspec.lock | 94 +++++++++++--------- pubspec.yaml | 2 + 4 files changed, 263 insertions(+), 50 deletions(-) create mode 100644 bin/commands/upload_so_files.dart diff --git a/bin/commands/upload_so_files.dart b/bin/commands/upload_so_files.dart new file mode 100644 index 000000000..834c6d978 --- /dev/null +++ b/bin/commands/upload_so_files.dart @@ -0,0 +1,140 @@ +import 'dart:developer'; +import 'dart:io'; +import 'package:args/args.dart'; +import 'package:http/http.dart' as http; + +/** + * This script uploads .so files to the specified endpoint used in NDK crash reporting. + * Usage: dart instabug.dart upload-so-files --arch --file --api_key --token --name + */ + +class UploadSoFilesOptions { + final String arch; + final String file; + final String apiKey; + final String token; + final String name; + + UploadSoFilesOptions({ + required this.arch, + required this.file, + required this.apiKey, + required this.token, + required this.name, + }); +} + +// ignore: avoid_classes_with_only_static_members +class UploadSoFilesCommand { + static const List validArchs = [ + 'x86', + 'x86_64', + 'arm64-v8a', + 'armeabi-v7a', + ]; + + static ArgParser createParser() { + final parser = ArgParser() + ..addFlag('help', abbr: 'h', help: 'Show this help message') + ..addOption( + 'arch', + abbr: 'a', + help: 'Architecture', + allowed: validArchs, + mandatory: true, + ) + ..addOption( + 'file', + abbr: 'f', + help: 'The path of the symbol files in Zip format', + mandatory: true, + ) + ..addOption( + 'api_key', + help: 'Your App key', + mandatory: true, + ) + ..addOption( + 'token', + abbr: 't', + help: 'Your App Token', + mandatory: true, + ) + ..addOption( + 'name', + abbr: 'n', + help: 'The app version name', + mandatory: true, + ); + + return parser; + } + + static void execute(ArgResults results) { + final options = UploadSoFilesOptions( + arch: results['arch'] as String, + file: results['file'] as String, + apiKey: results['api_key'] as String, + token: results['token'] as String, + name: results['name'] as String, + ); + + uploadSoFiles(options); + } + + static Future uploadSoFiles(UploadSoFilesOptions options) async { + try { + // Validate file exists + final file = File(options.file); + if (!await file.exists()) { + print('[Instabug-CLI] Error: File not found: ${options.file}'); + throw Exception('File not found: ${options.file}'); + } + + // validate file is a zip file + if (!file.path.endsWith('.zip')) { + print('[Instabug-CLI] Error: File is not a zip file: ${options.file}'); + throw Exception('File is not a zip file: ${options.file}'); + } + + // Validate architecture + if (!validArchs.contains(options.arch)) { + print('[Instabug-CLI] Error: Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}'); + throw Exception( + 'Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}'); + } + + print('Uploading .so files...'); + print('Architecture: ${options.arch}'); + print('File: ${options.file}'); + print('App Version: ${options.name}'); + + // TODO: Implement the actual upload logic here + // This would typically involve: + // 1. Reading the zip file + // 2. Making an HTTP request to the upload endpoint + // 3. Handling the response + + // Make an HTTP request to the upload endpoint + final body = { + 'arch': options.arch, + 'api_key': options.apiKey, + 'application_token': options.token, + 'so_file': options.file, + 'app_version': options.name, + }; + + const endPoint = 'https://api.instabug.com/api/web/public/so_files'; + + final response = await http.post( + Uri.parse(endPoint), + body: body, + ); + + print('Successfully uploaded .so files for version: ${options.name} with arch ${options.arch}'); + } catch (e) { + print('[Instabug-CLI] Error: Error uploading .so files: $e'); + exit(1); + } + } +} diff --git a/bin/instabug.dart b/bin/instabug.dart index 1451b0e3a..7e89c38bc 100755 --- a/bin/instabug.dart +++ b/bin/instabug.dart @@ -1,14 +1,77 @@ #!/usr/bin/env dart +import 'dart:developer'; +import 'dart:io'; + import 'package:args/args.dart'; -void main(List args) { +import 'commands/upload_so_files.dart'; + +// Command registry for easy management +class CommandRegistry { + static final Map _commands = { + 'upload-so-files': CommandHandler( + parser: UploadSoFilesCommand.createParser(), + execute: UploadSoFilesCommand.execute, + ), + }; + + static Map get commands => _commands; + static List get commandNames => _commands.keys.toList(); +} + +class CommandHandler { + final ArgParser parser; + final Function(ArgResults) execute; + + CommandHandler({required this.parser, required this.execute}); +} + +void main(List args) async { final parser = ArgParser()..addFlag('help', abbr: 'h'); - final result = parser.parse(args); - if (result['help'] as bool) { - print('Usage: instabug [options]'); + + // Add all commands to the parser + for (final entry in CommandRegistry.commands.entries) { + parser.addCommand(entry.key, entry.value.parser); + } + + print('--------------------------------'); + + try { + final result = parser.parse(args); + + final command = result.command; + if (command != null) { + // Check if help is requested for the subcommand (before mandatory validation) + if (command['help'] == true) { + final commandHandler = CommandRegistry.commands[command.name]; + if (commandHandler != null) { + print('Usage: instabug ${command.name} [options]'); + print(commandHandler.parser.usage); + } + return; + } + + final commandHandler = CommandRegistry.commands[command.name]; + // Extra safety check just in case + if (commandHandler != null) { + commandHandler.execute(command); + } else { + print('Unknown command: ${command.name}'); + print('Available commands: ${CommandRegistry.commandNames.join(', ')}'); + exit(1); + } + } else { + print('No applicable command found'); + print('Usage: instabug [options] '); + print('Available commands: ${CommandRegistry.commandNames.join(', ')}'); + print('\nFor help on a specific command:'); + print(' instabug --help\n'); + print(parser.usage); + } + } catch (e) { + print('[Instabug-CLI] Error: $e'); print(parser.usage); - return; + exit(1); } - // …call into your SDK’s API… -} \ No newline at end of file +} diff --git a/example/pubspec.lock b/example/pubspec.lock index 99e8f9d56..c04ce3b1b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -1,62 +1,70 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.19.1" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -120,18 +128,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.7" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.8" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -152,10 +160,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -168,34 +176,34 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" path: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" process: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.3" sky_engine: dependency: transitive description: flutter @@ -205,34 +213,34 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.1" sync_http: dependency: transitive description: @@ -245,18 +253,18 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" typed_data: dependency: transitive description: @@ -277,10 +285,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.3.0" + version: "14.3.1" webdriver: dependency: transitive description: @@ -290,5 +298,5 @@ packages: source: hosted version: "3.0.4" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index 2d5fa7b38..635afdc17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,8 +8,10 @@ repository: https://github.com/Instabug/Instabug-Flutter documentation: https://docs.instabug.com/docs/flutter-overview dependencies: + args: ^2.4.0 flutter: sdk: flutter + http: ^0.13.6 meta: ^1.3.0 stack_trace: ^1.10.0 From 3ea25faed842271bbc18752b801787caa7cd5d3e Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Wed, 23 Jul 2025 19:58:03 +0300 Subject: [PATCH 03/11] chore: update .gitignore to include Android C++ build files - Added `android/app/.cxx/` to ignore C++ build artifacts for Android. --- example/.gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/example/.gitignore b/example/.gitignore index 9d532b18a..81d19e2e5 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -39,3 +39,6 @@ app.*.symbols # Obfuscation related app.*.map.json + +# Android related +android/app/.cxx/ From a22fda19a17ea6e53b1e51bf241efd2b65ce7a5a Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Thu, 24 Jul 2025 03:36:04 +0300 Subject: [PATCH 04/11] feat: refactor HTTP request handling in instabug CLI - Removed `http` dependency from `pubspec.yaml`. - Added a new `makeHttpPostRequest` function in `bin/instabug.dart` for handling HTTP POST requests. - Updated `upload_so_files.dart` to utilize the new HTTP request function for uploading .so files. --- bin/commands/upload_so_files.dart | 11 ++++------ bin/instabug.dart | 34 +++++++++++++++++++++++++++++-- pubspec.yaml | 1 - 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/bin/commands/upload_so_files.dart b/bin/commands/upload_so_files.dart index 834c6d978..d4f4ace3a 100644 --- a/bin/commands/upload_so_files.dart +++ b/bin/commands/upload_so_files.dart @@ -1,11 +1,8 @@ -import 'dart:developer'; -import 'dart:io'; -import 'package:args/args.dart'; -import 'package:http/http.dart' as http; +part of '../instabug.dart'; /** * This script uploads .so files to the specified endpoint used in NDK crash reporting. - * Usage: dart instabug.dart upload-so-files --arch --file --api_key --token --name + * Usage: dart run instabug_flutter:instabug upload-so-files --arch --file --api_key --token --name */ class UploadSoFilesOptions { @@ -126,8 +123,8 @@ class UploadSoFilesCommand { const endPoint = 'https://api.instabug.com/api/web/public/so_files'; - final response = await http.post( - Uri.parse(endPoint), + final response = await makeHttpPostRequest( + url: endPoint, body: body, ); diff --git a/bin/instabug.dart b/bin/instabug.dart index 7e89c38bc..37becf1c4 100755 --- a/bin/instabug.dart +++ b/bin/instabug.dart @@ -1,11 +1,11 @@ #!/usr/bin/env dart -import 'dart:developer'; +import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; -import 'commands/upload_so_files.dart'; +part 'commands/upload_so_files.dart'; // Command registry for easy management class CommandRegistry { @@ -27,6 +27,36 @@ class CommandHandler { CommandHandler({required this.parser, required this.execute}); } +Future makeHttpPostRequest({ + required String url, + required Map body, + Map? headers, +}) async { + try { + final client = HttpClient(); + + final request = await client.postUrl(Uri.parse(url)); + + request.headers.contentType = ContentType.json; + + request.write(jsonEncode(body)); + + final response = await request.close(); + + if (response.statusCode >= 200 && response.statusCode < 300) { + final responseBody = await response.transform(utf8.decoder).join(); + return true; + } else { + print('Error: ${response.statusCode}'); + return false; + } + + } catch (e) { + print('[Instabug-CLI] Error while making HTTP POST request: $e'); + exit(1); + } +} + void main(List args) async { final parser = ArgParser()..addFlag('help', abbr: 'h'); diff --git a/pubspec.yaml b/pubspec.yaml index 635afdc17..907090ef9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,6 @@ dependencies: args: ^2.4.0 flutter: sdk: flutter - http: ^0.13.6 meta: ^1.3.0 stack_trace: ^1.10.0 From df7c66568fca6e2c38f5cde8f932d977fd895635 Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Thu, 24 Jul 2025 13:03:31 +0300 Subject: [PATCH 05/11] chore: downgrade args dependency in pubspec.yaml - Changed `args` dependency version from `^2.4.0` to `^2.3.0` for compatibility reasons. --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 907090ef9..96cdb745f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ repository: https://github.com/Instabug/Instabug-Flutter documentation: https://docs.instabug.com/docs/flutter-overview dependencies: - args: ^2.4.0 + args: ^2.3.0 flutter: sdk: flutter meta: ^1.3.0 From 3403f658f2aeb949e6506fa596a134616b55d6be Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Sun, 27 Jul 2025 19:32:47 +0300 Subject: [PATCH 06/11] fix: use http package to handle zip file upload - Add `http` package dependency, and use it instead of manually making the upload post request. --- bin/commands/upload_so_files.dart | 56 +++++++++++++++++++------------ bin/instabug.dart | 56 ++++++++++++++----------------- pubspec.yaml | 1 + 3 files changed, 62 insertions(+), 51 deletions(-) diff --git a/bin/commands/upload_so_files.dart b/bin/commands/upload_so_files.dart index d4f4ace3a..c2359303e 100644 --- a/bin/commands/upload_so_files.dart +++ b/bin/commands/upload_so_files.dart @@ -96,7 +96,8 @@ class UploadSoFilesCommand { // Validate architecture if (!validArchs.contains(options.arch)) { - print('[Instabug-CLI] Error: Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}'); + print( + '[Instabug-CLI] Error: Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}'); throw Exception( 'Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}'); } @@ -106,31 +107,44 @@ class UploadSoFilesCommand { print('File: ${options.file}'); print('App Version: ${options.name}'); - // TODO: Implement the actual upload logic here - // This would typically involve: - // 1. Reading the zip file - // 2. Making an HTTP request to the upload endpoint - // 3. Handling the response - - // Make an HTTP request to the upload endpoint - final body = { - 'arch': options.arch, - 'api_key': options.apiKey, - 'application_token': options.token, - 'so_file': options.file, - 'app_version': options.name, - }; - const endPoint = 'https://api.instabug.com/api/web/public/so_files'; - final response = await makeHttpPostRequest( - url: endPoint, - body: body, + // Create multipart request + final request = http.MultipartRequest('POST', Uri.parse(endPoint)); + + // Add form fields + request.fields['arch'] = options.arch; + request.fields['api_key'] = options.apiKey; + request.fields['application_token'] = options.token; + request.fields['app_version'] = options.name; + + // Add the zip file + final fileStream = http.ByteStream(file.openRead()); + final fileLength = await file.length(); + final multipartFile = http.MultipartFile( + 'so_file', + fileStream, + fileLength, + filename: file.path.split('/').last, ); + request.files.add(multipartFile); + + final response = await request.send(); + + if (response.statusCode < 200 || response.statusCode >= 300) { + final responseBody = await response.stream.bytesToString(); + print('[Instabug-CLI] Error: Failed to upload .so files'); + print('Status Code: ${response.statusCode}'); + print('Response: $responseBody'); + exit(1); + } - print('Successfully uploaded .so files for version: ${options.name} with arch ${options.arch}'); + print( + 'Successfully uploaded .so files for version: ${options.name} with arch ${options.arch}'); + exit(0); } catch (e) { - print('[Instabug-CLI] Error: Error uploading .so files: $e'); + print('[Instabug-CLI] Error uploading .so files, $e'); + print('[Instabug-CLI] Error Stack Trace: ${StackTrace.current}'); exit(1); } } diff --git a/bin/instabug.dart b/bin/instabug.dart index 37becf1c4..51459c73f 100755 --- a/bin/instabug.dart +++ b/bin/instabug.dart @@ -1,13 +1,14 @@ #!/usr/bin/env dart -import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; +import 'package:http/http.dart' as http; part 'commands/upload_so_files.dart'; // Command registry for easy management +// ignore: avoid_classes_with_only_static_members class CommandRegistry { static final Map _commands = { 'upload-so-files': CommandHandler( @@ -27,35 +28,30 @@ class CommandHandler { CommandHandler({required this.parser, required this.execute}); } -Future makeHttpPostRequest({ - required String url, - required Map body, - Map? headers, -}) async { - try { - final client = HttpClient(); - - final request = await client.postUrl(Uri.parse(url)); - - request.headers.contentType = ContentType.json; - - request.write(jsonEncode(body)); - - final response = await request.close(); - - if (response.statusCode >= 200 && response.statusCode < 300) { - final responseBody = await response.transform(utf8.decoder).join(); - return true; - } else { - print('Error: ${response.statusCode}'); - return false; - } - - } catch (e) { - print('[Instabug-CLI] Error while making HTTP POST request: $e'); - exit(1); - } -} +// Future makeHttpPostRequest({ +// required String url, +// required Map body, +// Map? headers, +// }) async { +// try { +// final client = HttpClient(); +// final request = await client.postUrl(Uri.parse(url)); +// request.headers.contentType = ContentType.json; +// request.write(jsonEncode(body)); +// final response = await request.close(); +// if (response.statusCode >= 200 && response.statusCode < 300) { +// final responseBody = await response.transform(utf8.decoder).join(); +// return true; +// } else { +// print('Error Code: ${response.statusCode}'); +// print('Error Body: ${await response.transform(utf8.decoder).join()}'); +// return false; +// } +// } catch (e) { +// print('[Instabug-CLI] Error while making HTTP POST request: $e'); +// exit(1); +// } +// } void main(List args) async { final parser = ArgParser()..addFlag('help', abbr: 'h'); diff --git a/pubspec.yaml b/pubspec.yaml index 96cdb745f..1ac17cded 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: args: ^2.3.0 flutter: sdk: flutter + http: ^0.13.3 meta: ^1.3.0 stack_trace: ^1.10.0 From b6a44d5df599ee94d7aeaef63e371ca4de963231 Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Mon, 28 Jul 2025 12:59:34 +0300 Subject: [PATCH 07/11] chore: update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb867f5c..2c4b2d880 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [Unreleased](https://github.com/Instabug/Instabug-Flutter/compare/v15.0.2...dev) + +## Added + +- Add new Instabug Flutter CLI tool. ([#608](https://github.com/Instabug/Instabug-Flutter/pull/608)) + ## [15.0.2](https://github.com/Instabug/Instabug-Flutter/compare/v14.3.0...15.0.2) (Jul 7, 2025) ### Added From d7ade1d1dfb58209729519a916e3ac084c304926 Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Mon, 28 Jul 2025 13:08:51 +0300 Subject: [PATCH 08/11] refactor: replace print statements with stdout and stderr --- bin/commands/upload_so_files.dart | 27 ++++++++-------- bin/instabug.dart | 51 ++++++++----------------------- 2 files changed, 27 insertions(+), 51 deletions(-) diff --git a/bin/commands/upload_so_files.dart b/bin/commands/upload_so_files.dart index c2359303e..9b828cba5 100644 --- a/bin/commands/upload_so_files.dart +++ b/bin/commands/upload_so_files.dart @@ -84,28 +84,29 @@ class UploadSoFilesCommand { // Validate file exists final file = File(options.file); if (!await file.exists()) { - print('[Instabug-CLI] Error: File not found: ${options.file}'); + stderr.writeln('[Instabug-CLI] Error: File not found: ${options.file}'); throw Exception('File not found: ${options.file}'); } // validate file is a zip file if (!file.path.endsWith('.zip')) { - print('[Instabug-CLI] Error: File is not a zip file: ${options.file}'); + stderr.writeln( + '[Instabug-CLI] Error: File is not a zip file: ${options.file}'); throw Exception('File is not a zip file: ${options.file}'); } // Validate architecture if (!validArchs.contains(options.arch)) { - print( + stderr.writeln( '[Instabug-CLI] Error: Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}'); throw Exception( 'Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}'); } - print('Uploading .so files...'); - print('Architecture: ${options.arch}'); - print('File: ${options.file}'); - print('App Version: ${options.name}'); + stdout.writeln('Uploading .so files...'); + stdout.writeln('Architecture: ${options.arch}'); + stdout.writeln('File: ${options.file}'); + stdout.writeln('App Version: ${options.name}'); const endPoint = 'https://api.instabug.com/api/web/public/so_files'; @@ -133,18 +134,18 @@ class UploadSoFilesCommand { if (response.statusCode < 200 || response.statusCode >= 300) { final responseBody = await response.stream.bytesToString(); - print('[Instabug-CLI] Error: Failed to upload .so files'); - print('Status Code: ${response.statusCode}'); - print('Response: $responseBody'); + stderr.writeln('[Instabug-CLI] Error: Failed to upload .so files'); + stderr.writeln('Status Code: ${response.statusCode}'); + stderr.writeln('Response: $responseBody'); exit(1); } - print( + stdout.writeln( 'Successfully uploaded .so files for version: ${options.name} with arch ${options.arch}'); exit(0); } catch (e) { - print('[Instabug-CLI] Error uploading .so files, $e'); - print('[Instabug-CLI] Error Stack Trace: ${StackTrace.current}'); + stderr.writeln('[Instabug-CLI] Error uploading .so files, $e'); + stderr.writeln('[Instabug-CLI] Error Stack Trace: ${StackTrace.current}'); exit(1); } } diff --git a/bin/instabug.dart b/bin/instabug.dart index 51459c73f..175bed65b 100755 --- a/bin/instabug.dart +++ b/bin/instabug.dart @@ -28,31 +28,6 @@ class CommandHandler { CommandHandler({required this.parser, required this.execute}); } -// Future makeHttpPostRequest({ -// required String url, -// required Map body, -// Map? headers, -// }) async { -// try { -// final client = HttpClient(); -// final request = await client.postUrl(Uri.parse(url)); -// request.headers.contentType = ContentType.json; -// request.write(jsonEncode(body)); -// final response = await request.close(); -// if (response.statusCode >= 200 && response.statusCode < 300) { -// final responseBody = await response.transform(utf8.decoder).join(); -// return true; -// } else { -// print('Error Code: ${response.statusCode}'); -// print('Error Body: ${await response.transform(utf8.decoder).join()}'); -// return false; -// } -// } catch (e) { -// print('[Instabug-CLI] Error while making HTTP POST request: $e'); -// exit(1); -// } -// } - void main(List args) async { final parser = ArgParser()..addFlag('help', abbr: 'h'); @@ -61,7 +36,7 @@ void main(List args) async { parser.addCommand(entry.key, entry.value.parser); } - print('--------------------------------'); + stdout.writeln('--------------------------------'); try { final result = parser.parse(args); @@ -72,8 +47,8 @@ void main(List args) async { if (command['help'] == true) { final commandHandler = CommandRegistry.commands[command.name]; if (commandHandler != null) { - print('Usage: instabug ${command.name} [options]'); - print(commandHandler.parser.usage); + stdout.writeln('Usage: instabug ${command.name} [options]'); + stdout.writeln(commandHandler.parser.usage); } return; } @@ -83,21 +58,21 @@ void main(List args) async { if (commandHandler != null) { commandHandler.execute(command); } else { - print('Unknown command: ${command.name}'); - print('Available commands: ${CommandRegistry.commandNames.join(', ')}'); + stderr.writeln('Unknown command: ${command.name}'); + stdout.writeln('Available commands: ${CommandRegistry.commandNames.join(', ')}'); exit(1); } } else { - print('No applicable command found'); - print('Usage: instabug [options] '); - print('Available commands: ${CommandRegistry.commandNames.join(', ')}'); - print('\nFor help on a specific command:'); - print(' instabug --help\n'); - print(parser.usage); + stderr.writeln('No applicable command found'); + stdout.writeln('Usage: instabug [options] '); + stdout.writeln('Available commands: ${CommandRegistry.commandNames.join(', ')}'); + stdout.writeln('For help on a specific command:'); + stdout.writeln(' instabug --help'); + stdout.writeln(parser.usage); } } catch (e) { - print('[Instabug-CLI] Error: $e'); - print(parser.usage); + stderr.writeln('[Instabug-CLI] Error: $e'); + stdout.writeln(parser.usage); exit(1); } } From 967404459808fdf9a41b0c92efbdd8c047e64c75 Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Mon, 28 Jul 2025 14:16:13 +0300 Subject: [PATCH 09/11] chore: run dart format --- bin/instabug.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/instabug.dart b/bin/instabug.dart index 175bed65b..b8a6d2b25 100755 --- a/bin/instabug.dart +++ b/bin/instabug.dart @@ -59,13 +59,15 @@ void main(List args) async { commandHandler.execute(command); } else { stderr.writeln('Unknown command: ${command.name}'); - stdout.writeln('Available commands: ${CommandRegistry.commandNames.join(', ')}'); + stdout.writeln( + 'Available commands: ${CommandRegistry.commandNames.join(', ')}'); exit(1); } } else { stderr.writeln('No applicable command found'); stdout.writeln('Usage: instabug [options] '); - stdout.writeln('Available commands: ${CommandRegistry.commandNames.join(', ')}'); + stdout.writeln( + 'Available commands: ${CommandRegistry.commandNames.join(', ')}'); stdout.writeln('For help on a specific command:'); stdout.writeln(' instabug --help'); stdout.writeln(parser.usage); From 5ec981b5be9db1b85f018168cb34f7b3d5dac70a Mon Sep 17 00:00:00 2001 From: Mohamed Kamal Date: Tue, 29 Jul 2025 12:08:24 +0300 Subject: [PATCH 10/11] chore: fix linter issues --- bin/commands/upload_so_files.dart | 19 ++++++++++--------- bin/instabug.dart | 8 +++++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/bin/commands/upload_so_files.dart b/bin/commands/upload_so_files.dart index 9b828cba5..227132f03 100644 --- a/bin/commands/upload_so_files.dart +++ b/bin/commands/upload_so_files.dart @@ -1,10 +1,5 @@ part of '../instabug.dart'; -/** - * This script uploads .so files to the specified endpoint used in NDK crash reporting. - * Usage: dart run instabug_flutter:instabug upload-so-files --arch --file --api_key --token --name - */ - class UploadSoFilesOptions { final String arch; final String file; @@ -22,6 +17,8 @@ class UploadSoFilesOptions { } // ignore: avoid_classes_with_only_static_members +/// This script uploads .so files to the specified endpoint used in NDK crash reporting. +/// Usage: dart run instabug_flutter:instabug upload-so-files --arch \ --file \ --api_key \ --token \ --name \ class UploadSoFilesCommand { static const List validArchs = [ 'x86', @@ -91,16 +88,19 @@ class UploadSoFilesCommand { // validate file is a zip file if (!file.path.endsWith('.zip')) { stderr.writeln( - '[Instabug-CLI] Error: File is not a zip file: ${options.file}'); + '[Instabug-CLI] Error: File is not a zip file: ${options.file}', + ); throw Exception('File is not a zip file: ${options.file}'); } // Validate architecture if (!validArchs.contains(options.arch)) { stderr.writeln( - '[Instabug-CLI] Error: Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}'); + '[Instabug-CLI] Error: Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}', + ); throw Exception( - 'Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}'); + 'Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}', + ); } stdout.writeln('Uploading .so files...'); @@ -141,7 +141,8 @@ class UploadSoFilesCommand { } stdout.writeln( - 'Successfully uploaded .so files for version: ${options.name} with arch ${options.arch}'); + 'Successfully uploaded .so files for version: ${options.name} with arch ${options.arch}', + ); exit(0); } catch (e) { stderr.writeln('[Instabug-CLI] Error uploading .so files, $e'); diff --git a/bin/instabug.dart b/bin/instabug.dart index b8a6d2b25..5a3a75caa 100755 --- a/bin/instabug.dart +++ b/bin/instabug.dart @@ -7,8 +7,8 @@ import 'package:http/http.dart' as http; part 'commands/upload_so_files.dart'; -// Command registry for easy management // ignore: avoid_classes_with_only_static_members +/// Command registry for easy management class CommandRegistry { static final Map _commands = { 'upload-so-files': CommandHandler( @@ -60,14 +60,16 @@ void main(List args) async { } else { stderr.writeln('Unknown command: ${command.name}'); stdout.writeln( - 'Available commands: ${CommandRegistry.commandNames.join(', ')}'); + 'Available commands: ${CommandRegistry.commandNames.join(', ')}', + ); exit(1); } } else { stderr.writeln('No applicable command found'); stdout.writeln('Usage: instabug [options] '); stdout.writeln( - 'Available commands: ${CommandRegistry.commandNames.join(', ')}'); + 'Available commands: ${CommandRegistry.commandNames.join(', ')}', + ); stdout.writeln('For help on a specific command:'); stdout.writeln(' instabug --help'); stdout.writeln(parser.usage); From 8c55f0004bd387a4bac7f77fea354362a1eb9f35 Mon Sep 17 00:00:00 2001 From: Andrew Amin Date: Mon, 4 Aug 2025 10:19:54 +0300 Subject: [PATCH 11/11] chore: add init command , add README.md file for cli usage --- bin/README.md | 96 ++++++++++ bin/commands/init.dart | 396 +++++++++++++++++++++++++++++++++++++++++ bin/instabug.dart | 6 + example/pubspec.lock | 20 +-- 4 files changed, 508 insertions(+), 10 deletions(-) create mode 100644 bin/README.md create mode 100644 bin/commands/init.dart diff --git a/bin/README.md b/bin/README.md new file mode 100644 index 000000000..bccf308c1 --- /dev/null +++ b/bin/README.md @@ -0,0 +1,96 @@ +# Instabug Flutter CLI Tool + +A command-line tool for managing Instabug Flutter SDK integration and utilities. + +## Available Commands + +### `init` - Initialize Instabug Flutter SDK + +Automatically sets up the Instabug Flutter SDK in your project with basic instrumentation. + +#### Usage + +```bash +dart run instabug_flutter:instabug init --token YOUR_APP_TOKEN [options] +``` + +#### Options + +- `--token, -t` (required): Your Instabug app token +- `--project-path, -p`: Path to your Flutter project (defaults to current directory) +- `--invocation-events, -i`: Comma-separated list of invocation events (default: shake) + - Available events: `shake`, `floating_button`, `screenshot`, `two_finger_swipe_left`, `none` +- `--setup-permissions`: Setup required permissions for iOS and Android (default: true) +- `--update-pubspec`: Add instabug_flutter dependency to pubspec.yaml (default: true) +- `--help, -h`: Show help message + +#### What it does + +1. **Updates pubspec.yaml**: Automatically fetches and adds the latest `instabug_flutter` dependency from pub.dev +2. **Updates main.dart**: Adds the Instabug import and initialization code in the `main()` function before `runApp()` +3. **Sets up permissions**: + - **iOS**: Adds required usage descriptions to Info.plist for microphone and photo library access + - **Android**: Permissions are automatically handled by the plugin +4. **Configures invocation events**: Sets up how users can invoke Instabug (shake, floating button, etc.) +5. **Latest version detection**: Automatically fetches the most recent version from pub.dev with fallback support + +#### Examples + +**Basic initialization with shake gesture:** +```bash +dart run instabug_flutter:instabug init --token YOUR_APP_TOKEN +``` + +**Initialize with multiple invocation events:** +```bash +dart run instabug_flutter:instabug init --token YOUR_APP_TOKEN --invocation-events shake,floating_button +``` + +**Initialize without automatic permission setup:** +```bash +dart run instabug_flutter:instabug init --token YOUR_APP_TOKEN --no-setup-permissions +``` + +**Initialize for a specific project path:** +```bash +dart run instabug_flutter:instabug init --token YOUR_APP_TOKEN --project-path /path/to/my/flutter/project +``` + +#### After running the command + +1. Run `flutter packages get` to install the new dependency +2. If using a placeholder token, replace `YOUR_APP_TOKEN` in your main.dart with your actual Instabug app token +3. Test the integration by using your configured invocation event (e.g., shake the device) + +#### Finding your App Token + +You can find your app token by: +1. Going to your [Instabug dashboard](https://dashboard.instabug.com) +2. Selecting **SDK Integration** in the **Settings** menu +3. Your app token will be displayed there + +### `upload-so-files` - Upload Symbol Files + +Uploads .so files for NDK crash reporting (existing command). + +## Getting Help + +For help on any command: +```bash +dart run instabug_flutter:instabug --help +``` + +For general help: +```bash +dart run instabug_flutter:instabug --help +``` + +## Requirements + +- Flutter SDK +- Dart SDK +- Valid Instabug app token + +## Documentation + +For more detailed information about Instabug Flutter integration, visit the [official documentation](https://docs.instabug.com/docs/flutter-integration). \ No newline at end of file diff --git a/bin/commands/init.dart b/bin/commands/init.dart new file mode 100644 index 000000000..affde824c --- /dev/null +++ b/bin/commands/init.dart @@ -0,0 +1,396 @@ +part of '../instabug.dart'; + +class InitOptions { + final String token; + final String? projectPath; + final List invocationEvents; + final bool setupPermissions; + final bool updatePubspec; + + InitOptions({ + required this.token, + this.projectPath, + required this.invocationEvents, + required this.setupPermissions, + required this.updatePubspec, + }); +} + +// ignore: avoid_classes_with_only_static_members +/// Initializes Instabug Flutter SDK in your project. +/// Usage: dart run instabug_flutter:instabug init --token [options] +class InitCommand { + static const List validInvocationEvents = [ + 'shake', + 'floating_button', + 'screenshot', + 'two_finger_swipe_left', + 'none', + ]; + + static ArgParser createParser() { + final parser = ArgParser() + ..addFlag('help', abbr: 'h', help: 'Show this help message') + ..addOption( + 'token', + abbr: 't', + help: 'Your Instabug app token (required)', + mandatory: true, + ) + ..addOption( + 'project-path', + abbr: 'p', + help: 'Path to your Flutter project (defaults to current directory)', + ) + ..addMultiOption( + 'invocation-events', + abbr: 'i', + help: 'Invocation events (comma-separated)', + allowed: validInvocationEvents, + defaultsTo: ['shake'], + ) + ..addFlag( + 'setup-permissions', + help: 'Setup required permissions for iOS and Android', + defaultsTo: true, + ) + ..addFlag( + 'update-pubspec', + help: 'Add instabug_flutter dependency to pubspec.yaml', + defaultsTo: true, + ); + + return parser; + } + + static Future execute(ArgResults args) async { + final options = InitOptions( + token: args['token'] as String, + projectPath: args['project-path'] as String?, + invocationEvents: args['invocation-events'] as List, + setupPermissions: args['setup-permissions'] as bool, + updatePubspec: args['update-pubspec'] as bool, + ); + + stdout.writeln('🚀 Initializing Instabug Flutter SDK...'); + + try { + await _initializeInstabug(options); + stdout.writeln('✅ Instabug Flutter SDK initialized successfully!'); + stdout.writeln(); + stdout.writeln('Next steps:'); + stdout.writeln('1. Run "flutter packages get" to install dependencies'); + stdout.writeln( + '2. Replace "APP_TOKEN" with your actual app token in main.dart', + ); + stdout.writeln( + '3. Test the integration by shaking your device or using your configured invocation event', + ); + } catch (e) { + stderr.writeln('❌ Failed to initialize Instabug: $e'); + exit(1); + } + } + + static Future _initializeInstabug(InitOptions options) async { + final projectPath = options.projectPath ?? Directory.current.path; + final projectDir = Directory(projectPath); + + // Verify this is a Flutter project + final pubspecFile = File('${projectDir.path}/pubspec.yaml'); + if (!await pubspecFile.exists()) { + throw Exception( + "No pubspec.yaml found. Make sure you're in a Flutter project directory.", + ); + } + + // Update pubspec.yaml if requested + if (options.updatePubspec) { + await _updatePubspec(pubspecFile); + } + + // Update main.dart + await _updateMainDart(projectDir, options); + + // Setup permissions if requested + if (options.setupPermissions) { + await _setupPermissions(projectDir); + } + } + + static Future _updatePubspec(File pubspecFile) async { + stdout.writeln('📝 Updating pubspec.yaml...'); + + final content = await pubspecFile.readAsString(); + + // Check if instabug_flutter is already added + if (content.contains('instabug_flutter:')) { + stdout.writeln('ℹ️ instabug_flutter already exists in pubspec.yaml'); + return; + } + + // Find dependencies section and add instabug_flutter + final lines = content.split('\n'); + var foundDependencies = false; + var insertIndex = -1; + + for (var i = 0; i < lines.length; i++) { + if (lines[i].trim() == 'dependencies:') { + foundDependencies = true; + continue; + } + + if (foundDependencies && lines[i].trim().startsWith('flutter:')) { + // Look for the end of the flutter dependency block + for (var j = i + 1; j < lines.length; j++) { + // If we find a line that doesn't start with spaces (new top-level key) + // or find another dependency, insert before it + if (lines[j].trim().isEmpty) { + continue; // Skip empty lines + } + if (!lines[j].startsWith(' ') || + (lines[j].trim().contains(':') && + !lines[j].trim().startsWith('sdk:'))) { + insertIndex = j; + break; + } + } + break; + } + } + + if (insertIndex == -1) { + throw Exception( + 'Could not find suitable location to add instabug_flutter dependency', + ); + } + + // Get the latest version and insert the dependency + final latestVersion = await _getLatestInstabugVersion(); + lines.insert(insertIndex, ' instabug_flutter: $latestVersion'); + + await pubspecFile.writeAsString(lines.join('\n')); + stdout.writeln('✅ Added instabug_flutter dependency to pubspec.yaml'); + } + + static Future _updateMainDart( + Directory projectDir, + InitOptions options, + ) async { + stdout.writeln('📝 Updating main.dart...'); + + final mainDartFile = File('${projectDir.path}/lib/main.dart'); + if (!await mainDartFile.exists()) { + throw Exception('main.dart not found in lib directory'); + } + + final content = await mainDartFile.readAsString(); + + // Check if Instabug is already imported + if (content.contains('instabug_flutter/instabug_flutter.dart')) { + stdout.writeln('ℹ️ Instabug import already exists in main.dart'); + return; + } + + final lines = content.split('\n'); + + // Find import section and add Instabug import + var importInsertIndex = 0; + for (var i = 0; i < lines.length; i++) { + if (lines[i].startsWith("import 'package:flutter/")) { + importInsertIndex = i + 1; + } + } + + lines.insert( + importInsertIndex, + "import 'package:instabug_flutter/instabug_flutter.dart';", + ); + + // Find main function and add Instabug initialization before runApp() + final invocationEventsCode = + _generateInvocationEventsCode(options.invocationEvents); + final initCode = ''' + // Initialize Instabug + + Instabug.init( + token: '${options.token}', + invocationEvents: $invocationEventsCode, + ); + +'''; + + // Look for main function and runApp call + var addedInit = false; + var inMainFunction = false; + + for (var i = 0; i < lines.length; i++) { + // Find the main function + if (lines[i].trim().startsWith('void main(') || + lines[i].trim() == 'void main() {' || + lines[i].trim().startsWith('main(')) { + inMainFunction = true; + continue; + } + + // If we're in main function, look for runApp call + if (inMainFunction && lines[i].trim().startsWith('runApp(')) { + // Insert Instabug initialization before runApp + lines.insert(i, initCode); + addedInit = true; + break; + } + + // If we encounter a closing brace at the beginning of a line while in main, we've left main + if (inMainFunction && lines[i].trim() == '}') { + inMainFunction = false; + } + } + + if (!addedInit) { + // If no main/runApp found, add a comment for manual setup + lines.add(''); + lines.add( + '// TODO: Add Instabug initialization to your main() function before runApp():', + ); + lines.add('// ${initCode.replaceAll('\n', '\n// ')}'); + } + + await mainDartFile.writeAsString(lines.join('\n')); + stdout.writeln('✅ Updated main.dart with Instabug initialization'); + + if (!addedInit) { + stdout.writeln('⚠️ Could not automatically add initialization code.'); + stdout.writeln( + ' Please manually add the initialization code to your main() function before runApp().', + ); + } + } + + static String _generateInvocationEventsCode(List events) { + final dartEvents = events.map((event) { + switch (event) { + case 'shake': + return 'InvocationEvent.shake'; + case 'floating_button': + return 'InvocationEvent.floatingButton'; + case 'screenshot': + return 'InvocationEvent.screenshot'; + case 'two_finger_swipe_left': + return 'InvocationEvent.twoFingersSwipeLeft'; + case 'none': + return 'InvocationEvent.none'; + default: + return 'InvocationEvent.shake'; + } + }).toList(); + + return '[${dartEvents.join(', ')}]'; + } + + static Future _setupPermissions(Directory projectDir) async { + stdout.writeln('🔐 Setting up permissions...'); + + // Setup iOS permissions + await _setupIOSPermissions(projectDir); + + // Setup Android permissions + await _setupAndroidPermissions(projectDir); + + stdout.writeln('✅ Permissions setup completed'); + } + + static Future _setupIOSPermissions(Directory projectDir) async { + final infoPlistPath = '${projectDir.path}/ios/Runner/Info.plist'; + final infoPlistFile = File(infoPlistPath); + + if (!await infoPlistFile.exists()) { + stdout.writeln( + '⚠️ iOS Info.plist not found, skipping iOS permission setup', + ); + return; + } + + final content = await infoPlistFile.readAsString(); + + const microphonePermission = + 'NSMicrophoneUsageDescription\n\t\$(PRODUCT_NAME) needs access to your microphone so you can attach voice notes.'; + const photoLibraryPermission = + 'NSPhotoLibraryUsageDescription\n\t\$(PRODUCT_NAME) needs access to your photo library so you can attach images.'; + + if (!content.contains('NSMicrophoneUsageDescription') || + !content.contains('NSPhotoLibraryUsageDescription')) { + // Find the closing tag before + final lines = content.split('\n'); + var insertIndex = -1; + + for (var i = lines.length - 1; i >= 0; i--) { + if (lines[i].trim() == '' && i < lines.length - 2) { + insertIndex = i; + break; + } + } + + if (insertIndex != -1) { + if (!content.contains('NSMicrophoneUsageDescription')) { + lines.insert(insertIndex, '\t$microphonePermission'); + insertIndex++; + } + if (!content.contains('NSPhotoLibraryUsageDescription')) { + lines.insert(insertIndex, '\t$photoLibraryPermission'); + } + + await infoPlistFile.writeAsString(lines.join('\n')); + stdout.writeln('✅ Added iOS permissions to Info.plist'); + } + } else { + stdout.writeln('ℹ️ iOS permissions already exist in Info.plist'); + } + } + + static Future _setupAndroidPermissions(Directory projectDir) async { + final manifestPath = + '${projectDir.path}/android/app/src/main/AndroidManifest.xml'; + final manifestFile = File(manifestPath); + + if (!await manifestFile.exists()) { + stdout.writeln( + '⚠️ Android AndroidManifest.xml not found, skipping Android permission setup', + ); + return; + } + + stdout.writeln( + 'ℹ️ Android permissions are automatically added by the Instabug Flutter plugin', + ); + stdout.writeln(' No manual setup required for Android permissions'); + } + + static Future _getLatestInstabugVersion() async { + try { + stdout.writeln('🔍 Fetching latest instabug_flutter version...'); + + final response = await http.get( + Uri.parse('https://pub.dev/api/packages/instabug_flutter'), + ); + + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final latest = data['latest'] as Map; + final version = latest['version'] as String; + + stdout.writeln('✅ Found latest version: $version'); + return '^$version'; + } else { + stdout.writeln( + '⚠️ Failed to fetch latest version from pub.dev, using fallback', + ); + return '^13.0.0'; // Fallback version + } + } catch (e) { + stdout.writeln('⚠️ Error fetching latest version: $e'); + stdout.writeln(' Using fallback version'); + return '^13.0.0'; // Fallback version + } + } +} diff --git a/bin/instabug.dart b/bin/instabug.dart index 5a3a75caa..52a8faf20 100755 --- a/bin/instabug.dart +++ b/bin/instabug.dart @@ -1,16 +1,22 @@ #!/usr/bin/env dart +import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; import 'package:http/http.dart' as http; part 'commands/upload_so_files.dart'; +part 'commands/init.dart'; // ignore: avoid_classes_with_only_static_members /// Command registry for easy management class CommandRegistry { static final Map _commands = { + 'init': CommandHandler( + parser: InitCommand.createParser(), + execute: InitCommand.execute, + ), 'upload-so-files': CommandHandler( parser: UploadSoFilesCommand.createParser(), execute: UploadSoFilesCommand.execute, diff --git a/example/pubspec.lock b/example/pubspec.lock index c04ce3b1b..fed05e1eb 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" file: dependency: transitive description: @@ -128,10 +128,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -285,18 +285,18 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" webdriver: dependency: transitive description: name: webdriver - sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.1.0" sdks: dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54"