@_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
|
@@ -38,6 +38,7 @@ export 'src/commands/channel.dart';
|
|
|
38
38
|
export 'src/commands/migrate.dart';
|
|
39
39
|
export 'src/commands/console.dart';
|
|
40
40
|
export 'src/commands/pocketbase.dart';
|
|
41
|
+
export 'src/commands/storage.dart';
|
|
41
42
|
|
|
42
43
|
import 'src/commands/build.dart';
|
|
43
44
|
import 'src/commands/bundle.dart';
|
|
@@ -183,11 +184,11 @@ void _printUnknownCommand(FlutterPatcherRunner runner, String typed) {
|
|
|
183
184
|
// If it looks like "parent sub" but the sub is wrong, show sub help.
|
|
184
185
|
if (parentCmd != null && subTyped != null) {
|
|
185
186
|
final cmd = runner.commands[parentCmd];
|
|
187
|
+
final sub = subTyped;
|
|
186
188
|
if (cmd != null && cmd.subcommands.isNotEmpty) {
|
|
187
|
-
// Find closest subcommand.
|
|
188
189
|
final subNames = cmd.subcommands.keys.toList();
|
|
189
190
|
final subSuggestion = subNames
|
|
190
|
-
.where((s) => s.startsWith(
|
|
191
|
+
.where((s) => s.startsWith(sub) || _levenshtein(sub, s) <= 2)
|
|
191
192
|
.toList();
|
|
192
193
|
if (subSuggestion.isNotEmpty) {
|
|
193
194
|
stderr.writeln(' ${_cyan('Did you mean?')} $parentCmd ${_green(subSuggestion.first)}');
|
|
@@ -244,7 +245,7 @@ void _printMissingSubcommand(Command<int> parent) {
|
|
|
244
245
|
stderr.writeln('');
|
|
245
246
|
|
|
246
247
|
if (subNames.isNotEmpty) {
|
|
247
|
-
stderr.writeln(' ${_cyan('
|
|
248
|
+
stderr.writeln(' ${_cyan('Available subcommands:')}');
|
|
248
249
|
for (final s in subNames) {
|
|
249
250
|
stderr.writeln(' ${_green('${parent.name} $s')}');
|
|
250
251
|
}
|
|
@@ -35,8 +35,9 @@ Backend requireBackend(FlutterPatcherConfig? config, {Backend? override}) {
|
|
|
35
35
|
if (override != null) return override;
|
|
36
36
|
if (config == null) {
|
|
37
37
|
throw StateError(
|
|
38
|
-
'No configuration found. Run `
|
|
39
|
-
'
|
|
38
|
+
'No configuration found. Run `flutter-ota init <backend>` or set '
|
|
39
|
+
'the required environment variables for your provider '
|
|
40
|
+
'(see `flutter-ota init --help`).',
|
|
40
41
|
);
|
|
41
42
|
}
|
|
42
43
|
return resolveBackend(config);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
|
|
1
3
|
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
2
4
|
|
|
3
5
|
import '../ui/ui.dart';
|
|
@@ -58,12 +60,15 @@ class BundleCommand extends FlutterPatcherCommand {
|
|
|
58
60
|
|
|
59
61
|
@override
|
|
60
62
|
String get description =>
|
|
61
|
-
'Manage bundles (list / show / delete / disable / enable / force / promote).';
|
|
63
|
+
'Manage bundles (list / show / delete / disable / enable / force / promote / update).';
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
class BundleListCommand extends FlutterPatcherCommand {
|
|
65
67
|
BundleListCommand({this.config, this.backendOverride}) {
|
|
66
|
-
|
|
68
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
69
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null
|
|
70
|
+
? 'Backend provider [detected: $detected].'
|
|
71
|
+
: 'Backend provider.');
|
|
67
72
|
argParser.addOption('channel', abbr: 'c', help: 'Filter by channel.');
|
|
68
73
|
argParser.addOption('platform', abbr: 'p', help: 'Filter by platform.');
|
|
69
74
|
argParser.addOption('enabled', help: 'Filter by enabled (true/false).');
|
|
@@ -130,7 +135,8 @@ class BundleListCommand extends FlutterPatcherCommand {
|
|
|
130
135
|
|
|
131
136
|
class BundleShowCommand extends FlutterPatcherCommand {
|
|
132
137
|
BundleShowCommand({this.config, this.backendOverride}) {
|
|
133
|
-
|
|
138
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
139
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
|
|
134
140
|
argParser.addOption('id', help: 'Bundle id.');
|
|
135
141
|
}
|
|
136
142
|
|
|
@@ -173,7 +179,8 @@ class BundleShowCommand extends FlutterPatcherCommand {
|
|
|
173
179
|
|
|
174
180
|
class BundleDeleteCommand extends FlutterPatcherCommand {
|
|
175
181
|
BundleDeleteCommand({this.config, this.backendOverride}) {
|
|
176
|
-
|
|
182
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
183
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
|
|
177
184
|
argParser.addOption('id', help: 'Bundle id.');
|
|
178
185
|
argParser.addFlag(
|
|
179
186
|
'keep-storage',
|
|
@@ -215,7 +222,8 @@ class BundleDeleteCommand extends FlutterPatcherCommand {
|
|
|
215
222
|
|
|
216
223
|
class BundleDisableCommand extends FlutterPatcherCommand {
|
|
217
224
|
BundleDisableCommand({this.config, this.backendOverride}) {
|
|
218
|
-
|
|
225
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
226
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
|
|
219
227
|
argParser.addOption('id', help: 'Bundle id.');
|
|
220
228
|
}
|
|
221
229
|
|
|
@@ -246,7 +254,8 @@ class BundleDisableCommand extends FlutterPatcherCommand {
|
|
|
246
254
|
|
|
247
255
|
class BundleEnableCommand extends FlutterPatcherCommand {
|
|
248
256
|
BundleEnableCommand({this.config, this.backendOverride}) {
|
|
249
|
-
|
|
257
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
258
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
|
|
250
259
|
argParser.addOption('id', help: 'Bundle id.');
|
|
251
260
|
}
|
|
252
261
|
|
|
@@ -277,7 +286,8 @@ class BundleEnableCommand extends FlutterPatcherCommand {
|
|
|
277
286
|
|
|
278
287
|
class BundleForceCommand extends FlutterPatcherCommand {
|
|
279
288
|
BundleForceCommand({this.config, this.backendOverride}) {
|
|
280
|
-
|
|
289
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
290
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
|
|
281
291
|
argParser.addOption('id', help: 'Bundle id.');
|
|
282
292
|
argParser.addFlag(
|
|
283
293
|
'off',
|
|
@@ -319,7 +329,8 @@ class BundleForceCommand extends FlutterPatcherCommand {
|
|
|
319
329
|
|
|
320
330
|
class BundlePromoteCommand extends FlutterPatcherCommand {
|
|
321
331
|
BundlePromoteCommand({this.config, this.backendOverride}) {
|
|
322
|
-
|
|
332
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
333
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
|
|
323
334
|
argParser.addOption('id', help: 'Bundle id.');
|
|
324
335
|
argParser.addOption('channel', abbr: 'c', help: 'Target channel.');
|
|
325
336
|
}
|
|
@@ -351,13 +362,14 @@ class BundlePromoteCommand extends FlutterPatcherCommand {
|
|
|
351
362
|
await steps.run('Promoting $id to $channel',
|
|
352
363
|
() => promoteBundle(backend, id!, channel));
|
|
353
364
|
steps.summary();
|
|
354
|
-
|
|
365
|
+
stdout.writeln(' ${dim('→')} bundle ${cyan(id!)} → channel ${cyan(channel)}');
|
|
355
366
|
});
|
|
356
367
|
}
|
|
357
368
|
|
|
358
369
|
class BundleUpdateCommand extends FlutterPatcherCommand {
|
|
359
370
|
BundleUpdateCommand({this.config, this.backendOverride}) {
|
|
360
|
-
|
|
371
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
372
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null ? 'Backend provider [detected: $detected].' : 'Backend provider.');
|
|
361
373
|
argParser.addOption('id', help: 'Bundle id.');
|
|
362
374
|
argParser.addOption('message', abbr: 'm', help: 'New release message.');
|
|
363
375
|
argParser.addOption('target-version', help: 'New target app version (e.g. 1.0.0).');
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
|
|
1
3
|
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
2
4
|
|
|
3
5
|
import '../ui/ui.dart';
|
|
@@ -28,7 +30,10 @@ class ChannelCommand extends FlutterPatcherCommand {
|
|
|
28
30
|
|
|
29
31
|
class ChannelListCommand extends FlutterPatcherCommand {
|
|
30
32
|
ChannelListCommand({this.config, this.backendOverride}) {
|
|
31
|
-
|
|
33
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
34
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null
|
|
35
|
+
? 'Backend provider [detected: $detected].'
|
|
36
|
+
: 'Backend provider.');
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
final FlutterPatcherConfig? config;
|
|
@@ -56,7 +61,10 @@ class ChannelListCommand extends FlutterPatcherCommand {
|
|
|
56
61
|
|
|
57
62
|
class ChannelGetCommand extends FlutterPatcherCommand {
|
|
58
63
|
ChannelGetCommand({this.config, this.backendOverride}) {
|
|
59
|
-
|
|
64
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
65
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null
|
|
66
|
+
? 'Backend provider [detected: $detected].'
|
|
67
|
+
: 'Backend provider.');
|
|
60
68
|
argParser.addOption('channel', abbr: 'c', help: 'Channel.');
|
|
61
69
|
}
|
|
62
70
|
|
|
@@ -100,7 +108,10 @@ class ChannelGetCommand extends FlutterPatcherCommand {
|
|
|
100
108
|
|
|
101
109
|
class ChannelSetCommand extends FlutterPatcherCommand {
|
|
102
110
|
ChannelSetCommand({this.config, this.backendOverride}) {
|
|
103
|
-
|
|
111
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
112
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null
|
|
113
|
+
? 'Backend provider [detected: $detected].'
|
|
114
|
+
: 'Backend provider.');
|
|
104
115
|
argParser.addOption('channel', abbr: 'c', help: 'Channel.');
|
|
105
116
|
argParser.addOption('bundle-id', abbr: 'i', help: 'Bundle id.');
|
|
106
117
|
}
|
|
@@ -112,7 +123,7 @@ class ChannelSetCommand extends FlutterPatcherCommand {
|
|
|
112
123
|
String get name => 'set';
|
|
113
124
|
|
|
114
125
|
@override
|
|
115
|
-
String get description => '
|
|
126
|
+
String get description => 'Set the active bundle for a channel.';
|
|
116
127
|
|
|
117
128
|
@override
|
|
118
129
|
Future<int> run() => runGuarded(() async {
|
|
@@ -134,6 +145,6 @@ class ChannelSetCommand extends FlutterPatcherCommand {
|
|
|
134
145
|
await steps.run('Promoting $bundleId to $channel',
|
|
135
146
|
() => promoteBundle(backend, bundleId, channel));
|
|
136
147
|
steps.summary();
|
|
137
|
-
|
|
148
|
+
stdout.writeln(' ${dim('→')} channel ${cyan(channel)} = ${cyan(bundleId)}');
|
|
138
149
|
});
|
|
139
150
|
}
|
|
@@ -32,6 +32,9 @@ void _saveProjectJson(Map<String, dynamic> json) {
|
|
|
32
32
|
final file = configCandidates().first;
|
|
33
33
|
file.parent.createSync(recursive: true);
|
|
34
34
|
file.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(json));
|
|
35
|
+
if (!Platform.isWindows) {
|
|
36
|
+
Process.runSync('chmod', ['600', file.path]);
|
|
37
|
+
}
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
class ConfigGetCommand extends FlutterPatcherCommand {
|
|
@@ -35,8 +35,10 @@ class ConsoleCommand extends FlutterPatcherCommand {
|
|
|
35
35
|
Future<int> run() => runGuarded(() async {
|
|
36
36
|
final consoleDir = _findConsoleDir();
|
|
37
37
|
if (consoleDir == null) {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
throw PackException(
|
|
39
|
+
'Console package not found (expected packages/console).',
|
|
40
|
+
1,
|
|
41
|
+
);
|
|
40
42
|
}
|
|
41
43
|
if (argResults!['open'] as bool) {
|
|
42
44
|
banner('console · open');
|
|
@@ -53,11 +55,8 @@ class ConsoleCommand extends FlutterPatcherCommand {
|
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
57
|
banner('console');
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
kv('dir', consoleDir.path),
|
|
60
|
-
kv('run', 'flutter run -d chrome'),
|
|
61
|
-
]);
|
|
58
|
+
info('Open the web console with Flutter:');
|
|
59
|
+
step('dir ${dim(consoleDir.path)}');
|
|
60
|
+
step('run ${cyan('flutter run -d chrome')}');
|
|
62
61
|
});
|
|
63
62
|
}
|
|
@@ -6,16 +6,20 @@ import '../backend.dart';
|
|
|
6
6
|
import '../cli_base.dart';
|
|
7
7
|
import '../config.dart';
|
|
8
8
|
import '../operations.dart';
|
|
9
|
+
import '../pack.dart';
|
|
9
10
|
import '../ui/ui.dart';
|
|
10
11
|
import '../util.dart';
|
|
11
12
|
|
|
12
13
|
/// `flutter-ota deploy` — zip + upload + register a new bundle.
|
|
13
14
|
class DeployCommand extends FlutterPatcherCommand {
|
|
14
15
|
DeployCommand({this.config, this.backendOverride}) {
|
|
16
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
15
17
|
argParser.addOption(
|
|
16
18
|
'backend',
|
|
17
19
|
abbr: 'b',
|
|
18
|
-
help:
|
|
20
|
+
help: detected != null
|
|
21
|
+
? 'Backend provider [detected: $detected].'
|
|
22
|
+
: 'Backend provider.',
|
|
19
23
|
);
|
|
20
24
|
argParser.addOption('source', abbr: 's', help: 'Source directory to zip + upload.');
|
|
21
25
|
argParser.addOption('channel', abbr: 'c', help: 'Target channel.');
|
|
@@ -68,14 +72,19 @@ class DeployCommand extends FlutterPatcherCommand {
|
|
|
68
72
|
final fingerprintHash = argResults!['fingerprint-hash'] as String?;
|
|
69
73
|
final keyPath = argResults!['key'] as String?;
|
|
70
74
|
String? signingKey;
|
|
71
|
-
if (keyPath != null)
|
|
75
|
+
if (keyPath != null) {
|
|
76
|
+
final keyFile = File(keyPath);
|
|
77
|
+
if (!keyFile.existsSync()) {
|
|
78
|
+
throw PackException('Signing key file not found: $keyPath', 64);
|
|
79
|
+
}
|
|
80
|
+
signingKey = keyFile.readAsStringSync().trim();
|
|
81
|
+
}
|
|
72
82
|
final resolvedGitCommitHash =
|
|
73
83
|
argResults!['git-commit-hash'] as String? ??
|
|
74
84
|
await gitCommitHash(source);
|
|
75
85
|
final bundleId = argResults!['bundle-id'] as String?;
|
|
76
86
|
|
|
77
87
|
banner('deploy');
|
|
78
|
-
info('channel ${cyan(channel)} platform ${cyan(platform)} source ${dim(source)}');
|
|
79
88
|
|
|
80
89
|
final steps = Steps('deploy');
|
|
81
90
|
final bundle = await _deployWithPhases(steps, backend, source, channel,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
1
2
|
import 'dart:io';
|
|
2
3
|
|
|
4
|
+
import 'package:http/http.dart' as http;
|
|
5
|
+
|
|
3
6
|
import 'package:flutter_ota_kit_cli/flutter_ota_kit_cli.dart';
|
|
4
7
|
|
|
5
8
|
import '../ui/ui.dart';
|
|
@@ -7,7 +10,10 @@ import '../ui/ui.dart';
|
|
|
7
10
|
/// `flutter-ota doctor` — environment + backend connectivity check.
|
|
8
11
|
class DoctorCommand extends FlutterPatcherCommand {
|
|
9
12
|
DoctorCommand({this.config, this.backendOverride}) {
|
|
10
|
-
|
|
13
|
+
final detected = config?.provider ?? loadConfig()?.provider;
|
|
14
|
+
argParser.addOption('backend', abbr: 'b', help: detected != null
|
|
15
|
+
? 'Backend provider [detected: $detected].'
|
|
16
|
+
: 'Backend provider.');
|
|
11
17
|
}
|
|
12
18
|
|
|
13
19
|
final FlutterPatcherConfig? config;
|
|
@@ -41,10 +47,8 @@ class DoctorCommand extends FlutterPatcherCommand {
|
|
|
41
47
|
steps.success('Channel ${cfg.channel}');
|
|
42
48
|
steps.success('Platform ${cfg.platform}');
|
|
43
49
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
await _checkPocketBase(cfg, steps);
|
|
47
|
-
}
|
|
50
|
+
// Backend-specific pre-flight checks.
|
|
51
|
+
await _checkProvider(cfg, steps);
|
|
48
52
|
|
|
49
53
|
try {
|
|
50
54
|
final backend = requireBackend(cfg, override: backendOverride);
|
|
@@ -64,6 +68,123 @@ class DoctorCommand extends FlutterPatcherCommand {
|
|
|
64
68
|
steps.summary();
|
|
65
69
|
});
|
|
66
70
|
|
|
71
|
+
Future<void> _checkProvider(FlutterPatcherConfig cfg, Steps steps) async {
|
|
72
|
+
switch (cfg.provider) {
|
|
73
|
+
case 'supabase':
|
|
74
|
+
await _checkSupabase(cfg, steps);
|
|
75
|
+
case 'postgres':
|
|
76
|
+
await _checkPostgres(cfg, steps);
|
|
77
|
+
case 'cloudflare':
|
|
78
|
+
await _checkCloudflare(cfg, steps);
|
|
79
|
+
case 'aws':
|
|
80
|
+
await _checkAws(cfg, steps);
|
|
81
|
+
case 'pocketbase':
|
|
82
|
+
await _checkPocketBase(cfg, steps);
|
|
83
|
+
default:
|
|
84
|
+
steps.fail('Unknown provider: ${cfg.provider}');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Future<void> _checkSupabase(
|
|
89
|
+
FlutterPatcherConfig cfg,
|
|
90
|
+
Steps steps,
|
|
91
|
+
) async {
|
|
92
|
+
final url = cfg.supabase.url;
|
|
93
|
+
if (url == null || url.isEmpty) {
|
|
94
|
+
steps.fail('SUPABASE_URL not set');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
final res = await http.get(Uri.parse('$url/rest/v1/')).timeout(
|
|
99
|
+
const Duration(seconds: 5),
|
|
100
|
+
);
|
|
101
|
+
if (res.statusCode < 400) {
|
|
102
|
+
steps.success('Supabase reachable ($url)');
|
|
103
|
+
} else {
|
|
104
|
+
steps.fail('Supabase returned ${res.statusCode}');
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
steps.fail('Supabase unreachable — $e');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
Future<void> _checkPostgres(
|
|
112
|
+
FlutterPatcherConfig cfg,
|
|
113
|
+
Steps steps,
|
|
114
|
+
) async {
|
|
115
|
+
final host = cfg.postgres.host;
|
|
116
|
+
if (host == null || host.isEmpty) {
|
|
117
|
+
steps.fail('POSTGRES_HOST not set');
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
final port = int.tryParse(cfg.postgres.port ?? '5432') ?? 5432;
|
|
121
|
+
try {
|
|
122
|
+
final socket = await Socket.connect(host, port).timeout(
|
|
123
|
+
const Duration(seconds: 5),
|
|
124
|
+
);
|
|
125
|
+
await socket.close();
|
|
126
|
+
steps.success('Postgres reachable ($host:$port)');
|
|
127
|
+
} catch (e) {
|
|
128
|
+
steps.fail('Postgres unreachable — $e');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Future<void> _checkCloudflare(
|
|
133
|
+
FlutterPatcherConfig cfg,
|
|
134
|
+
Steps steps,
|
|
135
|
+
) async {
|
|
136
|
+
final accountId = cfg.cloudflare.accountId;
|
|
137
|
+
final apiToken = cfg.cloudflare.apiToken;
|
|
138
|
+
if (accountId == null || accountId.isEmpty) {
|
|
139
|
+
steps.fail('CLOUDFLARE_ACCOUNT_ID not set');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (apiToken == null || apiToken.isEmpty) {
|
|
143
|
+
steps.fail('CLOUDFLARE_API_TOKEN not set');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
final res = await http
|
|
148
|
+
.get(
|
|
149
|
+
Uri.parse(
|
|
150
|
+
'https://api.cloudflare.com/client/v4/accounts/$accountId',
|
|
151
|
+
),
|
|
152
|
+
headers: {
|
|
153
|
+
'Authorization': 'Bearer $apiToken',
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
.timeout(const Duration(seconds: 5));
|
|
158
|
+
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
159
|
+
if (body['success'] == true) {
|
|
160
|
+
steps.success('Cloudflare account reachable');
|
|
161
|
+
} else {
|
|
162
|
+
final errors = body['errors'] as List? ?? [];
|
|
163
|
+
final msg = errors.isNotEmpty ? errors.first['message'] : res.body;
|
|
164
|
+
steps.fail('Cloudflare API error: $msg');
|
|
165
|
+
}
|
|
166
|
+
} catch (e) {
|
|
167
|
+
steps.fail('Cloudflare unreachable — $e');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
Future<void> _checkAws(
|
|
172
|
+
FlutterPatcherConfig cfg,
|
|
173
|
+
Steps steps,
|
|
174
|
+
) async {
|
|
175
|
+
final bucket = cfg.aws.bucket;
|
|
176
|
+
if (bucket == null || bucket.isEmpty) {
|
|
177
|
+
steps.fail('AWS_BUCKET not set');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
final region = cfg.aws.region;
|
|
181
|
+
if (region == null || region.isEmpty) {
|
|
182
|
+
steps.fail('AWS_REGION not set');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
steps.success('AWS S3 bucket "$bucket" in $region');
|
|
186
|
+
}
|
|
187
|
+
|
|
67
188
|
Future<void> _checkPocketBase(
|
|
68
189
|
FlutterPatcherConfig cfg,
|
|
69
190
|
Steps steps,
|
|
@@ -79,8 +200,8 @@ class DoctorCommand extends FlutterPatcherCommand {
|
|
|
79
200
|
}
|
|
80
201
|
final url = cfg.pocketbase.url;
|
|
81
202
|
if (url != null && url.isNotEmpty) {
|
|
203
|
+
final client = PocketBaseClient(url);
|
|
82
204
|
try {
|
|
83
|
-
final client = PocketBaseClient(url);
|
|
84
205
|
final ok = await client.health();
|
|
85
206
|
if (ok) {
|
|
86
207
|
steps.success('PocketBase reachable');
|
|
@@ -89,6 +210,8 @@ class DoctorCommand extends FlutterPatcherCommand {
|
|
|
89
210
|
}
|
|
90
211
|
} catch (e) {
|
|
91
212
|
steps.fail('PocketBase unreachable — $e');
|
|
213
|
+
} finally {
|
|
214
|
+
client.close();
|
|
92
215
|
}
|
|
93
216
|
}
|
|
94
217
|
}
|
|
@@ -25,6 +25,7 @@ class FingerprintCommand extends FlutterPatcherCommand {
|
|
|
25
25
|
banner(name);
|
|
26
26
|
final source = argResults!['source'] as String;
|
|
27
27
|
final hash = generateFingerprint(source);
|
|
28
|
-
|
|
28
|
+
step('source ${dim(source)}');
|
|
29
|
+
step('hash ${cyan(hash)}');
|
|
29
30
|
});
|
|
30
31
|
}
|
|
@@ -227,8 +227,8 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
227
227
|
env['SUPABASE_SERVICE_ROLE_KEY'],
|
|
228
228
|
),
|
|
229
229
|
anonKey: _p(
|
|
230
|
-
'Supabase
|
|
231
|
-
env['
|
|
230
|
+
'Supabase anon key',
|
|
231
|
+
env['SUPABASE_ANON_KEY'],
|
|
232
232
|
),
|
|
233
233
|
bucket: _p('Storage bucket', env['SUPABASE_BUCKET'] ?? 'bundles'),
|
|
234
234
|
basePath: _p('Storage base path', env['SUPABASE_BASE_PATH']),
|
|
@@ -267,12 +267,14 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
267
267
|
step('3. Provision the backend once: ${dim('flutter-ota migrate supabase')}');
|
|
268
268
|
} else if (provider == 'postgres') {
|
|
269
269
|
step('3. Provision the backend: ${dim('flutter-ota migrate postgres')}');
|
|
270
|
+
} else if (provider == 'cloudflare') {
|
|
271
|
+
step('3. Provision the backend: ${dim('flutter-ota migrate cloudflare')}');
|
|
272
|
+
} else if (provider == 'aws') {
|
|
273
|
+
step('3. Provision the backend: ${dim('flutter-ota migrate aws')}');
|
|
270
274
|
} else if (provider == 'pocketbase') {
|
|
271
|
-
step('3.
|
|
272
|
-
'${dim('flutter-ota pocketbase install')} then '
|
|
273
|
-
'${dim('flutter-ota pocketbase serve')}.');
|
|
275
|
+
step('3. Provision the backend: ${dim('flutter-ota migrate pocketbase')}');
|
|
274
276
|
}
|
|
275
|
-
step('4. Build a patch: ${dim('flutter-ota build --
|
|
277
|
+
step('4. Build a patch: ${dim('flutter-ota build --version 1.0.1')} '
|
|
276
278
|
'then ${dim('flutter-ota deploy')}');
|
|
277
279
|
});
|
|
278
280
|
|
|
@@ -420,7 +422,7 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
420
422
|
default:
|
|
421
423
|
final c = cfg.supabase;
|
|
422
424
|
entries['SUPABASE_URL'] = c.url ?? '';
|
|
423
|
-
entries['
|
|
425
|
+
entries['SUPABASE_ANON_KEY'] = c.anonKey ?? '';
|
|
424
426
|
entries['SUPABASE_BUCKET'] = c.bucket ?? 'bundles';
|
|
425
427
|
entries['CHANNEL'] = cfg.channel;
|
|
426
428
|
entries['APP_VERSION'] = '';
|
|
@@ -462,7 +464,14 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
462
464
|
step('Added .env to .gitignore');
|
|
463
465
|
}
|
|
464
466
|
|
|
465
|
-
String _q(String? s)
|
|
467
|
+
String _q(String? s) {
|
|
468
|
+
if (s == null) return "''";
|
|
469
|
+
final escaped = s
|
|
470
|
+
.replaceAll('\\', '\\\\')
|
|
471
|
+
.replaceAll('\$', '\\\$')
|
|
472
|
+
.replaceAll("'", "\\'");
|
|
473
|
+
return "'$escaped'";
|
|
474
|
+
}
|
|
466
475
|
|
|
467
476
|
/// Build the body of `lib/flutter_ota_kit_setup.dart` for the selected backend.
|
|
468
477
|
///
|
|
@@ -489,7 +498,7 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
489
498
|
password: const String.fromEnvironment('POSTGRES_PASSWORD', defaultValue: ''),
|
|
490
499
|
servingBaseUrl: const String.fromEnvironment('POSTGRES_SERVING_BASE_URL', defaultValue: ${_q(c.servingBaseUrl ?? '')}),
|
|
491
500
|
channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
|
|
492
|
-
platform: Platform.
|
|
501
|
+
platform: Platform.${cfg.platform},
|
|
493
502
|
updateStrategy: UpdateStrategy.appVersion,
|
|
494
503
|
appVersion: $appVersionExpr,
|
|
495
504
|
));''';
|
|
@@ -506,7 +515,7 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
506
515
|
secretAccessKey: const String.fromEnvironment('R2_SECRET_ACCESS_KEY', defaultValue: ''),
|
|
507
516
|
basePath: const String.fromEnvironment('R2_BASE_PATH', defaultValue: ${_q(c.r2BasePath ?? '')}),
|
|
508
517
|
channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
|
|
509
|
-
platform: Platform.
|
|
518
|
+
platform: Platform.${cfg.platform},
|
|
510
519
|
updateStrategy: UpdateStrategy.appVersion,
|
|
511
520
|
appVersion: $appVersionExpr,
|
|
512
521
|
));''';
|
|
@@ -522,7 +531,7 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
522
531
|
basePath: const String.fromEnvironment('AWS_BASE_PATH', defaultValue: ${_q(c.basePath ?? '')}),
|
|
523
532
|
endpoint: const String.fromEnvironment('AWS_ENDPOINT', defaultValue: ${_q(c.endpoint ?? '')}),
|
|
524
533
|
channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
|
|
525
|
-
platform: Platform.
|
|
534
|
+
platform: Platform.${cfg.platform},
|
|
526
535
|
updateStrategy: UpdateStrategy.appVersion,
|
|
527
536
|
appVersion: $appVersionExpr,
|
|
528
537
|
));''';
|
|
@@ -537,7 +546,7 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
537
546
|
bundlesCollection: const String.fromEnvironment('POCKETBASE_BUNDLES_COLLECTION', defaultValue: ${_q(c.bundlesCollection)}),
|
|
538
547
|
bundlesBucket: const String.fromEnvironment('POCKETBASE_BUCKET', defaultValue: ${_q(c.bundlesBucket)}),
|
|
539
548
|
channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
|
|
540
|
-
platform: Platform.
|
|
549
|
+
platform: Platform.${cfg.platform},
|
|
541
550
|
updateStrategy: UpdateStrategy.appVersion,
|
|
542
551
|
appVersion: $appVersionExpr,
|
|
543
552
|
));''';
|
|
@@ -548,10 +557,10 @@ class InitCommand extends FlutterPatcherCommand {
|
|
|
548
557
|
'''
|
|
549
558
|
FlutterPatcher.configureSupabase(SupabaseUpdateConfig(
|
|
550
559
|
supabaseUrl: const String.fromEnvironment('SUPABASE_URL', defaultValue: ${_q(c.url ?? '')}),
|
|
551
|
-
anonKey: const String.fromEnvironment('
|
|
560
|
+
anonKey: const String.fromEnvironment('SUPABASE_ANON_KEY', defaultValue: ''),
|
|
552
561
|
bucket: const String.fromEnvironment('SUPABASE_BUCKET', defaultValue: ${_q(c.bucket ?? 'bundles')}),
|
|
553
562
|
channel: const String.fromEnvironment('CHANNEL', defaultValue: $channel),
|
|
554
|
-
platform: Platform.
|
|
563
|
+
platform: Platform.${cfg.platform},
|
|
555
564
|
updateStrategy: UpdateStrategy.appVersion,
|
|
556
565
|
appVersion: $appVersionExpr,
|
|
557
566
|
sdkVersion: const String.fromEnvironment('SDK_VERSION', defaultValue: '1.0.0'),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import 'dart:convert';
|
|
2
|
+
import 'dart:io';
|
|
2
3
|
|
|
3
4
|
import '../cli_base.dart';
|
|
4
5
|
import '../config.dart';
|
|
@@ -53,5 +54,8 @@ class KeysCommand extends FlutterPatcherCommand {
|
|
|
53
54
|
final file = configCandidates().first;
|
|
54
55
|
file.parent.createSync(recursive: true);
|
|
55
56
|
file.writeAsStringSync(const JsonEncoder.withIndent(' ').convert(json));
|
|
57
|
+
if (!Platform.isWindows) {
|
|
58
|
+
Process.runSync('chmod', ['600', file.path]);
|
|
59
|
+
}
|
|
56
60
|
}
|
|
57
61
|
}
|