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
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ apple-docs types view URLSession.AsyncBytes --technology Foundation
apple-docs types view URLSession/AsyncBytes --technology Foundation
```

### Agent output

Use `--agent` on documentation commands for Markdown with explicit technology, paths, links, and safely quoted follow-up commands:

```bash
apple-docs types view String --technology Swift --agent
apple-docs types search Button --technology SwiftUI --agent
apple-docs technologies list --agent
```

Agent output uses the same normalized documentation content as human text. Follow-up commands are included only for destinations the CLI can represent safely. All commands remain one-shot, even when run in a terminal.

`--agent` no longer aliases `--json`. Existing scripts that require JSON should use `--json`, which takes precedence if both flags are supplied.

### JSON output

The documentation commands accept `--json`, but their output contracts differ:
Expand Down
11 changes: 3 additions & 8 deletions Sources/CLI/cmd/technologies/TechnologiesListCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,23 @@ import ArgumentParser

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

static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List Apple documentation technologies."
)

@Flag(
name: [.long, .customLong("agent")],
help: "Output a JSON array of technologies. --agent currently aliases --json."
)
var json = false

mutating func run() async throws {
try await run(telemetry: Dependencies.telemetry)
}

func run(telemetry: Telemetry) async throws {
let context = TelemetryCommandContext.technologiesList(json: json)
let context = TelemetryCommandContext.technologiesList(json: output.json)
telemetry.startCommand(context)
let result = try await TechnologiesListCommandRunner(
client: Dependencies.documentationClient,
renderer: Dependencies.technologyListRenderer(json: json)
renderer: Dependencies.technologyListRenderer(output: output)
).run()
telemetry.record(.technologyCatalog(count: result.technologyCount), context: context)
print(result.output)
Expand Down
11 changes: 3 additions & 8 deletions Sources/CLI/cmd/types/TypesListCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ArgumentParser

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

static let configuration = CommandConfiguration(
commandName: "list",
Expand All @@ -11,22 +12,16 @@ struct TypesListCommand: AsyncParsableCommand, GlobalOptionsProviding {
@Option(help: "The framework or technology whose types to list.")
var technology: String

@Flag(
name: [.long, .customLong("agent")],
help: "Output a JSON array of types. --agent currently aliases --json."
)
var json = false

mutating func run() async throws {
try await run(telemetry: Dependencies.telemetry)
}

func run(telemetry: Telemetry) async throws {
let context = TelemetryCommandContext.typesList(technology: technology, json: json)
let context = TelemetryCommandContext.typesList(technology: technology, json: output.json)
telemetry.startCommand(context)
let result = try await TypesListCommandRunner(
client: Dependencies.documentationClient,
renderer: Dependencies.documentationTypeListRenderer(json: json)
renderer: Dependencies.documentationTypeListRenderer(output: output, technology: technology)
).run(technology: technology)
telemetry.record(.typeCatalog(count: result.typeCount), context: context)
print(result.output)
Expand Down
11 changes: 3 additions & 8 deletions Sources/CLI/cmd/types/TypesSearchCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Foundation

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

static let configuration = CommandConfiguration(
commandName: "search",
Expand All @@ -15,23 +16,17 @@ struct TypesSearchCommand: AsyncParsableCommand, GlobalOptionsProviding {
@Option(help: "The framework or technology whose types to search.")
var technology: String

@Flag(
name: [.long, .customLong("agent")],
help: "Output a JSON array of matching types. --agent currently aliases --json."
)
var json = false

mutating func run() async throws {
try await run(telemetry: Dependencies.telemetry)
}

func run(telemetry: Telemetry) async throws {
// Search text can be user-authored, so it is deliberately excluded from telemetry context.
let context = TelemetryCommandContext.typesSearch(technology: technology, json: json)
let context = TelemetryCommandContext.typesSearch(technology: technology, json: output.json)
telemetry.startCommand(context)
let result = try await TypesSearchCommandRunner(
client: Dependencies.documentationClient,
renderer: Dependencies.documentationTypeListRenderer(json: json)
renderer: Dependencies.documentationTypeListRenderer(output: output, technology: technology)
).run(query: query, technology: technology)
telemetry.record(.typeSearch(matches: result.matchCount), context: context)
if result.unavailableCollectionCount > 0 {
Expand Down
11 changes: 3 additions & 8 deletions Sources/CLI/cmd/types/TypesViewCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ArgumentParser

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

static let configuration = CommandConfiguration(
commandName: "view",
Expand All @@ -14,22 +15,16 @@ struct TypesViewCommand: AsyncParsableCommand, GlobalOptionsProviding {
@Option(help: "The framework or technology containing the type.")
var technology: String

@Flag(
name: [.long, .customLong("agent")],
help: "Output the raw Apple DocC JSON document. --agent currently aliases --json."
)
var json = false

mutating func run() async throws {
try await run(telemetry: Dependencies.telemetry)
}

func run(telemetry: Telemetry) async throws {
let context = TelemetryCommandContext.typesView(name: name, technology: technology, json: json)
let context = TelemetryCommandContext.typesView(name: name, technology: technology, json: output.json)
telemetry.startCommand(context)
let result = try await TypesViewCommandRunner(
client: Dependencies.documentationClient,
renderer: Dependencies.documentationRenderer(json: json)
renderer: Dependencies.documentationRenderer(output: output)
).run(name: name, technology: technology)
telemetry.record(.typeView(responseBytes: result.responseByteCount), context: context)
print(result.output)
Expand Down
21 changes: 6 additions & 15 deletions Sources/CLI/main/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,27 +61,18 @@ enum Dependencies {
AgentSkillInstaller(logger: Logger(label: "com.techprimate.apple-docs.skills.installer"))
}

static func documentationRenderer(
json: Bool
) -> DefaultTypeDocumentationRenderer {
DefaultTypeDocumentationRenderer(
output: json ? .json : .text
)
static func documentationRenderer(output: OutputOptions) -> DefaultTypeDocumentationRenderer {
DefaultTypeDocumentationRenderer(output: output.format, audience: output.audience)
}

static func documentationTypeListRenderer(
json: Bool
output: OutputOptions, technology: String
) -> DefaultDocumentationTypeListRenderer {
DefaultDocumentationTypeListRenderer(
output: json ? .json : .table
)
output: output.json ? .json : .table, audience: output.audience, technology: technology)
}

static func technologyListRenderer(
json: Bool
) -> DefaultTechnologyListRenderer {
DefaultTechnologyListRenderer(
output: json ? .json : .table
)
static func technologyListRenderer(output: OutputOptions) -> DefaultTechnologyListRenderer {
DefaultTechnologyListRenderer(output: output.json ? .json : .table, audience: output.audience)
}
}
20 changes: 20 additions & 0 deletions Sources/CLI/main/OutputOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import ArgumentParser

enum OutputAudience: String, Codable, Equatable, Sendable {
case human, agent
}

enum OutputFormat: Equatable, Sendable {
case text, json
}

struct OutputOptions: ParsableArguments {
@Flag(help: "Print JSON. Page output preserves Apple's raw DocC document, even with --agent.")
var json = false

@Flag(help: "Print agent-oriented Markdown with follow-up commands. --json takes precedence.")
var agent = false

var audience: OutputAudience { agent && !json ? .agent : .human }
var format: OutputFormat { json ? .json : .text }
}
84 changes: 84 additions & 0 deletions Sources/CLI/renderer/AgentDocumentationRenderer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Foundation

struct AgentDocumentationRenderer: Sendable {
private let content = DocumentationContentRenderer(audience: .agent)

func render(_ presentation: PagePresentation) -> String {
let page = presentation.document
var sections = [
"# " + content.markdown(page.title),
"Technology: " + content.inlineCode(page.destination.technology)
+ "\nPath: " + content.inlineCode(page.destination.path)
+ "\nKind: " + content.markdown(page.kind)
+ "\nURL: " + page.url.absoluteString,
]
if !page.modules.isEmpty {
sections.append("Modules: " + page.modules.map(content.markdown).joined(separator: ", "))
}
if let navigation = presentation.navigation { sections.append(command(navigation)) }
append("Summary", content.inline(page.abstract), to: &sections)
append("Deprecated", content.blocks(page.deprecation), to: &sections)
append(
"Declaration",
page.declarations.map {
content.code($0.text.components(separatedBy: "\n"), language: $0.languages.first)
}.joined(separator: "\n\n"), to: &sections)
append(
"Availability",
page.availability.map {
"- " + content.markdown($0.name) + ": " + content.availability($0)
}.joined(separator: "\n"), to: &sections)
append("Content", content.blocks(page.content), to: &sections)
append("Relationships", groups(presentation.relationships), to: &sections)
append("Topics", groups(presentation.topics), to: &sections)
append("See Also", groups(presentation.seeAlso), to: &sections)
return terminalSafeText(sections.joined(separator: "\n\n"))
}

func render(_ symbols: [SymbolPresentation], technology: String) -> String {
let header = "# Symbols\n\nTechnology: " + content.inlineCode(technology)
let entries = symbols.map { symbol in
var parts = [
"## " + content.markdown(symbol.name), "Kind: " + content.markdown(symbol.kind),
"Path: " + content.inlineCode(symbol.path), "URL: " + symbol.url,
]
if let navigation = symbol.navigation { parts.append(command(navigation)) }
return parts.joined(separator: "\n\n")
}
return terminalSafeText(
([header] + (entries.isEmpty ? ["No symbols found."] : entries)).joined(separator: "\n\n"))
}

func render(_ technologies: [TechnologyPresentation]) -> String {
let entries = technologies.map { technology in
var parts = [
"## " + content.markdown(technology.name), "Identifier: " + content.inlineCode(technology.identifier),
]
if let navigation = technology.navigation { parts.append(command(navigation)) }
return parts.joined(separator: "\n\n")
}
return terminalSafeText((["# Technologies"] + entries).joined(separator: "\n\n"))
}

private func append(_ title: String, _ body: String, to sections: inout [String]) {
if !body.isEmpty { sections.append("## " + title + "\n\n" + body) }
}

private func groups(_ groups: [GroupPresentation]) -> String {
groups.map { group in
let entries = group.references.map { item in
let reference = item.reference
var parts = [content.link(content.markdown(reference.title), target: reference.target)]
let abstract = content.inline(reference.abstract)
if !abstract.isEmpty { parts.append(abstract) }
if let navigation = item.navigation { parts.append(command(navigation)) }
return parts.joined(separator: "\n\n")
}
return (["### " + content.markdown(group.title)] + entries).joined(separator: "\n\n")
}.joined(separator: "\n\n")
}

private func command(_ navigation: AgentNavigation) -> String {
content.code([navigation.command], language: "sh")
}
}
12 changes: 10 additions & 2 deletions Sources/CLI/renderer/DefaultDocumentationTypeListRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ struct DefaultDocumentationTypeListRenderer: Sendable {
}

private let output: Output
private let audience: OutputAudience
private let technology: String

init(output: Output) {
init(output: Output, audience: OutputAudience = .human, technology: String = "") {
self.output = output
self.audience = audience
self.technology = technology
}

func render(_ types: [DocumentationType]) throws -> String {
switch output {
case .table:
return renderTable(types)
if audience == .agent {
let presentation = DocumentationPresenter().symbols(types, technology: technology, audience: .agent)
return AgentDocumentationRenderer().render(presentation, technology: technology)
}
return terminalSafeText(renderTable(types))
case .json:
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
Expand Down
10 changes: 8 additions & 2 deletions Sources/CLI/renderer/DefaultTechnologyListRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,21 @@ struct DefaultTechnologyListRenderer: Sendable {
}

private let output: Output
private let audience: OutputAudience

init(output: Output) {
init(output: Output, audience: OutputAudience = .human) {
self.output = output
self.audience = audience
}

func render(_ technologies: [Technology]) throws -> String {
switch output {
case .table:
return renderTable(technologies)
if audience == .agent {
let presentation = DocumentationPresenter().technologies(technologies, audience: .agent)
return AgentDocumentationRenderer().render(presentation)
}
return terminalSafeText(renderTable(technologies))
case .json:
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
Expand Down
23 changes: 9 additions & 14 deletions Sources/CLI/renderer/DefaultTypeDocumentationRenderer.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
struct DefaultTypeDocumentationRenderer: Sendable {
enum Output: Sendable {
case text
case json
}

private let output: Output
typealias Output = OutputFormat

init(output: Output) {
self.output = output
}
let output: Output
var audience: OutputAudience = .human

func render(_ document: TypeDocumentationDocument) throws -> String {
switch output {
case .text:
let page = try DocumentationPageDecoder().decode(document.data, destination: document.destination)
return TextTypeDocumentationRenderer().render(page)
case .json:
if output == .json {
return RawJSONTypeDocumentationRenderer().render(document)
}
let page = try DocumentationPageDecoder().decode(document.data, destination: document.destination)
let presentation = DocumentationPresenter().page(page, audience: audience)
return audience == .agent
? AgentDocumentationRenderer().render(presentation)
: TextTypeDocumentationRenderer().render(page)
}
}
Loading
Loading