@_nazmiforreal/flutter-ota 0.1.3 → 0.1.4
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.
|
@@ -7,6 +7,12 @@ import 'package:flutter_patcher_cli/flutter_patcher_cli.dart';
|
|
|
7
7
|
import '../ui/ui.dart';
|
|
8
8
|
|
|
9
9
|
/// `flutter_patcher init` — scaffold a project config file.
|
|
10
|
+
///
|
|
11
|
+
/// Prompts (interactively, when attached to a terminal) for the credentials the
|
|
12
|
+
/// **selected** backend provider needs, defaulting each field to its
|
|
13
|
+
/// corresponding environment variable so CI runs can be non-interactive. The
|
|
14
|
+
/// result is saved as `.flutter_patcher.json` (cwd) or the global config when
|
|
15
|
+
/// `--global` is passed.
|
|
10
16
|
class InitCommand extends Command<int> {
|
|
11
17
|
InitCommand({this.config, this.backendOverride});
|
|
12
18
|
|
|
@@ -22,62 +28,136 @@ class InitCommand extends Command<int> {
|
|
|
22
28
|
|
|
23
29
|
@override
|
|
24
30
|
ArgParser get argParser => ArgParser()
|
|
25
|
-
..addOption('provider',
|
|
26
|
-
|
|
27
|
-
..addOption('key', help: 'Supabase service role key.')
|
|
28
|
-
..addOption('anon-key', help: 'Supabase anon key.')
|
|
29
|
-
..addOption('bucket', defaultsTo: 'bundles', help: 'Storage bucket.')
|
|
30
|
-
..addOption('base-path', help: 'Storage base path.')
|
|
31
|
+
..addOption('provider',
|
|
32
|
+
abbr: 'p', defaultsTo: 'supabase', help: 'Backend provider.')
|
|
31
33
|
..addOption('channel', defaultsTo: 'production', help: 'Default channel.')
|
|
32
34
|
..addOption('platform', defaultsTo: 'android', help: 'Default platform.')
|
|
33
35
|
..addOption('source', defaultsTo: './dist', help: 'Default deploy source.')
|
|
36
|
+
..addFlag('global', help: 'Write to the global ~/.flutter_patcher config.')
|
|
34
37
|
..addFlag('force', abbr: 'f', help: 'Overwrite an existing config.');
|
|
35
38
|
|
|
36
|
-
String?
|
|
39
|
+
String? _p(String label, String? current) {
|
|
37
40
|
if (!stdin.hasTerminal) return current;
|
|
38
|
-
|
|
41
|
+
final hint = current != null && current.isNotEmpty ? ' [$current]' : '';
|
|
42
|
+
stdout.write('$label$hint: ');
|
|
39
43
|
final line = stdin.readLineSync();
|
|
40
|
-
|
|
44
|
+
final trimmed = (line ?? '').trim();
|
|
45
|
+
return trimmed.isEmpty ? current : trimmed;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
@override
|
|
44
49
|
Future<int> run() => runGuarded(() async {
|
|
45
|
-
final file =
|
|
50
|
+
final file = argResults!['global'] as bool
|
|
51
|
+
? configCandidates().last
|
|
52
|
+
: configCandidates().first;
|
|
46
53
|
if (file.existsSync() && !(argResults!['force'] as bool)) {
|
|
47
54
|
throw PackException(
|
|
48
55
|
'Config already exists at ${file.path}. Use --force to overwrite.',
|
|
49
56
|
64,
|
|
50
57
|
);
|
|
51
58
|
}
|
|
52
|
-
banner('init');
|
|
53
59
|
|
|
54
60
|
final provider = argResults!['provider'] as String;
|
|
55
|
-
final url = _prompt('Supabase URL', argResults!['url'] as String?);
|
|
56
|
-
final key = _prompt(
|
|
57
|
-
'Supabase service role key',
|
|
58
|
-
argResults!['key'] as String?,
|
|
59
|
-
);
|
|
60
|
-
final anonKey = argResults!['anon-key'] as String?;
|
|
61
|
-
final bucket = argResults!['bucket'] as String? ?? 'bundles';
|
|
62
|
-
final basePath = argResults!['base-path'] as String?;
|
|
63
61
|
final channel = argResults!['channel'] as String? ?? 'production';
|
|
64
62
|
final platform = argResults!['platform'] as String? ?? 'android';
|
|
65
63
|
final source = argResults!['source'] as String? ?? './dist';
|
|
64
|
+
final env = Platform.environment;
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
66
|
+
banner('init');
|
|
67
|
+
|
|
68
|
+
late FlutterPatcherConfig cfg;
|
|
69
|
+
switch (provider) {
|
|
70
|
+
case 'postgres':
|
|
71
|
+
cfg = FlutterPatcherConfig(
|
|
72
|
+
provider: provider,
|
|
73
|
+
channel: channel,
|
|
74
|
+
platform: platform,
|
|
75
|
+
source: source,
|
|
76
|
+
supabase: const SupabaseConfigJson(),
|
|
77
|
+
postgres: PostgresConfigJson(
|
|
78
|
+
host: _p('Postgres host', env['POSTGRES_HOST']),
|
|
79
|
+
port: _p('Postgres port', env['POSTGRES_PORT'] ?? '5432'),
|
|
80
|
+
database: _p('Postgres database', env['POSTGRES_DB'] ?? 'postgres'),
|
|
81
|
+
username: _p('Postgres username', env['POSTGRES_USER']),
|
|
82
|
+
password: _p('Postgres password', env['POSTGRES_PASSWORD']),
|
|
83
|
+
sslMode: _p('Postgres sslmode', env['POSTGRES_SSLMODE']),
|
|
84
|
+
basePath: _p('Storage base path', env['POSTGRES_BASE_PATH']),
|
|
85
|
+
servingBaseUrl:
|
|
86
|
+
_p('Serving base url', env['POSTGRES_SERVING_BASE_URL']),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
case 'cloudflare':
|
|
90
|
+
cfg = FlutterPatcherConfig(
|
|
91
|
+
provider: provider,
|
|
92
|
+
channel: channel,
|
|
93
|
+
platform: platform,
|
|
94
|
+
source: source,
|
|
95
|
+
supabase: const SupabaseConfigJson(),
|
|
96
|
+
cloudflare: CloudflareConfigJson(
|
|
97
|
+
accountId: _p('Cloudflare account id', env['CLOUDFLARE_ACCOUNT_ID']),
|
|
98
|
+
d1DatabaseId:
|
|
99
|
+
_p('Cloudflare D1 database id', env['CLOUDFLARE_D1_DATABASE_ID']),
|
|
100
|
+
apiToken: _p('Cloudflare API token', env['CLOUDFLARE_API_TOKEN']),
|
|
101
|
+
r2Bucket:
|
|
102
|
+
_p('R2 bucket', env['R2_BUCKET'] ?? 'bundles'),
|
|
103
|
+
r2AccessKeyId: _p('R2 access key id', env['R2_ACCESS_KEY_ID']),
|
|
104
|
+
r2SecretAccessKey:
|
|
105
|
+
_p('R2 secret access key', env['R2_SECRET_ACCESS_KEY']),
|
|
106
|
+
r2BasePath: _p('R2 base path', env['R2_BASE_PATH']),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
case 'aws':
|
|
110
|
+
cfg = FlutterPatcherConfig(
|
|
111
|
+
provider: provider,
|
|
112
|
+
channel: channel,
|
|
113
|
+
platform: platform,
|
|
114
|
+
source: source,
|
|
115
|
+
supabase: const SupabaseConfigJson(),
|
|
116
|
+
aws: AwsConfigJson(
|
|
117
|
+
bucket: _p('AWS bucket', env['AWS_BUCKET']),
|
|
118
|
+
region: _p('AWS region', env['AWS_REGION']),
|
|
119
|
+
accessKeyId: _p('AWS access key id', env['AWS_ACCESS_KEY_ID']),
|
|
120
|
+
secretAccessKey:
|
|
121
|
+
_p('AWS secret access key', env['AWS_SECRET_ACCESS_KEY']),
|
|
122
|
+
endpoint: _p('AWS endpoint', env['AWS_ENDPOINT']),
|
|
123
|
+
basePath: _p('AWS base path', env['AWS_BASE_PATH']),
|
|
124
|
+
sessionToken: _p('AWS session token', env['AWS_SESSION_TOKEN']),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
case 'standalone':
|
|
128
|
+
cfg = FlutterPatcherConfig(
|
|
129
|
+
provider: provider,
|
|
130
|
+
channel: channel,
|
|
131
|
+
platform: platform,
|
|
132
|
+
source: source,
|
|
133
|
+
supabase: const SupabaseConfigJson(),
|
|
134
|
+
standalone: StandaloneConfigJson(
|
|
135
|
+
baseUrl: _p('Standalone base url', env['STANDALONE_BASE_URL']),
|
|
136
|
+
),
|
|
137
|
+
);
|
|
138
|
+
case 'supabase':
|
|
139
|
+
default:
|
|
140
|
+
cfg = FlutterPatcherConfig(
|
|
141
|
+
provider: 'supabase',
|
|
142
|
+
channel: channel,
|
|
143
|
+
platform: platform,
|
|
144
|
+
source: source,
|
|
145
|
+
supabase: SupabaseConfigJson(
|
|
146
|
+
url: _p('Supabase URL', env['SUPABASE_URL']),
|
|
147
|
+
serviceRoleKey:
|
|
148
|
+
_p('Supabase service role key', env['SUPABASE_SERVICE_ROLE_KEY']),
|
|
149
|
+
anonKey: _p('Supabase anon key', env['SUPABASE_ANON_KEY']),
|
|
150
|
+
bucket: _p('Storage bucket', env['SUPABASE_BUCKET'] ?? 'bundles'),
|
|
151
|
+
basePath: _p('Storage base path', env['SUPABASE_BASE_PATH']),
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
saveConfig(cfg, global: argResults!['global'] as bool);
|
|
81
157
|
step('Wrote ${file.path}');
|
|
158
|
+
stdout.writeln(
|
|
159
|
+
' ${yellow('⚠')} Secrets are stored in plaintext. Restrict file '
|
|
160
|
+
'permissions and use a secrets manager in production.',
|
|
161
|
+
);
|
|
82
162
|
});
|
|
83
163
|
}
|
|
@@ -161,13 +161,23 @@ FlutterPatcherConfig? loadConfig() {
|
|
|
161
161
|
return null;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/// Persist a config
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
/// Persist a config. By default writes the project file (cwd); pass
|
|
165
|
+
/// [global: true] to write the global `~/.flutter_patcher/config.json`.
|
|
166
|
+
void saveConfig(FlutterPatcherConfig config, {bool global = false}) {
|
|
167
|
+
final file = global ? configCandidates().last : configCandidates().first;
|
|
167
168
|
file.parent.createSync(recursive: true);
|
|
168
169
|
file.writeAsStringSync(
|
|
169
170
|
const JsonEncoder.withIndent(' ').convert(config.toJson()),
|
|
170
171
|
);
|
|
172
|
+
// Config may contain secrets (service-role keys, DB passwords, tokens). On
|
|
173
|
+
// POSIX systems tighten to owner-only read/write so they aren't world-readable.
|
|
174
|
+
if (!Platform.isWindows) {
|
|
175
|
+
try {
|
|
176
|
+
Process.runSync('chmod', ['600', file.path]);
|
|
177
|
+
} catch (_) {
|
|
178
|
+
// Non-fatal: chmod may be unavailable in some sandboxes.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
171
181
|
}
|
|
172
182
|
|
|
173
183
|
/// Nested dot-path get/set helpers used by `config get/set`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@_nazmiforreal/flutter-ota",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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"
|