@_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.
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +4 -3
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +3 -2
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +22 -10
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +16 -5
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +3 -0
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +7 -8
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +12 -3
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +129 -6
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +2 -1
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +23 -14
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +4 -0
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +266 -26
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +1422 -16
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +4 -1
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +10 -9
- package/dart-src/packages/cli-tools/lib/src/config.dart +14 -19
- package/dart-src/packages/cli-tools/lib/src/operations.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/installer.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/pocketbase/process_manager.dart +113 -10
- package/dart-src/packages/cli-tools/lib/src/pocketbase/schema_installer.dart +59 -6
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +2 -2
- package/dart-src/packages/cli-tools/test/mocks/mock_pocketbase_client.dart +149 -0
- package/dart-src/packages/cli-tools/test/pocketbase_schema_test.dart +14 -9
- package/dart-src/packages/core/CHANGELOG.md +6 -1
- package/dart-src/packages/core/lib/flutter_ota_kit_core.dart +2 -0
- package/dart-src/packages/core/lib/src/app_update_info.dart +73 -1
- package/dart-src/packages/core/lib/src/bundle.dart +62 -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/plugins/aws/CHANGELOG.md +5 -1
- package/dart-src/plugins/aws/lib/src/aws_cloudfront_client.dart +2 -2
- package/dart-src/plugins/cloudflare/CHANGELOG.md +5 -1
- package/dart-src/plugins/plugin-core/CHANGELOG.md +5 -1
- 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/pocketbase/CHANGELOG.md +5 -1
- package/dart-src/plugins/pocketbase/lib/flutter_ota_kit_pocketbase.dart +2 -1
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_bundle_mapper.dart +10 -2
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_client.dart +617 -4
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_database.dart +262 -42
- package/dart-src/plugins/pocketbase/lib/src/pocketbase_storage.dart +9 -2
- package/dart-src/plugins/postgres/CHANGELOG.md +5 -1
- package/dart-src/plugins/supabase/CHANGELOG.md +6 -1
- package/dart-src/plugins/supabase/lib/src/supabase_database.dart +4 -0
- package/package.json +2 -2
- package/bin/flutter-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +0 -36
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -458
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +0 -707
- package/dart-src/packages/cli-tools/pubspec.lock +0 -590
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
library;
|
|
3
3
|
|
|
4
4
|
import 'dart:async';
|
|
5
|
+
import 'dart:convert';
|
|
5
6
|
import 'dart:io';
|
|
6
7
|
|
|
8
|
+
import 'package:args/args.dart';
|
|
9
|
+
import 'package:flutter_ota_kit_pocketbase/flutter_ota_kit_pocketbase.dart';
|
|
7
10
|
import 'package:path/path.dart' as p;
|
|
8
11
|
|
|
9
12
|
import '../cli_base.dart';
|
|
13
|
+
import '../pack.dart';
|
|
10
14
|
import '../pocketbase/data_bootstrap.dart';
|
|
11
15
|
import '../pocketbase/installer.dart';
|
|
12
16
|
import '../pocketbase/process_manager.dart';
|
|
@@ -14,11 +18,25 @@ import '../pocketbase/schema_installer.dart';
|
|
|
14
18
|
import '../ui/ui.dart';
|
|
15
19
|
|
|
16
20
|
class PocketBaseCommand extends FlutterPatcherCommand {
|
|
17
|
-
|
|
21
|
+
PocketBaseCommand() {
|
|
18
22
|
addSubcommand(PocketBaseInstallCommand());
|
|
19
23
|
addSubcommand(PocketBaseServeCommand());
|
|
20
24
|
addSubcommand(PocketBaseStopCommand());
|
|
21
25
|
addSubcommand(PocketBaseStatusCommand());
|
|
26
|
+
addSubcommand(PocketBaseBackupCommand());
|
|
27
|
+
addSubcommand(PocketBaseExportCommand());
|
|
28
|
+
addSubcommand(PocketBaseImportCommand());
|
|
29
|
+
addSubcommand(PocketBaseAdminCommand());
|
|
30
|
+
addSubcommand(PocketBaseMigrateCommand());
|
|
31
|
+
addSubcommand(PocketBaseHealthCommand());
|
|
32
|
+
addSubcommand(PocketBaseLogsCommand());
|
|
33
|
+
addSubcommand(PocketBaseRecordsCommand());
|
|
34
|
+
addSubcommand(PocketBaseCollectionsCommand());
|
|
35
|
+
addSubcommand(PocketBaseSettingsCommand());
|
|
36
|
+
addSubcommand(PocketBaseSqlCommand());
|
|
37
|
+
addSubcommand(PocketBaseCronsCommand());
|
|
38
|
+
addSubcommand(PocketBaseQueryCommand());
|
|
39
|
+
addSubcommand(PocketBaseConfigCommand());
|
|
22
40
|
}
|
|
23
41
|
|
|
24
42
|
@override
|
|
@@ -26,7 +44,9 @@ class PocketBaseCommand extends FlutterPatcherCommand {
|
|
|
26
44
|
|
|
27
45
|
@override
|
|
28
46
|
String get description =>
|
|
29
|
-
'Manage a local PocketBase instance (install / serve / stop /
|
|
47
|
+
'Manage a local PocketBase instance (install / serve / stop / '
|
|
48
|
+
'status / backup / export / import / admin / migrate / health / '
|
|
49
|
+
'logs / records / collections / settings / sql / crons / query / config).';
|
|
30
50
|
}
|
|
31
51
|
|
|
32
52
|
class PocketBaseInstallCommand extends FlutterPatcherCommand {
|
|
@@ -136,6 +156,18 @@ class PocketBaseServeCommand extends FlutterPatcherCommand {
|
|
|
136
156
|
await manager.start(adminEmail: adminEmail, adminPassword: adminPassword);
|
|
137
157
|
step('PocketBase is running (pid ${manager.process!.pid}).');
|
|
138
158
|
|
|
159
|
+
// Wait for PB to be ready.
|
|
160
|
+
for (var i = 0; i < 20; i++) {
|
|
161
|
+
try {
|
|
162
|
+
final hc = PocketBaseClient('http://$host:$port');
|
|
163
|
+
final ok = await hc.health();
|
|
164
|
+
hc.close();
|
|
165
|
+
if (ok) break;
|
|
166
|
+
} catch (_) {
|
|
167
|
+
await Future<void>.delayed(const Duration(milliseconds: 500));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
139
171
|
if (adminEmail != null && adminPassword != null) {
|
|
140
172
|
step('Installing flutter-ota schema...');
|
|
141
173
|
final schemaInstaller = PocketBaseSchemaInstaller(
|
|
@@ -193,42 +225,1416 @@ class PocketBaseServeCommand extends FlutterPatcherCommand {
|
|
|
193
225
|
}
|
|
194
226
|
|
|
195
227
|
class PocketBaseStopCommand extends FlutterPatcherCommand {
|
|
228
|
+
PocketBaseStopCommand() {
|
|
229
|
+
argParser.addOption('port', help: 'HTTP port.', defaultsTo: '8090');
|
|
230
|
+
argParser.addOption('host', help: 'Bind address.', defaultsTo: '127.0.0.1');
|
|
231
|
+
}
|
|
232
|
+
|
|
196
233
|
@override
|
|
197
234
|
String get name => 'stop';
|
|
198
235
|
|
|
199
236
|
@override
|
|
200
|
-
String get description =>
|
|
201
|
-
'Hint to stop a running PocketBase (Ctrl+C in serve).';
|
|
237
|
+
String get description => 'Stop a running PocketBase instance.';
|
|
202
238
|
|
|
203
239
|
@override
|
|
204
240
|
Future<int> run() => runGuarded(() async {
|
|
205
241
|
banner('pocketbase · stop');
|
|
206
|
-
|
|
207
|
-
|
|
242
|
+
final port = int.tryParse(argResults!['port'] as String? ?? '8090') ?? 8090;
|
|
243
|
+
final host = argResults!['host'] as String? ?? '127.0.0.1';
|
|
244
|
+
final mgr = PocketBaseProcessManager(
|
|
245
|
+
binaryPath: PocketBaseInstallPaths.resolve().binaryPath,
|
|
246
|
+
dataDir: PocketBaseInstallPaths.resolve().installDir,
|
|
247
|
+
port: port,
|
|
248
|
+
host: host,
|
|
249
|
+
);
|
|
250
|
+
final pid = mgr.readPid();
|
|
251
|
+
if (pid <= 0) {
|
|
252
|
+
step('No PocketBase process found.');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
step('Stopping PocketBase (PID $pid)...');
|
|
256
|
+
await mgr.stop();
|
|
257
|
+
step('PocketBase stopped.');
|
|
208
258
|
});
|
|
209
259
|
}
|
|
210
260
|
|
|
211
261
|
class PocketBaseStatusCommand extends FlutterPatcherCommand {
|
|
262
|
+
PocketBaseStatusCommand() {
|
|
263
|
+
argParser.addOption('port', help: 'HTTP port.', defaultsTo: '8090');
|
|
264
|
+
argParser.addOption('host', help: 'Bind address.', defaultsTo: '127.0.0.1');
|
|
265
|
+
}
|
|
266
|
+
|
|
212
267
|
@override
|
|
213
268
|
String get name => 'status';
|
|
214
269
|
|
|
215
270
|
@override
|
|
216
|
-
String get description => 'Show
|
|
271
|
+
String get description => 'Show installed version, PID, and health.';
|
|
217
272
|
|
|
218
273
|
@override
|
|
219
274
|
Future<int> run() => runGuarded(() async {
|
|
220
275
|
banner('pocketbase · status');
|
|
276
|
+
final r = argResults!;
|
|
277
|
+
final port = int.tryParse(r['port'] as String? ?? '8090') ?? 8090;
|
|
278
|
+
final host = r['host'] as String? ?? '127.0.0.1';
|
|
279
|
+
|
|
221
280
|
final paths = PocketBaseInstallPaths.resolve();
|
|
222
281
|
final exists = await paths.binaryPath.exists();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
282
|
+
final manager = PocketBaseProcessManager(
|
|
283
|
+
binaryPath: paths.binaryPath,
|
|
284
|
+
dataDir: paths.installDir,
|
|
285
|
+
port: port,
|
|
286
|
+
host: host,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
final pid = manager.readPid();
|
|
290
|
+
final pidAlive = pid > 0 ? await manager.isRunningByPidFile() : false;
|
|
291
|
+
|
|
292
|
+
var healthy = false;
|
|
293
|
+
try {
|
|
294
|
+
final client = PocketBaseClient('http://$host:$port');
|
|
295
|
+
healthy = await client.health();
|
|
296
|
+
client.close();
|
|
297
|
+
} catch (_) {}
|
|
298
|
+
|
|
299
|
+
box('status', [
|
|
300
|
+
kv('version', paths.version),
|
|
301
|
+
kv('binary', exists ? green('installed') : red('not installed')),
|
|
302
|
+
kv('pid', pidAlive ? green('$pid') : (pid > 0 ? yellow('$pid (dead)') : dim('-'))),
|
|
303
|
+
kv('health', healthy ? green('healthy') : red('unreachable')),
|
|
304
|
+
kv('url', 'http://$host:$port'),
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// Shared helpers
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
void _addBackendOptions(ArgParser parser) {
|
|
314
|
+
parser.addOption('url', help: 'PocketBase URL (auto-detected if running).');
|
|
315
|
+
parser.addOption('admin-email', help: 'Admin email.');
|
|
316
|
+
parser.addOption('admin-password', help: 'Admin password.');
|
|
317
|
+
parser.addOption('version', help: 'PocketBase version.');
|
|
318
|
+
parser.addOption('data-dir', help: 'PB data directory.');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
String _safeSubstring(String? s, int length) {
|
|
322
|
+
if (s == null || s.isEmpty) return '';
|
|
323
|
+
return s.length > length ? s.substring(0, length) : s;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
(String url, String adminEmail, String adminPassword) _resolveBackend(ArgResults r) {
|
|
327
|
+
final url = r['url'] as String? ?? _detectRunningUrl(r);
|
|
328
|
+
if (url == null || url.isEmpty) {
|
|
329
|
+
throw const PackException(
|
|
330
|
+
'No PocketBase URL. Run `flutter-ota pocketbase serve` or pass --url.',
|
|
331
|
+
64,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
final admin = r['admin-email'] as String? ??
|
|
335
|
+
Platform.environment['POCKETBASE_ADMIN_EMAIL'] ??
|
|
336
|
+
'';
|
|
337
|
+
final pass = r['admin-password'] as String? ??
|
|
338
|
+
Platform.environment['POCKETBASE_ADMIN_PASSWORD'] ??
|
|
339
|
+
'';
|
|
340
|
+
if (admin.isEmpty || pass.isEmpty) {
|
|
341
|
+
throw const PackException(
|
|
342
|
+
'Admin credentials required. Use --admin-email/--admin-password '
|
|
343
|
+
'or set POCKETBASE_ADMIN_EMAIL/POCKETBASE_ADMIN_PASSWORD env vars.',
|
|
344
|
+
64,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return (url, admin, pass);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
Future<PocketBaseClient> _getClient(ArgResults r) async {
|
|
351
|
+
final (url, admin, pass) = _resolveBackend(r);
|
|
352
|
+
final client = PocketBaseClient(url);
|
|
353
|
+
await client.authenticate(admin, pass);
|
|
354
|
+
return client;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
String? _detectRunningUrl(ArgResults r) {
|
|
358
|
+
final version = r['version'] as String? ?? kDefaultPocketBaseVersion;
|
|
359
|
+
final dataDirOverride = r['data-dir'] as String?;
|
|
360
|
+
final paths = PocketBaseInstallPaths.resolve(version: version);
|
|
361
|
+
final dataDir = dataDirOverride != null
|
|
362
|
+
? Directory(dataDirOverride)
|
|
363
|
+
: paths.installDir;
|
|
364
|
+
final config = PocketBaseProcessManager(
|
|
365
|
+
binaryPath: paths.binaryPath,
|
|
366
|
+
dataDir: dataDir,
|
|
367
|
+
).readConfig();
|
|
368
|
+
return config?['url'] as String?;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
Future<T> _pbStep<T>(
|
|
372
|
+
String label,
|
|
373
|
+
Future<T> Function(PocketBaseClient client) fn,
|
|
374
|
+
ArgResults r,
|
|
375
|
+
) async {
|
|
376
|
+
final steps = Steps('pocketbase');
|
|
377
|
+
try {
|
|
378
|
+
final client = await _getClient(r);
|
|
379
|
+
try {
|
|
380
|
+
return await steps.run(label, () => fn(client));
|
|
381
|
+
} finally {
|
|
382
|
+
client.close();
|
|
231
383
|
}
|
|
232
|
-
|
|
384
|
+
} on PocketBaseException catch (e) {
|
|
385
|
+
final match = RegExp(r'"message":"([^"]+)"').firstMatch(e.message);
|
|
386
|
+
final detail = match?.group(1) ?? e.message;
|
|
387
|
+
throw PackException('$label: $detail', 1);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// backup
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
class PocketBaseBackupCommand extends FlutterPatcherCommand {
|
|
396
|
+
PocketBaseBackupCommand() {
|
|
397
|
+
addSubcommand(PocketBaseBackupCreateCommand());
|
|
398
|
+
addSubcommand(PocketBaseBackupListCommand());
|
|
399
|
+
addSubcommand(PocketBaseBackupDeleteCommand());
|
|
400
|
+
addSubcommand(PocketBaseBackupRestoreCommand());
|
|
401
|
+
addSubcommand(PocketBaseBackupDownloadCommand());
|
|
402
|
+
addSubcommand(PocketBaseBackupUploadCommand());
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@override
|
|
406
|
+
String get name => 'backup';
|
|
407
|
+
|
|
408
|
+
@override
|
|
409
|
+
String get description => 'Manage PocketBase backups.';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
class PocketBaseBackupCreateCommand extends FlutterPatcherCommand {
|
|
413
|
+
PocketBaseBackupCreateCommand() {
|
|
414
|
+
_addBackendOptions(argParser);
|
|
415
|
+
argParser.addOption('name', help: 'Backup file name.');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@override
|
|
419
|
+
String get name => 'create';
|
|
420
|
+
|
|
421
|
+
@override
|
|
422
|
+
String get description => 'Create a new backup.';
|
|
423
|
+
|
|
424
|
+
@override
|
|
425
|
+
Future<int> run() => runGuarded(() async {
|
|
426
|
+
banner('pocketbase · backup · create');
|
|
427
|
+
await _pbStep(
|
|
428
|
+
'Creating backup',
|
|
429
|
+
(c) => c.createBackup(name: argResults!['name'] as String?),
|
|
430
|
+
argResults!,
|
|
431
|
+
);
|
|
432
|
+
step(green('Backup created'));
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
class PocketBaseBackupListCommand extends FlutterPatcherCommand {
|
|
437
|
+
PocketBaseBackupListCommand() {
|
|
438
|
+
_addBackendOptions(argParser);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
@override
|
|
442
|
+
String get name => 'list';
|
|
443
|
+
|
|
444
|
+
@override
|
|
445
|
+
String get description => 'List available backups.';
|
|
446
|
+
|
|
447
|
+
@override
|
|
448
|
+
Future<int> run() => runGuarded(() async {
|
|
449
|
+
banner('pocketbase · backup · list');
|
|
450
|
+
final backups = await _pbStep(
|
|
451
|
+
'Listing backups',
|
|
452
|
+
(c) => c.listBackups(),
|
|
453
|
+
argResults!,
|
|
454
|
+
);
|
|
455
|
+
if (backups.isEmpty) {
|
|
456
|
+
warn('No backups found.');
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
final rows = backups
|
|
460
|
+
.map((b) => [cyan(b.key), b.sizeFormatted, b.modified])
|
|
461
|
+
.toList();
|
|
462
|
+
table('${backups.length} backups', ['KEY', 'SIZE', 'MODIFIED'], rows);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
class PocketBaseBackupDeleteCommand extends FlutterPatcherCommand {
|
|
467
|
+
PocketBaseBackupDeleteCommand() {
|
|
468
|
+
_addBackendOptions(argParser);
|
|
469
|
+
argParser.addOption('name', help: 'Backup key to delete.', mandatory: true);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
@override
|
|
473
|
+
String get name => 'delete';
|
|
474
|
+
|
|
475
|
+
@override
|
|
476
|
+
String get description => 'Delete a backup.';
|
|
477
|
+
|
|
478
|
+
@override
|
|
479
|
+
Future<int> run() => runGuarded(() async {
|
|
480
|
+
banner('pocketbase · backup · delete');
|
|
481
|
+
final key = argResults!['name'] as String;
|
|
482
|
+
await _pbStep('Deleting $key', (c) => c.deleteBackup(key), argResults!);
|
|
483
|
+
step(green('Deleted backup $key'));
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
class PocketBaseBackupRestoreCommand extends FlutterPatcherCommand {
|
|
488
|
+
PocketBaseBackupRestoreCommand() {
|
|
489
|
+
_addBackendOptions(argParser);
|
|
490
|
+
argParser.addOption('name', help: 'Backup key to restore.', mandatory: true);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
@override
|
|
494
|
+
String get name => 'restore';
|
|
495
|
+
|
|
496
|
+
@override
|
|
497
|
+
String get description => 'Restore a backup (restarts PB).';
|
|
498
|
+
|
|
499
|
+
@override
|
|
500
|
+
Future<int> run() => runGuarded(() async {
|
|
501
|
+
banner('pocketbase · backup · restore');
|
|
502
|
+
final key = argResults!['name'] as String;
|
|
503
|
+
await _pbStep('Restoring $key', (c) => c.restoreBackup(key), argResults!);
|
|
504
|
+
warn('PB will restart. Reconnect in a few seconds.');
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
class PocketBaseBackupDownloadCommand extends FlutterPatcherCommand {
|
|
509
|
+
PocketBaseBackupDownloadCommand() {
|
|
510
|
+
_addBackendOptions(argParser);
|
|
511
|
+
argParser.addOption('name', help: 'Backup key to download.', mandatory: true);
|
|
512
|
+
argParser.addOption('output', help: 'Output directory.', defaultsTo: '.');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
@override
|
|
516
|
+
String get name => 'download';
|
|
517
|
+
|
|
518
|
+
@override
|
|
519
|
+
String get description => 'Download a backup file.';
|
|
520
|
+
|
|
521
|
+
@override
|
|
522
|
+
Future<int> run() => runGuarded(() async {
|
|
523
|
+
banner('pocketbase · backup · download');
|
|
524
|
+
final key = argResults!['name'] as String;
|
|
525
|
+
final output = argResults!['output'] as String;
|
|
526
|
+
final (url, admin, pass) = _resolveBackend(argResults!);
|
|
527
|
+
final dlClient = PocketBaseClient(url);
|
|
528
|
+
try {
|
|
529
|
+
await dlClient.authenticate(admin, pass);
|
|
530
|
+
final token = dlClient.authToken;
|
|
531
|
+
final dlUrl = dlClient.backupDownloadUrl(key, token);
|
|
532
|
+
final bytes = await dlClient.downloadFileUnauth(dlUrl);
|
|
533
|
+
final out = File(p.join(output, key));
|
|
534
|
+
await out.parent.create(recursive: true);
|
|
535
|
+
await out.writeAsBytes(bytes);
|
|
536
|
+
step(green('Saved to ${out.path}'));
|
|
537
|
+
} finally {
|
|
538
|
+
dlClient.close();
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
class PocketBaseBackupUploadCommand extends FlutterPatcherCommand {
|
|
544
|
+
PocketBaseBackupUploadCommand() {
|
|
545
|
+
_addBackendOptions(argParser);
|
|
546
|
+
argParser.addOption('file', help: 'Backup zip to upload.', mandatory: true);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
@override
|
|
550
|
+
String get name => 'upload';
|
|
551
|
+
|
|
552
|
+
@override
|
|
553
|
+
String get description => 'Upload a backup zip file.';
|
|
554
|
+
|
|
555
|
+
@override
|
|
556
|
+
Future<int> run() => runGuarded(() async {
|
|
557
|
+
banner('pocketbase · backup · upload');
|
|
558
|
+
final fp = argResults!['file'] as String;
|
|
559
|
+
final f = File(fp);
|
|
560
|
+
if (!await f.exists()) throw PackException('File not found: $fp', 64);
|
|
561
|
+
final bytes = await f.readAsBytes();
|
|
562
|
+
await _pbStep(
|
|
563
|
+
'Uploading ${p.basename(fp)}',
|
|
564
|
+
(c) => c.uploadBackup(bytes, name: p.basename(fp)),
|
|
565
|
+
argResults!,
|
|
566
|
+
);
|
|
567
|
+
step(green('Uploaded ${p.basename(fp)}'));
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ---------------------------------------------------------------------------
|
|
572
|
+
// export / import
|
|
573
|
+
// ---------------------------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
class PocketBaseExportCommand extends FlutterPatcherCommand {
|
|
576
|
+
PocketBaseExportCommand() {
|
|
577
|
+
_addBackendOptions(argParser);
|
|
578
|
+
argParser.addOption('collection', help: 'Collection to export.');
|
|
579
|
+
argParser.addOption('output', help: 'Output directory.', defaultsTo: '.');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
@override
|
|
583
|
+
String get name => 'export';
|
|
584
|
+
|
|
585
|
+
@override
|
|
586
|
+
String get description =>
|
|
587
|
+
'Export collection(s) as JSON. Exports all if --collection omitted.';
|
|
588
|
+
|
|
589
|
+
@override
|
|
590
|
+
Future<int> run() => runGuarded(() async {
|
|
591
|
+
banner('pocketbase · export');
|
|
592
|
+
final r = argResults!;
|
|
593
|
+
final collection = r['collection'] as String?;
|
|
594
|
+
final output = r['output'] as String;
|
|
595
|
+
|
|
596
|
+
final (url, admin, pass) = _resolveBackend(r);
|
|
597
|
+
final client = PocketBaseClient(url);
|
|
598
|
+
try {
|
|
599
|
+
await client.authenticate(admin, pass);
|
|
600
|
+
|
|
601
|
+
final collections = <String>[];
|
|
602
|
+
if (collection != null && collection.isNotEmpty) {
|
|
603
|
+
collections.add(collection);
|
|
604
|
+
} else {
|
|
605
|
+
final all = await client.listCollections();
|
|
606
|
+
for (final c in all) {
|
|
607
|
+
final name = c['name'] as String? ?? '';
|
|
608
|
+
if (name.isNotEmpty && !name.startsWith('_') && c['system'] != true) {
|
|
609
|
+
collections.add(name);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (collections.isEmpty) {
|
|
613
|
+
collections.addAll(['bundles', 'channels', 'audit_log', 'bundles_patches']);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
for (final c in collections) {
|
|
618
|
+
try {
|
|
619
|
+
final records = await client.exportCollection(c);
|
|
620
|
+
final outFile = File(p.join(output, '$c.json'));
|
|
621
|
+
await outFile.parent.create(recursive: true);
|
|
622
|
+
await outFile.writeAsString(
|
|
623
|
+
const JsonEncoder.withIndent(' ').convert(records),
|
|
624
|
+
);
|
|
625
|
+
step('${records.length} records → ${outFile.path}');
|
|
626
|
+
} catch (e) {
|
|
627
|
+
warn('Failed to export $c: $e');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
} finally {
|
|
631
|
+
client.close();
|
|
632
|
+
}
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
class PocketBaseImportCommand extends FlutterPatcherCommand {
|
|
637
|
+
PocketBaseImportCommand() {
|
|
638
|
+
_addBackendOptions(argParser);
|
|
639
|
+
argParser.addOption('file', help: 'JSON file to import.', mandatory: true);
|
|
640
|
+
argParser.addOption('collection', help: 'Target collection name.');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
@override
|
|
644
|
+
String get name => 'import';
|
|
645
|
+
|
|
646
|
+
@override
|
|
647
|
+
String get description => 'Import records from a JSON file into a collection.';
|
|
648
|
+
|
|
649
|
+
@override
|
|
650
|
+
Future<int> run() => runGuarded(() async {
|
|
651
|
+
banner('pocketbase · import');
|
|
652
|
+
final r = argResults!;
|
|
653
|
+
final fp = r['file'] as String;
|
|
654
|
+
final f = File(fp);
|
|
655
|
+
if (!await f.exists()) throw PackException('File not found: $fp', 64);
|
|
656
|
+
final json = jsonDecode(await f.readAsString()) as List<dynamic>;
|
|
657
|
+
final records = json.cast<Map<String, dynamic>>();
|
|
658
|
+
final target = r['collection'] as String? ?? p.basenameWithoutExtension(fp);
|
|
659
|
+
await _pbStep(
|
|
660
|
+
'Importing ${records.length} records into $target',
|
|
661
|
+
(c) => c.importCollection(target, records),
|
|
662
|
+
r,
|
|
663
|
+
);
|
|
664
|
+
step(green('${records.length} records imported into $target'));
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
// admin
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
|
|
672
|
+
class PocketBaseAdminCommand extends FlutterPatcherCommand {
|
|
673
|
+
PocketBaseAdminCommand() {
|
|
674
|
+
addSubcommand(PocketBaseAdminCreateCommand());
|
|
675
|
+
addSubcommand(PocketBaseAdminListCommand());
|
|
676
|
+
addSubcommand(PocketBaseAdminUpdateCommand());
|
|
677
|
+
addSubcommand(PocketBaseAdminDeleteCommand());
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
@override
|
|
681
|
+
String get name => 'admin';
|
|
682
|
+
|
|
683
|
+
@override
|
|
684
|
+
String get description => 'Manage PocketBase admin accounts.';
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
class PocketBaseAdminCreateCommand extends FlutterPatcherCommand {
|
|
688
|
+
PocketBaseAdminCreateCommand() {
|
|
689
|
+
_addBackendOptions(argParser);
|
|
690
|
+
argParser.addOption('email', help: 'Admin email.', mandatory: true);
|
|
691
|
+
argParser.addOption('password', help: 'Admin password.', mandatory: true);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
@override
|
|
695
|
+
String get name => 'create';
|
|
696
|
+
|
|
697
|
+
@override
|
|
698
|
+
String get description => 'Create a new admin account.';
|
|
699
|
+
|
|
700
|
+
@override
|
|
701
|
+
Future<int> run() => runGuarded(() async {
|
|
702
|
+
banner('pocketbase · admin · create');
|
|
703
|
+
final r = argResults!;
|
|
704
|
+
final email = r['email'] as String;
|
|
705
|
+
final password = r['password'] as String;
|
|
706
|
+
final result = await _pbStep(
|
|
707
|
+
'Creating admin $email',
|
|
708
|
+
(c) => c.createAdmin(email: email, password: password, passwordConfirm: password),
|
|
709
|
+
r,
|
|
710
|
+
);
|
|
711
|
+
stdout.writeln(' ${dim('→')} admin ${cyan(result['id'] ?? '?')} $email');
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
class PocketBaseAdminListCommand extends FlutterPatcherCommand {
|
|
716
|
+
PocketBaseAdminListCommand() {
|
|
717
|
+
_addBackendOptions(argParser);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
@override
|
|
721
|
+
String get name => 'list';
|
|
722
|
+
|
|
723
|
+
@override
|
|
724
|
+
String get description => 'List admin accounts.';
|
|
725
|
+
|
|
726
|
+
@override
|
|
727
|
+
Future<int> run() => runGuarded(() async {
|
|
728
|
+
banner('pocketbase · admin · list');
|
|
729
|
+
final admins = await _pbStep('Listing admins', (c) => c.listAdmins(), argResults!);
|
|
730
|
+
if (admins.isEmpty) {
|
|
731
|
+
warn('No admins found.');
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
final rows = admins
|
|
735
|
+
.map((a) => [cyan(a['id']?.toString() ?? '?'), a['email']?.toString() ?? '?'])
|
|
736
|
+
.toList();
|
|
737
|
+
table('${admins.length} admins', ['ID', 'EMAIL'], rows);
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
class PocketBaseAdminUpdateCommand extends FlutterPatcherCommand {
|
|
742
|
+
PocketBaseAdminUpdateCommand() {
|
|
743
|
+
_addBackendOptions(argParser);
|
|
744
|
+
argParser.addOption('id', help: 'Admin ID to update.', mandatory: true);
|
|
745
|
+
argParser.addOption('body', help: 'JSON fields to update.', mandatory: true);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
@override
|
|
749
|
+
String get name => 'update';
|
|
750
|
+
|
|
751
|
+
@override
|
|
752
|
+
String get description => 'Update an admin account.';
|
|
753
|
+
|
|
754
|
+
@override
|
|
755
|
+
Future<int> run() => runGuarded(() async {
|
|
756
|
+
banner('pocketbase · admin · update');
|
|
757
|
+
final r = argResults!;
|
|
758
|
+
final id = r['id'] as String;
|
|
759
|
+
final body = jsonDecode(r['body'] as String) as Map<String, dynamic>;
|
|
760
|
+
await _pbStep('Updating admin $id', (c) => c.updateAdmin(id, body), r);
|
|
761
|
+
step(green('Updated admin $id'));
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
class PocketBaseAdminDeleteCommand extends FlutterPatcherCommand {
|
|
766
|
+
PocketBaseAdminDeleteCommand() {
|
|
767
|
+
_addBackendOptions(argParser);
|
|
768
|
+
argParser.addOption('id', help: 'Admin ID to delete.', mandatory: true);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
@override
|
|
772
|
+
String get name => 'delete';
|
|
773
|
+
|
|
774
|
+
@override
|
|
775
|
+
String get description => 'Delete an admin account.';
|
|
776
|
+
|
|
777
|
+
@override
|
|
778
|
+
Future<int> run() => runGuarded(() async {
|
|
779
|
+
banner('pocketbase · admin · delete');
|
|
780
|
+
final id = argResults!['id'] as String;
|
|
781
|
+
await _pbStep('Deleting admin $id', (c) => c.deleteAdmin(id), argResults!);
|
|
782
|
+
step(green('Deleted admin $id'));
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ---------------------------------------------------------------------------
|
|
787
|
+
// migrate
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
|
|
790
|
+
class PocketBaseMigrateCommand extends FlutterPatcherCommand {
|
|
791
|
+
PocketBaseMigrateCommand() {
|
|
792
|
+
_addBackendOptions(argParser);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
@override
|
|
796
|
+
String get name => 'migrate';
|
|
797
|
+
|
|
798
|
+
@override
|
|
799
|
+
String get description => 'Re-run schema migration on a running PocketBase.';
|
|
800
|
+
|
|
801
|
+
@override
|
|
802
|
+
Future<int> run() => runGuarded(() async {
|
|
803
|
+
banner('pocketbase · migrate');
|
|
804
|
+
final (url, admin, pass) = _resolveBackend(argResults!);
|
|
805
|
+
final installer = PocketBaseSchemaInstaller(
|
|
806
|
+
url: url,
|
|
807
|
+
adminEmail: admin,
|
|
808
|
+
adminPassword: pass,
|
|
809
|
+
);
|
|
810
|
+
final res = await installer.install();
|
|
811
|
+
if (res.created.isNotEmpty) {
|
|
812
|
+
step(green('Created: ${res.created.join(', ')}'));
|
|
813
|
+
}
|
|
814
|
+
if (res.skipped.isNotEmpty) {
|
|
815
|
+
step('Already present: ${res.skipped.join(', ')}');
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// ---------------------------------------------------------------------------
|
|
821
|
+
// health
|
|
822
|
+
// ---------------------------------------------------------------------------
|
|
823
|
+
|
|
824
|
+
class PocketBaseHealthCommand extends FlutterPatcherCommand {
|
|
825
|
+
PocketBaseHealthCommand() {
|
|
826
|
+
_addBackendOptions(argParser);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
@override
|
|
830
|
+
String get name => 'health';
|
|
831
|
+
|
|
832
|
+
@override
|
|
833
|
+
String get description => 'Check PocketBase health.';
|
|
834
|
+
|
|
835
|
+
@override
|
|
836
|
+
Future<int> run() => runGuarded(() async {
|
|
837
|
+
banner('pocketbase · health');
|
|
838
|
+
final health = await _pbStep('health', (c) => c.healthDetailed(), argResults!);
|
|
839
|
+
final code = health['code'] as int? ?? 0;
|
|
840
|
+
final message = health['message'] as String? ?? '';
|
|
841
|
+
final data = health['data'] as Map<String, dynamic>? ?? {};
|
|
842
|
+
final status = code == 200 ? green('healthy') : red('unhealthy');
|
|
843
|
+
step('status $status');
|
|
844
|
+
step('message $message');
|
|
845
|
+
step('canBackup ${data['canBackup'] ?? false}');
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
// logs
|
|
851
|
+
// ---------------------------------------------------------------------------
|
|
852
|
+
|
|
853
|
+
class PocketBaseLogsCommand extends FlutterPatcherCommand {
|
|
854
|
+
PocketBaseLogsCommand() {
|
|
855
|
+
addSubcommand(PocketBaseLogsListCommand());
|
|
856
|
+
addSubcommand(PocketBaseLogsStatsCommand());
|
|
857
|
+
addSubcommand(PocketBaseLogsTruncateCommand());
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
@override
|
|
861
|
+
String get name => 'logs';
|
|
862
|
+
|
|
863
|
+
@override
|
|
864
|
+
String get description => 'Manage PocketBase logs.';
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
class PocketBaseLogsListCommand extends FlutterPatcherCommand {
|
|
868
|
+
PocketBaseLogsListCommand() {
|
|
869
|
+
_addBackendOptions(argParser);
|
|
870
|
+
argParser.addOption('filter', help: 'PB filter expression.');
|
|
871
|
+
argParser.addOption('sort', help: 'Sort field.', defaultsTo: '-created');
|
|
872
|
+
argParser.addOption('page', help: 'Page number.', defaultsTo: '1');
|
|
873
|
+
argParser.addOption('per-page', help: 'Items per page.', defaultsTo: '30');
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
@override
|
|
877
|
+
String get name => 'list';
|
|
878
|
+
|
|
879
|
+
@override
|
|
880
|
+
String get description => 'List request logs.';
|
|
881
|
+
|
|
882
|
+
@override
|
|
883
|
+
Future<int> run() => runGuarded(() async {
|
|
884
|
+
banner('pocketbase · logs · list');
|
|
885
|
+
final r = argResults!;
|
|
886
|
+
final data = await _pbStep(
|
|
887
|
+
'Fetching logs',
|
|
888
|
+
(c) => c.listLogs(
|
|
889
|
+
filter: r['filter'] as String?,
|
|
890
|
+
sort: r['sort'] as String?,
|
|
891
|
+
page: int.tryParse(r['page'] as String? ?? '1') ?? 1,
|
|
892
|
+
perPage: int.tryParse(r['per-page'] as String? ?? '30') ?? 30,
|
|
893
|
+
),
|
|
894
|
+
r,
|
|
895
|
+
);
|
|
896
|
+
final items = (data['items'] as List? ?? []).cast<Map<String, dynamic>>();
|
|
897
|
+
final total = data['totalItems'] as int? ?? 0;
|
|
898
|
+
if (items.isEmpty) {
|
|
899
|
+
warn('No logs found.');
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
final rows = items.map((log) {
|
|
903
|
+
final level = log['level'] as int? ?? 0;
|
|
904
|
+
final lvl = level == 0 ? green('INFO') : level == 1 ? yellow('WARN') : red('ERROR');
|
|
905
|
+
final msg = (log['message'] as String? ?? '').length > 60
|
|
906
|
+
? '${(log['message'] as String).substring(0, 57)}...'
|
|
907
|
+
: log['message'] as String? ?? '';
|
|
908
|
+
return [lvl, _safeSubstring(log['created']?.toString(), 19), msg];
|
|
909
|
+
}).toList();
|
|
910
|
+
table('$total logs', ['LEVEL', 'TIME', 'MESSAGE'], rows);
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
class PocketBaseLogsStatsCommand extends FlutterPatcherCommand {
|
|
915
|
+
PocketBaseLogsStatsCommand() {
|
|
916
|
+
_addBackendOptions(argParser);
|
|
917
|
+
argParser.addOption('filter', help: 'PB filter expression.');
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
@override
|
|
921
|
+
String get name => 'stats';
|
|
922
|
+
|
|
923
|
+
@override
|
|
924
|
+
String get description => 'Show hourly log statistics.';
|
|
925
|
+
|
|
926
|
+
@override
|
|
927
|
+
Future<int> run() => runGuarded(() async {
|
|
928
|
+
banner('pocketbase · logs · stats');
|
|
929
|
+
final stats = await _pbStep(
|
|
930
|
+
'Fetching log stats',
|
|
931
|
+
(c) => c.getLogStats(filter: argResults!['filter'] as String?),
|
|
932
|
+
argResults!,
|
|
933
|
+
);
|
|
934
|
+
if (stats.isEmpty) {
|
|
935
|
+
warn('No stats available.');
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
final rows = stats.map((s) => [
|
|
939
|
+
_safeSubstring(s['date']?.toString(), 16),
|
|
940
|
+
'${s['total'] ?? 0}',
|
|
941
|
+
]).toList();
|
|
942
|
+
table('${stats.length} entries', ['TIME', 'COUNT'], rows);
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
class PocketBaseLogsTruncateCommand extends FlutterPatcherCommand {
|
|
947
|
+
PocketBaseLogsTruncateCommand() {
|
|
948
|
+
_addBackendOptions(argParser);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
@override
|
|
952
|
+
String get name => 'truncate';
|
|
953
|
+
|
|
954
|
+
@override
|
|
955
|
+
String get description => 'Delete all logs.';
|
|
956
|
+
|
|
957
|
+
@override
|
|
958
|
+
Future<int> run() => runGuarded(() async {
|
|
959
|
+
banner('pocketbase · logs · truncate');
|
|
960
|
+
await _pbStep('Truncating logs', (c) => c.truncateLogs(), argResults!);
|
|
961
|
+
step(green('All logs deleted'));
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// ---------------------------------------------------------------------------
|
|
966
|
+
// records
|
|
967
|
+
// ---------------------------------------------------------------------------
|
|
968
|
+
|
|
969
|
+
class PocketBaseRecordsCommand extends FlutterPatcherCommand {
|
|
970
|
+
PocketBaseRecordsCommand() {
|
|
971
|
+
addSubcommand(PocketBaseRecordsListCommand());
|
|
972
|
+
addSubcommand(PocketBaseRecordsGetCommand());
|
|
973
|
+
addSubcommand(PocketBaseRecordsCreateCommand());
|
|
974
|
+
addSubcommand(PocketBaseRecordsUpdateCommand());
|
|
975
|
+
addSubcommand(PocketBaseRecordsDeleteCommand());
|
|
976
|
+
addSubcommand(PocketBaseRecordsBatchCommand());
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
@override
|
|
980
|
+
String get name => 'records';
|
|
981
|
+
|
|
982
|
+
@override
|
|
983
|
+
String get description => 'CRUD operations on collection records.';
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
class PocketBaseRecordsListCommand extends FlutterPatcherCommand {
|
|
987
|
+
PocketBaseRecordsListCommand() {
|
|
988
|
+
_addBackendOptions(argParser);
|
|
989
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
990
|
+
argParser.addOption('filter', help: 'PB filter expression.');
|
|
991
|
+
argParser.addOption('sort', help: 'Sort field.');
|
|
992
|
+
argParser.addOption('page', help: 'Page number.', defaultsTo: '1');
|
|
993
|
+
argParser.addOption('per-page', help: 'Items per page.', defaultsTo: '30');
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
@override
|
|
997
|
+
String get name => 'list';
|
|
998
|
+
|
|
999
|
+
@override
|
|
1000
|
+
String get description => 'List records in a collection.';
|
|
1001
|
+
|
|
1002
|
+
@override
|
|
1003
|
+
Future<int> run() => runGuarded(() async {
|
|
1004
|
+
banner('pocketbase · records · list');
|
|
1005
|
+
final r = argResults!;
|
|
1006
|
+
final data = await _pbStep(
|
|
1007
|
+
'Listing ${r['collection']}',
|
|
1008
|
+
(c) => c.listRecords<dynamic>(
|
|
1009
|
+
r['collection'] as String,
|
|
1010
|
+
(j) => j,
|
|
1011
|
+
filter: r['filter'] as String?,
|
|
1012
|
+
sort: r['sort'] as String?,
|
|
1013
|
+
page: int.tryParse(r['page'] as String? ?? '1') ?? 1,
|
|
1014
|
+
perPage: int.tryParse(r['per-page'] as String? ?? '30') ?? 30,
|
|
1015
|
+
),
|
|
1016
|
+
r,
|
|
1017
|
+
);
|
|
1018
|
+
if (data.items.isEmpty) {
|
|
1019
|
+
warn('No records found.');
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
final rows = data.items.map((r) {
|
|
1023
|
+
final m = r as Map<String, dynamic>;
|
|
1024
|
+
final label = m['name']?.toString() ??
|
|
1025
|
+
m['updated']?.toString() ??
|
|
1026
|
+
m['created']?.toString() ??
|
|
1027
|
+
'';
|
|
1028
|
+
return [cyan(m['id']?.toString() ?? '?'), label];
|
|
1029
|
+
}).toList();
|
|
1030
|
+
table('${data.totalItems} records', ['ID', 'LABEL'], rows);
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
class PocketBaseRecordsGetCommand extends FlutterPatcherCommand {
|
|
1035
|
+
PocketBaseRecordsGetCommand() {
|
|
1036
|
+
_addBackendOptions(argParser);
|
|
1037
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
1038
|
+
argParser.addOption('id', help: 'Record ID.', mandatory: true);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
@override
|
|
1042
|
+
String get name => 'get';
|
|
1043
|
+
|
|
1044
|
+
@override
|
|
1045
|
+
String get description => 'Get a single record.';
|
|
1046
|
+
|
|
1047
|
+
@override
|
|
1048
|
+
Future<int> run() => runGuarded(() async {
|
|
1049
|
+
banner('pocketbase · records · get');
|
|
1050
|
+
final r = argResults!;
|
|
1051
|
+
final record = await _pbStep(
|
|
1052
|
+
'Getting ${r['collection']}/${r['id']}',
|
|
1053
|
+
(c) => c.getRecord<dynamic>(r['collection'] as String, r['id'] as String, (j) => j),
|
|
1054
|
+
r,
|
|
1055
|
+
);
|
|
1056
|
+
if (record == null) {
|
|
1057
|
+
warn('Record not found.');
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
stdout.writeln(const JsonEncoder.withIndent(' ').convert(record));
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
class PocketBaseRecordsCreateCommand extends FlutterPatcherCommand {
|
|
1065
|
+
PocketBaseRecordsCreateCommand() {
|
|
1066
|
+
_addBackendOptions(argParser);
|
|
1067
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
1068
|
+
argParser.addOption('body', help: 'JSON body.', mandatory: true);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
@override
|
|
1072
|
+
String get name => 'create';
|
|
1073
|
+
|
|
1074
|
+
@override
|
|
1075
|
+
String get description => 'Create a record.';
|
|
1076
|
+
|
|
1077
|
+
@override
|
|
1078
|
+
Future<int> run() => runGuarded(() async {
|
|
1079
|
+
banner('pocketbase · records · create');
|
|
1080
|
+
final r = argResults!;
|
|
1081
|
+
final body = jsonDecode(r['body'] as String) as Map<String, dynamic>;
|
|
1082
|
+
final record = await _pbStep(
|
|
1083
|
+
'Creating record in ${r['collection']}',
|
|
1084
|
+
(c) => c.createRecord<dynamic>(r['collection'] as String, body, (j) => j),
|
|
1085
|
+
r,
|
|
1086
|
+
);
|
|
1087
|
+
stdout.writeln(' ${dim('→')} record ${cyan((record as Map)['id']?.toString() ?? '?')}');
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
class PocketBaseRecordsUpdateCommand extends FlutterPatcherCommand {
|
|
1092
|
+
PocketBaseRecordsUpdateCommand() {
|
|
1093
|
+
_addBackendOptions(argParser);
|
|
1094
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
1095
|
+
argParser.addOption('id', help: 'Record ID.', mandatory: true);
|
|
1096
|
+
argParser.addOption('body', help: 'JSON body.', mandatory: true);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
@override
|
|
1100
|
+
String get name => 'update';
|
|
1101
|
+
|
|
1102
|
+
@override
|
|
1103
|
+
String get description => 'Update a record.';
|
|
1104
|
+
|
|
1105
|
+
@override
|
|
1106
|
+
Future<int> run() => runGuarded(() async {
|
|
1107
|
+
banner('pocketbase · records · update');
|
|
1108
|
+
final r = argResults!;
|
|
1109
|
+
final body = jsonDecode(r['body'] as String) as Map<String, dynamic>;
|
|
1110
|
+
await _pbStep(
|
|
1111
|
+
'Updating ${r['collection']}/${r['id']}',
|
|
1112
|
+
(c) => c.updateRecord<dynamic>(r['collection'] as String, r['id'] as String, body, (j) => j),
|
|
1113
|
+
r,
|
|
1114
|
+
);
|
|
1115
|
+
step(green('Updated ${r['collection']}/${r['id']}'));
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
class PocketBaseRecordsDeleteCommand extends FlutterPatcherCommand {
|
|
1120
|
+
PocketBaseRecordsDeleteCommand() {
|
|
1121
|
+
_addBackendOptions(argParser);
|
|
1122
|
+
argParser.addOption('collection', help: 'Collection name.', mandatory: true);
|
|
1123
|
+
argParser.addOption('id', help: 'Record ID.', mandatory: true);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
@override
|
|
1127
|
+
String get name => 'delete';
|
|
1128
|
+
|
|
1129
|
+
@override
|
|
1130
|
+
String get description => 'Delete a record.';
|
|
1131
|
+
|
|
1132
|
+
@override
|
|
1133
|
+
Future<int> run() => runGuarded(() async {
|
|
1134
|
+
banner('pocketbase · records · delete');
|
|
1135
|
+
final r = argResults!;
|
|
1136
|
+
await _pbStep(
|
|
1137
|
+
'Deleting ${r['collection']}/${r['id']}',
|
|
1138
|
+
(c) => c.deleteRecord(r['collection'] as String, r['id'] as String),
|
|
1139
|
+
r,
|
|
1140
|
+
);
|
|
1141
|
+
step(green('Deleted ${r['collection']}/${r['id']}'));
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
class PocketBaseRecordsBatchCommand extends FlutterPatcherCommand {
|
|
1146
|
+
PocketBaseRecordsBatchCommand() {
|
|
1147
|
+
_addBackendOptions(argParser);
|
|
1148
|
+
argParser.addOption('requests', help: 'JSON array of request objects.', mandatory: true);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
@override
|
|
1152
|
+
String get name => 'batch';
|
|
1153
|
+
|
|
1154
|
+
@override
|
|
1155
|
+
String get description => 'Execute multiple record operations in a single transaction.';
|
|
1156
|
+
|
|
1157
|
+
@override
|
|
1158
|
+
Future<int> run() => runGuarded(() async {
|
|
1159
|
+
banner('pocketbase · records · batch');
|
|
1160
|
+
final requests = jsonDecode(argResults!['requests'] as String) as List<dynamic>;
|
|
1161
|
+
final body = requests.cast<Map<String, dynamic>>();
|
|
1162
|
+
final results = await _pbStep(
|
|
1163
|
+
'Executing ${body.length} batch requests',
|
|
1164
|
+
(c) => c.batch(body),
|
|
1165
|
+
argResults!,
|
|
1166
|
+
);
|
|
1167
|
+
for (final (i, r) in results.indexed) {
|
|
1168
|
+
final status = r['status'] as int? ?? 0;
|
|
1169
|
+
final ok = status >= 200 && status < 300;
|
|
1170
|
+
step('${ok ? green('ok') : red('fail')} [${i + 1}] HTTP $status');
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
}
|
|
1174
|
+
// ---------------------------------------------------------------------------
|
|
1175
|
+
|
|
1176
|
+
class PocketBaseCollectionsCommand extends FlutterPatcherCommand {
|
|
1177
|
+
PocketBaseCollectionsCommand() {
|
|
1178
|
+
addSubcommand(PocketBaseCollectionsListCommand());
|
|
1179
|
+
addSubcommand(PocketBaseCollectionsViewCommand());
|
|
1180
|
+
addSubcommand(PocketBaseCollectionsCreateCommand());
|
|
1181
|
+
addSubcommand(PocketBaseCollectionsUpdateCommand());
|
|
1182
|
+
addSubcommand(PocketBaseCollectionsDeleteCommand());
|
|
1183
|
+
addSubcommand(PocketBaseCollectionsTruncateCommand());
|
|
1184
|
+
addSubcommand(PocketBaseCollectionsImportCommand());
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
@override
|
|
1188
|
+
String get name => 'collections';
|
|
1189
|
+
|
|
1190
|
+
@override
|
|
1191
|
+
String get description => 'Manage PocketBase collections.';
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
class PocketBaseCollectionsListCommand extends FlutterPatcherCommand {
|
|
1195
|
+
PocketBaseCollectionsListCommand() {
|
|
1196
|
+
_addBackendOptions(argParser);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
@override
|
|
1200
|
+
String get name => 'list';
|
|
1201
|
+
|
|
1202
|
+
@override
|
|
1203
|
+
String get description => 'List all collections.';
|
|
1204
|
+
|
|
1205
|
+
@override
|
|
1206
|
+
Future<int> run() => runGuarded(() async {
|
|
1207
|
+
banner('pocketbase · collections · list');
|
|
1208
|
+
final cols = await _pbStep('Listing collections', (c) => c.listCollections(), argResults!);
|
|
1209
|
+
if (cols.isEmpty) {
|
|
1210
|
+
warn('No collections found.');
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
final rows = cols.map((c) => [
|
|
1214
|
+
cyan(c['name']?.toString() ?? '?'),
|
|
1215
|
+
c['type']?.toString() ?? '?',
|
|
1216
|
+
'${(c['fields'] as List?)?.length ?? 0} fields',
|
|
1217
|
+
]).toList();
|
|
1218
|
+
table('${cols.length} collections', ['NAME', 'TYPE', 'FIELDS'], rows);
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
class PocketBaseCollectionsViewCommand extends FlutterPatcherCommand {
|
|
1223
|
+
PocketBaseCollectionsViewCommand() {
|
|
1224
|
+
_addBackendOptions(argParser);
|
|
1225
|
+
argParser.addOption('name', help: 'Collection name or ID.', mandatory: true);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
@override
|
|
1229
|
+
String get name => 'view';
|
|
1230
|
+
|
|
1231
|
+
@override
|
|
1232
|
+
String get description => 'View a collection schema.';
|
|
1233
|
+
|
|
1234
|
+
@override
|
|
1235
|
+
Future<int> run() => runGuarded(() async {
|
|
1236
|
+
banner('pocketbase · collections · view');
|
|
1237
|
+
final name = argResults!['name'] as String;
|
|
1238
|
+
final col = await _pbStep('Viewing $name', (c) => c.getCollection(name), argResults!);
|
|
1239
|
+
stdout.writeln(const JsonEncoder.withIndent(' ').convert(col));
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
class PocketBaseCollectionsCreateCommand extends FlutterPatcherCommand {
|
|
1244
|
+
PocketBaseCollectionsCreateCommand() {
|
|
1245
|
+
_addBackendOptions(argParser);
|
|
1246
|
+
argParser.addOption('body', help: 'JSON collection definition.', mandatory: true);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
@override
|
|
1250
|
+
String get name => 'create';
|
|
1251
|
+
|
|
1252
|
+
@override
|
|
1253
|
+
String get description => 'Create a new collection.';
|
|
1254
|
+
|
|
1255
|
+
@override
|
|
1256
|
+
Future<int> run() => runGuarded(() async {
|
|
1257
|
+
banner('pocketbase · collections · create');
|
|
1258
|
+
final body = jsonDecode(argResults!['body'] as String) as Map<String, dynamic>;
|
|
1259
|
+
await _pbStep('Creating ${body['name'] ?? 'collection'}',
|
|
1260
|
+
(c) => c.createCollection(body), argResults!);
|
|
1261
|
+
step(green('Created ${body['name'] ?? 'collection'}'));
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
class PocketBaseCollectionsUpdateCommand extends FlutterPatcherCommand {
|
|
1266
|
+
PocketBaseCollectionsUpdateCommand() {
|
|
1267
|
+
_addBackendOptions(argParser);
|
|
1268
|
+
argParser.addOption('name', help: 'Collection name or ID.', mandatory: true);
|
|
1269
|
+
argParser.addOption('body', help: 'JSON fields to update.', mandatory: true);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
@override
|
|
1273
|
+
String get name => 'update';
|
|
1274
|
+
|
|
1275
|
+
@override
|
|
1276
|
+
String get description => 'Update a collection.';
|
|
1277
|
+
|
|
1278
|
+
@override
|
|
1279
|
+
Future<int> run() => runGuarded(() async {
|
|
1280
|
+
banner('pocketbase · collections · update');
|
|
1281
|
+
final r = argResults!;
|
|
1282
|
+
final name = r['name'] as String;
|
|
1283
|
+
final body = jsonDecode(r['body'] as String) as Map<String, dynamic>;
|
|
1284
|
+
await _pbStep('Updating $name', (c) => c.updateCollection(name, body), r);
|
|
1285
|
+
step(green('Updated $name'));
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
class PocketBaseCollectionsDeleteCommand extends FlutterPatcherCommand {
|
|
1290
|
+
PocketBaseCollectionsDeleteCommand() {
|
|
1291
|
+
_addBackendOptions(argParser);
|
|
1292
|
+
argParser.addOption('name', help: 'Collection name or ID.', mandatory: true);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
@override
|
|
1296
|
+
String get name => 'delete';
|
|
1297
|
+
|
|
1298
|
+
@override
|
|
1299
|
+
String get description => 'Delete a collection.';
|
|
1300
|
+
|
|
1301
|
+
@override
|
|
1302
|
+
Future<int> run() => runGuarded(() async {
|
|
1303
|
+
banner('pocketbase · collections · delete');
|
|
1304
|
+
final name = argResults!['name'] as String;
|
|
1305
|
+
await _pbStep('Deleting $name', (c) => c.deleteCollection(name), argResults!);
|
|
1306
|
+
step(green('Deleted collection $name'));
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
class PocketBaseCollectionsTruncateCommand extends FlutterPatcherCommand {
|
|
1311
|
+
PocketBaseCollectionsTruncateCommand() {
|
|
1312
|
+
_addBackendOptions(argParser);
|
|
1313
|
+
argParser.addOption('name', help: 'Collection name or ID.', mandatory: true);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
@override
|
|
1317
|
+
String get name => 'truncate';
|
|
1318
|
+
|
|
1319
|
+
@override
|
|
1320
|
+
String get description => 'Delete all records in a collection.';
|
|
1321
|
+
|
|
1322
|
+
@override
|
|
1323
|
+
Future<int> run() => runGuarded(() async {
|
|
1324
|
+
banner('pocketbase · collections · truncate');
|
|
1325
|
+
final name = argResults!['name'] as String;
|
|
1326
|
+
await _pbStep('Truncating $name', (c) => c.truncateCollection(name), argResults!);
|
|
1327
|
+
step(green('Truncated $name'));
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
class PocketBaseCollectionsImportCommand extends FlutterPatcherCommand {
|
|
1332
|
+
PocketBaseCollectionsImportCommand() {
|
|
1333
|
+
_addBackendOptions(argParser);
|
|
1334
|
+
argParser.addOption('file', help: 'JSON file with collections config.', mandatory: true);
|
|
1335
|
+
argParser.addFlag('delete-missing', help: 'Delete collections not in file.', defaultsTo: false);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
@override
|
|
1339
|
+
String get name => 'import';
|
|
1340
|
+
|
|
1341
|
+
@override
|
|
1342
|
+
String get description => 'Import collections from a JSON file.';
|
|
1343
|
+
|
|
1344
|
+
@override
|
|
1345
|
+
Future<int> run() => runGuarded(() async {
|
|
1346
|
+
banner('pocketbase · collections · import');
|
|
1347
|
+
final r = argResults!;
|
|
1348
|
+
final fp = r['file'] as String;
|
|
1349
|
+
final f = File(fp);
|
|
1350
|
+
if (!await f.exists()) throw PackException('File not found: $fp', 64);
|
|
1351
|
+
final json = jsonDecode(await f.readAsString()) as List<dynamic>;
|
|
1352
|
+
final cols = json.cast<Map<String, dynamic>>();
|
|
1353
|
+
await _pbStep(
|
|
1354
|
+
'Importing ${cols.length} collections',
|
|
1355
|
+
(c) => c.importCollections(cols, deleteMissing: r['delete-missing'] as bool),
|
|
1356
|
+
r,
|
|
1357
|
+
);
|
|
1358
|
+
step(green('Imported ${cols.length} collections'));
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// ---------------------------------------------------------------------------
|
|
1363
|
+
// settings
|
|
1364
|
+
// ---------------------------------------------------------------------------
|
|
1365
|
+
|
|
1366
|
+
class PocketBaseSettingsCommand extends FlutterPatcherCommand {
|
|
1367
|
+
PocketBaseSettingsCommand() {
|
|
1368
|
+
addSubcommand(PocketBaseSettingsListCommand());
|
|
1369
|
+
addSubcommand(PocketBaseSettingsUpdateCommand());
|
|
1370
|
+
addSubcommand(PocketBaseSettingsTestS3Command());
|
|
1371
|
+
addSubcommand(PocketBaseSettingsTestEmailCommand());
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
@override
|
|
1375
|
+
String get name => 'settings';
|
|
1376
|
+
|
|
1377
|
+
@override
|
|
1378
|
+
String get description => 'Manage PocketBase application settings.';
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
class PocketBaseSettingsListCommand extends FlutterPatcherCommand {
|
|
1382
|
+
PocketBaseSettingsListCommand() {
|
|
1383
|
+
_addBackendOptions(argParser);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
@override
|
|
1387
|
+
String get name => 'list';
|
|
1388
|
+
|
|
1389
|
+
@override
|
|
1390
|
+
String get description => 'List all application settings.';
|
|
1391
|
+
|
|
1392
|
+
@override
|
|
1393
|
+
Future<int> run() => runGuarded(() async {
|
|
1394
|
+
banner('pocketbase · settings · list');
|
|
1395
|
+
final settings = await _pbStep('Fetching settings', (c) => c.listSettings(), argResults!);
|
|
1396
|
+
stdout.writeln(const JsonEncoder.withIndent(' ').convert(settings));
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
class PocketBaseSettingsUpdateCommand extends FlutterPatcherCommand {
|
|
1401
|
+
PocketBaseSettingsUpdateCommand() {
|
|
1402
|
+
_addBackendOptions(argParser);
|
|
1403
|
+
argParser.addOption('body', help: 'JSON object with settings to update.', mandatory: true);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
@override
|
|
1407
|
+
String get name => 'update';
|
|
1408
|
+
|
|
1409
|
+
@override
|
|
1410
|
+
String get description => 'Update application settings.';
|
|
1411
|
+
|
|
1412
|
+
@override
|
|
1413
|
+
Future<int> run() => runGuarded(() async {
|
|
1414
|
+
banner('pocketbase · settings · update');
|
|
1415
|
+
final body = jsonDecode(argResults!['body'] as String) as Map<String, dynamic>;
|
|
1416
|
+
await _pbStep('Updating settings', (c) => c.updateSettings(body), argResults!);
|
|
1417
|
+
step(green('Settings updated'));
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
class PocketBaseSettingsTestS3Command extends FlutterPatcherCommand {
|
|
1422
|
+
PocketBaseSettingsTestS3Command() {
|
|
1423
|
+
_addBackendOptions(argParser);
|
|
1424
|
+
argParser.addOption('filesystem', help: 'Storage filesystem to test.', defaultsTo: 'storage');
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
@override
|
|
1428
|
+
String get name => 'test-s3';
|
|
1429
|
+
|
|
1430
|
+
@override
|
|
1431
|
+
String get description => 'Test S3 storage connection.';
|
|
1432
|
+
|
|
1433
|
+
@override
|
|
1434
|
+
Future<int> run() => runGuarded(() async {
|
|
1435
|
+
banner('pocketbase · settings · test-s3');
|
|
1436
|
+
final fs = argResults!['filesystem'] as String;
|
|
1437
|
+
await _pbStep('Testing S3 ($fs)', (c) => c.testS3(fs), argResults!);
|
|
1438
|
+
step(green('S3 connection OK'));
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
class PocketBaseSettingsTestEmailCommand extends FlutterPatcherCommand {
|
|
1443
|
+
PocketBaseSettingsTestEmailCommand() {
|
|
1444
|
+
_addBackendOptions(argParser);
|
|
1445
|
+
argParser.addOption('email', help: 'Recipient email.', mandatory: true);
|
|
1446
|
+
argParser.addOption('template', help: 'Email template.', mandatory: true);
|
|
1447
|
+
argParser.addOption('collection', help: 'Auth collection name or ID.');
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
@override
|
|
1451
|
+
String get name => 'test-email';
|
|
1452
|
+
|
|
1453
|
+
@override
|
|
1454
|
+
String get description => 'Send a test email.';
|
|
1455
|
+
|
|
1456
|
+
@override
|
|
1457
|
+
Future<int> run() => runGuarded(() async {
|
|
1458
|
+
banner('pocketbase · settings · test-email');
|
|
1459
|
+
final r = argResults!;
|
|
1460
|
+
await _pbStep(
|
|
1461
|
+
'Sending test email to ${r['email']}',
|
|
1462
|
+
(c) => c.testEmail(
|
|
1463
|
+
email: r['email'] as String,
|
|
1464
|
+
template: r['template'] as String,
|
|
1465
|
+
collection: r['collection'] as String?,
|
|
1466
|
+
),
|
|
1467
|
+
r,
|
|
1468
|
+
);
|
|
1469
|
+
step(green('Test email sent'));
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// ---------------------------------------------------------------------------
|
|
1474
|
+
// sql
|
|
1475
|
+
// ---------------------------------------------------------------------------
|
|
1476
|
+
|
|
1477
|
+
class PocketBaseSqlCommand extends FlutterPatcherCommand {
|
|
1478
|
+
PocketBaseSqlCommand() {
|
|
1479
|
+
_addBackendOptions(argParser);
|
|
1480
|
+
argParser.addOption('query', help: 'SQL query to execute.', mandatory: true);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
@override
|
|
1484
|
+
String get name => 'sql';
|
|
1485
|
+
|
|
1486
|
+
@override
|
|
1487
|
+
String get description => 'Execute a raw SQL query.';
|
|
1488
|
+
|
|
1489
|
+
@override
|
|
1490
|
+
Future<int> run() => runGuarded(() async {
|
|
1491
|
+
banner('pocketbase · sql');
|
|
1492
|
+
final query = argResults!['query'] as String;
|
|
1493
|
+
final result = await _pbStep('Executing query', (c) => c.runSql(query), argResults!);
|
|
1494
|
+
final columns = (result['columns'] as List? ?? []).cast<Map<String, dynamic>>();
|
|
1495
|
+
final rows = (result['rows'] as List? ?? []).cast<List<dynamic>>();
|
|
1496
|
+
if (rows.isNotEmpty) {
|
|
1497
|
+
final colNames = columns.map((c) => '${c['name']}').toList();
|
|
1498
|
+
final tableRows = rows.map((r) => r.map((v) => '$v').toList()).toList();
|
|
1499
|
+
table('${rows.length} rows', colNames, tableRows);
|
|
1500
|
+
} else {
|
|
1501
|
+
step('Query executed. ${result['affectedRows'] ?? 0} rows affected.');
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// ---------------------------------------------------------------------------
|
|
1507
|
+
// crons
|
|
1508
|
+
// ---------------------------------------------------------------------------
|
|
1509
|
+
|
|
1510
|
+
class PocketBaseCronsCommand extends FlutterPatcherCommand {
|
|
1511
|
+
PocketBaseCronsCommand() {
|
|
1512
|
+
addSubcommand(PocketBaseCronsListCommand());
|
|
1513
|
+
addSubcommand(PocketBaseCronsRunCommand());
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
@override
|
|
1517
|
+
String get name => 'crons';
|
|
1518
|
+
|
|
1519
|
+
@override
|
|
1520
|
+
String get description => 'Manage PocketBase cron jobs.';
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
class PocketBaseCronsListCommand extends FlutterPatcherCommand {
|
|
1524
|
+
PocketBaseCronsListCommand() {
|
|
1525
|
+
_addBackendOptions(argParser);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
@override
|
|
1529
|
+
String get name => 'list';
|
|
1530
|
+
|
|
1531
|
+
@override
|
|
1532
|
+
String get description => 'List all registered cron jobs.';
|
|
1533
|
+
|
|
1534
|
+
@override
|
|
1535
|
+
Future<int> run() => runGuarded(() async {
|
|
1536
|
+
banner('pocketbase · crons · list');
|
|
1537
|
+
final crons = await _pbStep('Fetching cron jobs', (c) => c.listCrons(), argResults!);
|
|
1538
|
+
if (crons.isEmpty) {
|
|
1539
|
+
warn('No cron jobs registered.');
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
final rows = crons.map((c) => [
|
|
1543
|
+
cyan(c['id']?.toString() ?? '?'),
|
|
1544
|
+
c['expression']?.toString() ?? '?',
|
|
1545
|
+
]).toList();
|
|
1546
|
+
table('${crons.length} cron jobs', ['ID', 'EXPRESSION'], rows);
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
class PocketBaseCronsRunCommand extends FlutterPatcherCommand {
|
|
1551
|
+
PocketBaseCronsRunCommand() {
|
|
1552
|
+
_addBackendOptions(argParser);
|
|
1553
|
+
argParser.addOption('job-id', help: 'Cron job ID to run.', mandatory: true);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
@override
|
|
1557
|
+
String get name => 'run';
|
|
1558
|
+
|
|
1559
|
+
@override
|
|
1560
|
+
String get description => 'Trigger a cron job by ID.';
|
|
1561
|
+
|
|
1562
|
+
@override
|
|
1563
|
+
Future<int> run() => runGuarded(() async {
|
|
1564
|
+
banner('pocketbase · crons · run');
|
|
1565
|
+
final jobId = argResults!['job-id'] as String;
|
|
1566
|
+
await _pbStep('Running $jobId', (c) => c.runCron(jobId), argResults!);
|
|
1567
|
+
step(green('Cron $jobId triggered'));
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
// ---------------------------------------------------------------------------
|
|
1572
|
+
// query
|
|
1573
|
+
// ---------------------------------------------------------------------------
|
|
1574
|
+
|
|
1575
|
+
class PocketBaseQueryCommand extends FlutterPatcherCommand {
|
|
1576
|
+
PocketBaseQueryCommand() {
|
|
1577
|
+
_addBackendOptions(argParser);
|
|
1578
|
+
argParser.addOption('method', help: 'HTTP method.', defaultsTo: 'GET');
|
|
1579
|
+
argParser.addOption('path', help: 'API path.', mandatory: true);
|
|
1580
|
+
argParser.addOption('body', help: 'JSON body for POST/PATCH.');
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
@override
|
|
1584
|
+
String get name => 'query';
|
|
1585
|
+
|
|
1586
|
+
@override
|
|
1587
|
+
String get description => 'Execute an arbitrary API request.';
|
|
1588
|
+
|
|
1589
|
+
@override
|
|
1590
|
+
Future<int> run() => runGuarded(() async {
|
|
1591
|
+
banner('pocketbase · query');
|
|
1592
|
+
final r = argResults!;
|
|
1593
|
+
final method = r['method'] as String;
|
|
1594
|
+
final path = r['path'] as String;
|
|
1595
|
+
final bodyStr = r['body'] as String?;
|
|
1596
|
+
Map<String, dynamic>? body;
|
|
1597
|
+
if (bodyStr != null && bodyStr.isNotEmpty) {
|
|
1598
|
+
body = jsonDecode(bodyStr) as Map<String, dynamic>;
|
|
1599
|
+
}
|
|
1600
|
+
final (url, admin, pass) = _resolveBackend(r);
|
|
1601
|
+
final client = PocketBaseClient(url);
|
|
1602
|
+
try {
|
|
1603
|
+
await client.authenticate(admin, pass);
|
|
1604
|
+
final res = await client.rawRequest(method, path, body: body);
|
|
1605
|
+
stdout.writeln(const JsonEncoder.withIndent(' ').convert(jsonDecode(res.body)));
|
|
1606
|
+
} finally {
|
|
1607
|
+
client.close();
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// ---------------------------------------------------------------------------
|
|
1613
|
+
// config
|
|
1614
|
+
// ---------------------------------------------------------------------------
|
|
1615
|
+
|
|
1616
|
+
class PocketBaseConfigCommand extends FlutterPatcherCommand {
|
|
1617
|
+
PocketBaseConfigCommand() {
|
|
1618
|
+
_addBackendOptions(argParser);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
@override
|
|
1622
|
+
String get name => 'config';
|
|
1623
|
+
|
|
1624
|
+
@override
|
|
1625
|
+
String get description => 'Show current PocketBase configuration.';
|
|
1626
|
+
|
|
1627
|
+
@override
|
|
1628
|
+
Future<int> run() => runGuarded(() async {
|
|
1629
|
+
banner('pocketbase · config');
|
|
1630
|
+
final (url, admin, _) = _resolveBackend(argResults!);
|
|
1631
|
+
final paths = PocketBaseInstallPaths.resolve();
|
|
1632
|
+
final exists = await paths.binaryPath.exists();
|
|
1633
|
+
box('config', [
|
|
1634
|
+
kv('url', cyan(url)),
|
|
1635
|
+
kv('admin', admin),
|
|
1636
|
+
kv('version', paths.version),
|
|
1637
|
+
kv('binary', exists ? green(paths.binaryPath.path) : red('not installed')),
|
|
1638
|
+
]);
|
|
233
1639
|
});
|
|
234
1640
|
}
|