askServiceAccountKeyPath static method

Future<String?> askServiceAccountKeyPath({
  1. required String outputDir,
  2. String? serverPackageName,
})

Ask the user where the service-account JSON lives. Instead of typing a long absolute path (which caps out at the terminal width), Oracular opens the destination folder in the OS file browser and lets the user drag/drop the key in. Pressing Enter then auto-detects the file.

When serverPackageName is provided, the key is dropped into the server package folder (<outputDir>/<server>/) so server deployment can pick it up directly. When omitted (or the project has no server), the key lands in the canonical project-level keys folder (<outputDir>/config/keys/) which FirebaseService resolves automatically for IAM-gated Firebase setup steps.

Before prompting the user, this also walks common locations (output dir, current dir, parents up to 6 levels) for an existing service-account.json. When one is found, the user is offered the chance to reuse it — that's the hands-off path for users who keep a shared SA at their workspace root.

Implementation

static Future<String?> askServiceAccountKeyPath({
  required String outputDir,
  String? serverPackageName,
}) async {
  final bool forServer =
      serverPackageName != null && serverPackageName.trim().isNotEmpty;
  final Directory targetDir = forServer
      ? Directory(p.join(outputDir, serverPackageName))
      : Directory(p.join(outputDir, 'config', 'keys'));
  final String canonicalPath = p.join(targetDir.path, _canonicalFileName);

  // Step 1: see if we already have one we can use.
  final DiscoveredServiceAccount? discovered =
      findExistingServiceAccountKey(
    outputDir: outputDir,
    serverPackageName: serverPackageName,
  );
  if (discovered != null) {
    print('');
    success('Found an existing service account key.');
    UserPrompt.printList(<String>[
      'Path: ${discovered.path}',
      if (discovered.hasProjectId) 'Project: ${discovered.projectId}',
      if (discovered.clientEmail != null)
        'Service account: ${discovered.clientEmail}',
    ]);
    final bool reuse = await UserPrompt.askYesNo(
      'Use this service account?',
      defaultValue: true,
    );
    if (reuse) {
      return discovered.path;
    }
  }

  // Step 2: explain why this matters before asking to drop one in.
  print('');
  if (forServer) {
    info(
      'A service account key powers Firebase Admin SDK calls from your '
      'server and unlocks IAM-gated Firebase setup steps.',
    );
  } else {
    info(
      'A service account key lets Oracular run Firebase setup '
      '(IAM grants, Firestore/Storage init, hosting deploys) without '
      'manual console clicks. You can also add it later.',
    );
  }
  UserPrompt.printList(<String>[
    'You can skip this now and add the file later.',
    'Generate one at: '
        'Firebase Console \u2192 Project settings \u2192 Service accounts.',
    'It will be saved as: $canonicalPath',
  ]);

  final bool addNow = await UserPrompt.askYesNo(
    'Drop in a service account key now?',
    defaultValue: false,
  );
  if (!addNow) {
    return null;
  }

  // Make sure the destination folder exists before opening it. Without this
  // the OS file browser would refuse to open a non-existent path.
  if (!targetDir.existsSync()) {
    try {
      targetDir.createSync(recursive: true);
    } catch (e) {
      warn('Could not create $targetDir: $e');
      return null;
    }
  }

  return _runDropInLoop(targetDir, canonicalPath);
}