From 2b1b4fd53d37dd2a4826d7cbfe0e9ee93a99d68b Mon Sep 17 00:00:00 2001 From: Thomas Hartwig Date: Thu, 23 Apr 2026 14:50:16 +0200 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20feat(completion):=20auto-dete?= =?UTF-8?q?ct=20shell=20and=20install=20when=20no=20arg=20given?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit matter completion with no argument now reads $SHELL, maps it to a supported shell (bash/zsh/fish), and installs completions automatically. On Windows it defaults to powershell. When detection fails (unset $SHELL or unsupported shell like nushell) the error names the valid values explicitly instead of printing cobra's generic usage error. Closes #58 --- cli/completion.go | 46 +++++++++++++++++++---- cli/completion_test.go | 84 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 10 deletions(-) diff --git a/cli/completion.go b/cli/completion.go index d22dc7b..f6f9ef6 100644 --- a/cli/completion.go +++ b/cli/completion.go @@ -26,12 +26,15 @@ func newCompletionCmd() *cobra.Command { Short: "Generate or install shell completion scripts", Long: `Generate shell completion scripts for matter. -By default, the completion script is printed to stdout so you can redirect it -to a file or pipe it to your shell. +With no arguments, matter completion auto-detects your shell from $SHELL and +installs the completion script automatically. -Use --install to automatically write the script to the correct location for -your shell and configure it for use.`, - Example: ` # Print zsh completions to stdout +Specify a shell explicitly to print its completion script to stdout. Add +--install to install explicitly for a given shell.`, + Example: ` # Auto-detect shell and install completions (recommended) + matter completion + + # Print zsh completions to stdout matter completion zsh # Install zsh completions (writes file + configures shell) @@ -43,7 +46,7 @@ your shell and configure it for use.`, # Install fish completions matter completion fish --install`, ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Args: cobra.MatchAll(cobra.MaximumNArgs(1), cobra.OnlyValidArgs), RunE: runCompletion, } @@ -53,16 +56,43 @@ your shell and configure it for use.`, } func runCompletion(cmd *cobra.Command, args []string) error { - shell := args[0] install, _ := cmd.Flags().GetBool("install") + if len(args) == 0 { + shell, err := detectShell() + if err != nil { + return err + } + return installCompletion(cmd, shell) + } + + shell := args[0] if !install { return generateCompletion(cmd, shell) } - return installCompletion(cmd, shell) } +// detectShell returns the name of the current user's shell by inspecting $SHELL. +// On Windows it defaults to "powershell". Returns an error when the shell +// cannot be detected or is not among the supported set. +func detectShell() (string, error) { + if runtime.GOOS == "windows" { + return "powershell", nil + } + shellEnv := os.Getenv("SHELL") + if shellEnv == "" { + return "", fmt.Errorf("could not detect your shell — please specify one: bash, zsh, fish, powershell") + } + name := filepath.Base(shellEnv) + switch name { + case "bash", "zsh", "fish": + return name, nil + default: + return "", fmt.Errorf("unsupported shell %q — please specify one: bash, zsh, fish, powershell", name) + } +} + // generateCompletion prints the completion script to stdout. func generateCompletion(cmd *cobra.Command, shell string) error { root := cmd.Root() diff --git a/cli/completion_test.go b/cli/completion_test.go index 2081720..c68bc87 100644 --- a/cli/completion_test.go +++ b/cli/completion_test.go @@ -85,15 +85,95 @@ func TestCompletionInvalidShell(t *testing.T) { assert.Error(t, err) } -func TestCompletionNoArgs(t *testing.T) { +func TestCompletionNoArgs_UnsetShell(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("windows always falls back to powershell") + } + t.Setenv("SHELL", "") + root, _ := newTestRootWithCompletion() + root.SetOut(&bytes.Buffer{}) + root.SetErr(&bytes.Buffer{}) + root.SetArgs([]string{"completion"}) + err := root.Execute() + require.Error(t, err) + assert.Contains(t, err.Error(), "could not detect") +} + +func TestCompletionNoArgs_AutoDetect(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("windows always falls back to powershell") + } + shells := []string{"bash", "zsh", "fish"} + for _, shell := range shells { + t.Run(shell, func(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("XDG_CONFIG_HOME", "") + t.Setenv("SHELL", "/bin/"+shell) + + root, _ := newTestRootWithCompletion() + var stdout, stderr bytes.Buffer + root.SetOut(&stdout) + root.SetErr(&stderr) + root.SetArgs([]string{"completion"}) + + err := root.Execute() + require.NoError(t, err) + assert.Contains(t, stderr.String(), "✓") + }) + } +} + +func TestCompletionNoArgs_UnsupportedShell(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("windows always falls back to powershell") + } + t.Setenv("SHELL", "/bin/nushell") + + root, _ := newTestRootWithCompletion() root.SetOut(&bytes.Buffer{}) root.SetErr(&bytes.Buffer{}) root.SetArgs([]string{"completion"}) err := root.Execute() - assert.Error(t, err) + require.Error(t, err) + assert.Contains(t, err.Error(), "nushell") +} + +func TestDetectShell(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("windows detection is separate") + } + tests := []struct { + shellEnv string + wantShell string + wantErr bool + errContains string + }{ + {"/bin/bash", "bash", false, ""}, + {"/usr/local/bin/zsh", "zsh", false, ""}, + {"/usr/bin/fish", "fish", false, ""}, + {"", "", true, "could not detect"}, + {"/bin/nushell", "", true, "nushell"}, + {"/bin/dash", "", true, "dash"}, + } + for _, tt := range tests { + t.Run(tt.shellEnv, func(t *testing.T) { + t.Setenv("SHELL", tt.shellEnv) + got, err := detectShell() + if tt.wantErr { + require.Error(t, err) + if tt.errContains != "" { + assert.Contains(t, err.Error(), tt.errContains) + } + } else { + require.NoError(t, err) + assert.Equal(t, tt.wantShell, got) + } + }) + } } func TestCompletionInstallPath_Zsh(t *testing.T) { From 14764d60c37c00d8e62ef20113f0854c0a5050e5 Mon Sep 17 00:00:00 2001 From: Thomas Hartwig Date: Thu, 23 Apr 2026 14:55:21 +0200 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=8E=A8=20style:=20run=20gofmt=20acr?= =?UTF-8?q?oss=20all=20unformatted=20files=20to=20fix=20CI=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cli/code.go | 4 +-- cli/commission_window.go | 8 +++--- cli/decommission.go | 1 - cli/output/color.go | 14 +++++----- cli/output/tree_d2_test.go | 4 +-- cli/session.go | 4 +-- cli/target_test.go | 16 +++++------ internal/codegen/parser.go | 38 +++++++++++++------------- internal/commissioning/flow.go | 4 +-- internal/commissioning/network.go | 2 +- internal/commissioning/network_test.go | 14 +++++----- internal/controller/controller.go | 10 +++---- internal/controller/controller_test.go | 10 +++---- internal/crypto/mattercert.go | 32 +++++++++++----------- internal/crypto/mattercert_test.go | 4 +-- internal/daemon/protocol.go | 6 ++-- internal/interaction/invoke.go | 4 +-- internal/protocol/message_test.go | 24 ++++++++-------- internal/secure/pase.go | 6 ++-- internal/secure/pase_test.go | 2 +- internal/store/memory.go | 12 ++++---- internal/store/store_test.go | 6 ++-- internal/store/types.go | 6 ++-- internal/transport/ble.go | 4 +-- internal/transport/ble_scanner_test.go | 30 ++++++++++---------- internal/transport/btp.go | 2 +- internal/transport/btp_test.go | 10 +++---- internal/transport/mrp.go | 6 ++-- pkg/matter/matter_test.go | 6 ++-- 29 files changed, 144 insertions(+), 145 deletions(-) diff --git a/cli/code.go b/cli/code.go index 6f72df3..d74dea1 100644 --- a/cli/code.go +++ b/cli/code.go @@ -148,8 +148,8 @@ func looksLikeQRPayload(s string) bool { func newCodeGenerateCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "generate", - Short: "Generate a QR code and manual pairing code from parameters", + Use: "generate", + Short: "Generate a QR code and manual pairing code from parameters", Example: ` matter code generate --vid 0xFFF1 --pid 0x8000 --passcode 12345678 --discriminator 3840`, RunE: func(cmd *cobra.Command, args []string) error { vid, _ := cmd.Flags().GetUint16("vid") diff --git a/cli/commission_window.go b/cli/commission_window.go index 6898d5b..b1154bf 100644 --- a/cli/commission_window.go +++ b/cli/commission_window.go @@ -18,8 +18,8 @@ import ( "github.com/p0fi/matter-cli/internal/daemon" "github.com/p0fi/matter-cli/internal/interaction" "github.com/p0fi/matter-cli/internal/protocol" - "github.com/p0fi/matter-cli/internal/tlv" "github.com/p0fi/matter-cli/internal/store" + "github.com/p0fi/matter-cli/internal/tlv" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -32,9 +32,9 @@ const timedInteractionTimeoutMs uint16 = 5_000 // Administrator Commissioning cluster-specific status codes (Matter spec §11.19.6). const ( - adminBusy uint8 = 0x02 - adminPAKEParamError uint8 = 0x03 - adminWindowNotOpen uint8 = 0x04 + adminBusy uint8 = 0x02 + adminPAKEParamError uint8 = 0x03 + adminWindowNotOpen uint8 = 0x04 ) // newCommissionOpenWindowCmd creates the `commission open-window` subcommand. diff --git a/cli/decommission.go b/cli/decommission.go index ef602e7..4bf2919 100644 --- a/cli/decommission.go +++ b/cli/decommission.go @@ -385,4 +385,3 @@ func confirmForceDelete(cmd *cobra.Command, stepper *output.Stepper, force bool, answer := strings.TrimSpace(strings.ToLower(scanner.Text())) return answer == "y" || answer == "yes" } - diff --git a/cli/output/color.go b/cli/output/color.go index f432347..320d015 100644 --- a/cli/output/color.go +++ b/cli/output/color.go @@ -30,14 +30,14 @@ import ( var ( // Core colors — ANSI numbers, resolved by the terminal's own palette. - colorGreen = lipgloss.ANSIColor(10) // Bright Green → success - colorRed = lipgloss.ANSIColor(9) // Bright Red → errors - colorYellow = lipgloss.ANSIColor(3) // Yellow → warnings - colorBlue = lipgloss.ANSIColor(12) // Bright Blue → info / headers - colorCyan = lipgloss.ANSIColor(14) // Bright Cyan → labels / commands - colorMagenta = lipgloss.ANSIColor(13) // Bright Magenta→ accents / IDs + colorGreen = lipgloss.ANSIColor(10) // Bright Green → success + colorRed = lipgloss.ANSIColor(9) // Bright Red → errors + colorYellow = lipgloss.ANSIColor(3) // Yellow → warnings + colorBlue = lipgloss.ANSIColor(12) // Bright Blue → info / headers + colorCyan = lipgloss.ANSIColor(14) // Bright Cyan → labels / commands + colorMagenta = lipgloss.ANSIColor(13) // Bright Magenta→ accents / IDs colorLightGray = lipgloss.ANSIColor(7) // Light Gray → values - colorGray = lipgloss.ANSIColor(8) // Bright Black (Dark Gray) → muted/secondary + colorGray = lipgloss.ANSIColor(8) // Bright Black (Dark Gray) → muted/secondary // StyleSuccess renders text in green. StyleSuccess = lipgloss.NewStyle().Foreground(colorGreen) diff --git a/cli/output/tree_d2_test.go b/cli/output/tree_d2_test.go index 8db5092..445341d 100644 --- a/cli/output/tree_d2_test.go +++ b/cli/output/tree_d2_test.go @@ -209,8 +209,8 @@ func TestBuildD2Script_UtilityClusterOpacity(t *testing.T) { { ID: 0, Clusters: []TreeCluster{ - {ID: 0x001D, Name: "Descriptor"}, // utility - {ID: 0x0006, Name: "OnOff"}, // application + {ID: 0x001D, Name: "Descriptor"}, // utility + {ID: 0x0006, Name: "OnOff"}, // application }, }, }, diff --git a/cli/session.go b/cli/session.go index 0cca912..7ef6c90 100644 --- a/cli/session.go +++ b/cli/session.go @@ -125,8 +125,8 @@ node will use the daemon's cached session instead of establishing a new one.`, // newSessionStopCmd creates `matter session stop`. func newSessionStopCmd() *cobra.Command { return &cobra.Command{ - Use: "stop", - Short: "Stop the background session daemon", + Use: "stop", + Short: "Stop the background session daemon", Example: ` matter session stop`, RunE: func(cmd *cobra.Command, args []string) error { w := cmd.OutOrStdout() diff --git a/cli/target_test.go b/cli/target_test.go index 1670355..a8aa7df 100644 --- a/cli/target_test.go +++ b/cli/target_test.go @@ -11,14 +11,14 @@ import ( func TestParseTarget(t *testing.T) { tests := []struct { - name string - input string - wantNodeID uint64 - wantEP uint16 - wantEPSet bool - wantExplicit bool - wantErr bool - errContains string + name string + input string + wantNodeID uint64 + wantEP uint16 + wantEPSet bool + wantExplicit bool + wantErr bool + errContains string }{ { name: "numeric node only", diff --git a/internal/codegen/parser.go b/internal/codegen/parser.go index 9e277f1..1736f35 100644 --- a/internal/codegen/parser.go +++ b/internal/codegen/parser.go @@ -17,14 +17,14 @@ import ( // XMLCluster is the root element. type XMLCluster struct { - XMLName xml.Name `xml:"cluster"` - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - ClusterIDs XMLClusterIDs `xml:"clusterIds"` - Features XMLFeatures `xml:"features"` - DataTypes XMLDataTypes `xml:"dataTypes"` - Attributes []XMLAttribute `xml:"attributes>attribute"` - Commands []XMLCommand `xml:"commands>command"` + XMLName xml.Name `xml:"cluster"` + ID string `xml:"id,attr"` + Name string `xml:"name,attr"` + ClusterIDs XMLClusterIDs `xml:"clusterIds"` + Features XMLFeatures `xml:"features"` + DataTypes XMLDataTypes `xml:"dataTypes"` + Attributes []XMLAttribute `xml:"attributes>attribute"` + Commands []XMLCommand `xml:"commands>command"` } // XMLFeatures contains the element. @@ -72,8 +72,8 @@ type XMLEnumItem struct { // XMLBitmap is a data type. type XMLBitmap struct { - Name string `xml:"name,attr"` - Bitfields []XMLBitfield `xml:"bitfield"` + Name string `xml:"name,attr"` + Bitfields []XMLBitfield `xml:"bitfield"` } // XMLBitfield is a single within a bitmap. @@ -90,10 +90,10 @@ type XMLStruct struct { // XMLAttribute is an element. type XMLAttribute struct { - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - Type string `xml:"type,attr"` - Access XMLAccess `xml:"access"` + ID string `xml:"id,attr"` + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Access XMLAccess `xml:"access"` Quality XMLQuality `xml:"quality"` } @@ -108,11 +108,11 @@ type XMLCommand struct { // XMLField is a element inside a command or struct. type XMLField struct { - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - Type string `xml:"type,attr"` - Quality XMLQuality `xml:"quality"` - OptionalConform *XMLOptConform `xml:"optionalConform"` + ID string `xml:"id,attr"` + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Quality XMLQuality `xml:"quality"` + OptionalConform *XMLOptConform `xml:"optionalConform"` } // XMLOptConform exists when the field has . diff --git a/internal/commissioning/flow.go b/internal/commissioning/flow.go index 3539133..8a199ba 100644 --- a/internal/commissioning/flow.go +++ b/internal/commissioning/flow.go @@ -182,8 +182,8 @@ type CommissioningResult struct { // EndpointInfo describes a single endpoint discovered via the Descriptor cluster. type EndpointInfo struct { - ID uint16 - DeviceTypes []DeviceTypeInfo + ID uint16 + DeviceTypes []DeviceTypeInfo ServerClusters []uint32 } diff --git a/internal/commissioning/network.go b/internal/commissioning/network.go index f12769f..0966f1d 100644 --- a/internal/commissioning/network.go +++ b/internal/commissioning/network.go @@ -79,7 +79,7 @@ func NewThreadCredentials(dataset []byte) NetworkCredentials { // Thread operational dataset TLV type IDs (from the Thread specification). // Each TLV is encoded as: 1-byte type | 1-byte length | value. const ( - threadTLVActiveTimestamp = 0x0E // 8 bytes + threadTLVActiveTimestamp = 0x0E // 8 bytes threadTLVChannel = 0x00 // 3 bytes threadTLVChannelMask = 0x35 // variable threadTLVExtendedPANID = 0x02 // 8 bytes diff --git a/internal/commissioning/network_test.go b/internal/commissioning/network_test.go index 103781a..9111013 100644 --- a/internal/commissioning/network_test.go +++ b/internal/commissioning/network_test.go @@ -140,16 +140,16 @@ func TestValidateThreadDataset(t *testing.T) { t.Run("missing network key", func(t *testing.T) { // Build a dataset that is long enough but missing the Network Key TLV. var ds []byte - ds = append(ds, 0x00, 0x03, 0x00, 0x00, 0x0F) // Channel - ds = append(ds, 0x01, 0x02, 0xAB, 0xCD) // PAN ID + ds = append(ds, 0x00, 0x03, 0x00, 0x00, 0x0F) // Channel + ds = append(ds, 0x01, 0x02, 0xAB, 0xCD) // PAN ID ds = append(ds, 0x02, 0x08, 0xDE, 0xAD, 0x00, 0xBE, 0xEF, 0x00, 0xCA, 0xFE) // ExtPAN - ds = append(ds, 0x03, 0x07, 'T', 'e', 's', 't', 'N', 'e', 't') // Name - ds = append(ds, 0x04, 0x10) // PSKc - ds = append(ds, make([]byte, 16)...) // PSKc value + ds = append(ds, 0x03, 0x07, 'T', 'e', 's', 't', 'N', 'e', 't') // Name + ds = append(ds, 0x04, 0x10) // PSKc + ds = append(ds, make([]byte, 16)...) // PSKc value // Skip Network Key (type 0x05) ds = append(ds, 0x07, 0x08, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) // Mesh-Local - ds = append(ds, 0x0C, 0x03, 0x00, 0xF8, 0x00) // Security Policy - ds = append(ds, 0x0E, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00) // Timestamp + ds = append(ds, 0x0C, 0x03, 0x00, 0xF8, 0x00) // Security Policy + ds = append(ds, 0x0E, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00) // Timestamp err := ValidateThreadDataset(ds) if err == nil { diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 9b52360..8dc1a46 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -270,11 +270,11 @@ func (c *Controller) sendMRPAck(ctx context.Context, msg *protocol.Message) { SessionID: msg.Header.SessionID, }, Protocol: protocol.ProtocolHeader{ - ExchangeFlags: protocol.ExFlagACK, - ProtocolOpcode: 0x10, // MRP Standalone Ack - ProtocolID: 0x0000, // Secure Channel protocol - ExchangeID: msg.Protocol.ExchangeID, - HasAckCounter: true, + ExchangeFlags: protocol.ExFlagACK, + ProtocolOpcode: 0x10, // MRP Standalone Ack + ProtocolID: 0x0000, // Secure Channel protocol + ExchangeID: msg.Protocol.ExchangeID, + HasAckCounter: true, AckMessageCounter: msg.Header.MessageCounter, }, } diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 8362a60..39087b7 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -26,11 +26,11 @@ func (a *mockAddr) String() string { return a.address } // pipeConn is an in-memory transport.Conn that connects two ends via channels. type pipeConn struct { - send chan pipeMsg - recv chan pipeMsg - closed chan struct{} - once sync.Once - myAddr net.Addr + send chan pipeMsg + recv chan pipeMsg + closed chan struct{} + once sync.Once + myAddr net.Addr } type pipeMsg struct { diff --git a/internal/crypto/mattercert.go b/internal/crypto/mattercert.go index 6022156..6d92cd9 100644 --- a/internal/crypto/mattercert.go +++ b/internal/crypto/mattercert.go @@ -23,17 +23,17 @@ import ( // Matter TLV certificate field tags (from Matter spec section 6.6). const ( - certTagSerialNumber = 1 - certTagSigAlgo = 2 - certTagIssuer = 3 - certTagNotBefore = 4 - certTagNotAfter = 5 - certTagSubject = 6 - certTagPubKeyAlgo = 7 - certTagECCurveID = 8 - certTagECPubKey = 9 - certTagExtensions = 10 - certTagSignature = 11 + certTagSerialNumber = 1 + certTagSigAlgo = 2 + certTagIssuer = 3 + certTagNotBefore = 4 + certTagNotAfter = 5 + certTagSubject = 6 + certTagPubKeyAlgo = 7 + certTagECCurveID = 8 + certTagECPubKey = 9 + certTagExtensions = 10 + certTagSignature = 11 ) // Matter DN attribute tags. @@ -46,11 +46,11 @@ const ( // Matter extension tags. const ( - extTagBasicConstraints = 1 - extTagKeyUsage = 2 - extTagExtendedKeyUsage = 3 - extTagSubjectKeyID = 4 - extTagAuthorityKeyID = 5 + extTagBasicConstraints = 1 + extTagKeyUsage = 2 + extTagExtendedKeyUsage = 3 + extTagSubjectKeyID = 4 + extTagAuthorityKeyID = 5 ) // Matter algorithm constants. diff --git a/internal/crypto/mattercert_test.go b/internal/crypto/mattercert_test.go index 8136664..6697a83 100644 --- a/internal/crypto/mattercert_test.go +++ b/internal/crypto/mattercert_test.go @@ -400,8 +400,8 @@ func TestAddTrustedRootCertEncoding(t *testing.T) { Fields []byte `tlv:"1,rawstruct"` } type InvokeRequest struct { - SuppressResponse bool `tlv:"0,bool"` - TimedRequest bool `tlv:"1,bool"` + SuppressResponse bool `tlv:"0,bool"` + TimedRequest bool `tlv:"1,bool"` InvokeRequests []CommandDataIB `tlv:"2,array"` } diff --git a/internal/daemon/protocol.go b/internal/daemon/protocol.go index 238aa9c..b489c56 100644 --- a/internal/daemon/protocol.go +++ b/internal/daemon/protocol.go @@ -194,9 +194,9 @@ type StatusResp struct { // SessionInfo describes a single cached CASE session. type SessionInfo struct { - NodeID uint64 `json:"node_id"` - SessionID uint16 `json:"session_id"` - PeerAddress string `json:"peer_address"` + NodeID uint64 `json:"node_id"` + SessionID uint16 `json:"session_id"` + PeerAddress string `json:"peer_address"` Established Duration `json:"established"` } diff --git a/internal/interaction/invoke.go b/internal/interaction/invoke.go index 8d76d2a..18d8980 100644 --- a/internal/interaction/invoke.go +++ b/internal/interaction/invoke.go @@ -6,8 +6,8 @@ package interaction // InvokeRequest is the TLV structure for an Invoke Request message (opcode 0x08). // It carries one or more command invocations to execute on the peer. type InvokeRequest struct { - SuppressResponse bool `tlv:"0,bool"` - TimedRequest bool `tlv:"1,bool"` + SuppressResponse bool `tlv:"0,bool"` + TimedRequest bool `tlv:"1,bool"` InvokeRequests []CommandDataIB `tlv:"2,array"` } diff --git a/internal/protocol/message_test.go b/internal/protocol/message_test.go index a69d7dc..3168927 100644 --- a/internal/protocol/message_test.go +++ b/internal/protocol/message_test.go @@ -133,12 +133,12 @@ func TestProtocolHeaderRoundTrip(t *testing.T) { { name: "with ACK", header: ProtocolHeader{ - ExchangeFlags: ExFlagACK | ExFlagReliable, - ProtocolOpcode: 0x01, - ExchangeID: 42, - ProtocolID: 0x0001, + ExchangeFlags: ExFlagACK | ExFlagReliable, + ProtocolOpcode: 0x01, + ExchangeID: 42, + ProtocolID: 0x0001, AckMessageCounter: 99, - HasAckCounter: true, + HasAckCounter: true, }, }, { @@ -155,14 +155,14 @@ func TestProtocolHeaderRoundTrip(t *testing.T) { { name: "with vendor ID and ACK", header: ProtocolHeader{ - ExchangeFlags: ExFlagVendor | ExFlagACK | ExFlagInitiator, - ProtocolOpcode: 0x05, - ExchangeID: 200, - ProtocolID: 0x0002, - VendorID: 0x5678, - HasVendorID: true, + ExchangeFlags: ExFlagVendor | ExFlagACK | ExFlagInitiator, + ProtocolOpcode: 0x05, + ExchangeID: 200, + ProtocolID: 0x0002, + VendorID: 0x5678, + HasVendorID: true, AckMessageCounter: 12345, - HasAckCounter: true, + HasAckCounter: true, }, }, } diff --git a/internal/secure/pase.go b/internal/secure/pase.go index 485a0ea..0c29da0 100644 --- a/internal/secure/pase.go +++ b/internal/secure/pase.go @@ -96,9 +96,9 @@ type PASEInitiator struct { // State accumulated during the handshake. initiatorRandom []byte - spakeContext []byte // pbkdfReqBytes || pbkdfRespBytes - prover *crypto.SPAKE2PProver - sessionKeys *SessionKeys + spakeContext []byte // pbkdfReqBytes || pbkdfRespBytes + prover *crypto.SPAKE2PProver + sessionKeys *SessionKeys } // NewPASEInitiator creates a new PASE initiator (commissioner side) for the given diff --git a/internal/secure/pase_test.go b/internal/secure/pase_test.go index c08c651..af01466 100644 --- a/internal/secure/pase_test.go +++ b/internal/secure/pase_test.go @@ -232,7 +232,7 @@ func TestPASEMultiplePasscodes(t *testing.T) { passcodes := []uint32{20202021, 12345678, 1, 99999998} for _, passcode := range passcodes { - t.Run("passcode_" + itoa(passcode), func(t *testing.T) { + t.Run("passcode_"+itoa(passcode), func(t *testing.T) { initiator := NewPASEInitiator(passcode, 10) responder := NewPASEResponder(passcode, testPASESalt, testPASEIterations, 20) diff --git a/internal/store/memory.go b/internal/store/memory.go index 5a8e0f7..e7e6906 100644 --- a/internal/store/memory.go +++ b/internal/store/memory.go @@ -11,12 +11,12 @@ import ( // MemoryStore is a concurrency-safe, in-memory implementation of Store. It // never touches the filesystem and is intended for use in tests. type MemoryStore struct { - mu sync.RWMutex - fabrics map[uint64]*Fabric - nodes map[uint64]map[uint64]*Node // fabricID -> nodeID -> Node - resume map[uint64]*ResumptionInfo // peerNodeID -> info - kv map[string][]byte - closed bool + mu sync.RWMutex + fabrics map[uint64]*Fabric + nodes map[uint64]map[uint64]*Node // fabricID -> nodeID -> Node + resume map[uint64]*ResumptionInfo // peerNodeID -> info + kv map[string][]byte + closed bool } // NewMemoryStore returns a new empty MemoryStore. diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 828e8df..037d7d0 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -208,9 +208,9 @@ func storeTestSuite(t *testing.T, s Store) { t.Run("ResumptionInfo_CRUD", func(t *testing.T) { info := &ResumptionInfo{ - PeerNodeID: 42, - ResumptionID: []byte{1, 2, 3, 4}, - SharedSecret: []byte{5, 6, 7, 8}, + PeerNodeID: 42, + ResumptionID: []byte{1, 2, 3, 4}, + SharedSecret: []byte{5, 6, 7, 8}, CASESessionParams: []byte{9, 10}, } diff --git a/internal/store/types.go b/internal/store/types.go index fbf2924..d1a063e 100644 --- a/internal/store/types.go +++ b/internal/store/types.go @@ -54,8 +54,8 @@ type ClusterRef struct { // ResumptionInfo holds CASE session resumption data for a peer node. type ResumptionInfo struct { - PeerNodeID uint64 `json:"peer_node_id"` - ResumptionID []byte `json:"resumption_id"` - SharedSecret []byte `json:"shared_secret"` + PeerNodeID uint64 `json:"peer_node_id"` + ResumptionID []byte `json:"resumption_id"` + SharedSecret []byte `json:"shared_secret"` CASESessionParams []byte `json:"case_session_params"` } diff --git a/internal/transport/ble.go b/internal/transport/ble.go index 8f09dac..0e9fec9 100644 --- a/internal/transport/ble.go +++ b/internal/transport/ble.go @@ -223,8 +223,8 @@ func DialBLE(ctx context.Context, adapter bleAdapter, addr BLEAddress) (*BLEConn // subscribe is confirmed. We use both delivery paths (notification // callback and cached-value polling) and accept whichever fires first. const ( - btpHandshakeMaxAttempts = 5 - btpHandshakeRetryInterval = 3 * time.Second + btpHandshakeMaxAttempts = 5 + btpHandshakeRetryInterval = 3 * time.Second ) // Total handshake budget: 15 s from the start of this step. diff --git a/internal/transport/ble_scanner_test.go b/internal/transport/ble_scanner_test.go index 30dfb22..08a0c97 100644 --- a/internal/transport/ble_scanner_test.go +++ b/internal/transport/ble_scanner_test.go @@ -178,15 +178,15 @@ func (s *mockBLEService) DiscoverCharacteristics(uuids []BLEUUID) ([]bleCharacte } type mockBLECharacteristic struct { - uuid BLEUUID - writeData [][]byte - writeMu sync.Mutex - writeErr error - notifCb func([]byte) - notifMu sync.RWMutex - enableNotifErr error - waitCh chan []byte // delivers data for WaitForValue - disconnected atomic.Bool // when true, IsConnected() returns false + uuid BLEUUID + writeData [][]byte + writeMu sync.Mutex + writeErr error + notifCb func([]byte) + notifMu sync.RWMutex + enableNotifErr error + waitCh chan []byte // delivers data for WaitForValue + disconnected atomic.Bool // when true, IsConnected() returns false } func (c *mockBLECharacteristic) UUID() BLEUUID { return c.uuid } @@ -521,9 +521,9 @@ func TestBLEScanner_Scan_SingleMatterDevice(t *testing.T) { func TestBLEScanner_Scan_IgnoresNonMatterDevices(t *testing.T) { nonMatter := BLEScanAdvertisement{ - Address: "11:22:33:44:55:66", - RSSI: -50, - LocalName: "SomeFitnessBand", + Address: "11:22:33:44:55:66", + RSSI: -50, + LocalName: "SomeFitnessBand", ServiceData: map[BLEUUID][]byte{ // Different service UUID, not Matter. "00001800-0000-1000-8000-00805f9b34fb": {0x01, 0x02}, @@ -847,7 +847,7 @@ func TestMatterServiceUUIDs_AreCorrect(t *testing.T) { // ─── Mock adapter interface compliance ─────────────────────────────────────── // Compile-time checks: ensure mock types satisfy the interfaces. -var _ bleAdapter = (*mockBLEAdapter)(nil) -var _ bleDevice = (*mockBLEDevice)(nil) -var _ bleService = (*mockBLEService)(nil) +var _ bleAdapter = (*mockBLEAdapter)(nil) +var _ bleDevice = (*mockBLEDevice)(nil) +var _ bleService = (*mockBLEService)(nil) var _ bleCharacteristic = (*mockBLECharacteristic)(nil) diff --git a/internal/transport/btp.go b/internal/transport/btp.go index 636c7b9..1be67f1 100644 --- a/internal/transport/btp.go +++ b/internal/transport/btp.go @@ -147,7 +147,7 @@ func btpHandshakeRequest(versions []uint8, attMTU uint16, windowSize uint8) []by for i := 0; i < btpMaxVersionSlots && i < len(versions); i++ { byteIdx := 2 + i/2 if i%2 == 0 { - out[byteIdx] |= versions[i] & 0x0F // low nibble + out[byteIdx] |= versions[i] & 0x0F // low nibble } else { out[byteIdx] |= (versions[i] & 0x0F) << 4 // high nibble } diff --git a/internal/transport/btp_test.go b/internal/transport/btp_test.go index c9ede22..c6505a0 100644 --- a/internal/transport/btp_test.go +++ b/internal/transport/btp_test.go @@ -33,8 +33,8 @@ func TestBTPHandshakeRequest(t *testing.T) { 0x65, // magic byte 1 0x6C, // magic byte 2 0x04, 0x00, 0x00, 0x00, // versions: slot0=4 - 0xF7, 0x00, // ATT MTU: 247 (LE) - 0x06, // window size: 6 + 0xF7, 0x00, // ATT MTU: 247 (LE) + 0x06, // window size: 6 }, }, { @@ -99,7 +99,7 @@ func TestParseBTPHandshakeResponse_Valid(t *testing.T) { 0x6C, // magic byte 2 0x04, // selected version: 4 0x14, 0x00, // fragment size: 20 (LE) - 0x06, // window size: 6 + 0x06, // window size: 6 }, wantVersion: 4, wantFragmentSize: 20, @@ -873,7 +873,7 @@ func TestHandleSegment_MessageLengthMismatch(t *testing.T) { // Build a B+E segment that declares MsgLen=10 but carries only 5 bytes. var buf bytes.Buffer buf.WriteByte(btpFlagBegin | btpFlagEnd) // flags - buf.WriteByte(0x00) // seqNum + buf.WriteByte(0x00) // seqNum // MsgLen = 10 but payload is only 5 bytes. buf.WriteByte(0x0A) // MsgLen lo = 10 buf.WriteByte(0x00) // MsgLen hi = 0 @@ -1071,7 +1071,7 @@ func TestFlowControl_ProcessAck_SeqNumWrap(t *testing.T) { func TestFlowControl_WaitCanSend_UnblocksOnAck(t *testing.T) { s := newBTPSession() s.windowSize = 2 - s.txInflight = 2 // window full + s.txInflight = 2 // window full s.localSeq = 2 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) diff --git a/internal/transport/mrp.go b/internal/transport/mrp.go index 2684cdd..2e0da60 100644 --- a/internal/transport/mrp.go +++ b/internal/transport/mrp.go @@ -46,9 +46,9 @@ type MRP struct { conn Conn config MRPConfig - mu sync.Mutex - pending map[uint32]*pendingMessage - closed chan struct{} + mu sync.Mutex + pending map[uint32]*pendingMessage + closed chan struct{} closeOnce sync.Once } diff --git a/pkg/matter/matter_test.go b/pkg/matter/matter_test.go index 249a61c..16ab703 100644 --- a/pkg/matter/matter_test.go +++ b/pkg/matter/matter_test.go @@ -30,9 +30,9 @@ func TestNewClientInMemory(t *testing.T) { func TestLookupCluster(t *testing.T) { tests := []struct { - name string - wantID uint32 - wantOK bool + name string + wantID uint32 + wantOK bool }{ {"OnOff", 0x0006, true}, {"LevelControl", 0x0008, true}, From 0d30360fda9c8ee531e2b1a60992776591b41bca Mon Sep 17 00:00:00 2001 From: Thomas Hartwig Date: Thu, 23 Apr 2026 14:56:07 +0200 Subject: [PATCH 03/10] =?UTF-8?q?Revert=20"=F0=9F=8E=A8=20style:=20run=20g?= =?UTF-8?q?ofmt=20across=20all=20unformatted=20files=20to=20fix=20CI=20lin?= =?UTF-8?q?t"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 14764d60c37c00d8e62ef20113f0854c0a5050e5. --- cli/code.go | 4 +-- cli/commission_window.go | 8 +++--- cli/decommission.go | 1 + cli/output/color.go | 14 +++++----- cli/output/tree_d2_test.go | 4 +-- cli/session.go | 4 +-- cli/target_test.go | 16 +++++------ internal/codegen/parser.go | 38 +++++++++++++------------- internal/commissioning/flow.go | 4 +-- internal/commissioning/network.go | 2 +- internal/commissioning/network_test.go | 14 +++++----- internal/controller/controller.go | 10 +++---- internal/controller/controller_test.go | 10 +++---- internal/crypto/mattercert.go | 32 +++++++++++----------- internal/crypto/mattercert_test.go | 4 +-- internal/daemon/protocol.go | 6 ++-- internal/interaction/invoke.go | 4 +-- internal/protocol/message_test.go | 24 ++++++++-------- internal/secure/pase.go | 6 ++-- internal/secure/pase_test.go | 2 +- internal/store/memory.go | 12 ++++---- internal/store/store_test.go | 6 ++-- internal/store/types.go | 6 ++-- internal/transport/ble.go | 4 +-- internal/transport/ble_scanner_test.go | 30 ++++++++++---------- internal/transport/btp.go | 2 +- internal/transport/btp_test.go | 10 +++---- internal/transport/mrp.go | 6 ++-- pkg/matter/matter_test.go | 6 ++-- 29 files changed, 145 insertions(+), 144 deletions(-) diff --git a/cli/code.go b/cli/code.go index d74dea1..6f72df3 100644 --- a/cli/code.go +++ b/cli/code.go @@ -148,8 +148,8 @@ func looksLikeQRPayload(s string) bool { func newCodeGenerateCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "generate", - Short: "Generate a QR code and manual pairing code from parameters", + Use: "generate", + Short: "Generate a QR code and manual pairing code from parameters", Example: ` matter code generate --vid 0xFFF1 --pid 0x8000 --passcode 12345678 --discriminator 3840`, RunE: func(cmd *cobra.Command, args []string) error { vid, _ := cmd.Flags().GetUint16("vid") diff --git a/cli/commission_window.go b/cli/commission_window.go index b1154bf..6898d5b 100644 --- a/cli/commission_window.go +++ b/cli/commission_window.go @@ -18,8 +18,8 @@ import ( "github.com/p0fi/matter-cli/internal/daemon" "github.com/p0fi/matter-cli/internal/interaction" "github.com/p0fi/matter-cli/internal/protocol" - "github.com/p0fi/matter-cli/internal/store" "github.com/p0fi/matter-cli/internal/tlv" + "github.com/p0fi/matter-cli/internal/store" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -32,9 +32,9 @@ const timedInteractionTimeoutMs uint16 = 5_000 // Administrator Commissioning cluster-specific status codes (Matter spec §11.19.6). const ( - adminBusy uint8 = 0x02 - adminPAKEParamError uint8 = 0x03 - adminWindowNotOpen uint8 = 0x04 + adminBusy uint8 = 0x02 + adminPAKEParamError uint8 = 0x03 + adminWindowNotOpen uint8 = 0x04 ) // newCommissionOpenWindowCmd creates the `commission open-window` subcommand. diff --git a/cli/decommission.go b/cli/decommission.go index 4bf2919..ef602e7 100644 --- a/cli/decommission.go +++ b/cli/decommission.go @@ -385,3 +385,4 @@ func confirmForceDelete(cmd *cobra.Command, stepper *output.Stepper, force bool, answer := strings.TrimSpace(strings.ToLower(scanner.Text())) return answer == "y" || answer == "yes" } + diff --git a/cli/output/color.go b/cli/output/color.go index 320d015..f432347 100644 --- a/cli/output/color.go +++ b/cli/output/color.go @@ -30,14 +30,14 @@ import ( var ( // Core colors — ANSI numbers, resolved by the terminal's own palette. - colorGreen = lipgloss.ANSIColor(10) // Bright Green → success - colorRed = lipgloss.ANSIColor(9) // Bright Red → errors - colorYellow = lipgloss.ANSIColor(3) // Yellow → warnings - colorBlue = lipgloss.ANSIColor(12) // Bright Blue → info / headers - colorCyan = lipgloss.ANSIColor(14) // Bright Cyan → labels / commands - colorMagenta = lipgloss.ANSIColor(13) // Bright Magenta→ accents / IDs + colorGreen = lipgloss.ANSIColor(10) // Bright Green → success + colorRed = lipgloss.ANSIColor(9) // Bright Red → errors + colorYellow = lipgloss.ANSIColor(3) // Yellow → warnings + colorBlue = lipgloss.ANSIColor(12) // Bright Blue → info / headers + colorCyan = lipgloss.ANSIColor(14) // Bright Cyan → labels / commands + colorMagenta = lipgloss.ANSIColor(13) // Bright Magenta→ accents / IDs colorLightGray = lipgloss.ANSIColor(7) // Light Gray → values - colorGray = lipgloss.ANSIColor(8) // Bright Black (Dark Gray) → muted/secondary + colorGray = lipgloss.ANSIColor(8) // Bright Black (Dark Gray) → muted/secondary // StyleSuccess renders text in green. StyleSuccess = lipgloss.NewStyle().Foreground(colorGreen) diff --git a/cli/output/tree_d2_test.go b/cli/output/tree_d2_test.go index 445341d..8db5092 100644 --- a/cli/output/tree_d2_test.go +++ b/cli/output/tree_d2_test.go @@ -209,8 +209,8 @@ func TestBuildD2Script_UtilityClusterOpacity(t *testing.T) { { ID: 0, Clusters: []TreeCluster{ - {ID: 0x001D, Name: "Descriptor"}, // utility - {ID: 0x0006, Name: "OnOff"}, // application + {ID: 0x001D, Name: "Descriptor"}, // utility + {ID: 0x0006, Name: "OnOff"}, // application }, }, }, diff --git a/cli/session.go b/cli/session.go index 7ef6c90..0cca912 100644 --- a/cli/session.go +++ b/cli/session.go @@ -125,8 +125,8 @@ node will use the daemon's cached session instead of establishing a new one.`, // newSessionStopCmd creates `matter session stop`. func newSessionStopCmd() *cobra.Command { return &cobra.Command{ - Use: "stop", - Short: "Stop the background session daemon", + Use: "stop", + Short: "Stop the background session daemon", Example: ` matter session stop`, RunE: func(cmd *cobra.Command, args []string) error { w := cmd.OutOrStdout() diff --git a/cli/target_test.go b/cli/target_test.go index a8aa7df..1670355 100644 --- a/cli/target_test.go +++ b/cli/target_test.go @@ -11,14 +11,14 @@ import ( func TestParseTarget(t *testing.T) { tests := []struct { - name string - input string - wantNodeID uint64 - wantEP uint16 - wantEPSet bool - wantExplicit bool - wantErr bool - errContains string + name string + input string + wantNodeID uint64 + wantEP uint16 + wantEPSet bool + wantExplicit bool + wantErr bool + errContains string }{ { name: "numeric node only", diff --git a/internal/codegen/parser.go b/internal/codegen/parser.go index 1736f35..9e277f1 100644 --- a/internal/codegen/parser.go +++ b/internal/codegen/parser.go @@ -17,14 +17,14 @@ import ( // XMLCluster is the root element. type XMLCluster struct { - XMLName xml.Name `xml:"cluster"` - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - ClusterIDs XMLClusterIDs `xml:"clusterIds"` - Features XMLFeatures `xml:"features"` - DataTypes XMLDataTypes `xml:"dataTypes"` - Attributes []XMLAttribute `xml:"attributes>attribute"` - Commands []XMLCommand `xml:"commands>command"` + XMLName xml.Name `xml:"cluster"` + ID string `xml:"id,attr"` + Name string `xml:"name,attr"` + ClusterIDs XMLClusterIDs `xml:"clusterIds"` + Features XMLFeatures `xml:"features"` + DataTypes XMLDataTypes `xml:"dataTypes"` + Attributes []XMLAttribute `xml:"attributes>attribute"` + Commands []XMLCommand `xml:"commands>command"` } // XMLFeatures contains the element. @@ -72,8 +72,8 @@ type XMLEnumItem struct { // XMLBitmap is a data type. type XMLBitmap struct { - Name string `xml:"name,attr"` - Bitfields []XMLBitfield `xml:"bitfield"` + Name string `xml:"name,attr"` + Bitfields []XMLBitfield `xml:"bitfield"` } // XMLBitfield is a single within a bitmap. @@ -90,10 +90,10 @@ type XMLStruct struct { // XMLAttribute is an element. type XMLAttribute struct { - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - Type string `xml:"type,attr"` - Access XMLAccess `xml:"access"` + ID string `xml:"id,attr"` + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Access XMLAccess `xml:"access"` Quality XMLQuality `xml:"quality"` } @@ -108,11 +108,11 @@ type XMLCommand struct { // XMLField is a element inside a command or struct. type XMLField struct { - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - Type string `xml:"type,attr"` - Quality XMLQuality `xml:"quality"` - OptionalConform *XMLOptConform `xml:"optionalConform"` + ID string `xml:"id,attr"` + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Quality XMLQuality `xml:"quality"` + OptionalConform *XMLOptConform `xml:"optionalConform"` } // XMLOptConform exists when the field has . diff --git a/internal/commissioning/flow.go b/internal/commissioning/flow.go index 8a199ba..3539133 100644 --- a/internal/commissioning/flow.go +++ b/internal/commissioning/flow.go @@ -182,8 +182,8 @@ type CommissioningResult struct { // EndpointInfo describes a single endpoint discovered via the Descriptor cluster. type EndpointInfo struct { - ID uint16 - DeviceTypes []DeviceTypeInfo + ID uint16 + DeviceTypes []DeviceTypeInfo ServerClusters []uint32 } diff --git a/internal/commissioning/network.go b/internal/commissioning/network.go index 0966f1d..f12769f 100644 --- a/internal/commissioning/network.go +++ b/internal/commissioning/network.go @@ -79,7 +79,7 @@ func NewThreadCredentials(dataset []byte) NetworkCredentials { // Thread operational dataset TLV type IDs (from the Thread specification). // Each TLV is encoded as: 1-byte type | 1-byte length | value. const ( - threadTLVActiveTimestamp = 0x0E // 8 bytes + threadTLVActiveTimestamp = 0x0E // 8 bytes threadTLVChannel = 0x00 // 3 bytes threadTLVChannelMask = 0x35 // variable threadTLVExtendedPANID = 0x02 // 8 bytes diff --git a/internal/commissioning/network_test.go b/internal/commissioning/network_test.go index 9111013..103781a 100644 --- a/internal/commissioning/network_test.go +++ b/internal/commissioning/network_test.go @@ -140,16 +140,16 @@ func TestValidateThreadDataset(t *testing.T) { t.Run("missing network key", func(t *testing.T) { // Build a dataset that is long enough but missing the Network Key TLV. var ds []byte - ds = append(ds, 0x00, 0x03, 0x00, 0x00, 0x0F) // Channel - ds = append(ds, 0x01, 0x02, 0xAB, 0xCD) // PAN ID + ds = append(ds, 0x00, 0x03, 0x00, 0x00, 0x0F) // Channel + ds = append(ds, 0x01, 0x02, 0xAB, 0xCD) // PAN ID ds = append(ds, 0x02, 0x08, 0xDE, 0xAD, 0x00, 0xBE, 0xEF, 0x00, 0xCA, 0xFE) // ExtPAN - ds = append(ds, 0x03, 0x07, 'T', 'e', 's', 't', 'N', 'e', 't') // Name - ds = append(ds, 0x04, 0x10) // PSKc - ds = append(ds, make([]byte, 16)...) // PSKc value + ds = append(ds, 0x03, 0x07, 'T', 'e', 's', 't', 'N', 'e', 't') // Name + ds = append(ds, 0x04, 0x10) // PSKc + ds = append(ds, make([]byte, 16)...) // PSKc value // Skip Network Key (type 0x05) ds = append(ds, 0x07, 0x08, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) // Mesh-Local - ds = append(ds, 0x0C, 0x03, 0x00, 0xF8, 0x00) // Security Policy - ds = append(ds, 0x0E, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00) // Timestamp + ds = append(ds, 0x0C, 0x03, 0x00, 0xF8, 0x00) // Security Policy + ds = append(ds, 0x0E, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00) // Timestamp err := ValidateThreadDataset(ds) if err == nil { diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 8dc1a46..9b52360 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -270,11 +270,11 @@ func (c *Controller) sendMRPAck(ctx context.Context, msg *protocol.Message) { SessionID: msg.Header.SessionID, }, Protocol: protocol.ProtocolHeader{ - ExchangeFlags: protocol.ExFlagACK, - ProtocolOpcode: 0x10, // MRP Standalone Ack - ProtocolID: 0x0000, // Secure Channel protocol - ExchangeID: msg.Protocol.ExchangeID, - HasAckCounter: true, + ExchangeFlags: protocol.ExFlagACK, + ProtocolOpcode: 0x10, // MRP Standalone Ack + ProtocolID: 0x0000, // Secure Channel protocol + ExchangeID: msg.Protocol.ExchangeID, + HasAckCounter: true, AckMessageCounter: msg.Header.MessageCounter, }, } diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 39087b7..8362a60 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -26,11 +26,11 @@ func (a *mockAddr) String() string { return a.address } // pipeConn is an in-memory transport.Conn that connects two ends via channels. type pipeConn struct { - send chan pipeMsg - recv chan pipeMsg - closed chan struct{} - once sync.Once - myAddr net.Addr + send chan pipeMsg + recv chan pipeMsg + closed chan struct{} + once sync.Once + myAddr net.Addr } type pipeMsg struct { diff --git a/internal/crypto/mattercert.go b/internal/crypto/mattercert.go index 6d92cd9..6022156 100644 --- a/internal/crypto/mattercert.go +++ b/internal/crypto/mattercert.go @@ -23,17 +23,17 @@ import ( // Matter TLV certificate field tags (from Matter spec section 6.6). const ( - certTagSerialNumber = 1 - certTagSigAlgo = 2 - certTagIssuer = 3 - certTagNotBefore = 4 - certTagNotAfter = 5 - certTagSubject = 6 - certTagPubKeyAlgo = 7 - certTagECCurveID = 8 - certTagECPubKey = 9 - certTagExtensions = 10 - certTagSignature = 11 + certTagSerialNumber = 1 + certTagSigAlgo = 2 + certTagIssuer = 3 + certTagNotBefore = 4 + certTagNotAfter = 5 + certTagSubject = 6 + certTagPubKeyAlgo = 7 + certTagECCurveID = 8 + certTagECPubKey = 9 + certTagExtensions = 10 + certTagSignature = 11 ) // Matter DN attribute tags. @@ -46,11 +46,11 @@ const ( // Matter extension tags. const ( - extTagBasicConstraints = 1 - extTagKeyUsage = 2 - extTagExtendedKeyUsage = 3 - extTagSubjectKeyID = 4 - extTagAuthorityKeyID = 5 + extTagBasicConstraints = 1 + extTagKeyUsage = 2 + extTagExtendedKeyUsage = 3 + extTagSubjectKeyID = 4 + extTagAuthorityKeyID = 5 ) // Matter algorithm constants. diff --git a/internal/crypto/mattercert_test.go b/internal/crypto/mattercert_test.go index 6697a83..8136664 100644 --- a/internal/crypto/mattercert_test.go +++ b/internal/crypto/mattercert_test.go @@ -400,8 +400,8 @@ func TestAddTrustedRootCertEncoding(t *testing.T) { Fields []byte `tlv:"1,rawstruct"` } type InvokeRequest struct { - SuppressResponse bool `tlv:"0,bool"` - TimedRequest bool `tlv:"1,bool"` + SuppressResponse bool `tlv:"0,bool"` + TimedRequest bool `tlv:"1,bool"` InvokeRequests []CommandDataIB `tlv:"2,array"` } diff --git a/internal/daemon/protocol.go b/internal/daemon/protocol.go index b489c56..238aa9c 100644 --- a/internal/daemon/protocol.go +++ b/internal/daemon/protocol.go @@ -194,9 +194,9 @@ type StatusResp struct { // SessionInfo describes a single cached CASE session. type SessionInfo struct { - NodeID uint64 `json:"node_id"` - SessionID uint16 `json:"session_id"` - PeerAddress string `json:"peer_address"` + NodeID uint64 `json:"node_id"` + SessionID uint16 `json:"session_id"` + PeerAddress string `json:"peer_address"` Established Duration `json:"established"` } diff --git a/internal/interaction/invoke.go b/internal/interaction/invoke.go index 18d8980..8d76d2a 100644 --- a/internal/interaction/invoke.go +++ b/internal/interaction/invoke.go @@ -6,8 +6,8 @@ package interaction // InvokeRequest is the TLV structure for an Invoke Request message (opcode 0x08). // It carries one or more command invocations to execute on the peer. type InvokeRequest struct { - SuppressResponse bool `tlv:"0,bool"` - TimedRequest bool `tlv:"1,bool"` + SuppressResponse bool `tlv:"0,bool"` + TimedRequest bool `tlv:"1,bool"` InvokeRequests []CommandDataIB `tlv:"2,array"` } diff --git a/internal/protocol/message_test.go b/internal/protocol/message_test.go index 3168927..a69d7dc 100644 --- a/internal/protocol/message_test.go +++ b/internal/protocol/message_test.go @@ -133,12 +133,12 @@ func TestProtocolHeaderRoundTrip(t *testing.T) { { name: "with ACK", header: ProtocolHeader{ - ExchangeFlags: ExFlagACK | ExFlagReliable, - ProtocolOpcode: 0x01, - ExchangeID: 42, - ProtocolID: 0x0001, + ExchangeFlags: ExFlagACK | ExFlagReliable, + ProtocolOpcode: 0x01, + ExchangeID: 42, + ProtocolID: 0x0001, AckMessageCounter: 99, - HasAckCounter: true, + HasAckCounter: true, }, }, { @@ -155,14 +155,14 @@ func TestProtocolHeaderRoundTrip(t *testing.T) { { name: "with vendor ID and ACK", header: ProtocolHeader{ - ExchangeFlags: ExFlagVendor | ExFlagACK | ExFlagInitiator, - ProtocolOpcode: 0x05, - ExchangeID: 200, - ProtocolID: 0x0002, - VendorID: 0x5678, - HasVendorID: true, + ExchangeFlags: ExFlagVendor | ExFlagACK | ExFlagInitiator, + ProtocolOpcode: 0x05, + ExchangeID: 200, + ProtocolID: 0x0002, + VendorID: 0x5678, + HasVendorID: true, AckMessageCounter: 12345, - HasAckCounter: true, + HasAckCounter: true, }, }, } diff --git a/internal/secure/pase.go b/internal/secure/pase.go index 0c29da0..485a0ea 100644 --- a/internal/secure/pase.go +++ b/internal/secure/pase.go @@ -96,9 +96,9 @@ type PASEInitiator struct { // State accumulated during the handshake. initiatorRandom []byte - spakeContext []byte // pbkdfReqBytes || pbkdfRespBytes - prover *crypto.SPAKE2PProver - sessionKeys *SessionKeys + spakeContext []byte // pbkdfReqBytes || pbkdfRespBytes + prover *crypto.SPAKE2PProver + sessionKeys *SessionKeys } // NewPASEInitiator creates a new PASE initiator (commissioner side) for the given diff --git a/internal/secure/pase_test.go b/internal/secure/pase_test.go index af01466..c08c651 100644 --- a/internal/secure/pase_test.go +++ b/internal/secure/pase_test.go @@ -232,7 +232,7 @@ func TestPASEMultiplePasscodes(t *testing.T) { passcodes := []uint32{20202021, 12345678, 1, 99999998} for _, passcode := range passcodes { - t.Run("passcode_"+itoa(passcode), func(t *testing.T) { + t.Run("passcode_" + itoa(passcode), func(t *testing.T) { initiator := NewPASEInitiator(passcode, 10) responder := NewPASEResponder(passcode, testPASESalt, testPASEIterations, 20) diff --git a/internal/store/memory.go b/internal/store/memory.go index e7e6906..5a8e0f7 100644 --- a/internal/store/memory.go +++ b/internal/store/memory.go @@ -11,12 +11,12 @@ import ( // MemoryStore is a concurrency-safe, in-memory implementation of Store. It // never touches the filesystem and is intended for use in tests. type MemoryStore struct { - mu sync.RWMutex - fabrics map[uint64]*Fabric - nodes map[uint64]map[uint64]*Node // fabricID -> nodeID -> Node - resume map[uint64]*ResumptionInfo // peerNodeID -> info - kv map[string][]byte - closed bool + mu sync.RWMutex + fabrics map[uint64]*Fabric + nodes map[uint64]map[uint64]*Node // fabricID -> nodeID -> Node + resume map[uint64]*ResumptionInfo // peerNodeID -> info + kv map[string][]byte + closed bool } // NewMemoryStore returns a new empty MemoryStore. diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 037d7d0..828e8df 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -208,9 +208,9 @@ func storeTestSuite(t *testing.T, s Store) { t.Run("ResumptionInfo_CRUD", func(t *testing.T) { info := &ResumptionInfo{ - PeerNodeID: 42, - ResumptionID: []byte{1, 2, 3, 4}, - SharedSecret: []byte{5, 6, 7, 8}, + PeerNodeID: 42, + ResumptionID: []byte{1, 2, 3, 4}, + SharedSecret: []byte{5, 6, 7, 8}, CASESessionParams: []byte{9, 10}, } diff --git a/internal/store/types.go b/internal/store/types.go index d1a063e..fbf2924 100644 --- a/internal/store/types.go +++ b/internal/store/types.go @@ -54,8 +54,8 @@ type ClusterRef struct { // ResumptionInfo holds CASE session resumption data for a peer node. type ResumptionInfo struct { - PeerNodeID uint64 `json:"peer_node_id"` - ResumptionID []byte `json:"resumption_id"` - SharedSecret []byte `json:"shared_secret"` + PeerNodeID uint64 `json:"peer_node_id"` + ResumptionID []byte `json:"resumption_id"` + SharedSecret []byte `json:"shared_secret"` CASESessionParams []byte `json:"case_session_params"` } diff --git a/internal/transport/ble.go b/internal/transport/ble.go index 0e9fec9..8f09dac 100644 --- a/internal/transport/ble.go +++ b/internal/transport/ble.go @@ -223,8 +223,8 @@ func DialBLE(ctx context.Context, adapter bleAdapter, addr BLEAddress) (*BLEConn // subscribe is confirmed. We use both delivery paths (notification // callback and cached-value polling) and accept whichever fires first. const ( - btpHandshakeMaxAttempts = 5 - btpHandshakeRetryInterval = 3 * time.Second + btpHandshakeMaxAttempts = 5 + btpHandshakeRetryInterval = 3 * time.Second ) // Total handshake budget: 15 s from the start of this step. diff --git a/internal/transport/ble_scanner_test.go b/internal/transport/ble_scanner_test.go index 08a0c97..30dfb22 100644 --- a/internal/transport/ble_scanner_test.go +++ b/internal/transport/ble_scanner_test.go @@ -178,15 +178,15 @@ func (s *mockBLEService) DiscoverCharacteristics(uuids []BLEUUID) ([]bleCharacte } type mockBLECharacteristic struct { - uuid BLEUUID - writeData [][]byte - writeMu sync.Mutex - writeErr error - notifCb func([]byte) - notifMu sync.RWMutex - enableNotifErr error - waitCh chan []byte // delivers data for WaitForValue - disconnected atomic.Bool // when true, IsConnected() returns false + uuid BLEUUID + writeData [][]byte + writeMu sync.Mutex + writeErr error + notifCb func([]byte) + notifMu sync.RWMutex + enableNotifErr error + waitCh chan []byte // delivers data for WaitForValue + disconnected atomic.Bool // when true, IsConnected() returns false } func (c *mockBLECharacteristic) UUID() BLEUUID { return c.uuid } @@ -521,9 +521,9 @@ func TestBLEScanner_Scan_SingleMatterDevice(t *testing.T) { func TestBLEScanner_Scan_IgnoresNonMatterDevices(t *testing.T) { nonMatter := BLEScanAdvertisement{ - Address: "11:22:33:44:55:66", - RSSI: -50, - LocalName: "SomeFitnessBand", + Address: "11:22:33:44:55:66", + RSSI: -50, + LocalName: "SomeFitnessBand", ServiceData: map[BLEUUID][]byte{ // Different service UUID, not Matter. "00001800-0000-1000-8000-00805f9b34fb": {0x01, 0x02}, @@ -847,7 +847,7 @@ func TestMatterServiceUUIDs_AreCorrect(t *testing.T) { // ─── Mock adapter interface compliance ─────────────────────────────────────── // Compile-time checks: ensure mock types satisfy the interfaces. -var _ bleAdapter = (*mockBLEAdapter)(nil) -var _ bleDevice = (*mockBLEDevice)(nil) -var _ bleService = (*mockBLEService)(nil) +var _ bleAdapter = (*mockBLEAdapter)(nil) +var _ bleDevice = (*mockBLEDevice)(nil) +var _ bleService = (*mockBLEService)(nil) var _ bleCharacteristic = (*mockBLECharacteristic)(nil) diff --git a/internal/transport/btp.go b/internal/transport/btp.go index 1be67f1..636c7b9 100644 --- a/internal/transport/btp.go +++ b/internal/transport/btp.go @@ -147,7 +147,7 @@ func btpHandshakeRequest(versions []uint8, attMTU uint16, windowSize uint8) []by for i := 0; i < btpMaxVersionSlots && i < len(versions); i++ { byteIdx := 2 + i/2 if i%2 == 0 { - out[byteIdx] |= versions[i] & 0x0F // low nibble + out[byteIdx] |= versions[i] & 0x0F // low nibble } else { out[byteIdx] |= (versions[i] & 0x0F) << 4 // high nibble } diff --git a/internal/transport/btp_test.go b/internal/transport/btp_test.go index c6505a0..c9ede22 100644 --- a/internal/transport/btp_test.go +++ b/internal/transport/btp_test.go @@ -33,8 +33,8 @@ func TestBTPHandshakeRequest(t *testing.T) { 0x65, // magic byte 1 0x6C, // magic byte 2 0x04, 0x00, 0x00, 0x00, // versions: slot0=4 - 0xF7, 0x00, // ATT MTU: 247 (LE) - 0x06, // window size: 6 + 0xF7, 0x00, // ATT MTU: 247 (LE) + 0x06, // window size: 6 }, }, { @@ -99,7 +99,7 @@ func TestParseBTPHandshakeResponse_Valid(t *testing.T) { 0x6C, // magic byte 2 0x04, // selected version: 4 0x14, 0x00, // fragment size: 20 (LE) - 0x06, // window size: 6 + 0x06, // window size: 6 }, wantVersion: 4, wantFragmentSize: 20, @@ -873,7 +873,7 @@ func TestHandleSegment_MessageLengthMismatch(t *testing.T) { // Build a B+E segment that declares MsgLen=10 but carries only 5 bytes. var buf bytes.Buffer buf.WriteByte(btpFlagBegin | btpFlagEnd) // flags - buf.WriteByte(0x00) // seqNum + buf.WriteByte(0x00) // seqNum // MsgLen = 10 but payload is only 5 bytes. buf.WriteByte(0x0A) // MsgLen lo = 10 buf.WriteByte(0x00) // MsgLen hi = 0 @@ -1071,7 +1071,7 @@ func TestFlowControl_ProcessAck_SeqNumWrap(t *testing.T) { func TestFlowControl_WaitCanSend_UnblocksOnAck(t *testing.T) { s := newBTPSession() s.windowSize = 2 - s.txInflight = 2 // window full + s.txInflight = 2 // window full s.localSeq = 2 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) diff --git a/internal/transport/mrp.go b/internal/transport/mrp.go index 2e0da60..2684cdd 100644 --- a/internal/transport/mrp.go +++ b/internal/transport/mrp.go @@ -46,9 +46,9 @@ type MRP struct { conn Conn config MRPConfig - mu sync.Mutex - pending map[uint32]*pendingMessage - closed chan struct{} + mu sync.Mutex + pending map[uint32]*pendingMessage + closed chan struct{} closeOnce sync.Once } diff --git a/pkg/matter/matter_test.go b/pkg/matter/matter_test.go index 16ab703..249a61c 100644 --- a/pkg/matter/matter_test.go +++ b/pkg/matter/matter_test.go @@ -30,9 +30,9 @@ func TestNewClientInMemory(t *testing.T) { func TestLookupCluster(t *testing.T) { tests := []struct { - name string - wantID uint32 - wantOK bool + name string + wantID uint32 + wantOK bool }{ {"OnOff", 0x0006, true}, {"LevelControl", 0x0008, true}, From 584b741600bbf17c3cbad92bf9641d40b0484989 Mon Sep 17 00:00:00 2001 From: Thomas Hartwig Date: Thu, 23 Apr 2026 15:52:42 +0200 Subject: [PATCH 04/10] ci: run lint, test, and build on PRs (#63) Ensures Dependabot (and human) PRs can't be merged until gofmt, golangci-lint, `go test -race` on Ubuntu + macOS, and the build task all succeed. Also extends Dependabot to track GitHub Actions versions weekly. --- .github/dependabot.yml | 4 ++ .github/workflows/ci.yml | 68 +++++++++++++++++++++++++ cli/code.go | 4 +- cli/commission_window.go | 8 +-- cli/decommission.go | 38 +++++++------- cli/output/color.go | 14 ++--- cli/output/tree_d2_test.go | 4 +- cli/rename.go | 21 ++++---- cli/session.go | 4 +- cli/target_test.go | 16 +++--- go.mod | 6 +-- internal/codegen/parser.go | 38 +++++++------- internal/commissioning/flow.go | 4 +- internal/commissioning/network.go | 2 +- internal/commissioning/network_test.go | 14 ++--- internal/controller/controller.go | 10 ++-- internal/controller/controller_test.go | 10 ++-- internal/crypto/mattercert.go | 32 ++++++------ internal/crypto/mattercert_test.go | 4 +- internal/daemon/protocol.go | 6 +-- internal/interaction/invoke.go | 4 +- internal/protocol/message_test.go | 24 ++++----- internal/secure/pase.go | 6 +-- internal/secure/pase_test.go | 2 +- internal/store/memory.go | 12 ++--- internal/store/store_test.go | 6 +-- internal/store/types.go | 6 +-- internal/transport/ble.go | 4 +- internal/transport/ble_corebt_darwin.go | 60 +--------------------- internal/transport/ble_corebt_other.go | 10 ---- internal/transport/ble_scanner_test.go | 30 +++++------ internal/transport/btp.go | 2 +- internal/transport/btp_test.go | 10 ++-- internal/transport/mrp.go | 6 +-- pkg/matter/matter_test.go | 6 +-- 35 files changed, 251 insertions(+), 244 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9aa5f33..01fffd3 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,7 @@ updates: directory: / schedule: interval: weekly + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..04a1a94 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: jdx/mise-action@v2 + with: + cache: true + + - name: Verify formatting + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "::error::The following files are not gofmt'd:" + echo "$unformatted" + exit 1 + fi + + - name: Run linter + run: mise run lint + + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - uses: actions/checkout@v6 + + - uses: jdx/mise-action@v2 + with: + cache: true + + - name: Run tests + run: mise run test + + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: jdx/mise-action@v2 + with: + cache: true + + - name: Build binary + run: mise run build diff --git a/cli/code.go b/cli/code.go index 6f72df3..d74dea1 100644 --- a/cli/code.go +++ b/cli/code.go @@ -148,8 +148,8 @@ func looksLikeQRPayload(s string) bool { func newCodeGenerateCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "generate", - Short: "Generate a QR code and manual pairing code from parameters", + Use: "generate", + Short: "Generate a QR code and manual pairing code from parameters", Example: ` matter code generate --vid 0xFFF1 --pid 0x8000 --passcode 12345678 --discriminator 3840`, RunE: func(cmd *cobra.Command, args []string) error { vid, _ := cmd.Flags().GetUint16("vid") diff --git a/cli/commission_window.go b/cli/commission_window.go index 6898d5b..b1154bf 100644 --- a/cli/commission_window.go +++ b/cli/commission_window.go @@ -18,8 +18,8 @@ import ( "github.com/p0fi/matter-cli/internal/daemon" "github.com/p0fi/matter-cli/internal/interaction" "github.com/p0fi/matter-cli/internal/protocol" - "github.com/p0fi/matter-cli/internal/tlv" "github.com/p0fi/matter-cli/internal/store" + "github.com/p0fi/matter-cli/internal/tlv" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -32,9 +32,9 @@ const timedInteractionTimeoutMs uint16 = 5_000 // Administrator Commissioning cluster-specific status codes (Matter spec §11.19.6). const ( - adminBusy uint8 = 0x02 - adminPAKEParamError uint8 = 0x03 - adminWindowNotOpen uint8 = 0x04 + adminBusy uint8 = 0x02 + adminPAKEParamError uint8 = 0x03 + adminWindowNotOpen uint8 = 0x04 ) // newCommissionOpenWindowCmd creates the `commission open-window` subcommand. diff --git a/cli/decommission.go b/cli/decommission.go index ef602e7..4205786 100644 --- a/cli/decommission.go +++ b/cli/decommission.go @@ -177,17 +177,18 @@ func readCurrentFabricIndex(ctx context.Context, nodeID uint64) (uint8, error) { if err != nil { return 0, err } - for _, r := range resp.Reports { - if r.StatusCode != 0 { - return 0, fmt.Errorf("status 0x%02X", r.StatusCode) - } - data, derr := daemon.DecodeFields(r.Data) - if derr != nil { - return 0, fmt.Errorf("decoding fields: %w", derr) - } - return decodeFabricIndex(data) + if len(resp.Reports) == 0 { + return 0, fmt.Errorf("no report data") } - return 0, fmt.Errorf("no report data") + r := resp.Reports[0] + if r.StatusCode != 0 { + return 0, fmt.Errorf("status 0x%02X", r.StatusCode) + } + data, derr := daemon.DecodeFields(r.Data) + if derr != nil { + return 0, fmt.Errorf("decoding fields: %w", derr) + } + return decodeFabricIndex(data) } client, session, cleanup, err := connectToNode(ctx, nodeID) @@ -201,13 +202,15 @@ func readCurrentFabricIndex(ctx context.Context, nodeID uint64) (uint8, error) { if err != nil { return 0, err } - for _, r := range reports { - if r.Status != nil { - return 0, fmt.Errorf("status 0x%02X", r.Status.Status.Status) - } - if r.Data != nil { - return decodeFabricIndex(r.Data.Data) - } + if len(reports) == 0 { + return 0, fmt.Errorf("no report data") + } + r := reports[0] + if r.Status != nil { + return 0, fmt.Errorf("status 0x%02X", r.Status.Status.Status) + } + if r.Data != nil { + return decodeFabricIndex(r.Data.Data) } return 0, fmt.Errorf("no report data") } @@ -385,4 +388,3 @@ func confirmForceDelete(cmd *cobra.Command, stepper *output.Stepper, force bool, answer := strings.TrimSpace(strings.ToLower(scanner.Text())) return answer == "y" || answer == "yes" } - diff --git a/cli/output/color.go b/cli/output/color.go index f432347..320d015 100644 --- a/cli/output/color.go +++ b/cli/output/color.go @@ -30,14 +30,14 @@ import ( var ( // Core colors — ANSI numbers, resolved by the terminal's own palette. - colorGreen = lipgloss.ANSIColor(10) // Bright Green → success - colorRed = lipgloss.ANSIColor(9) // Bright Red → errors - colorYellow = lipgloss.ANSIColor(3) // Yellow → warnings - colorBlue = lipgloss.ANSIColor(12) // Bright Blue → info / headers - colorCyan = lipgloss.ANSIColor(14) // Bright Cyan → labels / commands - colorMagenta = lipgloss.ANSIColor(13) // Bright Magenta→ accents / IDs + colorGreen = lipgloss.ANSIColor(10) // Bright Green → success + colorRed = lipgloss.ANSIColor(9) // Bright Red → errors + colorYellow = lipgloss.ANSIColor(3) // Yellow → warnings + colorBlue = lipgloss.ANSIColor(12) // Bright Blue → info / headers + colorCyan = lipgloss.ANSIColor(14) // Bright Cyan → labels / commands + colorMagenta = lipgloss.ANSIColor(13) // Bright Magenta→ accents / IDs colorLightGray = lipgloss.ANSIColor(7) // Light Gray → values - colorGray = lipgloss.ANSIColor(8) // Bright Black (Dark Gray) → muted/secondary + colorGray = lipgloss.ANSIColor(8) // Bright Black (Dark Gray) → muted/secondary // StyleSuccess renders text in green. StyleSuccess = lipgloss.NewStyle().Foreground(colorGreen) diff --git a/cli/output/tree_d2_test.go b/cli/output/tree_d2_test.go index 8db5092..445341d 100644 --- a/cli/output/tree_d2_test.go +++ b/cli/output/tree_d2_test.go @@ -209,8 +209,8 @@ func TestBuildD2Script_UtilityClusterOpacity(t *testing.T) { { ID: 0, Clusters: []TreeCluster{ - {ID: 0x001D, Name: "Descriptor"}, // utility - {ID: 0x0006, Name: "OnOff"}, // application + {ID: 0x001D, Name: "Descriptor"}, // utility + {ID: 0x0006, Name: "OnOff"}, // application }, }, }, diff --git a/cli/rename.go b/cli/rename.go index 71b05b5..12f1e49 100644 --- a/cli/rename.go +++ b/cli/rename.go @@ -281,17 +281,18 @@ func readProductName(ctx context.Context, nodeID uint64) (string, error) { if err != nil { return "", err } - for _, r := range resp.Reports { - if r.StatusCode != 0 { - return "", fmt.Errorf("status 0x%02X", r.StatusCode) - } - data, derr := daemon.DecodeFields(r.Data) - if derr != nil { - return "", fmt.Errorf("decoding ProductName: %w", derr) - } - return decodeTLVString(data) + if len(resp.Reports) == 0 { + return "", errors.New("no report data") + } + r := resp.Reports[0] + if r.StatusCode != 0 { + return "", fmt.Errorf("status 0x%02X", r.StatusCode) + } + data, derr := daemon.DecodeFields(r.Data) + if derr != nil { + return "", fmt.Errorf("decoding ProductName: %w", derr) } - return "", errors.New("no report data") + return decodeTLVString(data) } client, session, cleanup, err := connectToNode(ctx, nodeID) diff --git a/cli/session.go b/cli/session.go index 0cca912..7ef6c90 100644 --- a/cli/session.go +++ b/cli/session.go @@ -125,8 +125,8 @@ node will use the daemon's cached session instead of establishing a new one.`, // newSessionStopCmd creates `matter session stop`. func newSessionStopCmd() *cobra.Command { return &cobra.Command{ - Use: "stop", - Short: "Stop the background session daemon", + Use: "stop", + Short: "Stop the background session daemon", Example: ` matter session stop`, RunE: func(cmd *cobra.Command, args []string) error { w := cmd.OutOrStdout() diff --git a/cli/target_test.go b/cli/target_test.go index 1670355..a8aa7df 100644 --- a/cli/target_test.go +++ b/cli/target_test.go @@ -11,14 +11,14 @@ import ( func TestParseTarget(t *testing.T) { tests := []struct { - name string - input string - wantNodeID uint64 - wantEP uint16 - wantEPSet bool - wantExplicit bool - wantErr bool - errContains string + name string + input string + wantNodeID uint64 + wantEP uint16 + wantEPSet bool + wantExplicit bool + wantErr bool + errContains string }{ { name: "numeric node only", diff --git a/go.mod b/go.mod index 282a033..3614542 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,20 @@ module github.com/p0fi/matter-cli -go 1.25.3 +go 1.26.2 require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/term v0.2.1 github.com/grandcat/zeroconf v1.0.0 github.com/mattn/go-isatty v0.0.20 + github.com/mdp/qrterminal/v3 v3.2.1 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 go.etcd.io/bbolt v1.4.3 golang.org/x/crypto v0.48.0 oss.terrastruct.com/d2 v0.7.1 + rsc.io/qr v0.2.0 tinygo.org/x/bluetooth v0.14.0 ) @@ -40,7 +42,6 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mazznoer/csscolorparser v0.1.5 // indirect - github.com/mdp/qrterminal/v3 v3.2.1 // indirect github.com/miekg/dns v1.1.27 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -72,5 +73,4 @@ require ( gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a // indirect - rsc.io/qr v0.2.0 // indirect ) diff --git a/internal/codegen/parser.go b/internal/codegen/parser.go index 9e277f1..1736f35 100644 --- a/internal/codegen/parser.go +++ b/internal/codegen/parser.go @@ -17,14 +17,14 @@ import ( // XMLCluster is the root element. type XMLCluster struct { - XMLName xml.Name `xml:"cluster"` - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - ClusterIDs XMLClusterIDs `xml:"clusterIds"` - Features XMLFeatures `xml:"features"` - DataTypes XMLDataTypes `xml:"dataTypes"` - Attributes []XMLAttribute `xml:"attributes>attribute"` - Commands []XMLCommand `xml:"commands>command"` + XMLName xml.Name `xml:"cluster"` + ID string `xml:"id,attr"` + Name string `xml:"name,attr"` + ClusterIDs XMLClusterIDs `xml:"clusterIds"` + Features XMLFeatures `xml:"features"` + DataTypes XMLDataTypes `xml:"dataTypes"` + Attributes []XMLAttribute `xml:"attributes>attribute"` + Commands []XMLCommand `xml:"commands>command"` } // XMLFeatures contains the element. @@ -72,8 +72,8 @@ type XMLEnumItem struct { // XMLBitmap is a data type. type XMLBitmap struct { - Name string `xml:"name,attr"` - Bitfields []XMLBitfield `xml:"bitfield"` + Name string `xml:"name,attr"` + Bitfields []XMLBitfield `xml:"bitfield"` } // XMLBitfield is a single within a bitmap. @@ -90,10 +90,10 @@ type XMLStruct struct { // XMLAttribute is an element. type XMLAttribute struct { - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - Type string `xml:"type,attr"` - Access XMLAccess `xml:"access"` + ID string `xml:"id,attr"` + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Access XMLAccess `xml:"access"` Quality XMLQuality `xml:"quality"` } @@ -108,11 +108,11 @@ type XMLCommand struct { // XMLField is a element inside a command or struct. type XMLField struct { - ID string `xml:"id,attr"` - Name string `xml:"name,attr"` - Type string `xml:"type,attr"` - Quality XMLQuality `xml:"quality"` - OptionalConform *XMLOptConform `xml:"optionalConform"` + ID string `xml:"id,attr"` + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + Quality XMLQuality `xml:"quality"` + OptionalConform *XMLOptConform `xml:"optionalConform"` } // XMLOptConform exists when the field has . diff --git a/internal/commissioning/flow.go b/internal/commissioning/flow.go index 3539133..8a199ba 100644 --- a/internal/commissioning/flow.go +++ b/internal/commissioning/flow.go @@ -182,8 +182,8 @@ type CommissioningResult struct { // EndpointInfo describes a single endpoint discovered via the Descriptor cluster. type EndpointInfo struct { - ID uint16 - DeviceTypes []DeviceTypeInfo + ID uint16 + DeviceTypes []DeviceTypeInfo ServerClusters []uint32 } diff --git a/internal/commissioning/network.go b/internal/commissioning/network.go index f12769f..0966f1d 100644 --- a/internal/commissioning/network.go +++ b/internal/commissioning/network.go @@ -79,7 +79,7 @@ func NewThreadCredentials(dataset []byte) NetworkCredentials { // Thread operational dataset TLV type IDs (from the Thread specification). // Each TLV is encoded as: 1-byte type | 1-byte length | value. const ( - threadTLVActiveTimestamp = 0x0E // 8 bytes + threadTLVActiveTimestamp = 0x0E // 8 bytes threadTLVChannel = 0x00 // 3 bytes threadTLVChannelMask = 0x35 // variable threadTLVExtendedPANID = 0x02 // 8 bytes diff --git a/internal/commissioning/network_test.go b/internal/commissioning/network_test.go index 103781a..9111013 100644 --- a/internal/commissioning/network_test.go +++ b/internal/commissioning/network_test.go @@ -140,16 +140,16 @@ func TestValidateThreadDataset(t *testing.T) { t.Run("missing network key", func(t *testing.T) { // Build a dataset that is long enough but missing the Network Key TLV. var ds []byte - ds = append(ds, 0x00, 0x03, 0x00, 0x00, 0x0F) // Channel - ds = append(ds, 0x01, 0x02, 0xAB, 0xCD) // PAN ID + ds = append(ds, 0x00, 0x03, 0x00, 0x00, 0x0F) // Channel + ds = append(ds, 0x01, 0x02, 0xAB, 0xCD) // PAN ID ds = append(ds, 0x02, 0x08, 0xDE, 0xAD, 0x00, 0xBE, 0xEF, 0x00, 0xCA, 0xFE) // ExtPAN - ds = append(ds, 0x03, 0x07, 'T', 'e', 's', 't', 'N', 'e', 't') // Name - ds = append(ds, 0x04, 0x10) // PSKc - ds = append(ds, make([]byte, 16)...) // PSKc value + ds = append(ds, 0x03, 0x07, 'T', 'e', 's', 't', 'N', 'e', 't') // Name + ds = append(ds, 0x04, 0x10) // PSKc + ds = append(ds, make([]byte, 16)...) // PSKc value // Skip Network Key (type 0x05) ds = append(ds, 0x07, 0x08, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) // Mesh-Local - ds = append(ds, 0x0C, 0x03, 0x00, 0xF8, 0x00) // Security Policy - ds = append(ds, 0x0E, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00) // Timestamp + ds = append(ds, 0x0C, 0x03, 0x00, 0xF8, 0x00) // Security Policy + ds = append(ds, 0x0E, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00) // Timestamp err := ValidateThreadDataset(ds) if err == nil { diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 9b52360..8dc1a46 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -270,11 +270,11 @@ func (c *Controller) sendMRPAck(ctx context.Context, msg *protocol.Message) { SessionID: msg.Header.SessionID, }, Protocol: protocol.ProtocolHeader{ - ExchangeFlags: protocol.ExFlagACK, - ProtocolOpcode: 0x10, // MRP Standalone Ack - ProtocolID: 0x0000, // Secure Channel protocol - ExchangeID: msg.Protocol.ExchangeID, - HasAckCounter: true, + ExchangeFlags: protocol.ExFlagACK, + ProtocolOpcode: 0x10, // MRP Standalone Ack + ProtocolID: 0x0000, // Secure Channel protocol + ExchangeID: msg.Protocol.ExchangeID, + HasAckCounter: true, AckMessageCounter: msg.Header.MessageCounter, }, } diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 8362a60..39087b7 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -26,11 +26,11 @@ func (a *mockAddr) String() string { return a.address } // pipeConn is an in-memory transport.Conn that connects two ends via channels. type pipeConn struct { - send chan pipeMsg - recv chan pipeMsg - closed chan struct{} - once sync.Once - myAddr net.Addr + send chan pipeMsg + recv chan pipeMsg + closed chan struct{} + once sync.Once + myAddr net.Addr } type pipeMsg struct { diff --git a/internal/crypto/mattercert.go b/internal/crypto/mattercert.go index 6022156..6d92cd9 100644 --- a/internal/crypto/mattercert.go +++ b/internal/crypto/mattercert.go @@ -23,17 +23,17 @@ import ( // Matter TLV certificate field tags (from Matter spec section 6.6). const ( - certTagSerialNumber = 1 - certTagSigAlgo = 2 - certTagIssuer = 3 - certTagNotBefore = 4 - certTagNotAfter = 5 - certTagSubject = 6 - certTagPubKeyAlgo = 7 - certTagECCurveID = 8 - certTagECPubKey = 9 - certTagExtensions = 10 - certTagSignature = 11 + certTagSerialNumber = 1 + certTagSigAlgo = 2 + certTagIssuer = 3 + certTagNotBefore = 4 + certTagNotAfter = 5 + certTagSubject = 6 + certTagPubKeyAlgo = 7 + certTagECCurveID = 8 + certTagECPubKey = 9 + certTagExtensions = 10 + certTagSignature = 11 ) // Matter DN attribute tags. @@ -46,11 +46,11 @@ const ( // Matter extension tags. const ( - extTagBasicConstraints = 1 - extTagKeyUsage = 2 - extTagExtendedKeyUsage = 3 - extTagSubjectKeyID = 4 - extTagAuthorityKeyID = 5 + extTagBasicConstraints = 1 + extTagKeyUsage = 2 + extTagExtendedKeyUsage = 3 + extTagSubjectKeyID = 4 + extTagAuthorityKeyID = 5 ) // Matter algorithm constants. diff --git a/internal/crypto/mattercert_test.go b/internal/crypto/mattercert_test.go index 8136664..6697a83 100644 --- a/internal/crypto/mattercert_test.go +++ b/internal/crypto/mattercert_test.go @@ -400,8 +400,8 @@ func TestAddTrustedRootCertEncoding(t *testing.T) { Fields []byte `tlv:"1,rawstruct"` } type InvokeRequest struct { - SuppressResponse bool `tlv:"0,bool"` - TimedRequest bool `tlv:"1,bool"` + SuppressResponse bool `tlv:"0,bool"` + TimedRequest bool `tlv:"1,bool"` InvokeRequests []CommandDataIB `tlv:"2,array"` } diff --git a/internal/daemon/protocol.go b/internal/daemon/protocol.go index 238aa9c..b489c56 100644 --- a/internal/daemon/protocol.go +++ b/internal/daemon/protocol.go @@ -194,9 +194,9 @@ type StatusResp struct { // SessionInfo describes a single cached CASE session. type SessionInfo struct { - NodeID uint64 `json:"node_id"` - SessionID uint16 `json:"session_id"` - PeerAddress string `json:"peer_address"` + NodeID uint64 `json:"node_id"` + SessionID uint16 `json:"session_id"` + PeerAddress string `json:"peer_address"` Established Duration `json:"established"` } diff --git a/internal/interaction/invoke.go b/internal/interaction/invoke.go index 8d76d2a..18d8980 100644 --- a/internal/interaction/invoke.go +++ b/internal/interaction/invoke.go @@ -6,8 +6,8 @@ package interaction // InvokeRequest is the TLV structure for an Invoke Request message (opcode 0x08). // It carries one or more command invocations to execute on the peer. type InvokeRequest struct { - SuppressResponse bool `tlv:"0,bool"` - TimedRequest bool `tlv:"1,bool"` + SuppressResponse bool `tlv:"0,bool"` + TimedRequest bool `tlv:"1,bool"` InvokeRequests []CommandDataIB `tlv:"2,array"` } diff --git a/internal/protocol/message_test.go b/internal/protocol/message_test.go index a69d7dc..3168927 100644 --- a/internal/protocol/message_test.go +++ b/internal/protocol/message_test.go @@ -133,12 +133,12 @@ func TestProtocolHeaderRoundTrip(t *testing.T) { { name: "with ACK", header: ProtocolHeader{ - ExchangeFlags: ExFlagACK | ExFlagReliable, - ProtocolOpcode: 0x01, - ExchangeID: 42, - ProtocolID: 0x0001, + ExchangeFlags: ExFlagACK | ExFlagReliable, + ProtocolOpcode: 0x01, + ExchangeID: 42, + ProtocolID: 0x0001, AckMessageCounter: 99, - HasAckCounter: true, + HasAckCounter: true, }, }, { @@ -155,14 +155,14 @@ func TestProtocolHeaderRoundTrip(t *testing.T) { { name: "with vendor ID and ACK", header: ProtocolHeader{ - ExchangeFlags: ExFlagVendor | ExFlagACK | ExFlagInitiator, - ProtocolOpcode: 0x05, - ExchangeID: 200, - ProtocolID: 0x0002, - VendorID: 0x5678, - HasVendorID: true, + ExchangeFlags: ExFlagVendor | ExFlagACK | ExFlagInitiator, + ProtocolOpcode: 0x05, + ExchangeID: 200, + ProtocolID: 0x0002, + VendorID: 0x5678, + HasVendorID: true, AckMessageCounter: 12345, - HasAckCounter: true, + HasAckCounter: true, }, }, } diff --git a/internal/secure/pase.go b/internal/secure/pase.go index 485a0ea..0c29da0 100644 --- a/internal/secure/pase.go +++ b/internal/secure/pase.go @@ -96,9 +96,9 @@ type PASEInitiator struct { // State accumulated during the handshake. initiatorRandom []byte - spakeContext []byte // pbkdfReqBytes || pbkdfRespBytes - prover *crypto.SPAKE2PProver - sessionKeys *SessionKeys + spakeContext []byte // pbkdfReqBytes || pbkdfRespBytes + prover *crypto.SPAKE2PProver + sessionKeys *SessionKeys } // NewPASEInitiator creates a new PASE initiator (commissioner side) for the given diff --git a/internal/secure/pase_test.go b/internal/secure/pase_test.go index c08c651..af01466 100644 --- a/internal/secure/pase_test.go +++ b/internal/secure/pase_test.go @@ -232,7 +232,7 @@ func TestPASEMultiplePasscodes(t *testing.T) { passcodes := []uint32{20202021, 12345678, 1, 99999998} for _, passcode := range passcodes { - t.Run("passcode_" + itoa(passcode), func(t *testing.T) { + t.Run("passcode_"+itoa(passcode), func(t *testing.T) { initiator := NewPASEInitiator(passcode, 10) responder := NewPASEResponder(passcode, testPASESalt, testPASEIterations, 20) diff --git a/internal/store/memory.go b/internal/store/memory.go index 5a8e0f7..e7e6906 100644 --- a/internal/store/memory.go +++ b/internal/store/memory.go @@ -11,12 +11,12 @@ import ( // MemoryStore is a concurrency-safe, in-memory implementation of Store. It // never touches the filesystem and is intended for use in tests. type MemoryStore struct { - mu sync.RWMutex - fabrics map[uint64]*Fabric - nodes map[uint64]map[uint64]*Node // fabricID -> nodeID -> Node - resume map[uint64]*ResumptionInfo // peerNodeID -> info - kv map[string][]byte - closed bool + mu sync.RWMutex + fabrics map[uint64]*Fabric + nodes map[uint64]map[uint64]*Node // fabricID -> nodeID -> Node + resume map[uint64]*ResumptionInfo // peerNodeID -> info + kv map[string][]byte + closed bool } // NewMemoryStore returns a new empty MemoryStore. diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 828e8df..037d7d0 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -208,9 +208,9 @@ func storeTestSuite(t *testing.T, s Store) { t.Run("ResumptionInfo_CRUD", func(t *testing.T) { info := &ResumptionInfo{ - PeerNodeID: 42, - ResumptionID: []byte{1, 2, 3, 4}, - SharedSecret: []byte{5, 6, 7, 8}, + PeerNodeID: 42, + ResumptionID: []byte{1, 2, 3, 4}, + SharedSecret: []byte{5, 6, 7, 8}, CASESessionParams: []byte{9, 10}, } diff --git a/internal/store/types.go b/internal/store/types.go index fbf2924..d1a063e 100644 --- a/internal/store/types.go +++ b/internal/store/types.go @@ -54,8 +54,8 @@ type ClusterRef struct { // ResumptionInfo holds CASE session resumption data for a peer node. type ResumptionInfo struct { - PeerNodeID uint64 `json:"peer_node_id"` - ResumptionID []byte `json:"resumption_id"` - SharedSecret []byte `json:"shared_secret"` + PeerNodeID uint64 `json:"peer_node_id"` + ResumptionID []byte `json:"resumption_id"` + SharedSecret []byte `json:"shared_secret"` CASESessionParams []byte `json:"case_session_params"` } diff --git a/internal/transport/ble.go b/internal/transport/ble.go index 8f09dac..0e9fec9 100644 --- a/internal/transport/ble.go +++ b/internal/transport/ble.go @@ -223,8 +223,8 @@ func DialBLE(ctx context.Context, adapter bleAdapter, addr BLEAddress) (*BLEConn // subscribe is confirmed. We use both delivery paths (notification // callback and cached-value polling) and accept whichever fires first. const ( - btpHandshakeMaxAttempts = 5 - btpHandshakeRetryInterval = 3 * time.Second + btpHandshakeMaxAttempts = 5 + btpHandshakeRetryInterval = 3 * time.Second ) // Total handshake budget: 15 s from the start of this step. diff --git a/internal/transport/ble_corebt_darwin.go b/internal/transport/ble_corebt_darwin.go index 9cc937d..7b42164 100644 --- a/internal/transport/ble_corebt_darwin.go +++ b/internal/transport/ble_corebt_darwin.go @@ -83,24 +83,6 @@ static void ble_chr_clear_value(void *chr) { }); } -// ble_peripheral_can_send_without_response returns whether the peripheral -// owning chr is ready to accept Write Without Response operations. On macOS -// 10.13+ a Write Without Response is silently DROPPED (not queued) when this -// returns false. The caller must wait for this to become true before writing. -// -// The check reads peripheral.canSendWriteWithoutResponse from the current -// thread (outside bt_queue) as a best-effort hint; it is only used for -// diagnostic logging and as a pre-write guard. -static bool ble_peripheral_can_send_without_response(void *chr) { - if (chr == NULL) return true; - CBCharacteristic *c = (CBCharacteristic *)chr; - CBService *svc = c.service; - if (svc == nil) return true; - CBPeripheral *peripheral = svc.peripheral; - if (peripheral == nil) return true; - return peripheral.canSendWriteWithoutResponse; -} - // ble_peripheral_is_connected returns whether the peripheral owning chr is // currently in the CBPeripheralStateConnected state. Used to detect surprise // disconnects during the BTP handshake wait. @@ -273,34 +255,9 @@ static void * ble_chr_get_fresh_ptr(void *chr, bool *was_stale) { } // ────────────────────────────────────────────────────────────────────── -// CCCD subscribe / unsubscribe helpers +// CCCD subscribe helpers // ────────────────────────────────────────────────────────────────────── -// ble_chr_unsubscribe calls [peripheral setNotifyValue:NO forCharacteristic:] -// on bt_queue to cancel any pending or broken subscription. This is used as -// the first step of the repair path in WaitForNotifying: if tinygo's wrong- -// thread setNotifyValue:YES left CoreBluetooth in a partially-processed state, -// a subsequent setNotifyValue:YES from bt_queue may be deduplicated as a no-op. -// Sending setNotifyValue:NO first forces CoreBluetooth to clear that state. -// -// The characteristic is looked up fresh from svc.characteristics to avoid -// the stale-pointer issue (CBError code 8). -static bool ble_chr_unsubscribe(void *chr) { - if (chr == NULL || bt_queue == NULL) return false; - CBCharacteristic *stale = (CBCharacteristic *)chr; - CBService *svc = stale.service; - if (svc == nil) return false; - CBPeripheral *peripheral = svc.peripheral; - if (peripheral == nil) return false; - - CBCharacteristic *fresh = ble_find_fresh_characteristic(stale); - - dispatch_sync(bt_queue, ^{ - [peripheral setNotifyValue:NO forCharacteristic:fresh]; - }); - return true; -} - // ble_chr_subscribe calls [peripheral setNotifyValue:YES forCharacteristic:] // directly on the CBPeripheral that owns this characteristic, dispatched // synchronously on bt_queue (cbgo's CoreBluetooth dispatch queue). @@ -370,14 +327,6 @@ func corebtIsBTQueueInitialized() bool { return C.ble_is_bt_queue_initialized() != 0 } -// corebtCanSendWithoutResponse returns whether the peripheral owning chrPtr -// is ready to accept Write Without Response operations. On macOS 10.13+, -// writes are silently DROPPED when this is false. Reads canSendWriteWithoutResponse -// as a best-effort check from the current thread. -func corebtCanSendWithoutResponse(chrPtr unsafe.Pointer) bool { - return bool(C.ble_peripheral_can_send_without_response(chrPtr)) -} - // corebtPeripheralIsConnected returns whether the peripheral that owns chrPtr // is in the CBPeripheralStateConnected state. func corebtPeripheralIsConnected(chrPtr unsafe.Pointer) bool { @@ -442,13 +391,6 @@ func corebtSubscribe(chrPtr unsafe.Pointer) bool { return bool(C.ble_chr_subscribe(chrPtr)) } -// corebtUnsubscribe calls [peripheral setNotifyValue:NO forCharacteristic:] -// on bt_queue to cancel any pending or broken subscription before -// re-subscribing cleanly. -func corebtUnsubscribe(chrPtr unsafe.Pointer) bool { - return bool(C.ble_chr_unsubscribe(chrPtr)) -} - // corebtGetFreshPtr looks up the live CBCharacteristic from // svc.characteristics by UUID. If the cbgo-cached pointer is stale // (not pointer-identical to the live object), it returns the fresh pointer diff --git a/internal/transport/ble_corebt_other.go b/internal/transport/ble_corebt_other.go index f08047f..07ccde8 100644 --- a/internal/transport/ble_corebt_other.go +++ b/internal/transport/ble_corebt_other.go @@ -23,18 +23,12 @@ import ( // On these platforms rawPtr is always nil, so this is never called. func corebtIsNotifying(_ unsafe.Pointer) bool { return false } -// corebtCachedValue always returns nil on non-Darwin platforms. -func corebtCachedValue(_ unsafe.Pointer) []byte { return nil } - // corebtClearValue is a no-op on non-Darwin platforms. func corebtClearValue(_ unsafe.Pointer) {} // corebtSubscribe always returns false on non-Darwin platforms. func corebtSubscribe(_ unsafe.Pointer) bool { return false } -// corebtUnsubscribe always returns false on non-Darwin platforms. -func corebtUnsubscribe(_ unsafe.Pointer) bool { return false } - // corebtGetFreshPtr returns the pointer unchanged on non-Darwin platforms. // wasStale is always false since there is no CoreBluetooth object graph to // check against. @@ -76,10 +70,6 @@ func corebtWriteWithResponse(_ unsafe.Pointer, _ []byte) int { return -1 } // corebtIsBTQueueInitialized always returns false on non-Darwin platforms. func corebtIsBTQueueInitialized() bool { return false } -// corebtCanSendWithoutResponse always returns true on non-Darwin platforms so -// callers proceed without waiting. rawPtr is always nil on these platforms. -func corebtCanSendWithoutResponse(_ unsafe.Pointer) bool { return true } - // corebtPeripheralIsConnected always returns true on non-Darwin platforms so // the disconnect guard in corebtPollValue never fires. rawPtr is always nil. func corebtPeripheralIsConnected(_ unsafe.Pointer) bool { return true } diff --git a/internal/transport/ble_scanner_test.go b/internal/transport/ble_scanner_test.go index 30dfb22..08a0c97 100644 --- a/internal/transport/ble_scanner_test.go +++ b/internal/transport/ble_scanner_test.go @@ -178,15 +178,15 @@ func (s *mockBLEService) DiscoverCharacteristics(uuids []BLEUUID) ([]bleCharacte } type mockBLECharacteristic struct { - uuid BLEUUID - writeData [][]byte - writeMu sync.Mutex - writeErr error - notifCb func([]byte) - notifMu sync.RWMutex - enableNotifErr error - waitCh chan []byte // delivers data for WaitForValue - disconnected atomic.Bool // when true, IsConnected() returns false + uuid BLEUUID + writeData [][]byte + writeMu sync.Mutex + writeErr error + notifCb func([]byte) + notifMu sync.RWMutex + enableNotifErr error + waitCh chan []byte // delivers data for WaitForValue + disconnected atomic.Bool // when true, IsConnected() returns false } func (c *mockBLECharacteristic) UUID() BLEUUID { return c.uuid } @@ -521,9 +521,9 @@ func TestBLEScanner_Scan_SingleMatterDevice(t *testing.T) { func TestBLEScanner_Scan_IgnoresNonMatterDevices(t *testing.T) { nonMatter := BLEScanAdvertisement{ - Address: "11:22:33:44:55:66", - RSSI: -50, - LocalName: "SomeFitnessBand", + Address: "11:22:33:44:55:66", + RSSI: -50, + LocalName: "SomeFitnessBand", ServiceData: map[BLEUUID][]byte{ // Different service UUID, not Matter. "00001800-0000-1000-8000-00805f9b34fb": {0x01, 0x02}, @@ -847,7 +847,7 @@ func TestMatterServiceUUIDs_AreCorrect(t *testing.T) { // ─── Mock adapter interface compliance ─────────────────────────────────────── // Compile-time checks: ensure mock types satisfy the interfaces. -var _ bleAdapter = (*mockBLEAdapter)(nil) -var _ bleDevice = (*mockBLEDevice)(nil) -var _ bleService = (*mockBLEService)(nil) +var _ bleAdapter = (*mockBLEAdapter)(nil) +var _ bleDevice = (*mockBLEDevice)(nil) +var _ bleService = (*mockBLEService)(nil) var _ bleCharacteristic = (*mockBLECharacteristic)(nil) diff --git a/internal/transport/btp.go b/internal/transport/btp.go index 636c7b9..1be67f1 100644 --- a/internal/transport/btp.go +++ b/internal/transport/btp.go @@ -147,7 +147,7 @@ func btpHandshakeRequest(versions []uint8, attMTU uint16, windowSize uint8) []by for i := 0; i < btpMaxVersionSlots && i < len(versions); i++ { byteIdx := 2 + i/2 if i%2 == 0 { - out[byteIdx] |= versions[i] & 0x0F // low nibble + out[byteIdx] |= versions[i] & 0x0F // low nibble } else { out[byteIdx] |= (versions[i] & 0x0F) << 4 // high nibble } diff --git a/internal/transport/btp_test.go b/internal/transport/btp_test.go index c9ede22..c6505a0 100644 --- a/internal/transport/btp_test.go +++ b/internal/transport/btp_test.go @@ -33,8 +33,8 @@ func TestBTPHandshakeRequest(t *testing.T) { 0x65, // magic byte 1 0x6C, // magic byte 2 0x04, 0x00, 0x00, 0x00, // versions: slot0=4 - 0xF7, 0x00, // ATT MTU: 247 (LE) - 0x06, // window size: 6 + 0xF7, 0x00, // ATT MTU: 247 (LE) + 0x06, // window size: 6 }, }, { @@ -99,7 +99,7 @@ func TestParseBTPHandshakeResponse_Valid(t *testing.T) { 0x6C, // magic byte 2 0x04, // selected version: 4 0x14, 0x00, // fragment size: 20 (LE) - 0x06, // window size: 6 + 0x06, // window size: 6 }, wantVersion: 4, wantFragmentSize: 20, @@ -873,7 +873,7 @@ func TestHandleSegment_MessageLengthMismatch(t *testing.T) { // Build a B+E segment that declares MsgLen=10 but carries only 5 bytes. var buf bytes.Buffer buf.WriteByte(btpFlagBegin | btpFlagEnd) // flags - buf.WriteByte(0x00) // seqNum + buf.WriteByte(0x00) // seqNum // MsgLen = 10 but payload is only 5 bytes. buf.WriteByte(0x0A) // MsgLen lo = 10 buf.WriteByte(0x00) // MsgLen hi = 0 @@ -1071,7 +1071,7 @@ func TestFlowControl_ProcessAck_SeqNumWrap(t *testing.T) { func TestFlowControl_WaitCanSend_UnblocksOnAck(t *testing.T) { s := newBTPSession() s.windowSize = 2 - s.txInflight = 2 // window full + s.txInflight = 2 // window full s.localSeq = 2 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) diff --git a/internal/transport/mrp.go b/internal/transport/mrp.go index 2684cdd..2e0da60 100644 --- a/internal/transport/mrp.go +++ b/internal/transport/mrp.go @@ -46,9 +46,9 @@ type MRP struct { conn Conn config MRPConfig - mu sync.Mutex - pending map[uint32]*pendingMessage - closed chan struct{} + mu sync.Mutex + pending map[uint32]*pendingMessage + closed chan struct{} closeOnce sync.Once } diff --git a/pkg/matter/matter_test.go b/pkg/matter/matter_test.go index 249a61c..16ab703 100644 --- a/pkg/matter/matter_test.go +++ b/pkg/matter/matter_test.go @@ -30,9 +30,9 @@ func TestNewClientInMemory(t *testing.T) { func TestLookupCluster(t *testing.T) { tests := []struct { - name string - wantID uint32 - wantOK bool + name string + wantID uint32 + wantOK bool }{ {"OnOff", 0x0006, true}, {"LevelControl", 0x0008, true}, From d52303ca7c280f8e5de4988c98f6c7e07f68498f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:00:23 +0200 Subject: [PATCH 05/10] chore(deps): bump jdx/mise-action from 2 to 4 (#65) Bumps [jdx/mise-action](https://github.com/jdx/mise-action) from 2 to 4. - [Release notes](https://github.com/jdx/mise-action/releases) - [Changelog](https://github.com/jdx/mise-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/jdx/mise-action/compare/v2...v4) --- updated-dependencies: - dependency-name: jdx/mise-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04a1a94..8e0d58c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: jdx/mise-action@v2 + - uses: jdx/mise-action@v4 with: cache: true @@ -45,7 +45,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: jdx/mise-action@v2 + - uses: jdx/mise-action@v4 with: cache: true @@ -60,7 +60,7 @@ jobs: with: fetch-depth: 0 - - uses: jdx/mise-action@v2 + - uses: jdx/mise-action@v4 with: cache: true From 50297ad75238d5080398bbb977eacc25d0326fcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:21:59 +0200 Subject: [PATCH 06/10] chore(deps): bump github.com/charmbracelet/x/term from 0.2.1 to 0.2.2 (#55) Bumps [github.com/charmbracelet/x/term](https://github.com/charmbracelet/x) from 0.2.1 to 0.2.2. - [Commits](https://github.com/charmbracelet/x/compare/ansi/v0.2.1...ansi/v0.2.2) --- updated-dependencies: - dependency-name: github.com/charmbracelet/x/term dependency-version: 0.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3614542..7117376 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.2 require ( github.com/charmbracelet/lipgloss v1.1.0 - github.com/charmbracelet/x/term v0.2.1 + github.com/charmbracelet/x/term v0.2.2 github.com/grandcat/zeroconf v1.0.0 github.com/mattn/go-isatty v0.0.20 github.com/mdp/qrterminal/v3 v3.2.1 diff --git a/go.sum b/go.sum index 6d1ee21..99694b3 100644 --- a/go.sum +++ b/go.sum @@ -24,8 +24,8 @@ github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2ll github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= From b9dd280262a4a2a0e8e56159847e63d371cd47c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:22:24 +0200 Subject: [PATCH 07/10] chore(deps): bump golang.org/x/crypto from 0.48.0 to 0.50.0 (#54) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.48.0 to 0.50.0. - [Commits](https://github.com/golang/crypto/compare/v0.48.0...v0.50.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.50.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 10 +++++----- go.sum | 24 ++++++++++++------------ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 7117376..92697a4 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 go.etcd.io/bbolt v1.4.3 - golang.org/x/crypto v0.48.0 + golang.org/x/crypto v0.50.0 oss.terrastruct.com/d2 v0.7.1 rsc.io/qr v0.2.0 tinygo.org/x/bluetooth v0.14.0 @@ -65,10 +65,10 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect golang.org/x/image v0.20.0 // indirect - golang.org/x/net v0.49.0 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/term v0.40.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 99694b3..95b0854 100644 --- a/go.sum +++ b/go.sum @@ -143,8 +143,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= @@ -160,13 +160,13 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -180,21 +180,21 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= From d9f9f5a5603ee9dbd10667422d0454cc455431c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:28:30 +0200 Subject: [PATCH 08/10] chore(deps): bump tinygo.org/x/bluetooth from 0.14.0 to 0.15.0 (#53) Bumps [tinygo.org/x/bluetooth](https://github.com/tinygo-org/bluetooth) from 0.14.0 to 0.15.0. - [Release notes](https://github.com/tinygo-org/bluetooth/releases) - [Changelog](https://github.com/tinygo-org/bluetooth/blob/dev/CHANGELOG.md) - [Commits](https://github.com/tinygo-org/bluetooth/compare/v0.14.0...v0.15.0) --- updated-dependencies: - dependency-name: tinygo.org/x/bluetooth dependency-version: 0.15.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 9 +++++---- go.sum | 18 ++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 92697a4..315c57c 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( golang.org/x/crypto v0.50.0 oss.terrastruct.com/d2 v0.7.1 rsc.io/qr v0.2.0 - tinygo.org/x/bluetooth v0.14.0 + tinygo.org/x/bluetooth v0.15.0 ) require ( @@ -49,17 +49,18 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b // indirect + github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect - github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af // indirect + github.com/soypat/cyw43439 v0.1.0 // indirect + github.com/soypat/lneto v0.1.0 // indirect github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tinygo-org/cbgo v0.0.4 // indirect - github.com/tinygo-org/pio v0.2.0 // indirect + github.com/tinygo-org/pio v0.3.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yuin/goldmark v1.7.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect diff --git a/go.sum b/go.sum index 95b0854..c6fa9d7 100644 --- a/go.sum +++ b/go.sum @@ -94,15 +94,17 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik= -github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= +github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96 h1:IXxzj3yjfDNXZJ35foY+RpFShqPsZZ81hhCckgfh5PI= +github.com/saltosystems/winrt-go v0.0.0-20260317170058-9c2fec580d96/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= -github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af h1:ZfFq94aH/BCSWWKd9RPUgdHOdgGKCnfl2VdvU9UksTA= -github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af/go.mod h1:MUaGO5m6X7xrkHrPDmnaxCEcuCCFN/0ZFh9oie+exbU= +github.com/soypat/cyw43439 v0.1.0 h1:3Nyqg2LSndhCYgCr2VXuL2nn73vyaJXAnD02veMoLvA= +github.com/soypat/cyw43439 v0.1.0/go.mod h1:R2uSILRwSPmcmmKy5Z0FtK4ypgiPf5YqK+F+IKmXqxc= +github.com/soypat/lneto v0.1.0 h1:VAHCJ33hvC3wDqhM0Vm7w0k6vwNsOCAsQ8XTrXJpS7I= +github.com/soypat/lneto v0.1.0/go.mod h1:g/8Lk+hIsMZydyWDJjK2YfsCuG6jA5mWCO6U+4S7w1U= github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA= github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -125,8 +127,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU= github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk= -github.com/tinygo-org/pio v0.2.0 h1:vo3xa6xDZ2rVtxrks/KcTZHF3qq4lyWOntvEvl2pOhU= -github.com/tinygo-org/pio v0.2.0/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8= +github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI= +github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= @@ -218,5 +220,5 @@ oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a h1:UXF/Z9i9tOx/wq oss.terrastruct.com/util-go v0.0.0-20250213174338-243d8661088a/go.mod h1:eMWv0sOtD9T2RUl90DLWfuShZCYp4NrsqNpI8eqO6U4= rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= -tinygo.org/x/bluetooth v0.14.0 h1:rrUaT+Fu6O0phGm4Y5UZULL8F7UahOq/JwGAPjJm+V4= -tinygo.org/x/bluetooth v0.14.0/go.mod h1:YnyJRVX09i+wkFeHpXut0b+qHq+T2WwKBRRiF/scANA= +tinygo.org/x/bluetooth v0.15.0 h1:hLn8+iZFXvVxBzPIdZfvc6TD8JP32ixF22lCEWHAbIo= +tinygo.org/x/bluetooth v0.15.0/go.mod h1:meayNB+9rC1igTUNmNU7KftlSEzrFHe37rBSQZjHN8Y= From dd4fdd92a41d986b23d06c3f227d313536b93690 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:17:17 +0200 Subject: [PATCH 09/10] chore(deps): bump github.com/mattn/go-isatty from 0.0.20 to 0.0.21 (#52) Bumps [github.com/mattn/go-isatty](https://github.com/mattn/go-isatty) from 0.0.20 to 0.0.21. - [Commits](https://github.com/mattn/go-isatty/compare/v0.0.20...v0.0.21) --- updated-dependencies: - dependency-name: github.com/mattn/go-isatty dependency-version: 0.0.21 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 315c57c..1ad7086 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/charmbracelet/x/term v0.2.2 github.com/grandcat/zeroconf v1.0.0 - github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-isatty v0.0.21 github.com/mdp/qrterminal/v3 v3.2.1 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 diff --git a/go.sum b/go.sum index c6fa9d7..1cdf2c0 100644 --- a/go.sum +++ b/go.sum @@ -68,8 +68,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= +github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mazznoer/csscolorparser v0.1.5 h1:Wr4uNIE+pHWN3TqZn2SGpA2nLRG064gB7WdSfSS5cz4= @@ -180,7 +180,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= From 4740397e4c139da7dbb43a33f84dd0a5c1afad24 Mon Sep 17 00:00:00 2001 From: Thomas Hartwig Date: Thu, 23 Apr 2026 21:21:44 +0200 Subject: [PATCH 10/10] fix(completion): clarify Windows behavior in help text and deduplicate shell list - Document in Long/Example that Windows no-arg invocation always uses PowerShell - Introduce supportedShells var as single source of truth for ValidArgs and detectShell error messages --- cli/completion.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cli/completion.go b/cli/completion.go index f6f9ef6..fbb2d9b 100644 --- a/cli/completion.go +++ b/cli/completion.go @@ -17,6 +17,10 @@ import ( "github.com/spf13/cobra" ) +// supportedShells is the single source of truth for shells that matter completion supports. +// Used for both ValidArgs and error messages in detectShell. +var supportedShells = []string{"bash", "zsh", "fish", "powershell"} + // newCompletionCmd creates the `matter completion` subcommand that generates // and optionally installs shell completion scripts for bash, zsh, fish, and // powershell. @@ -27,13 +31,17 @@ func newCompletionCmd() *cobra.Command { Long: `Generate shell completion scripts for matter. With no arguments, matter completion auto-detects your shell from $SHELL and -installs the completion script automatically. +installs the completion script automatically. On Windows, no-argument +invocation always installs PowerShell completions regardless of $SHELL. Specify a shell explicitly to print its completion script to stdout. Add --install to install explicitly for a given shell.`, Example: ` # Auto-detect shell and install completions (recommended) matter completion + # On Windows: always installs PowerShell completions + matter completion + # Print zsh completions to stdout matter completion zsh @@ -45,7 +53,7 @@ Specify a shell explicitly to print its completion script to stdout. Add # Install fish completions matter completion fish --install`, - ValidArgs: []string{"bash", "zsh", "fish", "powershell"}, + ValidArgs: supportedShells, Args: cobra.MatchAll(cobra.MaximumNArgs(1), cobra.OnlyValidArgs), RunE: runCompletion, } @@ -82,14 +90,14 @@ func detectShell() (string, error) { } shellEnv := os.Getenv("SHELL") if shellEnv == "" { - return "", fmt.Errorf("could not detect your shell — please specify one: bash, zsh, fish, powershell") + return "", fmt.Errorf("could not detect your shell — please specify one: %s", strings.Join(supportedShells, ", ")) } name := filepath.Base(shellEnv) switch name { case "bash", "zsh", "fish": return name, nil default: - return "", fmt.Errorf("unsupported shell %q — please specify one: bash, zsh, fish, powershell", name) + return "", fmt.Errorf("unsupported shell %q — please specify one: %s", name, strings.Join(supportedShells, ", ")) } }