Skip to content

feat: create cli tool #608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
153 changes: 153 additions & 0 deletions bin/commands/upload_so_files.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
part of '../instabug.dart';

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
/// This script uploads .so files to the specified endpoint used in NDK crash reporting.
/// Usage: dart run instabug_flutter:instabug upload-so-files --arch \<arch\> --file \<path\> --api_key \<key\> --token \<token\> --name \<name\>
class UploadSoFilesCommand {
static const List<String> 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<void> uploadSoFiles(UploadSoFilesOptions options) async {
try {
// Validate file exists
final file = File(options.file);
if (!await file.exists()) {
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')) {
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)) {
stderr.writeln(
'[Instabug-CLI] Error: Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}',
);
throw Exception(
'Invalid architecture: ${options.arch}. Valid options: ${validArchs.join(', ')}',
);
}

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';

// 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();
stderr.writeln('[Instabug-CLI] Error: Failed to upload .so files');
stderr.writeln('Status Code: ${response.statusCode}');
stderr.writeln('Response: $responseBody');
exit(1);
}

stdout.writeln(
'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');
stderr.writeln('[Instabug-CLI] Error Stack Trace: ${StackTrace.current}');
exit(1);
}
}
}
82 changes: 82 additions & 0 deletions bin/instabug.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env dart

import 'dart:io';

import 'package:args/args.dart';
import 'package:http/http.dart' as http;

part 'commands/upload_so_files.dart';

// ignore: avoid_classes_with_only_static_members
/// Command registry for easy management
class CommandRegistry {
static final Map<String, CommandHandler> _commands = {
'upload-so-files': CommandHandler(
parser: UploadSoFilesCommand.createParser(),
execute: UploadSoFilesCommand.execute,
),
};

static Map<String, CommandHandler> get commands => _commands;
static List<String> get commandNames => _commands.keys.toList();
}

class CommandHandler {
final ArgParser parser;
final Function(ArgResults) execute;

CommandHandler({required this.parser, required this.execute});
}

void main(List<String> args) async {
final parser = ArgParser()..addFlag('help', abbr: 'h');

// Add all commands to the parser
for (final entry in CommandRegistry.commands.entries) {
parser.addCommand(entry.key, entry.value.parser);
}

stdout.writeln('--------------------------------');

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) {
stdout.writeln('Usage: instabug ${command.name} [options]');
stdout.writeln(commandHandler.parser.usage);
}
return;
}

final commandHandler = CommandRegistry.commands[command.name];
// Extra safety check just in case
if (commandHandler != null) {
commandHandler.execute(command);
} else {
stderr.writeln('Unknown command: ${command.name}');
stdout.writeln(
'Available commands: ${CommandRegistry.commandNames.join(', ')}',
);
exit(1);
}
} else {
stderr.writeln('No applicable command found');
stdout.writeln('Usage: instabug [options] <command>');
stdout.writeln(
'Available commands: ${CommandRegistry.commandNames.join(', ')}',
);
stdout.writeln('For help on a specific command:');
stdout.writeln(' instabug <command> --help');
stdout.writeln(parser.usage);
}
} catch (e) {
stderr.writeln('[Instabug-CLI] Error: $e');
stdout.writeln(parser.usage);
exit(1);
}
}
3 changes: 3 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,6 @@ app.*.symbols

# Obfuscation related
app.*.map.json

# Android related
android/app/.cxx/
Loading