Skip to content

Commit e26d1d0

Browse files
authored
Merge pull request #12 from samzong/feat/cf-quick-conn
feat(CLI): simplify CLI installation and enhance configuration listing
2 parents 8a4d9cc + 1599381 commit e26d1d0

File tree

3 files changed

+125
-77
lines changed

3 files changed

+125
-77
lines changed

CLI/Sources/ConfigForgeCLI/Commands/KubeCommand.swift

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ struct KubeSetCommand: ParsableCommand {
4545
discussion: "Set the specified file as the active Kubernetes configuration"
4646
)
4747

48-
@Argument(help: "The configuration file name to set as active")
48+
@Argument(help: "The configuration number or filename to set as active")
4949
var filename: String
5050

5151
func run() throws {
@@ -91,7 +91,6 @@ class KubeConfigManager {
9191

9292
// Calculate MD5 hash for a file
9393
private func calculateMD5(filePath: String) throws -> String {
94-
// 使用Data直接读取文件内容,避免文件句柄可能的缓存问题
9594
let data = try Data(contentsOf: URL(fileURLWithPath: filePath))
9695

9796
var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
@@ -155,26 +154,34 @@ class KubeConfigManager {
155154
return dateFormatter.string(from: Date())
156155
}
157156

157+
// Get sorted list of config files
158+
private func getSortedConfigFiles() throws -> [String] {
159+
let contents = try fileManager.contentsOfDirectory(atPath: kubeConfigsDir)
160+
return contents.filter {
161+
$0.hasSuffix(".yaml") || $0.hasSuffix(".yml") || $0.hasSuffix(".kubeconfig")
162+
}.sorted()
163+
}
164+
158165
// List all Kubernetes configurations
159166
func listConfigurations(validate: Bool) throws {
160167
// Create configs directory if it doesn't exist
161168
try ensureConfigsDirExists()
162169

163-
// 确保活动配置文件存在
164170
guard fileManager.fileExists(atPath: activeConfigPath) else {
165171
print("No active Kubernetes configuration found.")
166-
if let configs = try? fileManager.contentsOfDirectory(atPath: kubeConfigsDir), !configs.isEmpty {
172+
if let configs = try? getSortedConfigFiles(), !configs.isEmpty {
167173
print("Available configurations (none active):")
168-
for config in configs {
169-
print(" \(config)")
174+
for (index, config) in configs.enumerated() {
175+
print(" \(index + 1). \(config)")
170176
}
177+
print("")
178+
print("Use 'cf k set <number>' or 'cf k set <filename>' to activate a configuration")
171179
} else {
172180
print("No configurations found in \(kubeConfigsDir)")
173181
}
174182
return
175183
}
176184

177-
// 计算活动配置的哈希值前,确保访问的是最新文件
178185
try? fileManager.attributesOfItem(atPath: activeConfigPath)
179186

180187
// Get active config hash
@@ -187,10 +194,7 @@ class KubeConfigManager {
187194
}
188195

189196
// List configurations
190-
let contents = try fileManager.contentsOfDirectory(atPath: kubeConfigsDir)
191-
let configFiles = contents.filter {
192-
$0.hasSuffix(".yaml") || $0.hasSuffix(".yml") || $0.hasSuffix(".kubeconfig")
193-
}.sorted()
197+
let configFiles = try getSortedConfigFiles()
194198

195199
if configFiles.isEmpty {
196200
print("No Kubernetes configurations found in \(kubeConfigsDir)")
@@ -203,9 +207,6 @@ class KubeConfigManager {
203207
var activeFound = false
204208
var exactActiveFilename: String? = nil
205209

206-
// Debug output for troubleshooting
207-
//print("Active config hash: \(activeConfigHash)")
208-
209210
// First pass - look for exact match
210211
for file in configFiles {
211212
let filePath = kubeConfigsDir + "/" + file
@@ -215,7 +216,6 @@ class KubeConfigManager {
215216

216217
do {
217218
let fileHash = try calculateMD5(filePath: filePath)
218-
//print("File \(file) hash: \(fileHash)")
219219
if fileHash == activeConfigHash {
220220
exactActiveFilename = file
221221
activeFound = true
@@ -227,8 +227,9 @@ class KubeConfigManager {
227227
}
228228

229229
// Second pass - display list
230-
for file in configFiles {
230+
for (index, file) in configFiles.enumerated() {
231231
let filePath = kubeConfigsDir + "/" + file
232+
let number = index + 1
232233

233234
// Check if this is the active config
234235
let isActive = file == exactActiveFilename
@@ -240,21 +241,17 @@ class KubeConfigManager {
240241
}
241242

242243
// Format and print
243-
if isActive {
244-
if !isValid && validate {
245-
print("* \(file) (active) [invalid]")
246-
} else {
247-
print("* \(file) (active)")
248-
}
249-
} else {
250-
if !isValid && validate {
251-
print(" \(file) [invalid]")
252-
} else {
253-
print(" \(file)")
254-
}
255-
}
244+
let activeMarker = isActive ? "* " : " "
245+
let validationMarker = (!isValid && validate) ? " [invalid]" : ""
246+
let activeLabel = isActive ? " (active)" : ""
247+
248+
print("\(activeMarker)\(number). \(file)\(activeLabel)\(validationMarker)")
256249
}
257250

251+
print("")
252+
print("Use 'cf k set <number>' or 'cf k set <filename>' to switch configuration")
253+
print("Use 'cf k current' to show current active configuration")
254+
258255
if !activeFound {
259256
print("\nNote: Active configuration not found in configs directory")
260257
print("Active config path: \(activeConfigPath)")
@@ -266,17 +263,36 @@ class KubeConfigManager {
266263
// Ensure configs directory exists
267264
try ensureConfigsDirExists()
268265

269-
let sourcePath = kubeConfigsDir + "/" + filename
266+
let configFiles = try getSortedConfigFiles()
267+
268+
if configFiles.isEmpty {
269+
print("Error: No configurations found in \(kubeConfigsDir)")
270+
return
271+
}
272+
273+
let targetFilename: String
274+
275+
// Check if input is a number
276+
if let index = Int(filename), index > 0, index <= configFiles.count {
277+
targetFilename = configFiles[index - 1]
278+
print("Selected configuration \(index): \(targetFilename)")
279+
} else {
280+
// Use filename directly
281+
targetFilename = filename
282+
}
283+
284+
let sourcePath = kubeConfigsDir + "/" + targetFilename
270285

271286
// Check if the file exists
272287
if !fileManager.fileExists(atPath: sourcePath) {
273-
print("Error: Configuration file '\(filename)' not found in \(kubeConfigsDir)")
288+
print("Error: Configuration file '\(targetFilename)' not found in \(kubeConfigsDir)")
289+
print("Use 'cf k l' to see available configurations")
274290
return
275291
}
276292

277293
// Validate the config
278294
if !isValidKubeConfig(filePath: sourcePath) {
279-
print("Warning: '\(filename)' may not be a valid Kubernetes configuration file")
295+
print("Warning: '\(targetFilename)' may not be a valid Kubernetes configuration file")
280296
print("Do you want to continue? (y/N): ", terminator: "")
281297
if let response = readLine()?.lowercased(), response != "y" {
282298
print("Operation canceled")
@@ -310,7 +326,7 @@ class KubeConfigManager {
310326
// Move temporary file to active config path
311327
try fileManager.moveItem(atPath: tempPath, toPath: activeConfigPath)
312328

313-
print("Successfully switched active Kubernetes configuration to '\(filename)'")
329+
print("Successfully switched active Kubernetes configuration to '\(targetFilename)'")
314330
}
315331

316332
// Show current Kubernetes configuration
@@ -328,26 +344,26 @@ class KubeConfigManager {
328344
let activeConfigHash = try calculateMD5(filePath: activeConfigPath)
329345

330346
// Try to find matching file
331-
let contents = try fileManager.contentsOfDirectory(atPath: kubeConfigsDir)
332-
let configFiles = contents.filter {
333-
$0.hasSuffix(".yaml") || $0.hasSuffix(".yml") || $0.hasSuffix(".kubeconfig")
334-
}
347+
let configFiles = try getSortedConfigFiles()
335348

336349
var activeFilename = "unknown"
337-
for file in configFiles {
350+
var activeIndex: Int? = nil
351+
352+
for (index, file) in configFiles.enumerated() {
338353
let filePath = kubeConfigsDir + "/" + file
339354
let fileHash = try calculateMD5(filePath: filePath)
340355

341356
if fileHash == activeConfigHash {
342357
activeFilename = file
358+
activeIndex = index + 1
343359
break
344360
}
345361
}
346362

347-
if activeFilename == "unknown" {
348-
print("Current configuration: Unknown (not in configs directory)")
363+
if let index = activeIndex {
364+
print("Current configuration: \(index). \(activeFilename)")
349365
} else {
350-
print("Current configuration: \(activeFilename)")
366+
print("Current configuration: Unknown (not in configs directory)")
351367
}
352368
}
353369
}

CLI/Sources/ConfigForgeCLI/Commands/SSHCommand.swift

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,31 +41,40 @@ struct SSHListCommand: ParsableCommand {
4141

4242
print("Available SSH hosts:")
4343
for (index, entry) in entries.enumerated() {
44+
let hostName = entry.host ?? "unnamed"
45+
let number = index + 1
46+
4447
if detail {
45-
print("\(index + 1). \(entry.host ?? "unnamed")")
48+
print(" \(number). \(hostName)")
4649
if let user = entry.user {
47-
print(" User: \(user)")
50+
print(" User: \(user)")
4851
}
4952
if let hostname = entry.hostname {
50-
print(" Hostname: \(hostname)")
53+
print(" Hostname: \(hostname)")
5154
}
5255
if let port = entry.port {
53-
print(" Port: \(port)")
56+
print(" Port: \(port)")
5457
}
5558
if let identityFile = entry.identityFile {
56-
print(" IdentityFile: \(identityFile)")
59+
print(" IdentityFile: \(identityFile)")
5760
}
5861
if let forwardAgent = entry.forwardAgent {
59-
print(" ForwardAgent: \(forwardAgent)")
62+
print(" ForwardAgent: \(forwardAgent)")
6063
}
6164
if let proxyCommand = entry.proxyCommand {
62-
print(" ProxyCommand: \(proxyCommand)")
65+
print(" ProxyCommand: \(proxyCommand)")
6366
}
6467
print("")
6568
} else {
66-
print("\(index + 1). \(entry.host ?? "unnamed")")
69+
print(" \(number). \(hostName)")
6770
}
6871
}
72+
73+
if !detail {
74+
print("")
75+
print("Use 'cf c <number>' or 'cf c <hostname>' to connect")
76+
print("Use 'cf s <number>' or 'cf s <hostname>' to show details")
77+
}
6978
} catch {
7079
print("Error: \(error.localizedDescription)")
7180
throw ExitCode.failure
@@ -80,7 +89,7 @@ struct SSHShowCommand: ParsableCommand {
8089
aliases: ["s"]
8190
)
8291

83-
@Argument(help: "The SSH host to show details for")
92+
@Argument(help: "The SSH host number or name to show details for")
8493
var host: String
8594

8695
func run() throws {
@@ -89,9 +98,19 @@ struct SSHShowCommand: ParsableCommand {
8998
do {
9099
let entries = try fileManager.getAllHosts()
91100

92-
guard let entry = entries.first(where: { $0.host == host }) else {
93-
print("Error: Host '\(host)' not found in SSH config.")
94-
throw ExitCode.failure
101+
let entry: SSHConfigEntry
102+
103+
// Check if input is a number
104+
if let index = Int(host), index > 0, index <= entries.count {
105+
entry = entries[index - 1]
106+
} else {
107+
// Search by host name
108+
guard let foundEntry = entries.first(where: { $0.host == host }) else {
109+
print("Error: Host '\(host)' not found in SSH config.")
110+
print("Use 'cf l' to see available hosts.")
111+
throw ExitCode.failure
112+
}
113+
entry = foundEntry
95114
}
96115

97116
print("Host: \(entry.host ?? "unnamed")")
@@ -150,7 +169,7 @@ struct SSHConnectCommand: ParsableCommand {
150169
aliases: ["c"]
151170
)
152171

153-
@Argument(help: "The SSH host to connect to")
172+
@Argument(help: "The SSH host number or name to connect to")
154173
var host: String
155174

156175
@Flag(name: .long, help: "Enable debug mode to show detailed connection diagnostics")
@@ -163,13 +182,26 @@ struct SSHConnectCommand: ParsableCommand {
163182
do {
164183
let entries = try fileManager.getAllHosts()
165184

166-
guard let hostEntry = entries.first(where: { $0.host == host }) else {
167-
print("错误: 在SSH配置中找不到主机 '\(host)'")
168-
throw ExitCode.failure
185+
let hostEntry: SSHConfigEntry
186+
let hostDisplayName: String
187+
188+
// Check if input is a number
189+
if let index = Int(host), index > 0, index <= entries.count {
190+
hostEntry = entries[index - 1]
191+
hostDisplayName = "\(index). \(hostEntry.host ?? "unnamed")"
192+
} else {
193+
// Search by host name
194+
guard let foundEntry = entries.first(where: { $0.host == host }) else {
195+
print("Error: Host '\(host)' not found in SSH config.")
196+
print("Use 'cf l' to see available hosts.")
197+
throw ExitCode.failure
198+
}
199+
hostEntry = foundEntry
200+
hostDisplayName = hostEntry.host ?? "unnamed"
169201
}
170202

171203
if isDebugMode {
172-
printDebugInfo(host: host, hostEntry: hostEntry)
204+
printDebugInfo(host: hostDisplayName, hostEntry: hostEntry)
173205
}
174206

175207
let sshCommand = "/usr/bin/ssh"
@@ -197,42 +229,42 @@ struct SSHConnectCommand: ParsableCommand {
197229
if let username = hostEntry.user, let hostname = hostEntry.hostname {
198230
targetAddress = "\(username)@\(hostname)"
199231
} else {
200-
targetAddress = host
232+
targetAddress = hostEntry.host ?? host
201233
}
202234
argsArray.append(targetAddress)
203235

204-
print("正在连接到 \(host)...")
236+
print("Connecting to \(hostDisplayName)...")
205237

206238
if isDebugMode {
207-
print("完整命令: \(sshCommand) \(argsArray.joined(separator: " "))")
239+
print("Full command: \(sshCommand) \(argsArray.joined(separator: " "))")
208240
}
209241

210242
let args: [UnsafeMutablePointer<CChar>?] = [strdup(sshCommand)] + argsArray.map { strdup($0) } + [nil]
211243
execv(sshCommand, args)
212244

213-
print("错误: 无法启动SSH会话")
245+
print("Error: Failed to start SSH session")
214246
throw ExitCode.failure
215247
} catch {
216-
print("错误: \(error.localizedDescription)")
248+
print("Error: \(error.localizedDescription)")
217249
throw ExitCode.failure
218250
}
219251
}
220252

221253
private func printDebugInfo(host: String, hostEntry: SSHConfigEntry) {
222-
print("======= 调试信息 =======")
223-
print("正在查找主机配置: \(host)")
224-
print("当前工作目录: \(FileManager.default.currentDirectoryPath)")
225-
print("系统版本: \(ProcessInfo.processInfo.operatingSystemVersionString)")
254+
print("======= Debug Information =======")
255+
print("Searching for host configuration: \(host)")
256+
print("Current working directory: \(FileManager.default.currentDirectoryPath)")
257+
print("System version: \(ProcessInfo.processInfo.operatingSystemVersionString)")
226258

227-
print("\n===== 主机配置信息 =====")
228-
print("主机: \(hostEntry.host ?? "未命名")")
229-
print("主机名: \(hostEntry.hostname ?? "未指定")")
230-
print("用户: \(hostEntry.user ?? "未指定")")
231-
print("端口: \(hostEntry.port ?? "未指定 (默认: 22)")")
232-
print("身份文件: \(hostEntry.identityFile ?? "未指定")")
259+
print("\n===== Host Configuration Information =====")
260+
print("Host: \(hostEntry.host ?? "unnamed")")
261+
print("Hostname: \(hostEntry.hostname ?? "not specified")")
262+
print("User: \(hostEntry.user ?? "not specified")")
263+
print("Port: \(hostEntry.port ?? "not specified (default: 22)")")
264+
print("IdentityFile: \(hostEntry.identityFile ?? "not specified")")
233265

234-
print("\n===== SSH连接信息 =====")
235-
print("启用SSH最高调试模式 (-vvv)")
266+
print("\n===== SSH Connection Information =====")
267+
print("Enable SSH highest debug mode (-vvv)")
236268
}
237269

238270
private func configureConnectionParameters(argsArray: inout [String], hostEntry: SSHConfigEntry, isDebug: Bool) {

0 commit comments

Comments
 (0)