renderList static method
Pure โ table shape only, so apps list's whole layout is asserted
in-process without a Dio. Grouped by capability, not ownership
(canPush): section 1 is what appflight upload can reach, section 2
is builds-only.
The N of M ceiling line belongs to section 1, and prints whenever it
has a row or the account is an organization โ an org member
shares the org's ceiling even though they can't push to it themselves,
unlike a lone individual tester, whose own used/max is always 0
(nothing of theirs to be capped) and stays hidden (#30 Phase 3).
The empty-section suggestion also checks limit.canCreate (#30 Phase
6) โ never point someone at a command the backend will 403.
Implementation
static String renderList({
required List<Map<String, dynamic>> apps,
required Map<String, dynamic> limit,
}) {
final pushable = apps.where((a) => a['canPush'] == true).toList();
final receiveOnly = apps.where((a) => a['canPush'] != true).toList();
final isOrg = limit['ownerType'] == 'organization';
final canCreate = limit['canCreate'] != false;
final lines = <String>['Apps you can push to:'];
if (pushable.isEmpty) {
lines.add(canCreate
? ' None yet. Run `appflight apps create --package <pkg> --name <name>`.'
: ' None you can push to. Ask an org admin or developer to create one.');
} else {
lines.addAll(pushable.map(_row));
}
if (pushable.isNotEmpty || isOrg) {
final tier = limit['tier'] as String;
lines.add(
' ${limit['used']} of ${limit['max']} apps ยท ${planLabel(tier)}');
}
lines
..add('')
..add('Apps you receive builds for:');
if (receiveOnly.isEmpty) {
lines.add(' None.');
} else {
lines.addAll(receiveOnly.map(_row));
}
return lines.join('\n');
}