@_nazmiforreal/flutter-ota 0.1.14 → 0.1.16
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-ota-linux-x64 +0 -0
- package/bin/flutter-ota.js +23 -72
- package/dart-src/packages/cli-tools/.dart_tool/package_config.json +0 -2
- package/dart-src/packages/cli-tools/lib/flutter_ota_kit_cli.dart +2 -2
- package/dart-src/packages/cli-tools/lib/src/commands/build.dart +1 -1
- package/dart-src/packages/cli-tools/lib/src/commands/bundle.dart +112 -124
- package/dart-src/packages/cli-tools/lib/src/commands/channel.dart +20 -28
- package/dart-src/packages/cli-tools/lib/src/commands/config_command.dart +40 -18
- package/dart-src/packages/cli-tools/lib/src/commands/console.dart +10 -11
- package/dart-src/packages/cli-tools/lib/src/commands/deploy.dart +30 -33
- package/dart-src/packages/cli-tools/lib/src/commands/doctor.dart +43 -41
- package/dart-src/packages/cli-tools/lib/src/commands/fingerprint.dart +9 -10
- package/dart-src/packages/cli-tools/lib/src/commands/init.dart +32 -35
- package/dart-src/packages/cli-tools/lib/src/commands/keys.dart +3 -6
- package/dart-src/packages/cli-tools/lib/src/commands/migrate.dart +59 -50
- package/dart-src/packages/cli-tools/lib/src/commands/pocketbase.dart +32 -37
- package/dart-src/packages/cli-tools/lib/src/commands/rollback.dart +13 -16
- package/dart-src/packages/cli-tools/lib/src/commands/storage.dart +12 -20
- package/dart-src/packages/cli-tools/lib/src/config.dart +10 -3
- package/dart-src/packages/cli-tools/lib/src/operations.dart +5 -0
- package/dart-src/packages/cli-tools/lib/src/ui/ui.dart +225 -31
- package/dart-src/packages/cli-tools/lib/src/util.dart +7 -3
- package/dart-src/packages/cli-tools/test/cli_test.dart +6 -0
- package/dart-src/plugins/supabase/supabase/migrations/20250103114225_init.sql +7 -3
- package/dart-src/plugins/supabase/supabase/migrations/20250314000000_hot-updater_0.13.0.sql +7 -4
- package/dart-src/plugins/supabase/supabase/migrations/20250516000000_hot-updater_0.18.0.sql +18 -6
- package/dart-src/plugins/supabase/supabase/migrations/20251014000000_hot-updater_0.21.0.sql +4 -2
- package/dart-src/plugins/supabase/supabase/migrations/20260401000000_hot-updater_0.29.0.sql +4 -2
- package/dart-src/plugins/supabase/supabase/migrations/20260520014100_hot-updater_rls.sql +22 -6
- package/migrations/supabase/20250103114225_init.sql +7 -3
- package/migrations/supabase/20250314000000_hot-updater_0.13.0.sql +7 -4
- package/migrations/supabase/20250516000000_hot-updater_0.18.0.sql +18 -6
- package/migrations/supabase/20251014000000_hot-updater_0.21.0.sql +4 -2
- package/migrations/supabase/20260401000000_hot-updater_0.29.0.sql +4 -2
- package/migrations/supabase/20260520014100_hot-updater_rls.sql +22 -6
- package/package.json +1 -1
- package/scripts/vendor.sh +32 -0
- package/migrations/aws/README.md +0 -7
- package/migrations/cloudflare/bundles.sql +0 -42
|
@@ -4,12 +4,15 @@ import 'dart:async';
|
|
|
4
4
|
import 'dart:io';
|
|
5
5
|
import 'dart:math' as math;
|
|
6
6
|
|
|
7
|
-
/// Terminal UI toolkit for the
|
|
8
|
-
/// bordered boxes, step logs, dynamic spinners and progress bars.
|
|
7
|
+
/// Terminal UI toolkit for the flutter-ota CLI.
|
|
9
8
|
///
|
|
10
|
-
///
|
|
11
|
-
///
|
|
12
|
-
///
|
|
9
|
+
/// Provides colored text, banners, bordered boxes, step logs, spinners,
|
|
10
|
+
/// thin progress bars, and column-aligned tables.
|
|
11
|
+
///
|
|
12
|
+
/// Everything degrades gracefully to plain text when stdout is not a TTY
|
|
13
|
+
/// or the `NO_COLOR` env var is set.
|
|
14
|
+
|
|
15
|
+
// ── ANSI codes ───────────────────────────────────────────────────────────────
|
|
13
16
|
|
|
14
17
|
const _kReset = '\x1b[0m';
|
|
15
18
|
const _kRed = '\x1b[31m';
|
|
@@ -23,6 +26,8 @@ const _kBold = '\x1b[1m';
|
|
|
23
26
|
|
|
24
27
|
const List<String> _frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
25
28
|
|
|
29
|
+
// ── TTY detection ────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
26
31
|
bool get _noColor {
|
|
27
32
|
final v = Platform.environment['NO_COLOR'];
|
|
28
33
|
return v != null && v.isNotEmpty;
|
|
@@ -31,6 +36,8 @@ bool get _noColor {
|
|
|
31
36
|
bool get _colorOn =>
|
|
32
37
|
!_noColor && stdout.hasTerminal && stdout.supportsAnsiEscapes;
|
|
33
38
|
|
|
39
|
+
// ── Color helpers ────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
34
41
|
String _c(String code, String s) => _colorOn ? '$code$s$_kReset' : s;
|
|
35
42
|
|
|
36
43
|
String red(String s) => _c(_kRed, s);
|
|
@@ -43,7 +50,19 @@ String gray(String s) => _c(_kGray, s);
|
|
|
43
50
|
String dim(String s) => _c(_kGray, s);
|
|
44
51
|
String bold(String s) => _c(_kBold, s);
|
|
45
52
|
|
|
46
|
-
|
|
53
|
+
// ── Terminal width ───────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
int get _cols {
|
|
56
|
+
if (stdout.hasTerminal) return stdout.terminalColumns;
|
|
57
|
+
final envCols = Platform.environment['COLUMNS'];
|
|
58
|
+
if (envCols != null) {
|
|
59
|
+
final parsed = int.tryParse(envCols);
|
|
60
|
+
if (parsed != null && parsed > 0) return parsed;
|
|
61
|
+
}
|
|
62
|
+
return 80;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Visible width (ANSI-aware) ──────────────────────────────────────────────
|
|
47
66
|
|
|
48
67
|
final _ansi = RegExp('\x1b\\[[0-9;]*m');
|
|
49
68
|
|
|
@@ -51,26 +70,38 @@ int _dispWidth(String s) => s.replaceAll(_ansi, '').length;
|
|
|
51
70
|
|
|
52
71
|
void _echo(String s) => stdout.writeln(s);
|
|
53
72
|
|
|
73
|
+
// ── Key-value pair ───────────────────────────────────────────────────────────
|
|
74
|
+
|
|
54
75
|
/// `key value` laid out in a fixed 18-char column (visible-width aware).
|
|
76
|
+
/// Long values are truncated with `…` to prevent box overflow.
|
|
55
77
|
String kv(String k, String v) {
|
|
56
78
|
final pad = math.max(0, 18 - _dispWidth(k));
|
|
57
|
-
|
|
79
|
+
final maxVal = math.max(0, _cols - 24 - _dispWidth(k));
|
|
80
|
+
final display = maxVal > 0 && _dispWidth(v) > maxVal
|
|
81
|
+
? '${_cut(v, maxVal - 1)}…'
|
|
82
|
+
: v;
|
|
83
|
+
return '${dim(k)}${' ' * pad} $display';
|
|
58
84
|
}
|
|
59
85
|
|
|
86
|
+
// ── Banner ───────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
60
88
|
void banner(String name) {
|
|
61
89
|
_echo('');
|
|
62
90
|
_echo(
|
|
63
|
-
' ${cyan('▶')} ${cyan(bold('
|
|
91
|
+
' ${cyan('▶')} ${cyan(bold('flutter-ota'))} ${dim('·')} ${bold(name)}',
|
|
64
92
|
);
|
|
65
|
-
|
|
93
|
+
final sepLen = math.max(20, math.min(_cols - 4, 72));
|
|
94
|
+
_echo(' ${dim('─' * sepLen)}');
|
|
66
95
|
}
|
|
67
96
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
void warn(String msg) => _echo(' ${yellow('!')} ${yellow(msg)}');
|
|
97
|
+
// ── Single-line status ──────────────────────────────────────────────────────
|
|
71
98
|
|
|
99
|
+
void step(String msg) => _echo(' ${green('✓')} $msg');
|
|
100
|
+
void warn(String msg) => _echo(' ${yellow('⚠')} ${yellow(msg)}');
|
|
72
101
|
void err(String msg) => _echo(' ${red('✗')} ${red(msg)}');
|
|
73
102
|
|
|
103
|
+
// ── Spinner ──────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
74
105
|
/// Run [task] while showing a live spinner; replaces the line with a
|
|
75
106
|
/// `${green('✓')} done` summary when finished (or `✗` on error).
|
|
76
107
|
Future<T> spinner<T>(
|
|
@@ -101,6 +132,8 @@ Future<T> spinner<T>(
|
|
|
101
132
|
}
|
|
102
133
|
}
|
|
103
134
|
|
|
135
|
+
// ── Word wrap ────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
104
137
|
List<String> _wrap(String text, int inner) {
|
|
105
138
|
if (text.isEmpty) return [''];
|
|
106
139
|
final result = <String>[];
|
|
@@ -148,6 +181,11 @@ String _cut(String s, int inner) {
|
|
|
148
181
|
String _splitLong(String word, int inner, List<String> out) {
|
|
149
182
|
var rem = word;
|
|
150
183
|
while (_dispWidth(rem) > inner) {
|
|
184
|
+
if (_dispWidth(rem) <= inner + 2) {
|
|
185
|
+
// Truncate with … instead of hard-breaking mid-word.
|
|
186
|
+
out.add('${_cut(rem, inner - 1)}…');
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
151
189
|
final cut = _cut(rem, inner);
|
|
152
190
|
out.add(cut);
|
|
153
191
|
rem = rem.substring(cut.length);
|
|
@@ -155,14 +193,19 @@ String _splitLong(String word, int inner, List<String> out) {
|
|
|
155
193
|
return rem;
|
|
156
194
|
}
|
|
157
195
|
|
|
196
|
+
// ── Box ──────────────────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
/// Mathematically precise box width: single formula for both title and content.
|
|
158
199
|
int _boxWidth(List<String> lines, String title) {
|
|
159
|
-
var longest =
|
|
200
|
+
var longest = 0;
|
|
160
201
|
for (final l in lines) {
|
|
161
202
|
for (final w in l.split('\n')) {
|
|
162
203
|
longest = math.max(longest, _dispWidth(w));
|
|
163
204
|
}
|
|
164
205
|
}
|
|
165
|
-
final
|
|
206
|
+
final titleNeeded = _dispWidth(title) + 4;
|
|
207
|
+
final contentNeeded = longest + 4;
|
|
208
|
+
final target = math.max(titleNeeded, math.max(contentNeeded, 40));
|
|
166
209
|
return math.max(math.min(_cols - 1, target), 20);
|
|
167
210
|
}
|
|
168
211
|
|
|
@@ -171,18 +214,25 @@ int _boxWidth(List<String> lines, String title) {
|
|
|
171
214
|
void box(String title, List<String> lines) {
|
|
172
215
|
final width = _boxWidth(lines, title);
|
|
173
216
|
final inner = width - 4;
|
|
217
|
+
|
|
218
|
+
// Wrap content to fit inner width.
|
|
174
219
|
final wrapped = <String>[];
|
|
175
220
|
for (final l in lines) {
|
|
176
|
-
|
|
221
|
+
if (l.isEmpty) {
|
|
222
|
+
wrapped.add('');
|
|
223
|
+
} else {
|
|
224
|
+
wrapped.addAll(_wrap(l, inner));
|
|
225
|
+
}
|
|
177
226
|
}
|
|
178
227
|
|
|
228
|
+
// Title centered in top border.
|
|
179
229
|
final titleText = ' $title ';
|
|
180
230
|
final tVisible = _dispWidth(titleText);
|
|
181
|
-
|
|
182
|
-
final
|
|
183
|
-
final
|
|
184
|
-
|
|
185
|
-
_echo(
|
|
231
|
+
final totalDashes = width - 2 - tVisible;
|
|
232
|
+
final leftDashes = math.max(1, totalDashes ~/ 2);
|
|
233
|
+
final rightDashes = totalDashes - leftDashes;
|
|
234
|
+
|
|
235
|
+
_echo('╭${'─' * leftDashes}$titleText${'─' * rightDashes}╮');
|
|
186
236
|
for (final l in wrapped) {
|
|
187
237
|
final pad = math.max(0, inner - _dispWidth(l));
|
|
188
238
|
_echo('│ $l${' ' * pad} │');
|
|
@@ -190,44 +240,188 @@ void box(String title, List<String> lines) {
|
|
|
190
240
|
_echo('╰${'─' * (width - 2)}╯');
|
|
191
241
|
}
|
|
192
242
|
|
|
193
|
-
|
|
194
|
-
|
|
243
|
+
// ── Steps ────────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
/// Multi-step progress tracker with uv-style clean output.
|
|
246
|
+
///
|
|
247
|
+
/// Each step prints a single line: `✓ label` (done), `✗ label` (failed),
|
|
248
|
+
/// or `· label` (skipped). After all steps, call [summary] to print a
|
|
249
|
+
/// boxed summary with counts and elapsed time.
|
|
250
|
+
///
|
|
251
|
+
/// Example:
|
|
252
|
+
/// ```dart
|
|
253
|
+
/// final s = Steps('migrate · supabase');
|
|
254
|
+
/// s.success('Applied 20250103_init.sql');
|
|
255
|
+
/// s.skip('Skipped 20251014_0.21.0.sql');
|
|
256
|
+
/// s.summary(); // prints "2 applied · 1 skipped · 0 errors · 245ms"
|
|
257
|
+
/// ```
|
|
258
|
+
class Steps {
|
|
259
|
+
Steps(this.title) : _sw = Stopwatch()..start();
|
|
260
|
+
|
|
261
|
+
final String title;
|
|
262
|
+
final Stopwatch _sw;
|
|
263
|
+
int _done = 0;
|
|
264
|
+
int _failed = 0;
|
|
265
|
+
int _skipped = 0;
|
|
266
|
+
|
|
267
|
+
/// Whether any step has failed.
|
|
268
|
+
bool get hasErrors => _failed > 0;
|
|
269
|
+
|
|
270
|
+
/// Step completed successfully.
|
|
271
|
+
void success(String label) {
|
|
272
|
+
_done++;
|
|
273
|
+
_echo(' ${green('✓')} $label');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Step failed.
|
|
277
|
+
void fail(String label) {
|
|
278
|
+
_failed++;
|
|
279
|
+
_echo(' ${red('✗')} $label');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Step skipped (e.g. already applied).
|
|
283
|
+
void skip(String label) {
|
|
284
|
+
_skipped++;
|
|
285
|
+
_echo(' ${dim('·')} $label');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// Print boxed summary: "N applied · N skipped · N errors · TIME"
|
|
289
|
+
void summary() {
|
|
290
|
+
_sw.stop();
|
|
291
|
+
final ms = _sw.elapsedMilliseconds;
|
|
292
|
+
final time = ms >= 1000
|
|
293
|
+
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
294
|
+
: '${ms}ms';
|
|
295
|
+
final parts = <String>[];
|
|
296
|
+
if (_done > 0) parts.add('${_done} completed');
|
|
297
|
+
if (_skipped > 0) parts.add('${_skipped} skipped');
|
|
298
|
+
if (_failed > 0) parts.add('${red('${_failed} error${_failed > 1 ? 's' : ''}')}');
|
|
299
|
+
if (parts.isEmpty) parts.add('no steps');
|
|
300
|
+
parts.add(time);
|
|
301
|
+
box('summary', [parts.join(' · ')]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Progress Bar (thin, single-line) ────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
/// Thin single-line progress bar with spinner, label, bar, percent, and time.
|
|
308
|
+
///
|
|
309
|
+
/// Renders as: `⠹ Label ████░░░░ 50% 1.3s`
|
|
310
|
+
/// On finish: `✓ Label in 1.3s`
|
|
195
311
|
class ProgressBar {
|
|
196
312
|
ProgressBar(this.total, this.label);
|
|
197
313
|
|
|
198
314
|
int total;
|
|
199
|
-
|
|
315
|
+
String label;
|
|
200
316
|
int _current = 0;
|
|
201
317
|
int _spin = 0;
|
|
202
318
|
bool _closed = false;
|
|
319
|
+
final Stopwatch _sw = Stopwatch()..start();
|
|
320
|
+
late String _label = label;
|
|
203
321
|
|
|
204
|
-
void update(int current, [String?
|
|
322
|
+
void update(int current, [String? newLabel]) {
|
|
205
323
|
_current = current;
|
|
206
|
-
if (
|
|
324
|
+
if (newLabel != null) _label = newLabel;
|
|
207
325
|
_draw();
|
|
208
326
|
if (total > 0 && _current >= total) _finish();
|
|
209
327
|
}
|
|
210
328
|
|
|
211
|
-
late String _label = label;
|
|
212
|
-
|
|
213
329
|
void _draw() {
|
|
214
330
|
if (_closed) return;
|
|
215
331
|
if (!_colorOn) {
|
|
216
332
|
_echo(' $_label: $_current/$total');
|
|
217
333
|
return;
|
|
218
334
|
}
|
|
219
|
-
|
|
335
|
+
|
|
220
336
|
final pct = total > 0 ? (_current / total).clamp(0.0, 1.0) : 1.0;
|
|
337
|
+
|
|
338
|
+
// Thin bar: width scales to terminal, max 20 chars.
|
|
339
|
+
final barW = math.min(20, math.max(8, _cols - 40));
|
|
221
340
|
final filled = (barW * pct).round();
|
|
222
|
-
final
|
|
341
|
+
final empty = barW - filled;
|
|
342
|
+
final bar = '${yellow('█' * filled)}${gray('░' * empty)}';
|
|
343
|
+
|
|
223
344
|
_spin = (_spin + 1) % _frames.length;
|
|
224
345
|
final pctStr = '${(pct * 100).round()}%'.padLeft(4);
|
|
225
|
-
|
|
346
|
+
|
|
347
|
+
stdout.write(
|
|
348
|
+
'\r\x1b[K ${cyan(_frames[_spin])} $_label $bar $pctStr',
|
|
349
|
+
);
|
|
226
350
|
}
|
|
227
351
|
|
|
228
352
|
void _finish() {
|
|
229
353
|
if (_closed) return;
|
|
230
354
|
_closed = true;
|
|
231
|
-
|
|
355
|
+
_sw.stop();
|
|
356
|
+
final ms = _sw.elapsedMilliseconds;
|
|
357
|
+
final time = ms >= 1000
|
|
358
|
+
? '${(ms / 1000).toStringAsFixed(1)}s'
|
|
359
|
+
: '${ms}ms';
|
|
360
|
+
if (_colorOn) {
|
|
361
|
+
stdout.write('\r\x1b[K ${green('✓')} $_label in $time\n');
|
|
362
|
+
} else {
|
|
363
|
+
_echo(' ✓ $_label in $time');
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/// Force finish (for when total is not reached).
|
|
368
|
+
void close() {
|
|
369
|
+
if (!_closed) _finish();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── Table ────────────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
/// Column-aligned table inside a bordered box.
|
|
376
|
+
///
|
|
377
|
+
/// Example:
|
|
378
|
+
/// ```dart
|
|
379
|
+
/// table('3 bundles', ['#', 'ID', 'CHANNEL', 'ON'], [
|
|
380
|
+
/// ['0', 'abc-123', 'production', '✓'],
|
|
381
|
+
/// ['1', 'def-456', 'staging', '✗'],
|
|
382
|
+
/// ]);
|
|
383
|
+
/// ```
|
|
384
|
+
void table(String title, List<String> headers, List<List<String>> rows) {
|
|
385
|
+
if (headers.isEmpty) return;
|
|
386
|
+
|
|
387
|
+
// Calculate column widths from content.
|
|
388
|
+
final colCount = headers.length;
|
|
389
|
+
final colWidths = List<int>.filled(colCount, 0);
|
|
390
|
+
for (var i = 0; i < colCount; i++) {
|
|
391
|
+
colWidths[i] = _dispWidth(headers[i]);
|
|
392
|
+
}
|
|
393
|
+
for (final row in rows) {
|
|
394
|
+
for (var i = 0; i < colCount && i < row.length; i++) {
|
|
395
|
+
colWidths[i] = math.max(colWidths[i], _dispWidth(row[i]));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Build formatted lines.
|
|
400
|
+
final formatted = <String>[];
|
|
401
|
+
|
|
402
|
+
// Header line.
|
|
403
|
+
final headerParts = <String>[];
|
|
404
|
+
for (var i = 0; i < colCount; i++) {
|
|
405
|
+
headerParts.add(headers[i].padRight(colWidths[i]));
|
|
406
|
+
}
|
|
407
|
+
formatted.add(bold(headerParts.join(' ')));
|
|
408
|
+
|
|
409
|
+
// Separator line.
|
|
410
|
+
final sepParts = <String>[];
|
|
411
|
+
for (var i = 0; i < colCount; i++) {
|
|
412
|
+
sepParts.add('─' * colWidths[i]);
|
|
232
413
|
}
|
|
414
|
+
formatted.add(dim(sepParts.join(' ')));
|
|
415
|
+
|
|
416
|
+
// Data rows.
|
|
417
|
+
for (final row in rows) {
|
|
418
|
+
final parts = <String>[];
|
|
419
|
+
for (var i = 0; i < colCount; i++) {
|
|
420
|
+
final cell = i < row.length ? row[i] : '';
|
|
421
|
+
parts.add(cell.padRight(colWidths[i]));
|
|
422
|
+
}
|
|
423
|
+
formatted.add(parts.join(' '));
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
box(title, formatted);
|
|
233
427
|
}
|
|
@@ -51,9 +51,13 @@ String generateFingerprint(String directory) {
|
|
|
51
51
|
for (final entity in dir.listSync(recursive: true)) {
|
|
52
52
|
if (entity is! File) continue;
|
|
53
53
|
final relative = p.relative(entity.path, from: dir.path);
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
try {
|
|
55
|
+
final content = File(entity.path).readAsBytesSync();
|
|
56
|
+
final hash = sha256Hex(content);
|
|
57
|
+
entries.add('$relative:$hash');
|
|
58
|
+
} on FileSystemException {
|
|
59
|
+
// Skip unreadable files (sockets, pipes, broken symlinks).
|
|
60
|
+
}
|
|
57
61
|
}
|
|
58
62
|
entries.sort();
|
|
59
63
|
return sha256Hex(utf8.encode(entries.join('\n')));
|
|
@@ -381,6 +381,7 @@ void main() {
|
|
|
381
381
|
source: dir.path,
|
|
382
382
|
channel: 'production',
|
|
383
383
|
platform: 'android',
|
|
384
|
+
targetAppVersion: '1.0.0',
|
|
384
385
|
),
|
|
385
386
|
);
|
|
386
387
|
expect(bundle.enabled, isTrue);
|
|
@@ -402,6 +403,7 @@ void main() {
|
|
|
402
403
|
source: dir.path,
|
|
403
404
|
channel: 'production',
|
|
404
405
|
platform: 'android',
|
|
406
|
+
targetAppVersion: '1.0.0',
|
|
405
407
|
),
|
|
406
408
|
);
|
|
407
409
|
final res = await listBundles(
|
|
@@ -429,6 +431,7 @@ void main() {
|
|
|
429
431
|
source: dir.path,
|
|
430
432
|
channel: 'production',
|
|
431
433
|
platform: 'android',
|
|
434
|
+
targetAppVersion: '1.0.0',
|
|
432
435
|
),
|
|
433
436
|
);
|
|
434
437
|
await deleteBundle(backend, deployed.id);
|
|
@@ -448,6 +451,7 @@ void main() {
|
|
|
448
451
|
source: dir.path,
|
|
449
452
|
channel: 'production',
|
|
450
453
|
platform: 'android',
|
|
454
|
+
targetAppVersion: '1.0.0',
|
|
451
455
|
),
|
|
452
456
|
);
|
|
453
457
|
final second = await deployBundle(
|
|
@@ -456,6 +460,7 @@ void main() {
|
|
|
456
460
|
source: dir.path,
|
|
457
461
|
channel: 'production',
|
|
458
462
|
platform: 'android',
|
|
463
|
+
targetAppVersion: '2.0.0',
|
|
459
464
|
),
|
|
460
465
|
);
|
|
461
466
|
final disabled = await rollbackChannel(backend, 'production');
|
|
@@ -477,6 +482,7 @@ void main() {
|
|
|
477
482
|
source: dir.path,
|
|
478
483
|
channel: 'production',
|
|
479
484
|
platform: 'android',
|
|
485
|
+
targetAppVersion: '1.0.0',
|
|
480
486
|
),
|
|
481
487
|
);
|
|
482
488
|
await promoteBundle(backend, b.id, 'staging');
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
-- HotUpdater.bundles
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DO $$ BEGIN
|
|
4
|
+
CREATE TYPE platforms AS ENUM ('ios', 'android');
|
|
5
|
+
EXCEPTION
|
|
6
|
+
WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
4
8
|
|
|
5
|
-
CREATE TABLE bundles (
|
|
9
|
+
CREATE TABLE IF NOT EXISTS bundles (
|
|
6
10
|
id uuid PRIMARY KEY,
|
|
7
11
|
platform platforms NOT NULL,
|
|
8
12
|
target_app_version text NOT NULL,
|
|
@@ -14,7 +18,7 @@ CREATE TABLE bundles (
|
|
|
14
18
|
message text
|
|
15
19
|
);
|
|
16
20
|
|
|
17
|
-
CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS bundles_target_app_version_idx ON bundles(target_app_version);
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
-- HotUpdater.get_update_info
|
|
@@ -88,10 +88,13 @@ $$;
|
|
|
88
88
|
|
|
89
89
|
-- HotUpdater.bundles
|
|
90
90
|
ALTER TABLE bundles
|
|
91
|
-
ADD COLUMN channel text NOT NULL DEFAULT 'production';
|
|
91
|
+
ADD COLUMN IF NOT EXISTS channel text NOT NULL DEFAULT 'production';
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
DROP COLUMN file_url;
|
|
93
|
+
DO $$ BEGIN
|
|
94
|
+
ALTER TABLE bundles DROP COLUMN file_url;
|
|
95
|
+
EXCEPTION
|
|
96
|
+
WHEN undefined_column THEN NULL;
|
|
97
|
+
END $$;
|
|
95
98
|
|
|
96
99
|
-- HotUpdater.get_target_app_version_list
|
|
97
100
|
|
|
@@ -131,4 +134,4 @@ BEGIN
|
|
|
131
134
|
END;
|
|
132
135
|
$$;
|
|
133
136
|
|
|
134
|
-
CREATE INDEX bundles_channel_idx ON bundles(channel);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS bundles_channel_idx ON bundles(channel);
|
|
@@ -8,14 +8,26 @@ UPDATE bundles
|
|
|
8
8
|
SET storage_uri = 'supabase-storage://%%BUCKET_NAME%%/' || id || '/bundle.zip'
|
|
9
9
|
WHERE storage_uri IS NULL;
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
ALTER TABLE bundles ALTER COLUMN
|
|
11
|
+
DO $$ BEGIN
|
|
12
|
+
ALTER TABLE bundles ALTER COLUMN storage_uri SET NOT NULL;
|
|
13
|
+
EXCEPTION
|
|
14
|
+
WHEN null_value_not_allowed THEN NULL;
|
|
15
|
+
END $$;
|
|
16
|
+
DO $$ BEGIN
|
|
17
|
+
ALTER TABLE bundles ALTER COLUMN target_app_version DROP NOT NULL;
|
|
18
|
+
EXCEPTION
|
|
19
|
+
WHEN null_value_not_allowed THEN NULL;
|
|
20
|
+
END $$;
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
)
|
|
22
|
+
DO $$ BEGIN
|
|
23
|
+
ALTER TABLE bundles ADD CONSTRAINT check_version_or_fingerprint CHECK (
|
|
24
|
+
(target_app_version IS NOT NULL) OR (fingerprint_hash IS NOT NULL)
|
|
25
|
+
);
|
|
26
|
+
EXCEPTION
|
|
27
|
+
WHEN duplicate_object THEN NULL;
|
|
28
|
+
END $$;
|
|
17
29
|
|
|
18
|
-
CREATE INDEX bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS bundles_fingerprint_hash_idx ON bundles(fingerprint_hash);
|
|
19
31
|
|
|
20
32
|
DROP FUNCTION IF EXISTS get_update_info;
|
|
21
33
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash;
|
|
2
|
-
DROP FUNCTION IF EXISTS
|
|
1
|
+
DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash(platforms, uuid, uuid, text, text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
|
|
2
|
+
DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash(platforms, uuid, uuid, text, text, text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
|
|
3
|
+
DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_app_version(platforms, text, uuid, uuid, text, text[]); EXCEPTION WHEN undefined_function THEN NULL; END $$;
|
|
4
|
+
DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_app_version(platforms, text, uuid, uuid, text, text[], text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
|
|
3
5
|
|
|
4
6
|
-- HotUpdater.get_update_info
|
|
5
7
|
CREATE OR REPLACE FUNCTION get_update_info_by_fingerprint_hash (
|
|
@@ -251,7 +251,8 @@ $$;
|
|
|
251
251
|
|
|
252
252
|
-- HotUpdater.get_update_info_by_fingerprint_hash
|
|
253
253
|
|
|
254
|
-
DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash;
|
|
254
|
+
DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash(platforms, uuid, uuid, text, text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
|
|
255
|
+
DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_fingerprint_hash(platforms, uuid, uuid, text, text, text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
|
|
255
256
|
|
|
256
257
|
CREATE OR REPLACE FUNCTION get_update_info_by_fingerprint_hash (
|
|
257
258
|
app_platform platforms,
|
|
@@ -377,7 +378,8 @@ $$;
|
|
|
377
378
|
|
|
378
379
|
-- HotUpdater.get_update_info_by_app_version
|
|
379
380
|
|
|
380
|
-
DROP FUNCTION IF EXISTS get_update_info_by_app_version;
|
|
381
|
+
DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_app_version(platforms, text, uuid, uuid, text, text[]); EXCEPTION WHEN undefined_function THEN NULL; END $$;
|
|
382
|
+
DO $$ BEGIN DROP FUNCTION IF EXISTS get_update_info_by_app_version(platforms, text, uuid, uuid, text, text[], text); EXCEPTION WHEN undefined_function THEN NULL; END $$;
|
|
381
383
|
|
|
382
384
|
CREATE OR REPLACE FUNCTION get_update_info_by_app_version (
|
|
383
385
|
app_platform platforms,
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
-- HotUpdater.supabase_rls
|
|
2
2
|
|
|
3
3
|
ALTER TABLE public.bundles ENABLE ROW LEVEL SECURITY;
|
|
4
|
-
|
|
4
|
+
DO $$ BEGIN
|
|
5
|
+
ALTER TABLE public.bundle_patches ENABLE ROW LEVEL SECURITY;
|
|
6
|
+
EXCEPTION
|
|
7
|
+
WHEN undefined_table THEN NULL;
|
|
8
|
+
END $$;
|
|
5
9
|
|
|
6
10
|
ALTER FUNCTION public.get_target_app_version_list(public.platforms, uuid)
|
|
7
11
|
SET search_path = public, pg_catalog;
|
|
@@ -50,10 +54,22 @@ ALTER FUNCTION public.get_update_info_by_app_version(
|
|
|
50
54
|
-- OTA update checks are unauthenticated by design (the device only carries the
|
|
51
55
|
-- anon key), so bundle metadata must be publicly readable. hot-updater's hosted
|
|
52
56
|
-- Supabase flow relies on these policies existing.
|
|
53
|
-
|
|
54
|
-
CREATE POLICY "allow public read
|
|
57
|
+
DO $$ BEGIN
|
|
58
|
+
CREATE POLICY "allow public read bundles" ON public.bundles FOR SELECT USING (true);
|
|
59
|
+
EXCEPTION
|
|
60
|
+
WHEN duplicate_object THEN NULL;
|
|
61
|
+
END $$;
|
|
62
|
+
DO $$ BEGIN
|
|
63
|
+
CREATE POLICY "allow public read bundle_patches" ON public.bundle_patches FOR SELECT USING (true);
|
|
64
|
+
EXCEPTION
|
|
65
|
+
WHEN duplicate_object THEN NULL;
|
|
66
|
+
END $$;
|
|
55
67
|
|
|
56
68
|
-- Allow anon/authenticated to resolve signed download URLs for the bundles bucket.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
DO $$ BEGIN
|
|
70
|
+
CREATE POLICY "public bundles read" ON storage.objects
|
|
71
|
+
FOR SELECT TO anon, authenticated
|
|
72
|
+
USING (bucket_id = 'bundles');
|
|
73
|
+
EXCEPTION
|
|
74
|
+
WHEN duplicate_object THEN NULL;
|
|
75
|
+
END $$;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
-- HotUpdater.bundles
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DO $$ BEGIN
|
|
4
|
+
CREATE TYPE platforms AS ENUM ('ios', 'android');
|
|
5
|
+
EXCEPTION
|
|
6
|
+
WHEN duplicate_object THEN NULL;
|
|
7
|
+
END $$;
|
|
4
8
|
|
|
5
|
-
CREATE TABLE bundles (
|
|
9
|
+
CREATE TABLE IF NOT EXISTS bundles (
|
|
6
10
|
id uuid PRIMARY KEY,
|
|
7
11
|
platform platforms NOT NULL,
|
|
8
12
|
target_app_version text NOT NULL,
|
|
@@ -14,7 +18,7 @@ CREATE TABLE bundles (
|
|
|
14
18
|
message text
|
|
15
19
|
);
|
|
16
20
|
|
|
17
|
-
CREATE INDEX bundles_target_app_version_idx ON bundles(target_app_version);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS bundles_target_app_version_idx ON bundles(target_app_version);
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
-- HotUpdater.get_update_info
|
|
@@ -88,10 +88,13 @@ $$;
|
|
|
88
88
|
|
|
89
89
|
-- HotUpdater.bundles
|
|
90
90
|
ALTER TABLE bundles
|
|
91
|
-
ADD COLUMN channel text NOT NULL DEFAULT 'production';
|
|
91
|
+
ADD COLUMN IF NOT EXISTS channel text NOT NULL DEFAULT 'production';
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
DROP COLUMN file_url;
|
|
93
|
+
DO $$ BEGIN
|
|
94
|
+
ALTER TABLE bundles DROP COLUMN file_url;
|
|
95
|
+
EXCEPTION
|
|
96
|
+
WHEN undefined_column THEN NULL;
|
|
97
|
+
END $$;
|
|
95
98
|
|
|
96
99
|
-- HotUpdater.get_target_app_version_list
|
|
97
100
|
|
|
@@ -131,4 +134,4 @@ BEGIN
|
|
|
131
134
|
END;
|
|
132
135
|
$$;
|
|
133
136
|
|
|
134
|
-
CREATE INDEX bundles_channel_idx ON bundles(channel);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS bundles_channel_idx ON bundles(channel);
|