From 3776463cfd17e696376c813af5e83f78b0a5d380 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Wed, 8 Jul 2026 13:44:12 -0300 Subject: [PATCH 1/4] refactor: improve command handling and output consistency across commands --- lib/commands/app/console.dart | 56 ++++++++++++++++----------------- lib/commands/system/locale.dart | 2 +- lib/commands/wait.dart | 2 +- lib/extensions/arg_results.dart | 47 ++++++++++++++------------- 4 files changed, 52 insertions(+), 55 deletions(-) diff --git a/lib/commands/app/console.dart b/lib/commands/app/console.dart index acb78be..196f44e 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/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/wait.dart b/lib/commands/wait.dart index 695ac38..5e68c7f 100644 --- a/lib/commands/wait.dart +++ b/lib/commands/wait.dart @@ -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/extensions/arg_results.dart b/lib/extensions/arg_results.dart index 71f0780..c2a1492 100644 --- a/lib/extensions/arg_results.dart +++ b/lib/extensions/arg_results.dart @@ -1,9 +1,18 @@ import "package:args/args.dart"; -import "package:args/command_runner.dart"; const _whiteSpace = " "; extension ArgResultsExtension on ArgResults { + List? multiOptionOrRest( + String name, [ + Iterable after = const [], + ]) { + if (wasParsed(name)) return multiOption(name); + final index = after.where(wasNotParsed).length; + if (rest.length > index) return rest.skip(index).toList(); + return null; + } + String? optionOrRest(String name, [Iterable after = const []]) { if (wasParsed(name)) return option(name); final index = after.where(wasNotParsed).length; @@ -11,45 +20,35 @@ extension ArgResultsExtension on ArgResults { return null; } - String requiredOptionOrRest( + List requiredMultiOptionOrRest( String name, [ Iterable after = const [], ]) { - if (optionOrRest(name, after) case final value?) return value; + if (multiOptionOrRest(name, after) case final values? + when values.isNotEmpty) { + return values; + } - throw UsageException("Missing required option or argument: $name", ""); + throw Exception("Missing required option or argument: $name"); } - int requiredIntOptionOrRest( + String requiredOptionOrRest( 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 (optionOrRest(name, after) case final value?) return value; - List? multiOptionOrRest( - String name, [ - Iterable after = const [], - ]) { - if (wasParsed(name)) return multiOption(name); - final index = after.where(wasNotParsed).length; - if (rest.length > index) return rest.skip(index).toList(); - return null; + throw Exception("Missing required option or argument: $name"); } - List requiredMultiOptionOrRest( + int requiredIntOptionOrRest( String name, [ Iterable after = const [], ]) { - if (multiOptionOrRest(name, after) case final values? - when values.isNotEmpty) { - return values; - } + final raw = requiredOptionOrRest(name, after); + if (int.tryParse(raw) case final value?) return value; - throw UsageException("Missing required option or argument: $name", ""); + throw Exception("Invalid integer for $name: $raw"); } bool wasNotParsed(String name) { From 885b0985f97554a24325bb15fd83f19bd66323f6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Jul 2026 16:45:21 +0000 Subject: [PATCH 2/4] docs: update 3776463 --- docs/commands.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commands.md b/docs/commands.md index 7267825..9c1535a 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 From 2474f7cf24de39face3fc950183d2f8abd04fb63 Mon Sep 17 00:00:00 2001 From: Gorniaky Date: Wed, 8 Jul 2026 16:48:35 -0300 Subject: [PATCH 3/4] refactor: ensure non-nullable access to argResults in command handling --- lib/commands/app/backup.dart | 2 +- lib/commands/app/commit.dart | 4 +-- lib/commands/app/console.dart | 2 +- lib/commands/app/info.dart | 2 +- lib/commands/app/profile.dart | 10 +++---- lib/commands/app/ram.dart | 2 +- lib/commands/snapshot/download.dart | 4 +-- lib/commands/snapshot/info.dart | 8 +++--- lib/commands/team/backup.dart | 2 +- lib/commands/team/commit.dart | 4 +-- lib/commands/team/ram.dart | 2 +- lib/commands/wait.dart | 2 +- lib/commands/zip.dart | 8 +++--- lib/extensions/arg_results.dart | 44 +++++++++++++++++++---------- 14 files changed, 55 insertions(+), 41 deletions(-) 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 196f44e..b9941f0 100644 --- a/lib/commands/app/console.dart +++ b/lib/commands/app/console.dart @@ -32,7 +32,7 @@ final class AppConsoleCommand extends Command { final spinner = context.printer.spin(start: false); - if (argResults?.multiOptionOrRest("command", const ["app"])?.join(" ") + if (argResults!.multiOptionOrRest("command", const ["app"])?.join(" ") case final command?) { await _send(appId, command, spinner); return; 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/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 5e68c7f..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)); } 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 c2a1492..311daa7 100644 --- a/lib/extensions/arg_results.dart +++ b/lib/extensions/arg_results.dart @@ -3,13 +3,29 @@ import "package:args/args.dart"; const _whiteSpace = " "; extension ArgResultsExtension on ArgResults { - List? multiOptionOrRest( + int? intOption(String name, {int? radix}) { + if (option(name) case final s?) return .tryParse(s, radix: radix); + return null; + } + + int? intOptionOrRest( + String name, { + Iterable after = const [], + int? radix, + }) { + if (optionOrRest(name, after) case final value?) { + return .tryParse(value, radix: radix); + } + return null; + } + + Iterable? multiOptionOrRest( String name, [ Iterable after = const [], ]) { if (wasParsed(name)) return multiOption(name); final index = after.where(wasNotParsed).length; - if (rest.length > index) return rest.skip(index).toList(); + if (rest.length > index) return rest.skip(index); return null; } @@ -20,7 +36,17 @@ extension ArgResultsExtension on ArgResults { 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 [], ]) { @@ -28,7 +54,6 @@ extension ArgResultsExtension on ArgResults { when values.isNotEmpty) { return values; } - throw Exception("Missing required option or argument: $name"); } @@ -37,20 +62,9 @@ extension ArgResultsExtension on ArgResults { Iterable after = const [], ]) { if (optionOrRest(name, after) case final value?) return value; - throw Exception("Missing required option or argument: $name"); } - int requiredIntOptionOrRest( - String name, [ - Iterable after = const [], - ]) { - final raw = requiredOptionOrRest(name, after); - if (int.tryParse(raw) case final value?) return value; - - throw Exception("Invalid integer for $name: $raw"); - } - bool wasNotParsed(String name) { return !wasParsed(name); } From a075ab437a1381bbf85d9cb90b411105ef8257de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 8 Jul 2026 19:49:32 +0000 Subject: [PATCH 4/4] docs: update 2474f7c --- docs/commands.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/commands.md b/docs/commands.md index 9c1535a..dae9ed0 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -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