runFirebase method
Run ulink import firebase. Returns the exit code + manifest; does not
call exit() itself so it stays unit-testable.
Implementation
Future<ImportRunResult> runFirebase(ImportOptions opts) async {
if (opts.help) {
_Log.out(_styledUsage());
return ImportRunResult(0, {});
}
if (opts.input == null) {
_Log.err('Missing --input. See `ulink import firebase --help`.');
return ImportRunResult(2, {});
}
if (opts.domain == null) {
_Log.err(
'Missing --domain (your ULink domain). See `ulink import firebase --help`.');
return ImportRunResult(2, {});
}
final live = opts.live;
final verify = opts.verify;
final outDir = opts.out ?? 'ulink-import';
final apiKey = opts.apiKey ?? Platform.environment['ULINK_API_KEY'];
// 1. Read + parse the export.
String raw;
try {
raw = await _readInput(opts.input!);
} catch (e) {
_Log.err('Could not read input "${opts.input}": $e');
return ImportRunResult(1, {});
}
List<Map<String, dynamic>> links;
try {
links = FdlParser.parseExport(raw);
} on FdlParseError catch (e) {
_Log.err('Could not parse FDL export: ${e.message}');
return ImportRunResult(1, {});
}
_Log.step(
'Parsed ${ConsoleStyle.bold('${links.length}')} Firebase Dynamic Link${links.length == 1 ? '' : 's'} from ${opts.input}');
if (live && (apiKey == null || apiKey.isEmpty)) {
_Log.err('--live requires --api-key or the ULINK_API_KEY env var.');
return ImportRunResult(2, {});
}
if (!live) {
_Log.info(ConsoleStyle.dim(
' (dry-run: no links will be created; pass --live to write to ULink)'));
}
// 2. Map -> 3. create/preview -> 4. verify, per link.
final results = <Map<String, dynamic>>[];
var created = 0;
var parityFailures = 0;
final slugSeen = <String, int>{};
for (var i = 0; i < links.length; i++) {
final fdl = links[i];
Map<String, dynamic> ulink;
try {
ulink = FdlMapper.mapToUlink(fdl, domain: opts.domain!, index: i);
} catch (e) {
_Log.err('Link #${i + 1}: mapping failed: $e');
results.add({'index': i, 'ok': false, 'error': '$e', 'source': fdl});
parityFailures++;
continue;
}
// Guard against duplicate slugs colliding silently.
final slug = ulink['slug'] as String;
final prev = slugSeen[slug];
if (prev != null) {
ulink['slug'] = '$slug-${i + 1}';
_Log.warn(
'Link #${i + 1}: slug collided with #${prev + 1}; using "${ulink['slug']}" instead.');
}
slugSeen[ulink['slug'] as String] = i;
CreateOutcome outcome;
try {
outcome = live
? await client.liveCreate(ulink, apiKey: apiKey!)
: SdkLinksClient.previewCreate(ulink);
if (outcome.status == 'created') created++;
} catch (e) {
_Log.err('Link #${i + 1}: create failed: $e');
results.add({'index': i, 'ok': false, 'error': '$e', 'link': ulink});
parityFailures++;
continue;
}
final short = outcome.shortLink;
// Static parity: the mapping preserves routing + attribution intent.
ParityResult? parity;
if (verify) parity = ImportParity.verifyParity(fdl, ulink);
// Live redirect parity: ask the deployed edge where the created link
// actually routes. Only meaningful for links created live; degrades to a
// warning (not a failure) when /sdk/resolve is unreachable.
LiveParityResult? liveParity;
var liveProbed = false;
if (verify && live && outcome.status == 'created') {
liveProbed = true;
final resolved = await client.liveResolve(short, apiKey: apiKey);
if (resolved != null) {
liveParity = ImportParity.verifyLiveRouting(outcome.payload, resolved);
}
}
final staticOk = !verify || parity!.ok;
final liveOk = liveParity == null || liveParity.ok;
final linkOk = staticOk && liveOk;
if (verify && !linkOk) parityFailures++;
final tag = !verify
? ''
: linkOk
? ' ${ConsoleStyle.success('parity OK')}'
: ' ${ConsoleStyle.error('PARITY FAIL')}';
final fromTo = fdl['shortLink'] != null
? '${ConsoleStyle.dim('${fdl['shortLink']}')} → '
: '';
_Log.ok('#${i + 1} $fromTo${ConsoleStyle.bold(short)}$tag');
if (verify && parity != null && !parity.ok) {
for (final ch in parity.failures) {
_Log.warn(
' ${ch.field}: FDL=${jsonEncode(ch.fdl)} ULink=${jsonEncode(ch.ulink)}');
}
}
if (liveParity != null && !liveParity.ok) {
for (final ch in liveParity.failures) {
_Log.warn(
' live ${ch.field}: sent=${jsonEncode(ch.sent)} resolved=${jsonEncode(ch.resolved)}');
}
}
if (liveProbed && liveParity == null) {
_Log.warn(
' live redirect parity skipped: GET /sdk/resolve unreachable for $short');
}
results.add({
'index': i,
'ok': linkOk,
'shortLink': short,
'status': outcome.status,
'link': ulink,
'parityOk': parity?.ok,
'liveParityOk': liveParity?.ok,
});
}
// 5. Write artifacts.
final manifest = <String, dynamic>{
'tool': 'ulink import firebase',
'generatedFrom': opts.input,
'domain': opts.domain,
'mode': live ? 'live' : 'dry-run',
'total': links.length,
'created': created,
'parityChecked': verify,
'parityFailures': parityFailures,
'links': results.map((r) {
final link = r['link'] as Map<String, dynamic>?;
return {
'index': r['index'],
'ok': r['ok'],
'shortLink': r['shortLink'],
'status': r['status'],
'error': r['error'],
'slug': link?['slug'],
'destination': link?['destination'],
'parityOk': r['parityOk'],
'liveParityOk': r['liveParityOk'],
};
}).toList(),
};
try {
await Directory(outDir).create(recursive: true);
await File(p.join(outDir, 'manifest.json'))
.writeAsString(_pretty(manifest));
await Directory(p.join(outDir, 'links')).create(recursive: true);
var linkCount = 0;
for (final r in results) {
final link = r['link'] as Map<String, dynamic>?;
if (link == null) continue;
await File(p.join(outDir, 'links', '${link['slug']}.json'))
.writeAsString(_pretty(link));
linkCount++;
}
_Log.step(
'Wrote manifest + $linkCount link definitions to ${ConsoleStyle.bold(outDir)}/');
} catch (e) {
_Log.warn('Could not write artifacts to $outDir: $e');
}
if (opts.json) _Log.out(_pretty(manifest));
// 6. Summary + exit code.
final okCount = results.where((r) => r['ok'] == true).length;
_Log.info('');
if (parityFailures == 0) {
_Log.ok(
'$okCount/${links.length} links ${live ? 'created' : 'previewed'} with ${verify ? 'verified routing + attribution parity' : 'parity check skipped'}.');
if (!live) {
_Log.info(ConsoleStyle.dim(
' Re-run with --live --api-key <key> to create them in ULink.'));
}
return ImportRunResult(0, manifest);
}
_Log.err(
'$parityFailures link(s) failed (mapping, create, or parity). See output above and $outDir/manifest.json.');
return ImportRunResult(1, manifest);
}