-
Notifications
You must be signed in to change notification settings - Fork 0
feat: per-person App default + shell-aware export snippet #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,77 @@ | ||||||||||||||||||||||||||||||||||||||||||
| package cli | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // shellProfile describes how to render the env-export snippet for a | ||||||||||||||||||||||||||||||||||||||||||
| // detected shell. Bash and zsh share the same `export` syntax; fish uses | ||||||||||||||||||||||||||||||||||||||||||
| // `set -x`. Unknown shells fall back to the bash/zsh snippet with a | ||||||||||||||||||||||||||||||||||||||||||
| // generic rc-file description, which is correct for ~99% of dev setups. | ||||||||||||||||||||||||||||||||||||||||||
| type shellProfile struct { | ||||||||||||||||||||||||||||||||||||||||||
| // rcDescription is the human-readable file path shown in the prompt | ||||||||||||||||||||||||||||||||||||||||||
| // (e.g. "~/.bashrc"). For unknown shells we widen this to a list. | ||||||||||||||||||||||||||||||||||||||||||
| rcDescription string | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // quotedAssign is the function that renders `KEY=value` (or its | ||||||||||||||||||||||||||||||||||||||||||
| // equivalent) for this shell, given a value that may contain a | ||||||||||||||||||||||||||||||||||||||||||
| // command substitution. | ||||||||||||||||||||||||||||||||||||||||||
| render func(key, value string, isCommandSub bool) string | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func (p shellProfile) exportLines(appID, instID, keyRef string) []string { | ||||||||||||||||||||||||||||||||||||||||||
| keyIsSub := strings.HasPrefix(keyRef, "$(") | ||||||||||||||||||||||||||||||||||||||||||
| return []string{ | ||||||||||||||||||||||||||||||||||||||||||
| p.render("GH_AS_BOT_APP_ID", appID, false), | ||||||||||||||||||||||||||||||||||||||||||
| p.render("GH_AS_BOT_INSTALLATION_ID", instID, false), | ||||||||||||||||||||||||||||||||||||||||||
| p.render("GH_AS_BOT_PRIVATE_KEY", keyRef, keyIsSub), | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // detectShell inspects $SHELL and returns a profile describing how to | ||||||||||||||||||||||||||||||||||||||||||
| // emit the export snippet. We deliberately keep the matching loose | ||||||||||||||||||||||||||||||||||||||||||
| // (basename suffix only) — exotic shell paths (/opt/homebrew/bin/zsh, | ||||||||||||||||||||||||||||||||||||||||||
| // custom builds, etc.) all resolve correctly that way. | ||||||||||||||||||||||||||||||||||||||||||
| func detectShell() shellProfile { | ||||||||||||||||||||||||||||||||||||||||||
| switch shellBasename() { | ||||||||||||||||||||||||||||||||||||||||||
| case "zsh": | ||||||||||||||||||||||||||||||||||||||||||
| return shellProfile{rcDescription: "~/.zshrc", render: bashStyleExport} | ||||||||||||||||||||||||||||||||||||||||||
| case "bash": | ||||||||||||||||||||||||||||||||||||||||||
| return shellProfile{rcDescription: "~/.bashrc", render: bashStyleExport} | ||||||||||||||||||||||||||||||||||||||||||
| case "fish": | ||||||||||||||||||||||||||||||||||||||||||
| return shellProfile{rcDescription: "~/.config/fish/config.fish", render: fishStyleExport} | ||||||||||||||||||||||||||||||||||||||||||
| default: | ||||||||||||||||||||||||||||||||||||||||||
| return shellProfile{rcDescription: "~/.zshrc, ~/.bashrc, or equivalent", render: bashStyleExport} | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func shellBasename() string { | ||||||||||||||||||||||||||||||||||||||||||
| sh := os.Getenv("SHELL") | ||||||||||||||||||||||||||||||||||||||||||
| if sh == "" { | ||||||||||||||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return filepath.Base(sh) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func bashStyleExport(key, value string, isCommandSub bool) string { | ||||||||||||||||||||||||||||||||||||||||||
| if isCommandSub { | ||||||||||||||||||||||||||||||||||||||||||
| // Quote command substitutions so the shell defers evaluation | ||||||||||||||||||||||||||||||||||||||||||
| // until the variable is read, not at profile load. | ||||||||||||||||||||||||||||||||||||||||||
| return fmt.Sprintf(`export %s="%s"`, key, value) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return fmt.Sprintf("export %s=%s", key, value) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| func fishStyleExport(key, value string, isCommandSub bool) string { | ||||||||||||||||||||||||||||||||||||||||||
| // fish uses `set -x` for exported vars. Command substitution in | ||||||||||||||||||||||||||||||||||||||||||
| // fish is `(cmd)` not `$(cmd)`, so we rewrite if we see one. | ||||||||||||||||||||||||||||||||||||||||||
| if isCommandSub { | ||||||||||||||||||||||||||||||||||||||||||
| v := strings.TrimPrefix(value, "$(") | ||||||||||||||||||||||||||||||||||||||||||
| v = strings.TrimSuffix(v, ")") | ||||||||||||||||||||||||||||||||||||||||||
| return fmt.Sprintf("set -x %s (%s)", key, v) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| return fmt.Sprintf("set -x %s %s", key, value) | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+68
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For literal values (where
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,77 @@ | ||||||||||||||||||||||||||||||||||||||
| package cli | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||
| "testing" | ||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| func TestDetectShell_Bash(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||
| t.Setenv("SHELL", "/bin/bash") | ||||||||||||||||||||||||||||||||||||||
| p := detectShell() | ||||||||||||||||||||||||||||||||||||||
| if p.rcDescription != "~/.bashrc" { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("rcDescription = %q, want ~/.bashrc", p.rcDescription) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| lines := p.exportLines("1", "2", "/path/to/key.pem") | ||||||||||||||||||||||||||||||||||||||
| if !strings.HasPrefix(lines[0], "export ") { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("bash should emit `export`; got %q", lines[0]) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| func TestDetectShell_Zsh(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||
| t.Setenv("SHELL", "/usr/bin/zsh") | ||||||||||||||||||||||||||||||||||||||
| p := detectShell() | ||||||||||||||||||||||||||||||||||||||
| if p.rcDescription != "~/.zshrc" { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("rcDescription = %q, want ~/.zshrc", p.rcDescription) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| func TestDetectShell_Fish(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||
| t.Setenv("SHELL", "/opt/homebrew/bin/fish") | ||||||||||||||||||||||||||||||||||||||
| p := detectShell() | ||||||||||||||||||||||||||||||||||||||
| if p.rcDescription != "~/.config/fish/config.fish" { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("rcDescription = %q", p.rcDescription) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| lines := p.exportLines("1", "2", "$(security find-generic-password -s gh-as-bot -w)") | ||||||||||||||||||||||||||||||||||||||
| keyLine := lines[2] | ||||||||||||||||||||||||||||||||||||||
| if !strings.HasPrefix(keyLine, "set -x ") { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("fish should emit `set -x`; got %q", keyLine) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| if strings.Contains(keyLine, "$(") { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("fish command substitution should be (...) not $(...); got %q", keyLine) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| if !strings.Contains(keyLine, "(security find-generic-password -s gh-as-bot -w)") { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("fish should keep the substitution body; got %q", keyLine) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| func TestDetectShell_UnknownFallsBackToBashStyle(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||
| t.Setenv("SHELL", "/usr/local/bin/elvish") | ||||||||||||||||||||||||||||||||||||||
| p := detectShell() | ||||||||||||||||||||||||||||||||||||||
| if !strings.Contains(p.rcDescription, "~/.zshrc") || !strings.Contains(p.rcDescription, "~/.bashrc") { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("unknown shell should mention both rc files; got %q", p.rcDescription) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| lines := p.exportLines("1", "2", "/path/key.pem") | ||||||||||||||||||||||||||||||||||||||
| if !strings.HasPrefix(lines[0], "export ") { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("fallback should use bash-style export; got %q", lines[0]) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| func TestExportLines_QuotesCommandSubstitution(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||
| t.Setenv("SHELL", "/bin/bash") | ||||||||||||||||||||||||||||||||||||||
| p := detectShell() | ||||||||||||||||||||||||||||||||||||||
| lines := p.exportLines("1", "2", "$(security find-generic-password -s gh-as-bot -w)") | ||||||||||||||||||||||||||||||||||||||
| keyLine := lines[2] | ||||||||||||||||||||||||||||||||||||||
| if !strings.Contains(keyLine, `"$(security`) { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("bash command sub should be wrapped in double quotes; got %q", keyLine) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| func TestExportLines_LiteralPathUnquoted(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||
| t.Setenv("SHELL", "/bin/bash") | ||||||||||||||||||||||||||||||||||||||
| p := detectShell() | ||||||||||||||||||||||||||||||||||||||
| lines := p.exportLines("1", "2", "/abs/path/key.pem") | ||||||||||||||||||||||||||||||||||||||
| keyLine := lines[2] | ||||||||||||||||||||||||||||||||||||||
| if strings.Contains(keyLine, `"`) { | ||||||||||||||||||||||||||||||||||||||
| t.Errorf("literal path should not be quoted; got %q", keyLine) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+69
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test explicitly checks that literal paths are unquoted. If the implementation is updated to quote values (to support paths with spaces), this test should be updated to expect quotes.
Suggested change
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The values in the export snippet should be quoted to handle paths or values containing spaces or special characters. Additionally, the comment regarding "deferring evaluation" is inaccurate; in bash/zsh,
export VAR="$(cmd)"executes the command at the time of assignment (e.g., when the shell profile is sourced), not when the variable is read. Quoting is primarily useful here to preserve newlines in PEM data and handle spaces in paths.