@_nazmiforreal/flutter-ota 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flutter-patcher.js +29 -12
- package/dart-src/packages/cli-tools/.dart_tool/package_graph.json +1 -1
- package/dart-src/packages/cli-tools/lib/flutter_patcher_cli.dart +2 -1
- package/dart-src/packages/cli-tools/lib/src/cli_base.dart +2 -0
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +1 -2
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +4 -5
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +4 -5
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +4 -5
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +1 -2
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +1 -2
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +1 -2
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +1 -2
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +1 -2
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +1 -2
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +175 -62
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +1 -2
- package/dart-src/packages/cli-tools/lib/src/runner.dart +73 -0
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +3 -1
- package/dart-src/packages/cli-tools/pubspec.lock +1 -1
- package/dart-src/packages/cli-tools/pubspec.yaml +1 -1
- package/dart-src/plugins/supabase/lib/src/supabase_client_http.dart +55 -19
- package/package.json +1 -1
- package/scripts/postinstall.js +14 -3
package/bin/flutter-patcher.js
CHANGED
|
@@ -34,29 +34,46 @@ const binPath = path.join(binDir, binName);
|
|
|
34
34
|
const sourceEntry = path.join(
|
|
35
35
|
binDir, '..', 'dart-src', 'packages', 'cli-tools', 'bin', 'flutter_patcher.dart',
|
|
36
36
|
);
|
|
37
|
+
const cliDir = path.dirname(path.dirname(sourceEntry));
|
|
37
38
|
|
|
38
|
-
function
|
|
39
|
-
|
|
39
|
+
function chmodIfNeeded(p) {
|
|
40
|
+
try {
|
|
41
|
+
const st = fs.statSync(p);
|
|
42
|
+
// Ensure owner-executable bit is set.
|
|
43
|
+
if (!(st.mode & 0o100)) fs.chmodSync(p, st.mode | 0o755);
|
|
44
|
+
} catch (_) {
|
|
45
|
+
// Non-fatal.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function run(bin, args) {
|
|
50
|
+
const res = spawnSync(bin, args, { stdio: 'inherit', windowsHide: false });
|
|
40
51
|
process.exit(res.status == null ? 1 : res.status);
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
if (fs.existsSync(binPath)) {
|
|
55
|
+
chmodIfNeeded(binPath);
|
|
44
56
|
run(binPath, process.argv.slice(2));
|
|
45
57
|
} else if (
|
|
46
58
|
spawnSync('dart', ['--version'], { stdio: 'ignore' }).status === 0 &&
|
|
47
59
|
fs.existsSync(sourceEntry)
|
|
48
60
|
) {
|
|
49
|
-
// Ensure dependencies are resolved before running from source.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
// Ensure dependencies are resolved before running from source. We only run
|
|
62
|
+
// `dart pub get` when the package config is missing, so repeated invocations
|
|
63
|
+
// don't re-resolve dependencies on every command.
|
|
64
|
+
const configPath = path.join(cliDir, '.dart_tool', 'package_config.json');
|
|
65
|
+
if (!fs.existsSync(configPath)) {
|
|
66
|
+
const getRes = spawnSync('dart', ['pub', 'get'], { stdio: 'inherit', cwd: cliDir });
|
|
67
|
+
if (getRes.status !== 0) {
|
|
68
|
+
console.error(
|
|
69
|
+
'flutter-patcher: `dart pub get` failed. Install dependencies or build ' +
|
|
70
|
+
`a prebuilt manually:\n cd ${cliDir} && dart compile exe ` +
|
|
71
|
+
`bin/flutter_patcher.dart -o ${binPath}`,
|
|
72
|
+
);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
59
75
|
}
|
|
76
|
+
chmodIfNeeded(binPath);
|
|
60
77
|
run('dart', [sourceEntry, ...process.argv.slice(2)]);
|
|
61
78
|
} else {
|
|
62
79
|
console.error(
|
|
@@ -31,6 +31,7 @@ export 'src/commands/console.dart';
|
|
|
31
31
|
|
|
32
32
|
import 'src/commands/build.dart';
|
|
33
33
|
import 'src/commands/bundle.dart';
|
|
34
|
+
import 'src/runner.dart';
|
|
34
35
|
import 'src/commands/channel.dart';
|
|
35
36
|
import 'src/commands/config_command.dart';
|
|
36
37
|
import 'src/commands/console.dart';
|
|
@@ -44,7 +45,7 @@ import 'src/commands/rollback.dart';
|
|
|
44
45
|
|
|
45
46
|
/// Entry point: build the command runner and dispatch [args].
|
|
46
47
|
Future<int> run(List<String> args) async {
|
|
47
|
-
final runner =
|
|
48
|
+
final runner = FlutterPatcherRunner(
|
|
48
49
|
'flutter_patcher',
|
|
49
50
|
'flutter_patcher CLI — OTA code push for Flutter (hot-updater compatible).',
|
|
50
51
|
)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import 'package:args/command_runner.dart';
|
|
2
1
|
import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
3
2
|
|
|
4
3
|
import '../ui/ui.dart';
|
|
@@ -8,7 +7,7 @@ import '../ui/ui.dart';
|
|
|
8
7
|
///
|
|
9
8
|
/// Includes every ABI found in the APK so a single bundle installs on all
|
|
10
9
|
/// device architectures.
|
|
11
|
-
class BuildCommand extends
|
|
10
|
+
class BuildCommand extends FlutterPatcherCommand {
|
|
12
11
|
BuildCommand() {
|
|
13
12
|
argParser
|
|
14
13
|
..addOption(
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import 'package:args/args.dart';
|
|
2
|
-
import 'package:args/command_runner.dart';
|
|
3
2
|
import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
4
3
|
|
|
5
4
|
import '../ui/ui.dart';
|
|
6
5
|
|
|
7
6
|
/// `flutter_patcher bundle` — manage bundles.
|
|
8
|
-
class BundleCommand extends
|
|
7
|
+
class BundleCommand extends FlutterPatcherCommand {
|
|
9
8
|
BundleCommand({this.config, this.backendOverride});
|
|
10
9
|
|
|
11
10
|
final FlutterPatcherConfig? config;
|
|
@@ -28,7 +27,7 @@ class BundleCommand extends Command<int> {
|
|
|
28
27
|
});
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
class BundleListCommand extends
|
|
30
|
+
class BundleListCommand extends FlutterPatcherCommand {
|
|
32
31
|
BundleListCommand({this.config, this.backendOverride});
|
|
33
32
|
|
|
34
33
|
final FlutterPatcherConfig? config;
|
|
@@ -94,7 +93,7 @@ class BundleListCommand extends Command<int> {
|
|
|
94
93
|
});
|
|
95
94
|
}
|
|
96
95
|
|
|
97
|
-
class BundleDeleteCommand extends
|
|
96
|
+
class BundleDeleteCommand extends FlutterPatcherCommand {
|
|
98
97
|
BundleDeleteCommand({this.config, this.backendOverride});
|
|
99
98
|
|
|
100
99
|
final FlutterPatcherConfig? config;
|
|
@@ -128,7 +127,7 @@ class BundleDeleteCommand extends Command<int> {
|
|
|
128
127
|
});
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
class BundlePromoteCommand extends
|
|
130
|
+
class BundlePromoteCommand extends FlutterPatcherCommand {
|
|
132
131
|
BundlePromoteCommand({this.config, this.backendOverride});
|
|
133
132
|
|
|
134
133
|
final FlutterPatcherConfig? config;
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import 'package:args/args.dart';
|
|
2
|
-
import 'package:args/command_runner.dart';
|
|
3
2
|
import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
4
3
|
|
|
5
4
|
import '../ui/ui.dart';
|
|
6
5
|
|
|
7
6
|
/// `flutter_patcher channel` — manage channels.
|
|
8
|
-
class ChannelCommand extends
|
|
7
|
+
class ChannelCommand extends FlutterPatcherCommand {
|
|
9
8
|
final FlutterPatcherConfig? config;
|
|
10
9
|
final Backend? backendOverride;
|
|
11
10
|
|
|
@@ -32,7 +31,7 @@ class ChannelCommand extends Command<int> {
|
|
|
32
31
|
});
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
class ChannelListCommand extends
|
|
34
|
+
class ChannelListCommand extends FlutterPatcherCommand {
|
|
36
35
|
ChannelListCommand({this.config, this.backendOverride});
|
|
37
36
|
|
|
38
37
|
final FlutterPatcherConfig? config;
|
|
@@ -62,7 +61,7 @@ class ChannelListCommand extends Command<int> {
|
|
|
62
61
|
});
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
class ChannelGetCommand extends
|
|
64
|
+
class ChannelGetCommand extends FlutterPatcherCommand {
|
|
66
65
|
ChannelGetCommand({this.config, this.backendOverride});
|
|
67
66
|
|
|
68
67
|
final FlutterPatcherConfig? config;
|
|
@@ -103,7 +102,7 @@ class ChannelGetCommand extends Command<int> {
|
|
|
103
102
|
});
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
class ChannelSetCommand extends
|
|
105
|
+
class ChannelSetCommand extends FlutterPatcherCommand {
|
|
107
106
|
ChannelSetCommand({this.config, this.backendOverride});
|
|
108
107
|
|
|
109
108
|
final FlutterPatcherConfig? config;
|
|
@@ -2,13 +2,12 @@ import 'dart:convert';
|
|
|
2
2
|
import 'dart:io';
|
|
3
3
|
|
|
4
4
|
import 'package:args/args.dart';
|
|
5
|
-
import 'package:args/command_runner.dart';
|
|
6
5
|
|
|
7
6
|
import '../cli_base.dart';
|
|
8
7
|
import '../config.dart';
|
|
9
8
|
|
|
10
9
|
/// `flutter_patcher config` — get/set/list config values.
|
|
11
|
-
class ConfigCommand extends
|
|
10
|
+
class ConfigCommand extends FlutterPatcherCommand {
|
|
12
11
|
ConfigCommand() {
|
|
13
12
|
addSubcommand(ConfigGetCommand());
|
|
14
13
|
addSubcommand(ConfigSetCommand());
|
|
@@ -45,7 +44,7 @@ void _saveProjectJson(Map<String, dynamic> json) {
|
|
|
45
44
|
);
|
|
46
45
|
}
|
|
47
46
|
|
|
48
|
-
class ConfigGetCommand extends
|
|
47
|
+
class ConfigGetCommand extends FlutterPatcherCommand {
|
|
49
48
|
@override
|
|
50
49
|
String get name => 'get';
|
|
51
50
|
|
|
@@ -72,7 +71,7 @@ class ConfigGetCommand extends Command<int> {
|
|
|
72
71
|
});
|
|
73
72
|
}
|
|
74
73
|
|
|
75
|
-
class ConfigSetCommand extends
|
|
74
|
+
class ConfigSetCommand extends FlutterPatcherCommand {
|
|
76
75
|
@override
|
|
77
76
|
String get name => 'set';
|
|
78
77
|
|
|
@@ -98,7 +97,7 @@ class ConfigSetCommand extends Command<int> {
|
|
|
98
97
|
});
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
class ConfigListCommand extends
|
|
100
|
+
class ConfigListCommand extends FlutterPatcherCommand {
|
|
102
101
|
@override
|
|
103
102
|
String get name => 'list';
|
|
104
103
|
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import 'dart:io';
|
|
2
2
|
|
|
3
3
|
import 'package:args/args.dart';
|
|
4
|
-
import 'package:args/command_runner.dart';
|
|
5
4
|
import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
6
5
|
import 'package:path/path.dart' as p;
|
|
7
6
|
|
|
8
7
|
import '../ui/ui.dart';
|
|
9
8
|
|
|
10
9
|
/// `flutter_patcher console` — open the web console.
|
|
11
|
-
class ConsoleCommand extends
|
|
10
|
+
class ConsoleCommand extends FlutterPatcherCommand {
|
|
12
11
|
@override
|
|
13
12
|
String get name => 'console';
|
|
14
13
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import 'dart:io';
|
|
2
2
|
|
|
3
3
|
import 'package:args/args.dart';
|
|
4
|
-
import 'package:args/command_runner.dart';
|
|
5
4
|
|
|
6
5
|
import '../backend.dart';
|
|
7
6
|
import '../cli_base.dart';
|
|
@@ -11,7 +10,7 @@ import '../ui/ui.dart';
|
|
|
11
10
|
import '../util.dart';
|
|
12
11
|
|
|
13
12
|
/// `flutter_patcher deploy` — zip + upload + register a new bundle.
|
|
14
|
-
class DeployCommand extends
|
|
13
|
+
class DeployCommand extends FlutterPatcherCommand {
|
|
15
14
|
DeployCommand({this.config, this.backendOverride});
|
|
16
15
|
|
|
17
16
|
final FlutterPatcherConfig? config;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import 'dart:io';
|
|
2
2
|
|
|
3
3
|
import 'package:args/args.dart';
|
|
4
|
-
import 'package:args/command_runner.dart';
|
|
5
4
|
import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
6
5
|
|
|
7
6
|
import '../ui/ui.dart';
|
|
8
7
|
|
|
9
8
|
/// `flutter_patcher doctor` — environment + backend connectivity check.
|
|
10
|
-
class DoctorCommand extends
|
|
9
|
+
class DoctorCommand extends FlutterPatcherCommand {
|
|
11
10
|
DoctorCommand({this.config, this.backendOverride});
|
|
12
11
|
|
|
13
12
|
final FlutterPatcherConfig? config;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import 'dart:io';
|
|
2
2
|
|
|
3
3
|
import 'package:args/args.dart';
|
|
4
|
-
import 'package:args/command_runner.dart';
|
|
5
4
|
|
|
6
5
|
import '../cli_base.dart';
|
|
7
6
|
import '../util.dart';
|
|
8
7
|
|
|
9
8
|
/// `flutter_patcher fingerprint` — compute a build-time fingerprint hash.
|
|
10
|
-
class FingerprintCommand extends
|
|
9
|
+
class FingerprintCommand extends FlutterPatcherCommand {
|
|
11
10
|
@override
|
|
12
11
|
String get name => 'fingerprint';
|
|
13
12
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import 'dart:io';
|
|
2
2
|
|
|
3
3
|
import 'package:args/args.dart';
|
|
4
|
-
import 'package:args/command_runner.dart';
|
|
5
4
|
import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
6
5
|
|
|
7
6
|
import '../ui/ui.dart';
|
|
@@ -13,7 +12,7 @@ import '../ui/ui.dart';
|
|
|
13
12
|
/// corresponding environment variable so CI runs can be non-interactive. The
|
|
14
13
|
/// result is saved as `.flutter_patcher.json` (cwd) or the global config when
|
|
15
14
|
/// `--global` is passed.
|
|
16
|
-
class InitCommand extends
|
|
15
|
+
class InitCommand extends FlutterPatcherCommand {
|
|
17
16
|
InitCommand({this.config, this.backendOverride});
|
|
18
17
|
|
|
19
18
|
final FlutterPatcherConfig? config;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import 'dart:convert';
|
|
2
2
|
|
|
3
3
|
import 'package:args/args.dart';
|
|
4
|
-
import 'package:args/command_runner.dart';
|
|
5
4
|
|
|
6
5
|
import '../backend.dart';
|
|
7
6
|
import '../cli_base.dart';
|
|
@@ -10,7 +9,7 @@ import '../sign.dart';
|
|
|
10
9
|
import '../ui/ui.dart';
|
|
11
10
|
|
|
12
11
|
/// `flutter_patcher keys` — generate an Ed25519 keypair for bundle signing.
|
|
13
|
-
class KeysCommand extends
|
|
12
|
+
class KeysCommand extends FlutterPatcherCommand {
|
|
14
13
|
KeysCommand({this.config, this.backendOverride});
|
|
15
14
|
|
|
16
15
|
final FlutterPatcherConfig? config;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
1
2
|
import 'dart:io';
|
|
2
3
|
|
|
3
4
|
import 'package:args/args.dart';
|
|
4
|
-
import 'package:args/command_runner.dart';
|
|
5
5
|
import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
6
|
+
import 'package:http/http.dart' as http;
|
|
6
7
|
import 'package:path/path.dart' as p;
|
|
7
8
|
import 'package:postgres/postgres.dart';
|
|
8
9
|
|
|
9
10
|
import '../ui/ui.dart';
|
|
10
11
|
|
|
11
12
|
/// `flutter_patcher migrate` — run backend SQL migrations.
|
|
12
|
-
class MigrateCommand extends
|
|
13
|
+
class MigrateCommand extends FlutterPatcherCommand {
|
|
13
14
|
@override
|
|
14
15
|
String get name => 'migrate';
|
|
15
16
|
|
|
@@ -22,6 +23,9 @@ class MigrateCommand extends Command<int> {
|
|
|
22
23
|
..addOption('backend', abbr: 'b', help: 'Backend provider.')
|
|
23
24
|
..addOption('database-url',
|
|
24
25
|
help: 'Postgres connection string (or DATABASE_URL env).')
|
|
26
|
+
..addOption('management-key',
|
|
27
|
+
help: 'Supabase Management API key (or SUPABASE_MANAGEMENT_KEY env) — '
|
|
28
|
+
'runs migrations without a separate Postgres connection.')
|
|
25
29
|
..addOption('migrations-dir',
|
|
26
30
|
help: 'Directory of *.sql migration files (ordered by name).')
|
|
27
31
|
..addFlag('dry-run', help: 'Print migrations instead of applying them.');
|
|
@@ -36,6 +40,23 @@ class MigrateCommand extends Command<int> {
|
|
|
36
40
|
return p.join(p.dirname(Platform.script.path), '..', 'migrations', sub);
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
List<File> _listMigrations(String dir) {
|
|
44
|
+
final migrationsDir = Directory(dir);
|
|
45
|
+
if (!migrationsDir.existsSync()) {
|
|
46
|
+
throw StateError('Migrations directory not found: $dir');
|
|
47
|
+
}
|
|
48
|
+
final files = migrationsDir
|
|
49
|
+
.listSync()
|
|
50
|
+
.whereType<File>()
|
|
51
|
+
.where((f) => f.path.endsWith('.sql'))
|
|
52
|
+
.toList()
|
|
53
|
+
..sort((a, b) => a.path.compareTo(b.path));
|
|
54
|
+
if (files.isEmpty) {
|
|
55
|
+
throw StateError('No *.sql files in $dir');
|
|
56
|
+
}
|
|
57
|
+
return files;
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
List<String> _splitStatements(String sql) {
|
|
40
61
|
return sql
|
|
41
62
|
.split(';')
|
|
@@ -49,19 +70,7 @@ class MigrateCommand extends Command<int> {
|
|
|
49
70
|
final provider = argResults!['backend'] as String? ?? 'supabase';
|
|
50
71
|
final dir = argResults!['migrations-dir'] as String? ??
|
|
51
72
|
defaultMigrationsDir(provider);
|
|
52
|
-
final
|
|
53
|
-
if (!migrationsDir.existsSync()) {
|
|
54
|
-
throw StateError('Migrations directory not found: $dir');
|
|
55
|
-
}
|
|
56
|
-
final files = migrationsDir
|
|
57
|
-
.listSync()
|
|
58
|
-
.whereType<File>()
|
|
59
|
-
.where((f) => f.path.endsWith('.sql'))
|
|
60
|
-
.toList()
|
|
61
|
-
..sort((a, b) => a.path.compareTo(b.path));
|
|
62
|
-
if (files.isEmpty) {
|
|
63
|
-
throw StateError('No *.sql files in $dir');
|
|
64
|
-
}
|
|
73
|
+
final files = _listMigrations(dir);
|
|
65
74
|
|
|
66
75
|
if (argResults!['dry-run'] as bool) {
|
|
67
76
|
banner('migrate · dry-run');
|
|
@@ -72,8 +81,6 @@ class MigrateCommand extends Command<int> {
|
|
|
72
81
|
return;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
final url = argResults!['database-url'] as String? ??
|
|
76
|
-
Platform.environment['DATABASE_URL'];
|
|
77
84
|
if (provider == 'cloudflare' || provider == 'aws') {
|
|
78
85
|
banner('migrate');
|
|
79
86
|
box('migrate', [
|
|
@@ -81,58 +88,164 @@ class MigrateCommand extends Command<int> {
|
|
|
81
88
|
' • cloudflare → `wrangler d1 execute`',
|
|
82
89
|
' • aws → AWS console (S3/DynamoDB)',
|
|
83
90
|
'',
|
|
84
|
-
'The SQL files under $
|
|
91
|
+
'The SQL files under $dir show the intended schema.',
|
|
85
92
|
]);
|
|
86
93
|
return;
|
|
87
94
|
}
|
|
88
|
-
|
|
95
|
+
|
|
96
|
+
if (provider == 'supabase') {
|
|
97
|
+
final mgmtKey = argResults!['management-key'] as String? ??
|
|
98
|
+
Platform.environment['SUPABASE_MANAGEMENT_KEY'];
|
|
99
|
+
final pgUrl = argResults!['database-url'] as String? ??
|
|
100
|
+
Platform.environment['DATABASE_URL'];
|
|
101
|
+
if (mgmtKey != null && mgmtKey.isNotEmpty) {
|
|
102
|
+
await _runViaManagementApi(mgmtKey, files);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (pgUrl != null && pgUrl.isNotEmpty) {
|
|
106
|
+
await _runViaPostgres(pgUrl, files);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
throw StateError(
|
|
110
|
+
'For the supabase backend, provide either:\n'
|
|
111
|
+
' • --management-key (or SUPABASE_MANAGEMENT_KEY) — a Supabase '
|
|
112
|
+
'Management API key, OR\n'
|
|
113
|
+
' • --database-url (or DATABASE_URL) — a Postgres connection string.',
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Any other provider (e.g. postgres) requires a Postgres connection.
|
|
118
|
+
final pgUrl = argResults!['database-url'] as String? ??
|
|
119
|
+
Platform.environment['DATABASE_URL'];
|
|
120
|
+
if (pgUrl == null || pgUrl.isEmpty) {
|
|
89
121
|
throw StateError('Provide --database-url or set DATABASE_URL.');
|
|
90
122
|
}
|
|
123
|
+
await _runViaPostgres(pgUrl, files);
|
|
124
|
+
});
|
|
91
125
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
126
|
+
/// Run migrations through the Supabase Management API (no Postgres creds
|
|
127
|
+
/// needed). Derived from the Supabase project ref in the configured URL.
|
|
128
|
+
Future<void> _runViaManagementApi(String mgmtKey, List<File> files) async {
|
|
129
|
+
final cfg = resolveSupabaseConfig(
|
|
130
|
+
loadConfig() ??
|
|
131
|
+
FlutterPatcherConfig(
|
|
132
|
+
provider: 'supabase',
|
|
133
|
+
supabase: SupabaseConfigJson(),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
final ref = Uri.parse(cfg.supabaseUrl).host.split('.').first;
|
|
137
|
+
final endpoint =
|
|
138
|
+
'https://api.supabase.com/v1/projects/$ref/database/query';
|
|
139
|
+
banner('migrate · supabase (management api)');
|
|
140
|
+
for (final file in files) {
|
|
141
|
+
final sql = file.readAsStringSync();
|
|
142
|
+
final res = await http.post(
|
|
143
|
+
Uri.parse(endpoint),
|
|
144
|
+
headers: {
|
|
145
|
+
'Authorization': 'Bearer $mgmtKey',
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
},
|
|
148
|
+
body: jsonEncode({'query': sql}),
|
|
149
|
+
);
|
|
150
|
+
if (res.statusCode >= 400) {
|
|
151
|
+
throw StateError(
|
|
152
|
+
'Migration ${p.basename(file.path)} failed '
|
|
153
|
+
'(${res.statusCode}): ${res.body}',
|
|
101
154
|
);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
155
|
+
}
|
|
156
|
+
step('applied ${p.basename(file.path)}');
|
|
157
|
+
}
|
|
158
|
+
await _ensureSupabaseBucket();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Create the Supabase Storage bucket (public) if it doesn't exist, so
|
|
162
|
+
/// `deploy` can upload artifacts without a manual setup step.
|
|
163
|
+
Future<void> _ensureSupabaseBucket() async {
|
|
164
|
+
final storage = resolveSupabaseStorageConfig(
|
|
165
|
+
loadConfig() ??
|
|
166
|
+
FlutterPatcherConfig(
|
|
167
|
+
provider: 'supabase',
|
|
168
|
+
supabase: SupabaseConfigJson(),
|
|
169
|
+
),
|
|
170
|
+
);
|
|
171
|
+
if (storage.supabaseServiceRoleKey == null) {
|
|
172
|
+
box('migrate', [
|
|
173
|
+
'Skipped storage bucket creation — set supabase.serviceRoleKey '
|
|
174
|
+
'(or SUPABASE_SERVICE_ROLE_KEY) to auto-create the '
|
|
175
|
+
'"${storage.bucketName}" bucket.',
|
|
176
|
+
]);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
final res = await http.post(
|
|
180
|
+
Uri.parse('${storage.supabaseUrl}/storage/v1/bucket'),
|
|
181
|
+
headers: {
|
|
182
|
+
'Authorization': 'Bearer ${storage.supabaseServiceRoleKey}',
|
|
183
|
+
'apikey': storage.supabaseServiceRoleKey!,
|
|
184
|
+
'Content-Type': 'application/json',
|
|
185
|
+
},
|
|
186
|
+
body: jsonEncode({
|
|
187
|
+
'name': storage.bucketName,
|
|
188
|
+
'public': true,
|
|
189
|
+
}),
|
|
190
|
+
);
|
|
191
|
+
if (res.statusCode >= 400) {
|
|
192
|
+
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
193
|
+
final msg = (body['message'] ?? body['error'] ?? '').toString();
|
|
194
|
+
if (!msg.contains('already exists') && res.statusCode != 409) {
|
|
195
|
+
box('migrate', [
|
|
196
|
+
'Warning: could not create bucket "${storage.bucketName}": $msg',
|
|
197
|
+
]);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
step('ensured storage bucket "${storage.bucketName}"');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Original path: connect directly to Postgres and execute each statement.
|
|
205
|
+
Future<void> _runViaPostgres(String url, List<File> files) async {
|
|
206
|
+
final uri = Uri.parse(url);
|
|
207
|
+
final endpoint = Endpoint(
|
|
208
|
+
host: uri.host,
|
|
209
|
+
port: uri.port == 0 ? 5432 : uri.port,
|
|
210
|
+
database: uri.path.isEmpty ? 'postgres' : uri.path.substring(1),
|
|
211
|
+
username: uri.userInfo.isEmpty ? null : uri.userInfo.split(':').first,
|
|
212
|
+
password: uri.userInfo.contains(':')
|
|
213
|
+
? uri.userInfo.split(':').last
|
|
214
|
+
: null,
|
|
215
|
+
);
|
|
216
|
+
banner('migrate · postgres');
|
|
217
|
+
final conn = await Connection.open(
|
|
218
|
+
endpoint,
|
|
219
|
+
settings: ConnectionSettings(sslMode: SslMode.disable),
|
|
220
|
+
);
|
|
221
|
+
try {
|
|
222
|
+
await conn.execute(
|
|
223
|
+
'CREATE TABLE IF NOT EXISTS _flutter_patcher_migrations '
|
|
224
|
+
'(name text primary key, applied_at timestamptz default now())',
|
|
225
|
+
queryMode: QueryMode.simple,
|
|
226
|
+
);
|
|
227
|
+
for (final file in files) {
|
|
228
|
+
final name = p.basename(file.path);
|
|
229
|
+
final escaped = name.replaceAll("'", "''");
|
|
230
|
+
final existing = await conn.execute(
|
|
231
|
+
"SELECT 1 FROM _flutter_patcher_migrations WHERE name = '$escaped'",
|
|
232
|
+
queryMode: QueryMode.simple,
|
|
106
233
|
);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
'(name text primary key, applied_at timestamptz default now())',
|
|
111
|
-
queryMode: QueryMode.simple,
|
|
112
|
-
);
|
|
113
|
-
for (final file in files) {
|
|
114
|
-
final name = p.basename(file.path);
|
|
115
|
-
final escaped = name.replaceAll("'", "''");
|
|
116
|
-
final existing = await conn.execute(
|
|
117
|
-
"SELECT 1 FROM _flutter_patcher_migrations WHERE name = '$escaped'",
|
|
118
|
-
queryMode: QueryMode.simple,
|
|
119
|
-
);
|
|
120
|
-
if (existing.isNotEmpty) {
|
|
121
|
-
stdout.writeln(' ${dim('skip')} $name (already applied)');
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
final statements = _splitStatements(file.readAsStringSync());
|
|
125
|
-
for (final stmt in statements) {
|
|
126
|
-
await conn.execute(stmt, queryMode: QueryMode.simple);
|
|
127
|
-
}
|
|
128
|
-
await conn.execute(
|
|
129
|
-
"INSERT INTO _flutter_patcher_migrations(name) VALUES ('$escaped')",
|
|
130
|
-
queryMode: QueryMode.simple,
|
|
131
|
-
);
|
|
132
|
-
step('applied $name');
|
|
133
|
-
}
|
|
134
|
-
} finally {
|
|
135
|
-
await conn.close();
|
|
234
|
+
if (existing.isNotEmpty) {
|
|
235
|
+
stdout.writeln(' ${dim('skip')} $name (already applied)');
|
|
236
|
+
continue;
|
|
136
237
|
}
|
|
137
|
-
|
|
238
|
+
for (final stmt in _splitStatements(file.readAsStringSync())) {
|
|
239
|
+
await conn.execute(stmt, queryMode: QueryMode.simple);
|
|
240
|
+
}
|
|
241
|
+
await conn.execute(
|
|
242
|
+
"INSERT INTO _flutter_patcher_migrations(name) VALUES ('$escaped')",
|
|
243
|
+
queryMode: QueryMode.simple,
|
|
244
|
+
);
|
|
245
|
+
step('applied $name');
|
|
246
|
+
}
|
|
247
|
+
} finally {
|
|
248
|
+
await conn.close();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
138
251
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import 'package:args/args.dart';
|
|
2
|
-
import 'package:args/command_runner.dart';
|
|
3
2
|
import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
4
3
|
import 'package:flutter_patcher_core/flutter_patcher_core.dart';
|
|
5
4
|
|
|
6
5
|
import '../ui/ui.dart';
|
|
7
6
|
|
|
8
7
|
/// `flutter_patcher rollback` — roll a channel back to a previous bundle.
|
|
9
|
-
class RollbackCommand extends
|
|
8
|
+
class RollbackCommand extends FlutterPatcherCommand {
|
|
10
9
|
RollbackCommand({this.config, this.backendOverride});
|
|
11
10
|
|
|
12
11
|
final FlutterPatcherConfig? config;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
|
|
3
|
+
import 'package:args/command_runner.dart';
|
|
4
|
+
import 'ui/ui.dart';
|
|
5
|
+
|
|
6
|
+
/// Custom [CommandRunner] that renders a clean, colorized top-level help.
|
|
7
|
+
class FlutterPatcherRunner extends CommandRunner<int> {
|
|
8
|
+
FlutterPatcherRunner(super.name, super.description);
|
|
9
|
+
|
|
10
|
+
@override
|
|
11
|
+
String get usage {
|
|
12
|
+
final names = commands.keys.toList()..sort();
|
|
13
|
+
var maxName = 0;
|
|
14
|
+
for (final n in names) {
|
|
15
|
+
maxName = maxName < n.length ? n.length : maxName;
|
|
16
|
+
}
|
|
17
|
+
final buf = StringBuffer();
|
|
18
|
+
buf.writeln(' ${cyan('▶')} ${cyan(bold(executableName))} '
|
|
19
|
+
'${dim('·')} ${bold(description)}');
|
|
20
|
+
buf.writeln(' ${dim('─' * 60)}');
|
|
21
|
+
buf.writeln('');
|
|
22
|
+
buf.writeln(' ${bold('USAGE')}');
|
|
23
|
+
buf.writeln(' $executableName <command> [arguments]');
|
|
24
|
+
buf.writeln('');
|
|
25
|
+
buf.writeln(' ${bold('COMMANDS')}');
|
|
26
|
+
for (final n in names) {
|
|
27
|
+
final c = commands[n]!;
|
|
28
|
+
buf.writeln(' ${cyan(n.padRight(maxName))} ${dim(c.description)}');
|
|
29
|
+
}
|
|
30
|
+
buf.writeln('');
|
|
31
|
+
buf.writeln(' ${dim('Run "$executableName help <command>" for more about a command.')}');
|
|
32
|
+
return buf.toString();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@override
|
|
36
|
+
void printUsage([String? usage]) {
|
|
37
|
+
stdout.writeln(usage ?? this.usage);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
String _fullName(Command<int> command) {
|
|
42
|
+
final parts = <String>[command.name];
|
|
43
|
+
var p = command.parent;
|
|
44
|
+
while (p != null) {
|
|
45
|
+
parts.insert(0, p.name);
|
|
46
|
+
p = p.parent;
|
|
47
|
+
}
|
|
48
|
+
return parts.join(' ');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Base class for flutter_patcher commands; renders a clean, colorized
|
|
52
|
+
/// per-command help (used by `flutter_patcher <cmd> --help`).
|
|
53
|
+
abstract class FlutterPatcherCommand extends Command<int> {
|
|
54
|
+
@override
|
|
55
|
+
String get usage {
|
|
56
|
+
final buf = StringBuffer();
|
|
57
|
+
buf.writeln(' ${cyan(bold(name))} ${dim('·')} $description');
|
|
58
|
+
buf.writeln('');
|
|
59
|
+
buf.writeln(' ${bold('USAGE')}');
|
|
60
|
+
buf.writeln(' ${_fullName(this)} [arguments]');
|
|
61
|
+
if (argParser.options.isNotEmpty) {
|
|
62
|
+
buf.writeln('');
|
|
63
|
+
buf.writeln(' ${bold('OPTIONS')}');
|
|
64
|
+
buf.writeln(' ${argParser.usage.replaceAll('\n', '\n ')}');
|
|
65
|
+
}
|
|
66
|
+
return buf.toString();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
void printUsage([String? usage]) {
|
|
71
|
+
stdout.writeln(usage ?? this.usage);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -173,7 +173,9 @@ void box(String title, List<String> lines) {
|
|
|
173
173
|
final width = _boxWidth(lines, title);
|
|
174
174
|
final inner = width - 4;
|
|
175
175
|
final wrapped = <String>[];
|
|
176
|
-
for (final l in lines)
|
|
176
|
+
for (final l in lines) {
|
|
177
|
+
wrapped.addAll(_wrap(l, inner));
|
|
178
|
+
}
|
|
177
179
|
|
|
178
180
|
final titleText = ' $title ';
|
|
179
181
|
final tVisible = _dispWidth(titleText);
|
|
@@ -245,17 +245,23 @@ class _StorageBucket implements SupabaseStorageBucketLike {
|
|
|
245
245
|
String path,
|
|
246
246
|
int expiresIn,
|
|
247
247
|
) async {
|
|
248
|
-
final uri = Uri.parse(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
248
|
+
final uri = Uri.parse('$_baseUrl/storage/v1/object/sign/$_bucket/$path');
|
|
249
|
+
final res = await http.post(
|
|
250
|
+
uri,
|
|
251
|
+
headers: {..._headers, 'Content-Type': 'application/json'},
|
|
252
|
+
body: jsonEncode({'expiresIn': expiresIn}),
|
|
253
|
+
);
|
|
252
254
|
if (res.statusCode >= 400) {
|
|
253
255
|
return _MockSignedUrlResult(null, jsonDecode(res.body));
|
|
254
256
|
}
|
|
255
257
|
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
258
|
+
final signed = body['signedURL'] as String?;
|
|
259
|
+
// Supabase returns a relative path ("/object/sign/...?token=..."); make it
|
|
260
|
+
// absolute against the project base URL.
|
|
256
261
|
return _MockSignedUrlResult(
|
|
257
|
-
|
|
258
|
-
|
|
262
|
+
signed == null ? null : '$_baseUrl/storage/v1$signed',
|
|
263
|
+
null,
|
|
264
|
+
);
|
|
259
265
|
}
|
|
260
266
|
|
|
261
267
|
@override
|
|
@@ -287,6 +293,22 @@ class _StorageBucket implements SupabaseStorageBucketLike {
|
|
|
287
293
|
);
|
|
288
294
|
}
|
|
289
295
|
|
|
296
|
+
Future<void> _ensureBucket() async {
|
|
297
|
+
final res = await http.post(
|
|
298
|
+
Uri.parse('$_baseUrl/storage/v1/bucket'),
|
|
299
|
+
headers: {..._headers, 'Content-Type': 'application/json'},
|
|
300
|
+
body: jsonEncode({'name': _bucket, 'public': true}),
|
|
301
|
+
);
|
|
302
|
+
// 400/409 just mean it already exists — that's fine.
|
|
303
|
+
if (res.statusCode >= 400) {
|
|
304
|
+
final body = jsonDecode(res.body) as Map<String, dynamic>;
|
|
305
|
+
final msg = (body['message'] ?? body['error'] ?? '').toString();
|
|
306
|
+
if (!msg.contains('already exists') && res.statusCode != 409) {
|
|
307
|
+
throw StateError('Failed to create bucket "$_bucket": $msg');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
290
312
|
@override
|
|
291
313
|
Future<SupabaseUploadResult> upload(
|
|
292
314
|
String path,
|
|
@@ -294,20 +316,34 @@ class _StorageBucket implements SupabaseStorageBucketLike {
|
|
|
294
316
|
String? contentType,
|
|
295
317
|
String? cacheControl,
|
|
296
318
|
}) async {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
319
|
+
Future<_MockUploadResult> doUpload() async {
|
|
320
|
+
final uri = Uri.parse('$_baseUrl/storage/v1/object/$_bucket/$path');
|
|
321
|
+
final res = await http.post(
|
|
322
|
+
uri,
|
|
323
|
+
headers: {
|
|
324
|
+
..._headers,
|
|
325
|
+
'Content-Type': contentType ?? 'application/octet-stream',
|
|
326
|
+
if (cacheControl != null) 'Cache-Control': cacheControl,
|
|
327
|
+
},
|
|
328
|
+
body: fileBytes,
|
|
329
|
+
);
|
|
330
|
+
if (res.statusCode >= 400) {
|
|
331
|
+
return _MockUploadResult(null, jsonDecode(res.body));
|
|
332
|
+
}
|
|
333
|
+
return _MockUploadResult(jsonDecode(res.body), null);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
final first = await doUpload();
|
|
337
|
+
if (first.error != null) {
|
|
338
|
+
final msg =
|
|
339
|
+
(first.error is Map ? (first.error as Map)['message'] : first.error)
|
|
340
|
+
.toString();
|
|
341
|
+
if (msg.contains('Bucket not found')) {
|
|
342
|
+
await _ensureBucket();
|
|
343
|
+
return doUpload();
|
|
344
|
+
}
|
|
309
345
|
}
|
|
310
|
-
return
|
|
346
|
+
return first;
|
|
311
347
|
}
|
|
312
348
|
|
|
313
349
|
@override
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@_nazmiforreal/flutter-ota",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Command-line interface for flutter_patcher — an OTA code-push tool for Flutter, hot-updater compatible.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"flutter-patcher": "./bin/flutter-patcher.js"
|
package/scripts/postinstall.js
CHANGED
|
@@ -34,7 +34,17 @@ const binDir = path.join(pkgDir, 'bin');
|
|
|
34
34
|
const target = path.join(binDir, `flutter-patcher-${platformName}-${archName}${ext}`);
|
|
35
35
|
const cliDir = path.join(pkgDir, 'dart-src', 'packages', 'cli-tools');
|
|
36
36
|
|
|
37
|
+
function chmod(pathname) {
|
|
38
|
+
try {
|
|
39
|
+
const st = fs.statSync(pathname);
|
|
40
|
+
if (!(st.mode & 0o100)) fs.chmodSync(pathname, st.mode | 0o755);
|
|
41
|
+
} catch (_) {
|
|
42
|
+
/* non-fatal */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
if (fs.existsSync(target)) {
|
|
47
|
+
chmod(target);
|
|
38
48
|
console.log(`flutter-patcher: prebuilt binary present (${platformName}-${archName}), skipping build.`);
|
|
39
49
|
process.exit(0);
|
|
40
50
|
}
|
|
@@ -57,7 +67,7 @@ if (!fs.existsSync(path.join(cliDir, 'pubspec.yaml'))) {
|
|
|
57
67
|
console.log('flutter-patcher: resolving Dart dependencies (dart pub get)...');
|
|
58
68
|
const getRes = spawnSync('dart', ['pub', 'get'], { stdio: 'inherit', cwd: cliDir });
|
|
59
69
|
if (getRes.status !== 0) {
|
|
60
|
-
console.
|
|
70
|
+
console.error(
|
|
61
71
|
'flutter-patcher: `dart pub get` failed (check your network / Dart version). ' +
|
|
62
72
|
`To build manually:\n cd ${cliDir} && dart compile exe bin/flutter_patcher.dart ` +
|
|
63
73
|
`-o ${target}`,
|
|
@@ -65,17 +75,18 @@ if (getRes.status !== 0) {
|
|
|
65
75
|
process.exit(0);
|
|
66
76
|
}
|
|
67
77
|
|
|
68
|
-
console.log(
|
|
78
|
+
console.log(`flutter-patcher: building native binary (${platformName}-${archName})...`);
|
|
69
79
|
const res = spawnSync(
|
|
70
80
|
'dart',
|
|
71
81
|
['compile', 'exe', 'bin/flutter_patcher.dart', '-o', target],
|
|
72
82
|
{ stdio: 'inherit', cwd: cliDir },
|
|
73
83
|
);
|
|
74
84
|
if (res.status !== 0) {
|
|
75
|
-
console.
|
|
85
|
+
console.error(
|
|
76
86
|
'flutter-patcher: build failed. To build manually:\n cd ' +
|
|
77
87
|
`${cliDir} && dart compile exe bin/flutter_patcher.dart -o ${target}`,
|
|
78
88
|
);
|
|
79
89
|
process.exit(0);
|
|
80
90
|
}
|
|
91
|
+
chmod(target);
|
|
81
92
|
console.log(`flutter-patcher: built ${target}`);
|