Commit ae55bc8935
ae55bc893563b9a45fa337e8a2e59102efab5b87
parent: a89f22df65
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-17 16:26 UTC
Add generate command
Layout: unified · split
Sources/keycask/Commands/Generate.swift
added
+28
| @@ -0,0 +1,28 @@ |
| |
1 | import ArgumentParser |
| |
2 | import KeycaskCore |
| |
3 | |
| |
4 | struct Generate: ParsableCommand { |
| |
5 | static let configuration = CommandConfiguration(abstract: "Generate a password.") |
| |
6 | |
| |
7 | @Option(name: .long, help: "Password length (default 24).") var length: Int? |
| |
8 | @Option(name: .long, help: "Passphrase of this many words instead.") var words: Int? |
| |
9 | @Flag(name: .long, help: "Copy to the clipboard instead of printing.") var copy = false |
| |
10 | |
| |
11 | mutating func validate() throws { |
| |
12 | if length != nil, words != nil { |
| |
13 | throw ValidationError("--length and --words are mutually exclusive") |
| |
14 | } |
| |
15 | if let length, length < 1 { throw ValidationError("--length must be at least 1") } |
| |
16 | if let words, words < 1 { throw ValidationError("--words must be at least 1") } |
| |
17 | } |
| |
18 | |
| |
19 | func run() throws { |
| |
20 | let secret = |
| |
21 | words.map { Generator.passphrase(words: $0) } |
| |
22 | ?? Generator.password(length: length ?? Generator.defaultLength) |
| |
23 | if copy { |
| |
24 | throw KeycaskError.failure("clipboard not available") |
| |
25 | } |
| |
26 | print(secret) |
| |
27 | } |
| |
28 | } |
Sources/keycask/Keycask.swift
+3 −1
| @@ -9,7 +9,9 @@ struct Keycask: ParsableCommand { |
| 9 | static let configuration = CommandConfiguration( |
9 | static let configuration = CommandConfiguration( |
| 10 | commandName: "keycask", |
10 | commandName: "keycask", |
| 11 | abstract: "Command-line password manager. One passphrase-encrypted vault file.", |
11 | abstract: "Command-line password manager. One passphrase-encrypted vault file.", |
| 12 | subcommands: [Init.self, Add.self, Show.self, Ls.self, Find.self, Edit.self, Rm.self] |
12 | subcommands: [ |
| |
13 | Init.self, Add.self, Show.self, Ls.self, Find.self, Edit.self, Rm.self, Generate.self, |
| |
14 | ] |
| 13 | ) |
15 | ) |
| 14 | |
16 | |
| 15 | @OptionGroup var global: GlobalOptions |
17 | @OptionGroup var global: GlobalOptions |
Tests/KeycaskCLITests/GenerateTests.swift
added
+32
| @@ -0,0 +1,32 @@ |
| |
1 | import Foundation |
| |
2 | import Testing |
| |
3 | |
| |
4 | @Suite struct GenerateTests { |
| |
5 | @Test func defaultIs24Characters() throws { |
| |
6 | let cli = try CLI() |
| |
7 | let r = try cli.run(["generate"], passphrase: nil) |
| |
8 | #expect(r.status == 0) |
| |
9 | #expect(r.stdout.count == 25) |
| |
10 | } |
| |
11 | |
| |
12 | @Test func lengthAndWords() throws { |
| |
13 | let cli = try CLI() |
| |
14 | let lenResult = try cli.run(["generate", "--length", "12"], passphrase: nil) |
| |
15 | #expect(lenResult.stdout.count == 13) |
| |
16 | let wordsOutput = try cli.run(["generate", "--words", "6"], passphrase: nil).stdout |
| |
17 | let w = wordsOutput.trimmingCharacters(in: .newlines).split(separator: "-") |
| |
18 | #expect(w.count == 6) |
| |
19 | } |
| |
20 | |
| |
21 | @Test func doesNotNeedAVault() throws { |
| |
22 | let cli = try CLI() |
| |
23 | #expect(!FileManager.default.fileExists(atPath: cli.vault.path)) |
| |
24 | #expect(try cli.run(["generate"], passphrase: nil).status == 0) |
| |
25 | } |
| |
26 | |
| |
27 | @Test func lengthAndWordsTogetherIsUsageError() throws { |
| |
28 | let cli = try CLI() |
| |
29 | #expect( |
| |
30 | try cli.run(["generate", "--length", "3", "--words", "3"], passphrase: nil).status == 2) |
| |
31 | } |
| |
32 | } |