{"id":1892,"date":"2025-07-13T23:15:31","date_gmt":"2025-07-13T23:15:31","guid":{"rendered":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/"},"modified":"2025-07-13T23:15:31","modified_gmt":"2025-07-13T23:15:31","slug":"how-to-implement-a-decentralised-cli-tool-manager","status":"publish","type":"post","link":"https:\/\/dev95.site\/ar\/how-to-implement-a-decentralised-cli-tool-manager\/","title":{"rendered":"How to Implement a Decentralised CLI Tool Manager"},"content":{"rendered":"<div id=\"dev95-1664837666\" class=\"dev95-- dev95-entity-placement\"><script async=\"async\" data-cfasync=\"false\" src=\"https:\/\/pl27862732.profitableratecpmnetwork.com\/2ad7a50e0bbc23ac6801d7b77c501463\/invoke.js\"><\/script>\r\n<div id=\"container-2ad7a50e0bbc23ac6801d7b77c501463\"><\/div><\/div><div>\n<div class=\"kg-card kg-cta-card kg-cta-bg-grey kg-cta-minimal    \" data-layout=\"minimal\">\n<div class=\"kg-cta-sponsor-label-wrapper\">\n<div class=\"kg-cta-sponsor-label\">\n                        <span style=\"white-space: pre-wrap;\">SPONSORED<\/span>\n                    <\/div>\n<\/p><\/div>\n<div class=\"kg-cta-content\">\n<div class=\"kg-cta-content-inner\">\n<div class=\"kg-cta-text\">\n                            <img data-recalc-dims=\"1\" decoding=\"async\" src=\"https:\/\/i0.wp.com\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2025\/07\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png?w=1280&#038;ssl=1\" alt=\"How to Implement a Decentralised CLI Tool Manager\"><\/p>\n<p dir=\"ltr\"><span style=\"white-space: pre-wrap;\">Based on this article, I&#8217;ve published Luca!<\/span><br \/><span style=\"white-space: pre-wrap;\">A lightweight decentralised tool manager for macOS to manage project-specific tool environments.<\/span><\/p>\n<p><span style=\"white-space: pre-wrap;\">\ud83d\udc76 Check it out:\u00a0<\/span><a href=\"https:\/\/luca.tools\/?ref=albertodebortoli.com\" rel=\"noreferrer\" class=\"cta-link-color\"><span style=\"white-space: pre-wrap;\">luca.tools<\/span><\/a><\/p>\n<\/p><div id=\"dev95-1399893478\" class=\"dev95- dev95-entity-placement\"><center>\r\n<script>\r\n  atOptions = {\r\n    'key' : '4ba6b6513c00e0ba76511f798ae56401',\r\n    'format' : 'iframe',\r\n    'height' : 50,\r\n    'width' : 320,\r\n    'params' : {}\r\n  };\r\n<\/script>\r\n<script src=\"https:\/\/www.highrevenueformat.com\/4ba6b6513c00e0ba76511f798ae56401\/invoke.js\"><\/script>\r\n\t<\/center><\/div><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<h2 id=\"overview\">Overview<\/h2>\n<p>It&#8217;s common for iOS teams to rely on various CLI tools such as SwiftLint, Tuist, and Fastlane. These tools are often installed in different ways. The most common way is to use <a href=\"https:\/\/brew.sh\/?ref=albertodebortoli.com\">Homebrew<\/a>, which is known to lack version pinning and, as Pedro <a href=\"https:\/\/tuist.dev\/blog\/2023\/12\/15\/rtx-default\/?ref=albertodebortoli.com\">puts it<\/a>:<\/p>\n<blockquote><p>Homebrew is not able to install and activate multiple versions of the same tool<\/p><\/blockquote>\n<p>I also fundamentally dislike the <code>tap<\/code> system for installing dependencies from third-party repositories. Although I don&#8217;t have concrete data, I feel that most development teams profoundly dislike Homebrew when used beyond the simple installation of individual tools from the command line and the <a href=\"https:\/\/docs.brew.sh\/Taps?ref=albertodebortoli.com\">brew taps<\/a> system is cumbersome and bizarre enough to often discourage developers from using it.<\/p>\n<p>Alternatives to manage sets of CLI tools that got traction in the past couple of years are <a href=\"https:\/\/github.com\/yonaskolb\/Mint?ref=albertodebortoli.com\">Mint<\/a> and <a href=\"https:\/\/mise.jdx.dev\/lang\/swift.html?ref=albertodebortoli.com\">Mise<\/a>. As Pedro again says in <a href=\"https:\/\/tuist.dev\/blog\/2025\/02\/04\/mise?ref=albertodebortoli.com\">his article about Mise<\/a>:<\/p>\n<blockquote><p>The first and most core feature of Mise is the ability to install and activate\u00a0<a href=\"https:\/\/mise.jdx.dev\/dev-tools\/?ref=albertodebortoli.com\">dev tools<\/a>. Note that we say &#8220;activate&#8221; because, unlike Homebrew, Mise differentiates between installing a tool and making a specific version of it available.\u00a0<\/p><\/blockquote>\n<p>While beyond the scope of this article, I recommend a great article about <a href=\"https:\/\/swifttoolkit.dev\/posts\/mise-swift?ref=albertodebortoli.com\">installing Swift executables from source with Mise<\/a> by  <a href=\"https:\/\/x.com\/natanrolnik?ref=albertodebortoli.com\">Natan Rolnik<\/a>.<\/p>\n<p>In this article I describe a CLI tool manager very similar to what I&#8217;ve implemented for my team. I&#8217;ll simply call it &#8220;ToolManager&#8221;. The tool is designed to:<\/p>\n<ol>\n<li>Support installing any external CLI tool distributed in zip archives<\/li>\n<li>Support activating specific versions per project<\/li>\n<li>Be decentralised (requiring no registry)<\/li>\n<\/ol>\n<p>I believe the decentralisation is an interesting aspect and makes the tool reusable in any development environment. Also, differently from the design of mise and mint, ToolManager doesn&#8217;t build from source and rather relies on pre-built executables.<\/p>\n<p>In the age of GenAI, it&#8217;s more important than ever to develop critical thinking and learn how to solve problems. For this reason, I won&#8217;t show the implementation of ToolManager, as it&#8217;s more important to understand how it&#8217;s meant to work. The code you&#8217;ll see in this article supports the overarching design, not the nitty-gritty details of how ToolManager&#8217;s commands are implemented.<\/p>\n<p>If, by the end of the article, you understand how the system should work and are interested in implementing it (perhaps using GenAI), you should be able to convert the design to code fairly easily\u2014hopefully, without losing the <a href=\"https:\/\/annievella.com\/posts\/the-software-engineering-identity-crisis\/?ref=albertodebortoli.com\">joy of coding<\/a>.<\/p>\n<p>I myself am considering implementing ToolManager as an open source project later, as I believe it might be very helpful to many teams, just as its incarnation was (and continues to be) for the platform team at JET. There doesn&#8217;t seem to be an existing tool with the design described in this article.<\/p>\n<p>A different title could have reasonably placed this article in &#8220;The easiest X&#8221; &#8220;series&#8221; (<a href=\"https:\/\/albertodebortoli.com\/2023\/03\/26\/toggles\/\">1<\/a>, <a href=\"https:\/\/albertodebortoli.com\/2018\/02\/12\/the-easiest-promises-in-swift\/\">2<\/a>, <a href=\"https:\/\/albertodebortoli.com\/2018\/12\/16\/the-easiest-state-machine-in-swift\/\">3<\/a>, <a href=\"https:\/\/albertodebortoli.com\/2016\/08\/05\/the-easiest-core-data\/\">4<\/a>), if I may say so.<\/p>\n<h2 id=\"design\">Design<\/h2>\n<p>The point here is to learn what implementing a tool manager entails. I&#8217;ll therefore describe the MVP of ToolManager, leaving out details that would make the design too straightforward to implement.<\/p>\n<p>The tool itself is a CLI and it&#8217;s reasonably implemented in Swift using <a href=\"https:\/\/github.com\/apple\/swift-argument-parser?ref=albertodebortoli.com\">ArgumentParser<\/a> like all modern Swift CLI tools are.<\/p>\n<p>In its simplest form, ToolManager exposes 3 commands:<\/p>\n<ul>\n<li><code>install<\/code>:\n<ul>\n<li>download and installs the tools defined in a spec file (<code>Toolfile.yml<\/code>) at\u00a0<code>~\/.toolManager\/tools<\/code> optionally validating the checksum<\/li>\n<li>creates symlinks to the installed versions at\u00a0<code>$(PWD)\/.toolManager\/active<\/code><\/li>\n<\/ul>\n<\/li>\n<li><code>uninstall<\/code>:\n<ul>\n<li>clears the entire or partial content of\u00a0<code>~\/.toolManager\/tools<\/code>\u00a0<\/li>\n<\/ul>\n<\/li>\n<ul>\n<li>clears the content of\u00a0<code>$(PWD)\/.toolManager\/active<\/code><\/li>\n<\/ul>\n<li><code>version<\/code>:\n<ul>\n<li>returns the version of the tool<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>The\u00a0<code>install<\/code>\u00a0commands allows to specify the location of the spec file using the\u00a0<code>--spec<\/code>\u00a0flag, which defaults to\u00a0<code>Toolfile.yml<\/code>\u00a0in the current directory.<\/p>\n<p>The installation of ToolManager should be done in the most raw way, i.e. via a remote script. It&#8217;d be quite laughable to rely on Brew, wouldn&#8217;t it?<\/p>\n<p>This practice is commonly used by a variety of tools, for example originally by Tuist (before the introduction of Mise) and&#8230; you guessed it&#8230; by Brew. We&#8217;ll see below a basic script to achieve so that you could host on something lik AWS S3 with the desired public permissions.<\/p>\n<p>The installation command would be:<\/p>\n<pre><code class=\"language-bash\">curl -Ls 'https:\/\/my-bucket.s3.eu-west-1.amazonaws.com\/install_toolmanager.sh' | bash<\/code><\/pre>\n<p>The version of ToolManager must be defined in the\u00a0<code>.toolmanager-version<\/code>\u00a0file in order for the installation script of the repo to work:<\/p>\n<pre><code class=\"language-bash\">echo \"1.2.0\" &gt; .toolmanager-version<\/code><\/pre>\n<p>ToolManager manages versions of CLI tools but it&#8217;s not in the business of managing its own versions. Back in the day, Tuist used to use <a href=\"https:\/\/tuist.dev\/blog\/2023\/12\/15\/rtx-default?ref=albertodebortoli.com\">tuistenv<\/a> to solve this problem. I simply avoid it and have single version of ToolManager available at <code>\/usr\/local\/bin\/<\/code> that the installation script overrides with the version defined for the project. The <code>version<\/code> command is used by the script to decide if a download is needed.<\/p>\n<blockquote><p>There will be only one version of ToolManager in the system at a given time, and that&#8217;s absolutely OK.<\/p><\/blockquote>\n<p>At this point, it&#8217;s time to show an example of installation script:<\/p>\n<pre><code class=\"language-bash\">#!\/bin\/bash\nset -euo pipefail\n\n# Fail fast if essential commands are missing.\ncommand -v curl &gt;\/dev\/null || { echo \"curl not found, please install it.\"; exit 1; }\ncommand -v unzip &gt;\/dev\/null || { echo \"unzip not found, please install it.\"; exit 1; }\n\nreadonly EXEC_NAME=\"ToolManager\"\nreadonly INSTALL_DIR=\"\/usr\/local\/bin\"\nreadonly EXEC_PATH=\"$INSTALL_DIR\/$EXEC_NAME\"\nreadonly HOOK_DIR=\"$HOME\/.toolManager\"\nreadonly REQUIRED_VERSION=$(cat .toolmanager-version)\n\n# Exit if the version file is missing or empty.\nif [[ -z \"$REQUIRED_VERSION\" ]]; then\n  echo \"Error: .toolmanager-version not found or is empty.\" &gt;&amp;2\n  exit 1\nfi\n\n# Exit if the tool is already installed and up to date.\nif [[ -f \"$EXEC_PATH\" ]] &amp;&amp; [[ \"$($EXEC_PATH version)\" == \"$REQUIRED_VERSION\" ]]; then\n  echo \"$EXEC_NAME version $REQUIRED_VERSION is already installed.\"\n  exit 0\nfi\n\n# Determine OS and the corresponding zip filename.\ncase \"$(uname -s)\" in\n  Darwin) ZIP_FILENAME=\"$EXEC_NAME-macOS.zip\" ;;\n  Linux)  ZIP_FILENAME=\"$EXEC_NAME-Linux.zip\" ;;\n  *)      echo \"Unsupported OS: $(uname -s)\" &gt;&amp;2; exit 1 ;;\nesac\n\n# Download and install in a temporary directory.\nTMP_DIR=$(mktemp -d)\ntrap 'rm -rf \"$TMP_DIR\"' EXIT # Ensure cleanup on script exit.\n\necho \"Downloading $EXEC_NAME ($REQUIRED_VERSION)...\"\nDOWNLOAD_URL=\"https:\/\/github.com\/MyOrg\/$EXEC_NAME\/releases\/download\/$REQUIRED_VERSION\/$ZIP_FILENAME\"\ncurl -LSsf --output \"$TMP_DIR\/$ZIP_FILENAME\" \"$DOWNLOAD_URL\"\nunzip -o -qq \"$TMP_DIR\/$ZIP_FILENAME\" -d \"$TMP_DIR\"\n\n# Use sudo only when the install directory is not writable.\nSUDO_CMD=\"\"\nif [[ ! -w \"$INSTALL_DIR\" ]]; then\n  SUDO_CMD=\"sudo\"\nfi\n\necho \"Installing $EXEC_NAME to $INSTALL_DIR...\"\n$SUDO_CMD mkdir -p \"$INSTALL_DIR\"\n$SUDO_CMD mv \"$TMP_DIR\/$EXEC_NAME\" \"$EXEC_PATH\"\n$SUDO_CMD chmod +x \"$EXEC_PATH\"\n\n# Download and source the shell hook to complete installation.\necho \"Installing shell hook...\"\nmkdir -p \"$HOOK_DIR\"\ncurl -LSsf --output \"$HOOK_DIR\/shell_hook.sh\" \"https:\/\/my-bucket.s3.eu-west-1.amazonaws.com\/shell_hook.sh\"\n# shellcheck source=\/dev\/null\nsource \"$HOOK_DIR\/shell_hook.sh\"\n\necho \"Installation complete.\"<\/code><\/pre>\n<p>You might have noticed that:<\/p>\n<ul>\n<li>the required version of ToolManager (defined in <code>.toolmanager-version<\/code>) is downloaded from the release from the corresponding GitHub repository if missing locally. The ToolManager repo should have a GHA workflow in place to build, archive and upload the version.<\/li>\n<li>a <code>shell_hook<\/code> script is downloaded and run to insert the following line in the shell profile: <code>[[ -s \"$HOME\/.toolManager\/shell_hook.sh\" ]] &amp;&amp; source \"$HOME\/.toolManager\/shell_hook.sh\"<\/code>. This allows switching location in the terminal and loading the active tools for the current project.<\/li>\n<\/ul>\n<p>Showing an example of <code>shell_hook.sh<\/code> is in order:<\/p>\n<pre><code class=\"language-bash\">#!\/bin\/bash\n# Overrides 'cd' to update PATH when entering a directory with a local tool setup.\n\n# Add the project-specific bin directory to PATH if it exists.\nupdate_tool_path() {\n  local tool_bin_dir=\"$PWD\/.toolManager\/active\"\n  if [[ -d \"$tool_bin_dir\" ]]; then\n    export PATH=\"$tool_bin_dir:$PATH\"\n  fi\n}\n\n# Redefine 'cd' to trigger the path update after changing directories.\ncd() {\n  builtin cd \"$@\" || return\n  update_tool_path\n}\n\n# --- Installation Logic ---\n# The following function only runs when this script is sourced by an installer.\n\ninstall_hook() {\n  local rc_file\n  case \"${SHELL##*\/}\" in\n    bash) rc_file=\"$HOME\/.bashrc\" ;;\n    zsh)  rc_file=\"$HOME\/.zshrc\" ;;\n    *)\n      echo \"Unsupported shell for hook installation: $SHELL\" &gt;&amp;2\n      return 1\n      ;;\n  esac\n\n  # The line to add to the shell's startup file.\n  local hook_line=\"[[ -s \"$HOME\/.toolManager\/shell_hook.sh\" ]] &amp;&amp; source \"$HOME\/.toolManager\/shell_hook.sh\"\"\n\n  # Add the hook if it's not already present.\n  if ! grep -Fxq \"$hook_line\" \"$rc_file\" &amp;&gt;\/dev\/null; then\n    printf \"n%sn\" \"$hook_line\" &gt;&gt; \"$rc_file\"\n    echo \"Shell hook installed in $rc_file. Restart your shell to apply changes.\"\n  fi\n}\n\n# This check ensures 'install_hook' only runs when sourced, not when executed.\nif [[ \"${BASH_SOURCE[0]}\" != \"$0\" ]]; then\n  install_hook\nfi<\/code><\/pre>\n<p>Now that we have a working installation of ToolManager, let define our <code>Toolfile.yml<\/code> in our project folder:<\/p>\n<pre><code class=\"language-yaml\">---\ntools:\n  - name: PackageGenerator\n    binaryPath: PackageGenerator\n    version: 3.3.0\n    zipUrl: https:\/\/github.com\/justeattakeaway\/PackageGenerator\/releases\/download\/3.3.0\/PackageGenerator-macOS.zip\n  - name: SwiftLint\n    binaryPath: swiftlint\n    version: 0.57.0\n    zipUrl: https:\/\/github.com\/realm\/SwiftLint\/releases\/download\/0.58.2\/portable_swiftlint.zip\n  - name: ToggleGen\n    binaryPath: ToggleGen\n    version: 1.0.0\n    zipUrl: https:\/\/github.com\/TogglesPlatform\/ToggleGen\/releases\/download\/1.0.0\/ToggleGen-macOS-universal-binary.zip\n  - name: Tuist\n    binaryPath: tuist\n    version: 4.48.0\n    zipUrl: https:\/\/github.com\/tuist\/tuist\/releases\/download\/4.54.3\/tuist.zip\n  - name: Sourcery\n    binaryPath: bin\/sourcery\n    version: 2.2.5\n    zipUrl: https:\/\/github.com\/krzysztofzablocki\/Sourcery\/releases\/download\/2.2.5\/sourcery-2.2.5.zip<\/code><\/pre>\n<p>The <code>install<\/code> command of ToolManager loads the Toolfile at the root of the repo and for each defined dependency, performs the following:<\/p>\n<ul>\n<li>checks if the version of the dependency already exists on the machine<\/li>\n<li>if it doesn\u2019t exist, downloads it, unzips it, and places the binary at <code>~\/.toolManager\/tools\/<\/code> (e.g. <code>~\/.toolManager\/tools\/PackageGenerator\/3.3.0\/PackageGenerator<\/code>)<\/li>\n<li>creates a symlink to the binary in the project directory from <code>.toolManager\/active<\/code> (e.g. <code>.toolManager\/active\/PackageGenerator<\/code>)<\/li>\n<\/ul>\n<p>After running <code>ToolManager install<\/code> (or <code>ToolManager install --spec=Toolfile.yml<\/code>), ToolManager should produce the following structure <\/p>\n<pre><code class=\"language-bash\">~ tree ~\/.toolManager\/tools -L 2\n\u251c\u2500\u2500 PackageGenerator\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 3.3.0\n\u251c\u2500\u2500 Sourcery\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 2.2.5\n\u251c\u2500\u2500 SwiftLint\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 0.57.0\n\u251c\u2500\u2500 ToggleGen\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 1.0.0\n\u2514\u2500\u2500 Tuist\n \u00a0\u00a0 \u2514\u2500\u2500 4.48.0<\/code><\/pre>\n<p>and from the project folder<\/p>\n<pre><code class=\"language-bash\">ls -la .toolManager\/active\n&lt;redacted&gt; PackageGenerator -&gt; \/Users\/alberto\/.toolManager\/tools\/PackageGenerator\/3.3.0\/PackageGenerator\n&lt;redacted&gt; Sourcery -&gt; \/Users\/alberto\/.toolManager\/tools\/Sourcery\/2.2.5\/Sourcery\n&lt;redacted&gt; SwiftLint -&gt; \/Users\/alberto\/.toolManager\/tools\/SwiftLint\/0.57.0\/SwiftLint\n&lt;redacted&gt; ToggleGen -&gt; \/Users\/alberto\/.toolManager\/tools\/ToggleGen\/1.0.0\/ToggleGen\n&lt;redacted&gt; Tuist -&gt; \/Users\/alberto\/.toolManager\/tools\/Tuist\/4.48.0\/Tuist<\/code><\/pre>\n<p>Bumping the versions of some tools in the Toolfile, for example SwiftLint and Tuist, and re-running the install command, should result in the following:<\/p>\n<pre><code class=\"language-bash\">~ tree ~\/.toolManager\/tools -L 2\n\u251c\u2500\u2500 PackageGenerator\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 3.3.0\n\u251c\u2500\u2500 Sourcery\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 2.2.5\n\u251c\u2500\u2500 SwiftLint\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 0.57.0\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 0.58.2\n\u251c\u2500\u2500 ToggleGen\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 1.0.0\n\u2514\u2500\u2500 Tuist\n \u00a0\u00a0 \u251c\u2500\u2500 4.48.0\n \u00a0\u00a0 \u2514\u2500\u2500 4.54.3<\/code><\/pre>\n<pre><code class=\"language-bash\">ls -la .toolManager\/active\n&lt;redacted&gt; PackageGenerator -&gt; \/Users\/alberto\/.toolManager\/tools\/PackageGenerator\/3.3.0\/PackageGenerator\n&lt;redacted&gt; Sourcery -&gt; \/Users\/alberto\/.toolManager\/tools\/Sourcery\/2.2.5\/Sourcery\n&lt;redacted&gt; SwiftLint -&gt; \/Users\/alberto\/.toolManager\/tools\/SwiftLint\/0.58.2\/SwiftLint\n&lt;redacted&gt; ToggleGen -&gt; \/Users\/alberto\/.toolManager\/tools\/ToggleGen\/1.0.0\/ToggleGen\n&lt;redacted&gt; Tuist -&gt; \/Users\/alberto\/.toolManager\/tools\/Tuist\/4.54.3\/Tuist<\/code><\/pre>\n<h3 id=\"ci-setup\">CI Setup<\/h3>\n<p>On CI, the setup is quite simple. It involves 2 steps:<\/p>\n<ul>\n<li>install ToolManager<\/li>\n<li>install the tools<\/li>\n<\/ul>\n<p>The commands can be wrapped in GitHub composite actions:<\/p>\n<pre><code class=\"language-yaml\">name: Install ToolManager\n\nruns:\n  using: composite\n  steps:\n    - name: Install ToolManager\n      shell: bash\n      run: curl -Ls 'https:\/\/my-bucket.s3.eu-west-1.amazonaws.com\/install_toolmanager.sh' | bash<\/code><\/pre>\n<pre><code class=\"language-yaml\">name: Install tools\n\ninputs:\n  spec:\n    description: The name of the ToolManager spec file\n    required: false\n    default: Toolfile.yml\n\nruns:\n  using: composite\n  steps:\n    - name: Install tools\n      shell: bash\n      run: |\n        ToolManager install --spec=${{ inputs.spec }}\n        echo \"$PWD\/.toolManager\/active\" &gt;&gt; $GITHUB_PATH\n<\/code><\/pre>\n<p>simply used in workflows:<\/p>\n<pre><code class=\"language-yaml\">- name: Install ToolManager\n  uses: .\/.github\/actions\/install-toolmanager\n\n- name: Install tools\n  uses: .\/.github\/actions\/install-tools\n  with:\n    spec: Toolfile.yml<\/code><\/pre>\n<h3 id=\"cli-tools-conformance\">CLI tools conformance<\/h3>\n<p>ToolManager can install tools that are made available in zip files, without the need of implementing any particular spec. Depending on the CLI tool, the executable can be at the root of the zip archive or in a subfolder. Sourcery for example places the executable in the bin folder.<\/p>\n<pre><code>- name: Sourcery\n  binaryPath: bin\/sourcery\n  version: 2.2.5\n  zipUrl: https:\/\/github.com\/krzysztofzablocki\/Sourcery\/releases\/download\/2.2.5\/sourcery-2.2.5.zip<\/code><\/pre>\n<p>GitHub releases are great to host releases as zip files and that&#8217;s all we need. Ideally, one should decorate the repositories with appropriate release workflows.<\/p>\n<p>Following is a simple example that builds a macOS binary. It could be extended to also create a Linux binary.<\/p>\n<pre><code>name: Publish Release\n\non:\n  push:\n    tags:\n      - '*'\n\nenv:\n  CLI_NAME: my-awesome-cli-tool\n\npermissions:\n  contents: write\n\njobs:\n  build-and-archive:\n    name: Build and Archive macOS Binary\n    \n    runs-on: macos-latest\n    \n    steps:\n      - name: Checkout repository\n        uses: actions\/checkout@v4\n\n      - name: Setup Xcode\n        uses: maxim-lobanov\/setup-xcode@v1\n        with:\n          xcode-version: '16.4'\n\n      - name: Build universal binary\n        run: swift build -c release --arch arm64 --arch x86_64\n      \n      - name: Archive the binary\n        run: |\n          cd .build\/apple\/Products\/Release\/\n          zip -r \"${{ env.CLI_NAME }}-macOS.zip\" \"${{ env.CLI_NAME }}\"\n      \n      - name: Upload artifact for release\n        uses: actions\/upload-artifact@v4\n        with:\n          name: cli-artifact\n          path: .build\/apple\/Products\/Release\/${{ env.CLI_NAME }}-macOS.zip\n\n  create-release:\n    name: Create GitHub Release\n    \n    needs: [build-and-archive]\n    \n    runs-on: ubuntu-latest\n    \n    steps:\n      - name: Download CLI artifact\n        uses: actions\/download-artifact@v4\n        with:\n          name: cli-artifact\n\n      - name: Create Release and Upload Asset\n        uses: softprops\/action-gh-release@v2\n        with:\n          files: \"${{ env.CLI_NAME }}-macOS.zip\"<\/code><\/pre>\n<h3 id=\"a-note-on-version-pinning\">A note on version pinning<\/h3>\n<p>Dependency management systems tend to use a lock file (like <code>Package.resolved<\/code> in Swift Package manager, <code>Podfile.lock<\/code> in the old days of CocoaPods, <code>yarn.lock<\/code>\/<code>package-lock.json<\/code> in JavaScript, etc.).<\/p>\n<p>The benefits of using a lock file are mainly 2:<\/p>\n<ol>\n<li><strong>Reproducibility<\/strong><br \/>It locks the exact versions (including transitive dependencies) so that every team member, CI server, or production environment installs the same versions.<\/li>\n<li><strong>Faster installs<\/strong><br \/>Dependency managers can skip version resolution if a lock file is present, using it directly to fetch the exact versions, improving speed.<\/li>\n<\/ol>\n<p>We can remove the need for lock files if we pin the versions in the spec (the file defining the tools). If version range operators like the CocoaPods&#8217; optimistic operator <code>~&gt;<\/code> and the SPM&#8217;s <code>.upToNextMajor<\/code> and similar one didn&#8217;t exist, usages of lock files would lose its utility.<\/p>\n<p>While useful, lock files are generally annoying and can create that odd feeling of seeing unexpected updates in pull requests made by others. ToolManager doesn&#8217;t use a lock file; <strong>instead<\/strong>, it requires teams to pin their tools&#8217; versions, which I strongly believe is a good practice.<\/p>\n<p>This approach comes at the cost of teams having to keep an eye out for patch releases and not leaving updates to the machine, which risks pulling in dependencies that don&#8217;t respect Semantic Versioning (<a href=\"https:\/\/semver.org\/?ref=albertodebortoli.com\">SemVer<\/a>).<\/p>\n<h3 id=\"support-for-different-architectures\">Support for different architectures<\/h3>\n<p>This design allows to support different architectures. Some CI workflows might only need a Linux runner to reduce the burden on precious macOS instances. Both macOS and Linux can be supported with individual Toolfile that can be specified when running the <code>install<\/code> command.<\/p>\n<pre><code class=\"language-bash\"># on macOS\nToolManager install --spec=Toolfile_macOS\n\n# on Linux\nToolManager install --spec=Toolfile_Linux<\/code><\/pre>\n<h2 id=\"conclusion\">Conclusion<\/h2>\n<p>The design described in this article powers the solution implemented at JET and has served our teams successfully since October 2023. JET has always preferred to implement in-house solutions where possible and sensible, and I can say that moving away from Homebrew was a blessing.<\/p>\n<p>With this design, the work usually done by a package manager and a central spec repository is shifted to individual components that are only required to publish releases in zip archives, ideally via a release workflow.<\/p>\n<p>By <u>decentralising<\/u> and requiring <u>version pinning<\/u>, we made ToolManager a simple yet powerful system for managing the installation of CLI tools.<\/p>\n<\/div>\n<div class=\"pvc_clear\"><\/div>\n<p id=\"pvc_stats_1892\" class=\"pvc_stats total_only  \" data-element-id=\"1892\" style=\"\"><i class=\"pvc-stats-icon medium\" aria-hidden=\"true\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" version=\"1.0\" viewBox=\"0 0 502 315\" preserveAspectRatio=\"xMidYMid meet\"><g transform=\"translate(0,332) scale(0.1,-0.1)\" fill=\"\" stroke=\"none\"><path d=\"M2394 3279 l-29 -30 -3 -207 c-2 -182 0 -211 15 -242 39 -76 157 -76 196 0 15 31 17 60 15 243 l-3 209 -33 29 c-26 23 -41 29 -80 29 -41 0 -53 -5 -78 -31z\"\/><path d=\"M3085 3251 c-45 -19 -58 -50 -96 -229 -47 -217 -49 -260 -13 -295 52 -53 146 -42 177 20 16 31 87 366 87 410 0 70 -86 122 -155 94z\"\/><path d=\"M1751 3234 c-13 -9 -29 -31 -37 -50 -12 -29 -10 -49 21 -204 19 -94 39 -189 45 -210 14 -50 54 -80 110 -80 34 0 48 6 76 34 21 21 34 44 34 59 0 14 -18 113 -40 219 -37 178 -43 195 -70 221 -36 32 -101 37 -139 11z\"\/><path d=\"M1163 3073 c-36 -7 -73 -59 -73 -102 0 -56 133 -378 171 -413 34 -32 83 -37 129 -13 70 36 67 87 -16 290 -86 209 -89 214 -129 231 -35 14 -42 15 -82 7z\"\/><path d=\"M3689 3066 c-15 -9 -33 -30 -42 -48 -48 -103 -147 -355 -147 -375 0 -98 131 -148 192 -74 13 15 57 108 97 206 80 196 84 226 37 273 -30 30 -99 39 -137 18z\"\/><path d=\"M583 2784 c-38 -19 -67 -74 -58 -113 9 -42 211 -354 242 -373 16 -10 45 -18 66 -18 51 0 107 52 107 100 0 39 -1 41 -124 234 -80 126 -108 162 -133 173 -41 17 -61 16 -100 -3z\"\/><path d=\"M4250 2784 c-14 -9 -74 -91 -133 -183 -95 -150 -107 -173 -107 -213 0 -55 33 -94 87 -104 67 -13 90 8 211 198 130 202 137 225 78 284 -27 27 -42 34 -72 34 -22 0 -50 -8 -64 -16z\"\/><path d=\"M2275 2693 c-553 -48 -1095 -270 -1585 -649 -135 -104 -459 -423 -483 -476 -23 -49 -22 -139 2 -186 73 -142 361 -457 571 -626 285 -228 642 -407 990 -497 242 -63 336 -73 660 -74 310 0 370 5 595 52 535 111 1045 392 1455 803 122 121 250 273 275 326 19 41 19 137 0 174 -41 79 -309 363 -465 492 -447 370 -946 591 -1479 653 -113 14 -422 18 -536 8z m395 -428 c171 -34 330 -124 456 -258 112 -119 167 -219 211 -378 27 -96 24 -300 -5 -401 -72 -255 -236 -447 -474 -557 -132 -62 -201 -76 -368 -76 -167 0 -236 14 -368 76 -213 98 -373 271 -451 485 -162 444 86 934 547 1084 153 49 292 57 452 25z m909 -232 c222 -123 408 -262 593 -441 76 -74 138 -139 138 -144 0 -16 -233 -242 -330 -319 -155 -123 -309 -223 -461 -299 l-81 -41 32 46 c18 26 49 83 70 128 143 306 141 649 -6 957 -25 52 -61 116 -79 142 l-34 47 45 -20 c26 -10 76 -36 113 -56z m-2057 25 c-40 -58 -105 -190 -130 -263 -110 -324 -59 -707 132 -981 25 -35 42 -64 37 -64 -19 0 -241 119 -326 174 -188 122 -406 314 -532 468 l-58 71 108 103 c185 178 428 349 672 473 66 33 121 60 123 61 2 0 -10 -19 -26 -42z\"\/><path d=\"M2375 1950 c-198 -44 -350 -190 -395 -379 -18 -76 -8 -221 19 -290 114 -284 457 -406 731 -260 98 52 188 154 231 260 27 69 37 214 19 290 -38 163 -166 304 -326 360 -67 23 -215 33 -279 19z\"\/><\/g><\/svg><\/i> <img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"16\" height=\"16\" alt=\"Loading\" src=\"https:\/\/i0.wp.com\/dev95.site\/wp-content\/plugins\/page-views-count\/ajax-loader-2x.gif?resize=16%2C16&#038;ssl=1\" border=0 \/><\/p>\n<div class=\"pvc_clear\"><\/div>\n","protected":false},"excerpt":{"rendered":"<p>SPONSORED Based on this article, I&#8217;ve published Luca!A lightweight decentralised tool manager for macOS to manage project-specific tool environments. \ud83d\udc76 Check it out:\u00a0luca.tools Overview It&#8217;s common for iOS teams to rely on various CLI tools such as SwiftLint, Tuist, and<\/p>\n<div class=\"hosteria-entry-more\"><a href=\"https:\/\/dev95.site\/ar\/how-to-implement-a-decentralised-cli-tool-manager\/\" class=\"no-underline font-light  group-hover:text-primary-800 dark:group-hover:text-primary-300 py-1\">Read more &gt;&gt;&gt;<\/a><\/div>\n<div class=\"pvc_clear\"><\/div>\n<p id=\"pvc_stats_1892\" class=\"pvc_stats total_only\" data-element-id=\"1892\" style=\"\"><i class=\"pvc-stats-icon medium\" aria-hidden=\"true\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" version=\"1.0\" viewbox=\"0 0 502 315\" preserveaspectratio=\"xMidYMid meet\"><g transform=\"translate(0,332) scale(0.1,-0.1)\" fill=\"\" stroke=\"none\"><path d=\"M2394 3279 l-29 -30 -3 -207 c-2 -182 0 -211 15 -242 39 -76 157 -76 196 0 15 31 17 60 15 243 l-3 209 -33 29 c-26 23 -41 29 -80 29 -41 0 -53 -5 -78 -31z\"\/><path d=\"M3085 3251 c-45 -19 -58 -50 -96 -229 -47 -217 -49 -260 -13 -295 52 -53 146 -42 177 20 16 31 87 366 87 410 0 70 -86 122 -155 94z\"\/><path d=\"M1751 3234 c-13 -9 -29 -31 -37 -50 -12 -29 -10 -49 21 -204 19 -94 39 -189 45 -210 14 -50 54 -80 110 -80 34 0 48 6 76 34 21 21 34 44 34 59 0 14 -18 113 -40 219 -37 178 -43 195 -70 221 -36 32 -101 37 -139 11z\"\/><path d=\"M1163 3073 c-36 -7 -73 -59 -73 -102 0 -56 133 -378 171 -413 34 -32 83 -37 129 -13 70 36 67 87 -16 290 -86 209 -89 214 -129 231 -35 14 -42 15 -82 7z\"\/><path d=\"M3689 3066 c-15 -9 -33 -30 -42 -48 -48 -103 -147 -355 -147 -375 0 -98 131 -148 192 -74 13 15 57 108 97 206 80 196 84 226 37 273 -30 30 -99 39 -137 18z\"\/><path d=\"M583 2784 c-38 -19 -67 -74 -58 -113 9 -42 211 -354 242 -373 16 -10 45 -18 66 -18 51 0 107 52 107 100 0 39 -1 41 -124 234 -80 126 -108 162 -133 173 -41 17 -61 16 -100 -3z\"\/><path d=\"M4250 2784 c-14 -9 -74 -91 -133 -183 -95 -150 -107 -173 -107 -213 0 -55 33 -94 87 -104 67 -13 90 8 211 198 130 202 137 225 78 284 -27 27 -42 34 -72 34 -22 0 -50 -8 -64 -16z\"\/><path d=\"M2275 2693 c-553 -48 -1095 -270 -1585 -649 -135 -104 -459 -423 -483 -476 -23 -49 -22 -139 2 -186 73 -142 361 -457 571 -626 285 -228 642 -407 990 -497 242 -63 336 -73 660 -74 310 0 370 5 595 52 535 111 1045 392 1455 803 122 121 250 273 275 326 19 41 19 137 0 174 -41 79 -309 363 -465 492 -447 370 -946 591 -1479 653 -113 14 -422 18 -536 8z m395 -428 c171 -34 330 -124 456 -258 112 -119 167 -219 211 -378 27 -96 24 -300 -5 -401 -72 -255 -236 -447 -474 -557 -132 -62 -201 -76 -368 -76 -167 0 -236 14 -368 76 -213 98 -373 271 -451 485 -162 444 86 934 547 1084 153 49 292 57 452 25z m909 -232 c222 -123 408 -262 593 -441 76 -74 138 -139 138 -144 0 -16 -233 -242 -330 -319 -155 -123 -309 -223 -461 -299 l-81 -41 32 46 c18 26 49 83 70 128 143 306 141 649 -6 957 -25 52 -61 116 -79 142 l-34 47 45 -20 c26 -10 76 -36 113 -56z m-2057 25 c-40 -58 -105 -190 -130 -263 -110 -324 -59 -707 132 -981 25 -35 42 -64 37 -64 -19 0 -241 119 -326 174 -188 122 -406 314 -532 468 l-58 71 108 103 c185 178 428 349 672 473 66 33 121 60 123 61 2 0 -10 -19 -26 -42z\"\/><path d=\"M2375 1950 c-198 -44 -350 -190 -395 -379 -18 -76 -8 -221 19 -290 114 -284 457 -406 731 -260 98 52 188 154 231 260 27 69 37 214 19 290 -38 163 -166 304 -326 360 -67 23 -215 33 -279 19z\"\/><\/g><\/svg><\/i> <img loading=\"lazy\" decoding=\"async\" width=\"16\" height=\"16\" alt=\"Loading\" src=\"https:\/\/dev95.site\/wp-content\/plugins\/page-views-count\/ajax-loader-2x.gif\" border=\"0\" \/><\/p>\n<div class=\"pvc_clear\"><\/div>","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"fp_fajr_begins":"","fp_fajr_iqamah":"","fp_dhuhr_begins":"","fp_dhuhr_iqamah":"","fp_asr_begins":"","fp_asr_iqamah":"","fp_maghrib_begins":"","fp_maghrib_iqamah":"","fp_isha_begins":"","fp_isha_iqamah":"","fp_midnight":"","fp_midnight_name":"","fp_sunrise":"","fp_single_prayer_begins_title":"","fp_single_prayer_iqamah_title":"","fp_prayer_times_for_today":"","fp_hijra_date":"","fp_fajr_name":"","fp_dhuhr_name":"","fp_asr_name":"","fp_maghrib_name":"","fp_isha_name":"","fp_sunrise_name":"","fp_currentDate":"","fp_current_time":"","fp_current_title":"","fp_current_location":"","fp_masjid_name":"","fp_prayer_title":"","fp_next_prayer_iqamah_time":"","fp_next_prayer_iqamah_title":"","fp_next_prayer_begins_time":"","fp_next_prayer_begins_title":"","fp_next_prayer_title":"","_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[37],"tags":[],"class_list":["post-1892","post","type-post","status-publish","format-standard","hentry","category-posts"],"a3_pvc":{"activated":true,"total_views":0,"today_views":0},"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>How to Implement a Decentralised CLI Tool Manager - Dev95<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/dev95.site\/ar\/how-to-implement-a-decentralised-cli-tool-manager\/\" \/>\n<meta property=\"og:locale\" content=\"ar_AR\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How to Implement a Decentralised CLI Tool Manager - Dev95\" \/>\n<meta property=\"og:description\" content=\"SPONSORED Based on this article, I&#8217;ve published Luca!A lightweight decentralised tool manager for macOS to manage project-specific tool environments. \ud83d\udc76 Check it out:\u00a0luca.tools Overview It&#8217;s common for iOS teams to rely on various CLI tools such as SwiftLint, Tuist, andRead more &gt;&gt;&gt;\" \/>\n<meta property=\"og:url\" content=\"https:\/\/dev95.site\/ar\/how-to-implement-a-decentralised-cli-tool-manager\/\" \/>\n<meta property=\"og:site_name\" content=\"Dev95\" \/>\n<meta property=\"article:published_time\" content=\"2025-07-13T23:15:31+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2025\/07\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png\" \/>\n<meta name=\"author\" content=\"dev95\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u0643\u064f\u062a\u0628 \u0628\u0648\u0627\u0633\u0637\u0629\" \/>\n\t<meta name=\"twitter:data1\" content=\"dev95\" \/>\n\t<meta name=\"twitter:label2\" content=\"\u0648\u0642\u062a \u0627\u0644\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0645\u064f\u0642\u062f\u0651\u0631\" \/>\n\t<meta name=\"twitter:data2\" content=\"13 \u062f\u0642\u064a\u0642\u0629\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/\"},\"author\":{\"name\":\"dev95\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/person\\\/b807805ffe2916206b04d0938bce0298\"},\"headline\":\"How to Implement a Decentralised CLI Tool Manager\",\"datePublished\":\"2025-07-13T23:15:31+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/\"},\"wordCount\":1533,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2025\\\/07\\\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png\",\"articleSection\":[\"Posts\"],\"inLanguage\":\"ar\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/\",\"url\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/\",\"name\":\"How to Implement a Decentralised CLI Tool Manager - Dev95\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2025\\\/07\\\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png\",\"datePublished\":\"2025-07-13T23:15:31+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/#breadcrumb\"},\"inLanguage\":\"ar\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/#primaryimage\",\"url\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2025\\\/07\\\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png\",\"contentUrl\":\"https:\\\/\\\/storage.ghost.io\\\/c\\\/ae\\\/f4\\\/aef4d625-32a2-417b-86f4-22c70a9b47a1\\\/content\\\/images\\\/2025\\\/07\\\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/dev95.site\\\/how-to-implement-a-decentralised-cli-tool-manager\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/dev95.site\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How to Implement a Decentralised CLI Tool Manager\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#website\",\"url\":\"https:\\\/\\\/dev95.site\\\/\",\"name\":\"Dev95\",\"description\":\"A comprehensive platform for data and knowledge, delivering reliable content that meets the aspirations of readers and enthusiasts.\",\"publisher\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/dev95.site\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"ar\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#organization\",\"name\":\"Dev95\",\"url\":\"https:\\\/\\\/dev95.site\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/i0.wp.com\\\/dev95.site\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/rbrrbr-6.png?fit=512%2C512&ssl=1\",\"contentUrl\":\"https:\\\/\\\/i0.wp.com\\\/dev95.site\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/rbrrbr-6.png?fit=512%2C512&ssl=1\",\"width\":512,\"height\":512,\"caption\":\"Dev95\"},\"image\":{\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/dev95.site\\\/#\\\/schema\\\/person\\\/b807805ffe2916206b04d0938bce0298\",\"name\":\"dev95\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"ar\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g\",\"caption\":\"dev95\"},\"url\":\"https:\\\/\\\/dev95.site\\\/ar\\\/author\\\/mohammad\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How to Implement a Decentralised CLI Tool Manager - Dev95","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/dev95.site\/ar\/how-to-implement-a-decentralised-cli-tool-manager\/","og_locale":"ar_AR","og_type":"article","og_title":"How to Implement a Decentralised CLI Tool Manager - Dev95","og_description":"SPONSORED Based on this article, I&#8217;ve published Luca!A lightweight decentralised tool manager for macOS to manage project-specific tool environments. \ud83d\udc76 Check it out:\u00a0luca.tools Overview It&#8217;s common for iOS teams to rely on various CLI tools such as SwiftLint, Tuist, andRead more &gt;&gt;&gt;","og_url":"https:\/\/dev95.site\/ar\/how-to-implement-a-decentralised-cli-tool-manager\/","og_site_name":"Dev95","article_published_time":"2025-07-13T23:15:31+00:00","og_image":[{"url":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2025\/07\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png","type":"","width":"","height":""}],"author":"dev95","twitter_card":"summary_large_image","twitter_misc":{"\u0643\u064f\u062a\u0628 \u0628\u0648\u0627\u0633\u0637\u0629":"dev95","\u0648\u0642\u062a \u0627\u0644\u0642\u0631\u0627\u0621\u0629 \u0627\u0644\u0645\u064f\u0642\u062f\u0651\u0631":"13 \u062f\u0642\u064a\u0642\u0629"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/#article","isPartOf":{"@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/"},"author":{"name":"dev95","@id":"https:\/\/dev95.site\/#\/schema\/person\/b807805ffe2916206b04d0938bce0298"},"headline":"How to Implement a Decentralised CLI Tool Manager","datePublished":"2025-07-13T23:15:31+00:00","mainEntityOfPage":{"@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/"},"wordCount":1533,"commentCount":0,"publisher":{"@id":"https:\/\/dev95.site\/#organization"},"image":{"@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/#primaryimage"},"thumbnailUrl":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2025\/07\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png","articleSection":["Posts"],"inLanguage":"ar","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/","url":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/","name":"How to Implement a Decentralised CLI Tool Manager - Dev95","isPartOf":{"@id":"https:\/\/dev95.site\/#website"},"primaryImageOfPage":{"@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/#primaryimage"},"image":{"@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/#primaryimage"},"thumbnailUrl":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2025\/07\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png","datePublished":"2025-07-13T23:15:31+00:00","breadcrumb":{"@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/#breadcrumb"},"inLanguage":"ar","potentialAction":[{"@type":"ReadAction","target":["https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/"]}]},{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/#primaryimage","url":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2025\/07\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png","contentUrl":"https:\/\/storage.ghost.io\/c\/ae\/f4\/aef4d625-32a2-417b-86f4-22c70a9b47a1\/content\/images\/2025\/07\/ChatGPT-Image-Jul-14--2025--12_07_43-AM.png"},{"@type":"BreadcrumbList","@id":"https:\/\/dev95.site\/how-to-implement-a-decentralised-cli-tool-manager\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/dev95.site\/"},{"@type":"ListItem","position":2,"name":"How to Implement a Decentralised CLI Tool Manager"}]},{"@type":"WebSite","@id":"https:\/\/dev95.site\/#website","url":"https:\/\/dev95.site\/","name":"Dev95","description":"A comprehensive platform for data and knowledge, delivering reliable content that meets the aspirations of readers and enthusiasts.","publisher":{"@id":"https:\/\/dev95.site\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/dev95.site\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"ar"},{"@type":"Organization","@id":"https:\/\/dev95.site\/#organization","name":"Dev95","url":"https:\/\/dev95.site\/","logo":{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/dev95.site\/#\/schema\/logo\/image\/","url":"https:\/\/i0.wp.com\/dev95.site\/wp-content\/uploads\/2026\/07\/rbrrbr-6.png?fit=512%2C512&ssl=1","contentUrl":"https:\/\/i0.wp.com\/dev95.site\/wp-content\/uploads\/2026\/07\/rbrrbr-6.png?fit=512%2C512&ssl=1","width":512,"height":512,"caption":"Dev95"},"image":{"@id":"https:\/\/dev95.site\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/dev95.site\/#\/schema\/person\/b807805ffe2916206b04d0938bce0298","name":"dev95","image":{"@type":"ImageObject","inLanguage":"ar","@id":"https:\/\/secure.gravatar.com\/avatar\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/a70a73d950838b20cd80d7ebdc955737e802e8cd896044c5473b32b946c0662a?s=96&d=mm&r=g","caption":"dev95"},"url":"https:\/\/dev95.site\/ar\/author\/mohammad\/"}]}},"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/posts\/1892","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/comments?post=1892"}],"version-history":[{"count":0,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/posts\/1892\/revisions"}],"wp:attachment":[{"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/media?parent=1892"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/categories?post=1892"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/dev95.site\/ar\/wp-json\/wp\/v2\/tags?post=1892"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}