Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
37 changes: 37 additions & 0 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Apidash CLI

A lightweight, portable, and **Flutter-free** Command Line Interface for Apidash. Access, test, and run your saved API requests directly from your terminal.

## Key Features

- **Zero Config**: Automatically detects your Apidash GUI workspace on Linux, macOS, and Windows.
- **Concurrency Safe**: Uses a "Shadow Copy" mechanism to read your data even while the Apidash desktop app is open and locking the database.
- **Interactive TUI**: A premium Terminal User Interface for selecting and editing requests.
- **UX-Focused Editor**: Pre-filled interactive line editor for URLs and Parameters—edit individual letters instead of re-typing.
- **Pure Dart**: Built as a standalone Dart package with no dependency on the Flutter SDK.


## Usage

### List & Interact
Browse your saved requests using the interactive TUI.
```bash
dart run bin/apidash.dart list
```
From the list, you can:
- **Run**: Execute the request and see beautifully formatted JSON responses.
- **Edit**: Temporarily modify the **URL**, **HTTP Method**, or **Query Parameters** before running.
- **Back**: Return to the list.

### Direct Run
Execute a specific request immediately if you know its ID.
```bash
dart run bin/apidash.dart run <request-id>
```

### Options
| Option | Abbr | Description |
| :--- | :--- | :--- |
| `--data-dir` | `-d` | Path to the directory containing `apidash-data.hive`. Overrides auto-detection. |
| `--help` | `-h` | Show usage information. |

Binary file added packages/cli/apidash
Binary file not shown.
37 changes: 37 additions & 0 deletions packages/cli/bin/apidash.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:hive_ce/hive.dart';
import 'package:apidash_cli/apidash_cli.dart';

void main(List<String> args) async {
final logger = Logger();

final runner = CommandRunner("apidash", "Apidash CLI tool ")
..argParser.addOption(
'data-dir',
abbr: 'd',
help: 'Directory where Apidash Hive files are stored.',
defaultsTo: detectWorkspacePath() ?? getDefaultDataDir(),
)
..addCommand(ListCommand(logger))
..addCommand(RunCommand(logger))
..addCommand(EnvCommand(logger))
..addCommand(CodeCommand(logger))
..addCommand(ExportCommand(logger));

try {
await runner.run(args);
} catch (e) {
if (e is UsageException) {
logger.err(e.message);
logger.info('');
logger.info(runner.usage);
} else {
logger.err(e.toString());
}
} finally {
await Hive.close();
exit(0);
}
}
6 changes: 6 additions & 0 deletions packages/cli/lib/apidash_cli.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export 'src/commands/list.dart';
export 'src/commands/run.dart';
export 'src/commands/stubs.dart';
export 'src/storage/storage.dart';
export 'src/ui/editor.dart';
export 'src/utils/workspace.dart';
173 changes: 173 additions & 0 deletions packages/cli/lib/src/commands/list.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:hive_ce/hive.dart';
import '../storage/storage.dart';
import '../ui/editor.dart';
import 'run.dart';

class ListCommand extends Command {
final Logger logger;
ListCommand(this.logger);

@override final name = "list";
@override final description = "Lists all saved requests. Features interactive TUI.";

@override
void run() async {
final dataDir = globalResults?['data-dir'] as String;
final storage = StorageHelper(dataDir, logger);

try {
await storage.init();
var requests = await storage.getRequests();

if (requests.isEmpty) {
logger.warn("No data found in $dataDir.");
logger.info("Using mock requests for demonstration...");
requests = _getMockRequests();
}

final choices = requests.map((r) {
final method = (r['method'] ?? r['httpRequestModel']?['method'] ?? 'GET').toString().toUpperCase().padRight(7);
return "$method | ${r['name']} [${r['id']}]";
}).toList();

final choice = logger.chooseOne('Select a request:', choices: choices,);

var selectedRequest = requests[choices.indexOf(choice)];

bool exitActions = false;
while (!exitActions) {
_displayRequest(selectedRequest);

final action = logger.chooseOne(
'Select Action:',
choices: ['Run', 'Edit', 'Curl', 'Back'],
);

switch (action) {
case 'Run':
await RunCommand(logger).executeRequest(selectedRequest);
exitActions = true;
break;
case 'Edit':
final editField = logger.chooseOne(
'What do you want to edit?',
choices: ['URL', 'Method', 'Params', 'Cancel'],
);

if (editField == 'URL') {
final currentUrl = _getHttpField(selectedRequest, 'url') ?? '';
final newUrl = interactiveEdit('Edit URL', currentUrl);
_updateRequestField(selectedRequest, 'url', newUrl);
logger.success('\nURL updated locally.');
} else if (editField == 'Method') {
final currentMethod = (_getHttpField(selectedRequest, 'method') ?? 'GET').toString().toUpperCase();
final newMethod = logger.chooseOne(
'Select HTTP Method:',
choices: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD'],
defaultValue: currentMethod,
);
_updateRequestField(selectedRequest, 'method', newMethod);
logger.success('Method updated locally.');
} else if (editField == 'Params') {
await _handleEditParams(selectedRequest);
}
break;
case 'Curl':
logger.info('Coming soon: Curl generation');
break;
case 'Back':
exitActions = true;
break;
}
}
} catch (e) {
logger.err(e.toString());
} finally {
await Hive.close();
await storage.cleanup();
}
}

Future<void> _handleEditParams(Map<String, dynamic> request) async {
var params = _getHttpField(request, 'params') as List? ?? [];
var mutableParams = params.map((p) => Map<String, dynamic>.from(p)).toList();

final paramChoices = mutableParams.map((p) => "${p['name']}: ${p['value']}").toList();
paramChoices.add('[Add New Parameter]');
paramChoices.add('[Back]');

final choice = logger.chooseOne('Select a parameter to edit:', choices: paramChoices);

if (choice == '[Back]') return;

if (choice == '[Add New Parameter]') {
final name = logger.prompt('Parameter Name:');
final value = logger.prompt('Parameter Value:');
mutableParams.add({'name': name, 'value': value});
} else {
final index = paramChoices.indexOf(choice);
final p = mutableParams[index];
final newName = interactiveEdit('Edit Name', p['name'].toString());
final newValue = interactiveEdit('Edit Value', p['value'].toString());
mutableParams[index] = {'name': newName, 'value': newValue};
}

_updateRequestField(request, 'params', mutableParams);
logger.success('\nParameters updated locally.');
}

dynamic _getHttpField(Map<String, dynamic> request, String field) {
return request[field] ?? request['httpRequestModel']?[field];
}

void _updateRequestField(Map<String, dynamic> request, String field, dynamic value) {
if (request.containsKey('httpRequestModel')) {
var model = Map<String, dynamic>.from(request['httpRequestModel']);
model[field] = value;
request['httpRequestModel'] = model;
} else {
request[field] = value;
}
}

List<Map<String, dynamic>> _getMockRequests() {
return [
{
'id': 'get-1',
'name': 'GitHub User Info',
'method': 'GET',
'url': 'https://api.github.com/users/badnikhil',
'headers': [{'name': 'User-Agent', 'value': 'Apidash-CLI'}]
},
{
'id': 'get-2',
'name': 'HTTPBin Get',
'method': 'GET',
'url': 'https://httpbin.org/get',
'params': [{'name': 'foo', 'value': 'bar'}]
},
];
}

void _displayRequest(Map<String, dynamic> request) {
final httpRequest = request['httpRequestModel'] ?? request;
final method = httpRequest['method'] ?? 'GET';
final url = httpRequest['url'] ?? 'N/A';
final params = httpRequest['params'] as List? ?? [];

logger.info('\n--- Request Details ---');
logger.info('Name: ${request['name']}');
logger.info('ID: ${request['id']}');
logger.info('Method: ${lightCyan.wrap(method.toString().toUpperCase())}');
logger.info('URL: $url');
if (params.isNotEmpty) {
logger.info('Params:');
for (var p in params) {
logger.info(' - ${p['name']}: ${p['value']}');
}
}
logger.info('-----------------------\n');
}
}
117 changes: 117 additions & 0 deletions packages/cli/lib/src/commands/run.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import 'dart:convert';
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';
import 'package:http/http.dart' as http;
import 'package:hive_ce/hive.dart';
import '../storage/storage.dart';

class RunCommand extends Command {
final Logger logger;
RunCommand(this.logger);

@override final name = "run";
@override final description = "Execute a saved request by ID.";

@override
void run() async {
final dataDir = globalResults?['data-dir'] as String;
if (argResults!.rest.isEmpty) throw UsageException('Missing request ID.', usage);
final requestId = argResults!.rest.first;

final storage = StorageHelper(dataDir, logger);
try {
await storage.init();
final request = await storage.getRequest(requestId);
if (request == null) {
logger.err("Request not found: $requestId");
return;
}
await executeRequest(request);
} catch (e) {
logger.err(e.toString());
} finally {
await Hive.close();
await storage.cleanup();
}
}

Future<void> executeRequest(Map<String, dynamic> request) async {
final httpRequest = request['httpRequestModel'] ?? request;
final method = (httpRequest['method'] ?? 'GET').toString().toUpperCase();
String url = (httpRequest['url'] as String?) ?? '';

if (url.isEmpty) {
logger.err("Cannot run request: URL is missing.");
return;
}

final Map<String, String> headers = {};
final headersList = httpRequest['headers'] as List?;
if (headersList != null) {
for (var h in headersList) {
if (h is Map && h['name'] != null && h['value'] != null) {
headers[h['name'].toString()] = h['value'].toString();
}
}
}

final paramsList = httpRequest['params'] as List?;
if (paramsList != null && paramsList.isNotEmpty) {
final queryParams = <String>[];
for (var p in paramsList) {
if (p is Map && p['name'] != null && p['value'] != null) {
queryParams.add('${Uri.encodeComponent(p['name'].toString())}=${Uri.encodeComponent(p['value'].toString())}');
}
}
if (queryParams.isNotEmpty) {
url += (url.contains('?') ? '&' : '?') + queryParams.join('&');
}
}

final progress = logger.progress('Executing $method $url');
final stopwatch = Stopwatch()..start();

try {
final client = http.Client();
final uri = Uri.parse(url);
late http.Response response;

switch (method) {
case 'GET': response = await client.get(uri, headers: headers); break;
case 'POST': response = await client.post(uri, headers: headers, body: httpRequest['body']); break;
case 'PUT': response = await client.put(uri, headers: headers, body: httpRequest['body']); break;
case 'DELETE': response = await client.delete(uri, headers: headers); break;
case 'PATCH': response = await client.patch(uri, headers: headers, body: httpRequest['body']); break;
case 'HEAD': response = await client.head(uri, headers: headers); break;
default: progress.fail("Unsupported method : $method"); return;
}

stopwatch.stop();
progress.complete('Completed in ${stopwatch.elapsedMilliseconds}ms');
_displayResponse(response);
} catch (e) {
progress.fail('Failed to connect: $e');
}
}

void _displayResponse(http.Response response) {
final statusColor = response.statusCode >= 200 && response.statusCode < 300
? lightGreen
: (response.statusCode >= 400 ? lightRed : lightYellow);

logger.info('\n${statusColor.wrap('Status: ${response.statusCode} ${response.reasonPhrase}')}');
logger.info('Size: ${response.bodyBytes.length} bytes');

if (response.body.isNotEmpty) {
logger.info('\n--- Response Body ---');
try {
final json = jsonDecode(response.body);
final prettyJson = JsonEncoder.withIndent(' ').convert(json);
logger.info(prettyJson);
} catch (_) {
logger.info(response.body);
}
logger.info('----------------------\n');
}
}
}
23 changes: 23 additions & 0 deletions packages/cli/lib/src/commands/stubs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';

class EnvCommand extends Command {
final Logger logger;
EnvCommand(this.logger);
@override final name = "env";
@override final description = "Environment management.";
}

class CodeCommand extends Command {
final Logger logger;
CodeCommand(this.logger);
@override final name = "code";
@override final description = "Generate code snippets.";
}

class ExportCommand extends Command {
final Logger logger;
ExportCommand(this.logger);
@override final name = "export";
@override final description = "Export collection.";
}
Loading