@_nazmiforreal/flutter-ota 0.1.16 → 0.1.18

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 (43) hide show
  1. package/bin/flutter-ota-linux-x64 +0 -0
  2. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +7 -7
  3. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +200 -0
  4. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +29 -65
  5. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +4 -15
  6. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +2 -12
  7. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +3 -1
  8. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +62 -24
  9. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +3 -4
  10. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +19 -47
  11. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +12 -13
  12. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +2 -0
  13. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +15 -17
  14. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +14 -16
  15. package/dart-src/packages/cli-tools/lib/src/operations.dart +9 -0
  16. package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +0 -1
  17. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +0 -3
  18. package/dart-src/packages/cli-tools/lib/src/runner.dart +15 -1
  19. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +126 -8
  20. package/dart-src/packages/cli-tools/pubspec.lock +7 -7
  21. package/dart-src/packages/core/CHANGELOG.md +1 -12
  22. package/dart-src/packages/core/LICENSE +1 -1
  23. package/dart-src/packages/core/pubspec.yaml +1 -1
  24. package/dart-src/plugins/aws/CHANGELOG.md +1 -18
  25. package/dart-src/plugins/aws/LICENSE +1 -1
  26. package/dart-src/plugins/aws/pubspec.yaml +1 -1
  27. package/dart-src/plugins/cloudflare/CHANGELOG.md +1 -18
  28. package/dart-src/plugins/cloudflare/LICENSE +1 -1
  29. package/dart-src/plugins/cloudflare/pubspec.yaml +1 -1
  30. package/dart-src/plugins/plugin-core/CHANGELOG.md +1 -17
  31. package/dart-src/plugins/plugin-core/LICENSE +1 -1
  32. package/dart-src/plugins/plugin-core/pubspec.yaml +1 -1
  33. package/dart-src/plugins/pocketbase/CHANGELOG.md +1 -8
  34. package/dart-src/plugins/pocketbase/LICENSE +1 -1
  35. package/dart-src/plugins/pocketbase/pubspec.yaml +1 -1
  36. package/dart-src/plugins/postgres/CHANGELOG.md +1 -19
  37. package/dart-src/plugins/postgres/LICENSE +1 -1
  38. package/dart-src/plugins/postgres/pubspec.yaml +1 -1
  39. package/dart-src/plugins/supabase/CHANGELOG.md +1 -23
  40. package/dart-src/plugins/supabase/LICENSE +1 -1
  41. package/dart-src/plugins/supabase/pubspec.yaml +1 -1
  42. package/package.json +1 -1
  43. package/scripts/vendor.sh +0 -0
Binary file
@@ -30,7 +30,7 @@
30
30
  },
31
31
  {
32
32
  "name": "flutter_ota_kit_pocketbase",
33
- "version": "0.1.0",
33
+ "version": "0.1.2",
34
34
  "dependencies": [
35
35
  "flutter_ota_kit_core",
36
36
  "flutter_ota_kit_plugin_core",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  {
41
41
  "name": "flutter_ota_kit_aws",
42
- "version": "0.1.3",
42
+ "version": "0.1.5",
43
43
  "dependencies": [
44
44
  "asn1lib",
45
45
  "crypto",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  {
54
54
  "name": "flutter_ota_kit_cloudflare",
55
- "version": "0.1.3",
55
+ "version": "0.1.5",
56
56
  "dependencies": [
57
57
  "crypto",
58
58
  "flutter_ota_kit_core",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  {
65
65
  "name": "flutter_ota_kit_postgres",
66
- "version": "0.1.3",
66
+ "version": "0.1.5",
67
67
  "dependencies": [
68
68
  "flutter_ota_kit_core",
69
69
  "flutter_ota_kit_plugin_core",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  {
74
74
  "name": "flutter_ota_kit_supabase",
75
- "version": "0.1.4",
75
+ "version": "0.1.6",
76
76
  "dependencies": [
77
77
  "flutter_ota_kit_core",
78
78
  "flutter_ota_kit_plugin_core",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  {
84
84
  "name": "flutter_ota_kit_plugin_core",
85
- "version": "0.0.4",
85
+ "version": "0.0.6",
86
86
  "dependencies": [
87
87
  "flutter_ota_kit_core",
88
88
  "mime"
@@ -90,7 +90,7 @@
90
90
  },
91
91
  {
92
92
  "name": "flutter_ota_kit_core",
93
- "version": "0.1.2",
93
+ "version": "0.1.4",
94
94
  "dependencies": []
95
95
  },
96
96
  {
@@ -81,9 +81,209 @@ Future<int> run(List<String> args) async {
81
81
  final result = await runner.run(args);
82
82
  return result ?? 0;
83
83
  } on UsageException catch (e) {
84
+ final msg = e.message;
85
+
86
+ // Missing subcommand → show the command's own help + suggestions.
87
+ if (msg.startsWith('Missing subcommand')) {
88
+ final match = RegExp(r'"flutter-ota (\w+)"').firstMatch(msg);
89
+ if (match != null) {
90
+ final cmd = runner.commands[match.group(1)];
91
+ if (cmd != null) {
92
+ _printMissingSubcommand(cmd);
93
+ return 0;
94
+ }
95
+ }
96
+ }
97
+
98
+ // No command given → show top-level help.
99
+ if (msg.contains('No command specified')) {
100
+ runner.printUsage();
101
+ return 0;
102
+ }
103
+
104
+ // Unknown command → fuzzy-match and suggest.
105
+ final unknownMatch =
106
+ RegExp(r'Could not find a command named "?(\w+)"?').firstMatch(msg);
107
+ if (unknownMatch != null) {
108
+ final typed = unknownMatch.group(1)!;
109
+ _printUnknownCommand(runner, typed);
110
+ return 64;
111
+ }
112
+
113
+ // Unknown subcommand → suggest closest subcommand.
114
+ final subMatch = RegExp(
115
+ r'Could not find a subcommand named "?(\w+)"? for "?flutter-ota (\w+)"?',
116
+ ).firstMatch(msg);
117
+ if (subMatch != null) {
118
+ final subTyped = subMatch.group(1)!;
119
+ final parentName = subMatch.group(2)!;
120
+ final parentCmd = runner.commands[parentName];
121
+ if (parentCmd != null) {
122
+ _printUnknownSubcommand(parentCmd, subTyped);
123
+ return 64;
124
+ }
125
+ }
126
+
127
+ // Fallback — show the raw error + usage.
84
128
  stderr.writeln(e.message);
85
129
  stderr.writeln('');
86
130
  stderr.writeln(e.usage);
87
131
  return 64;
88
132
  }
89
133
  }
134
+
135
+ /// Show a helpful "unknown command" message with fuzzy-match suggestions.
136
+ void _printUnknownCommand(FlutterPatcherRunner runner, String typed) {
137
+ final commands = runner.commands.keys.toList();
138
+
139
+ // Exact prefix match (e.g. "bund" → "bundle").
140
+ final prefixMatches = commands.where((c) => c.startsWith(typed)).toList();
141
+
142
+ // Levenshtein-based fuzzy match for short typos.
143
+ final fuzzyMatches = <String>[];
144
+ for (final c in commands) {
145
+ if (prefixMatches.contains(c)) continue;
146
+ if (_levenshtein(typed, c) <= 2) {
147
+ fuzzyMatches.add(c);
148
+ }
149
+ }
150
+
151
+ // Subcommand matching (e.g. "bundle lis" → suggest "list").
152
+ final subMatch = RegExp(r'^(\w+)\s+(\w+)$').firstMatch(typed);
153
+ String? parentCmd;
154
+ String? subTyped;
155
+ if (subMatch != null) {
156
+ parentCmd = subMatch.group(1);
157
+ subTyped = subMatch.group(2);
158
+ }
159
+ // Also try if typed is just the first part.
160
+ if (parentCmd != null && !commands.contains(parentCmd)) {
161
+ // Check if parentCmd is a fuzzy match for a real command.
162
+ final parentFuzzy = commands.where((c) {
163
+ if (c == parentCmd) return true;
164
+ return _levenshtein(parentCmd!, c) <= 2;
165
+ }).toList();
166
+ if (parentFuzzy.isNotEmpty) {
167
+ parentCmd = parentFuzzy.first;
168
+ }
169
+ }
170
+
171
+ stderr.writeln('');
172
+ stderr.writeln(' ${_red('✗')} Unknown command: ${_red(typed)}');
173
+ stderr.writeln('');
174
+
175
+ if (prefixMatches.isNotEmpty || fuzzyMatches.isNotEmpty) {
176
+ final suggestions = [...prefixMatches, ...fuzzyMatches];
177
+ stderr.writeln(' ${_cyan('Did you mean?')}');
178
+ for (final s in suggestions.take(3)) {
179
+ stderr.writeln(' ${_green(s)}');
180
+ }
181
+ stderr.writeln('');
182
+
183
+ // If it looks like "parent sub" but the sub is wrong, show sub help.
184
+ if (parentCmd != null && subTyped != null) {
185
+ final cmd = runner.commands[parentCmd];
186
+ if (cmd != null && cmd.subcommands.isNotEmpty) {
187
+ // Find closest subcommand.
188
+ final subNames = cmd.subcommands.keys.toList();
189
+ final subSuggestion = subNames
190
+ .where((s) => s.startsWith(subTyped!) || _levenshtein(subTyped!, s) <= 2)
191
+ .toList();
192
+ if (subSuggestion.isNotEmpty) {
193
+ stderr.writeln(' ${_cyan('Did you mean?')} $parentCmd ${_green(subSuggestion.first)}');
194
+ stderr.writeln('');
195
+ }
196
+ cmd.printUsage();
197
+ return;
198
+ }
199
+ }
200
+
201
+ // Show top-level help.
202
+ runner.printUsage();
203
+ return;
204
+ }
205
+
206
+ // No close matches — show all available commands.
207
+ runner.printUsage();
208
+ }
209
+
210
+ /// Show a helpful "unknown subcommand" message with suggestions.
211
+ void _printUnknownSubcommand(Command<int> parent, String typed) {
212
+ final subNames = parent.subcommands.keys.toList();
213
+
214
+ // Find closest subcommands.
215
+ final suggestions = subNames
216
+ .where((s) => s.startsWith(typed) || _levenshtein(typed, s) <= 2)
217
+ .toList();
218
+
219
+ stderr.writeln('');
220
+ stderr.writeln(
221
+ ' ${_red('✗')} Unknown subcommand: ${_red(typed)} for ${_cyan('flutter-ota ${parent.name}')}',
222
+ );
223
+ stderr.writeln('');
224
+
225
+ if (suggestions.isNotEmpty) {
226
+ stderr.writeln(' ${_cyan('Did you mean?')}');
227
+ for (final s in suggestions.take(3)) {
228
+ stderr.writeln(' ${_green(s)}');
229
+ }
230
+ stderr.writeln('');
231
+ }
232
+
233
+ parent.printUsage();
234
+ }
235
+
236
+ /// Show a helpful "missing subcommand" message with all subcommand suggestions.
237
+ void _printMissingSubcommand(Command<int> parent) {
238
+ final subNames = parent.subcommands.keys.toList()..sort();
239
+
240
+ stderr.writeln('');
241
+ stderr.writeln(
242
+ ' ${_red('✗')} Missing subcommand for ${_cyan('flutter-ota ${parent.name}')}',
243
+ );
244
+ stderr.writeln('');
245
+
246
+ if (subNames.isNotEmpty) {
247
+ stderr.writeln(' ${_cyan('Did you mean?')}');
248
+ for (final s in subNames) {
249
+ stderr.writeln(' ${_green('${parent.name} $s')}');
250
+ }
251
+ stderr.writeln('');
252
+ }
253
+
254
+ parent.printUsage();
255
+ }
256
+
257
+ // ── Minimal Levenshtein distance ──────────────────────────────────────────────
258
+
259
+ int _levenshtein(String a, String b) {
260
+ if (a.isEmpty) return b.length;
261
+ if (b.isEmpty) return a.length;
262
+ final dp = List.generate(a.length + 1, (i) => List.filled(b.length + 1, 0));
263
+ for (var i = 0; i <= a.length; i++) dp[i][0] = i;
264
+ for (var j = 0; j <= b.length; j++) dp[0][j] = j;
265
+ for (var i = 1; i <= a.length; i++) {
266
+ for (var j = 1; j <= b.length; j++) {
267
+ final cost = a[i - 1] == b[j - 1] ? 0 : 1;
268
+ dp[i][j] = [
269
+ dp[i - 1][j] + 1,
270
+ dp[i][j - 1] + 1,
271
+ dp[i - 1][j - 1] + cost,
272
+ ].reduce((a, b) => a < b ? a : b);
273
+ }
274
+ }
275
+ return dp[a.length][b.length];
276
+ }
277
+
278
+ // ── Minimal inline color helpers (stderr-safe, TTY-aware) ────────────────────
279
+
280
+ bool get _noColor {
281
+ final v = Platform.environment['NO_COLOR'];
282
+ return v != null && v.isNotEmpty;
283
+ }
284
+
285
+ bool get _colorOn => !_noColor && stderr.hasTerminal && stderr.supportsAnsiEscapes;
286
+
287
+ String _red(String s) => _colorOn ? '\x1b[31m$s\x1b[0m' : s;
288
+ String _green(String s) => _colorOn ? '\x1b[32m$s\x1b[0m' : s;
289
+ String _cyan(String s) => _colorOn ? '\x1b[36m$s\x1b[0m' : s;
@@ -59,27 +59,6 @@ class BundleCommand extends FlutterPatcherCommand {
59
59
  @override
60
60
  String get description =>
61
61
  'Manage bundles (list / show / delete / disable / enable / force / promote).';
62
-
63
- @override
64
- Future<int> run() => runGuarded(() async {
65
- print(description);
66
- print('');
67
- print('Subcommands:');
68
- print(
69
- ' list List bundles (filters: --channel/-c, --platform/-p, --enabled, --limit/-l)',
70
- );
71
- print(' show --id <id> Show a single bundle\'s details');
72
- print(' delete --id <id> Delete a bundle');
73
- print(' disable --id <id> Disable a bundle (stop serving it)');
74
- print(' enable --id <id> Enable a bundle');
75
- print(
76
- ' force --id <id> Force an update onto clients (--off to clear)',
77
- );
78
- print(' promote --id <id> -c Promote a bundle to a channel');
79
- print(
80
- ' update --id <id> ... Edit metadata (--message/--target-version/--enabled/--force)',
81
- );
82
- });
83
62
  }
84
63
 
85
64
  class BundleListCommand extends FlutterPatcherCommand {
@@ -225,19 +204,10 @@ class BundleDeleteCommand extends FlutterPatcherCommand {
225
204
  }
226
205
  final keepStorage = argResults!['keep-storage'] as bool;
227
206
  final steps = Steps('delete');
228
- await spinner(
229
- () => deleteBundle(backend, id),
230
- 'Deleting bundle $id',
231
- done: 'Deleted',
232
- );
233
- steps.success('Deleted bundle $id');
207
+ await steps.run('Deleting bundle $id', () => deleteBundle(backend, id));
234
208
  if (!keepStorage && existing.storageUri.isNotEmpty) {
235
- await spinner(
236
- () => backend.storage.delete(existing.storageUri),
237
- 'Removing storage object',
238
- done: 'Storage removed',
239
- );
240
- steps.success('Removed storage object');
209
+ await steps.run('Removing storage object',
210
+ () => backend.storage.delete(existing.storageUri));
241
211
  }
242
212
  steps.summary();
243
213
  });
@@ -265,14 +235,12 @@ class BundleDisableCommand extends FlutterPatcherCommand {
265
235
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
266
236
  final backend = requireBackend(cfg, override: backendOverride);
267
237
  banner('bundle · disable');
268
- await spinner(
269
- () async {
270
- await backend.db.updateBundle(id!, {'enabled': false});
271
- await backend.db.commitBundle();
272
- },
273
- 'Disabling bundle $id',
274
- done: 'Disabled',
275
- );
238
+ final steps = Steps('disable');
239
+ await steps.run('Disabling bundle $id', () async {
240
+ await backend.db.updateBundle(id!, {'enabled': false});
241
+ await backend.db.commitBundle();
242
+ });
243
+ steps.summary();
276
244
  });
277
245
  }
278
246
 
@@ -298,14 +266,12 @@ class BundleEnableCommand extends FlutterPatcherCommand {
298
266
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
299
267
  final backend = requireBackend(cfg, override: backendOverride);
300
268
  banner('bundle · enable');
301
- await spinner(
302
- () async {
303
- await backend.db.updateBundle(id!, {'enabled': true});
304
- await backend.db.commitBundle();
305
- },
306
- 'Enabling bundle $id',
307
- done: 'Enabled',
308
- );
269
+ final steps = Steps('enable');
270
+ await steps.run('Enabling bundle $id', () async {
271
+ await backend.db.updateBundle(id!, {'enabled': true});
272
+ await backend.db.commitBundle();
273
+ });
274
+ steps.summary();
309
275
  });
310
276
  }
311
277
 
@@ -336,7 +302,9 @@ class BundleForceCommand extends FlutterPatcherCommand {
336
302
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
337
303
  final backend = requireBackend(cfg, override: backendOverride);
338
304
  banner('bundle · force');
339
- await spinner(
305
+ final steps = Steps('force');
306
+ await steps.run(
307
+ off ? 'Clearing force flag on $id' : 'Forcing update for $id',
340
308
  () async {
341
309
  await backend.db.updateBundle(id!, {
342
310
  'shouldForceUpdate': !off,
@@ -344,9 +312,8 @@ class BundleForceCommand extends FlutterPatcherCommand {
344
312
  });
345
313
  await backend.db.commitBundle();
346
314
  },
347
- off ? 'Clearing force flag on $id' : 'Forcing update for $id',
348
- done: off ? 'Cleared' : 'Forced',
349
315
  );
316
+ steps.summary();
350
317
  });
351
318
  }
352
319
 
@@ -380,11 +347,10 @@ class BundlePromoteCommand extends FlutterPatcherCommand {
380
347
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
381
348
  final backend = requireBackend(cfg, override: backendOverride);
382
349
  banner('bundle · promote');
383
- await spinner(
384
- () => promoteBundle(backend, id!, channel),
385
- 'Promoting $id to $channel',
386
- done: 'Promoted',
387
- );
350
+ final steps = Steps('promote');
351
+ await steps.run('Promoting $id to $channel',
352
+ () => promoteBundle(backend, id!, channel));
353
+ steps.summary();
388
354
  box('promote', [kv('bundle', cyan(id!)), kv('channel', channel)]);
389
355
  });
390
356
  }
@@ -432,15 +398,13 @@ class BundleUpdateCommand extends FlutterPatcherCommand {
432
398
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
433
399
  final backend = requireBackend(cfg, override: backendOverride);
434
400
  banner('bundle · update');
435
- await spinner(
436
- () async {
437
- await backend.db.updateBundle(id!, patch);
438
- await backend.db.commitBundle();
439
- },
440
- 'Updating $id',
441
- done: 'Updated',
442
- );
401
+ final steps = Steps('update');
402
+ await steps.run('Updating $id', () async {
403
+ await backend.db.updateBundle(id!, patch);
404
+ await backend.db.commitBundle();
405
+ });
443
406
  final b = await backend.db.getBundleById(id!);
407
+ steps.summary();
444
408
  if (b != null) {
445
409
  box('bundle', [
446
410
  kv('id', cyan(b.id)),
@@ -24,16 +24,6 @@ class ChannelCommand extends FlutterPatcherCommand {
24
24
 
25
25
  @override
26
26
  String get description => 'Manage channels (list / get / set).';
27
-
28
- @override
29
- Future<int> run() => runGuarded(() async {
30
- print(description);
31
- print('');
32
- print('Subcommands:');
33
- print(' list List channels');
34
- print(' get <channel> Show the live bundle for a channel');
35
- print(' set <c> <id> Promote a bundle to a channel');
36
- });
37
27
  }
38
28
 
39
29
  class ChannelListCommand extends FlutterPatcherCommand {
@@ -140,11 +130,10 @@ class ChannelSetCommand extends FlutterPatcherCommand {
140
130
  }
141
131
  final backend = requireBackend(cfg, override: backendOverride);
142
132
  banner('channel · set');
143
- await spinner(
144
- () => promoteBundle(backend, bundleId, channel),
145
- 'Promoting $bundleId to $channel',
146
- done: 'Channel set',
147
- );
133
+ final steps = Steps('set');
134
+ await steps.run('Promoting $bundleId to $channel',
135
+ () => promoteBundle(backend, bundleId, channel));
136
+ steps.summary();
148
137
  box('channel set', [kv('channel', channel), kv('bundle', cyan(bundleId))]);
149
138
  });
150
139
  }
@@ -18,17 +18,6 @@ class ConfigCommand extends FlutterPatcherCommand {
18
18
 
19
19
  @override
20
20
  String get description => 'Get, set, or list configuration values.';
21
-
22
- @override
23
- Future<int> run() => runGuarded(() async {
24
- // No subcommand selected.
25
- print(description);
26
- print('');
27
- print('Subcommands:');
28
- print(' get <key>');
29
- print(' set <key> <value>');
30
- print(' list');
31
- });
32
21
  }
33
22
 
34
23
  Map<String, dynamic> _loadProjectJson() {
@@ -59,6 +48,7 @@ class ConfigGetCommand extends FlutterPatcherCommand {
59
48
 
60
49
  @override
61
50
  Future<int> run() => runGuarded(() async {
51
+ ui.banner('config · get');
62
52
  final key = argResults!['key'] as String? ??
63
53
  (argResults!.rest.isNotEmpty ? argResults!.rest.first : null);
64
54
  if (key == null || key.isEmpty) {
@@ -66,7 +56,7 @@ class ConfigGetCommand extends FlutterPatcherCommand {
66
56
  }
67
57
  final value = readPath(_loadProjectJson(), key);
68
58
  if (value == null) {
69
- stderr.writeln('(not set)');
59
+ ui.warn('(not set)');
70
60
  return;
71
61
  }
72
62
  stdout.writeln(
@@ -39,6 +39,7 @@ class ConsoleCommand extends FlutterPatcherCommand {
39
39
  return;
40
40
  }
41
41
  if (argResults!['open'] as bool) {
42
+ banner('console · open');
42
43
  step('Launching console...');
43
44
  final process = await Process.start(
44
45
  'flutter',
@@ -47,7 +48,8 @@ class ConsoleCommand extends FlutterPatcherCommand {
47
48
  runInShell: true,
48
49
  mode: ProcessStartMode.inheritStdio,
49
50
  );
50
- exitCode = await process.exitCode;
51
+ final code = await process.exitCode;
52
+ if (code != 0) err('flutter run exited with code $code');
51
53
  return;
52
54
  }
53
55
  banner('console');
@@ -1,5 +1,7 @@
1
1
  import 'dart:io';
2
2
 
3
+ import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart';
4
+
3
5
  import '../backend.dart';
4
6
  import '../cli_base.dart';
5
7
  import '../config.dart';
@@ -73,31 +75,13 @@ class DeployCommand extends FlutterPatcherCommand {
73
75
  final bundleId = argResults!['bundle-id'] as String?;
74
76
 
75
77
  banner('deploy');
76
- stdout.writeln(
77
- ' ${gray('channel')} ${cyan(channel)} '
78
- '${gray('platform')} ${cyan(platform)} '
79
- '${gray('source')} ${dim(source)}',
80
- );
78
+ info('channel ${cyan(channel)} platform ${cyan(platform)} source ${dim(source)}');
81
79
 
82
- final bundle = await spinner(
83
- () => deployBundle(
84
- backend,
85
- DeployOptions(
86
- source: source,
87
- channel: channel,
88
- platform: platform,
89
- message: message,
90
- force: force,
91
- targetAppVersion: targetAppVersion,
92
- fingerprintHash: fingerprintHash,
93
- signingKeyBase64: signingKey,
94
- gitCommitHash: resolvedGitCommitHash,
95
- bundleId: bundleId,
96
- ),
97
- ),
98
- 'Uploading & registering bundle',
99
- done: 'Bundle deployed',
100
- );
80
+ final steps = Steps('deploy');
81
+ final bundle = await _deployWithPhases(steps, backend, source, channel,
82
+ platform, message, force, targetAppVersion, fingerprintHash,
83
+ signingKey, resolvedGitCommitHash, bundleId);
84
+ steps.summary();
101
85
 
102
86
  final lines = <String>[
103
87
  kv('bundle id', cyan(bundle.id)),
@@ -122,3 +106,57 @@ class DeployCommand extends FlutterPatcherCommand {
122
106
  box('deployed', lines);
123
107
  });
124
108
  }
109
+
110
+ /// Deploy with per-phase UI. Each phase reported by `DeployOptions.onPhase`
111
+ /// is shown as an active spinner line that gets replaced in-place.
112
+ Future<Bundle> _deployWithPhases(
113
+ Steps steps,
114
+ Backend backend,
115
+ String source,
116
+ String channel,
117
+ String platform,
118
+ String? message,
119
+ bool force,
120
+ String? targetAppVersion,
121
+ String? fingerprintHash,
122
+ String? signingKey,
123
+ String? resolvedGitCommitHash,
124
+ String? bundleId,
125
+ ) async {
126
+ String? activeLabel;
127
+ try {
128
+ final bundle = await deployBundle(
129
+ backend,
130
+ DeployOptions(
131
+ source: source,
132
+ channel: channel,
133
+ platform: platform,
134
+ message: message,
135
+ force: force,
136
+ targetAppVersion: targetAppVersion,
137
+ fingerprintHash: fingerprintHash,
138
+ signingKeyBase64: signingKey,
139
+ gitCommitHash: resolvedGitCommitHash,
140
+ bundleId: bundleId,
141
+ onPhase: (phase) {
142
+ if (activeLabel != null) {
143
+ steps.completeActive();
144
+ }
145
+ activeLabel = phase;
146
+ steps.startActive(phase);
147
+ },
148
+ ),
149
+ );
150
+ if (activeLabel != null) {
151
+ steps.completeActive();
152
+ activeLabel = null;
153
+ }
154
+ return bundle;
155
+ } catch (e) {
156
+ if (activeLabel != null) {
157
+ steps.completeActive(success: false);
158
+ activeLabel = null;
159
+ }
160
+ rethrow;
161
+ }
162
+ }
@@ -1,7 +1,5 @@
1
- import 'dart:io';
2
-
3
-
4
1
  import '../cli_base.dart';
2
+ import '../ui/ui.dart';
5
3
  import '../util.dart';
6
4
 
7
5
  /// `flutter_ota_kit fingerprint` — compute a build-time fingerprint hash.
@@ -24,8 +22,9 @@ class FingerprintCommand extends FlutterPatcherCommand {
24
22
 
25
23
  @override
26
24
  Future<int> run() => runGuarded(() async {
25
+ banner(name);
27
26
  final source = argResults!['source'] as String;
28
27
  final hash = generateFingerprint(source);
29
- stdout.writeln(hash);
28
+ box('fingerprint', [kv('source', source), kv('hash', hash)]);
30
29
  });
31
30
  }