The Case for a Rewrite
Shopify CLI has become the primary entry point for developers building Themes, Apps, and Hydrogen storefronts on Shopify’s platform. It handles everything from scaffolding new projects that follow best practices to integrating with the platform during development and shipping production artifacts. The CLI Foundations team owns this developer experience, and it was clear that the Ruby-based Shopify CLI 2 wasn’t delivering the consistency developers needed. That led to a full rewrite in TypeScript on the Node runtime, launched in Shopify Editions in summer 2022.
The original CLI traces back to ThemeKit, a Ruby tool maintained since October 2014 and built on internal Ruby gems including cli-kit, cli-ui, and theme-check. When work began on the first Shopify CLI for App developers in December 2018, Ruby was a reasonable choice given the team’s existing expertise. Installers for Windows, Linux, and macOS handled the global Ruby requirement. In December 2020, ThemeKit was merged into Shopify CLI to centralize development efforts.
Several forces eventually made Ruby the wrong fit. The addition of UI extensions in June 2020 introduced a dependency on Node tooling for transpiling and bundling extension code. The hope was that compiled tools like ESBuild would eliminate that need, but extensibility relied on plugins dynamically evaluated on a Node runtime. Meanwhile, the Hydrogen team wanted to build workflows directly into the CLI rather than creating a separate tool, so their users wouldn’t need a Ruby runtime. Hydrogen developers expect npm install to resolve all project dependencies; a Ruby requirement breaks that mental model. Shopify’s broader shift toward JavaScript and web standards also made Node strategically appealing.
The decision came down to a few requirements: minimal system prerequisites, the ability to deliver a strong developer experience, and a technology that internal teams could easily contribute to. Among the languages used at Shopify—Ruby, Node, Go, and Rust—Go and Rust could ship static binaries with no runtime dependency, but fewer developers were familiar with them, which would hinder contributions from internal teams. Ruby remained an option, but Node’s module system offered a decisive advantage: it supports multiple versions of the same transitive package without conflicts. That enables a modular architecture where platform domains are encapsulated in separate NPM packages built on shared functionality.
There is a known tradeoff: while App and Hydrogen developers now need only Node, Theme developers temporarily require both Ruby and Node. Work to remove that dependency is underway and expected to complete later this year.
Seven Principles for Terminal UX
The technology choice was only the beginning. The team also had to define best practices, architecture, patterns, and conventions. Seven decisions had the most impact on building quality terminal experiences.
1. A Foundation for Consistency
Contributions to the Ruby CLI were loosely aligned and loosely coupled, causing fragmentation across internal and external surfaces. The Node version needed a way to align all contributions. The team addressed this with code patterns to model command business logic, UI components and a design system built on Ink, conventions for navigating projects and commands, and principles aimed at making developers succeed by default.
These efforts materialized in @shopify/cli-kit, a shared NPM package that all domains—Themes, Apps, and Hydrogen—build on. Static analysis with ESLint became the enforcement mechanism. Custom rules, such as command-flags-with-env, ensure flags support environment variables. The package exposes an idiomatic API that feature developers use to obtain a valid session for GraphQL API interactions.
2. Cross-OS Support
Developing on macOS while supporting Windows and Linux was cumbersome and led to skipped testing and regressions. Node APIs add further inconsistency across operating systems, though the community has addressed this with packages like pathe for path normalization. The team adopted three strategies:
- Expose modules from
@shopify/cli-kitfor environment interactions like IO operations, ensuring cross-OS compatibility at the API level. - Run a test suite of unit, integration, and end-to-end tests on all supported operating systems in CI, catching issues before merge. Integration tests cover modules that interact with the environment, such as Git utilities.
- Provide explicit instructions for testing changes on macOS, Linux, and Windows.
3. Moving to a Monorepo
Conway’s law had led to a proliferation of repositories containing CLI components, including templates and internal CLIs. This created zero user value while complicating contributions and testing. The rewrite became an opportunity to consolidate everything into the shopify/cli monorepo, enabling atomic changes that span multiple packages and templates.
4. Functional Programming
The Ruby CLI’s command logic was stateful, assumption-heavy, and scattered with side effects, making it hard to reason about, test, and contribute to. For the Node CLI, the team shifted toward functional design, concentrating side effects at the start of a command’s execution. Commands first load and validate the project in memory, similar to how a web API validates an incoming request before passing it downstream.
The approach is pragmatic rather than dogmatic. JavaScript objects and functions serve as composition units, and objects are copied rather than mutated. Classes are used sparingly, mainly for error types. A soft convention organizes functions into a Model View Controller pattern:
- Models are TypeScript interfaces representing state, such as an App project, its configuration, or a session.
- Commands act as the View, the user-facing surface that parses arguments and flags and supplies help content.
- Services act as Controllers, encapsulating business logic. Every command has an associated service, and some services exist independently.
Prompts handle user interaction through standard input, while utility modules group functions under specific domains, such as all functions for interacting with Shopify GraphQL APIs.
Testing at Two Levels
Shopify’s decision to embrace functional programming and keep side effects minimal made unit testing simpler. For defining and running those tests, the team adopted Vitest, a framework that was announced only weeks before work on the Node CLI began. Vitest was chosen because it fully supports ES modules, the module system the new CLI uses. The project hit some early rough patches with Vitest, which the team attributes to the tool’s maturity curve, but the experience has been positive, particularly because of its one-to-one mapping with Jest APIs.
Unit tests covered individual functions under various scenarios, but passing them didn’t guarantee that a workflow such as "app build" would succeed against a freshly created project. To close that gap, the team invested in an end-to-end testing suite using Cucumber. Cucumber provided the tools and APIs to describe, run, and debug E2E workflows.
E2E testing is often criticized for being brittle and hard to maintain, but Shopify found that doesn’t hold for CLIs. The critical difference is setup simplicity: CLI execution can be isolated and scoped to each test scenario, which prevents global state from leaking across tests and causing unpredictable behavior. The resulting tests follow a consistent pattern:
Both levels of testing rely on @shopify/cli-kit modules and external NPM packages that ship with type definitions, which the team says significantly improves ergonomics for contributors. TypeScript is especially useful in ongoing work implementing components for a new CLI design system, where the compiler enforces correct usage of each component.
An Extensible Foundation
In early conversations with CLI developers outside Shopify, oclif repeatedly came up as a strong framework for building Node CLIs. oclif was born from Heroku’s CLI and was designed to support the development of other CLIs. After choosing Node, Shopify evaluated oclif more thoroughly, built small prototypes, and ultimately built its Node CLI on oclif’s APIs, conventions, and ecosystem. In hindsight, the team considers it an excellent choice.
oclif provides idiomatic APIs for declaring a CLI’s interface and ships with well-designed customizable defaults. Help documentation is generated automatically from in-code declarations. The framework’s plugin system brings built-in extensibility, which Shopify leverages with separate plugins for App, Theme, and Hydrogen development. This structure allowed the project to be organized into modules with clear boundaries and distinct responsibilities.
Shopify also uses oclif’s hooks API with dependency inversion to keep @shopify/cli-kit from knowing about dependent plugins. Both plugins and @shopify/cli-kit implementations depend on interfaces rather than on each other’s internals.
Remaining Work on the Roadmap
Shopify describes the Node CLI as a significant boost to developer experience: app development has been unified and streamlined, consistency has improved across the board, and new extensibility capabilities such as functions have been added. But the work is not finished.
A near-term goal is aligning the Theme development experience with App and Hydrogen. Theme commands still run through the Ruby implementation today, which means users see the old Ruby CLI’s look and feel and are still required to have a Ruby runtime installed. That’s not ideal, and the team wants to bring Themes into the unified Node experience.
Work will also continue on app development flows, with an eye toward delivering commands that help developers create, develop, and deploy apps. Feedback gathered since Shopify announced its developer experience initiative is shaping that iteration. Some users, for example, have reported friction when moving to the unified app model from a multi-repo setup.
The team also sees the current foundation as a place to prototype new ideas. Extensibility will be a focus of future development, with more details promised in a later post.



