@_nazmiforreal/flutter-ota 0.1.11 → 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 +299 -190
  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 +2 -2
  176. package/scripts/vendor.sh +1 -1
@@ -0,0 +1,237 @@
1
+ /// Installs the flutter_ota_kit PocketBase schema (collections, indexes,
2
+ /// default admin) on a running PB instance.
3
+ ///
4
+ /// PocketBase is initialized with:
5
+ /// - `bundles` collection (the OTA bundle metadata)
6
+ /// - `channels` collection (release channels with a current_bundle pointer)
7
+ /// - `audit_log` collection (immutable event log)
8
+ /// - `admins` is left to PB's built-in bootstrapping
9
+ /// (set PB_ADMIN_EMAIL + PB_ADMIN_PASSWORD env vars on first start)
10
+ ///
11
+ /// Idempotent: re-running the installer is a no-op when collections exist.
12
+ library;
13
+
14
+ import 'dart:convert';
15
+
16
+ import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
17
+ import 'package:http/http.dart' as http;
18
+
19
+ const _kBundlesCollection = r'''
20
+ {
21
+ "name": "bundles",
22
+ "type": "base",
23
+ "listRule": null,
24
+ "viewRule": null,
25
+ "createRule": null,
26
+ "updateRule": null,
27
+ "deleteRule": null,
28
+ "fields": [
29
+ {"name": "channel", "type": "text", "required": true, "options": {"min": 1, "max": 64}},
30
+ {"name": "enabled", "type": "bool"},
31
+ {"name": "platform", "type": "text", "required": true, "options": {"min": 1, "max": 16}},
32
+ {"name": "should_force_update", "type": "bool"},
33
+ {"name": "file_hash", "type": "text", "required": true},
34
+ {"name": "storage_uri", "type": "text"},
35
+ {"name": "rollout_cohort_count", "type": "number", "options": {"min": 0, "max": 1000}},
36
+ {"name": "message", "type": "text", "options": {"max": 1024}},
37
+ {"name": "fingerprint_hash", "type": "text", "options": {"max": 128}},
38
+ {"name": "target_app_version", "type": "text", "options": {"max": 64}},
39
+ {"name": "git_commit_hash", "type": "text", "options": {"max": 64}},
40
+ {"name": "manifest_storage_uri", "type": "text", "options": {"max": 512}},
41
+ {"name": "manifest_file_hash", "type": "text", "options": {"max": 128}},
42
+ {"name": "asset_base_storage_uri", "type": "text", "options": {"max": 512}},
43
+ {"name": "target_cohorts", "type": "json"},
44
+ {"name": "metadata", "type": "json"},
45
+ {"name": "artifact", "type": "file", "options": {"maxSelect": 1, "maxSize": 0, "mimeTypes": []}}
46
+ ],
47
+ "indexes": [
48
+ "CREATE INDEX idx_bundles_channel ON bundles (channel)",
49
+ "CREATE INDEX idx_bundles_platform ON bundles (platform)",
50
+ "CREATE INDEX idx_bundles_enabled ON bundles (enabled)",
51
+ "CREATE INDEX idx_bundles_created ON bundles (created)"
52
+ ]
53
+ }
54
+ ''';
55
+
56
+ const _kChannelsCollection = r'''
57
+ {
58
+ "name": "channels",
59
+ "type": "base",
60
+ "listRule": null,
61
+ "viewRule": null,
62
+ "createRule": null,
63
+ "updateRule": null,
64
+ "deleteRule": null,
65
+ "fields": [
66
+ {"name": "name", "type": "text", "required": true, "options": {"min": 1, "max": 64, "pattern": "^[a-zA-Z0-9_-]+$"}},
67
+ {"name": "current_bundle", "type": "text", "options": {"max": 64}}
68
+ ],
69
+ "indexes": [
70
+ "CREATE UNIQUE INDEX idx_channels_name ON channels (name)"
71
+ ]
72
+ }
73
+ ''';
74
+
75
+ const _kAuditLogCollection = r'''
76
+ {
77
+ "name": "audit_log",
78
+ "type": "base",
79
+ "listRule": null,
80
+ "viewRule": null,
81
+ "createRule": null,
82
+ "updateRule": null,
83
+ "deleteRule": null,
84
+ "fields": [
85
+ {"name": "user", "type": "text", "options": {"max": 128}},
86
+ {"name": "action", "type": "text", "required": true, "options": {"min": 1, "max": 64}},
87
+ {"name": "bundle_id", "type": "text", "options": {"max": 64}},
88
+ {"name": "channel", "type": "text", "options": {"max": 64}},
89
+ {"name": "details", "type": "json"}
90
+ ],
91
+ "indexes": [
92
+ "CREATE INDEX idx_audit_action ON audit_log (action)",
93
+ "CREATE INDEX idx_audit_created ON audit_log (created)"
94
+ ]
95
+ }
96
+ ''';
97
+
98
+ class _RawAdminClient {
99
+ _RawAdminClient(this.baseUrl);
100
+ final String baseUrl;
101
+ String? _token;
102
+
103
+ Future<String> authenticate(String email, String password) async {
104
+ final res = await http.post(
105
+ Uri.parse('$baseUrl/api/admins/auth-with-password'),
106
+ headers: {'content-type': 'application/json'},
107
+ body: jsonEncode({'identity': email, 'password': password}),
108
+ );
109
+ if (res.statusCode != 200) {
110
+ throw StateError(
111
+ 'PocketBase admin auth failed (HTTP ${res.statusCode}). '
112
+ 'Is PB running and is the admin email/password correct?',
113
+ );
114
+ }
115
+ final body = jsonDecode(res.body) as Map<String, dynamic>;
116
+ final token = body['token'] as String?;
117
+ if (token == null || token.isEmpty) {
118
+ throw StateError('PocketBase returned no admin token.');
119
+ }
120
+ _token = token;
121
+ return token;
122
+ }
123
+
124
+ Future<List<String>> listCollectionNames() async {
125
+ final res = await http.get(
126
+ Uri.parse('$baseUrl/api/collections'),
127
+ headers: {'authorization': _token ?? ''},
128
+ );
129
+ if (res.statusCode != 200) {
130
+ throw StateError(
131
+ 'Failed to list collections (HTTP ${res.statusCode} ${res.body})',
132
+ );
133
+ }
134
+ final body = jsonDecode(res.body) as Map<String, dynamic>;
135
+ return ((body['items'] as List?) ?? [])
136
+ .map((c) => (c as Map)['name'] as String? ?? '')
137
+ .where((n) => n.isNotEmpty)
138
+ .toList();
139
+ }
140
+
141
+ Future<void> createCollection(String schemaJson) async {
142
+ final res = await http.post(
143
+ Uri.parse('$baseUrl/api/collections'),
144
+ headers: {
145
+ 'authorization': _token ?? '',
146
+ 'content-type': 'application/json',
147
+ },
148
+ body: schemaJson,
149
+ );
150
+ if (res.statusCode != 200 &&
151
+ res.statusCode != 201 &&
152
+ res.statusCode != 400) {
153
+ throw StateError(
154
+ 'Failed to create collection (HTTP ${res.statusCode} ${res.body})',
155
+ );
156
+ }
157
+ // 400 here usually means the collection already exists; the installer
158
+ // pre-checks names so this should be a true error.
159
+ }
160
+ }
161
+
162
+ class PocketBaseSchemaInstallResult {
163
+ const PocketBaseSchemaInstallResult({
164
+ required this.created,
165
+ required this.skipped,
166
+ });
167
+ final List<String> created;
168
+ final List<String> skipped;
169
+ }
170
+
171
+ /// Idempotently installs the flutter_ota_kit PocketBase schema.
172
+ class PocketBaseSchemaInstaller {
173
+ PocketBaseSchemaInstaller({
174
+ required this.url,
175
+ required this.adminEmail,
176
+ required this.adminPassword,
177
+ http.Client? httpClient,
178
+ this.bundlesCollection = 'bundles',
179
+ this.channelsCollection = 'channels',
180
+ this.auditLogCollection = 'audit_log',
181
+ });
182
+
183
+ final String url;
184
+ final String adminEmail;
185
+ final String adminPassword;
186
+ final String bundlesCollection;
187
+ final String channelsCollection;
188
+ final String auditLogCollection;
189
+
190
+ Future<PocketBaseSchemaInstallResult> install() async {
191
+ final client = _RawAdminClient(url);
192
+ await client.authenticate(adminEmail, adminPassword);
193
+ final existing = await client.listCollectionNames();
194
+ final created = <String>[];
195
+ final skipped = <String>[];
196
+ for (final spec in _schema()) {
197
+ if (existing.contains(spec.name)) {
198
+ skipped.add(spec.name);
199
+ continue;
200
+ }
201
+ await client.createCollection(spec.schema);
202
+ created.add(spec.name);
203
+ }
204
+ return PocketBaseSchemaInstallResult(created: created, skipped: skipped);
205
+ }
206
+
207
+ List<_CollectionSpec> _schema() {
208
+ // Replace the hard-coded collection names if the user customized them.
209
+ String rename(String json) {
210
+ if (bundlesCollection != 'bundles') {
211
+ json = json.replaceAll('"bundles"', '"$bundlesCollection"');
212
+ }
213
+ if (channelsCollection != 'channels') {
214
+ json = json.replaceAll('"channels"', '"$channelsCollection"');
215
+ }
216
+ if (auditLogCollection != 'audit_log') {
217
+ json = json.replaceAll('"audit_log"', '"$auditLogCollection"');
218
+ }
219
+ return json;
220
+ }
221
+
222
+ return [
223
+ _CollectionSpec(bundlesCollection, rename(_kBundlesCollection)),
224
+ _CollectionSpec(channelsCollection, rename(_kChannelsCollection)),
225
+ _CollectionSpec(auditLogCollection, rename(_kAuditLogCollection)),
226
+ ];
227
+ }
228
+ }
229
+
230
+ class _CollectionSpec {
231
+ const _CollectionSpec(this.name, this.schema);
232
+ final String name;
233
+ final String schema;
234
+ }
235
+
236
+ /// Re-export so callers can use the typed client as well.
237
+ typedef PocketBaseBackend = PocketBaseClient;
@@ -2,6 +2,7 @@ import 'dart:io';
2
2
 
3
3
  import 'package:args/args.dart';
4
4
  import 'package:args/command_runner.dart';
5
+
5
6
  import 'ui/ui.dart';
6
7
  import 'verbose.dart';
7
8
 
@@ -30,8 +31,10 @@ class FlutterPatcherRunner extends CommandRunner<int> {
30
31
  maxName = maxName < n.length ? n.length : maxName;
31
32
  }
32
33
  final buf = StringBuffer();
33
- buf.writeln(' ${cyan('▶')} ${cyan(bold(executableName))} '
34
- '${dim('·')} ${bold(description)}');
34
+ buf.writeln(
35
+ ' ${cyan('▶')} ${cyan(bold(executableName))} '
36
+ '${dim('·')} ${bold(description)}',
37
+ );
35
38
  buf.writeln(' ${dim('─' * 60)}');
36
39
  buf.writeln('');
37
40
  buf.writeln(' ${bold('USAGE')}');
@@ -43,10 +46,14 @@ class FlutterPatcherRunner extends CommandRunner<int> {
43
46
  buf.writeln(' ${cyan(n.padRight(maxName))} ${dim(c.description)}');
44
47
  }
45
48
  buf.writeln('');
46
- buf.writeln(' ${dim('Run "$executableName help <command>" for more about a command.')}');
49
+ buf.writeln(
50
+ ' ${dim('Run "$executableName help <command>" for more about a command.')}',
51
+ );
47
52
  buf.writeln('');
48
53
  buf.writeln(' ${bold('GLOBAL OPTIONS')}');
49
- buf.writeln(' ${cyan('-v, --verbose')} ${dim('Show full error stack traces.')}');
54
+ buf.writeln(
55
+ ' ${cyan('-v, --verbose')} ${dim('Show full error stack traces.')}',
56
+ );
50
57
  return buf.toString();
51
58
  }
52
59
 
@@ -72,35 +79,49 @@ abstract class FlutterPatcherCommand extends Command<int> {
72
79
  @override
73
80
  String get usage {
74
81
  final buf = StringBuffer();
75
- buf.writeln(' ${cyan('▶')} ${cyan(bold(name))} ${dim('·')} ${bold(description)}');
82
+ buf.writeln(
83
+ ' ${cyan('▶')} ${cyan(bold(name))} '
84
+ '${dim('·')} $description',
85
+ );
76
86
  buf.writeln(' ${dim('─' * 60)}');
77
87
  buf.writeln('');
78
88
  buf.writeln(' ${bold('USAGE')}');
79
89
  buf.writeln(' ${_fullName(this)} [arguments]');
80
90
  if (subcommands.isNotEmpty) {
91
+ buf.writeln('');
92
+ buf.writeln(' ${bold('COMMANDS')}');
81
93
  final names = subcommands.keys.toList()..sort();
82
94
  var maxName = 0;
83
95
  for (final n in names) {
84
- if (n.length > maxName) maxName = n.length;
96
+ maxName = maxName < n.length ? n.length : maxName;
85
97
  }
86
- buf.writeln('');
87
- buf.writeln(' ${bold('COMMANDS')}');
88
98
  for (final n in names) {
89
99
  final c = subcommands[n]!;
90
100
  buf.writeln(' ${cyan(n.padRight(maxName))} ${dim(c.description)}');
91
101
  }
92
102
  buf.writeln('');
93
- buf.writeln(' ${dim('Run "${_fullName(this)} <command>" for more about a command.')}');
103
+ buf.writeln(
104
+ ' ${dim('Run "${_fullName(this)} <command>" for more about a command.')}',
105
+ );
94
106
  }
95
107
  if (argParser.options.isNotEmpty) {
96
108
  buf.writeln('');
97
109
  buf.writeln(' ${bold('OPTIONS')}');
98
- for (final opt in argParser.options.values) {
99
- final flags = <String>[];
100
- if (opt.abbr != null && opt.abbr!.isNotEmpty) flags.add('-${opt.abbr}');
101
- flags.add('--${opt.name}');
102
- final flagStr = flags.join(', ');
103
- buf.writeln(' ${cyan(flagStr.padRight(22))} ${dim(opt.help ?? '')}');
110
+ final optLines = argParser.usage
111
+ .split('\n')
112
+ .where((l) => l.trim().isNotEmpty);
113
+ for (final l in optLines) {
114
+ final m = RegExp(r'^\s*(-{1,2}[\w-]+)(.*)$').firstMatch(l);
115
+ if (m != null) {
116
+ buf.writeln(
117
+ ' ${cyan(m.group(1)!)}${dim(m.group(2) ?? '')}'.replaceAll(
118
+ '\n',
119
+ '\n ',
120
+ ),
121
+ );
122
+ } else {
123
+ buf.writeln(' ${dim(l.trim())}');
124
+ }
104
125
  }
105
126
  }
106
127
  return buf.toString();
@@ -24,7 +24,18 @@ Future<bool> ed25519Verify(
24
24
 
25
25
  /// The 12-byte DER prefix of an Ed25519 X.509 SubjectPublicKeyInfo.
26
26
  const List<int> _ed25519SpkiPrefix = <int>[
27
- 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00,
27
+ 0x30,
28
+ 0x2a,
29
+ 0x30,
30
+ 0x05,
31
+ 0x06,
32
+ 0x03,
33
+ 0x2b,
34
+ 0x65,
35
+ 0x70,
36
+ 0x03,
37
+ 0x21,
38
+ 0x00,
28
39
  ];
29
40
 
30
41
  bool _startsWith(List<int> bytes, List<int> prefix) {
@@ -44,10 +55,7 @@ Future<(String, String)> generateEd25519KeyPair() async {
44
55
  final keyPair = await Ed25519().newKeyPair();
45
56
  final private = await keyPair.extractPrivateKeyBytes();
46
57
  final rawPublic = (await keyPair.extractPublicKey()).bytes;
47
- final spki = <int>[
48
- ..._ed25519SpkiPrefix,
49
- ...rawPublic,
50
- ];
58
+ final spki = <int>[..._ed25519SpkiPrefix, ...rawPublic];
51
59
  return (base64Encode(private), base64Encode(spki));
52
60
  }
53
61
 
@@ -21,9 +21,7 @@ const _kCyan = '\x1b[36m';
21
21
  const _kGray = '\x1b[90m';
22
22
  const _kBold = '\x1b[1m';
23
23
 
24
- const List<String> _frames = [
25
- '⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏',
26
- ];
24
+ const List<String> _frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
27
25
 
28
26
  bool get _noColor {
29
27
  final v = Platform.environment['NO_COLOR'];
@@ -61,14 +59,17 @@ String kv(String k, String v) {
61
59
 
62
60
  void banner(String name) {
63
61
  _echo('');
64
- _echo(' ${cyan('▶')} ${cyan(bold('flutter_ota_kit'))} ${dim('·')} ${bold(name)}');
62
+ _echo(
63
+ ' ${cyan('▶')} ${cyan(bold('flutter_ota_kit'))} ${dim('·')} ${bold(name)}',
64
+ );
65
65
  _echo(' ${dim('─' * math.min(_cols - 4, 60))}');
66
66
  }
67
67
 
68
68
  void step(String msg) => _echo(' ${green('✓')} $msg');
69
69
 
70
- void err(String msg) =>
71
- _echo(' ${red('✗')} ${red(msg)}');
70
+ void warn(String msg) => _echo(' ${yellow('!')} ${yellow(msg)}');
71
+
72
+ void err(String msg) => _echo(' ${red('✗')} ${red(msg)}');
72
73
 
73
74
  /// Run [task] while showing a live spinner; replaces the line with a
74
75
  /// `${green('✓')} done` summary when finished (or `✗` on error).
@@ -127,9 +128,7 @@ List<String> _wrap(String text, int inner) {
127
128
  line += ' $word';
128
129
  } else {
129
130
  result.add(line);
130
- line = word.length <= inner
131
- ? word
132
- : _splitLong(word, inner, result);
131
+ line = word.length <= inner ? word : _splitLong(word, inner, result);
133
132
  }
134
133
  }
135
134
  result.add(line);
@@ -182,8 +181,7 @@ void box(String title, List<String> lines) {
182
181
  var dashes = math.max(2, width - 2 - tVisible);
183
182
  final left = math.max(1, dashes ~/ 2);
184
183
  final right = dashes - left;
185
- final top =
186
- '╭${'─' * left}$titleText${'─' * right}╮';
184
+ final top = '╭${'─' * left}$titleText${'─' * right}╮';
187
185
  _echo(top);
188
186
  for (final l in wrapped) {
189
187
  final pad = math.max(0, inner - _dispWidth(l));
@@ -221,8 +219,7 @@ class ProgressBar {
221
219
  const barW = 28;
222
220
  final pct = total > 0 ? (_current / total).clamp(0.0, 1.0) : 1.0;
223
221
  final filled = (barW * pct).round();
224
- final bar =
225
- '${green('█' * filled)}${gray('·' * (barW - filled))}';
222
+ final bar = '${green('█' * filled)}${gray('·' * (barW - filled))}';
226
223
  _spin = (_spin + 1) % _frames.length;
227
224
  final pctStr = '${(pct * 100).round()}%'.padLeft(4);
228
225
  stdout.write('\r\x1b[K ${cyan(_frames[_spin])} $_label [$bar] $pctStr');
@@ -30,7 +30,9 @@ Future<String> zipDirectory(String directory) async {
30
30
  }
31
31
  final encoder = ZipEncoder();
32
32
  final out = encoder.encode(archive);
33
- final tmp = File('${Directory.systemTemp.path}/fp_${Random().nextInt(1 << 30)}.zip');
33
+ final tmp = File(
34
+ '${Directory.systemTemp.path}/fp_${Random().nextInt(1 << 30)}.zip',
35
+ );
34
36
  await tmp.writeAsBytes(out);
35
37
  return tmp.path;
36
38
  }