@_nazmiforreal/flutter-ota 0.1.19 → 0.1.21

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.
Files changed (59) hide show
  1. package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +4 -3
  2. package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
  3. package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +22 -10
  4. package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +16 -5
  5. package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +3 -0
  6. package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
  7. package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +12 -3
  8. package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +129 -6
  9. package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
  10. package/dart-src/packages/cli-tools/lib/src/commands/init.dart +23 -14
  11. package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +4 -0
  12. package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +266 -26
  13. package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1422 -16
  14. package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +4 -1
  15. package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +10 -9
  16. package/dart-src/packages/cli-tools/lib/src/config.dart +14 -19
  17. package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -1
  18. package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +1 -1
  19. package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +113 -10
  20. package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +59 -6
  21. package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
  22. package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +149 -0
  23. package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +14 -9
  24. package/dart-src/packages/core/CHANGELOG.md +6 -1
  25. package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
  26. package/dart-src/packages/core/lib/src/app_update_info.dart +73 -1
  27. package/dart-src/packages/core/lib/src/bundle.dart +62 -17
  28. package/dart-src/packages/core/lib/src/bundle_artifacts.dart +7 -3
  29. package/dart-src/packages/core/lib/src/bundle_patch_artifact.dart +16 -0
  30. package/dart-src/packages/core/lib/src/changed_asset.dart +33 -3
  31. package/dart-src/packages/core/lib/src/get_bundles_args.dart +24 -2
  32. package/dart-src/packages/core/lib/src/platform.dart +8 -0
  33. package/dart-src/packages/core/lib/src/semver.dart +0 -3
  34. package/dart-src/packages/core/lib/src/status.dart +20 -1
  35. package/dart-src/packages/core/lib/src/strategy.dart +10 -0
  36. package/dart-src/packages/core/lib/src/update_bundle_params.dart +15 -0
  37. package/dart-src/packages/core/lib/src/uuid.dart +11 -0
  38. package/dart-src/plugins/aws/CHANGELOG.md +5 -1
  39. package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +2 -2
  40. package/dart-src/plugins/cloudflare/CHANGELOG.md +5 -1
  41. package/dart-src/plugins/plugin-core/CHANGELOG.md +5 -1
  42. package/dart-src/plugins/plugin-core/lib/src/asset_storage_layout.dart +3 -0
  43. package/dart-src/plugins/plugin-core/lib/src/parse_storage_uri.dart +17 -0
  44. package/dart-src/plugins/plugin-core/lib/src/types.dart +94 -3
  45. package/dart-src/plugins/pocketbase/CHANGELOG.md +5 -1
  46. package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +2 -1
  47. package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +10 -2
  48. package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +617 -4
  49. package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +262 -42
  50. package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +9 -2
  51. package/dart-src/plugins/postgres/CHANGELOG.md +5 -1
  52. package/dart-src/plugins/supabase/CHANGELOG.md +6 -1
  53. package/dart-src/plugins/supabase/lib/src/supabase_database.dart +4 -0
  54. package/package.json +2 -2
  55. package/bin/flutter-ota-linux-x64 +0 -0
  56. package/bin/flutter-ota.js +0 -36
  57. package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -458
  58. package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -707
  59. package/dart-src/packages/cli-tools/pubspec.lock +0 -590
@@ -6,7 +6,10 @@ 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
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
9
+ final detected = config?.provider ?? loadConfig()?.provider;
10
+ argParser.addOption('backend', abbr: 'b', help: detected != null
11
+ ? 'Backend provider [detected: $detected].'
12
+ : 'Backend provider.');
10
13
  argParser.addOption('channel', abbr: 'c', help: 'Channel to roll back.');
11
14
  argParser.addOption(
12
15
  'bundle-id',
@@ -22,12 +22,15 @@ class StorageCommand extends FlutterPatcherCommand {
22
22
 
23
23
  @override
24
24
  String get description =>
25
- 'Inspect and manage bundle storage objects (find / delete orphans).';
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
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
30
+ final detected = config?.provider ?? loadConfig()?.provider;
31
+ argParser.addOption('backend', abbr: 'b', help: detected != null
32
+ ? 'Backend provider [detected: $detected].'
33
+ : 'Backend provider.');
31
34
  argParser.addOption('prefix', help: 'Key prefix filter (e.g. bundles).');
32
35
  }
33
36
 
@@ -57,12 +60,7 @@ class StorageListCommand extends FlutterPatcherCommand {
57
60
  }
58
61
  final rows = <List<String>>[];
59
62
  for (final o in objects) {
60
- final size = o.size >= 1024 * 1024
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]);
63
+ rows.add([cyan(o.key), humanSize(o.size)]);
66
64
  }
67
65
  table('${objects.length} objects', ['KEY', 'SIZE'], rows);
68
66
  steps.summary();
@@ -71,7 +69,10 @@ class StorageListCommand extends FlutterPatcherCommand {
71
69
 
72
70
  class StorageDeleteCommand extends FlutterPatcherCommand {
73
71
  StorageDeleteCommand({this.config, this.backendOverride}) {
74
- argParser.addOption('backend', abbr: 'b', help: 'Backend provider.');
72
+ final detected = config?.provider ?? loadConfig()?.provider;
73
+ argParser.addOption('backend', abbr: 'b', help: detected != null
74
+ ? 'Backend provider [detected: $detected].'
75
+ : 'Backend provider.');
75
76
  argParser.addMultiOption('key', help: 'Storage key to delete (repeatable).');
76
77
  argParser.addOption('uri', help: 'Full storage URI to delete.');
77
78
  }
@@ -232,8 +232,6 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
232
232
  String? url,
233
233
  String? serviceRoleKey,
234
234
  String? anonKey,
235
- String? bucket,
236
- String? basePath,
237
235
  SupabaseClientFactory? clientFactory,
238
236
  }) {
239
237
  final env = Platform.environment;
@@ -247,8 +245,8 @@ SupabaseServiceRoleConfig resolveSupabaseConfig(
247
245
 
248
246
  if (resolvedUrl == null || resolvedUrl.isEmpty) {
249
247
  throw StateError(
250
- 'Supabase URL is required. Set it via --url, SUPABASE_URL, or '
251
- '`flutter_ota_kit config set supabase.url <url>`.',
248
+ 'Supabase URL is required. Set it via SUPABASE_URL env var, '
249
+ 'or `flutter-ota config set supabase.url <url>`.',
252
250
  );
253
251
  }
254
252
 
@@ -275,8 +273,6 @@ SupabaseStorageConfig resolveSupabaseStorageConfig(
275
273
  url: url,
276
274
  serviceRoleKey: serviceRoleKey,
277
275
  anonKey: anonKey,
278
- bucket: bucket,
279
- basePath: basePath,
280
276
  clientFactory: clientFactory,
281
277
  );
282
278
  return SupabaseStorageConfig(
@@ -284,12 +280,10 @@ SupabaseStorageConfig resolveSupabaseStorageConfig(
284
280
  supabaseServiceRoleKey: db.supabaseServiceRoleKey,
285
281
  supabaseAnonKey: db.supabaseAnonKey,
286
282
  clientFactory: db.clientFactory,
287
- bucketName: db.supabaseServiceRoleKey != null || db.supabaseAnonKey != null
288
- ? (bucket ??
289
- Platform.environment['SUPABASE_BUCKET'] ??
290
- config.supabase.bucket ??
291
- 'bundles')
292
- : (bucket ?? config.supabase.bucket ?? 'bundles'),
283
+ bucketName: bucket ??
284
+ Platform.environment['SUPABASE_BUCKET'] ??
285
+ config.supabase.bucket ??
286
+ 'bundles',
293
287
  basePath:
294
288
  basePath ??
295
289
  Platform.environment['SUPABASE_BASE_PATH'] ??
@@ -387,8 +381,8 @@ PostgresConfig resolvePostgresDatabaseConfig(
387
381
 
388
382
  if (resolvedHost == null || resolvedHost.isEmpty) {
389
383
  throw StateError(
390
- 'Postgres host is required. Set --pg-host, POSTGRES_HOST, or '
391
- '`flutter_ota_kit config set postgres.host <host>`.',
384
+ 'Postgres host is required. Set POSTGRES_HOST env var, '
385
+ 'or `flutter-ota config set postgres.host <host>`.',
392
386
  );
393
387
  }
394
388
 
@@ -765,15 +759,16 @@ PocketBaseDatabaseConfig resolvePocketBaseDatabaseConfig(
765
759
 
766
760
  if (resolvedUrl == null || resolvedUrl.isEmpty) {
767
761
  throw StateError(
768
- 'PocketBase URL is required. Set --pb-url, POCKETBASE_URL, or '
769
- '`flutter_ota_kit config set pocketbase.url <url>`.',
762
+ 'PocketBase URL is required. Set POCKETBASE_URL env var, '
763
+ 'or `flutter-ota config set pocketbase.url <url>`.',
770
764
  );
771
765
  }
772
766
  if (resolvedEmail == null || resolvedPassword == null) {
773
767
  throw StateError(
774
- 'PocketBase admin credentials are required. Set --pb-email, '
775
- '--pb-password, POCKETBASE_ADMIN_EMAIL/PASSWORD, or '
776
- '`flutter_ota_kit config set pocketbase.*`.',
768
+ 'PocketBase admin credentials are required. Set '
769
+ 'POCKETBASE_ADMIN_EMAIL / POCKETBASE_ADMIN_PASSWORD env vars, or '
770
+ '`flutter-ota config set pocketbase.adminEmail/<email> '
771
+ 'pocketbase.adminPassword/<password>`.',
777
772
  );
778
773
  }
779
774
 
@@ -62,7 +62,7 @@ class ListOptions {
62
62
  Future<Bundle> deployBundle(Backend backend, DeployOptions opts) async {
63
63
  if (opts.targetAppVersion != null && opts.fingerprintHash != null) {
64
64
  throw StateError(
65
- 'Use only one of target-app-version / fingerprint-hash (DB CHECK).',
65
+ 'Use only one of target-app-version / fingerprint-hash.',
66
66
  );
67
67
  }
68
68
  if (opts.targetAppVersion == null && opts.fingerprintHash == null) {
@@ -17,7 +17,7 @@ import 'package:path/path.dart' as p;
17
17
 
18
18
  /// Default PocketBase version managed by the CLI. Bumped in lockstep with
19
19
  /// the rest of the stack.
20
- const String kDefaultPocketBaseVersion = '0.22.21';
20
+ const String kDefaultPocketBaseVersion = '0.40.3';
21
21
 
22
22
  /// Where the binary + extracted files live.
23
23
  class PocketBaseInstallPaths {
@@ -4,31 +4,37 @@
4
4
  /// PB reads its data directory from the `--dir` flag (default `./pb_data`).
5
5
  /// The CLI's installer pre-creates that directory and copies hooks into
6
6
  /// `pb_data/pb_hooks/` before the first start.
7
+ ///
8
+ /// A PID file (`pb.pid`) is written to the data directory on start so that
9
+ /// `stop` can find and terminate the process even after the CLI restarts.
7
10
  library;
8
11
 
9
12
  import 'dart:async';
13
+ import 'dart:convert';
10
14
  import 'dart:io';
11
15
 
12
16
  import 'package:path/path.dart' as p;
13
17
 
14
18
  class PocketBaseProcess {
15
- 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
+ }
16
23
 
17
24
  final Process process;
18
25
  final Directory dataDir;
19
26
  final File binaryPath;
27
+ bool _exitCodeCompleted;
20
28
 
21
- bool get isRunning => true; // Process instance is always present after start.
29
+ bool get isRunning => !_exitCodeCompleted;
22
30
 
23
31
  int get pid => process.pid;
24
32
 
25
33
  Future<int> get exitCode => process.exitCode;
26
34
 
27
- /// Returns the stdout/stderr streams merged.
28
35
  Stream<List<int>> get output => process.stdout;
29
36
  Stream<List<int>> get errors => process.stderr;
30
37
 
31
- /// Send SIGINT (Ctrl+C) for a graceful shutdown, then SIGKILL if needed.
32
38
  Future<void> stop({Duration timeout = const Duration(seconds: 5)}) async {
33
39
  if (Platform.isWindows) {
34
40
  process.kill();
@@ -59,6 +65,55 @@ class PocketBaseProcessManager {
59
65
  PocketBaseProcess? _process;
60
66
  PocketBaseProcess? get process => _process;
61
67
 
68
+ File get pidFile => File(p.join(dataDir.path, 'pb.pid'));
69
+ File get configFile => File(p.join(dataDir.path, 'pb.json'));
70
+
71
+ /// Read the stored PID from the PID file, or -1 if not present.
72
+ int readPid() {
73
+ if (!pidFile.existsSync()) return -1;
74
+ final content = pidFile.readAsStringSync().trim();
75
+ return int.tryParse(content) ?? -1;
76
+ }
77
+
78
+ /// Check if the process identified by the PID file is still alive.
79
+ Future<bool> isRunningByPidFile() async {
80
+ final pid = readPid();
81
+ if (pid <= 0) return false;
82
+ try {
83
+ final result = await Process.run('kill', ['-0', '$pid']);
84
+ return result.exitCode == 0;
85
+ } catch (_) {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ void _writePidFile(int pid) {
91
+ pidFile.writeAsStringSync(pid.toString());
92
+ }
93
+
94
+ void _writeConfigFile(int pid) {
95
+ configFile.writeAsStringSync(jsonEncode({
96
+ 'pid': pid,
97
+ 'host': host,
98
+ 'port': port,
99
+ 'url': 'http://$host:$port',
100
+ }));
101
+ }
102
+
103
+ Map<String, dynamic>? readConfig() {
104
+ if (!configFile.existsSync()) return null;
105
+ try {
106
+ return jsonDecode(configFile.readAsStringSync()) as Map<String, dynamic>;
107
+ } catch (_) {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ void deletePidFile() {
113
+ if (pidFile.existsSync()) pidFile.deleteSync();
114
+ if (configFile.existsSync()) configFile.deleteSync();
115
+ }
116
+
62
117
  /// Start PB in the background. Returns once PB is listening (or after a
63
118
  /// short timeout waiting for it to come up).
64
119
  Future<PocketBaseProcess> start({
@@ -82,11 +137,22 @@ class PocketBaseProcessManager {
82
137
 
83
138
  final mergedEnv = <String, String>{
84
139
  ...Platform.environment,
85
- if (adminEmail != null) 'PB_ADMIN_EMAIL': adminEmail,
86
- if (adminPassword != null) 'PB_ADMIN_PASSWORD': adminPassword,
87
140
  ...?env,
88
141
  };
89
142
 
143
+ // Create superuser if credentials provided (PB 0.40+ doesn't support
144
+ // PB_ADMIN_EMAIL env var, so we use the CLI directly).
145
+ if (adminEmail != null && adminPassword != null) {
146
+ final result = await Process.run(
147
+ binaryPath.path,
148
+ ['superuser', 'upsert', '--dir=${dataDir.path}', adminEmail, adminPassword],
149
+ environment: mergedEnv,
150
+ );
151
+ if (result.exitCode != 0) {
152
+ // Non-fatal; serve will continue but schema install may fail.
153
+ }
154
+ }
155
+
90
156
  final proc = await Process.start(
91
157
  binaryPath.path,
92
158
  ['serve', '--http=${host}:$port', '--dir=${dataDir.path}'],
@@ -94,8 +160,10 @@ class PocketBaseProcessManager {
94
160
  mode: ProcessStartMode.detached,
95
161
  );
96
162
 
97
- // Wait for the health endpoint to come up (best-effort; fall through
98
- // after readyTimeout either way).
163
+ _writePidFile(proc.pid);
164
+ _writeConfigFile(proc.pid);
165
+
166
+ // Wait for the health endpoint to come up.
99
167
  final deadline = DateTime.now().add(readyTimeout);
100
168
  while (DateTime.now().isBefore(deadline)) {
101
169
  try {
@@ -114,10 +182,45 @@ class PocketBaseProcessManager {
114
182
  return _process!;
115
183
  }
116
184
 
185
+ /// Stop the running process by sending SIGINT, then SIGKILL if needed.
186
+ /// Also cleans up the PID file.
117
187
  Future<void> stop() async {
188
+ final proc = _process;
189
+ if (proc != null) {
190
+ await proc.stop();
191
+ _process = null;
192
+ } else {
193
+ // Try stopping by PID file (for externally started processes).
194
+ final pid = readPid();
195
+ if (pid > 0) {
196
+ try {
197
+ Process.killPid(pid, ProcessSignal.sigint);
198
+ // Give it a moment to shut down gracefully.
199
+ await Future<void>.delayed(const Duration(seconds: 2));
200
+ // Force kill if still alive.
201
+ try {
202
+ final check = await Process.run('kill', ['-0', '$pid']);
203
+ if (check.exitCode == 0) {
204
+ Process.killPid(pid, ProcessSignal.sigkill);
205
+ }
206
+ } catch (_) {}
207
+ } catch (_) {}
208
+ }
209
+ }
210
+ deletePidFile();
211
+ }
212
+
213
+ /// Get combined stdout+stderr log lines from the running process.
214
+ Stream<String> get logStream async* {
118
215
  final proc = _process;
119
216
  if (proc == null) return;
120
- await proc.stop();
121
- _process = null;
217
+ yield* proc.output
218
+ .transform(utf8.decoder)
219
+ .transform(const LineSplitter())
220
+ .map((line) => '[stdout] $line');
221
+ yield* proc.errors
222
+ .transform(utf8.decoder)
223
+ .transform(const LineSplitter())
224
+ .map((line) => '[stderr] $line');
122
225
  }
123
226
  }
@@ -1,19 +1,22 @@
1
1
  /// Installs the flutter_ota_kit PocketBase schema (collections, indexes,
2
- /// default admin) on a running PB instance.
2
+ /// default superuser) on a running PB instance.
3
3
  ///
4
4
  /// PocketBase is initialized with:
5
5
  /// - `bundles` collection (the OTA bundle metadata)
6
6
  /// - `channels` collection (release channels with a current_bundle pointer)
7
7
  /// - `audit_log` collection (immutable event log)
8
- /// - `admins` is left to PB's built-in bootstrapping
9
- /// (set PB_ADMIN_EMAIL + PB_ADMIN_PASSWORD env vars on first start)
8
+ /// - `app_meta` collection (key-value app metadata)
9
+ /// - `bundles_patches` collection (per-bundle patch metadata)
10
+ /// - `users` is PB's built-in auth collection
11
+ ///
12
+ /// The first superuser is created via `pocketbase superuser upsert`
13
+ /// before starting the server.
10
14
  ///
11
15
  /// Idempotent: re-running the installer is a no-op when collections exist.
12
16
  library;
13
17
 
14
18
  import 'dart:convert';
15
19
 
16
- import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
17
20
  import 'package:http/http.dart' as http;
18
21
 
19
22
  const _kBundlesCollection = r'''
@@ -95,6 +98,50 @@ const _kAuditLogCollection = r'''
95
98
  }
96
99
  ''';
97
100
 
101
+ const _kBundlePatchesCollection = r'''
102
+ {
103
+ "name": "bundles_patches",
104
+ "type": "base",
105
+ "listRule": null,
106
+ "viewRule": null,
107
+ "createRule": null,
108
+ "updateRule": null,
109
+ "deleteRule": null,
110
+ "fields": [
111
+ {"name": "bundle_id", "type": "text", "required": true, "options": {"max": 64}},
112
+ {"name": "base_bundle_id", "type": "text", "required": true, "options": {"max": 64}},
113
+ {"name": "base_file_hash", "type": "text", "required": true, "options": {"max": 128}},
114
+ {"name": "patch_file_hash", "type": "text", "required": true, "options": {"max": 128}},
115
+ {"name": "patch_storage_uri", "type": "text", "required": true, "options": {"max": 512}},
116
+ {"name": "order_index", "type": "number", "options": {"min": 0}},
117
+ {"name": "artifact", "type": "file", "options": {"maxSelect": 1, "maxSize": 0, "mimeTypes": []}}
118
+ ],
119
+ "indexes": [
120
+ "CREATE INDEX idx_patches_bundle ON bundles_patches (bundle_id)",
121
+ "CREATE INDEX idx_patches_base ON bundles_patches (base_bundle_id)"
122
+ ]
123
+ }
124
+ ''';
125
+
126
+ const _kMetaCollection = r'''
127
+ {
128
+ "name": "app_meta",
129
+ "type": "base",
130
+ "listRule": null,
131
+ "viewRule": null,
132
+ "createRule": null,
133
+ "updateRule": null,
134
+ "deleteRule": null,
135
+ "fields": [
136
+ {"name": "key", "type": "text", "required": true, "options": {"min": 1, "max": 128}},
137
+ {"name": "value", "type": "json"}
138
+ ],
139
+ "indexes": [
140
+ "CREATE UNIQUE INDEX idx_meta_key ON app_meta (key)"
141
+ ]
142
+ }
143
+ ''';
144
+
98
145
  class _RawAdminClient {
99
146
  _RawAdminClient(this.baseUrl);
100
147
  final String baseUrl;
@@ -102,7 +149,7 @@ class _RawAdminClient {
102
149
 
103
150
  Future<String> authenticate(String email, String password) async {
104
151
  final res = await http.post(
105
- Uri.parse('$baseUrl/api/admins/auth-with-password'),
152
+ Uri.parse('$baseUrl/api/collections/_superusers/auth-with-password'),
106
153
  headers: {'content-type': 'application/json'},
107
154
  body: jsonEncode({'identity': email, 'password': password}),
108
155
  );
@@ -174,7 +221,6 @@ class PocketBaseSchemaInstaller {
174
221
  required this.url,
175
222
  required this.adminEmail,
176
223
  required this.adminPassword,
177
- http.Client? httpClient,
178
224
  this.bundlesCollection = 'bundles',
179
225
  this.channelsCollection = 'channels',
180
226
  this.auditLogCollection = 'audit_log',
@@ -209,6 +255,10 @@ class PocketBaseSchemaInstaller {
209
255
  String rename(String json) {
210
256
  if (bundlesCollection != 'bundles') {
211
257
  json = json.replaceAll('"bundles"', '"$bundlesCollection"');
258
+ json = json.replaceAll(
259
+ 'bundles_patches',
260
+ '${bundlesCollection}_patches',
261
+ );
212
262
  }
213
263
  if (channelsCollection != 'channels') {
214
264
  json = json.replaceAll('"channels"', '"$channelsCollection"');
@@ -219,10 +269,13 @@ class PocketBaseSchemaInstaller {
219
269
  return json;
220
270
  }
221
271
 
272
+ final patchesCollectionName = '${bundlesCollection}_patches';
222
273
  return [
223
274
  _CollectionSpec(bundlesCollection, rename(_kBundlesCollection)),
224
275
  _CollectionSpec(channelsCollection, rename(_kChannelsCollection)),
225
276
  _CollectionSpec(auditLogCollection, rename(_kAuditLogCollection)),
277
+ _CollectionSpec(patchesCollectionName, rename(_kBundlePatchesCollection)),
278
+ _CollectionSpec('app_meta', _kMetaCollection),
226
279
  ];
227
280
  }
228
281
  }
@@ -292,7 +292,7 @@ class Steps {
292
292
  _echo(' ${dim('·')} $label');
293
293
  }
294
294
 
295
- /// Print boxed summary: "N applied · N skipped · N errors · TIME"
295
+ /// Print summary. Always inline no box for a single line of text.
296
296
  void summary() {
297
297
  _sw.stop();
298
298
  final ms = _sw.elapsedMilliseconds;
@@ -307,7 +307,7 @@ class Steps {
307
307
  }
308
308
  if (parts.isEmpty) parts.add('no steps');
309
309
  parts.add(time);
310
- box('summary', [parts.join(' · ')]);
310
+ _echo(' ${dim(parts.join(' · '))}');
311
311
  }
312
312
 
313
313
  /// Run a step that shows a spinner while working, then marks it
@@ -6,6 +6,7 @@
6
6
  library;
7
7
 
8
8
  import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
9
+ import 'package:http/http.dart' as http;
9
10
 
10
11
  class MockPocketBaseClient implements PocketBaseClient {
11
12
  MockPocketBaseClient(this.store);
@@ -182,6 +183,154 @@ class MockPocketBaseClient implements PocketBaseClient {
182
183
  void close() {
183
184
  _closed = true;
184
185
  }
186
+
187
+ @override
188
+ Future<Map<String, dynamic>> healthDetailed() async => {
189
+ 'code': 200,
190
+ 'message': 'API is healthy.',
191
+ 'data': {'canBackup': false},
192
+ };
193
+
194
+ @override
195
+ Future<List<PocketBaseBackup>> listBackups() async => [];
196
+
197
+ @override
198
+ Future<void> createBackup({String? name}) async {}
199
+
200
+ @override
201
+ Future<void> deleteBackup(String key) async {}
202
+
203
+ @override
204
+ Future<void> restoreBackup(String key) async {}
205
+
206
+ @override
207
+ String backupDownloadUrl(String key, String token) => '';
208
+
209
+ @override
210
+ Future<List<Map<String, dynamic>>> listAdmins() async => [];
211
+
212
+ @override
213
+ Future<Map<String, dynamic>> createAdmin({
214
+ required String email,
215
+ required String password,
216
+ required String passwordConfirm,
217
+ }) async => {'id': 'mock_admin', 'email': email};
218
+
219
+ @override
220
+ Future<Map<String, dynamic>> updateAdmin(
221
+ String id,
222
+ Map<String, dynamic> data,
223
+ ) async => data;
224
+
225
+ @override
226
+ Future<void> deleteAdmin(String id) async {}
227
+
228
+ @override
229
+ Future<List<Map<String, dynamic>>> exportCollection(String collection) async =>
230
+ [];
231
+
232
+ @override
233
+ Future<void> importCollection(
234
+ String collection,
235
+ List<Map<String, dynamic>> records,
236
+ ) async {}
237
+
238
+ @override
239
+ Future<http.Response> rawRequest(
240
+ String method,
241
+ String path, {
242
+ Map<String, dynamic>? body,
243
+ }) async => http.Response('{}', 200);
244
+
245
+ @override
246
+ Future<List<int>> downloadFileUnauth(String url) async => [];
247
+
248
+ @override
249
+ Future<Map<String, dynamic>> listLogs({
250
+ String? filter, String? sort, int page = 1, int perPage = 30,
251
+ }) async => {'items': [], 'totalItems': 0};
252
+
253
+ @override
254
+ Future<Map<String, dynamic>> getLog(String id) async => {};
255
+
256
+ @override
257
+ Future<List<Map<String, dynamic>>> getLogStats({String? filter}) async => [];
258
+
259
+ @override
260
+ Future<void> truncateLogs() async {}
261
+
262
+ @override
263
+ Future<List<Map<String, dynamic>>> listCollections() async => [];
264
+
265
+ @override
266
+ Future<Map<String, dynamic>> getCollection(String nameOrId) async => {};
267
+
268
+ @override
269
+ Future<Map<String, dynamic>> createCollection(
270
+ Map<String, dynamic> body,
271
+ ) async => body;
272
+
273
+ @override
274
+ Future<Map<String, dynamic>> updateCollection(
275
+ String nameOrId,
276
+ Map<String, dynamic> body,
277
+ ) async => body;
278
+
279
+ @override
280
+ Future<void> deleteCollection(String nameOrId) async {}
281
+
282
+ @override
283
+ Future<void> truncateCollection(String nameOrId) async {}
284
+
285
+ @override
286
+ Future<void> importCollections(
287
+ List<Map<String, dynamic>> collections, {
288
+ bool deleteMissing = false,
289
+ }) async {}
290
+
291
+ @override
292
+ Future<void> uploadBackup(List<int> bytes, {String? name}) async {}
293
+
294
+ @override
295
+ Future<List<Map<String, dynamic>>> batch(
296
+ List<Map<String, dynamic>> requests,
297
+ ) async => [];
298
+
299
+ @override
300
+ Future<Map<String, dynamic>> listSettings() async => {};
301
+
302
+ @override
303
+ Future<Map<String, dynamic>> updateSettings(
304
+ Map<String, dynamic> body,
305
+ ) async => body;
306
+
307
+ @override
308
+ Future<void> testS3(String filesystem) async {}
309
+
310
+ @override
311
+ Future<void> testEmail({
312
+ required String email,
313
+ required String template,
314
+ String? collection,
315
+ }) async {}
316
+
317
+ @override
318
+ Future<Map<String, dynamic>> runSql(String query) async => {};
319
+
320
+ @override
321
+ Future<List<Map<String, dynamic>>> listCrons() async => [];
322
+
323
+ @override
324
+ Future<void> runCron(String jobId) async {}
325
+
326
+ @override
327
+ Future<Map<String, dynamic>> getCollectionScaffolds() async => {};
328
+
329
+ @override
330
+ Future<List<Map<String, dynamic>>> listOAuth2Providers() async => [];
331
+
332
+ @override
333
+ Future<Map<String, dynamic>> dryRunViewQuery(String query) async => {};
185
334
  }
186
335
 
187
336
  /// In-memory store backing [MockPocketBaseClient].