From 86b165f858de5bae66fef89ea767f7dfba066e01 Mon Sep 17 00:00:00 2001 From: "Matheus Henrique(Aka: TheusHen)" Date: Fri, 3 Jul 2026 21:58:16 -0300 Subject: [PATCH 01/24] Improve CLI parity and harden file handling --- lib/commands/app/apt/install.dart | 19 +++- lib/commands/app/apt/uninstall.dart | 19 +++- lib/commands/app/backup.dart | 54 +++++++++--- lib/commands/app/commit.dart | 10 ++- lib/commands/app/console.dart | 4 +- lib/commands/app/delete.dart | 4 +- lib/commands/app/info.dart | 2 +- lib/commands/app/logs.dart | 88 +++++++++++++++---- lib/commands/app/mod/add.dart | 18 ++-- lib/commands/app/mod/delete.dart | 10 +-- lib/commands/app/mod/edit.dart | 18 ++-- lib/commands/app/mod/info.dart | 15 ++-- lib/commands/app/profile.dart | 4 +- lib/commands/app/ram.dart | 7 +- lib/commands/app/restart.dart | 2 +- lib/commands/app/start.dart | 2 +- lib/commands/app/status.dart | 4 +- lib/commands/app/stop.dart | 2 +- lib/commands/app/upload.dart | 51 ++++++++++- lib/commands/domain/create.dart | 16 ++-- lib/commands/domain/delete.dart | 6 +- lib/commands/domain/edit.dart | 10 +-- lib/commands/domain/info.dart | 9 +- lib/commands/domain/verify.dart | 10 ++- lib/commands/init.dart | 66 ++++++++++---- lib/commands/subdomain/create.dart | 10 ++- lib/commands/subdomain/delete.dart | 6 +- lib/commands/subdomain/info.dart | 4 +- lib/commands/team/backup.dart | 54 +++++++++--- lib/commands/team/commit.dart | 10 ++- lib/commands/team/info.dart | 2 +- lib/commands/team/logs.dart | 88 +++++++++++++++---- lib/commands/team/ram.dart | 7 +- lib/commands/team/restart.dart | 2 +- lib/commands/team/start.dart | 2 +- lib/commands/team/status.dart | 4 +- lib/commands/team/stop.dart | 2 +- lib/commands/user/locale.dart | 24 ++++- lib/commands/zip.dart | 2 +- lib/extensions/command.dart | 47 ++++++++++ lib/services/discloud/api_client.dart | 25 ++++-- lib/services/discloud/constants.dart | 6 +- lib/utils/ascii_table.dart | 16 +++- lib/utils/bytes.dart | 6 +- lib/utils/download.dart | 28 +++--- lib/utils/progress.dart | 6 ++ modules/glob_zipper/lib/src/fs.dart | 2 +- modules/glob_zipper/lib/src/glob_zipper.dart | 42 ++++++++- .../local_store/lib/src/json_local_store.dart | 9 +- test/utils/ascii_table_test.dart | 18 ++++ test/utils/bytes_test.dart | 5 ++ test/utils/progress_test.dart | 13 +++ 52 files changed, 678 insertions(+), 212 deletions(-) create mode 100644 test/utils/ascii_table_test.dart create mode 100644 test/utils/progress_test.dart diff --git a/lib/commands/app/apt/install.dart b/lib/commands/app/apt/install.dart index 6a5f7d0..22d57ce 100644 --- a/lib/commands/app/apt/install.dart +++ b/lib/commands/app/apt/install.dart @@ -8,22 +8,26 @@ import "package:discloud/utils/messages.dart"; final class AppAptInstallCommand extends Command { AppAptInstallCommand() { argParser - ..addOption("app", mandatory: true) - ..addMultiOption("apt", help: appApts.join(",")); + ..addOption("app") + ..addMultiOption("apt", allowed: appApts, help: appApts.join(",")); } @override final name = "install"; + @override + final aliases = const ["i"]; + @override final description = "Install APT on your app"; @override Future run() async { - final appId = argResults!.option("app"); - final apts = argResults!.multiOption("apt"); + final appId = requiredOptionOrRest("app", 0); + final apts = multiOptionOrRest("apt", 1); if (apts.isEmpty) usageException("Apt option cannot be empty"); + _validateApts(apts); final spinner = context.printer.spin(text: "Installing app apt..."); @@ -34,4 +38,11 @@ final class AppAptInstallCommand extends Command { spinner.success(resolveResponseMessage(response)); } + + void _validateApts(List apts) { + final invalid = apts.where((apt) => !appApts.contains(apt)).toList(); + if (invalid.isEmpty) return; + + usageException("Invalid APT: ${invalid.join(", ")}"); + } } diff --git a/lib/commands/app/apt/uninstall.dart b/lib/commands/app/apt/uninstall.dart index 85f3fbe..05d6b97 100644 --- a/lib/commands/app/apt/uninstall.dart +++ b/lib/commands/app/apt/uninstall.dart @@ -8,22 +8,26 @@ import "package:discloud/utils/messages.dart"; final class AppAptUninstallCommand extends Command { AppAptUninstallCommand() { argParser - ..addOption("app", mandatory: true) - ..addMultiOption("apt", help: appApts.join(",")); + ..addOption("app") + ..addMultiOption("apt", allowed: appApts, help: appApts.join(",")); } @override final name = "uninstall"; + @override + final aliases = const ["u"]; + @override final description = "Uninstall APT from your app"; @override Future run() async { - final appId = argResults!.option("app"); - final apts = argResults!.multiOption("apt"); + final appId = requiredOptionOrRest("app", 0); + final apts = multiOptionOrRest("apt", 1); if (apts.isEmpty) usageException("Apt option cannot be empty"); + _validateApts(apts); final spinner = context.printer.spin(text: "Uninstalling app apt..."); @@ -34,4 +38,11 @@ final class AppAptUninstallCommand extends Command { spinner.success(resolveResponseMessage(response)); } + + void _validateApts(List apts) { + final invalid = apts.where((apt) => !appApts.contains(apt)).toList(); + if (invalid.isEmpty) return; + + usageException("Invalid APT: ${invalid.join(", ")}"); + } } diff --git a/lib/commands/app/backup.dart b/lib/commands/app/backup.dart index ca2d9aa..b64c262 100644 --- a/lib/commands/app/backup.dart +++ b/lib/commands/app/backup.dart @@ -6,23 +6,31 @@ import "package:discloud/cli/disposable.dart"; import "package:discloud/cli/spin/ispin.dart"; import "package:discloud/extensions/command.dart"; import "package:discloud/extensions/file.dart"; +import "package:discloud/utils/ascii_table.dart"; import "package:discloud/utils/download.dart"; import "package:discloud/utils/messages.dart"; import "package:discloud/utils/progress.dart"; import "package:discloud/utils/speed_monitor.dart"; +import "package:path/path.dart" hide context; -const _pSep = "/"; +const _defaultBackupDir = "discloud/backups"; final class AppBackupCommand extends Command with Disposable { AppBackupCommand() { argParser - ..addOption("app", mandatory: true, valueHelp: "all") + ..addOption("app", valueHelp: "all") ..addOption( "dir", abbr: "d", - aliases: const ["out"], + aliases: const ["out", "path"], help: "Specifies the destination path for downloading backups. The destination path will be considered a directory.", + ) + ..addFlag( + "save", + abbr: "s", + help: "Save your app backup", + negatable: false, ); } @@ -41,7 +49,9 @@ final class AppBackupCommand extends Command with Disposable { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; + final dir = optionOrRest("dir", 1); + final shouldDownload = argResults!.flag("save") || dir != null; final spinner = context.printer.spin(text: "Fetching backup..."); @@ -51,29 +61,49 @@ final class AppBackupCommand extends Command with Disposable { switch (response["backups"]) { case final Map data: - await _handleSingle(data, spinner); + await _handleSingle( + data, + dir: shouldDownload ? (dir ?? _defaultBackupDir) : null, + spinner: spinner, + ); break; case final List list: - await _handleMulti(list, spinner); + await _handleMulti( + list, + dir: shouldDownload ? (dir ?? _defaultBackupDir) : null, + spinner: spinner, + ); break; } } - Future _handleSingle(Map data, ISpin spinner) async { + Future _handleSingle( + Map data, { + required ISpin spinner, + String? dir, + }) async { if (data["url"] case final String url) { - if (argResults?.option("dir") case final dir?) { + if (dir != null) { final Uri uri = .parse(url); return _download(dir: dir, spinner: spinner, uri: uri); } - context.printer.writeln(url); + context.printer.writeln(mapToVerticalAsciiTable(data)); } } - Future _handleMulti(List list, ISpin spinner) async { + Future _handleMulti( + List list, { + required ISpin spinner, + String? dir, + }) async { + if (dir == null) { + context.printer.writeln(listToAsciiTable(list)); + return; + } + final client = _client = .new(); - final dir = argResults?.option("dir") ?? "."; for (final data in list) { final String appId = data["id"]; @@ -99,7 +129,7 @@ final class AppBackupCommand extends Command with Disposable { HttpClient? client, }) async { final filename = uri.pathSegments.last; - final filepath = "$dir$_pSep$filename"; + final filepath = joinAll([dir, filename]); final file = _file = .new(filepath); final monitor = _monitor = .new(); diff --git a/lib/commands/app/commit.dart b/lib/commands/app/commit.dart index 14cf318..7c3a397 100644 --- a/lib/commands/app/commit.dart +++ b/lib/commands/app/commit.dart @@ -39,12 +39,17 @@ final class AppCommitCommand extends Command with Disposable { Future run() async { final directory = context.workspaceFolder; + final optionAppId = argResults!.option("app"); final appId = - argResults!.option("app") ?? await _getDiscloudConfigAppId(directory); + optionAppId ?? + optionOrRest("app", 0) ?? + await _getDiscloudConfigAppId(directory); if (appId == null) throw Exception("Missing app id"); - final glob = argResults!.multiOption("glob"); + final glob = optionAppId == null + ? multiOptionOrRest("glob", 1, defaults: const ["**"]) + : multiOptionOrRest("glob", 0, defaults: const ["**"]); final spinner = context.printer.spin(text: "Zipping..."); @@ -82,7 +87,6 @@ final class AppCommitCommand extends Command with Disposable { ); }, onUploadDone: () { - _file = null; spinner.start("Processing..."); }, ); diff --git a/lib/commands/app/console.dart b/lib/commands/app/console.dart index 0d9058e..5cf6692 100644 --- a/lib/commands/app/console.dart +++ b/lib/commands/app/console.dart @@ -11,7 +11,7 @@ import "package:tint/tint.dart"; final class AppConsoleCommand extends Command { AppConsoleCommand() { argParser - ..addOption("app", mandatory: true) + ..addOption("app") ..addOption("command"); } @@ -26,7 +26,7 @@ final class AppConsoleCommand extends Command { @override Future run() async { - final appId = argResults!.option("app")!; + final appId = requiredOptionOrRest("app", 0); String? command = argResults!.option("command"); final spinner = context.printer.spin(start: false); diff --git a/lib/commands/app/delete.dart b/lib/commands/app/delete.dart index bc86e52..dd0fbf2 100644 --- a/lib/commands/app/delete.dart +++ b/lib/commands/app/delete.dart @@ -14,7 +14,7 @@ final class AppDeleteCommand extends Command { help: "Skip confirmation prompt", negatable: false, ) - ..addOption("app", mandatory: true); + ..addOption("app"); } @override @@ -25,7 +25,7 @@ final class AppDeleteCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = requiredOptionOrRest("app", 0); final result = argResults!.flag("yes") || diff --git a/lib/commands/app/info.dart b/lib/commands/app/info.dart index 787fd8d..151d711 100644 --- a/lib/commands/app/info.dart +++ b/lib/commands/app/info.dart @@ -33,7 +33,7 @@ final class AppInfoCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; final spinner = context.printer.spin(text: "Fetching app info..."); diff --git a/lib/commands/app/logs.dart b/lib/commands/app/logs.dart index 1882cab..7f617e4 100644 --- a/lib/commands/app/logs.dart +++ b/lib/commands/app/logs.dart @@ -8,20 +8,28 @@ import "package:discloud/extensions/command.dart"; import "package:discloud/utils/messages.dart"; import "package:path/path.dart" hide context; +const _defaultLogsDir = "discloud/logs"; + final class AppLogsCommand extends Command { AppLogsCommand() { argParser ..addOption( "app", - mandatory: true, valueHelp: "all", help: "When set to 'all', this command will automatically download logs and will not display URLs. If the 'out' option is not set, downloads will be made to the current folder.", ) ..addOption( "out", + aliases: const ["path"], help: "Specifies the destination path for downloading logs. When the application option is set to 'all', the destination path will be considered a directory where all downloads will be stored.", + ) + ..addFlag( + "overwrite", + abbr: "o", + help: "Overwrite log files", + negatable: false, ); } @@ -33,7 +41,9 @@ final class AppLogsCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; + final out = optionOrRest("out", 1); + final overwrite = argResults!.flag("overwrite"); final spinner = context.printer.spin(); @@ -43,10 +53,15 @@ final class AppLogsCommand extends Command { switch (response["apps"]) { case final Map data: - await _handleSingle(data, argResults?.option("out"), spinner); + await _handleSingle(data, out, spinner, overwrite: overwrite); break; case final List list: - await _handleMulti(list, spinner); + await _handleMulti( + list, + out: out ?? _defaultLogsDir, + overwrite: overwrite, + spinner: spinner, + ); break; } } @@ -54,29 +69,64 @@ final class AppLogsCommand extends Command { Future _handleSingle( Map data, String? out, - ISpin spinner, - ) async { - if (data["terminal"]?["big"] case final String data) { - if (out case final out?) return _saveLog(data, out, spinner); - context.printer.writeln(data); + ISpin spinner, { + required bool overwrite, + }) async { + if (data["terminal"]?["big"] case final String contents) { + if (out case final out?) { + final appId = data["id"]?.toString() ?? "app"; + return _saveLog( + contents, + _resolveLogPath(out, appId), + spinner, + overwrite: overwrite, + ); + } + context.printer.writeln(contents); } } - Future _handleMulti(List list, ISpin spinner) async { - final out = argResults?.option("out") ?? "."; - + Future _handleMulti( + List list, { + required String out, + required bool overwrite, + required ISpin spinner, + }) async { for (final data in list) { - final String appId = data["id"]; - final filename = "$appId.log"; - final filepath = joinAll([out, filename]); - await _handleSingle(data, filepath, spinner); + await _handleSingle(data, out, spinner, overwrite: overwrite); } } - Future _saveLog(String data, String out, ISpin spinner) async { + Future _saveLog( + String data, + String out, + ISpin spinner, { + required bool overwrite, + }) async { final file = File(out); - await file.create(recursive: true); - await file.writeAsString(stripAnsi(data)); + await file.parent.create(recursive: true); + final contents = _formatLog(data); + if (overwrite) { + await file.writeAsString(contents); + } else { + await file.writeAsString("$contents\n", mode: FileMode.append); + } spinner.success(out); } + + String _resolveLogPath(String out, String appId) { + if (extension(out).isNotEmpty) return out; + + return joinAll([out, "$appId.log"]); + } + + String _formatLog(String data) { + return [ + "", + "".padRight(60, "-"), + DateTime.now().toString(), + "", + stripAnsi(data), + ].join("\n"); + } } diff --git a/lib/commands/app/mod/add.dart b/lib/commands/app/mod/add.dart index 80be8e0..d42d55c 100644 --- a/lib/commands/app/mod/add.dart +++ b/lib/commands/app/mod/add.dart @@ -8,8 +8,8 @@ import "package:discloud/utils/messages.dart"; final class AppModAddCommand extends Command { AppModAddCommand() { argParser - ..addOption("app", mandatory: true) - ..addOption("mod", mandatory: true) + ..addOption("app") + ..addOption("mod") ..addMultiOption("perms", allowed: appModPerms); } @@ -21,9 +21,10 @@ final class AppModAddCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); - final modId = argResults!.option("mod"); - final perms = argResults!.multiOption("perms"); + final appId = requiredOptionOrRest("app", 0); + final modId = requiredOptionOrRest("mod", 1); + final perms = multiOptionOrRest("perms", 2); + _validatePerms(perms); final spinner = context.printer.spin(text: "Adding app MOD..."); @@ -34,4 +35,11 @@ final class AppModAddCommand extends Command { spinner.success(resolveResponseMessage(response)); } + + void _validatePerms(List perms) { + final invalid = perms.where((perm) => !appModPerms.contains(perm)).toList(); + if (invalid.isEmpty) return; + + usageException("Invalid MOD permission: ${invalid.join(", ")}"); + } } diff --git a/lib/commands/app/mod/delete.dart b/lib/commands/app/mod/delete.dart index 34b59c5..b7d5cd5 100644 --- a/lib/commands/app/mod/delete.dart +++ b/lib/commands/app/mod/delete.dart @@ -7,8 +7,8 @@ import "package:discloud/utils/messages.dart"; final class AppModDeleteCommand extends Command { AppModDeleteCommand() { argParser - ..addOption("app", mandatory: true) - ..addOption("mod", mandatory: true); + ..addOption("app") + ..addOption("mod"); } @override @@ -19,12 +19,12 @@ final class AppModDeleteCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); - final modId = argResults!.option("mod"); + final appId = requiredOptionOrRest("app", 0); + final modId = requiredOptionOrRest("mod", 1); final spinner = context.printer.spin(text: "Deleting app MOD..."); - final response = await context.api.delete("/app/$appId/team$modId"); + final response = await context.api.delete("/app/$appId/team/$modId"); spinner.success(resolveResponseMessage(response)); } diff --git a/lib/commands/app/mod/edit.dart b/lib/commands/app/mod/edit.dart index f8eeded..2f76e6f 100644 --- a/lib/commands/app/mod/edit.dart +++ b/lib/commands/app/mod/edit.dart @@ -8,8 +8,8 @@ import "package:discloud/utils/messages.dart"; final class AppModEditCommand extends Command { AppModEditCommand() { argParser - ..addOption("app", mandatory: true) - ..addOption("mod", mandatory: true) + ..addOption("app") + ..addOption("mod") ..addMultiOption("perms", allowed: appModPerms); } @@ -21,9 +21,10 @@ final class AppModEditCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); - final modId = argResults!.option("mod"); - final perms = argResults!.multiOption("perms"); + final appId = requiredOptionOrRest("app", 0); + final modId = requiredOptionOrRest("mod", 1); + final perms = multiOptionOrRest("perms", 2); + _validatePerms(perms); final spinner = context.printer.spin(text: "Editing app MOD..."); @@ -34,4 +35,11 @@ final class AppModEditCommand extends Command { spinner.success(resolveResponseMessage(response)); } + + void _validatePerms(List perms) { + final invalid = perms.where((perm) => !appModPerms.contains(perm)).toList(); + if (invalid.isEmpty) return; + + usageException("Invalid MOD permission: ${invalid.join(", ")}"); + } } diff --git a/lib/commands/app/mod/info.dart b/lib/commands/app/mod/info.dart index f8909b3..48acd02 100644 --- a/lib/commands/app/mod/info.dart +++ b/lib/commands/app/mod/info.dart @@ -1,6 +1,4 @@ import "dart:async"; -import "dart:io"; - import "package:args/command_runner.dart"; import "package:discloud/extensions/command.dart"; import "package:discloud/utils/ascii_table.dart"; @@ -8,7 +6,7 @@ import "package:discloud/utils/messages.dart"; final class AppModInfoCommand extends Command { AppModInfoCommand() { - argParser.addOption("app", mandatory: true); + argParser.addOption("app"); } @override @@ -19,16 +17,17 @@ final class AppModInfoCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = requiredOptionOrRest("app", 0); final spinner = context.printer.spin(text: "Fetching app MODs..."); final response = await context.api.get("/app/$appId/team"); - spinner.success(resolveResponseMessage(response)); - - if (response["app"] case final Map data) { - stdout.writeln(mapToVerticalAsciiTable(data)); + if (response["team"] case final List list when list.isNotEmpty) { + spinner.success(resolveResponseMessage(response)); + context.printer.writeln(listToAsciiTable(list)); + } else { + spinner.success(resolveResponseMessage(response)); } } } diff --git a/lib/commands/app/profile.dart b/lib/commands/app/profile.dart index 81dbc03..d16f3f4 100644 --- a/lib/commands/app/profile.dart +++ b/lib/commands/app/profile.dart @@ -7,7 +7,7 @@ import "package:discloud/utils/messages.dart"; final class AppProfileCommand extends Command { AppProfileCommand() { argParser - ..addOption("app", mandatory: true) + ..addOption("app") ..addOption("name") ..addOption("avatar"); } @@ -21,7 +21,7 @@ final class AppProfileCommand extends Command { @override Future run() async { - final appId = argResults!.option("app")!; + final appId = requiredOptionOrRest("app", 0); final name = argResults?.option("name"); final avatar = argResults?.option("avatar"); diff --git a/lib/commands/app/ram.dart b/lib/commands/app/ram.dart index 03aa634..3abd6c1 100644 --- a/lib/commands/app/ram.dart +++ b/lib/commands/app/ram.dart @@ -9,11 +9,10 @@ import "package:discloud_config/discloud_config.dart"; final class AppRamCommand extends Command { AppRamCommand() { argParser - ..addOption("app", mandatory: true) + ..addOption("app") ..addOption( "amount", aliases: const ["ram"], - mandatory: true, valueHelp: DiscloudRamMinByType.lowest.value.toString(), ); } @@ -26,9 +25,9 @@ final class AppRamCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = requiredOptionOrRest("app", 0); final ramMB = max( - int.parse(argResults!.option("amount")!), + requiredIntOptionOrRest("amount", 1), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/app/restart.dart b/lib/commands/app/restart.dart index 7971977..7a897c2 100644 --- a/lib/commands/app/restart.dart +++ b/lib/commands/app/restart.dart @@ -17,7 +17,7 @@ final class AppRestartCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; final spinner = context.printer.spin(text: "Restarting app..."); diff --git a/lib/commands/app/start.dart b/lib/commands/app/start.dart index 8e78a21..834d0a4 100644 --- a/lib/commands/app/start.dart +++ b/lib/commands/app/start.dart @@ -17,7 +17,7 @@ final class AppStartCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; final spinner = context.printer.spin(text: "Starting app..."); diff --git a/lib/commands/app/status.dart b/lib/commands/app/status.dart index 7f52666..26bef4a 100644 --- a/lib/commands/app/status.dart +++ b/lib/commands/app/status.dart @@ -10,7 +10,7 @@ const _keysToIgnore = {"netIO"}; final class AppStatusCommand extends Command { AppStatusCommand() { - argParser.addOption("app", mandatory: true); + argParser.addOption("app"); } @override @@ -21,7 +21,7 @@ final class AppStatusCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = requiredOptionOrRest("app", 0); final spinner = context.printer.spin(text: "Fetching app status..."); diff --git a/lib/commands/app/stop.dart b/lib/commands/app/stop.dart index d3d8d53..8451d93 100644 --- a/lib/commands/app/stop.dart +++ b/lib/commands/app/stop.dart @@ -17,7 +17,7 @@ final class AppStopCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; final spinner = context.printer.spin(text: "Stopping app..."); diff --git a/lib/commands/app/upload.dart b/lib/commands/app/upload.dart index 5d96e0b..ede32bc 100644 --- a/lib/commands/app/upload.dart +++ b/lib/commands/app/upload.dart @@ -12,6 +12,8 @@ import "package:discloud/utils/zip.dart"; import "package:discloud_config/discloud_config.dart"; import "package:path/path.dart" hide context; +final _configLineBreakPattern = RegExp(r"[\r\n]"); + final class AppUploadCommand extends Command with Disposable { AppUploadCommand() { argParser.addMultiOption("glob", abbr: "g", defaultsTo: const ["**"]); @@ -39,7 +41,7 @@ final class AppUploadCommand extends Command with Disposable { await config.validate(); - final glob = argResults!.multiOption("glob"); + final glob = multiOptionOrRest("glob", 0, defaults: const ["**"]); final spinner = context.printer.spin(text: "Zipping..."); @@ -77,12 +79,57 @@ final class AppUploadCommand extends Command with Disposable { ); }, onUploadDone: () { - _file = null; spinner.start("Processing..."); }, ); spinner.success(resolveResponseMessage(response)); + + if (response["status"] == "ok") { + if (response["app"] case final Map app) { + final updates = {}; + + if (app["avatarURL"] case final String avatarURL) { + updates["AVATAR"] = avatarURL; + } + + if (app["id"] case final String id) updates["ID"] = id; + + await _updateConfigFile(File(configFilePath), updates); + } + } + } + + Future _updateConfigFile(File file, Map updates) async { + if (updates.isEmpty) return; + + final seen = {}; + final lines = await file.readAsLines(); + + for (int i = 0; i < lines.length; i++) { + final line = lines[i]; + final index = line.indexOf("="); + if (index < 1) continue; + + final key = line.substring(0, index).trim(); + final value = updates[key]; + if (value == null) continue; + + lines[i] = "$key=${_configValue(value)}"; + seen.add(key); + } + + for (final entry in updates.entries) { + if (!seen.contains(entry.key)) { + lines.add("${entry.key}=${_configValue(entry.value)}"); + } + } + + await file.writeAsString("${lines.join("\n")}\n"); + } + + String _configValue(String value) { + return value.replaceAll(_configLineBreakPattern, ""); } @override diff --git a/lib/commands/domain/create.dart b/lib/commands/domain/create.dart index 3fafc3f..f2a1ac2 100644 --- a/lib/commands/domain/create.dart +++ b/lib/commands/domain/create.dart @@ -9,8 +9,8 @@ import "package:discloud/utils/messages.dart"; final class CustomdomainCreateCommand extends Command { CustomdomainCreateCommand() { argParser - ..addOption("id", aliases: const ["domain"], mandatory: true) - ..addOption("app", aliases: const ["subdomain"], mandatory: true); + ..addOption("id", aliases: const ["domain"]) + ..addOption("app", aliases: const ["subdomain"], defaultsTo: "all"); } @override @@ -21,18 +21,20 @@ final class CustomdomainCreateCommand extends Command { @override Future run() async { - final String domain = argResults!.option("id")!; - final String subdomain = argResults!.option("app")!; + final domain = requiredOptionOrRest("id", 0); + final subdomain = optionOrRest("app", 1) ?? "all"; - final spinner = context.printer.spin(text: "Creating domain..."); + final spinner = context.printer.spin(text: "Creating $domain..."); final response = await context.api.post( "/customdomain/create", - body: {"appID": subdomain, "domainName": domain}, + body: {"appID": subdomain, "domain": domain}, ); spinner.success(resolveResponseMessage(response)); - stdout.writeln(mapToVerticalAsciiTable(response["domain"])); + if ((response["domain"] ?? response["customdomain"]) case final Map data) { + stdout.writeln(mapToVerticalAsciiTable(data)); + } } } diff --git a/lib/commands/domain/delete.dart b/lib/commands/domain/delete.dart index bbf0749..2645029 100644 --- a/lib/commands/domain/delete.dart +++ b/lib/commands/domain/delete.dart @@ -6,7 +6,7 @@ import "package:discloud/utils/messages.dart"; final class CustomdomainDeleteCommand extends Command { CustomdomainDeleteCommand() { - argParser.addOption("id", aliases: const ["domain"], mandatory: true); + argParser.addOption("id", aliases: const ["domain"]); } @override @@ -17,9 +17,9 @@ final class CustomdomainDeleteCommand extends Command { @override Future run() async { - final String domain = argResults!.option("id")!; + final domain = requiredOptionOrRest("id", 0); - final spinner = context.printer.spin(text: "Deleting domain..."); + final spinner = context.printer.spin(text: "Deleting $domain..."); final response = await context.api.delete("/customdomain/$domain"); diff --git a/lib/commands/domain/edit.dart b/lib/commands/domain/edit.dart index 9ce2d97..a3c48d4 100644 --- a/lib/commands/domain/edit.dart +++ b/lib/commands/domain/edit.dart @@ -8,8 +8,8 @@ import "package:discloud/utils/messages.dart"; final class CustomdomainEditCommand extends Command with Disposable { CustomdomainEditCommand() { argParser - ..addOption("id", aliases: const ["domain"], mandatory: true) - ..addOption("app", aliases: const ["subdomain"], mandatory: true); + ..addOption("id", aliases: const ["domain"]) + ..addOption("app", aliases: const ["subdomain"], defaultsTo: "all"); } @override @@ -20,10 +20,10 @@ final class CustomdomainEditCommand extends Command with Disposable { @override Future run() async { - final String domain = argResults!.option("id")!; - final String subdomain = argResults!.option("app")!; + final domain = requiredOptionOrRest("id", 0); + final subdomain = optionOrRest("app", 1) ?? "all"; - final spinner = context.printer.spin(text: "Editting a domain..."); + final spinner = context.printer.spin(text: "Editting $domain..."); final response = await context.api.put( "/customdomain/$domain/edit", diff --git a/lib/commands/domain/info.dart b/lib/commands/domain/info.dart index 665df61..73a5e2a 100644 --- a/lib/commands/domain/info.dart +++ b/lib/commands/domain/info.dart @@ -19,15 +19,18 @@ final class CustomdomainInfoCommand extends Command { @override Future run() async { - final String domain = argResults!.option("id")!; + final domain = optionOrRest("id", 0) ?? "all"; - final spinner = context.printer.spin(text: "Fetching domain info..."); + final spinner = context.printer.spin(text: "Fetching $domain..."); final response = await context.api.get("/customdomain/$domain"); spinner.success(resolveResponseMessage(response)); - switch (response["customdomain"] ?? response["customdomains"]) { + switch (response["domain"] ?? + response["domains"] ?? + response["customdomain"] ?? + response["customdomains"]) { case final List list: stdout.writeln(listToAsciiTable(list)); break; diff --git a/lib/commands/domain/verify.dart b/lib/commands/domain/verify.dart index 07027ac..c5bb2e0 100644 --- a/lib/commands/domain/verify.dart +++ b/lib/commands/domain/verify.dart @@ -8,7 +8,7 @@ import "package:discloud/utils/messages.dart"; final class CustomdomainVerifyCommand extends Command { CustomdomainVerifyCommand() { - argParser.addOption("id", aliases: const ["domain"], mandatory: true); + argParser.addOption("id", aliases: const ["domain"], defaultsTo: "all"); } @override @@ -19,14 +19,16 @@ final class CustomdomainVerifyCommand extends Command { @override Future run() async { - final String domain = argResults!.option("id")!; + final domain = optionOrRest("id", 0) ?? "all"; - final spinner = context.printer.spin(text: "Verifying a domain..."); + final spinner = context.printer.spin(text: "Verifying $domain..."); final response = await context.api.get("/customdomain/$domain/verify"); spinner.success(resolveResponseMessage(response)); - stdout.writeln(mapToVerticalAsciiTable(response["domain"])); + if ((response["domain"] ?? response["customdomain"]) case final Map data) { + stdout.writeln(mapToVerticalAsciiTable(data)); + } } } diff --git a/lib/commands/init.dart b/lib/commands/init.dart index d2d9766..c8e0735 100644 --- a/lib/commands/init.dart +++ b/lib/commands/init.dart @@ -7,11 +7,14 @@ import "package:discloud/extensions/string.dart"; import "package:discloud/services/discloud/constants.dart"; import "package:discloud_config/discloud_config.dart"; +final _configLineBreakPattern = RegExp(r"[\r\n]"); + final class InitCommand extends Command { InitCommand() { argParser ..addFlag( "autorestart", + aliases: const ["ar"], help: "Determines whether the app should automatically restart if it crashes.", negatable: false, @@ -22,19 +25,36 @@ final class InitCommand extends Command { help: "Overwrite config file", negatable: false, ) + ..addFlag( + "overwrite", + abbr: "o", + help: "Overwrite config file", + negatable: false, + ) + ..addFlag( + "yes", + abbr: "y", + help: "Skip config prompts", + negatable: false, + ) ..addFlag("vlan", help: "Enables private networking", negatable: false) - ..addMultiOption("apt", help: appApts.join(",")) + ..addMultiOption("apt", abbr: "a", help: appApts.join(",")) ..addOption("avatar", help: "Image URL (.gif, .jpeg, .jpg, .png)") + ..addOption( + "engine-version", + aliases: const ["ev", "version"], + help: "current|latest|legacy|suja|x.x.x", + ) ..addOption( "hostname", help: "Custom hostname alias for other apps to reach this one", ) ..addOption("id", help: "User-defined subdomains") - ..addOption("main", help: "Relative file path") - ..addOption("name", help: "1 - 30 characters") - ..addOption("ram", help: "Amount in MB; min 100") - ..addOption("type", allowed: const ["bot", "site"]) - ..addOption("version", help: "current|latest|legacy|suja|x.x.x"); + ..addOption("main", abbr: "m", help: "Relative file path") + ..addOption("name", abbr: "n", help: "1 - 30 characters") + ..addOption("ram", abbr: "r", help: "Amount in MB; min 100") + ..addOption("start", abbr: "s", help: "App start") + ..addOption("type", abbr: "t", allowed: const ["bot", "site"]); } @override @@ -56,17 +76,20 @@ final class InitCommand extends Command { final StringBuffer buffer = .new() ..writeAll([ "# https://docs.discloud.com/en/discloud.config", - if (args.apt case final v when v.isNotEmpty) "APT=${v.join(",")}", - if (args.autorestart case final v when v) "AUTORESTART=$v", - if (args.avatar case final v?) "AVATAR=$v", - if (args.hostname case final v?) "HOSTNAME=$v", - if (args.id case final v?) "ID=$v", - "MAIN=${args.main.orEmpty}", - if (args.name case final v?) "NAME=$v", - if (args.ram case final v?) "RAM=$v", - if (args.type case final v?) "TYPE=$v", - if (args.version case final v?) "VERSION=$v", - if (args.vlan case final v when v) "VLAN=$v", + if (args.apt case final v when v.isNotEmpty) + _configLine("APT", v.join(",")), + if (args.autorestart case final v when v) + _configLine("AUTORESTART", v), + if (args.avatar case final v?) _configLine("AVATAR", v), + if (args.hostname case final v?) _configLine("HOSTNAME", v), + if (args.id case final v?) _configLine("ID", v), + _configLine("MAIN", args.main.orEmpty), + if (args.name case final v?) _configLine("NAME", v), + if (args.ram case final v?) _configLine("RAM", v), + if (args.start case final v?) _configLine("START", v), + if (args.type case final v?) _configLine("TYPE", v), + if (args.version case final v?) _configLine("VERSION", v), + if (args.vlan case final v when v) _configLine("VLAN", v), ], "\n"); final sink = file.openWrite()..write(buffer); @@ -84,7 +107,7 @@ class _InitArgs { List _multiOption(String n) => _argResults?.multiOption(n) ?? []; String? _option(String name) => _argResults?.option(name); - bool get force => _flag("force"); + bool get force => _flag("force") || _flag("overwrite"); List get apt => _multiOption("apt"); bool get autorestart => _flag("autorestart"); @@ -94,7 +117,12 @@ class _InitArgs { String? get main => _option("main"); String? get name => _option("name"); String? get ram => _option("ram"); + String? get start => _option("start"); String? get type => _option("type"); - String? get version => _option("version"); + String? get version => _option("engine-version"); bool get vlan => _flag("vlan"); } + +String _configLine(String key, Object value) { + return "$key=${value.toString().replaceAll(_configLineBreakPattern, "")}"; +} diff --git a/lib/commands/subdomain/create.dart b/lib/commands/subdomain/create.dart index ca30588..fd36aa3 100644 --- a/lib/commands/subdomain/create.dart +++ b/lib/commands/subdomain/create.dart @@ -8,7 +8,7 @@ import "package:discloud/utils/messages.dart"; final class SubdomainCreateCommand extends Command { SubdomainCreateCommand() { - argParser.addOption("id", aliases: const ["subdomain"], mandatory: true); + argParser.addOption("id", aliases: const ["subdomain"]); } @override @@ -19,14 +19,16 @@ final class SubdomainCreateCommand extends Command { @override Future run() async { - final String id = argResults!.option("id")!; + final id = requiredOptionOrRest("id", 0); - final spinner = context.printer.spin(text: "Creating subdomain..."); + final spinner = context.printer.spin(text: "Creating $id..."); final response = await context.api.post("/subdomain/$id"); spinner.success(resolveResponseMessage(response)); - stdout.writeln(mapToVerticalAsciiTable(response["subdomain"])); + if (response["subdomain"] case final Map data) { + stdout.writeln(mapToVerticalAsciiTable(data)); + } } } diff --git a/lib/commands/subdomain/delete.dart b/lib/commands/subdomain/delete.dart index 39dfec9..0109291 100644 --- a/lib/commands/subdomain/delete.dart +++ b/lib/commands/subdomain/delete.dart @@ -6,7 +6,7 @@ import "package:discloud/utils/messages.dart"; final class SubdomainDeleteCommand extends Command { SubdomainDeleteCommand() { - argParser.addOption("id", aliases: const ["subdomain"], mandatory: true); + argParser.addOption("id", aliases: const ["subdomain"]); } @override @@ -17,9 +17,9 @@ final class SubdomainDeleteCommand extends Command { @override Future run() async { - final String id = argResults!.option("id")!; + final id = requiredOptionOrRest("id", 0); - final spinner = context.printer.spin(text: "Deleting subdomain..."); + final spinner = context.printer.spin(text: "Deleting $id..."); final response = await context.api.delete("/subdomain/$id"); diff --git a/lib/commands/subdomain/info.dart b/lib/commands/subdomain/info.dart index 0c3abb6..f0947db 100644 --- a/lib/commands/subdomain/info.dart +++ b/lib/commands/subdomain/info.dart @@ -19,9 +19,9 @@ final class SubdomainInfoCommand extends Command { @override Future run() async { - final String id = argResults!.option("id")!; + final id = optionOrRest("id", 0) ?? "all"; - final spinner = context.printer.spin(text: "Fetching subdomain info..."); + final spinner = context.printer.spin(text: "Fetching $id..."); final response = await context.api.get("/subdomain/$id"); diff --git a/lib/commands/team/backup.dart b/lib/commands/team/backup.dart index c3b2732..342ad9b 100644 --- a/lib/commands/team/backup.dart +++ b/lib/commands/team/backup.dart @@ -6,23 +6,31 @@ import "package:discloud/cli/disposable.dart"; import "package:discloud/cli/spin/ispin.dart"; import "package:discloud/extensions/command.dart"; import "package:discloud/extensions/file.dart"; +import "package:discloud/utils/ascii_table.dart"; import "package:discloud/utils/download.dart"; import "package:discloud/utils/messages.dart"; import "package:discloud/utils/progress.dart"; import "package:discloud/utils/speed_monitor.dart"; +import "package:path/path.dart" hide context; -const _pSep = "/"; +const _defaultBackupDir = "discloud/backups"; final class TeamBackupCommand extends Command with Disposable { TeamBackupCommand() { argParser - ..addOption("app", mandatory: true, valueHelp: "all") + ..addOption("app", valueHelp: "all") ..addOption( "dir", abbr: "d", - aliases: const ["out"], + aliases: const ["out", "path"], help: "Specifies the destination path for downloading backups. The destination path will be considered a directory.", + ) + ..addFlag( + "save", + abbr: "s", + help: "Save your app backup", + negatable: false, ); } @@ -41,7 +49,9 @@ final class TeamBackupCommand extends Command with Disposable { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; + final dir = optionOrRest("dir", 1); + final shouldDownload = argResults!.flag("save") || dir != null; final spinner = context.printer.spin(text: "Fetching backup..."); @@ -51,29 +61,49 @@ final class TeamBackupCommand extends Command with Disposable { switch (response["backups"]) { case final Map data: - await _handleSingle(data, spinner); + await _handleSingle( + data, + dir: shouldDownload ? (dir ?? _defaultBackupDir) : null, + spinner: spinner, + ); break; case final List list: - await _handleMulti(list, spinner); + await _handleMulti( + list, + dir: shouldDownload ? (dir ?? _defaultBackupDir) : null, + spinner: spinner, + ); break; } } - Future _handleSingle(Map data, ISpin spinner) async { + Future _handleSingle( + Map data, { + required ISpin spinner, + String? dir, + }) async { if (data["url"] case final String url) { - if (argResults?.option("dir") case final dir?) { + if (dir != null) { final Uri uri = .parse(url); return _download(dir: dir, spinner: spinner, uri: uri); } - context.printer.writeln(url); + context.printer.writeln(mapToVerticalAsciiTable(data)); } } - Future _handleMulti(List list, ISpin spinner) async { + Future _handleMulti( + List list, { + required ISpin spinner, + String? dir, + }) async { + if (dir == null) { + context.printer.writeln(listToAsciiTable(list)); + return; + } + final client = _client = .new(); - final dir = argResults?.option("dir") ?? "."; for (final data in list) { final String appId = data["id"]; @@ -99,7 +129,7 @@ final class TeamBackupCommand extends Command with Disposable { HttpClient? client, }) async { final filename = uri.pathSegments.last; - final filepath = "$dir$_pSep$filename"; + final filepath = joinAll([dir, filename]); final file = _file = .new(filepath); final monitor = _monitor = .new(); diff --git a/lib/commands/team/commit.dart b/lib/commands/team/commit.dart index 1bfb7af..b998713 100644 --- a/lib/commands/team/commit.dart +++ b/lib/commands/team/commit.dart @@ -39,12 +39,17 @@ final class TeamCommitCommand extends Command with Disposable { Future run() async { final directory = context.workspaceFolder; + final optionAppId = argResults!.option("app"); final appId = - argResults!.option("app") ?? await _getDiscloudConfigAppId(directory); + optionAppId ?? + optionOrRest("app", 0) ?? + await _getDiscloudConfigAppId(directory); if (appId == null) throw Exception("Missing app id"); - final glob = argResults!.multiOption("glob"); + final glob = optionAppId == null + ? multiOptionOrRest("glob", 1, defaults: const ["**"]) + : multiOptionOrRest("glob", 0, defaults: const ["**"]); final spinner = context.printer.spin(text: "Zipping..."); @@ -82,7 +87,6 @@ final class TeamCommitCommand extends Command with Disposable { ); }, onUploadDone: () { - _file = null; spinner.start("Processing..."); }, ); diff --git a/lib/commands/team/info.dart b/lib/commands/team/info.dart index 0484575..1e38a60 100644 --- a/lib/commands/team/info.dart +++ b/lib/commands/team/info.dart @@ -32,7 +32,7 @@ final class TeamInfoCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0); final spinner = context.printer.spin(text: "Fetching app info..."); diff --git a/lib/commands/team/logs.dart b/lib/commands/team/logs.dart index c0e0125..ddb558f 100644 --- a/lib/commands/team/logs.dart +++ b/lib/commands/team/logs.dart @@ -8,20 +8,28 @@ import "package:discloud/extensions/command.dart"; import "package:discloud/utils/messages.dart"; import "package:path/path.dart" hide context; +const _defaultLogsDir = "discloud/logs"; + final class TeamLogsCommand extends Command { TeamLogsCommand() { argParser ..addOption( "app", - mandatory: true, valueHelp: "all", help: "When set to 'all', this command will automatically download logs and will not display URLs. If the 'out' option is not set, downloads will be made to the current folder.", ) ..addOption( "out", + aliases: const ["path"], help: "Specifies the destination path for downloading logs. When the application option is set to 'all', the destination path will be considered a directory where all downloads will be stored.", + ) + ..addFlag( + "overwrite", + abbr: "o", + help: "Overwrite log files", + negatable: false, ); } @@ -33,7 +41,9 @@ final class TeamLogsCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; + final out = optionOrRest("out", 1); + final overwrite = argResults!.flag("overwrite"); final spinner = context.printer.spin(); @@ -43,10 +53,15 @@ final class TeamLogsCommand extends Command { switch (response["apps"]) { case final Map data: - await _handleSingle(data, argResults?.option("out"), spinner); + await _handleSingle(data, out, spinner, overwrite: overwrite); break; case final List list: - await _handleMulti(list, spinner); + await _handleMulti( + list, + out: out ?? _defaultLogsDir, + overwrite: overwrite, + spinner: spinner, + ); break; } } @@ -54,29 +69,64 @@ final class TeamLogsCommand extends Command { Future _handleSingle( Map data, String? out, - ISpin spinner, - ) async { - if (data["terminal"]?["big"] case final String data) { - if (out case final out?) return _saveLog(data, out, spinner); - context.printer.writeln(data); + ISpin spinner, { + required bool overwrite, + }) async { + if (data["terminal"]?["big"] case final String contents) { + if (out case final out?) { + final appId = data["id"]?.toString() ?? "app"; + return _saveLog( + contents, + _resolveLogPath(out, appId), + spinner, + overwrite: overwrite, + ); + } + context.printer.writeln(contents); } } - Future _handleMulti(List list, ISpin spinner) async { - final out = argResults?.option("out") ?? "."; - + Future _handleMulti( + List list, { + required String out, + required bool overwrite, + required ISpin spinner, + }) async { for (final data in list) { - final String appId = data["id"]; - final filename = "$appId.log"; - final filepath = joinAll([out, filename]); - await _handleSingle(data, filepath, spinner); + await _handleSingle(data, out, spinner, overwrite: overwrite); } } - Future _saveLog(String data, String out, ISpin spinner) async { + Future _saveLog( + String data, + String out, + ISpin spinner, { + required bool overwrite, + }) async { final file = File(out); - await file.create(recursive: true); - await file.writeAsString(stripAnsi(data)); + await file.parent.create(recursive: true); + final contents = _formatLog(data); + if (overwrite) { + await file.writeAsString(contents); + } else { + await file.writeAsString("$contents\n", mode: FileMode.append); + } spinner.success(out); } + + String _resolveLogPath(String out, String appId) { + if (extension(out).isNotEmpty) return out; + + return joinAll([out, "$appId.log"]); + } + + String _formatLog(String data) { + return [ + "", + "".padRight(60, "-"), + DateTime.now().toString(), + "", + stripAnsi(data), + ].join("\n"); + } } diff --git a/lib/commands/team/ram.dart b/lib/commands/team/ram.dart index c4222f4..f03c721 100644 --- a/lib/commands/team/ram.dart +++ b/lib/commands/team/ram.dart @@ -9,11 +9,10 @@ import "package:discloud_config/discloud_config.dart"; final class TeamRamCommand extends Command { TeamRamCommand() { argParser - ..addOption("app", mandatory: true) + ..addOption("app") ..addOption( "amount", aliases: const ["ram"], - mandatory: true, valueHelp: DiscloudRamMinByType.lowest.value.toString(), ); } @@ -26,9 +25,9 @@ final class TeamRamCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = requiredOptionOrRest("app", 0); final ramMB = max( - int.parse(argResults!.option("amount")!), + requiredIntOptionOrRest("amount", 1), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/team/restart.dart b/lib/commands/team/restart.dart index c0038b8..f109a91 100644 --- a/lib/commands/team/restart.dart +++ b/lib/commands/team/restart.dart @@ -17,7 +17,7 @@ final class TeamRestartCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; final spinner = context.printer.spin(text: "Restarting app..."); diff --git a/lib/commands/team/start.dart b/lib/commands/team/start.dart index c7b6655..fe02b7b 100644 --- a/lib/commands/team/start.dart +++ b/lib/commands/team/start.dart @@ -17,7 +17,7 @@ final class TeamStartCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; final spinner = context.printer.spin(text: "Starting app..."); diff --git a/lib/commands/team/status.dart b/lib/commands/team/status.dart index b9b7430..8d373e4 100644 --- a/lib/commands/team/status.dart +++ b/lib/commands/team/status.dart @@ -10,7 +10,7 @@ const _keysToIgnore = {"netIO"}; final class TeamStatusCommand extends Command { TeamStatusCommand() { - argParser.addOption("app", mandatory: true); + argParser.addOption("app"); } @override @@ -21,7 +21,7 @@ final class TeamStatusCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = requiredOptionOrRest("app", 0); final spinner = context.printer.spin(text: "Fetching app status..."); diff --git a/lib/commands/team/stop.dart b/lib/commands/team/stop.dart index 3e3e523..d3f65e5 100644 --- a/lib/commands/team/stop.dart +++ b/lib/commands/team/stop.dart @@ -17,7 +17,7 @@ final class TeamStopCommand extends Command { @override Future run() async { - final appId = argResults!.option("app"); + final appId = optionOrRest("app", 0) ?? "all"; final spinner = context.printer.spin(text: "Stopping app..."); diff --git a/lib/commands/user/locale.dart b/lib/commands/user/locale.dart index a5735f7..b4aa5fe 100644 --- a/lib/commands/user/locale.dart +++ b/lib/commands/user/locale.dart @@ -1,12 +1,14 @@ import "package:args/command_runner.dart"; import "package:discloud/cli/context.dart"; import "package:discloud/extensions/command.dart"; +import "package:discloud/services/discloud/constants.dart"; import "package:discloud/utils/messages.dart"; +import "package:interact/interact.dart"; final class UserLocaleCommand extends Command { UserLocaleCommand() { argParser - ..addOption("locale", abbr: "l", mandatory: true, valueHelp: localeName) + ..addOption("locale", abbr: "l", valueHelp: localeName) ..addFlag( "system", abbr: "s", @@ -25,7 +27,7 @@ final class UserLocaleCommand extends Command { Future run() async { final system = argResults!.flag("system"); - final locale = system ? context.locale : argResults!.option("locale"); + final locale = _resolveLocale(system); final spinner = context.printer.spin( text: "Defining the user's locale to $locale...", @@ -35,4 +37,22 @@ final class UserLocaleCommand extends Command { spinner.success(resolveResponseMessage(response)); } + + String _resolveLocale(bool system) { + final locale = switch ((system, argResults!.option("locale"))) { + (true, _) => context.locale, + (_, final String value) => value, + _ => apiLocales.elementAt( + Select(prompt: "Choose your locale", options: apiLocales.toList()) + .interact(), + ), + } + .replaceAll("_", "-"); + + if (!apiLocales.contains(locale)) { + usageException("Unsupported locale: $locale (${apiLocales.join(", ")})"); + } + + return locale; + } } diff --git a/lib/commands/zip.dart b/lib/commands/zip.dart index 2fe6c9e..e143fa2 100644 --- a/lib/commands/zip.dart +++ b/lib/commands/zip.dart @@ -44,7 +44,7 @@ final class ZipCommand extends Command with Disposable { final encoding = argResults?.option("encoding"); final out = argResults?.option("out") ?? "${basename(directory.path)}.zip"; - final glob = argResults?.multiOption("glob") ?? const ["**"]; + final glob = multiOptionOrRest("glob", 0, defaults: const ["**"]); final level = _compressionLevel; final password = argResults?.option("password"); diff --git a/lib/extensions/command.dart b/lib/extensions/command.dart index 4d2b1f9..166b270 100644 --- a/lib/extensions/command.dart +++ b/lib/extensions/command.dart @@ -9,4 +9,51 @@ extension CommandExtension on Command { addSubcommand(command); } } + + String? optionOrRest(String name, int index) { + final parsed = argResults?.wasParsed(name) ?? false; + if (parsed) { + final value = argResults?.option(name); + if (value != null && value.isNotEmpty) return value; + } + + final rest = argResults?.rest ?? const []; + if (rest.length > index) return rest[index]; + + final value = argResults?.option(name); + if (value != null && value.isNotEmpty) return value; + + return null; + } + + String requiredOptionOrRest(String name, int index) { + final value = optionOrRest(name, index); + if (value != null) return value; + + usageException("Missing required option or argument: $name"); + throw StateError("unreachable"); + } + + int requiredIntOptionOrRest(String name, int index) { + final raw = requiredOptionOrRest(name, index); + final value = int.tryParse(raw); + if (value != null) return value; + + usageException("Invalid integer for $name: $raw"); + throw StateError("unreachable"); + } + + List multiOptionOrRest( + String name, + int index, { + List defaults = const [], + }) { + final rest = argResults?.rest ?? const []; + if (rest.length > index) return rest.skip(index).toList(); + + final values = argResults?.multiOption(name) ?? const []; + if (values.isNotEmpty) return values; + + return defaults; + } } diff --git a/lib/services/discloud/api_client.dart b/lib/services/discloud/api_client.dart index 21c59be..a28a525 100644 --- a/lib/services/discloud/api_client.dart +++ b/lib/services/discloud/api_client.dart @@ -4,7 +4,6 @@ import "dart:io"; import "package:discloud/cli/context.dart"; import "package:discloud/cli/disposable.dart"; -import "package:discloud/extensions/file.dart"; import "package:discloud/extensions/io_http_client.dart"; import "package:discloud/extensions/string.dart"; import "package:discloud/services/discloud/exception.dart"; @@ -271,8 +270,8 @@ class DiscloudApiClient implements Disposable { }) async { final now = DateTime.now(); final boundary = "formBoundary${now.microsecondsSinceEpoch}"; - final filename = file.uri.pathSegments.last; - final fileExtension = extension(filename); + final filename = _escapeMultipartValue(file.uri.pathSegments.last); + final contentType = _multipartContentType(file); await _prepareRequest(request, headers: headers ??= {}); @@ -287,7 +286,7 @@ class DiscloudApiClient implements Disposable { request.write( "--$boundary\r\n" "Content-Disposition: form-data; " - 'name="${e.key}"\r\n\r\n${e.value}\r\n', + 'name="${_escapeMultipartValue(e.key)}"\r\n\r\n${e.value}\r\n', ); } } @@ -297,7 +296,7 @@ class DiscloudApiClient implements Disposable { "Content-Disposition: form-data; " 'name="file"; ' 'filename="$filename"\r\n' - "Content-Type: application/$fileExtension\r\n\r\n", + "Content-Type: $contentType\r\n\r\n", ); int processed = 0; @@ -309,11 +308,23 @@ class DiscloudApiClient implements Disposable { request.write("\r\n--$boundary--\r\n"); - await file.safeDelete(); - await onUploadDone?.call(); } + String _escapeMultipartValue(String value) { + return value + .replaceAll('"', "%22") + .replaceAll("\r", "") + .replaceAll("\n", ""); + } + + String _multipartContentType(File file) { + return switch (extension(file.path).toLowerCase()) { + ".zip" => "application/zip", + _ => "application/octet-stream", + }; + } + Future _prepareRequest( HttpClientRequest request, { Map? headers, diff --git a/lib/services/discloud/constants.dart b/lib/services/discloud/constants.dart index c693c23..e1bd3f1 100644 --- a/lib/services/discloud/constants.dart +++ b/lib/services/discloud/constants.dart @@ -4,6 +4,8 @@ const discloudApikeyURL = "$discloudDashboardURL/apikey"; const discloudLegalURL = "$discloudURL/legal"; const discloudPlansURL = "$discloudURL/plans"; +const apiLocales = {"en-US", "pt-BR"}; + const Map> blockedFiles = { "common": {".git", ".vscode", ".cache", "temp"}, "go": {}, @@ -30,13 +32,9 @@ const appApts = { "ffmpeg", "java", "libgl", - "mysql", "openssl", "puppeteer", - "selenium", - "tesseract", "tools", - "unixodbc", }; const appModPerms = { diff --git a/lib/utils/ascii_table.dart b/lib/utils/ascii_table.dart index 58748a5..96c89f9 100644 --- a/lib/utils/ascii_table.dart +++ b/lib/utils/ascii_table.dart @@ -4,6 +4,8 @@ String mapToVerticalAsciiTable( Map data, [ Set keysToIgnore = const {}, ]) { + if (data.isEmpty) return ""; + final List> rows = []; for (final entry in data.entries) { @@ -24,13 +26,25 @@ String listToAsciiTable( List list, [ Set keysToIgnore = const {}, ]) { + if (list.isEmpty) return ""; + + final first = list.first; + if (first is! Map) { + return tabular([ + ["#", "value"], + for (int i = 0; i < list.length; i++) [i + 1, list[i]], + ]); + } + final keys = []; final List> rows = [keys]; - keys.addAll(["#", ...list[0].keys.where((k) => !keysToIgnore.contains(k))]); + keys.addAll(["#", ...first.keys.where((k) => !keysToIgnore.contains(k))]); for (int i = 0; i < list.length; i++) { final data = list[i]; + if (data is! Map) continue; + final row = [i + 1]; rows.add(row); diff --git a/lib/utils/bytes.dart b/lib/utils/bytes.dart index 9c2f3c1..2d6c6ba 100644 --- a/lib/utils/bytes.dart +++ b/lib/utils/bytes.dart @@ -40,10 +40,10 @@ class Bytes { double get bytes { if (_isInvalid) return 0; - return n / pow(dimension.value, index); + return n / pow(dimension.value, unitIndex); } - int get unitIndex => index.clamp(0, type.units.length); + int get unitIndex => index.clamp(0, type.units.length - 1).toInt(); bool get _isInvalid => n.isNaN || n.isInfinite || n.isZero || n.isNegative; @@ -165,7 +165,7 @@ class _Bits extends Bytes { UnitType get type => .bit; @override - int get unitIndex => index.clamp(0, type.units.length); + int get unitIndex => index.clamp(0, type.units.length - 1).toInt(); @override String toString([String separator = ""]) { diff --git a/lib/utils/download.dart b/lib/utils/download.dart index 9276059..f18ffa2 100644 --- a/lib/utils/download.dart +++ b/lib/utils/download.dart @@ -1,5 +1,7 @@ import "dart:io"; +import "package:discloud/extensions/io_http_client.dart"; + typedef VoidProgressCallback = void Function(int processed, int total); Future download( @@ -9,41 +11,47 @@ Future download( VoidProgressCallback? onProgress, }) async { file ??= .new(url.pathSegments.last); - await file.create(recursive: true); - final sink = file.openWrite(); + await file.parent.create(recursive: true); final client0 = client ?? .new(); + IOSink? sink; try { final request = await client0.getUrl(url); + final response = await request.close(); + if (!response.ok) { + throw HttpException( + "Download failed: ${response.statusCode} ${response.reasonPhrase}", + uri: url, + ); + } + sink = file.openWrite(); if (onProgress case final onProgress?) { await _downloadWithProgress( onProgress: onProgress, - responseFactory: request.close, + response: response, sink: sink, ); } else { - final response = await request.close(); await sink.addStream(response); } } catch (_) { - await sink.close(); - await file.delete(); + await sink?.close(); + sink = null; + if (await file.exists()) await file.delete(); rethrow; } finally { - await sink.close(); + await sink?.close(); if (client == null) client0.close(); } } Future _downloadWithProgress({ required VoidProgressCallback onProgress, - required Future Function() responseFactory, + required HttpClientResponse response, required IOSink sink, }) async { - final response = await responseFactory(); - final total = response.contentLength; int processed = 0; diff --git a/lib/utils/progress.dart b/lib/utils/progress.dart index e772d0a..0462c7e 100644 --- a/lib/utils/progress.dart +++ b/lib/utils/progress.dart @@ -27,6 +27,12 @@ String formatProgressMessage({ buffer.writeAll([" ", ?direction, Bytes.bits(speed * 8), "/s "]); } + if (total <= 0) { + buffer.write(" "); + buffer.write(Bytes(processed).toString(" ")); + return buffer.toString(); + } + buffer.write(percentFormatter.format(processed / total)); return buffer.toString(); diff --git a/modules/glob_zipper/lib/src/fs.dart b/modules/glob_zipper/lib/src/fs.dart index b9d458b..98437d4 100644 --- a/modules/glob_zipper/lib/src/fs.dart +++ b/modules/glob_zipper/lib/src/fs.dart @@ -81,7 +81,7 @@ class FS { } Future _resolveIgnoreFile(String folder, String filename) async { - final File file = .new("$folder$_pSep$filename"); + final File file = .new(joinAll([folder, filename])); if (!await file.exists()) return false; diff --git a/modules/glob_zipper/lib/src/glob_zipper.dart b/modules/glob_zipper/lib/src/glob_zipper.dart index 9d721e6..0321b2e 100644 --- a/modules/glob_zipper/lib/src/glob_zipper.dart +++ b/modules/glob_zipper/lib/src/glob_zipper.dart @@ -74,13 +74,14 @@ class GlobZipper { bool success = false; try { + final rootPath = await directory.resolveSymbolicLinks(); final encoder = ZipFileEncoder(password: password) ..create(zipfile.path, level: level); if (onData case final onData?) { - await _zipWithCallback(encoder, fs.list, onData, onError); + await _zipWithCallback(encoder, fs.list, onData, rootPath, onError); } else { - await _zipWithoutCallback(encoder, fs.list, onError); + await _zipWithoutCallback(encoder, fs.list, rootPath, onError); } await encoder.close(); @@ -97,12 +98,17 @@ class GlobZipper { Future _zipWithoutCallback( ZipFileEncoder encoder, Stream Function() fileStreamFactory, [ + String? rootPath, OnErrorCallback? onError, ]) async { await for (final file in fileStreamFactory().handleError( onError ?? _noop, )) { - final aFile = await _toArchiveFile(file, root: directory); + final aFile = await _toArchiveFile( + file, + root: directory, + rootPath: rootPath, + ); encoder.addArchiveFile(aFile); } @@ -112,12 +118,14 @@ class GlobZipper { ZipFileEncoder encoder, Stream Function() fileStreamFactory, ZipCallback onData, [ + String? rootPath, OnErrorCallback? onError, ]) async { int processed = 0, i = 0; await for (final file in fileStreamFactory().handleError( onError ?? _noop, )) { + await _ensureInsideRoot(file, directory, rootPath); final stat = await file.stat(); onData( @@ -130,7 +138,13 @@ class GlobZipper { ), ); - final aFile = await _toArchiveFile(file, root: directory, stat: stat); + final aFile = await _toArchiveFile( + file, + root: directory, + rootPath: rootPath, + stat: stat, + trusted: true, + ); encoder.addArchiveFile(aFile); @@ -141,8 +155,12 @@ class GlobZipper { Future _toArchiveFile( File file, { required Directory root, + String? rootPath, FileStat? stat, + bool trusted = false, }) async { + if (!trusted) await _ensureInsideRoot(file, root, rootPath); + stat ??= await file.stat(); final filename = relative(file.path, from: root.path); @@ -154,4 +172,20 @@ class GlobZipper { ..lastModTime = stat.modified.secondsSinceEpoch ..mode = stat.mode; } + + Future _ensureInsideRoot( + File file, + Directory root, + String? rootPath, + ) async { + rootPath ??= await root.resolveSymbolicLinks(); + final filePath = await file.resolveSymbolicLinks(); + + if (equals(rootPath, filePath) || isWithin(rootPath, filePath)) return; + + throw FileSystemException( + "Refusing to zip a file outside the root directory", + file.path, + ); + } } diff --git a/modules/local_store/lib/src/json_local_store.dart b/modules/local_store/lib/src/json_local_store.dart index b4a6639..c4b4f67 100644 --- a/modules/local_store/lib/src/json_local_store.dart +++ b/modules/local_store/lib/src/json_local_store.dart @@ -33,8 +33,13 @@ class _JSONLocalStore implements LocalStore { } Future _save() async { - if (!await _file.exists()) await _file.create(recursive: true); - await _file.writeAsBytes(_jsonBase64Encoder.convert(_cache), flush: true); + await _file.parent.create(recursive: true); + + final temp = File("${_file.path}.tmp"); + await temp.writeAsBytes(_jsonBase64Encoder.convert(_cache), flush: true); + + if (await _file.exists()) await _file.delete(); + await temp.rename(_file.path); } @override diff --git a/test/utils/ascii_table_test.dart b/test/utils/ascii_table_test.dart new file mode 100644 index 0000000..18c5cee --- /dev/null +++ b/test/utils/ascii_table_test.dart @@ -0,0 +1,18 @@ +import "package:discloud/utils/ascii_table.dart"; +import "package:test/test.dart"; + +void main() { + group("ascii table", () { + test("empty maps do not throw", () { + expect(mapToVerticalAsciiTable(const {}), ""); + }); + + test("empty lists do not throw", () { + expect(listToAsciiTable(const []), ""); + }); + + test("scalar lists do not throw", () { + expect(listToAsciiTable(const ["ok"]), contains("ok")); + }); + }); +} diff --git a/test/utils/bytes_test.dart b/test/utils/bytes_test.dart index 2365bd5..25902c0 100644 --- a/test/utils/bytes_test.dart +++ b/test/utils/bytes_test.dart @@ -12,6 +12,11 @@ void main() { const Bytes bytes = .new(1024 * 1024); expect(bytes.toString(), "1.0MB"); }); + + test("huge values clamp to the largest unit", () { + const Bytes bytes = .new(1e30); + expect(bytes.toString().endsWith("YB"), isTrue); + }); }); group("Bits", () { diff --git a/test/utils/progress_test.dart b/test/utils/progress_test.dart new file mode 100644 index 0000000..f646019 --- /dev/null +++ b/test/utils/progress_test.dart @@ -0,0 +1,13 @@ +import "package:discloud/utils/progress.dart"; +import "package:test/test.dart"; + +void main() { + group("formatProgressMessage", () { + test("handles unknown totals", () { + expect( + formatProgressMessage(processed: 1024, total: -1), + contains("1.0 KB"), + ); + }); + }); +} From 3b94435753f026ca049a054489f300407466805d Mon Sep 17 00:00:00 2001 From: "Matheus Henrique(Aka: TheusHen)" Date: Fri, 3 Jul 2026 22:10:26 -0300 Subject: [PATCH 02/24] Address Dart analyzer feedback --- lib/commands/domain/create.dart | 2 +- lib/commands/domain/verify.dart | 2 +- lib/commands/init.dart | 10 ++-------- lib/commands/user/locale.dart | 9 +++++---- lib/extensions/command.dart | 2 -- lib/utils/bytes.dart | 4 ++-- lib/utils/progress.dart | 5 +++-- 7 files changed, 14 insertions(+), 20 deletions(-) diff --git a/lib/commands/domain/create.dart b/lib/commands/domain/create.dart index f2a1ac2..bbb5495 100644 --- a/lib/commands/domain/create.dart +++ b/lib/commands/domain/create.dart @@ -33,7 +33,7 @@ final class CustomdomainCreateCommand extends Command { spinner.success(resolveResponseMessage(response)); - if ((response["domain"] ?? response["customdomain"]) case final Map data) { + if (response["domain"] ?? response["customdomain"] case final Map data) { stdout.writeln(mapToVerticalAsciiTable(data)); } } diff --git a/lib/commands/domain/verify.dart b/lib/commands/domain/verify.dart index c5bb2e0..53818f1 100644 --- a/lib/commands/domain/verify.dart +++ b/lib/commands/domain/verify.dart @@ -27,7 +27,7 @@ final class CustomdomainVerifyCommand extends Command { spinner.success(resolveResponseMessage(response)); - if ((response["domain"] ?? response["customdomain"]) case final Map data) { + if (response["domain"] ?? response["customdomain"] case final Map data) { stdout.writeln(mapToVerticalAsciiTable(data)); } } diff --git a/lib/commands/init.dart b/lib/commands/init.dart index c8e0735..c9e73ef 100644 --- a/lib/commands/init.dart +++ b/lib/commands/init.dart @@ -31,12 +31,7 @@ final class InitCommand extends Command { help: "Overwrite config file", negatable: false, ) - ..addFlag( - "yes", - abbr: "y", - help: "Skip config prompts", - negatable: false, - ) + ..addFlag("yes", abbr: "y", help: "Skip config prompts", negatable: false) ..addFlag("vlan", help: "Enables private networking", negatable: false) ..addMultiOption("apt", abbr: "a", help: appApts.join(",")) ..addOption("avatar", help: "Image URL (.gif, .jpeg, .jpg, .png)") @@ -78,8 +73,7 @@ final class InitCommand extends Command { "# https://docs.discloud.com/en/discloud.config", if (args.apt case final v when v.isNotEmpty) _configLine("APT", v.join(",")), - if (args.autorestart case final v when v) - _configLine("AUTORESTART", v), + if (args.autorestart case final v when v) _configLine("AUTORESTART", v), if (args.avatar case final v?) _configLine("AVATAR", v), if (args.hostname case final v?) _configLine("HOSTNAME", v), if (args.id case final v?) _configLine("ID", v), diff --git a/lib/commands/user/locale.dart b/lib/commands/user/locale.dart index b4aa5fe..2172102 100644 --- a/lib/commands/user/locale.dart +++ b/lib/commands/user/locale.dart @@ -43,11 +43,12 @@ final class UserLocaleCommand extends Command { (true, _) => context.locale, (_, final String value) => value, _ => apiLocales.elementAt( - Select(prompt: "Choose your locale", options: apiLocales.toList()) - .interact(), + Select( + prompt: "Choose your locale", + options: apiLocales.toList(), + ).interact(), ), - } - .replaceAll("_", "-"); + }.replaceAll("_", "-"); if (!apiLocales.contains(locale)) { usageException("Unsupported locale: $locale (${apiLocales.join(", ")})"); diff --git a/lib/extensions/command.dart b/lib/extensions/command.dart index 166b270..0621c34 100644 --- a/lib/extensions/command.dart +++ b/lib/extensions/command.dart @@ -31,7 +31,6 @@ extension CommandExtension on Command { if (value != null) return value; usageException("Missing required option or argument: $name"); - throw StateError("unreachable"); } int requiredIntOptionOrRest(String name, int index) { @@ -40,7 +39,6 @@ extension CommandExtension on Command { if (value != null) return value; usageException("Invalid integer for $name: $raw"); - throw StateError("unreachable"); } List multiOptionOrRest( diff --git a/lib/utils/bytes.dart b/lib/utils/bytes.dart index 2d6c6ba..b467e10 100644 --- a/lib/utils/bytes.dart +++ b/lib/utils/bytes.dart @@ -43,7 +43,7 @@ class Bytes { return n / pow(dimension.value, unitIndex); } - int get unitIndex => index.clamp(0, type.units.length - 1).toInt(); + int get unitIndex => index.clamp(0, type.units.length - 1); bool get _isInvalid => n.isNaN || n.isInfinite || n.isZero || n.isNegative; @@ -165,7 +165,7 @@ class _Bits extends Bytes { UnitType get type => .bit; @override - int get unitIndex => index.clamp(0, type.units.length - 1).toInt(); + int get unitIndex => index.clamp(0, type.units.length - 1); @override String toString([String separator = ""]) { diff --git a/lib/utils/progress.dart b/lib/utils/progress.dart index 0462c7e..46e2714 100644 --- a/lib/utils/progress.dart +++ b/lib/utils/progress.dart @@ -28,8 +28,9 @@ String formatProgressMessage({ } if (total <= 0) { - buffer.write(" "); - buffer.write(Bytes(processed).toString(" ")); + buffer + ..write(" ") + ..write(Bytes(processed).toString(" ")); return buffer.toString(); } From fa300f6c0ffc4e524b7c9373fa2addd76e623bae Mon Sep 17 00:00:00 2001 From: "Matheus Henrique(Aka: TheusHen)" Date: Mon, 6 Jul 2026 12:23:06 -0300 Subject: [PATCH 03/24] Address PR review feedback --- lib/commands/app/apt/install.dart | 12 +--- lib/commands/app/apt/uninstall.dart | 12 +--- lib/commands/app/backup.dart | 32 +++------- lib/commands/app/logs.dart | 2 +- lib/commands/app/mod/add.dart | 15 ++--- lib/commands/app/mod/delete.dart | 5 +- lib/commands/app/mod/edit.dart | 15 ++--- lib/commands/app/ram.dart | 2 +- lib/commands/domain/create.dart | 3 +- lib/commands/domain/edit.dart | 3 +- lib/commands/team/backup.dart | 32 +++------- lib/commands/team/logs.dart | 2 +- lib/commands/team/ram.dart | 2 +- lib/commands/user/locale.dart | 25 +------- lib/extensions/command.dart | 61 +++++++++++++------ lib/services/discloud/constants.dart | 2 - modules/glob_zipper/lib/src/glob_zipper.dart | 1 + .../local_store/lib/src/json_local_store.dart | 4 +- 18 files changed, 91 insertions(+), 139 deletions(-) diff --git a/lib/commands/app/apt/install.dart b/lib/commands/app/apt/install.dart index 22d57ce..fb1f414 100644 --- a/lib/commands/app/apt/install.dart +++ b/lib/commands/app/apt/install.dart @@ -9,7 +9,7 @@ final class AppAptInstallCommand extends Command { AppAptInstallCommand() { argParser ..addOption("app") - ..addMultiOption("apt", allowed: appApts, help: appApts.join(",")); + ..addMultiOption("apt", help: appApts.join(",")); } @override @@ -24,10 +24,9 @@ final class AppAptInstallCommand extends Command { @override Future run() async { final appId = requiredOptionOrRest("app", 0); - final apts = multiOptionOrRest("apt", 1); + final apts = multiOptionOrRest("apt", argResults!.restIndexAfter(["app"])); if (apts.isEmpty) usageException("Apt option cannot be empty"); - _validateApts(apts); final spinner = context.printer.spin(text: "Installing app apt..."); @@ -38,11 +37,4 @@ final class AppAptInstallCommand extends Command { spinner.success(resolveResponseMessage(response)); } - - void _validateApts(List apts) { - final invalid = apts.where((apt) => !appApts.contains(apt)).toList(); - if (invalid.isEmpty) return; - - usageException("Invalid APT: ${invalid.join(", ")}"); - } } diff --git a/lib/commands/app/apt/uninstall.dart b/lib/commands/app/apt/uninstall.dart index 05d6b97..22753c8 100644 --- a/lib/commands/app/apt/uninstall.dart +++ b/lib/commands/app/apt/uninstall.dart @@ -9,7 +9,7 @@ final class AppAptUninstallCommand extends Command { AppAptUninstallCommand() { argParser ..addOption("app") - ..addMultiOption("apt", allowed: appApts, help: appApts.join(",")); + ..addMultiOption("apt", help: appApts.join(",")); } @override @@ -24,10 +24,9 @@ final class AppAptUninstallCommand extends Command { @override Future run() async { final appId = requiredOptionOrRest("app", 0); - final apts = multiOptionOrRest("apt", 1); + final apts = multiOptionOrRest("apt", argResults!.restIndexAfter(["app"])); if (apts.isEmpty) usageException("Apt option cannot be empty"); - _validateApts(apts); final spinner = context.printer.spin(text: "Uninstalling app apt..."); @@ -38,11 +37,4 @@ final class AppAptUninstallCommand extends Command { spinner.success(resolveResponseMessage(response)); } - - void _validateApts(List apts) { - final invalid = apts.where((apt) => !appApts.contains(apt)).toList(); - if (invalid.isEmpty) return; - - usageException("Invalid APT: ${invalid.join(", ")}"); - } } diff --git a/lib/commands/app/backup.dart b/lib/commands/app/backup.dart index b64c262..e13d519 100644 --- a/lib/commands/app/backup.dart +++ b/lib/commands/app/backup.dart @@ -6,15 +6,12 @@ import "package:discloud/cli/disposable.dart"; import "package:discloud/cli/spin/ispin.dart"; import "package:discloud/extensions/command.dart"; import "package:discloud/extensions/file.dart"; -import "package:discloud/utils/ascii_table.dart"; import "package:discloud/utils/download.dart"; import "package:discloud/utils/messages.dart"; import "package:discloud/utils/progress.dart"; import "package:discloud/utils/speed_monitor.dart"; import "package:path/path.dart" hide context; -const _defaultBackupDir = "discloud/backups"; - final class AppBackupCommand extends Command with Disposable { AppBackupCommand() { argParser @@ -25,12 +22,6 @@ final class AppBackupCommand extends Command with Disposable { aliases: const ["out", "path"], help: "Specifies the destination path for downloading backups. The destination path will be considered a directory.", - ) - ..addFlag( - "save", - abbr: "s", - help: "Save your app backup", - negatable: false, ); } @@ -50,8 +41,7 @@ final class AppBackupCommand extends Command with Disposable { @override Future run() async { final appId = optionOrRest("app", 0) ?? "all"; - final dir = optionOrRest("dir", 1); - final shouldDownload = argResults!.flag("save") || dir != null; + final dir = optionOrRest("dir", argResults!.restIndexAfter(["app"])); final spinner = context.printer.spin(text: "Fetching backup..."); @@ -61,18 +51,10 @@ final class AppBackupCommand extends Command with Disposable { switch (response["backups"]) { case final Map data: - await _handleSingle( - data, - dir: shouldDownload ? (dir ?? _defaultBackupDir) : null, - spinner: spinner, - ); + await _handleSingle(data, dir: dir, spinner: spinner); break; case final List list: - await _handleMulti( - list, - dir: shouldDownload ? (dir ?? _defaultBackupDir) : null, - spinner: spinner, - ); + await _handleMulti(list, dir: dir, spinner: spinner); break; } } @@ -89,7 +71,7 @@ final class AppBackupCommand extends Command with Disposable { return _download(dir: dir, spinner: spinner, uri: uri); } - context.printer.writeln(mapToVerticalAsciiTable(data)); + context.printer.writeln(url); } } @@ -99,7 +81,11 @@ final class AppBackupCommand extends Command with Disposable { String? dir, }) async { if (dir == null) { - context.printer.writeln(listToAsciiTable(list)); + for (final data in list) { + if (data["url"] case final String url) { + context.printer.writeln(url); + } + } return; } diff --git a/lib/commands/app/logs.dart b/lib/commands/app/logs.dart index 7f617e4..dbbb74c 100644 --- a/lib/commands/app/logs.dart +++ b/lib/commands/app/logs.dart @@ -42,7 +42,7 @@ final class AppLogsCommand extends Command { @override Future run() async { final appId = optionOrRest("app", 0) ?? "all"; - final out = optionOrRest("out", 1); + final out = optionOrRest("out", argResults!.restIndexAfter(["app"])); final overwrite = argResults!.flag("overwrite"); final spinner = context.printer.spin(); diff --git a/lib/commands/app/mod/add.dart b/lib/commands/app/mod/add.dart index d42d55c..f06830b 100644 --- a/lib/commands/app/mod/add.dart +++ b/lib/commands/app/mod/add.dart @@ -22,9 +22,11 @@ final class AppModAddCommand extends Command { @override Future run() async { final appId = requiredOptionOrRest("app", 0); - final modId = requiredOptionOrRest("mod", 1); - final perms = multiOptionOrRest("perms", 2); - _validatePerms(perms); + final modId = requiredOptionOrRest( + "mod", + argResults!.restIndexAfter(["app"]), + ); + final perms = argResults!.multiOption("perms"); final spinner = context.printer.spin(text: "Adding app MOD..."); @@ -35,11 +37,4 @@ final class AppModAddCommand extends Command { spinner.success(resolveResponseMessage(response)); } - - void _validatePerms(List perms) { - final invalid = perms.where((perm) => !appModPerms.contains(perm)).toList(); - if (invalid.isEmpty) return; - - usageException("Invalid MOD permission: ${invalid.join(", ")}"); - } } diff --git a/lib/commands/app/mod/delete.dart b/lib/commands/app/mod/delete.dart index b7d5cd5..e9e6ecb 100644 --- a/lib/commands/app/mod/delete.dart +++ b/lib/commands/app/mod/delete.dart @@ -20,7 +20,10 @@ final class AppModDeleteCommand extends Command { @override Future run() async { final appId = requiredOptionOrRest("app", 0); - final modId = requiredOptionOrRest("mod", 1); + final modId = requiredOptionOrRest( + "mod", + argResults!.restIndexAfter(["app"]), + ); final spinner = context.printer.spin(text: "Deleting app MOD..."); diff --git a/lib/commands/app/mod/edit.dart b/lib/commands/app/mod/edit.dart index 2f76e6f..4456a65 100644 --- a/lib/commands/app/mod/edit.dart +++ b/lib/commands/app/mod/edit.dart @@ -22,9 +22,11 @@ final class AppModEditCommand extends Command { @override Future run() async { final appId = requiredOptionOrRest("app", 0); - final modId = requiredOptionOrRest("mod", 1); - final perms = multiOptionOrRest("perms", 2); - _validatePerms(perms); + final modId = requiredOptionOrRest( + "mod", + argResults!.restIndexAfter(["app"]), + ); + final perms = argResults!.multiOption("perms"); final spinner = context.printer.spin(text: "Editing app MOD..."); @@ -35,11 +37,4 @@ final class AppModEditCommand extends Command { spinner.success(resolveResponseMessage(response)); } - - void _validatePerms(List perms) { - final invalid = perms.where((perm) => !appModPerms.contains(perm)).toList(); - if (invalid.isEmpty) return; - - usageException("Invalid MOD permission: ${invalid.join(", ")}"); - } } diff --git a/lib/commands/app/ram.dart b/lib/commands/app/ram.dart index 3abd6c1..fe356af 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 = requiredOptionOrRest("app", 0); final ramMB = max( - requiredIntOptionOrRest("amount", 1), + requiredIntOptionOrRest("amount", argResults!.restIndexAfter(["app"])), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/domain/create.dart b/lib/commands/domain/create.dart index bbb5495..8bb8665 100644 --- a/lib/commands/domain/create.dart +++ b/lib/commands/domain/create.dart @@ -22,7 +22,8 @@ final class CustomdomainCreateCommand extends Command { @override Future run() async { final domain = requiredOptionOrRest("id", 0); - final subdomain = optionOrRest("app", 1) ?? "all"; + final subdomain = + optionOrRest("app", argResults!.restIndexAfter(["id"])) ?? "all"; final spinner = context.printer.spin(text: "Creating $domain..."); diff --git a/lib/commands/domain/edit.dart b/lib/commands/domain/edit.dart index a3c48d4..038a557 100644 --- a/lib/commands/domain/edit.dart +++ b/lib/commands/domain/edit.dart @@ -21,7 +21,8 @@ final class CustomdomainEditCommand extends Command with Disposable { @override Future run() async { final domain = requiredOptionOrRest("id", 0); - final subdomain = optionOrRest("app", 1) ?? "all"; + final subdomain = + optionOrRest("app", argResults!.restIndexAfter(["id"])) ?? "all"; final spinner = context.printer.spin(text: "Editting $domain..."); diff --git a/lib/commands/team/backup.dart b/lib/commands/team/backup.dart index 342ad9b..4648786 100644 --- a/lib/commands/team/backup.dart +++ b/lib/commands/team/backup.dart @@ -6,15 +6,12 @@ import "package:discloud/cli/disposable.dart"; import "package:discloud/cli/spin/ispin.dart"; import "package:discloud/extensions/command.dart"; import "package:discloud/extensions/file.dart"; -import "package:discloud/utils/ascii_table.dart"; import "package:discloud/utils/download.dart"; import "package:discloud/utils/messages.dart"; import "package:discloud/utils/progress.dart"; import "package:discloud/utils/speed_monitor.dart"; import "package:path/path.dart" hide context; -const _defaultBackupDir = "discloud/backups"; - final class TeamBackupCommand extends Command with Disposable { TeamBackupCommand() { argParser @@ -25,12 +22,6 @@ final class TeamBackupCommand extends Command with Disposable { aliases: const ["out", "path"], help: "Specifies the destination path for downloading backups. The destination path will be considered a directory.", - ) - ..addFlag( - "save", - abbr: "s", - help: "Save your app backup", - negatable: false, ); } @@ -50,8 +41,7 @@ final class TeamBackupCommand extends Command with Disposable { @override Future run() async { final appId = optionOrRest("app", 0) ?? "all"; - final dir = optionOrRest("dir", 1); - final shouldDownload = argResults!.flag("save") || dir != null; + final dir = optionOrRest("dir", argResults!.restIndexAfter(["app"])); final spinner = context.printer.spin(text: "Fetching backup..."); @@ -61,18 +51,10 @@ final class TeamBackupCommand extends Command with Disposable { switch (response["backups"]) { case final Map data: - await _handleSingle( - data, - dir: shouldDownload ? (dir ?? _defaultBackupDir) : null, - spinner: spinner, - ); + await _handleSingle(data, dir: dir, spinner: spinner); break; case final List list: - await _handleMulti( - list, - dir: shouldDownload ? (dir ?? _defaultBackupDir) : null, - spinner: spinner, - ); + await _handleMulti(list, dir: dir, spinner: spinner); break; } } @@ -89,7 +71,7 @@ final class TeamBackupCommand extends Command with Disposable { return _download(dir: dir, spinner: spinner, uri: uri); } - context.printer.writeln(mapToVerticalAsciiTable(data)); + context.printer.writeln(url); } } @@ -99,7 +81,11 @@ final class TeamBackupCommand extends Command with Disposable { String? dir, }) async { if (dir == null) { - context.printer.writeln(listToAsciiTable(list)); + for (final data in list) { + if (data["url"] case final String url) { + context.printer.writeln(url); + } + } return; } diff --git a/lib/commands/team/logs.dart b/lib/commands/team/logs.dart index ddb558f..30f7ce0 100644 --- a/lib/commands/team/logs.dart +++ b/lib/commands/team/logs.dart @@ -42,7 +42,7 @@ final class TeamLogsCommand extends Command { @override Future run() async { final appId = optionOrRest("app", 0) ?? "all"; - final out = optionOrRest("out", 1); + final out = optionOrRest("out", argResults!.restIndexAfter(["app"])); final overwrite = argResults!.flag("overwrite"); final spinner = context.printer.spin(); diff --git a/lib/commands/team/ram.dart b/lib/commands/team/ram.dart index f03c721..5900790 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 = requiredOptionOrRest("app", 0); final ramMB = max( - requiredIntOptionOrRest("amount", 1), + requiredIntOptionOrRest("amount", argResults!.restIndexAfter(["app"])), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/user/locale.dart b/lib/commands/user/locale.dart index 2172102..a5735f7 100644 --- a/lib/commands/user/locale.dart +++ b/lib/commands/user/locale.dart @@ -1,14 +1,12 @@ import "package:args/command_runner.dart"; import "package:discloud/cli/context.dart"; import "package:discloud/extensions/command.dart"; -import "package:discloud/services/discloud/constants.dart"; import "package:discloud/utils/messages.dart"; -import "package:interact/interact.dart"; final class UserLocaleCommand extends Command { UserLocaleCommand() { argParser - ..addOption("locale", abbr: "l", valueHelp: localeName) + ..addOption("locale", abbr: "l", mandatory: true, valueHelp: localeName) ..addFlag( "system", abbr: "s", @@ -27,7 +25,7 @@ final class UserLocaleCommand extends Command { Future run() async { final system = argResults!.flag("system"); - final locale = _resolveLocale(system); + final locale = system ? context.locale : argResults!.option("locale"); final spinner = context.printer.spin( text: "Defining the user's locale to $locale...", @@ -37,23 +35,4 @@ final class UserLocaleCommand extends Command { spinner.success(resolveResponseMessage(response)); } - - String _resolveLocale(bool system) { - final locale = switch ((system, argResults!.option("locale"))) { - (true, _) => context.locale, - (_, final String value) => value, - _ => apiLocales.elementAt( - Select( - prompt: "Choose your locale", - options: apiLocales.toList(), - ).interact(), - ), - }.replaceAll("_", "-"); - - if (!apiLocales.contains(locale)) { - usageException("Unsupported locale: $locale (${apiLocales.join(", ")})"); - } - - return locale; - } } diff --git a/lib/extensions/command.dart b/lib/extensions/command.dart index 0621c34..0b689f1 100644 --- a/lib/extensions/command.dart +++ b/lib/extensions/command.dart @@ -1,6 +1,44 @@ +import "package:args/args.dart"; import "package:args/command_runner.dart"; import "package:discloud/cli/context.dart"; +extension ArgResultsExtension on ArgResults { + String? optionOrRest(String name, int index) { + if (wasParsed(name)) { + final value = option(name); + if (value != null && value.isNotEmpty) return value; + } + + if (rest.length > index) return rest[index]; + + final value = option(name); + if (value != null && value.isNotEmpty) return value; + + return null; + } + + List multiOptionOrRest( + String name, + int index, { + List defaults = const [], + }) { + if (wasParsed(name)) { + final values = multiOption(name); + if (values.isNotEmpty) return values; + } + + if (rest.length > index) return rest.skip(index).toList(); + + final values = multiOption(name); + if (values.isNotEmpty) return values; + + return defaults; + } + + int restIndexAfter(Iterable previousOptions) => + previousOptions.where((name) => !wasParsed(name)).length; +} + extension CommandExtension on Command { CliContext get context => .I; @@ -11,19 +49,7 @@ extension CommandExtension on Command { } String? optionOrRest(String name, int index) { - final parsed = argResults?.wasParsed(name) ?? false; - if (parsed) { - final value = argResults?.option(name); - if (value != null && value.isNotEmpty) return value; - } - - final rest = argResults?.rest ?? const []; - if (rest.length > index) return rest[index]; - - final value = argResults?.option(name); - if (value != null && value.isNotEmpty) return value; - - return null; + return argResults?.optionOrRest(name, index); } String requiredOptionOrRest(String name, int index) { @@ -46,12 +72,7 @@ extension CommandExtension on Command { int index, { List defaults = const [], }) { - final rest = argResults?.rest ?? const []; - if (rest.length > index) return rest.skip(index).toList(); - - final values = argResults?.multiOption(name) ?? const []; - if (values.isNotEmpty) return values; - - return defaults; + return argResults?.multiOptionOrRest(name, index, defaults: defaults) ?? + defaults; } } diff --git a/lib/services/discloud/constants.dart b/lib/services/discloud/constants.dart index e1bd3f1..f9804ac 100644 --- a/lib/services/discloud/constants.dart +++ b/lib/services/discloud/constants.dart @@ -4,8 +4,6 @@ const discloudApikeyURL = "$discloudDashboardURL/apikey"; const discloudLegalURL = "$discloudURL/legal"; const discloudPlansURL = "$discloudURL/plans"; -const apiLocales = {"en-US", "pt-BR"}; - const Map> blockedFiles = { "common": {".git", ".vscode", ".cache", "temp"}, "go": {}, diff --git a/modules/glob_zipper/lib/src/glob_zipper.dart b/modules/glob_zipper/lib/src/glob_zipper.dart index 0321b2e..e5e8c57 100644 --- a/modules/glob_zipper/lib/src/glob_zipper.dart +++ b/modules/glob_zipper/lib/src/glob_zipper.dart @@ -74,6 +74,7 @@ class GlobZipper { bool success = false; try { + // Resolve the real root once so symlinked files cannot escape it. final rootPath = await directory.resolveSymbolicLinks(); final encoder = ZipFileEncoder(password: password) ..create(zipfile.path, level: level); diff --git a/modules/local_store/lib/src/json_local_store.dart b/modules/local_store/lib/src/json_local_store.dart index c4b4f67..3f2d567 100644 --- a/modules/local_store/lib/src/json_local_store.dart +++ b/modules/local_store/lib/src/json_local_store.dart @@ -38,7 +38,9 @@ class _JSONLocalStore implements LocalStore { final temp = File("${_file.path}.tmp"); await temp.writeAsBytes(_jsonBase64Encoder.convert(_cache), flush: true); - if (await _file.exists()) await _file.delete(); + try { + await _file.delete(); + } catch (_) {} await temp.rename(_file.path); } From 2e4ab90c4e420366dc3024ab5949cf005b6094b6 Mon Sep 17 00:00:00 2001 From: "Matheus Henrique(Aka: TheusHen)" Date: Mon, 6 Jul 2026 22:50:23 -0300 Subject: [PATCH 04/24] fix: skip symlinks in glob zipper --- modules/glob_zipper/lib/src/fs.dart | 10 ++++- modules/glob_zipper/lib/src/glob_zipper.dart | 43 ++------------------ 2 files changed, 12 insertions(+), 41 deletions(-) diff --git a/modules/glob_zipper/lib/src/fs.dart b/modules/glob_zipper/lib/src/fs.dart index 98437d4..ec71969 100644 --- a/modules/glob_zipper/lib/src/fs.dart +++ b/modules/glob_zipper/lib/src/fs.dart @@ -56,7 +56,10 @@ class FS { final visitedDirectories = {}; - await for (final entity in _glob.list(root: directory.path)) { + await for (final entity in _glob.list( + root: directory.path, + followLinks: false, + )) { if (entity is! File || ignore.matches(entity.path)) continue; final folder = entity.dirname; @@ -73,7 +76,10 @@ class FS { Stream _listWithoutIgnoreFilename() async* { final Glob ignore = _ignoreGlob; - await for (final entity in _glob.list(root: directory.path)) { + await for (final entity in _glob.list( + root: directory.path, + followLinks: false, + )) { if (entity is! File || ignore.matches(entity.path)) continue; yield entity as File; diff --git a/modules/glob_zipper/lib/src/glob_zipper.dart b/modules/glob_zipper/lib/src/glob_zipper.dart index e5e8c57..9d721e6 100644 --- a/modules/glob_zipper/lib/src/glob_zipper.dart +++ b/modules/glob_zipper/lib/src/glob_zipper.dart @@ -74,15 +74,13 @@ class GlobZipper { bool success = false; try { - // Resolve the real root once so symlinked files cannot escape it. - final rootPath = await directory.resolveSymbolicLinks(); final encoder = ZipFileEncoder(password: password) ..create(zipfile.path, level: level); if (onData case final onData?) { - await _zipWithCallback(encoder, fs.list, onData, rootPath, onError); + await _zipWithCallback(encoder, fs.list, onData, onError); } else { - await _zipWithoutCallback(encoder, fs.list, rootPath, onError); + await _zipWithoutCallback(encoder, fs.list, onError); } await encoder.close(); @@ -99,17 +97,12 @@ class GlobZipper { Future _zipWithoutCallback( ZipFileEncoder encoder, Stream Function() fileStreamFactory, [ - String? rootPath, OnErrorCallback? onError, ]) async { await for (final file in fileStreamFactory().handleError( onError ?? _noop, )) { - final aFile = await _toArchiveFile( - file, - root: directory, - rootPath: rootPath, - ); + final aFile = await _toArchiveFile(file, root: directory); encoder.addArchiveFile(aFile); } @@ -119,14 +112,12 @@ class GlobZipper { ZipFileEncoder encoder, Stream Function() fileStreamFactory, ZipCallback onData, [ - String? rootPath, OnErrorCallback? onError, ]) async { int processed = 0, i = 0; await for (final file in fileStreamFactory().handleError( onError ?? _noop, )) { - await _ensureInsideRoot(file, directory, rootPath); final stat = await file.stat(); onData( @@ -139,13 +130,7 @@ class GlobZipper { ), ); - final aFile = await _toArchiveFile( - file, - root: directory, - rootPath: rootPath, - stat: stat, - trusted: true, - ); + final aFile = await _toArchiveFile(file, root: directory, stat: stat); encoder.addArchiveFile(aFile); @@ -156,12 +141,8 @@ class GlobZipper { Future _toArchiveFile( File file, { required Directory root, - String? rootPath, FileStat? stat, - bool trusted = false, }) async { - if (!trusted) await _ensureInsideRoot(file, root, rootPath); - stat ??= await file.stat(); final filename = relative(file.path, from: root.path); @@ -173,20 +154,4 @@ class GlobZipper { ..lastModTime = stat.modified.secondsSinceEpoch ..mode = stat.mode; } - - Future _ensureInsideRoot( - File file, - Directory root, - String? rootPath, - ) async { - rootPath ??= await root.resolveSymbolicLinks(); - final filePath = await file.resolveSymbolicLinks(); - - if (equals(rootPath, filePath) || isWithin(rootPath, filePath)) return; - - throw FileSystemException( - "Refusing to zip a file outside the root directory", - file.path, - ); - } } From b5ac3800fdae172b325b8c2fed6cbe24047315f3 Mon Sep 17 00:00:00 2001 From: "Matheus Henrique(Aka: TheusHen)" Date: Tue, 7 Jul 2026 14:24:39 -0300 Subject: [PATCH 05/24] Address follow-up review feedback --- lib/commands/app/apt/install.dart | 6 +- lib/commands/app/apt/uninstall.dart | 6 +- lib/commands/app/backup.dart | 4 +- lib/commands/app/commit.dart | 13 +++- lib/commands/app/console.dart | 4 +- lib/commands/app/delete.dart | 4 +- lib/commands/app/info.dart | 2 +- lib/commands/app/logs.dart | 59 +++-------------- lib/commands/app/mod/add.dart | 11 ++-- lib/commands/app/mod/delete.dart | 11 ++-- lib/commands/app/mod/edit.dart | 11 ++-- lib/commands/app/mod/info.dart | 4 +- lib/commands/app/profile.dart | 4 +- lib/commands/app/ram.dart | 10 ++- lib/commands/app/restart.dart | 2 +- lib/commands/app/start.dart | 2 +- lib/commands/app/status.dart | 4 +- lib/commands/app/stop.dart | 2 +- lib/commands/app/upload.dart | 53 ++------------- lib/commands/domain/create.dart | 9 +-- lib/commands/domain/delete.dart | 4 +- lib/commands/domain/edit.dart | 7 +- lib/commands/domain/info.dart | 7 +- lib/commands/domain/verify.dart | 6 +- lib/commands/init.dart | 60 ++++++----------- lib/commands/subdomain/create.dart | 4 +- lib/commands/subdomain/delete.dart | 4 +- lib/commands/subdomain/info.dart | 2 +- lib/commands/team/backup.dart | 4 +- lib/commands/team/commit.dart | 13 +++- lib/commands/team/info.dart | 2 +- lib/commands/team/logs.dart | 59 +++-------------- lib/commands/team/ram.dart | 10 ++- lib/commands/team/restart.dart | 2 +- lib/commands/team/start.dart | 2 +- lib/commands/team/status.dart | 4 +- lib/commands/team/stop.dart | 2 +- lib/commands/zip.dart | 5 +- lib/extensions/arg_results.dart | 26 ++++++++ lib/extensions/command.dart | 66 +------------------ lib/services/discloud/constants.dart | 4 ++ lib/utils/download.dart | 8 +-- lib/utils/progress.dart | 16 ++--- .../local_store/lib/src/json_local_store.dart | 3 - 44 files changed, 207 insertions(+), 334 deletions(-) diff --git a/lib/commands/app/apt/install.dart b/lib/commands/app/apt/install.dart index fb1f414..e86532f 100644 --- a/lib/commands/app/apt/install.dart +++ b/lib/commands/app/apt/install.dart @@ -23,8 +23,10 @@ final class AppAptInstallCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); - final apts = multiOptionOrRest("apt", argResults!.restIndexAfter(["app"])); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); + final apts = argResults!.multiOptionOrRest("apt", after: ["app"]); if (apts.isEmpty) usageException("Apt option cannot be empty"); diff --git a/lib/commands/app/apt/uninstall.dart b/lib/commands/app/apt/uninstall.dart index 22753c8..61d73be 100644 --- a/lib/commands/app/apt/uninstall.dart +++ b/lib/commands/app/apt/uninstall.dart @@ -23,8 +23,10 @@ final class AppAptUninstallCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); - final apts = multiOptionOrRest("apt", argResults!.restIndexAfter(["app"])); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); + final apts = argResults!.multiOptionOrRest("apt", after: ["app"]); if (apts.isEmpty) usageException("Apt option cannot be empty"); diff --git a/lib/commands/app/backup.dart b/lib/commands/app/backup.dart index e13d519..084109f 100644 --- a/lib/commands/app/backup.dart +++ b/lib/commands/app/backup.dart @@ -40,8 +40,8 @@ final class AppBackupCommand extends Command with Disposable { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; - final dir = optionOrRest("dir", argResults!.restIndexAfter(["app"])); + final appId = argResults!.optionOrRest("app") ?? "all"; + final dir = argResults!.optionOrRest("dir", ["app"]); final spinner = context.printer.spin(text: "Fetching backup..."); diff --git a/lib/commands/app/commit.dart b/lib/commands/app/commit.dart index 7c3a397..d5fe8df 100644 --- a/lib/commands/app/commit.dart +++ b/lib/commands/app/commit.dart @@ -42,14 +42,21 @@ final class AppCommitCommand extends Command with Disposable { final optionAppId = argResults!.option("app"); final appId = optionAppId ?? - optionOrRest("app", 0) ?? + argResults!.optionOrRest("app") ?? await _getDiscloudConfigAppId(directory); if (appId == null) throw Exception("Missing app id"); final glob = optionAppId == null - ? multiOptionOrRest("glob", 1, defaults: const ["**"]) - : multiOptionOrRest("glob", 0, defaults: const ["**"]); + ? argResults!.multiOptionOrRest( + "glob", + after: ["app"], + defaultFactory: () => const ["**"], + ) + : argResults!.multiOptionOrRest( + "glob", + defaultFactory: () => const ["**"], + ); final spinner = context.printer.spin(text: "Zipping..."); diff --git a/lib/commands/app/console.dart b/lib/commands/app/console.dart index 5cf6692..bcff040 100644 --- a/lib/commands/app/console.dart +++ b/lib/commands/app/console.dart @@ -26,7 +26,9 @@ final class AppConsoleCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); String? command = argResults!.option("command"); final spinner = context.printer.spin(start: false); diff --git a/lib/commands/app/delete.dart b/lib/commands/app/delete.dart index dd0fbf2..951d8f2 100644 --- a/lib/commands/app/delete.dart +++ b/lib/commands/app/delete.dart @@ -25,7 +25,9 @@ final class AppDeleteCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); final result = argResults!.flag("yes") || diff --git a/lib/commands/app/info.dart b/lib/commands/app/info.dart index 151d711..9023810 100644 --- a/lib/commands/app/info.dart +++ b/lib/commands/app/info.dart @@ -33,7 +33,7 @@ final class AppInfoCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; + final appId = argResults!.optionOrRest("app") ?? "all"; final spinner = context.printer.spin(text: "Fetching app info..."); diff --git a/lib/commands/app/logs.dart b/lib/commands/app/logs.dart index dbbb74c..4e76bae 100644 --- a/lib/commands/app/logs.dart +++ b/lib/commands/app/logs.dart @@ -24,12 +24,6 @@ final class AppLogsCommand extends Command { aliases: const ["path"], help: "Specifies the destination path for downloading logs. When the application option is set to 'all', the destination path will be considered a directory where all downloads will be stored.", - ) - ..addFlag( - "overwrite", - abbr: "o", - help: "Overwrite log files", - negatable: false, ); } @@ -41,9 +35,8 @@ final class AppLogsCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; - final out = optionOrRest("out", argResults!.restIndexAfter(["app"])); - final overwrite = argResults!.flag("overwrite"); + final appId = argResults!.optionOrRest("app") ?? "all"; + final out = argResults!.optionOrRest("out", ["app"]); final spinner = context.printer.spin(); @@ -53,15 +46,10 @@ final class AppLogsCommand extends Command { switch (response["apps"]) { case final Map data: - await _handleSingle(data, out, spinner, overwrite: overwrite); + await _handleSingle(data, out, spinner); break; case final List list: - await _handleMulti( - list, - out: out ?? _defaultLogsDir, - overwrite: overwrite, - spinner: spinner, - ); + await _handleMulti(list, out: out ?? _defaultLogsDir, spinner: spinner); break; } } @@ -69,18 +57,12 @@ final class AppLogsCommand extends Command { Future _handleSingle( Map data, String? out, - ISpin spinner, { - required bool overwrite, - }) async { + ISpin spinner, + ) async { if (data["terminal"]?["big"] case final String contents) { if (out case final out?) { final appId = data["id"]?.toString() ?? "app"; - return _saveLog( - contents, - _resolveLogPath(out, appId), - spinner, - overwrite: overwrite, - ); + return _saveLog(contents, _resolveLogPath(out, appId), spinner); } context.printer.writeln(contents); } @@ -89,28 +71,17 @@ final class AppLogsCommand extends Command { Future _handleMulti( List list, { required String out, - required bool overwrite, required ISpin spinner, }) async { for (final data in list) { - await _handleSingle(data, out, spinner, overwrite: overwrite); + await _handleSingle(data, out, spinner); } } - Future _saveLog( - String data, - String out, - ISpin spinner, { - required bool overwrite, - }) async { + Future _saveLog(String data, String out, ISpin spinner) async { final file = File(out); await file.parent.create(recursive: true); - final contents = _formatLog(data); - if (overwrite) { - await file.writeAsString(contents); - } else { - await file.writeAsString("$contents\n", mode: FileMode.append); - } + await file.writeAsString(stripAnsi(data)); spinner.success(out); } @@ -119,14 +90,4 @@ final class AppLogsCommand extends Command { return joinAll([out, "$appId.log"]); } - - String _formatLog(String data) { - return [ - "", - "".padRight(60, "-"), - DateTime.now().toString(), - "", - stripAnsi(data), - ].join("\n"); - } } diff --git a/lib/commands/app/mod/add.dart b/lib/commands/app/mod/add.dart index f06830b..dcd0a0d 100644 --- a/lib/commands/app/mod/add.dart +++ b/lib/commands/app/mod/add.dart @@ -21,11 +21,12 @@ final class AppModAddCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); - final modId = requiredOptionOrRest( - "mod", - argResults!.restIndexAfter(["app"]), - ); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); + final modId = + argResults!.optionOrRest("mod", ["app"]) ?? + usageException("Missing required option or argument: mod"); final perms = argResults!.multiOption("perms"); final spinner = context.printer.spin(text: "Adding app MOD..."); diff --git a/lib/commands/app/mod/delete.dart b/lib/commands/app/mod/delete.dart index e9e6ecb..3fa2258 100644 --- a/lib/commands/app/mod/delete.dart +++ b/lib/commands/app/mod/delete.dart @@ -19,11 +19,12 @@ final class AppModDeleteCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); - final modId = requiredOptionOrRest( - "mod", - argResults!.restIndexAfter(["app"]), - ); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); + final modId = + argResults!.optionOrRest("mod", ["app"]) ?? + usageException("Missing required option or argument: mod"); final spinner = context.printer.spin(text: "Deleting app MOD..."); diff --git a/lib/commands/app/mod/edit.dart b/lib/commands/app/mod/edit.dart index 4456a65..b19a127 100644 --- a/lib/commands/app/mod/edit.dart +++ b/lib/commands/app/mod/edit.dart @@ -21,11 +21,12 @@ final class AppModEditCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); - final modId = requiredOptionOrRest( - "mod", - argResults!.restIndexAfter(["app"]), - ); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); + final modId = + argResults!.optionOrRest("mod", ["app"]) ?? + usageException("Missing required option or argument: mod"); final perms = argResults!.multiOption("perms"); final spinner = context.printer.spin(text: "Editing app MOD..."); diff --git a/lib/commands/app/mod/info.dart b/lib/commands/app/mod/info.dart index 48acd02..74578ee 100644 --- a/lib/commands/app/mod/info.dart +++ b/lib/commands/app/mod/info.dart @@ -17,7 +17,9 @@ final class AppModInfoCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); final spinner = context.printer.spin(text: "Fetching app MODs..."); diff --git a/lib/commands/app/profile.dart b/lib/commands/app/profile.dart index d16f3f4..eee3dce 100644 --- a/lib/commands/app/profile.dart +++ b/lib/commands/app/profile.dart @@ -21,7 +21,9 @@ final class AppProfileCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); final name = argResults?.option("name"); final avatar = argResults?.option("avatar"); diff --git a/lib/commands/app/ram.dart b/lib/commands/app/ram.dart index fe356af..0100792 100644 --- a/lib/commands/app/ram.dart +++ b/lib/commands/app/ram.dart @@ -25,9 +25,15 @@ final class AppRamCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); + final amount = + argResults!.optionOrRest("amount", ["app"]) ?? + usageException("Missing required option or argument: amount"); final ramMB = max( - requiredIntOptionOrRest("amount", argResults!.restIndexAfter(["app"])), + int.tryParse(amount) ?? + usageException("Invalid integer for amount: $amount"), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/app/restart.dart b/lib/commands/app/restart.dart index 7a897c2..cfa8ac6 100644 --- a/lib/commands/app/restart.dart +++ b/lib/commands/app/restart.dart @@ -17,7 +17,7 @@ final class AppRestartCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; + final appId = argResults!.optionOrRest("app") ?? "all"; final spinner = context.printer.spin(text: "Restarting app..."); diff --git a/lib/commands/app/start.dart b/lib/commands/app/start.dart index 834d0a4..4e8df33 100644 --- a/lib/commands/app/start.dart +++ b/lib/commands/app/start.dart @@ -17,7 +17,7 @@ final class AppStartCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; + final appId = argResults!.optionOrRest("app") ?? "all"; final spinner = context.printer.spin(text: "Starting app..."); diff --git a/lib/commands/app/status.dart b/lib/commands/app/status.dart index 26bef4a..6db3aee 100644 --- a/lib/commands/app/status.dart +++ b/lib/commands/app/status.dart @@ -21,7 +21,9 @@ final class AppStatusCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); final spinner = context.printer.spin(text: "Fetching app status..."); diff --git a/lib/commands/app/stop.dart b/lib/commands/app/stop.dart index 8451d93..4cbb365 100644 --- a/lib/commands/app/stop.dart +++ b/lib/commands/app/stop.dart @@ -17,7 +17,7 @@ final class AppStopCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; + final appId = argResults!.optionOrRest("app") ?? "all"; final spinner = context.printer.spin(text: "Stopping app..."); diff --git a/lib/commands/app/upload.dart b/lib/commands/app/upload.dart index ede32bc..e8cb446 100644 --- a/lib/commands/app/upload.dart +++ b/lib/commands/app/upload.dart @@ -12,8 +12,6 @@ import "package:discloud/utils/zip.dart"; import "package:discloud_config/discloud_config.dart"; import "package:path/path.dart" hide context; -final _configLineBreakPattern = RegExp(r"[\r\n]"); - final class AppUploadCommand extends Command with Disposable { AppUploadCommand() { argParser.addMultiOption("glob", abbr: "g", defaultsTo: const ["**"]); @@ -41,7 +39,10 @@ final class AppUploadCommand extends Command with Disposable { await config.validate(); - final glob = multiOptionOrRest("glob", 0, defaults: const ["**"]); + final glob = argResults!.multiOptionOrRest( + "glob", + defaultFactory: () => const ["**"], + ); final spinner = context.printer.spin(text: "Zipping..."); @@ -84,52 +85,6 @@ final class AppUploadCommand extends Command with Disposable { ); spinner.success(resolveResponseMessage(response)); - - if (response["status"] == "ok") { - if (response["app"] case final Map app) { - final updates = {}; - - if (app["avatarURL"] case final String avatarURL) { - updates["AVATAR"] = avatarURL; - } - - if (app["id"] case final String id) updates["ID"] = id; - - await _updateConfigFile(File(configFilePath), updates); - } - } - } - - Future _updateConfigFile(File file, Map updates) async { - if (updates.isEmpty) return; - - final seen = {}; - final lines = await file.readAsLines(); - - for (int i = 0; i < lines.length; i++) { - final line = lines[i]; - final index = line.indexOf("="); - if (index < 1) continue; - - final key = line.substring(0, index).trim(); - final value = updates[key]; - if (value == null) continue; - - lines[i] = "$key=${_configValue(value)}"; - seen.add(key); - } - - for (final entry in updates.entries) { - if (!seen.contains(entry.key)) { - lines.add("${entry.key}=${_configValue(entry.value)}"); - } - } - - await file.writeAsString("${lines.join("\n")}\n"); - } - - String _configValue(String value) { - return value.replaceAll(_configLineBreakPattern, ""); } @override diff --git a/lib/commands/domain/create.dart b/lib/commands/domain/create.dart index 8bb8665..89d82ab 100644 --- a/lib/commands/domain/create.dart +++ b/lib/commands/domain/create.dart @@ -21,15 +21,16 @@ final class CustomdomainCreateCommand extends Command { @override Future run() async { - final domain = requiredOptionOrRest("id", 0); - final subdomain = - optionOrRest("app", argResults!.restIndexAfter(["id"])) ?? "all"; + final domain = + argResults!.optionOrRest("id") ?? + usageException("Missing required option or argument: id"); + final subdomain = argResults!.optionOrRest("app", ["id"]) ?? "all"; final spinner = context.printer.spin(text: "Creating $domain..."); final response = await context.api.post( "/customdomain/create", - body: {"appID": subdomain, "domain": domain}, + body: {"appID": subdomain, "domainName": domain}, ); spinner.success(resolveResponseMessage(response)); diff --git a/lib/commands/domain/delete.dart b/lib/commands/domain/delete.dart index 2645029..59a3595 100644 --- a/lib/commands/domain/delete.dart +++ b/lib/commands/domain/delete.dart @@ -17,7 +17,9 @@ final class CustomdomainDeleteCommand extends Command { @override Future run() async { - final domain = requiredOptionOrRest("id", 0); + final domain = + argResults!.optionOrRest("id") ?? + usageException("Missing required option or argument: id"); final spinner = context.printer.spin(text: "Deleting $domain..."); diff --git a/lib/commands/domain/edit.dart b/lib/commands/domain/edit.dart index 038a557..de26724 100644 --- a/lib/commands/domain/edit.dart +++ b/lib/commands/domain/edit.dart @@ -20,9 +20,10 @@ final class CustomdomainEditCommand extends Command with Disposable { @override Future run() async { - final domain = requiredOptionOrRest("id", 0); - final subdomain = - optionOrRest("app", argResults!.restIndexAfter(["id"])) ?? "all"; + final domain = + argResults!.optionOrRest("id") ?? + usageException("Missing required option or argument: id"); + final subdomain = argResults!.optionOrRest("app", ["id"]) ?? "all"; final spinner = context.printer.spin(text: "Editting $domain..."); diff --git a/lib/commands/domain/info.dart b/lib/commands/domain/info.dart index 73a5e2a..0b05e17 100644 --- a/lib/commands/domain/info.dart +++ b/lib/commands/domain/info.dart @@ -19,7 +19,7 @@ final class CustomdomainInfoCommand extends Command { @override Future run() async { - final domain = optionOrRest("id", 0) ?? "all"; + final domain = argResults!.optionOrRest("id") ?? "all"; final spinner = context.printer.spin(text: "Fetching $domain..."); @@ -27,10 +27,7 @@ final class CustomdomainInfoCommand extends Command { spinner.success(resolveResponseMessage(response)); - switch (response["domain"] ?? - response["domains"] ?? - response["customdomain"] ?? - response["customdomains"]) { + switch (response["domain"] ?? response["domains"]) { case final List list: stdout.writeln(listToAsciiTable(list)); break; diff --git a/lib/commands/domain/verify.dart b/lib/commands/domain/verify.dart index 53818f1..f06e6e5 100644 --- a/lib/commands/domain/verify.dart +++ b/lib/commands/domain/verify.dart @@ -8,7 +8,7 @@ import "package:discloud/utils/messages.dart"; final class CustomdomainVerifyCommand extends Command { CustomdomainVerifyCommand() { - argParser.addOption("id", aliases: const ["domain"], defaultsTo: "all"); + argParser.addOption("id", aliases: const ["domain"]); } @override @@ -19,7 +19,9 @@ final class CustomdomainVerifyCommand extends Command { @override Future run() async { - final domain = optionOrRest("id", 0) ?? "all"; + final domain = + argResults!.optionOrRest("id") ?? + usageException("Missing required option or argument: id"); final spinner = context.printer.spin(text: "Verifying $domain..."); diff --git a/lib/commands/init.dart b/lib/commands/init.dart index c9e73ef..d2d9766 100644 --- a/lib/commands/init.dart +++ b/lib/commands/init.dart @@ -7,14 +7,11 @@ import "package:discloud/extensions/string.dart"; import "package:discloud/services/discloud/constants.dart"; import "package:discloud_config/discloud_config.dart"; -final _configLineBreakPattern = RegExp(r"[\r\n]"); - final class InitCommand extends Command { InitCommand() { argParser ..addFlag( "autorestart", - aliases: const ["ar"], help: "Determines whether the app should automatically restart if it crashes.", negatable: false, @@ -25,31 +22,19 @@ final class InitCommand extends Command { help: "Overwrite config file", negatable: false, ) - ..addFlag( - "overwrite", - abbr: "o", - help: "Overwrite config file", - negatable: false, - ) - ..addFlag("yes", abbr: "y", help: "Skip config prompts", negatable: false) ..addFlag("vlan", help: "Enables private networking", negatable: false) - ..addMultiOption("apt", abbr: "a", help: appApts.join(",")) + ..addMultiOption("apt", help: appApts.join(",")) ..addOption("avatar", help: "Image URL (.gif, .jpeg, .jpg, .png)") - ..addOption( - "engine-version", - aliases: const ["ev", "version"], - help: "current|latest|legacy|suja|x.x.x", - ) ..addOption( "hostname", help: "Custom hostname alias for other apps to reach this one", ) ..addOption("id", help: "User-defined subdomains") - ..addOption("main", abbr: "m", help: "Relative file path") - ..addOption("name", abbr: "n", help: "1 - 30 characters") - ..addOption("ram", abbr: "r", help: "Amount in MB; min 100") - ..addOption("start", abbr: "s", help: "App start") - ..addOption("type", abbr: "t", allowed: const ["bot", "site"]); + ..addOption("main", help: "Relative file path") + ..addOption("name", help: "1 - 30 characters") + ..addOption("ram", help: "Amount in MB; min 100") + ..addOption("type", allowed: const ["bot", "site"]) + ..addOption("version", help: "current|latest|legacy|suja|x.x.x"); } @override @@ -71,19 +56,17 @@ final class InitCommand extends Command { final StringBuffer buffer = .new() ..writeAll([ "# https://docs.discloud.com/en/discloud.config", - if (args.apt case final v when v.isNotEmpty) - _configLine("APT", v.join(",")), - if (args.autorestart case final v when v) _configLine("AUTORESTART", v), - if (args.avatar case final v?) _configLine("AVATAR", v), - if (args.hostname case final v?) _configLine("HOSTNAME", v), - if (args.id case final v?) _configLine("ID", v), - _configLine("MAIN", args.main.orEmpty), - if (args.name case final v?) _configLine("NAME", v), - if (args.ram case final v?) _configLine("RAM", v), - if (args.start case final v?) _configLine("START", v), - if (args.type case final v?) _configLine("TYPE", v), - if (args.version case final v?) _configLine("VERSION", v), - if (args.vlan case final v when v) _configLine("VLAN", v), + if (args.apt case final v when v.isNotEmpty) "APT=${v.join(",")}", + if (args.autorestart case final v when v) "AUTORESTART=$v", + if (args.avatar case final v?) "AVATAR=$v", + if (args.hostname case final v?) "HOSTNAME=$v", + if (args.id case final v?) "ID=$v", + "MAIN=${args.main.orEmpty}", + if (args.name case final v?) "NAME=$v", + if (args.ram case final v?) "RAM=$v", + if (args.type case final v?) "TYPE=$v", + if (args.version case final v?) "VERSION=$v", + if (args.vlan case final v when v) "VLAN=$v", ], "\n"); final sink = file.openWrite()..write(buffer); @@ -101,7 +84,7 @@ class _InitArgs { List _multiOption(String n) => _argResults?.multiOption(n) ?? []; String? _option(String name) => _argResults?.option(name); - bool get force => _flag("force") || _flag("overwrite"); + bool get force => _flag("force"); List get apt => _multiOption("apt"); bool get autorestart => _flag("autorestart"); @@ -111,12 +94,7 @@ class _InitArgs { String? get main => _option("main"); String? get name => _option("name"); String? get ram => _option("ram"); - String? get start => _option("start"); String? get type => _option("type"); - String? get version => _option("engine-version"); + String? get version => _option("version"); bool get vlan => _flag("vlan"); } - -String _configLine(String key, Object value) { - return "$key=${value.toString().replaceAll(_configLineBreakPattern, "")}"; -} diff --git a/lib/commands/subdomain/create.dart b/lib/commands/subdomain/create.dart index fd36aa3..5f50e44 100644 --- a/lib/commands/subdomain/create.dart +++ b/lib/commands/subdomain/create.dart @@ -19,7 +19,9 @@ final class SubdomainCreateCommand extends Command { @override Future run() async { - final id = requiredOptionOrRest("id", 0); + final id = + argResults!.optionOrRest("id") ?? + usageException("Missing required option or argument: id"); final spinner = context.printer.spin(text: "Creating $id..."); diff --git a/lib/commands/subdomain/delete.dart b/lib/commands/subdomain/delete.dart index 0109291..3e527ec 100644 --- a/lib/commands/subdomain/delete.dart +++ b/lib/commands/subdomain/delete.dart @@ -17,7 +17,9 @@ final class SubdomainDeleteCommand extends Command { @override Future run() async { - final id = requiredOptionOrRest("id", 0); + final id = + argResults!.optionOrRest("id") ?? + usageException("Missing required option or argument: id"); final spinner = context.printer.spin(text: "Deleting $id..."); diff --git a/lib/commands/subdomain/info.dart b/lib/commands/subdomain/info.dart index f0947db..0090392 100644 --- a/lib/commands/subdomain/info.dart +++ b/lib/commands/subdomain/info.dart @@ -19,7 +19,7 @@ final class SubdomainInfoCommand extends Command { @override Future run() async { - final id = optionOrRest("id", 0) ?? "all"; + final id = argResults!.optionOrRest("id") ?? "all"; final spinner = context.printer.spin(text: "Fetching $id..."); diff --git a/lib/commands/team/backup.dart b/lib/commands/team/backup.dart index 4648786..22f5559 100644 --- a/lib/commands/team/backup.dart +++ b/lib/commands/team/backup.dart @@ -40,8 +40,8 @@ final class TeamBackupCommand extends Command with Disposable { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; - final dir = optionOrRest("dir", argResults!.restIndexAfter(["app"])); + final appId = argResults!.optionOrRest("app") ?? "all"; + final dir = argResults!.optionOrRest("dir", ["app"]); final spinner = context.printer.spin(text: "Fetching backup..."); diff --git a/lib/commands/team/commit.dart b/lib/commands/team/commit.dart index b998713..fa49a7d 100644 --- a/lib/commands/team/commit.dart +++ b/lib/commands/team/commit.dart @@ -42,14 +42,21 @@ final class TeamCommitCommand extends Command with Disposable { final optionAppId = argResults!.option("app"); final appId = optionAppId ?? - optionOrRest("app", 0) ?? + argResults!.optionOrRest("app") ?? await _getDiscloudConfigAppId(directory); if (appId == null) throw Exception("Missing app id"); final glob = optionAppId == null - ? multiOptionOrRest("glob", 1, defaults: const ["**"]) - : multiOptionOrRest("glob", 0, defaults: const ["**"]); + ? argResults!.multiOptionOrRest( + "glob", + after: ["app"], + defaultFactory: () => const ["**"], + ) + : argResults!.multiOptionOrRest( + "glob", + defaultFactory: () => const ["**"], + ); final spinner = context.printer.spin(text: "Zipping..."); diff --git a/lib/commands/team/info.dart b/lib/commands/team/info.dart index 1e38a60..7364f3c 100644 --- a/lib/commands/team/info.dart +++ b/lib/commands/team/info.dart @@ -32,7 +32,7 @@ final class TeamInfoCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0); + final appId = argResults!.optionOrRest("app"); final spinner = context.printer.spin(text: "Fetching app info..."); diff --git a/lib/commands/team/logs.dart b/lib/commands/team/logs.dart index 30f7ce0..c138a9c 100644 --- a/lib/commands/team/logs.dart +++ b/lib/commands/team/logs.dart @@ -24,12 +24,6 @@ final class TeamLogsCommand extends Command { aliases: const ["path"], help: "Specifies the destination path for downloading logs. When the application option is set to 'all', the destination path will be considered a directory where all downloads will be stored.", - ) - ..addFlag( - "overwrite", - abbr: "o", - help: "Overwrite log files", - negatable: false, ); } @@ -41,9 +35,8 @@ final class TeamLogsCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; - final out = optionOrRest("out", argResults!.restIndexAfter(["app"])); - final overwrite = argResults!.flag("overwrite"); + final appId = argResults!.optionOrRest("app") ?? "all"; + final out = argResults!.optionOrRest("out", ["app"]); final spinner = context.printer.spin(); @@ -53,15 +46,10 @@ final class TeamLogsCommand extends Command { switch (response["apps"]) { case final Map data: - await _handleSingle(data, out, spinner, overwrite: overwrite); + await _handleSingle(data, out, spinner); break; case final List list: - await _handleMulti( - list, - out: out ?? _defaultLogsDir, - overwrite: overwrite, - spinner: spinner, - ); + await _handleMulti(list, out: out ?? _defaultLogsDir, spinner: spinner); break; } } @@ -69,18 +57,12 @@ final class TeamLogsCommand extends Command { Future _handleSingle( Map data, String? out, - ISpin spinner, { - required bool overwrite, - }) async { + ISpin spinner, + ) async { if (data["terminal"]?["big"] case final String contents) { if (out case final out?) { final appId = data["id"]?.toString() ?? "app"; - return _saveLog( - contents, - _resolveLogPath(out, appId), - spinner, - overwrite: overwrite, - ); + return _saveLog(contents, _resolveLogPath(out, appId), spinner); } context.printer.writeln(contents); } @@ -89,28 +71,17 @@ final class TeamLogsCommand extends Command { Future _handleMulti( List list, { required String out, - required bool overwrite, required ISpin spinner, }) async { for (final data in list) { - await _handleSingle(data, out, spinner, overwrite: overwrite); + await _handleSingle(data, out, spinner); } } - Future _saveLog( - String data, - String out, - ISpin spinner, { - required bool overwrite, - }) async { + Future _saveLog(String data, String out, ISpin spinner) async { final file = File(out); await file.parent.create(recursive: true); - final contents = _formatLog(data); - if (overwrite) { - await file.writeAsString(contents); - } else { - await file.writeAsString("$contents\n", mode: FileMode.append); - } + await file.writeAsString(stripAnsi(data)); spinner.success(out); } @@ -119,14 +90,4 @@ final class TeamLogsCommand extends Command { return joinAll([out, "$appId.log"]); } - - String _formatLog(String data) { - return [ - "", - "".padRight(60, "-"), - DateTime.now().toString(), - "", - stripAnsi(data), - ].join("\n"); - } } diff --git a/lib/commands/team/ram.dart b/lib/commands/team/ram.dart index 5900790..8319e46 100644 --- a/lib/commands/team/ram.dart +++ b/lib/commands/team/ram.dart @@ -25,9 +25,15 @@ final class TeamRamCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); + final amount = + argResults!.optionOrRest("amount", ["app"]) ?? + usageException("Missing required option or argument: amount"); final ramMB = max( - requiredIntOptionOrRest("amount", argResults!.restIndexAfter(["app"])), + int.tryParse(amount) ?? + usageException("Invalid integer for amount: $amount"), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/team/restart.dart b/lib/commands/team/restart.dart index f109a91..8b8acfc 100644 --- a/lib/commands/team/restart.dart +++ b/lib/commands/team/restart.dart @@ -17,7 +17,7 @@ final class TeamRestartCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; + final appId = argResults!.optionOrRest("app") ?? "all"; final spinner = context.printer.spin(text: "Restarting app..."); diff --git a/lib/commands/team/start.dart b/lib/commands/team/start.dart index fe02b7b..5966ad2 100644 --- a/lib/commands/team/start.dart +++ b/lib/commands/team/start.dart @@ -17,7 +17,7 @@ final class TeamStartCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; + final appId = argResults!.optionOrRest("app") ?? "all"; final spinner = context.printer.spin(text: "Starting app..."); diff --git a/lib/commands/team/status.dart b/lib/commands/team/status.dart index 8d373e4..7f3c6fa 100644 --- a/lib/commands/team/status.dart +++ b/lib/commands/team/status.dart @@ -21,7 +21,9 @@ final class TeamStatusCommand extends Command { @override Future run() async { - final appId = requiredOptionOrRest("app", 0); + final appId = + argResults!.optionOrRest("app") ?? + usageException("Missing required option or argument: app"); final spinner = context.printer.spin(text: "Fetching app status..."); diff --git a/lib/commands/team/stop.dart b/lib/commands/team/stop.dart index d3f65e5..baeb2be 100644 --- a/lib/commands/team/stop.dart +++ b/lib/commands/team/stop.dart @@ -17,7 +17,7 @@ final class TeamStopCommand extends Command { @override Future run() async { - final appId = optionOrRest("app", 0) ?? "all"; + final appId = argResults!.optionOrRest("app") ?? "all"; final spinner = context.printer.spin(text: "Stopping app..."); diff --git a/lib/commands/zip.dart b/lib/commands/zip.dart index e143fa2..725aac0 100644 --- a/lib/commands/zip.dart +++ b/lib/commands/zip.dart @@ -44,7 +44,10 @@ final class ZipCommand extends Command with Disposable { final encoding = argResults?.option("encoding"); final out = argResults?.option("out") ?? "${basename(directory.path)}.zip"; - final glob = multiOptionOrRest("glob", 0, defaults: const ["**"]); + final glob = argResults!.multiOptionOrRest( + "glob", + defaultFactory: () => const ["**"], + ); final level = _compressionLevel; final password = argResults?.option("password"); diff --git a/lib/extensions/arg_results.dart b/lib/extensions/arg_results.dart index 6d5f2ce..78b1b7d 100644 --- a/lib/extensions/arg_results.dart +++ b/lib/extensions/arg_results.dart @@ -15,3 +15,29 @@ extension NullableArgResultsExtension on ArgResults? { return list.join(" "); } } + +extension ArgResultsExtension on ArgResults { + String? optionOrRest(String name, [Iterable after = const []]) { + if (wasParsed(name)) return option(name); + + final index = after.where((name) => !wasParsed(name)).length; + + if (rest.length > index) return rest[index]; + + return null; + } + + List multiOptionOrRest( + String name, { + Iterable after = const [], + List Function()? defaultFactory, + }) { + if (wasParsed(name)) return multiOption(name); + + final index = after.where((name) => !wasParsed(name)).length; + + if (rest.length > index) return rest.skip(index).toList(); + + return defaultFactory?.call() ?? []; + } +} diff --git a/lib/extensions/command.dart b/lib/extensions/command.dart index 0b689f1..8d32b69 100644 --- a/lib/extensions/command.dart +++ b/lib/extensions/command.dart @@ -1,43 +1,7 @@ -import "package:args/args.dart"; import "package:args/command_runner.dart"; import "package:discloud/cli/context.dart"; -extension ArgResultsExtension on ArgResults { - String? optionOrRest(String name, int index) { - if (wasParsed(name)) { - final value = option(name); - if (value != null && value.isNotEmpty) return value; - } - - if (rest.length > index) return rest[index]; - - final value = option(name); - if (value != null && value.isNotEmpty) return value; - - return null; - } - - List multiOptionOrRest( - String name, - int index, { - List defaults = const [], - }) { - if (wasParsed(name)) { - final values = multiOption(name); - if (values.isNotEmpty) return values; - } - - if (rest.length > index) return rest.skip(index).toList(); - - final values = multiOption(name); - if (values.isNotEmpty) return values; - - return defaults; - } - - int restIndexAfter(Iterable previousOptions) => - previousOptions.where((name) => !wasParsed(name)).length; -} +export "package:discloud/extensions/arg_results.dart"; extension CommandExtension on Command { CliContext get context => .I; @@ -47,32 +11,4 @@ extension CommandExtension on Command { addSubcommand(command); } } - - String? optionOrRest(String name, int index) { - return argResults?.optionOrRest(name, index); - } - - String requiredOptionOrRest(String name, int index) { - final value = optionOrRest(name, index); - if (value != null) return value; - - usageException("Missing required option or argument: $name"); - } - - int requiredIntOptionOrRest(String name, int index) { - final raw = requiredOptionOrRest(name, index); - final value = int.tryParse(raw); - if (value != null) return value; - - usageException("Invalid integer for $name: $raw"); - } - - List multiOptionOrRest( - String name, - int index, { - List defaults = const [], - }) { - return argResults?.multiOptionOrRest(name, index, defaults: defaults) ?? - defaults; - } } diff --git a/lib/services/discloud/constants.dart b/lib/services/discloud/constants.dart index f9804ac..c693c23 100644 --- a/lib/services/discloud/constants.dart +++ b/lib/services/discloud/constants.dart @@ -30,9 +30,13 @@ const appApts = { "ffmpeg", "java", "libgl", + "mysql", "openssl", "puppeteer", + "selenium", + "tesseract", "tools", + "unixodbc", }; const appModPerms = { diff --git a/lib/utils/download.dart b/lib/utils/download.dart index f18ffa2..14c33cc 100644 --- a/lib/utils/download.dart +++ b/lib/utils/download.dart @@ -10,9 +10,6 @@ Future download( HttpClient? client, VoidProgressCallback? onProgress, }) async { - file ??= .new(url.pathSegments.last); - await file.parent.create(recursive: true); - final client0 = client ?? .new(); IOSink? sink; @@ -26,6 +23,9 @@ Future download( ); } + file ??= .new(url.pathSegments.last); + await file.parent.create(recursive: true); + sink = file.openWrite(); if (onProgress case final onProgress?) { await _downloadWithProgress( @@ -39,7 +39,7 @@ Future download( } catch (_) { await sink?.close(); sink = null; - if (await file.exists()) await file.delete(); + if (file != null && await file.exists()) await file.delete(); rethrow; } finally { await sink?.close(); diff --git a/lib/utils/progress.dart b/lib/utils/progress.dart index 46e2714..2ae2542 100644 --- a/lib/utils/progress.dart +++ b/lib/utils/progress.dart @@ -1,3 +1,4 @@ +import "package:discloud/extensions/num.dart"; import "package:discloud/utils/bytes.dart"; import "package:discloud/utils/formatters.dart"; @@ -24,17 +25,16 @@ String formatProgressMessage({ final StringBuffer buffer = .new(prefixText); if (speed case final speed?) { - buffer.writeAll([" ", ?direction, Bytes.bits(speed * 8), "/s "]); + buffer.writeAll([" ", ?direction, Bytes.bits(speed * 8), "/s"]); } - if (total <= 0) { - buffer - ..write(" ") - ..write(Bytes(processed).toString(" ")); - return buffer.toString(); - } + buffer.write(" "); - buffer.write(percentFormatter.format(processed / total)); + if (total.isNegativeOrZero) { + buffer.write(Bytes(processed).toString(" ")); + } else { + buffer.write(percentFormatter.format(processed / total)); + } return buffer.toString(); } diff --git a/modules/local_store/lib/src/json_local_store.dart b/modules/local_store/lib/src/json_local_store.dart index 3f2d567..1f1f52e 100644 --- a/modules/local_store/lib/src/json_local_store.dart +++ b/modules/local_store/lib/src/json_local_store.dart @@ -38,9 +38,6 @@ class _JSONLocalStore implements LocalStore { final temp = File("${_file.path}.tmp"); await temp.writeAsBytes(_jsonBase64Encoder.convert(_cache), flush: true); - try { - await _file.delete(); - } catch (_) {} await temp.rename(_file.path); } From d45149e3ba12c4b08a3aa125ed960a1c5f691527 Mon Sep 17 00:00:00 2001 From: "Matheus Henrique(Aka: TheusHen)" Date: Tue, 7 Jul 2026 15:32:52 -0300 Subject: [PATCH 06/24] Address latest PR review feedback --- lib/commands/app/apt/install.dart | 8 ++--- lib/commands/app/apt/uninstall.dart | 8 ++--- lib/commands/app/commit.dart | 11 ++---- lib/commands/app/console.dart | 4 +-- lib/commands/app/delete.dart | 4 +-- lib/commands/app/logs.dart | 4 +-- lib/commands/app/mod/add.dart | 8 ++--- lib/commands/app/mod/delete.dart | 8 ++--- lib/commands/app/mod/edit.dart | 8 ++--- lib/commands/app/mod/info.dart | 4 +-- lib/commands/app/profile.dart | 4 +-- lib/commands/app/ram.dart | 10 ++---- lib/commands/app/status.dart | 4 +-- lib/commands/app/upload.dart | 5 +-- lib/commands/domain/create.dart | 12 +++---- lib/commands/domain/delete.dart | 4 +-- lib/commands/domain/edit.dart | 8 ++--- lib/commands/domain/verify.dart | 8 ++--- lib/commands/subdomain/create.dart | 8 ++--- lib/commands/subdomain/delete.dart | 4 +-- lib/commands/team/commit.dart | 11 ++---- lib/commands/team/logs.dart | 4 +-- lib/commands/team/ram.dart | 10 ++---- lib/commands/team/status.dart | 4 +-- lib/commands/zip.dart | 5 +-- lib/extensions/arg_results.dart | 49 +++++++++++++++++++++++---- lib/services/discloud/api_client.dart | 3 ++ 27 files changed, 86 insertions(+), 134 deletions(-) diff --git a/lib/commands/app/apt/install.dart b/lib/commands/app/apt/install.dart index e86532f..cf5ee2c 100644 --- a/lib/commands/app/apt/install.dart +++ b/lib/commands/app/apt/install.dart @@ -23,12 +23,8 @@ final class AppAptInstallCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); - final apts = argResults!.multiOptionOrRest("apt", after: ["app"]); - - if (apts.isEmpty) usageException("Apt option cannot be empty"); + final appId = argResults!.requiredOptionOrRest("app"); + final apts = argResults!.requiredMultiOptionOrRest("apt", ["app"]); final spinner = context.printer.spin(text: "Installing app apt..."); diff --git a/lib/commands/app/apt/uninstall.dart b/lib/commands/app/apt/uninstall.dart index 61d73be..25672f7 100644 --- a/lib/commands/app/apt/uninstall.dart +++ b/lib/commands/app/apt/uninstall.dart @@ -23,12 +23,8 @@ final class AppAptUninstallCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); - final apts = argResults!.multiOptionOrRest("apt", after: ["app"]); - - if (apts.isEmpty) usageException("Apt option cannot be empty"); + final appId = argResults!.requiredOptionOrRest("app"); + final apts = argResults!.requiredMultiOptionOrRest("apt", ["app"]); final spinner = context.printer.spin(text: "Uninstalling app apt..."); diff --git a/lib/commands/app/commit.dart b/lib/commands/app/commit.dart index d5fe8df..9b87567 100644 --- a/lib/commands/app/commit.dart +++ b/lib/commands/app/commit.dart @@ -48,15 +48,8 @@ final class AppCommitCommand extends Command with Disposable { if (appId == null) throw Exception("Missing app id"); final glob = optionAppId == null - ? argResults!.multiOptionOrRest( - "glob", - after: ["app"], - defaultFactory: () => const ["**"], - ) - : argResults!.multiOptionOrRest( - "glob", - defaultFactory: () => const ["**"], - ); + ? argResults!.multiOptionOrRest("glob", ["app"]) ?? const ["**"] + : argResults!.multiOptionOrRest("glob") ?? const ["**"]; final spinner = context.printer.spin(text: "Zipping..."); diff --git a/lib/commands/app/console.dart b/lib/commands/app/console.dart index bcff040..acb78be 100644 --- a/lib/commands/app/console.dart +++ b/lib/commands/app/console.dart @@ -26,9 +26,7 @@ final class AppConsoleCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); + final appId = argResults!.requiredOptionOrRest("app"); String? command = argResults!.option("command"); final spinner = context.printer.spin(start: false); diff --git a/lib/commands/app/delete.dart b/lib/commands/app/delete.dart index 951d8f2..5c6ec44 100644 --- a/lib/commands/app/delete.dart +++ b/lib/commands/app/delete.dart @@ -25,9 +25,7 @@ final class AppDeleteCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); + final appId = argResults!.requiredOptionOrRest("app"); final result = argResults!.flag("yes") || diff --git a/lib/commands/app/logs.dart b/lib/commands/app/logs.dart index 4e76bae..d3ee396 100644 --- a/lib/commands/app/logs.dart +++ b/lib/commands/app/logs.dart @@ -8,8 +8,6 @@ import "package:discloud/extensions/command.dart"; import "package:discloud/utils/messages.dart"; import "package:path/path.dart" hide context; -const _defaultLogsDir = "discloud/logs"; - final class AppLogsCommand extends Command { AppLogsCommand() { argParser @@ -49,7 +47,7 @@ final class AppLogsCommand extends Command { await _handleSingle(data, out, spinner); break; case final List list: - await _handleMulti(list, out: out ?? _defaultLogsDir, spinner: spinner); + await _handleMulti(list, out: out ?? ".", spinner: spinner); break; } } diff --git a/lib/commands/app/mod/add.dart b/lib/commands/app/mod/add.dart index dcd0a0d..cf8b4ee 100644 --- a/lib/commands/app/mod/add.dart +++ b/lib/commands/app/mod/add.dart @@ -21,12 +21,8 @@ final class AppModAddCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); - final modId = - argResults!.optionOrRest("mod", ["app"]) ?? - usageException("Missing required option or argument: mod"); + final appId = argResults!.requiredOptionOrRest("app"); + final modId = argResults!.requiredOptionOrRest("mod", ["app"]); final perms = argResults!.multiOption("perms"); final spinner = context.printer.spin(text: "Adding app MOD..."); diff --git a/lib/commands/app/mod/delete.dart b/lib/commands/app/mod/delete.dart index 3fa2258..fcf9b84 100644 --- a/lib/commands/app/mod/delete.dart +++ b/lib/commands/app/mod/delete.dart @@ -19,12 +19,8 @@ final class AppModDeleteCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); - final modId = - argResults!.optionOrRest("mod", ["app"]) ?? - usageException("Missing required option or argument: mod"); + final appId = argResults!.requiredOptionOrRest("app"); + final modId = argResults!.requiredOptionOrRest("mod", ["app"]); final spinner = context.printer.spin(text: "Deleting app MOD..."); diff --git a/lib/commands/app/mod/edit.dart b/lib/commands/app/mod/edit.dart index b19a127..a80ecef 100644 --- a/lib/commands/app/mod/edit.dart +++ b/lib/commands/app/mod/edit.dart @@ -21,12 +21,8 @@ final class AppModEditCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); - final modId = - argResults!.optionOrRest("mod", ["app"]) ?? - usageException("Missing required option or argument: mod"); + final appId = argResults!.requiredOptionOrRest("app"); + final modId = argResults!.requiredOptionOrRest("mod", ["app"]); final perms = argResults!.multiOption("perms"); final spinner = context.printer.spin(text: "Editing app MOD..."); diff --git a/lib/commands/app/mod/info.dart b/lib/commands/app/mod/info.dart index 74578ee..fe0f1ae 100644 --- a/lib/commands/app/mod/info.dart +++ b/lib/commands/app/mod/info.dart @@ -17,9 +17,7 @@ final class AppModInfoCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); + final appId = argResults!.requiredOptionOrRest("app"); final spinner = context.printer.spin(text: "Fetching app MODs..."); diff --git a/lib/commands/app/profile.dart b/lib/commands/app/profile.dart index eee3dce..5b0d5f6 100644 --- a/lib/commands/app/profile.dart +++ b/lib/commands/app/profile.dart @@ -21,9 +21,7 @@ final class AppProfileCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); + final appId = argResults!.requiredOptionOrRest("app"); final name = argResults?.option("name"); final avatar = argResults?.option("avatar"); diff --git a/lib/commands/app/ram.dart b/lib/commands/app/ram.dart index 0100792..bedc086 100644 --- a/lib/commands/app/ram.dart +++ b/lib/commands/app/ram.dart @@ -25,15 +25,9 @@ final class AppRamCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); - final amount = - argResults!.optionOrRest("amount", ["app"]) ?? - usageException("Missing required option or argument: amount"); + final appId = argResults!.requiredOptionOrRest("app"); final ramMB = max( - int.tryParse(amount) ?? - usageException("Invalid integer for amount: $amount"), + argResults!.requiredIntOptionOrRest("amount", ["app"]), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/app/status.dart b/lib/commands/app/status.dart index 6db3aee..b854115 100644 --- a/lib/commands/app/status.dart +++ b/lib/commands/app/status.dart @@ -21,9 +21,7 @@ final class AppStatusCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); + final appId = argResults!.requiredOptionOrRest("app"); final spinner = context.printer.spin(text: "Fetching app status..."); diff --git a/lib/commands/app/upload.dart b/lib/commands/app/upload.dart index e8cb446..7e92dba 100644 --- a/lib/commands/app/upload.dart +++ b/lib/commands/app/upload.dart @@ -39,10 +39,7 @@ final class AppUploadCommand extends Command with Disposable { await config.validate(); - final glob = argResults!.multiOptionOrRest( - "glob", - defaultFactory: () => const ["**"], - ); + final glob = argResults!.multiOptionOrRest("glob") ?? const ["**"]; final spinner = context.printer.spin(text: "Zipping..."); diff --git a/lib/commands/domain/create.dart b/lib/commands/domain/create.dart index 89d82ab..8fe867f 100644 --- a/lib/commands/domain/create.dart +++ b/lib/commands/domain/create.dart @@ -10,7 +10,7 @@ final class CustomdomainCreateCommand extends Command { CustomdomainCreateCommand() { argParser ..addOption("id", aliases: const ["domain"]) - ..addOption("app", aliases: const ["subdomain"], defaultsTo: "all"); + ..addOption("app", aliases: const ["subdomain"]); } @override @@ -21,10 +21,8 @@ final class CustomdomainCreateCommand extends Command { @override Future run() async { - final domain = - argResults!.optionOrRest("id") ?? - usageException("Missing required option or argument: id"); - final subdomain = argResults!.optionOrRest("app", ["id"]) ?? "all"; + final domain = argResults!.requiredOptionOrRest("id"); + final subdomain = argResults!.requiredOptionOrRest("app", ["id"]); final spinner = context.printer.spin(text: "Creating $domain..."); @@ -35,8 +33,6 @@ final class CustomdomainCreateCommand extends Command { spinner.success(resolveResponseMessage(response)); - if (response["domain"] ?? response["customdomain"] case final Map data) { - stdout.writeln(mapToVerticalAsciiTable(data)); - } + stdout.writeln(mapToVerticalAsciiTable(response["domain"])); } } diff --git a/lib/commands/domain/delete.dart b/lib/commands/domain/delete.dart index 59a3595..a52d709 100644 --- a/lib/commands/domain/delete.dart +++ b/lib/commands/domain/delete.dart @@ -17,9 +17,7 @@ final class CustomdomainDeleteCommand extends Command { @override Future run() async { - final domain = - argResults!.optionOrRest("id") ?? - usageException("Missing required option or argument: id"); + final domain = argResults!.requiredOptionOrRest("id"); final spinner = context.printer.spin(text: "Deleting $domain..."); diff --git a/lib/commands/domain/edit.dart b/lib/commands/domain/edit.dart index de26724..bf1532b 100644 --- a/lib/commands/domain/edit.dart +++ b/lib/commands/domain/edit.dart @@ -9,7 +9,7 @@ final class CustomdomainEditCommand extends Command with Disposable { CustomdomainEditCommand() { argParser ..addOption("id", aliases: const ["domain"]) - ..addOption("app", aliases: const ["subdomain"], defaultsTo: "all"); + ..addOption("app", aliases: const ["subdomain"]); } @override @@ -20,10 +20,8 @@ final class CustomdomainEditCommand extends Command with Disposable { @override Future run() async { - final domain = - argResults!.optionOrRest("id") ?? - usageException("Missing required option or argument: id"); - final subdomain = argResults!.optionOrRest("app", ["id"]) ?? "all"; + final domain = argResults!.requiredOptionOrRest("id"); + final subdomain = argResults!.requiredOptionOrRest("app", ["id"]); final spinner = context.printer.spin(text: "Editting $domain..."); diff --git a/lib/commands/domain/verify.dart b/lib/commands/domain/verify.dart index f06e6e5..c6fda73 100644 --- a/lib/commands/domain/verify.dart +++ b/lib/commands/domain/verify.dart @@ -19,9 +19,7 @@ final class CustomdomainVerifyCommand extends Command { @override Future run() async { - final domain = - argResults!.optionOrRest("id") ?? - usageException("Missing required option or argument: id"); + final domain = argResults!.requiredOptionOrRest("id"); final spinner = context.printer.spin(text: "Verifying $domain..."); @@ -29,8 +27,6 @@ final class CustomdomainVerifyCommand extends Command { spinner.success(resolveResponseMessage(response)); - if (response["domain"] ?? response["customdomain"] case final Map data) { - stdout.writeln(mapToVerticalAsciiTable(data)); - } + stdout.writeln(mapToVerticalAsciiTable(response["domain"])); } } diff --git a/lib/commands/subdomain/create.dart b/lib/commands/subdomain/create.dart index 5f50e44..88d0416 100644 --- a/lib/commands/subdomain/create.dart +++ b/lib/commands/subdomain/create.dart @@ -19,9 +19,7 @@ final class SubdomainCreateCommand extends Command { @override Future run() async { - final id = - argResults!.optionOrRest("id") ?? - usageException("Missing required option or argument: id"); + final id = argResults!.requiredOptionOrRest("id"); final spinner = context.printer.spin(text: "Creating $id..."); @@ -29,8 +27,6 @@ final class SubdomainCreateCommand extends Command { spinner.success(resolveResponseMessage(response)); - if (response["subdomain"] case final Map data) { - stdout.writeln(mapToVerticalAsciiTable(data)); - } + stdout.writeln(mapToVerticalAsciiTable(response["subdomain"])); } } diff --git a/lib/commands/subdomain/delete.dart b/lib/commands/subdomain/delete.dart index 3e527ec..85dec9d 100644 --- a/lib/commands/subdomain/delete.dart +++ b/lib/commands/subdomain/delete.dart @@ -17,9 +17,7 @@ final class SubdomainDeleteCommand extends Command { @override Future run() async { - final id = - argResults!.optionOrRest("id") ?? - usageException("Missing required option or argument: id"); + final id = argResults!.requiredOptionOrRest("id"); final spinner = context.printer.spin(text: "Deleting $id..."); diff --git a/lib/commands/team/commit.dart b/lib/commands/team/commit.dart index fa49a7d..89b1cda 100644 --- a/lib/commands/team/commit.dart +++ b/lib/commands/team/commit.dart @@ -48,15 +48,8 @@ final class TeamCommitCommand extends Command with Disposable { if (appId == null) throw Exception("Missing app id"); final glob = optionAppId == null - ? argResults!.multiOptionOrRest( - "glob", - after: ["app"], - defaultFactory: () => const ["**"], - ) - : argResults!.multiOptionOrRest( - "glob", - defaultFactory: () => const ["**"], - ); + ? argResults!.multiOptionOrRest("glob", ["app"]) ?? const ["**"] + : argResults!.multiOptionOrRest("glob") ?? const ["**"]; final spinner = context.printer.spin(text: "Zipping..."); diff --git a/lib/commands/team/logs.dart b/lib/commands/team/logs.dart index c138a9c..ce3059a 100644 --- a/lib/commands/team/logs.dart +++ b/lib/commands/team/logs.dart @@ -8,8 +8,6 @@ import "package:discloud/extensions/command.dart"; import "package:discloud/utils/messages.dart"; import "package:path/path.dart" hide context; -const _defaultLogsDir = "discloud/logs"; - final class TeamLogsCommand extends Command { TeamLogsCommand() { argParser @@ -49,7 +47,7 @@ final class TeamLogsCommand extends Command { await _handleSingle(data, out, spinner); break; case final List list: - await _handleMulti(list, out: out ?? _defaultLogsDir, spinner: spinner); + await _handleMulti(list, out: out ?? ".", spinner: spinner); break; } } diff --git a/lib/commands/team/ram.dart b/lib/commands/team/ram.dart index 8319e46..b8d6843 100644 --- a/lib/commands/team/ram.dart +++ b/lib/commands/team/ram.dart @@ -25,15 +25,9 @@ final class TeamRamCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); - final amount = - argResults!.optionOrRest("amount", ["app"]) ?? - usageException("Missing required option or argument: amount"); + final appId = argResults!.requiredOptionOrRest("app"); final ramMB = max( - int.tryParse(amount) ?? - usageException("Invalid integer for amount: $amount"), + argResults!.requiredIntOptionOrRest("amount", ["app"]), DiscloudRamMinByType.lowest.value, ); diff --git a/lib/commands/team/status.dart b/lib/commands/team/status.dart index 7f3c6fa..9449601 100644 --- a/lib/commands/team/status.dart +++ b/lib/commands/team/status.dart @@ -21,9 +21,7 @@ final class TeamStatusCommand extends Command { @override Future run() async { - final appId = - argResults!.optionOrRest("app") ?? - usageException("Missing required option or argument: app"); + final appId = argResults!.requiredOptionOrRest("app"); final spinner = context.printer.spin(text: "Fetching app status..."); diff --git a/lib/commands/zip.dart b/lib/commands/zip.dart index 725aac0..b047440 100644 --- a/lib/commands/zip.dart +++ b/lib/commands/zip.dart @@ -44,10 +44,7 @@ final class ZipCommand extends Command with Disposable { final encoding = argResults?.option("encoding"); final out = argResults?.option("out") ?? "${basename(directory.path)}.zip"; - final glob = argResults!.multiOptionOrRest( - "glob", - defaultFactory: () => const ["**"], - ); + final glob = argResults!.multiOptionOrRest("glob") ?? const ["**"]; final level = _compressionLevel; final password = argResults?.option("password"); diff --git a/lib/extensions/arg_results.dart b/lib/extensions/arg_results.dart index 78b1b7d..e4cbc1d 100644 --- a/lib/extensions/arg_results.dart +++ b/lib/extensions/arg_results.dart @@ -1,4 +1,5 @@ import "package:args/args.dart"; +import "package:args/command_runner.dart"; extension NullableArgResultsExtension on ArgResults? { String? get commandName { @@ -17,27 +18,61 @@ extension NullableArgResultsExtension on ArgResults? { } extension ArgResultsExtension on ArgResults { + bool wasNotParsed(String name) { + return !wasParsed(name); + } + String? optionOrRest(String name, [Iterable after = const []]) { if (wasParsed(name)) return option(name); - final index = after.where((name) => !wasParsed(name)).length; + final index = after.where(wasNotParsed).length; if (rest.length > index) return rest[index]; return null; } - List multiOptionOrRest( - String name, { + String requiredOptionOrRest( + String name, [ + Iterable after = const [], + ]) { + final value = optionOrRest(name, after); + if (value != null) return value; + + throw UsageException("Missing required option or argument: $name", ""); + } + + int requiredIntOptionOrRest( + String name, [ Iterable after = const [], - List Function()? defaultFactory, - }) { + ]) { + final raw = requiredOptionOrRest(name, after); + final value = int.tryParse(raw); + if (value != null) return value; + + throw UsageException("Invalid integer for $name: $raw", ""); + } + + List? multiOptionOrRest( + String name, [ + Iterable after = const [], + ]) { if (wasParsed(name)) return multiOption(name); - final index = after.where((name) => !wasParsed(name)).length; + final index = after.where(wasNotParsed).length; if (rest.length > index) return rest.skip(index).toList(); - return defaultFactory?.call() ?? []; + return null; + } + + List requiredMultiOptionOrRest( + String name, [ + Iterable after = const [], + ]) { + final values = multiOptionOrRest(name, after); + if (values != null && values.isNotEmpty) return values; + + throw UsageException("Missing required option or argument: $name", ""); } } diff --git a/lib/services/discloud/api_client.dart b/lib/services/discloud/api_client.dart index a28a525..9f7bb3b 100644 --- a/lib/services/discloud/api_client.dart +++ b/lib/services/discloud/api_client.dart @@ -4,6 +4,7 @@ import "dart:io"; import "package:discloud/cli/context.dart"; import "package:discloud/cli/disposable.dart"; +import "package:discloud/extensions/file.dart"; import "package:discloud/extensions/io_http_client.dart"; import "package:discloud/extensions/string.dart"; import "package:discloud/services/discloud/exception.dart"; @@ -308,6 +309,8 @@ class DiscloudApiClient implements Disposable { request.write("\r\n--$boundary--\r\n"); + await file.safeDelete(); + await onUploadDone?.call(); } From 6fc88129de5f0c424df6137378b2522fddaa80f3 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:03:21 -0300 Subject: [PATCH 07/24] Add validation for non-empty apt options --- lib/commands/app/apt/install.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/commands/app/apt/install.dart b/lib/commands/app/apt/install.dart index cf5ee2c..57b18d5 100644 --- a/lib/commands/app/apt/install.dart +++ b/lib/commands/app/apt/install.dart @@ -26,6 +26,8 @@ final class AppAptInstallCommand extends Command { final appId = argResults!.requiredOptionOrRest("app"); final apts = argResults!.requiredMultiOptionOrRest("apt", ["app"]); + if (apts.isEmpty) usageException("Apt option cannot be empty"); + final spinner = context.printer.spin(text: "Installing app apt..."); final response = await context.api.put( From ea65bf496c56342fb0d36b6e33f17dfcfb01d583 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:04:00 -0300 Subject: [PATCH 08/24] Validate 'apt' options in uninstall command Added validation to ensure 'apt' options are not empty. --- lib/commands/app/apt/uninstall.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/commands/app/apt/uninstall.dart b/lib/commands/app/apt/uninstall.dart index 25672f7..8084089 100644 --- a/lib/commands/app/apt/uninstall.dart +++ b/lib/commands/app/apt/uninstall.dart @@ -26,6 +26,8 @@ final class AppAptUninstallCommand extends Command { final appId = argResults!.requiredOptionOrRest("app"); final apts = argResults!.requiredMultiOptionOrRest("apt", ["app"]); + if (apts.isEmpty) usageException("Apt option cannot be empty"); + final spinner = context.printer.spin(text: "Uninstalling app apt..."); final response = await context.api.delete( From 418a9cbe83a4047b7909e6fb4df31934b7030d31 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:06:02 -0300 Subject: [PATCH 09/24] Refactor spinner success call in info.dart Removed redundant success spinner call when team list is not empty. --- lib/commands/app/mod/info.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/commands/app/mod/info.dart b/lib/commands/app/mod/info.dart index fe0f1ae..85d46fa 100644 --- a/lib/commands/app/mod/info.dart +++ b/lib/commands/app/mod/info.dart @@ -23,11 +23,10 @@ final class AppModInfoCommand extends Command { final response = await context.api.get("/app/$appId/team"); + spinner.success(resolveResponseMessage(response)); + if (response["team"] case final List list when list.isNotEmpty) { - spinner.success(resolveResponseMessage(response)); context.printer.writeln(listToAsciiTable(list)); - } else { - spinner.success(resolveResponseMessage(response)); } } } From 818456bc2a00f05d681ec94886e0569691c31618 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:11:51 -0300 Subject: [PATCH 10/24] Require app option and simplify directory logic Refactor backup command to require 'app' option and streamline directory handling. --- lib/commands/app/backup.dart | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/lib/commands/app/backup.dart b/lib/commands/app/backup.dart index 084109f..cd102e4 100644 --- a/lib/commands/app/backup.dart +++ b/lib/commands/app/backup.dart @@ -40,8 +40,7 @@ final class AppBackupCommand extends Command with Disposable { @override Future run() async { - final appId = argResults!.optionOrRest("app") ?? "all"; - final dir = argResults!.optionOrRest("dir", ["app"]); + final appId = argResults!.requiredOptionOrRest("app"); final spinner = context.printer.spin(text: "Fetching backup..."); @@ -51,21 +50,17 @@ final class AppBackupCommand extends Command with Disposable { switch (response["backups"]) { case final Map data: - await _handleSingle(data, dir: dir, spinner: spinner); + await _handleSingle(data, spinner: spinner); break; case final List list: - await _handleMulti(list, dir: dir, spinner: spinner); + await _handleMulti(list, spinner: spinner); break; } } - Future _handleSingle( - Map data, { - required ISpin spinner, - String? dir, - }) async { + Future _handleSingle(Map data, {required ISpin spinner}) async { if (data["url"] case final String url) { - if (dir != null) { + if (argResults!.optionOrRest("dir", ["app"]) case final dir) { final Uri uri = .parse(url); return _download(dir: dir, spinner: spinner, uri: uri); @@ -75,21 +70,9 @@ final class AppBackupCommand extends Command with Disposable { } } - Future _handleMulti( - List list, { - required ISpin spinner, - String? dir, - }) async { - if (dir == null) { - for (final data in list) { - if (data["url"] case final String url) { - context.printer.writeln(url); - } - } - return; - } - + Future _handleMulti(List list, {required ISpin spinner}) async { final client = _client = .new(); + final dir = argResults?.optionOrRest("dir") ?? "."; for (final data in list) { final String appId = data["id"]; From 284b8165a635789dba208a2ba6afa17088c53b90 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:12:48 -0300 Subject: [PATCH 11/24] Refactor spinner parameter handling in backup.dart --- lib/commands/app/backup.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/commands/app/backup.dart b/lib/commands/app/backup.dart index cd102e4..15e9a58 100644 --- a/lib/commands/app/backup.dart +++ b/lib/commands/app/backup.dart @@ -50,15 +50,15 @@ final class AppBackupCommand extends Command with Disposable { switch (response["backups"]) { case final Map data: - await _handleSingle(data, spinner: spinner); + await _handleSingle(data, spinner); break; case final List list: - await _handleMulti(list, spinner: spinner); + await _handleMulti(list, spinner); break; } } - Future _handleSingle(Map data, {required ISpin spinner}) async { + Future _handleSingle(Map data, ISpin spinner) async { if (data["url"] case final String url) { if (argResults!.optionOrRest("dir", ["app"]) case final dir) { final Uri uri = .parse(url); @@ -70,7 +70,7 @@ final class AppBackupCommand extends Command with Disposable { } } - Future _handleMulti(List list, {required ISpin spinner}) async { + Future _handleMulti(List list, ISpin spinner) async { final client = _client = .new(); final dir = argResults?.optionOrRest("dir") ?? "."; From 7141841fdf49b33b9eb1e0b969eb8b2b56496f77 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:17:14 -0300 Subject: [PATCH 12/24] Refactor glob assignment logic in commit.dart --- lib/commands/app/commit.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/commands/app/commit.dart b/lib/commands/app/commit.dart index 9b87567..c5405e9 100644 --- a/lib/commands/app/commit.dart +++ b/lib/commands/app/commit.dart @@ -42,14 +42,12 @@ final class AppCommitCommand extends Command with Disposable { final optionAppId = argResults!.option("app"); final appId = optionAppId ?? - argResults!.optionOrRest("app") ?? + argResults?.rest.firstOrNull ?? await _getDiscloudConfigAppId(directory); if (appId == null) throw Exception("Missing app id"); - final glob = optionAppId == null - ? argResults!.multiOptionOrRest("glob", ["app"]) ?? const ["**"] - : argResults!.multiOptionOrRest("glob") ?? const ["**"]; + final glob = argResults!.multiOptionOrRest("glob", [if (optionAppId != null) "app"]) ?? const ["**"]; final spinner = context.printer.spin(text: "Zipping..."); From e2bc5c7ee246b23369acec7d2628e2ad629224ec Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:18:00 -0300 Subject: [PATCH 13/24] Fix null safety for multiOptionOrRest method --- lib/commands/app/commit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/app/commit.dart b/lib/commands/app/commit.dart index c5405e9..4de7024 100644 --- a/lib/commands/app/commit.dart +++ b/lib/commands/app/commit.dart @@ -47,7 +47,7 @@ final class AppCommitCommand extends Command with Disposable { if (appId == null) throw Exception("Missing app id"); - final glob = argResults!.multiOptionOrRest("glob", [if (optionAppId != null) "app"]) ?? const ["**"]; + final glob = argResults?.multiOptionOrRest("glob", [if (optionAppId != null) "app"]) ?? const ["**"]; final spinner = context.printer.spin(text: "Zipping..."); From 00b1efc1578d8ac6ef4710237b7d17ed7ca634a5 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:20:35 -0300 Subject: [PATCH 14/24] Update app option handling in AppInfoCommand --- lib/commands/app/info.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commands/app/info.dart b/lib/commands/app/info.dart index 9023810..699de24 100644 --- a/lib/commands/app/info.dart +++ b/lib/commands/app/info.dart @@ -22,7 +22,7 @@ const _keysIgnore = { final class AppInfoCommand extends Command { AppInfoCommand() { - argParser.addOption("app", defaultsTo: "all"); + argParser.addOption("app", valueHelp: "all"); } @override @@ -33,7 +33,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..."); From da02f672363c448d0d65c2fe63fc9686e82fa513 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:35:46 -0300 Subject: [PATCH 15/24] Refactor log handling functions and parameters --- lib/commands/app/logs.dart | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/commands/app/logs.dart b/lib/commands/app/logs.dart index d3ee396..d6a01ce 100644 --- a/lib/commands/app/logs.dart +++ b/lib/commands/app/logs.dart @@ -44,48 +44,48 @@ final class AppLogsCommand extends Command { switch (response["apps"]) { case final Map data: - await _handleSingle(data, out, spinner); + await _handleSingle(data, spinner, appId, out); break; case final List list: - await _handleMulti(list, out: out ?? ".", spinner: spinner); + await _handleMulti(list, spinner, appId, out ?? "."); break; } } Future _handleSingle( Map data, - String? out, ISpin spinner, + String appId, + String? out, ) async { if (data["terminal"]?["big"] case final String contents) { if (out case final out?) { - final appId = data["id"]?.toString() ?? "app"; - return _saveLog(contents, _resolveLogPath(out, appId), spinner); + return _saveLog(contents, _resolvePath(out, appId), spinner); } context.printer.writeln(contents); } } Future _handleMulti( - List list, { - required String out, - required ISpin spinner, - }) async { + List list, + ISpin spinner, + String appId, + String out, + ) async { for (final data in list) { - await _handleSingle(data, out, spinner); + await _handleSingle(data, spinner, appId, out); } } Future _saveLog(String data, String out, ISpin spinner) async { - final file = File(out); + final File file = .new(out); await file.parent.create(recursive: true); await file.writeAsString(stripAnsi(data)); spinner.success(out); } - String _resolveLogPath(String out, String appId) { + String _resolvePath(String out, String appId) { if (extension(out).isNotEmpty) return out; - return joinAll([out, "$appId.log"]); } } From 145a7acf07dedbb229fbf0c8a8052bf6d202f7b7 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:38:00 -0300 Subject: [PATCH 16/24] Refactor _handleMulti to remove appId parameter --- lib/commands/app/logs.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/commands/app/logs.dart b/lib/commands/app/logs.dart index d6a01ce..24b6c62 100644 --- a/lib/commands/app/logs.dart +++ b/lib/commands/app/logs.dart @@ -47,7 +47,7 @@ final class AppLogsCommand extends Command { await _handleSingle(data, spinner, appId, out); break; case final List list: - await _handleMulti(list, spinner, appId, out ?? "."); + await _handleMulti(list, spinner, out ?? "."); break; } } @@ -66,13 +66,9 @@ final class AppLogsCommand extends Command { } } - Future _handleMulti( - List list, - ISpin spinner, - String appId, - String out, - ) async { + Future _handleMulti(List list, ISpin spinner, String out) async { for (final data in list) { + final String appId = data["id"]; await _handleSingle(data, spinner, appId, out); } } From 2a7b2e0b8fe9c908806db56a01b8e82eb5a3b4b7 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:40:21 -0300 Subject: [PATCH 17/24] Update glob option to include valueHelp --- lib/commands/app/upload.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/app/upload.dart b/lib/commands/app/upload.dart index 7e92dba..a3b73a3 100644 --- a/lib/commands/app/upload.dart +++ b/lib/commands/app/upload.dart @@ -14,7 +14,7 @@ import "package:path/path.dart" hide context; final class AppUploadCommand extends Command with Disposable { AppUploadCommand() { - argParser.addMultiOption("glob", abbr: "g", defaultsTo: const ["**"]); + argParser.addMultiOption("glob", abbr: "g", valueHelp: "**"); } @override From a7824b79df44c1b8e013258eb791ba985b40c913 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:41:42 -0300 Subject: [PATCH 18/24] Update valueHelp for domain option in info command --- lib/commands/domain/info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/domain/info.dart b/lib/commands/domain/info.dart index 0b05e17..a8cb58b 100644 --- a/lib/commands/domain/info.dart +++ b/lib/commands/domain/info.dart @@ -8,7 +8,7 @@ import "package:discloud/utils/messages.dart"; final class CustomdomainInfoCommand extends Command { CustomdomainInfoCommand() { - argParser.addOption("id", aliases: const ["domain"], defaultsTo: "all"); + argParser.addOption("id", aliases: const ["domain"], valueHelp: "all"); } @override From b4d655fda26bf7172fa1be36c9bcfa9b38da2e78 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:42:39 -0300 Subject: [PATCH 19/24] Update valueHelp for subdomain id option --- lib/commands/subdomain/info.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/subdomain/info.dart b/lib/commands/subdomain/info.dart index 0090392..7521e2c 100644 --- a/lib/commands/subdomain/info.dart +++ b/lib/commands/subdomain/info.dart @@ -8,7 +8,7 @@ import "package:discloud/utils/messages.dart"; final class SubdomainInfoCommand extends Command { SubdomainInfoCommand() { - argParser.addOption("id", aliases: const ["subdomain"], defaultsTo: "all"); + argParser.addOption("id", aliases: const ["subdomain"], valueHelp: "all"); } @override From 9a161fb6c19ac9f6a3fd94fa309511fd52d89eef Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:46:20 -0300 Subject: [PATCH 20/24] Simplify _handleSingle and _handleMulti methods Refactor backup handling methods to remove 'dir' parameter. --- lib/commands/team/backup.dart | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/lib/commands/team/backup.dart b/lib/commands/team/backup.dart index 22f5559..4f6c9e7 100644 --- a/lib/commands/team/backup.dart +++ b/lib/commands/team/backup.dart @@ -51,21 +51,17 @@ final class TeamBackupCommand extends Command with Disposable { switch (response["backups"]) { case final Map data: - await _handleSingle(data, dir: dir, spinner: spinner); + await _handleSingle(data, spinner); break; case final List list: - await _handleMulti(list, dir: dir, spinner: spinner); + await _handleMulti(list, spinner); break; } } - Future _handleSingle( - Map data, { - required ISpin spinner, - String? dir, - }) async { + Future _handleSingle(Map data, ISpin spinner) async { if (data["url"] case final String url) { - if (dir != null) { + if (argResults!.optionOrRest("dir", ["app"]) case final dir) { final Uri uri = .parse(url); return _download(dir: dir, spinner: spinner, uri: uri); @@ -75,21 +71,9 @@ final class TeamBackupCommand extends Command with Disposable { } } - Future _handleMulti( - List list, { - required ISpin spinner, - String? dir, - }) async { - if (dir == null) { - for (final data in list) { - if (data["url"] case final String url) { - context.printer.writeln(url); - } - } - return; - } - + Future _handleMulti(List list, ISpin spinner) async { final client = _client = .new(); + final dir = argResults?.optionOrRest("dir") ?? "."; for (final data in list) { final String appId = data["id"]; From a4bf40fcf1e2680f60ebe0905468a5c6d23c4514 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:48:24 -0300 Subject: [PATCH 21/24] Refactor option handling for app ID and glob --- lib/commands/team/commit.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/commands/team/commit.dart b/lib/commands/team/commit.dart index 89b1cda..3fe2459 100644 --- a/lib/commands/team/commit.dart +++ b/lib/commands/team/commit.dart @@ -39,17 +39,15 @@ final class TeamCommitCommand extends Command with Disposable { Future run() async { final directory = context.workspaceFolder; - final optionAppId = argResults!.option("app"); + final optionAppId = argResults?.option("app"); final appId = optionAppId ?? - argResults!.optionOrRest("app") ?? + argResults?.rest.firstOrNull ?? await _getDiscloudConfigAppId(directory); if (appId == null) throw Exception("Missing app id"); - final glob = optionAppId == null - ? argResults!.multiOptionOrRest("glob", ["app"]) ?? const ["**"] - : argResults!.multiOptionOrRest("glob") ?? const ["**"]; + final glob = argResults?.multiOptionOrRest("glob", [if (optionAppId != null) "app"]) ?? const ["**"]; final spinner = context.printer.spin(text: "Zipping..."); From 681659d2e0bb2709fd00aca6f5f784c9230203cb Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:50:36 -0300 Subject: [PATCH 22/24] Refactor log handling methods for clarity and consistency --- lib/commands/team/logs.dart | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/lib/commands/team/logs.dart b/lib/commands/team/logs.dart index ce3059a..65ffad2 100644 --- a/lib/commands/team/logs.dart +++ b/lib/commands/team/logs.dart @@ -44,48 +44,44 @@ final class TeamLogsCommand extends Command { switch (response["apps"]) { case final Map data: - await _handleSingle(data, out, spinner); + await _handleSingle(data, spinner, appId, out); break; case final List list: - await _handleMulti(list, out: out ?? ".", spinner: spinner); + await _handleMulti(list, spinner, out ?? "."); break; } } Future _handleSingle( Map data, - String? out, ISpin spinner, + String appId, + String? out, ) async { if (data["terminal"]?["big"] case final String contents) { if (out case final out?) { - final appId = data["id"]?.toString() ?? "app"; - return _saveLog(contents, _resolveLogPath(out, appId), spinner); + return _saveLog(contents, _resolvePath(out, appId), spinner); } context.printer.writeln(contents); } } - Future _handleMulti( - List list, { - required String out, - required ISpin spinner, - }) async { + Future _handleMulti(List list, ISpin spinner, String out) async { for (final data in list) { - await _handleSingle(data, out, spinner); + final String appId = data["id"]; + await _handleSingle(data, spinner, appId, out); } } Future _saveLog(String data, String out, ISpin spinner) async { - final file = File(out); + final File file = .new(out); await file.parent.create(recursive: true); await file.writeAsString(stripAnsi(data)); spinner.success(out); } - String _resolveLogPath(String out, String appId) { + String _resolvePath(String out, String appId) { if (extension(out).isNotEmpty) return out; - return joinAll([out, "$appId.log"]); } } From f8867c3bbcc302023cf52f5e0d0f311359c80a05 Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:52:13 -0300 Subject: [PATCH 23/24] Update glob option to include valueHelp --- lib/commands/zip.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/zip.dart b/lib/commands/zip.dart index b047440..0467c15 100644 --- a/lib/commands/zip.dart +++ b/lib/commands/zip.dart @@ -14,7 +14,7 @@ final class ZipCommand extends Command with Disposable { ZipCommand() { argParser ..addOption("encoding", abbr: "e", allowed: const ["buffer"], hide: true) - ..addMultiOption("glob", abbr: "g", defaultsTo: const ["**"]) + ..addMultiOption("glob", abbr: "g", valueHelp: const ["**"]) ..addOption("out", abbr: "o", help: "Zip output") ..addOption( "level", From 49599903e2b1c1a2c8fc68572382877143172dba Mon Sep 17 00:00:00 2001 From: Gorniaky <30408913+Gorniaky@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:54:46 -0300 Subject: [PATCH 24/24] Refactor download function for better error handling Refactor download function to improve error handling and response management. --- lib/utils/download.dart | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/utils/download.dart b/lib/utils/download.dart index 14c33cc..af50fe7 100644 --- a/lib/utils/download.dart +++ b/lib/utils/download.dart @@ -11,22 +11,30 @@ Future download( VoidProgressCallback? onProgress, }) async { final client0 = client ?? .new(); - IOSink? sink; + final request = await client0.getUrl(url); + + HttpClientResponse? response; try { - final request = await client0.getUrl(url); - final response = await request.close(); + response = await request.close(); + if (!response.ok) { throw HttpException( "Download failed: ${response.statusCode} ${response.reasonPhrase}", uri: url, ); } + } catch (e) { + rethrow; + } finally { + if (client == null) client0.close(); + } - file ??= .new(url.pathSegments.last); - await file.parent.create(recursive: true); + file ??= .new(url.pathSegments.last); + await file.parent.create(recursive: true); + final sink = file.openWrite(); - sink = file.openWrite(); + try { if (onProgress case final onProgress?) { await _downloadWithProgress( onProgress: onProgress, @@ -37,12 +45,11 @@ Future download( await sink.addStream(response); } } catch (_) { - await sink?.close(); - sink = null; - if (file != null && await file.exists()) await file.delete(); + await sink.close(); + await file.delete(); rethrow; } finally { - await sink?.close(); + await sink.close(); if (client == null) client0.close(); } }