[Linux] Initial implementation of fabric-admin to facilitate Fabric Synchronization (#33393)

* Initial implementation of fabric-admin app

* Address review comments
diff --git a/examples/fabric-admin/.gn b/examples/fabric-admin/.gn
new file mode 100644
index 0000000..3b11e2b
--- /dev/null
+++ b/examples/fabric-admin/.gn
@@ -0,0 +1,25 @@
+# Copyright (c) 2024 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build_overrides/build.gni")
+
+# The location of the build configuration file.
+buildconfig = "${build_root}/config/BUILDCONFIG.gn"
+
+# CHIP uses angle bracket includes.
+check_system_includes = true
+
+default_args = {
+  import("//args.gni")
+}
diff --git a/examples/fabric-admin/BUILD.gn b/examples/fabric-admin/BUILD.gn
new file mode 100644
index 0000000..79e175f
--- /dev/null
+++ b/examples/fabric-admin/BUILD.gn
@@ -0,0 +1,120 @@
+# Copyright (c) 2024 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build_overrides/build.gni")
+import("//build_overrides/chip.gni")
+
+import("//build_overrides/editline.gni")
+import("${chip_root}/build/chip/tools.gni")
+import("${chip_root}/examples/fabric-admin/fabric-admin.gni")
+import("${chip_root}/src/lib/core/core.gni")
+
+assert(chip_build_tools)
+
+config("config") {
+  include_dirs = [
+    ".",
+    "${chip_root}/examples/common",
+    "${chip_root}/zzz_generated/app-common/app-common",
+    "${chip_root}/zzz_generated/chip-tool",
+    "${chip_root}/src/lib",
+  ]
+
+  defines = [ "CONFIG_USE_SEPARATE_EVENTLOOP=${config_use_separate_eventloop}" ]
+
+  # Note: CONFIG_USE_LOCAL_STORAGE is tested for via #ifdef, not #if.
+  if (config_use_local_storage) {
+    defines += [ "CONFIG_USE_LOCAL_STORAGE" ]
+  }
+
+  cflags = [ "-Wconversion" ]
+}
+
+static_library("fabric-admin-utils") {
+  sources = [
+    "${chip_root}/src/controller/ExamplePersistentStorage.cpp",
+    "${chip_root}/src/controller/ExamplePersistentStorage.h",
+    "${chip_root}/zzz_generated/chip-tool/zap-generated/cluster/ComplexArgumentParser.cpp",
+    "${chip_root}/zzz_generated/chip-tool/zap-generated/cluster/logging/DataModelLogger.cpp",
+    "commands/clusters/ModelCommand.cpp",
+    "commands/clusters/ModelCommand.h",
+    "commands/common/CHIPCommand.cpp",
+    "commands/common/CHIPCommand.h",
+    "commands/common/Command.cpp",
+    "commands/common/Command.h",
+    "commands/common/Commands.cpp",
+    "commands/common/Commands.h",
+    "commands/common/CredentialIssuerCommands.h",
+    "commands/common/HexConversion.h",
+    "commands/common/RemoteDataModelLogger.cpp",
+    "commands/common/RemoteDataModelLogger.h",
+    "commands/pairing/OpenCommissioningWindowCommand.cpp",
+    "commands/pairing/OpenCommissioningWindowCommand.h",
+    "commands/pairing/PairingCommand.cpp",
+    "commands/pairing/ToTLVCert.cpp",
+  ]
+
+  deps = [ "${chip_root}/src/app:events" ]
+
+  sources += [ "commands/interactive/InteractiveCommands.cpp" ]
+  deps += [
+    "${chip_root}/examples/common/websocket-server",
+    "${chip_root}/src/platform/logging:headers",
+    "${editline_root}:editline",
+  ]
+
+  if (chip_device_platform == "darwin") {
+    sources += [ "commands/common/DeviceScanner.cpp" ]
+  }
+
+  public_deps = [
+    "${chip_root}/examples/common/tracing:commandline",
+    "${chip_root}/src/app/icd/client:handler",
+    "${chip_root}/src/app/icd/client:manager",
+    "${chip_root}/src/app/server",
+    "${chip_root}/src/app/tests/suites/commands/interaction_model",
+    "${chip_root}/src/controller/data_model",
+    "${chip_root}/src/credentials:file_attestation_trust_store",
+    "${chip_root}/src/lib",
+    "${chip_root}/src/lib/core:types",
+    "${chip_root}/src/lib/support/jsontlv",
+    "${chip_root}/src/platform",
+    "${chip_root}/third_party/inipp",
+    "${chip_root}/third_party/jsoncpp",
+  ]
+
+  public_configs = [ ":config" ]
+
+  if (chip_enable_transport_trace) {
+    public_deps +=
+        [ "${chip_root}/examples/common/tracing:trace_handlers_decoder" ]
+  }
+
+  output_dir = root_out_dir
+}
+
+executable("fabric-admin") {
+  sources = [ "main.cpp" ]
+
+  deps = [
+    ":fabric-admin-utils",
+    "${chip_root}/src/platform/logging:force_stdio",
+  ]
+
+  output_dir = root_out_dir
+}
+
+group("default") {
+  deps = [ ":fabric-admin" ]
+}
diff --git a/examples/fabric-admin/args.gni b/examples/fabric-admin/args.gni
new file mode 100644
index 0000000..83300d7
--- /dev/null
+++ b/examples/fabric-admin/args.gni
@@ -0,0 +1,34 @@
+# Copyright (c) 2024 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build_overrides/chip.gni")
+
+import("${chip_root}/config/standalone/args.gni")
+
+chip_device_project_config_include = "<CHIPProjectAppConfig.h>"
+chip_project_config_include = "<CHIPProjectAppConfig.h>"
+chip_system_project_config_include = "<SystemProjectConfig.h>"
+
+chip_project_config_include_dirs =
+    [ "${chip_root}/examples/fabric-admin/include" ]
+chip_project_config_include_dirs += [ "${chip_root}/config/standalone" ]
+
+matter_enable_tracing_support = true
+
+matter_log_json_payload_hex = true
+matter_log_json_payload_decode_full = true
+
+# make fabric-admin very strict by default
+chip_tlv_validate_char_string_on_read = true
+chip_tlv_validate_char_string_on_write = true
diff --git a/examples/fabric-admin/build_overrides b/examples/fabric-admin/build_overrides
new file mode 120000
index 0000000..b430cf6
--- /dev/null
+++ b/examples/fabric-admin/build_overrides
@@ -0,0 +1 @@
+../build_overrides
\ No newline at end of file
diff --git a/examples/fabric-admin/commands/clusters/ClusterCommand.h b/examples/fabric-admin/commands/clusters/ClusterCommand.h
new file mode 100644
index 0000000..4865f05
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ClusterCommand.h
@@ -0,0 +1,196 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <app/tests/suites/commands/interaction_model/InteractionModel.h>
+
+#include "DataModelLogger.h"
+#include "ModelCommand.h"
+
+class ClusterCommand : public InteractionModelCommands, public ModelCommand, public chip::app::CommandSender::Callback
+{
+public:
+    ClusterCommand(CredentialIssuerCommands * credsIssuerConfig) :
+        InteractionModelCommands(this), ModelCommand("command-by-id", credsIssuerConfig)
+    {
+        AddArgument("cluster-id", 0, UINT32_MAX, &mClusterId);
+        AddByIdArguments();
+        AddArguments();
+    }
+
+    ClusterCommand(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+        InteractionModelCommands(this), ModelCommand("command-by-id", credsIssuerConfig), mClusterId(clusterId)
+    {
+        AddByIdArguments();
+        AddArguments();
+    }
+
+    ~ClusterCommand() {}
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        return InteractionModelCommands::SendCommand(device, endpointIds.at(0), mClusterId, mCommandId, mPayload);
+    }
+
+    template <class T>
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, chip::EndpointId endpointId, chip::ClusterId clusterId,
+                           chip::CommandId commandId, const T & value)
+    {
+        return InteractionModelCommands::SendCommand(device, endpointId, clusterId, commandId, value);
+    }
+
+    CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex) override
+    {
+        return InteractionModelCommands::SendGroupCommand(groupId, fabricIndex, mClusterId, mCommandId, mPayload);
+    }
+
+    template <class T>
+    CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex, chip::ClusterId clusterId,
+                                chip::CommandId commandId, const T & value)
+    {
+        return InteractionModelCommands::SendGroupCommand(groupId, fabricIndex, clusterId, commandId, value);
+    }
+
+    /////////// CommandSender Callback Interface /////////
+    virtual void OnResponse(chip::app::CommandSender * client, const chip::app::ConcreteCommandPath & path,
+                            const chip::app::StatusIB & status, chip::TLV::TLVReader * data) override
+    {
+        CHIP_ERROR error = status.ToChipError();
+        if (CHIP_NO_ERROR != error)
+        {
+            LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(path, status));
+
+            ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error));
+            mError = error;
+            return;
+        }
+
+        if (data != nullptr)
+        {
+            LogErrorOnFailure(RemoteDataModelLogger::LogCommandAsJSON(path, data));
+
+            error = DataModelLogger::LogCommand(path, data);
+            if (CHIP_NO_ERROR != error)
+            {
+                ChipLogError(NotSpecified, "Response Failure: Can not decode Data");
+                mError = error;
+                return;
+            }
+        }
+    }
+
+    virtual void OnError(const chip::app::CommandSender * client, CHIP_ERROR error) override
+    {
+        LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(error));
+
+        ChipLogProgress(NotSpecified, "Error: %s", chip::ErrorStr(error));
+        mError = error;
+    }
+
+    virtual void OnDone(chip::app::CommandSender * client) override
+    {
+        if (mCommandSender.size())
+        {
+            mCommandSender.front().reset();
+            mCommandSender.erase(mCommandSender.begin());
+        }
+
+        // If the command is repeated N times, wait for all the responses to comes in
+        // before exiting.
+        bool shouldStop = true;
+        if (mRepeatCount.HasValue())
+        {
+            mRepeatCount.SetValue(static_cast<uint16_t>(mRepeatCount.Value() - 1));
+            shouldStop = mRepeatCount.Value() == 0;
+        }
+
+        if (shouldStop)
+        {
+            SetCommandExitStatus(mError);
+        }
+    }
+
+    void Shutdown() override
+    {
+        mError = CHIP_NO_ERROR;
+        ModelCommand::Shutdown();
+    }
+
+protected:
+    ClusterCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) :
+        InteractionModelCommands(this), ModelCommand(commandName, credsIssuerConfig)
+    {
+        // Subclasses are responsible for calling AddArguments.
+    }
+
+    void AddByIdArguments()
+    {
+        AddArgument("command-id", 0, UINT32_MAX, &mCommandId);
+        AddArgument("payload", &mPayload,
+                    "The command payload.  This should be a JSON-encoded object, with string representations of field ids as keys. "
+                    " The values for the keys are represented as follows, depending on the type:\n"
+                    "  * struct: a JSON-encoded object, with field ids as keys.\n"
+                    "  * list: a JSON-encoded array of values.\n"
+                    "  * null: A literal null.\n"
+                    "  * boolean: A literal true or false.\n"
+                    "  * unsigned integer: One of:\n"
+                    "      a) The number directly, as decimal.\n"
+                    "      b) A string starting with \"u:\" followed by decimal digits\n"
+                    "  * signed integer: One of:\n"
+                    "      a) The number directly, if it's negative.\n"
+                    "      b) A string starting with \"s:\" followed by decimal digits\n"
+                    "  * single-precision float: A string starting with \"f:\" followed by the number.\n"
+                    "  * double-precision float: One of:\n"
+                    "      a) The number directly, if it's not an integer.\n"
+                    "      b) A string starting with \"d:\" followed by the number.\n"
+                    "  * octet string: A string starting with \"hex:\" followed by the hex encoding of the bytes.\n"
+                    "  * string: A string with the characters.\n"
+                    "\n"
+                    "  An example payload may look like this: '{ \"0x0\": { \"0\": null, \"1\": false }, \"1\": [17, \"u:17\"], "
+                    "\"0x2\": [ -17, \"s:17\", \"s:-17\" ], \"0x3\": \"f:2\", \"0x4\": [ \"d:3\", 4.5 ], \"0x5\": \"hex:ab12\", "
+                    "\"0x6\": \"ab12\" }' and represents:\n"
+                    "    Field 0: a struct with two fields, one with value null and one with value false.\n"
+                    "    Field 1: A list of unsigned integers.\n"
+                    "    Field 2: A list of signed integers.\n"
+                    "    Field 3: A single-precision float.\n"
+                    "    Field 4: A list of double-precision floats.\n"
+                    "    Field 5: A 2-byte octet string.\n"
+                    "    Field 6: A 4-char character string.");
+    }
+
+    void AddArguments()
+    {
+        AddArgument("timedInteractionTimeoutMs", 0, UINT16_MAX, &mTimedInteractionTimeoutMs,
+                    "If provided, do a timed invoke with the given timed interaction timeout. See \"7.6.10. Timed Interaction\" in "
+                    "the Matter specification.");
+        AddArgument("busyWaitForMs", 0, UINT16_MAX, &mBusyWaitForMs,
+                    "If provided, block the main thread processing for the given time right after sending a command.");
+        AddArgument("suppressResponse", 0, 1, &mSuppressResponse);
+        AddArgument("repeat-count", 1, UINT16_MAX, &mRepeatCount);
+        AddArgument("repeat-delay-ms", 0, UINT16_MAX, &mRepeatDelayInMs);
+        ModelCommand::AddArguments();
+    }
+
+private:
+    chip::ClusterId mClusterId;
+    chip::CommandId mCommandId;
+
+    CHIP_ERROR mError = CHIP_NO_ERROR;
+    CustomArgument mPayload;
+};
diff --git a/examples/fabric-admin/commands/clusters/ComplexArgument.h b/examples/fabric-admin/commands/clusters/ComplexArgument.h
new file mode 100644
index 0000000..954ea1d
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ComplexArgument.h
@@ -0,0 +1,422 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+/**
+ * This file allocate/free memory using the chip platform abstractions
+ * (Platform::MemoryCalloc and Platform::MemoryFree) for hosting a subset of the
+ * data model internal types until they are consumed by the DataModel::Encode machinery:
+ *   - chip::app:DataModel::List<T>
+ *   - chip::ByteSpan
+ *   - chip::CharSpan
+ *
+ * Memory allocation happens during the 'Setup' phase, while memory deallocation happens
+ * during the 'Finalize' phase.
+ *
+ * The 'Finalize' phase during the destructor phase, and if needed, 'Finalize' will call
+ * the 'Finalize' phase of its descendant.
+ */
+
+#pragma once
+
+#include <app-common/zap-generated/cluster-objects.h>
+#include <app/data-model/List.h>
+#include <app/data-model/Nullable.h>
+#include <commands/common/HexConversion.h>
+#include <json/json.h>
+#include <lib/core/Optional.h>
+#include <lib/support/BytesToHex.h>
+#include <lib/support/CHIPMemString.h>
+#include <lib/support/SafeInt.h>
+
+#include "JsonParser.h"
+
+inline constexpr uint8_t kMaxLabelLength = UINT8_MAX;
+inline constexpr char kNullString[]      = "null";
+
+class ComplexArgumentParser
+{
+public:
+    ComplexArgumentParser() {}
+
+    template <typename T,
+              typename std::enable_if_t<std::is_integral<T>::value && !std::is_signed<T>::value &&
+                                            !std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, bool>::value,
+                                        int> = 0>
+    static CHIP_ERROR Setup(const char * label, T & request, Json::Value value)
+    {
+        if (value.isNumeric())
+        {
+            if (chip::CanCastTo<T>(value.asLargestUInt()))
+            {
+                request = static_cast<T>(value.asLargestUInt());
+                return CHIP_NO_ERROR;
+            }
+        }
+        else if (value.isString())
+        {
+            // Check for a hex number; JSON does not support those as numbers,
+            // so they have to be done as strings.  And we might as well support
+            // string-encoded unsigned numbers in general if we're doing that.
+            bool isHexNotation = strncmp(value.asCString(), "0x", 2) == 0 || strncmp(value.asCString(), "0X", 2) == 0;
+
+            std::stringstream str;
+            isHexNotation ? str << std::hex << value.asCString() : str << value.asCString();
+            uint64_t val;
+            str >> val;
+            if (!str.fail() && str.eof() && chip::CanCastTo<T>(val))
+            {
+                request = static_cast<T>(val);
+                return CHIP_NO_ERROR;
+            }
+        }
+
+        ChipLogError(NotSpecified, "Error while encoding %s as an unsigned integer.", label);
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    template <typename T, std::enable_if_t<std::is_signed<T>::value, bool> = true>
+    static CHIP_ERROR Setup(const char * label, T & request, Json::Value value)
+    {
+        if (!value.isNumeric() || !chip::CanCastTo<T>(value.asLargestInt()))
+        {
+            ChipLogError(NotSpecified, "Error while encoding %s as an unsigned integer.", label);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        request = static_cast<T>(value.asLargestInt());
+        return CHIP_NO_ERROR;
+    }
+
+    template <typename T, typename std::enable_if_t<std::is_enum<T>::value, int> = 0>
+    static CHIP_ERROR Setup(const char * label, T & request, Json::Value value)
+    {
+        std::underlying_type_t<T> requestValue;
+        ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+        request = static_cast<T>(requestValue);
+        return CHIP_NO_ERROR;
+    }
+
+    template <typename T>
+    static CHIP_ERROR Setup(const char * label, chip::BitFlags<T> & request, Json::Value & value)
+    {
+        T requestValue;
+        ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+        request = chip::BitFlags<T>(requestValue);
+        return CHIP_NO_ERROR;
+    }
+
+    template <typename T>
+    static CHIP_ERROR Setup(const char * label, chip::BitMask<T> & request, Json::Value & value)
+    {
+        T requestValue;
+        ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+        request = chip::BitMask<T>(requestValue);
+        return CHIP_NO_ERROR;
+    }
+
+    template <typename T>
+    static CHIP_ERROR Setup(const char * label, chip::Optional<T> & request, Json::Value & value)
+    {
+        T requestValue;
+        ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+        request = chip::Optional<T>(requestValue);
+        return CHIP_NO_ERROR;
+    }
+
+    template <typename T>
+    static CHIP_ERROR Setup(const char * label, chip::app::DataModel::Nullable<T> & request, Json::Value & value)
+    {
+        if (value.isNull())
+        {
+            request.SetNull();
+            return CHIP_NO_ERROR;
+        }
+
+        T requestValue;
+        ReturnErrorOnFailure(ComplexArgumentParser::Setup(label, requestValue, value));
+
+        request = chip::app::DataModel::Nullable<T>(requestValue);
+        return CHIP_NO_ERROR;
+    }
+
+    template <typename T>
+    static CHIP_ERROR Setup(const char * label, chip::app::DataModel::List<T> & request, Json::Value & value)
+    {
+        if (!value.isArray())
+        {
+            ChipLogError(NotSpecified, "Error while encoding %s as an array.", label);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        auto content = static_cast<typename std::remove_const<T>::type *>(chip::Platform::MemoryCalloc(value.size(), sizeof(T)));
+        VerifyOrReturnError(content != nullptr, CHIP_ERROR_NO_MEMORY);
+
+        Json::ArrayIndex size = value.size();
+        for (Json::ArrayIndex i = 0; i < size; i++)
+        {
+            char labelWithIndex[kMaxLabelLength];
+            // GCC 7.0.1 has introduced some new warnings for snprintf (-Werror=format-truncation) by default.
+            // This is not particularly useful when using snprintf and especially in this context, so in order
+            // to disable the warning the %s is constrained to be of max length: (254 - 11 - 2) where:
+            //  - 254 is kMaxLabelLength - 1 (for null)
+            //  - 11 is the maximum length of a %d (-2147483648, 2147483647)
+            //  - 2 is the length for the "[" and "]" characters.
+            snprintf(labelWithIndex, sizeof(labelWithIndex), "%.241s[%d]", label, i);
+            ReturnErrorOnFailure(ComplexArgumentParser::Setup(labelWithIndex, content[i], value[i]));
+        }
+
+        request = chip::app::DataModel::List<T>(content, value.size());
+        return CHIP_NO_ERROR;
+    }
+
+    static CHIP_ERROR Setup(const char * label, chip::ByteSpan & request, Json::Value & value)
+    {
+        if (!value.isString())
+        {
+            ChipLogError(NotSpecified, "Error while encoding %s as an octet string: Not a string.", label);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        auto str         = value.asString();
+        auto size        = str.size();
+        uint8_t * buffer = nullptr;
+
+        if (IsStrString(str.c_str()))
+        {
+            // Skip the prefix
+            str.erase(0, kStrStringPrefixLen);
+            size = str.size();
+
+            buffer = static_cast<uint8_t *>(chip::Platform::MemoryCalloc(size, sizeof(uint8_t)));
+            VerifyOrReturnError(buffer != nullptr, CHIP_ERROR_NO_MEMORY);
+
+            memcpy(buffer, str.c_str(), size);
+        }
+        else
+        {
+            if (IsHexString(str.c_str()))
+            {
+                // Skip the prefix
+                str.erase(0, kHexStringPrefixLen);
+                size = str.size();
+            }
+
+            CHIP_ERROR err = HexToBytes(
+                chip::CharSpan(str.c_str(), size),
+                [&buffer](size_t allocSize) {
+                    buffer = static_cast<uint8_t *>(chip::Platform::MemoryCalloc(allocSize, sizeof(uint8_t)));
+                    return buffer;
+                },
+                &size);
+
+            if (err != CHIP_NO_ERROR)
+            {
+                if (buffer != nullptr)
+                {
+                    chip::Platform::MemoryFree(buffer);
+                }
+
+                return err;
+            }
+        }
+
+        request = chip::ByteSpan(buffer, size);
+        return CHIP_NO_ERROR;
+    }
+
+    static CHIP_ERROR Setup(const char * label, chip::CharSpan & request, Json::Value & value)
+    {
+        if (!value.isString())
+        {
+            ChipLogError(NotSpecified, "Error while encoding %s as a string: Not a string.", label);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        size_t size = strlen(value.asCString());
+        auto buffer = static_cast<char *>(chip::Platform::MemoryCalloc(size, sizeof(char)));
+        VerifyOrReturnError(buffer != nullptr, CHIP_ERROR_NO_MEMORY);
+
+        memcpy(buffer, value.asCString(), size);
+
+        request = chip::CharSpan(buffer, size);
+        return CHIP_NO_ERROR;
+    }
+
+    static CHIP_ERROR Setup(const char * label, float & request, Json::Value & value)
+    {
+        if (!value.isNumeric())
+        {
+            ChipLogError(NotSpecified, "Error while encoding %s as a float: Not a number.", label);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        request = static_cast<float>(value.asFloat());
+        return CHIP_NO_ERROR;
+    }
+
+    static CHIP_ERROR Setup(const char * label, double & request, Json::Value & value)
+    {
+        if (!value.isNumeric())
+        {
+            ChipLogError(NotSpecified, "Error while encoding %s as a double: Not a number.", label);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        request = static_cast<double>(value.asDouble());
+        return CHIP_NO_ERROR;
+    }
+
+    static CHIP_ERROR Setup(const char * label, bool & request, Json::Value & value)
+    {
+        if (!value.isBool())
+        {
+            ChipLogError(NotSpecified, "Error while encoding %s as a boolean: Not a boolean.", label);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        request = value.asBool();
+        return CHIP_NO_ERROR;
+    }
+
+    static CHIP_ERROR EnsureMemberExist(const char * label, const char * memberName, bool hasMember)
+    {
+        if (hasMember)
+        {
+            return CHIP_NO_ERROR;
+        }
+
+        ChipLogError(NotSpecified, "%s is required.  Should be provided as {\"%s\": value}", label, memberName);
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    static CHIP_ERROR EnsureNoMembersRemaining(const char * label, const Json::Value & value)
+    {
+        auto remainingFields = value.getMemberNames();
+        if (remainingFields.size() == 0)
+        {
+            return CHIP_NO_ERROR;
+        }
+#if CHIP_ERROR_LOGGING
+        for (auto & field : remainingFields)
+        {
+            ChipLogError(NotSpecified, "Unexpected field name: '%s.%s'", label, field.c_str());
+        }
+#endif // CHIP_ERROR_LOGGING
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    template <typename T>
+    static void Finalize(T & request)
+    {
+        // Nothing to do
+    }
+
+    template <typename T>
+    static void Finalize(chip::Optional<T> & request)
+    {
+        VerifyOrReturn(request.HasValue());
+        ComplexArgumentParser::Finalize(request.Value());
+    }
+
+    template <typename T>
+    static void Finalize(chip::app::DataModel::Nullable<T> & request)
+    {
+        VerifyOrReturn(!request.IsNull());
+        ComplexArgumentParser::Finalize(request.Value());
+    }
+
+    static void Finalize(chip::ByteSpan & request)
+    {
+        VerifyOrReturn(request.data() != nullptr);
+        chip::Platform::MemoryFree(reinterpret_cast<void *>(const_cast<uint8_t *>(request.data())));
+    }
+
+    static void Finalize(chip::CharSpan & request)
+    {
+        VerifyOrReturn(request.data() != nullptr);
+        chip::Platform::MemoryFree(reinterpret_cast<void *>(const_cast<char *>(request.data())));
+    }
+
+    template <typename T>
+    static void Finalize(chip::app::DataModel::List<T> & request)
+    {
+        VerifyOrReturn(request.data() != nullptr);
+
+        size_t size = request.size();
+        auto data   = const_cast<typename std::remove_const<T>::type *>(request.data());
+        for (size_t i = 0; i < size; i++)
+        {
+            Finalize(data[i]);
+        }
+
+        chip::Platform::MemoryFree(reinterpret_cast<void *>(data));
+    }
+
+#include <zap-generated/cluster/ComplexArgumentParser.h>
+};
+
+class ComplexArgument
+{
+public:
+    virtual ~ComplexArgument() {}
+
+    virtual CHIP_ERROR Parse(const char * label, const char * json) = 0;
+
+    virtual void Reset() = 0;
+};
+
+template <typename T>
+class TypedComplexArgument : public ComplexArgument
+{
+public:
+    TypedComplexArgument() {}
+    TypedComplexArgument(T * request) : mRequest(request) {}
+    ~TypedComplexArgument()
+    {
+        if (mRequest != nullptr)
+        {
+            ComplexArgumentParser::Finalize(*mRequest);
+        }
+    }
+
+    void SetArgument(T * request) { mRequest = request; };
+
+    CHIP_ERROR Parse(const char * label, const char * json)
+    {
+        Json::Value value;
+        if (strcmp(kNullString, json) == 0)
+        {
+            value = Json::nullValue;
+        }
+        else if (!JsonParser::ParseComplexArgument(label, json, value))
+        {
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        return ComplexArgumentParser::Setup(label, *mRequest, value);
+    }
+
+    void Reset() { *mRequest = T(); }
+
+private:
+    T * mRequest;
+};
diff --git a/examples/fabric-admin/commands/clusters/CustomArgument.h b/examples/fabric-admin/commands/clusters/CustomArgument.h
new file mode 100644
index 0000000..3769c00
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/CustomArgument.h
@@ -0,0 +1,297 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <app-common/zap-generated/cluster-objects.h>
+#include <commands/common/HexConversion.h>
+#include <lib/support/BytesToHex.h>
+#include <lib/support/CHIPMemString.h>
+#include <lib/support/SafeInt.h>
+
+#include <string>
+
+#include "JsonParser.h"
+
+namespace {
+static constexpr char kPayloadHexPrefix[]         = "hex:";
+static constexpr char kPayloadSignedPrefix[]      = "s:";
+static constexpr char kPayloadUnsignedPrefix[]    = "u:";
+static constexpr char kPayloadFloatPrefix[]       = "f:";
+static constexpr char kPayloadDoublePrefix[]      = "d:";
+static constexpr size_t kPayloadHexPrefixLen      = ArraySize(kPayloadHexPrefix) - 1;      // ignore null character
+static constexpr size_t kPayloadSignedPrefixLen   = ArraySize(kPayloadSignedPrefix) - 1;   // ignore null character
+static constexpr size_t kPayloadUnsignedPrefixLen = ArraySize(kPayloadUnsignedPrefix) - 1; // ignore null character
+static constexpr size_t kPayloadFloatPrefixLen    = ArraySize(kPayloadFloatPrefix) - 1;    // ignore null character
+static constexpr size_t kPayloadDoublePrefixLen   = ArraySize(kPayloadDoublePrefix) - 1;   // ignore null character
+} // namespace
+
+class CustomArgumentParser
+{
+public:
+    static CHIP_ERROR Put(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+    {
+        if (value.isObject())
+        {
+            return CustomArgumentParser::PutObject(writer, tag, value);
+        }
+
+        if (value.isArray())
+        {
+            return CustomArgumentParser::PutArray(writer, tag, value);
+        }
+
+        if (value.isString())
+        {
+            if (IsOctetString(value))
+            {
+                return CustomArgumentParser::PutOctetString(writer, tag, value);
+            }
+            if (IsUnsignedNumberPrefix(value))
+            {
+                return CustomArgumentParser::PutUnsignedFromString(writer, tag, value);
+            }
+            if (IsSignedNumberPrefix(value))
+            {
+                return CustomArgumentParser::PutSignedFromString(writer, tag, value);
+            }
+            if (IsFloatNumberPrefix(value))
+            {
+                return CustomArgumentParser::PutFloatFromString(writer, tag, value);
+            }
+            if (IsDoubleNumberPrefix(value))
+            {
+                return CustomArgumentParser::PutDoubleFromString(writer, tag, value);
+            }
+
+            return CustomArgumentParser::PutCharString(writer, tag, value);
+        }
+
+        if (value.isNull())
+        {
+            return chip::app::DataModel::Encode(*writer, tag, chip::app::DataModel::Nullable<uint8_t>());
+        }
+
+        if (value.isBool())
+        {
+            return chip::app::DataModel::Encode(*writer, tag, value.asBool());
+        }
+
+        if (value.isUInt())
+        {
+            return chip::app::DataModel::Encode(*writer, tag, value.asLargestUInt());
+        }
+
+        if (value.isInt())
+        {
+            return chip::app::DataModel::Encode(*writer, tag, value.asLargestInt());
+        }
+
+        if (value.isNumeric())
+        {
+            return chip::app::DataModel::Encode(*writer, tag, value.asDouble());
+        }
+
+        return CHIP_ERROR_NOT_IMPLEMENTED;
+    }
+
+private:
+    static CHIP_ERROR PutArray(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+    {
+        chip::TLV::TLVType outer;
+        ReturnErrorOnFailure(writer->StartContainer(tag, chip::TLV::kTLVType_Array, outer));
+
+        Json::ArrayIndex size = value.size();
+
+        for (Json::ArrayIndex i = 0; i < size; i++)
+        {
+            ReturnErrorOnFailure(CustomArgumentParser::Put(writer, chip::TLV::AnonymousTag(), value[i]));
+        }
+
+        return writer->EndContainer(outer);
+    }
+
+    static CHIP_ERROR PutObject(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+    {
+        chip::TLV::TLVType outer;
+        ReturnErrorOnFailure(writer->StartContainer(tag, chip::TLV::kTLVType_Structure, outer));
+
+        for (auto const & id : value.getMemberNames())
+        {
+            auto index = std::stoul(id, nullptr, 0);
+            VerifyOrReturnError(chip::CanCastTo<uint8_t>(index), CHIP_ERROR_INVALID_ARGUMENT);
+            ReturnErrorOnFailure(CustomArgumentParser::Put(writer, chip::TLV::ContextTag(static_cast<uint8_t>(index)), value[id]));
+        }
+
+        return writer->EndContainer(outer);
+    }
+
+    static CHIP_ERROR PutOctetString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+    {
+        const char * hexData = value.asCString() + kPayloadHexPrefixLen;
+        size_t hexDataLen    = strlen(hexData);
+        chip::Platform::ScopedMemoryBuffer<uint8_t> buffer;
+
+        size_t octetCount;
+        ReturnErrorOnFailure(HexToBytes(
+            chip::CharSpan(hexData, hexDataLen),
+            [&buffer](size_t allocSize) {
+                buffer.Calloc(allocSize);
+                return buffer.Get();
+            },
+            &octetCount));
+
+        return chip::app::DataModel::Encode(*writer, tag, chip::ByteSpan(buffer.Get(), octetCount));
+    }
+
+    static CHIP_ERROR PutCharString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+    {
+        size_t size = strlen(value.asCString());
+        return chip::app::DataModel::Encode(*writer, tag, chip::CharSpan(value.asCString(), size));
+    }
+
+    static CHIP_ERROR PutUnsignedFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+    {
+        char numberAsString[21];
+        chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadUnsignedPrefixLen);
+
+        auto number = std::stoull(numberAsString, nullptr, 0);
+        return chip::app::DataModel::Encode(*writer, tag, static_cast<uint64_t>(number));
+    }
+
+    static CHIP_ERROR PutSignedFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+    {
+        char numberAsString[21];
+        chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadSignedPrefixLen);
+
+        auto number = std::stoll(numberAsString, nullptr, 0);
+        return chip::app::DataModel::Encode(*writer, tag, static_cast<int64_t>(number));
+    }
+
+    static CHIP_ERROR PutFloatFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+    {
+        char numberAsString[21];
+        chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadFloatPrefixLen);
+
+        auto number = std::stof(numberAsString);
+        return chip::app::DataModel::Encode(*writer, tag, number);
+    }
+
+    static CHIP_ERROR PutDoubleFromString(chip::TLV::TLVWriter * writer, chip::TLV::Tag tag, Json::Value & value)
+    {
+        char numberAsString[21];
+        chip::Platform::CopyString(numberAsString, value.asCString() + kPayloadDoublePrefixLen);
+
+        auto number = std::stod(numberAsString);
+        return chip::app::DataModel::Encode(*writer, tag, number);
+    }
+
+    static bool IsOctetString(Json::Value & value)
+    {
+        return (strncmp(value.asCString(), kPayloadHexPrefix, kPayloadHexPrefixLen) == 0);
+    }
+
+    static bool IsUnsignedNumberPrefix(Json::Value & value)
+    {
+        return (strncmp(value.asCString(), kPayloadUnsignedPrefix, kPayloadUnsignedPrefixLen) == 0);
+    }
+
+    static bool IsSignedNumberPrefix(Json::Value & value)
+    {
+        return (strncmp(value.asCString(), kPayloadSignedPrefix, kPayloadSignedPrefixLen) == 0);
+    }
+
+    static bool IsFloatNumberPrefix(Json::Value & value)
+    {
+        return (strncmp(value.asCString(), kPayloadFloatPrefix, kPayloadFloatPrefixLen) == 0);
+    }
+
+    static bool IsDoubleNumberPrefix(Json::Value & value)
+    {
+        return (strncmp(value.asCString(), kPayloadDoublePrefix, kPayloadDoublePrefixLen) == 0);
+    }
+};
+
+class CustomArgument
+{
+public:
+    ~CustomArgument()
+    {
+        if (mData != nullptr)
+        {
+            chip::Platform::MemoryFree(mData);
+        }
+    }
+
+    CHIP_ERROR Parse(const char * label, const char * json)
+    {
+        Json::Value value;
+        static constexpr char kHexNumPrefix[] = "0x";
+        constexpr size_t kHexNumPrefixLen     = ArraySize(kHexNumPrefix) - 1;
+        if (strncmp(json, kPayloadHexPrefix, kPayloadHexPrefixLen) == 0 ||
+            strncmp(json, kPayloadSignedPrefix, kPayloadSignedPrefixLen) == 0 ||
+            strncmp(json, kPayloadUnsignedPrefix, kPayloadUnsignedPrefixLen) == 0 ||
+            strncmp(json, kPayloadFloatPrefix, kPayloadFloatPrefixLen) == 0 ||
+            strncmp(json, kPayloadDoublePrefix, kPayloadDoublePrefixLen) == 0)
+        {
+            value = Json::Value(json);
+        }
+        else if (strncmp(json, kHexNumPrefix, kHexNumPrefixLen) == 0)
+        {
+            // Assume that hex numbers are unsigned.  Prepend
+            // kPayloadUnsignedPrefix and then let the rest of the logic handle
+            // things.
+            std::string str(kPayloadUnsignedPrefix);
+            str += json;
+            value = Json::Value(str);
+        }
+        else if (!JsonParser::ParseCustomArgument(label, json, value))
+        {
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        mData = static_cast<uint8_t *>(chip::Platform::MemoryCalloc(sizeof(uint8_t), mDataMaxLen));
+        VerifyOrReturnError(mData != nullptr, CHIP_ERROR_NO_MEMORY);
+
+        chip::TLV::TLVWriter writer;
+        writer.Init(mData, mDataMaxLen);
+
+        ReturnErrorOnFailure(CustomArgumentParser::Put(&writer, chip::TLV::AnonymousTag(), value));
+
+        mDataLen = writer.GetLengthWritten();
+        return writer.Finalize();
+    }
+
+    CHIP_ERROR Encode(chip::TLV::TLVWriter & writer, chip::TLV::Tag tag) const
+    {
+        chip::TLV::TLVReader reader;
+        reader.Init(mData, mDataLen);
+        ReturnErrorOnFailure(reader.Next());
+
+        return writer.CopyElement(tag, reader);
+    }
+
+    // We trust our consumers to do the encoding of our data correctly, so don't
+    // need to know whether we are being encoded for a write.
+    static constexpr bool kIsFabricScoped = false;
+
+private:
+    uint8_t * mData                       = nullptr;
+    uint32_t mDataLen                     = 0;
+    static constexpr uint32_t mDataMaxLen = 4096;
+};
diff --git a/examples/fabric-admin/commands/clusters/DataModelLogger.h b/examples/fabric-admin/commands/clusters/DataModelLogger.h
new file mode 100644
index 0000000..ee64975
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/DataModelLogger.h
@@ -0,0 +1,190 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <string>
+
+#include <app-common/zap-generated/cluster-objects.h>
+#include <app/ConcreteAttributePath.h>
+#include <app/ConcreteCommandPath.h>
+#include <app/EventHeader.h>
+#include <app/MessageDef/StatusIB.h>
+#include <app/data-model/DecodableList.h>
+#include <commands/common/RemoteDataModelLogger.h>
+#include <lib/support/BytesToHex.h>
+
+class DataModelLogger
+{
+public:
+    static CHIP_ERROR LogAttribute(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data);
+    static CHIP_ERROR LogCommand(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader * data);
+    static CHIP_ERROR LogEvent(const chip::app::EventHeader & header, chip::TLV::TLVReader * data);
+
+private:
+    static CHIP_ERROR LogValue(const char * label, size_t indent, bool value)
+    {
+        DataModelLogger::LogString(label, indent, value ? "TRUE" : "FALSE");
+        return CHIP_NO_ERROR;
+    }
+
+    static CHIP_ERROR LogValue(const char * label, size_t indent, chip::CharSpan value)
+    {
+        DataModelLogger::LogString(label, indent, std::string(value.data(), value.size()));
+        return CHIP_NO_ERROR;
+    }
+
+    static CHIP_ERROR LogValue(const char * label, size_t indent, chip::ByteSpan value)
+    {
+        // CHIP_CONFIG_LOG_MESSAGE_MAX_SIZE includes various prefixes we don't
+        // control (timestamps, process ids, etc).  Let's assume (hope?) that
+        // those prefixes use up no more than half the total available space.
+        // Right now it looks like the prefixes are 45 chars out of a 255 char
+        // buffer.
+        char buffer[CHIP_CONFIG_LOG_MESSAGE_MAX_SIZE / 2];
+        size_t prefixSize = ComputePrefixSize(label, indent);
+        if (prefixSize > ArraySize(buffer))
+        {
+            DataModelLogger::LogString("", 0, "Prefix is too long to fit in buffer");
+            return CHIP_ERROR_INTERNAL;
+        }
+
+        const size_t availableSize = ArraySize(buffer) - prefixSize;
+        // Each byte ends up as two hex characters.
+        const size_t bytesPerLogCall = availableSize / 2;
+        std::string labelStr(label);
+        while (value.size() > bytesPerLogCall)
+        {
+            ReturnErrorOnFailure(
+                chip::Encoding::BytesToUppercaseHexString(value.data(), bytesPerLogCall, &buffer[0], ArraySize(buffer)));
+            LogString(labelStr, indent, buffer);
+            value = value.SubSpan(bytesPerLogCall);
+            // For the second and following lines, make it clear that they are
+            // continuation lines by replacing the label with "....".
+            labelStr.replace(labelStr.begin(), labelStr.end(), labelStr.size(), '.');
+        }
+        ReturnErrorOnFailure(chip::Encoding::BytesToUppercaseHexString(value.data(), value.size(), &buffer[0], ArraySize(buffer)));
+        LogString(labelStr, indent, buffer);
+
+        return CHIP_NO_ERROR;
+    }
+
+    template <typename X,
+              typename std::enable_if_t<
+                  std::is_integral<X>::value && !std::is_same<std::remove_cv_t<std::remove_reference_t<X>>, bool>::value, int> = 0>
+    static CHIP_ERROR LogValue(const char * label, size_t indent, X value)
+    {
+        DataModelLogger::LogString(label, indent, std::to_string(value));
+        return CHIP_NO_ERROR;
+    }
+
+    template <typename X, typename std::enable_if_t<std::is_floating_point<X>::value, int> = 0>
+    static CHIP_ERROR LogValue(const char * label, size_t indent, X value)
+    {
+        DataModelLogger::LogString(label, indent, std::to_string(value));
+        return CHIP_NO_ERROR;
+    }
+
+    template <typename X, typename std::enable_if_t<std::is_enum<X>::value, int> = 0>
+    static CHIP_ERROR LogValue(const char * label, size_t indent, X value)
+    {
+        return DataModelLogger::LogValue(label, indent, chip::to_underlying(value));
+    }
+
+    template <typename X>
+    static CHIP_ERROR LogValue(const char * label, size_t indent, chip::BitFlags<X> value)
+    {
+        return DataModelLogger::LogValue(label, indent, value.Raw());
+    }
+
+    template <typename T>
+    static CHIP_ERROR LogValue(const char * label, size_t indent, const chip::app::DataModel::DecodableList<T> & value)
+    {
+        size_t count = 0;
+        ReturnErrorOnFailure(value.ComputeSize(&count));
+        DataModelLogger::LogString(label, indent, std::to_string(count) + " entries");
+
+        auto iter = value.begin();
+        size_t i  = 0;
+        while (iter.Next())
+        {
+            ++i;
+            std::string itemLabel = std::string("[") + std::to_string(i) + "]";
+            ReturnErrorOnFailure(DataModelLogger::LogValue(itemLabel.c_str(), indent + 1, iter.GetValue()));
+        }
+        if (iter.GetStatus() != CHIP_NO_ERROR)
+        {
+            DataModelLogger::LogString(indent + 1, "List truncated due to invalid value");
+        }
+        return iter.GetStatus();
+    }
+
+    template <typename T>
+    static CHIP_ERROR LogValue(const char * label, size_t indent, const chip::app::DataModel::Nullable<T> & value)
+    {
+        if (value.IsNull())
+        {
+            DataModelLogger::LogString(label, indent, "null");
+            return CHIP_NO_ERROR;
+        }
+
+        return DataModelLogger::LogValue(label, indent, value.Value());
+    }
+
+    template <typename T>
+    static CHIP_ERROR LogValue(const char * label, size_t indent, const chip::Optional<T> & value)
+    {
+        if (value.HasValue())
+        {
+            return DataModelLogger::LogValue(label, indent, value.Value());
+        }
+
+        return CHIP_NO_ERROR;
+    }
+
+#include <zap-generated/cluster/logging/DataModelLogger.h>
+
+    static void LogString(size_t indent, const std::string string) { LogString("", indent, string); }
+
+    static void LogString(const std::string label, size_t indent, const std::string string)
+    {
+        std::string prefix = ComputePrefix(label, indent);
+
+        ChipLogProgress(NotSpecified, "%s%s", prefix.c_str(), string.c_str());
+    }
+
+private:
+    static std::string ComputePrefix(const std::string label, size_t indent)
+    {
+        std::string prefix;
+        for (size_t i = 0; i < indent; ++i)
+        {
+            prefix.append("  ");
+        }
+        if (label.size() > 0)
+        {
+            prefix.append(label);
+            prefix.append(":");
+        }
+        prefix.append(" ");
+
+        return prefix;
+    }
+
+    static size_t ComputePrefixSize(const std::string label, size_t indent) { return ComputePrefix(label, indent).size(); }
+};
diff --git a/examples/fabric-admin/commands/clusters/JsonParser.h b/examples/fabric-admin/commands/clusters/JsonParser.h
new file mode 100644
index 0000000..0871e76
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/JsonParser.h
@@ -0,0 +1,166 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "../common/CustomStringPrefix.h"
+
+#include <json/json.h>
+#include <lib/core/Optional.h>
+
+#include <memory>
+#include <sstream>
+#include <string>
+#include <vector>
+
+class JsonParser
+{
+public:
+    // Returns whether the parse succeeded.
+    static bool ParseComplexArgument(const char * label, const char * json, Json::Value & value)
+    {
+        return Parse(label, json, /* strictRoot = */ true, value);
+    }
+
+    // Returns whether the parse succeeded.
+    static bool ParseCustomArgument(const char * label, const char * json, Json::Value & value)
+    {
+        return Parse(label, json, /* strictRoot = */ false, value);
+    }
+
+private:
+    static bool Parse(const char * label, const char * json, bool strictRoot, Json::Value & value)
+    {
+        Json::CharReaderBuilder readerBuilder;
+        readerBuilder.settings_["strictRoot"]        = strictRoot;
+        readerBuilder.settings_["allowSingleQuotes"] = true;
+        readerBuilder.settings_["failIfExtra"]       = true;
+        readerBuilder.settings_["rejectDupKeys"]     = true;
+
+        auto reader = std::unique_ptr<Json::CharReader>(readerBuilder.newCharReader());
+        std::string errors;
+        if (reader->parse(json, json + strlen(json), &value, &errors))
+        {
+            return true;
+        }
+
+        // The CharReader API allows us to set failIfExtra, unlike Reader, but does
+        // not allow us to get structured errors.  We get to try to manually undo
+        // the work it did to create a string from the structured errors it had.
+        ChipLogError(NotSpecified, "Error parsing JSON for %s:", label);
+
+        // For each error "errors" has the following:
+        //
+        // 1) A line starting with "* " that has line/column info
+        // 2) A line with the error message.
+        // 3) An optional line with some extra info.
+        //
+        // We keep track of the last error column, in case the error message
+        // reporting needs it.
+        std::istringstream stream(errors);
+        std::string error;
+        chip::Optional<unsigned> errorColumn;
+        while (getline(stream, error))
+        {
+            if (error.rfind("* ", 0) == 0)
+            {
+                // Flush out any pending error location.
+                LogErrorLocation(errorColumn, json);
+
+                // The format of this line is:
+                //
+                // * Line N, Column M
+                //
+                // Unfortunately it does not indicate end of error, so we can only
+                // show its start.
+                unsigned errorLine; // ignored in practice
+                if (sscanf(error.c_str(), "* Line %u, Column %u", &errorLine, &errorColumn.Emplace()) != 2)
+                {
+                    ChipLogError(NotSpecified, "Unexpected location string: %s\n", error.c_str());
+                    // We don't know how to make sense of this thing anymore.
+                    break;
+                }
+                if (errorColumn.Value() == 0)
+                {
+                    ChipLogError(NotSpecified, "Expected error column to be at least 1");
+                    // We don't know how to make sense of this thing anymore.
+                    break;
+                }
+                // We are using our column numbers as offsets, so want them to be
+                // 0-based.
+                --errorColumn.Value();
+            }
+            else
+            {
+                ChipLogError(NotSpecified, "  %s", error.c_str());
+                if (error == "  Missing ',' or '}' in object declaration" && errorColumn.HasValue() && errorColumn.Value() > 0 &&
+                    json[errorColumn.Value() - 1] == '0' && (json[errorColumn.Value()] == 'x' || json[errorColumn.Value()] == 'X'))
+                {
+                    // Log the error location marker before showing the NOTE
+                    // message.
+                    LogErrorLocation(errorColumn, json);
+                    ChipLogError(NotSpecified,
+                                 "NOTE: JSON does not allow hex syntax beginning with 0x for numbers.  Try putting the hex number "
+                                 "in quotes (like {\"name\": \"0x100\"}).");
+                }
+            }
+        }
+
+        // Write out the marker for our last error.
+        LogErrorLocation(errorColumn, json);
+
+        return false;
+    }
+
+private:
+    static void LogErrorLocation(chip::Optional<unsigned> & errorColumn, const char * json)
+    {
+#if CHIP_ERROR_LOGGING
+        if (!errorColumn.HasValue())
+        {
+            return;
+        }
+
+        const char * sourceText = json;
+        unsigned error_start    = errorColumn.Value();
+        // The whole JSON string might be too long to fit in our log
+        // messages.  Just include 30 chars before the error.
+        constexpr ptrdiff_t kMaxContext = 30;
+        std::string errorMarker;
+        if (error_start > kMaxContext)
+        {
+            sourceText += (error_start - kMaxContext);
+            error_start = kMaxContext;
+            ChipLogError(NotSpecified, "... %s", sourceText);
+            // Add markers corresponding to the "... " above.
+            errorMarker += "----";
+        }
+        else
+        {
+            ChipLogError(NotSpecified, "%s", sourceText);
+        }
+        for (unsigned i = 0; i < error_start; ++i)
+        {
+            errorMarker += "-";
+        }
+        errorMarker += "^";
+        ChipLogError(NotSpecified, "%s", errorMarker.c_str());
+        errorColumn.ClearValue();
+#endif // CHIP_ERROR_LOGGING
+    }
+};
diff --git a/examples/fabric-admin/commands/clusters/ModelCommand.cpp b/examples/fabric-admin/commands/clusters/ModelCommand.cpp
new file mode 100644
index 0000000..8f379db
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ModelCommand.cpp
@@ -0,0 +1,105 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "ModelCommand.h"
+
+#include <app/InteractionModelEngine.h>
+#include <app/icd/client/DefaultICDClientStorage.h>
+#include <inttypes.h>
+
+using namespace ::chip;
+
+CHIP_ERROR ModelCommand::RunCommand()
+{
+
+    if (IsGroupId(mDestinationId))
+    {
+        FabricIndex fabricIndex = CurrentCommissioner().GetFabricIndex();
+        ChipLogProgress(chipTool, "Sending command to group 0x%x", GroupIdFromNodeId(mDestinationId));
+
+        return SendGroupCommand(GroupIdFromNodeId(mDestinationId), fabricIndex);
+    }
+
+    ChipLogProgress(NotSpecified, "Sending command to node " ChipLogFormatX64, ChipLogValueX64(mDestinationId));
+    CheckPeerICDType();
+
+    CommissioneeDeviceProxy * commissioneeDeviceProxy = nullptr;
+    if (CHIP_NO_ERROR == CurrentCommissioner().GetDeviceBeingCommissioned(mDestinationId, &commissioneeDeviceProxy))
+    {
+        return SendCommand(commissioneeDeviceProxy, mEndPointId);
+    }
+
+    return CurrentCommissioner().GetConnectedDevice(mDestinationId, &mOnDeviceConnectedCallback,
+                                                    &mOnDeviceConnectionFailureCallback);
+}
+
+void ModelCommand::OnDeviceConnectedFn(void * context, chip::Messaging::ExchangeManager & exchangeMgr,
+                                       const chip::SessionHandle & sessionHandle)
+{
+    ModelCommand * command = reinterpret_cast<ModelCommand *>(context);
+    VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnDeviceConnectedFn: context is null"));
+
+    chip::OperationalDeviceProxy device(&exchangeMgr, sessionHandle);
+    CHIP_ERROR err = command->SendCommand(&device, command->mEndPointId);
+    VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+}
+
+void ModelCommand::OnDeviceConnectionFailureFn(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR err)
+{
+    LogErrorOnFailure(err);
+
+    ModelCommand * command = reinterpret_cast<ModelCommand *>(context);
+    VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnDeviceConnectionFailureFn: context is null"));
+    command->SetCommandExitStatus(err);
+}
+
+void ModelCommand::Shutdown()
+{
+    mOnDeviceConnectedCallback.Cancel();
+    mOnDeviceConnectionFailureCallback.Cancel();
+
+    CHIPCommand::Shutdown();
+}
+
+void ModelCommand::CheckPeerICDType()
+{
+    if (mIsPeerLIT.HasValue())
+    {
+        ChipLogProgress(NotSpecified, "Peer ICD type is set to %s", mIsPeerLIT.Value() == 1 ? "LIT-ICD" : "non LIT-ICD");
+        return;
+    }
+
+    app::ICDClientInfo info;
+    auto destinationPeerId = chip::ScopedNodeId(mDestinationId, CurrentCommissioner().GetFabricIndex());
+    auto iter              = CHIPCommand::sICDClientStorage.IterateICDClientInfo();
+    if (iter == nullptr)
+    {
+        return;
+    }
+    app::DefaultICDClientStorage::ICDClientInfoIteratorWrapper clientInfoIteratorWrapper(iter);
+
+    while (iter->Next(info))
+    {
+        if (ScopedNodeId(info.peer_node.GetNodeId(), info.peer_node.GetFabricIndex()) == destinationPeerId)
+        {
+            ChipLogProgress(NotSpecified, "Peer is a registered LIT ICD.");
+            mIsPeerLIT.SetValue(true);
+            return;
+        }
+    }
+}
diff --git a/examples/fabric-admin/commands/clusters/ModelCommand.h b/examples/fabric-admin/commands/clusters/ModelCommand.h
new file mode 100644
index 0000000..c14d3c9
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ModelCommand.h
@@ -0,0 +1,91 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+#include <controller/ExamplePersistentStorage.h>
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+#include "../common/CHIPCommand.h"
+#include <lib/core/CHIPEncoding.h>
+
+class ModelCommand : public CHIPCommand
+{
+public:
+    ModelCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig, bool supportsMultipleEndpoints = false) :
+        CHIPCommand(commandName, credsIssuerConfig), mOnDeviceConnectedCallback(OnDeviceConnectedFn, this),
+        mOnDeviceConnectionFailureCallback(OnDeviceConnectionFailureFn, this), mSupportsMultipleEndpoints(supportsMultipleEndpoints)
+    {}
+
+    void AddArguments(bool skipEndpoints = false)
+    {
+        AddArgument(
+            "destination-id", 0, UINT64_MAX, &mDestinationId,
+            "64-bit node or group identifier.\n  Group identifiers are detected by being in the 0xFFFF'FFFF'FFFF'xxxx range.");
+        if (skipEndpoints == false)
+        {
+            if (mSupportsMultipleEndpoints)
+            {
+                AddArgument("endpoint-ids", 0, UINT16_MAX, &mEndPointId,
+                            "Comma-separated list of endpoint ids (e.g. \"1\" or \"1,2,3\").\n  Allowed to be 0xFFFF to indicate a "
+                            "wildcard endpoint.");
+            }
+            else
+            {
+                AddArgument("endpoint-id-ignored-for-group-commands", 0, UINT16_MAX, &mEndPointId,
+                            "Endpoint the command is targeted at.");
+            }
+        }
+        AddArgument(
+            "lit-icd-peer", 0, 1, &mIsPeerLIT,
+            "Whether to treat the peer as a LIT ICD. false: Always no, true: Always yes, (not set): Yes if the peer is registered "
+            "to this controller.");
+        AddArgument("timeout", 0, UINT16_MAX, &mTimeout);
+    }
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override;
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(20)); }
+
+    virtual CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endPointIds) = 0;
+
+    virtual CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex) { return CHIP_ERROR_BAD_REQUEST; };
+
+    void Shutdown() override;
+
+protected:
+    bool IsPeerLIT() { return mIsPeerLIT.ValueOr(false); }
+
+    chip::Optional<uint16_t> mTimeout;
+
+private:
+    chip::NodeId mDestinationId;
+    std::vector<chip::EndpointId> mEndPointId;
+    chip::Optional<bool> mIsPeerLIT;
+
+    void CheckPeerICDType();
+
+    static void OnDeviceConnectedFn(void * context, chip::Messaging::ExchangeManager & exchangeMgr,
+                                    const chip::SessionHandle & sessionHandle);
+    static void OnDeviceConnectionFailureFn(void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR error);
+
+    chip::Callback::Callback<chip::OnDeviceConnected> mOnDeviceConnectedCallback;
+    chip::Callback::Callback<chip::OnDeviceConnectionFailure> mOnDeviceConnectionFailureCallback;
+    const bool mSupportsMultipleEndpoints;
+};
diff --git a/examples/fabric-admin/commands/clusters/ReportCommand.h b/examples/fabric-admin/commands/clusters/ReportCommand.h
new file mode 100644
index 0000000..4e9dbd0
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/ReportCommand.h
@@ -0,0 +1,551 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <app/tests/suites/commands/interaction_model/InteractionModel.h>
+
+#include "DataModelLogger.h"
+#include "ModelCommand.h"
+
+class ReportCommand : public InteractionModelReports, public ModelCommand, public chip::app::ReadClient::Callback
+{
+public:
+    ReportCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) :
+        InteractionModelReports(this), ModelCommand(commandName, credsIssuerConfig, /* supportsMultipleEndpoints = */ true)
+    {}
+
+    /////////// ReadClient Callback Interface /////////
+    void OnAttributeData(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data,
+                         const chip::app::StatusIB & status) override
+    {
+        CHIP_ERROR error = status.ToChipError();
+        if (CHIP_NO_ERROR != error)
+        {
+            LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(path, status));
+
+            ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error));
+            mError = error;
+            return;
+        }
+
+        if (data == nullptr)
+        {
+            ChipLogError(NotSpecified, "Response Failure: No Data");
+            mError = CHIP_ERROR_INTERNAL;
+            return;
+        }
+
+        LogErrorOnFailure(RemoteDataModelLogger::LogAttributeAsJSON(path, data));
+
+        error = DataModelLogger::LogAttribute(path, data);
+        if (CHIP_NO_ERROR != error)
+        {
+            ChipLogError(NotSpecified, "Response Failure: Can not decode Data");
+            mError = error;
+            return;
+        }
+    }
+
+    void OnEventData(const chip::app::EventHeader & eventHeader, chip::TLV::TLVReader * data,
+                     const chip::app::StatusIB * status) override
+    {
+        if (status != nullptr)
+        {
+            CHIP_ERROR error = status->ToChipError();
+            if (CHIP_NO_ERROR != error)
+            {
+                LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(eventHeader, *status));
+
+                ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error));
+                mError = error;
+                return;
+            }
+        }
+
+        if (data == nullptr)
+        {
+            ChipLogError(NotSpecified, "Response Failure: No Data");
+            mError = CHIP_ERROR_INTERNAL;
+            return;
+        }
+
+        LogErrorOnFailure(RemoteDataModelLogger::LogEventAsJSON(eventHeader, data));
+
+        CHIP_ERROR error = DataModelLogger::LogEvent(eventHeader, data);
+        if (CHIP_NO_ERROR != error)
+        {
+            ChipLogError(NotSpecified, "Response Failure: Can not decode Data");
+            mError = error;
+            return;
+        }
+    }
+
+    void OnError(CHIP_ERROR error) override
+    {
+        LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(error));
+
+        ChipLogProgress(NotSpecified, "Error: %s", chip::ErrorStr(error));
+        mError = error;
+    }
+
+    void OnDeallocatePaths(chip::app::ReadPrepareParams && aReadPrepareParams) override
+    {
+        InteractionModelReports::OnDeallocatePaths(std::move(aReadPrepareParams));
+    }
+
+    void Shutdown() override
+    {
+        // We don't shut down InteractionModelReports here; we leave it for
+        // Cleanup to handle.
+        mError = CHIP_NO_ERROR;
+        ModelCommand::Shutdown();
+    }
+
+    void Cleanup() override { InteractionModelReports::Shutdown(); }
+
+protected:
+    // Use a 3x-longer-than-default timeout because wildcard reads can take a
+    // while.
+    chip::System::Clock::Timeout GetWaitDuration() const override
+    {
+        return mTimeout.HasValue() ? chip::System::Clock::Seconds16(mTimeout.Value()) : (ModelCommand::GetWaitDuration() * 3);
+    }
+
+    CHIP_ERROR mError = CHIP_NO_ERROR;
+};
+
+class ReadCommand : public ReportCommand
+{
+protected:
+    ReadCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) :
+        ReportCommand(commandName, credsIssuerConfig)
+    {}
+
+    void OnDone(chip::app::ReadClient * aReadClient) override
+    {
+        InteractionModelReports::CleanupReadClient(aReadClient);
+        SetCommandExitStatus(mError);
+    }
+};
+
+class SubscribeCommand : public ReportCommand
+{
+protected:
+    SubscribeCommand(const char * commandName, CredentialIssuerCommands * credsIssuerConfig) :
+        ReportCommand(commandName, credsIssuerConfig)
+    {}
+
+    void OnSubscriptionEstablished(chip::SubscriptionId subscriptionId) override
+    {
+        mSubscriptionEstablished = true;
+        SetCommandExitStatus(CHIP_NO_ERROR);
+    }
+
+    void OnDone(chip::app::ReadClient * aReadClient) override
+    {
+        InteractionModelReports::CleanupReadClient(aReadClient);
+
+        if (!mSubscriptionEstablished)
+        {
+            SetCommandExitStatus(mError);
+        }
+        // else we must be getting here from Cleanup(), which means we have
+        // already done our exit status thing.
+    }
+
+    void Shutdown() override
+    {
+        mSubscriptionEstablished = false;
+        ReportCommand::Shutdown();
+    }
+
+    // For subscriptions we always defer interactive cleanup.  Either our
+    // ReadClients will terminate themselves (in which case they will be removed
+    // from our list anyway), or they should hang around until shutdown.
+    bool DeferInteractiveCleanup() override { return true; }
+
+private:
+    bool mSubscriptionEstablished = false;
+};
+
+class ReadAttribute : public ReadCommand
+{
+public:
+    ReadAttribute(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-by-id", credsIssuerConfig)
+    {
+        AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+                    "Comma-separated list of cluster ids to read from (e.g. \"6\" or \"8,0x201\").\n  Allowed to be 0xFFFFFFFF to "
+                    "indicate a wildcard cluster.");
+        AddAttributeIdArgument();
+        AddCommonArguments();
+        ReadCommand::AddArguments();
+    }
+
+    ReadAttribute(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+        ReadCommand("read-by-id", credsIssuerConfig), mClusterIds(1, clusterId)
+    {
+        AddAttributeIdArgument();
+        AddCommonArguments();
+        ReadCommand::AddArguments();
+    }
+
+    ReadAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+                  CredentialIssuerCommands * credsIssuerConfig) :
+        ReadCommand("read", credsIssuerConfig),
+        mClusterIds(1, clusterId), mAttributeIds(1, attributeId)
+    {
+        AddArgument("attr-name", attributeName);
+        AddCommonArguments();
+        ReadCommand::AddArguments();
+    }
+
+    ~ReadAttribute() {}
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        return ReadCommand::ReadAttribute(device, endpointIds, mClusterIds, mAttributeIds);
+    }
+
+private:
+    void AddAttributeIdArgument()
+    {
+        AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+                    "Comma-separated list of attribute ids to read (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n  Allowed to be "
+                    "0xFFFFFFFF to indicate a wildcard attribute.");
+    }
+
+    void AddCommonArguments()
+    {
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+                    "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+        AddArgument("data-version", 0, UINT32_MAX, &mDataVersions,
+                    "Comma-separated list of data versions for the clusters being read.");
+    }
+
+    std::vector<chip::ClusterId> mClusterIds;
+    std::vector<chip::AttributeId> mAttributeIds;
+};
+
+class SubscribeAttribute : public SubscribeCommand
+{
+public:
+    SubscribeAttribute(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-by-id", credsIssuerConfig)
+    {
+        AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+                    "Comma-separated list of cluster ids to subscribe to (e.g. \"6\" or \"8,0x201\").\n  Allowed to be 0xFFFFFFFF "
+                    "to indicate a wildcard cluster.");
+        AddAttributeIdArgument();
+        AddCommonArguments();
+        SubscribeCommand::AddArguments();
+    }
+
+    SubscribeAttribute(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+        SubscribeCommand("subscribe-by-id", credsIssuerConfig), mClusterIds(1, clusterId)
+    {
+        AddAttributeIdArgument();
+        AddCommonArguments();
+        SubscribeCommand::AddArguments();
+    }
+
+    SubscribeAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+                       CredentialIssuerCommands * credsIssuerConfig) :
+        SubscribeCommand("subscribe", credsIssuerConfig),
+        mClusterIds(1, clusterId), mAttributeIds(1, attributeId)
+    {
+        AddArgument("attr-name", attributeName);
+        AddCommonArguments();
+        SubscribeCommand::AddArguments();
+    }
+
+    ~SubscribeAttribute() {}
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        SubscribeCommand::SetPeerLIT(IsPeerLIT());
+        return SubscribeCommand::SubscribeAttribute(device, endpointIds, mClusterIds, mAttributeIds);
+    }
+
+private:
+    void AddAttributeIdArgument()
+    {
+        AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+                    "Comma-separated list of attribute ids to subscribe to (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n  Allowed to be "
+                    "0xFFFFFFFF to indicate a wildcard attribute.");
+    }
+
+    void AddCommonArguments()
+    {
+        AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval,
+                    "Server should not send a new report if less than this number of seconds has elapsed since the last report.");
+        AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval,
+                    "Server must send a report if this number of seconds has elapsed since the last report.");
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+                    "Boolean indicating whether to do a fabric-filtered subscription. Defaults to true.");
+        AddArgument("data-version", 0, UINT32_MAX, &mDataVersions,
+                    "Comma-separated list of data versions for the clusters being subscribed to.");
+        AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions,
+                    "Boolean indicating whether to keep existing subscriptions when creating the new one. Defaults to false.");
+        AddArgument("auto-resubscribe", 0, 1, &mAutoResubscribe,
+                    "Boolean indicating whether the subscription should auto-resubscribe.  Defaults to false.");
+    }
+
+    std::vector<chip::ClusterId> mClusterIds;
+    std::vector<chip::AttributeId> mAttributeIds;
+};
+
+class ReadEvent : public ReadCommand
+{
+public:
+    ReadEvent(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-event-by-id", credsIssuerConfig)
+    {
+        AddArgument("cluster-id", 0, UINT32_MAX, &mClusterIds);
+        AddArgument("event-id", 0, UINT32_MAX, &mEventIds);
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered);
+        AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+        ReadCommand::AddArguments();
+    }
+
+    ReadEvent(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+        ReadCommand("read-event-by-id", credsIssuerConfig), mClusterIds(1, clusterId)
+    {
+        AddArgument("event-id", 0, UINT32_MAX, &mEventIds);
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered);
+        AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+        ReadCommand::AddArguments();
+    }
+
+    ReadEvent(chip::ClusterId clusterId, const char * eventName, chip::EventId eventId,
+              CredentialIssuerCommands * credsIssuerConfig) :
+        ReadCommand("read-event", credsIssuerConfig),
+        mClusterIds(1, clusterId), mEventIds(1, eventId)
+    {
+        AddArgument("event-name", eventName);
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered);
+        AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+        ReadCommand::AddArguments();
+    }
+
+    ~ReadEvent() {}
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        return ReadCommand::ReadEvent(device, endpointIds, mClusterIds, mEventIds);
+    }
+
+private:
+    std::vector<chip::ClusterId> mClusterIds;
+    std::vector<chip::EventId> mEventIds;
+};
+
+class SubscribeEvent : public SubscribeCommand
+{
+public:
+    SubscribeEvent(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-event-by-id", credsIssuerConfig)
+    {
+        AddArgument("cluster-id", 0, UINT32_MAX, &mClusterIds);
+        AddArgument("event-id", 0, UINT32_MAX, &mEventIds);
+        AddCommonArguments();
+        SubscribeCommand::AddArguments();
+    }
+
+    SubscribeEvent(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+        SubscribeCommand("subscribe-event-by-id", credsIssuerConfig), mClusterIds(1, clusterId)
+    {
+        AddArgument("event-id", 0, UINT32_MAX, &mEventIds);
+        AddCommonArguments();
+        SubscribeCommand::AddArguments();
+    }
+
+    SubscribeEvent(chip::ClusterId clusterId, const char * eventName, chip::EventId eventId,
+                   CredentialIssuerCommands * credsIssuerConfig) :
+        SubscribeCommand("subscribe-event", credsIssuerConfig),
+        mClusterIds(1, clusterId), mEventIds(1, eventId)
+    {
+        AddArgument("event-name", eventName, "Event name.");
+        AddCommonArguments();
+        SubscribeCommand::AddArguments();
+    }
+
+    void AddCommonArguments()
+    {
+        AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval,
+                    "The requested minimum interval between reports. Sets MinIntervalFloor in the Subscribe Request.");
+        AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval,
+                    "The requested maximum interval between reports. Sets MaxIntervalCeiling in the Subscribe Request.");
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered);
+        AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+        AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions,
+                    "false - Terminate existing subscriptions from initiator.\n  true - Leave existing subscriptions in place.");
+        AddArgument(
+            "is-urgent", 0, 1, &mIsUrgents,
+            "Sets isUrgent in the Subscribe Request.\n"
+            "  The queueing of any urgent event SHALL force an immediate generation of reports containing all events queued "
+            "leading up to (and including) the urgent event in question.\n"
+            "  This argument takes a comma separated list of true/false values.\n"
+            "  If the number of paths exceeds the number of entries provided to is-urgent, then isUrgent will be false for the "
+            "extra paths.");
+        AddArgument("auto-resubscribe", 0, 1, &mAutoResubscribe,
+                    "Boolean indicating whether the subscription should auto-resubscribe.  Defaults to false.");
+    }
+
+    ~SubscribeEvent() {}
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        SubscribeCommand::SetPeerLIT(IsPeerLIT());
+        return SubscribeCommand::SubscribeEvent(device, endpointIds, mClusterIds, mEventIds);
+    }
+
+private:
+    std::vector<chip::ClusterId> mClusterIds;
+    std::vector<chip::EventId> mEventIds;
+};
+
+class ReadNone : public ReadCommand
+{
+public:
+    ReadNone(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-none", credsIssuerConfig)
+    {
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+                    "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+        AddArgument("data-versions", 0, UINT32_MAX, &mDataVersions,
+                    "Comma-separated list of data versions for the clusters being read.");
+        AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+        ReadCommand::AddArguments(true /* skipEndpoints */);
+    }
+
+    ~ReadNone() {}
+
+    void OnDone(chip::app::ReadClient * aReadClient) override
+    {
+        InteractionModelReports::CleanupReadClient(aReadClient);
+        SetCommandExitStatus(mError);
+    }
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        return ReadCommand::ReadNone(device);
+    }
+};
+
+class ReadAll : public ReadCommand
+{
+public:
+    ReadAll(CredentialIssuerCommands * credsIssuerConfig) : ReadCommand("read-all", credsIssuerConfig)
+    {
+        AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+                    "Comma-separated list of cluster ids to read from (e.g. \"6\" or \"8,0x201\").\n  Allowed to be 0xFFFFFFFF to "
+                    "indicate a wildcard cluster.");
+        AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+                    "Comma-separated list of attribute ids to read (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n  Allowed to be "
+                    "0xFFFFFFFF to indicate a wildcard attribute.");
+        AddArgument("event-ids", 0, UINT32_MAX, &mEventIds,
+                    "Comma-separated list of event ids to read (e.g. \"0\" or \"1,2,3\").\n  Allowed to be "
+                    "0xFFFFFFFF to indicate a wildcard event.");
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+                    "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+        AddArgument("data-versions", 0, UINT32_MAX, &mDataVersions,
+                    "Comma-separated list of data versions for the clusters being read.");
+        AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+        ReadCommand::AddArguments();
+    }
+
+    ~ReadAll() {}
+
+    void OnDone(chip::app::ReadClient * aReadClient) override
+    {
+        InteractionModelReports::CleanupReadClient(aReadClient);
+        SetCommandExitStatus(mError);
+    }
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        return ReadCommand::ReadAll(device, endpointIds, mClusterIds, mAttributeIds, mEventIds);
+    }
+
+private:
+    std::vector<chip::ClusterId> mClusterIds;
+    std::vector<chip::AttributeId> mAttributeIds;
+    std::vector<chip::EventId> mEventIds;
+};
+
+class SubscribeNone : public SubscribeCommand
+{
+public:
+    SubscribeNone(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-none", credsIssuerConfig)
+    {
+        AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval,
+                    "The requested minimum interval between reports. Sets MinIntervalFloor in the Subscribe Request.");
+        AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval,
+                    "The requested maximum interval between reports. Sets MaxIntervalCeiling in the Subscribe Request.");
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+                    "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+        AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+        AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions,
+                    "false - Terminate existing subscriptions from initiator.\n  true - Leave existing subscriptions in place.");
+        SubscribeCommand::AddArguments(true /* skipEndpoints */);
+    }
+
+    ~SubscribeNone() {}
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        return SubscribeCommand::SubscribeNone(device);
+    }
+};
+
+class SubscribeAll : public SubscribeCommand
+{
+public:
+    SubscribeAll(CredentialIssuerCommands * credsIssuerConfig) : SubscribeCommand("subscribe-all", credsIssuerConfig)
+    {
+        AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+                    "Comma-separated list of cluster ids to read from (e.g. \"6\" or \"8,0x201\").\n  Allowed to be 0xFFFFFFFF to "
+                    "indicate a wildcard cluster.");
+        AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+                    "Comma-separated list of attribute ids to read (e.g. \"0\" or \"1,0xFFFC,0xFFFD\").\n  Allowed to be "
+                    "0xFFFFFFFF to indicate a wildcard attribute.");
+        AddArgument("event-ids", 0, UINT32_MAX, &mEventIds,
+                    "Comma-separated list of event ids to read (e.g. \"0\" or \"1,2,3\").\n  Allowed to be "
+                    "0xFFFFFFFF to indicate a wildcard event.");
+        AddArgument("min-interval", 0, UINT16_MAX, &mMinInterval,
+                    "The requested minimum interval between reports. Sets MinIntervalFloor in the Subscribe Request.");
+        AddArgument("max-interval", 0, UINT16_MAX, &mMaxInterval,
+                    "The requested maximum interval between reports. Sets MaxIntervalCeiling in the Subscribe Request.");
+        AddArgument("fabric-filtered", 0, 1, &mFabricFiltered,
+                    "Boolean indicating whether to do a fabric-filtered read. Defaults to true.");
+        AddArgument("event-min", 0, UINT64_MAX, &mEventNumber);
+        AddArgument("keepSubscriptions", 0, 1, &mKeepSubscriptions,
+                    "false - Terminate existing subscriptions from initiator.\n  true - Leave existing subscriptions in place.");
+        SubscribeCommand::AddArguments();
+    }
+
+    ~SubscribeAll() {}
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        SubscribeCommand::SetPeerLIT(IsPeerLIT());
+        return SubscribeCommand::SubscribeAll(device, endpointIds, mClusterIds, mAttributeIds, mEventIds);
+    }
+
+private:
+    std::vector<chip::ClusterId> mClusterIds;
+    std::vector<chip::AttributeId> mAttributeIds;
+    std::vector<chip::EventId> mEventIds;
+};
diff --git a/examples/fabric-admin/commands/clusters/SubscriptionsCommands.h b/examples/fabric-admin/commands/clusters/SubscriptionsCommands.h
new file mode 100644
index 0000000..625e5a7
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/SubscriptionsCommands.h
@@ -0,0 +1,108 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <lib/core/CHIPError.h>
+
+#include <commands/common/CHIPCommand.h>
+#include <commands/common/Commands.h>
+
+class ShutdownSubscription : public CHIPCommand
+{
+public:
+    ShutdownSubscription(CredentialIssuerCommands * credsIssuerConfig) :
+        CHIPCommand("shutdown-one", credsIssuerConfig,
+                    "Shut down a single subscription, identified by its subscription id and target node id.")
+    {
+        AddArgument("subscription-id", 0, UINT32_MAX, &mSubscriptionId);
+        AddArgument("node-id", 0, UINT64_MAX, &mNodeId,
+                    "The node id, scoped to the commissioner name the command is running under.");
+    }
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override
+    {
+        CHIP_ERROR err = chip::app::InteractionModelEngine::GetInstance()->ShutdownSubscription(
+            chip::ScopedNodeId(mNodeId, CurrentCommissioner().GetFabricIndex()), mSubscriptionId);
+        SetCommandExitStatus(err);
+        return CHIP_NO_ERROR;
+    }
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+
+private:
+    chip::SubscriptionId mSubscriptionId;
+    chip::NodeId mNodeId;
+};
+
+class ShutdownSubscriptionsForNode : public CHIPCommand
+{
+public:
+    ShutdownSubscriptionsForNode(CredentialIssuerCommands * credsIssuerConfig) :
+        CHIPCommand("shutdown-all-for-node", credsIssuerConfig, "Shut down all subscriptions targeting a given node.")
+    {
+        AddArgument("node-id", 0, UINT64_MAX, &mNodeId,
+                    "The node id, scoped to the commissioner name the command is running under.");
+    }
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override
+    {
+        chip::app::InteractionModelEngine::GetInstance()->ShutdownSubscriptions(CurrentCommissioner().GetFabricIndex(), mNodeId);
+
+        SetCommandExitStatus(CHIP_NO_ERROR);
+        return CHIP_NO_ERROR;
+    }
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+
+private:
+    chip::NodeId mNodeId;
+};
+
+class ShutdownAllSubscriptions : public CHIPCommand
+{
+public:
+    ShutdownAllSubscriptions(CredentialIssuerCommands * credsIssuerConfig) :
+        CHIPCommand("shutdown-all", credsIssuerConfig, "Shut down all subscriptions to all nodes.")
+    {}
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override
+    {
+        chip::app::InteractionModelEngine::GetInstance()->ShutdownAllSubscriptions();
+
+        SetCommandExitStatus(CHIP_NO_ERROR);
+        return CHIP_NO_ERROR;
+    }
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+
+private:
+};
+
+void registerCommandsSubscriptions(Commands & commands, CredentialIssuerCommands * credsIssuerConfig)
+{
+    const char * clusterName = "Subscriptions";
+
+    commands_list clusterCommands = {
+        make_unique<ShutdownSubscription>(credsIssuerConfig),         //
+        make_unique<ShutdownSubscriptionsForNode>(credsIssuerConfig), //
+        make_unique<ShutdownAllSubscriptions>(credsIssuerConfig),     //
+    };
+
+    commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for shutting down subscriptions.");
+}
diff --git a/examples/fabric-admin/commands/clusters/WriteAttributeCommand.h b/examples/fabric-admin/commands/clusters/WriteAttributeCommand.h
new file mode 100644
index 0000000..8424e95
--- /dev/null
+++ b/examples/fabric-admin/commands/clusters/WriteAttributeCommand.h
@@ -0,0 +1,286 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <app/tests/suites/commands/interaction_model/InteractionModel.h>
+
+#include "DataModelLogger.h"
+#include "ModelCommand.h"
+
+inline constexpr char kWriteCommandKey[]      = "write";
+inline constexpr char kWriteByIdCommandKey[]  = "write-by-id";
+inline constexpr char kForceWriteCommandKey[] = "force-write";
+
+enum class WriteCommandType
+{
+    kWrite,      // regular, writable attributes
+    kForceWrite, // forced writes, send a write command on something expected to fail
+};
+
+template <class T = std::vector<CustomArgument *>>
+class WriteAttribute : public InteractionModelWriter, public ModelCommand, public chip::app::WriteClient::Callback
+{
+public:
+    WriteAttribute(CredentialIssuerCommands * credsIssuerConfig) :
+        InteractionModelWriter(this), ModelCommand(kWriteByIdCommandKey, credsIssuerConfig)
+    {
+        AddArgumentClusterIds();
+        AddArgumentAttributeIds();
+        AddArgumentAttributeValues();
+        AddArguments();
+    }
+
+    WriteAttribute(chip::ClusterId clusterId, CredentialIssuerCommands * credsIssuerConfig) :
+        InteractionModelWriter(this), ModelCommand(kWriteByIdCommandKey, credsIssuerConfig), mClusterIds(1, clusterId)
+    {
+        AddArgumentAttributeIds();
+        AddArgumentAttributeValues();
+        AddArguments();
+    }
+
+    template <typename minType, typename maxType>
+    WriteAttribute(chip::ClusterId clusterId, const char * attributeName, minType minValue, maxType maxValue,
+                   chip::AttributeId attributeId, WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+        WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+    {
+        AddArgumentAttributeName(attributeName);
+        AddArgumentAttributeValues(static_cast<int64_t>(minValue), static_cast<uint64_t>(maxValue));
+        AddArguments();
+    }
+
+    WriteAttribute(chip::ClusterId clusterId, const char * attributeName, float minValue, float maxValue,
+                   chip::AttributeId attributeId, WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+        WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+    {
+        AddArgumentAttributeName(attributeName);
+        AddArgumentAttributeValues(minValue, maxValue);
+        AddArguments();
+    }
+
+    WriteAttribute(chip::ClusterId clusterId, const char * attributeName, double minValue, double maxValue,
+                   chip::AttributeId attributeId, WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+        WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+    {
+        AddArgumentAttributeName(attributeName);
+        AddArgumentAttributeValues(minValue, maxValue);
+        AddArguments();
+    }
+
+    WriteAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+                   WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+        WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+    {
+        AddArgumentAttributeName(attributeName);
+        AddArgumentAttributeValues();
+        AddArguments();
+    }
+
+    WriteAttribute(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+                   TypedComplexArgument<T> & attributeParser, WriteCommandType commandType,
+                   CredentialIssuerCommands * credsIssuerConfig) :
+        WriteAttribute(clusterId, attributeId, commandType, credsIssuerConfig)
+    {
+        AddArgumentAttributeName(attributeName);
+        AddArgumentAttributeValues(attributeParser);
+        AddArguments();
+    }
+
+    ~WriteAttribute() {}
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds) override
+    {
+        return WriteAttribute::SendCommand(device, endpointIds, mClusterIds, mAttributeIds, mAttributeValues);
+    }
+
+    CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex) override
+    {
+        return WriteAttribute::SendGroupCommand(groupId, fabricIndex, mClusterIds, mAttributeIds, mAttributeValues);
+    }
+
+    /////////// WriteClient Callback Interface /////////
+    void OnResponse(const chip::app::WriteClient * client, const chip::app::ConcreteDataAttributePath & path,
+                    chip::app::StatusIB status) override
+    {
+        CHIP_ERROR error = status.ToChipError();
+        if (CHIP_NO_ERROR != error)
+        {
+            LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(path, status));
+
+            ChipLogError(NotSpecified, "Response Failure: %s", chip::ErrorStr(error));
+            mError = error;
+        }
+    }
+
+    void OnError(const chip::app::WriteClient * client, CHIP_ERROR error) override
+    {
+        LogErrorOnFailure(RemoteDataModelLogger::LogErrorAsJSON(error));
+
+        ChipLogProgress(NotSpecified, "Error: %s", chip::ErrorStr(error));
+        mError = error;
+    }
+
+    void OnDone(chip::app::WriteClient * client) override
+    {
+        InteractionModelWriter::Shutdown();
+        SetCommandExitStatus(mError);
+    }
+
+    CHIP_ERROR SendCommand(chip::DeviceProxy * device, std::vector<chip::EndpointId> endpointIds,
+                           std::vector<chip::ClusterId> clusterIds, std::vector<chip::AttributeId> attributeIds, const T & values)
+    {
+        return InteractionModelWriter::WriteAttribute(device, endpointIds, clusterIds, attributeIds, values);
+    }
+
+    CHIP_ERROR SendGroupCommand(chip::GroupId groupId, chip::FabricIndex fabricIndex, std::vector<chip::ClusterId> clusterIds,
+                                std::vector<chip::AttributeId> attributeIds, const T & value)
+    {
+        ChipLogDetail(NotSpecified, "Sending Write Attribute to Group %u, on Fabric %x, for cluster %u with attributeId %u",
+                      groupId, fabricIndex, clusterIds.at(0), attributeIds.at(0));
+        chip::Optional<chip::DataVersion> dataVersion = chip::NullOptional;
+        if (mDataVersions.HasValue())
+        {
+            dataVersion.SetValue(mDataVersions.Value().at(0));
+        }
+
+        return InteractionModelWriter::WriteGroupAttribute(groupId, fabricIndex, clusterIds.at(0), attributeIds.at(0), value,
+                                                           dataVersion);
+    }
+
+    void Shutdown() override
+    {
+        mError = CHIP_NO_ERROR;
+        ModelCommand::Shutdown();
+    }
+
+protected:
+    WriteAttribute(const char * attributeName, CredentialIssuerCommands * credsIssuerConfig) :
+        InteractionModelWriter(this), ModelCommand(kWriteCommandKey, credsIssuerConfig)
+    {
+        // Subclasses are responsible for calling AddArguments.
+    }
+
+    void AddArgumentClusterIds()
+    {
+        AddArgument("cluster-ids", 0, UINT32_MAX, &mClusterIds,
+                    "Comma-separated list of cluster ids to write to (e.g. \"6\" or \"6,0x201\").");
+    }
+
+    void AddArgumentAttributeIds()
+    {
+        AddArgument("attribute-ids", 0, UINT32_MAX, &mAttributeIds,
+                    "Comma-separated list of attribute ids to write (e.g. \"16385\" or \"16385,0x4002\").");
+    }
+
+    void AddArgumentAttributeName(const char * attributeName)
+    {
+        AddArgument("attribute-name", attributeName, "The attribute name to write.");
+    }
+
+    template <typename U = T, std::enable_if_t<std::is_same<U, std::vector<CustomArgument *>>::value, int> = 0>
+    static const char * GetAttributeValuesDescription()
+    {
+        return "Semicolon-separated list of attribute values to write. Each value is represented as follows, depending on the "
+               "type:\n"
+               "  * struct: a JSON-encoded object, with field ids as keys.\n"
+               "  * list: a JSON-encoded array of values.\n"
+               "  * null: A literal null.\n"
+               "  * boolean: A literal true or false.\n"
+               "  * unsigned integer: One of:\n"
+               "      a) The number directly, as decimal.\n"
+               "      b) The number directly, as 0x followed by hex digits. (Only for the toplevel value, not inside structs or "
+               "lists.)\n"
+               "      c) A string starting with \"u:\" followed by decimal digits\n"
+               "  * signed integer: One of:\n"
+               "      a) The number directly, if it's negative.\n"
+               "      c) A string starting with \"s:\" followed by decimal digits\n"
+               "  * single-precision float: A string starting with \"f:\" followed by the number.\n"
+               "  * double-precision float: One of:\n"
+               "      a) The number directly, if it's not an integer.\n"
+               "      b) A string starting with \"d:\" followed by the number.\n"
+               "  * octet string: A string starting with \"hex:\" followed by the hex encoding of the bytes.\n"
+               "  * string: A string with the characters.\n"
+               "\n"
+               "  Example values: '10;20', '10;\"u:20\"', '\"hex:aabbcc\";\"hello\"'.";
+    }
+
+    static const char * GetTypedAttributeValuesDescription() { return "Comma-separated list of attribute values to write."; }
+
+    template <typename U = T, std::enable_if_t<!std::is_same<U, std::vector<CustomArgument *>>::value, int> = 0>
+    static const char * GetAttributeValuesDescription()
+    {
+        return GetTypedAttributeValuesDescription();
+    }
+
+    template <typename minType, typename maxType>
+    void AddArgumentAttributeValues(minType minValue, maxType maxValue)
+    {
+        AddArgument("attribute-values", minValue, maxValue, &mAttributeValues, GetTypedAttributeValuesDescription());
+    }
+
+    void AddArgumentAttributeValues() { AddArgument("attribute-values", &mAttributeValues, GetAttributeValuesDescription()); }
+
+    void AddArgumentAttributeValues(TypedComplexArgument<T> & attributeParser)
+    {
+        attributeParser.SetArgument(&mAttributeValues);
+        AddArgument("attribute-values", &attributeParser, GetTypedAttributeValuesDescription());
+    }
+
+    void AddArguments()
+    {
+        AddArgument("timedInteractionTimeoutMs", 0, UINT16_MAX, &mTimedInteractionTimeoutMs,
+                    "If provided, do a timed write with the given timed interaction timeout. See \"7.6.10. Timed Interaction\" in "
+                    "the Matter specification.");
+        AddArgument("busyWaitForMs", 0, UINT16_MAX, &mBusyWaitForMs,
+                    "If provided, block the main thread processing for the given time right after sending a command.");
+        AddArgument("data-version", 0, UINT32_MAX, &mDataVersions,
+                    "Comma-separated list of data versions for the clusters being written.");
+        AddArgument("suppressResponse", 0, 1, &mSuppressResponse);
+        AddArgument("repeat-count", 1, UINT16_MAX, &mRepeatCount);
+        AddArgument("repeat-delay-ms", 0, UINT16_MAX, &mRepeatDelayInMs);
+        ModelCommand::AddArguments();
+    }
+
+private:
+    // This constructor is private as it is not intended to be used from outside the class.
+    WriteAttribute(chip::ClusterId clusterId, chip::AttributeId attributeId, WriteCommandType commandType,
+                   CredentialIssuerCommands * credsIssuerConfig) :
+        InteractionModelWriter(this),
+        ModelCommand(commandType == WriteCommandType::kWrite ? kWriteCommandKey : kForceWriteCommandKey, credsIssuerConfig),
+        mClusterIds(1, clusterId), mAttributeIds(1, attributeId)
+    {}
+
+    std::vector<chip::ClusterId> mClusterIds;
+    std::vector<chip::AttributeId> mAttributeIds;
+
+    CHIP_ERROR mError = CHIP_NO_ERROR;
+    T mAttributeValues;
+};
+
+template <class T>
+class WriteAttributeAsComplex : public WriteAttribute<T>
+{
+public:
+    WriteAttributeAsComplex(chip::ClusterId clusterId, const char * attributeName, chip::AttributeId attributeId,
+                            WriteCommandType commandType, CredentialIssuerCommands * credsIssuerConfig) :
+        WriteAttribute<T>(clusterId, attributeName, attributeId, mAttributeParser, commandType, credsIssuerConfig)
+    {}
+
+private:
+    TypedComplexArgument<T> mAttributeParser;
+};
diff --git a/examples/fabric-admin/commands/common/CHIPCommand.cpp b/examples/fabric-admin/commands/common/CHIPCommand.cpp
new file mode 100644
index 0000000..982c857
--- /dev/null
+++ b/examples/fabric-admin/commands/common/CHIPCommand.cpp
@@ -0,0 +1,651 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "CHIPCommand.h"
+
+#include <controller/CHIPDeviceControllerFactory.h>
+#include <credentials/attestation_verifier/FileAttestationTrustStore.h>
+#include <lib/core/CHIPConfig.h>
+#include <lib/core/CHIPVendorIdentifiers.hpp>
+#include <lib/support/CodeUtils.h>
+#include <lib/support/ScopedBuffer.h>
+#include <lib/support/TestGroupData.h>
+#include <platform/LockTracker.h>
+
+#include <string>
+
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+#include "TraceDecoder.h"
+#include "TraceHandlers.h"
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+
+std::map<CHIPCommand::CommissionerIdentity, std::unique_ptr<chip::Controller::DeviceCommissioner>> CHIPCommand::mCommissioners;
+std::set<CHIPCommand *> CHIPCommand::sDeferredCleanups;
+
+using DeviceControllerFactory = chip::Controller::DeviceControllerFactory;
+
+constexpr chip::FabricId kIdentityNullFabricId  = chip::kUndefinedFabricId;
+constexpr chip::FabricId kIdentityAlphaFabricId = 1;
+constexpr chip::FabricId kIdentityBetaFabricId  = 2;
+constexpr chip::FabricId kIdentityGammaFabricId = 3;
+constexpr chip::FabricId kIdentityOtherFabricId = 4;
+constexpr char kPAATrustStorePathVariable[]     = "FABRICSYNC_PAA_TRUST_STORE_PATH";
+constexpr char kCDTrustStorePathVariable[]      = "FABRICSYNC_CD_TRUST_STORE_PATH";
+
+const chip::Credentials::AttestationTrustStore * CHIPCommand::sTrustStore = nullptr;
+chip::Credentials::GroupDataProviderImpl CHIPCommand::sGroupDataProvider{ kMaxGroupsPerFabric, kMaxGroupKeysPerFabric };
+// All fabrics share the same ICD client storage.
+chip::app::DefaultICDClientStorage CHIPCommand::sICDClientStorage;
+chip::Crypto::RawKeySessionKeystore CHIPCommand::sSessionKeystore;
+chip::app::DefaultCheckInDelegate CHIPCommand::sCheckInDelegate;
+chip::app::CheckInHandler CHIPCommand::sCheckInHandler;
+
+namespace {
+
+CHIP_ERROR GetAttestationTrustStore(const char * paaTrustStorePath, const chip::Credentials::AttestationTrustStore ** trustStore)
+{
+    if (paaTrustStorePath == nullptr)
+    {
+        paaTrustStorePath = getenv(kPAATrustStorePathVariable);
+    }
+
+    if (paaTrustStorePath == nullptr)
+    {
+        *trustStore = chip::Credentials::GetTestAttestationTrustStore();
+        return CHIP_NO_ERROR;
+    }
+
+    static chip::Credentials::FileAttestationTrustStore attestationTrustStore{ paaTrustStorePath };
+
+    if (paaTrustStorePath != nullptr && attestationTrustStore.paaCount() == 0)
+    {
+        ChipLogError(NotSpecified, "No PAAs found in path: %s", paaTrustStorePath);
+        ChipLogError(NotSpecified,
+                     "Please specify a valid path containing trusted PAA certificates using "
+                     "the argument [--paa-trust-store-path paa/file/path] "
+                     "or environment variable [%s=paa/file/path]",
+                     kPAATrustStorePathVariable);
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    *trustStore = &attestationTrustStore;
+    return CHIP_NO_ERROR;
+}
+
+} // namespace
+
+CHIP_ERROR CHIPCommand::MaybeSetUpStack()
+{
+    if (IsInteractive())
+    {
+        return CHIP_NO_ERROR;
+    }
+
+    StartTracing();
+
+#if (CHIP_DEVICE_LAYER_TARGET_LINUX || CHIP_DEVICE_LAYER_TARGET_TIZEN) && CHIP_DEVICE_CONFIG_ENABLE_CHIPOBLE
+    // By default, Linux device is configured as a BLE peripheral while the controller needs a BLE central.
+    ReturnLogErrorOnFailure(chip::DeviceLayer::Internal::BLEMgrImpl().ConfigureBle(mBleAdapterId.ValueOr(0), true));
+#endif
+
+    ReturnLogErrorOnFailure(mDefaultStorage.Init(nullptr, GetStorageDirectory().ValueOr(nullptr)));
+    ReturnLogErrorOnFailure(mOperationalKeystore.Init(&mDefaultStorage));
+    ReturnLogErrorOnFailure(mOpCertStore.Init(&mDefaultStorage));
+
+    // fabric-admin uses a non-persistent keystore.
+    // ICD storage lifetime is currently tied to the fabric-admin's lifetime. Since fabric-admin interactive mode is currently used
+    // for ICD commissioning and check-in validation, this temporary storage meets the test requirements.
+    // TODO: Implement persistent ICD storage for the fabric-admin.
+    ReturnLogErrorOnFailure(sICDClientStorage.Init(&mDefaultStorage, &sSessionKeystore));
+
+    chip::Controller::FactoryInitParams factoryInitParams;
+
+    factoryInitParams.fabricIndependentStorage = &mDefaultStorage;
+    factoryInitParams.operationalKeystore      = &mOperationalKeystore;
+    factoryInitParams.opCertStore              = &mOpCertStore;
+    factoryInitParams.enableServerInteractions = NeedsOperationalAdvertising();
+    factoryInitParams.sessionKeystore          = &sSessionKeystore;
+
+    // Init group data provider that will be used for all group keys and IPKs for the
+    // fabric-admin-configured fabrics. This is OK to do once since the fabric tables
+    // and the DeviceControllerFactory all "share" in the same underlying data.
+    // Different commissioner implementations may want to use alternate implementations
+    // of GroupDataProvider for injection through factoryInitParams.
+    sGroupDataProvider.SetStorageDelegate(&mDefaultStorage);
+    sGroupDataProvider.SetSessionKeystore(factoryInitParams.sessionKeystore);
+    ReturnLogErrorOnFailure(sGroupDataProvider.Init());
+    chip::Credentials::SetGroupDataProvider(&sGroupDataProvider);
+    factoryInitParams.groupDataProvider = &sGroupDataProvider;
+
+    uint16_t port = mDefaultStorage.GetListenPort();
+    if (port != 0)
+    {
+        // Make sure different commissioners run on different ports.
+        port = static_cast<uint16_t>(port + CurrentCommissionerId());
+    }
+    factoryInitParams.listenPort = port;
+    ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().Init(factoryInitParams));
+
+    auto systemState = chip::Controller::DeviceControllerFactory::GetInstance().GetSystemState();
+    VerifyOrReturnError(nullptr != systemState, CHIP_ERROR_INCORRECT_STATE);
+
+    ReturnErrorOnFailure(GetAttestationTrustStore(mPaaTrustStorePath.ValueOr(nullptr), &sTrustStore));
+
+    auto engine = chip::app::InteractionModelEngine::GetInstance();
+    VerifyOrReturnError(engine != nullptr, CHIP_ERROR_INCORRECT_STATE);
+    ReturnLogErrorOnFailure(sCheckInDelegate.Init(&sICDClientStorage, engine));
+    ReturnLogErrorOnFailure(sCheckInHandler.Init(DeviceControllerFactory::GetInstance().GetSystemState()->ExchangeMgr(),
+                                                 &sICDClientStorage, &sCheckInDelegate, engine));
+
+    CommissionerIdentity nullIdentity{ kIdentityNull, chip::kUndefinedNodeId };
+    ReturnLogErrorOnFailure(InitializeCommissioner(nullIdentity, kIdentityNullFabricId));
+
+    // After initializing first commissioner, add the additional CD certs once
+    {
+        const char * cdTrustStorePath = mCDTrustStorePath.ValueOr(nullptr);
+        if (cdTrustStorePath == nullptr)
+        {
+            cdTrustStorePath = getenv(kCDTrustStorePathVariable);
+        }
+
+        auto additionalCdCerts =
+            chip::Credentials::LoadAllX509DerCerts(cdTrustStorePath, chip::Credentials::CertificateValidationMode::kPublicKeyOnly);
+        if (cdTrustStorePath != nullptr && additionalCdCerts.size() == 0)
+        {
+            ChipLogError(NotSpecified, "Warning: no CD signing certs found in path: %s, only defaults will be used",
+                         cdTrustStorePath);
+            ChipLogError(NotSpecified,
+                         "Please specify a path containing trusted CD verifying key certificates using "
+                         "the argument [--cd-trust-store-path cd/file/path] "
+                         "or environment variable [%s=cd/file/path]",
+                         kCDTrustStorePathVariable);
+        }
+        ReturnErrorOnFailure(mCredIssuerCmds->AddAdditionalCDVerifyingCerts(additionalCdCerts));
+    }
+    bool allowTestCdSigningKey = !mOnlyAllowTrustedCdKeys.ValueOr(false);
+    mCredIssuerCmds->SetCredentialIssuerOption(CredentialIssuerCommands::CredentialIssuerOptions::kAllowTestCdSigningKey,
+                                               allowTestCdSigningKey);
+
+    return CHIP_NO_ERROR;
+}
+
+void CHIPCommand::MaybeTearDownStack()
+{
+    if (IsInteractive())
+    {
+        return;
+    }
+
+    //
+    // We can call DeviceController::Shutdown() safely without grabbing the stack lock
+    // since the CHIP thread and event queue have been stopped, preventing any thread
+    // races.
+    //
+    for (auto & commissioner : mCommissioners)
+    {
+        ShutdownCommissioner(commissioner.first);
+    }
+
+    StopTracing();
+}
+
+CHIP_ERROR CHIPCommand::EnsureCommissionerForIdentity(std::string identity)
+{
+    chip::NodeId nodeId;
+    ReturnErrorOnFailure(GetIdentityNodeId(identity, &nodeId));
+    CommissionerIdentity lookupKey{ identity, nodeId };
+    if (mCommissioners.find(lookupKey) != mCommissioners.end())
+    {
+        return CHIP_NO_ERROR;
+    }
+
+    // Need to initialize the commissioner.
+    chip::FabricId fabricId;
+    if (identity == kIdentityAlpha)
+    {
+        fabricId = kIdentityAlphaFabricId;
+    }
+    else if (identity == kIdentityBeta)
+    {
+        fabricId = kIdentityBetaFabricId;
+    }
+    else if (identity == kIdentityGamma)
+    {
+        fabricId = kIdentityGammaFabricId;
+    }
+    else
+    {
+        fabricId = strtoull(identity.c_str(), nullptr, 0);
+        if (fabricId < kIdentityOtherFabricId)
+        {
+            ChipLogError(NotSpecified, "Invalid identity: %s", identity.c_str());
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+    }
+
+    return InitializeCommissioner(lookupKey, fabricId);
+}
+
+CHIP_ERROR CHIPCommand::Run()
+{
+    ReturnErrorOnFailure(MaybeSetUpStack());
+
+    CHIP_ERROR err = StartWaiting(GetWaitDuration());
+
+    if (IsInteractive())
+    {
+        bool timedOut;
+        // Give it 2 hours to run our cleanup; that should never get hit in practice.
+        CHIP_ERROR cleanupErr = RunOnMatterQueue(RunCommandCleanup, chip::System::Clock::Seconds16(7200), &timedOut);
+        VerifyOrDie(cleanupErr == CHIP_NO_ERROR);
+        VerifyOrDie(!timedOut);
+    }
+    else
+    {
+        CleanupAfterRun();
+    }
+
+    MaybeTearDownStack();
+
+    return err;
+}
+
+void CHIPCommand::StartTracing()
+{
+    if (mTraceTo.HasValue())
+    {
+        for (const auto & destination : mTraceTo.Value())
+        {
+            mTracingSetup.EnableTracingFor(destination.c_str());
+        }
+    }
+
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+    chip::trace::InitTrace();
+
+    if (mTraceFile.HasValue())
+    {
+        chip::trace::AddTraceStream(new chip::trace::TraceStreamFile(mTraceFile.Value()));
+    }
+    else if (mTraceLog.HasValue() && mTraceLog.Value())
+    {
+        chip::trace::AddTraceStream(new chip::trace::TraceStreamLog());
+    }
+
+    if (mTraceDecode.HasValue() && mTraceDecode.Value())
+    {
+        chip::trace::TraceDecoderOptions options;
+        // The interaction model protocol is already logged, so just disable logging those.
+        options.mEnableProtocolInteractionModelResponse = false;
+        chip::trace::TraceDecoder * decoder             = new chip::trace::TraceDecoder();
+        decoder->SetOptions(options);
+        chip::trace::AddTraceStream(decoder);
+    }
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+}
+
+void CHIPCommand::StopTracing()
+{
+    mTracingSetup.StopTracing();
+
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+    chip::trace::DeInitTrace();
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+}
+
+void CHIPCommand::SetIdentity(const char * identity)
+{
+    std::string name = std::string(identity);
+    if (name.compare(kIdentityAlpha) != 0 && name.compare(kIdentityBeta) != 0 && name.compare(kIdentityGamma) != 0 &&
+        name.compare(kIdentityNull) != 0 && strtoull(name.c_str(), nullptr, 0) < kIdentityOtherFabricId)
+    {
+        ChipLogError(NotSpecified, "Unknown commissioner name: %s. Supported names are [%s, %s, %s, 4, 5...]", name.c_str(),
+                     kIdentityAlpha, kIdentityBeta, kIdentityGamma);
+        chipDie();
+    }
+
+    mCommissionerName.SetValue(const_cast<char *>(identity));
+}
+
+std::string CHIPCommand::GetIdentity()
+{
+    std::string name = mCommissionerName.HasValue() ? mCommissionerName.Value() : kIdentityAlpha;
+    if (name.compare(kIdentityAlpha) != 0 && name.compare(kIdentityBeta) != 0 && name.compare(kIdentityGamma) != 0 &&
+        name.compare(kIdentityNull) != 0)
+    {
+        chip::FabricId fabricId = strtoull(name.c_str(), nullptr, 0);
+        if (fabricId >= kIdentityOtherFabricId)
+        {
+            // normalize name since it is used in persistent storage
+
+            char s[24];
+            sprintf(s, "%lx", fabricId);
+
+            name = s;
+        }
+        else
+        {
+            ChipLogError(NotSpecified, "Unknown commissioner name: %s. Supported names are [%s, %s, %s, 4, 5...]", name.c_str(),
+                         kIdentityAlpha, kIdentityBeta, kIdentityGamma);
+            chipDie();
+        }
+    }
+
+    return name;
+}
+
+CHIP_ERROR CHIPCommand::GetIdentityNodeId(std::string identity, chip::NodeId * nodeId)
+{
+    if (mCommissionerNodeId.HasValue())
+    {
+        *nodeId = mCommissionerNodeId.Value();
+        return CHIP_NO_ERROR;
+    }
+
+    if (identity == kIdentityNull)
+    {
+        *nodeId = chip::kUndefinedNodeId;
+        return CHIP_NO_ERROR;
+    }
+
+    ReturnLogErrorOnFailure(mCommissionerStorage.Init(identity.c_str(), GetStorageDirectory().ValueOr(nullptr)));
+
+    *nodeId = mCommissionerStorage.GetLocalNodeId();
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR CHIPCommand::GetIdentityRootCertificate(std::string identity, chip::ByteSpan & span)
+{
+    if (identity == kIdentityNull)
+    {
+        return CHIP_ERROR_NOT_FOUND;
+    }
+
+    chip::NodeId nodeId;
+    VerifyOrDie(GetIdentityNodeId(identity, &nodeId) == CHIP_NO_ERROR);
+    CommissionerIdentity lookupKey{ identity, nodeId };
+    auto item = mCommissioners.find(lookupKey);
+
+    span = chip::ByteSpan(item->first.mRCAC, item->first.mRCACLen);
+    return CHIP_NO_ERROR;
+}
+
+chip::FabricId CHIPCommand::CurrentCommissionerId()
+{
+    chip::FabricId id;
+
+    std::string name = GetIdentity();
+    if (name.compare(kIdentityAlpha) == 0)
+    {
+        id = kIdentityAlphaFabricId;
+    }
+    else if (name.compare(kIdentityBeta) == 0)
+    {
+        id = kIdentityBetaFabricId;
+    }
+    else if (name.compare(kIdentityGamma) == 0)
+    {
+        id = kIdentityGammaFabricId;
+    }
+    else if (name.compare(kIdentityNull) == 0)
+    {
+        id = kIdentityNullFabricId;
+    }
+    else if ((id = strtoull(name.c_str(), nullptr, 0)) < kIdentityOtherFabricId)
+    {
+        VerifyOrDieWithMsg(false, NotSpecified, "Unknown commissioner name: %s. Supported names are [%s, %s, %s, 4, 5...]",
+                           name.c_str(), kIdentityAlpha, kIdentityBeta, kIdentityGamma);
+    }
+
+    return id;
+}
+
+chip::Controller::DeviceCommissioner & CHIPCommand::CurrentCommissioner()
+{
+    return GetCommissioner(GetIdentity());
+}
+
+chip::Controller::DeviceCommissioner & CHIPCommand::GetCommissioner(std::string identity)
+{
+    // We don't have a great way to handle commissioner setup failures here.
+    // This only matters for commands (like TestCommand) that involve multiple
+    // identities.
+    VerifyOrDie(EnsureCommissionerForIdentity(identity) == CHIP_NO_ERROR);
+
+    chip::NodeId nodeId;
+    VerifyOrDie(GetIdentityNodeId(identity, &nodeId) == CHIP_NO_ERROR);
+    CommissionerIdentity lookupKey{ identity, nodeId };
+    auto item = mCommissioners.find(lookupKey);
+    VerifyOrDie(item != mCommissioners.end());
+    return *item->second;
+}
+
+void CHIPCommand::ShutdownCommissioner(const CommissionerIdentity & key)
+{
+    mCommissioners[key].get()->Shutdown();
+}
+
+CHIP_ERROR CHIPCommand::InitializeCommissioner(CommissionerIdentity & identity, chip::FabricId fabricId)
+{
+    std::unique_ptr<ChipDeviceCommissioner> commissioner = std::make_unique<ChipDeviceCommissioner>();
+    chip::Controller::SetupParams commissionerParams;
+
+    ReturnLogErrorOnFailure(mCredIssuerCmds->SetupDeviceAttestation(commissionerParams, sTrustStore));
+
+    chip::Crypto::P256Keypair ephemeralKey;
+
+    if (fabricId != chip::kUndefinedFabricId)
+    {
+
+        // TODO - OpCreds should only be generated for pairing command
+        //        store the credentials in persistent storage, and
+        //        generate when not available in the storage.
+        ReturnLogErrorOnFailure(mCommissionerStorage.Init(identity.mName.c_str(), GetStorageDirectory().ValueOr(nullptr)));
+        if (mUseMaxSizedCerts.HasValue())
+        {
+            auto option = CredentialIssuerCommands::CredentialIssuerOptions::kMaximizeCertificateSizes;
+            mCredIssuerCmds->SetCredentialIssuerOption(option, mUseMaxSizedCerts.Value());
+        }
+
+        ReturnLogErrorOnFailure(mCredIssuerCmds->InitializeCredentialsIssuer(mCommissionerStorage));
+
+        chip::MutableByteSpan nocSpan(identity.mNOC);
+        chip::MutableByteSpan icacSpan(identity.mICAC);
+        chip::MutableByteSpan rcacSpan(identity.mRCAC);
+
+        ReturnLogErrorOnFailure(ephemeralKey.Initialize(chip::Crypto::ECPKeyTarget::ECDSA));
+
+        ReturnLogErrorOnFailure(mCredIssuerCmds->GenerateControllerNOCChain(identity.mLocalNodeId, fabricId,
+                                                                            mCommissionerStorage.GetCommissionerCATs(),
+                                                                            ephemeralKey, rcacSpan, icacSpan, nocSpan));
+
+        identity.mRCACLen = rcacSpan.size();
+        identity.mICACLen = icacSpan.size();
+        identity.mNOCLen  = nocSpan.size();
+
+        commissionerParams.operationalKeypair           = &ephemeralKey;
+        commissionerParams.controllerRCAC               = rcacSpan;
+        commissionerParams.controllerICAC               = icacSpan;
+        commissionerParams.controllerNOC                = nocSpan;
+        commissionerParams.permitMultiControllerFabrics = true;
+        commissionerParams.enableServerInteractions     = NeedsOperationalAdvertising();
+    }
+
+    // TODO: Initialize IPK epoch key in ExampleOperationalCredentials issuer rather than relying on DefaultIpkValue
+    commissionerParams.operationalCredentialsDelegate = mCredIssuerCmds->GetCredentialIssuer();
+    commissionerParams.controllerVendorId             = mCommissionerVendorId.ValueOr(chip::VendorId::TestVendor1);
+
+    ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().SetupCommissioner(commissionerParams, *(commissioner.get())));
+
+    if (identity.mName != kIdentityNull)
+    {
+        // Initialize Group Data, including IPK
+        chip::FabricIndex fabricIndex = commissioner->GetFabricIndex();
+        uint8_t compressed_fabric_id[sizeof(uint64_t)];
+        chip::MutableByteSpan compressed_fabric_id_span(compressed_fabric_id);
+        ReturnLogErrorOnFailure(commissioner->GetCompressedFabricIdBytes(compressed_fabric_id_span));
+
+        ReturnLogErrorOnFailure(chip::GroupTesting::InitData(&sGroupDataProvider, fabricIndex, compressed_fabric_id_span));
+
+        // Configure the default IPK for all fabrics used by CHIP-tool. The epoch
+        // key is the same, but the derived keys will be different for each fabric.
+        chip::ByteSpan defaultIpk = chip::GroupTesting::DefaultIpkValue::GetDefaultIpk();
+        ReturnLogErrorOnFailure(
+            chip::Credentials::SetSingleIpkEpochKey(&sGroupDataProvider, fabricIndex, defaultIpk, compressed_fabric_id_span));
+    }
+
+    CHIPCommand::sICDClientStorage.UpdateFabricList(commissioner->GetFabricIndex());
+
+    mCommissioners[identity] = std::move(commissioner);
+
+    return CHIP_NO_ERROR;
+}
+
+void CHIPCommand::RunQueuedCommand(intptr_t commandArg)
+{
+    auto * command = reinterpret_cast<CHIPCommand *>(commandArg);
+    CHIP_ERROR err = command->EnsureCommissionerForIdentity(command->GetIdentity());
+    if (err == CHIP_NO_ERROR)
+    {
+        err = command->RunCommand();
+    }
+
+    if (err != CHIP_NO_ERROR)
+    {
+        command->SetCommandExitStatus(err);
+    }
+}
+
+void CHIPCommand::RunCommandCleanup(intptr_t commandArg)
+{
+    auto * command = reinterpret_cast<CHIPCommand *>(commandArg);
+    command->CleanupAfterRun();
+    command->StopWaiting();
+}
+
+void CHIPCommand::CleanupAfterRun()
+{
+    assertChipStackLockedByCurrentThread();
+    bool deferCleanup = (IsInteractive() && DeferInteractiveCleanup());
+
+    Shutdown();
+
+    if (deferCleanup)
+    {
+        sDeferredCleanups.insert(this);
+    }
+    else
+    {
+        Cleanup();
+    }
+}
+
+CHIP_ERROR CHIPCommand::RunOnMatterQueue(MatterWorkCallback callback, chip::System::Clock::Timeout timeout, bool * timedOut)
+{
+    {
+        std::lock_guard<std::mutex> lk(cvWaitingForResponseMutex);
+        mWaitingForResponse = true;
+    }
+
+    auto err = chip::DeviceLayer::PlatformMgr().ScheduleWork(callback, reinterpret_cast<intptr_t>(this));
+    if (CHIP_NO_ERROR != err)
+    {
+        {
+            std::lock_guard<std::mutex> lk(cvWaitingForResponseMutex);
+            mWaitingForResponse = false;
+        }
+        return err;
+    }
+
+    auto waitingUntil = std::chrono::system_clock::now() + std::chrono::duration_cast<std::chrono::seconds>(timeout);
+    {
+        std::unique_lock<std::mutex> lk(cvWaitingForResponseMutex);
+        *timedOut = !cvWaitingForResponse.wait_until(lk, waitingUntil, [this]() { return !this->mWaitingForResponse; });
+    }
+
+    return CHIP_NO_ERROR;
+}
+
+#if !CONFIG_USE_SEPARATE_EVENTLOOP
+static void OnResponseTimeout(chip::System::Layer *, void * appState)
+{
+    (reinterpret_cast<CHIPCommand *>(appState))->SetCommandExitStatus(CHIP_ERROR_TIMEOUT);
+}
+#endif // !CONFIG_USE_SEPARATE_EVENTLOOP
+
+CHIP_ERROR CHIPCommand::StartWaiting(chip::System::Clock::Timeout duration)
+{
+#if CONFIG_USE_SEPARATE_EVENTLOOP
+    // ServiceEvents() calls StartEventLoopTask(), which is paired with the StopEventLoopTask() below.
+    if (!IsInteractive())
+    {
+        ReturnLogErrorOnFailure(DeviceControllerFactory::GetInstance().ServiceEvents());
+    }
+
+    if (duration.count() == 0)
+    {
+        mCommandExitStatus = RunCommand();
+    }
+    else
+    {
+        bool timedOut;
+        CHIP_ERROR err = RunOnMatterQueue(RunQueuedCommand, duration, &timedOut);
+        if (CHIP_NO_ERROR != err)
+        {
+            return err;
+        }
+        if (timedOut)
+        {
+            mCommandExitStatus = CHIP_ERROR_TIMEOUT;
+        }
+    }
+    if (!IsInteractive())
+    {
+        LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().StopEventLoopTask());
+    }
+#else
+    chip::DeviceLayer::PlatformMgr().ScheduleWork(RunQueuedCommand, reinterpret_cast<intptr_t>(this));
+    ReturnLogErrorOnFailure(chip::DeviceLayer::SystemLayer().StartTimer(duration, OnResponseTimeout, this));
+    chip::DeviceLayer::PlatformMgr().RunEventLoop();
+#endif // CONFIG_USE_SEPARATE_EVENTLOOP
+
+    return mCommandExitStatus;
+}
+
+void CHIPCommand::StopWaiting()
+{
+#if CONFIG_USE_SEPARATE_EVENTLOOP
+    {
+        std::lock_guard<std::mutex> lk(cvWaitingForResponseMutex);
+        mWaitingForResponse = false;
+    }
+    cvWaitingForResponse.notify_all();
+#else  // CONFIG_USE_SEPARATE_EVENTLOOP
+    LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().StopEventLoopTask());
+#endif // CONFIG_USE_SEPARATE_EVENTLOOP
+}
+
+void CHIPCommand::ExecuteDeferredCleanups(intptr_t ignored)
+{
+    for (auto * cmd : sDeferredCleanups)
+    {
+        cmd->Cleanup();
+    }
+    sDeferredCleanups.clear();
+}
diff --git a/examples/fabric-admin/commands/common/CHIPCommand.h b/examples/fabric-admin/commands/common/CHIPCommand.h
new file mode 100644
index 0000000..856a4da
--- /dev/null
+++ b/examples/fabric-admin/commands/common/CHIPCommand.h
@@ -0,0 +1,264 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+#include <controller/ExamplePersistentStorage.h>
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+#include "Command.h"
+
+#include <TracingCommandLineArgument.h>
+#include <app/icd/client/CheckInHandler.h>
+#include <app/icd/client/DefaultCheckInDelegate.h>
+#include <app/icd/client/DefaultICDClientStorage.h>
+#include <commands/common/CredentialIssuerCommands.h>
+#include <commands/example/ExampleCredentialIssuerCommands.h>
+#include <credentials/GroupDataProviderImpl.h>
+#include <credentials/PersistentStorageOpCertStore.h>
+#include <crypto/PersistentStorageOperationalKeystore.h>
+#include <crypto/RawKeySessionKeystore.h>
+
+#include <string>
+
+inline constexpr char kIdentityAlpha[] = "alpha";
+inline constexpr char kIdentityBeta[]  = "beta";
+inline constexpr char kIdentityGamma[] = "gamma";
+// The null fabric commissioner is a commissioner that isn't on a fabric.
+// This is a legal configuration in which the commissioner delegates
+// operational communication and invocation of the commssioning complete
+// command to a separate on-fabric administrator node.
+//
+// The null-fabric-commissioner identity is provided here to demonstrate the
+// commissioner portion of such an architecture.  The null-fabric-commissioner
+// can carry a commissioning flow up until the point of operational channel
+// (CASE) communcation.
+inline constexpr char kIdentityNull[] = "null-fabric-commissioner";
+
+class CHIPCommand : public Command
+{
+public:
+    using ChipDeviceCommissioner = ::chip::Controller::DeviceCommissioner;
+    using ChipDeviceController   = ::chip::Controller::DeviceController;
+    using IPAddress              = ::chip::Inet::IPAddress;
+    using NodeId                 = ::chip::NodeId;
+    using PeerId                 = ::chip::PeerId;
+    using PeerAddress            = ::chip::Transport::PeerAddress;
+
+    static constexpr uint16_t kMaxGroupsPerFabric    = 50;
+    static constexpr uint16_t kMaxGroupKeysPerFabric = 25;
+
+    CHIPCommand(const char * commandName, CredentialIssuerCommands * credIssuerCmds, const char * helpText = nullptr) :
+        Command(commandName, helpText), mCredIssuerCmds(credIssuerCmds)
+    {
+        AddArgument("paa-trust-store-path", &mPaaTrustStorePath,
+                    "Path to directory holding PAA certificate information.  Can be absolute or relative to the current working "
+                    "directory.");
+        AddArgument("cd-trust-store-path", &mCDTrustStorePath,
+                    "Path to directory holding CD certificate information.  Can be absolute or relative to the current working "
+                    "directory.");
+        AddArgument("commissioner-name", &mCommissionerName,
+                    "Name of fabric to use. Valid values are \"alpha\", \"beta\", \"gamma\", and integers greater than or equal to "
+                    "4.  The default if not specified is \"alpha\".");
+        AddArgument("commissioner-nodeid", 0, UINT64_MAX, &mCommissionerNodeId,
+                    "The node id to use for fabric-admin.  If not provided, kTestControllerNodeId (112233, 0x1B669) will be used.");
+        AddArgument("use-max-sized-certs", 0, 1, &mUseMaxSizedCerts,
+                    "Maximize the size of operational certificates. If not provided or 0 (\"false\"), normally sized operational "
+                    "certificates are generated.");
+        AddArgument("only-allow-trusted-cd-keys", 0, 1, &mOnlyAllowTrustedCdKeys,
+                    "Only allow trusted CD verifying keys (disallow test keys). If not provided or 0 (\"false\"), untrusted CD "
+                    "verifying keys are allowed. If 1 (\"true\"), test keys are disallowed.");
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+        AddArgument("trace_file", &mTraceFile);
+        AddArgument("trace_log", 0, 1, &mTraceLog);
+        AddArgument("trace_decode", 0, 1, &mTraceDecode);
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+        AddArgument("trace-to", &mTraceTo, "Trace destinations, comma-separated (" SUPPORTED_COMMAND_LINE_TRACING_TARGETS ")");
+        AddArgument("ble-adapter", 0, UINT16_MAX, &mBleAdapterId);
+        AddArgument("storage-directory", &mStorageDirectory,
+                    "Directory to place fabric-admin's storage files in.  Defaults to $TMPDIR, with fallback to /tmp");
+        AddArgument(
+            "commissioner-vendor-id", 0, UINT16_MAX, &mCommissionerVendorId,
+            "The vendor id to use for fabric-admin. If not provided, chip::VendorId::TestVendor1 (65521, 0xFFF1) will be used.");
+    }
+
+    /////////// Command Interface /////////
+    CHIP_ERROR Run() override;
+
+    void SetCommandExitStatus(CHIP_ERROR status)
+    {
+        mCommandExitStatus = status;
+        // In interactive mode the stack is not shut down once a command is ended.
+        // That means calling `ErrorStr(err)` from the main thread when command
+        // completion is signaled may race since `ErrorStr` uses a static sErrorStr
+        // buffer for computing the error string.  Call it here instead.
+        if (IsInteractive() && CHIP_NO_ERROR != status)
+        {
+            ChipLogError(NotSpecified, "Run command failure: %s", chip::ErrorStr(status));
+        }
+        StopWaiting();
+    }
+
+protected:
+    // Will be called in a setting in which it's safe to touch the CHIP
+    // stack. The rules for Run() are as follows:
+    //
+    // 1) If error is returned, Run() must not call SetCommandExitStatus.
+    // 2) If success is returned Run() must either have called
+    //    SetCommandExitStatus() or scheduled async work that will do that.
+    virtual CHIP_ERROR RunCommand() = 0;
+
+    // Get the wait duration, in seconds, before the command times out.
+    virtual chip::System::Clock::Timeout GetWaitDuration() const = 0;
+
+    // Shut down the command.  After a Shutdown call the command object is ready
+    // to be used for another command invocation.
+    virtual void Shutdown() { ResetArguments(); }
+
+    // Clean up any resources allocated by the command.  Some commands may hold
+    // on to resources after Shutdown(), but Cleanup() will guarantee those are
+    // cleaned up.
+    virtual void Cleanup() {}
+
+    // If true, skip calling Cleanup() when in interactive mode, so the command
+    // can keep doing work as needed.  Cleanup() will be called when quitting
+    // interactive mode.  This method will be called before Shutdown, so it can
+    // use member values that Shutdown will normally reset.
+    virtual bool DeferInteractiveCleanup() { return false; }
+
+    // If true, the controller will be created with server capabilities enabled,
+    // such as advertising operational nodes over DNS-SD and accepting incoming
+    // CASE sessions.
+    virtual bool NeedsOperationalAdvertising() { return mAdvertiseOperational; }
+
+    // Execute any deferred cleanups.  Used when exiting interactive mode.
+    static void ExecuteDeferredCleanups(intptr_t ignored);
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+    PersistentStorage mDefaultStorage;
+    // TODO: It's pretty weird that we re-init mCommissionerStorage for every
+    // identity without shutting it down or something in between...
+    PersistentStorage mCommissionerStorage;
+#endif // CONFIG_USE_LOCAL_STORAGE
+    chip::PersistentStorageOperationalKeystore mOperationalKeystore;
+    chip::Credentials::PersistentStorageOpCertStore mOpCertStore;
+    static chip::Crypto::RawKeySessionKeystore sSessionKeystore;
+
+    static chip::Credentials::GroupDataProviderImpl sGroupDataProvider;
+    static chip::app::DefaultICDClientStorage sICDClientStorage;
+    static chip::app::DefaultCheckInDelegate sCheckInDelegate;
+    static chip::app::CheckInHandler sCheckInHandler;
+    CredentialIssuerCommands * mCredIssuerCmds;
+
+    std::string GetIdentity();
+    CHIP_ERROR GetIdentityNodeId(std::string identity, chip::NodeId * nodeId);
+    CHIP_ERROR GetIdentityRootCertificate(std::string identity, chip::ByteSpan & span);
+    void SetIdentity(const char * name);
+
+    // This method returns the commissioner instance to be used for running the command.
+    // The default commissioner instance name is "alpha", but it can be overridden by passing
+    // --identity "instance name" when running a command.
+    ChipDeviceCommissioner & CurrentCommissioner();
+
+    ChipDeviceCommissioner & GetCommissioner(std::string identity);
+
+private:
+    CHIP_ERROR MaybeSetUpStack();
+    void MaybeTearDownStack();
+
+    CHIP_ERROR EnsureCommissionerForIdentity(std::string identity);
+
+    // Commissioners are keyed by name and local node id.
+    struct CommissionerIdentity
+    {
+        bool operator<(const CommissionerIdentity & other) const
+        {
+            return mName < other.mName || (mName == other.mName && mLocalNodeId < other.mLocalNodeId);
+        }
+        std::string mName;
+        chip::NodeId mLocalNodeId;
+        uint8_t mRCAC[chip::Controller::kMaxCHIPDERCertLength] = {};
+        uint8_t mICAC[chip::Controller::kMaxCHIPDERCertLength] = {};
+        uint8_t mNOC[chip::Controller::kMaxCHIPDERCertLength]  = {};
+
+        size_t mRCACLen;
+        size_t mICACLen;
+        size_t mNOCLen;
+    };
+
+    // InitializeCommissioner uses various members, so can't be static.  This is
+    // obviously a little odd, since the commissioners are then shared across
+    // multiple commands in interactive mode...
+    CHIP_ERROR InitializeCommissioner(CommissionerIdentity & identity, chip::FabricId fabricId);
+    void ShutdownCommissioner(const CommissionerIdentity & key);
+    chip::FabricId CurrentCommissionerId();
+
+    static std::map<CommissionerIdentity, std::unique_ptr<ChipDeviceCommissioner>> mCommissioners;
+    static std::set<CHIPCommand *> sDeferredCleanups;
+
+    chip::Optional<char *> mCommissionerName;
+    chip::Optional<chip::NodeId> mCommissionerNodeId;
+    chip::Optional<chip::VendorId> mCommissionerVendorId;
+    chip::Optional<uint16_t> mBleAdapterId;
+    chip::Optional<char *> mPaaTrustStorePath;
+    chip::Optional<char *> mCDTrustStorePath;
+    chip::Optional<bool> mUseMaxSizedCerts;
+    chip::Optional<bool> mOnlyAllowTrustedCdKeys;
+
+    // Cached trust store so commands other than the original startup command
+    // can spin up commissioners as needed.
+    static const chip::Credentials::AttestationTrustStore * sTrustStore;
+
+    static void RunQueuedCommand(intptr_t commandArg);
+    typedef decltype(RunQueuedCommand) MatterWorkCallback;
+    static void RunCommandCleanup(intptr_t commandArg);
+
+    // Do cleanup after a commmand is done running.  Must happen with the
+    // Matter stack locked.
+    void CleanupAfterRun();
+
+    // Run the given callback on the Matter thread.  Return whether we managed
+    // to successfully dispatch it to the Matter thread.  If we did, *timedOut
+    // will be set to whether we timed out or whether our mWaitingForResponse
+    // got set to false by the callback itself.
+    CHIP_ERROR RunOnMatterQueue(MatterWorkCallback callback, chip::System::Clock::Timeout timeout, bool * timedOut);
+
+    CHIP_ERROR mCommandExitStatus = CHIP_ERROR_INTERNAL;
+
+    CHIP_ERROR StartWaiting(chip::System::Clock::Timeout seconds);
+    void StopWaiting();
+
+#if CONFIG_USE_SEPARATE_EVENTLOOP
+    std::condition_variable cvWaitingForResponse;
+    std::mutex cvWaitingForResponseMutex;
+    bool mWaitingForResponse{ true };
+#endif // CONFIG_USE_SEPARATE_EVENTLOOP
+
+    void StartTracing();
+    void StopTracing();
+
+#if CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+    chip::Optional<char *> mTraceFile;
+    chip::Optional<bool> mTraceLog;
+    chip::Optional<bool> mTraceDecode;
+#endif // CHIP_CONFIG_TRANSPORT_TRACE_ENABLED
+
+    chip::CommandLineApp::TracingSetup mTracingSetup;
+    chip::Optional<std::vector<std::string>> mTraceTo;
+};
diff --git a/examples/fabric-admin/commands/common/Command.cpp b/examples/fabric-admin/commands/common/Command.cpp
new file mode 100644
index 0000000..7e56891
--- /dev/null
+++ b/examples/fabric-admin/commands/common/Command.cpp
@@ -0,0 +1,1088 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "Command.h"
+#include "CustomStringPrefix.h"
+#include "HexConversion.h"
+#include "platform/PlatformManager.h"
+
+#include <functional>
+#include <netdb.h>
+#include <sstream>
+#include <string>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <math.h> // For INFINITY
+
+#include <lib/core/CHIPSafeCasts.h>
+#include <lib/support/BytesToHex.h>
+#include <lib/support/CHIPMem.h>
+#include <lib/support/CodeUtils.h>
+#include <lib/support/SafeInt.h>
+#include <lib/support/ScopedBuffer.h>
+#include <lib/support/StringSplitter.h>
+#include <lib/support/logging/CHIPLogging.h>
+
+constexpr char kOptionalArgumentPrefix[]       = "--";
+constexpr size_t kOptionalArgumentPrefixLength = 2;
+
+bool Command::InitArguments(int argc, char ** argv)
+{
+    bool isValidCommand = false;
+
+    size_t argvExtraArgsCount = (size_t) argc;
+    size_t mandatoryArgsCount = 0;
+    size_t optionalArgsCount  = 0;
+    for (auto & arg : mArgs)
+    {
+        if (arg.isOptional())
+        {
+            optionalArgsCount++;
+        }
+        else
+        {
+            mandatoryArgsCount++;
+            argvExtraArgsCount--;
+        }
+    }
+
+    VerifyOrExit((size_t) (argc) >= mandatoryArgsCount && (argvExtraArgsCount == 0 || (argvExtraArgsCount && optionalArgsCount)),
+                 ChipLogError(NotSpecified, "InitArgs: Wrong arguments number: %d instead of %u", argc,
+                              static_cast<unsigned int>(mandatoryArgsCount)));
+
+    // Initialize mandatory arguments
+    for (size_t i = 0; i < mandatoryArgsCount; i++)
+    {
+        char * arg = argv[i];
+        if (!InitArgument(i, arg))
+        {
+            ExitNow();
+        }
+    }
+
+    // Initialize optional arguments
+    // Optional arguments expect a name and a value, so i is increased by 2 on every step.
+    for (size_t i = mandatoryArgsCount; i < (size_t) argc; i += 2)
+    {
+        bool found = false;
+        for (size_t j = mandatoryArgsCount; j < mandatoryArgsCount + optionalArgsCount; j++)
+        {
+            // optional arguments starts with kOptionalArgumentPrefix
+            if (strlen(argv[i]) <= kOptionalArgumentPrefixLength &&
+                strncmp(argv[i], kOptionalArgumentPrefix, kOptionalArgumentPrefixLength) != 0)
+            {
+                continue;
+            }
+
+            if (strcmp(argv[i] + strlen(kOptionalArgumentPrefix), mArgs[j].name) == 0)
+            {
+                found = true;
+
+                VerifyOrExit((size_t) argc > (i + 1),
+                             ChipLogError(NotSpecified, "InitArgs: Optional argument %s missing value.", argv[i]));
+                if (!InitArgument(j, argv[i + 1]))
+                {
+                    ExitNow();
+                }
+            }
+        }
+        VerifyOrExit(found, ChipLogError(NotSpecified, "InitArgs: Optional argument %s does not exist.", argv[i]));
+    }
+
+    isValidCommand = true;
+
+exit:
+    return isValidCommand;
+}
+
+static bool ParseAddressWithInterface(const char * addressString, Command::AddressWithInterface * address)
+{
+    struct addrinfo hints;
+    struct addrinfo * result;
+    int ret;
+
+    memset(&hints, 0, sizeof(hints));
+    hints.ai_family   = AF_UNSPEC;
+    hints.ai_socktype = SOCK_DGRAM;
+    ret               = getaddrinfo(addressString, nullptr, &hints, &result);
+    if (ret < 0)
+    {
+        ChipLogError(NotSpecified, "Invalid address: %s", addressString);
+        return false;
+    }
+
+    if (result->ai_family == AF_INET6)
+    {
+        struct sockaddr_in6 * addr = reinterpret_cast<struct sockaddr_in6 *>(result->ai_addr);
+        address->address           = ::chip::Inet::IPAddress::FromSockAddr(*addr);
+        address->interfaceId       = ::chip::Inet::InterfaceId(addr->sin6_scope_id);
+    }
+#if INET_CONFIG_ENABLE_IPV4
+    else if (result->ai_family == AF_INET)
+    {
+        address->address     = ::chip::Inet::IPAddress::FromSockAddr(*reinterpret_cast<struct sockaddr_in *>(result->ai_addr));
+        address->interfaceId = chip::Inet::InterfaceId::Null();
+    }
+#endif // INET_CONFIG_ENABLE_IPV4
+    else
+    {
+        ChipLogError(NotSpecified, "Unsupported address: %s", addressString);
+        return false;
+    }
+
+    return true;
+}
+
+// The callback should return whether the argument is valid, for the non-null
+// case.  It can't directly write to isValidArgument (by closing over it)
+// because in the nullable-and-null case we need to do that from this function,
+// via the return value.
+template <typename T>
+bool HandleNullableOptional(Argument & arg, char * argValue, std::function<bool(T * value)> callback)
+{
+    if (arg.isOptional())
+    {
+        if (arg.isNullable())
+        {
+            arg.value = &(reinterpret_cast<chip::Optional<chip::app::DataModel::Nullable<T>> *>(arg.value)->Emplace());
+        }
+        else
+        {
+            arg.value = &(reinterpret_cast<chip::Optional<T> *>(arg.value)->Emplace());
+        }
+    }
+
+    if (arg.isNullable())
+    {
+        auto * nullable = reinterpret_cast<chip::app::DataModel::Nullable<T> *>(arg.value);
+        if (argValue != nullptr && strncmp(argValue, "null", 4) == 0)
+        {
+            nullable->SetNull();
+            return true;
+        }
+
+        arg.value = &(nullable->SetNonNull());
+    }
+
+    return callback(reinterpret_cast<T *>(arg.value));
+}
+
+bool Command::InitArgument(size_t argIndex, char * argValue)
+{
+    bool isValidArgument = false;
+    bool isHexNotation   = strncmp(argValue, "0x", 2) == 0 || strncmp(argValue, "0X", 2) == 0;
+
+    Argument arg = mArgs.at(argIndex);
+
+    // We have two places where we handle uint8_t-typed args (actual int8u and
+    // bool args), so declare the handler function here so it can be reused.
+    auto uint8Handler = [&](uint8_t * value) {
+        // stringstream treats uint8_t as char, which is not what we want here.
+        uint16_t tmpValue;
+        std::stringstream ss;
+        isHexNotation ? (ss << std::hex << argValue) : (ss << argValue);
+        ss >> tmpValue;
+        if (chip::CanCastTo<uint8_t>(tmpValue))
+        {
+            *value = static_cast<uint8_t>(tmpValue);
+
+            uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+            uint64_t max = arg.max;
+            return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+        }
+
+        return false;
+    };
+
+    switch (arg.type)
+    {
+    case ArgumentType::Complex: {
+        // Complex arguments may be optional, but they are not currently supported via the <chip::Optional> class.
+        // Instead, they must be explicitly specified as optional using the kOptional flag,
+        // and the base TypedComplexArgument<T> class is still referenced.
+        auto complexArgument = static_cast<ComplexArgument *>(arg.value);
+        return CHIP_NO_ERROR == complexArgument->Parse(arg.name, argValue);
+    }
+
+    case ArgumentType::Custom: {
+        auto customArgument = static_cast<CustomArgument *>(arg.value);
+        return CHIP_NO_ERROR == customArgument->Parse(arg.name, argValue);
+    }
+
+    case ArgumentType::VectorString: {
+        std::vector<std::string> vectorArgument;
+
+        chip::StringSplitter splitter(argValue, ',');
+        chip::CharSpan value;
+
+        while (splitter.Next(value))
+        {
+            vectorArgument.push_back(std::string(value.data(), value.size()));
+        }
+
+        if (arg.flags == Argument::kOptional)
+        {
+            auto argument = static_cast<chip::Optional<std::vector<std::string>> *>(arg.value);
+            argument->SetValue(vectorArgument);
+        }
+        else
+        {
+            auto argument = static_cast<std::vector<std::string> *>(arg.value);
+            *argument     = vectorArgument;
+        }
+        return true;
+    }
+    case ArgumentType::VectorBool: {
+        // Currently only chip::Optional<std::vector<bool>> is supported.
+        if (arg.flags != Argument::kOptional)
+        {
+            return false;
+        }
+
+        std::vector<bool> vectorArgument;
+        std::stringstream ss(argValue);
+        while (ss.good())
+        {
+            std::string valueAsString;
+            getline(ss, valueAsString, ',');
+
+            if (strcasecmp(valueAsString.c_str(), "true") == 0)
+            {
+                vectorArgument.push_back(true);
+            }
+            else if (strcasecmp(valueAsString.c_str(), "false") == 0)
+            {
+                vectorArgument.push_back(false);
+            }
+            else
+            {
+                return false;
+            }
+        }
+
+        auto optionalArgument = static_cast<chip::Optional<std::vector<bool>> *>(arg.value);
+        optionalArgument->SetValue(vectorArgument);
+        return true;
+    }
+
+    case ArgumentType::Vector16:
+    case ArgumentType::Vector32: {
+        std::vector<uint64_t> values;
+        uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+        uint64_t max = arg.max;
+
+        std::stringstream ss(argValue);
+        while (ss.good())
+        {
+            std::string valueAsString;
+            getline(ss, valueAsString, ',');
+            isHexNotation = strncmp(valueAsString.c_str(), "0x", 2) == 0 || strncmp(valueAsString.c_str(), "0X", 2) == 0;
+
+            std::stringstream subss;
+            isHexNotation ? subss << std::hex << valueAsString : subss << valueAsString;
+
+            uint64_t value;
+            subss >> value;
+            VerifyOrReturnError(!subss.fail() && subss.eof() && value >= min && value <= max, false);
+            values.push_back(value);
+        }
+
+        if (arg.type == ArgumentType::Vector16)
+        {
+            auto vectorArgument = static_cast<std::vector<uint16_t> *>(arg.value);
+            for (uint64_t v : values)
+            {
+                vectorArgument->push_back(static_cast<uint16_t>(v));
+            }
+        }
+        else if (arg.type == ArgumentType::Vector32 && arg.flags != Argument::kOptional)
+        {
+            auto vectorArgument = static_cast<std::vector<uint32_t> *>(arg.value);
+            for (uint64_t v : values)
+            {
+                vectorArgument->push_back(static_cast<uint32_t>(v));
+            }
+        }
+        else if (arg.type == ArgumentType::Vector32 && arg.flags == Argument::kOptional)
+        {
+            std::vector<uint32_t> vectorArgument;
+            for (uint64_t v : values)
+            {
+                vectorArgument.push_back(static_cast<uint32_t>(v));
+            }
+
+            auto optionalArgument = static_cast<chip::Optional<std::vector<uint32_t>> *>(arg.value);
+            optionalArgument->SetValue(vectorArgument);
+        }
+        else
+        {
+            return false;
+        }
+
+        return true;
+    }
+
+    case ArgumentType::VectorCustom: {
+        auto vectorArgument = static_cast<std::vector<CustomArgument *> *>(arg.value);
+
+        std::stringstream ss(argValue);
+        while (ss.good())
+        {
+            std::string valueAsString;
+            // By default the parameter separator is ";" in order to not collapse with the argument itself if it contains commas
+            // (e.g a struct argument with multiple fields). In case one needs to use ";" it can be overriden with the following
+            // environment variable.
+            static constexpr char kSeparatorVariable[] = "NotSpecified,_CUSTOM_ARGUMENTS_SEPARATOR";
+            char * getenvSeparatorVariableResult       = getenv(kSeparatorVariable);
+            getline(ss, valueAsString, getenvSeparatorVariableResult ? getenvSeparatorVariableResult[0] : ';');
+
+            CustomArgument * customArgument = new CustomArgument();
+            vectorArgument->push_back(customArgument);
+            VerifyOrReturnError(CHIP_NO_ERROR == vectorArgument->back()->Parse(arg.name, valueAsString.c_str()), false);
+        }
+
+        return true;
+    }
+
+    case ArgumentType::String: {
+        isValidArgument = HandleNullableOptional<char *>(arg, argValue, [&](auto * value) {
+            *value = argValue;
+            return true;
+        });
+        break;
+    }
+
+    case ArgumentType::CharString: {
+        isValidArgument = HandleNullableOptional<chip::CharSpan>(arg, argValue, [&](auto * value) {
+            *value = chip::Span<const char>(argValue, strlen(argValue));
+            return true;
+        });
+        break;
+    }
+
+    case ArgumentType::OctetString: {
+        isValidArgument = HandleNullableOptional<chip::ByteSpan>(arg, argValue, [&](auto * value) {
+            // We support two ways to pass an octet string argument.  If it happens
+            // to be all-ASCII, you can just pass it in.  Otherwise you can pass in
+            // "hex:" followed by the hex-encoded bytes.
+            size_t argLen = strlen(argValue);
+
+            if (IsHexString(argValue))
+            {
+                // Hex-encoded.  Decode it into a temporary buffer first, so if we
+                // run into errors we can do correct "argument is not valid" logging
+                // that actually shows the value that was passed in.  After we
+                // determine it's valid, modify the passed-in value to hold the
+                // right bytes, so we don't need to worry about allocating storage
+                // for this somewhere else.  This works because the hex
+                // representation is always longer than the octet string it encodes,
+                // so we have enough space in argValue for the decoded version.
+                chip::Platform::ScopedMemoryBuffer<uint8_t> buffer;
+
+                size_t octetCount;
+                CHIP_ERROR err = HexToBytes(
+                    chip::CharSpan(argValue + kHexStringPrefixLen, argLen - kHexStringPrefixLen),
+                    [&buffer](size_t allocSize) {
+                        buffer.Calloc(allocSize);
+                        return buffer.Get();
+                    },
+                    &octetCount);
+                if (err != CHIP_NO_ERROR)
+                {
+                    return false;
+                }
+
+                memcpy(argValue, buffer.Get(), octetCount);
+                *value = chip::ByteSpan(chip::Uint8::from_char(argValue), octetCount);
+                return true;
+            }
+
+            // Just ASCII.  Check for the "str:" prefix.
+            if (IsStrString(argValue))
+            {
+                // Skip the prefix
+                argValue += kStrStringPrefixLen;
+                argLen -= kStrStringPrefixLen;
+            }
+            *value = chip::ByteSpan(chip::Uint8::from_char(argValue), argLen);
+            return true;
+        });
+        break;
+    }
+
+    case ArgumentType::Bool: {
+        isValidArgument = HandleNullableOptional<bool>(arg, argValue, [&](auto * value) {
+            // Start with checking for actual boolean values.
+            if (strcasecmp(argValue, "true") == 0)
+            {
+                *value = true;
+                return true;
+            }
+
+            if (strcasecmp(argValue, "false") == 0)
+            {
+                *value = false;
+                return true;
+            }
+
+            // For backwards compat, keep accepting 0 and 1 for now as synonyms
+            // for false and true.  Since we set our min to 0 and max to 1 for
+            // booleans, calling uint8Handler does the right thing in terms of
+            // only allowing those two values.
+            uint8_t temp = 0;
+            if (!uint8Handler(&temp))
+            {
+                return false;
+            }
+            *value = (temp == 1);
+            return true;
+        });
+        break;
+    }
+
+    case ArgumentType::Number_uint8: {
+        isValidArgument = HandleNullableOptional<uint8_t>(arg, argValue, uint8Handler);
+        break;
+    }
+
+    case ArgumentType::Number_uint16: {
+        isValidArgument = HandleNullableOptional<uint16_t>(arg, argValue, [&](auto * value) {
+            std::stringstream ss;
+            isHexNotation ? ss << std::hex << argValue : ss << argValue;
+            ss >> *value;
+
+            uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+            uint64_t max = arg.max;
+            return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+        });
+        break;
+    }
+
+    case ArgumentType::Number_uint32: {
+        isValidArgument = HandleNullableOptional<uint32_t>(arg, argValue, [&](auto * value) {
+            std::stringstream ss;
+            isHexNotation ? ss << std::hex << argValue : ss << argValue;
+            ss >> *value;
+
+            uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+            uint64_t max = arg.max;
+            return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+        });
+        break;
+    }
+
+    case ArgumentType::Number_uint64: {
+        isValidArgument = HandleNullableOptional<uint64_t>(arg, argValue, [&](auto * value) {
+            std::stringstream ss;
+            isHexNotation ? ss << std::hex << argValue : ss << argValue;
+            ss >> *value;
+
+            uint64_t min = chip::CanCastTo<uint64_t>(arg.min) ? static_cast<uint64_t>(arg.min) : 0;
+            uint64_t max = arg.max;
+            return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+        });
+        break;
+    }
+
+    case ArgumentType::Number_int8: {
+        isValidArgument = HandleNullableOptional<int8_t>(arg, argValue, [&](auto * value) {
+            // stringstream treats int8_t as char, which is not what we want here.
+            int16_t tmpValue;
+            std::stringstream ss;
+            isHexNotation ? ss << std::hex << argValue : ss << argValue;
+            ss >> tmpValue;
+            if (chip::CanCastTo<int8_t>(tmpValue))
+            {
+                *value = static_cast<int8_t>(tmpValue);
+
+                int64_t min = arg.min;
+                int64_t max = chip::CanCastTo<int64_t>(arg.max) ? static_cast<int64_t>(arg.max) : INT64_MAX;
+                return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+            }
+
+            return false;
+        });
+        break;
+    }
+
+    case ArgumentType::Number_int16: {
+        isValidArgument = HandleNullableOptional<int16_t>(arg, argValue, [&](auto * value) {
+            std::stringstream ss;
+            isHexNotation ? ss << std::hex << argValue : ss << argValue;
+            ss >> *value;
+
+            int64_t min = arg.min;
+            int64_t max = chip::CanCastTo<int64_t>(arg.max) ? static_cast<int64_t>(arg.max) : INT64_MAX;
+            return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+        });
+        break;
+    }
+
+    case ArgumentType::Number_int32: {
+        isValidArgument = HandleNullableOptional<int32_t>(arg, argValue, [&](auto * value) {
+            std::stringstream ss;
+            isHexNotation ? ss << std::hex << argValue : ss << argValue;
+            ss >> *value;
+
+            int64_t min = arg.min;
+            int64_t max = chip::CanCastTo<int64_t>(arg.max) ? static_cast<int64_t>(arg.max) : INT64_MAX;
+            return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+        });
+        break;
+    }
+
+    case ArgumentType::Number_int64: {
+        isValidArgument = HandleNullableOptional<int64_t>(arg, argValue, [&](auto * value) {
+            std::stringstream ss;
+            isHexNotation ? ss << std::hex << argValue : ss << argValue;
+            ss >> *value;
+
+            int64_t min = arg.min;
+            int64_t max = chip::CanCastTo<int64_t>(arg.max) ? static_cast<int64_t>(arg.max) : INT64_MAX;
+            return (!ss.fail() && ss.eof() && *value >= min && *value <= max);
+        });
+        break;
+    }
+
+    case ArgumentType::Float: {
+        isValidArgument = HandleNullableOptional<float>(arg, argValue, [&](auto * value) {
+            if (strcmp(argValue, "Infinity") == 0)
+            {
+                *value = INFINITY;
+                return true;
+            }
+
+            if (strcmp(argValue, "-Infinity") == 0)
+            {
+                *value = -INFINITY;
+                return true;
+            }
+
+            std::stringstream ss;
+            ss << argValue;
+            ss >> *value;
+            return (!ss.fail() && ss.eof());
+        });
+        break;
+    }
+
+    case ArgumentType::Double: {
+        isValidArgument = HandleNullableOptional<double>(arg, argValue, [&](auto * value) {
+            if (strcmp(argValue, "Infinity") == 0)
+            {
+                *value = INFINITY;
+                return true;
+            }
+
+            if (strcmp(argValue, "-Infinity") == 0)
+            {
+                *value = -INFINITY;
+                return true;
+            }
+
+            std::stringstream ss;
+            ss << argValue;
+            ss >> *value;
+            return (!ss.fail() && ss.eof());
+        });
+        break;
+    }
+
+    case ArgumentType::Address: {
+        isValidArgument = HandleNullableOptional<AddressWithInterface>(
+            arg, argValue, [&](auto * value) { return ParseAddressWithInterface(argValue, value); });
+        break;
+    }
+    }
+
+    if (!isValidArgument)
+    {
+        ChipLogError(NotSpecified, "InitArgs: Invalid argument %s: %s", arg.name, argValue);
+    }
+
+    return isValidArgument;
+}
+
+void Command::AddArgument(const char * name, const char * value, const char * desc)
+{
+    ReadOnlyGlobalCommandArgument arg;
+    arg.name  = name;
+    arg.value = value;
+    arg.desc  = desc;
+
+    mReadOnlyGlobalCommandArgument.SetValue(arg);
+}
+
+size_t Command::AddArgument(const char * name, char ** value, const char * desc, uint8_t flags)
+{
+    Argument arg;
+    arg.type  = ArgumentType::String;
+    arg.name  = name;
+    arg.value = reinterpret_cast<void *>(value);
+    arg.flags = flags;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, chip::CharSpan * value, const char * desc, uint8_t flags)
+{
+    Argument arg;
+    arg.type  = ArgumentType::CharString;
+    arg.name  = name;
+    arg.value = reinterpret_cast<void *>(value);
+    arg.flags = flags;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, chip::ByteSpan * value, const char * desc, uint8_t flags)
+{
+    Argument arg;
+    arg.type  = ArgumentType::OctetString;
+    arg.name  = name;
+    arg.value = reinterpret_cast<void *>(value);
+    arg.flags = flags;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, AddressWithInterface * out, const char * desc, uint8_t flags)
+{
+    Argument arg;
+    arg.type  = ArgumentType::Address;
+    arg.name  = name;
+    arg.value = reinterpret_cast<void *>(out);
+    arg.flags = flags;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, std::vector<uint16_t> * value, const char * desc)
+{
+    Argument arg;
+    arg.type  = ArgumentType::Vector16;
+    arg.name  = name;
+    arg.value = static_cast<void *>(value);
+    arg.min   = min;
+    arg.max   = max;
+    arg.flags = 0;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, std::vector<uint32_t> * value, const char * desc)
+{
+    Argument arg;
+    arg.type  = ArgumentType::Vector32;
+    arg.name  = name;
+    arg.value = static_cast<void *>(value);
+    arg.min   = min;
+    arg.max   = max;
+    arg.flags = 0;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<std::vector<uint32_t>> * value,
+                            const char * desc)
+{
+    Argument arg;
+    arg.type  = ArgumentType::Vector32;
+    arg.name  = name;
+    arg.value = static_cast<void *>(value);
+    arg.min   = min;
+    arg.max   = max;
+    arg.flags = Argument::kOptional;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<std::vector<bool>> * value,
+                            const char * desc)
+{
+    Argument arg;
+    arg.type  = ArgumentType::VectorBool;
+    arg.name  = name;
+    arg.value = static_cast<void *>(value);
+    arg.min   = min;
+    arg.max   = max;
+    arg.flags = Argument::kOptional;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, ComplexArgument * value, const char * desc, uint8_t flags)
+{
+    Argument arg;
+    arg.type  = ArgumentType::Complex;
+    arg.name  = name;
+    arg.value = static_cast<void *>(value);
+    arg.flags = flags;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, CustomArgument * value, const char * desc)
+{
+    Argument arg;
+    arg.type  = ArgumentType::Custom;
+    arg.name  = name;
+    arg.value = const_cast<void *>(reinterpret_cast<const void *>(value));
+    arg.flags = 0;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, std::vector<CustomArgument *> * value, const char * desc)
+{
+    Argument arg;
+    arg.type  = ArgumentType::VectorCustom;
+    arg.name  = name;
+    arg.value = static_cast<void *>(value);
+    arg.flags = 0;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, float min, float max, float * out, const char * desc, uint8_t flags)
+{
+    Argument arg;
+    arg.type  = ArgumentType::Float;
+    arg.name  = name;
+    arg.value = reinterpret_cast<void *>(out);
+    arg.flags = flags;
+    arg.desc  = desc;
+    // Ignore min/max for now; they're always +-Infinity anyway.
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, double min, double max, double * out, const char * desc, uint8_t flags)
+{
+    Argument arg;
+    arg.type  = ArgumentType::Double;
+    arg.name  = name;
+    arg.value = reinterpret_cast<void *>(out);
+    arg.flags = flags;
+    arg.desc  = desc;
+    // Ignore min/max for now; they're always +-Infinity anyway.
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, void * out, ArgumentType type, const char * desc,
+                            uint8_t flags)
+{
+    Argument arg;
+    arg.type  = type;
+    arg.name  = name;
+    arg.value = out;
+    arg.min   = min;
+    arg.max   = max;
+    arg.flags = flags;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, int64_t min, uint64_t max, void * out, const char * desc, uint8_t flags)
+{
+    Argument arg;
+    arg.type  = ArgumentType::Number_uint8;
+    arg.name  = name;
+    arg.value = out;
+    arg.min   = min;
+    arg.max   = max;
+    arg.flags = flags;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, std::vector<std::string> * value, const char * desc)
+{
+    Argument arg;
+    arg.type  = ArgumentType::VectorString;
+    arg.name  = name;
+    arg.value = static_cast<void *>(value);
+    arg.flags = 0;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+size_t Command::AddArgument(const char * name, chip::Optional<std::vector<std::string>> * value, const char * desc)
+{
+    Argument arg;
+    arg.type  = ArgumentType::VectorString;
+    arg.name  = name;
+    arg.value = static_cast<void *>(value);
+    arg.flags = Argument::kOptional;
+    arg.desc  = desc;
+
+    return AddArgumentToList(std::move(arg));
+}
+
+const char * Command::GetArgumentName(size_t index) const
+{
+    if (index < mArgs.size())
+    {
+        return mArgs.at(index).name;
+    }
+
+    return nullptr;
+}
+
+const char * Command::GetArgumentDescription(size_t index) const
+{
+    if (index < mArgs.size())
+    {
+        return mArgs.at(index).desc;
+    }
+
+    return nullptr;
+}
+
+const char * Command::GetReadOnlyGlobalCommandArgument() const
+{
+    if (GetAttribute())
+    {
+        return GetAttribute();
+    }
+
+    if (GetEvent())
+    {
+        return GetEvent();
+    }
+
+    return nullptr;
+}
+
+const char * Command::GetAttribute() const
+{
+    if (mReadOnlyGlobalCommandArgument.HasValue())
+    {
+        return mReadOnlyGlobalCommandArgument.Value().value;
+    }
+
+    return nullptr;
+}
+
+const char * Command::GetEvent() const
+{
+    if (mReadOnlyGlobalCommandArgument.HasValue())
+    {
+        return mReadOnlyGlobalCommandArgument.Value().value;
+    }
+
+    return nullptr;
+}
+
+size_t Command::AddArgumentToList(Argument && argument)
+{
+    if (argument.isOptional() || mArgs.empty() || !mArgs.back().isOptional())
+    {
+        // Safe to just append.
+        mArgs.emplace_back(std::move(argument));
+        return mArgs.size();
+    }
+
+    // We're inserting a non-optional arg but we already have something optional
+    // in the list.  Insert before the first optional arg.
+    for (auto cur = mArgs.cbegin(), end = mArgs.cend(); cur != end; ++cur)
+    {
+        if ((*cur).isOptional())
+        {
+            mArgs.emplace(cur, std::move(argument));
+            return mArgs.size();
+        }
+    }
+
+    // Never reached.
+    VerifyOrDie(false);
+    return 0;
+}
+
+namespace {
+template <typename T>
+void ResetOptionalArg(const Argument & arg)
+{
+    VerifyOrDie(arg.isOptional());
+
+    if (arg.isNullable())
+    {
+        reinterpret_cast<chip::Optional<chip::app::DataModel::Nullable<T>> *>(arg.value)->ClearValue();
+    }
+    else
+    {
+        reinterpret_cast<chip::Optional<T> *>(arg.value)->ClearValue();
+    }
+}
+} // anonymous namespace
+
+void Command::ResetArguments()
+{
+    for (const auto & arg : mArgs)
+    {
+        const ArgumentType type = arg.type;
+        if (arg.isOptional())
+        {
+            // Must always clean these up so they don't carry over to the next
+            // command invocation in interactive mode.
+            switch (type)
+            {
+            case ArgumentType::Complex: {
+                // Optional Complex arguments are not currently supported via the <chip::Optional> class.
+                // Instead, they must be explicitly specified as optional using the kOptional flag,
+                // and the base TypedComplexArgument<T> class is referenced.
+                auto argument = static_cast<ComplexArgument *>(arg.value);
+                argument->Reset();
+                break;
+            }
+            case ArgumentType::Custom: {
+                // No optional custom arguments so far.
+                VerifyOrDie(false);
+                break;
+            }
+            case ArgumentType::VectorString: {
+                ResetOptionalArg<std::vector<std::string>>(arg);
+                break;
+            }
+            case ArgumentType::VectorBool: {
+                ResetOptionalArg<std::vector<bool>>(arg);
+                break;
+            }
+            case ArgumentType::Vector16: {
+                // No optional Vector16 arguments so far.
+                VerifyOrDie(false);
+                break;
+            }
+            case ArgumentType::Vector32: {
+                ResetOptionalArg<std::vector<uint32_t>>(arg);
+                break;
+            }
+            case ArgumentType::VectorCustom: {
+                // No optional VectorCustom arguments so far.
+                VerifyOrDie(false);
+                break;
+            }
+            case ArgumentType::String: {
+                ResetOptionalArg<char *>(arg);
+                break;
+            }
+            case ArgumentType::CharString: {
+                ResetOptionalArg<chip::CharSpan>(arg);
+                break;
+            }
+            case ArgumentType::OctetString: {
+                ResetOptionalArg<chip::ByteSpan>(arg);
+                break;
+            }
+            case ArgumentType::Bool: {
+                ResetOptionalArg<bool>(arg);
+                break;
+            }
+            case ArgumentType::Number_uint8: {
+                ResetOptionalArg<uint8_t>(arg);
+                break;
+            }
+            case ArgumentType::Number_uint16: {
+                ResetOptionalArg<uint16_t>(arg);
+                break;
+            }
+            case ArgumentType::Number_uint32: {
+                ResetOptionalArg<uint32_t>(arg);
+                break;
+            }
+            case ArgumentType::Number_uint64: {
+                ResetOptionalArg<uint64_t>(arg);
+                break;
+            }
+            case ArgumentType::Number_int8: {
+                ResetOptionalArg<int8_t>(arg);
+                break;
+            }
+            case ArgumentType::Number_int16: {
+                ResetOptionalArg<int16_t>(arg);
+                break;
+            }
+            case ArgumentType::Number_int32: {
+                ResetOptionalArg<int32_t>(arg);
+                break;
+            }
+            case ArgumentType::Number_int64: {
+                ResetOptionalArg<int64_t>(arg);
+                break;
+            }
+            case ArgumentType::Float: {
+                ResetOptionalArg<float>(arg);
+                break;
+            }
+            case ArgumentType::Double: {
+                ResetOptionalArg<double>(arg);
+                break;
+            }
+            case ArgumentType::Address: {
+                ResetOptionalArg<AddressWithInterface>(arg);
+                break;
+            }
+            }
+        }
+        else
+        {
+            // Some non-optional arguments have state that needs to be cleaned
+            // up too.
+            if (type == ArgumentType::Vector16)
+            {
+                auto vectorArgument = static_cast<std::vector<uint16_t> *>(arg.value);
+                vectorArgument->clear();
+            }
+            else if (type == ArgumentType::Vector32)
+            {
+                auto vectorArgument = static_cast<std::vector<uint32_t> *>(arg.value);
+                vectorArgument->clear();
+            }
+            else if (type == ArgumentType::VectorCustom)
+            {
+                auto vectorArgument = static_cast<std::vector<CustomArgument *> *>(arg.value);
+                for (auto & customArgument : *vectorArgument)
+                {
+                    delete customArgument;
+                }
+                vectorArgument->clear();
+            }
+            else if (type == ArgumentType::Complex)
+            {
+                auto argument = static_cast<ComplexArgument *>(arg.value);
+                argument->Reset();
+            }
+        }
+    }
+}
diff --git a/examples/fabric-admin/commands/common/Command.h b/examples/fabric-admin/commands/common/Command.h
new file mode 100644
index 0000000..ec8b51d
--- /dev/null
+++ b/examples/fabric-admin/commands/common/Command.h
@@ -0,0 +1,307 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <app/data-model/Nullable.h>
+#include <commands/clusters/ComplexArgument.h>
+#include <commands/clusters/CustomArgument.h>
+#include <inet/InetInterface.h>
+#include <lib/core/Optional.h>
+#include <lib/support/Span.h>
+#include <lib/support/logging/CHIPLogging.h>
+
+#include <atomic>
+#include <condition_variable>
+#include <memory>
+#include <mutex>
+#include <string>
+#include <vector>
+
+class Command;
+
+template <typename T, typename... Args>
+std::unique_ptr<Command> make_unique(Args &&... args)
+{
+    return std::unique_ptr<Command>(new T(std::forward<Args>(args)...));
+}
+
+struct movable_initializer_list
+{
+    movable_initializer_list(std::unique_ptr<Command> && in) : item(std::move(in)) {}
+    operator std::unique_ptr<Command>() const && { return std::move(item); }
+    mutable std::unique_ptr<Command> item;
+};
+
+typedef std::initializer_list<movable_initializer_list> commands_list;
+
+enum ArgumentType
+{
+    Number_uint8,
+    Number_uint16,
+    Number_uint32,
+    Number_uint64,
+    Number_int8,
+    Number_int16,
+    Number_int32,
+    Number_int64,
+    Float,
+    Double,
+    Bool,
+    String,
+    CharString,
+    OctetString,
+    Address,
+    Complex,
+    Custom,
+    VectorBool,
+    Vector16,
+    Vector32,
+    VectorCustom,
+    VectorString, // comma separated string items
+};
+
+struct Argument
+{
+    const char * name;
+    ArgumentType type;
+    int64_t min;
+    uint64_t max;
+    void * value;
+    uint8_t flags;
+    const char * desc;
+
+    enum
+    {
+        kOptional = (1 << 0),
+        kNullable = (1 << 1),
+    };
+
+    bool isOptional() const { return flags & kOptional; }
+    bool isNullable() const { return flags & kNullable; }
+};
+
+struct ReadOnlyGlobalCommandArgument
+{
+    const char * name;
+    const char * value;
+    const char * desc;
+};
+
+class Command
+{
+public:
+    struct AddressWithInterface
+    {
+        ::chip::Inet::IPAddress address;
+        ::chip::Inet::InterfaceId interfaceId;
+    };
+
+    Command(const char * commandName, const char * helpText = nullptr) : mName(commandName), mHelpText(helpText) {}
+    virtual ~Command() {}
+
+    const char * GetName(void) const { return mName; }
+    const char * GetHelpText() const { return mHelpText; }
+    const char * GetReadOnlyGlobalCommandArgument(void) const;
+    const char * GetAttribute(void) const;
+    const char * GetEvent(void) const;
+    const char * GetArgumentName(size_t index) const;
+    const char * GetArgumentDescription(size_t index) const;
+    bool GetArgumentIsOptional(size_t index) const { return mArgs[index].isOptional(); }
+    size_t GetArgumentsCount(void) const { return mArgs.size(); }
+
+    bool InitArguments(int argc, char ** argv);
+    void AddArgument(const char * name, const char * value, const char * desc = "");
+    /**
+     * @brief
+     *   Add a char string command argument
+     *
+     * @param name  The name that will be displayed in the command help
+     * @param value A pointer to a `char *` where the argv value will be stored
+     * @param flags
+     * @param desc The description of the argument that will be displayed in the command help
+     * @returns The number of arguments currently added to the command
+     */
+    size_t AddArgument(const char * name, char ** value, const char * desc = "", uint8_t flags = 0);
+
+    /**
+     * Add an octet string command argument
+     */
+    size_t AddArgument(const char * name, chip::ByteSpan * value, const char * desc = "", uint8_t flags = 0);
+    size_t AddArgument(const char * name, chip::Span<const char> * value, const char * desc = "", uint8_t flags = 0);
+    size_t AddArgument(const char * name, AddressWithInterface * out, const char * desc = "", uint8_t flags = 0);
+    // Optional Complex arguments are not currently supported via the <chip::Optional> class.
+    // Instead, they must be explicitly specified as optional using kOptional in the flags parameter,
+    // and the base TypedComplexArgument<T> class is referenced.
+    size_t AddArgument(const char * name, ComplexArgument * value, const char * desc = "", uint8_t flags = 0);
+    size_t AddArgument(const char * name, CustomArgument * value, const char * desc = "");
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, bool * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<void *>(out), Bool, desc, flags);
+    }
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, int8_t * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_int8, desc, flags);
+    }
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, int16_t * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_int16, desc, flags);
+    }
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, int32_t * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_int32, desc, flags);
+    }
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, int64_t * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_int64, desc, flags);
+    }
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, uint8_t * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_uint8, desc, flags);
+    }
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, uint16_t * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_uint16, desc, flags);
+    }
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, uint32_t * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_uint32, desc, flags);
+    }
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, uint64_t * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<void *>(out), Number_uint64, desc, flags);
+    }
+
+    size_t AddArgument(const char * name, float min, float max, float * out, const char * desc = "", uint8_t flags = 0);
+    size_t AddArgument(const char * name, double min, double max, double * out, const char * desc = "", uint8_t flags = 0);
+
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, std::vector<uint16_t> * value, const char * desc = "");
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, std::vector<uint32_t> * value, const char * desc = "");
+    size_t AddArgument(const char * name, std::vector<CustomArgument *> * value, const char * desc = "");
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<std::vector<bool>> * value,
+                       const char * desc = "");
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<std::vector<uint32_t>> * value,
+                       const char * desc = "");
+
+    template <typename T, typename = std::enable_if_t<std::is_enum<T>::value>>
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, T * out, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<std::underlying_type_t<T> *>(out), desc, flags);
+    }
+
+    template <typename T>
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::BitFlags<T> * out, const char * desc = "",
+                       uint8_t flags = 0)
+    {
+        // This is a terrible hack that relies on BitFlags only having the one
+        // mValue member.
+        return AddArgument(name, min, max, reinterpret_cast<T *>(out), desc, flags);
+    }
+
+    template <typename T>
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::BitMask<T> * out, const char * desc = "",
+                       uint8_t flags = 0)
+    {
+        // This is a terrible hack that relies on BitMask only having the one
+        // mValue member.
+        return AddArgument(name, min, max, reinterpret_cast<T *>(out), desc, flags);
+    }
+
+    template <typename T>
+    size_t AddArgument(const char * name, chip::Optional<T> * value, const char * desc = "")
+    {
+        return AddArgument(name, reinterpret_cast<T *>(value), desc, Argument::kOptional);
+    }
+
+    template <typename T>
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::Optional<T> * value, const char * desc = "")
+    {
+        return AddArgument(name, min, max, reinterpret_cast<T *>(value), desc, Argument::kOptional);
+    }
+
+    template <typename T>
+    size_t AddArgument(const char * name, chip::app::DataModel::Nullable<T> * value, const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, reinterpret_cast<T *>(value), desc, flags | Argument::kNullable);
+    }
+
+    template <typename T>
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, chip::app::DataModel::Nullable<T> * value,
+                       const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<T *>(value), desc, flags | Argument::kNullable);
+    }
+
+    size_t AddArgument(const char * name, float min, float max, chip::app::DataModel::Nullable<float> * value,
+                       const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<float *>(value), desc, flags | Argument::kNullable);
+    }
+
+    size_t AddArgument(const char * name, double min, double max, chip::app::DataModel::Nullable<double> * value,
+                       const char * desc = "", uint8_t flags = 0)
+    {
+        return AddArgument(name, min, max, reinterpret_cast<double *>(value), desc, flags | Argument::kNullable);
+    }
+
+    size_t AddArgument(const char * name, std::vector<std::string> * value, const char * desc);
+    size_t AddArgument(const char * name, chip::Optional<std::vector<std::string>> * value, const char * desc);
+
+    void ResetArguments();
+
+    virtual CHIP_ERROR Run() = 0;
+
+    bool IsInteractive() { return mIsInteractive; }
+
+    CHIP_ERROR RunAsInteractive(const chip::Optional<char *> & interactiveStorageDirectory, bool advertiseOperational)
+    {
+        mStorageDirectory     = interactiveStorageDirectory;
+        mIsInteractive        = true;
+        mAdvertiseOperational = advertiseOperational;
+        return Run();
+    }
+
+    const chip::Optional<char *> & GetStorageDirectory() const { return mStorageDirectory; }
+
+protected:
+    // mStorageDirectory lives here so we can just set it in RunAsInteractive.
+    chip::Optional<char *> mStorageDirectory;
+
+    // mAdvertiseOperational lives here so we can just set it in
+    // RunAsInteractive; it's only used by CHIPCommand.
+    bool mAdvertiseOperational = false;
+
+private:
+    bool InitArgument(size_t argIndex, char * argValue);
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, void * out, ArgumentType type, const char * desc,
+                       uint8_t flags);
+    size_t AddArgument(const char * name, int64_t min, uint64_t max, void * out, const char * desc, uint8_t flags);
+
+    /**
+     * Add the Argument to our list.  This preserves the property that all
+     * optional arguments come at the end of the list.
+     */
+    size_t AddArgumentToList(Argument && argument);
+
+    const char * mName     = nullptr;
+    const char * mHelpText = nullptr;
+    bool mIsInteractive    = false;
+
+    chip::Optional<ReadOnlyGlobalCommandArgument> mReadOnlyGlobalCommandArgument;
+    std::vector<Argument> mArgs;
+};
diff --git a/examples/fabric-admin/commands/common/Commands.cpp b/examples/fabric-admin/commands/common/Commands.cpp
new file mode 100644
index 0000000..3742978
--- /dev/null
+++ b/examples/fabric-admin/commands/common/Commands.cpp
@@ -0,0 +1,703 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "Commands.h"
+
+#include "Command.h"
+
+#include <algorithm>
+#include <iomanip>
+#include <sstream>
+#include <string>
+
+#include <lib/support/Base64.h>
+#include <lib/support/CHIPMem.h>
+#include <lib/support/CodeUtils.h>
+#include <platform/CHIPDeviceConfig.h>
+#include <platform/KeyValueStoreManager.h>
+
+#include "../clusters/JsonParser.h"
+
+namespace {
+
+char kInteractiveModeName[]                         = "";
+constexpr size_t kInteractiveModeArgumentsMaxLength = 32;
+constexpr char kOptionalArgumentPrefix[]            = "--";
+constexpr char kJsonClusterKey[]                    = "cluster";
+constexpr char kJsonCommandKey[]                    = "command";
+constexpr char kJsonCommandSpecifierKey[]           = "command_specifier";
+constexpr char kJsonArgumentsKey[]                  = "arguments";
+
+#if !CHIP_DISABLE_PLATFORM_KVS
+template <typename T>
+struct HasInitWithString
+{
+    template <typename U>
+    static constexpr auto check(U *) -> typename std::is_same<decltype(std::declval<U>().Init("")), CHIP_ERROR>::type;
+
+    template <typename>
+    static constexpr std::false_type check(...);
+
+    typedef decltype(check<std::remove_reference_t<T>>(nullptr)) type;
+
+public:
+    static constexpr bool value = type::value;
+};
+
+// Template so we can do conditional enabling
+template <typename T, std::enable_if_t<HasInitWithString<T>::value, int> = 0>
+static void UseStorageDirectory(T & storageManagerImpl, const char * storageDirectory)
+{
+    std::string platformKVS = std::string(storageDirectory) + "/chip_tool_kvs";
+    storageManagerImpl.Init(platformKVS.c_str());
+}
+
+template <typename T, std::enable_if_t<!HasInitWithString<T>::value, int> = 0>
+static void UseStorageDirectory(T & storageManagerImpl, const char * storageDirectory)
+{}
+#endif // !CHIP_DISABLE_PLATFORM_KVS
+
+bool GetArgumentsFromJson(Command * command, Json::Value & value, bool optional, std::vector<std::string> & outArgs)
+{
+    auto memberNames = value.getMemberNames();
+
+    std::vector<std::string> args;
+    for (size_t i = 0; i < command->GetArgumentsCount(); i++)
+    {
+        auto argName             = command->GetArgumentName(i);
+        auto memberNamesIterator = memberNames.begin();
+        while (memberNamesIterator != memberNames.end())
+        {
+            auto memberName = *memberNamesIterator;
+            if (strcasecmp(argName, memberName.c_str()) != 0)
+            {
+                memberNamesIterator++;
+                continue;
+            }
+
+            if (command->GetArgumentIsOptional(i) != optional)
+            {
+                memberNamesIterator = memberNames.erase(memberNamesIterator);
+                continue;
+            }
+
+            if (optional)
+            {
+                args.push_back(std::string(kOptionalArgumentPrefix) + argName);
+            }
+
+            auto argValue = value[memberName].asString();
+            args.push_back(std::move(argValue));
+            memberNamesIterator = memberNames.erase(memberNamesIterator);
+            break;
+        }
+    }
+
+    if (memberNames.size())
+    {
+        auto memberName = memberNames.front();
+        ChipLogError(NotSpecified, "The argument \"\%s\" is not supported.", memberName.c_str());
+        return false;
+    }
+
+    outArgs = args;
+    return true;
+};
+
+// Check for arguments with a starting '"' but no ending '"': those
+// would indicate that people are using double-quoting, not single
+// quoting, on arguments with spaces.
+static void DetectAndLogMismatchedDoubleQuotes(int argc, char ** argv)
+{
+    for (int curArg = 0; curArg < argc; ++curArg)
+    {
+        char * arg = argv[curArg];
+        if (!arg)
+        {
+            continue;
+        }
+
+        auto len = strlen(arg);
+        if (len == 0)
+        {
+            continue;
+        }
+
+        if (arg[0] == '"' && arg[len - 1] != '"')
+        {
+            ChipLogError(NotSpecified,
+                         "Mismatched '\"' detected in argument: '%s'.  Use single quotes to delimit arguments with spaces "
+                         "in them: 'x y', not \"x y\".",
+                         arg);
+        }
+    }
+}
+
+} // namespace
+
+void Commands::Register(const char * commandSetName, commands_list commandsList, const char * helpText, bool isCluster)
+{
+    VerifyOrDieWithMsg(isCluster || helpText != nullptr, NotSpecified, "Non-cluster command sets must have help text");
+    mCommandSets[commandSetName].isCluster = isCluster;
+    mCommandSets[commandSetName].helpText  = helpText;
+    for (auto & command : commandsList)
+    {
+        mCommandSets[commandSetName].commands.push_back(std::move(command));
+    }
+}
+
+int Commands::Run(int argc, char ** argv)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+
+    err = chip::Platform::MemoryInit();
+    VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Init Memory failure: %s", chip::ErrorStr(err)));
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+    err = mStorage.Init();
+    VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(Controller, "Init Storage failure: %s", chip::ErrorStr(err)));
+
+    chip::Logging::SetLogFilter(mStorage.GetLoggingLevel());
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+    err = RunCommand(argc, argv);
+    VerifyOrExit(err == CHIP_NO_ERROR, ChipLogError(NotSpecified, "Run command failure: %s", chip::ErrorStr(err)));
+
+exit:
+    return (err == CHIP_NO_ERROR) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+int Commands::RunInteractive(const char * command, const chip::Optional<char *> & storageDirectory, bool advertiseOperational)
+{
+    std::vector<std::string> arguments;
+    VerifyOrReturnValue(DecodeArgumentsFromInteractiveMode(command, arguments), EXIT_FAILURE);
+
+    if (arguments.size() > (kInteractiveModeArgumentsMaxLength - 1 /* for interactive mode name */))
+    {
+        ChipLogError(NotSpecified, "Too many arguments. Ignoring.");
+        arguments.resize(kInteractiveModeArgumentsMaxLength - 1);
+    }
+
+    int argc                                        = 0;
+    char * argv[kInteractiveModeArgumentsMaxLength] = {};
+    argv[argc++]                                    = kInteractiveModeName;
+
+    std::string commandStr;
+    for (auto & arg : arguments)
+    {
+        argv[argc] = new char[arg.size() + 1];
+        strcpy(argv[argc++], arg.c_str());
+        commandStr += arg;
+        commandStr += " ";
+    }
+
+    ChipLogProgress(NotSpecified, "Command: %s", commandStr.c_str());
+    auto err = RunCommand(argc, argv, true, storageDirectory, advertiseOperational);
+
+    // Do not delete arg[0]
+    for (auto i = 1; i < argc; i++)
+    {
+        delete[] argv[i];
+    }
+
+    return (err == CHIP_NO_ERROR) ? EXIT_SUCCESS : EXIT_FAILURE;
+}
+
+CHIP_ERROR Commands::RunCommand(int argc, char ** argv, bool interactive,
+                                const chip::Optional<char *> & interactiveStorageDirectory, bool interactiveAdvertiseOperational)
+{
+    Command * command = nullptr;
+
+    if (argc <= 1)
+    {
+        ChipLogError(NotSpecified, "Missing cluster or command set name");
+        ShowCommandSets(argv[0]);
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    auto commandSetIter = GetCommandSet(argv[1]);
+    if (commandSetIter == mCommandSets.end())
+    {
+        ChipLogError(NotSpecified, "Unknown cluster or command set: %s", argv[1]);
+        ShowCommandSets(argv[0]);
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    auto & commandList = commandSetIter->second.commands;
+    auto * helpText    = commandSetIter->second.helpText;
+
+    if (argc <= 2)
+    {
+        ChipLogError(NotSpecified, "Missing command name");
+        ShowCommandSet(argv[0], argv[1], commandList, helpText);
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    bool isGlobalCommand = IsGlobalCommand(argv[2]);
+    if (!isGlobalCommand)
+    {
+        command = GetCommand(commandList, argv[2]);
+        if (command == nullptr)
+        {
+            ChipLogError(NotSpecified, "Unknown command: %s", argv[2]);
+            ShowCommandSet(argv[0], argv[1], commandList, helpText);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+    }
+    else if (IsEventCommand(argv[2]))
+    {
+        if (argc <= 3)
+        {
+            ChipLogError(NotSpecified, "Missing event name");
+            ShowClusterEvents(argv[0], argv[1], argv[2], commandList);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        command = GetGlobalCommand(commandList, argv[2], argv[3]);
+        if (command == nullptr)
+        {
+            ChipLogError(NotSpecified, "Unknown event: %s", argv[3]);
+            ShowClusterEvents(argv[0], argv[1], argv[2], commandList);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+    }
+    else
+    {
+        if (argc <= 3)
+        {
+            ChipLogError(NotSpecified, "Missing attribute name");
+            ShowClusterAttributes(argv[0], argv[1], argv[2], commandList);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+
+        command = GetGlobalCommand(commandList, argv[2], argv[3]);
+        if (command == nullptr)
+        {
+            ChipLogError(NotSpecified, "Unknown attribute: %s", argv[3]);
+            ShowClusterAttributes(argv[0], argv[1], argv[2], commandList);
+            return CHIP_ERROR_INVALID_ARGUMENT;
+        }
+    }
+
+    int argumentsPosition = isGlobalCommand ? 4 : 3;
+    if (!command->InitArguments(argc - argumentsPosition, &argv[argumentsPosition]))
+    {
+        if (interactive)
+        {
+            DetectAndLogMismatchedDoubleQuotes(argc - argumentsPosition, &argv[argumentsPosition]);
+        }
+        ShowCommand(argv[0], argv[1], command);
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    if (interactive)
+    {
+        return command->RunAsInteractive(interactiveStorageDirectory, interactiveAdvertiseOperational);
+    }
+
+    // Now that the command is initialized, get our storage from it as needed
+    // and set up our loging level.
+#ifdef CONFIG_USE_LOCAL_STORAGE
+    CHIP_ERROR err = mStorage.Init(nullptr, command->GetStorageDirectory().ValueOr(nullptr));
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(Controller, "Init Storage failure: %s", chip::ErrorStr(err));
+        return err;
+    }
+
+    chip::Logging::SetLogFilter(mStorage.GetLoggingLevel());
+
+#if !CHIP_DISABLE_PLATFORM_KVS
+    UseStorageDirectory(chip::DeviceLayer::PersistedStorage::KeyValueStoreMgrImpl(), mStorage.GetDirectory());
+#endif // !CHIP_DISABLE_PLATFORM_KVS
+
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+    return command->Run();
+}
+
+Commands::CommandSetMap::iterator Commands::GetCommandSet(std::string commandSetName)
+{
+    for (auto & commandSet : mCommandSets)
+    {
+        std::string key(commandSet.first);
+        std::transform(key.begin(), key.end(), key.begin(), ::tolower);
+        if (key.compare(commandSetName) == 0)
+        {
+            return mCommandSets.find(commandSet.first);
+        }
+    }
+
+    return mCommandSets.end();
+}
+
+Command * Commands::GetCommand(CommandsVector & commands, std::string commandName)
+{
+    for (auto & command : commands)
+    {
+        if (commandName.compare(command->GetName()) == 0)
+        {
+            return command.get();
+        }
+    }
+
+    return nullptr;
+}
+
+Command * Commands::GetGlobalCommand(CommandsVector & commands, std::string commandName, std::string attributeName)
+{
+    for (auto & command : commands)
+    {
+        if (commandName.compare(command->GetName()) == 0 && attributeName.compare(command->GetAttribute()) == 0)
+        {
+            return command.get();
+        }
+    }
+
+    return nullptr;
+}
+
+bool Commands::IsAttributeCommand(std::string commandName) const
+{
+    return commandName.compare("read") == 0 || commandName.compare("write") == 0 || commandName.compare("force-write") == 0 ||
+        commandName.compare("subscribe") == 0;
+}
+
+bool Commands::IsEventCommand(std::string commandName) const
+{
+    return commandName.compare("read-event") == 0 || commandName.compare("subscribe-event") == 0;
+}
+
+bool Commands::IsGlobalCommand(std::string commandName) const
+{
+    return IsAttributeCommand(commandName) || IsEventCommand(commandName);
+}
+
+void Commands::ShowCommandSetOverview(std::string commandSetName, const CommandSet & commandSet)
+{
+    std::transform(commandSetName.begin(), commandSetName.end(), commandSetName.begin(),
+                   [](unsigned char c) { return std::tolower(c); });
+    fprintf(stderr, "  | * %-82s|\n", commandSetName.c_str());
+    ShowHelpText(commandSet.helpText);
+}
+
+void Commands::ShowCommandSets(std::string executable)
+{
+    fprintf(stderr, "Usage:\n");
+    fprintf(stderr, "  %s cluster_name command_name [param1 param2 ...]\n", executable.c_str());
+    fprintf(stderr, "or:\n");
+    fprintf(stderr, "  %s command_set_name command_name [param1 param2 ...]\n", executable.c_str());
+    fprintf(stderr, "\n");
+    // Table of clusters
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    fprintf(stderr, "  | Clusters:                                                                           |\n");
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    for (auto & commandSet : mCommandSets)
+    {
+        if (commandSet.second.isCluster)
+        {
+            ShowCommandSetOverview(commandSet.first, commandSet.second);
+        }
+    }
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    fprintf(stderr, "\n");
+
+    // Table of command sets
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    fprintf(stderr, "  | Command sets:                                                                       |\n");
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    for (auto & commandSet : mCommandSets)
+    {
+        if (!commandSet.second.isCluster)
+        {
+            ShowCommandSetOverview(commandSet.first, commandSet.second);
+        }
+    }
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+}
+
+void Commands::ShowCommandSet(std::string executable, std::string commandSetName, CommandsVector & commands, const char * helpText)
+{
+    fprintf(stderr, "Usage:\n");
+    fprintf(stderr, "  %s %s command_name [param1 param2 ...]\n", executable.c_str(), commandSetName.c_str());
+
+    if (helpText)
+    {
+        fprintf(stderr, "\n%s\n", helpText);
+    }
+
+    fprintf(stderr, "\n");
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    fprintf(stderr, "  | Commands:                                                                           |\n");
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    bool readCommand           = false;
+    bool writeCommand          = false;
+    bool writeOverrideCommand  = false;
+    bool subscribeCommand      = false;
+    bool readEventCommand      = false;
+    bool subscribeEventCommand = false;
+    for (auto & command : commands)
+    {
+        bool shouldPrint = true;
+
+        if (IsGlobalCommand(command->GetName()))
+        {
+            if (strcmp(command->GetName(), "read") == 0 && !readCommand)
+            {
+                readCommand = true;
+            }
+            else if (strcmp(command->GetName(), "write") == 0 && !writeCommand)
+            {
+                writeCommand = true;
+            }
+            else if (strcmp(command->GetName(), "force-write") == 0 && !writeOverrideCommand)
+            {
+                writeOverrideCommand = true;
+            }
+            else if (strcmp(command->GetName(), "subscribe") == 0 && !subscribeCommand)
+            {
+                subscribeCommand = true;
+            }
+            else if (strcmp(command->GetName(), "read-event") == 0 && !readEventCommand)
+            {
+                readEventCommand = true;
+            }
+            else if (strcmp(command->GetName(), "subscribe-event") == 0 && !subscribeEventCommand)
+            {
+                subscribeEventCommand = true;
+            }
+            else
+            {
+                shouldPrint = false;
+            }
+        }
+
+        if (shouldPrint)
+        {
+            fprintf(stderr, "  | * %-82s|\n", command->GetName());
+            ShowHelpText(command->GetHelpText());
+        }
+    }
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+}
+
+void Commands::ShowClusterAttributes(std::string executable, std::string clusterName, std::string commandName,
+                                     CommandsVector & commands)
+{
+    fprintf(stderr, "Usage:\n");
+    fprintf(stderr, "  %s %s %s attribute-name [param1 param2 ...]\n", executable.c_str(), clusterName.c_str(),
+            commandName.c_str());
+    fprintf(stderr, "\n");
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    fprintf(stderr, "  | Attributes:                                                                         |\n");
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    for (auto & command : commands)
+    {
+        if (commandName.compare(command->GetName()) == 0)
+        {
+            fprintf(stderr, "  | * %-82s|\n", command->GetAttribute());
+        }
+    }
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+}
+
+void Commands::ShowClusterEvents(std::string executable, std::string clusterName, std::string commandName,
+                                 CommandsVector & commands)
+{
+    fprintf(stderr, "Usage:\n");
+    fprintf(stderr, "  %s %s %s event-name [param1 param2 ...]\n", executable.c_str(), clusterName.c_str(), commandName.c_str());
+    fprintf(stderr, "\n");
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    fprintf(stderr, "  | Events:                                                                             |\n");
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+    for (auto & command : commands)
+    {
+        if (commandName.compare(command->GetName()) == 0)
+        {
+            fprintf(stderr, "  | * %-82s|\n", command->GetEvent());
+        }
+    }
+    fprintf(stderr, "  +-------------------------------------------------------------------------------------+\n");
+}
+
+void Commands::ShowCommand(std::string executable, std::string clusterName, Command * command)
+{
+    fprintf(stderr, "Usage:\n");
+
+    std::string arguments;
+    std::string description;
+    arguments += command->GetName();
+
+    if (command->GetReadOnlyGlobalCommandArgument())
+    {
+        arguments += ' ';
+        arguments += command->GetReadOnlyGlobalCommandArgument();
+    }
+
+    size_t argumentsCount = command->GetArgumentsCount();
+    for (size_t i = 0; i < argumentsCount; i++)
+    {
+        std::string arg;
+        bool isOptional = command->GetArgumentIsOptional(i);
+        if (isOptional)
+        {
+            arg += "[--";
+        }
+        arg += command->GetArgumentName(i);
+        if (isOptional)
+        {
+            arg += "]";
+        }
+        arguments += " ";
+        arguments += arg;
+
+        const char * argDescription = command->GetArgumentDescription(i);
+        if ((argDescription != nullptr) && (strlen(argDescription) > 0))
+        {
+            description += "\n";
+            description += arg;
+            description += ":\n  ";
+            description += argDescription;
+            description += "\n";
+        }
+    }
+    fprintf(stderr, "  %s %s %s\n", executable.c_str(), clusterName.c_str(), arguments.c_str());
+
+    if (command->GetHelpText())
+    {
+        fprintf(stderr, "\n%s\n", command->GetHelpText());
+    }
+
+    if (description.size() > 0)
+    {
+        fprintf(stderr, "%s\n", description.c_str());
+    }
+}
+
+bool Commands::DecodeArgumentsFromInteractiveMode(const char * command, std::vector<std::string> & args)
+{
+    // Remote clients may not know the ordering of arguments, so instead of a strict ordering arguments can
+    // be passed in as a json payload encoded in base64 and are reordered on the fly.
+    return IsJsonString(command) ? DecodeArgumentsFromBase64EncodedJson(command, args)
+                                 : DecodeArgumentsFromStringStream(command, args);
+}
+
+bool Commands::DecodeArgumentsFromBase64EncodedJson(const char * json, std::vector<std::string> & args)
+{
+    Json::Value jsonValue;
+    bool parsed = JsonParser::ParseCustomArgument(json, json + kJsonStringPrefixLen, jsonValue);
+    VerifyOrReturnValue(parsed, false, ChipLogError(NotSpecified, "Error while parsing json."));
+    VerifyOrReturnValue(jsonValue.isObject(), false, ChipLogError(NotSpecified, "Unexpected json type."));
+    VerifyOrReturnValue(jsonValue.isMember(kJsonClusterKey), false,
+                        ChipLogError(NotSpecified, "'%s' key not found in json.", kJsonClusterKey));
+    VerifyOrReturnValue(jsonValue.isMember(kJsonCommandKey), false,
+                        ChipLogError(NotSpecified, "'%s' key not found in json.", kJsonCommandKey));
+    VerifyOrReturnValue(jsonValue.isMember(kJsonArgumentsKey), false,
+                        ChipLogError(NotSpecified, "'%s' key not found in json.", kJsonArgumentsKey));
+    VerifyOrReturnValue(IsBase64String(jsonValue[kJsonArgumentsKey].asString().c_str()), false,
+                        ChipLogError(NotSpecified, "'arguments' is not a base64 string."));
+
+    auto clusterName = jsonValue[kJsonClusterKey].asString();
+    auto commandName = jsonValue[kJsonCommandKey].asString();
+    auto arguments   = jsonValue[kJsonArgumentsKey].asString();
+
+    auto clusterIter = GetCommandSet(clusterName);
+    VerifyOrReturnValue(clusterIter != mCommandSets.end(), false,
+                        ChipLogError(NotSpecified, "Cluster '%s' is not supported.", clusterName.c_str()));
+
+    auto & commandList = clusterIter->second.commands;
+
+    auto command = GetCommand(commandList, commandName);
+
+    if (jsonValue.isMember(kJsonCommandSpecifierKey) && IsGlobalCommand(commandName))
+    {
+        auto commandSpecifierName = jsonValue[kJsonCommandSpecifierKey].asString();
+        command                   = GetGlobalCommand(commandList, commandName, commandSpecifierName);
+    }
+    VerifyOrReturnValue(nullptr != command, false, ChipLogError(NotSpecified, "Unknown command."));
+
+    auto encodedData = arguments.c_str();
+    encodedData += kBase64StringPrefixLen;
+
+    size_t encodedDataSize        = strlen(encodedData);
+    size_t expectedMaxDecodedSize = BASE64_MAX_DECODED_LEN(encodedDataSize);
+
+    chip::Platform::ScopedMemoryBuffer<uint8_t> decodedData;
+    VerifyOrReturnValue(decodedData.Calloc(expectedMaxDecodedSize + 1 /* for null */), false);
+
+    size_t decodedDataSize = chip::Base64Decode(encodedData, static_cast<uint16_t>(encodedDataSize), decodedData.Get());
+    VerifyOrReturnValue(decodedDataSize != 0, false, ChipLogError(NotSpecified, "Error while decoding base64 data."));
+
+    decodedData.Get()[decodedDataSize] = '\0';
+
+    Json::Value jsonArguments;
+    bool parsedArguments = JsonParser::ParseCustomArgument(encodedData, chip::Uint8::to_char(decodedData.Get()), jsonArguments);
+    VerifyOrReturnValue(parsedArguments, false, ChipLogError(NotSpecified, "Error while parsing json."));
+    VerifyOrReturnValue(jsonArguments.isObject(), false, ChipLogError(NotSpecified, "Unexpected json type, expects and object."));
+
+    std::vector<std::string> mandatoryArguments;
+    std::vector<std::string> optionalArguments;
+    VerifyOrReturnValue(GetArgumentsFromJson(command, jsonArguments, false /* addOptional */, mandatoryArguments), false);
+    VerifyOrReturnValue(GetArgumentsFromJson(command, jsonArguments, true /* addOptional */, optionalArguments), false);
+
+    args.push_back(std::move(clusterName));
+    args.push_back(std::move(commandName));
+    if (jsonValue.isMember(kJsonCommandSpecifierKey))
+    {
+        auto commandSpecifierName = jsonValue[kJsonCommandSpecifierKey].asString();
+        args.push_back(std::move(commandSpecifierName));
+    }
+    args.insert(args.end(), mandatoryArguments.begin(), mandatoryArguments.end());
+    args.insert(args.end(), optionalArguments.begin(), optionalArguments.end());
+
+    return true;
+}
+
+bool Commands::DecodeArgumentsFromStringStream(const char * command, std::vector<std::string> & args)
+{
+    std::string arg;
+    std::stringstream ss(command);
+    while (ss >> std::quoted(arg, '\''))
+    {
+        args.push_back(std::move(arg));
+    }
+
+    return true;
+}
+
+void Commands::ShowHelpText(const char * helpText)
+{
+    if (helpText == nullptr)
+    {
+        return;
+    }
+
+    // We leave 82 chars for command/cluster names.  The help text starts
+    // two chars further to the right, so there are 80 chars left
+    // for it.
+    if (strlen(helpText) > 80)
+    {
+        // Add "..." at the end to indicate truncation, and only
+        // show the first 77 chars, since that's what will fit.
+        fprintf(stderr, "  |   - %.77s...|\n", helpText);
+    }
+    else
+    {
+        fprintf(stderr, "  |   - %-80s|\n", helpText);
+    }
+}
diff --git a/examples/fabric-admin/commands/common/Commands.h b/examples/fabric-admin/commands/common/Commands.h
new file mode 100644
index 0000000..8638ede
--- /dev/null
+++ b/examples/fabric-admin/commands/common/Commands.h
@@ -0,0 +1,90 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#ifdef CONFIG_USE_LOCAL_STORAGE
+#include <controller/ExamplePersistentStorage.h>
+#endif // CONFIG_USE_LOCAL_STORAGE
+
+#include "Command.h"
+#include <map>
+#include <string>
+
+class Commands
+{
+public:
+    using CommandsVector = ::std::vector<std::unique_ptr<Command>>;
+
+    void RegisterCluster(const char * clusterName, commands_list commandsList)
+    {
+        Register(clusterName, commandsList, nullptr, true);
+    }
+    // Command sets represent fabric-admin functionality that is not actually
+    // XML-defined clusters.  All command sets should have help text explaining
+    // what sort of commands one should expect to find in the set.
+    void RegisterCommandSet(const char * commandSetName, commands_list commandsList, const char * helpText)
+    {
+        Register(commandSetName, commandsList, helpText, false);
+    }
+    int Run(int argc, char ** argv);
+    int RunInteractive(const char * command, const chip::Optional<char *> & storageDirectory, bool advertiseOperational);
+
+private:
+    struct CommandSet
+    {
+        CommandsVector commands;
+        bool isCluster        = false;
+        const char * helpText = nullptr;
+    };
+    // The tuple contains the commands, whether it's a synthetic cluster, and
+    // the help text for the cluster (which may be null).
+    using CommandSetMap = std::map<std::string, CommandSet>;
+
+    CHIP_ERROR RunCommand(int argc, char ** argv, bool interactive = false,
+                          const chip::Optional<char *> & interactiveStorageDirectory = chip::NullOptional,
+                          bool interactiveAdvertiseOperational                       = false);
+
+    CommandSetMap::iterator GetCommandSet(std::string commandSetName);
+    Command * GetCommand(CommandsVector & commands, std::string commandName);
+    Command * GetGlobalCommand(CommandsVector & commands, std::string commandName, std::string attributeName);
+    bool IsAttributeCommand(std::string commandName) const;
+    bool IsEventCommand(std::string commandName) const;
+    bool IsGlobalCommand(std::string commandName) const;
+
+    void ShowCommandSets(std::string executable);
+    static void ShowCommandSetOverview(std::string commandSetName, const CommandSet & commandSet);
+    void ShowCommandSet(std::string executable, std::string commandSetName, CommandsVector & commands, const char * helpText);
+    void ShowClusterAttributes(std::string executable, std::string clusterName, std::string commandName, CommandsVector & commands);
+    void ShowClusterEvents(std::string executable, std::string clusterName, std::string commandName, CommandsVector & commands);
+    void ShowCommand(std::string executable, std::string clusterName, Command * command);
+
+    bool DecodeArgumentsFromInteractiveMode(const char * command, std::vector<std::string> & args);
+    bool DecodeArgumentsFromBase64EncodedJson(const char * encodedData, std::vector<std::string> & args);
+    bool DecodeArgumentsFromStringStream(const char * command, std::vector<std::string> & args);
+
+    // helpText may be null, in which case it's not shown.
+    static void ShowHelpText(const char * helpText);
+
+    void Register(const char * commandSetName, commands_list commandsList, const char * helpText, bool isCluster);
+
+    CommandSetMap mCommandSets;
+#ifdef CONFIG_USE_LOCAL_STORAGE
+    PersistentStorage mStorage;
+#endif // CONFIG_USE_LOCAL_STORAGE
+};
diff --git a/examples/fabric-admin/commands/common/CredentialIssuerCommands.h b/examples/fabric-admin/commands/common/CredentialIssuerCommands.h
new file mode 100644
index 0000000..c36948f
--- /dev/null
+++ b/examples/fabric-admin/commands/common/CredentialIssuerCommands.h
@@ -0,0 +1,117 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <app/util/basic-types.h>
+#include <credentials/attestation_verifier/DeviceAttestationVerifier.h>
+#include <lib/core/CASEAuthTag.h>
+#include <lib/core/CHIPCore.h>
+#include <lib/core/CHIPPersistentStorageDelegate.h>
+#include <vector>
+
+namespace chip {
+namespace Controller {
+struct SetupParams;
+class OperationalCredentialsDelegate;
+} // namespace Controller
+} // namespace chip
+
+class CredentialIssuerCommands
+{
+public:
+    virtual ~CredentialIssuerCommands() {}
+
+    /**
+     * @brief
+     *   This function is used to initialize the Credentials Issuer, if needed.
+     *
+     * @param[in] storage A reference to the storage, where the Credentials Issuer can optionally use to access the keypair in
+     *                    storage.
+     *
+     * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code.
+     */
+    virtual CHIP_ERROR InitializeCredentialsIssuer(chip::PersistentStorageDelegate & storage) = 0;
+
+    /**
+     * @brief
+     *   This function is used to setup Device Attestation Singletons and intialize Setup/Commissioning Parameters with a custom
+     *   Device Attestation Verifier object.
+     *
+     * @param[in] setupParams A reference to the Setup/Commissioning Parameters, to be initialized with custom Device Attestation
+     *                        Verifier.
+     * @param[in] trustStore  A pointer to the PAA trust store to use to find valid PAA roots.
+     *
+     * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code.
+     */
+    virtual CHIP_ERROR SetupDeviceAttestation(chip::Controller::SetupParams & setupParams,
+                                              const chip::Credentials::AttestationTrustStore * trustStore) = 0;
+
+    /**
+     * @brief Add a list of additional non-default CD verifying keys (by certificate)
+     *
+     * Must be called AFTER SetupDeviceAttestation.
+     *
+     * @param additionalCdCerts - vector of X.509 DER verifying cert bodies
+     * @return CHIP_NO_ERROR on succes, another CHIP_ERROR on internal failures.
+     */
+    virtual CHIP_ERROR AddAdditionalCDVerifyingCerts(const std::vector<std::vector<uint8_t>> & additionalCdCerts) = 0;
+
+    virtual chip::Controller::OperationalCredentialsDelegate * GetCredentialIssuer() = 0;
+
+    virtual void SetCredentialIssuerCATValues(chip::CATValues cats) = 0;
+
+    /**
+     * @brief
+     *   This function is used to Generate NOC Chain for the Controller/Commissioner. Parameters follow the example implementation,
+     *   so some parameters may not translate to the real remote Credentials Issuer policy.
+     *
+     * @param[in] nodeId   The desired NodeId for the generated NOC Chain - May be optional/unused in some implementations.
+     * @param[in] fabricId The desired FabricId for the generated NOC Chain - May be optional/unused in some implementations.
+     * @param[in] cats     The desired CATs for the generated NOC Chain - May be optional/unused in some implementations.
+     * @param[in] keypair  The desired Keypair for the generated NOC Chain - May be optional/unused in some implementations.
+     * @param[in,out] rcac  Buffer to hold the Root Certificate of the generated NOC Chain.
+     * @param[in,out] icac  Buffer to hold the Intermediate Certificate of the generated NOC Chain.
+     * @param[in,out] noc   Buffer to hold the Leaf Certificate of the generated NOC Chain.
+     *
+     * @return CHIP_ERROR CHIP_NO_ERROR on success, or corresponding error code.
+     */
+    virtual CHIP_ERROR GenerateControllerNOCChain(chip::NodeId nodeId, chip::FabricId fabricId, const chip::CATValues & cats,
+                                                  chip::Crypto::P256Keypair & keypair, chip::MutableByteSpan & rcac,
+                                                  chip::MutableByteSpan & icac, chip::MutableByteSpan & noc) = 0;
+
+    // All options must start false
+    enum CredentialIssuerOptions : uint8_t
+    {
+        kMaximizeCertificateSizes = 0, // If set, certificate chains will be maximized for testing via padding
+        kAllowTestCdSigningKey    = 1, // If set, allow development/test SDK CD verifying key to be used
+    };
+
+    virtual void SetCredentialIssuerOption(CredentialIssuerOptions option, bool isEnabled)
+    {
+        // Do nothing
+        (void) option;
+        (void) isEnabled;
+    }
+
+    virtual bool GetCredentialIssuerOption(CredentialIssuerOptions option)
+    {
+        // All options always start false
+        return false;
+    }
+};
diff --git a/examples/fabric-admin/commands/common/CustomStringPrefix.h b/examples/fabric-admin/commands/common/CustomStringPrefix.h
new file mode 100644
index 0000000..be84429
--- /dev/null
+++ b/examples/fabric-admin/commands/common/CustomStringPrefix.h
@@ -0,0 +1,55 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <string.h>
+
+#include <lib/support/CodeUtils.h>
+
+static constexpr char kJsonStringPrefix[]    = "json:";
+inline constexpr size_t kJsonStringPrefixLen = ArraySize(kJsonStringPrefix) - 1; // Don't count the null
+
+static constexpr char kBase64StringPrefix[]    = "base64:";
+inline constexpr size_t kBase64StringPrefixLen = ArraySize(kBase64StringPrefix) - 1; // Don't count the null
+
+static constexpr char kHexStringPrefix[]    = "hex:";
+inline constexpr size_t kHexStringPrefixLen = ArraySize(kHexStringPrefix) - 1; // Don't count the null
+
+static constexpr char kStrStringPrefix[]    = "str:";
+inline constexpr size_t kStrStringPrefixLen = ArraySize(kStrStringPrefix) - 1; // Don't count the null
+
+inline bool IsJsonString(const char * str)
+{
+    return strncmp(str, kJsonStringPrefix, kJsonStringPrefixLen) == 0;
+}
+
+inline bool IsBase64String(const char * str)
+{
+    return strncmp(str, kBase64StringPrefix, kBase64StringPrefixLen) == 0;
+}
+
+inline bool IsHexString(const char * str)
+{
+    return strncmp(str, kHexStringPrefix, kHexStringPrefixLen) == 0;
+}
+
+inline bool IsStrString(const char * str)
+{
+    return strncmp(str, kStrStringPrefix, kStrStringPrefixLen) == 0;
+}
diff --git a/examples/fabric-admin/commands/common/DeviceScanner.cpp b/examples/fabric-admin/commands/common/DeviceScanner.cpp
new file mode 100644
index 0000000..e49eb85
--- /dev/null
+++ b/examples/fabric-admin/commands/common/DeviceScanner.cpp
@@ -0,0 +1,245 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "DeviceScanner.h"
+
+using namespace chip;
+using namespace chip::Dnssd;
+
+#if CONFIG_NETWORK_LAYER_BLE
+using namespace chip::Ble;
+constexpr char kBleKey[] = "BLE";
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+CHIP_ERROR DeviceScanner::Start()
+{
+    mDiscoveredResults.clear();
+
+#if CONFIG_NETWORK_LAYER_BLE
+    ReturnErrorOnFailure(DeviceLayer::PlatformMgrImpl().StartBleScan(this));
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+    ReturnErrorOnFailure(chip::Dnssd::Resolver::Instance().Init(DeviceLayer::UDPEndPointManager()));
+
+    char serviceName[kMaxCommissionableServiceNameSize];
+    auto filter = DiscoveryFilterType::kNone;
+    ReturnErrorOnFailure(MakeServiceTypeName(serviceName, sizeof(serviceName), filter, DiscoveryType::kCommissionableNode));
+
+    return ChipDnssdBrowse(serviceName, DnssdServiceProtocol::kDnssdProtocolUdp, Inet::IPAddressType::kAny,
+                           Inet::InterfaceId::Null(), this);
+}
+
+CHIP_ERROR DeviceScanner::Stop()
+{
+#if CONFIG_NETWORK_LAYER_BLE
+    ReturnErrorOnFailure(DeviceLayer::PlatformMgrImpl().StopBleScan());
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+    return ChipDnssdStopBrowse(this);
+}
+
+void DeviceScanner::OnNodeDiscovered(const DiscoveredNodeData & nodeData)
+{
+    VerifyOrReturn(nodeData.Is<CommissionNodeData>());
+    auto & commissionData = nodeData.Get<CommissionNodeData>();
+
+    auto discriminator = commissionData.longDiscriminator;
+    auto vendorId      = static_cast<VendorId>(commissionData.vendorId);
+    auto productId     = commissionData.productId;
+
+    ChipLogProgress(NotSpecified, "OnNodeDiscovered (MDNS): discriminator: %u, vendorId: %u, productId: %u", discriminator,
+                    vendorId, productId);
+
+    const CommonResolutionData & resolutionData = commissionData;
+
+    auto & instanceData  = mDiscoveredResults[commissionData.instanceName];
+    auto & interfaceData = instanceData[resolutionData.interfaceId.GetPlatformInterface()];
+
+    for (size_t i = 0; i < resolutionData.numIPs; i++)
+    {
+        auto params                = Controller::SetUpCodePairerParameters(resolutionData, i);
+        DeviceScannerResult result = { params, vendorId, productId, discriminator, chip::MakeOptional(resolutionData) };
+        interfaceData.push_back(result);
+    }
+
+    commissionData.LogDetail();
+}
+
+void DeviceScanner::OnBrowseAdd(chip::Dnssd::DnssdService service)
+{
+    ChipLogProgress(NotSpecified, "OnBrowseAdd: %s", service.mName);
+    LogErrorOnFailure(ChipDnssdResolve(&service, service.mInterface, this));
+
+    auto & instanceData  = mDiscoveredResults[service.mName];
+    auto & interfaceData = instanceData[service.mInterface.GetPlatformInterface()];
+    (void) interfaceData;
+}
+
+void DeviceScanner::OnBrowseRemove(chip::Dnssd::DnssdService service)
+{
+    ChipLogProgress(NotSpecified, "OnBrowseRemove: %s", service.mName);
+    auto & instanceData  = mDiscoveredResults[service.mName];
+    auto & interfaceData = instanceData[service.mInterface.GetPlatformInterface()];
+
+    // Check if the interface data has been resolved already, otherwise, just inform the
+    // back end that we may not need it anymore.
+    if (interfaceData.size() == 0)
+    {
+        ChipDnssdResolveNoLongerNeeded(service.mName);
+    }
+
+    // Delete the interface placeholder.
+    instanceData.erase(service.mInterface.GetPlatformInterface());
+
+    // If there is nothing else to resolve for the given instance name, just remove it
+    // too.
+    if (instanceData.size() == 0)
+    {
+        mDiscoveredResults.erase(service.mName);
+    }
+}
+
+void DeviceScanner::OnBrowseStop(CHIP_ERROR error)
+{
+    ChipLogProgress(NotSpecified, "OnBrowseStop: %" CHIP_ERROR_FORMAT, error.Format());
+
+    for (auto & instance : mDiscoveredResults)
+    {
+        for (auto & interface : instance.second)
+        {
+            if (interface.second.size() == 0)
+            {
+                ChipDnssdResolveNoLongerNeeded(instance.first.c_str());
+            }
+        }
+    }
+}
+
+#if CONFIG_NETWORK_LAYER_BLE
+void DeviceScanner::OnBleScanAdd(BLE_CONNECTION_OBJECT connObj, const ChipBLEDeviceIdentificationInfo & info)
+{
+    auto discriminator = info.GetDeviceDiscriminator();
+    auto vendorId      = static_cast<VendorId>(info.GetVendorId());
+    auto productId     = info.GetProductId();
+
+    ChipLogProgress(NotSpecified, "OnBleScanAdd (BLE): %p, discriminator: %u, vendorId: %u, productId: %u", connObj, discriminator,
+                    vendorId, productId);
+
+    auto params                = Controller::SetUpCodePairerParameters(connObj, false /* connected */);
+    DeviceScannerResult result = { params, vendorId, productId, discriminator };
+
+    auto & instanceData  = mDiscoveredResults[kBleKey];
+    auto & interfaceData = instanceData[chip::Inet::InterfaceId::Null().GetPlatformInterface()];
+    interfaceData.push_back(result);
+}
+
+void DeviceScanner::OnBleScanRemove(BLE_CONNECTION_OBJECT connObj)
+{
+    ChipLogProgress(NotSpecified, "OnBleScanRemove: %p", connObj);
+
+    auto & instanceData  = mDiscoveredResults[kBleKey];
+    auto & interfaceData = instanceData[chip::Inet::InterfaceId::Null().GetPlatformInterface()];
+
+    interfaceData.erase(std::remove_if(interfaceData.begin(), interfaceData.end(),
+                                       [connObj](const DeviceScannerResult & result) {
+                                           return result.mParams.HasDiscoveredObject() &&
+                                               result.mParams.GetDiscoveredObject() == connObj;
+                                       }),
+                        interfaceData.end());
+
+    if (interfaceData.size() == 0)
+    {
+        instanceData.clear();
+        mDiscoveredResults.erase(kBleKey);
+    }
+}
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+CHIP_ERROR DeviceScanner::Get(uint16_t index, RendezvousParameters & params)
+{
+    uint16_t currentIndex = 0;
+    for (auto & instance : mDiscoveredResults)
+    {
+        for (auto & interface : instance.second)
+        {
+            for (auto & result : interface.second)
+            {
+                if (currentIndex == index)
+                {
+                    params = result.mParams;
+                    return CHIP_NO_ERROR;
+                }
+                currentIndex++;
+            }
+        }
+    }
+
+    return CHIP_ERROR_NOT_FOUND;
+}
+
+CHIP_ERROR DeviceScanner::Get(uint16_t index, Dnssd::CommonResolutionData & resolutionData)
+{
+    uint16_t currentIndex = 0;
+    for (auto & instance : mDiscoveredResults)
+    {
+        for (auto & interface : instance.second)
+        {
+            for (auto & result : interface.second)
+            {
+                if (currentIndex == index && result.mResolutionData.HasValue())
+                {
+                    resolutionData = result.mResolutionData.Value();
+                    return CHIP_NO_ERROR;
+                }
+                currentIndex++;
+            }
+        }
+    }
+
+    return CHIP_ERROR_NOT_FOUND;
+}
+
+void DeviceScanner::Log() const
+{
+    auto resultsCount = mDiscoveredResults.size();
+    VerifyOrReturn(resultsCount > 0, ChipLogProgress(NotSpecified, "No device discovered."));
+
+    [[maybe_unused]] uint16_t index = 0;
+    for (auto & instance : mDiscoveredResults)
+    {
+        ChipLogProgress(NotSpecified, "Instance Name: %s ", instance.first.c_str());
+        for (auto & interface : instance.second)
+        {
+            for (auto & result : interface.second)
+            {
+                char addr[Transport::PeerAddress::kMaxToStringSize];
+                result.mParams.GetPeerAddress().ToString(addr);
+
+                ChipLogProgress(NotSpecified, "\t %u - Discriminator: %u - Vendor: %u - Product: %u - %s", index,
+                                result.mDiscriminator, result.mVendorId, result.mProductId, addr);
+                index++;
+            }
+        }
+    }
+}
+
+DeviceScanner & GetDeviceScanner()
+{
+    static DeviceScanner scanner;
+    return scanner;
+}
diff --git a/examples/fabric-admin/commands/common/DeviceScanner.h b/examples/fabric-admin/commands/common/DeviceScanner.h
new file mode 100644
index 0000000..c3e81a5
--- /dev/null
+++ b/examples/fabric-admin/commands/common/DeviceScanner.h
@@ -0,0 +1,80 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <platform/CHIPDeviceConfig.h>
+
+#if CHIP_DEVICE_LAYER_TARGET_DARWIN
+
+#include <controller/CHIPDeviceController.h>
+#include <lib/dnssd/platform/Dnssd.h>
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#if CONFIG_NETWORK_LAYER_BLE
+#include <platform/Darwin/BleScannerDelegate.h>
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+struct DeviceScannerResult
+{
+    chip::Controller::SetUpCodePairerParameters mParams;
+    chip::VendorId mVendorId;
+    uint16_t mProductId;
+    uint16_t mDiscriminator;
+    chip::Optional<chip::Dnssd::CommonResolutionData> mResolutionData;
+};
+
+class DeviceScanner : public chip::Dnssd::DiscoverNodeDelegate,
+                      public chip::Dnssd::DnssdBrowseDelegate
+#if CONFIG_NETWORK_LAYER_BLE
+    ,
+                      public chip::DeviceLayer::BleScannerDelegate
+#endif // CONFIG_NETWORK_LAYER_BLE
+{
+public:
+    CHIP_ERROR Start();
+    CHIP_ERROR Stop();
+    CHIP_ERROR Get(uint16_t index, chip::RendezvousParameters & params);
+    CHIP_ERROR Get(uint16_t index, chip::Dnssd::CommonResolutionData & resolutionData);
+    void Log() const;
+
+    /////////// DiscoverNodeDelegate Interface /////////
+    void OnNodeDiscovered(const chip::Dnssd::DiscoveredNodeData & nodeData) override;
+
+    /////////// DnssdBrowseDelegate Interface /////////
+    void OnBrowseAdd(chip::Dnssd::DnssdService service) override;
+    void OnBrowseRemove(chip::Dnssd::DnssdService service) override;
+    void OnBrowseStop(CHIP_ERROR error) override;
+
+#if CONFIG_NETWORK_LAYER_BLE
+    /////////// BleScannerDelegate Interface /////////
+    void OnBleScanAdd(BLE_CONNECTION_OBJECT connObj, const chip::Ble::ChipBLEDeviceIdentificationInfo & info) override;
+    void OnBleScanRemove(BLE_CONNECTION_OBJECT connObj) override;
+#endif // CONFIG_NETWORK_LAYER_BLE
+
+private:
+    std::unordered_map<std::string, std::map<chip::Inet::InterfaceId::PlatformType, std::vector<DeviceScannerResult>>>
+        mDiscoveredResults;
+};
+
+DeviceScanner & GetDeviceScanner();
+
+#endif // CHIP_DEVICE_LAYER_TARGET_DARWIN
diff --git a/examples/fabric-admin/commands/common/HexConversion.h b/examples/fabric-admin/commands/common/HexConversion.h
new file mode 100644
index 0000000..99b7f65
--- /dev/null
+++ b/examples/fabric-admin/commands/common/HexConversion.h
@@ -0,0 +1,65 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+#pragma once
+
+#include <lib/core/CHIPError.h>
+#include <lib/support/BytesToHex.h>
+#include <lib/support/Span.h>
+#include <lib/support/logging/CHIPLogging.h>
+
+/**
+ * Utility for converting a hex string to bytes, with the right error checking
+ * and allocation size computation.
+ *
+ * Takes a functor to allocate the buffer to use for the hex bytes.  The functor
+ * is expected to return uint8_t *.  The caller is responsible for cleaning up
+ * this buffer as needed.
+ *
+ * On success, *octetCount is filled with the number of octets placed in the
+ * buffer.  On failure, the value of *octetCount is undefined.
+ */
+template <typename F>
+CHIP_ERROR HexToBytes(chip::CharSpan hex, F bufferAllocator, size_t * octetCount)
+{
+    *octetCount = 0;
+
+    if (hex.size() % 2 != 0)
+    {
+        ChipLogError(NotSpecified, "Error while encoding '%.*s' as an octet string: Odd number of characters.",
+                     static_cast<int>(hex.size()), hex.data());
+        return CHIP_ERROR_INVALID_STRING_LENGTH;
+    }
+
+    const size_t bufferSize = hex.size() / 2;
+    uint8_t * buffer        = bufferAllocator(bufferSize);
+    if (buffer == nullptr && bufferSize != 0)
+    {
+        ChipLogError(NotSpecified, "Failed to allocate buffer of size: %llu", static_cast<unsigned long long>(bufferSize));
+        return CHIP_ERROR_NO_MEMORY;
+    }
+
+    size_t byteCount = chip::Encoding::HexToBytes(hex.data(), hex.size(), buffer, bufferSize);
+    if (byteCount == 0 && hex.size() != 0)
+    {
+        ChipLogError(NotSpecified, "Error while encoding '%.*s' as an octet string.", static_cast<int>(hex.size()), hex.data());
+        return CHIP_ERROR_INTERNAL;
+    }
+
+    *octetCount = byteCount;
+    return CHIP_NO_ERROR;
+}
diff --git a/examples/fabric-admin/commands/common/RemoteDataModelLogger.cpp b/examples/fabric-admin/commands/common/RemoteDataModelLogger.cpp
new file mode 100644
index 0000000..c8e9d5a
--- /dev/null
+++ b/examples/fabric-admin/commands/common/RemoteDataModelLogger.cpp
@@ -0,0 +1,279 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "RemoteDataModelLogger.h"
+
+#include <lib/support/SafeInt.h>
+#include <lib/support/jsontlv/TlvJson.h>
+
+constexpr char kEventNumberKey[]    = "eventNumber";
+constexpr char kDataVersionKey[]    = "dataVersion";
+constexpr char kClusterIdKey[]      = "clusterId";
+constexpr char kEndpointIdKey[]     = "endpointId";
+constexpr char kAttributeIdKey[]    = "attributeId";
+constexpr char kEventIdKey[]        = "eventId";
+constexpr char kCommandIdKey[]      = "commandId";
+constexpr char kErrorIdKey[]        = "error";
+constexpr char kClusterErrorIdKey[] = "clusterError";
+constexpr char kValueKey[]          = "value";
+constexpr char kNodeIdKey[]         = "nodeId";
+constexpr char kNOCKey[]            = "NOC";
+constexpr char kICACKey[]           = "ICAC";
+constexpr char kRCACKey[]           = "RCAC";
+constexpr char kIPKKey[]            = "IPK";
+
+namespace {
+RemoteDataModelLoggerDelegate * gDelegate;
+
+CHIP_ERROR LogError(Json::Value & value, const chip::app::StatusIB & status)
+{
+    if (status.mClusterStatus.HasValue())
+    {
+        auto statusValue          = status.mClusterStatus.Value();
+        value[kClusterErrorIdKey] = statusValue;
+    }
+
+#if CHIP_CONFIG_IM_STATUS_CODE_VERBOSE_FORMAT
+    auto statusName    = chip::Protocols::InteractionModel::StatusName(status.mStatus);
+    value[kErrorIdKey] = statusName;
+#else
+    auto statusName    = status.mStatus;
+    value[kErrorIdKey] = chip::to_underlying(statusName);
+#endif // CHIP_CONFIG_IM_STATUS_CODE_VERBOSE_FORMAT
+
+    auto valueStr = chip::JsonToString(value);
+    return gDelegate->LogJSON(valueStr.c_str());
+}
+
+} // namespace
+
+namespace RemoteDataModelLogger {
+CHIP_ERROR LogAttributeAsJSON(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value value;
+    value[kClusterIdKey]   = path.mClusterId;
+    value[kEndpointIdKey]  = path.mEndpointId;
+    value[kAttributeIdKey] = path.mAttributeId;
+    if (path.mDataVersion.HasValue())
+    {
+        value[kDataVersionKey] = path.mDataVersion.Value();
+    }
+
+    chip::TLV::TLVReader reader;
+    reader.Init(*data);
+    ReturnErrorOnFailure(chip::TlvToJson(reader, value));
+
+    auto valueStr = chip::JsonToString(value);
+    return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteDataAttributePath & path, const chip::app::StatusIB & status)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value value;
+    value[kClusterIdKey]   = path.mClusterId;
+    value[kEndpointIdKey]  = path.mEndpointId;
+    value[kAttributeIdKey] = path.mAttributeId;
+
+    return LogError(value, status);
+}
+
+CHIP_ERROR LogCommandAsJSON(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader * data)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value value;
+    value[kClusterIdKey]  = path.mClusterId;
+    value[kEndpointIdKey] = path.mEndpointId;
+    value[kCommandIdKey]  = path.mCommandId;
+
+    chip::TLV::TLVReader reader;
+    reader.Init(*data);
+    ReturnErrorOnFailure(chip::TlvToJson(reader, value));
+
+    auto valueStr = chip::JsonToString(value);
+    return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteCommandPath & path, const chip::app::StatusIB & status)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value value;
+    value[kClusterIdKey]  = path.mClusterId;
+    value[kEndpointIdKey] = path.mEndpointId;
+    value[kCommandIdKey]  = path.mCommandId;
+
+    return LogError(value, status);
+}
+
+CHIP_ERROR LogEventAsJSON(const chip::app::EventHeader & header, chip::TLV::TLVReader * data)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value value;
+    value[kClusterIdKey]   = header.mPath.mClusterId;
+    value[kEndpointIdKey]  = header.mPath.mEndpointId;
+    value[kEventIdKey]     = header.mPath.mEventId;
+    value[kEventNumberKey] = header.mEventNumber;
+
+    chip::TLV::TLVReader reader;
+    reader.Init(*data);
+    ReturnErrorOnFailure(chip::TlvToJson(reader, value));
+
+    auto valueStr = chip::JsonToString(value);
+    return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogErrorAsJSON(const chip::app::EventHeader & header, const chip::app::StatusIB & status)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value value;
+    value[kClusterIdKey]  = header.mPath.mClusterId;
+    value[kEndpointIdKey] = header.mPath.mEndpointId;
+    value[kEventIdKey]    = header.mPath.mEventId;
+
+    return LogError(value, status);
+}
+
+CHIP_ERROR LogErrorAsJSON(const CHIP_ERROR & error)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value value;
+    chip::app::StatusIB status;
+    status.InitFromChipError(error);
+    return LogError(value, status);
+}
+
+CHIP_ERROR LogGetCommissionerNodeId(chip::NodeId value)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value rootValue;
+    rootValue[kValueKey]             = Json::Value();
+    rootValue[kValueKey][kNodeIdKey] = value;
+
+    auto valueStr = chip::JsonToString(rootValue);
+    return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogGetCommissionerRootCertificate(const char * value)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value rootValue;
+    rootValue[kValueKey]           = Json::Value();
+    rootValue[kValueKey][kRCACKey] = value;
+
+    auto valueStr = chip::JsonToString(rootValue);
+    return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogIssueNOCChain(const char * noc, const char * icac, const char * rcac, const char * ipk)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    Json::Value rootValue;
+    rootValue[kValueKey]           = Json::Value();
+    rootValue[kValueKey][kNOCKey]  = noc;
+    rootValue[kValueKey][kICACKey] = icac;
+    rootValue[kValueKey][kRCACKey] = rcac;
+    rootValue[kValueKey][kIPKKey]  = ipk;
+
+    auto valueStr = chip::JsonToString(rootValue);
+    return gDelegate->LogJSON(valueStr.c_str());
+}
+
+CHIP_ERROR LogDiscoveredNodeData(const chip::Dnssd::CommissionNodeData & nodeData)
+{
+    VerifyOrReturnError(gDelegate != nullptr, CHIP_NO_ERROR);
+
+    auto & commissionData = nodeData;
+    auto & resolutionData = commissionData;
+
+    if (!chip::CanCastTo<uint8_t>(resolutionData.numIPs))
+    {
+        ChipLogError(NotSpecified, "Too many ips.");
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    if (!chip::CanCastTo<uint64_t>(commissionData.rotatingIdLen))
+    {
+        ChipLogError(NotSpecified, "Can not convert rotatingId to json format.");
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    char rotatingId[chip::Dnssd::kMaxRotatingIdLen * 2 + 1] = "";
+    ReturnErrorOnFailure(chip::Encoding::BytesToUppercaseHexString(commissionData.rotatingId, commissionData.rotatingIdLen,
+                                                                   rotatingId, sizeof(rotatingId)));
+
+    Json::Value value;
+    value["hostName"]           = resolutionData.hostName;
+    value["instanceName"]       = commissionData.instanceName;
+    value["longDiscriminator"]  = commissionData.longDiscriminator;
+    value["shortDiscriminator"] = ((commissionData.longDiscriminator >> 8) & 0x0F);
+    value["vendorId"]           = commissionData.vendorId;
+    value["productId"]          = commissionData.productId;
+    value["commissioningMode"]  = commissionData.commissioningMode;
+    value["deviceType"]         = commissionData.deviceType;
+    value["deviceName"]         = commissionData.deviceName;
+    value["rotatingId"]         = rotatingId;
+    value["rotatingIdLen"]      = static_cast<uint64_t>(commissionData.rotatingIdLen);
+    value["pairingHint"]        = commissionData.pairingHint;
+    value["pairingInstruction"] = commissionData.pairingInstruction;
+    value["supportsTcp"]        = resolutionData.supportsTcp;
+    value["port"]               = resolutionData.port;
+    value["numIPs"]             = static_cast<uint8_t>(resolutionData.numIPs);
+
+    if (resolutionData.mrpRetryIntervalIdle.has_value())
+    {
+        value["mrpRetryIntervalIdle"] = resolutionData.mrpRetryIntervalIdle->count();
+    }
+
+    if (resolutionData.mrpRetryIntervalActive.has_value())
+    {
+        value["mrpRetryIntervalActive"] = resolutionData.mrpRetryIntervalActive->count();
+    }
+
+    if (resolutionData.mrpRetryActiveThreshold.has_value())
+    {
+        value["mrpRetryActiveThreshold"] = resolutionData.mrpRetryActiveThreshold->count();
+    }
+
+    if (resolutionData.isICDOperatingAsLIT.has_value())
+    {
+        value["isICDOperatingAsLIT"] = *(resolutionData.isICDOperatingAsLIT);
+    }
+
+    Json::Value rootValue;
+    rootValue[kValueKey] = value;
+
+    auto valueStr = chip::JsonToString(rootValue);
+    return gDelegate->LogJSON(valueStr.c_str());
+}
+
+void SetDelegate(RemoteDataModelLoggerDelegate * delegate)
+{
+    gDelegate = delegate;
+}
+}; // namespace RemoteDataModelLogger
diff --git a/examples/fabric-admin/commands/common/RemoteDataModelLogger.h b/examples/fabric-admin/commands/common/RemoteDataModelLogger.h
new file mode 100644
index 0000000..c31636e
--- /dev/null
+++ b/examples/fabric-admin/commands/common/RemoteDataModelLogger.h
@@ -0,0 +1,48 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <app/ConcreteAttributePath.h>
+#include <app/ConcreteCommandPath.h>
+#include <app/EventHeader.h>
+#include <app/MessageDef/StatusIB.h>
+#include <crypto/CHIPCryptoPAL.h>
+#include <lib/dnssd/Resolver.h>
+
+class RemoteDataModelLoggerDelegate
+{
+public:
+    CHIP_ERROR virtual LogJSON(const char *) = 0;
+    virtual ~RemoteDataModelLoggerDelegate(){};
+};
+
+namespace RemoteDataModelLogger {
+CHIP_ERROR LogAttributeAsJSON(const chip::app::ConcreteDataAttributePath & path, chip::TLV::TLVReader * data);
+CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteDataAttributePath & path, const chip::app::StatusIB & status);
+CHIP_ERROR LogCommandAsJSON(const chip::app::ConcreteCommandPath & path, chip::TLV::TLVReader * data);
+CHIP_ERROR LogErrorAsJSON(const chip::app::ConcreteCommandPath & path, const chip::app::StatusIB & status);
+CHIP_ERROR LogEventAsJSON(const chip::app::EventHeader & header, chip::TLV::TLVReader * data);
+CHIP_ERROR LogErrorAsJSON(const chip::app::EventHeader & header, const chip::app::StatusIB & status);
+CHIP_ERROR LogErrorAsJSON(const CHIP_ERROR & error);
+CHIP_ERROR LogGetCommissionerNodeId(chip::NodeId value);
+CHIP_ERROR LogGetCommissionerRootCertificate(const char * value);
+CHIP_ERROR LogIssueNOCChain(const char * noc, const char * icac, const char * rcac, const char * ipk);
+CHIP_ERROR LogDiscoveredNodeData(const chip::Dnssd::CommissionNodeData & nodeData);
+void SetDelegate(RemoteDataModelLoggerDelegate * delegate);
+}; // namespace RemoteDataModelLogger
diff --git a/examples/fabric-admin/commands/example/ExampleCredentialIssuerCommands.h b/examples/fabric-admin/commands/example/ExampleCredentialIssuerCommands.h
new file mode 100644
index 0000000..a739ac7
--- /dev/null
+++ b/examples/fabric-admin/commands/example/ExampleCredentialIssuerCommands.h
@@ -0,0 +1,111 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <commands/common/CredentialIssuerCommands.h>
+#include <controller/CHIPDeviceControllerFactory.h>
+#include <controller/ExampleOperationalCredentialsIssuer.h>
+#include <credentials/DeviceAttestationCredsProvider.h>
+#include <credentials/attestation_verifier/DefaultDeviceAttestationVerifier.h>
+#include <credentials/attestation_verifier/DeviceAttestationVerifier.h>
+#include <credentials/examples/DeviceAttestationCredsExample.h>
+
+class ExampleCredentialIssuerCommands : public CredentialIssuerCommands
+{
+public:
+    CHIP_ERROR InitializeCredentialsIssuer(chip::PersistentStorageDelegate & storage) override
+    {
+        return mOpCredsIssuer.Initialize(storage);
+    }
+    CHIP_ERROR SetupDeviceAttestation(chip::Controller::SetupParams & setupParams,
+                                      const chip::Credentials::AttestationTrustStore * trustStore) override
+    {
+        chip::Credentials::SetDeviceAttestationCredentialsProvider(chip::Credentials::Examples::GetExampleDACProvider());
+
+        mDacVerifier                          = chip::Credentials::GetDefaultDACVerifier(trustStore);
+        setupParams.deviceAttestationVerifier = mDacVerifier;
+        mDacVerifier->EnableCdTestKeySupport(mAllowTestCdSigningKey);
+
+        return CHIP_NO_ERROR;
+    }
+    chip::Controller::OperationalCredentialsDelegate * GetCredentialIssuer() override { return &mOpCredsIssuer; }
+    void SetCredentialIssuerCATValues(chip::CATValues cats) override { mOpCredsIssuer.SetCATValuesForNextNOCRequest(cats); }
+    CHIP_ERROR GenerateControllerNOCChain(chip::NodeId nodeId, chip::FabricId fabricId, const chip::CATValues & cats,
+                                          chip::Crypto::P256Keypair & keypair, chip::MutableByteSpan & rcac,
+                                          chip::MutableByteSpan & icac, chip::MutableByteSpan & noc) override
+    {
+        return mOpCredsIssuer.GenerateNOCChainAfterValidation(nodeId, fabricId, cats, keypair.Pubkey(), rcac, icac, noc);
+    }
+
+    CHIP_ERROR AddAdditionalCDVerifyingCerts(const std::vector<std::vector<uint8_t>> & additionalCdCerts) override
+    {
+        VerifyOrReturnError(mDacVerifier != nullptr, CHIP_ERROR_INCORRECT_STATE);
+
+        for (const auto & cert : additionalCdCerts)
+        {
+            auto cdTrustStore = mDacVerifier->GetCertificationDeclarationTrustStore();
+            VerifyOrReturnError(cdTrustStore != nullptr, CHIP_ERROR_INCORRECT_STATE);
+            ReturnErrorOnFailure(cdTrustStore->AddTrustedKey(chip::ByteSpan(cert.data(), cert.size())));
+        }
+
+        return CHIP_NO_ERROR;
+    }
+
+    void SetCredentialIssuerOption(CredentialIssuerOptions option, bool isEnabled) override
+    {
+        switch (option)
+        {
+        case CredentialIssuerOptions::kMaximizeCertificateSizes:
+            mUsesMaxSizedCerts = isEnabled;
+            mOpCredsIssuer.SetMaximallyLargeCertsUsed(mUsesMaxSizedCerts);
+            break;
+        case CredentialIssuerOptions::kAllowTestCdSigningKey:
+            mAllowTestCdSigningKey = isEnabled;
+            if (mDacVerifier != nullptr)
+            {
+                mDacVerifier->EnableCdTestKeySupport(isEnabled);
+            }
+            break;
+        default:
+            break;
+        }
+    }
+
+    bool GetCredentialIssuerOption(CredentialIssuerOptions option) override
+    {
+        switch (option)
+        {
+        case CredentialIssuerOptions::kMaximizeCertificateSizes:
+            return mUsesMaxSizedCerts;
+        case CredentialIssuerOptions::kAllowTestCdSigningKey:
+            return mAllowTestCdSigningKey;
+        default:
+            return false;
+        }
+    }
+
+protected:
+    bool mUsesMaxSizedCerts = false;
+    // Starts true for legacy purposes
+    bool mAllowTestCdSigningKey = true;
+
+private:
+    chip::Controller::ExampleOperationalCredentialsIssuer mOpCredsIssuer;
+    chip::Credentials::DeviceAttestationVerifier * mDacVerifier;
+};
diff --git a/examples/fabric-admin/commands/interactive/Commands.h b/examples/fabric-admin/commands/interactive/Commands.h
new file mode 100644
index 0000000..e324dda
--- /dev/null
+++ b/examples/fabric-admin/commands/interactive/Commands.h
@@ -0,0 +1,34 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "commands/common/CHIPCommand.h"
+#include "commands/common/Commands.h"
+#include "commands/interactive/InteractiveCommands.h"
+
+void registerCommandsInteractive(Commands & commands, CredentialIssuerCommands * credsIssuerConfig)
+{
+    const char * clusterName = "interactive";
+
+    commands_list clusterCommands = {
+        make_unique<InteractiveStartCommand>(&commands, credsIssuerConfig),
+    };
+
+    commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for starting long-lived interactive modes.");
+}
diff --git a/examples/fabric-admin/commands/interactive/InteractiveCommands.cpp b/examples/fabric-admin/commands/interactive/InteractiveCommands.cpp
new file mode 100644
index 0000000..9ef07e7
--- /dev/null
+++ b/examples/fabric-admin/commands/interactive/InteractiveCommands.cpp
@@ -0,0 +1,139 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "InteractiveCommands.h"
+
+#include <platform/logging/LogV.h>
+
+#include <editline.h>
+
+#include <string>
+#include <vector>
+
+constexpr char kInteractiveModePrompt[]          = ">>> ";
+constexpr char kInteractiveModeHistoryFileName[] = "chip_tool_history";
+constexpr char kInteractiveModeStopCommand[]     = "quit()";
+
+namespace {
+
+void ClearLine()
+{
+    printf("\r\x1B[0J"); // Move cursor to the beginning of the line and clear from cursor to end of the screen
+}
+
+void ENFORCE_FORMAT(3, 0) LoggingCallback(const char * module, uint8_t category, const char * msg, va_list args)
+{
+    ClearLine();
+    chip::Logging::Platform::LogV(module, category, msg, args);
+    ClearLine();
+}
+
+} // namespace
+
+char * InteractiveStartCommand::GetCommand(char * command)
+{
+    if (command != nullptr)
+    {
+        free(command);
+        command = nullptr;
+    }
+
+    command = readline(kInteractiveModePrompt);
+
+    // Do not save empty lines
+    if (command != nullptr && *command)
+    {
+        add_history(command);
+        write_history(GetHistoryFilePath().c_str());
+    }
+
+    return command;
+}
+
+std::string InteractiveStartCommand::GetHistoryFilePath() const
+{
+    std::string storageDir;
+    if (GetStorageDirectory().HasValue())
+    {
+        storageDir = GetStorageDirectory().Value();
+    }
+    else
+    {
+        // Match what GetFilename in ExamplePersistentStorage.cpp does.
+        const char * dir = getenv("TMPDIR");
+        if (dir == nullptr)
+        {
+            dir = "/tmp";
+        }
+        storageDir = dir;
+    }
+
+    return storageDir + "/" + kInteractiveModeHistoryFileName;
+}
+
+CHIP_ERROR InteractiveStartCommand::RunCommand()
+{
+    read_history(GetHistoryFilePath().c_str());
+
+    // Logs needs to be redirected in order to refresh the screen appropriately when something
+    // is dumped to stdout while the user is typing a command.
+    chip::Logging::SetLogRedirectCallback(LoggingCallback);
+
+    char * command = nullptr;
+    int status;
+    while (true)
+    {
+        command = GetCommand(command);
+        if (command != nullptr && !ParseCommand(command, &status))
+        {
+            break;
+        }
+    }
+
+    if (command != nullptr)
+    {
+        free(command);
+        command = nullptr;
+    }
+
+    SetCommandExitStatus(CHIP_NO_ERROR);
+    return CHIP_NO_ERROR;
+}
+
+bool InteractiveCommand::ParseCommand(char * command, int * status)
+{
+    if (strcmp(command, kInteractiveModeStopCommand) == 0)
+    {
+        // If scheduling the cleanup fails, there is not much we can do.
+        // But if something went wrong while the application is leaving it could be because things have
+        // not been cleaned up properly, so it is still useful to log the failure.
+        LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().ScheduleWork(ExecuteDeferredCleanups, 0));
+        return false;
+    }
+
+    ClearLine();
+
+    *status = mHandler->RunInteractive(command, GetStorageDirectory(), NeedsOperationalAdvertising());
+
+    return true;
+}
+
+bool InteractiveCommand::NeedsOperationalAdvertising()
+{
+    return mAdvertiseOperational.ValueOr(true);
+}
diff --git a/examples/fabric-admin/commands/interactive/InteractiveCommands.h b/examples/fabric-admin/commands/interactive/InteractiveCommands.h
new file mode 100644
index 0000000..21c14a7
--- /dev/null
+++ b/examples/fabric-admin/commands/interactive/InteractiveCommands.h
@@ -0,0 +1,68 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "../clusters/DataModelLogger.h"
+#include "../common/CHIPCommand.h"
+#include "../common/Commands.h"
+
+#include <websocket-server/WebSocketServer.h>
+
+#include <string>
+
+class Commands;
+
+class InteractiveCommand : public CHIPCommand
+{
+public:
+    InteractiveCommand(const char * name, Commands * commandsHandler, const char * helpText,
+                       CredentialIssuerCommands * credsIssuerConfig) :
+        CHIPCommand(name, credsIssuerConfig, helpText),
+        mHandler(commandsHandler)
+    {
+        AddArgument("advertise-operational", 0, 1, &mAdvertiseOperational,
+                    "Advertise operational node over DNS-SD and accept incoming CASE sessions.");
+    }
+
+    /////////// CHIPCommand Interface /////////
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(0); }
+    bool NeedsOperationalAdvertising() override;
+
+    bool ParseCommand(char * command, int * status);
+
+private:
+    Commands * mHandler = nullptr;
+    chip::Optional<bool> mAdvertiseOperational;
+};
+
+class InteractiveStartCommand : public InteractiveCommand
+{
+public:
+    InteractiveStartCommand(Commands * commandsHandler, CredentialIssuerCommands * credsIssuerConfig) :
+        InteractiveCommand("start", commandsHandler, "Start an interactive shell that can then run other commands.",
+                           credsIssuerConfig)
+    {}
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override;
+
+private:
+    char * GetCommand(char * command);
+    std::string GetHistoryFilePath() const;
+};
diff --git a/examples/fabric-admin/commands/pairing/Commands.h b/examples/fabric-admin/commands/pairing/Commands.h
new file mode 100644
index 0000000..6fdface
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/Commands.h
@@ -0,0 +1,259 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "commands/common/Commands.h"
+#include "commands/pairing/GetCommissionerNodeIdCommand.h"
+#include "commands/pairing/GetCommissionerRootCertificateCommand.h"
+#include "commands/pairing/IssueNOCChainCommand.h"
+#include "commands/pairing/OpenCommissioningWindowCommand.h"
+#include "commands/pairing/PairingCommand.h"
+
+#include <app/server/Dnssd.h>
+#include <commands/common/CredentialIssuerCommands.h>
+#include <lib/dnssd/Resolver.h>
+
+class Unpair : public PairingCommand
+{
+public:
+    Unpair(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("unpair", PairingMode::None, PairingNetworkType::None, credsIssuerConfig)
+    {}
+};
+
+class PairCode : public PairingCommand
+{
+public:
+    PairCode(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("code", PairingMode::Code, PairingNetworkType::None, credsIssuerConfig)
+    {}
+};
+
+class PairCodePase : public PairingCommand
+{
+public:
+    PairCodePase(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("code-paseonly", PairingMode::CodePaseOnly, PairingNetworkType::None, credsIssuerConfig)
+    {}
+};
+
+class PairCodeWifi : public PairingCommand
+{
+public:
+    PairCodeWifi(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("code-wifi", PairingMode::Code, PairingNetworkType::WiFi, credsIssuerConfig)
+    {}
+};
+
+class PairCodeThread : public PairingCommand
+{
+public:
+    PairCodeThread(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("code-thread", PairingMode::Code, PairingNetworkType::Thread, credsIssuerConfig)
+    {}
+};
+
+class PairOnNetwork : public PairingCommand
+{
+public:
+    PairOnNetwork(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("onnetwork", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig)
+    {}
+};
+
+class PairOnNetworkShort : public PairingCommand
+{
+public:
+    PairOnNetworkShort(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("onnetwork-short", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+                       chip::Dnssd::DiscoveryFilterType::kShortDiscriminator)
+    {}
+};
+
+class PairOnNetworkLong : public PairingCommand
+{
+public:
+    PairOnNetworkLong(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("onnetwork-long", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+                       chip::Dnssd::DiscoveryFilterType::kLongDiscriminator)
+    {}
+};
+
+class PairOnNetworkVendor : public PairingCommand
+{
+public:
+    PairOnNetworkVendor(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("onnetwork-vendor", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+                       chip::Dnssd::DiscoveryFilterType::kVendorId)
+    {}
+};
+
+class PairOnNetworkFabric : public PairingCommand
+{
+public:
+    PairOnNetworkFabric(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("onnetwork-fabric", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+                       chip::Dnssd::DiscoveryFilterType::kCompressedFabricId)
+    {}
+};
+
+class PairOnNetworkCommissioningMode : public PairingCommand
+{
+public:
+    PairOnNetworkCommissioningMode(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("onnetwork-commissioning-mode", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+                       chip::Dnssd::DiscoveryFilterType::kCommissioningMode)
+    {}
+};
+
+class PairOnNetworkCommissioner : public PairingCommand
+{
+public:
+    PairOnNetworkCommissioner(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("onnetwork-commissioner", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+                       chip::Dnssd::DiscoveryFilterType::kCommissioner)
+    {}
+};
+
+class PairOnNetworkDeviceType : public PairingCommand
+{
+public:
+    PairOnNetworkDeviceType(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("onnetwork-device-type", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+                       chip::Dnssd::DiscoveryFilterType::kDeviceType)
+    {}
+};
+
+class PairOnNetworkInstanceName : public PairingCommand
+{
+public:
+    PairOnNetworkInstanceName(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("onnetwork-instance-name", PairingMode::OnNetwork, PairingNetworkType::None, credsIssuerConfig,
+                       chip::Dnssd::DiscoveryFilterType::kInstanceName)
+    {}
+};
+
+class PairBleWiFi : public PairingCommand
+{
+public:
+    PairBleWiFi(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("ble-wifi", PairingMode::Ble, PairingNetworkType::WiFi, credsIssuerConfig)
+    {}
+};
+
+class PairBleThread : public PairingCommand
+{
+public:
+    PairBleThread(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("ble-thread", PairingMode::Ble, PairingNetworkType::Thread, credsIssuerConfig)
+    {}
+};
+
+class PairSoftAP : public PairingCommand
+{
+public:
+    PairSoftAP(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("softap", PairingMode::SoftAP, PairingNetworkType::WiFi, credsIssuerConfig)
+    {}
+};
+
+class PairAlreadyDiscovered : public PairingCommand
+{
+public:
+    PairAlreadyDiscovered(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("already-discovered", PairingMode::AlreadyDiscovered, PairingNetworkType::None, credsIssuerConfig)
+    {}
+};
+
+class PairAlreadyDiscoveredByIndex : public PairingCommand
+{
+public:
+    PairAlreadyDiscoveredByIndex(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("already-discovered-by-index", PairingMode::AlreadyDiscoveredByIndex, PairingNetworkType::None,
+                       credsIssuerConfig)
+    {}
+};
+
+class PairAlreadyDiscoveredByIndexWithWiFi : public PairingCommand
+{
+public:
+    PairAlreadyDiscoveredByIndexWithWiFi(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("already-discovered-by-index-with-wifi", PairingMode::AlreadyDiscoveredByIndex, PairingNetworkType::WiFi,
+                       credsIssuerConfig)
+    {}
+};
+
+class PairAlreadyDiscoveredByIndexWithCode : public PairingCommand
+{
+public:
+    PairAlreadyDiscoveredByIndexWithCode(CredentialIssuerCommands * credsIssuerConfig) :
+        PairingCommand("already-discovered-by-index-with-code", PairingMode::AlreadyDiscoveredByIndexWithCode,
+                       PairingNetworkType::None, credsIssuerConfig)
+    {}
+};
+
+class StartUdcServerCommand : public CHIPCommand
+{
+public:
+    StartUdcServerCommand(CredentialIssuerCommands * credsIssuerConfig) : CHIPCommand("start-udc-server", credsIssuerConfig) {}
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(300); }
+
+    CHIP_ERROR RunCommand() override
+    {
+        chip::app::DnssdServer::Instance().StartServer(chip::Dnssd::CommissioningMode::kDisabled);
+        return CHIP_NO_ERROR;
+    }
+};
+
+void registerCommandsPairing(Commands & commands, CredentialIssuerCommands * credsIssuerConfig)
+{
+    const char * clusterName = "Pairing";
+
+    commands_list clusterCommands = {
+        make_unique<Unpair>(credsIssuerConfig),
+        make_unique<PairCode>(credsIssuerConfig),
+        make_unique<PairCodePase>(credsIssuerConfig),
+        make_unique<PairCodeWifi>(credsIssuerConfig),
+        make_unique<PairCodeThread>(credsIssuerConfig),
+        make_unique<PairBleWiFi>(credsIssuerConfig),
+        make_unique<PairBleThread>(credsIssuerConfig),
+        make_unique<PairSoftAP>(credsIssuerConfig),
+        make_unique<PairAlreadyDiscovered>(credsIssuerConfig),
+        make_unique<PairAlreadyDiscoveredByIndex>(credsIssuerConfig),
+        make_unique<PairAlreadyDiscoveredByIndexWithWiFi>(credsIssuerConfig),
+        make_unique<PairAlreadyDiscoveredByIndexWithCode>(credsIssuerConfig),
+        make_unique<PairOnNetwork>(credsIssuerConfig),
+        make_unique<PairOnNetworkShort>(credsIssuerConfig),
+        make_unique<PairOnNetworkLong>(credsIssuerConfig),
+        make_unique<PairOnNetworkVendor>(credsIssuerConfig),
+        make_unique<PairOnNetworkCommissioningMode>(credsIssuerConfig),
+        make_unique<PairOnNetworkCommissioner>(credsIssuerConfig),
+        make_unique<PairOnNetworkDeviceType>(credsIssuerConfig),
+        make_unique<PairOnNetworkInstanceName>(credsIssuerConfig),
+        // TODO(#13973) - enable CommissionedListCommand once DNS Cache is implemented
+        //        make_unique<CommissionedListCommand>(),
+        make_unique<StartUdcServerCommand>(credsIssuerConfig),
+        make_unique<OpenCommissioningWindowCommand>(credsIssuerConfig),
+        make_unique<GetCommissionerNodeIdCommand>(credsIssuerConfig),
+        make_unique<GetCommissionerRootCertificateCommand>(credsIssuerConfig),
+        make_unique<IssueNOCChainCommand>(credsIssuerConfig),
+    };
+
+    commands.RegisterCommandSet(clusterName, clusterCommands, "Commands for commissioning devices.");
+}
diff --git a/examples/fabric-admin/commands/pairing/GetCommissionerNodeIdCommand.h b/examples/fabric-admin/commands/pairing/GetCommissionerNodeIdCommand.h
new file mode 100644
index 0000000..3234cfe
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/GetCommissionerNodeIdCommand.h
@@ -0,0 +1,44 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "../common/CHIPCommand.h"
+#include "../common/RemoteDataModelLogger.h"
+
+class GetCommissionerNodeIdCommand : public CHIPCommand
+{
+public:
+    GetCommissionerNodeIdCommand(CredentialIssuerCommands * credIssuerCommands) :
+        CHIPCommand("get-commissioner-node-id", credIssuerCommands)
+    {}
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override
+    {
+        chip::NodeId id;
+        ReturnErrorOnFailure(GetIdentityNodeId(GetIdentity(), &id));
+        ChipLogProgress(NotSpecified, "Commissioner Node Id 0x:" ChipLogFormatX64, ChipLogValueX64(id));
+
+        ReturnErrorOnFailure(RemoteDataModelLogger::LogGetCommissionerNodeId(id));
+        SetCommandExitStatus(CHIP_NO_ERROR);
+        return CHIP_NO_ERROR;
+    }
+
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+};
diff --git a/examples/fabric-admin/commands/pairing/GetCommissionerRootCertificateCommand.h b/examples/fabric-admin/commands/pairing/GetCommissionerRootCertificateCommand.h
new file mode 100644
index 0000000..1d25efc
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/GetCommissionerRootCertificateCommand.h
@@ -0,0 +1,53 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "../common/CHIPCommand.h"
+#include "../common/RemoteDataModelLogger.h"
+
+#include "ToTLVCert.h"
+
+#include <string>
+
+class GetCommissionerRootCertificateCommand : public CHIPCommand
+{
+public:
+    GetCommissionerRootCertificateCommand(CredentialIssuerCommands * credIssuerCommands) :
+        CHIPCommand("get-commissioner-root-certificate", credIssuerCommands,
+                    "Returns a base64-encoded RCAC prefixed with: 'base64:'")
+    {}
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override
+    {
+        chip::ByteSpan span;
+        ReturnErrorOnFailure(GetIdentityRootCertificate(GetIdentity(), span));
+
+        std::string rcac;
+        ReturnErrorOnFailure(ToTLVCert(span, rcac));
+        ChipLogProgress(NotSpecified, "RCAC: %s", rcac.c_str());
+
+        ReturnErrorOnFailure(RemoteDataModelLogger::LogGetCommissionerRootCertificate(rcac.c_str()));
+
+        SetCommandExitStatus(CHIP_NO_ERROR);
+        return CHIP_NO_ERROR;
+    }
+
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+};
diff --git a/examples/fabric-admin/commands/pairing/IssueNOCChainCommand.h b/examples/fabric-admin/commands/pairing/IssueNOCChainCommand.h
new file mode 100644
index 0000000..0103b26
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/IssueNOCChainCommand.h
@@ -0,0 +1,91 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "../common/CHIPCommand.h"
+#include "../common/RemoteDataModelLogger.h"
+
+#include "ToTLVCert.h"
+
+#include <string>
+
+class IssueNOCChainCommand : public CHIPCommand
+{
+public:
+    IssueNOCChainCommand(CredentialIssuerCommands * credIssuerCommands) :
+        CHIPCommand("issue-noc-chain", credIssuerCommands,
+                    "Returns a base64-encoded NOC, ICAC, RCAC, and IPK prefixed with: 'base64:'"),
+        mDeviceNOCChainCallback(OnDeviceNOCChainGeneration, this)
+    {
+        AddArgument("elements", &mNOCSRElements, "NOCSRElements encoded in hexadecimal");
+        AddArgument("node-id", 0, UINT64_MAX, &mNodeId, "The target node id");
+    }
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override
+    {
+        auto & commissioner = CurrentCommissioner();
+        ReturnErrorOnFailure(commissioner.IssueNOCChain(mNOCSRElements, mNodeId, &mDeviceNOCChainCallback));
+        return CHIP_NO_ERROR;
+    }
+
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(10); }
+
+    static void OnDeviceNOCChainGeneration(void * context, CHIP_ERROR status, const chip::ByteSpan & noc,
+                                           const chip::ByteSpan & icac, const chip::ByteSpan & rcac,
+                                           chip::Optional<chip::Crypto::IdentityProtectionKeySpan> ipk,
+                                           chip::Optional<chip::NodeId> adminSubject)
+    {
+        auto command = static_cast<IssueNOCChainCommand *>(context);
+
+        auto err = status;
+        VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+
+        std::string nocStr;
+        err = ToTLVCert(noc, nocStr);
+        VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+        ChipLogProgress(NotSpecified, "NOC: %s", nocStr.c_str());
+
+        std::string icacStr;
+        err = ToTLVCert(icac, icacStr);
+        VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+        ChipLogProgress(NotSpecified, "ICAC: %s", icacStr.c_str());
+
+        std::string rcacStr;
+        err = ToTLVCert(rcac, rcacStr);
+        VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+        ChipLogProgress(NotSpecified, "RCAC: %s", rcacStr.c_str());
+
+        std::string ipkStr;
+        if (ipk.HasValue())
+        {
+            err = ToBase64(ipk.Value(), ipkStr);
+            VerifyOrReturn(CHIP_NO_ERROR == err, command->SetCommandExitStatus(err));
+        }
+        ChipLogProgress(NotSpecified, "IPK: %s", ipkStr.c_str());
+
+        err = RemoteDataModelLogger::LogIssueNOCChain(nocStr.c_str(), icacStr.c_str(), rcacStr.c_str(), ipkStr.c_str());
+        command->SetCommandExitStatus(err);
+    }
+
+private:
+    chip::Callback::Callback<chip::Controller::OnNOCChainGeneration> mDeviceNOCChainCallback;
+    chip::ByteSpan mNOCSRElements;
+    chip::NodeId mNodeId;
+};
diff --git a/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.cpp b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.cpp
new file mode 100644
index 0000000..bc4af6c
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.cpp
@@ -0,0 +1,62 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "OpenCommissioningWindowCommand.h"
+
+#include <system/SystemClock.h>
+
+using namespace ::chip;
+
+CHIP_ERROR OpenCommissioningWindowCommand::RunCommand()
+{
+    mWindowOpener = Platform::MakeUnique<Controller::CommissioningWindowOpener>(&CurrentCommissioner());
+    if (mCommissioningWindowOption == Controller::CommissioningWindowOpener::CommissioningWindowOption::kOriginalSetupCode)
+    {
+        return mWindowOpener->OpenBasicCommissioningWindow(mNodeId, System::Clock::Seconds16(mCommissioningWindowTimeout),
+                                                           &mOnOpenBasicCommissioningWindowCallback);
+    }
+
+    if (mCommissioningWindowOption == Controller::CommissioningWindowOpener::CommissioningWindowOption::kTokenWithRandomPIN)
+    {
+        SetupPayload ignored;
+        return mWindowOpener->OpenCommissioningWindow(mNodeId, System::Clock::Seconds16(mCommissioningWindowTimeout), mIteration,
+                                                      mDiscriminator, NullOptional, NullOptional,
+                                                      &mOnOpenCommissioningWindowCallback, ignored,
+                                                      /* readVIDPIDAttributes */ true);
+    }
+
+    ChipLogError(NotSpecified, "Unknown commissioning window option: %d", to_underlying(mCommissioningWindowOption));
+    return CHIP_ERROR_INVALID_ARGUMENT;
+}
+
+void OpenCommissioningWindowCommand::OnOpenCommissioningWindowResponse(void * context, NodeId remoteId, CHIP_ERROR err,
+                                                                       chip::SetupPayload payload)
+{
+    LogErrorOnFailure(err);
+
+    OnOpenBasicCommissioningWindowResponse(context, remoteId, err);
+}
+
+void OpenCommissioningWindowCommand::OnOpenBasicCommissioningWindowResponse(void * context, NodeId remoteId, CHIP_ERROR err)
+{
+    LogErrorOnFailure(err);
+
+    OpenCommissioningWindowCommand * command = reinterpret_cast<OpenCommissioningWindowCommand *>(context);
+    VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnOpenCommissioningWindowCommand: context is null"));
+    command->SetCommandExitStatus(err);
+}
diff --git a/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.h b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.h
new file mode 100644
index 0000000..99b179d
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/OpenCommissioningWindowCommand.h
@@ -0,0 +1,67 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "../common/CHIPCommand.h"
+
+#include <controller/CommissioningWindowOpener.h>
+#include <lib/support/CHIPMem.h>
+
+class OpenCommissioningWindowCommand : public CHIPCommand
+{
+public:
+    OpenCommissioningWindowCommand(CredentialIssuerCommands * credIssuerCommands) :
+        CHIPCommand("open-commissioning-window", credIssuerCommands),
+        mOnOpenCommissioningWindowCallback(OnOpenCommissioningWindowResponse, this),
+        mOnOpenBasicCommissioningWindowCallback(OnOpenBasicCommissioningWindowResponse, this)
+    {
+        AddArgument("node-id", 0, UINT64_MAX, &mNodeId, "Node to send command to.");
+        AddArgument("option", 0, 2, &mCommissioningWindowOption,
+                    "1 to use Enhanced Commissioning Method.\n  0 to use Basic Commissioning Method.");
+        AddArgument("window-timeout", 0, UINT16_MAX, &mCommissioningWindowTimeout,
+                    "Time, in seconds, before the commissioning window closes.");
+        AddArgument("iteration", chip::Crypto::kSpake2p_Min_PBKDF_Iterations, chip::Crypto::kSpake2p_Max_PBKDF_Iterations,
+                    &mIteration, "Number of PBKDF iterations to use to derive the verifier.  Ignored if 'option' is 0.");
+        AddArgument("discriminator", 0, 4096, &mDiscriminator, "Discriminator to use for advertising.  Ignored if 'option' is 0.");
+        AddArgument("timeout", 0, UINT16_MAX, &mTimeout, "Time, in seconds, before this command is considered to have timed out.");
+    }
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override;
+    // We issue multiple data model operations for this command, and the default
+    // timeout for those is 10 seconds, so default to 20 seconds.
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(20)); }
+
+private:
+    NodeId mNodeId;
+    chip::Controller::CommissioningWindowOpener::CommissioningWindowOption mCommissioningWindowOption;
+    uint16_t mCommissioningWindowTimeout;
+    uint32_t mIteration;
+    uint16_t mDiscriminator;
+
+    chip::Optional<uint16_t> mTimeout;
+
+    chip::Platform::UniquePtr<chip::Controller::CommissioningWindowOpener> mWindowOpener;
+
+    static void OnOpenCommissioningWindowResponse(void * context, NodeId deviceId, CHIP_ERROR status, chip::SetupPayload payload);
+    static void OnOpenBasicCommissioningWindowResponse(void * context, NodeId deviceId, CHIP_ERROR status);
+
+    chip::Callback::Callback<chip::Controller::OnOpenCommissioningWindow> mOnOpenCommissioningWindowCallback;
+    chip::Callback::Callback<chip::Controller::OnOpenBasicCommissioningWindow> mOnOpenBasicCommissioningWindowCallback;
+};
diff --git a/examples/fabric-admin/commands/pairing/PairingCommand.cpp b/examples/fabric-admin/commands/pairing/PairingCommand.cpp
new file mode 100644
index 0000000..80775f0
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/PairingCommand.cpp
@@ -0,0 +1,551 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "PairingCommand.h"
+#include "platform/PlatformManager.h"
+#include <commands/common/DeviceScanner.h>
+#include <controller/ExampleOperationalCredentialsIssuer.h>
+#include <crypto/CHIPCryptoPAL.h>
+#include <lib/core/CHIPSafeCasts.h>
+#include <lib/support/logging/CHIPLogging.h>
+#include <protocols/secure_channel/PASESession.h>
+
+#include <setup_payload/ManualSetupPayloadParser.h>
+#include <setup_payload/QRCodeSetupPayloadParser.h>
+
+#include <string>
+
+using namespace ::chip;
+using namespace ::chip::Controller;
+
+CHIP_ERROR PairingCommand::RunCommand()
+{
+    CurrentCommissioner().RegisterPairingDelegate(this);
+    // Clear the CATs in OperationalCredentialsIssuer
+    mCredIssuerCmds->SetCredentialIssuerCATValues(kUndefinedCATs);
+
+    mDeviceIsICD = false;
+
+    if (mCASEAuthTags.HasValue() && mCASEAuthTags.Value().size() <= kMaxSubjectCATAttributeCount)
+    {
+        CATValues cats = kUndefinedCATs;
+        for (size_t index = 0; index < mCASEAuthTags.Value().size(); ++index)
+        {
+            cats.values[index] = mCASEAuthTags.Value()[index];
+        }
+        if (cats.AreValid())
+        {
+            mCredIssuerCmds->SetCredentialIssuerCATValues(cats);
+        }
+    }
+    return RunInternal(mNodeId);
+}
+
+CHIP_ERROR PairingCommand::RunInternal(NodeId remoteId)
+{
+    CHIP_ERROR err = CHIP_NO_ERROR;
+
+    switch (mPairingMode)
+    {
+    case PairingMode::None:
+        err = Unpair(remoteId);
+        break;
+    case PairingMode::Code:
+        err = PairWithCode(remoteId);
+        break;
+    case PairingMode::CodePaseOnly:
+        err = PaseWithCode(remoteId);
+        break;
+    case PairingMode::Ble:
+        err = Pair(remoteId, PeerAddress::BLE());
+        break;
+    case PairingMode::OnNetwork:
+        err = PairWithMdns(remoteId);
+        break;
+    case PairingMode::SoftAP:
+        err = Pair(remoteId, PeerAddress::UDP(mRemoteAddr.address, mRemotePort, mRemoteAddr.interfaceId));
+        break;
+    case PairingMode::AlreadyDiscovered:
+        err = Pair(remoteId, PeerAddress::UDP(mRemoteAddr.address, mRemotePort, mRemoteAddr.interfaceId));
+        break;
+    case PairingMode::AlreadyDiscoveredByIndex:
+        err = PairWithMdnsOrBleByIndex(remoteId, mIndex);
+        break;
+    case PairingMode::AlreadyDiscoveredByIndexWithCode:
+        err = PairWithMdnsOrBleByIndexWithCode(remoteId, mIndex);
+        break;
+    }
+
+    return err;
+}
+
+CommissioningParameters PairingCommand::GetCommissioningParameters()
+{
+    auto params = CommissioningParameters();
+    params.SetSkipCommissioningComplete(mSkipCommissioningComplete.ValueOr(false));
+    if (mBypassAttestationVerifier.ValueOr(false))
+    {
+        params.SetDeviceAttestationDelegate(this);
+    }
+
+    switch (mNetworkType)
+    {
+    case PairingNetworkType::WiFi:
+        params.SetWiFiCredentials(Controller::WiFiCredentials(mSSID, mPassword));
+        break;
+    case PairingNetworkType::Thread:
+        params.SetThreadOperationalDataset(mOperationalDataset);
+        break;
+    case PairingNetworkType::None:
+        break;
+    }
+
+    if (mCountryCode.HasValue())
+    {
+        params.SetCountryCode(CharSpan::fromCharString(mCountryCode.Value()));
+    }
+
+    // mTimeZoneList is an optional argument managed by TypedComplexArgument mComplex_TimeZones.
+    // Since optional Complex arguments are not currently supported via the <chip::Optional> class,
+    // we will use mTimeZoneList.data() value to determine if the argument was provided.
+    if (mTimeZoneList.data())
+    {
+        params.SetTimeZone(mTimeZoneList);
+    }
+
+    // miDSTOffsetList is an optional argument managed by TypedComplexArgument mComplex_DSTOffsets.
+    // Since optional Complex arguments are not currently supported via the <chip::Optional> class,
+    // we will use mTimeZoneList.data() value to determine if the argument was provided.
+    if (mDSTOffsetList.data())
+    {
+        params.SetDSTOffsets(mDSTOffsetList);
+    }
+
+    if (mICDRegistration.ValueOr(false))
+    {
+        params.SetICDRegistrationStrategy(ICDRegistrationStrategy::kBeforeComplete);
+
+        if (!mICDSymmetricKey.HasValue())
+        {
+            chip::Crypto::DRBG_get_bytes(mRandomGeneratedICDSymmetricKey, sizeof(mRandomGeneratedICDSymmetricKey));
+            mICDSymmetricKey.SetValue(ByteSpan(mRandomGeneratedICDSymmetricKey));
+        }
+        if (!mICDCheckInNodeId.HasValue())
+        {
+            mICDCheckInNodeId.SetValue(CurrentCommissioner().GetNodeId());
+        }
+        if (!mICDMonitoredSubject.HasValue())
+        {
+            mICDMonitoredSubject.SetValue(mICDCheckInNodeId.Value());
+        }
+        // These Optionals must have values now.
+        // The commissioner will verify these values.
+        params.SetICDSymmetricKey(mICDSymmetricKey.Value());
+        if (mICDStayActiveDurationMsec.HasValue())
+        {
+            params.SetICDStayActiveDurationMsec(mICDStayActiveDurationMsec.Value());
+        }
+        params.SetICDCheckInNodeId(mICDCheckInNodeId.Value());
+        params.SetICDMonitoredSubject(mICDMonitoredSubject.Value());
+    }
+
+    return params;
+}
+
+CHIP_ERROR PairingCommand::PaseWithCode(NodeId remoteId)
+{
+    auto discoveryType = DiscoveryType::kAll;
+    if (mUseOnlyOnNetworkDiscovery.ValueOr(false))
+    {
+        discoveryType = DiscoveryType::kDiscoveryNetworkOnly;
+    }
+
+    if (mDiscoverOnce.ValueOr(false))
+    {
+        discoveryType = DiscoveryType::kDiscoveryNetworkOnlyWithoutPASEAutoRetry;
+    }
+
+    return CurrentCommissioner().EstablishPASEConnection(remoteId, mOnboardingPayload, discoveryType);
+}
+
+CHIP_ERROR PairingCommand::PairWithCode(NodeId remoteId)
+{
+    CommissioningParameters commissioningParams = GetCommissioningParameters();
+
+    // If no network discovery behavior and no network credentials are provided, assume that the pairing command is trying to pair
+    // with an on-network device.
+    if (!mUseOnlyOnNetworkDiscovery.HasValue())
+    {
+        auto threadCredentials = commissioningParams.GetThreadOperationalDataset();
+        auto wiFiCredentials   = commissioningParams.GetWiFiCredentials();
+        mUseOnlyOnNetworkDiscovery.SetValue(!threadCredentials.HasValue() && !wiFiCredentials.HasValue());
+    }
+
+    auto discoveryType = DiscoveryType::kAll;
+    if (mUseOnlyOnNetworkDiscovery.ValueOr(false))
+    {
+        discoveryType = DiscoveryType::kDiscoveryNetworkOnly;
+    }
+
+    if (mDiscoverOnce.ValueOr(false))
+    {
+        discoveryType = DiscoveryType::kDiscoveryNetworkOnlyWithoutPASEAutoRetry;
+    }
+
+    return CurrentCommissioner().PairDevice(remoteId, mOnboardingPayload, commissioningParams, discoveryType);
+}
+
+CHIP_ERROR PairingCommand::Pair(NodeId remoteId, PeerAddress address)
+{
+    auto params = RendezvousParameters().SetSetupPINCode(mSetupPINCode).SetDiscriminator(mDiscriminator).SetPeerAddress(address);
+
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    if (mPaseOnly.ValueOr(false))
+    {
+        err = CurrentCommissioner().EstablishPASEConnection(remoteId, params);
+    }
+    else
+    {
+        auto commissioningParams = GetCommissioningParameters();
+        err                      = CurrentCommissioner().PairDevice(remoteId, params, commissioningParams);
+    }
+    return err;
+}
+
+CHIP_ERROR PairingCommand::PairWithMdnsOrBleByIndex(NodeId remoteId, uint16_t index)
+{
+#if CHIP_DEVICE_LAYER_TARGET_DARWIN
+    VerifyOrReturnError(IsInteractive(), CHIP_ERROR_INCORRECT_STATE);
+
+    RendezvousParameters params;
+    ReturnErrorOnFailure(GetDeviceScanner().Get(index, params));
+    params.SetSetupPINCode(mSetupPINCode);
+
+    CHIP_ERROR err = CHIP_NO_ERROR;
+    if (mPaseOnly.ValueOr(false))
+    {
+        err = CurrentCommissioner().EstablishPASEConnection(remoteId, params);
+    }
+    else
+    {
+        auto commissioningParams = GetCommissioningParameters();
+        err                      = CurrentCommissioner().PairDevice(remoteId, params, commissioningParams);
+    }
+    return err;
+#else
+    return CHIP_ERROR_NOT_IMPLEMENTED;
+#endif // CHIP_DEVICE_LAYER_TARGET_DARWIN
+}
+
+CHIP_ERROR PairingCommand::PairWithMdnsOrBleByIndexWithCode(NodeId remoteId, uint16_t index)
+{
+#if CHIP_DEVICE_LAYER_TARGET_DARWIN
+    VerifyOrReturnError(IsInteractive(), CHIP_ERROR_INCORRECT_STATE);
+
+    Dnssd::CommonResolutionData resolutionData;
+    auto err = GetDeviceScanner().Get(index, resolutionData);
+    if (CHIP_ERROR_NOT_FOUND == err)
+    {
+        // There is no device with this index that has some resolution data. This could simply
+        // be because the device is a ble device. In this case let's fall back to looking for
+        // a device with this index and some RendezvousParameters.
+        chip::SetupPayload payload;
+        bool isQRCode = strncmp(mOnboardingPayload, kQRCodePrefix, strlen(kQRCodePrefix)) == 0;
+        if (isQRCode)
+        {
+            ReturnErrorOnFailure(QRCodeSetupPayloadParser(mOnboardingPayload).populatePayload(payload));
+            VerifyOrReturnError(payload.isValidQRCodePayload(), CHIP_ERROR_INVALID_ARGUMENT);
+        }
+        else
+        {
+            ReturnErrorOnFailure(ManualSetupPayloadParser(mOnboardingPayload).populatePayload(payload));
+            VerifyOrReturnError(payload.isValidManualCode(), CHIP_ERROR_INVALID_ARGUMENT);
+        }
+
+        mSetupPINCode = payload.setUpPINCode;
+        return PairWithMdnsOrBleByIndex(remoteId, index);
+    }
+
+    err = CHIP_NO_ERROR;
+    if (mPaseOnly.ValueOr(false))
+    {
+        err = CurrentCommissioner().EstablishPASEConnection(remoteId, mOnboardingPayload, DiscoveryType::kDiscoveryNetworkOnly,
+                                                            MakeOptional(resolutionData));
+    }
+    else
+    {
+        auto commissioningParams = GetCommissioningParameters();
+        err                      = CurrentCommissioner().PairDevice(remoteId, mOnboardingPayload, commissioningParams,
+                                                                    DiscoveryType::kDiscoveryNetworkOnly, MakeOptional(resolutionData));
+    }
+    return err;
+#else
+    return CHIP_ERROR_NOT_IMPLEMENTED;
+#endif // CHIP_DEVICE_LAYER_TARGET_DARWIN
+}
+
+CHIP_ERROR PairingCommand::PairWithMdns(NodeId remoteId)
+{
+    Dnssd::DiscoveryFilter filter(mFilterType);
+    switch (mFilterType)
+    {
+    case chip::Dnssd::DiscoveryFilterType::kNone:
+        break;
+    case chip::Dnssd::DiscoveryFilterType::kShortDiscriminator:
+    case chip::Dnssd::DiscoveryFilterType::kLongDiscriminator:
+    case chip::Dnssd::DiscoveryFilterType::kCompressedFabricId:
+    case chip::Dnssd::DiscoveryFilterType::kVendorId:
+    case chip::Dnssd::DiscoveryFilterType::kDeviceType:
+        filter.code = mDiscoveryFilterCode;
+        break;
+    case chip::Dnssd::DiscoveryFilterType::kCommissioningMode:
+        break;
+    case chip::Dnssd::DiscoveryFilterType::kCommissioner:
+        filter.code = 1;
+        break;
+    case chip::Dnssd::DiscoveryFilterType::kInstanceName:
+        filter.code         = 0;
+        filter.instanceName = mDiscoveryFilterInstanceName;
+        break;
+    }
+
+    CurrentCommissioner().RegisterDeviceDiscoveryDelegate(this);
+    return CurrentCommissioner().DiscoverCommissionableNodes(filter);
+}
+
+CHIP_ERROR PairingCommand::Unpair(NodeId remoteId)
+{
+    mCurrentFabricRemover = Platform::MakeUnique<Controller::CurrentFabricRemover>(&CurrentCommissioner());
+    return mCurrentFabricRemover->RemoveCurrentFabric(remoteId, &mCurrentFabricRemoveCallback);
+}
+
+void PairingCommand::OnStatusUpdate(DevicePairingDelegate::Status status)
+{
+    switch (status)
+    {
+    case DevicePairingDelegate::Status::SecurePairingSuccess:
+        ChipLogProgress(NotSpecified, "Secure Pairing Success");
+        ChipLogProgress(NotSpecified, "CASE establishment successful");
+        break;
+    case DevicePairingDelegate::Status::SecurePairingFailed:
+        ChipLogError(NotSpecified, "Secure Pairing Failed");
+        SetCommandExitStatus(CHIP_ERROR_INCORRECT_STATE);
+        break;
+    }
+}
+
+void PairingCommand::OnPairingComplete(CHIP_ERROR err)
+{
+    if (err == CHIP_NO_ERROR)
+    {
+        ChipLogProgress(NotSpecified, "Pairing Success");
+        ChipLogProgress(NotSpecified, "PASE establishment successful");
+        if (mPairingMode == PairingMode::CodePaseOnly || mPaseOnly.ValueOr(false))
+        {
+            SetCommandExitStatus(err);
+        }
+    }
+    else
+    {
+        ChipLogProgress(NotSpecified, "Pairing Failure: %s", ErrorStr(err));
+    }
+
+    if (err != CHIP_NO_ERROR)
+    {
+        SetCommandExitStatus(err);
+    }
+}
+
+void PairingCommand::OnPairingDeleted(CHIP_ERROR err)
+{
+    if (err == CHIP_NO_ERROR)
+    {
+        ChipLogProgress(NotSpecified, "Pairing Deleted Success");
+    }
+    else
+    {
+        ChipLogProgress(NotSpecified, "Pairing Deleted Failure: %s", ErrorStr(err));
+    }
+
+    SetCommandExitStatus(err);
+}
+
+void PairingCommand::OnCommissioningComplete(NodeId nodeId, CHIP_ERROR err)
+{
+    if (err == CHIP_NO_ERROR)
+    {
+        ChipLogProgress(NotSpecified, "Device commissioning completed with success");
+    }
+    else
+    {
+        // When ICD device commissioning fails, the ICDClientInfo stored in OnICDRegistrationComplete needs to be removed.
+        if (mDeviceIsICD)
+        {
+            CHIP_ERROR deleteEntryError =
+                CHIPCommand::sICDClientStorage.DeleteEntry(ScopedNodeId(mNodeId, CurrentCommissioner().GetFabricIndex()));
+            if (deleteEntryError != CHIP_NO_ERROR)
+            {
+                ChipLogError(NotSpecified, "Failed to delete ICD entry: %s", ErrorStr(err));
+            }
+        }
+        ChipLogProgress(NotSpecified, "Device commissioning Failure: %s", ErrorStr(err));
+    }
+
+    SetCommandExitStatus(err);
+}
+
+void PairingCommand::OnReadCommissioningInfo(const Controller::ReadCommissioningInfo & info)
+{
+    ChipLogProgress(AppServer, "OnReadCommissioningInfo - vendorId=0x%04X productId=0x%04X", info.basic.vendorId,
+                    info.basic.productId);
+
+    // The string in CharSpan received from the device is not null-terminated, we use std::string here for coping and
+    // appending a numm-terminator at the end of the string.
+    std::string userActiveModeTriggerInstruction;
+
+    // Note: the callback doesn't own the buffer, should make a copy if it will be used it later.
+    if (info.icd.userActiveModeTriggerInstruction.size() != 0)
+    {
+        userActiveModeTriggerInstruction =
+            std::string(info.icd.userActiveModeTriggerInstruction.data(), info.icd.userActiveModeTriggerInstruction.size());
+    }
+
+    if (info.icd.userActiveModeTriggerHint.HasAny())
+    {
+        ChipLogProgress(AppServer, "OnReadCommissioningInfo - LIT UserActiveModeTriggerHint=0x%08x",
+                        info.icd.userActiveModeTriggerHint.Raw());
+        ChipLogProgress(AppServer, "OnReadCommissioningInfo - LIT UserActiveModeTriggerInstruction=%s",
+                        userActiveModeTriggerInstruction.c_str());
+    }
+    ChipLogProgress(AppServer, "OnReadCommissioningInfo ICD - IdleModeDuration=%u activeModeDuration=%u activeModeThreshold=%u",
+                    info.icd.idleModeDuration, info.icd.activeModeDuration, info.icd.activeModeThreshold);
+}
+
+void PairingCommand::OnICDRegistrationComplete(NodeId nodeId, uint32_t icdCounter)
+{
+    char icdSymmetricKeyHex[chip::Crypto::kAES_CCM128_Key_Length * 2 + 1];
+
+    chip::Encoding::BytesToHex(mICDSymmetricKey.Value().data(), mICDSymmetricKey.Value().size(), icdSymmetricKeyHex,
+                               sizeof(icdSymmetricKeyHex), chip::Encoding::HexFlags::kNullTerminate);
+
+    app::ICDClientInfo clientInfo;
+    clientInfo.peer_node         = ScopedNodeId(nodeId, CurrentCommissioner().GetFabricIndex());
+    clientInfo.monitored_subject = mICDMonitoredSubject.Value();
+    clientInfo.start_icd_counter = icdCounter;
+
+    CHIP_ERROR err = CHIPCommand::sICDClientStorage.SetKey(clientInfo, mICDSymmetricKey.Value());
+    if (err == CHIP_NO_ERROR)
+    {
+        err = CHIPCommand::sICDClientStorage.StoreEntry(clientInfo);
+    }
+
+    if (err != CHIP_NO_ERROR)
+    {
+        CHIPCommand::sICDClientStorage.RemoveKey(clientInfo);
+        ChipLogError(NotSpecified, "Failed to persist symmetric key for " ChipLogFormatX64 ": %s", ChipLogValueX64(nodeId),
+                     err.AsString());
+        SetCommandExitStatus(err);
+        return;
+    }
+
+    mDeviceIsICD = true;
+
+    ChipLogProgress(NotSpecified, "Saved ICD Symmetric key for " ChipLogFormatX64, ChipLogValueX64(nodeId));
+    ChipLogProgress(NotSpecified,
+                    "ICD Registration Complete for device " ChipLogFormatX64 " / Check-In NodeID: " ChipLogFormatX64
+                    " / Monitored Subject: " ChipLogFormatX64 " / Symmetric Key: %s / ICDCounter %u",
+                    ChipLogValueX64(nodeId), ChipLogValueX64(mICDCheckInNodeId.Value()),
+                    ChipLogValueX64(mICDMonitoredSubject.Value()), icdSymmetricKeyHex, icdCounter);
+}
+
+void PairingCommand::OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration)
+{
+    ChipLogProgress(NotSpecified, "ICD Stay Active Complete for device " ChipLogFormatX64 " / promisedActiveDuration: %u",
+                    ChipLogValueX64(deviceId), promisedActiveDuration);
+}
+
+void PairingCommand::OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData)
+{
+    // Ignore nodes with closed commissioning window
+    VerifyOrReturn(nodeData.commissioningMode != 0);
+
+    auto & resolutionData = nodeData;
+
+    const uint16_t port = resolutionData.port;
+    char buf[chip::Inet::IPAddress::kMaxStringLength];
+    resolutionData.ipAddress[0].ToString(buf);
+    ChipLogProgress(NotSpecified, "Discovered Device: %s:%u", buf, port);
+
+    // Stop Mdns discovery.
+    auto err = CurrentCommissioner().StopCommissionableDiscovery();
+
+    // Some platforms does not implement a mechanism to stop mdns browse, so
+    // we just ignore CHIP_ERROR_NOT_IMPLEMENTED instead of bailing out.
+    if (CHIP_NO_ERROR != err && CHIP_ERROR_NOT_IMPLEMENTED != err)
+    {
+        SetCommandExitStatus(err);
+        return;
+    }
+
+    CurrentCommissioner().RegisterDeviceDiscoveryDelegate(nullptr);
+
+    auto interfaceId = resolutionData.ipAddress[0].IsIPv6LinkLocal() ? resolutionData.interfaceId : Inet::InterfaceId::Null();
+    auto peerAddress = PeerAddress::UDP(resolutionData.ipAddress[0], port, interfaceId);
+    err              = Pair(mNodeId, peerAddress);
+    if (CHIP_NO_ERROR != err)
+    {
+        SetCommandExitStatus(err);
+    }
+}
+
+void PairingCommand::OnCurrentFabricRemove(void * context, NodeId nodeId, CHIP_ERROR err)
+{
+    PairingCommand * command = reinterpret_cast<PairingCommand *>(context);
+    VerifyOrReturn(command != nullptr, ChipLogError(NotSpecified, "OnCurrentFabricRemove: context is null"));
+
+    if (err == CHIP_NO_ERROR)
+    {
+        ChipLogProgress(NotSpecified, "Device unpair completed with success: " ChipLogFormatX64, ChipLogValueX64(nodeId));
+    }
+    else
+    {
+        ChipLogProgress(NotSpecified, "Device unpair Failure: " ChipLogFormatX64 " %s", ChipLogValueX64(nodeId), ErrorStr(err));
+    }
+
+    command->SetCommandExitStatus(err);
+}
+
+chip::Optional<uint16_t> PairingCommand::FailSafeExpiryTimeoutSecs() const
+{
+    // We don't need to set additional failsafe timeout as we don't ask the final user if he wants to continue
+    return chip::Optional<uint16_t>();
+}
+
+void PairingCommand::OnDeviceAttestationCompleted(chip::Controller::DeviceCommissioner * deviceCommissioner,
+                                                  chip::DeviceProxy * device,
+                                                  const chip::Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info,
+                                                  chip::Credentials::AttestationVerificationResult attestationResult)
+{
+    // Bypass attestation verification, continue with success
+    auto err = deviceCommissioner->ContinueCommissioningAfterDeviceAttestation(
+        device, chip::Credentials::AttestationVerificationResult::kSuccess);
+    if (CHIP_NO_ERROR != err)
+    {
+        SetCommandExitStatus(err);
+    }
+}
diff --git a/examples/fabric-admin/commands/pairing/PairingCommand.h b/examples/fabric-admin/commands/pairing/PairingCommand.h
new file mode 100644
index 0000000..4ff3903
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/PairingCommand.h
@@ -0,0 +1,268 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include "../common/CHIPCommand.h"
+#include <controller/CommissioningDelegate.h>
+#include <controller/CurrentFabricRemover.h>
+
+#include <commands/common/CredentialIssuerCommands.h>
+#include <lib/support/Span.h>
+#include <lib/support/ThreadOperationalDataset.h>
+
+enum class PairingMode
+{
+    None,
+    Code,
+    CodePaseOnly,
+    Ble,
+    SoftAP,
+    AlreadyDiscovered,
+    AlreadyDiscoveredByIndex,
+    AlreadyDiscoveredByIndexWithCode,
+    OnNetwork,
+};
+
+enum class PairingNetworkType
+{
+    None,
+    WiFi,
+    Thread,
+};
+
+class PairingCommand : public CHIPCommand,
+                       public chip::Controller::DevicePairingDelegate,
+                       public chip::Controller::DeviceDiscoveryDelegate,
+                       public chip::Credentials::DeviceAttestationDelegate
+{
+public:
+    PairingCommand(const char * commandName, PairingMode mode, PairingNetworkType networkType,
+                   CredentialIssuerCommands * credIssuerCmds,
+                   chip::Dnssd::DiscoveryFilterType filterType = chip::Dnssd::DiscoveryFilterType::kNone) :
+        CHIPCommand(commandName, credIssuerCmds),
+        mPairingMode(mode), mNetworkType(networkType), mFilterType(filterType),
+        mRemoteAddr{ IPAddress::Any, chip::Inet::InterfaceId::Null() }, mComplex_TimeZones(&mTimeZoneList),
+        mComplex_DSTOffsets(&mDSTOffsetList), mCurrentFabricRemoveCallback(OnCurrentFabricRemove, this)
+    {
+        AddArgument("node-id", 0, UINT64_MAX, &mNodeId);
+        AddArgument("bypass-attestation-verifier", 0, 1, &mBypassAttestationVerifier,
+                    "Bypass the attestation verifier. If not provided or false, the attestation verifier is not bypassed."
+                    " If true, the commissioning will continue in case of attestation verification failure.");
+        AddArgument("case-auth-tags", 1, UINT32_MAX, &mCASEAuthTags, "The CATs to be encoded in the NOC sent to the commissionee");
+        AddArgument("icd-registration", 0, 1, &mICDRegistration,
+                    "Whether to register for check-ins from ICDs during commissioning. Default: false");
+        AddArgument("icd-check-in-nodeid", 0, UINT64_MAX, &mICDCheckInNodeId,
+                    "The check-in node id for the ICD, default: node id of the commissioner.");
+        AddArgument("icd-monitored-subject", 0, UINT64_MAX, &mICDMonitoredSubject,
+                    "The monitored subject of the ICD, default: The node id used for icd-check-in-nodeid.");
+        AddArgument("icd-symmetric-key", &mICDSymmetricKey, "The 16 bytes ICD symmetric key, default: randomly generated.");
+        AddArgument("icd-stay-active-duration", 0, UINT32_MAX, &mICDStayActiveDurationMsec,
+                    "If set, a LIT ICD that is commissioned will be requested to stay active for this many milliseconds");
+        switch (networkType)
+        {
+        case PairingNetworkType::None:
+            break;
+        case PairingNetworkType::WiFi:
+            AddArgument("ssid", &mSSID);
+            AddArgument("password", &mPassword);
+            break;
+        case PairingNetworkType::Thread:
+            AddArgument("operationalDataset", &mOperationalDataset);
+            break;
+        }
+
+        switch (mode)
+        {
+        case PairingMode::None:
+            break;
+        case PairingMode::Code:
+            AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+            FALLTHROUGH;
+        case PairingMode::CodePaseOnly:
+            AddArgument("payload", &mOnboardingPayload);
+            AddArgument("discover-once", 0, 1, &mDiscoverOnce);
+            AddArgument("use-only-onnetwork-discovery", 0, 1, &mUseOnlyOnNetworkDiscovery);
+            break;
+        case PairingMode::Ble:
+            AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+            AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+            AddArgument("discriminator", 0, 4096, &mDiscriminator);
+            break;
+        case PairingMode::OnNetwork:
+            AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+            AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+            AddArgument("pase-only", 0, 1, &mPaseOnly);
+            break;
+        case PairingMode::SoftAP:
+            AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+            AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+            AddArgument("discriminator", 0, 4096, &mDiscriminator);
+            AddArgument("device-remote-ip", &mRemoteAddr);
+            AddArgument("device-remote-port", 0, UINT16_MAX, &mRemotePort);
+            AddArgument("pase-only", 0, 1, &mPaseOnly);
+            break;
+        case PairingMode::AlreadyDiscovered:
+            AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+            AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+            AddArgument("device-remote-ip", &mRemoteAddr);
+            AddArgument("device-remote-port", 0, UINT16_MAX, &mRemotePort);
+            AddArgument("pase-only", 0, 1, &mPaseOnly);
+            break;
+        case PairingMode::AlreadyDiscoveredByIndex:
+            AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+            AddArgument("setup-pin-code", 0, 134217727, &mSetupPINCode);
+            AddArgument("index", 0, UINT16_MAX, &mIndex);
+            AddArgument("pase-only", 0, 1, &mPaseOnly);
+            break;
+        case PairingMode::AlreadyDiscoveredByIndexWithCode:
+            AddArgument("skip-commissioning-complete", 0, 1, &mSkipCommissioningComplete);
+            AddArgument("payload", &mOnboardingPayload);
+            AddArgument("index", 0, UINT16_MAX, &mIndex);
+            AddArgument("pase-only", 0, 1, &mPaseOnly);
+            break;
+        }
+
+        switch (filterType)
+        {
+        case chip::Dnssd::DiscoveryFilterType::kNone:
+            break;
+        case chip::Dnssd::DiscoveryFilterType::kShortDiscriminator:
+            AddArgument("discriminator", 0, 15, &mDiscoveryFilterCode);
+            break;
+        case chip::Dnssd::DiscoveryFilterType::kLongDiscriminator:
+            AddArgument("discriminator", 0, 4096, &mDiscoveryFilterCode);
+            break;
+        case chip::Dnssd::DiscoveryFilterType::kVendorId:
+            AddArgument("vendor-id", 0, UINT16_MAX, &mDiscoveryFilterCode);
+            break;
+        case chip::Dnssd::DiscoveryFilterType::kCompressedFabricId:
+            AddArgument("fabric-id", 0, UINT64_MAX, &mDiscoveryFilterCode);
+            break;
+        case chip::Dnssd::DiscoveryFilterType::kCommissioningMode:
+        case chip::Dnssd::DiscoveryFilterType::kCommissioner:
+            break;
+        case chip::Dnssd::DiscoveryFilterType::kDeviceType:
+            AddArgument("device-type", 0, UINT16_MAX, &mDiscoveryFilterCode);
+            break;
+        case chip::Dnssd::DiscoveryFilterType::kInstanceName:
+            AddArgument("name", &mDiscoveryFilterInstanceName);
+            break;
+        }
+
+        if (mode != PairingMode::None)
+        {
+            AddArgument("country-code", &mCountryCode,
+                        "Country code to use to set the Basic Information cluster's Location attribute");
+
+            // mTimeZoneList is an optional argument managed by TypedComplexArgument mComplex_TimeZones.
+            // Since optional Complex arguments are not currently supported via the <chip::Optional> class,
+            // we explicitly set the kOptional flag.
+            AddArgument("time-zone", &mComplex_TimeZones,
+                        "TimeZone list to use when setting Time Synchronization cluster's TimeZone attribute", Argument::kOptional);
+
+            // mDSTOffsetList is an optional argument managed by TypedComplexArgument mComplex_DSTOffsets.
+            // Since optional Complex arguments are not currently supported via the <chip::Optional> class,
+            // we explicitly set the kOptional flag.
+            AddArgument("dst-offset", &mComplex_DSTOffsets,
+                        "DSTOffset list to use when setting Time Synchronization cluster's DSTOffset attribute",
+                        Argument::kOptional);
+        }
+
+        AddArgument("timeout", 0, UINT16_MAX, &mTimeout);
+    }
+
+    /////////// CHIPCommand Interface /////////
+    CHIP_ERROR RunCommand() override;
+    chip::System::Clock::Timeout GetWaitDuration() const override { return chip::System::Clock::Seconds16(mTimeout.ValueOr(120)); }
+
+    /////////// DevicePairingDelegate Interface /////////
+    void OnStatusUpdate(chip::Controller::DevicePairingDelegate::Status status) override;
+    void OnPairingComplete(CHIP_ERROR error) override;
+    void OnPairingDeleted(CHIP_ERROR error) override;
+    void OnReadCommissioningInfo(const chip::Controller::ReadCommissioningInfo & info) override;
+    void OnCommissioningComplete(NodeId deviceId, CHIP_ERROR error) override;
+    void OnICDRegistrationComplete(NodeId deviceId, uint32_t icdCounter) override;
+    void OnICDStayActiveComplete(NodeId deviceId, uint32_t promisedActiveDuration) override;
+
+    /////////// DeviceDiscoveryDelegate Interface /////////
+    void OnDiscoveredDevice(const chip::Dnssd::CommissionNodeData & nodeData) override;
+
+    /////////// DeviceAttestationDelegate /////////
+    chip::Optional<uint16_t> FailSafeExpiryTimeoutSecs() const override;
+    void OnDeviceAttestationCompleted(chip::Controller::DeviceCommissioner * deviceCommissioner, chip::DeviceProxy * device,
+                                      const chip::Credentials::DeviceAttestationVerifier::AttestationDeviceInfo & info,
+                                      chip::Credentials::AttestationVerificationResult attestationResult) override;
+
+private:
+    CHIP_ERROR RunInternal(NodeId remoteId);
+    CHIP_ERROR Pair(NodeId remoteId, PeerAddress address);
+    CHIP_ERROR PairWithMdns(NodeId remoteId);
+    CHIP_ERROR PairWithCode(NodeId remoteId);
+    CHIP_ERROR PaseWithCode(NodeId remoteId);
+    CHIP_ERROR PairWithMdnsOrBleByIndex(NodeId remoteId, uint16_t index);
+    CHIP_ERROR PairWithMdnsOrBleByIndexWithCode(NodeId remoteId, uint16_t index);
+    CHIP_ERROR Unpair(NodeId remoteId);
+    chip::Controller::CommissioningParameters GetCommissioningParameters();
+
+    const PairingMode mPairingMode;
+    const PairingNetworkType mNetworkType;
+    const chip::Dnssd::DiscoveryFilterType mFilterType;
+    Command::AddressWithInterface mRemoteAddr;
+    NodeId mNodeId;
+    chip::Optional<uint16_t> mTimeout;
+    chip::Optional<bool> mDiscoverOnce;
+    chip::Optional<bool> mUseOnlyOnNetworkDiscovery;
+    chip::Optional<bool> mPaseOnly;
+    chip::Optional<bool> mSkipCommissioningComplete;
+    chip::Optional<bool> mBypassAttestationVerifier;
+    chip::Optional<std::vector<uint32_t>> mCASEAuthTags;
+    chip::Optional<char *> mCountryCode;
+    chip::Optional<bool> mICDRegistration;
+    chip::Optional<NodeId> mICDCheckInNodeId;
+    chip::Optional<chip::ByteSpan> mICDSymmetricKey;
+    chip::Optional<uint64_t> mICDMonitoredSubject;
+    chip::Optional<uint32_t> mICDStayActiveDurationMsec;
+    chip::app::DataModel::List<chip::app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type> mTimeZoneList;
+    TypedComplexArgument<chip::app::DataModel::List<chip::app::Clusters::TimeSynchronization::Structs::TimeZoneStruct::Type>>
+        mComplex_TimeZones;
+    chip::app::DataModel::List<chip::app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type> mDSTOffsetList;
+    TypedComplexArgument<chip::app::DataModel::List<chip::app::Clusters::TimeSynchronization::Structs::DSTOffsetStruct::Type>>
+        mComplex_DSTOffsets;
+
+    uint16_t mRemotePort;
+    uint16_t mDiscriminator;
+    uint32_t mSetupPINCode;
+    uint16_t mIndex;
+    chip::ByteSpan mOperationalDataset;
+    chip::ByteSpan mSSID;
+    chip::ByteSpan mPassword;
+    char * mOnboardingPayload;
+    uint64_t mDiscoveryFilterCode;
+    char * mDiscoveryFilterInstanceName;
+
+    bool mDeviceIsICD;
+    uint8_t mRandomGeneratedICDSymmetricKey[chip::Crypto::kAES_CCM128_Key_Length];
+
+    // For unpair
+    chip::Platform::UniquePtr<chip::Controller::CurrentFabricRemover> mCurrentFabricRemover;
+    chip::Callback::Callback<chip::Controller::OnCurrentFabricRemove> mCurrentFabricRemoveCallback;
+
+    static void OnCurrentFabricRemove(void * context, NodeId remoteNodeId, CHIP_ERROR status);
+    void PersistIcdInfo();
+};
diff --git a/examples/fabric-admin/commands/pairing/ToTLVCert.cpp b/examples/fabric-admin/commands/pairing/ToTLVCert.cpp
new file mode 100644
index 0000000..01f9156
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/ToTLVCert.cpp
@@ -0,0 +1,54 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "ToTLVCert.h"
+
+#include <credentials/CHIPCert.h>
+#include <lib/support/Base64.h>
+
+#include <string>
+
+constexpr char kBase64Header[]    = "base64:";
+constexpr size_t kBase64HeaderLen = ArraySize(kBase64Header) - 1;
+
+CHIP_ERROR ToBase64(const chip::ByteSpan & input, std::string & outputAsPrefixedBase64)
+{
+    chip::Platform::ScopedMemoryBuffer<char> base64String;
+    base64String.Alloc(kBase64HeaderLen + BASE64_ENCODED_LEN(input.size()) + 1);
+    VerifyOrReturnError(base64String.Get() != nullptr, CHIP_ERROR_NO_MEMORY);
+
+    auto encodedLen = chip::Base64Encode(input.data(), static_cast<uint16_t>(input.size()), base64String.Get() + kBase64HeaderLen);
+    if (encodedLen)
+    {
+        memcpy(base64String.Get(), kBase64Header, kBase64HeaderLen);
+        encodedLen = static_cast<uint16_t>(encodedLen + kBase64HeaderLen);
+    }
+    base64String.Get()[encodedLen] = '\0';
+    outputAsPrefixedBase64         = std::string(base64String.Get(), encodedLen);
+
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR ToTLVCert(const chip::ByteSpan & derEncodedCertificate, std::string & tlvCertAsPrefixedBase64)
+{
+    uint8_t chipCertBuffer[chip::Credentials::kMaxCHIPCertLength];
+    chip::MutableByteSpan chipCertBytes(chipCertBuffer);
+    ReturnErrorOnFailure(chip::Credentials::ConvertX509CertToChipCert(derEncodedCertificate, chipCertBytes));
+    ReturnErrorOnFailure(ToBase64(chipCertBytes, tlvCertAsPrefixedBase64));
+    return CHIP_NO_ERROR;
+}
diff --git a/examples/fabric-admin/commands/pairing/ToTLVCert.h b/examples/fabric-admin/commands/pairing/ToTLVCert.h
new file mode 100644
index 0000000..2995647
--- /dev/null
+++ b/examples/fabric-admin/commands/pairing/ToTLVCert.h
@@ -0,0 +1,25 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#pragma once
+
+#include <lib/support/Span.h>
+#include <string>
+
+CHIP_ERROR ToBase64(const chip::ByteSpan & input, std::string & outputAsPrefixedBase64);
+CHIP_ERROR ToTLVCert(const chip::ByteSpan & derEncodedCertificate, std::string & tlvCertAsPrefixedBase64);
diff --git a/examples/fabric-admin/fabric-admin.gni b/examples/fabric-admin/fabric-admin.gni
new file mode 100644
index 0000000..021ab77
--- /dev/null
+++ b/examples/fabric-admin/fabric-admin.gni
@@ -0,0 +1,22 @@
+# Copyright (c) 2024 Project CHIP Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("//build_overrides/build.gni")
+import("//build_overrides/chip.gni")
+
+declare_args() {
+  # Use a separate eventloop for CHIP tasks
+  config_use_separate_eventloop = true
+  config_use_local_storage = true
+}
diff --git a/examples/fabric-admin/include/CHIPProjectAppConfig.h b/examples/fabric-admin/include/CHIPProjectAppConfig.h
new file mode 100644
index 0000000..b3f85d6
--- /dev/null
+++ b/examples/fabric-admin/include/CHIPProjectAppConfig.h
@@ -0,0 +1,67 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ */
+
+/**
+ *    @file
+ *      Project configuration for Fabric Admin.
+ *
+ */
+#ifndef CHIPPROJECTCONFIG_H
+#define CHIPPROJECTCONFIG_H
+
+#define CHIP_CONFIG_MAX_FABRICS 17
+
+#define CHIP_CONFIG_EVENT_LOGGING_NUM_EXTERNAL_CALLBACKS 2
+
+// Uncomment this for a large Tunnel MTU.
+// #define CHIP_CONFIG_TUNNEL_INTERFACE_MTU                           (9000)
+
+// Enable support functions for parsing command-line arguments
+#define CHIP_CONFIG_ENABLE_ARG_PARSER 1
+
+// Use a default pairing code if one hasn't been provisioned in flash.
+#define CHIP_DEVICE_CONFIG_USE_TEST_SETUP_PIN_CODE 20202021
+#define CHIP_DEVICE_CONFIG_USE_TEST_SETUP_DISCRIMINATOR 0xF00
+
+// Enable reading DRBG seed data from /dev/(u)random.
+// This is needed for test applications and the CHIP device manager to function
+// properly when CHIP_CONFIG_RNG_IMPLEMENTATION_CHIPDRBG is enabled.
+#define CHIP_CONFIG_DEV_RANDOM_DRBG_SEED 1
+
+// For convenience, Chip Security Test Mode can be enabled and the
+// requirement for authentication in various protocols can be disabled.
+//
+//    WARNING: These options make it possible to circumvent basic Chip security functionality,
+//    including message encryption. Because of this they MUST NEVER BE ENABLED IN PRODUCTION BUILDS.
+//
+#define CHIP_CONFIG_SECURITY_TEST_MODE 0
+
+#define CHIP_CONFIG_ENABLE_UPDATE 1
+
+#define CHIP_SYSTEM_CONFIG_PACKETBUFFER_POOL_SIZE 0
+
+#define CHIP_CONFIG_DATA_MANAGEMENT_CLIENT_EXPERIMENTAL 1
+
+#define CHIP_DEVICE_CONFIG_ENABLE_COMMISSIONER_DISCOVERY 1
+
+// Enable some test-only interaction model APIs.
+#define CONFIG_BUILD_FOR_HOST_UNIT_TEST 1
+
+// Allow us, for test purposes, to encode invalid enum values.
+#define CHIP_CONFIG_IM_ENABLE_ENCODING_SENTINEL_ENUM_VALUES 1
+
+#endif /* CHIPPROJECTCONFIG_H */
diff --git a/examples/fabric-admin/main.cpp b/examples/fabric-admin/main.cpp
new file mode 100644
index 0000000..e517c67
--- /dev/null
+++ b/examples/fabric-admin/main.cpp
@@ -0,0 +1,40 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   Licensed under the Apache License, Version 2.0 (the "License");
+ *   you may not use this file except in compliance with the License.
+ *   You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ *
+ */
+
+#include "commands/common/Commands.h"
+
+#include "commands/clusters/SubscriptionsCommands.h"
+#include "commands/interactive/Commands.h"
+#include "commands/pairing/Commands.h"
+#include <zap-generated/cluster/Commands.h>
+
+// ================================================================================
+// Main Code
+// ================================================================================
+int main(int argc, char * argv[])
+{
+    ExampleCredentialIssuerCommands credIssuerCommands;
+    Commands commands;
+
+    registerCommandsInteractive(commands, &credIssuerCommands);
+    registerCommandsPairing(commands, &credIssuerCommands);
+    registerClusters(commands, &credIssuerCommands);
+    registerCommandsSubscriptions(commands, &credIssuerCommands);
+
+    return commands.Run(argc, argv);
+}
diff --git a/examples/fabric-admin/third_party/connectedhomeip b/examples/fabric-admin/third_party/connectedhomeip
new file mode 120000
index 0000000..1b20c9f
--- /dev/null
+++ b/examples/fabric-admin/third_party/connectedhomeip
@@ -0,0 +1 @@
+../../../
\ No newline at end of file