@_nazmiforreal/flutter-ota 0.1.0 → 0.1.1

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/bin/flutter-patcher.js +37 -39
  2. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
  3. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
  4. package/dart-src/packages/cli-tools/.gitkeep +0 -0
  5. package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
  6. package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
  7. package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
  8. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
  9. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
  10. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
  11. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
  13. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
  14. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
  15. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
  16. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
  17. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
  18. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
  19. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
  20. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
  21. package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
  22. package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
  23. package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
  24. package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
  25. package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
  26. package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  27. package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  28. package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  29. package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  30. package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  31. package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
  32. package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
  33. package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
  35. package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
  36. package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
  37. package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
  38. package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
  39. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
  40. package/dart-src/packages/cli-tools/pubspec.lock +638 -0
  41. package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
  42. package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
  43. package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
  44. package/dart-src/packages/core/lib/src/bundle.dart +191 -0
  45. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
  46. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
  47. package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
  48. package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
  49. package/dart-src/packages/core/lib/src/metadata.dart +31 -0
  50. package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
  51. package/dart-src/packages/core/lib/src/platform.dart +15 -0
  52. package/dart-src/packages/core/lib/src/rollout.dart +209 -0
  53. package/dart-src/packages/core/lib/src/semver.dart +70 -0
  54. package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
  55. package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
  56. package/dart-src/packages/core/lib/src/status.dart +24 -0
  57. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  58. package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
  59. package/dart-src/packages/core/lib/src/uuid.dart +45 -0
  60. package/dart-src/packages/core/pubspec.lock +381 -0
  61. package/dart-src/packages/core/pubspec.yaml +9 -0
  62. package/dart-src/packages/core/tool/dbg.dart +6 -0
  63. package/dart-src/plugins/aws/.gitkeep +0 -0
  64. package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
  65. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
  66. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
  67. package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
  68. package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
  69. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
  70. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
  71. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
  72. package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
  73. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
  74. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
  75. package/dart-src/plugins/aws/pubspec.lock +435 -0
  76. package/dart-src/plugins/aws/pubspec.yaml +22 -0
  77. package/dart-src/plugins/cloudflare/.gitkeep +0 -0
  78. package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
  79. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
  80. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
  81. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
  82. package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
  83. package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
  84. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
  85. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
  86. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
  87. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
  88. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
  89. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
  90. package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
  91. package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
  92. package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
  93. package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
  94. package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
  95. package/dart-src/plugins/cloudflare/pubspec.lock +411 -0
  96. package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
  97. package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
  98. package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
  99. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
  100. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
  101. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
  102. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
  103. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
  104. package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
  105. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
  106. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
  107. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
  108. package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
  109. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
  110. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
  111. package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
  112. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
  113. package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
  114. package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
  115. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
  116. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
  117. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
  118. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
  119. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
  120. package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
  121. package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
  122. package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
  123. package/dart-src/plugins/plugin-core/pubspec.lock +396 -0
  124. package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
  125. package/dart-src/plugins/postgres/.gitkeep +0 -0
  126. package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
  127. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
  128. package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
  129. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
  130. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
  131. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
  132. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
  133. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
  134. package/dart-src/plugins/postgres/pubspec.lock +427 -0
  135. package/dart-src/plugins/postgres/pubspec.yaml +18 -0
  136. package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
  137. package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
  138. package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
  139. package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
  140. package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
  141. package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
  142. package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
  143. package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
  144. package/dart-src/plugins/standalone/.gitkeep +0 -0
  145. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
  146. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
  147. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
  148. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
  149. package/dart-src/plugins/standalone/pubspec.lock +411 -0
  150. package/dart-src/plugins/standalone/pubspec.yaml +18 -0
  151. package/dart-src/plugins/supabase/lib/edge.dart +10 -0
  152. package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
  153. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
  154. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
  155. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
  156. package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
  157. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
  158. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
  159. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
  160. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
  161. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
  162. package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
  163. package/dart-src/plugins/supabase/pubspec.lock +515 -0
  164. package/dart-src/plugins/supabase/pubspec.yaml +19 -0
  165. package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
  166. package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
  167. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
  168. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
  169. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
  170. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
  171. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
  172. package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
  173. package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
  174. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
  175. package/package.json +2 -1
  176. package/scripts/postinstall.js +32 -23
  177. /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
@@ -0,0 +1,113 @@
1
+ import 'dart:convert';
2
+ import 'dart:io';
3
+
4
+ import 'package:args/args.dart';
5
+ import 'package:args/command_runner.dart';
6
+
7
+ import '../cli_base.dart';
8
+ import '../config.dart';
9
+
10
+ /// `flutter_patcher config` — get/set/list config values.
11
+ class ConfigCommand extends Command<int> {
12
+ ConfigCommand() {
13
+ addSubcommand(ConfigGetCommand());
14
+ addSubcommand(ConfigSetCommand());
15
+ addSubcommand(ConfigListCommand());
16
+ }
17
+
18
+ @override
19
+ String get name => 'config';
20
+
21
+ @override
22
+ String get description => 'Get, set, or list configuration values.';
23
+
24
+ @override
25
+ Future<int> run() => runGuarded(() async {
26
+ // No subcommand selected.
27
+ print(description);
28
+ print(''); print('Subcommands:'); print(' get <key>'); print(' set <key> <value>'); print(' list');
29
+ });
30
+ }
31
+
32
+ Map<String, dynamic> _loadProjectJson() {
33
+ final file = configCandidates().first;
34
+ if (!file.existsSync()) return <String, dynamic>{};
35
+ final raw = file.readAsStringSync();
36
+ if (raw.trim().isEmpty) return <String, dynamic>{};
37
+ return jsonDecode(raw) as Map<String, dynamic>;
38
+ }
39
+
40
+ void _saveProjectJson(Map<String, dynamic> json) {
41
+ final file = configCandidates().first;
42
+ file.parent.createSync(recursive: true);
43
+ file.writeAsStringSync(
44
+ const JsonEncoder.withIndent(' ').convert(json),
45
+ );
46
+ }
47
+
48
+ class ConfigGetCommand extends Command<int> {
49
+ @override
50
+ String get name => 'get';
51
+
52
+ @override
53
+ String get description => 'Print a config value by dot-path (e.g. supabase.url).';
54
+
55
+ @override
56
+ ArgParser get argParser => ArgParser()..addOption('key', help: 'Dot-path key.');
57
+
58
+ @override
59
+ Future<int> run() => runGuarded(() async {
60
+ final key = argResults!['key'] as String?;
61
+ if (key == null || key.isEmpty) {
62
+ throw StateError('Usage: flutter_patcher config get <key>');
63
+ }
64
+ final value = readPath(_loadProjectJson(), key);
65
+ if (value == null) {
66
+ stderr.writeln('(not set)');
67
+ return;
68
+ }
69
+ stdout.writeln(
70
+ value is String ? value : const JsonEncoder().convert(value),
71
+ );
72
+ });
73
+ }
74
+
75
+ class ConfigSetCommand extends Command<int> {
76
+ @override
77
+ String get name => 'set';
78
+
79
+ @override
80
+ String get description => 'Set a config value by dot-path.';
81
+
82
+ @override
83
+ ArgParser get argParser => ArgParser()
84
+ ..addOption('key', help: 'Dot-path key.')
85
+ ..addOption('value', help: 'Value.');
86
+
87
+ @override
88
+ Future<int> run() => runGuarded(() async {
89
+ final key = argResults!['key'] as String?;
90
+ final value = argResults!['value'] as String?;
91
+ if (key == null || key.isEmpty || value == null) {
92
+ throw StateError('Usage: flutter_patcher config set <key> <value>');
93
+ }
94
+ final json = _loadProjectJson();
95
+ writePath(json, key, value);
96
+ _saveProjectJson(json);
97
+ stdout.writeln('Set $key = $value');
98
+ });
99
+ }
100
+
101
+ class ConfigListCommand extends Command<int> {
102
+ @override
103
+ String get name => 'list';
104
+
105
+ @override
106
+ String get description => 'Print the full config.';
107
+
108
+ @override
109
+ Future<int> run() => runGuarded(() async {
110
+ final json = _loadProjectJson();
111
+ stdout.writeln(const JsonEncoder.withIndent(' ').convert(json));
112
+ });
113
+ }
@@ -0,0 +1,55 @@
1
+ import 'dart:io';
2
+
3
+ import 'package:args/args.dart';
4
+ import 'package:args/command_runner.dart';
5
+ import 'package:path/path.dart' as p;
6
+
7
+ import '../cli_base.dart';
8
+
9
+ /// `flutter_patcher console` — open the web console.
10
+ class ConsoleCommand extends Command<int> {
11
+ @override
12
+ String get name => 'console';
13
+
14
+ @override
15
+ String get description => 'Open the flutter_patcher web console.';
16
+
17
+ @override
18
+ ArgParser get argParser => ArgParser()
19
+ ..addFlag('open',
20
+ help: 'Launch the console with `flutter run -d chrome` (requires Flutter).');
21
+
22
+ Directory? _findConsoleDir() {
23
+ var dir = Directory(p.dirname(Platform.script.path));
24
+ while (dir.path != dir.parent.path) {
25
+ final candidate = Directory(p.join(dir.path, 'packages', 'console'));
26
+ if (candidate.existsSync()) return candidate;
27
+ dir = dir.parent;
28
+ }
29
+ return null;
30
+ }
31
+
32
+ @override
33
+ Future<int> run() => runGuarded(() async {
34
+ final consoleDir = _findConsoleDir();
35
+ if (consoleDir == null) {
36
+ stdout.writeln('Console package not found (expected packages/console).');
37
+ return;
38
+ }
39
+ if (argResults!['open'] as bool) {
40
+ stdout.writeln('Launching console...');
41
+ final process = await Process.start(
42
+ 'flutter',
43
+ ['run', '-d', 'chrome'],
44
+ workingDirectory: consoleDir.path,
45
+ runInShell: true,
46
+ mode: ProcessStartMode.inheritStdio,
47
+ );
48
+ exitCode = await process.exitCode;
49
+ return;
50
+ }
51
+ stdout.writeln('flutter_patcher web console:');
52
+ stdout.writeln(' cd ${consoleDir.path}');
53
+ stdout.writeln(' flutter run -d chrome');
54
+ });
55
+ }
@@ -0,0 +1,95 @@
1
+ import 'dart:io';
2
+
3
+ import 'package:args/args.dart';
4
+ import 'package:args/command_runner.dart';
5
+
6
+ import '../backend.dart';
7
+ import '../cli_base.dart';
8
+ import '../config.dart';
9
+ import '../operations.dart';
10
+ import '../util.dart';
11
+
12
+ /// `flutter_patcher deploy` — zip + upload + register a new bundle.
13
+ class DeployCommand extends Command<int> {
14
+ DeployCommand({this.config, this.backendOverride});
15
+
16
+ final FlutterPatcherConfig? config;
17
+ final Backend? backendOverride;
18
+
19
+ @override
20
+ String get name => 'deploy';
21
+
22
+ @override
23
+ String get description =>
24
+ 'Zip a source directory, upload it, and register a new bundle.';
25
+
26
+ @override
27
+ ArgParser get argParser => ArgParser()
28
+ ..addOption('backend',
29
+ abbr: 'b', help: 'Backend provider (supabase/postgres/cloudflare/aws).')
30
+ ..addOption('source', abbr: 's', help: 'Source directory to zip + upload.')
31
+ ..addOption('channel', abbr: 'c', help: 'Target channel.')
32
+ ..addOption('platform', abbr: 'p', defaultsTo: 'android', help: 'Platform.')
33
+ ..addOption('message', abbr: 'm', help: 'Release message.')
34
+ ..addFlag('force', help: 'Force the update on clients.')
35
+ ..addOption('target-app-version',
36
+ help: 'Semver range target (XOR with fingerprint-hash).')
37
+ ..addOption('fingerprint-hash',
38
+ help: 'Fingerprint hash target (XOR with target-app-version).')
39
+ ..addOption('key', help: 'Path to Ed25519 private key file (sign bundle).')
40
+ ..addOption('git-commit-hash', help: 'Git commit hash (auto-detected).')
41
+ ..addOption('bundle-id', help: 'Explicit bundle id (uuidv7 by default).');
42
+
43
+ @override
44
+ Future<int> run() => runGuarded(() async {
45
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
46
+ final backend = requireBackend(cfg, override: backendOverride);
47
+ final source = argResults!['source'] as String? ?? cfg?.source ?? './dist';
48
+ final channel =
49
+ argResults!['channel'] as String? ?? cfg?.channel ?? 'production';
50
+ final platform =
51
+ argResults!['platform'] as String? ?? cfg?.platform ?? 'android';
52
+ final message = argResults!['message'] as String?;
53
+ final force = argResults!['force'] as bool;
54
+ final targetAppVersion = argResults!['target-app-version'] as String?;
55
+ final fingerprintHash = argResults!['fingerprint-hash'] as String?;
56
+ final keyPath = argResults!['key'] as String?;
57
+ String? signingKey;
58
+ if (keyPath != null) signingKey = File(keyPath).readAsStringSync().trim();
59
+ final resolvedGitCommitHash =
60
+ argResults!['git-commit-hash'] as String? ?? await gitCommitHash(source);
61
+ final bundleId = argResults!['bundle-id'] as String?;
62
+
63
+ final bundle = await deployBundle(
64
+ backend,
65
+ DeployOptions(
66
+ source: source,
67
+ channel: channel,
68
+ platform: platform,
69
+ message: message,
70
+ force: force,
71
+ targetAppVersion: targetAppVersion,
72
+ fingerprintHash: fingerprintHash,
73
+ signingKeyBase64: signingKey,
74
+ gitCommitHash: resolvedGitCommitHash,
75
+ bundleId: bundleId,
76
+ ),
77
+ );
78
+
79
+ stdout.writeln('Deployed bundle ${bundle.id}');
80
+ stdout.writeln(' channel: ${bundle.channel}');
81
+ stdout.writeln(' platform: ${bundle.platform.value}');
82
+ stdout.writeln(' enabled: ${bundle.enabled}');
83
+ stdout.writeln(' storageUri: ${bundle.storageUri}');
84
+ if (bundle.targetAppVersion != null) {
85
+ stdout.writeln(' targetAppVersion: ${bundle.targetAppVersion}');
86
+ }
87
+ if (bundle.fingerprintHash != null) {
88
+ stdout.writeln(' fingerprintHash: ${bundle.fingerprintHash}');
89
+ }
90
+ if (bundle.gitCommitHash != null) {
91
+ stdout.writeln(' gitCommitHash: ${bundle.gitCommitHash}');
92
+ }
93
+ if (bundle.message != null) stdout.writeln(' message: ${bundle.message}');
94
+ });
95
+ }
@@ -0,0 +1,48 @@
1
+ import 'dart:io';
2
+
3
+ import 'package:args/args.dart';
4
+ import 'package:args/command_runner.dart';
5
+
6
+ import '../backend.dart';
7
+ import '../cli_base.dart';
8
+ import '../config.dart';
9
+
10
+ /// `flutter_patcher doctor` — environment + backend connectivity check.
11
+ class DoctorCommand extends Command<int> {
12
+ DoctorCommand({this.config, this.backendOverride});
13
+
14
+ final FlutterPatcherConfig? config;
15
+ final Backend? backendOverride;
16
+
17
+ @override
18
+ String get name => 'doctor';
19
+
20
+ @override
21
+ String get description => 'Diagnose the local environment and backend connection.';
22
+
23
+ @override
24
+ ArgParser get argParser => ArgParser()
25
+ ..addOption('backend', abbr: 'b', help: 'Backend provider.');
26
+
27
+ @override
28
+ Future<int> run() => runGuarded(() async {
29
+ stdout.writeln('flutter_patcher CLI doctor');
30
+ stdout.writeln(' dart: ${Platform.version.split(' ').first}');
31
+ stdout.writeln(' os: ${Platform.operatingSystem} '
32
+ '${Platform.operatingSystemVersion.split('\n').first}');
33
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
34
+ if (cfg == null) {
35
+ stdout.writeln(' config: not found (run `flutter_patcher init`)');
36
+ return;
37
+ }
38
+ stdout.writeln(' config: provider=${cfg.provider} '
39
+ 'channel=${cfg.channel} platform=${cfg.platform}');
40
+ try {
41
+ final backend = requireBackend(cfg, override: backendOverride);
42
+ final channels = await backend.db.getChannels();
43
+ stdout.writeln(' backend: reachable (channels: ${channels.join(', ')})');
44
+ } catch (e) {
45
+ stdout.writeln(' backend: unreachable — $e');
46
+ }
47
+ });
48
+ }
@@ -0,0 +1,29 @@
1
+ import 'dart:io';
2
+
3
+ import 'package:args/args.dart';
4
+ import 'package:args/command_runner.dart';
5
+
6
+ import '../cli_base.dart';
7
+ import '../util.dart';
8
+
9
+ /// `flutter_patcher fingerprint` — compute a build-time fingerprint hash.
10
+ class FingerprintCommand extends Command<int> {
11
+ @override
12
+ String get name => 'fingerprint';
13
+
14
+ @override
15
+ String get description =>
16
+ 'Compute a deterministic fingerprint hash for a directory (build-time).';
17
+
18
+ @override
19
+ ArgParser get argParser => ArgParser()
20
+ ..addOption('source',
21
+ abbr: 's', defaultsTo: './dist', help: 'Directory to fingerprint.');
22
+
23
+ @override
24
+ Future<int> run() => runGuarded(() async {
25
+ final source = argResults!['source'] as String;
26
+ final hash = generateFingerprint(source);
27
+ stdout.writeln(hash);
28
+ });
29
+ }
@@ -0,0 +1,82 @@
1
+ import 'dart:io';
2
+
3
+ import 'package:args/args.dart';
4
+ import 'package:args/command_runner.dart';
5
+
6
+ import '../backend.dart';
7
+ import '../cli_base.dart';
8
+ import '../config.dart';
9
+
10
+ /// `flutter_patcher init` — scaffold a project config file.
11
+ class InitCommand extends Command<int> {
12
+ InitCommand({this.config, this.backendOverride});
13
+
14
+ final FlutterPatcherConfig? config;
15
+ final Backend? backendOverride;
16
+
17
+ @override
18
+ String get name => 'init';
19
+
20
+ @override
21
+ String get description =>
22
+ 'Scaffold a .flutter_patcher.json config for the current project.';
23
+
24
+ @override
25
+ ArgParser get argParser => ArgParser()
26
+ ..addOption('provider', defaultsTo: 'supabase', help: 'Backend provider.')
27
+ ..addOption('url', help: 'Supabase project URL.')
28
+ ..addOption('key', help: 'Supabase service role key.')
29
+ ..addOption('anon-key', help: 'Supabase anon key.')
30
+ ..addOption('bucket', defaultsTo: 'bundles', help: 'Storage bucket.')
31
+ ..addOption('base-path', help: 'Storage base path.')
32
+ ..addOption('channel', defaultsTo: 'production', help: 'Default channel.')
33
+ ..addOption('platform', defaultsTo: 'android', help: 'Default platform.')
34
+ ..addOption('source', defaultsTo: './dist', help: 'Default deploy source.')
35
+ ..addFlag('force', abbr: 'f', help: 'Overwrite an existing config.');
36
+
37
+ String? _prompt(String label, String? current) {
38
+ if (!stdin.hasTerminal) return current;
39
+ stdout.write('$label${current != null ? ' [$current]' : ''}: ');
40
+ final line = stdin.readLineSync();
41
+ return (line == null || line.trim().isEmpty) ? current : line.trim();
42
+ }
43
+
44
+ @override
45
+ Future<int> run() => runGuarded(() async {
46
+ final file = configCandidates().first;
47
+ if (file.existsSync() && !(argResults!['force'] as bool)) {
48
+ throw StateError(
49
+ 'Config already exists at ${file.path}. Use --force to overwrite.',
50
+ );
51
+ }
52
+
53
+ final provider = argResults!['provider'] as String;
54
+ final url = _prompt('Supabase URL', argResults!['url'] as String?);
55
+ final key = _prompt(
56
+ 'Supabase service role key',
57
+ argResults!['key'] as String?,
58
+ );
59
+ final anonKey = argResults!['anon-key'] as String?;
60
+ final bucket = argResults!['bucket'] as String? ?? 'bundles';
61
+ final basePath = argResults!['base-path'] as String?;
62
+ final channel = argResults!['channel'] as String? ?? 'production';
63
+ final platform = argResults!['platform'] as String? ?? 'android';
64
+ final source = argResults!['source'] as String? ?? './dist';
65
+
66
+ final cfg = FlutterPatcherConfig(
67
+ provider: provider,
68
+ supabase: SupabaseConfigJson(
69
+ url: url,
70
+ serviceRoleKey: key,
71
+ anonKey: anonKey,
72
+ bucket: bucket,
73
+ basePath: basePath,
74
+ ),
75
+ channel: channel,
76
+ platform: platform,
77
+ source: source,
78
+ );
79
+ saveConfig(cfg);
80
+ stdout.writeln('Wrote ${file.path}');
81
+ });
82
+ }
@@ -0,0 +1,55 @@
1
+ import 'dart:convert';
2
+ import 'dart:io';
3
+
4
+ import 'package:args/args.dart';
5
+ import 'package:args/command_runner.dart';
6
+
7
+ import '../backend.dart';
8
+ import '../cli_base.dart';
9
+ import '../config.dart';
10
+ import '../sign.dart';
11
+
12
+ /// `flutter_patcher keys` — generate an Ed25519 keypair for bundle signing.
13
+ class KeysCommand extends Command<int> {
14
+ KeysCommand({this.config, this.backendOverride});
15
+
16
+ final FlutterPatcherConfig? config;
17
+ final Backend? backendOverride;
18
+
19
+ @override
20
+ String get name => 'keys';
21
+
22
+ @override
23
+ String get description =>
24
+ 'Generate an Ed25519 keypair used to sign deployed bundles.';
25
+
26
+ @override
27
+ ArgParser get argParser => ArgParser()
28
+ ..addFlag('save', help: 'Persist the public key into the project config.');
29
+
30
+ @override
31
+ Future<int> run() => runGuarded(() async {
32
+ final (privateB64, publicB64) = await generateEd25519KeyPair();
33
+ stdout.writeln('Private key (keep secret, use with `deploy --key`):');
34
+ stdout.writeln(' $privateB64');
35
+ stdout.writeln('');
36
+ stdout.writeln('Public key (safe to embed / configure on device):');
37
+ stdout.writeln(' $publicB64');
38
+ if (argResults!['save'] as bool) {
39
+ final file = configCandidates().first;
40
+ final json = file.existsSync() && file.readAsStringSync().trim().isNotEmpty
41
+ ? (jsonDecode(file.readAsStringSync()) as Map<String, dynamic>)
42
+ : <String, dynamic>{};
43
+ writePath(json, 'publicKey', publicB64);
44
+ _save(json);
45
+ stdout.writeln('');
46
+ stdout.writeln('Saved public key to ${file.path}');
47
+ }
48
+ });
49
+
50
+ void _save(Map<String, dynamic> json) {
51
+ final file = configCandidates().first;
52
+ file.parent.createSync(recursive: true);
53
+ file.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(json));
54
+ }
55
+ }
@@ -0,0 +1,132 @@
1
+ import 'dart:io';
2
+
3
+ import 'package:args/args.dart';
4
+ import 'package:args/command_runner.dart';
5
+ import 'package:path/path.dart' as p;
6
+ import 'package:postgres/postgres.dart';
7
+
8
+ import '../cli_base.dart';
9
+
10
+ /// `flutter_patcher migrate` — run backend SQL migrations.
11
+ class MigrateCommand extends Command<int> {
12
+ @override
13
+ String get name => 'migrate';
14
+
15
+ @override
16
+ String get description =>
17
+ 'Run SQL migrations against the backend database (Supabase Postgres).';
18
+
19
+ @override
20
+ ArgParser get argParser => ArgParser()
21
+ ..addOption('backend', abbr: 'b', help: 'Backend provider.')
22
+ ..addOption('database-url',
23
+ help: 'Postgres connection string (or DATABASE_URL env).')
24
+ ..addOption('migrations-dir',
25
+ help: 'Directory of *.sql migration files (ordered by name).')
26
+ ..addFlag('dry-run', help: 'Print migrations instead of applying them.');
27
+
28
+ /// Default migrations dir lives next to the CLI entrypoint; the backend
29
+ /// selects which dialect's migrations are used (cloudflare = D1 SQLite).
30
+ String defaultMigrationsDir(String provider) {
31
+ final sub = switch (provider) {
32
+ 'cloudflare' => 'cloudflare',
33
+ _ => 'supabase',
34
+ };
35
+ return p.join(p.dirname(Platform.script.path), '..', 'migrations', sub);
36
+ }
37
+
38
+ List<String> _splitStatements(String sql) {
39
+ return sql
40
+ .split(';')
41
+ .map((s) => s.trim())
42
+ .where((s) => s.isNotEmpty && !s.startsWith('--'))
43
+ .toList();
44
+ }
45
+
46
+ @override
47
+ Future<int> run() => runGuarded(() async {
48
+ final provider = argResults!['backend'] as String? ?? 'supabase';
49
+ final dir = argResults!['migrations-dir'] as String? ??
50
+ defaultMigrationsDir(provider);
51
+ final migrationsDir = Directory(dir);
52
+ if (!migrationsDir.existsSync()) {
53
+ throw StateError('Migrations directory not found: $dir');
54
+ }
55
+ final files = migrationsDir
56
+ .listSync()
57
+ .whereType<File>()
58
+ .where((f) => f.path.endsWith('.sql'))
59
+ .toList()
60
+ ..sort((a, b) => a.path.compareTo(b.path));
61
+ if (files.isEmpty) {
62
+ throw StateError('No *.sql files in $dir');
63
+ }
64
+
65
+ if (argResults!['dry-run'] as bool) {
66
+ for (final f in files) {
67
+ stdout.writeln('--- ${p.basename(f.path)} ---');
68
+ stdout.writeln(f.readAsStringSync());
69
+ }
70
+ return;
71
+ }
72
+
73
+ final url = argResults!['database-url'] as String? ??
74
+ Platform.environment['DATABASE_URL'];
75
+ if (provider == 'cloudflare' || provider == 'aws') {
76
+ stdout.writeln(
77
+ 'Migrations for "$provider" are applied via its platform tooling '
78
+ '(e.g. `wrangler d1 execute` for cloudflare, or the AWS console for '
79
+ 'S3/DynamoDB). The SQL above shows the intended schema.',
80
+ );
81
+ return;
82
+ }
83
+ if (url == null || url.isEmpty) {
84
+ throw StateError('Provide --database-url or set DATABASE_URL.');
85
+ }
86
+
87
+ final uri = Uri.parse(url);
88
+ final endpoint = Endpoint(
89
+ host: uri.host,
90
+ port: uri.port == 0 ? 5432 : uri.port,
91
+ database: uri.path.isEmpty ? 'postgres' : uri.path.substring(1),
92
+ username: uri.userInfo.isEmpty ? null : uri.userInfo.split(':').first,
93
+ password: uri.userInfo.contains(':')
94
+ ? uri.userInfo.split(':').last
95
+ : null,
96
+ );
97
+ final conn = await Connection.open(
98
+ endpoint,
99
+ settings: ConnectionSettings(sslMode: SslMode.disable),
100
+ );
101
+ try {
102
+ await conn.execute(
103
+ 'CREATE TABLE IF NOT EXISTS _flutter_patcher_migrations '
104
+ '(name text primary key, applied_at timestamptz default now())',
105
+ queryMode: QueryMode.simple,
106
+ );
107
+ for (final file in files) {
108
+ final name = p.basename(file.path);
109
+ final escaped = name.replaceAll("'", "''");
110
+ final existing = await conn.execute(
111
+ "SELECT 1 FROM _flutter_patcher_migrations WHERE name = '$escaped'",
112
+ queryMode: QueryMode.simple,
113
+ );
114
+ if (existing.isNotEmpty) {
115
+ stdout.writeln('skip $name (already applied)');
116
+ continue;
117
+ }
118
+ final statements = _splitStatements(file.readAsStringSync());
119
+ for (final stmt in statements) {
120
+ await conn.execute(stmt, queryMode: QueryMode.simple);
121
+ }
122
+ await conn.execute(
123
+ "INSERT INTO _flutter_patcher_migrations(name) VALUES ('$escaped')",
124
+ queryMode: QueryMode.simple,
125
+ );
126
+ stdout.writeln('applied $name');
127
+ }
128
+ } finally {
129
+ await conn.close();
130
+ }
131
+ });
132
+ }
@@ -0,0 +1,41 @@
1
+ import 'dart:io';
2
+
3
+ import 'package:args/args.dart';
4
+ import 'package:args/command_runner.dart';
5
+
6
+ import '../backend.dart';
7
+ import '../cli_base.dart';
8
+ import '../config.dart';
9
+ import '../operations.dart';
10
+
11
+ /// `flutter_patcher rollback` — disable the latest enabled bundle on a channel.
12
+ class RollbackCommand extends Command<int> {
13
+ RollbackCommand({this.config, this.backendOverride});
14
+
15
+ final FlutterPatcherConfig? config;
16
+ final Backend? backendOverride;
17
+
18
+ @override
19
+ String get name => 'rollback';
20
+
21
+ @override
22
+ String get description =>
23
+ 'Disable the latest enabled bundle on a channel (roll back).';
24
+
25
+ @override
26
+ ArgParser get argParser => ArgParser()
27
+ ..addOption('backend', abbr: 'b', help: 'Backend provider.')
28
+ ..addOption('channel', abbr: 'c', help: 'Channel to roll back.');
29
+
30
+ @override
31
+ Future<int> run() => runGuarded(() async {
32
+ final channel = argResults!['channel'] as String?;
33
+ if (channel == null || channel.isEmpty) {
34
+ throw StateError('Usage: flutter_patcher rollback --channel <channel>');
35
+ }
36
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
37
+ final backend = requireBackend(cfg, override: backendOverride);
38
+ final id = await rollbackChannel(backend, channel);
39
+ stdout.writeln('Rolled back channel "$channel" (disabled bundle $id)');
40
+ });
41
+ }