renderList static method

String renderList({
  1. required List<Map<String, dynamic>> apps,
  2. required Map<String, dynamic> limit,
})

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');
}