# Writing Your First CocoaPacketAnalyzer Plugin

Build a complete `.cpaplugin` bundle from scratch in under fifteen minutes.

## Overview

This walkthrough builds a working analyzer for a fictional `EXAMPLE` protocol on TCP/9999. The example is intentionally trivial so you can focus on the wiring — the same steps produce a real RFC-compliant analyzer.

The companion `NTPAnalyzer` example under `SamplePlugIn/NTPAnalyzer-XCF/` is a complete RFC 5905 implementation you can copy and modify.

## Step 1 — Create the project

In Xcode, create a new **macOS Bundle** target. In project settings:

| Setting | Value |
|---------|-------|
| Bundle extension (`WRAPPER_EXTENSION`) | `cpaplugin` |
| Deployment target | macOS 26.0 |
| Code-signing | match the host app or "Sign to Run Locally" |
| `LD_RUNPATH_SEARCH_PATHS` | `@executable_path/../Frameworks @loader_path/../Frameworks` |
| `INSTALL_PATH` | `$(LOCAL_LIBRARY_DIR)/Bundles` |

Add `CPAPluginSDK` as a Swift Package dependency (path-reference `Packages/cpa_plugin_sdk` if you cloned the CPA repo, or the GitHub URL once published). Link the framework but **do not embed it** — the host app provides it at runtime.

## Step 2 — Write the principal class

```swift
import CPAPluginSDK
import AppKit
import SwiftUI

@MainActor
final class ExamplePlugInController: NSObject, ObservableObject,
    CPAPluginPrincipalClassProtocol,
    CPAPluginPreferencesProtocol {

    required convenience init?(plugInsController: Any) { self.init() }
    required convenience init?(plugInManager: Any) { self.init() }

    override required convenience init() {
        self.init(controller: CPAPlugInsController.sharedManager)!
    }

    init?(controller: CPAPlugInsController) {
        super.init()
        registerWithHost(controller)
    }

    static func pluginTitle() -> String? { "Example Analyzer" }
    static func pluginDescription() -> String? { "Demo plugin for EXAMPLE/9999" }
    static func pluginVersion() -> String? { "1.0.0" }
    static func pluginAuthor() -> String? { "Your Name" }

    func usesColor() -> Bool { false }
    func protocolColor() -> NSColor { .systemTeal }
    func settingsView(_ size: Binding<CGSize>) -> AnyView {
        AnyView(Text("EXAMPLE has no settings"))
    }
    func preferencesSheet() -> NSWindow? { nil }

    private func registerWithHost(_ controller: CPAPlugInsController) {
        let bundle = Bundle(for: type(of: self))

        let columns = [
            CPAPlugInColumn.column(
                title: "EXAMPLE Size",
                keyPath: "exampleSize",
                predicateDisplayName: "EXAMPLE Size contains",
                predicateFormat: "exampleSize.description CONTAINS $value",
                attrType: NSString.self
            )
        ]

        let attributes = [
            CPAPlugInAnalyzerAttribute.attribute(
                class: ExampleProtocolAnalyzer.self,
                title: "EXAMPLE Size",
                key: "exampleSize",
                type: NSNumber.self
            )
        ]

        let plugin = CPAPlugInStructure.register(
            title: type(of: self).pluginTitle() ?? "Example",
            description: type(of: self).pluginDescription() ?? "",
            version: type(of: self).pluginVersion() ?? "0",
            author: type(of: self).pluginAuthor() ?? "",
            principalClass: type(of: self),
            preferences: self,
            analyzerClass: ExampleProtocolAnalyzer.self,
            attributes: attributes,
            targets: [.tcp(name: "EXAMPLE", ports: [9999])],
            columns: columns
        )

        _ = controller.register(CPAPlugIn: plugin, forBundle: bundle)
    }
}
```

## Step 3 — Write the analyzer

```swift
import CPAPluginSDK
import Foundation

final class ExampleProtocolAnalyzer: NSObject, CPAPluginAnalyzerProtocol, @unchecked Sendable {
    private let payloadSize: UInt16

    @objc dynamic var exampleSize: NSNumber { NSNumber(value: payloadSize) }

    required init?(data: Data, packet: Any?) {
        guard data.count >= 1 else { return nil }
        payloadSize = UInt16(clamping: data.count)
        super.init()
    }

    func protocolSize() -> UInt16 { payloadSize }
    func protocolName() -> String? { nil }
    func protocolInfo() -> String? { "" }
    func protocolInfo(_ data: Data) -> String? { "EXAMPLE/\(data.count)B" }
    func nextProtocol() -> String? { nil }
    func hasPayload() -> Bool { false }
    func protocolNodes() -> [Any]? { [] }

    func protocolNodes(_ data: Data) -> [Any] {
        let root = CPANode.nodeWithName("EXAMPLE-Header")
        root.headerLength = protocolSize()
        root.headerLocation = 0
        root.addChild(CPANode.nodeWithName("Length", value: "\(data.count) bytes"))
        return [root]
    }

    func value(_ attributeKey: String, data: Data) -> Any? {
        switch attributeKey {
        case "exampleSize":
            return exampleSize
        default:
            return nil
        }
    }
}
```

## Step 4 — Wire `Info.plist`

```xml
<key>NSPrincipalClass</key>
<string>ExampleAnalyzer.ExamplePlugInController</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
```

The `NSPrincipalClass` value must be **`<ModuleName>.<ClassName>`**.

## Step 5 — Install and verify

```bash
xcodebuild -project ExampleAnalyzer.xcodeproj -configuration Debug
mkdir -p ~/Library/Application\ Support/CocoaPacketAnalyzer/PlugIns/
cp -R build/Debug/ExampleAnalyzer.cpaplugin \
      ~/Library/Application\ Support/CocoaPacketAnalyzer/PlugIns/
open -a CocoaPacketAnalyzer
```

The plugin appears in CPA's preferences under **PlugIns**, and packets on TCP/9999 are analyzed using your code.
