@_nazmiforreal/flutter-ota 0.1.6 → 0.1.7

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 (147) hide show
  1. package/README.md +13 -13
  2. package/bin/{flutter-patcher-linux-x64 → flutter-ota-linux-x64} +0 -0
  3. package/bin/{flutter-patcher.js → flutter-ota.js} +4 -4
  4. package/dart-src/packages/cli-tools/bin/{flutter_patcher.dart → flutter_ota_kit.dart} +2 -2
  5. package/dart-src/packages/cli-tools/lib/{flutter_patcher_cli.dart → flutter_ota_kit_cli.dart} +5 -3
  6. package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -25
  7. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +10 -4
  8. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +5 -3
  9. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +279 -10
  10. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +7 -7
  11. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +5 -5
  12. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +4 -4
  13. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -4
  14. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +3 -3
  15. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +1 -1
  16. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +294 -45
  17. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +1 -1
  18. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +59 -24
  19. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +5 -5
  20. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +117 -0
  21. package/dart-src/packages/cli-tools/lib/src/config.dart +29 -78
  22. package/dart-src/packages/cli-tools/lib/src/operations.dart +4 -4
  23. package/dart-src/packages/cli-tools/lib/src/pack.dart +12 -6
  24. package/dart-src/packages/cli-tools/lib/src/runner.dart +21 -3
  25. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
  26. package/dart-src/packages/cli-tools/lib/src/verbose.dart +3 -0
  27. package/dart-src/packages/cli-tools/migrations/aws/README.md +7 -0
  28. package/dart-src/packages/cli-tools/migrations/postgres/bundles.sql +45 -0
  29. package/dart-src/packages/cli-tools/migrations/postgres/get_target_app_version_list.sql +21 -0
  30. package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_app_version.sql +126 -0
  31. package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
  32. package/dart-src/packages/cli-tools/migrations/postgres/hash_user_id.sql +30 -0
  33. package/dart-src/packages/cli-tools/migrations/postgres/is_device_eligible.sql +237 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +11 -0
  35. package/dart-src/packages/cli-tools/pubspec.yaml +8 -10
  36. package/dart-src/packages/cli-tools/test/backends_test.dart +4 -4
  37. package/dart-src/packages/cli-tools/test/build_pack_test.dart +1 -1
  38. package/dart-src/packages/cli-tools/test/cli_test.dart +3 -3
  39. package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
  40. package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
  41. package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +7 -7
  42. package/dart-src/packages/core/lib/{flutter_patcher_core.dart → flutter_ota_kit_core.dart} +1 -1
  43. package/dart-src/packages/core/pubspec.yaml +2 -2
  44. package/dart-src/packages/core/test/rollout_test.dart +1 -1
  45. package/dart-src/packages/core/test/semver_test.dart +1 -1
  46. package/dart-src/packages/core/test/types_test.dart +2 -2
  47. package/dart-src/packages/core/test/uuid_test.dart +1 -1
  48. package/dart-src/packages/core/tool/dbg.dart +1 -1
  49. package/dart-src/plugins/aws/lib/{flutter_patcher_aws.dart → flutter_ota_kit_aws.dart} +0 -2
  50. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +1 -1
  51. package/dart-src/plugins/aws/lib/src/aws_database.dart +1 -1
  52. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +1 -1
  53. package/dart-src/plugins/aws/lib/src/aws_storage.dart +1 -1
  54. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +25 -3
  55. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +1 -1
  56. package/dart-src/plugins/aws/pubspec.yaml +4 -4
  57. package/dart-src/plugins/aws/test/aws_database_test.dart +3 -3
  58. package/dart-src/plugins/aws/test/aws_storage_test.dart +2 -2
  59. package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +2 -2
  60. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +1 -1
  61. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +1 -1
  62. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +1 -1
  63. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +1 -1
  64. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +2 -2
  65. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +1 -1
  66. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +1 -1
  67. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +1 -1
  68. package/dart-src/plugins/cloudflare/pubspec.yaml +4 -4
  69. package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +2 -2
  70. package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +2 -2
  71. package/dart-src/plugins/cloudflare/test/d1_database_test.dart +2 -2
  72. package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +2 -2
  73. package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +2 -2
  74. package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +2 -2
  75. package/dart-src/plugins/plugin-core/lib/{flutter_patcher_plugin_core.dart → flutter_ota_kit_plugin_core.dart} +1 -1
  76. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +1 -1
  77. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +1 -1
  78. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +1 -1
  79. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +1 -1
  80. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +1 -1
  81. package/dart-src/plugins/plugin-core/lib/src/types.dart +2 -2
  82. package/dart-src/plugins/plugin-core/pubspec.yaml +3 -3
  83. package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +3 -3
  84. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +2 -2
  85. package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +2 -2
  86. package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +2 -2
  87. package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +5 -5
  88. package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +2 -2
  89. package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +2 -2
  90. package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +2 -2
  91. package/dart-src/plugins/plugin-core/test/helpers_test.dart +2 -2
  92. package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +2 -2
  93. package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +3 -3
  94. package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +1 -1
  95. package/dart-src/plugins/postgres/lib/{flutter_patcher_postgres.dart → flutter_ota_kit_postgres.dart} +1 -1
  96. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +1 -1
  97. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +2 -2
  98. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +2 -2
  99. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +9 -9
  100. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +1 -1
  101. package/dart-src/plugins/postgres/pubspec.yaml +4 -4
  102. package/dart-src/plugins/postgres/test/mock_postgres_client.dart +2 -2
  103. package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +2 -2
  104. package/dart-src/plugins/postgres/test/postgres_database_test.dart +2 -2
  105. package/dart-src/plugins/supabase/lib/edge.dart +1 -1
  106. package/dart-src/plugins/supabase/lib/{flutter_patcher_supabase.dart → flutter_ota_kit_supabase.dart} +1 -1
  107. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +1 -1
  108. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +5 -1
  109. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +2 -2
  110. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +1 -1
  111. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +1 -1
  112. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +2 -5
  113. package/dart-src/plugins/supabase/pubspec.yaml +4 -4
  114. package/dart-src/plugins/supabase/test/mock_supabase_client.dart +5 -5
  115. package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +3 -3
  116. package/dart-src/plugins/supabase/test/supabase_database_test.dart +3 -3
  117. package/dart-src/plugins/supabase/test/supabase_storage_test.dart +2 -2
  118. package/migrations/aws/README.md +7 -0
  119. package/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  120. package/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  121. package/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  122. package/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  123. package/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  124. package/migrations/cloudflare/bundles.sql +42 -0
  125. package/migrations/postgres/bundles.sql +45 -0
  126. package/migrations/postgres/get_target_app_version_list.sql +21 -0
  127. package/migrations/postgres/get_update_info_by_app_version.sql +126 -0
  128. package/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
  129. package/migrations/postgres/hash_user_id.sql +30 -0
  130. package/migrations/postgres/is_device_eligible.sql +237 -0
  131. package/package.json +4 -4
  132. package/scripts/postinstall.js +15 -15
  133. package/scripts/vendor.sh +1 -1
  134. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -496
  135. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -743
  136. package/dart-src/packages/cli-tools/pubspec.lock +0 -638
  137. package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +0 -315
  138. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +0 -72
  139. package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +0 -190
  140. package/dart-src/plugins/standalone/.gitkeep +0 -0
  141. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +0 -12
  142. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +0 -107
  143. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +0 -233
  144. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +0 -175
  145. package/dart-src/plugins/standalone/pubspec.yaml +0 -18
  146. package/dart-src/plugins/standalone/test/standalone_test.dart +0 -264
  147. /package/dart-src/plugins/cloudflare/lib/{flutter_patcher_cloudflare.dart → flutter_ota_kit_cloudflare.dart} +0 -0
@@ -2,14 +2,14 @@ import 'dart:convert';
2
2
  import 'dart:io';
3
3
 
4
4
  import 'package:args/args.dart';
5
- import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
5
+ import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
6
6
  import 'package:http/http.dart' as http;
7
7
  import 'package:path/path.dart' as p;
8
8
  import 'package:postgres/postgres.dart';
9
9
 
10
10
  import '../ui/ui.dart';
11
11
 
12
- /// `flutter_patcher migrate` — run backend SQL migrations.
12
+ /// `flutter_ota_kit migrate` — run backend SQL migrations.
13
13
  class MigrateCommand extends FlutterPatcherCommand {
14
14
  @override
15
15
  String get name => 'migrate';
@@ -28,13 +28,17 @@ class MigrateCommand extends FlutterPatcherCommand {
28
28
  'runs migrations without a separate Postgres connection.')
29
29
  ..addOption('migrations-dir',
30
30
  help: 'Directory of *.sql migration files (ordered by name).')
31
- ..addFlag('dry-run', help: 'Print migrations instead of applying them.');
31
+ ..addFlag('dry-run', abbr: 'd', help: 'Print migrations instead of applying them.');
32
32
 
33
- /// Default migrations dir lives next to the CLI entrypoint; the backend
34
- /// selects which dialect's migrations are used (cloudflare = D1 SQLite).
33
+ /// Default migrations dir lives next to the CLI entrypoint; each backend has
34
+ /// its own dialect's migrations (cloudflare = D1 SQLite, postgres = plain
35
+ /// Postgres, supabase = PostgREST, aws = no relational SQL).
35
36
  String defaultMigrationsDir(String provider) {
36
37
  final sub = switch (provider) {
38
+ 'supabase' => 'supabase',
39
+ 'postgres' => 'postgres',
37
40
  'cloudflare' => 'cloudflare',
41
+ 'aws' => 'aws',
38
42
  _ => 'supabase',
39
43
  };
40
44
  return p.join(p.dirname(Platform.script.path), '..', 'migrations', sub);
@@ -68,6 +72,23 @@ class MigrateCommand extends FlutterPatcherCommand {
68
72
  @override
69
73
  Future<int> run() => runGuarded(() async {
70
74
  final provider = argResults!['backend'] as String? ?? 'supabase';
75
+
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
+ }
91
+
71
92
  final dir = argResults!['migrations-dir'] as String? ??
72
93
  defaultMigrationsDir(provider);
73
94
  final files = _listMigrations(dir);
@@ -81,23 +102,30 @@ class MigrateCommand extends FlutterPatcherCommand {
81
102
  return;
82
103
  }
83
104
 
84
- if (provider == 'cloudflare' || provider == 'aws') {
105
+ if (provider == 'cloudflare') {
85
106
  banner('migrate');
86
107
  box('migrate', [
87
- 'Migrations for "$provider" are applied via its platform tooling:',
88
- ' • cloudflare → `wrangler d1 execute`',
89
- ' • aws → AWS console (S3/DynamoDB)',
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)',
90
111
  '',
91
- 'The SQL files under $dir show the intended schema.',
112
+ 'The SQL files under $dir show the intended D1 schema.',
92
113
  ]);
93
114
  return;
94
115
  }
95
116
 
96
117
  if (provider == 'supabase') {
118
+ final cfg = loadConfig() ??
119
+ FlutterPatcherConfig(
120
+ provider: 'supabase',
121
+ supabase: const SupabaseConfigJson(),
122
+ );
97
123
  final mgmtKey = argResults!['management-key'] as String? ??
98
- Platform.environment['SUPABASE_MANAGEMENT_KEY'];
124
+ Platform.environment['SUPABASE_MANAGEMENT_KEY'] ??
125
+ cfg.supabase.managementKey;
99
126
  final pgUrl = argResults!['database-url'] as String? ??
100
- Platform.environment['DATABASE_URL'];
127
+ Platform.environment['DATABASE_URL'] ??
128
+ cfg.supabase.databaseUrl;
101
129
  if (mgmtKey != null && mgmtKey.isNotEmpty) {
102
130
  await _runViaManagementApi(mgmtKey, files);
103
131
  return;
@@ -108,19 +136,26 @@ class MigrateCommand extends FlutterPatcherCommand {
108
136
  }
109
137
  throw StateError(
110
138
  'For the supabase backend, provide either:\n'
111
- ' • --management-key (or SUPABASE_MANAGEMENT_KEY) — a Supabase '
112
- 'Management API key, OR\n'
113
- ' • --database-url (or DATABASE_URL) — a Postgres connection string.',
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.',
114
143
  );
115
144
  }
116
145
 
117
- // Any other provider (e.g. postgres) requires a Postgres connection.
118
- final pgUrl = argResults!['database-url'] as String? ??
119
- Platform.environment['DATABASE_URL'];
120
- if (pgUrl == null || pgUrl.isEmpty) {
121
- throw StateError('Provide --database-url or set DATABASE_URL.');
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;
122
156
  }
123
- await _runViaPostgres(pgUrl, files);
157
+
158
+ throw StateError('Unknown backend provider: "$provider".');
124
159
  });
125
160
 
126
161
  /// Run migrations through the Supabase Management API (no Postgres creds
@@ -220,7 +255,7 @@ class MigrateCommand extends FlutterPatcherCommand {
220
255
  );
221
256
  try {
222
257
  await conn.execute(
223
- 'CREATE TABLE IF NOT EXISTS _flutter_patcher_migrations '
258
+ 'CREATE TABLE IF NOT EXISTS _flutter_ota_kit_migrations '
224
259
  '(name text primary key, applied_at timestamptz default now())',
225
260
  queryMode: QueryMode.simple,
226
261
  );
@@ -228,7 +263,7 @@ class MigrateCommand extends FlutterPatcherCommand {
228
263
  final name = p.basename(file.path);
229
264
  final escaped = name.replaceAll("'", "''");
230
265
  final existing = await conn.execute(
231
- "SELECT 1 FROM _flutter_patcher_migrations WHERE name = '$escaped'",
266
+ "SELECT 1 FROM _flutter_ota_kit_migrations WHERE name = '$escaped'",
232
267
  queryMode: QueryMode.simple,
233
268
  );
234
269
  if (existing.isNotEmpty) {
@@ -239,7 +274,7 @@ class MigrateCommand extends FlutterPatcherCommand {
239
274
  await conn.execute(stmt, queryMode: QueryMode.simple);
240
275
  }
241
276
  await conn.execute(
242
- "INSERT INTO _flutter_patcher_migrations(name) VALUES ('$escaped')",
277
+ "INSERT INTO _flutter_ota_kit_migrations(name) VALUES ('$escaped')",
243
278
  queryMode: QueryMode.simple,
244
279
  );
245
280
  step('applied $name');
@@ -1,10 +1,10 @@
1
1
  import 'package:args/args.dart';
2
- import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
3
- import 'package:flutter_patcher_core/flutter_patcher_core.dart';
2
+ import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
3
+ import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart';
4
4
 
5
5
  import '../ui/ui.dart';
6
6
 
7
- /// `flutter_patcher rollback` — roll a channel back to a previous bundle.
7
+ /// `flutter_ota_kit rollback` — roll a channel back to a previous bundle.
8
8
  class RollbackCommand extends FlutterPatcherCommand {
9
9
  RollbackCommand({this.config, this.backendOverride});
10
10
 
@@ -22,7 +22,7 @@ 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', help: 'Roll back to this specific bundle id.')
25
+ ..addOption('bundle-id', abbr: 'i', help: 'Roll back to this specific bundle id.')
26
26
  ..addOption('platform', abbr: 'p', help: 'Platform filter (e.g. android).');
27
27
 
28
28
  @override
@@ -30,7 +30,7 @@ class RollbackCommand extends FlutterPatcherCommand {
30
30
  final channel = argResults!['channel'] as String?;
31
31
  if (channel == null || channel.isEmpty) {
32
32
  throw PackException(
33
- 'Usage: flutter_patcher rollback --channel <channel> [--bundle-id <id>]',
33
+ 'Usage: flutter_ota_kit rollback --channel <channel> [--bundle-id <id>]',
34
34
  64,
35
35
  );
36
36
  }
@@ -0,0 +1,117 @@
1
+ import 'package:args/args.dart';
2
+ import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
3
+
4
+ import '../ui/ui.dart';
5
+
6
+ /// `flutter_ota_kit storage` — inspect and clean the blob storage bucket
7
+ /// directly from the CLI (no need to open the provider dashboard).
8
+ class StorageCommand extends FlutterPatcherCommand {
9
+ StorageCommand({this.config, this.backendOverride}) {
10
+ addSubcommand(StorageListCommand(config: config, backendOverride: backendOverride));
11
+ addSubcommand(StorageDeleteCommand(config: config, backendOverride: backendOverride));
12
+ }
13
+
14
+ final FlutterPatcherConfig? config;
15
+ final Backend? backendOverride;
16
+
17
+ @override
18
+ String get name => 'storage';
19
+
20
+ @override
21
+ String get description =>
22
+ 'Inspect and manage bundle storage objects (find / delete orphans).';
23
+ }
24
+
25
+ class StorageListCommand extends FlutterPatcherCommand {
26
+ StorageListCommand({this.config, this.backendOverride});
27
+
28
+ final FlutterPatcherConfig? config;
29
+ final Backend? backendOverride;
30
+
31
+ @override
32
+ String get name => 'list';
33
+
34
+ @override
35
+ String get description => 'List storage objects.';
36
+
37
+ @override
38
+ ArgParser get argParser => ArgParser()
39
+ ..addOption('backend', abbr: 'b', help: 'Backend provider.')
40
+ ..addOption('prefix', help: 'Key prefix filter (e.g. bundles).');
41
+
42
+ @override
43
+ Future<int> run() => runGuarded(() async {
44
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
45
+ final backend = requireBackend(cfg, override: backendOverride);
46
+ final prefix = argResults!['prefix'] as String?;
47
+ banner('storage · list');
48
+ final objects = await backend.storage.listObjects(
49
+ prefix == null || prefix.isEmpty ? null : prefix,
50
+ );
51
+ if (objects.isEmpty) {
52
+ step('(no objects)');
53
+ return;
54
+ }
55
+ final lines = <String>[];
56
+ for (final o in objects) {
57
+ final size = o.size >= 1024 * 1024
58
+ ? '${(o.size / (1024 * 1024)).toStringAsFixed(2)} MB'
59
+ : o.size >= 1024
60
+ ? '${(o.size / 1024).toStringAsFixed(1)} KB'
61
+ : '${o.size} B';
62
+ lines
63
+ ..add(kv('key', cyan(o.key)))
64
+ ..add(kv('size', size))
65
+ ..add('');
66
+ }
67
+ box('${objects.length} objects', lines);
68
+ });
69
+ }
70
+
71
+ class StorageDeleteCommand extends FlutterPatcherCommand {
72
+ StorageDeleteCommand({this.config, this.backendOverride});
73
+
74
+ final FlutterPatcherConfig? config;
75
+ final Backend? backendOverride;
76
+
77
+ @override
78
+ String get name => 'delete';
79
+
80
+ @override
81
+ String get description => 'Delete storage object(s) by key or full URI.';
82
+
83
+ @override
84
+ ArgParser get argParser => ArgParser()
85
+ ..addOption('backend', abbr: 'b', help: 'Backend provider.')
86
+ ..addMultiOption('key', help: 'Storage key to delete (repeatable).')
87
+ ..addOption('uri', help: 'Full storage URI to delete.');
88
+
89
+ @override
90
+ Future<int> run() => runGuarded(() async {
91
+ final keys = (argResults!['key'] as List<String>?) ?? <String>[];
92
+ final uri = argResults!['uri'] as String?;
93
+ if (keys.isEmpty && (uri == null || uri.isEmpty)) {
94
+ throw PackException(
95
+ 'Usage: flutter-ota storage delete --key <key> [--key <key>...] '
96
+ '| --uri <storageUri>',
97
+ 64,
98
+ );
99
+ }
100
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
101
+ final backend = requireBackend(cfg, override: backendOverride);
102
+ banner('storage · delete');
103
+ if (keys.isNotEmpty) {
104
+ await spinner(
105
+ () => backend.storage.deleteObjects(keys),
106
+ 'Deleting ${keys.length} object(s)',
107
+ done: 'Deleted',
108
+ );
109
+ } else {
110
+ await spinner(
111
+ () => backend.storage.delete(uri!),
112
+ 'Deleting $uri',
113
+ done: 'Deleted',
114
+ );
115
+ }
116
+ });
117
+ }
@@ -1,19 +1,20 @@
1
1
  import 'dart:convert';
2
2
  import 'dart:io';
3
3
 
4
- import 'package:flutter_patcher_aws/flutter_patcher_aws.dart';
4
+ import 'package:flutter_ota_kit_aws/flutter_ota_kit_aws.dart';
5
5
  import 'package:postgres/postgres.dart';
6
- import 'package:flutter_patcher_cloudflare/flutter_patcher_cloudflare.dart';
7
- import 'package:flutter_patcher_postgres/flutter_patcher_postgres.dart';
8
- import 'package:flutter_patcher_standalone/flutter_patcher_standalone.dart';
9
- import 'package:flutter_patcher_supabase/flutter_patcher_supabase.dart';
6
+ import 'package:flutter_ota_kit_cloudflare/flutter_ota_kit_cloudflare.dart';
7
+ import 'package:flutter_ota_kit_postgres/flutter_ota_kit_postgres.dart';
8
+
9
+ import 'package:flutter_ota_kit_supabase/flutter_ota_kit_supabase.dart';
10
10
  import 'package:path/path.dart' as p;
11
11
 
12
- /// Resolved configuration for the flutter_patcher CLI.
12
+ /// Resolved configuration for the flutter_ota_kit CLI.
13
13
  ///
14
14
  /// Mirrors hot-updater's config precedence: explicit flags > environment
15
- /// variables > the project `.flutter_patcher.json` (falling back to the global
16
- /// `~/.flutter_patcher/config.json`).
15
+ /// variables > the project `.otat/config.json` (falling back to the global
16
+ /// `~/.otat/config.json`). All CLI working state lives under `.otat/` so it can
17
+ /// be git-ignored as a single unit.
17
18
  class FlutterPatcherConfig {
18
19
  FlutterPatcherConfig({
19
20
  required this.provider,
@@ -21,7 +22,6 @@ class FlutterPatcherConfig {
21
22
  this.postgres = const PostgresConfigJson(),
22
23
  this.cloudflare = const CloudflareConfigJson(),
23
24
  this.aws = const AwsConfigJson(),
24
- this.standalone = const StandaloneConfigJson(),
25
25
  this.channel = 'production',
26
26
  this.platform = 'android',
27
27
  this.source = './dist',
@@ -43,9 +43,6 @@ class FlutterPatcherConfig {
43
43
  aws: AwsConfigJson.fromJson(
44
44
  (json['aws'] as Map?)?.cast<String, dynamic>() ?? {},
45
45
  ),
46
- standalone: StandaloneConfigJson.fromJson(
47
- (json['standalone'] as Map?)?.cast<String, dynamic>() ?? {},
48
- ),
49
46
  channel: json['channel'] as String? ?? 'production',
50
47
  platform: json['platform'] as String? ?? 'android',
51
48
  source: json['source'] as String? ?? './dist',
@@ -57,7 +54,6 @@ class FlutterPatcherConfig {
57
54
  final PostgresConfigJson postgres;
58
55
  final CloudflareConfigJson cloudflare;
59
56
  final AwsConfigJson aws;
60
- final StandaloneConfigJson standalone;
61
57
  final String channel;
62
58
  final String platform;
63
59
  final String source;
@@ -69,7 +65,6 @@ class FlutterPatcherConfig {
69
65
  'postgres': postgres.toJson(),
70
66
  'cloudflare': cloudflare.toJson(),
71
67
  'aws': aws.toJson(),
72
- 'standalone': standalone.toJson(),
73
68
  'channel': channel,
74
69
  'platform': platform,
75
70
  'source': source,
@@ -82,7 +77,6 @@ class FlutterPatcherConfig {
82
77
  PostgresConfigJson? postgres,
83
78
  CloudflareConfigJson? cloudflare,
84
79
  AwsConfigJson? aws,
85
- StandaloneConfigJson? standalone,
86
80
  String? channel,
87
81
  String? platform,
88
82
  String? source,
@@ -94,7 +88,6 @@ class FlutterPatcherConfig {
94
88
  postgres: postgres ?? this.postgres,
95
89
  cloudflare: cloudflare ?? this.cloudflare,
96
90
  aws: aws ?? this.aws,
97
- standalone: standalone ?? this.standalone,
98
91
  channel: channel ?? this.channel,
99
92
  platform: platform ?? this.platform,
100
93
  source: source ?? this.source,
@@ -110,6 +103,8 @@ class SupabaseConfigJson {
110
103
  this.anonKey,
111
104
  this.bucket,
112
105
  this.basePath,
106
+ this.managementKey,
107
+ this.databaseUrl,
113
108
  });
114
109
 
115
110
  factory SupabaseConfigJson.fromJson(Map<String, dynamic> json) =>
@@ -119,6 +114,8 @@ class SupabaseConfigJson {
119
114
  anonKey: json['anonKey'] as String?,
120
115
  bucket: json['bucket'] as String?,
121
116
  basePath: json['basePath'] as String?,
117
+ managementKey: json['managementKey'] as String?,
118
+ databaseUrl: json['databaseUrl'] as String?,
122
119
  );
123
120
 
124
121
  final String? url;
@@ -126,6 +123,8 @@ class SupabaseConfigJson {
126
123
  final String? anonKey;
127
124
  final String? bucket;
128
125
  final String? basePath;
126
+ final String? managementKey;
127
+ final String? databaseUrl;
129
128
 
130
129
  Map<String, dynamic> toJson() => {
131
130
  if (url != null) 'url': url,
@@ -133,16 +132,20 @@ class SupabaseConfigJson {
133
132
  if (anonKey != null) 'anonKey': anonKey,
134
133
  if (bucket != null) 'bucket': bucket,
135
134
  if (basePath != null) 'basePath': basePath,
135
+ if (managementKey != null) 'managementKey': managementKey,
136
+ if (databaseUrl != null) 'databaseUrl': databaseUrl,
136
137
  };
137
138
  }
138
139
 
139
- /// Search order for a config file on disk.
140
+ /// Search order for a config file on disk. Both the project config and the
141
+ /// global config live under a `.otat/` directory so all CLI state is grouped
142
+ /// in one place (and can be git-ignored together).
140
143
  List<File> configCandidates() {
141
- final cwd = File(p.join(Directory.current.path, '.flutter_patcher.json'));
144
+ final cwd = File(p.join(Directory.current.path, '.otat', 'config.json'));
142
145
  final home = Platform.environment['HOME'] ??
143
146
  Platform.environment['USERPROFILE'] ??
144
147
  '';
145
- final global = File(p.join(home, '.flutter_patcher', 'config.json'));
148
+ final global = File(p.join(home, '.otat', 'config.json'));
146
149
  return [cwd, global];
147
150
  }
148
151
 
@@ -162,7 +165,7 @@ FlutterPatcherConfig? loadConfig() {
162
165
  }
163
166
 
164
167
  /// Persist a config. By default writes the project file (cwd); pass
165
- /// [global: true] to write the global `~/.flutter_patcher/config.json`.
168
+ /// [global: true] to write the global `~/.flutter_ota_kit/config.json`.
166
169
  void saveConfig(FlutterPatcherConfig config, {bool global = false}) {
167
170
  final file = global ? configCandidates().last : configCandidates().first;
168
171
  file.parent.createSync(recursive: true);
@@ -228,7 +231,7 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
228
231
  if (resolvedUrl == null || resolvedUrl.isEmpty) {
229
232
  throw StateError(
230
233
  'Supabase URL is required. Set it via --url, SUPABASE_URL, or '
231
- '`flutter_patcher config set supabase.url <url>`.',
234
+ '`flutter_ota_kit config set supabase.url <url>`.',
232
235
  );
233
236
  }
234
237
 
@@ -368,7 +371,7 @@ PostgresConfig resolvePostgresDatabaseConfig(
368
371
  if (resolvedHost == null || resolvedHost.isEmpty) {
369
372
  throw StateError(
370
373
  'Postgres host is required. Set --pg-host, POSTGRES_HOST, or '
371
- '`flutter_patcher config set postgres.host <host>`.',
374
+ '`flutter_ota_kit config set postgres.host <host>`.',
372
375
  );
373
376
  }
374
377
 
@@ -485,7 +488,7 @@ D1DatabaseConfig resolveCloudflareDatabaseConfig(
485
488
  resolvedToken == null) {
486
489
  throw StateError(
487
490
  'Cloudflare requires accountId, d1DatabaseId and apiToken. Set them via '
488
- 'flags, CLOUDFLARE_* env vars, or `flutter_patcher config set cloudflare.*`.',
491
+ 'flags, CLOUDFLARE_* env vars, or `flutter_ota_kit config set cloudflare.*`.',
489
492
  );
490
493
  }
491
494
 
@@ -526,7 +529,7 @@ R2S3StorageConfig resolveCloudflareStorageConfig(
526
529
  resolvedSecret == null) {
527
530
  throw StateError(
528
531
  'Cloudflare R2 requires accountId, accessKeyId and secretAccessKey. Set '
529
- 'them via flags, R2_* env vars, or `flutter_patcher config set cloudflare.*`.',
532
+ 'them via flags, R2_* env vars, or `flutter_ota_kit config set cloudflare.*`.',
530
533
  );
531
534
  }
532
535
 
@@ -612,7 +615,7 @@ S3DatabaseConfig resolveAwsDatabaseConfig(
612
615
  resolvedSecret == null) {
613
616
  throw StateError(
614
617
  'AWS requires bucket, region, accessKeyId and secretAccessKey. Set them '
615
- 'via flags, AWS_* env vars, or `flutter_patcher config set aws.*`.',
618
+ 'via flags, AWS_* env vars, or `flutter_ota_kit config set aws.*`.',
616
619
  );
617
620
  }
618
621
 
@@ -656,7 +659,7 @@ AwsS3StorageConfig resolveAwsStorageConfig(
656
659
  resolvedSecret == null) {
657
660
  throw StateError(
658
661
  'AWS requires bucket, region, accessKeyId and secretAccessKey. Set them '
659
- 'via flags, AWS_* env vars, or `flutter_patcher config set aws.*`.',
662
+ 'via flags, AWS_* env vars, or `flutter_ota_kit config set aws.*`.',
660
663
  );
661
664
  }
662
665
 
@@ -671,55 +674,3 @@ AwsS3StorageConfig resolveAwsStorageConfig(
671
674
  clientFactory: clientFactory ?? defaultAwsS3ClientFactory,
672
675
  );
673
676
  }
674
-
675
- /// Standalone (self-hosted) section of [FlutterPatcherConfig].
676
- class StandaloneConfigJson {
677
- const StandaloneConfigJson({this.baseUrl});
678
-
679
- factory StandaloneConfigJson.fromJson(Map<String, dynamic> json) =>
680
- StandaloneConfigJson(baseUrl: json['baseUrl'] as String?);
681
-
682
- final String? baseUrl;
683
-
684
- Map<String, dynamic> toJson() =>
685
- {if (baseUrl != null) 'baseUrl': baseUrl};
686
- }
687
-
688
- /// Resolve the standalone repository (database) config.
689
- StandaloneRepositoryConfig resolveStandaloneDatabaseConfig(
690
- FlutterPatcherConfig config, {
691
- String? baseUrl,
692
- StandaloneClientFactory? clientFactory,
693
- }) {
694
- final env = Platform.environment;
695
- final resolvedBaseUrl =
696
- baseUrl ?? env['STANDALONE_BASE_URL'] ?? config.standalone.baseUrl;
697
- if (resolvedBaseUrl == null || resolvedBaseUrl.isEmpty) {
698
- throw StateError(
699
- 'Standalone baseUrl is required. Set --base-url, STANDALONE_BASE_URL, '
700
- 'or `flutter_patcher config set standalone.baseUrl <url>`.',
701
- );
702
- }
703
- return StandaloneRepositoryConfig(
704
- baseUrl: resolvedBaseUrl,
705
- clientFactory: clientFactory,
706
- );
707
- }
708
-
709
- /// Resolve the standalone storage config (shares the same self-hosted base).
710
- StandaloneStorageConfig resolveStandaloneStorageConfig(
711
- FlutterPatcherConfig config, {
712
- String? baseUrl,
713
- StandaloneClientFactory? clientFactory,
714
- }) {
715
- final db = resolveStandaloneDatabaseConfig(
716
- config,
717
- baseUrl: baseUrl,
718
- clientFactory: clientFactory,
719
- );
720
- return StandaloneStorageConfig(
721
- baseUrl: db.baseUrl,
722
- commonHeaders: db.commonHeaders,
723
- clientFactory: clientFactory,
724
- );
725
- }
@@ -2,8 +2,8 @@ import 'dart:convert';
2
2
  import 'dart:io';
3
3
 
4
4
  import 'package:crypto/crypto.dart';
5
- import 'package:flutter_patcher_core/flutter_patcher_core.dart';
6
- import 'package:flutter_patcher_plugin_core/flutter_patcher_plugin_core.dart';
5
+ import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart';
6
+ import 'package:flutter_ota_kit_plugin_core/flutter_ota_kit_plugin_core.dart';
7
7
 
8
8
  import 'backend.dart';
9
9
  import 'sign.dart';
@@ -61,7 +61,7 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
61
61
  );
62
62
  }
63
63
  // Resolve the artifact to upload. If the source is already a zip (e.g. the
64
- // `patch.zip` produced by `flutter_patcher build`/`pack`), upload it directly.
64
+ // `patch.zip` produced by `flutter_ota_kit build`/`pack`), upload it directly.
65
65
  // Otherwise, if the source directory contains a `patch.zip`, use that. As a
66
66
  // fallback, zip the source directory (for pre-built bundle layouts).
67
67
  final sourceFile = File(opts.source);
@@ -93,7 +93,7 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
93
93
  : null;
94
94
 
95
95
  final bundleId = opts.bundleId ?? uuidV7();
96
- final key = backend.storageKeyFor(bundleId, 'patch.zip');
96
+ final key = bundleId;
97
97
  final uploaded = await backend.storage.upload(key, zipPath);
98
98
  final storageUri = uploaded['storageUri'];
99
99
  if (storageUri == null || storageUri.isEmpty) {
@@ -33,7 +33,7 @@ class PackResult {
33
33
  /// Pack a release APK into a device-compatible OTA patch (`patch.zip`).
34
34
  ///
35
35
  /// Mirrors the standalone `bin/pack.dart` CLI, but as a library usable by the
36
- /// `flutter_patcher build` command. Includes **every** ABI found in the APK so
36
+ /// `flutter_ota_kit build` command. Includes **every** ABI found in the APK so
37
37
  /// a single bundle serves all device ABIs (the device SDK picks its ABI).
38
38
  Future<PackResult> packPatch({
39
39
  required String apkPath,
@@ -73,6 +73,7 @@ Future<PackResult> packPatch({
73
73
  version: version,
74
74
  targetVersionCode: targetVersionCode,
75
75
  requestedAssets: requestedAssets,
76
+ abi: abi,
76
77
  onProgress: onProgress,
77
78
  );
78
79
  return result;
@@ -85,8 +86,13 @@ PackResult _writePatchPackage({
85
86
  required String version,
86
87
  required int targetVersionCode,
87
88
  required List<String> requestedAssets,
89
+ String? abi,
88
90
  void Function(int done, int total, String label)? onProgress,
89
91
  }) {
92
+ // When an ABI is requested, restrict the patch to only that ABI instead of
93
+ // shipping every architecture found in the APK.
94
+ final effectiveLibs =
95
+ abi == null ? libs : libs.where((e) => e.$1 == abi).toList();
90
96
  final patchFiles = <String, _PatchAssetFile>{};
91
97
  final operations = <Map<String, dynamic>>[];
92
98
  var baseManifestSize = 0;
@@ -144,7 +150,7 @@ PackResult _writePatchPackage({
144
150
  }
145
151
 
146
152
  final libManifest = <String, dynamic>{
147
- for (final (currentAbi, soBytes) in libs) currentAbi: {
153
+ for (final (currentAbi, soBytes) in effectiveLibs) currentAbi: {
148
154
  'path': 'lib/$currentAbi/libapp.so',
149
155
  'md5': md5.convert(soBytes).toString(),
150
156
  },
@@ -172,10 +178,10 @@ PackResult _writePatchPackage({
172
178
 
173
179
  final package = Archive()
174
180
  ..addFile(_jsonArchiveFile('manifest.json', zipManifest));
175
- final progressTotal = libs.length + 1;
181
+ final progressTotal = effectiveLibs.length + 1;
176
182
  onProgress?.call(0, progressTotal, 'libapp.so');
177
183
  var progressDone = 0;
178
- for (final (currentAbi, soBytes) in libs) {
184
+ for (final (currentAbi, soBytes) in effectiveLibs) {
179
185
  package.addFile(
180
186
  ArchiveFile('lib/$currentAbi/libapp.so', soBytes.length, soBytes));
181
187
  progressDone++;
@@ -210,7 +216,7 @@ PackResult _writePatchPackage({
210
216
  'version': version,
211
217
  'md5': payloadMd5,
212
218
  'targetVersionCode': targetVersionCode,
213
- 'abis': libs.map((e) => e.$1).toList(),
219
+ 'abis': effectiveLibs.map((e) => e.$1).toList(),
214
220
  'payload': 'patch.zip',
215
221
  };
216
222
  _writeJson(File('${outDir.path}/manifest.json'), outerManifest);
@@ -218,7 +224,7 @@ PackResult _writePatchPackage({
218
224
  return PackResult(
219
225
  payloadPath: outZip.path,
220
226
  manifestPath: '${outDir.path}/manifest.json',
221
- abis: libs.map((e) => e.$1).toList(),
227
+ abis: effectiveLibs.map((e) => e.$1).toList(),
222
228
  md5: payloadMd5,
223
229
  version: version,
224
230
  targetVersionCode: targetVersionCode,