[Fabric-Sync] Add 'add-device' and 'remove-device' commands (#36390)

diff --git a/examples/fabric-sync/admin/DeviceManager.cpp b/examples/fabric-sync/admin/DeviceManager.cpp
index 3d20554..814b7f3 100644
--- a/examples/fabric-sync/admin/DeviceManager.cpp
+++ b/examples/fabric-sync/admin/DeviceManager.cpp
@@ -79,6 +79,37 @@
     return CHIP_NO_ERROR;
 }
 
+CHIP_ERROR DeviceManager::PairRemoteDevice(NodeId nodeId, const char * payload)
+{
+    CHIP_ERROR err = PairingManager::Instance().PairDeviceWithCode(nodeId, payload);
+
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(NotSpecified, "Failed to pair device: Node ID " ChipLogFormatX64 " with error: %" CHIP_ERROR_FORMAT,
+                     ChipLogValueX64(nodeId), err.Format());
+        return err;
+    }
+
+    ChipLogProgress(NotSpecified, "Successfully paired device: Node ID " ChipLogFormatX64, ChipLogValueX64(nodeId));
+    return CHIP_NO_ERROR;
+}
+
+CHIP_ERROR DeviceManager::PairRemoteDevice(NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp,
+                                           uint16_t deviceRemotePort)
+{
+    CHIP_ERROR err = PairingManager::Instance().PairDevice(nodeId, setupPINCode, deviceRemoteIp, deviceRemotePort);
+
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(NotSpecified, "Failed to pair device: Node ID " ChipLogFormatX64 " with error: %" CHIP_ERROR_FORMAT,
+                     ChipLogValueX64(nodeId), err.Format());
+        return err;
+    }
+
+    ChipLogProgress(NotSpecified, "Successfully paired device: Node ID " ChipLogFormatX64, ChipLogValueX64(nodeId));
+    return CHIP_NO_ERROR;
+}
+
 CHIP_ERROR DeviceManager::UnpairRemoteFabricBridge()
 {
     if (mRemoteBridgeNodeId == kUndefinedNodeId)
@@ -99,6 +130,19 @@
     return CHIP_NO_ERROR;
 }
 
+CHIP_ERROR DeviceManager::UnpairRemoteDevice(NodeId nodeId)
+{
+    CHIP_ERROR err = PairingManager::Instance().UnpairDevice(nodeId);
+    if (err != CHIP_NO_ERROR)
+    {
+        ChipLogError(NotSpecified, "Failed to unpair remote device " ChipLogFormatX64, ChipLogValueX64(nodeId));
+        return err;
+    }
+
+    ChipLogProgress(NotSpecified, "Successfully unpaired remote device: Node ID " ChipLogFormatX64, ChipLogValueX64(nodeId));
+    return CHIP_NO_ERROR;
+}
+
 void DeviceManager::OnDeviceRemoved(NodeId deviceId, CHIP_ERROR err)
 {
     if (err != CHIP_NO_ERROR)
diff --git a/examples/fabric-sync/admin/DeviceManager.h b/examples/fabric-sync/admin/DeviceManager.h
index 866e0c2..57a3bd3 100644
--- a/examples/fabric-sync/admin/DeviceManager.h
+++ b/examples/fabric-sync/admin/DeviceManager.h
@@ -67,8 +67,55 @@
     CHIP_ERROR PairRemoteFabricBridge(chip::NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp,
                                       uint16_t deviceRemotePort);
 
+    /**
+     * @brief Pair a remote Matter device to the current fabric.
+     *
+     * This function initiates the pairing process for a remote device using the specified parameters.
+
+     * @param nodeId            The user-defined ID for the node being commissioned. It doesn’t need to be the same ID,
+     *                          as for the first fabric.
+     * @param payload           The the QR code payload or a manual pairing code generated by the first commissioner
+     *                          instance when opened commissioning window.
+     *
+     * @return CHIP_ERROR       Returns CHIP_NO_ERROR on success or an appropriate error code on failure.
+     */
+    CHIP_ERROR PairRemoteDevice(chip::NodeId nodeId, const char * payload);
+
+    /**
+     * @brief Pair a remote Matter device to the current fabric.
+     *
+     * This function initiates the pairing process for a remote device using the specified parameters.
+
+     * @param nodeId            The user-defined ID for the node being commissioned. It doesn’t need to be the same ID,
+     *                          as for the first fabric.
+     * @param setupPINCode      The setup PIN code used to authenticate the pairing process.
+     * @param deviceRemoteIp    The IP address of the remote device that is being paired as part of the fabric bridge.
+     * @param deviceRemotePort  The secured device port of the remote device that is being paired as part of the fabric bridge.
+     *
+     * @return CHIP_ERROR       Returns CHIP_NO_ERROR on success or an appropriate error code on failure.
+     */
+    CHIP_ERROR PairRemoteDevice(chip::NodeId nodeId, uint32_t setupPINCode, const char * deviceRemoteIp, uint16_t deviceRemotePort);
+
+    /**
+     * @brief Unpair the remote Matter fabric bridge.
+     *
+     * This function initiates the unpairing process for the remote Matter fabric bridge from the current fabric.
+     *
+     * @return CHIP_ERROR Returns CHIP_NO_ERROR on success or an appropriate error code on failure.
+     */
     CHIP_ERROR UnpairRemoteFabricBridge();
 
+    /**
+     * @brief Unpair a specific remote Matter device from the current fabric.
+     *
+     * This function removes a specific remote device, identified by the node ID, from the fabric.
+     *
+     * @param nodeId The user-defined ID of the node that is being unpaired.
+     *
+     * @return CHIP_ERROR Returns CHIP_NO_ERROR on success or an appropriate error code on failure.
+     */
+    CHIP_ERROR UnpairRemoteDevice(chip::NodeId nodeId);
+
 private:
     friend DeviceManager & DeviceMgr();
 
diff --git a/examples/fabric-sync/admin/PairingManager.cpp b/examples/fabric-sync/admin/PairingManager.cpp
index 32317b4..6b5f707 100644
--- a/examples/fabric-sync/admin/PairingManager.cpp
+++ b/examples/fabric-sync/admin/PairingManager.cpp
@@ -290,10 +290,10 @@
         ChipLogProgress(NotSpecified, "Device commissioning Failure: %s", ErrorStr(err));
     }
 
-    if (mCommissioningDelegate)
+    if (mPairingDelegate)
     {
-        mCommissioningDelegate->OnCommissioningComplete(nodeId, err);
-        SetCommissioningDelegate(nullptr);
+        mPairingDelegate->OnCommissioningComplete(nodeId, err);
+        SetPairingDelegate(nullptr);
     }
 }
 
@@ -454,6 +454,12 @@
     {
         // print to console
         fprintf(stderr, "Device with Node ID: " ChipLogFormatX64 " has been successfully removed.\n", ChipLogValueX64(nodeId));
+
+        if (self->mPairingDelegate)
+        {
+            self->mPairingDelegate->OnDeviceRemoved(nodeId, err);
+            self->SetPairingDelegate(nullptr);
+        }
     }
     else
     {
diff --git a/examples/fabric-sync/admin/PairingManager.h b/examples/fabric-sync/admin/PairingManager.h
index 5ef8a07..b9b404e 100644
--- a/examples/fabric-sync/admin/PairingManager.h
+++ b/examples/fabric-sync/admin/PairingManager.h
@@ -36,18 +36,12 @@
     virtual ~CommissioningWindowDelegate()                                                                      = default;
 };
 
-class CommissioningDelegate
-{
-public:
-    virtual void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR err) = 0;
-    virtual ~CommissioningDelegate()                                            = default;
-};
-
 class PairingDelegate
 {
 public:
-    virtual void OnDeviceRemoved(chip::NodeId deviceId, CHIP_ERROR err) = 0;
-    virtual ~PairingDelegate()                                          = default;
+    virtual void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR err) {}
+    virtual void OnDeviceRemoved(chip::NodeId deviceId, CHIP_ERROR err) {}
+    virtual ~PairingDelegate() = default;
 };
 
 /**
@@ -84,7 +78,6 @@
     CHIP_ERROR Init(chip::Controller::DeviceCommissioner * commissioner);
 
     void SetOpenCommissioningWindowDelegate(CommissioningWindowDelegate * delegate) { mCommissioningWindowDelegate = delegate; }
-    void SetCommissioningDelegate(CommissioningDelegate * delegate) { mCommissioningDelegate = delegate; }
     void SetPairingDelegate(PairingDelegate * delegate) { mPairingDelegate = delegate; }
     PairingDelegate * GetPairingDelegate() { return mPairingDelegate; }
 
@@ -177,7 +170,6 @@
     chip::Controller::DeviceCommissioner * mCommissioner = nullptr;
 
     CommissioningWindowDelegate * mCommissioningWindowDelegate = nullptr;
-    CommissioningDelegate * mCommissioningDelegate             = nullptr;
     PairingDelegate * mPairingDelegate                         = nullptr;
 
     chip::NodeId mNodeId = chip::kUndefinedNodeId;
diff --git a/examples/fabric-sync/bridge/include/CHIPProjectAppConfig.h b/examples/fabric-sync/bridge/include/CHIPProjectAppConfig.h
index b1a7116..58f45c0 100644
--- a/examples/fabric-sync/bridge/include/CHIPProjectAppConfig.h
+++ b/examples/fabric-sync/bridge/include/CHIPProjectAppConfig.h
@@ -36,6 +36,9 @@
 // FabricSync needs to be both commissioners and commissionees
 #define CHIP_DEVICE_CONFIG_ENABLE_BOTH_COMMISSIONER_AND_COMMISSIONEE 1
 
+// See issue 23625.
+#define CHIP_CONFIG_UNSAFE_SUBSCRIPTION_EXCHANGE_MANAGER_USE 1
+
 // Enable app platform
 #define CHIP_DEVICE_CONFIG_APP_PLATFORM_ENABLED 1
 
diff --git a/examples/fabric-sync/shell/AddBridgeCommand.cpp b/examples/fabric-sync/shell/AddBridgeCommand.cpp
index 09246c1..936ac1c 100644
--- a/examples/fabric-sync/shell/AddBridgeCommand.cpp
+++ b/examples/fabric-sync/shell/AddBridgeCommand.cpp
@@ -73,7 +73,7 @@
         return CHIP_NO_ERROR;
     }
 
-    admin::PairingManager::Instance().SetCommissioningDelegate(this);
+    admin::PairingManager::Instance().SetPairingDelegate(this);
 
     return admin::DeviceMgr().PairRemoteFabricBridge(mBridgeNodeId, mSetupPINCode, mRemoteAddr, mRemotePort);
 }
diff --git a/examples/fabric-sync/shell/AddBridgeCommand.h b/examples/fabric-sync/shell/AddBridgeCommand.h
index 48c1830..d2f6ff7 100644
--- a/examples/fabric-sync/shell/AddBridgeCommand.h
+++ b/examples/fabric-sync/shell/AddBridgeCommand.h
@@ -23,7 +23,7 @@
 
 namespace commands {
 
-class AddBridgeCommand : public Command, public admin::CommissioningDelegate
+class AddBridgeCommand : public Command, public admin::PairingDelegate
 {
 public:
     AddBridgeCommand(chip::NodeId nodeId, uint32_t setupPINCode, const char * remoteAddr, uint16_t remotePort);
diff --git a/examples/fabric-sync/shell/AddDeviceCommand.cpp b/examples/fabric-sync/shell/AddDeviceCommand.cpp
new file mode 100644
index 0000000..218691e
--- /dev/null
+++ b/examples/fabric-sync/shell/AddDeviceCommand.cpp
@@ -0,0 +1,79 @@
+/*
+ *   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 "AddDeviceCommand.h"
+
+#include <admin/DeviceManager.h>
+#include <lib/shell/streamer.h>
+
+using namespace ::chip;
+
+namespace commands {
+
+AddDeviceCommand::AddDeviceCommand(chip::NodeId nodeId, uint32_t setupPINCode, const char * remoteAddr, uint16_t remotePort) :
+    mNodeId(nodeId), mSetupPINCode(setupPINCode), mRemoteAddr(remoteAddr), mRemotePort(remotePort)
+{}
+
+void AddDeviceCommand::OnCommissioningComplete(NodeId deviceId, CHIP_ERROR err)
+{
+    if (mNodeId != deviceId)
+    {
+        if (err != CHIP_NO_ERROR)
+        {
+            ChipLogError(NotSpecified,
+                         "Failed to pair non-specified device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+                         ChipLogValueX64(deviceId), err.Format());
+        }
+        else
+        {
+            ChipLogProgress(NotSpecified, "Commissioning complete for non-specified device: NodeId: " ChipLogFormatX64,
+                            ChipLogValueX64(deviceId));
+        }
+        return;
+    }
+
+    if (err == CHIP_NO_ERROR)
+    {
+        ChipLogProgress(NotSpecified, "Successfully paired device: NodeId: " ChipLogFormatX64, ChipLogValueX64(mNodeId));
+
+        admin::DeviceMgr().UpdateLastUsedNodeId(mNodeId);
+    }
+    else
+    {
+        ChipLogError(NotSpecified, "Failed to pair device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+                     ChipLogValueX64(deviceId), err.Format());
+    }
+
+    CommandRegistry::Instance().ResetActiveCommand();
+}
+
+CHIP_ERROR AddDeviceCommand::RunCommand()
+{
+    if (admin::DeviceMgr().IsCurrentBridgeDevice(mNodeId))
+    {
+        // print to console
+        fprintf(stderr, "The specified node ID has been reserved by the Fabric Bridge.\n");
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    admin::PairingManager::Instance().SetPairingDelegate(this);
+
+    return admin::DeviceMgr().PairRemoteDevice(mNodeId, mSetupPINCode, mRemoteAddr, mRemotePort);
+}
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/AddDeviceCommand.h b/examples/fabric-sync/shell/AddDeviceCommand.h
new file mode 100644
index 0000000..35d0ed9
--- /dev/null
+++ b/examples/fabric-sync/shell/AddDeviceCommand.h
@@ -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.
+ *
+ */
+
+#pragma once
+
+#include <CommandRegistry.h>
+#include <admin/PairingManager.h>
+
+namespace commands {
+
+class AddDeviceCommand : public Command, public admin::PairingDelegate
+{
+public:
+    AddDeviceCommand(chip::NodeId nodeId, uint32_t setupPINCode, const char * remoteAddr, uint16_t remotePort);
+    void OnCommissioningComplete(chip::NodeId deviceId, CHIP_ERROR err) override;
+    CHIP_ERROR RunCommand() override;
+
+private:
+    chip::NodeId mNodeId;
+    uint32_t mSetupPINCode;
+    const char * mRemoteAddr;
+    uint16_t mRemotePort;
+};
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/BUILD.gn b/examples/fabric-sync/shell/BUILD.gn
index 6f64b91..b3726ee 100644
--- a/examples/fabric-sync/shell/BUILD.gn
+++ b/examples/fabric-sync/shell/BUILD.gn
@@ -29,9 +29,13 @@
   sources = [
     "AddBridgeCommand.cpp",
     "AddBridgeCommand.h",
+    "AddDeviceCommand.cpp",
+    "AddDeviceCommand.h",
     "CommandRegistry.h",
     "RemoveBridgeCommand.cpp",
     "RemoveBridgeCommand.h",
+    "RemoveDeviceCommand.cpp",
+    "RemoveDeviceCommand.h",
     "ShellCommands.cpp",
     "ShellCommands.h",
   ]
diff --git a/examples/fabric-sync/shell/RemoveDeviceCommand.cpp b/examples/fabric-sync/shell/RemoveDeviceCommand.cpp
new file mode 100644
index 0000000..266a2ad
--- /dev/null
+++ b/examples/fabric-sync/shell/RemoveDeviceCommand.cpp
@@ -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.
+ *
+ */
+
+#include "RemoveDeviceCommand.h"
+
+#include <admin/DeviceManager.h>
+#include <lib/shell/streamer.h>
+
+using namespace ::chip;
+
+namespace commands {
+
+RemoveDeviceCommand::RemoveDeviceCommand(NodeId nodeId) : mNodeId(nodeId) {}
+
+void RemoveDeviceCommand::OnDeviceRemoved(NodeId deviceId, CHIP_ERROR err)
+{
+    if (mNodeId != deviceId)
+    {
+        ChipLogProgress(NotSpecified, "An non-specified device: NodeId: " ChipLogFormatX64 " is removed.",
+                        ChipLogValueX64(deviceId));
+        return;
+    }
+
+    if (err == CHIP_NO_ERROR)
+    {
+        // print to console
+        fprintf(stderr, "Successfully removed device: NodeId: " ChipLogFormatX64 "\n", ChipLogValueX64(mNodeId));
+    }
+    else
+    {
+        ChipLogError(NotSpecified, "Failed to remove device (0x:" ChipLogFormatX64 ") with error: %" CHIP_ERROR_FORMAT,
+                     ChipLogValueX64(deviceId), err.Format());
+    }
+
+    CommandRegistry::Instance().ResetActiveCommand();
+}
+
+CHIP_ERROR RemoveDeviceCommand::RunCommand()
+{
+    if (admin::DeviceMgr().IsCurrentBridgeDevice(mNodeId))
+    {
+        // print to console
+        fprintf(stderr, "The specified node ID has been reserved by the Fabric Bridge.\n");
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    admin::PairingManager::Instance().SetPairingDelegate(this);
+
+    return admin::DeviceMgr().UnpairRemoteDevice(mNodeId);
+}
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/RemoveDeviceCommand.h b/examples/fabric-sync/shell/RemoveDeviceCommand.h
new file mode 100644
index 0000000..cb394d6
--- /dev/null
+++ b/examples/fabric-sync/shell/RemoveDeviceCommand.h
@@ -0,0 +1,37 @@
+/*
+ *   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 <CommandRegistry.h>
+#include <admin/PairingManager.h>
+
+namespace commands {
+
+class RemoveDeviceCommand : public Command, public admin::PairingDelegate
+{
+public:
+    RemoveDeviceCommand(chip::NodeId nodeId);
+    void OnDeviceRemoved(chip::NodeId deviceId, CHIP_ERROR err) override;
+    CHIP_ERROR RunCommand() override;
+
+private:
+    chip::NodeId mNodeId = chip::kUndefinedNodeId;
+};
+
+} // namespace commands
diff --git a/examples/fabric-sync/shell/ShellCommands.cpp b/examples/fabric-sync/shell/ShellCommands.cpp
index 1edbd43..a654fbc 100644
--- a/examples/fabric-sync/shell/ShellCommands.cpp
+++ b/examples/fabric-sync/shell/ShellCommands.cpp
@@ -17,7 +17,9 @@
 
 #include "ShellCommands.h"
 #include "AddBridgeCommand.h"
+#include "AddDeviceCommand.h"
 #include "RemoveBridgeCommand.h"
+#include "RemoveDeviceCommand.h"
 
 #include <admin/DeviceManager.h>
 #include <inttypes.h>
@@ -41,6 +43,10 @@
                     "  add-bridge       Pair remote fabric bridge to local fabric. Usage: app add-bridge node-id setup-pin-code "
                     "device-remote-ip device-remote-port\r\n");
     streamer_printf(sout, "  remove-bridge    Remove the remote fabric bridge from the local fabric. Usage: app remove-bridge\r\n");
+    streamer_printf(sout,
+                    "  add-device       Pair a device to local fabric. Usage: app add-device node-id setup-pin-code "
+                    "device-remote-ip device-remote-port\r\n");
+    streamer_printf(sout, "  remove-device    Remove a device from the local fabric. Usage: app remove-device node-id\r\n");
     streamer_printf(sout, "  sync-device      Sync a device from other ecosystem. Usage: app sync-device endpointid\r\n");
     streamer_printf(sout, "\r\n");
 
@@ -106,6 +112,68 @@
     return result;
 }
 
+static CHIP_ERROR HandleAddDeviceCommand(int argc, char ** argv)
+{
+    if (argc != 5)
+    {
+        fprintf(stderr,
+                "Invalid arguments. Usage: app add-device <node-id> <setup-pin-code> <device-remote-ip> <device-remote-port>\n");
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    // Check if there is already an active command
+    if (commands::CommandRegistry::Instance().IsCommandActive())
+    {
+        fprintf(stderr, "Another command is currently active. Please wait until it completes.\n");
+        return CHIP_ERROR_BUSY;
+    }
+
+    // Parse arguments
+    chip::NodeId nodeId     = static_cast<chip::NodeId>(strtoull(argv[1], nullptr, 10));
+    uint32_t setupPINCode   = static_cast<uint32_t>(strtoul(argv[2], nullptr, 10));
+    const char * remoteAddr = argv[3];
+    uint16_t remotePort     = static_cast<uint16_t>(strtoul(argv[4], nullptr, 10));
+
+    auto command = std::make_unique<commands::AddBridgeCommand>(nodeId, setupPINCode, remoteAddr, remotePort);
+
+    CHIP_ERROR result = command->RunCommand();
+    if (result == CHIP_NO_ERROR)
+    {
+        commands::CommandRegistry::Instance().SetActiveCommand(std::move(command));
+    }
+
+    return result;
+}
+
+static CHIP_ERROR HandleRemoveDeviceCommand(int argc, char ** argv)
+{
+    if (argc != 2)
+    {
+        fprintf(stderr, "Invalid arguments. Usage: app remove-device <node-id>\n");
+        return CHIP_ERROR_INVALID_ARGUMENT;
+    }
+
+    // Check if there is already an active command
+    if (commands::CommandRegistry::Instance().IsCommandActive())
+    {
+        fprintf(stderr, "Another command is currently active. Please wait until it completes.\n");
+        return CHIP_ERROR_BUSY;
+    }
+
+    // Parse arguments
+    chip::NodeId nodeId = static_cast<chip::NodeId>(strtoull(argv[1], nullptr, 10));
+
+    auto command = std::make_unique<commands::RemoveDeviceCommand>(nodeId);
+
+    CHIP_ERROR result = command->RunCommand();
+    if (result == CHIP_NO_ERROR)
+    {
+        commands::CommandRegistry::Instance().SetActiveCommand(std::move(command));
+    }
+
+    return result;
+}
+
 static CHIP_ERROR AppPlatformHandler(int argc, char ** argv)
 {
     CHIP_ERROR error = CHIP_NO_ERROR;
@@ -126,6 +194,14 @@
     {
         return HandleRemoveBridgeCommand(argc, argv);
     }
+    else if (strcmp(argv[0], "add-device") == 0)
+    {
+        return HandleAddDeviceCommand(argc, argv);
+    }
+    else if (strcmp(argv[0], "remove-device") == 0)
+    {
+        return HandleRemoveDeviceCommand(argc, argv);
+    }
     else
     {
         return CHIP_ERROR_INVALID_ARGUMENT;