@_nazmiforreal/flutter-ota 0.1.0 → 0.1.2

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 (167) hide show
  1. package/bin/flutter-patcher.js +48 -39
  2. package/dart-src/packages/cli-tools/.gitkeep +0 -0
  3. package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
  4. package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
  5. package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
  6. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
  7. package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
  8. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
  9. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
  10. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
  11. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
  13. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
  14. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
  15. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
  16. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
  17. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
  18. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
  19. package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
  20. package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
  21. package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
  22. package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
  23. package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
  24. package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
  25. package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
  26. package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
  27. package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
  28. package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
  29. package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
  30. package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
  31. package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
  32. package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
  33. package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
  34. package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
  35. package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
  36. package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
  37. package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
  38. package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
  39. package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
  40. package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
  41. package/dart-src/packages/core/lib/src/bundle.dart +191 -0
  42. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
  43. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
  44. package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
  45. package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
  46. package/dart-src/packages/core/lib/src/metadata.dart +31 -0
  47. package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
  48. package/dart-src/packages/core/lib/src/platform.dart +15 -0
  49. package/dart-src/packages/core/lib/src/rollout.dart +209 -0
  50. package/dart-src/packages/core/lib/src/semver.dart +70 -0
  51. package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
  52. package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
  53. package/dart-src/packages/core/lib/src/status.dart +24 -0
  54. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  55. package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
  56. package/dart-src/packages/core/lib/src/uuid.dart +45 -0
  57. package/dart-src/packages/core/pubspec.yaml +9 -0
  58. package/dart-src/packages/core/tool/dbg.dart +6 -0
  59. package/dart-src/plugins/aws/.gitkeep +0 -0
  60. package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
  61. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
  62. package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
  63. package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
  64. package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
  65. package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
  66. package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
  67. package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
  68. package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
  69. package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
  70. package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
  71. package/dart-src/plugins/aws/pubspec.yaml +22 -0
  72. package/dart-src/plugins/cloudflare/.gitkeep +0 -0
  73. package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
  74. package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
  75. package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
  76. package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
  77. package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
  78. package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
  79. package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
  80. package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
  81. package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
  82. package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
  83. package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
  84. package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
  85. package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
  86. package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
  87. package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
  88. package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
  89. package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
  90. package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
  91. package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
  92. package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
  93. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
  94. package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
  95. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
  96. package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
  97. package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
  98. package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
  99. package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
  100. package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
  101. package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
  102. package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
  103. package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
  104. package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
  105. package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
  106. package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
  107. package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
  108. package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
  109. package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
  110. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
  111. package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
  112. package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
  113. package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
  114. package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
  115. package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
  116. package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
  117. package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
  118. package/dart-src/plugins/postgres/.gitkeep +0 -0
  119. package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
  120. package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
  121. package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
  122. package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
  123. package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
  124. package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
  125. package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
  126. package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
  127. package/dart-src/plugins/postgres/pubspec.yaml +18 -0
  128. package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
  129. package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
  130. package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
  131. package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
  132. package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
  133. package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
  134. package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
  135. package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
  136. package/dart-src/plugins/standalone/.gitkeep +0 -0
  137. package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
  138. package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
  139. package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
  140. package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
  141. package/dart-src/plugins/standalone/pubspec.yaml +18 -0
  142. package/dart-src/plugins/supabase/lib/edge.dart +10 -0
  143. package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
  144. package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
  145. package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
  146. package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
  147. package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
  148. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
  149. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
  150. package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
  151. package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
  152. package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
  153. package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
  154. package/dart-src/plugins/supabase/pubspec.yaml +19 -0
  155. package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
  156. package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
  157. package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
  158. package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
  159. package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
  160. package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
  161. package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
  162. package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
  163. package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
  164. package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
  165. package/package.json +2 -1
  166. package/scripts/postinstall.js +53 -25
  167. /package/bin/{flutter-patcher-linux → flutter-patcher-linux-x64} +0 -0
@@ -0,0 +1,140 @@
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 channel` — manage channels.
12
+ class ChannelCommand extends Command<int> {
13
+ final FlutterPatcherConfig? config;
14
+ final Backend? backendOverride;
15
+
16
+ ChannelCommand({this.config, this.backendOverride}) {
17
+ addSubcommand(ChannelListCommand(config: config, backendOverride: backendOverride));
18
+ addSubcommand(ChannelGetCommand(config: config, backendOverride: backendOverride));
19
+ addSubcommand(ChannelSetCommand(config: config, backendOverride: backendOverride));
20
+ }
21
+
22
+ @override
23
+ String get name => 'channel';
24
+
25
+ @override
26
+ String get description => 'Manage channels (list / get / set).';
27
+
28
+ @override
29
+ Future<int> run() => runGuarded(() async {
30
+ print(description);
31
+ print('');
32
+ print('Subcommands:');
33
+ print(' list List channels');
34
+ print(' get <channel> Show the live bundle for a channel');
35
+ print(' set <c> <id> Promote a bundle to a channel');
36
+ });
37
+ }
38
+
39
+ class ChannelListCommand extends Command<int> {
40
+ ChannelListCommand({this.config, this.backendOverride});
41
+
42
+ final FlutterPatcherConfig? config;
43
+ final Backend? backendOverride;
44
+
45
+ @override
46
+ String get name => 'list';
47
+
48
+ @override
49
+ String get description => 'List channels.';
50
+
51
+ @override
52
+ ArgParser get argParser => ArgParser()
53
+ ..addOption('backend', abbr: 'b', help: 'Backend provider.');
54
+
55
+ @override
56
+ Future<int> run() => runGuarded(() async {
57
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
58
+ final backend = requireBackend(cfg, override: backendOverride);
59
+ final channels = await listChannels(backend);
60
+ if (channels.isEmpty) {
61
+ stdout.writeln('(no channels)');
62
+ return;
63
+ }
64
+ for (final c in channels) {
65
+ stdout.writeln(' $c');
66
+ }
67
+ });
68
+ }
69
+
70
+ class ChannelGetCommand extends Command<int> {
71
+ ChannelGetCommand({this.config, this.backendOverride});
72
+
73
+ final FlutterPatcherConfig? config;
74
+ final Backend? backendOverride;
75
+
76
+ @override
77
+ String get name => 'get';
78
+
79
+ @override
80
+ String get description => 'Show the live (enabled) bundle for a channel.';
81
+
82
+ @override
83
+ ArgParser get argParser => ArgParser()
84
+ ..addOption('backend', abbr: 'b', help: 'Backend provider.')
85
+ ..addOption('channel', help: 'Channel.');
86
+
87
+ @override
88
+ Future<int> run() => runGuarded(() async {
89
+ final channel = argResults!['channel'] as String?;
90
+ if (channel == null || channel.isEmpty) {
91
+ throw StateError('Usage: flutter_patcher channel get <channel>');
92
+ }
93
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
94
+ final backend = requireBackend(cfg, override: backendOverride);
95
+ final bundle = await getChannel(backend, channel);
96
+ if (bundle == null) {
97
+ stdout.writeln('(no live bundle on channel "$channel")');
98
+ return;
99
+ }
100
+ stdout.writeln('channel "$channel" -> bundle ${bundle.id} '
101
+ '(${bundle.platform.value})');
102
+ });
103
+ }
104
+
105
+ class ChannelSetCommand extends Command<int> {
106
+ ChannelSetCommand({this.config, this.backendOverride});
107
+
108
+ final FlutterPatcherConfig? config;
109
+ final Backend? backendOverride;
110
+
111
+ @override
112
+ String get name => 'set';
113
+
114
+ @override
115
+ String get description => 'Promote a bundle to a channel.';
116
+
117
+ @override
118
+ ArgParser get argParser => ArgParser()
119
+ ..addOption('backend', abbr: 'b', help: 'Backend provider.')
120
+ ..addOption('channel', help: 'Channel.')
121
+ ..addOption('bundle-id', help: 'Bundle id.');
122
+
123
+ @override
124
+ Future<int> run() => runGuarded(() async {
125
+ final channel = argResults!['channel'] as String?;
126
+ final bundleId = argResults!['bundle-id'] as String?;
127
+ if (channel == null ||
128
+ channel.isEmpty ||
129
+ bundleId == null ||
130
+ bundleId.isEmpty) {
131
+ throw StateError(
132
+ 'Usage: flutter_patcher channel set --channel <c> --bundle-id <id>',
133
+ );
134
+ }
135
+ final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
136
+ final backend = requireBackend(cfg, override: backendOverride);
137
+ await promoteBundle(backend, bundleId, channel);
138
+ stdout.writeln('Channel "$channel" -> bundle $bundleId');
139
+ });
140
+ }
@@ -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
+ }