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 | 9 | static let configuration = CommandConfiguration( |
| 10 | 10 | commandName: "keycask", |
| 11 | 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 | 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 | } |