diff --git a/docs/commands.md b/docs/commands.md index 7267825..dae9ed0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -93,7 +93,7 @@ Use the app terminal Usage: discloud app console [arguments] -h, --help Print this usage information. --app - --command +-c, --command ``` #### app delete @@ -196,8 +196,8 @@ Updates the profile information (avatar and name) for a specific app Usage: discloud app profile [arguments] -h, --help Print this usage information. --app - --name - --avatar +-n, --name +-a, --avatar ``` #### app ram diff --git a/lib/commands/app/backup.dart b/lib/commands/app/backup.dart index bd3e364..632254c 100644 --- a/lib/commands/app/backup.dart +++ b/lib/commands/app/backup.dart @@ -72,7 +72,7 @@ final class AppBackupCommand extends Command with Disposable { Future _handleMulti(List list, ISpin spinner) async { final client = _client = .new(); - final dir = argResults?.optionOrRest("dir") ?? "."; + final dir = argResults!.optionOrRest("dir") ?? "."; for (final data in list) { final String appId = data["id"]; diff --git a/lib/commands/app/commit.dart b/lib/commands/app/commit.dart index 5d55e6b..33339c3 100644 --- a/lib/commands/app/commit.dart +++ b/lib/commands/app/commit.dart @@ -39,13 +39,13 @@ final class AppCommitCommand extends Command with Disposable { Future run() async { final directory = context.workspaceFolder; - final optionAppId = argResults?.optionOrRest("app"); + final optionAppId = argResults!.optionOrRest("app"); final appId = optionAppId ?? await _getDiscloudConfigAppId(directory); if (appId == null) throw Exception("Missing app id"); final glob = - argResults?.multiOptionOrRest("glob", [ + argResults!.multiOptionOrRest("glob", [ if (optionAppId != null) "app", ]) ?? const ["**"]; diff --git a/lib/commands/app/console.dart b/lib/commands/app/console.dart index acb78be..b9941f0 100644 --- a/lib/commands/app/console.dart +++ b/lib/commands/app/console.dart @@ -6,13 +6,15 @@ import "package:discloud/cli/spin/ispin.dart"; import "package:discloud/extensions/command.dart"; import "package:discloud/services/discloud/exception.dart"; import "package:discloud/utils/messages.dart"; -import "package:tint/tint.dart"; + +const _commandPrefix = "\x1B[2m?> \x1B[22m"; +const _exitCommand = "exit"; final class AppConsoleCommand extends Command { AppConsoleCommand() { argParser ..addOption("app") - ..addOption("command"); + ..addMultiOption("command", abbr: "c"); } @override @@ -27,33 +29,30 @@ final class AppConsoleCommand extends Command { @override Future run() async { final appId = argResults!.requiredOptionOrRest("app"); - String? command = argResults!.option("command"); final spinner = context.printer.spin(start: false); - if (command case final command?) { - await _send(appId: appId, command: command, spinner: spinner); + if (argResults!.multiOptionOrRest("command", const ["app"])?.join(" ") + case final command?) { + await _send(appId, command, spinner); return; } context.printer("Enter 'exit' to stop."); - while (true) { - context.printer.write("?> ".dim()); + await Future.doWhile(() { + context.printer.write(_commandPrefix); - command = stdin.readLineSync(); - if (command == null || command == "exit") break; - if (command.isEmpty) continue; + if (stdin.readLineSync() case final command? + when command.isNotEmpty && command != _exitCommand) { + return _send(appId, command, spinner); + } - if (!await _send(appId: appId, command: command, spinner: spinner)) break; - } + return false; + }); } - Future _send({ - required String appId, - required String command, - required ISpin spinner, - }) async { + Future _send(String appId, String command, ISpin spinner) async { spinner.start("Sending command..."); try { @@ -62,23 +61,22 @@ final class AppConsoleCommand extends Command { body: {"command": command}, ); - if (response["status"] == "ok") { + if (response["apps"]?["shell"] case final Map shell) { spinner.stop(); - if (response["apps"]?["shell"] case final Map shell) { - if (shell["stdout"] case final String content - when content.isNotEmpty) { - stdout.writeln(content); - } + if (shell["stdout"] case final String content when content.isNotEmpty) { + stdout.writeln(content); + } - if (shell["stderr"] case final String content - when content.isNotEmpty) { - stderr.writeln(content); - } + if (shell["stderr"] case final String content when content.isNotEmpty) { + stderr.writeln(content); } - } else { - spinner.fail(resolveResponseMessage(response)); + + return true; } + + spinner.fail(resolveResponseMessage(response)); + return true; } on DiscloudApiException catch (e, s) { final text = switch (e.code) { diff --git a/lib/commands/app/info.dart b/lib/commands/app/info.dart index b3071f0..ddc5430 100644 --- a/lib/commands/app/info.dart +++ b/lib/commands/app/info.dart @@ -32,7 +32,7 @@ final class AppInfoCommand extends Command { @override Future run() async { - final appId = argResults?.optionOrRest("app") ?? "all"; + final appId = argResults!.optionOrRest("app") ?? "all"; final spinner = context.printer.spin(text: "Fetching app info..."); diff --git a/lib/commands/app/profile.dart b/lib/commands/app/profile.dart index 5b0d5f6..4bbd669 100644 --- a/lib/commands/app/profile.dart +++ b/lib/commands/app/profile.dart @@ -8,8 +8,8 @@ final class AppProfileCommand extends Command { AppProfileCommand() { argParser ..addOption("app") - ..addOption("name") - ..addOption("avatar"); + ..addOption("name", abbr: "n") + ..addOption("avatar", abbr: "a"); } @override @@ -22,14 +22,14 @@ final class AppProfileCommand extends Command { @override Future run() async { final appId = argResults!.requiredOptionOrRest("app"); - final name = argResults?.option("name"); - final avatar = argResults?.option("avatar"); + final name = argResults!.option("name"); + final avatar = argResults!.option("avatar"); final spinner = context.printer.spin(text: "Changing app profile..."); final response = await context.api.put( "/app/$appId/profile", - body: {"name": name, "avatarURL": avatar}, + body: {"name": ?name, "avatarURL": ?avatar}, ); spinner.success(resolveResponseMessage(response)); diff --git a/lib/commands/app/ram.dart b/lib/commands/app/ram.dart index 030c697..680bbdc 100644 --- a/lib/commands/app/ram.dart +++ b/lib/commands/app/ram.dart @@ -27,7 +27,7 @@ final class AppRamCommand extends Command { Future run() async { final appId = argResults!.requiredOptionOrRest("app"); final ramMB = max( - argResults!.requiredIntOptionOrRest("amount", const ["app"]), + argResults!.requiredIntOptionOrRest("amount", after: const ["app"]), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/snapshot/download.dart b/lib/commands/snapshot/download.dart index 5218a8e..d5cb18d 100644 --- a/lib/commands/snapshot/download.dart +++ b/lib/commands/snapshot/download.dart @@ -64,7 +64,7 @@ final class SnapshotDownloadCommand extends Command with Disposable { String appId, { required ISpin spinner, }) async { - if (argResults?.optionOrRest("version", const ["app"]) + if (argResults!.optionOrRest("version", const ["app"]) case final String version) { return version; } @@ -92,7 +92,7 @@ final class SnapshotDownloadCommand extends Command with Disposable { Future _download({required ISpin spinner, required Uri uri}) async { final filename = uri.pathSegments.last; - final filepath = joinAll([?argResults?.option("dir"), filename]); + final filepath = joinAll([?argResults!.option("dir"), filename]); final file = _file = .new(filepath); final monitor = _monitor = .new(); diff --git a/lib/commands/snapshot/info.dart b/lib/commands/snapshot/info.dart index 1799e00..ee6bf8c 100644 --- a/lib/commands/snapshot/info.dart +++ b/lib/commands/snapshot/info.dart @@ -40,7 +40,7 @@ final class SnapshotInfoCommand extends Command { @override Future run() { - return switch (argResults?.optionOrRest("app")) { + return switch (argResults!.optionOrRest("app")) { final String appId => _runSingle(appId), _ => _runMulti(), }; @@ -64,9 +64,9 @@ final class SnapshotInfoCommand extends Command { final response = await context.api.get( "/snapshot", query: { - "page": ?argResults?.option("page"), - "limit": ?argResults?.option("limit"), - "summary": ?argResults?.flag("summary").toString(), + "page": ?argResults!.option("page"), + "limit": ?argResults!.option("limit"), + "summary": argResults!.flag("summary").toString(), }, ); diff --git a/lib/commands/system/locale.dart b/lib/commands/system/locale.dart index 9dc3e4b..7a10d86 100644 --- a/lib/commands/system/locale.dart +++ b/lib/commands/system/locale.dart @@ -26,6 +26,6 @@ final class SystemLocaleCommand extends Command { "Short locale: ${Intl.shortLocale(Platform.localeName)}", ], "\n"); - context.printer.info(sb); + context.printer(sb); } } diff --git a/lib/commands/team/backup.dart b/lib/commands/team/backup.dart index 7656124..1037e04 100644 --- a/lib/commands/team/backup.dart +++ b/lib/commands/team/backup.dart @@ -72,7 +72,7 @@ final class TeamBackupCommand extends Command with Disposable { Future _handleMulti(List list, ISpin spinner) async { final client = _client = .new(); - final dir = argResults?.optionOrRest("dir") ?? "."; + final dir = argResults!.optionOrRest("dir") ?? "."; for (final data in list) { final String appId = data["id"]; diff --git a/lib/commands/team/commit.dart b/lib/commands/team/commit.dart index 273590e..2ea01a2 100644 --- a/lib/commands/team/commit.dart +++ b/lib/commands/team/commit.dart @@ -39,13 +39,13 @@ final class TeamCommitCommand extends Command with Disposable { Future run() async { final directory = context.workspaceFolder; - final optionAppId = argResults?.optionOrRest("app"); + final optionAppId = argResults!.optionOrRest("app"); final appId = optionAppId ?? await _getDiscloudConfigAppId(directory); if (appId == null) throw Exception("Missing app id"); final glob = - argResults?.multiOptionOrRest("glob", [ + argResults!.multiOptionOrRest("glob", [ if (optionAppId != null) "app", ]) ?? const ["**"]; diff --git a/lib/commands/team/ram.dart b/lib/commands/team/ram.dart index ce1be9f..2f49c31 100644 --- a/lib/commands/team/ram.dart +++ b/lib/commands/team/ram.dart @@ -27,7 +27,7 @@ final class TeamRamCommand extends Command { Future run() async { final appId = argResults!.requiredOptionOrRest("app"); final ramMB = max( - argResults!.requiredIntOptionOrRest("amount", const ["app"]), + argResults!.requiredIntOptionOrRest("amount", after: const ["app"]), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/wait.dart b/lib/commands/wait.dart index 695ac38..56e870e 100644 --- a/lib/commands/wait.dart +++ b/lib/commands/wait.dart @@ -19,7 +19,7 @@ final class WaitCommand extends Command with Disposable { @override Future run() async { - if (argResults?.rest.firstOrNull case final seconds?) { + if (argResults!.rest.firstOrNull case final seconds?) { if (int.tryParse(seconds) case final seconds? when seconds.isPositive) { await Future.delayed(.new(seconds: seconds)); } @@ -29,6 +29,6 @@ final class WaitCommand extends Command with Disposable { @override FutureOr dispose() { // ignore: no_runtimetype_tostring - context.printer.info("$hashCode $runtimeType disposed"); + context.printer("$hashCode $runtimeType disposed"); } } diff --git a/lib/commands/zip.dart b/lib/commands/zip.dart index eb10a6a..52a9a77 100644 --- a/lib/commands/zip.dart +++ b/lib/commands/zip.dart @@ -32,7 +32,7 @@ final class ZipCommand extends Command with Disposable { final description = "Make zip"; int? get _compressionLevel { - if (argResults?.option("level") case final l?) return .parse(l); + if (argResults!.option("level") case final l?) return .parse(l); return null; } @@ -42,11 +42,11 @@ final class ZipCommand extends Command with Disposable { Future run() async { final directory = context.workspaceFolder; - final encoding = argResults?.option("encoding"); - final out = argResults?.option("out") ?? "${basename(directory.path)}.zip"; + final encoding = argResults!.option("encoding"); + final out = argResults!.option("out") ?? "${basename(directory.path)}.zip"; final glob = argResults!.multiOptionOrRest("glob") ?? const ["**"]; final level = _compressionLevel; - final password = argResults?.option("password"); + final password = argResults!.option("password"); final spinner = context.printer.spin(text: "Zipping..."); diff --git a/lib/extensions/arg_results.dart b/lib/extensions/arg_results.dart index 71f0780..311daa7 100644 --- a/lib/extensions/arg_results.dart +++ b/lib/extensions/arg_results.dart @@ -1,46 +1,52 @@ import "package:args/args.dart"; -import "package:args/command_runner.dart"; const _whiteSpace = " "; extension ArgResultsExtension on ArgResults { - String? optionOrRest(String name, [Iterable after = const []]) { - if (wasParsed(name)) return option(name); - final index = after.where(wasNotParsed).length; - if (rest.length > index) return rest[index]; + int? intOption(String name, {int? radix}) { + if (option(name) case final s?) return .tryParse(s, radix: radix); return null; } - String requiredOptionOrRest( - String name, [ + int? intOptionOrRest( + String name, { Iterable after = const [], - ]) { - if (optionOrRest(name, after) case final value?) return value; - - throw UsageException("Missing required option or argument: $name", ""); + int? radix, + }) { + if (optionOrRest(name, after) case final value?) { + return .tryParse(value, radix: radix); + } + return null; } - int requiredIntOptionOrRest( + Iterable? multiOptionOrRest( String name, [ Iterable after = const [], ]) { - final raw = requiredOptionOrRest(name, after); - if (int.tryParse(raw) case final value?) return value; - - throw UsageException("Invalid integer for $name: $raw", ""); + if (wasParsed(name)) return multiOption(name); + final index = after.where(wasNotParsed).length; + if (rest.length > index) return rest.skip(index); + return null; } - List? multiOptionOrRest( - String name, [ - Iterable after = const [], - ]) { - if (wasParsed(name)) return multiOption(name); + String? optionOrRest(String name, [Iterable after = const []]) { + if (wasParsed(name)) return option(name); final index = after.where(wasNotParsed).length; - if (rest.length > index) return rest.skip(index).toList(); + if (rest.length > index) return rest[index]; return null; } - List requiredMultiOptionOrRest( + int requiredIntOptionOrRest( + String name, { + Iterable after = const [], + int? radix, + }) { + final raw = requiredOptionOrRest(name, after); + if (int.tryParse(raw, radix: radix) case final value?) return value; + throw Exception("Invalid integer for $name: $raw"); + } + + Iterable requiredMultiOptionOrRest( String name, [ Iterable after = const [], ]) { @@ -48,8 +54,15 @@ extension ArgResultsExtension on ArgResults { when values.isNotEmpty) { return values; } + throw Exception("Missing required option or argument: $name"); + } - throw UsageException("Missing required option or argument: $name", ""); + String requiredOptionOrRest( + String name, [ + Iterable after = const [], + ]) { + if (optionOrRest(name, after) case final value?) return value; + throw Exception("Missing required option or argument: $name"); } bool wasNotParsed(String name) {