@_nazmiforreal/flutter-ota 0.1.12 → 0.1.13

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 (176) 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/README.md +2 -2
  48. package/dart-src/packages/core/example/lib/main.dart +69 -0
  49. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +1 -1
  50. package/dart-src/packages/core/lib/src/app_update_info.dart +17 -15
  51. package/dart-src/packages/core/lib/src/bundle.dart +102 -94
  52. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +1 -2
  53. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +15 -16
  54. package/dart-src/packages/core/lib/src/changed_asset.dart +24 -22
  55. package/dart-src/packages/core/lib/src/get_bundles_args.dart +13 -13
  56. package/dart-src/packages/core/lib/src/metadata.dart +9 -7
  57. package/dart-src/packages/core/lib/src/patch_info.dart +8 -7
  58. package/dart-src/packages/core/lib/src/platform.dart +4 -3
  59. package/dart-src/packages/core/lib/src/rollout.dart +14 -11
  60. package/dart-src/packages/core/lib/src/semver_range.dart +49 -39
  61. package/dart-src/packages/core/lib/src/semver_version.dart +8 -7
  62. package/dart-src/packages/core/lib/src/status.dart +4 -3
  63. package/dart-src/packages/core/lib/src/update_bundle_params.dart +7 -7
  64. package/dart-src/packages/core/lib/src/uuid.dart +1 -2
  65. package/dart-src/packages/core/pubspec.yaml +6 -7
  66. package/dart-src/packages/core/test/rollout_test.dart +15 -8
  67. package/dart-src/packages/core/test/semver_test.dart +5 -2
  68. package/dart-src/packages/core/test/types_test.dart +26 -24
  69. package/dart-src/packages/core/test/uuid_test.dart +3 -1
  70. package/dart-src/packages/core/tool/dbg.dart +1 -0
  71. package/dart-src/plugins/aws/CHANGELOG.md +6 -0
  72. package/dart-src/plugins/aws/example/lib/main.dart +60 -0
  73. package/dart-src/plugins/aws/lib/flutter_ota_kit_aws.dart +5 -1
  74. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +11 -16
  75. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +7 -2
  76. package/dart-src/plugins/aws/lib/src/aws_database.dart +14 -18
  77. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +23 -18
  78. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +5 -4
  79. package/dart-src/plugins/aws/lib/src/aws_storage.dart +2 -3
  80. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +10 -16
  81. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +6 -9
  82. package/dart-src/plugins/aws/pubspec.yaml +15 -10
  83. package/dart-src/plugins/aws/test/aws_database_test.dart +51 -57
  84. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +8 -13
  85. package/dart-src/plugins/cloudflare/CHANGELOG.md +7 -0
  86. package/dart-src/plugins/cloudflare/example/lib/main.dart +62 -0
  87. package/dart-src/plugins/cloudflare/lib/flutter_ota_kit_cloudflare.dart +4 -2
  88. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +3 -3
  89. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +3 -2
  90. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +24 -27
  91. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +22 -30
  92. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +1 -1
  93. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +21 -29
  94. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +7 -7
  95. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +7 -15
  96. package/dart-src/plugins/cloudflare/pubspec.yaml +14 -9
  97. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +46 -42
  98. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +100 -90
  99. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +23 -18
  100. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +8 -13
  101. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +1 -4
  102. package/dart-src/plugins/plugin-core/CHANGELOG.md +16 -0
  103. package/dart-src/plugins/plugin-core/example/lib/main.dart +61 -0
  104. package/dart-src/plugins/plugin-core/lib/flutter_ota_kit_plugin_core.dart +3 -7
  105. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +5 -5
  106. package/dart-src/plugins/plugin-core/lib/src/bundle_helpers.dart +23 -0
  107. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +16 -10
  108. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +31 -26
  109. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +5 -1
  110. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +1 -1
  111. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +78 -71
  112. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +56 -35
  113. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +10 -13
  114. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +30 -17
  115. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +3 -1
  116. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +5 -3
  117. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +2 -4
  118. package/dart-src/plugins/plugin-core/lib/src/storage_helpers.dart +18 -0
  119. package/dart-src/plugins/plugin-core/lib/src/types.dart +7 -6
  120. package/dart-src/plugins/plugin-core/pubspec.yaml +12 -9
  121. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +13 -9
  122. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +28 -15
  123. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +254 -122
  124. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +36 -32
  125. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +18 -15
  126. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +8 -3
  127. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +682 -693
  128. package/dart-src/plugins/plugin-core/test/helpers_test.dart +45 -21
  129. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +76 -13
  130. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +20 -9
  131. package/dart-src/plugins/pocketbase/CHANGELOG.md +8 -0
  132. package/dart-src/plugins/pocketbase/LICENSE +21 -0
  133. package/dart-src/plugins/pocketbase/README.md +54 -0
  134. package/dart-src/plugins/pocketbase/example/lib/main.dart +62 -0
  135. package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +28 -0
  136. package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +121 -0
  137. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +349 -0
  138. package/dart-src/plugins/pocketbase/lib/src/pocketbase_config.dart +61 -0
  139. package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +278 -0
  140. package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +255 -0
  141. package/dart-src/plugins/pocketbase/pubspec.yaml +26 -0
  142. package/dart-src/plugins/pocketbase/test/pb_uri_test.dart +12 -0
  143. package/dart-src/plugins/postgres/CHANGELOG.md +6 -0
  144. package/dart-src/plugins/postgres/example/lib/main.dart +67 -0
  145. package/dart-src/plugins/postgres/lib/flutter_ota_kit_postgres.dart +3 -6
  146. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +32 -52
  147. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +3 -1
  148. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +28 -38
  149. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +16 -14
  150. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +26 -14
  151. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +27 -28
  152. package/dart-src/plugins/postgres/pubspec.yaml +14 -9
  153. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +62 -39
  154. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +3 -4
  155. package/dart-src/plugins/postgres/test/postgres_database_test.dart +102 -73
  156. package/dart-src/plugins/postgres/test/postgres_storage_runtime_test.dart +60 -0
  157. package/dart-src/plugins/supabase/CHANGELOG.md +17 -0
  158. package/dart-src/plugins/supabase/README.md +2 -2
  159. package/dart-src/plugins/supabase/example/lib/main.dart +63 -0
  160. package/dart-src/plugins/supabase/lib/flutter_ota_kit_supabase.dart +2 -0
  161. package/dart-src/plugins/supabase/lib/src/error_message.dart +11 -0
  162. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +34 -58
  163. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +3 -13
  164. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +34 -52
  165. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +38 -30
  166. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +21 -29
  167. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +7 -19
  168. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +18 -29
  169. package/dart-src/plugins/supabase/lib/src/types.dart +26 -26
  170. package/dart-src/plugins/supabase/pubspec.yaml +14 -9
  171. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +57 -57
  172. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +30 -27
  173. package/dart-src/plugins/supabase/test/supabase_database_test.dart +26 -35
  174. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +27 -34
  175. package/package.json +3 -3
  176. package/scripts/vendor.sh +1 -1
@@ -16,13 +16,17 @@ class FingerprintCommand extends FlutterPatcherCommand {
16
16
 
17
17
  @override
18
18
  ArgParser get argParser => ArgParser()
19
- ..addOption('source',
20
- abbr: 's', defaultsTo: './dist', help: 'Directory to fingerprint.');
19
+ ..addOption(
20
+ 'source',
21
+ abbr: 's',
22
+ defaultsTo: './dist',
23
+ help: 'Directory to fingerprint.',
24
+ );
21
25
 
22
26
  @override
23
27
  Future<int> run() => runGuarded(() async {
24
- final source = argResults!['source'] as String;
25
- final hash = generateFingerprint(source);
26
- stdout.writeln(hash);
27
- });
28
+ final source = argResults!['source'] as String;
29
+ final hash = generateFingerprint(source);
30
+ stdout.writeln(hash);
31
+ });
28
32
  }
@@ -29,18 +29,34 @@ class InitCommand extends FlutterPatcherCommand {
29
29
  @override
30
30
  String get description =>
31
31
  'Scaffold flutter_ota_kit integration files + .flutter_ota_kit config for the current '
32
- 'Flutter project. Usage: flutter_ota_kit init <supabase|postgres|cloudflare|aws>';
32
+ 'Flutter project. Usage: flutter_ota_kit init <supabase|postgres|cloudflare|aws|pocketbase>';
33
33
 
34
34
  @override
35
35
  ArgParser get argParser => ArgParser()
36
- ..addOption('provider',
37
- abbr: 'p',
38
- defaultsTo: 'supabase',
39
- help: 'Backend provider (also accepted as the first positional argument).')
40
- ..addOption('channel', abbr: 'c', defaultsTo: 'production', help: 'Default channel.')
36
+ ..addOption(
37
+ 'provider',
38
+ abbr: 'p',
39
+ defaultsTo: 'supabase',
40
+ help:
41
+ 'Backend provider (also accepted as the first positional argument).',
42
+ )
43
+ ..addOption(
44
+ 'channel',
45
+ abbr: 'c',
46
+ defaultsTo: 'production',
47
+ help: 'Default channel.',
48
+ )
41
49
  ..addOption('platform', defaultsTo: 'android', help: 'Default platform.')
42
- ..addOption('source', abbr: 's', defaultsTo: './dist', help: 'Default deploy source.')
43
- ..addFlag('global', help: 'Write to the global ~/.flutter_ota_kit config (no scaffolding).')
50
+ ..addOption(
51
+ 'source',
52
+ abbr: 's',
53
+ defaultsTo: './dist',
54
+ help: 'Default deploy source.',
55
+ )
56
+ ..addFlag(
57
+ 'global',
58
+ help: 'Write to the global ~/.flutter_ota_kit config (no scaffolding).',
59
+ )
44
60
  ..addFlag('force', abbr: 'f', help: 'Overwrite an existing config.');
45
61
 
46
62
  String? _p(String label, String? current) {
@@ -58,6 +74,7 @@ class InitCommand extends FlutterPatcherCommand {
58
74
  'postgres',
59
75
  'cloudflare',
60
76
  'aws',
77
+ 'pocketbase',
61
78
  ];
62
79
 
63
80
  /// Interactive backend picker. Returns the chosen provider, defaulting to
@@ -80,168 +97,208 @@ class InitCommand extends FlutterPatcherCommand {
80
97
 
81
98
  @override
82
99
  Future<int> run() => runGuarded(() async {
83
- // A positional backend (`flutter_ota_kit init supabase`) takes precedence
84
- // over the --provider flag.
85
- final positional = argResults!.rest;
86
- final requested = positional.isNotEmpty
87
- ? positional.first
88
- : (argResults!['provider'] as String);
89
- final provider =
90
- stdin.hasTerminal ? _chooseProvider(requested) : requested;
91
- if (!_providers.contains(provider)) {
92
- throw PackException(
93
- 'Unknown provider "$provider". Choose one of: '
94
- '${_providers.join(', ')}.',
95
- 64,
96
- );
97
- }
98
-
99
- final global = argResults!['global'] as bool;
100
- final file = global
101
- ? configCandidates().last
102
- : configCandidates().first;
103
- if (file.existsSync() && !(argResults!['force'] as bool)) {
104
- throw PackException(
105
- 'Config already exists at ${file.path}. Use --force to overwrite.',
106
- 64,
107
- );
108
- }
109
-
110
- final channel =
111
- argResults!['channel'] as String? ?? 'production';
112
- final platform =
113
- argResults!['platform'] as String? ?? 'android';
114
- final source = argResults!['source'] as String? ?? './dist';
115
- final env = Platform.environment;
116
-
117
- banner('init');
118
-
119
- late FlutterPatcherConfig cfg;
120
- switch (provider) {
121
- case 'postgres':
122
- cfg = FlutterPatcherConfig(
123
- provider: provider,
124
- channel: channel,
125
- platform: platform,
126
- source: source,
127
- supabase: const SupabaseConfigJson(),
128
- postgres: PostgresConfigJson(
129
- host: _p('Postgres host', env['POSTGRES_HOST']),
130
- port: _p('Postgres port', env['POSTGRES_PORT'] ?? '5432'),
131
- database:
132
- _p('Postgres database', env['POSTGRES_DB'] ?? 'postgres'),
133
- username: _p('Postgres username', env['POSTGRES_USER']),
134
- password: _p('Postgres password', env['POSTGRES_PASSWORD']),
135
- sslMode: _p('Postgres sslmode', env['POSTGRES_SSLMODE']),
136
- basePath: _p('Storage base path', env['POSTGRES_BASE_PATH']),
137
- servingBaseUrl: _p(
138
- 'Serving base url', env['POSTGRES_SERVING_BASE_URL']),
139
- ),
140
- );
141
- case 'cloudflare':
142
- cfg = FlutterPatcherConfig(
143
- provider: provider,
144
- channel: channel,
145
- platform: platform,
146
- source: source,
147
- supabase: const SupabaseConfigJson(),
148
- cloudflare: CloudflareConfigJson(
149
- accountId:
150
- _p('Cloudflare account id', env['CLOUDFLARE_ACCOUNT_ID']),
151
- d1DatabaseId: _p(
152
- 'Cloudflare D1 database id', env['CLOUDFLARE_D1_DATABASE_ID']),
153
- apiToken:
154
- _p('Cloudflare API token', env['CLOUDFLARE_API_TOKEN']),
155
- r2Bucket: _p('R2 bucket', env['R2_BUCKET'] ?? 'bundles'),
156
- r2AccessKeyId:
157
- _p('R2 access key id', env['R2_ACCESS_KEY_ID']),
158
- r2SecretAccessKey:
159
- _p('R2 secret access key', env['R2_SECRET_ACCESS_KEY']),
160
- r2BasePath: _p('R2 base path', env['R2_BASE_PATH']),
161
- ),
162
- );
163
- case 'aws':
164
- cfg = FlutterPatcherConfig(
165
- provider: provider,
166
- channel: channel,
167
- platform: platform,
168
- source: source,
169
- supabase: const SupabaseConfigJson(),
170
- aws: AwsConfigJson(
171
- bucket: _p('AWS bucket', env['AWS_BUCKET']),
172
- region: _p('AWS region', env['AWS_REGION']),
173
- accessKeyId:
174
- _p('AWS access key id', env['AWS_ACCESS_KEY_ID']),
175
- secretAccessKey:
176
- _p('AWS secret access key', env['AWS_SECRET_ACCESS_KEY']),
177
- endpoint: _p('AWS endpoint', env['AWS_ENDPOINT']),
178
- basePath: _p('AWS base path', env['AWS_BASE_PATH']),
179
- sessionToken:
180
- _p('AWS session token', env['AWS_SESSION_TOKEN']),
181
- ),
182
- );
183
- case 'supabase':
184
- default:
185
- cfg = FlutterPatcherConfig(
186
- provider: 'supabase',
187
- channel: channel,
188
- platform: platform,
189
- source: source,
190
- supabase: SupabaseConfigJson(
191
- url: _p('Supabase URL', env['SUPABASE_URL']),
192
- serviceRoleKey: _p(
193
- 'Supabase service role key', env['SUPABASE_SERVICE_ROLE_KEY']),
194
- anonKey: _p('Supabase publishable key', env['SUPABASE_PUBLISHABLE_KEY']),
195
- bucket: _p('Storage bucket', env['SUPABASE_BUCKET'] ?? 'bundles'),
196
- basePath: _p('Storage base path', env['SUPABASE_BASE_PATH']),
197
- managementKey: _p(
198
- 'Supabase Management API key (optional, used by `migrate`)',
199
- env['SUPABASE_MANAGEMENT_KEY']),
200
- databaseUrl: _p(
201
- 'Postgres DATABASE_URL (optional, used by `migrate`)',
202
- env['SUPABASE_DATABASE_URL']),
203
- ),
204
- );
205
- }
206
-
207
- saveConfig(cfg, global: global);
208
- step('Wrote ${file.path}');
209
-
210
- if (global) {
211
- stdout.writeln(
212
- ' ${yellow('⚠')} Secrets are stored in plaintext under ~/.flutter_ota_kit. '
213
- 'Restrict permissions and use a secrets manager in production.',
214
- );
215
- return;
216
- }
217
-
218
- // Scaffold the host project integration files.
219
- await _scaffoldProject(cfg, provider);
220
-
221
- stdout.writeln();
222
- stdout.writeln(cyan('Next steps:'));
223
- stdout.writeln(' 1. ${dim('flutter pub get')}');
224
- stdout.writeln(
225
- ' 2. In lib/main.dart call ${green('await setupFlutterOta();')} '
226
- 'right after WidgetsFlutterBinding.ensureInitialized() (before runApp).');
227
- stdout.writeln(
228
- ' A `.env` scaffold was written — put secrets there, then build with '
229
- '`--dart-define-from-file=.env` (environment overrides config).');
230
- if (provider == 'supabase') {
231
- stdout.writeln(
232
- ' 3. Provision the backend once: ${dim('flutter_ota_kit migrate supabase')}');
233
- } else if (provider == 'postgres') {
234
- stdout.writeln(
235
- ' 3. Provision the backend: ${dim('flutter_ota_kit migrate postgres')}');
236
- }
237
- stdout.writeln(
238
- ' 4. Build a patch: ${dim('flutter_ota_kit build --name 1.0.1')} '
239
- 'then ${dim('flutter_ota_kit deploy')}');
240
- });
100
+ // A positional backend (`flutter_ota_kit init supabase`) takes precedence
101
+ // over the --provider flag.
102
+ final positional = argResults!.rest;
103
+ final requested = positional.isNotEmpty
104
+ ? positional.first
105
+ : (argResults!['provider'] as String);
106
+ final provider = stdin.hasTerminal ? _chooseProvider(requested) : requested;
107
+ if (!_providers.contains(provider)) {
108
+ throw PackException(
109
+ 'Unknown provider "$provider". Choose one of: '
110
+ '${_providers.join(', ')}.',
111
+ 64,
112
+ );
113
+ }
114
+
115
+ final global = argResults!['global'] as bool;
116
+ final file = global ? configCandidates().last : configCandidates().first;
117
+ if (file.existsSync() && !(argResults!['force'] as bool)) {
118
+ throw PackException(
119
+ 'Config already exists at ${file.path}. Use --force to overwrite.',
120
+ 64,
121
+ );
122
+ }
123
+
124
+ final channel = argResults!['channel'] as String? ?? 'production';
125
+ final platform = argResults!['platform'] as String? ?? 'android';
126
+ final source = argResults!['source'] as String? ?? './dist';
127
+ final env = Platform.environment;
128
+
129
+ banner('init');
130
+
131
+ late FlutterPatcherConfig cfg;
132
+ switch (provider) {
133
+ case 'postgres':
134
+ cfg = FlutterPatcherConfig(
135
+ provider: provider,
136
+ channel: channel,
137
+ platform: platform,
138
+ source: source,
139
+ supabase: const SupabaseConfigJson(),
140
+ postgres: PostgresConfigJson(
141
+ host: _p('Postgres host', env['POSTGRES_HOST']),
142
+ port: _p('Postgres port', env['POSTGRES_PORT'] ?? '5432'),
143
+ database: _p('Postgres database', env['POSTGRES_DB'] ?? 'postgres'),
144
+ username: _p('Postgres username', env['POSTGRES_USER']),
145
+ password: _p('Postgres password', env['POSTGRES_PASSWORD']),
146
+ sslMode: _p('Postgres sslmode', env['POSTGRES_SSLMODE']),
147
+ basePath: _p('Storage base path', env['POSTGRES_BASE_PATH']),
148
+ servingBaseUrl: _p(
149
+ 'Serving base url',
150
+ env['POSTGRES_SERVING_BASE_URL'],
151
+ ),
152
+ ),
153
+ );
154
+ case 'cloudflare':
155
+ cfg = FlutterPatcherConfig(
156
+ provider: provider,
157
+ channel: channel,
158
+ platform: platform,
159
+ source: source,
160
+ supabase: const SupabaseConfigJson(),
161
+ cloudflare: CloudflareConfigJson(
162
+ accountId: _p(
163
+ 'Cloudflare account id',
164
+ env['CLOUDFLARE_ACCOUNT_ID'],
165
+ ),
166
+ d1DatabaseId: _p(
167
+ 'Cloudflare D1 database id',
168
+ env['CLOUDFLARE_D1_DATABASE_ID'],
169
+ ),
170
+ apiToken: _p('Cloudflare API token', env['CLOUDFLARE_API_TOKEN']),
171
+ r2Bucket: _p('R2 bucket', env['R2_BUCKET'] ?? 'bundles'),
172
+ r2AccessKeyId: _p('R2 access key id', env['R2_ACCESS_KEY_ID']),
173
+ r2SecretAccessKey: _p(
174
+ 'R2 secret access key',
175
+ env['R2_SECRET_ACCESS_KEY'],
176
+ ),
177
+ r2BasePath: _p('R2 base path', env['R2_BASE_PATH']),
178
+ ),
179
+ );
180
+ case 'aws':
181
+ cfg = FlutterPatcherConfig(
182
+ provider: provider,
183
+ channel: channel,
184
+ platform: platform,
185
+ source: source,
186
+ supabase: const SupabaseConfigJson(),
187
+ aws: AwsConfigJson(
188
+ bucket: _p('AWS bucket', env['AWS_BUCKET']),
189
+ region: _p('AWS region', env['AWS_REGION']),
190
+ accessKeyId: _p('AWS access key id', env['AWS_ACCESS_KEY_ID']),
191
+ secretAccessKey: _p(
192
+ 'AWS secret access key',
193
+ env['AWS_SECRET_ACCESS_KEY'],
194
+ ),
195
+ endpoint: _p('AWS endpoint', env['AWS_ENDPOINT']),
196
+ basePath: _p('AWS base path', env['AWS_BASE_PATH']),
197
+ sessionToken: _p('AWS session token', env['AWS_SESSION_TOKEN']),
198
+ ),
199
+ );
200
+ case 'pocketbase':
201
+ cfg = FlutterPatcherConfig(
202
+ provider: provider,
203
+ channel: channel,
204
+ platform: platform,
205
+ source: source,
206
+ supabase: const SupabaseConfigJson(),
207
+ pocketbase: PocketBaseConfigJson(
208
+ url: _p('PocketBase URL', env['POCKETBASE_URL']),
209
+ adminEmail: _p(
210
+ 'PocketBase admin email',
211
+ env['POCKETBASE_ADMIN_EMAIL'],
212
+ ),
213
+ adminPassword: _p(
214
+ 'PocketBase admin password',
215
+ env['POCKETBASE_ADMIN_PASSWORD'],
216
+ ),
217
+ ),
218
+ );
219
+ case 'supabase':
220
+ default:
221
+ cfg = FlutterPatcherConfig(
222
+ provider: 'supabase',
223
+ channel: channel,
224
+ platform: platform,
225
+ source: source,
226
+ supabase: SupabaseConfigJson(
227
+ url: _p('Supabase URL', env['SUPABASE_URL']),
228
+ serviceRoleKey: _p(
229
+ 'Supabase service role key',
230
+ env['SUPABASE_SERVICE_ROLE_KEY'],
231
+ ),
232
+ anonKey: _p(
233
+ 'Supabase publishable key',
234
+ env['SUPABASE_PUBLISHABLE_KEY'],
235
+ ),
236
+ bucket: _p('Storage bucket', env['SUPABASE_BUCKET'] ?? 'bundles'),
237
+ basePath: _p('Storage base path', env['SUPABASE_BASE_PATH']),
238
+ managementKey: _p(
239
+ 'Supabase Management API key (optional, used by `migrate`)',
240
+ env['SUPABASE_MANAGEMENT_KEY'],
241
+ ),
242
+ databaseUrl: _p(
243
+ 'Postgres DATABASE_URL (optional, used by `migrate`)',
244
+ env['SUPABASE_DATABASE_URL'],
245
+ ),
246
+ ),
247
+ );
248
+ }
249
+
250
+ saveConfig(cfg, global: global);
251
+ step('Wrote ${file.path}');
252
+
253
+ if (global) {
254
+ stdout.writeln(
255
+ ' ${yellow('⚠')} Secrets are stored in plaintext under ~/.flutter_ota_kit. '
256
+ 'Restrict permissions and use a secrets manager in production.',
257
+ );
258
+ return;
259
+ }
260
+
261
+ // Scaffold the host project integration files.
262
+ await _scaffoldProject(cfg, provider);
263
+
264
+ stdout.writeln();
265
+ stdout.writeln(cyan('Next steps:'));
266
+ stdout.writeln(' 1. ${dim('flutter pub get')}');
267
+ stdout.writeln(
268
+ ' 2. In lib/main.dart call ${green('await setupFlutterOta();')} '
269
+ 'right after WidgetsFlutterBinding.ensureInitialized() (before runApp).',
270
+ );
271
+ stdout.writeln(
272
+ ' A `.env` scaffold was written — put secrets there, then build with '
273
+ '`--dart-define-from-file=.env` (environment overrides config).',
274
+ );
275
+ if (provider == 'supabase') {
276
+ stdout.writeln(
277
+ ' 3. Provision the backend once: ${dim('flutter_ota_kit migrate supabase')}',
278
+ );
279
+ } else if (provider == 'postgres') {
280
+ stdout.writeln(
281
+ ' 3. Provision the backend: ${dim('flutter_ota_kit migrate postgres')}',
282
+ );
283
+ } else if (provider == 'pocketbase') {
284
+ stdout.writeln(
285
+ ' 3. Install PocketBase + provision the schema: '
286
+ '${dim('flutter_ota_kit pocketbase install')} then '
287
+ '${dim('flutter_ota_kit pocketbase serve')}.',
288
+ );
289
+ }
290
+ stdout.writeln(
291
+ ' 4. Build a patch: ${dim('flutter_ota_kit build --name 1.0.1')} '
292
+ 'then ${dim('flutter_ota_kit deploy')}',
293
+ );
294
+ });
241
295
 
242
296
  /// Generate the integration files (pubspec dep, manifest permission, setup dart
243
297
  /// file, .gitignore entry) inside the current directory.
244
- Future<void> _scaffoldProject(FlutterPatcherConfig cfg, String provider) async {
298
+ Future<void> _scaffoldProject(
299
+ FlutterPatcherConfig cfg,
300
+ String provider,
301
+ ) async {
245
302
  _addPubDependency();
246
303
  _addInternetPermission();
247
304
  await _writeSetupFile(cfg, provider);
@@ -252,7 +309,9 @@ class InitCommand extends FlutterPatcherCommand {
252
309
  void _addPubDependency() {
253
310
  final pubspec = File('pubspec.yaml');
254
311
  if (!pubspec.existsSync()) {
255
- stdout.writeln(yellow(' ⚠ pubspec.yaml not found; skipping dependency injection.'));
312
+ stdout.writeln(
313
+ yellow(' ⚠ pubspec.yaml not found; skipping dependency injection.'),
314
+ );
256
315
  return;
257
316
  }
258
317
  var content = pubspec.readAsStringSync();
@@ -264,7 +323,11 @@ class InitCommand extends FlutterPatcherCommand {
264
323
  final marker = '\ndependencies:';
265
324
  final idx = content.indexOf(marker);
266
325
  if (idx == -1) {
267
- stdout.writeln(yellow(' ⚠ could not find a `dependencies:` block; add `flutter_ota_kit` manually.'));
326
+ stdout.writeln(
327
+ yellow(
328
+ ' ⚠ could not find a `dependencies:` block; add `flutter_ota_kit` manually.',
329
+ ),
330
+ );
268
331
  return;
269
332
  }
270
333
  final insertAt = idx + marker.length;
@@ -276,7 +339,11 @@ class InitCommand extends FlutterPatcherCommand {
276
339
  void _addInternetPermission() {
277
340
  final mf = File('android/app/src/main/AndroidManifest.xml');
278
341
  if (!mf.existsSync()) {
279
- stdout.writeln(yellow(' ⚠ AndroidManifest.xml not found; skipping INTERNET permission.'));
342
+ stdout.writeln(
343
+ yellow(
344
+ ' ⚠ AndroidManifest.xml not found; skipping INTERNET permission.',
345
+ ),
346
+ );
280
347
  return;
281
348
  }
282
349
  var content = mf.readAsStringSync();
@@ -286,7 +353,11 @@ class InitCommand extends FlutterPatcherCommand {
286
353
  }
287
354
  final match = RegExp(r'<manifest[^>]*>').firstMatch(content);
288
355
  if (match == null) {
289
- stdout.writeln(yellow(' ⚠ could not locate <manifest> tag; add INTERNET permission manually.'));
356
+ stdout.writeln(
357
+ yellow(
358
+ ' ⚠ could not locate <manifest> tag; add INTERNET permission manually.',
359
+ ),
360
+ );
290
361
  return;
291
362
  }
292
363
  final insertAt = match.end;
@@ -296,10 +367,15 @@ class InitCommand extends FlutterPatcherCommand {
296
367
  '\n <uses-permission android:name="android.permission.INTERNET" />',
297
368
  );
298
369
  mf.writeAsStringSync(content);
299
- step('Added INTERNET permission to android/app/src/main/AndroidManifest.xml');
370
+ step(
371
+ 'Added INTERNET permission to android/app/src/main/AndroidManifest.xml',
372
+ );
300
373
  }
301
374
 
302
- Future<void> _writeSetupFile(FlutterPatcherConfig cfg, String provider) async {
375
+ Future<void> _writeSetupFile(
376
+ FlutterPatcherConfig cfg,
377
+ String provider,
378
+ ) async {
303
379
  final dir = Directory('lib');
304
380
  if (!dir.existsSync()) dir.createSync(recursive: true);
305
381
  final file = File('lib/flutter_ota_kit_setup.dart');
@@ -314,7 +390,9 @@ class InitCommand extends FlutterPatcherCommand {
314
390
  step('.gitignore already ignores .flutter_ota_kit/');
315
391
  return;
316
392
  }
317
- final next = content.isEmpty ? '.flutter_ota_kit/\n' : '$content\n.flutter_ota_kit/\n';
393
+ final next = content.isEmpty
394
+ ? '.flutter_ota_kit/\n'
395
+ : '$content\n.flutter_ota_kit/\n';
318
396
  gi.writeAsStringSync(next);
319
397
  step('Added .flutter_ota_kit/ to .gitignore');
320
398
  }
@@ -360,6 +438,15 @@ class InitCommand extends FlutterPatcherCommand {
360
438
  entries['AWS_SESSION_TOKEN'] = c.sessionToken ?? '';
361
439
  entries['CHANNEL'] = cfg.channel;
362
440
  entries['APP_VERSION'] = '';
441
+ case 'pocketbase':
442
+ final c = cfg.pocketbase;
443
+ entries['POCKETBASE_URL'] = c.url ?? '';
444
+ entries['POCKETBASE_ADMIN_EMAIL'] = c.adminEmail ?? '';
445
+ entries['POCKETBASE_ADMIN_PASSWORD'] = c.adminPassword ?? '';
446
+ entries['POCKETBASE_BUNDLES_COLLECTION'] = c.bundlesCollection;
447
+ entries['POCKETBASE_BUCKET'] = c.bundlesBucket;
448
+ entries['CHANNEL'] = cfg.channel;
449
+ entries['APP_VERSION'] = '';
363
450
  case 'supabase':
364
451
  default:
365
452
  final c = cfg.supabase;
@@ -375,7 +462,9 @@ class InitCommand extends FlutterPatcherCommand {
375
462
  buffer.writeln('# Generated by `flutter-ota init $provider`.');
376
463
  buffer.writeln('# Fill in (or override) values, then build with:');
377
464
  buffer.writeln('# flutter run --dart-define-from-file=.env');
378
- buffer.writeln('# Environment overrides the project config and any defaults.');
465
+ buffer.writeln(
466
+ '# Environment overrides the project config and any defaults.',
467
+ );
379
468
  for (final e in entries.entries) {
380
469
  buffer.writeln('${e.key}=${e.value}');
381
470
  }
@@ -392,8 +481,9 @@ class InitCommand extends FlutterPatcherCommand {
392
481
 
393
482
  void _ensureEnvGitignored() {
394
483
  final gi = File('.gitignore');
395
- final lines =
396
- gi.existsSync() ? gi.readAsStringSync().split('\n') : const <String>[];
484
+ final lines = gi.existsSync()
485
+ ? gi.readAsStringSync().split('\n')
486
+ : const <String>[];
397
487
  if (lines.contains('.env')) {
398
488
  step('.gitignore already ignores .env');
399
489
  return;
@@ -420,7 +510,8 @@ class InitCommand extends FlutterPatcherCommand {
420
510
  switch (provider) {
421
511
  case 'postgres':
422
512
  final c = cfg.postgres;
423
- configure = '''
513
+ configure =
514
+ '''
424
515
  FlutterPatcher.configurePostgres(PostgresUpdateConfig(
425
516
  host: const String.fromEnvironment('POSTGRES_HOST', defaultValue: ${_q(c.host ?? '')}),
426
517
  port: int.tryParse(const String.fromEnvironment('POSTGRES_PORT', defaultValue: ${_q(c.port ?? '5432')})) ?? 5432,
@@ -435,7 +526,8 @@ class InitCommand extends FlutterPatcherCommand {
435
526
  ));''';
436
527
  case 'cloudflare':
437
528
  final c = cfg.cloudflare;
438
- configure = '''
529
+ configure =
530
+ '''
439
531
  FlutterPatcher.configureCloudflare(CloudflareUpdateConfig(
440
532
  accountId: const String.fromEnvironment('CLOUDFLARE_ACCOUNT_ID', defaultValue: ${_q(c.accountId ?? '')}),
441
533
  databaseId: const String.fromEnvironment('CLOUDFLARE_D1_DATABASE_ID', defaultValue: ${_q(c.d1DatabaseId ?? '')}),
@@ -451,7 +543,8 @@ class InitCommand extends FlutterPatcherCommand {
451
543
  ));''';
452
544
  case 'aws':
453
545
  final c = cfg.aws;
454
- configure = '''
546
+ configure =
547
+ '''
455
548
  FlutterPatcher.configureAws(AwsUpdateConfig(
456
549
  bucketName: const String.fromEnvironment('AWS_BUCKET', defaultValue: ${_q(c.bucket ?? '')}),
457
550
  region: const String.fromEnvironment('AWS_REGION', defaultValue: ${_q(c.region ?? '')}),
@@ -463,11 +556,27 @@ class InitCommand extends FlutterPatcherCommand {
463
556
  platform: Platform.android,
464
557
  updateStrategy: UpdateStrategy.appVersion,
465
558
  appVersion: $appVersionExpr,
559
+ ));''';
560
+ case 'pocketbase':
561
+ final c = cfg.pocketbase;
562
+ configure =
563
+ '''
564
+ FlutterPatcher.configurePocketBase(PocketBaseUpdateConfig(
565
+ url: const String.fromEnvironment('POCKETBASE_URL', defaultValue: ${_q(c.url ?? '')}),
566
+ adminEmail: const String.fromEnvironment('POCKETBASE_ADMIN_EMAIL', defaultValue: ''),
567
+ adminPassword: const String.fromEnvironment('POCKETBASE_ADMIN_PASSWORD', defaultValue: ''),
568
+ bundlesCollection: const String.fromEnvironment('POCKETBASE_BUNDLES_COLLECTION', defaultValue: ${_q(c.bundlesCollection)}),
569
+ bundlesBucket: const String.fromEnvironment('POCKETBASE_BUCKET', defaultValue: ${_q(c.bundlesBucket)}),
570
+ channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
571
+ platform: Platform.android,
572
+ updateStrategy: UpdateStrategy.appVersion,
573
+ appVersion: $appVersionExpr,
466
574
  ));''';
467
575
  case 'supabase':
468
576
  default:
469
577
  final c = cfg.supabase;
470
- configure = '''
578
+ configure =
579
+ '''
471
580
  FlutterPatcher.configureSupabase(SupabaseUpdateConfig(
472
581
  supabaseUrl: const String.fromEnvironment('SUPABASE_URL', defaultValue: ${_q(c.url ?? '')}),
473
582
  anonKey: const String.fromEnvironment('SUPABASE_PUBLISHABLE_KEY', defaultValue: ''),