@_nazmiforreal/flutter-ota 0.1.20 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/bin/flutter-ota-linux-x64 +0 -0
  2. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +1 -1
  3. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +340 -340
  4. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +9 -5
  5. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
  6. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +106 -31
  7. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +34 -7
  8. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +12 -4
  9. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
  10. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +41 -9
  11. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +124 -13
  12. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
  13. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +58 -28
  14. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +11 -5
  15. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +283 -32
  16. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +347 -121
  17. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +18 -9
  18. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +32 -15
  19. package/dart-src/packages/cli-tools/lib/src/config.dart +16 -22
  20. package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -3
  21. package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +26 -17
  22. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +0 -1
  23. package/dart-src/packages/cli-tools/lib/src/runner.dart +3 -1
  24. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +10 -16
  25. package/dart-src/packages/cli-tools/pubspec.lock +7 -7
  26. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +7 -3
  27. package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +13 -16
  28. package/dart-src/packages/core/CHANGELOG.md +15 -0
  29. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
  30. package/dart-src/packages/core/lib/src/app_update_info.dart +71 -1
  31. package/dart-src/packages/core/lib/src/bundle.dart +59 -17
  32. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +7 -3
  33. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +16 -0
  34. package/dart-src/packages/core/lib/src/changed_asset.dart +33 -3
  35. package/dart-src/packages/core/lib/src/get_bundles_args.dart +24 -2
  36. package/dart-src/packages/core/lib/src/platform.dart +8 -0
  37. package/dart-src/packages/core/lib/src/semver.dart +0 -3
  38. package/dart-src/packages/core/lib/src/status.dart +20 -1
  39. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  40. package/dart-src/packages/core/lib/src/update_bundle_params.dart +15 -0
  41. package/dart-src/packages/core/lib/src/uuid.dart +11 -0
  42. package/dart-src/packages/core/pubspec.yaml +1 -1
  43. package/dart-src/plugins/aws/CHANGELOG.md +18 -0
  44. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +0 -1
  45. package/dart-src/plugins/aws/pubspec.yaml +3 -3
  46. package/dart-src/plugins/cloudflare/CHANGELOG.md +18 -0
  47. package/dart-src/plugins/cloudflare/pubspec.yaml +3 -3
  48. package/dart-src/plugins/plugin-core/CHANGELOG.md +15 -0
  49. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +3 -0
  50. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +17 -0
  51. package/dart-src/plugins/plugin-core/lib/src/types.dart +94 -3
  52. package/dart-src/plugins/plugin-core/pubspec.yaml +2 -2
  53. package/dart-src/plugins/pocketbase/CHANGELOG.md +18 -0
  54. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +3 -0
  55. package/dart-src/plugins/pocketbase/pubspec.yaml +3 -3
  56. package/dart-src/plugins/postgres/CHANGELOG.md +18 -0
  57. package/dart-src/plugins/postgres/pubspec.yaml +3 -3
  58. package/dart-src/plugins/supabase/CHANGELOG.md +18 -0
  59. package/dart-src/plugins/supabase/pubspec.yaml +3 -3
  60. package/package.json +2 -2
  61. package/scripts/postinstall.js +5 -1
  62. package/bin/flutter-ota.js +0 -36
@@ -38,6 +38,7 @@ export 'src/commands/channel.dart';
38
38
  export 'src/commands/migrate.dart';
39
39
  export 'src/commands/console.dart';
40
40
  export 'src/commands/pocketbase.dart';
41
+ export 'src/commands/storage.dart';
41
42
 
42
43
  import 'src/commands/build.dart';
43
44
  import 'src/commands/bundle.dart';
@@ -102,8 +103,8 @@ Future<int> run(List<String> args) async {
102
103
  }
103
104
 
104
105
  // Unknown command → fuzzy-match and suggest.
105
- final unknownMatch =
106
- RegExp(r'Could not find a command named "?(\w+)"?').firstMatch(msg);
106
+ final unknownMatch = RegExp(r'Could not find a command named "?(\w+)"?')
107
+ .firstMatch(msg);
107
108
  if (unknownMatch != null) {
108
109
  final typed = unknownMatch.group(1)!;
109
110
  _printUnknownCommand(runner, typed);
@@ -190,7 +191,9 @@ void _printUnknownCommand(FlutterPatcherRunner runner, String typed) {
190
191
  .where((s) => s.startsWith(sub) || _levenshtein(sub, s) <= 2)
191
192
  .toList();
192
193
  if (subSuggestion.isNotEmpty) {
193
- stderr.writeln(' ${_cyan('Did you mean?')} $parentCmd ${_green(subSuggestion.first)}');
194
+ stderr.writeln(
195
+ ' ${_cyan('Did you mean?')} $parentCmd ${_green(subSuggestion.first)}',
196
+ );
194
197
  stderr.writeln('');
195
198
  }
196
199
  cmd.printUsage();
@@ -244,7 +247,7 @@ void _printMissingSubcommand(Command<int> parent) {
244
247
  stderr.writeln('');
245
248
 
246
249
  if (subNames.isNotEmpty) {
247
- stderr.writeln(' ${_cyan('Did you mean?')}');
250
+ stderr.writeln(' ${_cyan('Available subcommands:')}');
248
251
  for (final s in subNames) {
249
252
  stderr.writeln(' ${_green('${parent.name} $s')}');
250
253
  }
@@ -282,7 +285,8 @@ bool get _noColor {
282
285
  return v != null && v.isNotEmpty;
283
286
  }
284
287
 
285
- bool get _colorOn => !_noColor && stderr.hasTerminal && stderr.supportsAnsiEscapes;
288
+ bool get _colorOn =>
289
+ !_noColor && stderr.hasTerminal && stderr.supportsAnsiEscapes;
286
290
 
287
291
  String _red(String s) => _colorOn ? '\x1b[31m$s\x1b[0m' : s;
288
292
  String _green(String s) => _colorOn ? '\x1b[32m$s\x1b[0m' : s;
@@ -35,8 +35,9 @@ Backend requireBackend(FlutterPatcherConfig? config, {Backend? override}) {
35
35
  if (override != null) return override;
36
36
  if (config == null) {
37
37
  throw StateError(
38
- 'No configuration found. Run `flutter_ota_kit init` or set '
39
- 'SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY.',
38
+ 'No configuration found. Run `flutter-ota init <backend>` or set '
39
+ 'the required environment variables for your provider '
40
+ '(see `flutter-ota init --help`).',
40
41
  );
41
42
  }
42
43
  return resolveBackend(config);
@@ -1,3 +1,5 @@
1
+ import 'dart:io';
2
+
1
3
  import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
2
4
 
3
5
  import '../ui/ui.dart';
@@ -58,16 +60,28 @@ class BundleCommand extends FlutterPatcherCommand {
58
60
 
59
61
  @override
60
62
  String get description =>
61
- 'Manage bundles (list / show / delete / disable / enable / force / promote).';
63
+ 'Manage bundles (list / show / delete / disable / enable / force / promote / update).';
62
64
  }
63
65
 
64
66
  class BundleListCommand extends FlutterPatcherCommand {
65
67
  BundleListCommand({this.config, this.backendOverride}) {
66
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
68
+ final detected = config?.provider ?? loadConfig()?.provider;
69
+ argParser.addOption(
70
+ 'backend',
71
+ abbr: 'b',
72
+ help: detected != null
73
+ ? 'Backend provider [detected: $detected].'
74
+ : 'Backend provider.',
75
+ );
67
76
  argParser.addOption('channel', abbr: 'c', help: 'Filter by channel.');
68
77
  argParser.addOption('platform', abbr: 'p', help: 'Filter by platform.');
69
78
  argParser.addOption('enabled', help: 'Filter by enabled (true/false).');
70
- argParser.addOption('limit', abbr: 'l', defaultsTo: '20', help: 'Page size.');
79
+ argParser.addOption(
80
+ 'limit',
81
+ abbr: 'l',
82
+ defaultsTo: '20',
83
+ help: 'Page size.',
84
+ );
71
85
  }
72
86
 
73
87
  final FlutterPatcherConfig? config;
@@ -90,7 +104,10 @@ class BundleListCommand extends FlutterPatcherCommand {
90
104
  final limitRaw = argResults!['limit'] as String;
91
105
  final limit = int.tryParse(limitRaw);
92
106
  if (limit == null || limit < 1) {
93
- throw PackException('--limit must be a positive integer (got "$limitRaw")', 64);
107
+ throw PackException(
108
+ '--limit must be a positive integer (got "$limitRaw")',
109
+ 64,
110
+ );
94
111
  }
95
112
  final res = await listBundles(
96
113
  backend,
@@ -111,26 +128,29 @@ class BundleListCommand extends FlutterPatcherCommand {
111
128
  final b = res.data[i];
112
129
  final enabled = b.enabled ? green('✓') : red('✗');
113
130
  final force = b.shouldForceUpdate ? green('✓') : dim('✗');
114
- rows.add([
115
- '$i',
116
- cyan(b.id),
117
- b.channel,
118
- b.platform.value,
119
- force,
120
- enabled,
121
- ]);
131
+ rows.add(['$i', cyan(b.id), b.channel, b.platform.value, force, enabled]);
122
132
  }
123
- table(
124
- '${res.data.length} bundles (total: ${res.pagination.total})',
125
- ['#', 'ID', 'CHANNEL', 'PLAT', 'FORCE', 'ON'],
126
- rows,
127
- );
133
+ table('${res.data.length} bundles (total: ${res.pagination.total})', [
134
+ '#',
135
+ 'ID',
136
+ 'CHANNEL',
137
+ 'PLAT',
138
+ 'FORCE',
139
+ 'ON',
140
+ ], rows);
128
141
  });
129
142
  }
130
143
 
131
144
  class BundleShowCommand extends FlutterPatcherCommand {
132
145
  BundleShowCommand({this.config, this.backendOverride}) {
133
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
146
+ final detected = config?.provider ?? loadConfig()?.provider;
147
+ argParser.addOption(
148
+ 'backend',
149
+ abbr: 'b',
150
+ help: detected != null
151
+ ? 'Backend provider [detected: $detected].'
152
+ : 'Backend provider.',
153
+ );
134
154
  argParser.addOption('id', help: 'Bundle id.');
135
155
  }
136
156
 
@@ -173,7 +193,14 @@ class BundleShowCommand extends FlutterPatcherCommand {
173
193
 
174
194
  class BundleDeleteCommand extends FlutterPatcherCommand {
175
195
  BundleDeleteCommand({this.config, this.backendOverride}) {
176
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
196
+ final detected = config?.provider ?? loadConfig()?.provider;
197
+ argParser.addOption(
198
+ 'backend',
199
+ abbr: 'b',
200
+ help: detected != null
201
+ ? 'Backend provider [detected: $detected].'
202
+ : 'Backend provider.',
203
+ );
177
204
  argParser.addOption('id', help: 'Bundle id.');
178
205
  argParser.addFlag(
179
206
  'keep-storage',
@@ -206,8 +233,10 @@ class BundleDeleteCommand extends FlutterPatcherCommand {
206
233
  final steps = Steps('delete');
207
234
  await steps.run('Deleting bundle $id', () => deleteBundle(backend, id));
208
235
  if (!keepStorage && existing.storageUri.isNotEmpty) {
209
- await steps.run('Removing storage object',
210
- () => backend.storage.delete(existing.storageUri));
236
+ await steps.run(
237
+ 'Removing storage object',
238
+ () => backend.storage.delete(existing.storageUri),
239
+ );
211
240
  }
212
241
  steps.summary();
213
242
  });
@@ -215,7 +244,14 @@ class BundleDeleteCommand extends FlutterPatcherCommand {
215
244
 
216
245
  class BundleDisableCommand extends FlutterPatcherCommand {
217
246
  BundleDisableCommand({this.config, this.backendOverride}) {
218
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
247
+ final detected = config?.provider ?? loadConfig()?.provider;
248
+ argParser.addOption(
249
+ 'backend',
250
+ abbr: 'b',
251
+ help: detected != null
252
+ ? 'Backend provider [detected: $detected].'
253
+ : 'Backend provider.',
254
+ );
219
255
  argParser.addOption('id', help: 'Bundle id.');
220
256
  }
221
257
 
@@ -246,7 +282,14 @@ class BundleDisableCommand extends FlutterPatcherCommand {
246
282
 
247
283
  class BundleEnableCommand extends FlutterPatcherCommand {
248
284
  BundleEnableCommand({this.config, this.backendOverride}) {
249
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
285
+ final detected = config?.provider ?? loadConfig()?.provider;
286
+ argParser.addOption(
287
+ 'backend',
288
+ abbr: 'b',
289
+ help: detected != null
290
+ ? 'Backend provider [detected: $detected].'
291
+ : 'Backend provider.',
292
+ );
250
293
  argParser.addOption('id', help: 'Bundle id.');
251
294
  }
252
295
 
@@ -277,7 +320,14 @@ class BundleEnableCommand extends FlutterPatcherCommand {
277
320
 
278
321
  class BundleForceCommand extends FlutterPatcherCommand {
279
322
  BundleForceCommand({this.config, this.backendOverride}) {
280
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
323
+ final detected = config?.provider ?? loadConfig()?.provider;
324
+ argParser.addOption(
325
+ 'backend',
326
+ abbr: 'b',
327
+ help: detected != null
328
+ ? 'Backend provider [detected: $detected].'
329
+ : 'Backend provider.',
330
+ );
281
331
  argParser.addOption('id', help: 'Bundle id.');
282
332
  argParser.addFlag(
283
333
  'off',
@@ -319,7 +369,14 @@ class BundleForceCommand extends FlutterPatcherCommand {
319
369
 
320
370
  class BundlePromoteCommand extends FlutterPatcherCommand {
321
371
  BundlePromoteCommand({this.config, this.backendOverride}) {
322
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
372
+ final detected = config?.provider ?? loadConfig()?.provider;
373
+ argParser.addOption(
374
+ 'backend',
375
+ abbr: 'b',
376
+ help: detected != null
377
+ ? 'Backend provider [detected: $detected].'
378
+ : 'Backend provider.',
379
+ );
323
380
  argParser.addOption('id', help: 'Bundle id.');
324
381
  argParser.addOption('channel', abbr: 'c', help: 'Target channel.');
325
382
  }
@@ -348,21 +405,39 @@ class BundlePromoteCommand extends FlutterPatcherCommand {
348
405
  final backend = requireBackend(cfg, override: backendOverride);
349
406
  banner('bundle · promote');
350
407
  final steps = Steps('promote');
351
- await steps.run('Promoting $id to $channel',
352
- () => promoteBundle(backend, id!, channel));
408
+ await steps.run(
409
+ 'Promoting $id to $channel',
410
+ () => promoteBundle(backend, id!, channel),
411
+ );
353
412
  steps.summary();
354
- box('promote', [kv('bundle', cyan(id!)), kv('channel', channel)]);
413
+ stdout.writeln(
414
+ ' ${dim('→')} bundle ${cyan(id!)} → channel ${cyan(channel)}',
415
+ );
355
416
  });
356
417
  }
357
418
 
358
419
  class BundleUpdateCommand extends FlutterPatcherCommand {
359
420
  BundleUpdateCommand({this.config, this.backendOverride}) {
360
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
421
+ final detected = config?.provider ?? loadConfig()?.provider;
422
+ argParser.addOption(
423
+ 'backend',
424
+ abbr: 'b',
425
+ help: detected != null
426
+ ? 'Backend provider [detected: $detected].'
427
+ : 'Backend provider.',
428
+ );
361
429
  argParser.addOption('id', help: 'Bundle id.');
362
430
  argParser.addOption('message', abbr: 'm', help: 'New release message.');
363
- argParser.addOption('target-version', help: 'New target app version (e.g. 1.0.0).');
431
+ argParser.addOption(
432
+ 'target-version',
433
+ help: 'New target app version (e.g. 1.0.0).',
434
+ );
364
435
  argParser.addOption('enabled', help: 'Set enabled (true/false).');
365
- argParser.addOption('force', abbr: 'f', help: 'Set force-update (true/false).');
436
+ argParser.addOption(
437
+ 'force',
438
+ abbr: 'f',
439
+ help: 'Set force-update (true/false).',
440
+ );
366
441
  }
367
442
 
368
443
  final FlutterPatcherConfig? config;
@@ -1,3 +1,5 @@
1
+ import 'dart:io';
2
+
1
3
  import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
2
4
 
3
5
  import '../ui/ui.dart';
@@ -28,7 +30,14 @@ class ChannelCommand extends FlutterPatcherCommand {
28
30
 
29
31
  class ChannelListCommand extends FlutterPatcherCommand {
30
32
  ChannelListCommand({this.config, this.backendOverride}) {
31
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
33
+ final detected = config?.provider ?? loadConfig()?.provider;
34
+ argParser.addOption(
35
+ 'backend',
36
+ abbr: 'b',
37
+ help: detected != null
38
+ ? 'Backend provider [detected: $detected].'
39
+ : 'Backend provider.',
40
+ );
32
41
  }
33
42
 
34
43
  final FlutterPatcherConfig? config;
@@ -56,7 +65,14 @@ class ChannelListCommand extends FlutterPatcherCommand {
56
65
 
57
66
  class ChannelGetCommand extends FlutterPatcherCommand {
58
67
  ChannelGetCommand({this.config, this.backendOverride}) {
59
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
68
+ final detected = config?.provider ?? loadConfig()?.provider;
69
+ argParser.addOption(
70
+ 'backend',
71
+ abbr: 'b',
72
+ help: detected != null
73
+ ? 'Backend provider [detected: $detected].'
74
+ : 'Backend provider.',
75
+ );
60
76
  argParser.addOption('channel', abbr: 'c', help: 'Channel.');
61
77
  }
62
78
 
@@ -100,7 +116,14 @@ class ChannelGetCommand extends FlutterPatcherCommand {
100
116
 
101
117
  class ChannelSetCommand extends FlutterPatcherCommand {
102
118
  ChannelSetCommand({this.config, this.backendOverride}) {
103
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
119
+ final detected = config?.provider ?? loadConfig()?.provider;
120
+ argParser.addOption(
121
+ 'backend',
122
+ abbr: 'b',
123
+ help: detected != null
124
+ ? 'Backend provider [detected: $detected].'
125
+ : 'Backend provider.',
126
+ );
104
127
  argParser.addOption('channel', abbr: 'c', help: 'Channel.');
105
128
  argParser.addOption('bundle-id', abbr: 'i', help: 'Bundle id.');
106
129
  }
@@ -112,7 +135,7 @@ class ChannelSetCommand extends FlutterPatcherCommand {
112
135
  String get name => 'set';
113
136
 
114
137
  @override
115
- String get description => 'Promote a bundle to a channel.';
138
+ String get description => 'Set the active bundle for a channel.';
116
139
 
117
140
  @override
118
141
  Future<int> run() => runGuarded(() async {
@@ -131,9 +154,13 @@ class ChannelSetCommand extends FlutterPatcherCommand {
131
154
  final backend = requireBackend(cfg, override: backendOverride);
132
155
  banner('channel · set');
133
156
  final steps = Steps('set');
134
- await steps.run('Promoting $bundleId to $channel',
135
- () => promoteBundle(backend, bundleId, channel));
157
+ await steps.run(
158
+ 'Promoting $bundleId to $channel',
159
+ () => promoteBundle(backend, bundleId, channel),
160
+ );
136
161
  steps.summary();
137
- box('channel set', [kv('channel', channel), kv('bundle', cyan(bundleId))]);
162
+ stdout.writeln(
163
+ ' ${dim('→')} channel ${cyan(channel)} = ${cyan(bundleId)}',
164
+ );
138
165
  });
139
166
  }
@@ -32,6 +32,9 @@ void _saveProjectJson(Map<String, dynamic> json) {
32
32
  final file = configCandidates().first;
33
33
  file.parent.createSync(recursive: true);
34
34
  file.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(json));
35
+ if (!Platform.isWindows) {
36
+ Process.runSync('chmod', ['600', file.path]);
37
+ }
35
38
  }
36
39
 
37
40
  class ConfigGetCommand extends FlutterPatcherCommand {
@@ -49,7 +52,8 @@ class ConfigGetCommand extends FlutterPatcherCommand {
49
52
  @override
50
53
  Future<int> run() => runGuarded(() async {
51
54
  ui.banner('config · get');
52
- final key = argResults!['key'] as String? ??
55
+ final key =
56
+ argResults!['key'] as String? ??
53
57
  (argResults!.rest.isNotEmpty ? argResults!.rest.first : null);
54
58
  if (key == null || key.isEmpty) {
55
59
  throw StateError('Usage: flutter-ota config get <key>');
@@ -79,9 +83,11 @@ class ConfigSetCommand extends FlutterPatcherCommand {
79
83
 
80
84
  @override
81
85
  Future<int> run() => runGuarded(() async {
82
- final key = argResults!['key'] as String? ??
86
+ final key =
87
+ argResults!['key'] as String? ??
83
88
  (argResults!.rest.isNotEmpty ? argResults!.rest.first : null);
84
- final value = argResults!['value'] as String? ??
89
+ final value =
90
+ argResults!['value'] as String? ??
85
91
  (argResults!.rest.length > 1 ? argResults!.rest[1] : null);
86
92
  if (key == null || key.isEmpty || value == null) {
87
93
  throw StateError('Usage: flutter-ota config set <key> <value>');
@@ -121,7 +127,9 @@ void _printConfig(Map<String, dynamic> json, String prefix) {
121
127
  if (value is Map<String, dynamic>) {
122
128
  _printConfig(value, key);
123
129
  } else {
124
- final display = value is String ? value : const JsonEncoder().convert(value);
130
+ final display = value is String
131
+ ? value
132
+ : const JsonEncoder().convert(value);
125
133
  stdout.writeln(ui.kv(key, display));
126
134
  }
127
135
  }
@@ -35,8 +35,10 @@ class ConsoleCommand extends FlutterPatcherCommand {
35
35
  Future<int> run() => runGuarded(() async {
36
36
  final consoleDir = _findConsoleDir();
37
37
  if (consoleDir == null) {
38
- err('Console package not found (expected packages/console).');
39
- return;
38
+ throw PackException(
39
+ 'Console package not found (expected packages/console).',
40
+ 1,
41
+ );
40
42
  }
41
43
  if (argResults!['open'] as bool) {
42
44
  banner('console · open');
@@ -53,11 +55,8 @@ class ConsoleCommand extends FlutterPatcherCommand {
53
55
  return;
54
56
  }
55
57
  banner('console');
56
- box('flutter-ota console', [
57
- 'Open the web console with Flutter:',
58
- '',
59
- kv('dir', consoleDir.path),
60
- kv('run', 'flutter run -d chrome'),
61
- ]);
58
+ info('Open the web console with Flutter:');
59
+ step('dir ${dim(consoleDir.path)}');
60
+ step('run ${cyan('flutter run -d chrome')}');
62
61
  });
63
62
  }
@@ -6,20 +6,33 @@ import '../backend.dart';
6
6
  import '../cli_base.dart';
7
7
  import '../config.dart';
8
8
  import '../operations.dart';
9
+ import '../pack.dart';
9
10
  import '../ui/ui.dart';
10
11
  import '../util.dart';
11
12
 
12
13
  /// `flutter-ota deploy` — zip + upload + register a new bundle.
13
14
  class DeployCommand extends FlutterPatcherCommand {
14
15
  DeployCommand({this.config, this.backendOverride}) {
16
+ final detected = config?.provider ?? loadConfig()?.provider;
15
17
  argParser.addOption(
16
18
  'backend',
17
19
  abbr: 'b',
18
- help: 'Backend provider (supabase/postgres/cloudflare/aws).',
20
+ help: detected != null
21
+ ? 'Backend provider [detected: $detected].'
22
+ : 'Backend provider.',
23
+ );
24
+ argParser.addOption(
25
+ 'source',
26
+ abbr: 's',
27
+ help: 'Source directory to zip + upload.',
19
28
  );
20
- argParser.addOption('source', abbr: 's', help: 'Source directory to zip + upload.');
21
29
  argParser.addOption('channel', abbr: 'c', help: 'Target channel.');
22
- argParser.addOption('platform', abbr: 'p', defaultsTo: 'android', help: 'Platform.');
30
+ argParser.addOption(
31
+ 'platform',
32
+ abbr: 'p',
33
+ defaultsTo: 'android',
34
+ help: 'Platform.',
35
+ );
23
36
  argParser.addOption('message', abbr: 'm', help: 'Release message.');
24
37
  argParser.addFlag('force', abbr: 'f', help: 'Force the update on clients.');
25
38
  argParser.addOption(
@@ -35,7 +48,10 @@ class DeployCommand extends FlutterPatcherCommand {
35
48
  abbr: 'k',
36
49
  help: 'Path to Ed25519 private key file (sign bundle).',
37
50
  );
38
- argParser.addOption('git-commit-hash', help: 'Git commit hash (auto-detected).');
51
+ argParser.addOption(
52
+ 'git-commit-hash',
53
+ help: 'Git commit hash (auto-detected).',
54
+ );
39
55
  argParser.addOption(
40
56
  'bundle-id',
41
57
  abbr: 'i',
@@ -68,19 +84,35 @@ class DeployCommand extends FlutterPatcherCommand {
68
84
  final fingerprintHash = argResults!['fingerprint-hash'] as String?;
69
85
  final keyPath = argResults!['key'] as String?;
70
86
  String? signingKey;
71
- if (keyPath != null) signingKey = File(keyPath).readAsStringSync().trim();
87
+ if (keyPath != null) {
88
+ final keyFile = File(keyPath);
89
+ if (!keyFile.existsSync()) {
90
+ throw PackException('Signing key file not found: $keyPath', 64);
91
+ }
92
+ signingKey = keyFile.readAsStringSync().trim();
93
+ }
72
94
  final resolvedGitCommitHash =
73
95
  argResults!['git-commit-hash'] as String? ??
74
96
  await gitCommitHash(source);
75
97
  final bundleId = argResults!['bundle-id'] as String?;
76
98
 
77
99
  banner('deploy');
78
- info('channel ${cyan(channel)} platform ${cyan(platform)} source ${dim(source)}');
79
100
 
80
101
  final steps = Steps('deploy');
81
- final bundle = await _deployWithPhases(steps, backend, source, channel,
82
- platform, message, force, targetAppVersion, fingerprintHash,
83
- signingKey, resolvedGitCommitHash, bundleId);
102
+ final bundle = await _deployWithPhases(
103
+ steps,
104
+ backend,
105
+ source,
106
+ channel,
107
+ platform,
108
+ message,
109
+ force,
110
+ targetAppVersion,
111
+ fingerprintHash,
112
+ signingKey,
113
+ resolvedGitCommitHash,
114
+ bundleId,
115
+ );
84
116
  steps.summary();
85
117
 
86
118
  final lines = <String>[