@_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.
- package/bin/flutter-patcher.js +37 -39
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +496 -0
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +743 -0
- package/dart-src/packages/cli-tools/.gitkeep +0 -0
- package/dart-src/packages/cli-tools/bin/flutter_patcher.dart +9 -0
- package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +73 -0
- package/dart-src/packages/cli-tools/lib/src/backend.dart +162 -0
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +56 -0
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +99 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +151 -0
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +140 -0
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +113 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +95 -0
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +48 -0
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +29 -0
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +82 -0
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +55 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +132 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +41 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +715 -0
- package/dart-src/packages/cli-tools/lib/src/operations.dart +206 -0
- package/dart-src/packages/cli-tools/lib/src/pack.dart +417 -0
- package/dart-src/packages/cli-tools/lib/src/sign.dart +60 -0
- package/dart-src/packages/cli-tools/lib/src/util.dart +86 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/packages/cli-tools/migrations/cloudflare/bundles.sql +42 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250103114225_init.sql +228 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +48 -0
- package/dart-src/packages/cli-tools/pubspec.lock +638 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +37 -0
- package/dart-src/packages/core/lib/flutter_patcher_core.dart +29 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +111 -0
- package/dart-src/packages/core/lib/src/bundle.dart +191 -0
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +39 -0
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +47 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +77 -0
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +78 -0
- package/dart-src/packages/core/lib/src/metadata.dart +31 -0
- package/dart-src/packages/core/lib/src/patch_info.dart +284 -0
- package/dart-src/packages/core/lib/src/platform.dart +15 -0
- package/dart-src/packages/core/lib/src/rollout.dart +209 -0
- package/dart-src/packages/core/lib/src/semver.dart +70 -0
- package/dart-src/packages/core/lib/src/semver_range.dart +358 -0
- package/dart-src/packages/core/lib/src/semver_version.dart +94 -0
- package/dart-src/packages/core/lib/src/status.dart +24 -0
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +30 -0
- package/dart-src/packages/core/lib/src/uuid.dart +45 -0
- package/dart-src/packages/core/pubspec.lock +381 -0
- package/dart-src/packages/core/pubspec.yaml +9 -0
- package/dart-src/packages/core/tool/dbg.dart +6 -0
- package/dart-src/plugins/aws/.gitkeep +0 -0
- package/dart-src/plugins/aws/lib/flutter_patcher_aws.dart +12 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +207 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_signer.dart +130 -0
- package/dart-src/plugins/aws/lib/src/aws_config.dart +59 -0
- package/dart-src/plugins/aws/lib/src/aws_database.dart +198 -0
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +72 -0
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +312 -0
- package/dart-src/plugins/aws/lib/src/aws_ssm_client.dart +125 -0
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +20 -0
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +123 -0
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +136 -0
- package/dart-src/plugins/aws/pubspec.lock +435 -0
- package/dart-src/plugins/aws/pubspec.yaml +22 -0
- package/dart-src/plugins/cloudflare/.gitkeep +0 -0
- package/dart-src/plugins/cloudflare/lib/flutter_patcher_cloudflare.dart +18 -0
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +29 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +93 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +131 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_client.dart +66 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_config.dart +30 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +16 -0
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +314 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_config.dart +50 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +287 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +23 -0
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +110 -0
- package/dart-src/plugins/cloudflare/migrations/0001_hot-updater_init.sql +16 -0
- package/dart-src/plugins/cloudflare/migrations/0002_hot-updater_0.13.0.sql +8 -0
- package/dart-src/plugins/cloudflare/migrations/0003_hot-updater_0.18.0.sql +38 -0
- package/dart-src/plugins/cloudflare/migrations/0004_hot-updater_0.29.0.sql +11 -0
- package/dart-src/plugins/cloudflare/migrations/0005_hot-updater_0.31.0.sql +24 -0
- package/dart-src/plugins/cloudflare/pubspec.lock +411 -0
- package/dart-src/plugins/cloudflare/pubspec.yaml +20 -0
- package/dart-src/plugins/cloudflare/sql/bundles.sql +42 -0
- package/dart-src/plugins/plugin-core/lib/flutter_patcher_plugin_core.dart +42 -0
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +94 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_storage_layout.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work.dart +259 -0
- package/dart-src/plugins/plugin-core/lib/src/bundle_unit_of_work_store.dart +20 -0
- package/dart-src/plugins/plugin-core/lib/src/calculate_pagination.dart +29 -0
- package/dart-src/plugins/plugin-core/lib/src/compression_format.dart +63 -0
- package/dart-src/plugins/plugin-core/lib/src/content_addressed_assets.dart +22 -0
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +853 -0
- package/dart-src/plugins/plugin-core/lib/src/create_database_plugin.dart +525 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_key_builder.dart +15 -0
- package/dart-src/plugins/plugin-core/lib/src/create_storage_plugin.dart +130 -0
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +18 -0
- package/dart-src/plugins/plugin-core/lib/src/generate_min_bundle_id.dart +19 -0
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +241 -0
- package/dart-src/plugins/plugin-core/lib/src/is_object.dart +5 -0
- package/dart-src/plugins/plugin-core/lib/src/legacy_asset_storage_layout.dart +9 -0
- package/dart-src/plugins/plugin-core/lib/src/paginate_bundles.dart +112 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +44 -0
- package/dart-src/plugins/plugin-core/lib/src/query_bundles.dart +70 -0
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +62 -0
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +43 -0
- package/dart-src/plugins/plugin-core/lib/src/storage_profile.dart +27 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +454 -0
- package/dart-src/plugins/plugin-core/lib/src/uuidv7.dart +41 -0
- package/dart-src/plugins/plugin-core/pubspec.lock +396 -0
- package/dart-src/plugins/plugin-core/pubspec.yaml +15 -0
- package/dart-src/plugins/postgres/.gitkeep +0 -0
- package/dart-src/plugins/postgres/lib/flutter_patcher_postgres.dart +17 -0
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +130 -0
- package/dart-src/plugins/postgres/lib/src/postgres_client.dart +90 -0
- package/dart-src/plugins/postgres/lib/src/postgres_config.dart +35 -0
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +302 -0
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +120 -0
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +205 -0
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +132 -0
- package/dart-src/plugins/postgres/pubspec.lock +427 -0
- package/dart-src/plugins/postgres/pubspec.yaml +18 -0
- package/dart-src/plugins/postgres/sql/bundles.sql +45 -0
- package/dart-src/plugins/postgres/sql/get_target_app_version_list.sql +21 -0
- package/dart-src/plugins/postgres/sql/get_update_info.spec.ts +171 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_app_version.sql +126 -0
- package/dart-src/plugins/postgres/sql/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/plugins/postgres/sql/hash_user_id.sql +30 -0
- package/dart-src/plugins/postgres/sql/is_device_eligible.sql +237 -0
- package/dart-src/plugins/postgres/sql/prepareSql.ts +11 -0
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +12 -0
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +107 -0
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +233 -0
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +175 -0
- package/dart-src/plugins/standalone/pubspec.lock +411 -0
- package/dart-src/plugins/standalone/pubspec.yaml +18 -0
- package/dart-src/plugins/supabase/lib/edge.dart +10 -0
- package/dart-src/plugins/supabase/lib/flutter_patcher_supabase.dart +21 -0
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +129 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_adapter.dart +154 -0
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +516 -0
- package/dart-src/plugins/supabase/lib/src/supabase_config.dart +34 -0
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +422 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +32 -0
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +99 -0
- package/dart-src/plugins/supabase/lib/src/supabase_signed_url_batcher.dart +145 -0
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +298 -0
- package/dart-src/plugins/supabase/lib/src/types.dart +130 -0
- package/dart-src/plugins/supabase/pubspec.lock +515 -0
- package/dart-src/plugins/supabase/pubspec.yaml +19 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/index.ts +44 -0
- package/dart-src/plugins/supabase/supabase/edge-functions/runtime.docker.integration.spec.ts +968 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +228 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +134 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +194 -0
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +184 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +503 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260414000000_hot-updater_0.30.0.sql +50 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260422000000_hot-updater_0.31.0.sql +20 -0
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +48 -0
- package/package.json +2 -1
- package/scripts/postinstall.js +32 -23
- /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
|
+
}
|