@_nazmiforreal/flutter-ota 0.1.20 → 0.1.21

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 (44) hide show
  1. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +2 -1
  2. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
  3. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +22 -10
  4. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +16 -5
  5. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +3 -0
  6. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
  7. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +12 -3
  8. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +129 -6
  9. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
  10. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +23 -14
  11. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +4 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +266 -26
  13. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +96 -58
  14. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +4 -1
  15. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +10 -9
  16. package/dart-src/packages/cli-tools/lib/src/config.dart +14 -19
  17. package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -1
  18. package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +10 -2
  19. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +0 -1
  20. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
  21. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
  22. package/dart-src/packages/core/lib/src/app_update_info.dart +71 -1
  23. package/dart-src/packages/core/lib/src/bundle.dart +62 -17
  24. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +7 -3
  25. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +16 -0
  26. package/dart-src/packages/core/lib/src/changed_asset.dart +33 -3
  27. package/dart-src/packages/core/lib/src/get_bundles_args.dart +24 -2
  28. package/dart-src/packages/core/lib/src/platform.dart +8 -0
  29. package/dart-src/packages/core/lib/src/semver.dart +0 -3
  30. package/dart-src/packages/core/lib/src/status.dart +20 -1
  31. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  32. package/dart-src/packages/core/lib/src/update_bundle_params.dart +15 -0
  33. package/dart-src/packages/core/lib/src/uuid.dart +11 -0
  34. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +2 -2
  35. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +3 -0
  36. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +17 -0
  37. package/dart-src/plugins/plugin-core/lib/src/types.dart +94 -3
  38. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +3 -0
  39. package/package.json +1 -1
  40. package/bin/flutter-ota-linux-x64 +0 -0
  41. package/bin/flutter-ota.js +0 -36
  42. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -460
  43. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -707
  44. package/dart-src/packages/cli-tools/pubspec.lock +0 -590
@@ -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';
@@ -244,7 +245,7 @@ void _printMissingSubcommand(Command<int> parent) {
244
245
  stderr.writeln('');
245
246
 
246
247
  if (subNames.isNotEmpty) {
247
- stderr.writeln(' ${_cyan('Did you mean?')}');
248
+ stderr.writeln(' ${_cyan('Available subcommands:')}');
248
249
  for (final s in subNames) {
249
250
  stderr.writeln(' ${_green('${parent.name} $s')}');
250
251
  }
@@ -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,12 +60,15 @@ 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('backend', abbr: 'b', help: detected != null
70
+ ? 'Backend provider [detected: $detected].'
71
+ : 'Backend provider.');
67
72
  argParser.addOption('channel', abbr: 'c', help: 'Filter by channel.');
68
73
  argParser.addOption('platform', abbr: 'p', help: 'Filter by platform.');
69
74
  argParser.addOption('enabled', help: 'Filter by enabled (true/false).');
@@ -130,7 +135,8 @@ class BundleListCommand extends FlutterPatcherCommand {
130
135
 
131
136
  class BundleShowCommand extends FlutterPatcherCommand {
132
137
  BundleShowCommand({this.config, this.backendOverride}) {
133
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
138
+ final detected = config?.provider ?? loadConfig()?.provider;
139
+ argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
134
140
  argParser.addOption('id', help: 'Bundle id.');
135
141
  }
136
142
 
@@ -173,7 +179,8 @@ class BundleShowCommand extends FlutterPatcherCommand {
173
179
 
174
180
  class BundleDeleteCommand extends FlutterPatcherCommand {
175
181
  BundleDeleteCommand({this.config, this.backendOverride}) {
176
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
182
+ final detected = config?.provider ?? loadConfig()?.provider;
183
+ argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
177
184
  argParser.addOption('id', help: 'Bundle id.');
178
185
  argParser.addFlag(
179
186
  'keep-storage',
@@ -215,7 +222,8 @@ class BundleDeleteCommand extends FlutterPatcherCommand {
215
222
 
216
223
  class BundleDisableCommand extends FlutterPatcherCommand {
217
224
  BundleDisableCommand({this.config, this.backendOverride}) {
218
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
225
+ final detected = config?.provider ?? loadConfig()?.provider;
226
+ argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
219
227
  argParser.addOption('id', help: 'Bundle id.');
220
228
  }
221
229
 
@@ -246,7 +254,8 @@ class BundleDisableCommand extends FlutterPatcherCommand {
246
254
 
247
255
  class BundleEnableCommand extends FlutterPatcherCommand {
248
256
  BundleEnableCommand({this.config, this.backendOverride}) {
249
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
257
+ final detected = config?.provider ?? loadConfig()?.provider;
258
+ argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
250
259
  argParser.addOption('id', help: 'Bundle id.');
251
260
  }
252
261
 
@@ -277,7 +286,8 @@ class BundleEnableCommand extends FlutterPatcherCommand {
277
286
 
278
287
  class BundleForceCommand extends FlutterPatcherCommand {
279
288
  BundleForceCommand({this.config, this.backendOverride}) {
280
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
289
+ final detected = config?.provider ?? loadConfig()?.provider;
290
+ argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
281
291
  argParser.addOption('id', help: 'Bundle id.');
282
292
  argParser.addFlag(
283
293
  'off',
@@ -319,7 +329,8 @@ class BundleForceCommand extends FlutterPatcherCommand {
319
329
 
320
330
  class BundlePromoteCommand extends FlutterPatcherCommand {
321
331
  BundlePromoteCommand({this.config, this.backendOverride}) {
322
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
332
+ final detected = config?.provider ?? loadConfig()?.provider;
333
+ argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
323
334
  argParser.addOption('id', help: 'Bundle id.');
324
335
  argParser.addOption('channel', abbr: 'c', help: 'Target channel.');
325
336
  }
@@ -351,13 +362,14 @@ class BundlePromoteCommand extends FlutterPatcherCommand {
351
362
  await steps.run('Promoting $id to $channel',
352
363
  () => promoteBundle(backend, id!, channel));
353
364
  steps.summary();
354
- box('promote', [kv('bundle', cyan(id!)), kv('channel', channel)]);
365
+ stdout.writeln(' ${dim('')} bundle ${cyan(id!)} channel ${cyan(channel)}');
355
366
  });
356
367
  }
357
368
 
358
369
  class BundleUpdateCommand extends FlutterPatcherCommand {
359
370
  BundleUpdateCommand({this.config, this.backendOverride}) {
360
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
371
+ final detected = config?.provider ?? loadConfig()?.provider;
372
+ argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
361
373
  argParser.addOption('id', help: 'Bundle id.');
362
374
  argParser.addOption('message', abbr: 'm', help: 'New release message.');
363
375
  argParser.addOption('target-version', help: 'New target app version (e.g. 1.0.0).');
@@ -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,10 @@ 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('backend', abbr: 'b', help: detected != null
35
+ ? 'Backend provider [detected: $detected].'
36
+ : 'Backend provider.');
32
37
  }
33
38
 
34
39
  final FlutterPatcherConfig? config;
@@ -56,7 +61,10 @@ class ChannelListCommand extends FlutterPatcherCommand {
56
61
 
57
62
  class ChannelGetCommand extends FlutterPatcherCommand {
58
63
  ChannelGetCommand({this.config, this.backendOverride}) {
59
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
64
+ final detected = config?.provider ?? loadConfig()?.provider;
65
+ argParser.addOption('backend', abbr: 'b', help: detected != null
66
+ ? 'Backend provider [detected: $detected].'
67
+ : 'Backend provider.');
60
68
  argParser.addOption('channel', abbr: 'c', help: 'Channel.');
61
69
  }
62
70
 
@@ -100,7 +108,10 @@ class ChannelGetCommand extends FlutterPatcherCommand {
100
108
 
101
109
  class ChannelSetCommand extends FlutterPatcherCommand {
102
110
  ChannelSetCommand({this.config, this.backendOverride}) {
103
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
111
+ final detected = config?.provider ?? loadConfig()?.provider;
112
+ argParser.addOption('backend', abbr: 'b', help: detected != null
113
+ ? 'Backend provider [detected: $detected].'
114
+ : 'Backend provider.');
104
115
  argParser.addOption('channel', abbr: 'c', help: 'Channel.');
105
116
  argParser.addOption('bundle-id', abbr: 'i', help: 'Bundle id.');
106
117
  }
@@ -112,7 +123,7 @@ class ChannelSetCommand extends FlutterPatcherCommand {
112
123
  String get name => 'set';
113
124
 
114
125
  @override
115
- String get description => 'Promote a bundle to a channel.';
126
+ String get description => 'Set the active bundle for a channel.';
116
127
 
117
128
  @override
118
129
  Future<int> run() => runGuarded(() async {
@@ -134,6 +145,6 @@ class ChannelSetCommand extends FlutterPatcherCommand {
134
145
  await steps.run('Promoting $bundleId to $channel',
135
146
  () => promoteBundle(backend, bundleId, channel));
136
147
  steps.summary();
137
- box('channel set', [kv('channel', channel), kv('bundle', cyan(bundleId))]);
148
+ stdout.writeln(' ${dim('')} channel ${cyan(channel)} = ${cyan(bundleId)}');
138
149
  });
139
150
  }
@@ -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 {
@@ -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,16 +6,20 @@ 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.',
19
23
  );
20
24
  argParser.addOption('source', abbr: 's', help: 'Source directory to zip + upload.');
21
25
  argParser.addOption('channel', abbr: 'c', help: 'Target channel.');
@@ -68,14 +72,19 @@ class DeployCommand extends FlutterPatcherCommand {
68
72
  final fingerprintHash = argResults!['fingerprint-hash'] as String?;
69
73
  final keyPath = argResults!['key'] as String?;
70
74
  String? signingKey;
71
- if (keyPath != null) signingKey = File(keyPath).readAsStringSync().trim();
75
+ if (keyPath != null) {
76
+ final keyFile = File(keyPath);
77
+ if (!keyFile.existsSync()) {
78
+ throw PackException('Signing key file not found: $keyPath', 64);
79
+ }
80
+ signingKey = keyFile.readAsStringSync().trim();
81
+ }
72
82
  final resolvedGitCommitHash =
73
83
  argResults!['git-commit-hash'] as String? ??
74
84
  await gitCommitHash(source);
75
85
  final bundleId = argResults!['bundle-id'] as String?;
76
86
 
77
87
  banner('deploy');
78
- info('channel ${cyan(channel)} platform ${cyan(platform)} source ${dim(source)}');
79
88
 
80
89
  final steps = Steps('deploy');
81
90
  final bundle = await _deployWithPhases(steps, backend, source, channel,
@@ -1,5 +1,8 @@
1
+ import 'dart:convert';
1
2
  import 'dart:io';
2
3
 
4
+ import 'package:http/http.dart' as http;
5
+
3
6
  import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
4
7
 
5
8
  import '../ui/ui.dart';
@@ -7,7 +10,10 @@ import '../ui/ui.dart';
7
10
  /// `flutter-ota doctor` — environment + backend connectivity check.
8
11
  class DoctorCommand extends FlutterPatcherCommand {
9
12
  DoctorCommand({this.config, this.backendOverride}) {
10
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
13
+ final detected = config?.provider ?? loadConfig()?.provider;
14
+ argParser.addOption('backend', abbr: 'b', help: detected != null
15
+ ? 'Backend provider [detected: $detected].'
16
+ : 'Backend provider.');
11
17
  }
12
18
 
13
19
  final FlutterPatcherConfig? config;
@@ -41,10 +47,8 @@ class DoctorCommand extends FlutterPatcherCommand {
41
47
  steps.success('Channel ${cfg.channel}');
42
48
  steps.success('Platform ${cfg.platform}');
43
49
 
44
- // PocketBase-specific: show binary + health endpoint status.
45
- if (cfg.provider == 'pocketbase') {
46
- await _checkPocketBase(cfg, steps);
47
- }
50
+ // Backend-specific pre-flight checks.
51
+ await _checkProvider(cfg, steps);
48
52
 
49
53
  try {
50
54
  final backend = requireBackend(cfg, override: backendOverride);
@@ -64,6 +68,123 @@ class DoctorCommand extends FlutterPatcherCommand {
64
68
  steps.summary();
65
69
  });
66
70
 
71
+ Future<void> _checkProvider(FlutterPatcherConfig cfg, Steps steps) async {
72
+ switch (cfg.provider) {
73
+ case 'supabase':
74
+ await _checkSupabase(cfg, steps);
75
+ case 'postgres':
76
+ await _checkPostgres(cfg, steps);
77
+ case 'cloudflare':
78
+ await _checkCloudflare(cfg, steps);
79
+ case 'aws':
80
+ await _checkAws(cfg, steps);
81
+ case 'pocketbase':
82
+ await _checkPocketBase(cfg, steps);
83
+ default:
84
+ steps.fail('Unknown provider: ${cfg.provider}');
85
+ }
86
+ }
87
+
88
+ Future<void> _checkSupabase(
89
+ FlutterPatcherConfig cfg,
90
+ Steps steps,
91
+ ) async {
92
+ final url = cfg.supabase.url;
93
+ if (url == null || url.isEmpty) {
94
+ steps.fail('SUPABASE_URL not set');
95
+ return;
96
+ }
97
+ try {
98
+ final res = await http.get(Uri.parse('$url/rest/v1/')).timeout(
99
+ const Duration(seconds: 5),
100
+ );
101
+ if (res.statusCode < 400) {
102
+ steps.success('Supabase reachable ($url)');
103
+ } else {
104
+ steps.fail('Supabase returned ${res.statusCode}');
105
+ }
106
+ } catch (e) {
107
+ steps.fail('Supabase unreachable — $e');
108
+ }
109
+ }
110
+
111
+ Future<void> _checkPostgres(
112
+ FlutterPatcherConfig cfg,
113
+ Steps steps,
114
+ ) async {
115
+ final host = cfg.postgres.host;
116
+ if (host == null || host.isEmpty) {
117
+ steps.fail('POSTGRES_HOST not set');
118
+ return;
119
+ }
120
+ final port = int.tryParse(cfg.postgres.port ?? '5432') ?? 5432;
121
+ try {
122
+ final socket = await Socket.connect(host, port).timeout(
123
+ const Duration(seconds: 5),
124
+ );
125
+ await socket.close();
126
+ steps.success('Postgres reachable ($host:$port)');
127
+ } catch (e) {
128
+ steps.fail('Postgres unreachable — $e');
129
+ }
130
+ }
131
+
132
+ Future<void> _checkCloudflare(
133
+ FlutterPatcherConfig cfg,
134
+ Steps steps,
135
+ ) async {
136
+ final accountId = cfg.cloudflare.accountId;
137
+ final apiToken = cfg.cloudflare.apiToken;
138
+ if (accountId == null || accountId.isEmpty) {
139
+ steps.fail('CLOUDFLARE_ACCOUNT_ID not set');
140
+ return;
141
+ }
142
+ if (apiToken == null || apiToken.isEmpty) {
143
+ steps.fail('CLOUDFLARE_API_TOKEN not set');
144
+ return;
145
+ }
146
+ try {
147
+ final res = await http
148
+ .get(
149
+ Uri.parse(
150
+ 'https://api.cloudflare.com/client/v4/accounts/$accountId',
151
+ ),
152
+ headers: {
153
+ 'Authorization': 'Bearer $apiToken',
154
+ 'Content-Type': 'application/json',
155
+ },
156
+ )
157
+ .timeout(const Duration(seconds: 5));
158
+ final body = jsonDecode(res.body) as Map<String, dynamic>;
159
+ if (body['success'] == true) {
160
+ steps.success('Cloudflare account reachable');
161
+ } else {
162
+ final errors = body['errors'] as List? ?? [];
163
+ final msg = errors.isNotEmpty ? errors.first['message'] : res.body;
164
+ steps.fail('Cloudflare API error: $msg');
165
+ }
166
+ } catch (e) {
167
+ steps.fail('Cloudflare unreachable — $e');
168
+ }
169
+ }
170
+
171
+ Future<void> _checkAws(
172
+ FlutterPatcherConfig cfg,
173
+ Steps steps,
174
+ ) async {
175
+ final bucket = cfg.aws.bucket;
176
+ if (bucket == null || bucket.isEmpty) {
177
+ steps.fail('AWS_BUCKET not set');
178
+ return;
179
+ }
180
+ final region = cfg.aws.region;
181
+ if (region == null || region.isEmpty) {
182
+ steps.fail('AWS_REGION not set');
183
+ return;
184
+ }
185
+ steps.success('AWS S3 bucket "$bucket" in $region');
186
+ }
187
+
67
188
  Future<void> _checkPocketBase(
68
189
  FlutterPatcherConfig cfg,
69
190
  Steps steps,
@@ -79,8 +200,8 @@ class DoctorCommand extends FlutterPatcherCommand {
79
200
  }
80
201
  final url = cfg.pocketbase.url;
81
202
  if (url != null && url.isNotEmpty) {
203
+ final client = PocketBaseClient(url);
82
204
  try {
83
- final client = PocketBaseClient(url);
84
205
  final ok = await client.health();
85
206
  if (ok) {
86
207
  steps.success('PocketBase reachable');
@@ -89,6 +210,8 @@ class DoctorCommand extends FlutterPatcherCommand {
89
210
  }
90
211
  } catch (e) {
91
212
  steps.fail('PocketBase unreachable — $e');
213
+ } finally {
214
+ client.close();
92
215
  }
93
216
  }
94
217
  }
@@ -25,6 +25,7 @@ class FingerprintCommand extends FlutterPatcherCommand {
25
25
  banner(name);
26
26
  final source = argResults!['source'] as String;
27
27
  final hash = generateFingerprint(source);
28
- box('fingerprint', [kv('source', source), kv('hash', hash)]);
28
+ step('source ${dim(source)}');
29
+ step('hash ${cyan(hash)}');
29
30
  });
30
31
  }
@@ -227,8 +227,8 @@ class InitCommand extends FlutterPatcherCommand {
227
227
  env['SUPABASE_SERVICE_ROLE_KEY'],
228
228
  ),
229
229
  anonKey: _p(
230
- 'Supabase publishable key',
231
- env['SUPABASE_PUBLISHABLE_KEY'],
230
+ 'Supabase anon key',
231
+ env['SUPABASE_ANON_KEY'],
232
232
  ),
233
233
  bucket: _p('Storage bucket', env['SUPABASE_BUCKET'] ?? 'bundles'),
234
234
  basePath: _p('Storage base path', env['SUPABASE_BASE_PATH']),
@@ -267,12 +267,14 @@ class InitCommand extends FlutterPatcherCommand {
267
267
  step('3. Provision the backend once: ${dim('flutter-ota migrate supabase')}');
268
268
  } else if (provider == 'postgres') {
269
269
  step('3. Provision the backend: ${dim('flutter-ota migrate postgres')}');
270
+ } else if (provider == 'cloudflare') {
271
+ step('3. Provision the backend: ${dim('flutter-ota migrate cloudflare')}');
272
+ } else if (provider == 'aws') {
273
+ step('3. Provision the backend: ${dim('flutter-ota migrate aws')}');
270
274
  } else if (provider == 'pocketbase') {
271
- step('3. Install PocketBase + provision the schema: '
272
- '${dim('flutter-ota pocketbase install')} then '
273
- '${dim('flutter-ota pocketbase serve')}.');
275
+ step('3. Provision the backend: ${dim('flutter-ota migrate pocketbase')}');
274
276
  }
275
- step('4. Build a patch: ${dim('flutter-ota build --name 1.0.1')} '
277
+ step('4. Build a patch: ${dim('flutter-ota build --version 1.0.1')} '
276
278
  'then ${dim('flutter-ota deploy')}');
277
279
  });
278
280
 
@@ -420,7 +422,7 @@ class InitCommand extends FlutterPatcherCommand {
420
422
  default:
421
423
  final c = cfg.supabase;
422
424
  entries['SUPABASE_URL'] = c.url ?? '';
423
- entries['SUPABASE_PUBLISHABLE_KEY'] = c.anonKey ?? '';
425
+ entries['SUPABASE_ANON_KEY'] = c.anonKey ?? '';
424
426
  entries['SUPABASE_BUCKET'] = c.bucket ?? 'bundles';
425
427
  entries['CHANNEL'] = cfg.channel;
426
428
  entries['APP_VERSION'] = '';
@@ -462,7 +464,14 @@ class InitCommand extends FlutterPatcherCommand {
462
464
  step('Added .env to .gitignore');
463
465
  }
464
466
 
465
- String _q(String? s) => s == null ? "''" : "'${s.replaceAll("'", "\\'")}'";
467
+ String _q(String? s) {
468
+ if (s == null) return "''";
469
+ final escaped = s
470
+ .replaceAll('\\', '\\\\')
471
+ .replaceAll('\$', '\\\$')
472
+ .replaceAll("'", "\\'");
473
+ return "'$escaped'";
474
+ }
466
475
 
467
476
  /// Build the body of `lib/flutter_ota_kit_setup.dart` for the selected backend.
468
477
  ///
@@ -489,7 +498,7 @@ class InitCommand extends FlutterPatcherCommand {
489
498
  password: const String.fromEnvironment('POSTGRES_PASSWORD', defaultValue: ''),
490
499
  servingBaseUrl: const String.fromEnvironment('POSTGRES_SERVING_BASE_URL', defaultValue: ${_q(c.servingBaseUrl ?? '')}),
491
500
  channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
492
- platform: Platform.android,
501
+ platform: Platform.${cfg.platform},
493
502
  updateStrategy: UpdateStrategy.appVersion,
494
503
  appVersion: $appVersionExpr,
495
504
  ));''';
@@ -506,7 +515,7 @@ class InitCommand extends FlutterPatcherCommand {
506
515
  secretAccessKey: const String.fromEnvironment('R2_SECRET_ACCESS_KEY', defaultValue: ''),
507
516
  basePath: const String.fromEnvironment('R2_BASE_PATH', defaultValue: ${_q(c.r2BasePath ?? '')}),
508
517
  channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
509
- platform: Platform.android,
518
+ platform: Platform.${cfg.platform},
510
519
  updateStrategy: UpdateStrategy.appVersion,
511
520
  appVersion: $appVersionExpr,
512
521
  ));''';
@@ -522,7 +531,7 @@ class InitCommand extends FlutterPatcherCommand {
522
531
  basePath: const String.fromEnvironment('AWS_BASE_PATH', defaultValue: ${_q(c.basePath ?? '')}),
523
532
  endpoint: const String.fromEnvironment('AWS_ENDPOINT', defaultValue: ${_q(c.endpoint ?? '')}),
524
533
  channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
525
- platform: Platform.android,
534
+ platform: Platform.${cfg.platform},
526
535
  updateStrategy: UpdateStrategy.appVersion,
527
536
  appVersion: $appVersionExpr,
528
537
  ));''';
@@ -537,7 +546,7 @@ class InitCommand extends FlutterPatcherCommand {
537
546
  bundlesCollection: const String.fromEnvironment('POCKETBASE_BUNDLES_COLLECTION', defaultValue: ${_q(c.bundlesCollection)}),
538
547
  bundlesBucket: const String.fromEnvironment('POCKETBASE_BUCKET', defaultValue: ${_q(c.bundlesBucket)}),
539
548
  channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
540
- platform: Platform.android,
549
+ platform: Platform.${cfg.platform},
541
550
  updateStrategy: UpdateStrategy.appVersion,
542
551
  appVersion: $appVersionExpr,
543
552
  ));''';
@@ -548,10 +557,10 @@ class InitCommand extends FlutterPatcherCommand {
548
557
  '''
549
558
  FlutterPatcher.configureSupabase(SupabaseUpdateConfig(
550
559
  supabaseUrl: const String.fromEnvironment('SUPABASE_URL', defaultValue: ${_q(c.url ?? '')}),
551
- anonKey: const String.fromEnvironment('SUPABASE_PUBLISHABLE_KEY', defaultValue: ''),
560
+ anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY', defaultValue: ''),
552
561
  bucket: const String.fromEnvironment('SUPABASE_BUCKET', defaultValue: ${_q(c.bucket ?? 'bundles')}),
553
562
  channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
554
- platform: Platform.android,
563
+ platform: Platform.${cfg.platform},
555
564
  updateStrategy: UpdateStrategy.appVersion,
556
565
  appVersion: $appVersionExpr,
557
566
  sdkVersion: const String.fromEnvironment('SDK_VERSION', defaultValue: '1.0.0'),
@@ -1,4 +1,5 @@
1
1
  import 'dart:convert';
2
+ import 'dart:io';
2
3
 
3
4
  import '../cli_base.dart';
4
5
  import '../config.dart';
@@ -53,5 +54,8 @@ class KeysCommand extends FlutterPatcherCommand {
53
54
  final file = configCandidates().first;
54
55
  file.parent.createSync(recursive: true);
55
56
  file.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(json));
57
+ if (!Platform.isWindows) {
58
+ Process.runSync('chmod', ['600', file.path]);
59
+ }
56
60
  }
57
61
  }