@_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', defaultsTo: 'supabase', help: 'Backend provider.')
26
- ..addOption('url', help: 'Supabase project URL.')
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? _prompt(String label, String? current) {
39
+ String? _p(String label, String? current) {
37
40
  if (!stdin.hasTerminal) return current;
38
- stdout.write('$label${current != null ? ' [$current]' : ''}: ');
41
+ final hint = current != null && current.isNotEmpty ? ' [$current]' : '';
42
+ stdout.write('$label$hint: ');
39
43
  final line = stdin.readLineSync();
40
- return (line == null || line.trim().isEmpty) ? current : line.trim();
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 = configCandidates().first;
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
- final cfg = FlutterPatcherConfig(
68
- provider: provider,
69
- supabase: SupabaseConfigJson(
70
- url: url,
71
- serviceRoleKey: key,
72
- anonKey: anonKey,
73
- bucket: bucket,
74
- basePath: basePath,
75
- ),
76
- channel: channel,
77
- platform: platform,
78
- source: source,
79
- );
80
- saveConfig(cfg);
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 to the project file (cwd).
165
- void saveConfig(FlutterPatcherConfig config) {
166
- final file = configCandidates().first;
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",
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"