@_nazmiforreal/flutter-ota 0.1.16 → 0.1.18
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_graph.json +7 -7
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +200 -0
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +29 -65
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +4 -15
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +2 -12
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +3 -1
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +62 -24
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +3 -4
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +19 -47
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +12 -13
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +2 -0
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +15 -17
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +14 -16
- package/dart-src/packages/cli-tools/lib/src/operations.dart +9 -0
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +0 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +0 -3
- package/dart-src/packages/cli-tools/lib/src/runner.dart +15 -1
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +126 -8
- package/dart-src/packages/cli-tools/pubspec.lock +7 -7
- package/dart-src/packages/core/CHANGELOG.md +1 -12
- package/dart-src/packages/core/LICENSE +1 -1
- package/dart-src/packages/core/pubspec.yaml +1 -1
- package/dart-src/plugins/aws/CHANGELOG.md +1 -18
- package/dart-src/plugins/aws/LICENSE +1 -1
- package/dart-src/plugins/aws/pubspec.yaml +1 -1
- package/dart-src/plugins/cloudflare/CHANGELOG.md +1 -18
- package/dart-src/plugins/cloudflare/LICENSE +1 -1
- package/dart-src/plugins/cloudflare/pubspec.yaml +1 -1
- package/dart-src/plugins/plugin-core/CHANGELOG.md +1 -17
- package/dart-src/plugins/plugin-core/LICENSE +1 -1
- package/dart-src/plugins/plugin-core/pubspec.yaml +1 -1
- package/dart-src/plugins/pocketbase/CHANGELOG.md +1 -8
- package/dart-src/plugins/pocketbase/LICENSE +1 -1
- package/dart-src/plugins/pocketbase/pubspec.yaml +1 -1
- package/dart-src/plugins/postgres/CHANGELOG.md +1 -19
- package/dart-src/plugins/postgres/LICENSE +1 -1
- package/dart-src/plugins/postgres/pubspec.yaml +1 -1
- package/dart-src/plugins/supabase/CHANGELOG.md +1 -23
- package/dart-src/plugins/supabase/LICENSE +1 -1
- package/dart-src/plugins/supabase/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/scripts/vendor.sh +0 -0
|
@@ -248,10 +248,8 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
248
248
|
step('Wrote ${file.path}');
|
|
249
249
|
|
|
250
250
|
if (global) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
'Restrict permissions and use a secrets manager in production.',
|
|
254
|
-
);
|
|
251
|
+
warn('Secrets are stored in plaintext under ~/.flutter_ota_kit. '
|
|
252
|
+
'Restrict permissions and use a secrets manager in production.');
|
|
255
253
|
return;
|
|
256
254
|
}
|
|
257
255
|
|
|
@@ -259,35 +257,23 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
259
257
|
await _scaffoldProject(cfg, provider);
|
|
260
258
|
|
|
261
259
|
stdout.writeln();
|
|
262
|
-
stdout.writeln(cyan('Next steps:'));
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
stdout.writeln(
|
|
269
|
-
' A `.env` scaffold was written — put secrets there, then build with '
|
|
270
|
-
'`--dart-define-from-file=.env` (environment overrides config).',
|
|
271
|
-
);
|
|
260
|
+
stdout.writeln(' ${cyan('Next steps:')}');
|
|
261
|
+
step('1. ${dim('flutter pub get')}');
|
|
262
|
+
step('2. In lib/main.dart call ${green('await setupFlutterOta();')} '
|
|
263
|
+
'right after WidgetsFlutterBinding.ensureInitialized() (before runApp). '
|
|
264
|
+
'A `.env` scaffold was written — put secrets there, then build with '
|
|
265
|
+
'`--dart-define-from-file=.env` (environment overrides config).');
|
|
272
266
|
if (provider == 'supabase') {
|
|
273
|
-
|
|
274
|
-
' 3. Provision the backend once: ${dim('flutter-ota migrate supabase')}',
|
|
275
|
-
);
|
|
267
|
+
step('3. Provision the backend once: ${dim('flutter-ota migrate supabase')}');
|
|
276
268
|
} else if (provider == 'postgres') {
|
|
277
|
-
|
|
278
|
-
' 3. Provision the backend: ${dim('flutter-ota migrate postgres')}',
|
|
279
|
-
);
|
|
269
|
+
step('3. Provision the backend: ${dim('flutter-ota migrate postgres')}');
|
|
280
270
|
} else if (provider == 'pocketbase') {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
'${dim('flutter-ota pocketbase serve')}.',
|
|
285
|
-
);
|
|
271
|
+
step('3. Install PocketBase + provision the schema: '
|
|
272
|
+
'${dim('flutter-ota pocketbase install')} then '
|
|
273
|
+
'${dim('flutter-ota pocketbase serve')}.');
|
|
286
274
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
'then ${dim('flutter-ota deploy')}',
|
|
290
|
-
);
|
|
275
|
+
step('4. Build a patch: ${dim('flutter-ota build --name 1.0.1')} '
|
|
276
|
+
'then ${dim('flutter-ota deploy')}');
|
|
291
277
|
});
|
|
292
278
|
|
|
293
279
|
/// Generate the integration files (pubspec dep, manifest permission, setup dart
|
|
@@ -306,9 +292,7 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
306
292
|
void _addPubDependency() {
|
|
307
293
|
final pubspec = File('pubspec.yaml');
|
|
308
294
|
if (!pubspec.existsSync()) {
|
|
309
|
-
|
|
310
|
-
yellow(' ⚠ pubspec.yaml not found; skipping dependency injection.'),
|
|
311
|
-
);
|
|
295
|
+
warn('pubspec.yaml not found; skipping dependency injection.');
|
|
312
296
|
return;
|
|
313
297
|
}
|
|
314
298
|
var content = pubspec.readAsStringSync();
|
|
@@ -320,11 +304,7 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
320
304
|
final marker = '\ndependencies:';
|
|
321
305
|
final idx = content.indexOf(marker);
|
|
322
306
|
if (idx == -1) {
|
|
323
|
-
|
|
324
|
-
yellow(
|
|
325
|
-
' ⚠ could not find a `dependencies:` block; add `flutter_ota_kit` manually.',
|
|
326
|
-
),
|
|
327
|
-
);
|
|
307
|
+
warn('Could not find a `dependencies:` block; add `flutter_ota_kit` manually.');
|
|
328
308
|
return;
|
|
329
309
|
}
|
|
330
310
|
final insertAt = idx + marker.length;
|
|
@@ -336,11 +316,7 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
336
316
|
void _addInternetPermission() {
|
|
337
317
|
final mf = File('android/app/src/main/AndroidManifest.xml');
|
|
338
318
|
if (!mf.existsSync()) {
|
|
339
|
-
|
|
340
|
-
yellow(
|
|
341
|
-
' ⚠ AndroidManifest.xml not found; skipping INTERNET permission.',
|
|
342
|
-
),
|
|
343
|
-
);
|
|
319
|
+
warn('AndroidManifest.xml not found; skipping INTERNET permission.');
|
|
344
320
|
return;
|
|
345
321
|
}
|
|
346
322
|
var content = mf.readAsStringSync();
|
|
@@ -350,11 +326,7 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
350
326
|
}
|
|
351
327
|
final match = RegExp(r'<manifest[^>]*>').firstMatch(content);
|
|
352
328
|
if (match == null) {
|
|
353
|
-
|
|
354
|
-
yellow(
|
|
355
|
-
' ⚠ could not locate <manifest> tag; add INTERNET permission manually.',
|
|
356
|
-
),
|
|
357
|
-
);
|
|
329
|
+
warn('Could not locate <manifest> tag; add INTERNET permission manually.');
|
|
358
330
|
return;
|
|
359
331
|
}
|
|
360
332
|
final insertAt = match.end;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import 'dart:convert';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import '../backend.dart';
|
|
5
3
|
import '../cli_base.dart';
|
|
6
4
|
import '../config.dart';
|
|
7
5
|
import '../sign.dart';
|
|
@@ -9,13 +7,10 @@ import '../ui/ui.dart';
|
|
|
9
7
|
|
|
10
8
|
/// `flutter_ota_kit keys` — generate an Ed25519 keypair for bundle signing.
|
|
11
9
|
class KeysCommand extends FlutterPatcherCommand {
|
|
12
|
-
KeysCommand(
|
|
10
|
+
KeysCommand() {
|
|
13
11
|
argParser.addFlag('save', help: 'Persist the public key into the project config.');
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
final FlutterPatcherConfig? config;
|
|
17
|
-
final Backend? backendOverride;
|
|
18
|
-
|
|
19
14
|
@override
|
|
20
15
|
String get name => 'keys';
|
|
21
16
|
|
|
@@ -26,10 +21,11 @@ class KeysCommand extends FlutterPatcherCommand {
|
|
|
26
21
|
@override
|
|
27
22
|
Future<int> run() => runGuarded(() async {
|
|
28
23
|
banner('keys');
|
|
29
|
-
final
|
|
30
|
-
|
|
24
|
+
final steps = Steps('keys');
|
|
25
|
+
final (privateB64, publicB64) = await steps.run(
|
|
31
26
|
'Generating Ed25519 keypair',
|
|
32
|
-
|
|
27
|
+
() => generateEd25519KeyPair(),
|
|
28
|
+
// Use Steps.run without the spinner pattern; keys is fast.
|
|
33
29
|
);
|
|
34
30
|
box('ed25519 keys', [
|
|
35
31
|
'${bold('private')} (keep secret — use with `deploy --key`):',
|
|
@@ -42,12 +38,15 @@ class KeysCommand extends FlutterPatcherCommand {
|
|
|
42
38
|
final file = configCandidates().first;
|
|
43
39
|
final json =
|
|
44
40
|
file.existsSync() && file.readAsStringSync().trim().isNotEmpty
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
? (jsonDecode(file.readAsStringSync()) as Map<String, dynamic>)
|
|
42
|
+
: <String, dynamic>{};
|
|
47
43
|
writePath(json, 'publicKey', publicB64);
|
|
48
|
-
|
|
49
|
-
|
|
44
|
+
await steps.run<void>('Saving public key to ${file.path}',
|
|
45
|
+
() async {
|
|
46
|
+
_save(json);
|
|
47
|
+
});
|
|
50
48
|
}
|
|
49
|
+
steps.summary();
|
|
51
50
|
});
|
|
52
51
|
|
|
53
52
|
void _save(Map<String, dynamic> json) {
|
|
@@ -202,6 +202,7 @@ class PocketBaseStopCommand extends FlutterPatcherCommand {
|
|
|
202
202
|
|
|
203
203
|
@override
|
|
204
204
|
Future<int> run() => runGuarded(() async {
|
|
205
|
+
banner('pocketbase · stop');
|
|
205
206
|
step('Use Ctrl+C on the running serve process to stop PocketBase.');
|
|
206
207
|
return;
|
|
207
208
|
});
|
|
@@ -216,6 +217,7 @@ class PocketBaseStatusCommand extends FlutterPatcherCommand {
|
|
|
216
217
|
|
|
217
218
|
@override
|
|
218
219
|
Future<int> run() => runGuarded(() async {
|
|
220
|
+
banner('pocketbase · status');
|
|
219
221
|
final paths = PocketBaseInstallPaths.resolve();
|
|
220
222
|
final exists = await paths.binaryPath.exists();
|
|
221
223
|
if (exists) {
|
|
@@ -45,23 +45,21 @@ class RollbackCommand extends FlutterPatcherCommand {
|
|
|
45
45
|
final backend = requireBackend(cfg, override: backendOverride);
|
|
46
46
|
|
|
47
47
|
banner('rollback');
|
|
48
|
-
final
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
done: 'Rolled back',
|
|
64
|
-
);
|
|
48
|
+
final steps = Steps('rollback');
|
|
49
|
+
final (disabled, live) = await steps.run('Rolling back channel "$channel"', () async {
|
|
50
|
+
if (bundleId != null && bundleId.isNotEmpty) {
|
|
51
|
+
return rollbackToBundle(
|
|
52
|
+
backend,
|
|
53
|
+
channel,
|
|
54
|
+
bundleId,
|
|
55
|
+
platform: platform,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
final id = await rollbackChannel(backend, channel);
|
|
59
|
+
final nowLive = (await getChannel(backend, channel))?.id ?? '';
|
|
60
|
+
return (<String>[id], nowLive);
|
|
61
|
+
});
|
|
62
|
+
steps.summary();
|
|
65
63
|
box('rollback', [
|
|
66
64
|
kv('channel', channel),
|
|
67
65
|
kv('now live', live.isEmpty ? dim('(none)') : cyan(live)),
|
|
@@ -46,11 +46,13 @@ class StorageListCommand extends FlutterPatcherCommand {
|
|
|
46
46
|
final backend = requireBackend(cfg, override: backendOverride);
|
|
47
47
|
final prefix = argResults!['prefix'] as String?;
|
|
48
48
|
banner('storage · list');
|
|
49
|
-
final
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
final steps = Steps('list');
|
|
50
|
+
final objects = await steps.run('Listing storage objects', () =>
|
|
51
|
+
backend.storage.listObjects(
|
|
52
|
+
prefix == null || prefix.isEmpty ? null : prefix));
|
|
52
53
|
if (objects.isEmpty) {
|
|
53
|
-
|
|
54
|
+
steps.skip('(no objects)');
|
|
55
|
+
steps.summary();
|
|
54
56
|
return;
|
|
55
57
|
}
|
|
56
58
|
final rows = <List<String>>[];
|
|
@@ -58,11 +60,12 @@ class StorageListCommand extends FlutterPatcherCommand {
|
|
|
58
60
|
final size = o.size >= 1024 * 1024
|
|
59
61
|
? '${(o.size / (1024 * 1024)).toStringAsFixed(2)} MB'
|
|
60
62
|
: o.size >= 1024
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
? '${(o.size / 1024).toStringAsFixed(1)} KB'
|
|
64
|
+
: '${o.size} B';
|
|
63
65
|
rows.add([cyan(o.key), size]);
|
|
64
66
|
}
|
|
65
67
|
table('${objects.length} objects', ['KEY', 'SIZE'], rows);
|
|
68
|
+
steps.summary();
|
|
66
69
|
});
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -96,18 +99,13 @@ class StorageDeleteCommand extends FlutterPatcherCommand {
|
|
|
96
99
|
final cfg = effectiveConfig(config ?? loadConfig(), argResults!);
|
|
97
100
|
final backend = requireBackend(cfg, override: backendOverride);
|
|
98
101
|
banner('storage · delete');
|
|
102
|
+
final steps = Steps('delete');
|
|
99
103
|
if (keys.isNotEmpty) {
|
|
100
|
-
await
|
|
101
|
-
|
|
102
|
-
'Deleting ${keys.length} object(s)',
|
|
103
|
-
done: 'Deleted',
|
|
104
|
-
);
|
|
104
|
+
await steps.run('Deleting ${keys.length} object(s)',
|
|
105
|
+
() => backend.storage.deleteObjects(keys));
|
|
105
106
|
} else {
|
|
106
|
-
await
|
|
107
|
-
() => backend.storage.delete(uri!),
|
|
108
|
-
'Deleting $uri',
|
|
109
|
-
done: 'Deleted',
|
|
110
|
-
);
|
|
107
|
+
await steps.run('Deleting $uri', () => backend.storage.delete(uri!));
|
|
111
108
|
}
|
|
109
|
+
steps.summary();
|
|
112
110
|
});
|
|
113
111
|
}
|
|
@@ -23,6 +23,7 @@ class DeployOptions {
|
|
|
23
23
|
this.gitCommitHash,
|
|
24
24
|
this.bundleId,
|
|
25
25
|
this.metadata,
|
|
26
|
+
this.onPhase,
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
final String source;
|
|
@@ -36,6 +37,10 @@ class DeployOptions {
|
|
|
36
37
|
final String? gitCommitHash;
|
|
37
38
|
final String? bundleId;
|
|
38
39
|
final BundleMetadata? metadata;
|
|
40
|
+
|
|
41
|
+
/// Called with a human-readable phase label as deploy progresses.
|
|
42
|
+
/// Used by the CLI to show per-step progress (e.g. Steps.run()).
|
|
43
|
+
final void Function(String phase)? onPhase;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
/// Options for [listBundles].
|
|
@@ -85,6 +90,7 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
|
|
|
85
90
|
zipPath = packaged.path;
|
|
86
91
|
shouldDelete = false;
|
|
87
92
|
} else {
|
|
93
|
+
opts.onPhase?.call('Zipping source');
|
|
88
94
|
zipPath = await zipDirectory(opts.source);
|
|
89
95
|
shouldDelete = true;
|
|
90
96
|
}
|
|
@@ -94,6 +100,7 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
|
|
|
94
100
|
final zipBytes = await File(zipPath).readAsBytes();
|
|
95
101
|
// Device SDK verifies the artifact against an MD5 hex of the whole file,
|
|
96
102
|
// so `fileHash` is always the MD5 hex (never a `sig:`-prefixed value).
|
|
103
|
+
opts.onPhase?.call('Hashing artifact');
|
|
97
104
|
final md5Hex = md5.convert(zipBytes).toString();
|
|
98
105
|
final signature = opts.signingKeyBase64 != null
|
|
99
106
|
? await ed25519Sign(utf8.encode(md5Hex), opts.signingKeyBase64!)
|
|
@@ -101,6 +108,7 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
|
|
|
101
108
|
|
|
102
109
|
final bundleId = opts.bundleId ?? uuidV7();
|
|
103
110
|
final key = bundleId;
|
|
111
|
+
opts.onPhase?.call('Uploading to storage');
|
|
104
112
|
final uploaded = await backend.storage.upload(key, zipPath);
|
|
105
113
|
final storageUri = uploaded['storageUri'];
|
|
106
114
|
if (storageUri == null || storageUri.isEmpty) {
|
|
@@ -125,6 +133,7 @@ Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
|
|
|
125
133
|
rolloutCohortCount: defaultRolloutCohortCount,
|
|
126
134
|
);
|
|
127
135
|
|
|
136
|
+
opts.onPhase?.call('Registering bundle in database');
|
|
128
137
|
await backend.db.appendBundle(bundle);
|
|
129
138
|
await backend.db.commitBundle();
|
|
130
139
|
return bundle;
|
|
@@ -18,8 +18,22 @@ class FlutterPatcherRunner extends CommandRunner<int> {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
@override
|
|
21
|
-
Future<int?> runCommand(ArgResults results) {
|
|
21
|
+
Future<int?> runCommand(ArgResults results) async {
|
|
22
22
|
verboseMode = results['verbose'] as bool? ?? false;
|
|
23
|
+
|
|
24
|
+
// Intercept missing subcommand for parent commands so we show our
|
|
25
|
+
// colored usage text instead of the args package's plain text.
|
|
26
|
+
if (results.command == null && results.rest.isEmpty) {
|
|
27
|
+
final cmdName = results.arguments.isNotEmpty ? results.arguments.first : null;
|
|
28
|
+
if (cmdName != null) {
|
|
29
|
+
final cmd = commands[cmdName];
|
|
30
|
+
if (cmd != null && cmd.subcommands.isNotEmpty) {
|
|
31
|
+
cmd.printUsage();
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
23
37
|
return super.runCommand(results);
|
|
24
38
|
}
|
|
25
39
|
|
|
@@ -99,6 +99,8 @@ void banner(String name) {
|
|
|
99
99
|
void step(String msg) => _echo(' ${green('✓')} $msg');
|
|
100
100
|
void warn(String msg) => _echo(' ${yellow('⚠')} ${yellow(msg)}');
|
|
101
101
|
void err(String msg) => _echo(' ${red('✗')} ${red(msg)}');
|
|
102
|
+
void info(String msg) => _echo(' ${blue('ℹ')} $msg');
|
|
103
|
+
void active(String msg) => _echo(' ${cyan('⠹')} $msg');
|
|
102
104
|
|
|
103
105
|
// ── Spinner ──────────────────────────────────────────────────────────────────
|
|
104
106
|
|
|
@@ -116,13 +118,18 @@ Future<T> spinner<T>(
|
|
|
116
118
|
return r;
|
|
117
119
|
}
|
|
118
120
|
var i = 0;
|
|
121
|
+
final sw = Stopwatch()..start();
|
|
119
122
|
final timer = Timer.periodic(const Duration(milliseconds: 80), (_) {
|
|
120
123
|
i = (i + 1) % _frames.length;
|
|
121
124
|
stdout.write('\r\x1b[K ${cyan(_frames[i])} $label');
|
|
122
125
|
});
|
|
123
126
|
try {
|
|
124
127
|
final r = await task();
|
|
128
|
+
// Keep spinner visible for at least 400ms so fast ops are readable.
|
|
129
|
+
final elapsed = sw.elapsedMilliseconds;
|
|
130
|
+
if (elapsed < 400) await Future.delayed(Duration(milliseconds: 400 - elapsed));
|
|
125
131
|
timer.cancel();
|
|
132
|
+
sw.stop();
|
|
126
133
|
stdout.write('\r\x1b[K ${green('✓')} ${done ?? label}\n');
|
|
127
134
|
return r;
|
|
128
135
|
} catch (e) {
|
|
@@ -232,12 +239,12 @@ void box(String title, List<String> lines) {
|
|
|
232
239
|
final leftDashes = math.max(1, totalDashes ~/ 2);
|
|
233
240
|
final rightDashes = totalDashes - leftDashes;
|
|
234
241
|
|
|
235
|
-
_echo('
|
|
242
|
+
_echo('┌${'─' * leftDashes}$titleText${'─' * rightDashes}┐');
|
|
236
243
|
for (final l in wrapped) {
|
|
237
244
|
final pad = math.max(0, inner - _dispWidth(l));
|
|
238
245
|
_echo('│ $l${' ' * pad} │');
|
|
239
246
|
}
|
|
240
|
-
_echo('
|
|
247
|
+
_echo('└${'─' * (width - 2)}┘');
|
|
241
248
|
}
|
|
242
249
|
|
|
243
250
|
// ── Steps ────────────────────────────────────────────────────────────────────
|
|
@@ -295,18 +302,122 @@ class Steps {
|
|
|
295
302
|
final parts = <String>[];
|
|
296
303
|
if (_done > 0) parts.add('${_done} completed');
|
|
297
304
|
if (_skipped > 0) parts.add('${_skipped} skipped');
|
|
298
|
-
if (_failed > 0)
|
|
305
|
+
if (_failed > 0) {
|
|
306
|
+
parts.add('${red('${_failed} error${_failed > 1 ? 's' : ''}')}');
|
|
307
|
+
}
|
|
299
308
|
if (parts.isEmpty) parts.add('no steps');
|
|
300
309
|
parts.add(time);
|
|
301
310
|
box('summary', [parts.join(' · ')]);
|
|
302
311
|
}
|
|
312
|
+
|
|
313
|
+
/// Run a step that shows a spinner while working, then marks it
|
|
314
|
+
/// success/fail/skip based on the result. This is the recommended way to
|
|
315
|
+
/// show multi-phase work where each phase has a distinct label the user
|
|
316
|
+
/// can understand.
|
|
317
|
+
///
|
|
318
|
+
/// Example:
|
|
319
|
+
/// ```dart
|
|
320
|
+
/// final s = Steps('deploy');
|
|
321
|
+
/// final zipped = await s.run('Zipping source', () async {
|
|
322
|
+
/// return await zipDir(source);
|
|
323
|
+
/// });
|
|
324
|
+
/// final uploaded = await s.run('Uploading to storage', () async {
|
|
325
|
+
/// return await backend.putObject(bucket, zipped);
|
|
326
|
+
/// });
|
|
327
|
+
/// await s.run('Registering bundle', () async {
|
|
328
|
+
/// return await backend.insertBundle(...);
|
|
329
|
+
/// });
|
|
330
|
+
/// s.summary();
|
|
331
|
+
/// ```
|
|
332
|
+
Future<T> run<T>(String label, Future<T> Function() task) async {
|
|
333
|
+
if (!_colorOn) {
|
|
334
|
+
_echo(' ${dim('•')} $label');
|
|
335
|
+
try {
|
|
336
|
+
final r = await task();
|
|
337
|
+
_done++;
|
|
338
|
+
_echo(' ${green('✓')} $label');
|
|
339
|
+
return r;
|
|
340
|
+
} catch (e) {
|
|
341
|
+
_failed++;
|
|
342
|
+
_echo(' ${red('✗')} $label');
|
|
343
|
+
rethrow;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
var i = 0;
|
|
347
|
+
final sw = Stopwatch()..start();
|
|
348
|
+
final timer = Timer.periodic(const Duration(milliseconds: 80), (_) {
|
|
349
|
+
i = (i + 1) % _frames.length;
|
|
350
|
+
stdout.write('\r\x1b[K ${cyan(_frames[i])} $label');
|
|
351
|
+
});
|
|
352
|
+
try {
|
|
353
|
+
final r = await task();
|
|
354
|
+
final elapsed = sw.elapsedMilliseconds;
|
|
355
|
+
if (elapsed < 200) {
|
|
356
|
+
await Future.delayed(Duration(milliseconds: 200 - elapsed));
|
|
357
|
+
}
|
|
358
|
+
timer.cancel();
|
|
359
|
+
sw.stop();
|
|
360
|
+
stdout.write('\r\x1b[K ${green('✓')} $label\n');
|
|
361
|
+
_done++;
|
|
362
|
+
return r;
|
|
363
|
+
} catch (e) {
|
|
364
|
+
timer.cancel();
|
|
365
|
+
stdout.write('\r\x1b[K ${red('✗')} $label\n');
|
|
366
|
+
_failed++;
|
|
367
|
+
rethrow;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
Timer? _activeTimer;
|
|
372
|
+
int _activeIdx = 0;
|
|
373
|
+
String? _activeLabel;
|
|
374
|
+
|
|
375
|
+
/// Print an "active" phase line (⠹ label) that will be replaced in-place
|
|
376
|
+
/// by the next call to [startActive] or finalized by [completeActive].
|
|
377
|
+
/// Use this when a long-running operation reports multiple internal phases
|
|
378
|
+
/// via a callback (e.g. `DeployOptions.onPhase`).
|
|
379
|
+
void startActive(String label) {
|
|
380
|
+
_activeLabel = label;
|
|
381
|
+
if (!_colorOn) {
|
|
382
|
+
_echo(' ${dim('•')} $label');
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
_activeIdx = 0;
|
|
386
|
+
_activeTimer?.cancel();
|
|
387
|
+
_activeTimer = Timer.periodic(const Duration(milliseconds: 80), (_) {
|
|
388
|
+
_activeIdx = (_activeIdx + 1) % _frames.length;
|
|
389
|
+
stdout.write('\r\x1b[K ${cyan(_frames[_activeIdx])} $label');
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/// Finalize the current active line. If [success] is true (default) the
|
|
394
|
+
/// line becomes `✓ label` and counts toward `_done`. If false, it becomes
|
|
395
|
+
/// `✗ label` and counts toward `_failed`. Call this when the long-running
|
|
396
|
+
/// operation finishes.
|
|
397
|
+
void completeActive({bool success = true}) {
|
|
398
|
+
final label = _activeLabel;
|
|
399
|
+
if (label == null) return;
|
|
400
|
+
_activeTimer?.cancel();
|
|
401
|
+
_activeTimer = null;
|
|
402
|
+
_activeLabel = null;
|
|
403
|
+
if (_colorOn) {
|
|
404
|
+
stdout.write(
|
|
405
|
+
'\r\x1b[K ${success ? green('✓') : red('✗')} $label\n',
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (success) {
|
|
409
|
+
_done++;
|
|
410
|
+
} else {
|
|
411
|
+
_failed++;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
303
414
|
}
|
|
304
415
|
|
|
305
416
|
// ── Progress Bar (thin, single-line) ────────────────────────────────────────
|
|
306
417
|
|
|
307
|
-
///
|
|
418
|
+
/// Braille single-line progress bar with spinner, label, bar, and time.
|
|
308
419
|
///
|
|
309
|
-
/// Renders as: `⠹ Label
|
|
420
|
+
/// Renders as: `⠹ Label ⣿⣿⣿⣀⣀⣀ 50%`
|
|
310
421
|
/// On finish: `✓ Label in 1.3s`
|
|
311
422
|
class ProgressBar {
|
|
312
423
|
ProgressBar(this.total, this.label);
|
|
@@ -335,11 +446,11 @@ class ProgressBar {
|
|
|
335
446
|
|
|
336
447
|
final pct = total > 0 ? (_current / total).clamp(0.0, 1.0) : 1.0;
|
|
337
448
|
|
|
338
|
-
//
|
|
449
|
+
// Braille bar: width scales to terminal, max 20 chars.
|
|
339
450
|
final barW = math.min(20, math.max(8, _cols - 40));
|
|
340
451
|
final filled = (barW * pct).round();
|
|
341
452
|
final empty = barW - filled;
|
|
342
|
-
final bar = '${yellow('
|
|
453
|
+
final bar = '${yellow('⣿' * filled)}${gray('⣀' * empty)}';
|
|
343
454
|
|
|
344
455
|
_spin = (_spin + 1) % _frames.length;
|
|
345
456
|
final pctStr = '${(pct * 100).round()}%'.padLeft(4);
|
|
@@ -349,10 +460,17 @@ class ProgressBar {
|
|
|
349
460
|
);
|
|
350
461
|
}
|
|
351
462
|
|
|
352
|
-
|
|
463
|
+
/// Finish: print the final status line. This is intentionally async and
|
|
464
|
+
/// fire-and-forget (called without `await` from [update] and [close]).
|
|
465
|
+
/// The 500ms delay keeps fast ops readable; if the process exits early,
|
|
466
|
+
/// the final line may not print — acceptable for a CLI progress aid.
|
|
467
|
+
void _finish() async {
|
|
353
468
|
if (_closed) return;
|
|
354
469
|
_closed = true;
|
|
355
470
|
_sw.stop();
|
|
471
|
+
// Keep bar visible for at least 500ms so fast ops are readable.
|
|
472
|
+
final elapsed = _sw.elapsedMilliseconds;
|
|
473
|
+
if (elapsed < 500) await Future.delayed(Duration(milliseconds: 500 - elapsed));
|
|
356
474
|
final ms = _sw.elapsedMilliseconds;
|
|
357
475
|
final time = ms >= 1000
|
|
358
476
|
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
@@ -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.5"
|
|
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.5"
|
|
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.4"
|
|
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.6"
|
|
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.2"
|
|
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.5"
|
|
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.6"
|
|
205
205
|
frontend_server_client:
|
|
206
206
|
dependency: transitive
|
|
207
207
|
description:
|
|
@@ -1,12 +1 @@
|
|
|
1
|
-
## 0.1.
|
|
2
|
-
|
|
3
|
-
- Add dartdoc on internal API surfaces (10/10 pana dartdoc coverage).
|
|
4
|
-
|
|
5
|
-
## 0.1.1
|
|
6
|
-
|
|
7
|
-
- Fix `<id>` angle bracket in dartdoc (`bundle.dart:23`) being interpreted as HTML by Markdown renderers.
|
|
8
|
-
- Repository moved to `https://github.com/HYPER12755/flutter-ota-kit`.
|
|
9
|
-
|
|
10
|
-
## 0.1.0
|
|
11
|
-
|
|
12
|
-
- Initial release of `flutter_ota_kit_core` as part of the flutter_ota_kit OTA toolkit.
|
|
1
|
+
## 0.1.4
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
name: flutter_ota_kit_core
|
|
2
2
|
description: Core data model, rollout cohorts, and shared utilities for flutter_ota_kit — Dart translation of hot-updater's packages/core.
|
|
3
|
-
version: 0.1.
|
|
3
|
+
version: 0.1.4
|
|
4
4
|
homepage: https://github.com/HYPER12755/flutter-ota-kit
|
|
5
5
|
repository: https://github.com/HYPER12755/flutter-ota-kit
|
|
6
6
|
issue_tracker: https://github.com/HYPER12755/flutter-ota-kit/issues
|