@_nazmiforreal/flutter-ota 0.1.12 → 0.1.14

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 (177) hide show
  1. package/README.md +1 -1
  2. package/bin/flutter-ota-linux-x64 +0 -0
  3. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +46 -76
  4. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +140 -166
  5. package/dart-src/packages/cli-tools/README.md +1 -1
  6. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +30 -17
  7. package/dart-src/packages/cli-tools/lib/src/backend.dart +30 -31
  8. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -1
  9. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +44 -43
  10. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +265 -239
  11. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +75 -66
  12. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +39 -35
  13. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +30 -27
  14. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +88 -72
  15. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +66 -29
  16. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +10 -6
  17. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +290 -181
  18. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +24 -23
  19. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +111 -97
  20. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +237 -0
  21. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +43 -34
  22. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +57 -53
  23. package/dart-src/packages/cli-tools/lib/src/config.dart +227 -93
  24. package/dart-src/packages/cli-tools/lib/src/operations.dart +9 -8
  25. package/dart-src/packages/cli-tools/lib/src/pack.dart +26 -25
  26. package/dart-src/packages/cli-tools/lib/src/pocketbase/data_bootstrap.dart +39 -0
  27. package/dart-src/packages/cli-tools/lib/src/pocketbase/hooks/bundles.pb.js +56 -0
  28. package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +189 -0
  29. package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +123 -0
  30. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +237 -0
  31. package/dart-src/packages/cli-tools/lib/src/runner.dart +36 -15
  32. package/dart-src/packages/cli-tools/lib/src/sign.dart +13 -5
  33. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +10 -13
  34. package/dart-src/packages/cli-tools/lib/src/util.dart +3 -1
  35. package/dart-src/packages/cli-tools/pubspec.lock +69 -116
  36. package/dart-src/packages/cli-tools/pubspec.yaml +25 -7
  37. package/dart-src/packages/cli-tools/test/backends_test.dart +62 -32
  38. package/dart-src/packages/cli-tools/test/build_pack_test.dart +4 -4
  39. package/dart-src/packages/cli-tools/test/cli_test.dart +89 -42
  40. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +8 -13
  41. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +23 -18
  42. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +197 -0
  43. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +70 -46
  44. package/dart-src/packages/cli-tools/test/pocketbase_installer_test.dart +72 -0
  45. package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +107 -0
  46. package/dart-src/packages/cli-tools/test/pocketbase_smoke_test.dart +59 -0
  47. package/dart-src/packages/core/CHANGELOG.md +9 -0
  48. package/dart-src/packages/core/README.md +2 -2
  49. package/dart-src/packages/core/example/lib/main.dart +69 -0
  50. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +1 -1
  51. package/dart-src/packages/core/lib/src/app_update_info.dart +17 -15
  52. package/dart-src/packages/core/lib/src/bundle.dart +102 -94
  53. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +1 -2
  54. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +15 -16
  55. package/dart-src/packages/core/lib/src/changed_asset.dart +24 -22
  56. package/dart-src/packages/core/lib/src/get_bundles_args.dart +13 -13
  57. package/dart-src/packages/core/lib/src/metadata.dart +9 -7
  58. package/dart-src/packages/core/lib/src/patch_info.dart +8 -7
  59. package/dart-src/packages/core/lib/src/platform.dart +4 -3
  60. package/dart-src/packages/core/lib/src/rollout.dart +14 -11
  61. package/dart-src/packages/core/lib/src/semver_range.dart +49 -39
  62. package/dart-src/packages/core/lib/src/semver_version.dart +8 -7
  63. package/dart-src/packages/core/lib/src/status.dart +4 -3
  64. package/dart-src/packages/core/lib/src/update_bundle_params.dart +7 -7
  65. package/dart-src/packages/core/lib/src/uuid.dart +1 -2
  66. package/dart-src/packages/core/pubspec.yaml +6 -7
  67. package/dart-src/packages/core/test/rollout_test.dart +15 -8
  68. package/dart-src/packages/core/test/semver_test.dart +5 -2
  69. package/dart-src/packages/core/test/types_test.dart +26 -24
  70. package/dart-src/packages/core/test/uuid_test.dart +3 -1
  71. package/dart-src/packages/core/tool/dbg.dart +1 -0
  72. package/dart-src/plugins/aws/CHANGELOG.md +15 -0
  73. package/dart-src/plugins/aws/example/lib/main.dart +60 -0
  74. package/dart-src/plugins/aws/lib/flutter_ota_kit_aws.dart +5 -1
  75. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +11 -16
  76. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +7 -2
  77. package/dart-src/plugins/aws/lib/src/aws_database.dart +14 -18
  78. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +23 -18
  79. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +5 -4
  80. package/dart-src/plugins/aws/lib/src/aws_storage.dart +2 -3
  81. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +10 -16
  82. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +6 -9
  83. package/dart-src/plugins/aws/pubspec.yaml +15 -10
  84. package/dart-src/plugins/aws/test/aws_database_test.dart +51 -57
  85. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +8 -13
  86. package/dart-src/plugins/cloudflare/CHANGELOG.md +15 -0
  87. package/dart-src/plugins/cloudflare/example/lib/main.dart +62 -0
  88. package/dart-src/plugins/cloudflare/lib/flutter_ota_kit_cloudflare.dart +4 -2
  89. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +3 -3
  90. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +3 -2
  91. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +24 -27
  92. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +22 -30
  93. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +1 -1
  94. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +21 -29
  95. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +7 -7
  96. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +7 -15
  97. package/dart-src/plugins/cloudflare/pubspec.yaml +14 -9
  98. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +46 -42
  99. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +100 -90
  100. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +23 -18
  101. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +8 -13
  102. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +1 -4
  103. package/dart-src/plugins/plugin-core/CHANGELOG.md +15 -1
  104. package/dart-src/plugins/plugin-core/example/lib/main.dart +61 -0
  105. package/dart-src/plugins/plugin-core/lib/flutter_ota_kit_plugin_core.dart +3 -7
  106. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +5 -5
  107. package/dart-src/plugins/plugin-core/lib/src/bundle_helpers.dart +23 -0
  108. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +16 -10
  109. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +31 -26
  110. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +5 -1
  111. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +1 -1
  112. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +78 -71
  113. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +56 -35
  114. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +10 -13
  115. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +30 -17
  116. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +3 -1
  117. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +5 -3
  118. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +2 -4
  119. package/dart-src/plugins/plugin-core/lib/src/storage_helpers.dart +18 -0
  120. package/dart-src/plugins/plugin-core/lib/src/types.dart +7 -6
  121. package/dart-src/plugins/plugin-core/pubspec.yaml +12 -9
  122. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +13 -9
  123. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +28 -15
  124. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +254 -122
  125. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +36 -32
  126. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +18 -15
  127. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +8 -3
  128. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +682 -693
  129. package/dart-src/plugins/plugin-core/test/helpers_test.dart +45 -21
  130. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +76 -13
  131. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +20 -9
  132. package/dart-src/plugins/pocketbase/CHANGELOG.md +8 -0
  133. package/dart-src/plugins/pocketbase/LICENSE +21 -0
  134. package/dart-src/plugins/pocketbase/README.md +54 -0
  135. package/dart-src/plugins/pocketbase/example/lib/main.dart +62 -0
  136. package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +28 -0
  137. package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +121 -0
  138. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +349 -0
  139. package/dart-src/plugins/pocketbase/lib/src/pocketbase_config.dart +61 -0
  140. package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +278 -0
  141. package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +255 -0
  142. package/dart-src/plugins/pocketbase/pubspec.yaml +26 -0
  143. package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +12 -0
  144. package/dart-src/plugins/postgres/CHANGELOG.md +16 -0
  145. package/dart-src/plugins/postgres/example/lib/main.dart +67 -0
  146. package/dart-src/plugins/postgres/lib/flutter_ota_kit_postgres.dart +3 -6
  147. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +32 -52
  148. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +3 -1
  149. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +28 -38
  150. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +16 -14
  151. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +26 -14
  152. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +27 -28
  153. package/dart-src/plugins/postgres/pubspec.yaml +14 -9
  154. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +62 -39
  155. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +3 -4
  156. package/dart-src/plugins/postgres/test/postgres_database_test.dart +102 -73
  157. package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +60 -0
  158. package/dart-src/plugins/supabase/CHANGELOG.md +20 -0
  159. package/dart-src/plugins/supabase/README.md +2 -2
  160. package/dart-src/plugins/supabase/example/lib/main.dart +63 -0
  161. package/dart-src/plugins/supabase/lib/flutter_ota_kit_supabase.dart +2 -0
  162. package/dart-src/plugins/supabase/lib/src/error_message.dart +11 -0
  163. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +34 -58
  164. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +3 -13
  165. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +34 -52
  166. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +38 -30
  167. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +21 -29
  168. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +7 -19
  169. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +18 -29
  170. package/dart-src/plugins/supabase/lib/src/types.dart +26 -26
  171. package/dart-src/plugins/supabase/pubspec.yaml +14 -9
  172. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +57 -57
  173. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +30 -27
  174. package/dart-src/plugins/supabase/test/supabase_database_test.dart +26 -35
  175. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +27 -34
  176. package/package.json +3 -3
  177. package/scripts/vendor.sh +1 -1
@@ -7,8 +7,12 @@ import '../ui/ui.dart';
7
7
  /// directly from the CLI (no need to open the provider dashboard).
8
8
  class StorageCommand extends FlutterPatcherCommand {
9
9
  StorageCommand({this.config, this.backendOverride}) {
10
- addSubcommand(StorageListCommand(config: config, backendOverride: backendOverride));
11
- addSubcommand(StorageDeleteCommand(config: config, backendOverride: backendOverride));
10
+ addSubcommand(
11
+ StorageListCommand(config: config, backendOverride: backendOverride),
12
+ );
13
+ addSubcommand(
14
+ StorageDeleteCommand(config: config, backendOverride: backendOverride),
15
+ );
12
16
  }
13
17
 
14
18
  final FlutterPatcherConfig? config;
@@ -41,31 +45,31 @@ class StorageListCommand extends FlutterPatcherCommand {
41
45
 
42
46
  @override
43
47
  Future<int> run() => runGuarded(() async {
44
- final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
45
- final backend = requireBackend(cfg, override: backendOverride);
46
- final prefix = argResults!['prefix'] as String?;
47
- banner('storage · list');
48
- final objects = await backend.storage.listObjects(
49
- prefix == null || prefix.isEmpty ? null : prefix,
50
- );
51
- if (objects.isEmpty) {
52
- step('(no objects)');
53
- return;
54
- }
55
- final lines = <String>[];
56
- for (final o in objects) {
57
- final size = o.size >= 1024 * 1024
58
- ? '${(o.size / (1024 * 1024)).toStringAsFixed(2)} MB'
59
- : o.size >= 1024
60
- ? '${(o.size / 1024).toStringAsFixed(1)} KB'
61
- : '${o.size} B';
62
- lines
63
- ..add(kv('key', cyan(o.key)))
64
- ..add(kv('size', size))
65
- ..add('');
66
- }
67
- box('${objects.length} objects', lines);
68
- });
48
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
49
+ final backend = requireBackend(cfg, override: backendOverride);
50
+ final prefix = argResults!['prefix'] as String?;
51
+ banner('storage · list');
52
+ final objects = await backend.storage.listObjects(
53
+ prefix == null || prefix.isEmpty ? null : prefix,
54
+ );
55
+ if (objects.isEmpty) {
56
+ step('(no objects)');
57
+ return;
58
+ }
59
+ final lines = <String>[];
60
+ for (final o in objects) {
61
+ final size = o.size >= 1024 * 1024
62
+ ? '${(o.size / (1024 * 1024)).toStringAsFixed(2)} MB'
63
+ : o.size >= 1024
64
+ ? '${(o.size / 1024).toStringAsFixed(1)} KB'
65
+ : '${o.size} B';
66
+ lines
67
+ ..add(kv('key', cyan(o.key)))
68
+ ..add(kv('size', size))
69
+ ..add('');
70
+ }
71
+ box('${objects.length} objects', lines);
72
+ });
69
73
  }
70
74
 
71
75
  class StorageDeleteCommand extends FlutterPatcherCommand {
@@ -88,30 +92,30 @@ class StorageDeleteCommand extends FlutterPatcherCommand {
88
92
 
89
93
  @override
90
94
  Future<int> run() => runGuarded(() async {
91
- final keys = (argResults!['key'] as List<String>?) ?? <String>[];
92
- final uri = argResults!['uri'] as String?;
93
- if (keys.isEmpty && (uri == null || uri.isEmpty)) {
94
- throw PackException(
95
- 'Usage: flutter-ota storage delete --key <key> [--key <key>...] '
96
- '| --uri <storageUri>',
97
- 64,
98
- );
99
- }
100
- final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
101
- final backend = requireBackend(cfg, override: backendOverride);
102
- banner('storage · delete');
103
- if (keys.isNotEmpty) {
104
- await spinner(
105
- () => backend.storage.deleteObjects(keys),
106
- 'Deleting ${keys.length} object(s)',
107
- done: 'Deleted',
108
- );
109
- } else {
110
- await spinner(
111
- () => backend.storage.delete(uri!),
112
- 'Deleting $uri',
113
- done: 'Deleted',
114
- );
115
- }
116
- });
95
+ final keys = (argResults!['key'] as List<String>?) ?? <String>[];
96
+ final uri = argResults!['uri'] as String?;
97
+ if (keys.isEmpty && (uri == null || uri.isEmpty)) {
98
+ throw PackException(
99
+ 'Usage: flutter-ota storage delete --key <key> [--key <key>...] '
100
+ '| --uri <storageUri>',
101
+ 64,
102
+ );
103
+ }
104
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
105
+ final backend = requireBackend(cfg, override: backendOverride);
106
+ banner('storage · delete');
107
+ if (keys.isNotEmpty) {
108
+ await spinner(
109
+ () => backend.storage.deleteObjects(keys),
110
+ 'Deleting ${keys.length} object(s)',
111
+ done: 'Deleted',
112
+ );
113
+ } else {
114
+ await spinner(
115
+ () => backend.storage.delete(uri!),
116
+ 'Deleting $uri',
117
+ done: 'Deleted',
118
+ );
119
+ }
120
+ });
117
121
  }
@@ -4,6 +4,7 @@ import 'dart:io';
4
4
  import 'package:flutter_ota_kit_aws/flutter_ota_kit_aws.dart';
5
5
  import 'package:postgres/postgres.dart';
6
6
  import 'package:flutter_ota_kit_cloudflare/flutter_ota_kit_cloudflare.dart';
7
+ import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
7
8
  import 'package:flutter_ota_kit_postgres/flutter_ota_kit_postgres.dart';
8
9
 
9
10
  import 'package:flutter_ota_kit_supabase/flutter_ota_kit_supabase.dart';
@@ -22,6 +23,7 @@ class FlutterPatcherConfig {
22
23
  this.postgres = const PostgresConfigJson(),
23
24
  this.cloudflare = const CloudflareConfigJson(),
24
25
  this.aws = const AwsConfigJson(),
26
+ this.pocketbase = const PocketBaseConfigJson(),
25
27
  this.channel = 'production',
26
28
  this.platform = 'android',
27
29
  this.source = './dist',
@@ -43,6 +45,9 @@ class FlutterPatcherConfig {
43
45
  aws: AwsConfigJson.fromJson(
44
46
  (json['aws'] as Map?)?.cast<String, dynamic>() ?? {},
45
47
  ),
48
+ pocketbase: PocketBaseConfigJson.fromJson(
49
+ (json['pocketbase'] as Map?)?.cast<String, dynamic>() ?? {},
50
+ ),
46
51
  channel: json['channel'] as String? ?? 'production',
47
52
  platform: json['platform'] as String? ?? 'android',
48
53
  source: json['source'] as String? ?? './dist',
@@ -54,22 +59,24 @@ class FlutterPatcherConfig {
54
59
  final PostgresConfigJson postgres;
55
60
  final CloudflareConfigJson cloudflare;
56
61
  final AwsConfigJson aws;
62
+ final PocketBaseConfigJson pocketbase;
57
63
  final String channel;
58
64
  final String platform;
59
65
  final String source;
60
66
  final String? publicKey;
61
67
 
62
68
  Map<String, dynamic> toJson() => {
63
- 'provider': provider,
64
- 'supabase': supabase.toJson(),
65
- 'postgres': postgres.toJson(),
66
- 'cloudflare': cloudflare.toJson(),
67
- 'aws': aws.toJson(),
68
- 'channel': channel,
69
- 'platform': platform,
70
- 'source': source,
71
- if (publicKey != null) 'publicKey': publicKey,
72
- };
69
+ 'provider': provider,
70
+ 'supabase': supabase.toJson(),
71
+ 'postgres': postgres.toJson(),
72
+ 'cloudflare': cloudflare.toJson(),
73
+ 'aws': aws.toJson(),
74
+ 'pocketbase': pocketbase.toJson(),
75
+ 'channel': channel,
76
+ 'platform': platform,
77
+ 'source': source,
78
+ if (publicKey != null) 'publicKey': publicKey,
79
+ };
73
80
 
74
81
  FlutterPatcherConfig copyWith({
75
82
  String? provider,
@@ -77,22 +84,23 @@ class FlutterPatcherConfig {
77
84
  PostgresConfigJson? postgres,
78
85
  CloudflareConfigJson? cloudflare,
79
86
  AwsConfigJson? aws,
87
+ PocketBaseConfigJson? pocketbase,
80
88
  String? channel,
81
89
  String? platform,
82
90
  String? source,
83
91
  String? publicKey,
84
- }) =>
85
- FlutterPatcherConfig(
86
- provider: provider ?? this.provider,
87
- supabase: supabase ?? this.supabase,
88
- postgres: postgres ?? this.postgres,
89
- cloudflare: cloudflare ?? this.cloudflare,
90
- aws: aws ?? this.aws,
91
- channel: channel ?? this.channel,
92
- platform: platform ?? this.platform,
93
- source: source ?? this.source,
94
- publicKey: publicKey ?? this.publicKey,
95
- );
92
+ }) => FlutterPatcherConfig(
93
+ provider: provider ?? this.provider,
94
+ supabase: supabase ?? this.supabase,
95
+ postgres: postgres ?? this.postgres,
96
+ cloudflare: cloudflare ?? this.cloudflare,
97
+ aws: aws ?? this.aws,
98
+ pocketbase: pocketbase ?? this.pocketbase,
99
+ channel: channel ?? this.channel,
100
+ platform: platform ?? this.platform,
101
+ source: source ?? this.source,
102
+ publicKey: publicKey ?? this.publicKey,
103
+ );
96
104
  }
97
105
 
98
106
  /// Supabase section of [FlutterPatcherConfig].
@@ -127,24 +135,25 @@ class SupabaseConfigJson {
127
135
  final String? databaseUrl;
128
136
 
129
137
  Map<String, dynamic> toJson() => {
130
- if (url != null) 'url': url,
131
- if (serviceRoleKey != null) 'serviceRoleKey': serviceRoleKey,
132
- if (anonKey != null) 'anonKey': anonKey,
133
- if (bucket != null) 'bucket': bucket,
134
- if (basePath != null) 'basePath': basePath,
135
- if (managementKey != null) 'managementKey': managementKey,
136
- if (databaseUrl != null) 'databaseUrl': databaseUrl,
137
- };
138
+ if (url != null) 'url': url,
139
+ if (serviceRoleKey != null) 'serviceRoleKey': serviceRoleKey,
140
+ if (anonKey != null) 'anonKey': anonKey,
141
+ if (bucket != null) 'bucket': bucket,
142
+ if (basePath != null) 'basePath': basePath,
143
+ if (managementKey != null) 'managementKey': managementKey,
144
+ if (databaseUrl != null) 'databaseUrl': databaseUrl,
145
+ };
138
146
  }
139
147
 
140
148
  /// Search order for a config file on disk. Both the project config and the
141
149
  /// global config live under a `.flutter_ota_kit/` directory so all CLI state is
142
150
  /// grouped in one place (and can be git-ignored together).
143
151
  List<File> configCandidates() {
144
- final cwd = File(p.join(Directory.current.path, '.flutter_ota_kit', 'config.json'));
145
- final home = Platform.environment['HOME'] ??
146
- Platform.environment['USERPROFILE'] ??
147
- '';
152
+ final cwd = File(
153
+ p.join(Directory.current.path, '.flutter_ota_kit', 'config.json'),
154
+ );
155
+ final home =
156
+ Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'] ?? '';
148
157
  final global = File(p.join(home, '.flutter_ota_kit', 'config.json'));
149
158
  return [cwd, global];
150
159
  }
@@ -222,7 +231,8 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
222
231
  }) {
223
232
  final env = Platform.environment;
224
233
  final resolvedUrl = url ?? env['SUPABASE_URL'] ?? config.supabase.url;
225
- final resolvedService = serviceRoleKey ??
234
+ final resolvedService =
235
+ serviceRoleKey ??
226
236
  env['SUPABASE_SERVICE_ROLE_KEY'] ??
227
237
  config.supabase.serviceRoleKey;
228
238
  final resolvedAnon =
@@ -269,11 +279,12 @@ SupabaseStorageConfig resolveSupabaseStorageConfig(
269
279
  clientFactory: db.clientFactory,
270
280
  bucketName: db.supabaseServiceRoleKey != null || db.supabaseAnonKey != null
271
281
  ? (bucket ??
272
- Platform.environment['SUPABASE_BUCKET'] ??
273
- config.supabase.bucket ??
274
- 'bundles')
282
+ Platform.environment['SUPABASE_BUCKET'] ??
283
+ config.supabase.bucket ??
284
+ 'bundles')
275
285
  : (bucket ?? config.supabase.bucket ?? 'bundles'),
276
- basePath: basePath ??
286
+ basePath:
287
+ basePath ??
277
288
  Platform.environment['SUPABASE_BASE_PATH'] ??
278
289
  config.supabase.basePath,
279
290
  );
@@ -318,15 +329,15 @@ class PostgresConfigJson {
318
329
  final String? servingBaseUrl;
319
330
 
320
331
  Map<String, dynamic> toJson() => {
321
- if (host != null) 'host': host,
322
- if (port != null) 'port': port,
323
- if (database != null) 'database': database,
324
- if (username != null) 'username': username,
325
- if (password != null) 'password': password,
326
- if (sslMode != null) 'sslMode': sslMode,
327
- if (basePath != null) 'basePath': basePath,
328
- if (servingBaseUrl != null) 'servingBaseUrl': servingBaseUrl,
329
- };
332
+ if (host != null) 'host': host,
333
+ if (port != null) 'port': port,
334
+ if (database != null) 'database': database,
335
+ if (username != null) 'username': username,
336
+ if (password != null) 'password': password,
337
+ if (sslMode != null) 'sslMode': sslMode,
338
+ if (basePath != null) 'basePath': basePath,
339
+ if (servingBaseUrl != null) 'servingBaseUrl': servingBaseUrl,
340
+ };
330
341
  }
331
342
 
332
343
  SslMode? _parseSslMode(String? value) {
@@ -358,13 +369,12 @@ PostgresConfig resolvePostgresDatabaseConfig(
358
369
  final resolvedHost = host ?? env['POSTGRES_HOST'] ?? config.postgres.host;
359
370
  final resolvedDatabase =
360
371
  database ?? env['POSTGRES_DB'] ?? config.postgres.database;
361
- final resolvedUser = username ?? env['POSTGRES_USER'] ?? config.postgres.username;
372
+ final resolvedUser =
373
+ username ?? env['POSTGRES_USER'] ?? config.postgres.username;
362
374
  final resolvedPassword =
363
375
  password ?? env['POSTGRES_PASSWORD'] ?? config.postgres.password;
364
- final resolvedPort = port ??
365
- env['POSTGRES_PORT'] ??
366
- config.postgres.port ??
367
- '5432';
376
+ final resolvedPort =
377
+ port ?? env['POSTGRES_PORT'] ?? config.postgres.port ?? '5432';
368
378
  final resolvedSsl =
369
379
  sslMode ?? env['POSTGRES_SSLMODE'] ?? config.postgres.sslMode;
370
380
 
@@ -411,10 +421,12 @@ PostgresStorageConfig resolvePostgresStorageConfig(
411
421
  );
412
422
  return PostgresStorageConfig(
413
423
  db: db,
414
- basePath: basePath ??
424
+ basePath:
425
+ basePath ??
415
426
  Platform.environment['POSTGRES_BASE_PATH'] ??
416
427
  config.postgres.basePath,
417
- servingBaseUrl: servingBaseUrl ??
428
+ servingBaseUrl:
429
+ servingBaseUrl ??
418
430
  Platform.environment['POSTGRES_SERVING_BASE_URL'] ??
419
431
  config.postgres.servingBaseUrl,
420
432
  );
@@ -456,14 +468,14 @@ class CloudflareConfigJson {
456
468
  final String? r2BasePath;
457
469
 
458
470
  Map<String, dynamic> toJson() => {
459
- if (accountId != null) 'accountId': accountId,
460
- if (d1DatabaseId != null) 'd1DatabaseId': d1DatabaseId,
461
- if (apiToken != null) 'apiToken': apiToken,
462
- if (r2Bucket != null) 'r2Bucket': r2Bucket,
463
- if (r2AccessKeyId != null) 'r2AccessKeyId': r2AccessKeyId,
464
- if (r2SecretAccessKey != null) 'r2SecretAccessKey': r2SecretAccessKey,
465
- if (r2BasePath != null) 'r2BasePath': r2BasePath,
466
- };
471
+ if (accountId != null) 'accountId': accountId,
472
+ if (d1DatabaseId != null) 'd1DatabaseId': d1DatabaseId,
473
+ if (apiToken != null) 'apiToken': apiToken,
474
+ if (r2Bucket != null) 'r2Bucket': r2Bucket,
475
+ if (r2AccessKeyId != null) 'r2AccessKeyId': r2AccessKeyId,
476
+ if (r2SecretAccessKey != null) 'r2SecretAccessKey': r2SecretAccessKey,
477
+ if (r2BasePath != null) 'r2BasePath': r2BasePath,
478
+ };
467
479
  }
468
480
 
469
481
  /// Resolve the Cloudflare D1 database config.
@@ -477,15 +489,14 @@ D1DatabaseConfig resolveCloudflareDatabaseConfig(
477
489
  final env = Platform.environment;
478
490
  final resolvedAccount =
479
491
  accountId ?? env['CLOUDFLARE_ACCOUNT_ID'] ?? config.cloudflare.accountId;
480
- final resolvedDb = d1DatabaseId ??
492
+ final resolvedDb =
493
+ d1DatabaseId ??
481
494
  env['CLOUDFLARE_D1_DATABASE_ID'] ??
482
495
  config.cloudflare.d1DatabaseId;
483
496
  final resolvedToken =
484
497
  apiToken ?? env['CLOUDFLARE_API_TOKEN'] ?? config.cloudflare.apiToken;
485
498
 
486
- if (resolvedAccount == null ||
487
- resolvedDb == null ||
488
- resolvedToken == null) {
499
+ if (resolvedAccount == null || resolvedDb == null || resolvedToken == null) {
489
500
  throw StateError(
490
501
  'Cloudflare requires accountId, d1DatabaseId and apiToken. Set them via '
491
502
  'flags, CLOUDFLARE_* env vars, or `flutter_ota_kit config set cloudflare.*`.',
@@ -511,16 +522,17 @@ R2S3StorageConfig resolveCloudflareStorageConfig(
511
522
  R2S3ClientFactory? clientFactory,
512
523
  }) {
513
524
  final env = Platform.environment;
514
- final resolvedAccount = accountId ??
525
+ final resolvedAccount =
526
+ accountId ??
515
527
  env['CLOUDFLARE_ACCOUNT_ID'] ??
516
528
  env['R2_ACCOUNT_ID'] ??
517
529
  config.cloudflare.accountId;
518
530
  final resolvedBucket =
519
531
  bucket ?? env['R2_BUCKET'] ?? config.cloudflare.r2Bucket ?? 'bundles';
520
- final resolvedKey = accessKeyId ??
521
- env['R2_ACCESS_KEY_ID'] ??
522
- config.cloudflare.r2AccessKeyId;
523
- final resolvedSecret = secretAccessKey ??
532
+ final resolvedKey =
533
+ accessKeyId ?? env['R2_ACCESS_KEY_ID'] ?? config.cloudflare.r2AccessKeyId;
534
+ final resolvedSecret =
535
+ secretAccessKey ??
524
536
  env['R2_SECRET_ACCESS_KEY'] ??
525
537
  config.cloudflare.r2SecretAccessKey;
526
538
 
@@ -560,14 +572,14 @@ class AwsConfigJson {
560
572
  });
561
573
 
562
574
  factory AwsConfigJson.fromJson(Map<String, dynamic> json) => AwsConfigJson(
563
- bucket: json['bucket'] as String?,
564
- region: json['region'] as String?,
565
- accessKeyId: json['accessKeyId'] as String?,
566
- secretAccessKey: json['secretAccessKey'] as String?,
567
- endpoint: json['endpoint'] as String?,
568
- basePath: json['basePath'] as String?,
569
- sessionToken: json['sessionToken'] as String?,
570
- );
575
+ bucket: json['bucket'] as String?,
576
+ region: json['region'] as String?,
577
+ accessKeyId: json['accessKeyId'] as String?,
578
+ secretAccessKey: json['secretAccessKey'] as String?,
579
+ endpoint: json['endpoint'] as String?,
580
+ basePath: json['basePath'] as String?,
581
+ sessionToken: json['sessionToken'] as String?,
582
+ );
571
583
 
572
584
  final String? bucket;
573
585
  final String? region;
@@ -578,14 +590,14 @@ class AwsConfigJson {
578
590
  final String? sessionToken;
579
591
 
580
592
  Map<String, dynamic> toJson() => {
581
- if (bucket != null) 'bucket': bucket,
582
- if (region != null) 'region': region,
583
- if (accessKeyId != null) 'accessKeyId': accessKeyId,
584
- if (secretAccessKey != null) 'secretAccessKey': secretAccessKey,
585
- if (endpoint != null) 'endpoint': endpoint,
586
- if (basePath != null) 'basePath': basePath,
587
- if (sessionToken != null) 'sessionToken': sessionToken,
588
- };
593
+ if (bucket != null) 'bucket': bucket,
594
+ if (region != null) 'region': region,
595
+ if (accessKeyId != null) 'accessKeyId': accessKeyId,
596
+ if (secretAccessKey != null) 'secretAccessKey': secretAccessKey,
597
+ if (endpoint != null) 'endpoint': endpoint,
598
+ if (basePath != null) 'basePath': basePath,
599
+ if (sessionToken != null) 'sessionToken': sessionToken,
600
+ };
589
601
  }
590
602
 
591
603
  /// Resolve the AWS S3 database config.
@@ -599,7 +611,8 @@ S3DatabaseConfig resolveAwsDatabaseConfig(
599
611
  String? endpoint,
600
612
  String? sessionToken,
601
613
  AwsS3ClientLike Function(S3DatabaseConfig config)? clientFactory,
602
- AwsCloudFrontClientLike Function(S3DatabaseConfig config)? cloudfrontClientFactory,
614
+ AwsCloudFrontClientLike Function(S3DatabaseConfig config)?
615
+ cloudfrontClientFactory,
603
616
  }) {
604
617
  final env = Platform.environment;
605
618
  final resolvedBucket = bucket ?? env['AWS_BUCKET'] ?? config.aws.bucket;
@@ -607,7 +620,9 @@ S3DatabaseConfig resolveAwsDatabaseConfig(
607
620
  final resolvedKey =
608
621
  accessKeyId ?? env['AWS_ACCESS_KEY_ID'] ?? config.aws.accessKeyId;
609
622
  final resolvedSecret =
610
- secretAccessKey ?? env['AWS_SECRET_ACCESS_KEY'] ?? config.aws.secretAccessKey;
623
+ secretAccessKey ??
624
+ env['AWS_SECRET_ACCESS_KEY'] ??
625
+ config.aws.secretAccessKey;
611
626
 
612
627
  if (resolvedBucket == null ||
613
628
  resolvedRegion == null ||
@@ -626,7 +641,8 @@ S3DatabaseConfig resolveAwsDatabaseConfig(
626
641
  secretAccessKey: resolvedSecret,
627
642
  basePath: basePath ?? env['AWS_BASE_PATH'] ?? config.aws.basePath,
628
643
  endpoint: endpoint ?? env['AWS_ENDPOINT'] ?? config.aws.endpoint,
629
- sessionToken: sessionToken ?? env['AWS_SESSION_TOKEN'] ?? config.aws.sessionToken,
644
+ sessionToken:
645
+ sessionToken ?? env['AWS_SESSION_TOKEN'] ?? config.aws.sessionToken,
630
646
  clientFactory: clientFactory ?? defaultAwsS3DatabaseClientFactory,
631
647
  cloudfrontClientFactory:
632
648
  cloudfrontClientFactory ?? defaultAwsCloudFrontClientFactory,
@@ -651,7 +667,9 @@ AwsS3StorageConfig resolveAwsStorageConfig(
651
667
  final resolvedKey =
652
668
  accessKeyId ?? env['AWS_ACCESS_KEY_ID'] ?? config.aws.accessKeyId;
653
669
  final resolvedSecret =
654
- secretAccessKey ?? env['AWS_SECRET_ACCESS_KEY'] ?? config.aws.secretAccessKey;
670
+ secretAccessKey ??
671
+ env['AWS_SECRET_ACCESS_KEY'] ??
672
+ config.aws.secretAccessKey;
655
673
 
656
674
  if (resolvedBucket == null ||
657
675
  resolvedRegion == null ||
@@ -670,7 +688,123 @@ AwsS3StorageConfig resolveAwsStorageConfig(
670
688
  secretAccessKey: resolvedSecret,
671
689
  basePath: basePath ?? env['AWS_BASE_PATH'] ?? config.aws.basePath,
672
690
  endpoint: endpoint ?? env['AWS_ENDPOINT'] ?? config.aws.endpoint,
673
- sessionToken: sessionToken ?? env['AWS_SESSION_TOKEN'] ?? config.aws.sessionToken,
691
+ sessionToken:
692
+ sessionToken ?? env['AWS_SESSION_TOKEN'] ?? config.aws.sessionToken,
674
693
  clientFactory: clientFactory ?? defaultAwsS3ClientFactory,
675
694
  );
676
695
  }
696
+
697
+ // ---------------------------------------------------------------------------
698
+ // PocketBase backend config
699
+ // ---------------------------------------------------------------------------
700
+
701
+ /// PocketBase section of [FlutterPatcherConfig].
702
+ class PocketBaseConfigJson {
703
+ const PocketBaseConfigJson({
704
+ this.url,
705
+ this.adminEmail,
706
+ this.adminPassword,
707
+ this.bundlesCollection = 'bundles',
708
+ this.channelsCollection = 'channels',
709
+ this.bundlesBucket = 'bundles',
710
+ });
711
+
712
+ factory PocketBaseConfigJson.fromJson(Map<String, dynamic> json) =>
713
+ PocketBaseConfigJson(
714
+ url: json['url'] as String?,
715
+ adminEmail: json['adminEmail'] as String?,
716
+ adminPassword: json['adminPassword'] as String?,
717
+ bundlesCollection: (json['bundlesCollection'] as String?) ?? 'bundles',
718
+ channelsCollection:
719
+ (json['channelsCollection'] as String?) ?? 'channels',
720
+ bundlesBucket: (json['bundlesBucket'] as String?) ?? 'bundles',
721
+ );
722
+
723
+ final String? url;
724
+ final String? adminEmail;
725
+ final String? adminPassword;
726
+ final String bundlesCollection;
727
+ final String channelsCollection;
728
+ final String bundlesBucket;
729
+
730
+ Map<String, dynamic> toJson() => {
731
+ if (url != null) 'url': url,
732
+ if (adminEmail != null) 'adminEmail': adminEmail,
733
+ if (adminPassword != null) 'adminPassword': adminPassword,
734
+ 'bundlesCollection': bundlesCollection,
735
+ 'channelsCollection': channelsCollection,
736
+ 'bundlesBucket': bundlesBucket,
737
+ };
738
+ }
739
+
740
+ /// Resolve the PocketBase database config (flag > env > json precedence).
741
+ PocketBaseDatabaseConfig resolvePocketBaseDatabaseConfig(
742
+ FlutterPatcherConfig config, {
743
+ String? url,
744
+ String? adminEmail,
745
+ String? adminPassword,
746
+ PocketBaseClientFactory? clientFactory,
747
+ }) {
748
+ final env = Platform.environment;
749
+ final resolvedUrl = url ?? env['POCKETBASE_URL'] ?? config.pocketbase.url;
750
+ final resolvedEmail =
751
+ adminEmail ??
752
+ env['POCKETBASE_ADMIN_EMAIL'] ??
753
+ config.pocketbase.adminEmail;
754
+ final resolvedPassword =
755
+ adminPassword ??
756
+ env['POCKETBASE_ADMIN_PASSWORD'] ??
757
+ config.pocketbase.adminPassword;
758
+
759
+ if (resolvedUrl == null || resolvedUrl.isEmpty) {
760
+ throw StateError(
761
+ 'PocketBase URL is required. Set --pb-url, POCKETBASE_URL, or '
762
+ '`flutter_ota_kit config set pocketbase.url <url>`.',
763
+ );
764
+ }
765
+ if (resolvedEmail == null || resolvedPassword == null) {
766
+ throw StateError(
767
+ 'PocketBase admin credentials are required. Set --pb-email, '
768
+ '--pb-password, POCKETBASE_ADMIN_EMAIL/PASSWORD, or '
769
+ '`flutter_ota_kit config set pocketbase.*`.',
770
+ );
771
+ }
772
+
773
+ return PocketBaseDatabaseConfig(
774
+ url: resolvedUrl.replaceAll(RegExp(r'/$'), ''),
775
+ adminEmail: resolvedEmail,
776
+ adminPassword: resolvedPassword,
777
+ bundlesCollection: config.pocketbase.bundlesCollection,
778
+ channelsCollection: config.pocketbase.channelsCollection,
779
+ bundlesBucket: config.pocketbase.bundlesBucket,
780
+ clientFactory: clientFactory,
781
+ );
782
+ }
783
+
784
+ /// Resolve the PocketBase storage config (shares the database connection).
785
+ PocketBaseStorageConfig resolvePocketBaseStorageConfig(
786
+ FlutterPatcherConfig config, {
787
+ String? url,
788
+ String? adminEmail,
789
+ String? adminPassword,
790
+ String? bundlesBucket,
791
+ String? basePath,
792
+ PocketBaseClientFactory? clientFactory,
793
+ }) {
794
+ final db = resolvePocketBaseDatabaseConfig(
795
+ config,
796
+ url: url,
797
+ adminEmail: adminEmail,
798
+ adminPassword: adminPassword,
799
+ clientFactory: clientFactory,
800
+ );
801
+ return PocketBaseStorageConfig(
802
+ url: db.url,
803
+ adminEmail: db.adminEmail,
804
+ adminPassword: db.adminPassword,
805
+ bundlesCollection: db.bundlesCollection,
806
+ bundlesBucket: bundlesBucket ?? db.bundlesBucket,
807
+ basePath: basePath,
808
+ clientFactory: clientFactory,
809
+ );
810
+ }
@@ -65,7 +65,9 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
65
65
  // Otherwise, if the source directory contains a `patch.zip`, use that. As a
66
66
  // fallback, zip the source directory (for pre-built bundle layouts).
67
67
  final sourceFile = File(opts.source);
68
- final bool sourceIsFile = sourceFile.existsSync() && sourceFile.statSync().type == FileSystemEntityType.file;
68
+ final bool sourceIsFile =
69
+ sourceFile.existsSync() &&
70
+ sourceFile.statSync().type == FileSystemEntityType.file;
69
71
  final String zipPath;
70
72
  final bool shouldDelete;
71
73
  if (sourceIsFile && opts.source.endsWith('.zip')) {
@@ -113,7 +115,8 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
113
115
  targetAppVersion: opts.targetAppVersion,
114
116
  fingerprintHash: opts.fingerprintHash,
115
117
  metadata:
116
- opts.metadata ?? (signature != null ? BundleMetadata(signature: signature) : null),
118
+ opts.metadata ??
119
+ (signature != null ? BundleMetadata(signature: signature) : null),
117
120
  rolloutCohortCount: defaultRolloutCohortCount,
118
121
  );
119
122
 
@@ -140,7 +143,9 @@ Future<Paginated<List<Bundle>>> listBundles(
140
143
  DatabaseBundleQueryOptions(
141
144
  where: DatabaseBundleQueryWhere(
142
145
  channel: opts.channel,
143
- platform: opts.platform != null ? Platform.fromValue(opts.platform!) : null,
146
+ platform: opts.platform != null
147
+ ? Platform.fromValue(opts.platform!)
148
+ : null,
144
149
  enabled: opts.enabled,
145
150
  ),
146
151
  limit: opts.limit,
@@ -160,11 +165,7 @@ Future<void> deleteBundle(Backend backend, String id) async {
160
165
  }
161
166
 
162
167
  /// Promote a bundle to a channel (enable + assign channel).
163
- Future<void> promoteBundle(
164
- Backend backend,
165
- String id,
166
- String channel,
167
- ) async {
168
+ Future<void> promoteBundle(Backend backend, String id, String channel) async {
168
169
  final existing = await backend.db.getBundleById(id);
169
170
  if (existing == null) {
170
171
  throw StateError('Bundle "$id" not found.');