1use anyhow::{Context, Result, anyhow};
7use clap::{Args, ValueEnum};
8use dialoguer::{Confirm, Input, Select};
9use std::io::IsTerminal;
10use std::path::Path;
11use std::sync::Arc;
12
13use auths_core::PrefilledPassphraseProvider;
14use auths_core::signing::{PassphraseProvider, StorageSigner};
15use auths_core::storage::keychain::{KeyAlias, KeyStorage, get_platform_keychain};
16use auths_id::storage::attestation::AttestationSource;
17use auths_id::storage::identity::IdentityStorage;
18use auths_infra_http::HttpRegistryClient;
19use auths_sdk::ports::git_config::GitConfigProvider;
20use auths_sdk::registration::DEFAULT_REGISTRY_URL;
21use auths_sdk::result::InitializeResult;
22use auths_sdk::setup::initialize;
23use auths_sdk::types::{
24 CiEnvironment, CiIdentityConfig, CreateDeveloperIdentityConfig, GitSigningScope,
25 IdentityConfig, IdentityConflictPolicy,
26};
27use auths_storage::git::{
28 GitRegistryBackend, RegistryAttestationStorage, RegistryConfig, RegistryIdentityStorage,
29};
30
31use crate::adapters::git_config::SystemGitConfigProvider;
32use crate::factories::storage::build_auths_context;
33
34use super::init_helpers::{
35 check_git_version, detect_ci_environment, get_auths_repo_path, offer_shell_completions,
36 select_agent_capabilities, write_allowed_signers,
37};
38use crate::config::CliConfig;
39use crate::ux::format::Output;
40
41const DEFAULT_KEY_ALIAS: &str = "main";
42
43#[derive(Debug, Clone, Copy, ValueEnum)]
45pub enum InitProfile {
46 Developer,
48 Ci,
50 Agent,
52}
53
54impl std::fmt::Display for InitProfile {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 match self {
57 InitProfile::Developer => write!(f, "developer"),
58 InitProfile::Ci => write!(f, "ci"),
59 InitProfile::Agent => write!(f, "agent"),
60 }
61 }
62}
63
64#[derive(Args, Debug, Clone)]
77#[command(
78 name = "init",
79 about = "Set up your cryptographic identity and Git signing"
80)]
81pub struct InitCommand {
82 #[clap(long)]
84 pub non_interactive: bool,
85
86 #[clap(long, value_enum)]
88 pub profile: Option<InitProfile>,
89
90 #[clap(long, default_value = DEFAULT_KEY_ALIAS)]
92 pub key_alias: String,
93
94 #[clap(long)]
96 pub force: bool,
97
98 #[clap(long)]
100 pub dry_run: bool,
101
102 #[clap(long, default_value = DEFAULT_REGISTRY_URL)]
104 pub registry: String,
105
106 #[clap(long)]
108 pub skip_registration: bool,
109}
110
111pub fn handle_init(cmd: InitCommand, ctx: &CliConfig) -> Result<()> {
124 let out = Output::new();
125 let interactive = !cmd.non_interactive && std::io::stdin().is_terminal();
126
127 let profile = match cmd.profile {
128 Some(p) => p,
129 None if !interactive => {
130 out.println("No profile specified in non-interactive mode, defaulting to developer.");
131 InitProfile::Developer
132 }
133 None => prompt_profile(&out)?,
134 };
135
136 out.print_heading(&format!("Auths Setup ({})", profile));
137 out.println("=".repeat(40).as_str());
138 out.newline();
139
140 match profile {
141 InitProfile::Developer => {
142 let (keychain, mut config) = gather_developer_config(interactive, &out, &cmd)?;
144 let registry_path = get_auths_repo_path()?;
145
146 ensure_registry_dir(®istry_path)?;
148
149 let sign_binary_path = which::which("auths-sign").ok();
151 if let Some(ref path) = sign_binary_path {
152 config.sign_binary_path = Some(path.clone());
153 }
154 let git_config_provider: Option<Box<dyn GitConfigProvider>> =
155 match &config.git_signing_scope {
156 GitSigningScope::Skip => None,
157 GitSigningScope::Global => Some(Box::new(SystemGitConfigProvider::global())),
158 GitSigningScope::Local { repo_path } => {
159 Some(Box::new(SystemGitConfigProvider::local(repo_path.clone())))
160 }
161 };
162
163 let sdk_ctx = build_auths_context(®istry_path, &ctx.env_config, None)?;
165
166 let keychain_arc: Arc<dyn KeyStorage + Send + Sync> = Arc::from(keychain);
168 let signer = StorageSigner::new(Arc::clone(&keychain_arc));
169 let result = initialize(
170 IdentityConfig::Developer(config),
171 &sdk_ctx,
172 keychain_arc,
173 &signer,
174 ctx.passphrase_provider.as_ref(),
175 git_config_provider.as_deref(),
176 )?;
177 let result = match result {
178 InitializeResult::Developer(r) => r,
179 _ => unreachable!(),
180 };
181
182 out.print_success(&format!("Identity ready: {}", &result.identity_did));
183 out.print_success(&format!("Device linked: {}", result.device_did.as_str()));
184 out.newline();
185
186 let proof_url = if interactive && !cmd.skip_registration {
188 out.print_info("Claim your Developer Passport");
189 out.newline();
190 match prompt_platform_verification(
191 &out,
192 Arc::clone(&ctx.passphrase_provider),
193 &ctx.env_config,
194 )? {
195 Some((url, _username)) => {
196 out.print_success(&format!("Proof anchored: {}", url));
197 Some(url)
198 }
199 None => {
200 out.println(" Continuing as anonymous identity");
201 None
202 }
203 }
204 } else {
205 None
206 };
207 out.newline();
208
209 offer_shell_completions(interactive, &out)?;
210 write_allowed_signers(&result.key_alias, &out)?;
211
212 let registered = submit_registration(
214 &get_auths_repo_path()?,
215 &cmd.registry,
216 proof_url,
217 cmd.skip_registration,
218 &out,
219 );
220
221 display_developer_result(&out, &result, registered.as_deref());
223 }
224 InitProfile::Ci => {
225 let (ci_env, config, keychain, passphrase_str) = gather_ci_config(&out)?;
227 let registry_path = config.registry_path.clone();
228
229 ensure_registry_dir(®istry_path)?;
231
232 let sdk_ctx = build_auths_context(®istry_path, &ctx.env_config, None)?;
234
235 let keychain_arc: Arc<dyn KeyStorage + Send + Sync> = Arc::from(keychain);
237 let signer = StorageSigner::new(Arc::clone(&keychain_arc));
238 let provider = PrefilledPassphraseProvider::new(&passphrase_str);
239 let result = initialize(
240 IdentityConfig::Ci(config),
241 &sdk_ctx,
242 keychain_arc,
243 &signer,
244 &provider,
245 None,
246 )?;
247 let result = match result {
248 InitializeResult::Ci(r) => r,
249 _ => unreachable!(),
250 };
251
252 display_ci_result(&out, &result, ci_env.as_deref());
254 }
255 InitProfile::Agent => {
256 let (keychain, config) = gather_agent_config(interactive, &out, &cmd)?;
258 let registry_path = config.registry_path.clone();
259
260 if config.dry_run {
261 display_agent_dry_run(&out, &config);
262 } else {
263 ensure_registry_dir(®istry_path)?;
265
266 let sdk_ctx = build_auths_context(®istry_path, &ctx.env_config, None)?;
268
269 let keychain_arc: Arc<dyn KeyStorage + Send + Sync> = Arc::from(keychain);
271 let signer = StorageSigner::new(Arc::clone(&keychain_arc));
272 let result = initialize(
273 IdentityConfig::Agent(config),
274 &sdk_ctx,
275 keychain_arc,
276 &signer,
277 ctx.passphrase_provider.as_ref(),
278 None,
279 )?;
280 let result = match result {
281 InitializeResult::Agent(r) => r,
282 _ => unreachable!(),
283 };
284
285 display_agent_result(&out, &result);
287 }
288 }
289 }
290
291 Ok(())
292}
293
294fn gather_developer_config(
297 interactive: bool,
298 out: &Output,
299 cmd: &InitCommand,
300) -> Result<(
301 Box<dyn KeyStorage + Send + Sync>,
302 CreateDeveloperIdentityConfig,
303)> {
304 out.print_info("Checking prerequisites...");
305 let keychain = check_keychain_access(out)?;
306 check_git_version(out)?;
307 out.print_success("Prerequisites OK");
308 out.newline();
309
310 let registry_path = get_auths_repo_path()?;
311 let alias = prompt_for_alias(interactive, cmd)?;
312 let conflict_policy = prompt_for_conflict_policy(interactive, cmd, ®istry_path, out)?;
313 let git_scope = prompt_for_git_scope(interactive)?;
314
315 let mut builder = CreateDeveloperIdentityConfig::builder(KeyAlias::new_unchecked(&alias))
316 .with_conflict_policy(conflict_policy)
317 .with_git_signing_scope(git_scope);
318
319 if !cmd.skip_registration {
320 builder = builder.with_registration(&cmd.registry);
321 }
322
323 Ok((keychain, builder.build()))
324}
325
326#[allow(clippy::type_complexity)]
327fn gather_ci_config(
328 out: &Output,
329) -> Result<(
330 Option<String>,
331 CiIdentityConfig,
332 Box<dyn KeyStorage + Send + Sync>,
333 String,
334)> {
335 out.print_info("Detecting CI environment...");
336 let ci_env = detect_ci_environment();
337 if let Some(ref vendor) = ci_env {
338 out.print_success(&format!("Detected: {}", vendor));
339 } else {
340 out.print_warn("No CI environment detected, proceeding anyway");
341 }
342 out.newline();
343
344 let registry_path = std::env::current_dir()?.join(".auths-ci");
345 let passphrase =
346 std::env::var("AUTHS_PASSPHRASE").unwrap_or_else(|_| "Ci-ephemeral-pass1!".to_string());
347
348 unsafe {
350 std::env::set_var("AUTHS_KEYCHAIN_BACKEND", "memory");
351 }
352 let keychain =
353 get_platform_keychain().map_err(|e| anyhow!("Failed to get memory keychain: {}", e))?;
354
355 out.println(&format!(" Using keychain: {}", keychain.backend_name()));
356
357 let config = CiIdentityConfig {
358 ci_environment: map_ci_environment(&ci_env),
359 registry_path,
360 };
361
362 Ok((ci_env, config, keychain, passphrase))
363}
364
365fn gather_agent_config(
366 interactive: bool,
367 out: &Output,
368 cmd: &InitCommand,
369) -> Result<(
370 Box<dyn KeyStorage + Send + Sync>,
371 auths_sdk::types::CreateAgentIdentityConfig,
372)> {
373 out.print_info("Setting capability scope...");
374 let capabilities = select_agent_capabilities(interactive, out)?;
375 let cap_names: Vec<String> = capabilities.iter().map(|c| c.name.clone()).collect();
376 out.print_success(&format!("Capabilities: {}", cap_names.join(", ")));
377 out.newline();
378
379 let parsed_caps: Vec<auths_verifier::Capability> = cap_names
380 .into_iter()
381 .filter_map(|s| auths_verifier::Capability::parse(&s).ok())
382 .collect();
383
384 let keychain = check_keychain_access(out)?;
385 let registry_path = get_auths_repo_path()?;
386
387 let config = auths_sdk::types::CreateAgentIdentityConfig::builder(
388 KeyAlias::new_unchecked("agent"),
389 ®istry_path,
390 )
391 .with_capabilities(parsed_caps)
392 .with_expiry(365 * 24 * 3600)
393 .dry_run(cmd.dry_run)
394 .build();
395
396 Ok((keychain, config))
397}
398
399fn prompt_profile(out: &Output) -> Result<InitProfile> {
402 out.print_heading("Select Setup Profile");
403 out.newline();
404
405 let items = [
406 "Developer - Full local setup with keychain and git signing",
407 "CI - Ephemeral identity for CI/CD pipelines",
408 "Agent - Scoped identity for AI agents",
409 ];
410
411 let selection = Select::new()
412 .with_prompt("Choose your setup profile")
413 .items(items)
414 .default(0)
415 .interact()?;
416
417 Ok(match selection {
418 0 => InitProfile::Developer,
419 1 => InitProfile::Ci,
420 _ => InitProfile::Agent,
421 })
422}
423
424fn prompt_for_alias(interactive: bool, cmd: &InitCommand) -> Result<String> {
425 if interactive {
426 Ok(Input::new()
427 .with_prompt("Key alias")
428 .default(cmd.key_alias.clone())
429 .interact_text()?)
430 } else {
431 Ok(cmd.key_alias.clone())
432 }
433}
434
435fn prompt_for_conflict_policy(
436 interactive: bool,
437 cmd: &InitCommand,
438 registry_path: &Path,
439 out: &Output,
440) -> Result<IdentityConflictPolicy> {
441 if cmd.force {
442 return Ok(IdentityConflictPolicy::ForceNew);
443 }
444
445 let identity_storage = RegistryIdentityStorage::new(registry_path.to_path_buf());
446 if let Ok(existing) = identity_storage.load_identity() {
447 out.println(&format!(
448 " Found existing identity: {}",
449 out.info(existing.controller_did.as_str())
450 ));
451
452 if !interactive {
453 return Ok(IdentityConflictPolicy::ReuseExisting);
454 }
455
456 let use_existing = Confirm::new()
457 .with_prompt("Use existing identity?")
458 .default(true)
459 .interact()?;
460 if use_existing {
461 return Ok(IdentityConflictPolicy::ReuseExisting);
462 }
463
464 let overwrite = Confirm::new()
465 .with_prompt("Create new identity? This will NOT delete the old one.")
466 .default(false)
467 .interact()?;
468 if !overwrite {
469 return Err(anyhow!("Setup cancelled by user"));
470 }
471 }
472
473 Ok(IdentityConflictPolicy::ForceNew)
474}
475
476fn prompt_for_git_scope(interactive: bool) -> Result<GitSigningScope> {
477 if !interactive {
478 return Ok(GitSigningScope::Global);
479 }
480
481 let choice = Select::new()
482 .with_prompt("Configure git signing for")
483 .items([
484 "This repository only (--local)",
485 "All repositories (--global)",
486 ])
487 .default(1)
488 .interact()?;
489
490 if choice == 0 {
491 let repo_path = std::env::current_dir()?;
492 Ok(GitSigningScope::Local { repo_path })
493 } else {
494 Ok(GitSigningScope::Global)
495 }
496}
497
498fn prompt_platform_verification(
499 out: &Output,
500 passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
501 env_config: &auths_core::config::EnvironmentConfig,
502) -> Result<Option<(String, String)>> {
503 let items = [
504 "GitHub — link your GitHub identity (recommended)",
505 "GitLab — coming soon",
506 "Anonymous — skip platform verification",
507 ];
508
509 let selection = Select::new()
510 .with_prompt("Claim your Developer Passport")
511 .items(items)
512 .default(0)
513 .interact()?;
514
515 match selection {
516 0 => {
517 use std::time::Duration;
518
519 use auths_core::ports::platform::OAuthDeviceFlowProvider;
520 use auths_core::ports::platform::PlatformProofPublisher;
521 use auths_infra_http::{HttpGistPublisher, HttpGitHubOAuthProvider};
522 use auths_sdk::workflows::platform::create_signed_platform_claim;
523
524 const GITHUB_CLIENT_ID: &str = "Ov23lio2CiTHBjM2uIL4";
525 let client_id = std::env::var("AUTHS_GITHUB_CLIENT_ID")
526 .unwrap_or_else(|_| GITHUB_CLIENT_ID.to_string());
527
528 let auths_dir = get_auths_repo_path()?;
529 let ctx = build_auths_context(&auths_dir, env_config, Some(passphrase_provider))?;
530
531 let oauth = HttpGitHubOAuthProvider::new();
532 let publisher = HttpGistPublisher::new();
533
534 let rt = tokio::runtime::Runtime::new().context("failed to create async runtime")?;
535
536 let device_code = rt
537 .block_on(oauth.request_device_code(&client_id, "read:user gist"))
538 .map_err(|e| anyhow::anyhow!("{e}"))?;
539
540 out.println(&format!(
541 " Enter this code: {}",
542 out.bold(&device_code.user_code)
543 ));
544 out.println(&format!(
545 " At: {}",
546 out.info(&device_code.verification_uri)
547 ));
548 if let Err(e) = open::that(&device_code.verification_uri) {
549 out.print_warn(&format!("Could not open browser automatically: {e}"));
550 out.println(" Please open the URL above manually.");
551 } else {
552 out.println(" Browser opened — waiting for authorization...");
553 }
554
555 let expires_in = Duration::from_secs(device_code.expires_in);
556 let interval = Duration::from_secs(device_code.interval);
557
558 let access_token = rt
559 .block_on(oauth.poll_for_token(
560 &client_id,
561 &device_code.device_code,
562 interval,
563 expires_in,
564 ))
565 .map_err(|e| anyhow::anyhow!("{e}"))?;
566
567 let profile = rt
568 .block_on(oauth.fetch_user_profile(&access_token))
569 .map_err(|e| anyhow::anyhow!("{e}"))?;
570
571 out.print_success(&format!("Authenticated as @{}", profile.login));
572
573 let controller_did =
574 auths_sdk::pairing::load_controller_did(ctx.identity_storage.as_ref())
575 .map_err(|e| anyhow::anyhow!("{e}"))?;
576
577 let identity_did =
578 auths_core::storage::keychain::IdentityDID::new_unchecked(controller_did.clone());
579 let aliases = ctx
580 .key_storage
581 .list_aliases_for_identity(&identity_did)
582 .context("failed to list key aliases")?;
583 let key_alias = aliases
584 .into_iter()
585 .find(|a| !a.contains("--next-"))
586 .ok_or_else(|| anyhow::anyhow!("no signing key found for {controller_did}"))?;
587
588 let claim_json = create_signed_platform_claim(
589 "github",
590 &profile.login,
591 &controller_did,
592 &key_alias,
593 &ctx,
594 chrono::Utc::now(),
595 )
596 .map_err(|e| anyhow::anyhow!("{e}"))?;
597
598 let proof_url = rt
599 .block_on(publisher.publish_proof(&access_token, &claim_json))
600 .map_err(|e| anyhow::anyhow!("{e}"))?;
601
602 out.print_success(&format!("Published proof Gist: {}", out.info(&proof_url)));
603
604 Ok(Some((proof_url, profile.login)))
605 }
606 1 => {
607 out.print_warn("GitLab integration is coming soon. Continuing as anonymous.");
608 Ok(None)
609 }
610 _ => Ok(None),
611 }
612}
613
614fn display_developer_result(
617 out: &Output,
618 result: &auths_sdk::result::DeveloperIdentityResult,
619 registered: Option<&str>,
620) {
621 out.newline();
622 out.print_heading("You are on the Web of Trust!");
623 out.newline();
624 out.println(&format!(" Identity: {}", out.info(&result.identity_did)));
625 out.println(&format!(" Key alias: {}", out.info(&result.key_alias)));
626 if let Some(registry) = registered {
627 out.println(&format!(" Registry: {}", out.info(registry)));
628 }
629 let did_prefix = result
630 .identity_did
631 .strip_prefix("did:keri:")
632 .unwrap_or(&result.identity_did);
633 out.println(&format!(
634 " Profile: {}",
635 out.info(&format!("https://auths.dev/registry/identity/{did_prefix}"))
636 ));
637 out.newline();
638 out.print_success("Your next commit will be signed with Auths!");
639 out.println(" Run `auths status` to check your identity");
640}
641
642fn display_ci_result(
643 out: &Output,
644 result: &auths_sdk::result::CiIdentityResult,
645 ci_vendor: Option<&str>,
646) {
647 out.print_success(&format!("CI identity: {}", &result.identity_did));
648 out.newline();
649
650 out.print_heading("Add these to your CI secrets:");
651 out.println("─".repeat(50).as_str());
652 for line in &result.env_block {
653 println!("{}", line);
654 }
655 out.println("─".repeat(50).as_str());
656 out.newline();
657
658 if let Some(vendor) = ci_vendor {
659 write_ci_vendor_hints(out, vendor);
660 }
661
662 out.print_success("CI setup complete!");
663 out.println(" Add the environment variables to your CI secrets");
664 out.println(" Commits made in CI will be signed with the ephemeral identity");
665}
666
667fn display_agent_result(out: &Output, result: &auths_sdk::result::AgentIdentityResult) {
668 out.print_heading("Agent Setup Complete!");
669 out.newline();
670 out.println(&format!(" Identity: {}", out.info(&result.agent_did)));
671 let cap_display: Vec<String> = result.capabilities.iter().map(|c| c.to_string()).collect();
672 out.println(&format!(" Capabilities: {}", cap_display.join(", ")));
673 out.newline();
674 out.print_success("Agent is ready to sign commits!");
675 out.println(" Start the agent: auths agent start");
676 out.println(" Check status: auths agent status");
677}
678
679fn display_agent_dry_run(out: &Output, config: &auths_sdk::types::CreateAgentIdentityConfig) {
680 out.print_heading("Dry Run — no files or identities will be created");
681 out.newline();
682 out.println(&format!(" Storage: {}", config.registry_path.display()));
683 out.println(&format!(" Capabilities: {:?}", config.capabilities));
684 if let Some(secs) = config.expires_in_secs {
685 out.println(&format!(" Expires in: {}s", secs));
686 }
687 out.newline();
688 out.print_info("TOML config that would be generated:");
689 let provisioning_config = auths_id::agent_identity::AgentProvisioningConfig {
690 agent_name: config.alias.to_string(),
691 capabilities: config.capabilities.iter().map(|c| c.to_string()).collect(),
692 expires_in_secs: config.expires_in_secs,
693 delegated_by: None,
694 storage_mode: auths_id::agent_identity::AgentStorageMode::Persistent { repo_path: None },
695 };
696 out.println(&auths_id::agent_identity::format_agent_toml(
697 "did:keri:E<pending>",
698 "agent-key",
699 &provisioning_config,
700 ));
701}
702
703fn submit_registration(
706 repo_path: &Path,
707 registry_url: &str,
708 proof_url: Option<String>,
709 skip: bool,
710 out: &Output,
711) -> Option<String> {
712 if skip {
713 out.print_info("Registration skipped (--skip-registration)");
714 return None;
715 }
716
717 out.print_info("Publishing identity to Auths Registry...");
718 let rt = match tokio::runtime::Runtime::new() {
719 Ok(rt) => rt,
720 Err(e) => {
721 out.print_warn(&format!("Could not create async runtime: {e}"));
722 return None;
723 }
724 };
725
726 let backend = Arc::new(GitRegistryBackend::from_config_unchecked(
727 RegistryConfig::single_tenant(repo_path),
728 ));
729 let identity_storage: Arc<dyn IdentityStorage + Send + Sync> =
730 Arc::new(RegistryIdentityStorage::new(repo_path.to_path_buf()));
731 let attestation_store = Arc::new(RegistryAttestationStorage::new(repo_path));
732 let attestation_source: Arc<dyn AttestationSource + Send + Sync> = attestation_store;
733
734 let registry_client = HttpRegistryClient::new();
735
736 match rt.block_on(auths_sdk::registration::register_identity(
737 identity_storage,
738 backend,
739 attestation_source,
740 registry_url,
741 proof_url,
742 ®istry_client,
743 )) {
744 Ok(outcome) => {
745 out.print_success(&format!("Identity registered at {}", outcome.registry));
746 Some(outcome.registry)
747 }
748 Err(auths_sdk::error::RegistrationError::AlreadyRegistered) => {
749 out.print_success("Already registered on this registry");
750 Some(registry_url.to_string())
751 }
752 Err(auths_sdk::error::RegistrationError::QuotaExceeded) => {
753 out.print_warn("Registration quota exceeded. Run `auths id register` to retry later.");
754 None
755 }
756 Err(auths_sdk::error::RegistrationError::NetworkError(_)) => {
757 out.print_warn(
758 "Could not reach the registry (offline?). Your local setup is complete.",
759 );
760 out.println(" Run `auths id register` when you're back online.");
761 None
762 }
763 Err(auths_sdk::error::RegistrationError::LocalDataError(e)) => {
764 out.print_warn(&format!("Could not prepare registration payload: {e}"));
765 out.println(" Run `auths id register` to retry.");
766 None
767 }
768 Err(e) => {
769 out.print_warn(&format!("Registration failed: {e}"));
770 None
771 }
772 }
773}
774
775fn ensure_registry_dir(registry_path: &Path) -> Result<()> {
776 if !registry_path.exists() {
777 std::fs::create_dir_all(registry_path).with_context(|| {
778 format!(
779 "Failed to create registry directory: {}",
780 registry_path.display()
781 )
782 })?;
783 }
784 if git2::Repository::open(registry_path).is_err() {
785 git2::Repository::init(registry_path).with_context(|| {
786 format!(
787 "Failed to initialize git repository: {}",
788 registry_path.display()
789 )
790 })?;
791 }
792 auths_sdk::setup::install_registry_hook(registry_path);
793 Ok(())
794}
795
796fn check_keychain_access(out: &Output) -> Result<Box<dyn KeyStorage + Send + Sync>> {
797 match get_platform_keychain() {
798 Ok(keychain) => {
799 out.println(&format!(
800 " Keychain: {} (accessible)",
801 out.success(keychain.backend_name())
802 ));
803 Ok(keychain)
804 }
805 Err(e) => Err(anyhow!("Keychain not accessible: {}", e)),
806 }
807}
808
809fn map_ci_environment(detected: &Option<String>) -> CiEnvironment {
810 match detected.as_deref() {
811 Some("GitHub Actions") => CiEnvironment::GitHubActions,
812 Some("GitLab CI") => CiEnvironment::GitLabCi,
813 Some(name) => CiEnvironment::Custom {
814 name: name.to_string(),
815 },
816 None => CiEnvironment::Unknown,
817 }
818}
819
820fn write_ci_vendor_hints(out: &Output, vendor: &str) {
821 out.newline();
822 out.print_heading(&format!("Hints for {}", vendor));
823
824 match vendor {
825 "GitHub Actions" => {
826 out.println("Add to your workflow (.github/workflows/*.yml):");
827 out.newline();
828 out.println(" env:");
829 out.println(" AUTHS_KEYCHAIN_BACKEND: memory");
830 out.newline();
831 out.println(" steps:");
832 out.println(" - uses: actions/checkout@v4");
833 out.println(" - run: auths init --profile ci --non-interactive");
834 }
835 "GitLab CI" => {
836 out.println("Add to .gitlab-ci.yml:");
837 out.newline();
838 out.println(" variables:");
839 out.println(" AUTHS_KEYCHAIN_BACKEND: memory");
840 out.newline();
841 out.println(" before_script:");
842 out.println(" - auths init --profile ci --non-interactive");
843 }
844 _ => {
845 out.println("Set these environment variables in your CI:");
846 out.println(" AUTHS_KEYCHAIN_BACKEND=memory");
847 }
848 }
849 out.newline();
850}
851
852impl crate::commands::executable::ExecutableCommand for InitCommand {
855 fn execute(&self, ctx: &CliConfig) -> anyhow::Result<()> {
856 handle_init(self.clone(), ctx)
857 }
858}
859
860#[cfg(test)]
861mod tests {
862 use super::*;
863
864 #[test]
865 fn test_setup_profile_display() {
866 assert_eq!(InitProfile::Developer.to_string(), "developer");
867 assert_eq!(InitProfile::Ci.to_string(), "ci");
868 assert_eq!(InitProfile::Agent.to_string(), "agent");
869 }
870
871 #[test]
872 fn test_setup_command_defaults() {
873 let cmd = InitCommand {
874 non_interactive: false,
875 profile: None,
876 key_alias: DEFAULT_KEY_ALIAS.to_string(),
877 force: false,
878 dry_run: false,
879 registry: DEFAULT_REGISTRY_URL.to_string(),
880 skip_registration: false,
881 };
882 assert!(!cmd.non_interactive);
883 assert!(cmd.profile.is_none());
884 assert_eq!(cmd.key_alias, "main");
885 assert!(!cmd.force);
886 assert!(!cmd.dry_run);
887 assert_eq!(cmd.registry, "https://auths-registry.fly.dev");
888 assert!(!cmd.skip_registration);
889 }
890
891 #[test]
892 fn test_setup_command_with_profile() {
893 let cmd = InitCommand {
894 non_interactive: true,
895 profile: Some(InitProfile::Ci),
896 key_alias: "ci-key".to_string(),
897 force: true,
898 dry_run: false,
899 registry: DEFAULT_REGISTRY_URL.to_string(),
900 skip_registration: false,
901 };
902 assert!(cmd.non_interactive);
903 assert!(matches!(cmd.profile, Some(InitProfile::Ci)));
904 assert_eq!(cmd.key_alias, "ci-key");
905 assert!(cmd.force);
906 }
907
908 #[test]
909 fn test_map_ci_environment() {
910 assert!(matches!(
911 map_ci_environment(&Some("GitHub Actions".into())),
912 CiEnvironment::GitHubActions
913 ));
914 assert!(matches!(
915 map_ci_environment(&Some("GitLab CI".into())),
916 CiEnvironment::GitLabCi
917 ));
918 assert!(matches!(map_ci_environment(&None), CiEnvironment::Unknown));
919 }
920}