askServiceAccountKeyPath static method
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);
}