@_nazmiforreal/flutter-ota 0.1.2 → 0.1.3

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 (61) hide show
  1. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
  2. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
  3. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +7 -6
  4. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +46 -49
  5. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +44 -23
  6. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +27 -17
  7. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +11 -6
  8. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +39 -22
  9. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +22 -13
  10. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +6 -5
  11. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +15 -9
  12. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +14 -8
  13. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +37 -12
  14. package/dart-src/packages/cli-tools/lib/src/operations.dart +45 -0
  15. package/dart-src/packages/cli-tools/lib/src/pack.dart +9 -0
  16. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +234 -0
  17. package/dart-src/packages/cli-tools/pubspec.lock +638 -0
  18. package/dart-src/packages/cli-tools/test/backends_test.dart +207 -0
  19. package/dart-src/packages/cli-tools/test/build_pack_test.dart +101 -0
  20. package/dart-src/packages/cli-tools/test/cli_test.dart +442 -0
  21. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +83 -0
  22. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +256 -0
  23. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +347 -0
  24. package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +315 -0
  25. package/dart-src/packages/core/test/rollout_test.dart +128 -0
  26. package/dart-src/packages/core/test/semver_test.dart +52 -0
  27. package/dart-src/packages/core/test/semver_vectors.json +1 -0
  28. package/dart-src/packages/core/test/types_test.dart +129 -0
  29. package/dart-src/packages/core/test/uuid_test.dart +38 -0
  30. package/dart-src/plugins/aws/test/aws_database_test.dart +167 -0
  31. package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +190 -0
  32. package/dart-src/plugins/aws/test/aws_storage_test.dart +73 -0
  33. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +83 -0
  34. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +122 -0
  35. package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +106 -0
  36. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +190 -0
  37. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +256 -0
  38. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +83 -0
  39. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +77 -0
  40. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +166 -0
  41. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +74 -0
  42. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +332 -0
  43. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +711 -0
  44. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +294 -0
  45. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +192 -0
  46. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +51 -0
  47. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +1600 -0
  48. package/dart-src/plugins/plugin-core/test/helpers_test.dart +259 -0
  49. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +139 -0
  50. package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +86 -0
  51. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +77 -0
  52. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +297 -0
  53. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +140 -0
  54. package/dart-src/plugins/postgres/test/postgres_database_test.dart +181 -0
  55. package/dart-src/plugins/standalone/test/standalone_test.dart +264 -0
  56. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +654 -0
  57. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +112 -0
  58. package/dart-src/plugins/supabase/test/supabase_database_test.dart +207 -0
  59. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +175 -0
  60. package/package.json +1 -1
  61. package/scripts/vendor.sh +29 -0
@@ -1,11 +1,9 @@
1
- import 'dart:io';
2
-
3
1
  import 'package:args/args.dart';
4
2
 
5
3
  import 'backend.dart';
6
4
  import 'config.dart';
7
-
8
- /// Run [body], mapping errors to a non-zero exit code with a clean message.
5
+ import 'pack.dart';
6
+ import 'ui/ui.dart';
9
7
 
10
8
  /// Run [body], mapping errors to a non-zero exit code with a clean message.
11
9
  Future<int> runGuarded(Future<void> Function() body) async {
@@ -13,10 +11,13 @@ Future<int> runGuarded(Future<void> Function() body) async {
13
11
  await body();
14
12
  return 0;
15
13
  } on StateError catch (e) {
16
- stderr.writeln('error: ${e.message}');
14
+ err(e.message);
17
15
  return 1;
16
+ } on PackException catch (e) {
17
+ err(e.message);
18
+ return e.exitCode;
18
19
  } catch (e) {
19
- stderr.writeln('error: $e');
20
+ err(e.toString());
20
21
  return 1;
21
22
  }
22
23
  }
@@ -1,8 +1,8 @@
1
- import 'dart:io';
2
-
3
1
  import 'package:args/command_runner.dart';
4
2
  import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
5
3
 
4
+ import '../ui/ui.dart';
5
+
6
6
  /// `flutter_patcher build` — package a release APK into a device-compatible
7
7
  /// OTA patch (`dist/patch.zip`) that the `deploy` command can publish.
8
8
  ///
@@ -48,52 +48,49 @@ class BuildCommand extends Command<int> {
48
48
  'Build a device-compatible OTA patch (patch.zip) from a release APK.';
49
49
 
50
50
  @override
51
- Future<int> run() async {
52
- final results = argResults!;
53
- final apk = results['apk'] as String?;
54
- if (apk == null || apk.isEmpty) {
55
- stderr.writeln('error: missing required --apk <path>');
56
- return 64;
57
- }
58
- final version = results['version'] as String?;
59
- if (version == null || version.isEmpty) {
60
- stderr.writeln('error: missing required --version <string>');
61
- return 64;
62
- }
63
- final targetVersionCodeRaw = results['target-version-code'] as String?;
64
- if (targetVersionCodeRaw == null || targetVersionCodeRaw.isEmpty) {
65
- stderr.writeln('error: missing required --target-version-code <int>');
66
- return 64;
67
- }
68
- final targetVersionCode = int.tryParse(targetVersionCodeRaw);
69
- if (targetVersionCode == null) {
70
- stderr.writeln('error: --target-version-code must be an integer');
71
- return 64;
72
- }
51
+ Future<int> run() => runGuarded(() async {
52
+ final results = argResults!;
53
+ final apk = results['apk'] as String?;
54
+ if (apk == null || apk.isEmpty) {
55
+ throw PackException('missing required --apk <path>', 64);
56
+ }
57
+ final version = results['version'] as String?;
58
+ if (version == null || version.isEmpty) {
59
+ throw PackException('missing required --version <string>', 64);
60
+ }
61
+ final targetVersionCodeRaw = results['target-version-code'] as String?;
62
+ if (targetVersionCodeRaw == null || targetVersionCodeRaw.isEmpty) {
63
+ throw PackException('missing required --target-version-code <int>', 64);
64
+ }
65
+ final targetVersionCode = int.tryParse(targetVersionCodeRaw);
66
+ if (targetVersionCode == null) {
67
+ throw PackException('--target-version-code must be an integer', 64);
68
+ }
73
69
 
74
- try {
75
- final result = await packPatch(
76
- apkPath: apk,
77
- version: version,
78
- targetVersionCode: targetVersionCode,
79
- abi: results['abi'] as String?,
80
- requestedAssets: results['assets'] as List<String>,
81
- out: results['out'] as String,
82
- );
83
- stdout.writeln('[build] version: ${result.version}');
84
- stdout.writeln('[build] targetVersionCode: ${result.targetVersionCode}');
85
- stdout.writeln('[build] abis: ${result.abis.join(', ')}');
86
- stdout.writeln('[build] bundle md5: ${result.md5}');
87
- stdout.writeln('[build] overlay assets: ${result.assetCount}');
88
- stdout.writeln('[build] payload: ${result.payloadPath}');
89
- stdout.writeln('[build] manifest: ${result.manifestPath}');
90
- return 0;
91
- } on PackException catch (e) {
92
- stderr.writeln('error: ${e.message}');
93
- return e.exitCode;
94
- } on Object catch (e) {
95
- stderr.writeln('error: $e');
96
- return 1;
97
- }
98
- }
70
+ banner('build');
71
+ final bar = ProgressBar(1, 'pack');
72
+ final result = await packPatch(
73
+ apkPath: apk,
74
+ version: version,
75
+ targetVersionCode: targetVersionCode,
76
+ abi: results['abi'] as String?,
77
+ requestedAssets: results['assets'] as List<String>,
78
+ out: results['out'] as String,
79
+ onProgress: (done, total, label) {
80
+ bar.total = total;
81
+ bar.update(done, label);
82
+ },
83
+ );
84
+
85
+ box('patch.zip', [
86
+ kv('version', result.version),
87
+ kv('target code', result.targetVersionCode.toString()),
88
+ kv('abis', cyan(result.abis.join(' · '))),
89
+ kv('bundle md5', gray(result.md5)),
90
+ kv('overlay assets', result.assetCount.toString()),
91
+ kv('payload', result.payloadPath),
92
+ kv('manifest', result.manifestPath),
93
+ ]);
94
+ step('Build complete — ready to deploy.');
95
+ });
99
96
  }
@@ -1,12 +1,8 @@
1
- import 'dart:io';
2
-
3
1
  import 'package:args/args.dart';
4
2
  import 'package:args/command_runner.dart';
3
+ import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
5
4
 
6
- import '../backend.dart';
7
- import '../cli_base.dart';
8
- import '../config.dart';
9
- import '../operations.dart';
5
+ import '../ui/ui.dart';
10
6
 
11
7
  /// `flutter_patcher bundle` — manage bundles.
12
8
  class BundleCommand extends Command<int> {
@@ -69,20 +65,32 @@ class BundleListCommand extends Command<int> {
69
65
  limit: int.parse(argResults!['limit'] as String),
70
66
  ),
71
67
  );
68
+ banner('bundle · list');
72
69
  if (res.data.isEmpty) {
73
- stdout.writeln('(no bundles)');
70
+ step('(no bundles)');
74
71
  return;
75
72
  }
76
- stdout.writeln('id channel '
77
- 'enabled platform target');
78
- for (final b in res.data) {
79
- final target = b.targetAppVersion ?? b.fingerprintHash ?? '-';
80
- stdout.writeln('${b.id} ${b.channel.padRight(12)} '
81
- '${(b.enabled ? 'yes' : 'no').padRight(7)} '
82
- '${b.platform.value.padRight(7)} $target');
73
+ final lines = <String>[];
74
+ for (var i = 0; i < res.data.length; i++) {
75
+ final b = res.data[i];
76
+ final target = b.targetAppVersion ?? b.fingerprintHash ?? dim('-');
77
+ lines
78
+ ..add(kv('#$i', cyan(b.id)))
79
+ ..add(kv('channel', b.channel))
80
+ ..add(kv(
81
+ 'enabled',
82
+ b.enabled ? green('yes') : yellow('no'),
83
+ ))
84
+ ..add(kv('platform', b.platform.value))
85
+ ..add(kv('target', target));
86
+ if (b.message != null) lines.add(kv('message', b.message!));
87
+ if (b.metadata?.signature != null) {
88
+ lines.add(kv('signature', green('✓ signed')));
89
+ }
90
+ lines.add('');
83
91
  }
84
- stdout.writeln('');
85
- stdout.writeln('total: ${res.pagination.total}');
92
+ box('${res.data.length} bundles', lines);
93
+ step('total: ${res.pagination.total}');
86
94
  });
87
95
  }
88
96
 
@@ -107,12 +115,16 @@ class BundleDeleteCommand extends Command<int> {
107
115
  Future<int> run() => runGuarded(() async {
108
116
  final id = argResults!['id'] as String?;
109
117
  if (id == null || id.isEmpty) {
110
- throw StateError('Usage: flutter_patcher bundle delete <id>');
118
+ throw PackException('Usage: flutter_patcher bundle delete --id <id>', 64);
111
119
  }
112
120
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
113
121
  final backend = requireBackend(cfg, override: backendOverride);
114
- await deleteBundle(backend, id);
115
- stdout.writeln('Deleted bundle $id');
122
+ banner('bundle · delete');
123
+ await spinner(
124
+ () => deleteBundle(backend, id),
125
+ 'Deleting bundle $id',
126
+ done: 'Deleted',
127
+ );
116
128
  });
117
129
  }
118
130
 
@@ -139,13 +151,22 @@ class BundlePromoteCommand extends Command<int> {
139
151
  final id = argResults!['id'] as String?;
140
152
  final channel = argResults!['channel'] as String?;
141
153
  if (id == null || id.isEmpty || channel == null || channel.isEmpty) {
142
- throw StateError(
143
- 'Usage: flutter_patcher bundle promote <id> --channel <channel>',
154
+ throw PackException(
155
+ 'Usage: flutter_patcher bundle promote --id <id> --channel <channel>',
156
+ 64,
144
157
  );
145
158
  }
146
159
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
147
160
  final backend = requireBackend(cfg, override: backendOverride);
148
- await promoteBundle(backend, id, channel);
149
- stdout.writeln('Promoted bundle $id to channel $channel');
161
+ banner('bundle · promote');
162
+ await spinner(
163
+ () => promoteBundle(backend, id, channel),
164
+ 'Promoting $id to $channel',
165
+ done: 'Promoted',
166
+ );
167
+ box('promote', [
168
+ kv('bundle', cyan(id)),
169
+ kv('channel', channel),
170
+ ]);
150
171
  });
151
172
  }
@@ -1,12 +1,8 @@
1
- import 'dart:io';
2
-
3
1
  import 'package:args/args.dart';
4
2
  import 'package:args/command_runner.dart';
3
+ import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
5
4
 
6
- import '../backend.dart';
7
- import '../cli_base.dart';
8
- import '../config.dart';
9
- import '../operations.dart';
5
+ import '../ui/ui.dart';
10
6
 
11
7
  /// `flutter_patcher channel` — manage channels.
12
8
  class ChannelCommand extends Command<int> {
@@ -56,14 +52,13 @@ class ChannelListCommand extends Command<int> {
56
52
  Future<int> run() => runGuarded(() async {
57
53
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
58
54
  final backend = requireBackend(cfg, override: backendOverride);
55
+ banner('channel · list');
59
56
  final channels = await listChannels(backend);
60
57
  if (channels.isEmpty) {
61
- stdout.writeln('(no channels)');
58
+ step('(no channels)');
62
59
  return;
63
60
  }
64
- for (final c in channels) {
65
- stdout.writeln(' $c');
66
- }
61
+ box('${channels.length} channels', channels.map((c) => ' $c').toList());
67
62
  });
68
63
  }
69
64
 
@@ -88,17 +83,23 @@ class ChannelGetCommand extends Command<int> {
88
83
  Future<int> run() => runGuarded(() async {
89
84
  final channel = argResults!['channel'] as String?;
90
85
  if (channel == null || channel.isEmpty) {
91
- throw StateError('Usage: flutter_patcher channel get <channel>');
86
+ throw PackException('Usage: flutter_patcher channel get --channel <channel>', 64);
92
87
  }
93
88
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
94
89
  final backend = requireBackend(cfg, override: backendOverride);
90
+ banner('channel · get');
95
91
  final bundle = await getChannel(backend, channel);
96
92
  if (bundle == null) {
97
- stdout.writeln('(no live bundle on channel "$channel")');
93
+ err('no live bundle on channel "$channel"');
98
94
  return;
99
95
  }
100
- stdout.writeln('channel "$channel" -> bundle ${bundle.id} '
101
- '(${bundle.platform.value})');
96
+ box('channel "$channel"', [
97
+ kv('live bundle', cyan(bundle.id)),
98
+ kv('platform', bundle.platform.value),
99
+ kv('enabled', bundle.enabled ? green('yes') : yellow('no')),
100
+ kv('target', bundle.targetAppVersion ?? bundle.fingerprintHash ?? dim('-')),
101
+ if (bundle.message != null) kv('message', bundle.message!),
102
+ ]);
102
103
  });
103
104
  }
104
105
 
@@ -128,13 +129,22 @@ class ChannelSetCommand extends Command<int> {
128
129
  channel.isEmpty ||
129
130
  bundleId == null ||
130
131
  bundleId.isEmpty) {
131
- throw StateError(
132
+ throw PackException(
132
133
  'Usage: flutter_patcher channel set --channel <c> --bundle-id <id>',
134
+ 64,
133
135
  );
134
136
  }
135
137
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
136
138
  final backend = requireBackend(cfg, override: backendOverride);
137
- await promoteBundle(backend, bundleId, channel);
138
- stdout.writeln('Channel "$channel" -> bundle $bundleId');
139
+ banner('channel · set');
140
+ await spinner(
141
+ () => promoteBundle(backend, bundleId, channel),
142
+ 'Promoting $bundleId to $channel',
143
+ done: 'Channel set',
144
+ );
145
+ box('channel set', [
146
+ kv('channel', channel),
147
+ kv('bundle', cyan(bundleId)),
148
+ ]);
139
149
  });
140
150
  }
@@ -2,9 +2,10 @@ import 'dart:io';
2
2
 
3
3
  import 'package:args/args.dart';
4
4
  import 'package:args/command_runner.dart';
5
+ import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
5
6
  import 'package:path/path.dart' as p;
6
7
 
7
- import '../cli_base.dart';
8
+ import '../ui/ui.dart';
8
9
 
9
10
  /// `flutter_patcher console` — open the web console.
10
11
  class ConsoleCommand extends Command<int> {
@@ -33,11 +34,11 @@ class ConsoleCommand extends Command<int> {
33
34
  Future<int> run() => runGuarded(() async {
34
35
  final consoleDir = _findConsoleDir();
35
36
  if (consoleDir == null) {
36
- stdout.writeln('Console package not found (expected packages/console).');
37
+ err('Console package not found (expected packages/console).');
37
38
  return;
38
39
  }
39
40
  if (argResults!['open'] as bool) {
40
- stdout.writeln('Launching console...');
41
+ step('Launching console...');
41
42
  final process = await Process.start(
42
43
  'flutter',
43
44
  ['run', '-d', 'chrome'],
@@ -48,8 +49,12 @@ class ConsoleCommand extends Command<int> {
48
49
  exitCode = await process.exitCode;
49
50
  return;
50
51
  }
51
- stdout.writeln('flutter_patcher web console:');
52
- stdout.writeln(' cd ${consoleDir.path}');
53
- stdout.writeln(' flutter run -d chrome');
52
+ banner('console');
53
+ box('flutter_patcher console', [
54
+ 'Open the web console with Flutter:',
55
+ '',
56
+ kv('dir', consoleDir.path),
57
+ kv('run', 'flutter run -d chrome'),
58
+ ]);
54
59
  });
55
60
  }
@@ -7,6 +7,7 @@ import '../backend.dart';
7
7
  import '../cli_base.dart';
8
8
  import '../config.dart';
9
9
  import '../operations.dart';
10
+ import '../ui/ui.dart';
10
11
  import '../util.dart';
11
12
 
12
13
  /// `flutter_patcher deploy` — zip + upload + register a new bundle.
@@ -60,36 +61,52 @@ class DeployCommand extends Command<int> {
60
61
  argResults!['git-commit-hash'] as String? ?? await gitCommitHash(source);
61
62
  final bundleId = argResults!['bundle-id'] as String?;
62
63
 
63
- final bundle = await deployBundle(
64
- backend,
65
- DeployOptions(
66
- source: source,
67
- channel: channel,
68
- platform: platform,
69
- message: message,
70
- force: force,
71
- targetAppVersion: targetAppVersion,
72
- fingerprintHash: fingerprintHash,
73
- signingKeyBase64: signingKey,
74
- gitCommitHash: resolvedGitCommitHash,
75
- bundleId: bundleId,
64
+ banner('deploy');
65
+ stdout.writeln(
66
+ '${gray('channel')} ${cyan(channel)} '
67
+ '${gray('platform')} ${cyan(platform)} '
68
+ '${gray('source')} ${dim(source)}',
69
+ );
70
+ final bundle = await spinner(
71
+ () => deployBundle(
72
+ backend,
73
+ DeployOptions(
74
+ source: source,
75
+ channel: channel,
76
+ platform: platform,
77
+ message: message,
78
+ force: force,
79
+ targetAppVersion: targetAppVersion,
80
+ fingerprintHash: fingerprintHash,
81
+ signingKeyBase64: signingKey,
82
+ gitCommitHash: resolvedGitCommitHash,
83
+ bundleId: bundleId,
84
+ ),
76
85
  ),
86
+ 'Uploading & registering bundle',
87
+ done: 'Bundle deployed',
77
88
  );
78
89
 
79
- stdout.writeln('Deployed bundle ${bundle.id}');
80
- stdout.writeln(' channel: ${bundle.channel}');
81
- stdout.writeln(' platform: ${bundle.platform.value}');
82
- stdout.writeln(' enabled: ${bundle.enabled}');
83
- stdout.writeln(' storageUri: ${bundle.storageUri}');
90
+ final lines = <String>[
91
+ kv('bundle id', cyan(bundle.id)),
92
+ kv('channel', bundle.channel),
93
+ kv('platform', bundle.platform.value),
94
+ kv('enabled', bundle.enabled ? green('true') : yellow('false')),
95
+ kv('storage uri', gray(bundle.storageUri)),
96
+ ];
84
97
  if (bundle.targetAppVersion != null) {
85
- stdout.writeln(' targetAppVersion: ${bundle.targetAppVersion}');
98
+ lines.add(kv('target app', bundle.targetAppVersion!));
86
99
  }
87
100
  if (bundle.fingerprintHash != null) {
88
- stdout.writeln(' fingerprintHash: ${bundle.fingerprintHash}');
101
+ lines.add(kv('fingerprint', bundle.fingerprintHash!));
89
102
  }
90
103
  if (bundle.gitCommitHash != null) {
91
- stdout.writeln(' gitCommitHash: ${bundle.gitCommitHash}');
104
+ lines.add(kv('git commit', bundle.gitCommitHash!));
105
+ }
106
+ if (bundle.message != null) lines.add(kv('message', bundle.message!));
107
+ if (bundle.metadata?.signature != null) {
108
+ lines.add(kv('signature', dim('✓ signed')));
92
109
  }
93
- if (bundle.message != null) stdout.writeln(' message: ${bundle.message}');
110
+ box('deployed', lines);
94
111
  });
95
112
  }
@@ -2,10 +2,9 @@ import 'dart:io';
2
2
 
3
3
  import 'package:args/args.dart';
4
4
  import 'package:args/command_runner.dart';
5
+ import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
5
6
 
6
- import '../backend.dart';
7
- import '../cli_base.dart';
8
- import '../config.dart';
7
+ import '../ui/ui.dart';
9
8
 
10
9
  /// `flutter_patcher doctor` — environment + backend connectivity check.
11
10
  class DoctorCommand extends Command<int> {
@@ -18,7 +17,8 @@ class DoctorCommand extends Command<int> {
18
17
  String get name => 'doctor';
19
18
 
20
19
  @override
21
- String get description => 'Diagnose the local environment and backend connection.';
20
+ String get description =>
21
+ 'Diagnose the local environment and backend connection.';
22
22
 
23
23
  @override
24
24
  ArgParser get argParser => ArgParser()
@@ -26,23 +26,32 @@ class DoctorCommand extends Command<int> {
26
26
 
27
27
  @override
28
28
  Future<int> run() => runGuarded(() async {
29
- stdout.writeln('flutter_patcher CLI doctor');
30
- stdout.writeln(' dart: ${Platform.version.split(' ').first}');
31
- stdout.writeln(' os: ${Platform.operatingSystem} '
32
- '${Platform.operatingSystemVersion.split('\n').first}');
29
+ banner('doctor');
30
+ final lines = <String>[
31
+ kv('dart', Platform.version.split(' ').first),
32
+ kv(
33
+ 'os',
34
+ '${Platform.operatingSystem} '
35
+ '${Platform.operatingSystemVersion.split('\n').first}',
36
+ ),
37
+ ];
33
38
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
34
39
  if (cfg == null) {
35
- stdout.writeln(' config: not found (run `flutter_patcher init`)');
40
+ lines.add(kv('config', yellow('not found run `flutter_patcher init`')));
41
+ box('doctor', lines);
36
42
  return;
37
43
  }
38
- stdout.writeln(' config: provider=${cfg.provider} '
39
- 'channel=${cfg.channel} platform=${cfg.platform}');
44
+ lines
45
+ ..add(kv('provider', cfg.provider))
46
+ ..add(kv('channel', cfg.channel))
47
+ ..add(kv('platform', cfg.platform));
40
48
  try {
41
49
  final backend = requireBackend(cfg, override: backendOverride);
42
50
  final channels = await backend.db.getChannels();
43
- stdout.writeln(' backend: reachable (channels: ${channels.join(', ')})');
51
+ lines.add(kv('backend', green('reachable ${channels.join(', ')}')));
44
52
  } catch (e) {
45
- stdout.writeln(' backend: unreachable — $e');
53
+ lines.add(kv('backend', red('unreachable — $e')));
46
54
  }
55
+ box('doctor', lines);
47
56
  });
48
57
  }
@@ -2,10 +2,9 @@ import 'dart:io';
2
2
 
3
3
  import 'package:args/args.dart';
4
4
  import 'package:args/command_runner.dart';
5
+ import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
5
6
 
6
- import '../backend.dart';
7
- import '../cli_base.dart';
8
- import '../config.dart';
7
+ import '../ui/ui.dart';
9
8
 
10
9
  /// `flutter_patcher init` — scaffold a project config file.
11
10
  class InitCommand extends Command<int> {
@@ -45,10 +44,12 @@ class InitCommand extends Command<int> {
45
44
  Future<int> run() => runGuarded(() async {
46
45
  final file = configCandidates().first;
47
46
  if (file.existsSync() && !(argResults!['force'] as bool)) {
48
- throw StateError(
47
+ throw PackException(
49
48
  'Config already exists at ${file.path}. Use --force to overwrite.',
49
+ 64,
50
50
  );
51
51
  }
52
+ banner('init');
52
53
 
53
54
  final provider = argResults!['provider'] as String;
54
55
  final url = _prompt('Supabase URL', argResults!['url'] as String?);
@@ -77,6 +78,6 @@ class InitCommand extends Command<int> {
77
78
  source: source,
78
79
  );
79
80
  saveConfig(cfg);
80
- stdout.writeln('Wrote ${file.path}');
81
+ step('Wrote ${file.path}');
81
82
  });
82
83
  }
@@ -1,5 +1,4 @@
1
1
  import 'dart:convert';
2
- import 'dart:io';
3
2
 
4
3
  import 'package:args/args.dart';
5
4
  import 'package:args/command_runner.dart';
@@ -8,6 +7,7 @@ import '../backend.dart';
8
7
  import '../cli_base.dart';
9
8
  import '../config.dart';
10
9
  import '../sign.dart';
10
+ import '../ui/ui.dart';
11
11
 
12
12
  /// `flutter_patcher keys` — generate an Ed25519 keypair for bundle signing.
13
13
  class KeysCommand extends Command<int> {
@@ -29,12 +29,19 @@ class KeysCommand extends Command<int> {
29
29
 
30
30
  @override
31
31
  Future<int> run() => runGuarded(() async {
32
- final (privateB64, publicB64) = await generateEd25519KeyPair();
33
- stdout.writeln('Private key (keep secret, use with `deploy --key`):');
34
- stdout.writeln(' $privateB64');
35
- stdout.writeln('');
36
- stdout.writeln('Public key (safe to embed / configure on device):');
37
- stdout.writeln(' $publicB64');
32
+ banner('keys');
33
+ final (privateB64, publicB64) = await spinner(
34
+ () => generateEd25519KeyPair(),
35
+ 'Generating Ed25519 keypair',
36
+ done: 'Keypair generated',
37
+ );
38
+ box('ed25519 keys', [
39
+ '${bold('private')} (keep secret — use with `deploy --key`):',
40
+ ' ${red(privateB64)}',
41
+ '',
42
+ '${bold('public')} (safe to embed / configure on device):',
43
+ ' ${green(publicB64)}',
44
+ ]);
38
45
  if (argResults!['save'] as bool) {
39
46
  final file = configCandidates().first;
40
47
  final json = file.existsSync() && file.readAsStringSync().trim().isNotEmpty
@@ -42,8 +49,7 @@ class KeysCommand extends Command<int> {
42
49
  : <String, dynamic>{};
43
50
  writePath(json, 'publicKey', publicB64);
44
51
  _save(json);
45
- stdout.writeln('');
46
- stdout.writeln('Saved public key to ${file.path}');
52
+ step('Saved public key to ${file.path}');
47
53
  }
48
54
  });
49
55