@_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
@@ -6,7 +6,7 @@ import 'package:args/args.dart';
6
6
  import '../cli_base.dart';
7
7
  import '../config.dart';
8
8
 
9
- /// `flutter_patcher config` — get/set/list config values.
9
+ /// `flutter_ota_kit config` — get/set/list config values.
10
10
  class ConfigCommand extends FlutterPatcherCommand {
11
11
  ConfigCommand() {
12
12
  addSubcommand(ConfigGetCommand());
@@ -52,13 +52,13 @@ class ConfigGetCommand extends FlutterPatcherCommand {
52
52
  String get description => 'Print a config value by dot-path (e.g. supabase.url).';
53
53
 
54
54
  @override
55
- ArgParser get argParser => ArgParser()..addOption('key', help: 'Dot-path key.');
55
+ ArgParser get argParser => ArgParser()..addOption('key', abbr: 'k', help: 'Dot-path key.');
56
56
 
57
57
  @override
58
58
  Future<int> run() => runGuarded(() async {
59
59
  final key = argResults!['key'] as String?;
60
60
  if (key == null || key.isEmpty) {
61
- throw StateError('Usage: flutter_patcher config get <key>');
61
+ throw StateError('Usage: flutter_ota_kit config get <key>');
62
62
  }
63
63
  final value = readPath(_loadProjectJson(), key);
64
64
  if (value == null) {
@@ -80,7 +80,7 @@ class ConfigSetCommand extends FlutterPatcherCommand {
80
80
 
81
81
  @override
82
82
  ArgParser get argParser => ArgParser()
83
- ..addOption('key', help: 'Dot-path key.')
83
+ ..addOption('key', abbr: 'k', help: 'Dot-path key.')
84
84
  ..addOption('value', help: 'Value.');
85
85
 
86
86
  @override
@@ -88,7 +88,7 @@ class ConfigSetCommand extends FlutterPatcherCommand {
88
88
  final key = argResults!['key'] as String?;
89
89
  final value = argResults!['value'] as String?;
90
90
  if (key == null || key.isEmpty || value == null) {
91
- throw StateError('Usage: flutter_patcher config set <key> <value>');
91
+ throw StateError('Usage: flutter_ota_kit config set <key> <value>');
92
92
  }
93
93
  final json = _loadProjectJson();
94
94
  writePath(json, key, value);
@@ -1,18 +1,18 @@
1
1
  import 'dart:io';
2
2
 
3
3
  import 'package:args/args.dart';
4
- import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
4
+ import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
5
5
  import 'package:path/path.dart' as p;
6
6
 
7
7
  import '../ui/ui.dart';
8
8
 
9
- /// `flutter_patcher console` — open the web console.
9
+ /// `flutter_ota_kit console` — open the web console.
10
10
  class ConsoleCommand extends FlutterPatcherCommand {
11
11
  @override
12
12
  String get name => 'console';
13
13
 
14
14
  @override
15
- String get description => 'Open the flutter_patcher web console.';
15
+ String get description => 'Open the flutter_ota_kit web console.';
16
16
 
17
17
  @override
18
18
  ArgParser get argParser => ArgParser()
@@ -49,7 +49,7 @@ class ConsoleCommand extends FlutterPatcherCommand {
49
49
  return;
50
50
  }
51
51
  banner('console');
52
- box('flutter_patcher console', [
52
+ box('flutter_ota_kit console', [
53
53
  'Open the web console with Flutter:',
54
54
  '',
55
55
  kv('dir', consoleDir.path),
@@ -9,7 +9,7 @@ import '../operations.dart';
9
9
  import '../ui/ui.dart';
10
10
  import '../util.dart';
11
11
 
12
- /// `flutter_patcher deploy` — zip + upload + register a new bundle.
12
+ /// `flutter_ota_kit deploy` — zip + upload + register a new bundle.
13
13
  class DeployCommand extends FlutterPatcherCommand {
14
14
  DeployCommand({this.config, this.backendOverride});
15
15
 
@@ -31,14 +31,14 @@ class DeployCommand extends FlutterPatcherCommand {
31
31
  ..addOption('channel', abbr: 'c', help: 'Target channel.')
32
32
  ..addOption('platform', abbr: 'p', defaultsTo: 'android', help: 'Platform.')
33
33
  ..addOption('message', abbr: 'm', help: 'Release message.')
34
- ..addFlag('force', help: 'Force the update on clients.')
34
+ ..addFlag('force', abbr: 'f', help: 'Force the update on clients.')
35
35
  ..addOption('target-app-version',
36
36
  help: 'Semver range target (XOR with fingerprint-hash).')
37
37
  ..addOption('fingerprint-hash',
38
38
  help: 'Fingerprint hash target (XOR with target-app-version).')
39
- ..addOption('key', help: 'Path to Ed25519 private key file (sign bundle).')
39
+ ..addOption('key', abbr: 'k', help: 'Path to Ed25519 private key file (sign bundle).')
40
40
  ..addOption('git-commit-hash', help: 'Git commit hash (auto-detected).')
41
- ..addOption('bundle-id', help: 'Explicit bundle id (uuidv7 by default).');
41
+ ..addOption('bundle-id', abbr: 'i', help: 'Explicit bundle id (uuidv7 by default).');
42
42
 
43
43
  @override
44
44
  Future<int> run() => runGuarded(() async {
@@ -1,11 +1,11 @@
1
1
  import 'dart:io';
2
2
 
3
3
  import 'package:args/args.dart';
4
- import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
4
+ import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
5
5
 
6
6
  import '../ui/ui.dart';
7
7
 
8
- /// `flutter_patcher doctor` — environment + backend connectivity check.
8
+ /// `flutter_ota_kit doctor` — environment + backend connectivity check.
9
9
  class DoctorCommand extends FlutterPatcherCommand {
10
10
  DoctorCommand({this.config, this.backendOverride});
11
11
 
@@ -36,7 +36,7 @@ class DoctorCommand extends FlutterPatcherCommand {
36
36
  ];
37
37
  final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
38
38
  if (cfg == null) {
39
- lines.add(kv('config', yellow('not found — run `flutter_patcher init`')));
39
+ lines.add(kv('config', yellow('not found — run `flutter_ota_kit init`')));
40
40
  box('doctor', lines);
41
41
  return;
42
42
  }
@@ -5,7 +5,7 @@ import 'package:args/args.dart';
5
5
  import '../cli_base.dart';
6
6
  import '../util.dart';
7
7
 
8
- /// `flutter_patcher fingerprint` — compute a build-time fingerprint hash.
8
+ /// `flutter_ota_kit fingerprint` — compute a build-time fingerprint hash.
9
9
  class FingerprintCommand extends FlutterPatcherCommand {
10
10
  @override
11
11
  String get name => 'fingerprint';
@@ -1,17 +1,22 @@
1
1
  import 'dart:io';
2
2
 
3
3
  import 'package:args/args.dart';
4
- import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
4
+ import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
5
5
 
6
6
  import '../ui/ui.dart';
7
7
 
8
- /// `flutter_patcher init` — scaffold a project config file.
8
+ /// `flutter_ota_kit init <backend>` — scaffold a Flutter project for OTA updates.
9
9
  ///
10
- /// Prompts (interactively, when attached to a terminal) for the credentials the
11
- /// **selected** backend provider needs, defaulting each field to its
12
- /// corresponding environment variable so CI runs can be non-interactive. The
13
- /// result is saved as `.flutter_patcher.json` (cwd) or the global config when
14
- /// `--global` is passed.
10
+ /// Beyond writing the `.otat/config.json` (so every other `flutter_ota_kit` command
11
+ /// knows the backend), it also drops the files a host app actually needs:
12
+ ///
13
+ /// * adds the `flutter_ota_kit` pub dependency to `pubspec.yaml`,
14
+ /// * adds the `INTERNET` permission to the Android manifest (the auto-init
15
+ /// `ContentProvider` is merged in automatically by the plugin),
16
+ /// * generates `lib/flutter_ota_kit_setup.dart` that wires the selected backend
17
+ /// and enables zero-click forced updates.
18
+ ///
19
+ /// All CLI working state is stored under `.otat/`.
15
20
  class InitCommand extends FlutterPatcherCommand {
16
21
  InitCommand({this.config, this.backendOverride});
17
22
 
@@ -23,16 +28,19 @@ class InitCommand extends FlutterPatcherCommand {
23
28
 
24
29
  @override
25
30
  String get description =>
26
- 'Scaffold a .flutter_patcher.json config for the current project.';
31
+ 'Scaffold flutter_ota_kit integration files + .otat config for the current '
32
+ 'Flutter project. Usage: flutter_ota_kit init <supabase|postgres|cloudflare|aws>';
27
33
 
28
34
  @override
29
35
  ArgParser get argParser => ArgParser()
30
36
  ..addOption('provider',
31
- abbr: 'p', defaultsTo: 'supabase', help: 'Backend provider.')
32
- ..addOption('channel', defaultsTo: 'production', help: 'Default channel.')
37
+ abbr: 'p',
38
+ defaultsTo: 'supabase',
39
+ help: 'Backend provider (also accepted as the first positional argument).')
40
+ ..addOption('channel', abbr: 'c', defaultsTo: 'production', help: 'Default channel.')
33
41
  ..addOption('platform', defaultsTo: 'android', help: 'Default platform.')
34
- ..addOption('source', defaultsTo: './dist', help: 'Default deploy source.')
35
- ..addFlag('global', help: 'Write to the global ~/.flutter_patcher config.')
42
+ ..addOption('source', abbr: 's', defaultsTo: './dist', help: 'Default deploy source.')
43
+ ..addFlag('global', help: 'Write to the global ~/.otat config (no scaffolding).')
36
44
  ..addFlag('force', abbr: 'f', help: 'Overwrite an existing config.');
37
45
 
38
46
  String? _p(String label, String? current) {
@@ -44,9 +52,52 @@ class InitCommand extends FlutterPatcherCommand {
44
52
  return trimmed.isEmpty ? current : trimmed;
45
53
  }
46
54
 
55
+ /// Backends `init` can scaffold, in display order.
56
+ static const List<String> _providers = [
57
+ 'supabase',
58
+ 'postgres',
59
+ 'cloudflare',
60
+ 'aws',
61
+ ];
62
+
63
+ /// Interactive backend picker. Returns the chosen provider, defaulting to
64
+ /// [requested] when the user just presses enter.
65
+ String _chooseProvider(String requested) {
66
+ stdout.writeln(cyan('Select a backend provider:'));
67
+ for (var i = 0; i < _providers.length; i++) {
68
+ final marker = _providers[i] == requested ? '*' : ' ';
69
+ stdout.writeln(' ${dim(marker)} ${i + 1}. ${_providers[i]}');
70
+ }
71
+ stdout.write('Provider ${dim('[1]')}: ');
72
+ final line = stdin.readLineSync()?.trim();
73
+ if (line == null || line.isEmpty) return requested;
74
+ final n = int.tryParse(line);
75
+ if (n != null && n >= 1 && n <= _providers.length) {
76
+ return _providers[n - 1];
77
+ }
78
+ return line;
79
+ }
80
+
47
81
  @override
48
82
  Future<int> run() => runGuarded(() async {
49
- final file = argResults!['global'] as bool
83
+ // A positional backend (`flutter_ota_kit init supabase`) takes precedence
84
+ // over the --provider flag.
85
+ final positional = argResults!.rest;
86
+ final requested = positional.isNotEmpty
87
+ ? positional.first
88
+ : (argResults!['provider'] as String);
89
+ final provider =
90
+ stdin.hasTerminal ? _chooseProvider(requested) : requested;
91
+ if (!_providers.contains(provider)) {
92
+ throw PackException(
93
+ 'Unknown provider "$provider". Choose one of: '
94
+ '${_providers.join(', ')}.',
95
+ 64,
96
+ );
97
+ }
98
+
99
+ final global = argResults!['global'] as bool;
100
+ final file = global
50
101
  ? configCandidates().last
51
102
  : configCandidates().first;
52
103
  if (file.existsSync() && !(argResults!['force'] as bool)) {
@@ -56,9 +107,10 @@ class InitCommand extends FlutterPatcherCommand {
56
107
  );
57
108
  }
58
109
 
59
- final provider = argResults!['provider'] as String;
60
- final channel = argResults!['channel'] as String? ?? 'production';
61
- final platform = argResults!['platform'] as String? ?? 'android';
110
+ final channel =
111
+ argResults!['channel'] as String? ?? 'production';
112
+ final platform =
113
+ argResults!['platform'] as String? ?? 'android';
62
114
  final source = argResults!['source'] as String? ?? './dist';
63
115
  final env = Platform.environment;
64
116
 
@@ -76,13 +128,14 @@ class InitCommand extends FlutterPatcherCommand {
76
128
  postgres: PostgresConfigJson(
77
129
  host: _p('Postgres host', env['POSTGRES_HOST']),
78
130
  port: _p('Postgres port', env['POSTGRES_PORT'] ?? '5432'),
79
- database: _p('Postgres database', env['POSTGRES_DB'] ?? 'postgres'),
131
+ database:
132
+ _p('Postgres database', env['POSTGRES_DB'] ?? 'postgres'),
80
133
  username: _p('Postgres username', env['POSTGRES_USER']),
81
134
  password: _p('Postgres password', env['POSTGRES_PASSWORD']),
82
135
  sslMode: _p('Postgres sslmode', env['POSTGRES_SSLMODE']),
83
136
  basePath: _p('Storage base path', env['POSTGRES_BASE_PATH']),
84
- servingBaseUrl:
85
- _p('Serving base url', env['POSTGRES_SERVING_BASE_URL']),
137
+ servingBaseUrl: _p(
138
+ 'Serving base url', env['POSTGRES_SERVING_BASE_URL']),
86
139
  ),
87
140
  );
88
141
  case 'cloudflare':
@@ -93,13 +146,15 @@ class InitCommand extends FlutterPatcherCommand {
93
146
  source: source,
94
147
  supabase: const SupabaseConfigJson(),
95
148
  cloudflare: CloudflareConfigJson(
96
- accountId: _p('Cloudflare account id', env['CLOUDFLARE_ACCOUNT_ID']),
97
- d1DatabaseId:
98
- _p('Cloudflare D1 database id', env['CLOUDFLARE_D1_DATABASE_ID']),
99
- apiToken: _p('Cloudflare API token', env['CLOUDFLARE_API_TOKEN']),
100
- r2Bucket:
101
- _p('R2 bucket', env['R2_BUCKET'] ?? 'bundles'),
102
- r2AccessKeyId: _p('R2 access key id', env['R2_ACCESS_KEY_ID']),
149
+ accountId:
150
+ _p('Cloudflare account id', env['CLOUDFLARE_ACCOUNT_ID']),
151
+ d1DatabaseId: _p(
152
+ 'Cloudflare D1 database id', env['CLOUDFLARE_D1_DATABASE_ID']),
153
+ apiToken:
154
+ _p('Cloudflare API token', env['CLOUDFLARE_API_TOKEN']),
155
+ r2Bucket: _p('R2 bucket', env['R2_BUCKET'] ?? 'bundles'),
156
+ r2AccessKeyId:
157
+ _p('R2 access key id', env['R2_ACCESS_KEY_ID']),
103
158
  r2SecretAccessKey:
104
159
  _p('R2 secret access key', env['R2_SECRET_ACCESS_KEY']),
105
160
  r2BasePath: _p('R2 base path', env['R2_BASE_PATH']),
@@ -115,23 +170,14 @@ class InitCommand extends FlutterPatcherCommand {
115
170
  aws: AwsConfigJson(
116
171
  bucket: _p('AWS bucket', env['AWS_BUCKET']),
117
172
  region: _p('AWS region', env['AWS_REGION']),
118
- accessKeyId: _p('AWS access key id', env['AWS_ACCESS_KEY_ID']),
173
+ accessKeyId:
174
+ _p('AWS access key id', env['AWS_ACCESS_KEY_ID']),
119
175
  secretAccessKey:
120
176
  _p('AWS secret access key', env['AWS_SECRET_ACCESS_KEY']),
121
177
  endpoint: _p('AWS endpoint', env['AWS_ENDPOINT']),
122
178
  basePath: _p('AWS base path', env['AWS_BASE_PATH']),
123
- sessionToken: _p('AWS session token', env['AWS_SESSION_TOKEN']),
124
- ),
125
- );
126
- case 'standalone':
127
- cfg = FlutterPatcherConfig(
128
- provider: provider,
129
- channel: channel,
130
- platform: platform,
131
- source: source,
132
- supabase: const SupabaseConfigJson(),
133
- standalone: StandaloneConfigJson(
134
- baseUrl: _p('Standalone base url', env['STANDALONE_BASE_URL']),
179
+ sessionToken:
180
+ _p('AWS session token', env['AWS_SESSION_TOKEN']),
135
181
  ),
136
182
  );
137
183
  case 'supabase':
@@ -143,20 +189,223 @@ class InitCommand extends FlutterPatcherCommand {
143
189
  source: source,
144
190
  supabase: SupabaseConfigJson(
145
191
  url: _p('Supabase URL', env['SUPABASE_URL']),
146
- serviceRoleKey:
147
- _p('Supabase service role key', env['SUPABASE_SERVICE_ROLE_KEY']),
192
+ serviceRoleKey: _p(
193
+ 'Supabase service role key', env['SUPABASE_SERVICE_ROLE_KEY']),
148
194
  anonKey: _p('Supabase anon key', env['SUPABASE_ANON_KEY']),
149
195
  bucket: _p('Storage bucket', env['SUPABASE_BUCKET'] ?? 'bundles'),
150
196
  basePath: _p('Storage base path', env['SUPABASE_BASE_PATH']),
197
+ managementKey: _p(
198
+ 'Supabase Management API key (optional, used by `migrate`)',
199
+ env['SUPABASE_MANAGEMENT_KEY']),
200
+ databaseUrl: _p(
201
+ 'Postgres DATABASE_URL (optional, used by `migrate`)',
202
+ env['SUPABASE_DATABASE_URL']),
151
203
  ),
152
204
  );
153
205
  }
154
206
 
155
- saveConfig(cfg, global: argResults!['global'] as bool);
207
+ saveConfig(cfg, global: global);
156
208
  step('Wrote ${file.path}');
209
+
210
+ if (global) {
211
+ stdout.writeln(
212
+ ' ${yellow('⚠')} Secrets are stored in plaintext under ~/.otat. '
213
+ 'Restrict permissions and use a secrets manager in production.',
214
+ );
215
+ return;
216
+ }
217
+
218
+ // Scaffold the host project integration files.
219
+ await _scaffoldProject(cfg, provider);
220
+
221
+ stdout.writeln();
222
+ stdout.writeln(cyan('Next steps:'));
223
+ stdout.writeln(' 1. ${dim('flutter pub get')}');
224
+ stdout.writeln(
225
+ ' 2. In lib/main.dart call ${green('await setupFlutterOta();')} '
226
+ 'right after WidgetsFlutterBinding.ensureInitialized() (before runApp).');
227
+ if (provider == 'supabase') {
228
+ stdout.writeln(
229
+ ' 3. Provision the backend once: ${dim('flutter_ota_kit migrate supabase')}');
230
+ } else if (provider == 'postgres') {
231
+ stdout.writeln(
232
+ ' 3. Provision the backend: ${dim('flutter_ota_kit migrate postgres')}');
233
+ }
157
234
  stdout.writeln(
158
- ' ${yellow('⚠')} Secrets are stored in plaintext. Restrict file '
159
- 'permissions and use a secrets manager in production.',
160
- );
235
+ ' 4. Build a patch: ${dim('flutter_ota_kit build --name 1.0.1')} '
236
+ 'then ${dim('flutter_ota_kit deploy')}');
161
237
  });
238
+
239
+ /// Generate the integration files (pubspec dep, manifest permission, setup dart
240
+ /// file, .gitignore entry) inside the current directory.
241
+ Future<void> _scaffoldProject(FlutterPatcherConfig cfg, String provider) async {
242
+ _addPubDependency();
243
+ _addInternetPermission();
244
+ await _writeSetupFile(cfg, provider);
245
+ _ensureGitignore();
246
+ }
247
+
248
+ void _addPubDependency() {
249
+ final pubspec = File('pubspec.yaml');
250
+ if (!pubspec.existsSync()) {
251
+ stdout.writeln(yellow(' ⚠ pubspec.yaml not found; skipping dependency injection.'));
252
+ return;
253
+ }
254
+ var content = pubspec.readAsStringSync();
255
+ if (content.contains(RegExp(r'^\s*flutter_ota_kit\s*:', multiLine: true))) {
256
+ step('pubspec.yaml already references flutter_ota_kit');
257
+ return;
258
+ }
259
+ const dep = ' flutter_ota_kit: ^0.1.4\n';
260
+ final marker = '\ndependencies:';
261
+ final idx = content.indexOf(marker);
262
+ if (idx == -1) {
263
+ stdout.writeln(yellow(' ⚠ could not find a `dependencies:` block; add `flutter_ota_kit` manually.'));
264
+ return;
265
+ }
266
+ final insertAt = idx + marker.length;
267
+ content = content.replaceRange(insertAt, insertAt, '\n$dep');
268
+ pubspec.writeAsStringSync(content);
269
+ step('Added `flutter_ota_kit` dependency to pubspec.yaml');
270
+ }
271
+
272
+ void _addInternetPermission() {
273
+ final mf = File('android/app/src/main/AndroidManifest.xml');
274
+ if (!mf.existsSync()) {
275
+ stdout.writeln(yellow(' ⚠ AndroidManifest.xml not found; skipping INTERNET permission.'));
276
+ return;
277
+ }
278
+ var content = mf.readAsStringSync();
279
+ if (content.contains('android.permission.INTERNET')) {
280
+ step('INTERNET permission already present');
281
+ return;
282
+ }
283
+ final match = RegExp(r'<manifest[^>]*>').firstMatch(content);
284
+ if (match == null) {
285
+ stdout.writeln(yellow(' ⚠ could not locate <manifest> tag; add INTERNET permission manually.'));
286
+ return;
287
+ }
288
+ final insertAt = match.end;
289
+ content = content.replaceRange(
290
+ insertAt,
291
+ insertAt,
292
+ '\n <uses-permission android:name="android.permission.INTERNET" />',
293
+ );
294
+ mf.writeAsStringSync(content);
295
+ step('Added INTERNET permission to android/app/src/main/AndroidManifest.xml');
296
+ }
297
+
298
+ Future<void> _writeSetupFile(FlutterPatcherConfig cfg, String provider) async {
299
+ final dir = Directory('lib');
300
+ if (!dir.existsSync()) dir.createSync(recursive: true);
301
+ final file = File('lib/flutter_ota_kit_setup.dart');
302
+ file.writeAsStringSync(_setupBody(cfg, provider));
303
+ step('Wrote lib/flutter_ota_kit_setup.dart');
304
+ }
305
+
306
+ void _ensureGitignore() {
307
+ final gi = File('.gitignore');
308
+ final content = gi.existsSync() ? gi.readAsStringSync() : '';
309
+ if (content.contains('.otat/')) {
310
+ step('.gitignore already ignores .otat/');
311
+ return;
312
+ }
313
+ final next = content.isEmpty ? '.otat/\n' : '$content\n.otat/\n';
314
+ gi.writeAsStringSync(next);
315
+ step('Added .otat/ to .gitignore');
316
+ }
317
+
318
+ String _q(String? s) => s == null ? "''" : "'${s.replaceAll("'", "\\'")}'";
319
+
320
+ /// Build the body of `lib/flutter_ota_kit_setup.dart` for the selected backend.
321
+ String _setupBody(FlutterPatcherConfig cfg, String provider) {
322
+ final channel = _q(cfg.channel);
323
+ final strategy = 'UpdateStrategy.appVersion';
324
+ final appVersion =
325
+ "const String.fromEnvironment('APP_VERSION', defaultValue: '1.0.0')";
326
+
327
+ late final String configure;
328
+ switch (provider) {
329
+ case 'postgres':
330
+ final c = cfg.postgres;
331
+ configure = '''
332
+ FlutterPatcher.configurePostgres(PostgresUpdateConfig(
333
+ host: ${_q(c.host)},
334
+ port: ${_q(c.port ?? '5432')},
335
+ database: ${_q(c.database)},
336
+ username: ${_q(c.username)},
337
+ password: ${_q(c.password)},
338
+ servingBaseUrl: ${_q(c.servingBaseUrl)},
339
+ channel: $channel,
340
+ platform: Platform.android,
341
+ updateStrategy: $strategy,
342
+ appVersion: $appVersion,
343
+ ));''';
344
+ case 'cloudflare':
345
+ final c = cfg.cloudflare;
346
+ configure = '''
347
+ FlutterPatcher.configureCloudflare(CloudflareUpdateConfig(
348
+ databaseId: ${_q(c.d1DatabaseId)},
349
+ accountId: ${_q(c.accountId)},
350
+ cloudflareApiToken: ${_q(c.apiToken)},
351
+ bucketName: ${_q(c.r2Bucket ?? 'bundles')},
352
+ accessKeyId: ${_q(c.r2AccessKeyId)},
353
+ secretAccessKey: ${_q(c.r2SecretAccessKey)},
354
+ basePath: ${_q(c.r2BasePath)},
355
+ channel: $channel,
356
+ platform: Platform.android,
357
+ updateStrategy: $strategy,
358
+ appVersion: $appVersion,
359
+ ));''';
360
+ case 'aws':
361
+ final c = cfg.aws;
362
+ configure = '''
363
+ FlutterPatcher.configureAws(AwsUpdateConfig(
364
+ bucketName: ${_q(c.bucket)},
365
+ region: ${_q(c.region)},
366
+ accessKeyId: ${_q(c.accessKeyId)},
367
+ secretAccessKey: ${_q(c.secretAccessKey)},
368
+ basePath: ${_q(c.basePath)},
369
+ endpoint: ${_q(c.endpoint)},
370
+ channel: $channel,
371
+ platform: Platform.android,
372
+ updateStrategy: $strategy,
373
+ appVersion: $appVersion,
374
+ ));''';
375
+ case 'supabase':
376
+ default:
377
+ final c = cfg.supabase;
378
+ configure = '''
379
+ FlutterPatcher.configureSupabase(SupabaseUpdateConfig(
380
+ supabaseUrl: ${_q(c.url)},
381
+ anonKey: ${_q(c.anonKey)},
382
+ bucket: ${_q(c.bucket ?? 'bundles')},
383
+ channel: $channel,
384
+ platform: Platform.android,
385
+ updateStrategy: $strategy,
386
+ appVersion: $appVersion,
387
+ sdkVersion: '1.0.0',
388
+ ));''';
389
+ }
390
+
391
+ return '''
392
+ import 'dart:io' show Platform;
393
+
394
+ import 'package:flutter_ota_kit/flutter_ota_kit.dart';
395
+
396
+ // Generated by `flutter_ota_kit init $provider`.
397
+ //
398
+ // Secrets are hard-coded here for a zero-config quick start. For production,
399
+ // prefer reading them from --dart-define / environment variables instead.
400
+ //
401
+ // Call [setupFlutterOta] once in `main()`, after
402
+ // WidgetsFlutterBinding.ensureInitialized() and before runApp(). It wires the
403
+ // $provider update source and enables zero-click forced updates (the process
404
+ // restarts automatically when a forced bundle is available).
405
+ Future<void> setupFlutterOta() async {
406
+ $configure
407
+ await FlutterPatcher.init(autoApplyUpdates: true);
408
+ }
409
+ ''';
410
+ }
162
411
  }
@@ -8,7 +8,7 @@ import '../config.dart';
8
8
  import '../sign.dart';
9
9
  import '../ui/ui.dart';
10
10
 
11
- /// `flutter_patcher keys` — generate an Ed25519 keypair for bundle signing.
11
+ /// `flutter_ota_kit keys` — generate an Ed25519 keypair for bundle signing.
12
12
  class KeysCommand extends FlutterPatcherCommand {
13
13
  KeysCommand({this.config, this.backendOverride});
14
14