@_nazmiforreal/flutter-ota 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -13
- package/bin/{flutter-patcher-linux-x64 → flutter-ota-linux-x64} +0 -0
- package/bin/{flutter-patcher.js → flutter-ota.js} +4 -4
- package/dart-src/packages/cli-tools/bin/{flutter_patcher.dart → flutter_ota_kit.dart} +2 -2
- package/dart-src/packages/cli-tools/lib/{flutter_patcher_cli.dart → flutter_ota_kit_cli.dart} +5 -3
- package/dart-src/packages/cli-tools/lib/src/backend.dart +6 -25
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +10 -4
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +5 -3
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +279 -10
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +7 -7
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +3 -3
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +294 -45
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +218 -69
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +5 -5
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +117 -0
- package/dart-src/packages/cli-tools/lib/src/config.dart +29 -78
- package/dart-src/packages/cli-tools/lib/src/operations.dart +4 -4
- package/dart-src/packages/cli-tools/lib/src/pack.dart +12 -6
- package/dart-src/packages/cli-tools/lib/src/runner.dart +25 -6
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +5 -3
- package/dart-src/packages/cli-tools/lib/src/verbose.dart +3 -0
- package/dart-src/packages/cli-tools/migrations/aws/README.md +7 -0
- package/dart-src/packages/cli-tools/migrations/postgres/bundles.sql +45 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_target_app_version_list.sql +21 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_app_version.sql +126 -0
- package/dart-src/packages/cli-tools/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
- package/dart-src/packages/cli-tools/migrations/postgres/hash_user_id.sql +30 -0
- package/dart-src/packages/cli-tools/migrations/postgres/is_device_eligible.sql +237 -0
- package/dart-src/packages/cli-tools/migrations/supabase/20260520014100_hot-updater_rls.sql +11 -0
- package/dart-src/packages/cli-tools/pubspec.yaml +9 -11
- package/dart-src/packages/cli-tools/test/backends_test.dart +4 -4
- package/dart-src/packages/cli-tools/test/build_pack_test.dart +1 -1
- package/dart-src/packages/cli-tools/test/cli_test.dart +3 -3
- package/dart-src/packages/cli-tools/test/mocks/mock_aws_s3_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_d1_client.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_postgres_client.dart +7 -7
- package/dart-src/packages/core/lib/{flutter_patcher_core.dart → flutter_ota_kit_core.dart} +1 -1
- package/dart-src/packages/core/pubspec.yaml +2 -2
- package/dart-src/packages/core/test/rollout_test.dart +1 -1
- package/dart-src/packages/core/test/semver_test.dart +1 -1
- package/dart-src/packages/core/test/types_test.dart +2 -2
- package/dart-src/packages/core/test/uuid_test.dart +1 -1
- package/dart-src/packages/core/tool/dbg.dart +1 -1
- package/dart-src/plugins/aws/lib/{flutter_patcher_aws.dart → flutter_ota_kit_aws.dart} +0 -2
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_database.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_s3_client.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_storage.dart +1 -1
- package/dart-src/plugins/aws/lib/src/aws_storage_profile.dart +25 -3
- package/dart-src/plugins/aws/lib/src/with_cloudfront_signed_url.dart +1 -1
- package/dart-src/plugins/aws/pubspec.yaml +4 -4
- package/dart-src/plugins/aws/test/aws_database_test.dart +3 -3
- package/dart-src/plugins/aws/test/aws_storage_test.dart +2 -2
- package/dart-src/plugins/aws/test/mock_aws_s3_client.dart +2 -2
- package/dart-src/plugins/cloudflare/lib/src/cloudflare_worker_database.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_build_where.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_bundle_mapper.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_database.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/d1_database_plugin.dart +2 -2
- package/dart-src/plugins/cloudflare/lib/src/r2_s3_client.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_storage.dart +1 -1
- package/dart-src/plugins/cloudflare/lib/src/r2_storage_profile.dart +1 -1
- package/dart-src/plugins/cloudflare/pubspec.yaml +4 -4
- package/dart-src/plugins/cloudflare/test/cloudflare_worker_database_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/d1_bundle_mapper_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/d1_database_test.dart +2 -2
- package/dart-src/plugins/cloudflare/test/mock_d1_client.dart +2 -2
- package/dart-src/plugins/cloudflare/test/mock_r2_client.dart +2 -2
- package/dart-src/plugins/cloudflare/test/r2_storage_test.dart +2 -2
- package/dart-src/plugins/plugin-core/lib/{flutter_patcher_plugin_core.dart → flutter_ota_kit_plugin_core.dart} +1 -1
- package/dart-src/plugins/plugin-core/lib/src/create_blob_database_plugin.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/filter_compatible_app_versions.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/get_update_info.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/request_update_bundle_state.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/resolve_update_info_from_bundles.dart +1 -1
- package/dart-src/plugins/plugin-core/lib/src/types.dart +2 -2
- package/dart-src/plugins/plugin-core/pubspec.yaml +3 -3
- package/dart-src/plugins/plugin-core/test/asset_storage_layout_test.dart +3 -3
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_store_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/bundle_unit_of_work_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/create_blob_database_plugin_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/create_database_plugin_test.dart +5 -5
- package/dart-src/plugins/plugin-core/test/create_storage_plugin_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/generate_min_bundle_id_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/get_update_info_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/helpers_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/query_bundles_test.dart +2 -2
- package/dart-src/plugins/plugin-core/test/request_update_bundle_state_test.dart +3 -3
- package/dart-src/plugins/plugin-core/test/uuidv7_test.dart +1 -1
- package/dart-src/plugins/postgres/lib/{flutter_patcher_postgres.dart → flutter_ota_kit_postgres.dart} +1 -1
- package/dart-src/plugins/postgres/lib/src/postgres_bundle_mapper.dart +1 -1
- package/dart-src/plugins/postgres/lib/src/postgres_database.dart +2 -2
- package/dart-src/plugins/postgres/lib/src/postgres_get_update_info.dart +2 -2
- package/dart-src/plugins/postgres/lib/src/postgres_storage.dart +9 -9
- package/dart-src/plugins/postgres/lib/src/postgres_types.dart +1 -1
- package/dart-src/plugins/postgres/pubspec.yaml +4 -4
- package/dart-src/plugins/postgres/test/mock_postgres_client.dart +2 -2
- package/dart-src/plugins/postgres/test/postgres_bundle_mapper_test.dart +2 -2
- package/dart-src/plugins/postgres/test/postgres_database_test.dart +2 -2
- package/dart-src/plugins/supabase/lib/edge.dart +1 -1
- package/dart-src/plugins/supabase/lib/{flutter_patcher_supabase.dart → flutter_ota_kit_supabase.dart} +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_bundle_mapper.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +60 -20
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +2 -2
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_database.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_edge_function_storage.dart +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_storage.dart +2 -5
- package/dart-src/plugins/supabase/pubspec.yaml +4 -4
- package/dart-src/plugins/supabase/test/mock_supabase_client.dart +5 -5
- package/dart-src/plugins/supabase/test/supabase_bundle_mapper_test.dart +3 -3
- package/dart-src/plugins/supabase/test/supabase_database_test.dart +3 -3
- package/dart-src/plugins/supabase/test/supabase_storage_test.dart +2 -2
- package/migrations/aws/README.md +7 -0
- package/migrations/cloudflare/0001_hot-updater_init.sql +16 -0
- package/migrations/cloudflare/0002_hot-updater_0.13.0.sql +8 -0
- package/migrations/cloudflare/0003_hot-updater_0.18.0.sql +38 -0
- package/migrations/cloudflare/0004_hot-updater_0.29.0.sql +11 -0
- package/migrations/cloudflare/0005_hot-updater_0.31.0.sql +24 -0
- package/migrations/cloudflare/bundles.sql +42 -0
- package/migrations/postgres/bundles.sql +45 -0
- package/migrations/postgres/get_target_app_version_list.sql +21 -0
- package/migrations/postgres/get_update_info_by_app_version.sql +126 -0
- package/migrations/postgres/get_update_info_by_fingerprint_hash.sql +125 -0
- package/migrations/postgres/hash_user_id.sql +30 -0
- package/migrations/postgres/is_device_eligible.sql +237 -0
- package/package.json +4 -4
- package/scripts/postinstall.js +15 -15
- package/scripts/vendor.sh +1 -1
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -496
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -743
- package/dart-src/packages/cli-tools/pubspec.lock +0 -638
- package/dart-src/packages/cli-tools/test/standalone_backend_test.dart +0 -315
- package/dart-src/plugins/aws/lib/src/aws_lambda_edge_storage.dart +0 -72
- package/dart-src/plugins/aws/test/aws_lambda_edge_storage_test.dart +0 -190
- package/dart-src/plugins/standalone/.gitkeep +0 -0
- package/dart-src/plugins/standalone/lib/flutter_patcher_standalone.dart +0 -12
- package/dart-src/plugins/standalone/lib/src/standalone_config.dart +0 -107
- package/dart-src/plugins/standalone/lib/src/standalone_database.dart +0 -233
- package/dart-src/plugins/standalone/lib/src/standalone_storage.dart +0 -175
- package/dart-src/plugins/standalone/pubspec.yaml +0 -18
- package/dart-src/plugins/standalone/test/standalone_test.dart +0 -264
- /package/dart-src/plugins/cloudflare/lib/{flutter_patcher_cloudflare.dart → flutter_ota_kit_cloudflare.dart} +0 -0
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
1
2
|
import 'dart:io';
|
|
2
3
|
|
|
3
4
|
import 'package:args/args.dart';
|
|
4
|
-
import 'package:
|
|
5
|
+
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
6
|
+
import 'package:http/http.dart' as http;
|
|
5
7
|
import 'package:path/path.dart' as p;
|
|
6
8
|
import 'package:postgres/postgres.dart';
|
|
7
9
|
|
|
8
10
|
import '../ui/ui.dart';
|
|
9
11
|
|
|
10
|
-
/// `
|
|
12
|
+
/// `flutter_ota_kit migrate` — run backend SQL migrations.
|
|
11
13
|
class MigrateCommand extends FlutterPatcherCommand {
|
|
12
14
|
@override
|
|
13
15
|
String get name => 'migrate';
|
|
@@ -21,20 +23,44 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
21
23
|
..addOption('backend', abbr: 'b', help: 'Backend provider.')
|
|
22
24
|
..addOption('database-url',
|
|
23
25
|
help: 'Postgres connection string (or DATABASE_URL env).')
|
|
26
|
+
..addOption('management-key',
|
|
27
|
+
help: 'Supabase Management API key (or SUPABASE_MANAGEMENT_KEY env) — '
|
|
28
|
+
'runs migrations without a separate Postgres connection.')
|
|
24
29
|
..addOption('migrations-dir',
|
|
25
30
|
help: 'Directory of *.sql migration files (ordered by name).')
|
|
26
|
-
..addFlag('dry-run', help: 'Print migrations instead of applying them.');
|
|
31
|
+
..addFlag('dry-run', abbr: 'd', help: 'Print migrations instead of applying them.');
|
|
27
32
|
|
|
28
|
-
/// Default migrations dir lives next to the CLI entrypoint;
|
|
29
|
-
///
|
|
33
|
+
/// Default migrations dir lives next to the CLI entrypoint; each backend has
|
|
34
|
+
/// its own dialect's migrations (cloudflare = D1 SQLite, postgres = plain
|
|
35
|
+
/// Postgres, supabase = PostgREST, aws = no relational SQL).
|
|
30
36
|
String defaultMigrationsDir(String provider) {
|
|
31
37
|
final sub = switch (provider) {
|
|
38
|
+
'supabase' => 'supabase',
|
|
39
|
+
'postgres' => 'postgres',
|
|
32
40
|
'cloudflare' => 'cloudflare',
|
|
41
|
+
'aws' => 'aws',
|
|
33
42
|
_ => 'supabase',
|
|
34
43
|
};
|
|
35
44
|
return p.join(p.dirname(Platform.script.path), '..', 'migrations', sub);
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
List<File> _listMigrations(String dir) {
|
|
48
|
+
final migrationsDir = Directory(dir);
|
|
49
|
+
if (!migrationsDir.existsSync()) {
|
|
50
|
+
throw StateError('Migrations directory not found: $dir');
|
|
51
|
+
}
|
|
52
|
+
final files = migrationsDir
|
|
53
|
+
.listSync()
|
|
54
|
+
.whereType<File>()
|
|
55
|
+
.where((f) => f.path.endsWith('.sql'))
|
|
56
|
+
.toList()
|
|
57
|
+
..sort((a, b) => a.path.compareTo(b.path));
|
|
58
|
+
if (files.isEmpty) {
|
|
59
|
+
throw StateError('No *.sql files in $dir');
|
|
60
|
+
}
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
|
|
38
64
|
List<String> _splitStatements(String sql) {
|
|
39
65
|
return sql
|
|
40
66
|
.split(';')
|
|
@@ -46,21 +72,26 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
46
72
|
@override
|
|
47
73
|
Future<int> run() => runGuarded(() async {
|
|
48
74
|
final provider = argResults!['backend'] as String? ?? 'supabase';
|
|
75
|
+
|
|
76
|
+
// Backends with no relational SQL handled out-of-band (S3 blob store).
|
|
77
|
+
// Short-circuit before any SQL discovery so a missing or empty
|
|
78
|
+
// (README-only) migrations dir does not abort.
|
|
79
|
+
if (provider == 'aws') {
|
|
80
|
+
final dir = argResults!['migrations-dir'] as String? ??
|
|
81
|
+
defaultMigrationsDir(provider);
|
|
82
|
+
banner('migrate');
|
|
83
|
+
box('migrate', [
|
|
84
|
+
'Migrations for "$provider" do not run through this command:',
|
|
85
|
+
' • aws → S3 blob store, no relational SQL (see $dir/README.md)',
|
|
86
|
+
'',
|
|
87
|
+
'See the README under $dir for the intended setup.',
|
|
88
|
+
]);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
49
92
|
final dir = argResults!['migrations-dir'] as String? ??
|
|
50
93
|
defaultMigrationsDir(provider);
|
|
51
|
-
final
|
|
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
|
-
}
|
|
94
|
+
final files = _listMigrations(dir);
|
|
64
95
|
|
|
65
96
|
if (argResults!['dry-run'] as bool) {
|
|
66
97
|
banner('migrate · dry-run');
|
|
@@ -71,67 +102,185 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
71
102
|
return;
|
|
72
103
|
}
|
|
73
104
|
|
|
74
|
-
|
|
75
|
-
Platform.environment['DATABASE_URL'];
|
|
76
|
-
if (provider == 'cloudflare' || provider == 'aws') {
|
|
105
|
+
if (provider == 'cloudflare') {
|
|
77
106
|
banner('migrate');
|
|
78
107
|
box('migrate', [
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
'
|
|
108
|
+
'Cloudflare (D1 SQLite) migrations run via `wrangler d1 execute`:',
|
|
109
|
+
' wrangler d1 execute <DB> --file=$dir/0001_hot-updater_init.sql',
|
|
110
|
+
' (apply each *.sql under $dir in filename order)',
|
|
82
111
|
'',
|
|
83
|
-
'The SQL files under $
|
|
112
|
+
'The SQL files under $dir show the intended D1 schema.',
|
|
84
113
|
]);
|
|
85
114
|
return;
|
|
86
115
|
}
|
|
87
|
-
if (url == null || url.isEmpty) {
|
|
88
|
-
throw StateError('Provide --database-url or set DATABASE_URL.');
|
|
89
|
-
}
|
|
90
116
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
username: uri.userInfo.isEmpty ? null : uri.userInfo.split(':').first,
|
|
97
|
-
password: uri.userInfo.contains(':')
|
|
98
|
-
? uri.userInfo.split(':').last
|
|
99
|
-
: null,
|
|
100
|
-
);
|
|
101
|
-
banner('migrate');
|
|
102
|
-
final conn = await Connection.open(
|
|
103
|
-
endpoint,
|
|
104
|
-
settings: ConnectionSettings(sslMode: SslMode.disable),
|
|
105
|
-
);
|
|
106
|
-
try {
|
|
107
|
-
await conn.execute(
|
|
108
|
-
'CREATE TABLE IF NOT EXISTS _flutter_patcher_migrations '
|
|
109
|
-
'(name text primary key, applied_at timestamptz default now())',
|
|
110
|
-
queryMode: QueryMode.simple,
|
|
117
|
+
if (provider == 'supabase') {
|
|
118
|
+
final cfg = loadConfig() ??
|
|
119
|
+
FlutterPatcherConfig(
|
|
120
|
+
provider: 'supabase',
|
|
121
|
+
supabase: const SupabaseConfigJson(),
|
|
111
122
|
);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
123
|
+
final mgmtKey = argResults!['management-key'] as String? ??
|
|
124
|
+
Platform.environment['SUPABASE_MANAGEMENT_KEY'] ??
|
|
125
|
+
cfg.supabase.managementKey;
|
|
126
|
+
final pgUrl = argResults!['database-url'] as String? ??
|
|
127
|
+
Platform.environment['DATABASE_URL'] ??
|
|
128
|
+
cfg.supabase.databaseUrl;
|
|
129
|
+
if (mgmtKey != null && mgmtKey.isNotEmpty) {
|
|
130
|
+
await _runViaManagementApi(mgmtKey, files);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (pgUrl != null && pgUrl.isNotEmpty) {
|
|
134
|
+
await _runViaPostgres(pgUrl, files);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
throw StateError(
|
|
138
|
+
'For the supabase backend, provide either:\n'
|
|
139
|
+
' • --management-key (or SUPABASE_MANAGEMENT_KEY, or set it via '
|
|
140
|
+
'`flutter-ota init`) — a Supabase Management API key, OR\n'
|
|
141
|
+
' • --database-url (or DATABASE_URL, or set it via `init`) — a '
|
|
142
|
+
'Postgres connection string.',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (provider == 'postgres') {
|
|
147
|
+
final pgUrl = argResults!['database-url'] as String? ??
|
|
148
|
+
Platform.environment['DATABASE_URL'];
|
|
149
|
+
if (pgUrl == null || pgUrl.isEmpty) {
|
|
150
|
+
throw StateError(
|
|
151
|
+
'For the postgres backend, provide --database-url or set DATABASE_URL.',
|
|
130
152
|
);
|
|
131
|
-
step('applied $name');
|
|
132
153
|
}
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
await _runViaPostgres(pgUrl, files);
|
|
155
|
+
return;
|
|
135
156
|
}
|
|
157
|
+
|
|
158
|
+
throw StateError('Unknown backend provider: "$provider".');
|
|
136
159
|
});
|
|
160
|
+
|
|
161
|
+
/// Run migrations through the Supabase Management API (no Postgres creds
|
|
162
|
+
/// needed). Derived from the Supabase project ref in the configured URL.
|
|
163
|
+
Future<void> _runViaManagementApi(String mgmtKey, List<File> files) async {
|
|
164
|
+
final cfg = resolveSupabaseConfig(
|
|
165
|
+
loadConfig() ??
|
|
166
|
+
FlutterPatcherConfig(
|
|
167
|
+
provider: 'supabase',
|
|
168
|
+
supabase: SupabaseConfigJson(),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
final ref = Uri.parse(cfg.supabaseUrl).host.split('.').first;
|
|
172
|
+
final endpoint =
|
|
173
|
+
'https://api.supabase.com/v1/projects/$ref/database/query';
|
|
174
|
+
banner('migrate · supabase (management api)');
|
|
175
|
+
for (final file in files) {
|
|
176
|
+
final sql = file.readAsStringSync();
|
|
177
|
+
final res = await http.post(
|
|
178
|
+
Uri.parse(endpoint),
|
|
179
|
+
headers: {
|
|
180
|
+
'Authorization': 'Bearer $mgmtKey',
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
},
|
|
183
|
+
body: jsonEncode({'query': sql}),
|
|
184
|
+
);
|
|
185
|
+
if (res.statusCode >= 400) {
|
|
186
|
+
throw StateError(
|
|
187
|
+
'Migration ${p.basename(file.path)} failed '
|
|
188
|
+
'(${res.statusCode}): ${res.body}',
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
step('applied ${p.basename(file.path)}');
|
|
192
|
+
}
|
|
193
|
+
await _ensureSupabaseBucket();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// Create the Supabase Storage bucket (public) if it doesn't exist, so
|
|
197
|
+
/// `deploy` can upload artifacts without a manual setup step.
|
|
198
|
+
Future<void> _ensureSupabaseBucket() async {
|
|
199
|
+
final storage = resolveSupabaseStorageConfig(
|
|
200
|
+
loadConfig() ??
|
|
201
|
+
FlutterPatcherConfig(
|
|
202
|
+
provider: 'supabase',
|
|
203
|
+
supabase: SupabaseConfigJson(),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
if (storage.supabaseServiceRoleKey == null) {
|
|
207
|
+
box('migrate', [
|
|
208
|
+
'Skipped storage bucket creation — set supabase.serviceRoleKey '
|
|
209
|
+
'(or SUPABASE_SERVICE_ROLE_KEY) to auto-create the '
|
|
210
|
+
'"${storage.bucketName}" bucket.',
|
|
211
|
+
]);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
final res = await http.post(
|
|
215
|
+
Uri.parse('${storage.supabaseUrl}/storage/v1/bucket'),
|
|
216
|
+
headers: {
|
|
217
|
+
'Authorization': 'Bearer ${storage.supabaseServiceRoleKey}',
|
|
218
|
+
'apikey': storage.supabaseServiceRoleKey!,
|
|
219
|
+
'Content-Type': 'application/json',
|
|
220
|
+
},
|
|
221
|
+
body: jsonEncode({
|
|
222
|
+
'name': storage.bucketName,
|
|
223
|
+
'public': true,
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
if (res.statusCode >= 400) {
|
|
227
|
+
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
228
|
+
final msg = (body['message'] ?? body['error'] ?? '').toString();
|
|
229
|
+
if (!msg.contains('already exists') && res.statusCode != 409) {
|
|
230
|
+
box('migrate', [
|
|
231
|
+
'Warning: could not create bucket "${storage.bucketName}": $msg',
|
|
232
|
+
]);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
step('ensured storage bucket "${storage.bucketName}"');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Original path: connect directly to Postgres and execute each statement.
|
|
240
|
+
Future<void> _runViaPostgres(String url, List<File> files) async {
|
|
241
|
+
final uri = Uri.parse(url);
|
|
242
|
+
final endpoint = Endpoint(
|
|
243
|
+
host: uri.host,
|
|
244
|
+
port: uri.port == 0 ? 5432 : uri.port,
|
|
245
|
+
database: uri.path.isEmpty ? 'postgres' : uri.path.substring(1),
|
|
246
|
+
username: uri.userInfo.isEmpty ? null : uri.userInfo.split(':').first,
|
|
247
|
+
password: uri.userInfo.contains(':')
|
|
248
|
+
? uri.userInfo.split(':').last
|
|
249
|
+
: null,
|
|
250
|
+
);
|
|
251
|
+
banner('migrate · postgres');
|
|
252
|
+
final conn = await Connection.open(
|
|
253
|
+
endpoint,
|
|
254
|
+
settings: ConnectionSettings(sslMode: SslMode.disable),
|
|
255
|
+
);
|
|
256
|
+
try {
|
|
257
|
+
await conn.execute(
|
|
258
|
+
'CREATE TABLE IF NOT EXISTS _flutter_ota_kit_migrations '
|
|
259
|
+
'(name text primary key, applied_at timestamptz default now())',
|
|
260
|
+
queryMode: QueryMode.simple,
|
|
261
|
+
);
|
|
262
|
+
for (final file in files) {
|
|
263
|
+
final name = p.basename(file.path);
|
|
264
|
+
final escaped = name.replaceAll("'", "''");
|
|
265
|
+
final existing = await conn.execute(
|
|
266
|
+
"SELECT 1 FROM _flutter_ota_kit_migrations WHERE name = '$escaped'",
|
|
267
|
+
queryMode: QueryMode.simple,
|
|
268
|
+
);
|
|
269
|
+
if (existing.isNotEmpty) {
|
|
270
|
+
stdout.writeln(' ${dim('skip')} $name (already applied)');
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
for (final stmt in _splitStatements(file.readAsStringSync())) {
|
|
274
|
+
await conn.execute(stmt, queryMode: QueryMode.simple);
|
|
275
|
+
}
|
|
276
|
+
await conn.execute(
|
|
277
|
+
"INSERT INTO _flutter_ota_kit_migrations(name) VALUES ('$escaped')",
|
|
278
|
+
queryMode: QueryMode.simple,
|
|
279
|
+
);
|
|
280
|
+
step('applied $name');
|
|
281
|
+
}
|
|
282
|
+
} finally {
|
|
283
|
+
await conn.close();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
137
286
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import 'package:args/args.dart';
|
|
2
|
-
import 'package:
|
|
3
|
-
import 'package:
|
|
2
|
+
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
3
|
+
import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart';
|
|
4
4
|
|
|
5
5
|
import '../ui/ui.dart';
|
|
6
6
|
|
|
7
|
-
/// `
|
|
7
|
+
/// `flutter_ota_kit rollback` — roll a channel back to a previous bundle.
|
|
8
8
|
class RollbackCommand extends FlutterPatcherCommand {
|
|
9
9
|
RollbackCommand({this.config, this.backendOverride});
|
|
10
10
|
|
|
@@ -22,7 +22,7 @@ class RollbackCommand extends FlutterPatcherCommand {
|
|
|
22
22
|
ArgParser get argParser => ArgParser()
|
|
23
23
|
..addOption('backend', abbr: 'b', help: 'Backend provider.')
|
|
24
24
|
..addOption('channel', abbr: 'c', help: 'Channel to roll back.')
|
|
25
|
-
..addOption('bundle-id', help: 'Roll back to this specific bundle id.')
|
|
25
|
+
..addOption('bundle-id', abbr: 'i', help: 'Roll back to this specific bundle id.')
|
|
26
26
|
..addOption('platform', abbr: 'p', help: 'Platform filter (e.g. android).');
|
|
27
27
|
|
|
28
28
|
@override
|
|
@@ -30,7 +30,7 @@ class RollbackCommand extends FlutterPatcherCommand {
|
|
|
30
30
|
final channel = argResults!['channel'] as String?;
|
|
31
31
|
if (channel == null || channel.isEmpty) {
|
|
32
32
|
throw PackException(
|
|
33
|
-
'Usage:
|
|
33
|
+
'Usage: flutter_ota_kit rollback --channel <channel> [--bundle-id <id>]',
|
|
34
34
|
64,
|
|
35
35
|
);
|
|
36
36
|
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import 'package:args/args.dart';
|
|
2
|
+
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
3
|
+
|
|
4
|
+
import '../ui/ui.dart';
|
|
5
|
+
|
|
6
|
+
/// `flutter_ota_kit storage` — inspect and clean the blob storage bucket
|
|
7
|
+
/// directly from the CLI (no need to open the provider dashboard).
|
|
8
|
+
class StorageCommand extends FlutterPatcherCommand {
|
|
9
|
+
StorageCommand({this.config, this.backendOverride}) {
|
|
10
|
+
addSubcommand(StorageListCommand(config: config, backendOverride: backendOverride));
|
|
11
|
+
addSubcommand(StorageDeleteCommand(config: config, backendOverride: backendOverride));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
final FlutterPatcherConfig? config;
|
|
15
|
+
final Backend? backendOverride;
|
|
16
|
+
|
|
17
|
+
@override
|
|
18
|
+
String get name => 'storage';
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
String get description =>
|
|
22
|
+
'Inspect and manage bundle storage objects (find / delete orphans).';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class StorageListCommand extends FlutterPatcherCommand {
|
|
26
|
+
StorageListCommand({this.config, this.backendOverride});
|
|
27
|
+
|
|
28
|
+
final FlutterPatcherConfig? config;
|
|
29
|
+
final Backend? backendOverride;
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
String get name => 'list';
|
|
33
|
+
|
|
34
|
+
@override
|
|
35
|
+
String get description => 'List storage objects.';
|
|
36
|
+
|
|
37
|
+
@override
|
|
38
|
+
ArgParser get argParser => ArgParser()
|
|
39
|
+
..addOption('backend', abbr: 'b', help: 'Backend provider.')
|
|
40
|
+
..addOption('prefix', help: 'Key prefix filter (e.g. bundles).');
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
Future<int> run() => runGuarded(() async {
|
|
44
|
+
final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
|
|
45
|
+
final backend = requireBackend(cfg, override: backendOverride);
|
|
46
|
+
final prefix = argResults!['prefix'] as String?;
|
|
47
|
+
banner('storage · list');
|
|
48
|
+
final objects = await backend.storage.listObjects(
|
|
49
|
+
prefix == null || prefix.isEmpty ? null : prefix,
|
|
50
|
+
);
|
|
51
|
+
if (objects.isEmpty) {
|
|
52
|
+
step('(no objects)');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
final lines = <String>[];
|
|
56
|
+
for (final o in objects) {
|
|
57
|
+
final size = o.size >= 1024 * 1024
|
|
58
|
+
? '${(o.size / (1024 * 1024)).toStringAsFixed(2)} MB'
|
|
59
|
+
: o.size >= 1024
|
|
60
|
+
? '${(o.size / 1024).toStringAsFixed(1)} KB'
|
|
61
|
+
: '${o.size} B';
|
|
62
|
+
lines
|
|
63
|
+
..add(kv('key', cyan(o.key)))
|
|
64
|
+
..add(kv('size', size))
|
|
65
|
+
..add('');
|
|
66
|
+
}
|
|
67
|
+
box('${objects.length} objects', lines);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class StorageDeleteCommand extends FlutterPatcherCommand {
|
|
72
|
+
StorageDeleteCommand({this.config, this.backendOverride});
|
|
73
|
+
|
|
74
|
+
final FlutterPatcherConfig? config;
|
|
75
|
+
final Backend? backendOverride;
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
String get name => 'delete';
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
String get description => 'Delete storage object(s) by key or full URI.';
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
ArgParser get argParser => ArgParser()
|
|
85
|
+
..addOption('backend', abbr: 'b', help: 'Backend provider.')
|
|
86
|
+
..addMultiOption('key', help: 'Storage key to delete (repeatable).')
|
|
87
|
+
..addOption('uri', help: 'Full storage URI to delete.');
|
|
88
|
+
|
|
89
|
+
@override
|
|
90
|
+
Future<int> run() => runGuarded(() async {
|
|
91
|
+
final keys = (argResults!['key'] as List<String>?) ?? <String>[];
|
|
92
|
+
final uri = argResults!['uri'] as String?;
|
|
93
|
+
if (keys.isEmpty && (uri == null || uri.isEmpty)) {
|
|
94
|
+
throw PackException(
|
|
95
|
+
'Usage: flutter-ota storage delete --key <key> [--key <key>...] '
|
|
96
|
+
'| --uri <storageUri>',
|
|
97
|
+
64,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
|
|
101
|
+
final backend = requireBackend(cfg, override: backendOverride);
|
|
102
|
+
banner('storage · delete');
|
|
103
|
+
if (keys.isNotEmpty) {
|
|
104
|
+
await spinner(
|
|
105
|
+
() => backend.storage.deleteObjects(keys),
|
|
106
|
+
'Deleting ${keys.length} object(s)',
|
|
107
|
+
done: 'Deleted',
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
await spinner(
|
|
111
|
+
() => backend.storage.delete(uri!),
|
|
112
|
+
'Deleting $uri',
|
|
113
|
+
done: 'Deleted',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|