diff --git a/cmd/app.go b/cmd/app.go index dfc7581..c62ac52 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -49,6 +49,7 @@ type Handlers struct { RunReviewCleanup cli.ActionFunc RunAttestationTrailer cli.ActionFunc RunSetup cli.ActionFunc + RunOnboard cli.ActionFunc RunUI cli.ActionFunc RunUsageInspect cli.ActionFunc RunInternalClaudePreToolUse cli.ActionFunc @@ -321,6 +322,29 @@ func BuildApp(version, buildTime, gitCommit, reviewMode string, baseFlags, debug }, Action: h.RunSetup, }, + { + Name: "onboard", + Usage: "Perform non-interactive onboarding using an onboarding API key", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "api-url", + Aliases: []string{"base-url"}, + Usage: "override LiveReview API base URL for onboarding", + }, + &cli.StringFlag{ + Name: "onboarding-key", + Aliases: []string{"key"}, + Usage: "onboarding API key to use for authentication", + Required: true, + EnvVars: []string{"LRC_API_KEY"}, + }, + &cli.BoolFlag{ + Name: "yes", + Usage: "run non-interactively; replaces config file if already exists without confirmation", + }, + }, + Action: h.RunOnboard, + }, { Name: "ui", Usage: "Open local web UI to manage your git-lrc", diff --git a/internal/appui/onboard.go b/internal/appui/onboard.go new file mode 100644 index 0000000..b9598bf --- /dev/null +++ b/internal/appui/onboard.go @@ -0,0 +1,110 @@ +package appui + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "os" + "strings" + "time" + + "github.com/HexmosTech/git-lrc/network" + setuptpl "github.com/HexmosTech/git-lrc/setup" + "github.com/urfave/cli/v2" +) + +// RunOnboard handles the "lrc onboard" command. +func RunOnboard(c *cli.Context) error { + // 1. Resolve API URL + apiURL := strings.TrimSpace(c.String("api-url")) + if apiURL == "" { + apiURL = os.Getenv("LRC_API_URL") + } + if apiURL == "" { + apiURL = setuptpl.CloudAPIURL + } + apiURL = strings.TrimRight(apiURL, "/") + + // 2. Resolve Onboarding API Key + onboardingKey := strings.TrimSpace(c.String("onboarding-key")) + if onboardingKey == "" { + return errors.New("onboarding API key cannot be empty; pass via --onboarding-key or LRC_API_KEY env var") + } + + // 3. Check existing config details + details, err := setuptpl.ReadExistingConfigDetails() + if err != nil { + return fmt.Errorf("failed to read existing config details: %w", err) + } + + // If config exists and we are interactive / no "--yes" flag, ask the user + if details.Exists && !c.Bool("yes") && isInteractiveSetupStdin() { + replace, err := promptSetupYesNo(" Existing config file detected. Replace it?", false) + if err != nil { + return err + } + if !replace { + fmt.Println(" Onboarding cancelled. Existing configuration preserved.") + return nil + } + } + + fmt.Printf(" Onboarding to LiveReview server at %s...\n", apiURL) + + // 4. Hit the onboard endpoint + client := network.NewClient(30 * time.Second) + resp, err := network.SetupOnboard(client, apiURL, onboardingKey) + if err != nil { + return fmt.Errorf("failed to contact LiveReview API: %w", err) + } + if resp.StatusCode != http.StatusOK { + var errorResp struct { + Error string `json:"error"` + } + if err := json.Unmarshal(resp.Body, &errorResp); err == nil && errorResp.Error != "" { + return fmt.Errorf("onboarding failed (status %d): %s", resp.StatusCode, errorResp.Error) + } + return fmt.Errorf("onboarding failed (status %d)", resp.StatusCode) + } + + // 5. Parse response + var onboardResp struct { + APIKey string `json:"api_key"` + OrgID json.Number `json:"org_id"` + OrgName string `json:"org_name"` + JWT string `json:"jwt"` + RefreshToken string `json:"refresh_token"` + } + if err := json.Unmarshal(resp.Body, &onboardResp); err != nil { + return fmt.Errorf("failed to parse onboarding response: %w", err) + } + + // 6. Backup config if it exists + if details.Exists { + backupPath, err := setuptpl.BackupExistingConfig(nil) + if err != nil { + return fmt.Errorf("failed to backup existing config: %w", err) + } + if backupPath != "" { + fmt.Printf(" 📦 Backed up existing config to %s\n", backupPath) + } + } + + // 7. Write new configuration + result := &setuptpl.SetupResult{ + PlainAPIKey: onboardResp.APIKey, + OrgID: onboardResp.OrgID.String(), + OrgName: onboardResp.OrgName, + AccessToken: onboardResp.JWT, + RefreshToken: onboardResp.RefreshToken, + } + + if err := setuptpl.WriteConfigWithOptions(result, setuptpl.WriteConfigOptions{APIURL: apiURL}); err != nil { + return fmt.Errorf("failed to write configuration: %w", err) + } + + fmt.Println(" ✅ Onboarding completed successfully!") + fmt.Printf(" Configuration saved to %s\n", details.Path) + return nil +} diff --git a/main.go b/main.go index fb7dafb..0b6f3b9 100644 --- a/main.go +++ b/main.go @@ -73,6 +73,7 @@ func main() { RunReviewCleanup: func(c *cli.Context) error { return reviewdb.RunReviewDBCleanup(c.Bool("verbose")) }, RunAttestationTrailer: appcore.RunAttestationTrailer, RunSetup: appui.RunSetup, + RunOnboard: appui.RunOnboard, RunUI: appui.RunUI, RunUsageInspect: appcore.RunUsageInspect, RunInternalClaudePreToolUse: appcore.RunInternalClaudePreToolUse, diff --git a/network/endpoints.go b/network/endpoints.go index d08d8f9..fcb8c53 100644 --- a/network/endpoints.go +++ b/network/endpoints.go @@ -14,6 +14,10 @@ func SetupEnsureCloudUserURL(baseURL string) string { return strings.TrimSuffix(baseURL, "/") + "/api/v1/auth/ensure-cloud-user" } +func SetupOnboardURL(baseURL string) string { + return strings.TrimSuffix(baseURL, "/") + "/api/v1/auth/onboard" +} + func SetupAuthLoginURL(baseURL string) string { return strings.TrimSuffix(baseURL, "/") + "/api/v1/auth/login" } diff --git a/network/network_status.md b/network/network_status.md index f4176be..55623a0 100644 --- a/network/network_status.md +++ b/network/network_status.md @@ -10,7 +10,7 @@ This document tracks network-side operations in git-lrc as an auditable inventor - Network boundary: outbound HTTP API operations and response handling in network package. - Modes represented: api. -- Operation count tracked: 22 operations. +- Operation count tracked: 24 operations. - Severity distribution: High 10, Medium 7, Low 2. - Current diff note: self-hosted setup now uses LiveReview email/password auth endpoints (`/api/v1/auth/login`, `/api/v1/auth/setup-status`, `/api/v1/auth/setup`) in addition to existing cloud ensure-cloud-user setup path. - Current diff note: internal reviewapi helper evidence links were revalidated after git path helper additions; network inventory scope is unchanged. @@ -53,6 +53,7 @@ This document tracks network-side operations in git-lrc as an auditable inventor | SetupValidateConnectorKey | api | Provider key and validation request body | Validate AI connector key before persistence | High | Third-party key exposure/handling risk | Compensated by authenticated request path plus connector key redaction in setup error surfaces; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L41) | | SetupCreateConnector | api | Connector configuration payload | Persist connector configuration via LiveReview API | High | Misconfiguration and sensitive metadata transmission risk | Compensated by bearer auth plus org context boundary; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L46) | | SetupListConnectors | api | Existing connector inventory for current org | Inspect minimum AI readiness during setup and re-auth preflight | High | Sensitive connector metadata and auth-context exposure risk | Compensated by bearer auth plus org context boundary; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L51) | +| SetupOnboard | api | Onboarding API key (X-API-Key header) and returned token set | Non-interactively onboard a new machine using an onboarding API key | High | Exposure of onboarding key and returned credentials | Compensated by direct, secure binary execution, atomic config writes (0600), and short-lived setup context; residual risk acceptable | [network/setup_operations.go](setup_operations.go#L56) | ## Inventory: Proxy And Forwarding APIs @@ -76,10 +77,11 @@ This document tracks network-side operations in git-lrc as an auditable inventor | Client.DoJSON | api | Request/response JSON payload bytes | Standard JSON HTTP call wrapper | Medium | Medium risk from broad transport usage and status-handling variance | Compensated by centralized transport wrapper with timeout controls; acceptable risk | [network/http_client.go](http_client.go#L43) | | Client.Do | api | Raw HTTP request/response bytes | Generic HTTP call wrapper for non-JSON/raw workflows | Medium | Medium risk from raw payload handling flexibility | Partially compensated by shared client boundary; Suggestion: document callsite expectations for raw bodies | [network/http_client.go](http_client.go#L89) | | SetupEnsureCloudUserURL | api | Base URL plus endpoint normalization inputs | Normalize endpoint composition and reduce path ambiguity | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L13) | -| SetupAuthLoginURL | api | Base URL plus endpoint normalization inputs | Build self-hosted login endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L17) | -| SetupUIConfigURL | api | Base URL | Build UI config URL | Low | Low risk | Compensated by public endpoint utility; acceptable risk | [network/endpoints.go](endpoints.go#L21) | -| SetupAuthSetupStatusURL | api | Base URL plus endpoint normalization inputs | Build self-hosted setup-status endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L25) | -| SetupAuthSetupURL | api | Base URL plus endpoint normalization inputs | Build self-hosted initial-admin setup endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L29) | +| SetupOnboardURL | api | Base URL plus endpoint normalization inputs | Build onboarding endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L17) | +| SetupAuthLoginURL | api | Base URL plus endpoint normalization inputs | Build self-hosted login endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L21) | +| SetupUIConfigURL | api | Base URL | Build UI config URL | Low | Low risk | Compensated by public endpoint utility; acceptable risk | [network/endpoints.go](endpoints.go#L25) | +| SetupAuthSetupStatusURL | api | Base URL plus endpoint normalization inputs | Build self-hosted setup-status endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L29) | +| SetupAuthSetupURL | api | Base URL plus endpoint normalization inputs | Build self-hosted initial-admin setup endpoint URL | Medium | Medium risk if normalization logic diverges from endpoint assumptions | Compensated by centralized URL builder utility; acceptable risk | [network/endpoints.go](endpoints.go#L33) | | PollReview | api | Review IDs, status payloads, timeout state | Timeout-bounded polling orchestration in review runtime | High | High availability/latency risk if review service is degraded | Compensated by bounded timeout and interval controls; residual risk acceptable | [internal/reviewapi/helpers.go](../internal/reviewapi/helpers.go#L201) | | formatJSONParseError | api | Response body text for parse diagnostics | Improve operator diagnostics when endpoint/port mismatches occur | Low | Low risk diagnostic utility behavior | Compensated by safer error interpretation path; acceptable risk | [internal/reviewapi/helpers.go](../internal/reviewapi/helpers.go#L129) | diff --git a/network/setup_operations.go b/network/setup_operations.go index fec020a..22dce98 100644 --- a/network/setup_operations.go +++ b/network/setup_operations.go @@ -51,3 +51,10 @@ func SetupCreateConnector(client *Client, cloudBase, orgID string, payload any, func SetupListConnectors(client *Client, cloudBase, orgID, accessToken string) (*Response, error) { return client.DoJSON(http.MethodGet, SetupCreateConnectorURL(cloudBase), nil, accessToken, orgID, nil) } + +// SetupOnboard submits the onboarding request to LiveReview. +func SetupOnboard(client *Client, cloudBase, onboardingKey string) (*Response, error) { + return client.DoJSON(http.MethodPost, SetupOnboardURL(cloudBase), nil, "", "", map[string]string{ + "X-API-Key": onboardingKey, + }) +} diff --git a/scripts/lrc-install.ps1 b/scripts/lrc-install.ps1 index 35143a0..672b436 100644 --- a/scripts/lrc-install.ps1 +++ b/scripts/lrc-install.ps1 @@ -165,90 +165,6 @@ function Print-ElevationHelp { Write-Host "" } -function ConvertTo-TomlQuotedValue { - param([string]$Value) - $escaped = $Value - $escaped = $escaped -replace '\\', '\\\\' - $escaped = $escaped -replace '"', '\\"' - $escaped = $escaped -replace "`t", '\\t' - $escaped = $escaped -replace "`r", '\\r' - $escaped = $escaped -replace "`n", '\\n' - return '"' + $escaped + '"' -} - -function Upsert-LrcConfigValues { - param( - [string]$Content, - [string]$Key1, - [string]$Value1, - [string]$Key2, - [string]$Value2 - ) - - $replacement1 = "$Key1 = $(ConvertTo-TomlQuotedValue -Value $Value1)" - $replacement2 = "$Key2 = $(ConvertTo-TomlQuotedValue -Value $Value2)" - $lines = @($Content -split "`r?`n", -1) - $updatedLines = New-Object System.Collections.Generic.List[string] - $found1 = $false - $found2 = $false - $insertedBeforeSection = $false - - for ($i = 0; $i -lt $lines.Count; $i++) { - $line = $lines[$i] - $trimmed = $line.TrimStart() - - if ($trimmed -match '^#|^;') { - $updatedLines.Add($line) - continue - } - - if (-not $found1 -and $trimmed -match "^$([regex]::Escape($Key1))\s*=") { - $updatedLines.Add($replacement1) - $found1 = $true - continue - } - - if (-not $found2 -and $trimmed -match "^$([regex]::Escape($Key2))\s*=") { - $updatedLines.Add($replacement2) - $found2 = $true - continue - } - - if (-not $insertedBeforeSection -and $trimmed -match '^\[') { - $insertedAny = $false - if (-not $found1) { - $updatedLines.Add($replacement1) - $found1 = $true - $insertedAny = $true - } - if (-not $found2) { - $updatedLines.Add($replacement2) - $found2 = $true - $insertedAny = $true - } - if ($insertedAny) { - $updatedLines.Add('') - } - $insertedBeforeSection = $true - } - - $updatedLines.Add($line) - } - - if (-not $found1 -or -not $found2) { - if ($updatedLines.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($updatedLines[$updatedLines.Count - 1])) { - $updatedLines.Add('') - } - if (-not $found1) { - $updatedLines.Add($replacement1) - } - if (-not $found2) { - $updatedLines.Add($replacement2) - } - } - - return ($updatedLines -join "`n") -} # Detect admin status once; elevation is only needed for legacy cleanup, not for fresh installs $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) @@ -532,88 +448,11 @@ try { # Create config file if API key and URL are provided if ($env:LRC_API_KEY -and $env:LRC_API_URL) { - $CONFIG_FILE = "$env:USERPROFILE\.lrc.toml" - - # Check if config already exists - if (Test-Path $CONFIG_FILE) { - Write-Host "Note: Config file already exists at $CONFIG_FILE" -ForegroundColor Yellow - - # Read from console host even when piped - $replaceConfig = "n" - try { - if ([Environment]::UserInteractive) { - Write-Host -NoNewline "Replace existing config? [y/N]: " - $replaceConfig = [Console]::ReadLine() - if ([string]::IsNullOrWhiteSpace($replaceConfig)) { - $replaceConfig = "n" - } - } - } catch { - Write-Host "Replace existing config? [y/N]: n (defaulting to No)" -ForegroundColor Yellow - } - - if ($replaceConfig -match '^[Yy]$') { - Write-Host -NoNewline "Replacing config file at $CONFIG_FILE (with backup + merge)... " - try { - $backupPath = "$CONFIG_FILE.bak.$(Get-Date -Format 'yyyyMMdd-HHmmss')" - Copy-Item -Path $CONFIG_FILE -Destination $backupPath -Force -ErrorAction Stop - - $existingConfig = Get-Content -Path $CONFIG_FILE -Raw -ErrorAction Stop - $updatedConfig = Upsert-LrcConfigValues -Content $existingConfig -Key1 "api_key" -Value1 $env:LRC_API_KEY -Key2 "api_url" -Value2 $env:LRC_API_URL - Set-Content -Path $CONFIG_FILE -Value $updatedConfig -NoNewline -Encoding UTF8 - - # Restrict config file to current user only (contains API key) - $acl = Get-Acl $CONFIG_FILE - $acl.SetAccessRuleProtection($true, $false) - $acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) } | Out-Null - $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( - [System.Security.Principal.WindowsIdentity]::GetCurrent().Name, - "FullControl", "Allow") - $acl.AddAccessRule($rule) - Set-Acl -Path $CONFIG_FILE -AclObject $acl - Write-Host "$OK" -ForegroundColor Green - Write-Host "Config file updated and backed up to: $backupPath" -ForegroundColor Green - } catch { - try { - if ($backupPath -and (Test-Path $backupPath)) { - Copy-Item -Path $backupPath -Destination $CONFIG_FILE -Force -ErrorAction SilentlyContinue - } - } catch { - Write-Host "Warning: Failed to restore backup $backupPath" -ForegroundColor Yellow - Write-Host $_.Exception.Message -ForegroundColor Yellow - } - Write-Host "$FAIL" -ForegroundColor Red - Write-Host "Warning: Failed to update config file (restored from backup when possible)" -ForegroundColor Yellow - Write-Host $_.Exception.Message -ForegroundColor Yellow - exit 1 - } - } else { - Write-Host "Skipping config creation to preserve existing settings" -ForegroundColor Yellow - } - } else { - Write-Host -NoNewline "Creating config file at $CONFIG_FILE... " - try { - $configContent = @" -api_key = "$($env:LRC_API_KEY)" -api_url = "$($env:LRC_API_URL)" -"@ - Set-Content -Path $CONFIG_FILE -Value $configContent -NoNewline - # Restrict config file to current user only (contains API key) - $acl = Get-Acl $CONFIG_FILE - $acl.SetAccessRuleProtection($true, $false) - $acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) } | Out-Null - $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( - [System.Security.Principal.WindowsIdentity]::GetCurrent().Name, - "FullControl", "Allow") - $acl.AddAccessRule($rule) - Set-Acl -Path $CONFIG_FILE -AclObject $acl - Write-Host "$OK" -ForegroundColor Green - Write-Host "Config file created with your API credentials" -ForegroundColor Green - } catch { - Write-Host "$FAIL" -ForegroundColor Red - Write-Host "Warning: Failed to create config file" -ForegroundColor Yellow - Write-Host $_.Exception.Message -ForegroundColor Yellow - } + # Delegate to the newly compiled lrc binary for a secure, transaction-safe, native onboarding flow + & $INSTALL_PATH onboard --api-url $env:LRC_API_URL --yes + if ($LASTEXITCODE -ne 0) { + Write-Host "Error: Onboarding failed." -ForegroundColor Red + exit 1 } } diff --git a/scripts/lrc-install.sh b/scripts/lrc-install.sh index 2bbf251..5b2b40d 100755 --- a/scripts/lrc-install.sh +++ b/scripts/lrc-install.sh @@ -146,106 +146,6 @@ print_windows_handoff_help() { echo "" } -toml_escape() { - printf '%s' "$1" | sed -e ':a' -e 'N' -e '$!ba' \ - -e 's/\\/\\\\/g' \ - -e 's/"/\\"/g' \ - -e 's/\t/\\t/g' \ - -e 's/\r/\\r/g' \ - -e 's/\n/\\n/g' -} - -upsert_config_values() { - local file_path="$1" - local key1="$2" - local value1="$3" - local key2="$4" - local value2="$5" - local escaped_value1 - local escaped_value2 - escaped_value1="$(toml_escape "$value1")" - escaped_value2="$(toml_escape "$value2")" - local replacement1 - local replacement2 - replacement1="$key1 = \"$escaped_value1\"" - replacement2="$key2 = \"$escaped_value2\"" - local tmp_path - tmp_path="${file_path}.tmp.$$" - - awk -v key1="$key1" -v key2="$key2" -v replacement1="$replacement1" -v replacement2="$replacement2" ' - BEGIN { - found1 = 0 - found2 = 0 - inserted_before_section = 0 - saw_nonempty = 0 - } - { - line = $0 - trimmed = line - sub(/^[[:space:]]+/, "", trimmed) - - if (trimmed ~ /^#|^;/) { - print line - if (line ~ /[^[:space:]]/) { - saw_nonempty = 1 - } - next - } - - if (found1 == 0 && trimmed ~ "^" key1 "[[:space:]]*=") { - print replacement1 - found1 = 1 - saw_nonempty = 1 - next - } - if (found2 == 0 && trimmed ~ "^" key2 "[[:space:]]*=") { - print replacement2 - found2 = 1 - saw_nonempty = 1 - next - } - - if (inserted_before_section == 0 && trimmed ~ /^\[/) { - inserted_any = 0 - if (found1 == 0) { - print replacement1 - found1 = 1 - inserted_any = 1 - } - if (found2 == 0) { - print replacement2 - found2 = 1 - inserted_any = 1 - } - if (inserted_any == 1) { - print "" - } - inserted_before_section = 1 - } - - print line - - if (line ~ /[^[:space:]]/) { - saw_nonempty = 1 - } - } - END { - if (found1 == 0 || found2 == 0) { - if (saw_nonempty == 1) { - print "" - } - if (found1 == 0) { - print replacement1 - } - if (found2 == 0) { - print replacement2 - } - } - } - ' "$file_path" > "$tmp_path" - - mv "$tmp_path" "$file_path" -} # Require git to be present; we also install lrc alongside the git binary if ! command -v git >/dev/null 2>&1; then @@ -606,49 +506,12 @@ fi # Create config file if API key and URL are provided if [ -n "$LRC_API_KEY" ] && [ -n "$LRC_API_URL" ]; then - CONFIG_DIR="$HOME/.config" - CONFIG_FILE="$HOME/.lrc.toml" - - # Check if config already exists - if [ -f "$CONFIG_FILE" ]; then - echo -e "${YELLOW}Note: Config file already exists at $CONFIG_FILE${NC}" - echo -n "Replace existing config? [y/N]: " - # Read from terminal even when stdin is piped - if [ -t 0 ]; then - read -r REPLACE_CONFIG - else - read -r REPLACE_CONFIG < /dev/tty 2>/dev/null || REPLACE_CONFIG="n" - fi - if [[ "$REPLACE_CONFIG" =~ ^[Yy]$ ]]; then - echo -n "Replacing config file at $CONFIG_FILE (with backup + merge)... " - mkdir -p "$CONFIG_DIR" - BACKUP_PATH="${CONFIG_FILE}.bak.$(date +%Y%m%d-%H%M%S)" - cp "$CONFIG_FILE" "$BACKUP_PATH" - - if upsert_config_values "$CONFIG_FILE" "api_key" "$LRC_API_KEY" "api_url" "$LRC_API_URL"; then - chmod 600 "$CONFIG_FILE" - echo -e "${GREEN}OK${NC}" - echo -e "${GREEN}Config file updated and backed up to:${NC} $BACKUP_PATH" - else - cp "$BACKUP_PATH" "$CONFIG_FILE" - chmod 600 "$CONFIG_FILE" - echo -e "${RED}FAIL${NC}" - echo -e "${RED}Error: Failed to update config; restored from backup${NC}" - exit 1 - fi - else - echo -e "${YELLOW}Skipping config creation to preserve existing settings${NC}" - fi + # Delegate to the newly compiled lrc binary for a secure, transaction-safe, native onboarding flow + if "$INSTALL_PATH" onboard --api-url "$LRC_API_URL" --yes; then + : else - echo -n "Creating config file at $CONFIG_FILE... " - mkdir -p "$CONFIG_DIR" - cat > "$CONFIG_FILE" <