Conversation
Centralize validation for SECONDARY_PATH and SECONDARY_LOG_PATH so all entrypoints enforce the same absolute-local-path rules. Reject remote/UNC-style secondary paths during config loading, keep SECONDARY_LOG_PATH optional, and update the CLI installer to retry on invalid secondary path input instead of aborting. Add coverage for config parsing, migration, installer, runtime validation, and TUI flows.
Refactor new-install to use a shared reset plan and a single source of truth for preserved entries (build/env/identity). Route --new-install --cli through CLI confirmation only, keep TUI confirmation as a pure adapter, and propagate TUI runner errors instead of swallowing them. Update related help/log messaging and add tests for new-install planning, CLI confirm behavior, TUI confirm rendering/error handling, and reset/preserve consistency.
Introduce a shared decision flow for pre-existing backup.env with four explicit actions: Overwrite, Edit existing, Keep existing & continue, and Cancel. Update CLI prompts to support all modes (including Edit existing and explicit Cancel), update TUI action mapping to the same semantics, and treat “keep existing” as continue (not abort). Ensure TUI post-config steps are skipped consistently when configuration wizard is skipped (AGE setup, post-install audit, Telegram pairing), while finalization steps still run. Propagate CheckExistingConfig runner errors instead of swallowing them. Add/adjust unit tests for decision resolution, CLI prompts, TUI actions, runner error propagation, and prepareBaseTemplate behavior. Update INSTALL and CLI_REFERENCE docs to match the new aligned behavior.
Tighten AGE setup consistency after the shared CLI/TUI refactor. Reuse a shared private-key validator so the TUI rejects malformed AGE identities before they reach the orchestrator, eliminating silent retry loops. Extend the AGE setup workflow to return explicit outcome details (recipient path, wrote file vs reused existing recipients) and update install TUI messaging to report “saved” only on real writes, while showing reuse clearly when existing recipient configuration is kept. Add regression coverage for private-key validation, reuse-vs-write setup results, and the updated TUI wizard behavior.
Introduce a shared Telegram setup bootstrap so CLI and TUI use the same eligibility rules before showing pairing steps. Stop the TUI from falling back to raw backup.env parsing, skip Telegram setup consistently when config loading fails, personal mode is selected, or no Server ID is available, and centralize skip-reason logging in the command layer. Update the TUI install flow to log shared Telegram bootstrap outcomes, add dedicated tests for bootstrap/CLI/TUI behavior, align user-facing docs, and remove now-unreachable TUI branches left over from the old local decision logic.
Restore consistent decrypt prompt behavior between CLI and TUI by treating "0" as an explicit abort in both flows. Update the TUI decrypt secret prompt to advertise the exit semantics clearly, return ErrDecryptAborted on zero input, and keep Cancel as an equivalent exit path. Adjust TUI simulation coverage so the shared decrypt workflow no longer carries a UI-specific semantic drift on secret entry.
Add deterministic end-to-end smoke tests for RunDecryptWorkflowTUI so the real decrypt TUI production path is covered from entrypoint through source selection, candidate selection, secret prompt, destination prompt, and final bundle creation. Introduce test-only helpers for a real AGE-encrypted raw-backup fixture, serialized TUI simulation across multi-screen workflows, bundle-content inspection, and guarded workflow execution. Verify both the success path (including final *.decrypted.bundle.tar contents, metadata, and checksum) and clean abort at the decrypt secret prompt, without changing production behavior.
Introduce a shared env-template helper for secondary storage state and use it from both installer flows so disabling secondary storage always writes the same canonical config: SECONDARY_ENABLED=false, SECONDARY_PATH=, and SECONDARY_LOG_PATH=. This removes the previous TUI-only drift where editing an existing backup.env could leave stale secondary paths after the user disabled the feature. Add focused unit coverage for the shared helper plus CLI and TUI regression tests covering disabled state and clearing of pre-existing secondary values, and clarify the installer docs to note that disabling secondary storage clears the saved secondary paths.
Introduce shared cron parsing/normalization for install workflows and align CLI with the existing TUI cron capability. Add a neutral internal cron helper package, collect cron time during the CLI install wizard, propagate an explicit CronSchedule through the CLI install result, and make install-time cron finalization honor wizard-selected/default cron values instead of falling back to env overrides after a normal wizard run. Keep skip-config-wizard and upgrade flows on their existing env/default behavior, update the TUI wizard to reuse the same cron validation logic, add regression coverage for shared cron parsing, CLI prompt/result propagation, and install schedule precedence, and update install/CLI docs to reflect cron selection in both modes.
Close the remaining cron-install test gaps after aligning CLI and TUI scheduling behavior. Add a TUI wizard regression test that proves blank cron input resolves to the installer default (02:00) even when CRON_SCHEDULE is set in the environment, and add a CLI wizard regression test that aborting exactly at the cron prompt propagates the interactive abort and leaves backup.env unwritten. Introduce minimal test seams for the install wizard runner and cron prompt boundary to exercise the real command/wizard paths without changing production semantics.
Reduces flakiness in the decrypt TUI end-to-end tests when run with coverage enabled or under package-level load. - increases simulated input delays - extends end-to-end test timeouts and contexts - avoids false negatives without changing production code
📝 WalkthroughWalkthroughThis comprehensive refactoring introduces new encryption setup orchestration, installs an existing-config decision workflow, adds new-install reset capabilities, implements secondary storage validation, creates cron scheduling utilities, and restructures Telegram setup and decrypt TUI flows. Major changes span CLI, TUI, orchestrator, and configuration subsystems with extensive test coverage. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as Install CLI
participant Config as Config Loader
participant Orchestrator
participant UI as Age Setup UI
participant FileSystem
User->>CLI: Start installation
CLI->>Config: Load configuration
Config-->>CLI: Config/Error
alt Config exists
CLI->>User: Prompt for action
User->>CLI: Choose "Keep & Continue"
CLI->>FileSystem: Use existing config
else Config new
CLI->>Orchestrator: Initialize encryption
Orchestrator->>UI: Request recipient input
UI->>User: Prompt for recipient
User->>UI: Provide recipient
UI-->>Orchestrator: AgeRecipientDraft
Orchestrator->>FileSystem: Write recipient file
Orchestrator-->>CLI: Setup result
end
CLI->>User: Complete installation
sequenceDiagram
participant User
participant TUI as Install TUI
participant Bootstrap as Telegram Bootstrap
participant ConfigLoader
participant IdentityDetect
participant Telegram as Telegram API
User->>TUI: Start install wizard
TUI->>Bootstrap: Build bootstrap state
Bootstrap->>ConfigLoader: Load config
ConfigLoader-->>Bootstrap: Config/Error
alt Config error
Bootstrap-->>TUI: SkipConfigError
else Config loaded
Bootstrap->>IdentityDetect: Detect identity
IdentityDetect-->>Bootstrap: Identity/Error
alt Identity found and centralized
Bootstrap-->>TUI: EligibleCentralized
TUI->>User: Show Telegram pairing
User->>TUI: Confirm verification
TUI->>Telegram: Check registration
Telegram-->>TUI: Status
else Skip condition met
Bootstrap-->>TUI: SkipPersonalMode/SkipIdentity
end
end
TUI->>User: Continue installation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~65 minutes Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Comment Tip CodeRabbit can use OpenGrep to find security vulnerabilities and bugs across 17+ programming languages.OpenGrep is compatible with Semgrep configurations. Add an |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
cmd/proxsave/main.go (1)
1567-1572:⚠️ Potential issue | 🟡 MinorFix staticcheck S1000: use simple channel receive instead of
selectwith single case.The pipeline flagged this issue. A
selectwith only one case is equivalent to a direct channel operation.🔧 Proposed fix
fmt.Printf("\r Remaining: %ds ", int(remaining.Seconds())) - select { - case <-ticker.C: - continue - } + <-ticker.C }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/proxsave/main.go` around lines 1567 - 1572, Replace the unnecessary single-case select by performing a direct receive on the ticker channel: remove the select block and use a plain channel receive from ticker.C (the same ticker used in the loop) to wait for ticks; ensure the surrounding loop logic that currently does "case <-ticker.C: continue" still continues the loop after the receive.cmd/proxsave/newkey.go (1)
116-124:⚠️ Potential issue | 🟠 MajorLoad the persisted config before forcing a new recipient.
Lines 119-124 synthesize a fresh
config.Configand hard-codeAgeRecipientFileto the default path. That drops any recipient-file override already saved inconfigPath, sonewkeycan rotate one file while the installed config still points at another.cmd/proxsave/encryption_setup.goalready avoids this by loading the config first.Patch suggestion
- cfg := &config.Config{ - BaseDir: baseDir, - ConfigPath: configPath, - EncryptArchive: true, - AgeRecipientFile: recipientPath, - } + cfg, loadErr := config.LoadConfig(configPath) + if loadErr != nil { + if !errors.Is(loadErr, os.ErrNotExist) { + return fmt.Errorf("load config: %w", loadErr) + } + cfg = &config.Config{} + } + cfg.BaseDir = baseDir + cfg.ConfigPath = configPath + cfg.EncryptArchive = true + if strings.TrimSpace(cfg.AgeRecipientFile) == "" { + cfg.AgeRecipientFile = recipientPath + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/proxsave/newkey.go` around lines 116 - 124, In runNewKeySetup, don't overwrite any persisted AgeRecipientFile; first load the existing config from configPath (as encryption_setup.go does) and if it contains a non-empty AgeRecipientFile, use that value instead of hard-coding recipientPath into the new config.Config; otherwise fall back to the default filepath.Join(baseDir, "identity", "age", "recipient.txt"). Ensure you still set BaseDir and ConfigPath on the resulting config.Config and proceed with key rotation using that merged configuration.
🧹 Nitpick comments (11)
internal/orchestrator/tui_simulation_test.go (1)
61-74: Consider renaming the test to match the function under test.The test function
TestPromptOverwriteAction_SelectsOverwritestill references the old function namepromptOverwriteAction, but now testspromptExistingPathDecisionTUI. Consider renaming toTestPromptExistingPathDecisionTUI_SelectsOverwritefor consistency and discoverability.The test logic itself correctly validates the new return signature
(PathDecision, string, error)and appropriately asserts thatnewPathis empty when the decision is to overwrite.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/orchestrator/tui_simulation_test.go` around lines 61 - 74, Rename the test function TestPromptOverwriteAction_SelectsOverwrite to TestPromptExistingPathDecisionTUI_SelectsOverwrite so the name matches the function under test promptExistingPathDecisionTUI; update the function declaration (the test name) and any references to the old test identifier in the file to ensure consistency and discoverability while keeping the test body (calls to promptExistingPathDecisionTUI and assertions on PathDecision, newPath, err) unchanged.internal/orchestrator/decrypt_tui_e2e_helpers_test.go (1)
57-93: Consider finalizing the simulation screen in cleanup.The simulation screen is initialized at line 62 but not finalized in the cleanup. While this may work for tests, explicitly calling
screen.Fini()in the cleanup would be more robust and prevent potential resource leaks.♻️ Suggested improvement
t.Cleanup(func() { + screen.Fini() newTUIApp = orig })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/orchestrator/decrypt_tui_e2e_helpers_test.go` around lines 57 - 93, withTimedSimAppSequence currently initializes a tcell SimulationScreen (screen) but never finalizes it; update the t.Cleanup closure inside withTimedSimAppSequence to call screen.Fini() (and restore newTUIApp as existing) so the simulation screen is properly closed after the test, preventing resource leaks; ensure the cleanup runs even if other cleanup logic errors by invoking screen.Fini() before resetting newTUIApp.internal/orchestrator/telegram_setup_bootstrap.go (1)
46-104: Consider simplifying the return type since error is always nil.The function signature is
BuildTelegramSetupBootstrap(configPath, baseDir string) (TelegramSetupBootstrap, error), but the function always returnsnilfor the error. The error information is captured in the struct fields (ConfigError,IdentityDetectError). This is a valid pattern (embedding errors in result structs), but the nil error return could be misleading to callers expecting error conditions to be signaled via the error return.Either:
- Remove the error return:
func BuildTelegramSetupBootstrap(...) TelegramSetupBootstrap- Or document that errors are always embedded in the struct and the error return is reserved for future use
♻️ Option 1: Remove unused error return
-func BuildTelegramSetupBootstrap(configPath, baseDir string) (TelegramSetupBootstrap, error) { +func BuildTelegramSetupBootstrap(configPath, baseDir string) TelegramSetupBootstrap { state := TelegramSetupBootstrap{} cfg, err := telegramSetupBootstrapLoadConfig(configPath) if err != nil { state.Eligibility = TelegramSetupSkipConfigError state.ConfigError = err.Error() - return state, nil + return state }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/orchestrator/telegram_setup_bootstrap.go` around lines 46 - 104, The BuildTelegramSetupBootstrap function always returns a nil error and embeds errors in the TelegramSetupBootstrap fields (ConfigError, IdentityDetectError); change the function signature to remove the unused error return (make it func BuildTelegramSetupBootstrap(configPath, baseDir string) TelegramSetupBootstrap), update all return sites inside the function to return just the state value, and update any callers to no longer expect a second error return (they should inspect state.ConfigError and state.IdentityDetectError instead).internal/cron/cron_test.go (1)
43-61: Consider adding edge case tests for boundary times.The tests cover typical cases well. Consider adding tests for boundary values like
"00:00"(midnight) and"23:59"(end of day) to ensure the schedule conversion handles edge times correctly.♻️ Suggested additional test cases
tests := []struct { name string in string want string }{ {name: "valid", in: "02:05", want: "05 02 * * *"}, {name: "normalized short", in: "2:5", want: "05 02 * * *"}, + {name: "midnight", in: "00:00", want: "00 00 * * *"}, + {name: "end of day", in: "23:59", want: "59 23 * * *"}, {name: "invalid", in: "bad", want: ""}, }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/cron/cron_test.go` around lines 43 - 61, Add tests for boundary times to TestTimeToSchedule to ensure TimeToSchedule handles edge values; specifically add cases with in "00:00" expecting "00 00 * * *" and "23:59" expecting "59 23 * * *" (and keep existing cases). Locate the test table in TestTimeToSchedule and append these two entries (use the same struct fields name/in/want) and run the test suite to verify behavior; if the assertions fail, update TimeToSchedule accordingly to normalize leading zeros and validate 00–23 hours and 00–59 minutes.internal/config/validation_secondary_test.go (1)
24-37: Consider addingt.Parallel()to subtests for faster execution.The parent test calls
t.Parallel(), but subtests don't. For table-driven tests with independent cases, parallelizing subtests improves test execution time.♻️ Proposed fix to parallelize subtests
for _, tt := range tests { + tt := tt // capture range variable t.Run(tt.name, func(t *testing.T) { + t.Parallel() err := ValidateRequiredSecondaryPath(tt.path)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/config/validation_secondary_test.go` around lines 24 - 37, Add parallel execution to the subtests by capturing the loop variable and calling t.Parallel() inside each subtest: before calling t.Run, shadow the loop variable (e.g., tt := tt) to avoid closure capture issues, then inside the subtest function immediately call t.Parallel() so each table-driven case that calls ValidateRequiredSecondaryPath runs concurrently and speeds up the test suite.internal/tui/wizard/age_ui_adapter.go (2)
60-62: Context not propagated to ConfirmAddRecipient.Similar to
ConfirmOverwriteExistingRecipient, the context parameter is received but not passed through. This is a minor consistency issue.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/tui/wizard/age_ui_adapter.go` around lines 60 - 62, ConfirmAddAnotherRecipient on ageSetupUIAdapter receives a context but doesn't pass it through; update the call in ageSetupUIAdapter.ConfirmAddAnotherRecipient to forward ctx into ConfirmAddRecipient (matching how ConfirmOverwriteExistingRecipient does), e.g. call ConfirmAddRecipient(ctx, a.configPath, a.buildSig, currentCount) so the context is propagated.
23-25: Context not propagated to underlying wizard function.
ConfirmRecipientOverwritereceivesctxbut doesn't pass it to the underlying function. If the wizard supports context-based cancellation, this could lead to unresponsive UI during cancellation requests.Consider updating the signature if the underlying function supports context
If
ConfirmRecipientOverwriteinage.gocan be updated to accept context:func (a *ageSetupUIAdapter) ConfirmOverwriteExistingRecipient(ctx context.Context, recipientPath string) (bool, error) { - return ConfirmRecipientOverwrite(recipientPath, a.configPath, a.buildSig) + return ConfirmRecipientOverwrite(ctx, recipientPath, a.configPath, a.buildSig) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/tui/wizard/age_ui_adapter.go` around lines 23 - 25, The method ageSetupUIAdapter.ConfirmOverwriteExistingRecipient is not propagating the received ctx to the underlying ConfirmRecipientOverwrite call; update the call to pass ctx (i.e., ConfirmRecipientOverwrite(ctx, recipientPath, a.configPath, a.buildSig)) and, if ConfirmRecipientOverwrite's signature currently lacks a context parameter, modify its signature to accept context.Context as the first parameter and update all callers accordingly so cancellation propagates through the wizard flow.cmd/proxsave/install_existing_config.go (1)
28-38: Consider using safefs.Stat for timeout protection on network paths.The relevant code snippets show
internal/safefs/safefs.goprovides aStatfunction with timeout protection. Usingos.Statdirectly could hang indefinitely ifconfigPathpoints to an unresponsive network mount.Use safefs.Stat with timeout
+import ( + "time" + "github.com/tis24dev/proxsave/internal/safefs" +) + func promptExistingConfigModeCLI(ctx context.Context, reader *bufio.Reader, configPath string) (existingConfigMode, error) { - info, err := os.Stat(configPath) + info, err := safefs.Stat(ctx, configPath, 5*time.Second) if err != nil { if os.IsNotExist(err) { return existingConfigOverwrite, nil } return existingConfigCancel, fmt.Errorf("failed to access configuration file: %w", err) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/proxsave/install_existing_config.go` around lines 28 - 38, Replace the direct os.Stat call in promptExistingConfigModeCLI with the safefs.Stat wrapper to get timeout protection for network mounts: call safefs.Stat(ctx, configPath) (and import the safefs package), use the returned info and error exactly as before (preserve the os.IsNotExist check to return existingConfigOverwrite, wrap other errors with "failed to access configuration file: %w"), and keep the subsequent info.Mode().IsRegular() check and its error path unchanged so behavior remains identical except for the added timeout safety.internal/orchestrator/age_setup_ui.go (1)
13-18: Consider adding documentation for the exported struct.The
AgeRecipientDraftstruct contains sensitive fields (Passphrase,PrivateKey). Adding a doc comment clarifying that these should be zeroed after use would help consumers handle them securely.📝 Suggested documentation
+// AgeRecipientDraft holds user-provided input for AGE recipient setup. +// Callers should zero Passphrase and PrivateKey fields after use. type AgeRecipientDraft struct { Kind AgeRecipientInputKind PublicKey string Passphrase string PrivateKey string }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/orchestrator/age_setup_ui.go` around lines 13 - 18, Add a doc comment above the exported AgeRecipientDraft struct describing its purpose and explicitly warning that Passphrase and PrivateKey contain sensitive material that must be cleared from memory when no longer needed (e.g., zeroed or overwritten). Reference the struct name AgeRecipientDraft and its sensitive fields Passphrase and PrivateKey in the comment, and include recommended handling guidance such as zeroing/overwriting and limiting lifetime in memory.internal/orchestrator/age_setup_ui_cli.go (1)
47-70: Invalid menu options silently loop without user feedback.When the user enters an option outside the valid range (e.g., "5", "abc", empty string), the switch statement falls through without handling, causing the menu to redisplay without any error message. This could be confusing to users.
♻️ Add default case for invalid options
switch option { case "1": value, err := promptPublicRecipientAge(ctx, u.reader) if err != nil { u.warn(err) continue } return &AgeRecipientDraft{Kind: AgeRecipientInputExisting, PublicKey: value}, nil case "2": passphrase, err := promptAndConfirmPassphraseAge(ctx) if err != nil { u.warn(err) continue } return &AgeRecipientDraft{Kind: AgeRecipientInputPassphrase, Passphrase: passphrase}, nil case "3": privateKey, err := promptPrivateKeyValueAge(ctx) if err != nil { u.warn(err) continue } return &AgeRecipientDraft{Kind: AgeRecipientInputPrivateKey, PrivateKey: privateKey}, nil + default: + fmt.Println("Invalid option. Please select 1-4.") }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/orchestrator/age_setup_ui_cli.go` around lines 47 - 70, The menu switch on variable option (the block that returns AgeRecipientDraft via promptPublicRecipientAge, promptAndConfirmPassphraseAge, promptPrivateKeyValueAge) lacks a default branch so invalid inputs silently loop; add a default case to the switch that calls u.warn with a clear message like "Invalid option, please choose 1, 2, or 3" (and continue the loop) so users get feedback when they enter "5", "abc", empty, etc.; keep using the existing u.warn method and the same loop control to re-prompt.internal/orchestrator/decrypt_workflow_ui_test.go (1)
64-80: Global state mutation may cause test flakiness in parallel runs.The test mutates the package-level
restoreFSvariable without usingt.Parallel()protection. While the deferred restore mitigates issues within this test, if other tests in the package run in parallel and also depend onrestoreFS, race conditions could occur.Consider using a test-local approach or ensuring tests that touch
restoreFSare explicitly serialized.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/orchestrator/decrypt_workflow_ui_test.go` around lines 64 - 80, The test mutates the package-level restoreFS (osFS{}) causing potential races; refactor to avoid global mutation by adding an fs parameter (e.g., fs aferio-like interface) to ensureWritablePathWithUI (or create a helper ensureWritablePathWithUIWithFS(ctx, ui, fs, path, desc)) and update TestEnsureWritablePathWithUI_ReturnsCleanMissingPath to pass a local osFS{} instead of setting restoreFS; remove the restoreFS assignment and deferred restore from the test and update any callers to use the new parameter so tests no longer rely on mutating the shared restoreFS variable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmd/proxsave/install_tui.go`:
- Around line 139-150: The new AGE success-path logging calls use bootstrap
without nil checks and can panic if bootstrap is nil; update the block after
runInitialEncryptionSetupWithUI (where setupResult is handled) to only call
bootstrap.Info(...) when bootstrap != nil, i.e., guard the three bootstrap.Info
invocations (the general "AGE encryption configured successfully", the
"Recipient saved to: %s" branch checking setupResult.WroteRecipientFile &&
setupResult.RecipientPath != "" and the else-if branch checking
setupResult.ReusedExistingRecipients) with a nil-check of the bootstrap logger
so the function works when bootstrap is optional.
In `@cmd/proxsave/newkey.go`:
- Around line 91-92: runNewKey currently calls bootstrap.Info unconditionally at
the end, which will panic if the bootstrap variable is nil; update the success
logging to first check if bootstrap != nil and only call bootstrap.Info(...)
when it's present, otherwise fall back to printing the same messages via
fmt.Println (or log) so TUI-less flows don't panic—locate the bootstrap variable
and the two bootstrap.Info(...) calls near the end of runNewKey and wrap them in
a nil-check.
In `@internal/orchestrator/workflow_ui_tui_decrypt_prompts.go`:
- Around line 75-85: The form currently pre-fills defaultPath and only checks
for emptiness, letting users submit the same conflicting path; update the
AddInputFieldWithValidation callback (the input added via
form.AddInputFieldWithValidation using label "New path") to also compare the
trimmed value against defaultPath and return an error like "path must be
different from existing path" when they match, and likewise guard in
form.SetOnSubmit (which sets newPath from values[label]) to reject or prevent
assigning newPath if it equals defaultPath so the Submit cannot return the same
path; reference the symbols AddInputFieldWithValidation, SetOnSubmit,
defaultPath, newPath, and label when making the change.
---
Outside diff comments:
In `@cmd/proxsave/main.go`:
- Around line 1567-1572: Replace the unnecessary single-case select by
performing a direct receive on the ticker channel: remove the select block and
use a plain channel receive from ticker.C (the same ticker used in the loop) to
wait for ticks; ensure the surrounding loop logic that currently does "case
<-ticker.C: continue" still continues the loop after the receive.
In `@cmd/proxsave/newkey.go`:
- Around line 116-124: In runNewKeySetup, don't overwrite any persisted
AgeRecipientFile; first load the existing config from configPath (as
encryption_setup.go does) and if it contains a non-empty AgeRecipientFile, use
that value instead of hard-coding recipientPath into the new config.Config;
otherwise fall back to the default filepath.Join(baseDir, "identity", "age",
"recipient.txt"). Ensure you still set BaseDir and ConfigPath on the resulting
config.Config and proceed with key rotation using that merged configuration.
---
Nitpick comments:
In `@cmd/proxsave/install_existing_config.go`:
- Around line 28-38: Replace the direct os.Stat call in
promptExistingConfigModeCLI with the safefs.Stat wrapper to get timeout
protection for network mounts: call safefs.Stat(ctx, configPath) (and import the
safefs package), use the returned info and error exactly as before (preserve the
os.IsNotExist check to return existingConfigOverwrite, wrap other errors with
"failed to access configuration file: %w"), and keep the subsequent
info.Mode().IsRegular() check and its error path unchanged so behavior remains
identical except for the added timeout safety.
In `@internal/config/validation_secondary_test.go`:
- Around line 24-37: Add parallel execution to the subtests by capturing the
loop variable and calling t.Parallel() inside each subtest: before calling
t.Run, shadow the loop variable (e.g., tt := tt) to avoid closure capture
issues, then inside the subtest function immediately call t.Parallel() so each
table-driven case that calls ValidateRequiredSecondaryPath runs concurrently and
speeds up the test suite.
In `@internal/cron/cron_test.go`:
- Around line 43-61: Add tests for boundary times to TestTimeToSchedule to
ensure TimeToSchedule handles edge values; specifically add cases with in
"00:00" expecting "00 00 * * *" and "23:59" expecting "59 23 * * *" (and keep
existing cases). Locate the test table in TestTimeToSchedule and append these
two entries (use the same struct fields name/in/want) and run the test suite to
verify behavior; if the assertions fail, update TimeToSchedule accordingly to
normalize leading zeros and validate 00–23 hours and 00–59 minutes.
In `@internal/orchestrator/age_setup_ui_cli.go`:
- Around line 47-70: The menu switch on variable option (the block that returns
AgeRecipientDraft via promptPublicRecipientAge, promptAndConfirmPassphraseAge,
promptPrivateKeyValueAge) lacks a default branch so invalid inputs silently
loop; add a default case to the switch that calls u.warn with a clear message
like "Invalid option, please choose 1, 2, or 3" (and continue the loop) so users
get feedback when they enter "5", "abc", empty, etc.; keep using the existing
u.warn method and the same loop control to re-prompt.
In `@internal/orchestrator/age_setup_ui.go`:
- Around line 13-18: Add a doc comment above the exported AgeRecipientDraft
struct describing its purpose and explicitly warning that Passphrase and
PrivateKey contain sensitive material that must be cleared from memory when no
longer needed (e.g., zeroed or overwritten). Reference the struct name
AgeRecipientDraft and its sensitive fields Passphrase and PrivateKey in the
comment, and include recommended handling guidance such as zeroing/overwriting
and limiting lifetime in memory.
In `@internal/orchestrator/decrypt_tui_e2e_helpers_test.go`:
- Around line 57-93: withTimedSimAppSequence currently initializes a tcell
SimulationScreen (screen) but never finalizes it; update the t.Cleanup closure
inside withTimedSimAppSequence to call screen.Fini() (and restore newTUIApp as
existing) so the simulation screen is properly closed after the test, preventing
resource leaks; ensure the cleanup runs even if other cleanup logic errors by
invoking screen.Fini() before resetting newTUIApp.
In `@internal/orchestrator/decrypt_workflow_ui_test.go`:
- Around line 64-80: The test mutates the package-level restoreFS (osFS{})
causing potential races; refactor to avoid global mutation by adding an fs
parameter (e.g., fs aferio-like interface) to ensureWritablePathWithUI (or
create a helper ensureWritablePathWithUIWithFS(ctx, ui, fs, path, desc)) and
update TestEnsureWritablePathWithUI_ReturnsCleanMissingPath to pass a local
osFS{} instead of setting restoreFS; remove the restoreFS assignment and
deferred restore from the test and update any callers to use the new parameter
so tests no longer rely on mutating the shared restoreFS variable.
In `@internal/orchestrator/telegram_setup_bootstrap.go`:
- Around line 46-104: The BuildTelegramSetupBootstrap function always returns a
nil error and embeds errors in the TelegramSetupBootstrap fields (ConfigError,
IdentityDetectError); change the function signature to remove the unused error
return (make it func BuildTelegramSetupBootstrap(configPath, baseDir string)
TelegramSetupBootstrap), update all return sites inside the function to return
just the state value, and update any callers to no longer expect a second error
return (they should inspect state.ConfigError and state.IdentityDetectError
instead).
In `@internal/orchestrator/tui_simulation_test.go`:
- Around line 61-74: Rename the test function
TestPromptOverwriteAction_SelectsOverwrite to
TestPromptExistingPathDecisionTUI_SelectsOverwrite so the name matches the
function under test promptExistingPathDecisionTUI; update the function
declaration (the test name) and any references to the old test identifier in the
file to ensure consistency and discoverability while keeping the test body
(calls to promptExistingPathDecisionTUI and assertions on PathDecision, newPath,
err) unchanged.
In `@internal/tui/wizard/age_ui_adapter.go`:
- Around line 60-62: ConfirmAddAnotherRecipient on ageSetupUIAdapter receives a
context but doesn't pass it through; update the call in
ageSetupUIAdapter.ConfirmAddAnotherRecipient to forward ctx into
ConfirmAddRecipient (matching how ConfirmOverwriteExistingRecipient does), e.g.
call ConfirmAddRecipient(ctx, a.configPath, a.buildSig, currentCount) so the
context is propagated.
- Around line 23-25: The method
ageSetupUIAdapter.ConfirmOverwriteExistingRecipient is not propagating the
received ctx to the underlying ConfirmRecipientOverwrite call; update the call
to pass ctx (i.e., ConfirmRecipientOverwrite(ctx, recipientPath, a.configPath,
a.buildSig)) and, if ConfirmRecipientOverwrite's signature currently lacks a
context parameter, modify its signature to accept context.Context as the first
parameter and update all callers accordingly so cancellation propagates through
the wizard flow.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b5227adc-f049-49d4-a1dc-3049839ce6c9
📒 Files selected for processing (65)
cmd/proxsave/encryption_setup.gocmd/proxsave/encryption_setup_test.gocmd/proxsave/helpers_test.gocmd/proxsave/install.gocmd/proxsave/install_existing_config.gocmd/proxsave/install_existing_config_test.gocmd/proxsave/install_test.gocmd/proxsave/install_tui.gocmd/proxsave/main.gocmd/proxsave/new_install.gocmd/proxsave/new_install_test.gocmd/proxsave/newkey.gocmd/proxsave/prompts.gocmd/proxsave/runtime_helpers.gocmd/proxsave/schedule_helpers.gocmd/proxsave/schedule_helpers_test.gocmd/proxsave/telegram_setup_cli.gocmd/proxsave/telegram_setup_cli_test.gocmd/proxsave/upgrade.godocs/CLI_REFERENCE.mddocs/CLOUD_STORAGE.mddocs/CONFIGURATION.mddocs/ENCRYPTION.mddocs/INSTALL.mdinternal/config/config.gointernal/config/config_test.gointernal/config/env_mutation.gointernal/config/env_mutation_test.gointernal/config/migration.gointernal/config/migration_test.gointernal/config/templates/backup.envinternal/config/validation_secondary.gointernal/config/validation_secondary_test.gointernal/cron/cron.gointernal/cron/cron_test.gointernal/orchestrator/age_setup_ui.gointernal/orchestrator/age_setup_ui_cli.gointernal/orchestrator/age_setup_workflow.gointernal/orchestrator/age_setup_workflow_test.gointernal/orchestrator/decrypt_tui.gointernal/orchestrator/decrypt_tui_e2e_helpers_test.gointernal/orchestrator/decrypt_tui_e2e_test.gointernal/orchestrator/decrypt_tui_simulation_test.gointernal/orchestrator/decrypt_tui_test.gointernal/orchestrator/decrypt_workflow_ui.gointernal/orchestrator/decrypt_workflow_ui_test.gointernal/orchestrator/encryption.gointernal/orchestrator/encryption_exported_test.gointernal/orchestrator/telegram_setup_bootstrap.gointernal/orchestrator/telegram_setup_bootstrap_test.gointernal/orchestrator/tui_simulation_test.gointernal/orchestrator/workflow_ui_tui_decrypt.gointernal/orchestrator/workflow_ui_tui_decrypt_prompts.gointernal/orchestrator/workflow_ui_tui_decrypt_test.gointernal/orchestrator/workflow_ui_tui_shared.gointernal/tui/wizard/age.gointernal/tui/wizard/age_test.gointernal/tui/wizard/age_ui_adapter.gointernal/tui/wizard/age_ui_adapter_test.gointernal/tui/wizard/install.gointernal/tui/wizard/install_test.gointernal/tui/wizard/new_install.gointernal/tui/wizard/new_install_test.gointernal/tui/wizard/telegram_setup_tui.gointernal/tui/wizard/telegram_setup_tui_test.go
💤 Files with no reviewable changes (2)
- internal/orchestrator/decrypt_tui_test.go
- internal/orchestrator/decrypt_tui.go
| logging.DebugStepBootstrap(bootstrap, "install workflow (tui)", "running AGE setup via orchestrator") | ||
| setupResult, err := runInitialEncryptionSetupWithUI(ctx, configPath, wizard.NewAgeSetupUI(configPath, buildSig)) | ||
| if err != nil { | ||
| if errors.Is(err, wizard.ErrAgeSetupCancelled) { | ||
| return fmt.Errorf("encryption setup aborted by user: %w", errInteractiveAborted) | ||
| } else { | ||
| return fmt.Errorf("AGE setup failed: %w", err) | ||
| } | ||
| } | ||
|
|
||
| // Process the AGE data based on setup type | ||
| var recipientKey string | ||
| switch ageData.SetupType { | ||
| case "existing": | ||
| recipientKey = ageData.PublicKey | ||
| case "passphrase": | ||
| // Derive recipient from passphrase | ||
| recipient, err := deriveRecipientFromPassphrase(ageData.Passphrase) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to derive recipient from passphrase: %w", err) | ||
| } | ||
| recipientKey = recipient | ||
| case "privatekey": | ||
| // Derive recipient from private key | ||
| recipient, err := deriveRecipientFromPrivateKey(ageData.PrivateKey) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to derive recipient from private key: %w", err) | ||
| } | ||
| recipientKey = recipient | ||
| } | ||
|
|
||
| // Save the recipient | ||
| logging.DebugStepBootstrap(bootstrap, "install workflow (tui)", "saving AGE recipient") | ||
| if err := wizard.SaveAgeRecipient(recipientPath, recipientKey); err != nil { | ||
| return fmt.Errorf("failed to save AGE recipient: %w", err) | ||
| return err | ||
| } | ||
|
|
||
| bootstrap.Info("AGE encryption configured successfully") | ||
| bootstrap.Info("Recipient saved to: %s", recipientPath) | ||
| if setupResult.WroteRecipientFile && setupResult.RecipientPath != "" { | ||
| bootstrap.Info("Recipient saved to: %s", setupResult.RecipientPath) | ||
| } else if setupResult.ReusedExistingRecipients { | ||
| bootstrap.Info("Using existing AGE recipient configuration") | ||
| } |
There was a problem hiding this comment.
Guard the AGE success-path logs when bootstrap is nil.
This function consistently treats bootstrap as optional, but this block now adds more unconditional bootstrap.Info(...) calls. If runInstallTUI is invoked without a bootstrap logger, the install can panic immediately after a successful AGE setup.
Proposed fix
- bootstrap.Info("AGE encryption configured successfully")
- if setupResult.WroteRecipientFile && setupResult.RecipientPath != "" {
- bootstrap.Info("Recipient saved to: %s", setupResult.RecipientPath)
- } else if setupResult.ReusedExistingRecipients {
- bootstrap.Info("Using existing AGE recipient configuration")
- }
- bootstrap.Info("IMPORTANT: Keep your passphrase/private key offline and secure!")
+ if bootstrap != nil {
+ bootstrap.Info("AGE encryption configured successfully")
+ if setupResult.WroteRecipientFile && setupResult.RecipientPath != "" {
+ bootstrap.Info("Recipient saved to: %s", setupResult.RecipientPath)
+ } else if setupResult.ReusedExistingRecipients {
+ bootstrap.Info("Using existing AGE recipient configuration")
+ }
+ bootstrap.Info("IMPORTANT: Keep your passphrase/private key offline and secure!")
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cmd/proxsave/install_tui.go` around lines 139 - 150, The new AGE success-path
logging calls use bootstrap without nil checks and can panic if bootstrap is
nil; update the block after runInitialEncryptionSetupWithUI (where setupResult
is handled) to only call bootstrap.Info(...) when bootstrap != nil, i.e., guard
the three bootstrap.Info invocations (the general "AGE encryption configured
successfully", the "Recipient saved to: %s" branch checking
setupResult.WroteRecipientFile && setupResult.RecipientPath != "" and the
else-if branch checking setupResult.ReusedExistingRecipients) with a nil-check
of the bootstrap logger so the function works when bootstrap is optional.
| bootstrap.Info("✓ New AGE recipient(s) generated and saved to %s", recipientPath) | ||
| bootstrap.Info("IMPORTANT: Keep your passphrase/private key offline and secure!") |
There was a problem hiding this comment.
Guard the TUI success logging when bootstrap is optional.
runNewKey already treats bootstrap as nullable earlier in this file, but Lines 91-92 call Info unconditionally. A nil bootstrap now turns a successful TUI run into a panic.
Patch suggestion
- bootstrap.Info("✓ New AGE recipient(s) generated and saved to %s", recipientPath)
- bootstrap.Info("IMPORTANT: Keep your passphrase/private key offline and secure!")
+ if bootstrap != nil {
+ bootstrap.Info("✓ New AGE recipient(s) generated and saved to %s", recipientPath)
+ bootstrap.Info("IMPORTANT: Keep your passphrase/private key offline and secure!")
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| bootstrap.Info("✓ New AGE recipient(s) generated and saved to %s", recipientPath) | |
| bootstrap.Info("IMPORTANT: Keep your passphrase/private key offline and secure!") | |
| if bootstrap != nil { | |
| bootstrap.Info("✓ New AGE recipient(s) generated and saved to %s", recipientPath) | |
| bootstrap.Info("IMPORTANT: Keep your passphrase/private key offline and secure!") | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cmd/proxsave/newkey.go` around lines 91 - 92, runNewKey currently calls
bootstrap.Info unconditionally at the end, which will panic if the bootstrap
variable is nil; update the success logging to first check if bootstrap != nil
and only call bootstrap.Info(...) when it's present, otherwise fall back to
printing the same messages via fmt.Println (or log) so TUI-less flows don't
panic—locate the bootstrap variable and the two bootstrap.Info(...) calls near
the end of runNewKey and wrap them in a nil-check.
| form := components.NewForm(app) | ||
| label := "New path" | ||
| form.AddInputFieldWithValidation(label, defaultPath, 64, func(value string) error { | ||
| if strings.TrimSpace(value) == "" { | ||
| return fmt.Errorf("path cannot be empty") | ||
| } | ||
| return nil | ||
| }) | ||
| form.SetOnSubmit(func(values map[string]string) error { | ||
| newPath = strings.TrimSpace(values[label]) | ||
| return nil |
There was a problem hiding this comment.
Reject the current path in the “Use different path” form.
The field is prefilled with the conflicting path and only checks for emptiness. Pressing Enter immediately returns PathDecisionNewPath with the same value, which defeats the user's explicit choice to avoid overwrite.
Patch suggestion
func promptNewPathInputTUI(defaultPath, configPath, buildSig string) (string, error) {
app := newTUIApp()
var newPath string
var cancelled bool
+ currentPath := filepath.Clean(defaultPath)
form := components.NewForm(app)
label := "New path"
form.AddInputFieldWithValidation(label, defaultPath, 64, func(value string) error {
- if strings.TrimSpace(value) == "" {
+ trimmed := strings.TrimSpace(value)
+ if trimmed == "" {
return fmt.Errorf("path cannot be empty")
}
+ if filepath.Clean(trimmed) == currentPath {
+ return fmt.Errorf("choose a different path or go back and select overwrite")
+ }
return nil
})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/orchestrator/workflow_ui_tui_decrypt_prompts.go` around lines 75 -
85, The form currently pre-fills defaultPath and only checks for emptiness,
letting users submit the same conflicting path; update the
AddInputFieldWithValidation callback (the input added via
form.AddInputFieldWithValidation using label "New path") to also compare the
trimmed value against defaultPath and return an error like "path must be
different from existing path" when they match, and likewise guard in
form.SetOnSubmit (which sets newPath from values[label]) to reject or prevent
assigning newPath if it equals defaultPath so the Submit cannot return the same
path; reference the symbols AddInputFieldWithValidation, SetOnSubmit,
defaultPath, newPath, and label when making the change.
Summary by CodeRabbit
Release Notes
New Features
Improvements
Documentation