From 70896a0009d19886d53149806468d671da1c7836 Mon Sep 17 00:00:00 2001 From: joshp123 Date: Sun, 7 Jun 2026 20:46:03 +0200 Subject: [PATCH] Add CrawlBar dev binary registration --- README.md | 3 + Sources/CrawlBarCLI/CLI.swift | 5 + Sources/CrawlBarCLI/CLIDevCommands.swift | 152 +++++++++++++++++++++++ Sources/CrawlBarCLI/CLIModels.swift | 3 + 4 files changed, 163 insertions(+) create mode 100644 Sources/CrawlBarCLI/CLIDevCommands.swift diff --git a/README.md b/README.md index b95c353..18ab157 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,9 @@ crawlbar action cloud-publish --app discrawl [--json] crawlbar action remote-status --app gitcrawl [--json] crawlbar logs [--json] crawlbar config path|validate|init +crawlbar dev register --app gitcrawl --binary /path/to/gitcrawl [--json] +crawlbar dev unregister --app gitcrawl [--json] +crawlbar dev list [--json] ``` During development the SwiftPM product is `crawlbarctl` to avoid colliding with diff --git a/Sources/CrawlBarCLI/CLI.swift b/Sources/CrawlBarCLI/CLI.swift index 03ae41e..2b10b70 100644 --- a/Sources/CrawlBarCLI/CLI.swift +++ b/Sources/CrawlBarCLI/CLI.swift @@ -54,6 +54,8 @@ enum CrawlBarCLI { try Self.runAction(action, registry: registry, runner: runner, json: options.json, appID: options.requiredAppID()) case "config": try Self.runConfig(options, registry: registry) + case "dev": + try Self.runDev(options, registry: registry) case "help", "--help", "-h": Self.printHelp() default: @@ -347,6 +349,9 @@ enum CrawlBarCLI { config path|validate|init config get --app [--key ] [--json] [--reveal] config set --app --key --value [--json] + dev register --app --binary [--json] + dev unregister --app [--json] + dev list [--json] """) } } diff --git a/Sources/CrawlBarCLI/CLIDevCommands.swift b/Sources/CrawlBarCLI/CLIDevCommands.swift new file mode 100644 index 0000000..b457222 --- /dev/null +++ b/Sources/CrawlBarCLI/CLIDevCommands.swift @@ -0,0 +1,152 @@ +import CrawlBarCore +import Foundation + +extension CrawlBarCLI { + static func runDev(_ options: CLIOptions, registry: CrawlAppRegistry) throws { + switch options.positionals.first { + case "register": + try Self.registerDevBinary(options, registry: registry) + case "unregister": + try Self.unregisterDevBinary(options, registry: registry) + case "list": + try Self.listDevBinaries(options, registry: registry) + case let command?: + throw CLIError.usage("unknown dev command: \(command)") + case nil: + throw CLIError.usage("dev requires register, unregister, or list") + } + } + + private static func registerDevBinary(_ options: CLIOptions, registry: CrawlAppRegistry) throws { + let appID = try options.requiredAppID() + let binary = try Self.requiredDevBinaryPath(options.binary) + let store = CrawlBarConfigStore() + var config = try store.loadOrCreateDefault() + + guard try registry.installation(for: appID) != nil else { + throw CLIError.usage("unknown app: \(appID.rawValue)") + } + + let index = Self.ensureAppConfig(appID: appID, in: &config) + config.apps[index].binaryPath = binary + config.apps[index].enabled = true + config.apps[index].showInMenuBar = true + try store.save(config) + + try Self.printDevBinaryUpdate( + appID: appID, + binaryPath: binary, + registry: CrawlAppRegistry(), + json: options.json) + } + + private static func unregisterDevBinary(_ options: CLIOptions, registry: CrawlAppRegistry) throws { + let appID = try options.requiredAppID() + let store = CrawlBarConfigStore() + var config = try store.loadOrCreateDefault() + + guard try registry.installation(for: appID) != nil else { + throw CLIError.usage("unknown app: \(appID.rawValue)") + } + + let index = Self.ensureAppConfig(appID: appID, in: &config) + config.apps[index].binaryPath = nil + try store.save(config) + + try Self.printDevBinaryUpdate( + appID: appID, + binaryPath: nil, + registry: CrawlAppRegistry(), + json: options.json) + } + + private static func listDevBinaries(_ options: CLIOptions, registry: CrawlAppRegistry) throws { + let config = try registry.loadConfig() + let installationsByID = Dictionary(uniqueKeysWithValues: try registry + .installations(includeDisabled: true) + .map { ($0.id, $0) }) + let rows = config.apps.compactMap { appConfig -> CLIDevBinary? in + guard let configuredPath = appConfig.binaryPath?.nilIfBlank else { return nil } + let installation = installationsByID[appConfig.id] + return CLIDevBinary( + appID: appConfig.id.rawValue, + displayName: installation?.manifest.displayName ?? appConfig.id.rawValue, + configuredBinaryPath: configuredPath, + resolvedBinaryPath: installation?.binaryPath) + } + + if options.json { + try CLIOutput.writeJSON(rows) + return + } + if rows.isEmpty { + print("no dev binaries registered") + return + } + for row in rows { + let configured = row.configuredBinaryPath ?? "unset" + let resolved = row.resolvedBinaryPath ?? "missing" + print("\(row.appID)\t\(configured)\t\(resolved)") + } + } + + private static func requiredDevBinaryPath(_ value: String?) throws -> String { + guard let value = value?.nilIfBlank else { + throw CLIError.usage("dev register requires --binary ") + } + let expanded = PathExpander.expandHome(value) + guard expanded.hasPrefix("/") else { + throw CLIError.usage("--binary must be an absolute path or ~/ path") + } + guard FileManager.default.isExecutableFile(atPath: expanded) else { + throw CLIError.usage("--binary is not executable: \(expanded)") + } + return value + } + + private static func ensureAppConfig(appID: CrawlAppID, in config: inout CrawlBarConfig) -> Int { + if let index = config.apps.firstIndex(where: { $0.id == appID }) { + return index + } + config.apps.append(CrawlBarAppConfig(id: appID)) + return config.apps.index(before: config.apps.endIndex) + } + + private static func printDevBinaryUpdate( + appID: CrawlAppID, + binaryPath: String?, + registry: CrawlAppRegistry, + json: Bool) + throws + { + let installation = try registry.installation(for: appID) + let output = CLIDevBinary( + appID: appID.rawValue, + displayName: installation?.manifest.displayName ?? appID.rawValue, + configuredBinaryPath: binaryPath, + resolvedBinaryPath: installation?.binaryPath) + if json { + try CLIOutput.writeJSON(output) + return + } + if let binaryPath { + print("registered\t\(appID.rawValue)\t\(binaryPath)") + } else { + print("unregistered\t\(appID.rawValue)") + } + } +} + +struct CLIDevBinary: Encodable { + var appID: String + var displayName: String + var configuredBinaryPath: String? + var resolvedBinaryPath: String? + + private enum CodingKeys: String, CodingKey { + case appID = "app_id" + case displayName = "display_name" + case configuredBinaryPath = "configured_binary_path" + case resolvedBinaryPath = "resolved_binary_path" + } +} diff --git a/Sources/CrawlBarCLI/CLIModels.swift b/Sources/CrawlBarCLI/CLIModels.swift index 3f35a00..583348b 100644 --- a/Sources/CrawlBarCLI/CLIModels.swift +++ b/Sources/CrawlBarCLI/CLIModels.swift @@ -52,6 +52,7 @@ struct CLIApp: Encodable { struct CLIOptions { var json = false var appID: CrawlAppID? + var binary: String? var key: String? var value: String? var revealSecrets = false @@ -68,6 +69,8 @@ struct CLIOptions { if let value = iterator.next() { self.appID = CrawlAppID(rawValue: value) } + case "--binary": + self.binary = iterator.next() case "--key": self.key = iterator.next() case "--value":