@_nazmiforreal/flutter-ota 0.1.20 → 0.1.22
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/dart-src/packages/cli-tools/.dart_tool/package_config.json +1 -1
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +340 -340
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +9 -5
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +106 -31
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +34 -7
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +12 -4
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +41 -9
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +124 -13
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +58 -28
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +11 -5
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +283 -32
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +347 -121
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +18 -9
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +32 -15
- package/dart-src/packages/cli-tools/lib/src/config.dart +16 -22
- package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -3
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +26 -17
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +0 -1
- package/dart-src/packages/cli-tools/lib/src/runner.dart +3 -1
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +10 -16
- package/dart-src/packages/cli-tools/pubspec.lock +7 -7
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +7 -3
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +13 -16
- package/dart-src/packages/core/CHANGELOG.md +15 -0
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +71 -1
- package/dart-src/packages/core/lib/src/bundle.dart +59 -17
- package/dart-src/packages/core/lib/src/bundle_artifacts.dart +7 -3
- package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +16 -0
- package/dart-src/packages/core/lib/src/changed_asset.dart +33 -3
- package/dart-src/packages/core/lib/src/get_bundles_args.dart +24 -2
- package/dart-src/packages/core/lib/src/platform.dart +8 -0
- package/dart-src/packages/core/lib/src/semver.dart +0 -3
- package/dart-src/packages/core/lib/src/status.dart +20 -1
- package/dart-src/packages/core/lib/src/strategy.dart +10 -0
- package/dart-src/packages/core/lib/src/update_bundle_params.dart +15 -0
- package/dart-src/packages/core/lib/src/uuid.dart +11 -0
- package/dart-src/packages/core/pubspec.yaml +1 -1
- package/dart-src/plugins/aws/CHANGELOG.md +18 -0
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +0 -1
- package/dart-src/plugins/aws/pubspec.yaml +3 -3
- package/dart-src/plugins/cloudflare/CHANGELOG.md +18 -0
- package/dart-src/plugins/cloudflare/pubspec.yaml +3 -3
- package/dart-src/plugins/plugin-core/CHANGELOG.md +15 -0
- package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +3 -0
- package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +17 -0
- package/dart-src/plugins/plugin-core/lib/src/types.dart +94 -3
- package/dart-src/plugins/plugin-core/pubspec.yaml +2 -2
- package/dart-src/plugins/pocketbase/CHANGELOG.md +18 -0
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +3 -0
- package/dart-src/plugins/pocketbase/pubspec.yaml +3 -3
- package/dart-src/plugins/postgres/CHANGELOG.md +18 -0
- package/dart-src/plugins/postgres/pubspec.yaml +3 -3
- package/dart-src/plugins/supabase/CHANGELOG.md +18 -0
- package/dart-src/plugins/supabase/pubspec.yaml +3 -3
- package/package.json +2 -2
- package/scripts/postinstall.js +5 -1
- package/bin/flutter-ota.js +0 -36
|
@@ -6,14 +6,25 @@ import '../ui/ui.dart';
|
|
|
6
6
|
/// `flutter-ota rollback` — roll a channel back to a previous bundle.
|
|
7
7
|
class RollbackCommand extends FlutterPatcherCommand {
|
|
8
8
|
RollbackCommand({this.config, this.backendOverride}) {
|
|
9
|
-
|
|
9
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
10
|
+
argParser.addOption(
|
|
11
|
+
'backend',
|
|
12
|
+
abbr: 'b',
|
|
13
|
+
help: detected != null
|
|
14
|
+
? 'Backend provider [detected: $detected].'
|
|
15
|
+
: 'Backend provider.',
|
|
16
|
+
);
|
|
10
17
|
argParser.addOption('channel', abbr: 'c', help: 'Channel to roll back.');
|
|
11
18
|
argParser.addOption(
|
|
12
19
|
'bundle-id',
|
|
13
20
|
abbr: 'i',
|
|
14
21
|
help: 'Roll back to this specific bundle id.',
|
|
15
22
|
);
|
|
16
|
-
argParser.addOption(
|
|
23
|
+
argParser.addOption(
|
|
24
|
+
'platform',
|
|
25
|
+
abbr: 'p',
|
|
26
|
+
help: 'Platform filter (e.g. android).',
|
|
27
|
+
);
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
final FlutterPatcherConfig? config;
|
|
@@ -46,14 +57,12 @@ class RollbackCommand extends FlutterPatcherCommand {
|
|
|
46
57
|
|
|
47
58
|
banner('rollback');
|
|
48
59
|
final steps = Steps('rollback');
|
|
49
|
-
final (
|
|
60
|
+
final (
|
|
61
|
+
disabled,
|
|
62
|
+
live,
|
|
63
|
+
) = await steps.run('Rolling back channel "$channel"', () async {
|
|
50
64
|
if (bundleId != null && bundleId.isNotEmpty) {
|
|
51
|
-
return rollbackToBundle(
|
|
52
|
-
backend,
|
|
53
|
-
channel,
|
|
54
|
-
bundleId,
|
|
55
|
-
platform: platform,
|
|
56
|
-
);
|
|
65
|
+
return rollbackToBundle(backend, channel, bundleId, platform: platform);
|
|
57
66
|
}
|
|
58
67
|
final id = await rollbackChannel(backend, channel);
|
|
59
68
|
final nowLive = (await getChannel(backend, channel))?.id ?? '';
|
|
@@ -22,12 +22,19 @@ class StorageCommand extends FlutterPatcherCommand {
|
|
|
22
22
|
|
|
23
23
|
@override
|
|
24
24
|
String get description =>
|
|
25
|
-
'Inspect and manage bundle storage objects (
|
|
25
|
+
'Inspect and manage bundle storage objects (list / delete).';
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
class StorageListCommand extends FlutterPatcherCommand {
|
|
29
29
|
StorageListCommand({this.config, this.backendOverride}) {
|
|
30
|
-
|
|
30
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
31
|
+
argParser.addOption(
|
|
32
|
+
'backend',
|
|
33
|
+
abbr: 'b',
|
|
34
|
+
help: detected != null
|
|
35
|
+
? 'Backend provider [detected: $detected].'
|
|
36
|
+
: 'Backend provider.',
|
|
37
|
+
);
|
|
31
38
|
argParser.addOption('prefix', help: 'Key prefix filter (e.g. bundles).');
|
|
32
39
|
}
|
|
33
40
|
|
|
@@ -47,9 +54,12 @@ class StorageListCommand extends FlutterPatcherCommand {
|
|
|
47
54
|
final prefix = argResults!['prefix'] as String?;
|
|
48
55
|
banner('storage · list');
|
|
49
56
|
final steps = Steps('list');
|
|
50
|
-
final objects = await steps.run(
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
final objects = await steps.run(
|
|
58
|
+
'Listing storage objects',
|
|
59
|
+
() => backend.storage.listObjects(
|
|
60
|
+
prefix == null || prefix.isEmpty ? null : prefix,
|
|
61
|
+
),
|
|
62
|
+
);
|
|
53
63
|
if (objects.isEmpty) {
|
|
54
64
|
steps.skip('(no objects)');
|
|
55
65
|
steps.summary();
|
|
@@ -57,12 +67,7 @@ class StorageListCommand extends FlutterPatcherCommand {
|
|
|
57
67
|
}
|
|
58
68
|
final rows = <List<String>>[];
|
|
59
69
|
for (final o in objects) {
|
|
60
|
-
|
|
61
|
-
? '${(o.size / (1024 * 1024)).toStringAsFixed(2)} MB'
|
|
62
|
-
: o.size >= 1024
|
|
63
|
-
? '${(o.size / 1024).toStringAsFixed(1)} KB'
|
|
64
|
-
: '${o.size} B';
|
|
65
|
-
rows.add([cyan(o.key), size]);
|
|
70
|
+
rows.add([cyan(o.key), humanSize(o.size)]);
|
|
66
71
|
}
|
|
67
72
|
table('${objects.length} objects', ['KEY', 'SIZE'], rows);
|
|
68
73
|
steps.summary();
|
|
@@ -71,8 +76,18 @@ class StorageListCommand extends FlutterPatcherCommand {
|
|
|
71
76
|
|
|
72
77
|
class StorageDeleteCommand extends FlutterPatcherCommand {
|
|
73
78
|
StorageDeleteCommand({this.config, this.backendOverride}) {
|
|
74
|
-
|
|
75
|
-
argParser.
|
|
79
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
80
|
+
argParser.addOption(
|
|
81
|
+
'backend',
|
|
82
|
+
abbr: 'b',
|
|
83
|
+
help: detected != null
|
|
84
|
+
? 'Backend provider [detected: $detected].'
|
|
85
|
+
: 'Backend provider.',
|
|
86
|
+
);
|
|
87
|
+
argParser.addMultiOption(
|
|
88
|
+
'key',
|
|
89
|
+
help: 'Storage key to delete (repeatable).',
|
|
90
|
+
);
|
|
76
91
|
argParser.addOption('uri', help: 'Full storage URI to delete.');
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -101,8 +116,10 @@ class StorageDeleteCommand extends FlutterPatcherCommand {
|
|
|
101
116
|
banner('storage · delete');
|
|
102
117
|
final steps = Steps('delete');
|
|
103
118
|
if (keys.isNotEmpty) {
|
|
104
|
-
await steps.run(
|
|
105
|
-
|
|
119
|
+
await steps.run(
|
|
120
|
+
'Deleting ${keys.length} object(s)',
|
|
121
|
+
() => backend.storage.deleteObjects(keys),
|
|
122
|
+
);
|
|
106
123
|
} else {
|
|
107
124
|
await steps.run('Deleting $uri', () => backend.storage.delete(uri!));
|
|
108
125
|
}
|
|
@@ -170,9 +170,7 @@ FlutterPatcherConfig? loadConfig() {
|
|
|
170
170
|
jsonDecode(raw) as Map<String, dynamic>,
|
|
171
171
|
);
|
|
172
172
|
} on FormatException {
|
|
173
|
-
stderr.writeln(
|
|
174
|
-
'Warning: invalid config at ${file.path} — ignoring.',
|
|
175
|
-
);
|
|
173
|
+
stderr.writeln('Warning: invalid config at ${file.path} — ignoring.');
|
|
176
174
|
return null;
|
|
177
175
|
}
|
|
178
176
|
}
|
|
@@ -232,8 +230,6 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
|
|
|
232
230
|
String? url,
|
|
233
231
|
String? serviceRoleKey,
|
|
234
232
|
String? anonKey,
|
|
235
|
-
String? bucket,
|
|
236
|
-
String? basePath,
|
|
237
233
|
SupabaseClientFactory? clientFactory,
|
|
238
234
|
}) {
|
|
239
235
|
final env = Platform.environment;
|
|
@@ -247,8 +243,8 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
|
|
|
247
243
|
|
|
248
244
|
if (resolvedUrl == null || resolvedUrl.isEmpty) {
|
|
249
245
|
throw StateError(
|
|
250
|
-
'Supabase URL is required. Set it via
|
|
251
|
-
'`
|
|
246
|
+
'Supabase URL is required. Set it via SUPABASE_URL env var, '
|
|
247
|
+
'or `flutter-ota config set supabase.url <url>`.',
|
|
252
248
|
);
|
|
253
249
|
}
|
|
254
250
|
|
|
@@ -275,8 +271,6 @@ SupabaseStorageConfig resolveSupabaseStorageConfig(
|
|
|
275
271
|
url: url,
|
|
276
272
|
serviceRoleKey: serviceRoleKey,
|
|
277
273
|
anonKey: anonKey,
|
|
278
|
-
bucket: bucket,
|
|
279
|
-
basePath: basePath,
|
|
280
274
|
clientFactory: clientFactory,
|
|
281
275
|
);
|
|
282
276
|
return SupabaseStorageConfig(
|
|
@@ -284,12 +278,11 @@ SupabaseStorageConfig resolveSupabaseStorageConfig(
|
|
|
284
278
|
supabaseServiceRoleKey: db.supabaseServiceRoleKey,
|
|
285
279
|
supabaseAnonKey: db.supabaseAnonKey,
|
|
286
280
|
clientFactory: db.clientFactory,
|
|
287
|
-
bucketName:
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
: (bucket ?? config.supabase.bucket ?? 'bundles'),
|
|
281
|
+
bucketName:
|
|
282
|
+
bucket ??
|
|
283
|
+
Platform.environment['SUPABASE_BUCKET'] ??
|
|
284
|
+
config.supabase.bucket ??
|
|
285
|
+
'bundles',
|
|
293
286
|
basePath:
|
|
294
287
|
basePath ??
|
|
295
288
|
Platform.environment['SUPABASE_BASE_PATH'] ??
|
|
@@ -387,8 +380,8 @@ PostgresConfig resolvePostgresDatabaseConfig(
|
|
|
387
380
|
|
|
388
381
|
if (resolvedHost == null || resolvedHost.isEmpty) {
|
|
389
382
|
throw StateError(
|
|
390
|
-
'Postgres host is required. Set
|
|
391
|
-
'`
|
|
383
|
+
'Postgres host is required. Set POSTGRES_HOST env var, '
|
|
384
|
+
'or `flutter-ota config set postgres.host <host>`.',
|
|
392
385
|
);
|
|
393
386
|
}
|
|
394
387
|
|
|
@@ -765,15 +758,16 @@ PocketBaseDatabaseConfig resolvePocketBaseDatabaseConfig(
|
|
|
765
758
|
|
|
766
759
|
if (resolvedUrl == null || resolvedUrl.isEmpty) {
|
|
767
760
|
throw StateError(
|
|
768
|
-
'PocketBase URL is required. Set
|
|
769
|
-
'`
|
|
761
|
+
'PocketBase URL is required. Set POCKETBASE_URL env var, '
|
|
762
|
+
'or `flutter-ota config set pocketbase.url <url>`.',
|
|
770
763
|
);
|
|
771
764
|
}
|
|
772
765
|
if (resolvedEmail == null || resolvedPassword == null) {
|
|
773
766
|
throw StateError(
|
|
774
|
-
'PocketBase admin credentials are required. Set
|
|
775
|
-
'
|
|
776
|
-
'`
|
|
767
|
+
'PocketBase admin credentials are required. Set '
|
|
768
|
+
'POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD env vars, or '
|
|
769
|
+
'`flutter-ota config set pocketbase.adminEmail/<email> '
|
|
770
|
+
'pocketbase.adminPassword/<password>`.',
|
|
777
771
|
);
|
|
778
772
|
}
|
|
779
773
|
|
|
@@ -61,9 +61,7 @@ class ListOptions {
|
|
|
61
61
|
/// Build the zip, upload it, and register a [Bundle] via the backend.
|
|
62
62
|
Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
|
|
63
63
|
if (opts.targetAppVersion != null && opts.fingerprintHash != null) {
|
|
64
|
-
throw StateError(
|
|
65
|
-
'Use only one of target-app-version / fingerprint-hash (DB CHECK).',
|
|
66
|
-
);
|
|
64
|
+
throw StateError('Use only one of target-app-version / fingerprint-hash.');
|
|
67
65
|
}
|
|
68
66
|
if (opts.targetAppVersion == null && opts.fingerprintHash == null) {
|
|
69
67
|
throw StateError(
|
|
@@ -16,13 +16,17 @@ import 'dart:io';
|
|
|
16
16
|
import 'package:path/path.dart' as p;
|
|
17
17
|
|
|
18
18
|
class PocketBaseProcess {
|
|
19
|
-
PocketBaseProcess._(this.process, this.dataDir, this.binaryPath)
|
|
19
|
+
PocketBaseProcess._(this.process, this.dataDir, this.binaryPath)
|
|
20
|
+
: _exitCodeCompleted = false {
|
|
21
|
+
process.exitCode.then((_) => _exitCodeCompleted = true);
|
|
22
|
+
}
|
|
20
23
|
|
|
21
24
|
final Process process;
|
|
22
25
|
final Directory dataDir;
|
|
23
26
|
final File binaryPath;
|
|
27
|
+
bool _exitCodeCompleted;
|
|
24
28
|
|
|
25
|
-
bool get isRunning =>
|
|
29
|
+
bool get isRunning => !_exitCodeCompleted;
|
|
26
30
|
|
|
27
31
|
int get pid => process.pid;
|
|
28
32
|
|
|
@@ -88,12 +92,14 @@ class PocketBaseProcessManager {
|
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
void _writeConfigFile(int pid) {
|
|
91
|
-
configFile.writeAsStringSync(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
configFile.writeAsStringSync(
|
|
96
|
+
jsonEncode({
|
|
97
|
+
'pid': pid,
|
|
98
|
+
'host': host,
|
|
99
|
+
'port': port,
|
|
100
|
+
'url': 'http://$host:$port',
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
Map<String, dynamic>? readConfig() {
|
|
@@ -131,19 +137,18 @@ class PocketBaseProcessManager {
|
|
|
131
137
|
final hooksDir = Directory(p.join(dataDir.path, 'pb_hooks'));
|
|
132
138
|
await hooksDir.create(recursive: true);
|
|
133
139
|
|
|
134
|
-
final mergedEnv = <String, String>{
|
|
135
|
-
...Platform.environment,
|
|
136
|
-
...?env,
|
|
137
|
-
};
|
|
140
|
+
final mergedEnv = <String, String>{...Platform.environment, ...?env};
|
|
138
141
|
|
|
139
142
|
// Create superuser if credentials provided (PB 0.40+ doesn't support
|
|
140
143
|
// PB_ADMIN_EMAIL env var, so we use the CLI directly).
|
|
141
144
|
if (adminEmail != null && adminPassword != null) {
|
|
142
|
-
final result = await Process.run(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
final result = await Process.run(binaryPath.path, [
|
|
146
|
+
'superuser',
|
|
147
|
+
'upsert',
|
|
148
|
+
'--dir=${dataDir.path}',
|
|
149
|
+
adminEmail,
|
|
150
|
+
adminPassword,
|
|
151
|
+
], environment: mergedEnv);
|
|
147
152
|
if (result.exitCode != 0) {
|
|
148
153
|
// Non-fatal; serve will continue but schema install may fail.
|
|
149
154
|
}
|
|
@@ -214,5 +219,9 @@ class PocketBaseProcessManager {
|
|
|
214
219
|
.transform(utf8.decoder)
|
|
215
220
|
.transform(const LineSplitter())
|
|
216
221
|
.map((line) => '[stdout] $line');
|
|
222
|
+
yield* proc.errors
|
|
223
|
+
.transform(utf8.decoder)
|
|
224
|
+
.transform(const LineSplitter())
|
|
225
|
+
.map((line) => '[stderr] $line');
|
|
217
226
|
}
|
|
218
227
|
}
|
|
@@ -221,7 +221,6 @@ class PocketBaseSchemaInstaller {
|
|
|
221
221
|
required this.url,
|
|
222
222
|
required this.adminEmail,
|
|
223
223
|
required this.adminPassword,
|
|
224
|
-
http.Client? httpClient,
|
|
225
224
|
this.bundlesCollection = 'bundles',
|
|
226
225
|
this.channelsCollection = 'channels',
|
|
227
226
|
this.auditLogCollection = 'audit_log',
|
|
@@ -24,7 +24,9 @@ class FlutterPatcherRunner extends CommandRunner<int> {
|
|
|
24
24
|
// Intercept missing subcommand for parent commands so we show our
|
|
25
25
|
// colored usage text instead of the args package's plain text.
|
|
26
26
|
if (results.command == null && results.rest.isEmpty) {
|
|
27
|
-
final cmdName = results.arguments.isNotEmpty
|
|
27
|
+
final cmdName = results.arguments.isNotEmpty
|
|
28
|
+
? results.arguments.first
|
|
29
|
+
: null;
|
|
28
30
|
if (cmdName != null) {
|
|
29
31
|
final cmd = commands[cmdName];
|
|
30
32
|
if (cmd != null && cmd.subcommands.isNotEmpty) {
|
|
@@ -127,7 +127,8 @@ Future<T> spinner<T>(
|
|
|
127
127
|
final r = await task();
|
|
128
128
|
// Keep spinner visible for at least 400ms so fast ops are readable.
|
|
129
129
|
final elapsed = sw.elapsedMilliseconds;
|
|
130
|
-
if (elapsed < 400)
|
|
130
|
+
if (elapsed < 400)
|
|
131
|
+
await Future.delayed(Duration(milliseconds: 400 - elapsed));
|
|
131
132
|
timer.cancel();
|
|
132
133
|
sw.stop();
|
|
133
134
|
stdout.write('\r\x1b[K ${green('✓')} ${done ?? label}\n');
|
|
@@ -292,13 +293,11 @@ class Steps {
|
|
|
292
293
|
_echo(' ${dim('·')} $label');
|
|
293
294
|
}
|
|
294
295
|
|
|
295
|
-
/// Print
|
|
296
|
+
/// Print summary. Always inline — no box for a single line of text.
|
|
296
297
|
void summary() {
|
|
297
298
|
_sw.stop();
|
|
298
299
|
final ms = _sw.elapsedMilliseconds;
|
|
299
|
-
final time = ms >= 1000
|
|
300
|
-
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
301
|
-
: '${ms}ms';
|
|
300
|
+
final time = ms >= 1000 ? '${(ms / 1000).toStringAsFixed(1)}s' : '${ms}ms';
|
|
302
301
|
final parts = <String>[];
|
|
303
302
|
if (_done > 0) parts.add('${_done} completed');
|
|
304
303
|
if (_skipped > 0) parts.add('${_skipped} skipped');
|
|
@@ -307,7 +306,7 @@ class Steps {
|
|
|
307
306
|
}
|
|
308
307
|
if (parts.isEmpty) parts.add('no steps');
|
|
309
308
|
parts.add(time);
|
|
310
|
-
|
|
309
|
+
_echo(' ${dim(parts.join(' · '))}');
|
|
311
310
|
}
|
|
312
311
|
|
|
313
312
|
/// Run a step that shows a spinner while working, then marks it
|
|
@@ -401,9 +400,7 @@ class Steps {
|
|
|
401
400
|
_activeTimer = null;
|
|
402
401
|
_activeLabel = null;
|
|
403
402
|
if (_colorOn) {
|
|
404
|
-
stdout.write(
|
|
405
|
-
'\r\x1b[K ${success ? green('✓') : red('✗')} $label\n',
|
|
406
|
-
);
|
|
403
|
+
stdout.write('\r\x1b[K ${success ? green('✓') : red('✗')} $label\n');
|
|
407
404
|
}
|
|
408
405
|
if (success) {
|
|
409
406
|
_done++;
|
|
@@ -455,9 +452,7 @@ class ProgressBar {
|
|
|
455
452
|
_spin = (_spin + 1) % _frames.length;
|
|
456
453
|
final pctStr = '${(pct * 100).round()}%'.padLeft(4);
|
|
457
454
|
|
|
458
|
-
stdout.write(
|
|
459
|
-
'\r\x1b[K ${cyan(_frames[_spin])} $_label $bar $pctStr',
|
|
460
|
-
);
|
|
455
|
+
stdout.write('\r\x1b[K ${cyan(_frames[_spin])} $_label $bar $pctStr');
|
|
461
456
|
}
|
|
462
457
|
|
|
463
458
|
/// Finish: print the final status line. This is intentionally async and
|
|
@@ -470,11 +465,10 @@ class ProgressBar {
|
|
|
470
465
|
_sw.stop();
|
|
471
466
|
// Keep bar visible for at least 500ms so fast ops are readable.
|
|
472
467
|
final elapsed = _sw.elapsedMilliseconds;
|
|
473
|
-
if (elapsed < 500)
|
|
468
|
+
if (elapsed < 500)
|
|
469
|
+
await Future.delayed(Duration(milliseconds: 500 - elapsed));
|
|
474
470
|
final ms = _sw.elapsedMilliseconds;
|
|
475
|
-
final time = ms >= 1000
|
|
476
|
-
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
477
|
-
: '${ms}ms';
|
|
471
|
+
final time = ms >= 1000 ? '${(ms / 1000).toStringAsFixed(1)}s' : '${ms}ms';
|
|
478
472
|
if (_colorOn) {
|
|
479
473
|
stdout.write('\r\x1b[K ${green('✓')} $_label in $time\n');
|
|
480
474
|
} else {
|
|
@@ -159,49 +159,49 @@ packages:
|
|
|
159
159
|
path: "../../plugins/aws"
|
|
160
160
|
relative: true
|
|
161
161
|
source: path
|
|
162
|
-
version: "0.1.
|
|
162
|
+
version: "0.1.10"
|
|
163
163
|
flutter_ota_kit_cloudflare:
|
|
164
164
|
dependency: "direct main"
|
|
165
165
|
description:
|
|
166
166
|
path: "../../plugins/cloudflare"
|
|
167
167
|
relative: true
|
|
168
168
|
source: path
|
|
169
|
-
version: "0.1.
|
|
169
|
+
version: "0.1.10"
|
|
170
170
|
flutter_ota_kit_core:
|
|
171
171
|
dependency: "direct main"
|
|
172
172
|
description:
|
|
173
173
|
path: "../core"
|
|
174
174
|
relative: true
|
|
175
175
|
source: path
|
|
176
|
-
version: "0.1.
|
|
176
|
+
version: "0.1.8"
|
|
177
177
|
flutter_ota_kit_plugin_core:
|
|
178
178
|
dependency: "direct main"
|
|
179
179
|
description:
|
|
180
180
|
path: "../../plugins/plugin-core"
|
|
181
181
|
relative: true
|
|
182
182
|
source: path
|
|
183
|
-
version: "0.0.
|
|
183
|
+
version: "0.0.10"
|
|
184
184
|
flutter_ota_kit_pocketbase:
|
|
185
185
|
dependency: "direct main"
|
|
186
186
|
description:
|
|
187
187
|
path: "../../plugins/pocketbase"
|
|
188
188
|
relative: true
|
|
189
189
|
source: path
|
|
190
|
-
version: "0.1.
|
|
190
|
+
version: "0.1.7"
|
|
191
191
|
flutter_ota_kit_postgres:
|
|
192
192
|
dependency: "direct main"
|
|
193
193
|
description:
|
|
194
194
|
path: "../../plugins/postgres"
|
|
195
195
|
relative: true
|
|
196
196
|
source: path
|
|
197
|
-
version: "0.1.
|
|
197
|
+
version: "0.1.10"
|
|
198
198
|
flutter_ota_kit_supabase:
|
|
199
199
|
dependency: "direct main"
|
|
200
200
|
description:
|
|
201
201
|
path: "../../plugins/supabase"
|
|
202
202
|
relative: true
|
|
203
203
|
source: path
|
|
204
|
-
version: "0.1.
|
|
204
|
+
version: "0.1.11"
|
|
205
205
|
frontend_server_client:
|
|
206
206
|
dependency: transitive
|
|
207
207
|
description:
|
|
@@ -226,8 +226,9 @@ class MockPocketBaseClient implements PocketBaseClient {
|
|
|
226
226
|
Future<void> deleteAdmin(String id) async {}
|
|
227
227
|
|
|
228
228
|
@override
|
|
229
|
-
Future<List<Map<String, dynamic>>> exportCollection(
|
|
230
|
-
|
|
229
|
+
Future<List<Map<String, dynamic>>> exportCollection(
|
|
230
|
+
String collection,
|
|
231
|
+
) async => [];
|
|
231
232
|
|
|
232
233
|
@override
|
|
233
234
|
Future<void> importCollection(
|
|
@@ -247,7 +248,10 @@ class MockPocketBaseClient implements PocketBaseClient {
|
|
|
247
248
|
|
|
248
249
|
@override
|
|
249
250
|
Future<Map<String, dynamic>> listLogs({
|
|
250
|
-
String? filter,
|
|
251
|
+
String? filter,
|
|
252
|
+
String? sort,
|
|
253
|
+
int page = 1,
|
|
254
|
+
int perPage = 30,
|
|
251
255
|
}) async => {'items': [], 'totalItems': 0};
|
|
252
256
|
|
|
253
257
|
@override
|
|
@@ -80,22 +80,19 @@ void main() {
|
|
|
80
80
|
await mock.stop();
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
test(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
expect(mock.createdCollections, containsAll(_allCollections));
|
|
97
|
-
},
|
|
98
|
-
);
|
|
83
|
+
test('schema installer creates all five collections on a fresh PB', () async {
|
|
84
|
+
final installer = PocketBaseSchemaInstaller(
|
|
85
|
+
url: mock.baseUrl,
|
|
86
|
+
adminEmail: 'admin@x.com',
|
|
87
|
+
adminPassword: 'secret',
|
|
88
|
+
);
|
|
89
|
+
final result = await installer.install();
|
|
90
|
+
expect(mock.authCalled, isTrue);
|
|
91
|
+
expect(mock.lastAuthEmail, 'admin@x.com');
|
|
92
|
+
expect(result.created, containsAll(_allCollections));
|
|
93
|
+
expect(result.skipped, isEmpty);
|
|
94
|
+
expect(mock.createdCollections, containsAll(_allCollections));
|
|
95
|
+
});
|
|
99
96
|
|
|
100
97
|
test('schema installer skips collections that already exist', () async {
|
|
101
98
|
mock.existingCollections = _allCollections;
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## 0.1.8
|
|
2
|
+
|
|
3
|
+
- Release 2026-09-10
|
|
4
|
+
|
|
5
|
+
## 0.1.8
|
|
6
|
+
|
|
7
|
+
- Automated release 2026-09-10
|
|
8
|
+
|
|
9
|
+
## 0.1.7
|
|
10
|
+
|
|
11
|
+
- Added comprehensive documentation for all public APIs.
|
|
12
|
+
- Improved `Bundle` equality and hashCode implementation.
|
|
13
|
+
- Fixed `toSqlJson()` nullable field handling.
|
|
14
|
+
- Added `semver` exports to barrel file.
|
|
15
|
+
|
|
1
16
|
## 0.1.6
|
|
2
17
|
|
|
3
18
|
- Added `gitCommitHash` field to `UpdateInfo` for commit tracking.
|
|
@@ -21,6 +21,8 @@ export 'src/update_bundle_params.dart';
|
|
|
21
21
|
// Algorithms.
|
|
22
22
|
export 'src/rollout.dart';
|
|
23
23
|
export 'src/semver.dart';
|
|
24
|
+
export 'src/semver_range.dart' show satisfies;
|
|
25
|
+
export 'src/semver_version.dart' show SemVer;
|
|
24
26
|
export 'src/uuid.dart';
|
|
25
27
|
|
|
26
28
|
// Bundle artifact helpers.
|