feat(cli): add build credentials manage interactive command#2052
Conversation
A keyboard-driven TUI for browsing, exporting, and editing build
credentials stored under ~/.capgo-credentials/credentials.json (or the
project-local .capgo-credentials.json). Reuses the same Ink runtime
as `capgo init` — no new dependencies.
Top-level actions per app:
- View credentials — flat inspector across both platforms with [SHARED]
/ [SHARED·ios] / [SHARED·android] / [ios] /
[android] tags. Drift between platforms surfaces
as two rows so users can resolve it.
- Export to .env — writes a CI/CD-ready file. macOS uses the native
"Save As…" sheet (osascript `choose file name`);
other platforms get a text prompt. Provisioning
map is emitted as base64 to dodge newline/quoting
issues in CI secret stores.
- Delete — clears credentials for one platform.
Per-field actions in the inspector:
- Show — print the raw value.
- Decode — base64 → JSON pretty-print / printable text /
"binary, N bytes" depending on the payload.
- Copy to clipboard — pbcopy / clip / wl-copy / xclip / xsel.
- Edit — type-aware:
boolean → true / false select
enum → select from allowed values
duration→ text prompt accepting 1h/6h/2d
else → temp file in $EDITOR, JSON-validated
for CAPGO_IOS_PROVISIONING_MAP.
- Explain — short authoritative description sourced from the
Capgo wiki (code-signing, cli-native-builds,
android-keystore-handling pages).
- Remove — deletes the field from all source platforms.
UX details:
- Auto-detects the current app from capacitor.config so single-app users
skip the picker entirely.
- "Back" only appears when there is somewhere to go back to.
- Logs cleared only when entering a new field's submenu, so action
confirmations stay visible while the user navigates back to the list.
- New helper `openSaveFilePicker` in file-picker.ts is reusable for any
future "save file" prompt in the CLI.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds an interactive CLI credentials manager (view/edit/export/delete/add) driven by a credential knowledge map, a macOS save-file picker, and a new ChangesInteractive Credentials Manager
Sequence DiagramsequenceDiagram
participant User
participant CLI as Capgo CLI
participant Store as CredentialsStore
participant FilePicker as SaveFilePicker/FS
User->>CLI: open manage credentials
CLI->>Store: load entries (local/global)
CLI->>User: render TUI (apps, fields)
User->>CLI: request export / add / edit / delete
CLI->>FilePicker: choose save path (when exporting)
CLI->>Store: write/update/delete credentials
CLI->>User: show result / confirmation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Merging this PR will not alter performance
Comparing Footnotes
|
adamsardo
left a comment
There was a problem hiding this comment.
I spotted one functional gap in the new command surface: build credentials manage --platform ios|android is documented and registered, but ManageCredentialsOptions.platform is never used in manageCredentialsCommand.
That means --platform ios still shows/manages/export/deletes Android credentials when both platforms exist, and --platform android likewise does not constrain the flow. This is especially risky on the delete/export paths because the CLI help implies the caller has already scoped the operation to one platform.
A small fix would be to validate options.platform early and filter currentEntry.platforms / saved views to that platform, or remove the option from the command until it is implemented.
|
One security UX issue I’d tighten before this ships:
|
zinc-builds
left a comment
There was a problem hiding this comment.
Security Review: #2052 — build credentials manage interactive command
Looks Good ✅
- File permissions 0o600 for exported .env files and temp edit files — prevents other users from reading secrets
- Temp files in
tmpdir()with unique names (capgo-{timestamp}-{random}-{key}.{ext}) — avoids predictable paths finallyblock cleanup of temp files after editing — best-effort unlinking- Secret masking —
SECRET_KEYSset defines which fields show****by default in the picker UI - No network transmission — all operations are local-only (read/write to local credential stores)
.envexport warns about .gitignore — user is told "never commit this file" and given the docs URLescapeDotenvValueproperly handles special characters ($, `, ", , newlines) in exported env values- Validation on edit — boolean/enum/duration fields validated before write; JSON parsing validated for provisioning map
- CSV/JSON export uses base64 for multi-line values (CAPGO_IOS_PROVISIONING_MAP exported as _BASE64 variant) — avoids newline injection in .env
sanitizeForFilenamestrips non-alphanumeric chars from temp filenames — prevents path traversal via key names
Warnings ⚠️
- Clipboard copy of secrets:
actionCopyFieldpipes raw secrets topbcopy/xclip/wl-copy. Once in the clipboard, the secret persists until overwritten. Some clipboard managers retain history. Consider adding a warning after copy or using platform-specific transient clipboard APIs where available. - Temp file crash persistence: If the process is killed (SIGKILL) during
editTextField, the temp file at/tmp/capgo-*containing the credential value remains on disk. Thefinallyblock cannot run on SIGKILL. Mitigated by unique naming and /tmp cleanup policies, but worth noting. readFileSyncof temp file: If an attacker replaces the temp file between the user saving and the confirm prompt (TOCTOU), the edited content could be different from what the user saved. Mitigated by0o600permissions on the temp file and the confirm prompt, but worth documenting.- Export to arbitrary path:
resolveExportTargetwrites to any path the user specifies. No check that the target is within the project directory. Users could accidentally export to system paths. The overwrite confirmation mitigates accidental overwrites.
Suggestions 💡
- After clipboard copy of a secret key, consider displaying a brief warning: "Copied — clear your clipboard after use"
- For
editTextField, consider reading the file immediately after the user confirms and comparing mtime to detect external modification - Consider adding a
--dry-runflag for export that prints to stdout instead of writing a file - The
SANITIZE_KEYSset is comprehensive but consider making it a shared constant withcredentials-command.tsto avoid drift
Summary
Solid local credential management tool with good security hygiene. File permissions, secret masking, and input validation are all properly handled. The clipboard and temp-file concerns are inherent to any local CLI tool working with secrets and are adequately mitigated. No injection, no exposure, no privilege escalation. Approved.
Extends `build credentials manage` with a new top-level action — "Add
credential…" — that branches into two paths:
- Add platform support
Shows only platforms not yet configured for the app. After a confirm
that explicitly warns the user the manager will close and they will
not return automatically, hands off to `onboardingBuilderCommand`
(the existing `build init` Ink wizard). The manager's Ink session is
stopped via `stopInitInkSession` before the wizard takes over.
- Add configuration option
Lists every CREDENTIAL_KNOWLEDGE entry tagged `category: 'configuration'`
that is NOT already set on the relevant platform(s). Reuses the
type-aware editor — boolean → true/false select, enum → enum select,
duration → 1h/6h/2d text prompt with range validation, string →
single-line text prompt. Shared keys ask whether to write to both
platforms or one, when multiple are configured and missing.
Categorisation:
- credential — signing/auth material (certs, keystores, API keys,
passwords). Added through the onboarding wizard, not this menu.
- configuration — behaviour knobs (BUILD_OUTPUT_UPLOAD_ENABLED,
BUILD_OUTPUT_RETENTION_SECONDS, SKIP_BUILD_NUMBER_BUMP,
CAPGO_IOS_DISTRIBUTION, CAPGO_ANDROID_FLAVOR, CAPGO_IOS_SCHEME,
CAPGO_IOS_TARGET).
Main loop changes:
- New `add` branch wired between `view` and `export`.
- `handedOffToOnboarding` flag skips the final `pOutro('Done.')` so the
user sees only the wizard's own outcome after a platform handoff.
…ling Both `handleAddCredential` and `handleAddConfiguration` previously called their inner prompts once and returned on any cancel, so pressing Esc in the value/target picker or the config picker bubbled straight back to the main action menu. Wrap each in a while-loop: - `handleAddCredential` re-shows the Add sub-menu after Add platform (handoff declined) or Add configuration (whether something was added or not). Only Esc/Back from the Add sub-menu itself returns to the main action menu. - `handleAddConfiguration` re-shows the configuration picker after the platform-target or value prompts cancel, and after a successful add. Only Esc from the picker returns to the Add credential menu. After a successful add the working entry is reloaded from disk so the just-added key drops out of the next picker iteration. Also updates the status-line wording on the sub-screens to make the new "Esc returns to the previous menu" semantics explicit.
The --platform option was advertised in the command help and declared on ManageCredentialsOptions, but the flat-inspector refactor removed the only line that read it. Result: --platform ios still showed Android fields in the inspector and let Export/Delete operate on the unscoped platform — a real inconsistency between help and behaviour, with medium-severity risk on the destructive paths. Honour the flag end-to-end: - Validate the value at entry: reject anything other than ios/android. - After the appId filter, run entries through applyPlatformFilter: every entry is narrowed to just the requested platform via narrowEntryToPlatform (drops the other platform's saved data from the working copy; on-disk credentials are untouched). - Entries that don't have the requested platform configured drop out. If nothing remains, fail fast with a clear message. - Re-apply narrowing at every refresh site: post-view-mutate, post-add-mutate, and post-delete. When the user deletes the last field of the scoped platform via the inspector, exit cleanly with "All <platform> credentials cleared." instead of falling back to the other platform. The narrowing is a view filter only — it never modifies on-disk credentials for the non-scoped platform.
# Conflicts: # cli/src/build/onboarding/file-picker.ts # cli/src/index.ts
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cli/src/build/credentials-manage.ts`:
- Around line 813-845: actionDecodeField currently prints decoded secrets
directly to the terminal which can expose sensitive fields; update this function
to either (A) redact known sensitive keys when the decoded payload is JSON —
detect JSON in actionDecodeField, walk object keys (e.g., private_key,
private_key_id, client_secret, api_key, secret, password) and replace their
values with a placeholder before logging — and log a single line indicating
redaction, or (B) if row.tag or row.key indicates a secret, require an explicit
user confirmation flag (e.g., --confirm-decode) before printing the decoded
content; implement one approach, keep JSON pretty-printing for non-sensitive
keys, and ensure binary/text handling remains unchanged while avoiding printing
raw secret values.
- Around line 1109-1111: Replace the custom character class in
sanitizeForFilename with the \w shorthand and allow hyphens: change the regex
from /[^a-z0-9_-]/gi to /[^\w-]/g (drop the i flag since \w covers letters
regardless of case) so sanitizeForFilename(value: string) uses
value.replace(/[^\w-]/g, '_').slice(0, 40); keep the function name
sanitizeForFilename and the slice(0, 40) behavior unchanged.
- Around line 23-31: Reorder the import statements so that the module
'./credentials' is imported before './onboarding/file-picker' to satisfy the
perfectionist/sort-imports rule; locate the import block that includes symbols
like clearSavedCredentials, getGlobalCredentialsPath, getLocalCredentialsPath,
listAllApps, loadSavedCredentials, removeSavedCredentialKeys,
updateSavedCredentials and move that whole import above the import for
'./onboarding/file-picker', then run the linter to confirm the import ordering
issue is resolved.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 13d9e3eb-90b0-4121-bc6c-2b819bb29573
📒 Files selected for processing (3)
cli/src/build/credentials-manage.tscli/src/build/onboarding/file-picker.tscli/src/index.ts
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a20c10d45a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Addresses three review comments on credentials-manage.ts plus one lint error surfaced while validating them. - actionDecodeField redacts known-sensitive JSON object keys (private_key, private_key_id, client_secret, api_key, secret, password, access_token, refresh_token; case-insensitive) before printing the pretty-printed JSON. The decoded service-account JSON for PLAY_CONFIG_JSON would otherwise dump the RSA private key to the terminal. Binary and printable-text decode paths are unchanged. A warn line lists which keys were redacted; the success summary now notes the redacted count. Users who really want the raw value can still pick "Show value" to dump the base64. - Import block reordered so './credentials' precedes './onboarding/…' to satisfy perfectionist/sort-imports. - sanitizeForFilename now uses /[^\w-]/g (functionally equivalent to the previous /[^a-z0-9_-]/gi but accepted by regexp/prefer-w). - Removed unused getFieldScope helper (dead since the flat-inspector refactor; was a lint baseline failure). Verified: bun run lint → clean, bun run typecheck → clean, bun run build → green.
Node's writeFile mode option only applies when the file is newly created — overwriting an existing file leaves the inode's permission bits untouched. A pre-existing 0644 .env.capgo.* file would therefore stay world-readable after Export, even though the success log claims "(mode 0600)". Add an explicit chmod(target.path, 0o600) after writeFile so the on-disk mode always matches the message and credential files never end up group- or world-readable. Verified empirically: pre-existing 0644 file → after writeFile still 0644 → after chmod correctly 0600.
|



Summary
Adds
npx @capgo/cli build credentials manage— a keyboard-driven TUI for browsing, exporting, and editing the build credentials saved at~/.capgo-credentials/credentials.json(or the project-local.capgo-credentials.json). Reuses the same Ink runtime ascapgo initso no new dependencies.The motivating use case: a user setting up CI for the first time wants to inspect what they have stored, paste the right env vars into GitHub Actions / GitLab / etc., and tweak booleans like
BUILD_OUTPUT_UPLOAD_ENABLEDwithout touching JSON by hand.What it does
Action menu (per app, no platform pre-pick):
[SHARED],[SHARED·ios],[SHARED·android],[ios], or[android]. Drift between platforms surfaces as two rows so the user can resolve it explicitly.osascript choose file name; other platforms fall back to a text prompt. The provisioning map is emitted asCAPGO_IOS_PROVISIONING_MAP_BASE64(already supported onmain) to dodge newline/quoting issues in CI secret stores.Per-field actions in the inspector:
binary, N bytesdepending on the payload.pbcopy/clip/wl-copy/xclip/xselwith a graceful fallback message.boolean(BUILD_OUTPUT_UPLOAD_ENABLED,SKIP_BUILD_NUMBER_BUMP) →true/falseselect.enum(CAPGO_IOS_DISTRIBUTION) → select fromapp_store/ad_hoc.duration(BUILD_OUTPUT_RETENTION_SECONDS) → text prompt accepting1h,6h,2d(1h–7d range), stored as seconds.os.tmpdir()(mode 0600), opened in the user's editor of choice, read back on confirm. JSON-validated forCAPGO_IOS_PROVISIONING_MAP.code-signing,cli-native-builds, andandroid-keystore-handling.[SHARED]row writes/removes on both).UX details:
capacitor.config.*(mirrorslistCredentialsCommand's pattern) — single-app users skip the picker entirely.openSaveFilePickerhelper is generic and reusable for any future "save file" prompt in the CLI.Files
cli/src/build/credentials-manage.ts(new, ~1170 lines) — command implementation, knowledge map, action handlers.cli/src/build/onboarding/file-picker.ts— addsopenSaveFilePicker.cli/src/index.ts— wires themanagesubcommand underbuild credentials.Test plan
bun run buildsucceeds (verified locally — typecheck + bundle clean).node cli/dist/index.js build credentials manage --helpprints the new subcommand.booleanfield → Edit offerstrue/false.CAPGO_IOS_DISTRIBUTION→ Edit offersapp_store/ad_hoc.BUILD_OUTPUT_RETENTION_SECONDS→ Edit accepts2h, rejects0, rejects8d.CAPGO_IOS_PROVISIONING_MAP→ Edit opens temp file; saving invalid JSON aborts with an error and keeps the previous value.pbcopy; failure path shows the warn line.Notes
CREDENTIAL_KNOWLEDGEin the source. If those wiki pages change, the strings need to be regenerated.BUILD_OUTPUT_*andSKIP_BUILD_NUMBER_BUMPdon't have dedicated wiki pages yet, so their explanations were synthesised from the CLI flag descriptions oncli-native-builds.BUILD_OUTPUT_UPLOAD_ENABLEDis stored differently per platform (a real wart in the underlying data model), the inspector shows two rows so the user can edit each side. Closing the underlying duplication is out of scope for this PR.gatherFieldRows,escapeDotenvValue,parseOutputRetentionLocal,inferScope,tagOrder) if you want them in this PR.Summary by CodeRabbit