Skip to content

Commit e79c642

Browse files
committed
v1.4.0
feat: add shell completions and unit tests - Add completions command for bash, zsh, fish and powershell - Dynamic snippet name TAB completion via list-names - Unit tests for var, storage, encryption and parse_key_val
1 parent 2a4ceb8 commit e79c642

14 files changed

Lines changed: 556 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to sinbo will be documented here.
44

55
---
66

7+
## 1.4.0 - 2026-04-12
8+
9+
### Added
10+
- Shell completions for bash, zsh, fish, and powershell (`sinbo completions <shell>`)
11+
- Dynamic snippet name completion on TAB for `get`, `remove`, `edit`, `encrypt`, `decrypt`, `export`
12+
- Hidden `list-names` command for shell completion scripts
13+
- Unit tests for `var.rs`, `storage.rs`, `encryption.rs`, and `main.rs`
14+
715
## 1.3.0 - 2026-04-12
816

917
### Added

Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sinbo"
3-
version = "1.3.0"
3+
version = "1.4.0"
44
edition = "2024"
55
description = "A CLI snippet manager. Store code once, retrieve it anywhere."
66
license = "MIT"
@@ -24,4 +24,7 @@ rand = "0.8"
2424
rpassword = "7"
2525
zeroize = "1.8.2"
2626
clearscreen = "4.0.6"
27-
dialoguer = "0.12.0"
27+
dialoguer = "0.12.0"
28+
29+
[dev-dependencies]
30+
tempfile = "3"

SECURITY.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Security Policy
2+
3+
## Reporting a Vulnerability
4+
5+
If you discover a security vulnerability, **do not open a public issue**.
6+
7+
Email: your@email.com
8+
9+
Include a description, steps to reproduce, and potential impact. I'll respond as soon as possible.
10+
11+
## Supported Versions
12+
13+
Only the latest version of sinbo receives security fixes.
14+
15+
## Install Script
16+
17+
The install script at `install.sh` and `install.ps1` should always be reviewed before piping to a shell. You can inspect them directly:
18+
19+
```bash
20+
curl -sSf https://raw.githubusercontent.com/opmr0/sinbo/main/install.sh | less
21+
```
22+
23+
## Scope
24+
25+
- Vulnerabilities in sinbo's encryption implementation
26+
- Supply chain or install script tampering
27+
- Sensitive data exposure via metadata or plaintext leaks

readme.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,30 @@ sinbo get docker-run --args port=8080 | sh # substitute then run
270270

271271
---
272272

273+
## Shell Completions
274+
275+
**bash**
276+
```bash
277+
echo 'eval "$(sinbo completions bash)"' >> ~/.bashrc && source ~/.bashrc
278+
```
279+
280+
**zsh**
281+
```bash
282+
echo 'eval "$(sinbo completions zsh)"' >> ~/.zshrc && source ~/.zshrc
283+
```
284+
285+
**fish**
286+
```bash
287+
sinbo completions fish > ~/.config/fish/completions/sinbo.fish
288+
```
289+
290+
**powershell**
291+
```powershell
292+
Add-Content $PROFILE "`nsinbo completions powershell | Invoke-Expression"
293+
```
294+
295+
---
296+
273297
## How It Works
274298

275299
Snippets are stored as plain files in your system config directory:

src/completions/sinbo.bash

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
_sinbo() {
2+
local cur prev
3+
cur="${COMP_WORDS[COMP_CWORD]}"
4+
prev="${COMP_WORDS[COMP_CWORD-1]}"
5+
6+
local subcommands="get g add a list l remove r edit e search s encrypt decrypt export import completions"
7+
8+
if [[ $COMP_CWORD -eq 1 ]]; then
9+
COMPREPLY=($(compgen -W "$subcommands" -- "$cur"))
10+
return
11+
fi
12+
13+
if [[ $COMP_CWORD -eq 2 ]]; then
14+
case "$prev" in
15+
get|g|remove|r|edit|e|encrypt|decrypt|export)
16+
local snippets=$(sinbo list-names 2>/dev/null)
17+
COMPREPLY=($(compgen -W "$snippets" -- "$cur"))
18+
return ;;
19+
esac
20+
fi
21+
22+
COMPREPLY=()
23+
}
24+
25+
complete -F _sinbo sinbo

src/completions/sinbo.fish

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
function __sinbo_snippets
2+
sinbo list-names 2>/dev/null
3+
end
4+
5+
set -l subcommands get g add a list l remove r edit e search s encrypt decrypt export import completions
6+
7+
complete -c sinbo -f -n "not __fish_seen_subcommand_from $subcommands" -a "$subcommands"
8+
9+
for cmd in get g remove r edit e encrypt decrypt export
10+
complete -c sinbo -f -n "__fish_seen_subcommand_from $cmd" -a "(__sinbo_snippets)"
11+
end

src/completions/sinbo.ps1

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Register-ArgumentCompleter -Native -CommandName sinbo -ScriptBlock {
2+
param($wordToComplete, $commandAst, $cursorPosition)
3+
4+
$words = $commandAst.CommandElements
5+
$count = $words.Count
6+
7+
$subcommands = @('get','g','add','a','list','l','remove','r','edit','e','search','s','encrypt','decrypt','export','import','completions')
8+
9+
$snippetCommands = @('get','g','remove','r','edit','e','encrypt','decrypt','export')
10+
11+
if ($count -eq 1 -or ($count -eq 2 -and $wordToComplete -ne '')) {
12+
$subcommands | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
13+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
14+
}
15+
} elseif ($count -ge 2 -and $snippetCommands -contains $words[1].Value) {
16+
sinbo list-names 2>$null | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
17+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
18+
}
19+
}
20+
}

src/completions/sinbo.zsh

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#compdef sinbo
2+
3+
_sinbo() {
4+
local state
5+
6+
_arguments \
7+
'1: :->subcommand' \
8+
'*: :->args'
9+
10+
case $state in
11+
subcommand)
12+
local subcommands=(
13+
'get:Print or copy a snippet'
14+
'g:Print or copy a snippet'
15+
'add:Add a new snippet'
16+
'a:Add a new snippet'
17+
'list:List all snippets'
18+
'l:List all snippets'
19+
'remove:Remove a snippet'
20+
'r:Remove a snippet'
21+
'edit:Edit an existing snippet'
22+
'e:Edit an existing snippet'
23+
'search:Search snippets'
24+
's:Search snippets'
25+
'encrypt:Encrypt a snippet'
26+
'decrypt:Decrypt a snippet'
27+
'export:Export a snippet'
28+
'import:Import a snippet'
29+
'completions:Generate shell completions'
30+
)
31+
_describe 'subcommand' subcommands ;;
32+
args)
33+
case $words[2] in
34+
get|g|remove|r|edit|e|encrypt|decrypt|export)
35+
local snippets=(${(f)"$(sinbo list-names 2>/dev/null)"})
36+
_describe 'snippet' snippets ;;
37+
esac ;;
38+
esac
39+
}
40+
41+
_sinbo

src/encryption.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,63 @@ pub fn secure_delete(path: &Path) -> io::Result<()> {
133133
}
134134
Ok(())
135135
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use super::*;
140+
141+
#[test]
142+
fn test_encrypt_decrypt_roundtrip() {
143+
let plaintext = b"hello secret world";
144+
let password = b"strongpassword";
145+
let encrypted = encrypt(plaintext, password);
146+
let decrypted = decrypt(&encrypted, password).unwrap();
147+
assert_eq!(decrypted, plaintext);
148+
}
149+
150+
#[test]
151+
fn test_wrong_password_fails() {
152+
let plaintext = b"hello secret world";
153+
let encrypted = encrypt(plaintext, b"correctpassword");
154+
let result = decrypt(&encrypted, b"wrongpassword");
155+
assert!(result.is_err());
156+
assert!(matches!(
157+
result.unwrap_err(),
158+
EncryptionError::DecryptFailed
159+
));
160+
}
161+
162+
#[test]
163+
fn test_corrupted_data_fails() {
164+
let result = decrypt(b"tooshort", b"password");
165+
assert!(matches!(
166+
result.unwrap_err(),
167+
EncryptionError::CorruptedFile
168+
));
169+
}
170+
171+
#[test]
172+
fn test_empty_plaintext() {
173+
let encrypted = encrypt(b"", b"password");
174+
let decrypted = decrypt(&encrypted, b"password").unwrap();
175+
assert_eq!(decrypted, b"");
176+
}
177+
178+
#[test]
179+
fn test_encrypt_produces_different_ciphertext_each_time() {
180+
let plaintext = b"same content";
181+
let password = b"password";
182+
let enc1 = encrypt(plaintext, password);
183+
let enc2 = encrypt(plaintext, password);
184+
assert_ne!(enc1, enc2);
185+
}
186+
187+
#[test]
188+
fn test_secure_delete_removes_file() {
189+
let dir = tempfile::tempdir().unwrap();
190+
let path = dir.path().join("test.tmp");
191+
std::fs::write(&path, b"sensitive data").unwrap();
192+
secure_delete(&path).unwrap();
193+
assert!(!path.exists());
194+
}
195+
}

0 commit comments

Comments
 (0)