Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/agent/AgentCommand.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ArgumentParser

struct AgentCommand: ParsableCommand {
struct AgentCommand: ParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

static let configuration = CommandConfiguration(
commandName: "agent",
abstract: "Agent mode utilities.",
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/agent/skills/AgentSkillsCommand.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ArgumentParser

struct AgentSkillsCommand: ParsableCommand {
struct AgentSkillsCommand: ParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

static let configuration = CommandConfiguration(
commandName: "skills",
abstract: "Access portable Agent Skills bundled with apple-docs.",
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/agent/skills/AgentSkillsGetCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import Logging
@preconcurrency import SentrySwift
#endif

struct AgentSkillsGetCommand: ParsableCommand {
struct AgentSkillsGetCommand: ParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

private static let logger = Logger(
label: "com.techprimate.apple-docs.agent-skills-get"
)
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/agent/skills/AgentSkillsInstallCommand.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ArgumentParser

struct AgentSkillsInstallCommand: ParsableCommand {
struct AgentSkillsInstallCommand: ParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

static let configuration = CommandConfiguration(
commandName: "install",
abstract: "Install bundled Agent Skills into a .agents directory.",
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/agent/skills/AgentSkillsListCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import Logging
@preconcurrency import SentrySwift
#endif

struct AgentSkillsListCommand: ParsableCommand {
struct AgentSkillsListCommand: ParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

private static let logger = Logger(
label: "com.techprimate.apple-docs.agent-skills-list"
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ArgumentParser

struct AgentSkillsUninstallCommand: ParsableCommand {
struct AgentSkillsUninstallCommand: ParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

static let configuration = CommandConfiguration(
commandName: "uninstall",
abstract: "Remove skills managed by apple-docs from a .agents directory.",
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/cache/CacheCleanCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import Logging
@preconcurrency import SentrySwift
#endif

struct CacheCleanCommand: ParsableCommand {
struct CacheCleanCommand: ParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

private static let logger = Logger(
label: "com.techprimate.apple-docs.cache-clean"
)
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/cache/CacheCommand.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ArgumentParser

struct CacheCommand: ParsableCommand {
struct CacheCommand: ParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

static let configuration = CommandConfiguration(
commandName: "cache",
abstract: "Manage cached Apple documentation.",
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/technologies/TechnologiesCommand.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ArgumentParser

struct TechnologiesCommand: AsyncParsableCommand {
struct TechnologiesCommand: AsyncParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

static let configuration = CommandConfiguration(
commandName: "technologies",
abstract: "Work with Apple documentation technologies.",
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/technologies/TechnologiesListCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import Logging
@preconcurrency import SentrySwift
#endif

struct TechnologiesListCommand: AsyncParsableCommand {
struct TechnologiesListCommand: AsyncParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

private static let logger = Logger(
label: "com.techprimate.apple-docs.technologies-list"
)
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/types/TypesCommand.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ArgumentParser

struct TypesCommand: AsyncParsableCommand {
struct TypesCommand: AsyncParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

static let configuration = CommandConfiguration(
commandName: "types",
abstract: "Work with Apple documentation types.",
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/types/TypesListCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import Logging
@preconcurrency import SentrySwift
#endif

struct TypesListCommand: AsyncParsableCommand {
struct TypesListCommand: AsyncParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

private static let logger = Logger(
label: "com.techprimate.apple-docs.types-list"
)
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/types/TypesSearchCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import Logging
@preconcurrency import SentrySwift
#endif

struct TypesSearchCommand: AsyncParsableCommand {
struct TypesSearchCommand: AsyncParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

private static let logger = Logger(
label: "com.techprimate.apple-docs.types-search"
)
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/cmd/types/TypesViewCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import Logging
@preconcurrency import SentrySwift
#endif

struct TypesViewCommand: AsyncParsableCommand {
struct TypesViewCommand: AsyncParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

private static let logger = Logger(
label: "com.techprimate.apple-docs.types-view"
)
Expand Down
26 changes: 15 additions & 11 deletions Sources/CLI/main/AppleDocs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Logging

#if canImport(SentrySwift)
@preconcurrency import SentrySwift
import SentrySwiftLog
#endif

@main
Expand All @@ -14,21 +13,36 @@ enum AppleDocs {
@MainActor
static func main() async {
let telemetryEnabled = configureTelemetry()
var loggingConfigured = false

do {
var command = try await CLI.asyncParseAsRoot()
LoggingConfiguration.bootstrap(
verbose: verboseLoggingEnabled(for: command),
telemetryEnabled: telemetryEnabled
)
loggingConfigured = true
Self.logger.debug("CLI command parsed")
if var asyncCommand = command as? any AsyncParsableCommand {
try await asyncCommand.run()
} else {
try command.run()
}
Self.logger.debug("CLI command finished")
finishTelemetry(enabled: telemetryEnabled)
} catch {
if !loggingConfigured {
LoggingConfiguration.bootstrap(verbose: false, telemetryEnabled: telemetryEnabled)
}
captureTelemetry(error, enabled: telemetryEnabled)
CLI.exit(withError: error)
}
}

private static func verboseLoggingEnabled(for command: any ParsableCommand) -> Bool {
(command as? any GlobalOptionsProviding)?.global.verbose ?? false
}

private static func configureTelemetry() -> Bool {
#if canImport(SentrySwift)
let enabled = SentryConfiguration.isEnabled(
Expand All @@ -38,19 +52,9 @@ enum AppleDocs {
SentrySDK.start { options in
SentryConfiguration.configure(options)
}
LoggingSystem.bootstrap { _ in
SentryLogHandler(logLevel: .info)
}
} else {
LoggingSystem.bootstrap { _ in
SwiftLogNoOpLogHandler()
}
}
return enabled
#else
LoggingSystem.bootstrap { _ in
SwiftLogNoOpLogHandler()
}
return false
#endif
}
Expand Down
4 changes: 3 additions & 1 deletion Sources/CLI/main/CLI.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ArgumentParser

struct CLI: AsyncParsableCommand {
struct CLI: AsyncParsableCommand, GlobalOptionsProviding {
@OptionGroup var global: GlobalOptions

static let configuration = CommandConfiguration(
commandName: "apple-docs",
abstract: "Access Apple developer documentation from the command line.",
Expand Down
10 changes: 10 additions & 0 deletions Sources/CLI/main/GlobalOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ArgumentParser

struct GlobalOptions: ParsableArguments {
@Flag(help: "Show debug and higher-level logs on stderr.")
var verbose = false
}

protocol GlobalOptionsProviding {
var global: GlobalOptions { get }
}
40 changes: 40 additions & 0 deletions Sources/CLI/main/LoggingConfiguration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Logging

#if canImport(SentrySwift)
import SentrySwiftLog
#endif

enum LoggingConfiguration {
static func bootstrap(verbose: Bool, telemetryEnabled: Bool) {
LoggingSystem.bootstrap { label in
var telemetry: (any LogHandler)?
#if canImport(SentrySwift)
if telemetryEnabled {
telemetry = SentryLogHandler(logLevel: .info)
}
#endif
return handler(
console: StreamLogHandler.standardError(label: label),
telemetry: telemetry,
verbose: verbose
)
}
}

static func handler(
console: any LogHandler, telemetry: (any LogHandler)?, verbose: Bool
) -> any LogHandler {
var handlers: [any LogHandler] = []
if verbose {
var console = console
console.logLevel = .debug
handlers.append(console)
}
if var telemetry {
telemetry.logLevel = .info
handlers.append(telemetry)
}
guard !handlers.isEmpty else { return SwiftLogNoOpLogHandler() }
return MultiplexLogHandler(handlers)
}
}
6 changes: 5 additions & 1 deletion Tests/CLITests/client/ClientLogRecorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ final class ClientLogRecorder: Sendable {

func logger() -> Logger {
Logger(label: "test.client") { _ in
RecordingHandler(recorder: self)
self.handler()
}
}

func handler() -> any LogHandler {
RecordingHandler(recorder: self)
}

private func append(_ event: LogEvent) {
storage.withLock { $0.append(event) }
}
Expand Down
72 changes: 72 additions & 0 deletions Tests/CLITests/main/CLITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,78 @@ import Testing

@Suite("CLI help")
struct CLITests {
@Test(
"accepts verbose at every command depth",
arguments: [
["--verbose", "agent", "skills", "list"],
["agent", "--verbose", "skills", "list"],
["agent", "skills", "--verbose", "list"],
["agent", "skills", "list", "--verbose"],
])
func acceptsVerboseAtEveryDepth(arguments: [String]) throws {
// -- Arrange --
let expectedType = AgentSkillsListCommand.self

// -- Act --
let command = try CLI.parseAsRoot(arguments)

// -- Assert --
#expect(type(of: command) == expectedType)
let options = try #require(command as? any GlobalOptionsProviding)
#expect(options.global.verbose)
}

@Test(
"accepts verbose on each executable command",
arguments: [
["types", "view", "String", "--technology", "Swift", "--json"],
["types", "list", "--technology", "Swift", "--agent"],
["types", "search", "String", "--technology", "Swift"],
["technologies", "list", "--json"],
["cache", "clean"],
["agent", "skills", "get", "apple-docs"],
["agent", "skills", "list"],
["agent", "skills", "install", "apple-docs", "--dry-run"],
["agent", "skills", "uninstall", "apple-docs", "--dry-run"],
])
func acceptsVerboseOnEachCommand(arguments: [String]) throws {
// -- Arrange --
let arguments = arguments + ["--verbose"]

// -- Act --
let command = try CLI.parseAsRoot(arguments)

// -- Assert --
#expect(type(of: command).configuration.subcommands.isEmpty)
let options = try #require(command as? any GlobalOptionsProviding)
#expect(options.global.verbose)
}

@Test("does not treat positional values after the terminator as flags")
func respectsArgumentTerminator() throws {
// -- Arrange --
let arguments = ["agent", "skills", "get", "--", "--verbose"]

// -- Act --
let command = try #require(CLI.parseAsRoot(arguments) as? AgentSkillsGetCommand)

// -- Assert --
#expect(command.name == "--verbose")
#expect(!command.global.verbose)
}

@Test("leaves verbose disabled by default")
func defaultsToQuietLogging() throws {
// -- Arrange --
let arguments = ["agent", "skills", "list"]

// -- Act --
let command = try #require(CLI.parseAsRoot(arguments) as? AgentSkillsListCommand)

// -- Assert --
#expect(!command.global.verbose)
}

@Test("directs agents to bundled skills")
func includesAgentSkillsHint() {
#expect(
Expand Down
Loading
Loading