canListFirebaseApps method

Future<bool> canListFirebaseApps({
  1. String? account,
})

Probe whether the active principal has firebase.apps.list on the configured project — i.e. the permission needed by step 5.5 (Configure Firebase client wiring).

Why this exists: canEnableServices only verifies serviceusage.services.enable which is granted by roles/serviceusage.serviceUsageAdmin. A service account with that role but no roles/firebase.admin will pass the existing IAM gate in step 5.4 yet fail step 5.5 with firebase.apps.list PERMISSION_DENIED. This probe closes that gap so the gate can grant the missing firebase.admin role before step 5.5 runs.

Returns: • true → the principal can list Firebase apps OR the call failed for a reason that is NOT a missing IAM role (e.g. the Firebase Management API is not enabled yet — that's fine because step 5.4 will enable it immediately after the gate passes). We err on the side of "let the wizard proceed" so a brand-new project doesn't get stuck in the gate trying to auto-grant a role the SA already has. • false → ONLY when the failure is unambiguously PERMISSION_DENIED. This is what triggers the auto-grant flow.

We use apps:list --json so the Firebase CLI emits a structured {"status":"error", …} envelope instead of a generic "Failed to list" stderr line, which lets _classifyFirebaseError tell the difference between SERVICE_DISABLED (API not enabled — not an IAM problem) and PERMISSION_DENIED (the actual IAM gap we want to fix).

Implementation

Future<bool> canListFirebaseApps({String? account}) async {
  final String? projectId = config.firebaseProjectId;
  if (projectId == null) return false;
  final ProcessResult r = await _runner.run(
    'firebase',
    <String>[
      'apps:list',
      '--project',
      projectId,
      '--json',
      if (account != null && account.trim().isNotEmpty)
        '--account=${account.trim()}',
    ],
    environment: _authEnvironment,
    workingDirectory: config.outputDir,
  );
  final Map<String, dynamic>? body = _parseFirebaseJson(r.stdout);
  if (body != null && body['status'] == 'success') {
    return true;
  }
  // Failure path: only treat PERMISSION_DENIED as "gate failed". If
  // the firebase.googleapis.com API just isn't enabled yet (the
  // SERVICE_DISABLED case), the gate should let the wizard continue
  // — step 5.4 will enable the API, and the post-enable propagation
  // poll will revalidate. Without this carve-out, a brand-new
  // project where the API has never been enabled would force the
  // gate into auto-grant for a role the SA may already have.
  final String context = _collectFirebaseFailureContext(r);
  final FirebaseFailureKind kind = _classifyFirebaseError(context);
  if (kind == FirebaseFailureKind.permissionDenied) {
    return false;
  }
  // serviceDisabled / transient / unknown → presume the SA has the
  // role; the next step will surface any real failure.
  return true;
}