@_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
@@ -91,8 +91,9 @@ PackResult _writePatchPackage({
91
91
  }) {
92
92
  // When an ABI is requested, restrict the patch to only that ABI instead of
93
93
  // shipping every architecture found in the APK.
94
- final effectiveLibs =
95
- abi == null ? libs : libs.where((e) => e.$1 == abi).toList();
94
+ final effectiveLibs = abi == null
95
+ ? libs
96
+ : libs.where((e) => e.$1 == abi).toList();
96
97
  final patchFiles = <String, _PatchAssetFile>{};
97
98
  final operations = <Map<String, dynamic>>[];
98
99
  var baseManifestSize = 0;
@@ -115,17 +116,12 @@ PackResult _writePatchPackage({
115
116
  for (final key in requestedAssets) {
116
117
  final variantsRaw = assetManifest[key];
117
118
  if (variantsRaw is! List) {
118
- throw PackException(
119
- 'asset key not found in AssetManifest.bin: $key');
119
+ throw PackException('asset key not found in AssetManifest.bin: $key');
120
120
  }
121
121
  final variants = variantsRaw
122
122
  .map((variant) => Map<String, dynamic>.from(variant as Map))
123
123
  .toList(growable: false);
124
- operations.add({
125
- 'op': 'upsert',
126
- 'key': key,
127
- 'variants': variants,
128
- });
124
+ operations.add({'op': 'upsert', 'key': key, 'variants': variants});
129
125
  for (final variant in variants) {
130
126
  final assetPath = variant['asset'];
131
127
  if (assetPath is! String || assetPath.isEmpty) {
@@ -150,10 +146,11 @@ PackResult _writePatchPackage({
150
146
  }
151
147
 
152
148
  final libManifest = <String, dynamic>{
153
- for (final (currentAbi, soBytes) in effectiveLibs) currentAbi: {
154
- 'path': 'lib/$currentAbi/libapp.so',
155
- 'md5': md5.convert(soBytes).toString(),
156
- },
149
+ for (final (currentAbi, soBytes) in effectiveLibs)
150
+ currentAbi: {
151
+ 'path': 'lib/$currentAbi/libapp.so',
152
+ 'md5': md5.convert(soBytes).toString(),
153
+ },
157
154
  };
158
155
 
159
156
  final zipManifest = <String, dynamic>{
@@ -167,11 +164,13 @@ PackResult _writePatchPackage({
167
164
  'manifestPatch': 'manifest_patch.json',
168
165
  'prefix': 'assets/',
169
166
  'files': patchFiles.values
170
- .map((file) => {
171
- 'path': file.path,
172
- 'md5': file.md5Hex,
173
- 'size': file.bytes.length,
174
- })
167
+ .map(
168
+ (file) => {
169
+ 'path': file.path,
170
+ 'md5': file.md5Hex,
171
+ 'size': file.bytes.length,
172
+ },
173
+ )
175
174
  .toList(),
176
175
  },
177
176
  };
@@ -183,7 +182,8 @@ PackResult _writePatchPackage({
183
182
  var progressDone = 0;
184
183
  for (final (currentAbi, soBytes) in effectiveLibs) {
185
184
  package.addFile(
186
- ArchiveFile('lib/$currentAbi/libapp.so', soBytes.length, soBytes));
185
+ ArchiveFile('lib/$currentAbi/libapp.so', soBytes.length, soBytes),
186
+ );
187
187
  progressDone++;
188
188
  onProgress?.call(progressDone, progressTotal, 'lib/$currentAbi/libapp.so');
189
189
  }
@@ -204,8 +204,7 @@ PackResult _writePatchPackage({
204
204
  }
205
205
 
206
206
  // archive 3.x returns List<int>?, 4.x returns List<int>; coerce to bytes.
207
- // ignore: unnecessary_nullable_for_final_variable_declarations, dead_null_aware_expression
208
- final List<int> packageBytes = ZipEncoder().encode(package) ?? const <int>[];
207
+ final List<int> packageBytes = ZipEncoder().encode(package);
209
208
  final outZip = File('${outDir.path}/patch.zip');
210
209
  outZip.writeAsBytesSync(packageBytes);
211
210
  onProgress?.call(progressTotal, progressTotal, 'patch.zip');
@@ -233,8 +232,9 @@ PackResult _writePatchPackage({
233
232
  }
234
233
 
235
234
  ArchiveFile _jsonArchiveFile(String name, Map<String, dynamic> json) {
236
- final bytes =
237
- utf8.encode('${const JsonEncoder.withIndent(' ').convert(json)}\n');
235
+ final bytes = utf8.encode(
236
+ '${const JsonEncoder.withIndent(' ').convert(json)}\n',
237
+ );
238
238
  return ArchiveFile(name, bytes.length, bytes);
239
239
  }
240
240
 
@@ -271,7 +271,8 @@ ArchiveFile? _findFile(Archive archive, String name) {
271
271
 
272
272
  void _writeJson(File file, Map<String, dynamic> json) {
273
273
  file.writeAsStringSync(
274
- '${const JsonEncoder.withIndent(' ').convert(json)}\n');
274
+ '${const JsonEncoder.withIndent(' ').convert(json)}\n',
275
+ );
275
276
  }
276
277
 
277
278
  void _validateArchiveRelativePath(String path, String label) {
@@ -429,4 +430,4 @@ class _FlutterStandardMessageCodec {
429
430
  throw PackException('truncated AssetManifest.bin');
430
431
  }
431
432
  }
432
- }
433
+ }
@@ -0,0 +1,39 @@
1
+ /// Bootstraps a fresh PocketBase data directory with the flutter_ota_kit
2
+ /// schema defaults (hooks, settings) the first time PB is started.
3
+ library;
4
+
5
+ import 'dart:io';
6
+
7
+ import 'package:path/path.dart' as p;
8
+
9
+ /// Copies the bundled hooks into `pb_data/pb_hooks/` so PB picks them up on
10
+ /// the next `serve`. Existing files are left alone so user edits are kept.
11
+ class PocketBaseDataBootstrap {
12
+ const PocketBaseDataBootstrap({
13
+ required this.dataDir,
14
+ required this.hooksSourceDir,
15
+ });
16
+
17
+ final Directory dataDir;
18
+ final Directory hooksSourceDir;
19
+
20
+ Future<List<String>> install() async {
21
+ await dataDir.create(recursive: true);
22
+ final hooksDir = Directory(p.join(dataDir.path, 'pb_hooks'));
23
+ await hooksDir.create(recursive: true);
24
+ final installed = <String>[];
25
+ if (!hooksSourceDir.existsSync()) {
26
+ return installed;
27
+ }
28
+ for (final entry in hooksSourceDir.listSync()) {
29
+ if (entry is! File) continue;
30
+ if (!entry.path.endsWith('.pb.js')) continue;
31
+ final out = File(p.join(hooksDir.path, p.basename(entry.path)));
32
+ if (!await out.exists()) {
33
+ await entry.copy(out.path);
34
+ installed.add(p.basename(entry.path));
35
+ }
36
+ }
37
+ return installed;
38
+ }
39
+ }
@@ -0,0 +1,56 @@
1
+ /// <reference path="../pb_data/types.d.ts" />
2
+
3
+ // PocketBase JS hook: validate and enrich bundle records on create.
4
+ // Runs inside the PB Go binary's embedded JS VM (goja). Use $app.dao() to
5
+ // query, $app.settings() to read config, $apis.request() to call webhooks.
6
+
7
+ /** @type {OnRecordCreateRequestHandler} */
8
+ onRecordCreateRequest((e) => {
9
+ // Reject bundles that look malformed.
10
+ const b = e.record;
11
+ if (!b.get("channel") || !b.get("platform") || !b.get("file_hash")) {
12
+ throw new BadRequestError("channel, platform and file_hash are required");
13
+ }
14
+ if (b.getString("platform").length > 16) {
15
+ throw new BadRequestError("platform must be <= 16 chars");
16
+ }
17
+ if (b.getInt("rollout_cohort_count") > 1000) {
18
+ throw new BadRequestError("rollout_cohort_count must be 0..1000");
19
+ }
20
+
21
+ // Append to the audit log.
22
+ const audit = $app.dao().findCollectionByNameOrId("audit_log");
23
+ const row = new Record(audit, {
24
+ action: "deploy",
25
+ bundle_id: b.get("id"),
26
+ channel: b.get("channel"),
27
+ details: JSON.stringify({
28
+ platform: b.get("platform"),
29
+ message: b.get("message"),
30
+ should_force_update: b.getBool("should_force_update"),
31
+ }),
32
+ });
33
+ $app.dao().save(row);
34
+
35
+ e.next();
36
+ }, "bundles");
37
+
38
+ /** @type {OnRecordUpdateRequestHandler} */
39
+ onRecordUpdateRequest((e) => {
40
+ const b = e.record;
41
+ if (b.getInt("rollout_cohort_count") > 1000) {
42
+ throw new BadRequestError("rollout_cohort_count must be 0..1000");
43
+ }
44
+ e.next();
45
+ }, "bundles");
46
+
47
+ /** @type {OnRecordDeleteRequestHandler} */
48
+ onRecordDeleteRequest((e) => {
49
+ const audit = $app.dao().findCollectionByNameOrId("audit_log");
50
+ const row = new Record(audit, {
51
+ action: "delete_bundle",
52
+ bundle_id: e.record.get("id"),
53
+ });
54
+ $app.dao().save(row);
55
+ e.next();
56
+ }, "bundles");
@@ -0,0 +1,189 @@
1
+ /// Manages the PocketBase server binary — download, version, run, upgrade.
2
+ ///
3
+ /// PocketBase is a single Go binary (~15MB) that the CLI can download
4
+ /// automatically on first run. The binary is cached in
5
+ /// `~/.flutter_ota_kit/pocketbase/` so subsequent `serve` invocations are
6
+ /// instant.
7
+ ///
8
+ /// Supported platforms: linux/amd64, linux/arm64, macos/amd64, macos/arm64,
9
+ /// windows/amd64. Other combinations fall back to a helpful error.
10
+ library;
11
+
12
+ import 'dart:async';
13
+ import 'dart:io';
14
+
15
+ import 'package:archive/archive.dart';
16
+ import 'package:http/http.dart' as http;
17
+ import 'package:path/path.dart' as p;
18
+
19
+ /// Default PocketBase version managed by the CLI. Bumped in lockstep with
20
+ /// the rest of the stack.
21
+ const String kDefaultPocketBaseVersion = '0.22.21';
22
+
23
+ /// Where the binary + extracted files live.
24
+ class PocketBaseInstallPaths {
25
+ const PocketBaseInstallPaths({
26
+ required this.installDir,
27
+ required this.binaryPath,
28
+ });
29
+
30
+ /// `~/.flutter_ota_kit/pocketbase/<version>/`
31
+ final Directory installDir;
32
+
33
+ /// `~/.flutter_ota_kit/pocketbase/<version>/pocketbase`
34
+ /// (or `pocketbase.exe` on Windows).
35
+ final File binaryPath;
36
+
37
+ String get version => p.basename(installDir.path);
38
+
39
+ static PocketBaseInstallPaths resolve({
40
+ String? version = kDefaultPocketBaseVersion,
41
+ Directory? root,
42
+ }) {
43
+ final base =
44
+ root ??
45
+ Directory(
46
+ p.join(
47
+ Platform.environment['HOME'] ??
48
+ Platform.environment['USERPROFILE'] ??
49
+ '.',
50
+ '.flutter_ota_kit',
51
+ 'pocketbase',
52
+ version!,
53
+ ),
54
+ );
55
+ final exe = Platform.isWindows ? 'pocketbase.exe' : 'pocketbase';
56
+ return PocketBaseInstallPaths(
57
+ installDir: base,
58
+ binaryPath: File(p.join(base.path, exe)),
59
+ );
60
+ }
61
+ }
62
+
63
+ class _OsArch {
64
+ const _OsArch(this.os, this.arch, this.assetSuffix);
65
+ final String os;
66
+ final String arch;
67
+ final String assetSuffix;
68
+ }
69
+
70
+ _OsArch _detectOsArch() {
71
+ // Map Platform.operatingSystem + Platform.numberOfProcessors to a PB asset.
72
+ // PB asset names: linux_amd64, linux_arm64, darwin_amd64, darwin_arm64,
73
+ // windows_amd64.
74
+ final os = Platform.operatingSystem;
75
+ String assetOs;
76
+ if (os == 'linux') {
77
+ assetOs = 'linux';
78
+ } else if (os == 'macos') {
79
+ assetOs = 'darwin';
80
+ } else if (os == 'windows') {
81
+ assetOs = 'windows';
82
+ } else {
83
+ throw StateError('Unsupported OS for PocketBase: $os');
84
+ }
85
+ // Dart on ARM Macs returns 'macos' with arm64 in the version string; we
86
+ // detect arm64 via the Dart version since `Platform` doesn't expose arch
87
+ // directly in stable Dart.
88
+ final dartVersion = Platform.version.toLowerCase();
89
+ final isArm =
90
+ dartVersion.contains('arm64') || dartVersion.contains('aarch64');
91
+ final arch = isArm ? 'arm64' : 'amd64';
92
+ return _OsArch(os, arch, '${assetOs}_$arch');
93
+ }
94
+
95
+ /// Outcome of [PocketBaseInstaller.ensureInstalled].
96
+ class PocketBaseInstallResult {
97
+ const PocketBaseInstallResult({
98
+ required this.paths,
99
+ required this.alreadyInstalled,
100
+ required this.bytesDownloaded,
101
+ });
102
+
103
+ final PocketBaseInstallPaths paths;
104
+ final bool alreadyInstalled;
105
+ final int bytesDownloaded;
106
+ }
107
+
108
+ /// Downloads, extracts, and verifies the PocketBase binary.
109
+ class PocketBaseInstaller {
110
+ PocketBaseInstaller({http.Client? httpClient, String? version})
111
+ : _http = httpClient ?? http.Client(),
112
+ _version = version ?? kDefaultPocketBaseVersion;
113
+
114
+ final http.Client _http;
115
+ final String _version;
116
+
117
+ /// Returns the install paths and the binary is guaranteed to exist on disk.
118
+ Future<PocketBaseInstallResult> ensureInstalled({
119
+ PocketBaseInstallPaths? paths,
120
+ void Function(double progress)? onProgress,
121
+ }) async {
122
+ final install = paths ?? PocketBaseInstallPaths.resolve(version: _version);
123
+ if (await install.binaryPath.exists()) {
124
+ return PocketBaseInstallResult(
125
+ paths: install,
126
+ alreadyInstalled: true,
127
+ bytesDownloaded: 0,
128
+ );
129
+ }
130
+ await install.installDir.create(recursive: true);
131
+
132
+ final osArch = _detectOsArch();
133
+ final url = Uri.parse(
134
+ 'https://github.com/pocketbase/pocketbase/releases/download/'
135
+ 'v$_version/pocketbase_${_version}_${osArch.assetSuffix}.zip',
136
+ );
137
+ final req = http.Request('GET', url);
138
+ final streamed = await _http.send(req);
139
+ if (streamed.statusCode != 200) {
140
+ throw StateError(
141
+ 'Failed to download PocketBase v$_version for ${osArch.assetSuffix}: '
142
+ 'HTTP ${streamed.statusCode}. Check your internet connection or set '
143
+ '--pb-version to a known-good release.',
144
+ );
145
+ }
146
+ final bytes = <int>[];
147
+ var received = 0;
148
+ final total = streamed.contentLength ?? 0;
149
+ await for (final chunk in streamed.stream) {
150
+ bytes.addAll(chunk);
151
+ received += chunk.length;
152
+ if (onProgress != null && total > 0) {
153
+ onProgress(received / total);
154
+ }
155
+ }
156
+ final zipPath = File(p.join(install.installDir.path, 'pocketbase.zip'))
157
+ ..writeAsBytesSync(bytes);
158
+
159
+ // Extract just the binary (skip the README/CHANGELOG that ship in the
160
+ // archive to keep the install dir tidy).
161
+ final archive = ZipDecoder().decodeBytes(bytes);
162
+ for (final entry in archive) {
163
+ if (!entry.isFile) continue;
164
+ final entryName = entry.name.split('/').last;
165
+ if (entryName == 'pocketbase' || entryName == 'pocketbase.exe') {
166
+ final out = File(p.join(install.installDir.path, entryName));
167
+ out.writeAsBytesSync(entry.content as List<int>);
168
+ if (!Platform.isWindows) {
169
+ Process.runSync('chmod', ['+x', out.path]);
170
+ }
171
+ }
172
+ }
173
+ try {
174
+ await zipPath.delete();
175
+ } catch (_) {
176
+ // best-effort cleanup
177
+ }
178
+
179
+ return PocketBaseInstallResult(
180
+ paths: install,
181
+ alreadyInstalled: false,
182
+ bytesDownloaded: bytes.length,
183
+ );
184
+ }
185
+
186
+ /// Resolves the install paths without downloading.
187
+ PocketBaseInstallPaths paths() =>
188
+ PocketBaseInstallPaths.resolve(version: _version);
189
+ }
@@ -0,0 +1,123 @@
1
+ /// Spawns the PocketBase binary as a child process, capturing its stdout/stderr
2
+ /// and providing clean start/stop semantics.
3
+ ///
4
+ /// PB reads its data directory from the `--dir` flag (default `./pb_data`).
5
+ /// The CLI's installer pre-creates that directory and copies hooks into
6
+ /// `pb_data/pb_hooks/` before the first start.
7
+ library;
8
+
9
+ import 'dart:async';
10
+ import 'dart:io';
11
+
12
+ import 'package:path/path.dart' as p;
13
+
14
+ class PocketBaseProcess {
15
+ PocketBaseProcess._(this.process, this.dataDir, this.binaryPath);
16
+
17
+ final Process process;
18
+ final Directory dataDir;
19
+ final File binaryPath;
20
+
21
+ bool get isRunning => true; // Process instance is always present after start.
22
+
23
+ int get pid => process.pid;
24
+
25
+ Future<int> get exitCode => process.exitCode;
26
+
27
+ /// Returns the stdout/stderr streams merged.
28
+ Stream<List<int>> get output => process.stdout;
29
+ Stream<List<int>> get errors => process.stderr;
30
+
31
+ /// Send SIGINT (Ctrl+C) for a graceful shutdown, then SIGKILL if needed.
32
+ Future<void> stop({Duration timeout = const Duration(seconds: 5)}) async {
33
+ if (Platform.isWindows) {
34
+ process.kill();
35
+ } else {
36
+ process.kill(ProcessSignal.sigint);
37
+ }
38
+ try {
39
+ await process.exitCode.timeout(timeout);
40
+ } on TimeoutException {
41
+ process.kill(ProcessSignal.sigkill);
42
+ }
43
+ }
44
+ }
45
+
46
+ class PocketBaseProcessManager {
47
+ PocketBaseProcessManager({
48
+ required this.binaryPath,
49
+ required this.dataDir,
50
+ this.port = 8090,
51
+ this.host = '127.0.0.1',
52
+ });
53
+
54
+ final File binaryPath;
55
+ final Directory dataDir;
56
+ final int port;
57
+ final String host;
58
+
59
+ PocketBaseProcess? _process;
60
+ PocketBaseProcess? get process => _process;
61
+
62
+ /// Start PB in the background. Returns once PB is listening (or after a
63
+ /// short timeout waiting for it to come up).
64
+ Future<PocketBaseProcess> start({
65
+ Map<String, String>? env,
66
+ String? adminEmail,
67
+ String? adminPassword,
68
+ Duration readyTimeout = const Duration(seconds: 15),
69
+ }) async {
70
+ if (_process != null) {
71
+ throw StateError('PocketBase is already running (pid ${_process!.pid})');
72
+ }
73
+ if (!await binaryPath.exists()) {
74
+ throw StateError(
75
+ 'PocketBase binary not found at ${binaryPath.path}. '
76
+ 'Run `flutter_ota_kit pocketbase install` first.',
77
+ );
78
+ }
79
+ await dataDir.create(recursive: true);
80
+ final hooksDir = Directory(p.join(dataDir.path, 'pb_hooks'));
81
+ await hooksDir.create(recursive: true);
82
+
83
+ final mergedEnv = <String, String>{
84
+ ...Platform.environment,
85
+ if (adminEmail != null) 'PB_ADMIN_EMAIL': adminEmail,
86
+ if (adminPassword != null) 'PB_ADMIN_PASSWORD': adminPassword,
87
+ ...?env,
88
+ };
89
+
90
+ final proc = await Process.start(
91
+ binaryPath.path,
92
+ ['serve', '--http=${host}:$port', '--dir=${dataDir.path}'],
93
+ environment: mergedEnv,
94
+ mode: ProcessStartMode.detached,
95
+ );
96
+
97
+ // Wait for the health endpoint to come up (best-effort; fall through
98
+ // after readyTimeout either way).
99
+ final deadline = DateTime.now().add(readyTimeout);
100
+ while (DateTime.now().isBefore(deadline)) {
101
+ try {
102
+ final socket = await Socket.connect(
103
+ host,
104
+ port,
105
+ timeout: const Duration(milliseconds: 250),
106
+ );
107
+ socket.destroy();
108
+ break;
109
+ } catch (_) {
110
+ await Future<void>.delayed(const Duration(milliseconds: 200));
111
+ }
112
+ }
113
+ _process = PocketBaseProcess._(proc, dataDir, binaryPath);
114
+ return _process!;
115
+ }
116
+
117
+ Future<void> stop() async {
118
+ final proc = _process;
119
+ if (proc == null) return;
120
+ await proc.stop();
121
+ _process = null;
122
+ }
123
+ }