@_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
@@ -28,29 +28,30 @@ class KeysCommand extends FlutterPatcherCommand {
28
28
 
29
29
  @override
30
30
  Future<int> run() => runGuarded(() async {
31
- banner('keys');
32
- final (privateB64, publicB64) = await spinner(
33
- () => generateEd25519KeyPair(),
34
- 'Generating Ed25519 keypair',
35
- done: 'Keypair generated',
36
- );
37
- box('ed25519 keys', [
38
- '${bold('private')} (keep secret — use with `deploy --key`):',
39
- ' ${red(privateB64)}',
40
- '',
41
- '${bold('public')} (safe to embed / configure on device):',
42
- ' ${green(publicB64)}',
43
- ]);
44
- if (argResults!['save'] as bool) {
45
- final file = configCandidates().first;
46
- final json = file.existsSync() && file.readAsStringSync().trim().isNotEmpty
47
- ? (jsonDecode(file.readAsStringSync()) as Map<String, dynamic>)
48
- : <String, dynamic>{};
49
- writePath(json, 'publicKey', publicB64);
50
- _save(json);
51
- step('Saved public key to ${file.path}');
52
- }
53
- });
31
+ banner('keys');
32
+ final (privateB64, publicB64) = await spinner(
33
+ () => generateEd25519KeyPair(),
34
+ 'Generating Ed25519 keypair',
35
+ done: 'Keypair generated',
36
+ );
37
+ box('ed25519 keys', [
38
+ '${bold('private')} (keep secret — use with `deploy --key`):',
39
+ ' ${red(privateB64)}',
40
+ '',
41
+ '${bold('public')} (safe to embed / configure on device):',
42
+ ' ${green(publicB64)}',
43
+ ]);
44
+ if (argResults!['save'] as bool) {
45
+ final file = configCandidates().first;
46
+ final json =
47
+ file.existsSync() && file.readAsStringSync().trim().isNotEmpty
48
+ ? (jsonDecode(file.readAsStringSync()) as Map<String, dynamic>)
49
+ : <String, dynamic>{};
50
+ writePath(json, 'publicKey', publicB64);
51
+ _save(json);
52
+ step('Saved public key to ${file.path}');
53
+ }
54
+ });
54
55
 
55
56
  void _save(Map<String, dynamic> json) {
56
57
  final file = configCandidates().first;
@@ -21,14 +21,25 @@ class MigrateCommand extends FlutterPatcherCommand {
21
21
  @override
22
22
  ArgParser get argParser => ArgParser()
23
23
  ..addOption('backend', abbr: 'b', help: 'Backend provider.')
24
- ..addOption('database-url',
25
- help: 'Postgres connection string (or DATABASE_URL env).')
26
- ..addOption('management-key',
27
- help: 'Supabase Management API key (or SUPABASE_MANAGEMENT_KEY env) — '
28
- 'runs migrations without a separate Postgres connection.')
29
- ..addOption('migrations-dir',
30
- help: 'Directory of *.sql migration files (ordered by name).')
31
- ..addFlag('dry-run', abbr: 'd', help: 'Print migrations instead of applying them.');
24
+ ..addOption(
25
+ 'database-url',
26
+ help: 'Postgres connection string (or DATABASE_URL env).',
27
+ )
28
+ ..addOption(
29
+ 'management-key',
30
+ help:
31
+ 'Supabase Management API key (or SUPABASE_MANAGEMENT_KEY env) — '
32
+ 'runs migrations without a separate Postgres connection.',
33
+ )
34
+ ..addOption(
35
+ 'migrations-dir',
36
+ help: 'Directory of *.sql migration files (ordered by name).',
37
+ )
38
+ ..addFlag(
39
+ 'dry-run',
40
+ abbr: 'd',
41
+ help: 'Print migrations instead of applying them.',
42
+ );
32
43
 
33
44
  /// Default migrations dir lives next to the CLI entrypoint; each backend has
34
45
  /// its own dialect's migrations (cloudflare = D1 SQLite, postgres = plain
@@ -49,12 +60,13 @@ class MigrateCommand extends FlutterPatcherCommand {
49
60
  if (!migrationsDir.existsSync()) {
50
61
  throw StateError('Migrations directory not found: $dir');
51
62
  }
52
- final files = migrationsDir
53
- .listSync()
54
- .whereType<File>()
55
- .where((f) => f.path.endsWith('.sql'))
56
- .toList()
57
- ..sort((a, b) => a.path.compareTo(b.path));
63
+ final files =
64
+ migrationsDir
65
+ .listSync()
66
+ .whereType<File>()
67
+ .where((f) => f.path.endsWith('.sql'))
68
+ .toList()
69
+ ..sort((a, b) => a.path.compareTo(b.path));
58
70
  if (files.isEmpty) {
59
71
  throw StateError('No *.sql files in $dir');
60
72
  }
@@ -71,92 +83,98 @@ class MigrateCommand extends FlutterPatcherCommand {
71
83
 
72
84
  @override
73
85
  Future<int> run() => runGuarded(() async {
74
- final provider = argResults!['backend'] as String? ?? 'supabase';
86
+ final provider = argResults!['backend'] as String? ?? 'supabase';
75
87
 
76
- // Backends with no relational SQL handled out-of-band (S3 blob store).
77
- // Short-circuit before any SQL discovery so a missing or empty
78
- // (README-only) migrations dir does not abort.
79
- if (provider == 'aws') {
80
- final dir = argResults!['migrations-dir'] as String? ??
81
- defaultMigrationsDir(provider);
82
- banner('migrate');
83
- box('migrate', [
84
- 'Migrations for "$provider" do not run through this command:',
85
- ' • aws → S3 blob store, no relational SQL (see $dir/README.md)',
86
- '',
87
- 'See the README under $dir for the intended setup.',
88
- ]);
89
- return;
90
- }
88
+ // Backends with no relational SQL handled out-of-band (S3 blob store).
89
+ // Short-circuit before any SQL discovery so a missing or empty
90
+ // (README-only) migrations dir does not abort.
91
+ if (provider == 'aws') {
92
+ final dir =
93
+ argResults!['migrations-dir'] as String? ??
94
+ defaultMigrationsDir(provider);
95
+ banner('migrate');
96
+ box('migrate', [
97
+ 'Migrations for "$provider" do not run through this command:',
98
+ ' • aws → S3 blob store, no relational SQL (see $dir/README.md)',
99
+ '',
100
+ 'See the README under $dir for the intended setup.',
101
+ ]);
102
+ return;
103
+ }
91
104
 
92
- final dir = argResults!['migrations-dir'] as String? ??
93
- defaultMigrationsDir(provider);
94
- final files = _listMigrations(dir);
105
+ final dir =
106
+ argResults!['migrations-dir'] as String? ??
107
+ defaultMigrationsDir(provider);
108
+ final files = _listMigrations(dir);
95
109
 
96
- if (argResults!['dry-run'] as bool) {
97
- banner('migrate · dry-run');
98
- for (final f in files) {
99
- stdout.writeln('--- ${p.basename(f.path)} ---');
100
- stdout.writeln(f.readAsStringSync());
101
- }
102
- return;
103
- }
110
+ if (argResults!['dry-run'] as bool) {
111
+ banner('migrate · dry-run');
112
+ for (final f in files) {
113
+ stdout.writeln('--- ${p.basename(f.path)} ---');
114
+ stdout.writeln(f.readAsStringSync());
115
+ }
116
+ return;
117
+ }
104
118
 
105
- if (provider == 'cloudflare') {
106
- banner('migrate');
107
- box('migrate', [
108
- 'Cloudflare (D1 SQLite) migrations run via `wrangler d1 execute`:',
109
- ' wrangler d1 execute <DB> --file=$dir/0001_hot-updater_init.sql',
110
- ' (apply each *.sql under $dir in filename order)',
111
- '',
112
- 'The SQL files under $dir show the intended D1 schema.',
113
- ]);
114
- return;
115
- }
119
+ if (provider == 'cloudflare') {
120
+ banner('migrate');
121
+ box('migrate', [
122
+ 'Cloudflare (D1 SQLite) migrations run via `wrangler d1 execute`:',
123
+ ' wrangler d1 execute <DB> --file=$dir/0001_hot-updater_init.sql',
124
+ ' (apply each *.sql under $dir in filename order)',
125
+ '',
126
+ 'The SQL files under $dir show the intended D1 schema.',
127
+ ]);
128
+ return;
129
+ }
116
130
 
117
- if (provider == 'supabase') {
118
- final cfg = loadConfig() ??
131
+ if (provider == 'supabase') {
132
+ final cfg =
133
+ loadConfig() ??
119
134
  FlutterPatcherConfig(
120
135
  provider: 'supabase',
121
136
  supabase: const SupabaseConfigJson(),
122
137
  );
123
- final mgmtKey = argResults!['management-key'] as String? ??
124
- Platform.environment['SUPABASE_MANAGEMENT_KEY'] ??
125
- cfg.supabase.managementKey;
126
- final pgUrl = argResults!['database-url'] as String? ??
127
- Platform.environment['DATABASE_URL'] ??
128
- cfg.supabase.databaseUrl;
129
- if (mgmtKey != null && mgmtKey.isNotEmpty) {
130
- await _runViaManagementApi(mgmtKey, files);
131
- return;
132
- }
133
- if (pgUrl != null && pgUrl.isNotEmpty) {
134
- await _runViaPostgres(pgUrl, files);
135
- return;
136
- }
137
- throw StateError(
138
- 'For the supabase backend, provide either:\n'
139
- ' • --management-key (or SUPABASE_MANAGEMENT_KEY, or set it via '
140
- '`flutter-ota init`) — a Supabase Management API key, OR\n'
141
- ' • --database-url (or DATABASE_URL, or set it via `init`) — a '
142
- 'Postgres connection string.',
143
- );
144
- }
138
+ final mgmtKey =
139
+ argResults!['management-key'] as String? ??
140
+ Platform.environment['SUPABASE_MANAGEMENT_KEY'] ??
141
+ cfg.supabase.managementKey;
142
+ final pgUrl =
143
+ argResults!['database-url'] as String? ??
144
+ Platform.environment['DATABASE_URL'] ??
145
+ cfg.supabase.databaseUrl;
146
+ if (mgmtKey != null && mgmtKey.isNotEmpty) {
147
+ await _runViaManagementApi(mgmtKey, files);
148
+ return;
149
+ }
150
+ if (pgUrl != null && pgUrl.isNotEmpty) {
151
+ await _runViaPostgres(pgUrl, files);
152
+ return;
153
+ }
154
+ throw StateError(
155
+ 'For the supabase backend, provide either:\n'
156
+ ' • --management-key (or SUPABASE_MANAGEMENT_KEY, or set it via '
157
+ '`flutter-ota init`) — a Supabase Management API key, OR\n'
158
+ ' • --database-url (or DATABASE_URL, or set it via `init`) — a '
159
+ 'Postgres connection string.',
160
+ );
161
+ }
145
162
 
146
- if (provider == 'postgres') {
147
- final pgUrl = argResults!['database-url'] as String? ??
148
- Platform.environment['DATABASE_URL'];
149
- if (pgUrl == null || pgUrl.isEmpty) {
150
- throw StateError(
151
- 'For the postgres backend, provide --database-url or set DATABASE_URL.',
152
- );
153
- }
154
- await _runViaPostgres(pgUrl, files);
155
- return;
156
- }
163
+ if (provider == 'postgres') {
164
+ final pgUrl =
165
+ argResults!['database-url'] as String? ??
166
+ Platform.environment['DATABASE_URL'];
167
+ if (pgUrl == null || pgUrl.isEmpty) {
168
+ throw StateError(
169
+ 'For the postgres backend, provide --database-url or set DATABASE_URL.',
170
+ );
171
+ }
172
+ await _runViaPostgres(pgUrl, files);
173
+ return;
174
+ }
157
175
 
158
- throw StateError('Unknown backend provider: "$provider".');
159
- });
176
+ throw StateError('Unknown backend provider: "$provider".');
177
+ });
160
178
 
161
179
  /// Run migrations through the Supabase Management API (no Postgres creds
162
180
  /// needed). Derived from the Supabase project ref in the configured URL.
@@ -169,8 +187,7 @@ class MigrateCommand extends FlutterPatcherCommand {
169
187
  ),
170
188
  );
171
189
  final ref = Uri.parse(cfg.supabaseUrl).host.split('.').first;
172
- final endpoint =
173
- 'https://api.supabase.com/v1/projects/$ref/database/query';
190
+ final endpoint = 'https://api.supabase.com/v1/projects/$ref/database/query';
174
191
  banner('migrate · supabase (management api)');
175
192
  for (final file in files) {
176
193
  final sql = file.readAsStringSync();
@@ -206,8 +223,8 @@ class MigrateCommand extends FlutterPatcherCommand {
206
223
  if (storage.supabaseServiceRoleKey == null) {
207
224
  box('migrate', [
208
225
  'Skipped storage bucket creation — set supabase.serviceRoleKey '
209
- '(or SUPABASE_SERVICE_ROLE_KEY) to auto-create the '
210
- '"${storage.bucketName}" bucket.',
226
+ '(or SUPABASE_SERVICE_ROLE_KEY) to auto-create the '
227
+ '"${storage.bucketName}" bucket.',
211
228
  ]);
212
229
  return;
213
230
  }
@@ -218,10 +235,7 @@ class MigrateCommand extends FlutterPatcherCommand {
218
235
  'apikey': storage.supabaseServiceRoleKey!,
219
236
  'Content-Type': 'application/json',
220
237
  },
221
- body: jsonEncode({
222
- 'name': storage.bucketName,
223
- 'public': true,
224
- }),
238
+ body: jsonEncode({'name': storage.bucketName, 'public': true}),
225
239
  );
226
240
  if (res.statusCode >= 400) {
227
241
  final body = jsonDecode(res.body) as Map<String, dynamic>;
@@ -0,0 +1,237 @@
1
+ /// `flutter_ota_kit pocketbase` — manage a local PocketBase instance.
2
+ library;
3
+
4
+ import 'dart:async';
5
+ import 'dart:io';
6
+
7
+ import 'package:args/args.dart';
8
+ import 'package:path/path.dart' as p;
9
+
10
+ import '../cli_base.dart';
11
+ import '../pocketbase/data_bootstrap.dart';
12
+ import '../pocketbase/installer.dart';
13
+ import '../pocketbase/process_manager.dart';
14
+ import '../pocketbase/schema_installer.dart';
15
+ import '../ui/ui.dart';
16
+
17
+ class PocketBaseCommand extends FlutterPatcherCommand {
18
+ PocketBaseCommand() {
19
+ addSubcommand(PocketBaseInstallCommand());
20
+ addSubcommand(PocketBaseServeCommand());
21
+ addSubcommand(PocketBaseStopCommand());
22
+ addSubcommand(PocketBaseStatusCommand());
23
+ }
24
+
25
+ @override
26
+ String get name => 'pocketbase';
27
+
28
+ @override
29
+ String get description =>
30
+ 'Manage a local PocketBase instance (install / serve / stop / status).';
31
+
32
+ @override
33
+ ArgParser get argParser => ArgParser();
34
+
35
+ @override
36
+ Future<int> run() => runGuarded(() async {
37
+ err('Missing subcommand. Use: install | serve | stop | status');
38
+ return;
39
+ });
40
+ }
41
+
42
+ class PocketBaseInstallCommand extends FlutterPatcherCommand {
43
+ @override
44
+ String get name => 'install';
45
+
46
+ @override
47
+ String get description => 'Download + extract the PocketBase binary.';
48
+
49
+ @override
50
+ ArgParser get argParser =>
51
+ ArgParser()..addOption('version', help: 'PocketBase version to install.');
52
+
53
+ @override
54
+ Future<int> run() => runGuarded(() async {
55
+ final version =
56
+ argResults!['version'] as String? ?? kDefaultPocketBaseVersion;
57
+ final installer = PocketBaseInstaller(version: version);
58
+ final paths = installer.paths();
59
+ step('Installing PocketBase v$version to ${paths.installDir.path}...');
60
+ final result = await installer.ensureInstalled(
61
+ paths: paths,
62
+ onProgress: (p) {
63
+ stdout.write('\r ${(p * 100).toStringAsFixed(0)}% ');
64
+ },
65
+ );
66
+ stdout.writeln();
67
+ if (result.alreadyInstalled) {
68
+ step('Already installed at ${paths.binaryPath.path}');
69
+ } else {
70
+ step(
71
+ 'Downloaded ${(result.bytesDownloaded / 1024 / 1024).toStringAsFixed(1)} MB',
72
+ );
73
+ step('Binary ready at ${paths.binaryPath.path}');
74
+ }
75
+ return;
76
+ });
77
+ }
78
+
79
+ class PocketBaseServeCommand extends FlutterPatcherCommand {
80
+ @override
81
+ String get name => 'serve';
82
+
83
+ @override
84
+ String get description =>
85
+ 'Start a local PocketBase + install the flutter_ota_kit schema.';
86
+
87
+ @override
88
+ ArgParser get argParser => ArgParser()
89
+ ..addOption('version', help: 'PocketBase version.')
90
+ ..addOption('port', help: 'PB HTTP port.', defaultsTo: '8090')
91
+ ..addOption('host', help: 'PB bind address.', defaultsTo: '127.0.0.1')
92
+ ..addOption('data-dir', help: 'PB data directory.')
93
+ ..addOption('admin-email', help: 'Bootstrap admin email.')
94
+ ..addOption('admin-password', help: 'Bootstrap admin password.')
95
+ ..addFlag(
96
+ 'install-hooks',
97
+ help: 'Copy bundled JS hooks into the data dir on start.',
98
+ defaultsTo: true,
99
+ );
100
+
101
+ @override
102
+ Future<int> run() => runGuarded(() async {
103
+ final r = argResults!;
104
+ final version = r['version'] as String? ?? kDefaultPocketBaseVersion;
105
+ final port = int.tryParse(r['port'] as String? ?? '8090') ?? 8090;
106
+ final host = r['host'] as String? ?? '127.0.0.1';
107
+ final dataDirOverride = r['data-dir'] as String?;
108
+ final adminEmail =
109
+ r['admin-email'] as String? ??
110
+ Platform.environment['POCKETBASE_ADMIN_EMAIL'];
111
+ final adminPassword =
112
+ r['admin-password'] as String? ??
113
+ Platform.environment['POCKETBASE_ADMIN_PASSWORD'];
114
+ final installHooks = r['install-hooks'] as bool? ?? true;
115
+
116
+ final paths = PocketBaseInstallPaths.resolve(version: version);
117
+ final dataDir = dataDirOverride != null
118
+ ? Directory(dataDirOverride)
119
+ : paths.installDir;
120
+ final installer = PocketBaseInstaller(version: version);
121
+ step('Ensuring PocketBase v$version is installed...');
122
+ await installer.ensureInstalled(paths: paths);
123
+ step('Binary at ${paths.binaryPath.path}');
124
+
125
+ if (installHooks) {
126
+ final bootstrap = PocketBaseDataBootstrap(
127
+ dataDir: dataDir,
128
+ hooksSourceDir: _hooksSourceDir(),
129
+ );
130
+ final installed = await bootstrap.install();
131
+ if (installed.isNotEmpty) {
132
+ step('Installed hooks: ${installed.join(', ')}');
133
+ }
134
+ }
135
+
136
+ final manager = PocketBaseProcessManager(
137
+ binaryPath: paths.binaryPath,
138
+ dataDir: dataDir,
139
+ port: port,
140
+ host: host,
141
+ );
142
+ step(
143
+ 'Starting PocketBase on http://$host:$port (data: ${dataDir.path})...',
144
+ );
145
+ await manager.start(adminEmail: adminEmail, adminPassword: adminPassword);
146
+ step('PocketBase is running (pid ${manager.process!.pid}).');
147
+
148
+ if (adminEmail != null && adminPassword != null) {
149
+ step('Installing flutter_ota_kit schema...');
150
+ final schemaInstaller = PocketBaseSchemaInstaller(
151
+ url: 'http://$host:$port',
152
+ adminEmail: adminEmail,
153
+ adminPassword: adminPassword,
154
+ );
155
+ try {
156
+ final res = await schemaInstaller.install();
157
+ if (res.created.isNotEmpty) {
158
+ step('Created collections: ${res.created.join(', ')}');
159
+ }
160
+ if (res.skipped.isNotEmpty) {
161
+ step('Already present: ${res.skipped.join(', ')}');
162
+ }
163
+ } catch (e) {
164
+ warn('Schema install failed: $e');
165
+ }
166
+ } else {
167
+ step('Skipping schema install (no --admin-email/--admin-password).');
168
+ }
169
+
170
+ banner('pocketbase');
171
+ box('flutter_ota_kit pocketbase serve', [
172
+ 'Local PocketBase is running:',
173
+ '',
174
+ kv('url', 'http://$host:$port'),
175
+ kv('admin-ui', 'http://$host:$port/_/'),
176
+ kv('data', dataDir.path),
177
+ kv('pid', '${manager.process!.pid}'),
178
+ ]);
179
+
180
+ // Wait for SIGINT/SIGTERM.
181
+ final done = Completer<void>();
182
+ ProcessSignal.sigint.watch().listen((_) => done.complete());
183
+ if (!Platform.isWindows) {
184
+ ProcessSignal.sigterm.watch().listen((_) => done.complete());
185
+ }
186
+ await done.future;
187
+ step('Shutting down PocketBase...');
188
+ await manager.stop();
189
+ step('PocketBase stopped.');
190
+ return;
191
+ });
192
+
193
+ static Directory _hooksSourceDir() {
194
+ final exe = Platform.script.toFilePath();
195
+ final pkgRoot = Directory(p.dirname(p.dirname(p.dirname(exe))));
196
+ return Directory(p.join(pkgRoot.path, 'lib', 'src', 'pocketbase', 'hooks'));
197
+ }
198
+ }
199
+
200
+ class PocketBaseStopCommand extends FlutterPatcherCommand {
201
+ @override
202
+ String get name => 'stop';
203
+
204
+ @override
205
+ String get description =>
206
+ 'Hint to stop a running PocketBase (Ctrl+C in serve).';
207
+
208
+ @override
209
+ Future<int> run() => runGuarded(() async {
210
+ step('Use Ctrl+C on the running serve process to stop PocketBase.');
211
+ return;
212
+ });
213
+ }
214
+
215
+ class PocketBaseStatusCommand extends FlutterPatcherCommand {
216
+ @override
217
+ String get name => 'status';
218
+
219
+ @override
220
+ String get description => 'Show the installed PocketBase version + path.';
221
+
222
+ @override
223
+ Future<int> run() => runGuarded(() async {
224
+ final paths = PocketBaseInstallPaths.resolve();
225
+ final exists = await paths.binaryPath.exists();
226
+ if (exists) {
227
+ step(
228
+ 'PocketBase v${paths.version}: installed at ${paths.binaryPath.path}',
229
+ );
230
+ } else {
231
+ step(
232
+ 'PocketBase: not installed. Run `flutter_ota_kit pocketbase install`.',
233
+ );
234
+ }
235
+ return;
236
+ });
237
+ }
@@ -22,44 +22,53 @@ class RollbackCommand extends FlutterPatcherCommand {
22
22
  ArgParser get argParser => ArgParser()
23
23
  ..addOption('backend', abbr: 'b', help: 'Backend provider.')
24
24
  ..addOption('channel', abbr: 'c', help: 'Channel to roll back.')
25
- ..addOption('bundle-id', abbr: 'i', help: 'Roll back to this specific bundle id.')
25
+ ..addOption(
26
+ 'bundle-id',
27
+ abbr: 'i',
28
+ help: 'Roll back to this specific bundle id.',
29
+ )
26
30
  ..addOption('platform', abbr: 'p', help: 'Platform filter (e.g. android).');
27
31
 
28
32
  @override
29
33
  Future<int> run() => runGuarded(() async {
30
- final channel = argResults!['channel'] as String?;
31
- if (channel == null || channel.isEmpty) {
32
- throw PackException(
33
- 'Usage: flutter_ota_kit rollback --channel <channel> [--bundle-id <id>]',
34
- 64,
35
- );
36
- }
37
- final bundleId = argResults!['bundle-id'] as String?;
38
- final platformRaw = argResults!['platform'] as String?;
39
- final platform =
40
- platformRaw == null ? null : Platform.fromValue(platformRaw);
34
+ final channel = argResults!['channel'] as String?;
35
+ if (channel == null || channel.isEmpty) {
36
+ throw PackException(
37
+ 'Usage: flutter_ota_kit rollback --channel <channel> [--bundle-id <id>]',
38
+ 64,
39
+ );
40
+ }
41
+ final bundleId = argResults!['bundle-id'] as String?;
42
+ final platformRaw = argResults!['platform'] as String?;
43
+ final platform = platformRaw == null
44
+ ? null
45
+ : Platform.fromValue(platformRaw);
41
46
 
42
- final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
43
- final backend = requireBackend(cfg, override: backendOverride);
47
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
48
+ final backend = requireBackend(cfg, override: backendOverride);
44
49
 
45
- banner('rollback');
46
- final (disabled, live) = await spinner(
47
- () async {
48
- if (bundleId != null && bundleId.isNotEmpty) {
49
- return rollbackToBundle(backend, channel, bundleId,
50
- platform: platform);
51
- }
52
- final id = await rollbackChannel(backend, channel);
53
- final nowLive = (await getChannel(backend, channel))?.id ?? '';
54
- return (<String>[id], nowLive);
55
- },
56
- 'Rolling back channel "$channel"',
57
- done: 'Rolled back',
58
- );
59
- box('rollback', [
60
- kv('channel', channel),
61
- kv('now live', cyan(live)),
62
- kv('disabled', disabled.isEmpty ? dim('(none)') : disabled.join(', ')),
63
- ]);
64
- });
50
+ banner('rollback');
51
+ final (disabled, live) = await spinner(
52
+ () async {
53
+ if (bundleId != null && bundleId.isNotEmpty) {
54
+ return rollbackToBundle(
55
+ backend,
56
+ channel,
57
+ bundleId,
58
+ platform: platform,
59
+ );
60
+ }
61
+ final id = await rollbackChannel(backend, channel);
62
+ final nowLive = (await getChannel(backend, channel))?.id ?? '';
63
+ return (<String>[id], nowLive);
64
+ },
65
+ 'Rolling back channel "$channel"',
66
+ done: 'Rolled back',
67
+ );
68
+ box('rollback', [
69
+ kv('channel', channel),
70
+ kv('now live', cyan(live)),
71
+ kv('disabled', disabled.isEmpty ? dim('(none)') : disabled.join(', ')),
72
+ ]);
73
+ });
65
74
  }