@_nazmiforreal/flutter-ota 0.1.14 → 0.1.16
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-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +23 -72
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -2
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +2 -2
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +112 -124
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +20 -28
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +40 -18
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +10 -11
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +30 -33
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +43 -41
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +9 -10
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +32 -35
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +3 -6
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +59 -50
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +32 -37
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +13 -16
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +12 -20
- package/dart-src/packages/cli-tools/lib/src/config.dart +10 -3
- package/dart-src/packages/cli-tools/lib/src/operations.dart +5 -0
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +225 -31
- package/dart-src/packages/cli-tools/lib/src/util.dart +7 -3
- package/dart-src/packages/cli-tools/test/cli_test.dart +6 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +7 -3
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +7 -4
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +18 -6
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +4 -2
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +4 -2
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +22 -6
- package/migrations/supabase/20250103114225_init.sql +7 -3
- package/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +7 -4
- package/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +18 -6
- package/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +4 -2
- package/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +4 -2
- package/migrations/supabase/20260520014100_hot-updater_rls.sql +22 -6
- package/package.json +1 -1
- package/scripts/vendor.sh +32 -0
- package/migrations/aws/README.md +0 -7
- package/migrations/cloudflare/bundles.sql +0 -42
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import 'dart:convert';
|
|
2
2
|
|
|
3
|
-
import 'package:args/args.dart';
|
|
4
3
|
|
|
5
4
|
import '../backend.dart';
|
|
6
5
|
import '../cli_base.dart';
|
|
@@ -10,7 +9,9 @@ import '../ui/ui.dart';
|
|
|
10
9
|
|
|
11
10
|
/// `flutter_ota_kit keys` — generate an Ed25519 keypair for bundle signing.
|
|
12
11
|
class KeysCommand extends FlutterPatcherCommand {
|
|
13
|
-
KeysCommand({this.config, this.backendOverride})
|
|
12
|
+
KeysCommand({this.config, this.backendOverride}) {
|
|
13
|
+
argParser.addFlag('save', help: 'Persist the public key into the project config.');
|
|
14
|
+
}
|
|
14
15
|
|
|
15
16
|
final FlutterPatcherConfig? config;
|
|
16
17
|
final Backend? backendOverride;
|
|
@@ -22,10 +23,6 @@ class KeysCommand extends FlutterPatcherCommand {
|
|
|
22
23
|
String get description =>
|
|
23
24
|
'Generate an Ed25519 keypair used to sign deployed bundles.';
|
|
24
25
|
|
|
25
|
-
@override
|
|
26
|
-
ArgParser get argParser => ArgParser()
|
|
27
|
-
..addFlag('save', help: 'Persist the public key into the project config.');
|
|
28
|
-
|
|
29
26
|
@override
|
|
30
27
|
Future<int> run() => runGuarded(() async {
|
|
31
28
|
banner('keys');
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import 'dart:convert';
|
|
2
2
|
import 'dart:io';
|
|
3
3
|
|
|
4
|
-
import 'package:args/args.dart';
|
|
5
4
|
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
6
5
|
import 'package:http/http.dart' as http;
|
|
7
6
|
import 'package:path/path.dart' as p;
|
|
@@ -9,41 +8,38 @@ import 'package:postgres/postgres.dart';
|
|
|
9
8
|
|
|
10
9
|
import '../ui/ui.dart';
|
|
11
10
|
|
|
12
|
-
/// `
|
|
11
|
+
/// `flutter-ota migrate` — run backend SQL migrations.
|
|
13
12
|
class MigrateCommand extends FlutterPatcherCommand {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@override
|
|
18
|
-
String get description =>
|
|
19
|
-
'Run SQL migrations against the backend database (Supabase Postgres).';
|
|
20
|
-
|
|
21
|
-
@override
|
|
22
|
-
ArgParser get argParser => ArgParser()
|
|
23
|
-
..addOption('backend', abbr: 'b', help: 'Backend provider.')
|
|
24
|
-
..addOption(
|
|
13
|
+
MigrateCommand() {
|
|
14
|
+
argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
|
|
15
|
+
argParser.addOption(
|
|
25
16
|
'database-url',
|
|
26
17
|
help: 'Postgres connection string (or DATABASE_URL env).',
|
|
27
|
-
)
|
|
28
|
-
|
|
18
|
+
);
|
|
19
|
+
argParser.addOption(
|
|
29
20
|
'management-key',
|
|
30
21
|
help:
|
|
31
22
|
'Supabase Management API key (or SUPABASE_MANAGEMENT_KEY env) — '
|
|
32
23
|
'runs migrations without a separate Postgres connection.',
|
|
33
|
-
)
|
|
34
|
-
|
|
24
|
+
);
|
|
25
|
+
argParser.addOption(
|
|
35
26
|
'migrations-dir',
|
|
36
27
|
help: 'Directory of *.sql migration files (ordered by name).',
|
|
37
|
-
)
|
|
38
|
-
|
|
28
|
+
);
|
|
29
|
+
argParser.addFlag(
|
|
39
30
|
'dry-run',
|
|
40
31
|
abbr: 'd',
|
|
41
32
|
help: 'Print migrations instead of applying them.',
|
|
42
33
|
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
String get name => 'migrate';
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
String get description =>
|
|
41
|
+
'Run SQL migrations against the backend database (Supabase Postgres).';
|
|
43
42
|
|
|
44
|
-
/// Default migrations dir lives next to the CLI entrypoint; each backend has
|
|
45
|
-
/// its own dialect's migrations (cloudflare = D1 SQLite, postgres = plain
|
|
46
|
-
/// Postgres, supabase = PostgREST, aws = no relational SQL).
|
|
47
43
|
String defaultMigrationsDir(String provider) {
|
|
48
44
|
final sub = switch (provider) {
|
|
49
45
|
'supabase' => 'supabase',
|
|
@@ -85,9 +81,6 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
85
81
|
Future<int> run() => runGuarded(() async {
|
|
86
82
|
final provider = argResults!['backend'] as String? ?? 'supabase';
|
|
87
83
|
|
|
88
|
-
// Backends with no relational SQL handled out-of-band (S3 blob store).
|
|
89
|
-
// Short-circuit before any SQL discovery so a missing or empty
|
|
90
|
-
// (README-only) migrations dir does not abort.
|
|
91
84
|
if (provider == 'aws') {
|
|
92
85
|
final dir =
|
|
93
86
|
argResults!['migrations-dir'] as String? ??
|
|
@@ -95,7 +88,7 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
95
88
|
banner('migrate');
|
|
96
89
|
box('migrate', [
|
|
97
90
|
'Migrations for "$provider" do not run through this command:',
|
|
98
|
-
'
|
|
91
|
+
' · aws → S3 blob store, no relational SQL (see $dir/README.md)',
|
|
99
92
|
'',
|
|
100
93
|
'See the README under $dir for the intended setup.',
|
|
101
94
|
]);
|
|
@@ -153,9 +146,9 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
153
146
|
}
|
|
154
147
|
throw StateError(
|
|
155
148
|
'For the supabase backend, provide either:\n'
|
|
156
|
-
'
|
|
149
|
+
' · --management-key (or SUPABASE_MANAGEMENT_KEY, or set it via '
|
|
157
150
|
'`flutter-ota init`) — a Supabase Management API key, OR\n'
|
|
158
|
-
'
|
|
151
|
+
' · --database-url (or DATABASE_URL, or set it via `init`) — a '
|
|
159
152
|
'Postgres connection string.',
|
|
160
153
|
);
|
|
161
154
|
}
|
|
@@ -176,8 +169,6 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
176
169
|
throw StateError('Unknown backend provider: "$provider".');
|
|
177
170
|
});
|
|
178
171
|
|
|
179
|
-
/// Run migrations through the Supabase Management API (no Postgres creds
|
|
180
|
-
/// needed). Derived from the Supabase project ref in the configured URL.
|
|
181
172
|
Future<void> _runViaManagementApi(String mgmtKey, List<File> files) async {
|
|
182
173
|
final cfg = resolveSupabaseConfig(
|
|
183
174
|
loadConfig() ??
|
|
@@ -189,7 +180,9 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
189
180
|
final ref = Uri.parse(cfg.supabaseUrl).host.split('.').first;
|
|
190
181
|
final endpoint = 'https://api.supabase.com/v1/projects/$ref/database/query';
|
|
191
182
|
banner('migrate · supabase (management api)');
|
|
183
|
+
final steps = Steps('migrate');
|
|
192
184
|
for (final file in files) {
|
|
185
|
+
final sw = Stopwatch()..start();
|
|
193
186
|
final sql = file.readAsStringSync();
|
|
194
187
|
final res = await http.post(
|
|
195
188
|
Uri.parse(endpoint),
|
|
@@ -199,20 +192,27 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
199
192
|
},
|
|
200
193
|
body: jsonEncode({'query': sql}),
|
|
201
194
|
);
|
|
195
|
+
sw.stop();
|
|
202
196
|
if (res.statusCode >= 400) {
|
|
203
|
-
|
|
204
|
-
'
|
|
205
|
-
'(${res.statusCode}): ${res.body}',
|
|
197
|
+
steps.fail(
|
|
198
|
+
'${p.basename(file.path)} (${res.statusCode}): ${res.body}',
|
|
206
199
|
);
|
|
200
|
+
} else {
|
|
201
|
+
final ms = sw.elapsedMilliseconds;
|
|
202
|
+
final time = ms >= 1000
|
|
203
|
+
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
204
|
+
: '${ms}ms';
|
|
205
|
+
steps.success('Applied ${p.basename(file.path)} in $time');
|
|
207
206
|
}
|
|
208
|
-
step('applied ${p.basename(file.path)}');
|
|
209
207
|
}
|
|
210
|
-
await _ensureSupabaseBucket();
|
|
208
|
+
await _ensureSupabaseBucket(steps);
|
|
209
|
+
steps.summary();
|
|
210
|
+
if (steps.hasErrors) {
|
|
211
|
+
throw StateError('One or more migrations failed.');
|
|
212
|
+
}
|
|
211
213
|
}
|
|
212
214
|
|
|
213
|
-
|
|
214
|
-
/// `deploy` can upload artifacts without a manual setup step.
|
|
215
|
-
Future<void> _ensureSupabaseBucket() async {
|
|
215
|
+
Future<void> _ensureSupabaseBucket(Steps steps) async {
|
|
216
216
|
final storage = resolveSupabaseStorageConfig(
|
|
217
217
|
loadConfig() ??
|
|
218
218
|
FlutterPatcherConfig(
|
|
@@ -221,13 +221,12 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
221
221
|
),
|
|
222
222
|
);
|
|
223
223
|
if (storage.supabaseServiceRoleKey == null) {
|
|
224
|
-
|
|
225
|
-
'Skipped storage bucket creation — set supabase.serviceRoleKey
|
|
226
|
-
|
|
227
|
-
'"${storage.bucketName}" bucket.',
|
|
228
|
-
]);
|
|
224
|
+
steps.skip(
|
|
225
|
+
'Skipped storage bucket creation — set supabase.serviceRoleKey',
|
|
226
|
+
);
|
|
229
227
|
return;
|
|
230
228
|
}
|
|
229
|
+
final sw = Stopwatch()..start();
|
|
231
230
|
final res = await http.post(
|
|
232
231
|
Uri.parse('${storage.supabaseUrl}/storage/v1/bucket'),
|
|
233
232
|
headers: {
|
|
@@ -237,20 +236,22 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
237
236
|
},
|
|
238
237
|
body: jsonEncode({'name': storage.bucketName, 'public': true}),
|
|
239
238
|
);
|
|
239
|
+
sw.stop();
|
|
240
240
|
if (res.statusCode >= 400) {
|
|
241
241
|
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
242
242
|
final msg = (body['message'] ?? body['error'] ?? '').toString();
|
|
243
243
|
if (!msg.contains('already exists') && res.statusCode != 409) {
|
|
244
|
-
|
|
245
|
-
'Warning: could not create bucket "${storage.bucketName}": $msg',
|
|
246
|
-
]);
|
|
244
|
+
steps.fail('Could not create bucket "${storage.bucketName}": $msg');
|
|
247
245
|
return;
|
|
248
246
|
}
|
|
249
247
|
}
|
|
250
|
-
|
|
248
|
+
final ms = sw.elapsedMilliseconds;
|
|
249
|
+
final time = ms >= 1000
|
|
250
|
+
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
251
|
+
: '${ms}ms';
|
|
252
|
+
steps.success('Ensured storage bucket "${storage.bucketName}" in $time');
|
|
251
253
|
}
|
|
252
254
|
|
|
253
|
-
/// Original path: connect directly to Postgres and execute each statement.
|
|
254
255
|
Future<void> _runViaPostgres(String url, List<File> files) async {
|
|
255
256
|
final uri = Uri.parse(url);
|
|
256
257
|
final endpoint = Endpoint(
|
|
@@ -263,6 +264,7 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
263
264
|
: null,
|
|
264
265
|
);
|
|
265
266
|
banner('migrate · postgres');
|
|
267
|
+
final steps = Steps('migrate');
|
|
266
268
|
final conn = await Connection.open(
|
|
267
269
|
endpoint,
|
|
268
270
|
settings: ConnectionSettings(sslMode: SslMode.disable),
|
|
@@ -281,9 +283,10 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
281
283
|
queryMode: QueryMode.simple,
|
|
282
284
|
);
|
|
283
285
|
if (existing.isNotEmpty) {
|
|
284
|
-
|
|
286
|
+
steps.skip('Skipped $name (already applied)');
|
|
285
287
|
continue;
|
|
286
288
|
}
|
|
289
|
+
final sw = Stopwatch()..start();
|
|
287
290
|
for (final stmt in _splitStatements(file.readAsStringSync())) {
|
|
288
291
|
await conn.execute(stmt, queryMode: QueryMode.simple);
|
|
289
292
|
}
|
|
@@ -291,10 +294,16 @@ class MigrateCommand extends FlutterPatcherCommand {
|
|
|
291
294
|
"INSERT INTO _flutter_ota_kit_migrations(name) VALUES ('$escaped')",
|
|
292
295
|
queryMode: QueryMode.simple,
|
|
293
296
|
);
|
|
294
|
-
|
|
297
|
+
sw.stop();
|
|
298
|
+
final ms = sw.elapsedMilliseconds;
|
|
299
|
+
final time = ms >= 1000
|
|
300
|
+
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
301
|
+
: '${ms}ms';
|
|
302
|
+
steps.success('Applied $name in $time');
|
|
295
303
|
}
|
|
296
304
|
} finally {
|
|
297
305
|
await conn.close();
|
|
298
306
|
}
|
|
307
|
+
steps.summary();
|
|
299
308
|
}
|
|
300
309
|
}
|
|
@@ -4,7 +4,6 @@ library;
|
|
|
4
4
|
import 'dart:async';
|
|
5
5
|
import 'dart:io';
|
|
6
6
|
|
|
7
|
-
import 'package:args/args.dart';
|
|
8
7
|
import 'package:path/path.dart' as p;
|
|
9
8
|
|
|
10
9
|
import '../cli_base.dart';
|
|
@@ -28,42 +27,34 @@ class PocketBaseCommand extends FlutterPatcherCommand {
|
|
|
28
27
|
@override
|
|
29
28
|
String get description =>
|
|
30
29
|
'Manage a local PocketBase instance (install / serve / stop / status).';
|
|
31
|
-
|
|
32
|
-
@override
|
|
33
|
-
ArgParser get argParser => ArgParser();
|
|
34
|
-
|
|
35
|
-
@override
|
|
36
|
-
Future<int> run() => runGuarded(() async {
|
|
37
|
-
err('Missing subcommand. Use: install | serve | stop | status');
|
|
38
|
-
return;
|
|
39
|
-
});
|
|
40
30
|
}
|
|
41
31
|
|
|
42
32
|
class PocketBaseInstallCommand extends FlutterPatcherCommand {
|
|
33
|
+
PocketBaseInstallCommand() {
|
|
34
|
+
argParser.addOption('version', help: 'PocketBase version to install.');
|
|
35
|
+
}
|
|
36
|
+
|
|
43
37
|
@override
|
|
44
38
|
String get name => 'install';
|
|
45
39
|
|
|
46
40
|
@override
|
|
47
41
|
String get description => 'Download + extract the PocketBase binary.';
|
|
48
42
|
|
|
49
|
-
@override
|
|
50
|
-
ArgParser get argParser =>
|
|
51
|
-
ArgParser()..addOption('version', help: 'PocketBase version to install.');
|
|
52
|
-
|
|
53
43
|
@override
|
|
54
44
|
Future<int> run() => runGuarded(() async {
|
|
55
45
|
final version =
|
|
56
46
|
argResults!['version'] as String? ?? kDefaultPocketBaseVersion;
|
|
57
47
|
final installer = PocketBaseInstaller(version: version);
|
|
58
48
|
final paths = installer.paths();
|
|
59
|
-
|
|
49
|
+
banner('pocketbase · install');
|
|
50
|
+
final bar = ProgressBar(1, 'Downloading PocketBase v$version');
|
|
60
51
|
final result = await installer.ensureInstalled(
|
|
61
52
|
paths: paths,
|
|
62
53
|
onProgress: (p) {
|
|
63
|
-
|
|
54
|
+
bar.update((p * 100).round());
|
|
64
55
|
},
|
|
65
56
|
);
|
|
66
|
-
|
|
57
|
+
bar.close();
|
|
67
58
|
if (result.alreadyInstalled) {
|
|
68
59
|
step('Already installed at ${paths.binaryPath.path}');
|
|
69
60
|
} else {
|
|
@@ -77,26 +68,26 @@ class PocketBaseInstallCommand extends FlutterPatcherCommand {
|
|
|
77
68
|
}
|
|
78
69
|
|
|
79
70
|
class PocketBaseServeCommand extends FlutterPatcherCommand {
|
|
71
|
+
PocketBaseServeCommand() {
|
|
72
|
+
argParser.addOption('version', help: 'PocketBase version.');
|
|
73
|
+
argParser.addOption('port', help: 'PB HTTP port.', defaultsTo: '8090');
|
|
74
|
+
argParser.addOption('host', help: 'PB bind address.', defaultsTo: '127.0.0.1');
|
|
75
|
+
argParser.addOption('data-dir', help: 'PB data directory.');
|
|
76
|
+
argParser.addOption('admin-email', help: 'Bootstrap admin email.');
|
|
77
|
+
argParser.addOption('admin-password', help: 'Bootstrap admin password.');
|
|
78
|
+
argParser.addFlag(
|
|
79
|
+
'install-hooks',
|
|
80
|
+
help: 'Copy bundled JS hooks into the data dir on start.',
|
|
81
|
+
defaultsTo: true,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
80
85
|
@override
|
|
81
86
|
String get name => 'serve';
|
|
82
87
|
|
|
83
88
|
@override
|
|
84
89
|
String get description =>
|
|
85
|
-
'Start a local PocketBase + install the
|
|
86
|
-
|
|
87
|
-
@override
|
|
88
|
-
ArgParser get argParser => ArgParser()
|
|
89
|
-
..addOption('version', help: 'PocketBase version.')
|
|
90
|
-
..addOption('port', help: 'PB HTTP port.', defaultsTo: '8090')
|
|
91
|
-
..addOption('host', help: 'PB bind address.', defaultsTo: '127.0.0.1')
|
|
92
|
-
..addOption('data-dir', help: 'PB data directory.')
|
|
93
|
-
..addOption('admin-email', help: 'Bootstrap admin email.')
|
|
94
|
-
..addOption('admin-password', help: 'Bootstrap admin password.')
|
|
95
|
-
..addFlag(
|
|
96
|
-
'install-hooks',
|
|
97
|
-
help: 'Copy bundled JS hooks into the data dir on start.',
|
|
98
|
-
defaultsTo: true,
|
|
99
|
-
);
|
|
90
|
+
'Start a local PocketBase + install the flutter-ota schema.';
|
|
100
91
|
|
|
101
92
|
@override
|
|
102
93
|
Future<int> run() => runGuarded(() async {
|
|
@@ -146,7 +137,7 @@ class PocketBaseServeCommand extends FlutterPatcherCommand {
|
|
|
146
137
|
step('PocketBase is running (pid ${manager.process!.pid}).');
|
|
147
138
|
|
|
148
139
|
if (adminEmail != null && adminPassword != null) {
|
|
149
|
-
step('Installing
|
|
140
|
+
step('Installing flutter-ota schema...');
|
|
150
141
|
final schemaInstaller = PocketBaseSchemaInstaller(
|
|
151
142
|
url: 'http://$host:$port',
|
|
152
143
|
adminEmail: adminEmail,
|
|
@@ -168,7 +159,7 @@ class PocketBaseServeCommand extends FlutterPatcherCommand {
|
|
|
168
159
|
}
|
|
169
160
|
|
|
170
161
|
banner('pocketbase');
|
|
171
|
-
box('
|
|
162
|
+
box('flutter-ota pocketbase serve', [
|
|
172
163
|
'Local PocketBase is running:',
|
|
173
164
|
'',
|
|
174
165
|
kv('url', 'http://$host:$port'),
|
|
@@ -179,9 +170,13 @@ class PocketBaseServeCommand extends FlutterPatcherCommand {
|
|
|
179
170
|
|
|
180
171
|
// Wait for SIGINT/SIGTERM.
|
|
181
172
|
final done = Completer<void>();
|
|
182
|
-
ProcessSignal.sigint.watch().listen((_)
|
|
173
|
+
ProcessSignal.sigint.watch().listen((_) {
|
|
174
|
+
if (!done.isCompleted) done.complete();
|
|
175
|
+
});
|
|
183
176
|
if (!Platform.isWindows) {
|
|
184
|
-
ProcessSignal.sigterm.watch().listen((_)
|
|
177
|
+
ProcessSignal.sigterm.watch().listen((_) {
|
|
178
|
+
if (!done.isCompleted) done.complete();
|
|
179
|
+
});
|
|
185
180
|
}
|
|
186
181
|
await done.future;
|
|
187
182
|
step('Shutting down PocketBase...');
|
|
@@ -229,7 +224,7 @@ class PocketBaseStatusCommand extends FlutterPatcherCommand {
|
|
|
229
224
|
);
|
|
230
225
|
} else {
|
|
231
226
|
step(
|
|
232
|
-
'PocketBase: not installed. Run `
|
|
227
|
+
'PocketBase: not installed. Run `flutter-ota pocketbase install`.',
|
|
233
228
|
);
|
|
234
229
|
}
|
|
235
230
|
return;
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import 'package:args/args.dart';
|
|
2
1
|
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
3
2
|
import 'package:flutter_ota_kit_core/flutter_ota_kit_core.dart';
|
|
4
3
|
|
|
5
4
|
import '../ui/ui.dart';
|
|
6
5
|
|
|
7
|
-
/// `
|
|
6
|
+
/// `flutter-ota rollback` — roll a channel back to a previous bundle.
|
|
8
7
|
class RollbackCommand extends FlutterPatcherCommand {
|
|
9
|
-
RollbackCommand({this.config, this.backendOverride})
|
|
8
|
+
RollbackCommand({this.config, this.backendOverride}) {
|
|
9
|
+
argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
|
|
10
|
+
argParser.addOption('channel', abbr: 'c', help: 'Channel to roll back.');
|
|
11
|
+
argParser.addOption(
|
|
12
|
+
'bundle-id',
|
|
13
|
+
abbr: 'i',
|
|
14
|
+
help: 'Roll back to this specific bundle id.',
|
|
15
|
+
);
|
|
16
|
+
argParser.addOption('platform', abbr: 'p', help: 'Platform filter (e.g. android).');
|
|
17
|
+
}
|
|
10
18
|
|
|
11
19
|
final FlutterPatcherConfig? config;
|
|
12
20
|
final Backend? backendOverride;
|
|
@@ -18,23 +26,12 @@ class RollbackCommand extends FlutterPatcherCommand {
|
|
|
18
26
|
String get description =>
|
|
19
27
|
'Roll a channel back: disable the latest enabled bundle (or a specific --bundle-id).';
|
|
20
28
|
|
|
21
|
-
@override
|
|
22
|
-
ArgParser get argParser => ArgParser()
|
|
23
|
-
..addOption('backend', abbr: 'b', help: 'Backend provider.')
|
|
24
|
-
..addOption('channel', abbr: 'c', help: 'Channel to roll back.')
|
|
25
|
-
..addOption(
|
|
26
|
-
'bundle-id',
|
|
27
|
-
abbr: 'i',
|
|
28
|
-
help: 'Roll back to this specific bundle id.',
|
|
29
|
-
)
|
|
30
|
-
..addOption('platform', abbr: 'p', help: 'Platform filter (e.g. android).');
|
|
31
|
-
|
|
32
29
|
@override
|
|
33
30
|
Future<int> run() => runGuarded(() async {
|
|
34
31
|
final channel = argResults!['channel'] as String?;
|
|
35
32
|
if (channel == null || channel.isEmpty) {
|
|
36
33
|
throw PackException(
|
|
37
|
-
'Usage:
|
|
34
|
+
'Usage: flutter-ota rollback --channel <channel> [--bundle-id <id>]',
|
|
38
35
|
64,
|
|
39
36
|
);
|
|
40
37
|
}
|
|
@@ -67,7 +64,7 @@ class RollbackCommand extends FlutterPatcherCommand {
|
|
|
67
64
|
);
|
|
68
65
|
box('rollback', [
|
|
69
66
|
kv('channel', channel),
|
|
70
|
-
kv('now live', cyan(live)),
|
|
67
|
+
kv('now live', live.isEmpty ? dim('(none)') : cyan(live)),
|
|
71
68
|
kv('disabled', disabled.isEmpty ? dim('(none)') : disabled.join(', ')),
|
|
72
69
|
]);
|
|
73
70
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import 'package:args/args.dart';
|
|
2
1
|
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
3
2
|
|
|
4
3
|
import '../ui/ui.dart';
|
|
@@ -27,7 +26,10 @@ class StorageCommand extends FlutterPatcherCommand {
|
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
class StorageListCommand extends FlutterPatcherCommand {
|
|
30
|
-
StorageListCommand({this.config, this.backendOverride})
|
|
29
|
+
StorageListCommand({this.config, this.backendOverride}) {
|
|
30
|
+
argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
|
|
31
|
+
argParser.addOption('prefix', help: 'Key prefix filter (e.g. bundles).');
|
|
32
|
+
}
|
|
31
33
|
|
|
32
34
|
final FlutterPatcherConfig? config;
|
|
33
35
|
final Backend? backendOverride;
|
|
@@ -38,11 +40,6 @@ class StorageListCommand extends FlutterPatcherCommand {
|
|
|
38
40
|
@override
|
|
39
41
|
String get description => 'List storage objects.';
|
|
40
42
|
|
|
41
|
-
@override
|
|
42
|
-
ArgParser get argParser => ArgParser()
|
|
43
|
-
..addOption('backend', abbr: 'b', help: 'Backend provider.')
|
|
44
|
-
..addOption('prefix', help: 'Key prefix filter (e.g. bundles).');
|
|
45
|
-
|
|
46
43
|
@override
|
|
47
44
|
Future<int> run() => runGuarded(() async {
|
|
48
45
|
final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
|
|
@@ -56,24 +53,25 @@ class StorageListCommand extends FlutterPatcherCommand {
|
|
|
56
53
|
step('(no objects)');
|
|
57
54
|
return;
|
|
58
55
|
}
|
|
59
|
-
final
|
|
56
|
+
final rows = <List<String>>[];
|
|
60
57
|
for (final o in objects) {
|
|
61
58
|
final size = o.size >= 1024 * 1024
|
|
62
59
|
? '${(o.size / (1024 * 1024)).toStringAsFixed(2)} MB'
|
|
63
60
|
: o.size >= 1024
|
|
64
61
|
? '${(o.size / 1024).toStringAsFixed(1)} KB'
|
|
65
62
|
: '${o.size} B';
|
|
66
|
-
|
|
67
|
-
..add(kv('key', cyan(o.key)))
|
|
68
|
-
..add(kv('size', size))
|
|
69
|
-
..add('');
|
|
63
|
+
rows.add([cyan(o.key), size]);
|
|
70
64
|
}
|
|
71
|
-
|
|
65
|
+
table('${objects.length} objects', ['KEY', 'SIZE'], rows);
|
|
72
66
|
});
|
|
73
67
|
}
|
|
74
68
|
|
|
75
69
|
class StorageDeleteCommand extends FlutterPatcherCommand {
|
|
76
|
-
StorageDeleteCommand({this.config, this.backendOverride})
|
|
70
|
+
StorageDeleteCommand({this.config, this.backendOverride}) {
|
|
71
|
+
argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
|
|
72
|
+
argParser.addMultiOption('key', help: 'Storage key to delete (repeatable).');
|
|
73
|
+
argParser.addOption('uri', help: 'Full storage URI to delete.');
|
|
74
|
+
}
|
|
77
75
|
|
|
78
76
|
final FlutterPatcherConfig? config;
|
|
79
77
|
final Backend? backendOverride;
|
|
@@ -84,12 +82,6 @@ class StorageDeleteCommand extends FlutterPatcherCommand {
|
|
|
84
82
|
@override
|
|
85
83
|
String get description => 'Delete storage object(s) by key or full URI.';
|
|
86
84
|
|
|
87
|
-
@override
|
|
88
|
-
ArgParser get argParser => ArgParser()
|
|
89
|
-
..addOption('backend', abbr: 'b', help: 'Backend provider.')
|
|
90
|
-
..addMultiOption('key', help: 'Storage key to delete (repeatable).')
|
|
91
|
-
..addOption('uri', help: 'Full storage URI to delete.');
|
|
92
|
-
|
|
93
85
|
@override
|
|
94
86
|
Future<int> run() => runGuarded(() async {
|
|
95
87
|
final keys = (argResults!['key'] as List<String>?) ?? <String>[];
|
|
@@ -165,9 +165,16 @@ FlutterPatcherConfig? loadConfig() {
|
|
|
165
165
|
if (file.existsSync()) {
|
|
166
166
|
final raw = file.readAsStringSync();
|
|
167
167
|
if (raw.trim().isEmpty) continue;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
try {
|
|
169
|
+
return FlutterPatcherConfig.fromJson(
|
|
170
|
+
jsonDecode(raw) as Map<String, dynamic>,
|
|
171
|
+
);
|
|
172
|
+
} on FormatException {
|
|
173
|
+
stderr.writeln(
|
|
174
|
+
'Warning: invalid config at ${file.path} — ignoring.',
|
|
175
|
+
);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
171
178
|
}
|
|
172
179
|
}
|
|
173
180
|
return null;
|
|
@@ -60,6 +60,11 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
|
|
|
60
60
|
'Use only one of target-app-version / fingerprint-hash (DB CHECK).',
|
|
61
61
|
);
|
|
62
62
|
}
|
|
63
|
+
if (opts.targetAppVersion == null && opts.fingerprintHash == null) {
|
|
64
|
+
throw StateError(
|
|
65
|
+
'Provide either --target-app-version or --fingerprint-hash.',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
63
68
|
// Resolve the artifact to upload. If the source is already a zip (e.g. the
|
|
64
69
|
// `patch.zip` produced by `flutter_ota_kit build`/`pack`), upload it directly.
|
|
65
70
|
// Otherwise, if the source directory contains a `patch.zip`, use that. As a
|