Skip to content

Commit 3d95cd6

Browse files
committed
Initialize opssh project with core functionality and configuration management
0 parents  commit 3d95cd6

14 files changed

Lines changed: 886 additions & 0 deletions

File tree

.gitignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# If you prefer the allow list template instead of the deny list, see community template:
2+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
3+
#
4+
# Binaries for programs and plugins
5+
*.exe
6+
*.exe~
7+
*.dll
8+
*.so
9+
*.dylib
10+
11+
# Test binary, built with `go test -c`
12+
*.test
13+
14+
# Code coverage profiles and other test artifacts
15+
*.out
16+
coverage.*
17+
*.coverprofile
18+
profile.cov
19+
20+
# Dependency directories (remove the comment below to include it)
21+
# vendor/
22+
23+
# Go workspace file
24+
go.work
25+
go.work.sum
26+
27+
# env file
28+
.env
29+
30+
# Editor/IDE
31+
# .idea/
32+
# .vscode/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 [ccxdev]
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
# opssh
2+
3+
**SSH Key Profile Switcher** - A CLI tool for managing SSH key switching for 1Password's SSH agent.
4+
5+
## Description
6+
7+
`opssh` simplifies switching between different SSH key profiles (e.g., personal vs work) by managing the 1Password SSH agent configuration file (`agent.toml`). It allows you to:
8+
9+
- Store multiple SSH key profiles in a simple YAML configuration
10+
- Quickly switch between profiles without manually editing configuration files
11+
- View and manage your SSH key profiles
12+
13+
## Installation
14+
15+
### Prerequisites
16+
17+
- Go 1.25.5 or later
18+
19+
### Install from Source
20+
21+
```bash
22+
go install github.com/ccxdev/opssh@latest
23+
```
24+
25+
### Build from Source
26+
27+
```bash
28+
git clone https://github.com/ccxdev/opssh.git
29+
cd opssh
30+
go build -o opssh
31+
32+
./opssh <command>
33+
```
34+
35+
## Usage
36+
37+
### Add a Profile
38+
39+
Add a new SSH key profile:
40+
41+
```bash
42+
opssh add <profile-name>
43+
```
44+
45+
You'll be prompted to enter:
46+
- Account name
47+
- Vault name
48+
- Item name
49+
50+
### List Profiles
51+
52+
View all configured profiles:
53+
54+
```bash
55+
opssh list
56+
```
57+
58+
The currently active profile will be marked with an asterisk (`*`).
59+
60+
### Switch Profile
61+
62+
Switch to a different SSH key profile:
63+
64+
```bash
65+
opssh switch <profile-name>
66+
```
67+
68+
This updates the `~/.config/1Password/ssh/agent.toml` file with the selected profile.
69+
70+
### Show Current Profile
71+
72+
Display the currently active SSH key profile:
73+
74+
```bash
75+
opssh current
76+
```
77+
78+
### Remove a Profile
79+
80+
Remove a profile from your configuration:
81+
82+
```bash
83+
opssh remove <profile-name>
84+
```
85+
86+
**Note:** If the profile is currently active, you'll receive a warning.
87+
88+
## Configuration
89+
90+
Profiles are stored in `~/.opssh.yaml` with the following structure:
91+
92+
```yaml
93+
profiles:
94+
personal:
95+
account: "myaccount"
96+
vault: "Personal"
97+
item: "My SSH Key"
98+
work:
99+
account: "myaccount"
100+
vault: "Work"
101+
item: "Work SSH Key"
102+
```
103+
104+
## Global Flags
105+
106+
- `--verbose, -v`: Enable verbose output
107+
- `--config`: Specify a custom config file path (default: `~/.opssh.yaml`)
108+
109+
## Examples
110+
111+
### Quick Setup
112+
113+
```bash
114+
# Add a profile
115+
opssh add work
116+
117+
# Switch to work profile
118+
opssh switch work
119+
120+
# Verify current profile
121+
opssh current
122+
```
123+
124+
### Manual Profile Management
125+
126+
```bash
127+
# Add a profile manually
128+
opssh add personal
129+
130+
# List all profiles
131+
opssh list
132+
133+
# Switch between profiles
134+
opssh switch personal
135+
opssh switch work
136+
137+
# Remove a profile
138+
opssh remove old-profile
139+
```
140+
141+
## How It Works
142+
143+
`opssh` manages SSH key profiles by:
144+
145+
1. **Storing profiles** in `~/.opssh.yaml` - a simple YAML configuration file
146+
2. **Updating agent.toml** at `~/.config/1Password/ssh/agent.toml` when switching profiles
147+
3. **Reading agent.toml** to determine the currently active profile
148+
149+
The tool works with a simple YAML configuration file to manage your SSH key profiles.
150+
151+
## Requirements
152+
153+
- **1Password SSH Agent**: This tool is designed to work with 1Password's SSH agent feature
154+
155+
### Agent.toml Not Found
156+
157+
If `agent.toml` doesn't exist, 1Password SSH agent may not be set up. Ensure:
158+
- 1Password SSH agent is enabled
159+
- The directory `~/.config/1Password/ssh/` exists
160+
161+
### Profile Not Found
162+
163+
If you get "profile not found" errors:
164+
- Use `opssh list` to see available profiles
165+
- Check your `~/.opssh.yaml` file for typos
166+
167+
## License
168+
169+
[MIT](./LICENSE)
170+
171+
## Contributing
172+
173+
Please feel free to open an issue or submit a pull request.
174+

cmd/add.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/AlecAivazis/survey/v2"
7+
"github.com/ccxdev/opssh/internal/config"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var addCmd = &cobra.Command{
12+
Use: "add <name>",
13+
Short: "Add a new SSH key profile",
14+
Long: `Add a new SSH key profile to your config file.
15+
You'll be prompted to enter the account, vault, and item details.`,
16+
Args: cobra.ExactArgs(1),
17+
RunE: func(cmd *cobra.Command, args []string) error {
18+
profileName := args[0]
19+
20+
cfg, err := config.LoadConfig()
21+
if err != nil {
22+
return fmt.Errorf("failed to load config: %w", err)
23+
}
24+
25+
if _, err := config.GetProfile(cfg, profileName); err == nil {
26+
return fmt.Errorf("profile '%s' already exists", profileName)
27+
}
28+
29+
var profile *config.Profile
30+
31+
profile, err = promptManualProfile()
32+
33+
if err != nil {
34+
return err
35+
}
36+
37+
if err := config.AddProfile(cfg, profileName, *profile); err != nil {
38+
return fmt.Errorf("failed to add profile: %w", err)
39+
}
40+
41+
if err := config.SaveConfig(cfg); err != nil {
42+
return fmt.Errorf("failed to save config: %w", err)
43+
}
44+
45+
fmt.Printf("✓ Added profile '%s'\n", profileName)
46+
fmt.Printf(" Account: %s\n", profile.Account)
47+
fmt.Printf(" Vault: %s\n", profile.Vault)
48+
fmt.Printf(" Item: %s\n", profile.Item)
49+
50+
var switchNow bool
51+
switchPrompt := &survey.Confirm{
52+
Message: "Switch to this profile now?",
53+
Default: false,
54+
}
55+
if err := survey.AskOne(switchPrompt, &switchNow); err == nil && switchNow {
56+
fmt.Println("Use 'opssh switch " + profileName + "' to switch to this profile.")
57+
}
58+
59+
return nil
60+
},
61+
}
62+
63+
func promptManualProfile() (*config.Profile, error) {
64+
var account, vault, item string
65+
66+
accountPrompt := &survey.Input{
67+
Message: "Account:",
68+
}
69+
if err := survey.AskOne(accountPrompt, &account); err != nil {
70+
return nil, fmt.Errorf("failed to get account: %w", err)
71+
}
72+
73+
vaultPrompt := &survey.Input{
74+
Message: "Vault:",
75+
}
76+
if err := survey.AskOne(vaultPrompt, &vault); err != nil {
77+
return nil, fmt.Errorf("failed to get vault: %w", err)
78+
}
79+
80+
itemPrompt := &survey.Input{
81+
Message: "Item:",
82+
}
83+
if err := survey.AskOne(itemPrompt, &item); err != nil {
84+
return nil, fmt.Errorf("failed to get item: %w", err)
85+
}
86+
87+
return &config.Profile{
88+
Account: account,
89+
Vault: vault,
90+
Item: item,
91+
}, nil
92+
}
93+
94+
func init() {
95+
rootCmd.AddCommand(addCmd)
96+
}

cmd/current.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/ccxdev/opssh/internal/agent"
7+
"github.com/ccxdev/opssh/internal/config"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var currentCmd = &cobra.Command{
12+
Use: "current",
13+
Short: "Show the currently active SSH key profile",
14+
Long: `Display the currently active SSH key profile by reading the agent.toml file
15+
and matching it against your configured profiles.`,
16+
RunE: func(cmd *cobra.Command, args []string) error {
17+
activeProfile, err := agent.GetCurrentActiveProfile()
18+
if err != nil {
19+
return fmt.Errorf("failed to get current profile: %w", err)
20+
}
21+
22+
cfg, err := config.LoadConfig()
23+
if err != nil {
24+
return fmt.Errorf("failed to load config: %w", err)
25+
}
26+
27+
var profileName string
28+
for name, profile := range cfg.Profiles {
29+
if profile.Account == activeProfile.Account &&
30+
profile.Vault == activeProfile.Vault &&
31+
profile.Item == activeProfile.Item {
32+
profileName = name
33+
break
34+
}
35+
}
36+
37+
if profileName != "" {
38+
fmt.Printf("Current profile: %s\n", profileName)
39+
} else {
40+
fmt.Println("Current profile (not in config):")
41+
}
42+
43+
fmt.Printf(" Account: %s\n", activeProfile.Account)
44+
fmt.Printf(" Vault: %s\n", activeProfile.Vault)
45+
fmt.Printf(" Item: %s\n", activeProfile.Item)
46+
47+
return nil
48+
},
49+
}
50+
51+
func init() {
52+
rootCmd.AddCommand(currentCmd)
53+
}
54+

0 commit comments

Comments
 (0)