# alt CLI Tool

## 1. Context
The company built a line of consumer products through four loosely-coupled units. Because the products didn't need to share a stack, each team had drifted toward its own conventions over the years (their own server-side git hooks, their own security checks, their own unit test harness) while still relying on the same underlying infrastructure and internal services. That autonomy was healthy, but it had a cost.

By the time the org reached its current size, every team's daily workflow had grown into a long manual checklist: create a branch following internal naming rules, write a commit message in the right format, kick off a dev build on Jenkins, run unit tests, push reports to the developer portal, publish artifacts to the registry, mark the branch or artifact as a release candidate. Engineers were doing all of it by hand, switching between four or five web UIs to get a single change shipped. Onboarding a new engineer took days of shoulder-surfing. Veterans forgot steps. Each missed step typically added one to two days to the feedback loop on quality assurance.

The scope was meaningful: roughly 10 services of ~100 modules each, plus ~100 reusable npm packages, spread across more than 100 repositories. Every engineer ran through some version of this checklist two to three times a day.

Goals for the project:
- Automate the full lifecycle of frontend code units: modules, services, and npm packages
- Cover the workflows of all four product units while respecting their differences
- Keep everything in one context so engineers stop tab-hopping
- Integrate into existing repositories without invasive changes
- Stay extensible enough to absorb future process changes without a rewrite

## 2. Constraints and non-goals
- **Time budget:** 3 months, one person
- **No GUI in v1**
- **Local-first:** no cloud services, no paid licenses
- **Not a replacement for git, npm, or Jenkins:** a thin orchestrator that wraps their APIs, nothing more
- **Adoption target:** 80% of engineers and NPS > 7. Time-saved was the real prize but too noisy to measure directly, so adoption and satisfaction served as the proxy.

## 3. The decision / approach
A round of CustDev interviews surfaced a clear set of functional requirements: a plugin system with a scripting language, lifecycle commands for services / packages / modules (generate, build, test, publish, monitor), CLI-first on macOS and Linux (no Windows), and configuration at both user and project levels.

With requirements in hand, the next question was build vs. buy. None of the obvious candidates fit:

- **Nx, Turborepo, Lerna** are built around monorepos and their build graphs. Adapting them to ~100 polyrepos would have cost about as much as building from scratch.
- **Grunt, Gulp** are marketed as task runners, but in practice they're build tools, not workflow automation.
- **CMake, Bazel** and similar are foreign to frontend developers and expensive to maintain on a frontend stack.

Building it ourselves on Node.js and TypeScript was the natural fit: the language was already native to every engineer who'd touch the tool, plugin authoring would feel like writing the code they wrote every day, and scripting maps cleanly onto pipeline-style workflows.

Once that was settled, I mapped each unit's existing workflows onto a single combined diagram, looking for shared artifacts, entities, and steps. That map drove a four-iteration rollout plan:

1. **Application core** — basic operations (code generation, building) ready for early adopters
2. **Data team build-out** — fill in everything one specific team needed end-to-end. The data team was chosen as anchor adopter because they were the most pain-felt and the most enthusiastic, and the smallest blast radius if something broke.
3. **Essentials for the rest** — extend plugins and pipelines to cover the other three units
4. **Polish** — optional features and DX improvements driven by feedback

One architectural decision was made before any code was written: the core would run as a background daemon. The idea was that a long-running process could cache state across many local repositories and could expose itself through multiple front-ends down the line: a CLI today, a web GUI or system notifier tomorrow.

## 4. Implementation highlights

<figure class="case--figure">
	<picture>
		<source media="(prefers-color-scheme: dark)" srcset="/case-pic/alt-cli-1-dark.svg">
		<source media="(prefers-color-scheme: light)" srcset="/case-pic/alt-cli-1-light.svg">
		<img class="case--picture" alt="Pic 1. Diagram of three processes architecture" src="/case-pic/alt-cli-1-light.svg">
	</picture>
	<figcaption class="case--caption">
		Pic 1. Diagram of three processes architecture
	</figcaption>
</figure>

The application is structured as three processes (pic 1): the *CLI*, the *Core Daemon*, and the *Notifier*. The CLI is the entry point. It loads and validates user- and project-level config, parses the user's command, spawns the Core daemon if it isn't already running, and forwards the command to it. The Notifier is a small platform-specific process that surfaces native push notifications. The Core daemon is where the actual work happens.

The three processes talk over Unix domain sockets, exchanging HTTP-like messages written to a socket file. This keeps communication entirely at the OS level (no network interface) and leaves the door open for additional front-ends (a web GUI, an IDE plugin) without changing the daemon at all.

The Core itself follows a classic three-layer architecture: Public, Domain, and Data (pic 2), and persists its state (auth tokens, caches, temp files, in-flight pipeline state) to disk under the directories defined by the XDG Base Directory specification.

<figure class="case--figure">
	<picture>
		<source media="(prefers-color-scheme: dark)" srcset="/case-pic/alt-cli-2-dark.svg">
		<source media="(prefers-color-scheme: light)" srcset="/case-pic/alt-cli-2-light.svg">
		<img class="case--picture" alt="Pic 2. Core Daemon process architecture" src="/case-pic/alt-cli-2-light.svg">
	</picture>
	<figcaption class="case--caption">
		Pic 2. Core Daemon process architecture
	</figcaption>
</figure>

On the **Public layer**, an *IPC Listener* translates incoming messages into internal DTOs, and a *Command Controller* loads the right plugins and commands for the active context (user or project). The Command Controller extracts the task list for a given command and hands it off to the Domain layer.

On the **Domain layer**, the *Task Manager* runs the scripted pipelines. It owns the queue and the runner pool, and based on user config it can spin up several *Runners* in parallel. A pipeline can request access to a local tool (`git`, `npm`, `node`) or an external service (Jira, Jenkins, Backstage, Artifactory). Access is declared in the plugin manifest and granted either ahead of time in user config or interactively at runtime; if granted, the Runner invokes the corresponding RPC on the Data layer.

On the **Data layer**, a *Service Provider* and *Local Tool Provider* expose those RPCs. An *IPC Sender* on the same layer streams progress and notifications back out to the CLI or Notifier. Both the Command Controller and the Task Manager can publish to it.

A plugin is a directory with at minimum an `alt-plugin.yaml` manifest and an `index.js` or `index.ts` entry point. The manifest declares which command the plugin implements, what access it needs, and what context level it runs at. The entry point is a scripted pipeline: a mix of a small DSL exposed as JavaScript helpers, plus plain JavaScript anywhere custom logic is needed.


A real example. The manifest for `alt branch`:

```yaml
id: "@alt/cmd-branch"
description: "This command helps to create a new branch that is compliant with Git server rules. It can take a Jira ticket number as an optional argument, and if none is provided, it will prompt the user to select from the open tickets assigned to them. This tool aims to simplify the branch creation process and improve the organization of your Git workflow."
scope: project
args:
  - name: "<JIRA_TICKET>"
    isRequired: false
    description: "Optional Jira ticket number that will be used as a part of the branch name."
examples:
  - command: "branch"
    description: "Creates new branch, shows interactive interface to choose Jira ticket."
  - command: "branch ABC-123"
    description: "Creates new branch with provided Jira ticket"
access:
  services:
    - jira
  tools:
    - git
```

Note how `access` is declared statically: the runtime enforces it via the Service Provider, so a plugin can never reach for a tool it didn't ask for in the manifest.

The pipeline itself is a list of named steps. Each step is a pure function that returns an effect; this makes the whole thing dry-runnable and easy to reason about:

```typescript
import { pipeline, step } from "@alt/cli";
import { args, listChoiceWithSuggest, prompt, fail } from "@alt/core/ui";
import { slugify, template, rand } from "@alt/core/helpers";
import { jira } from "@alt/core/services";
import { git } from "@alt/core/tools";

export default pipeline([
	step("Get Jira ticket", async ({ ctx }) => {
		const argTicket = args<string>(1);
		
		if (argTicket) {
			ctx.ticket = jira.getTicket(argTicket);
			if (!ctx.ticket) {
				return fail(`Ticket ${argTicket} does not exist`);
			}
			return;
		} else {
			const tickets = await jira.getTickets({ assignee: "me", sort: "date", sortDir: "desc", limit: 5 });
			ctx.ticket = null;
			return listChoiceWithSuggest(
				tickets.map(t => `#${t.key} ${t.summary}`),
				{ ctx, key: 'ticket' }
			);
		}
	}),
	step("Create branch name", async ({ ctx, config }) => {
		const slug = slugify(
			ctx.ticket.summary
				.replace(/\[.*?\]/g, "")
				.replace(/\s+/g, " ")
				.replace(/[\'\"\`\:\(\)\.]+/g, "")
				.trim(),
			{ lower: true }
		);
		ctx.branch = template(
			config.templates.branch,
			{
				"ticket-category": ctx.ticket.category,
				"jira-ticket-number": ctx.ticket.key,
				"slug": slug,
			},
			{
				maxLength: 64,
			}
		);
		return;
	}),
	step("Check if unique", async ({ ctx }) => {
		if (await git.getBranch(ctx.branch)) {
			ctx.answer = 'checkout';
			return prompt(
				{
					type: "expand",
					name: "branch",
					message: `There is a branch "${ctx.branch}" for the ticket ${ctx.ticket.key}.\n  Do you want to checkout to this branch or create a new one?`,
					choices: [
						{
							default: true,
							key: "c",
							name: "checkout",
							value: "checkout",
						},
						{
							key: "n",
							name: "new",
							value: "new",
						},
					],
				},
				{ ctx, key: 'answer'}
			);
		} else {
			ctx.skip = true;
			return git.checkout(ctx.branch, { flags: "-b" });
		}
	}),
	step("Create or checkout", ({ ctx }) => {
		if (ctx.answer === 'checkout') {
			return git.checkout(ctx.branch);
		} else {
			return git.checkout(ctx.branch + `-${rand({ type: "a-z", maxChars: 6 })}`, { flags: "-b" });
		}
	}, {
		skip: ctx.skip,
	}),
]);
```

The essential commands shipped:

- `alt config` — bootstraps the local config and connects the user to Jira, Bitbucket, and Jenkins
- `alt branch` — creates a git branch following project naming conventions. With no arguments it pulls the user's open Jira tickets and offers a picker; given a ticket like `DATA-1242 Fix provider icon position` it generates a branch like `bugfix/DATA-1242_provider_icon_position`. The template lives in project config.
- `alt commit` — formats a git commit message according to project convention. Later extended by the Core team with an AI-based summarizer that reads the diff.
- `alt generate` — scaffolds source code for a service, a module, or an npm package depending on context. It looks for generators in `node_modules`, so any team can extend it by publishing a generator package.
- `alt test` — runs the linters and unit test runners present in the project (`jest`, `mocha`, `playwright`, `eslint`), then adapts and publishes the reports to Backstage, linked back to the Jira ticket.
- `alt deploy` — bumps the version of an artifact (service or package) and triggers the Jenkins CI/CD pipeline that publishes to DEV or Artifactory. Uses semver; later picked up the AI summarizer for release notes; updates the Jira ticket on success.
- `alt pipeline` — shows the live status of an artifact's CI/CD pipeline, filtered to the current user by default. Can run in the background and surface system push notifications when a pipeline finishes.

## 5. Rollout
The design landed in front of the department at a regular all-hands for early feedback. The first month went into the application core. Once a Data team-ready slice was working, I introduced it to that team in a dedicated workshop. Adoption hit 100% within the week. The next iteration, covering the remaining three units, was ready a month later and went out at the next department meeting. Adoption again reached 100% within a week, well above the 80% target.

A few things made the rollout that smooth: the CustDev interviews up front meant the tool actually solved the problems engineers were complaining about; tight feedback loops at design and department meetings caught course corrections early; a dedicated Slack channel kept user support visible; documentation existed for both users and future maintainers; and the Data team's enthusiasm meant the rest of the org saw real users vouching for the tool before they had to try it themselves.

After the two months of development and rollout, I handed the tool off to the Core team, full source, documentation, two two-hour workshops on architecture and maintenance, and a month of joint user support with me on third line. The tool is still maintained by the Core team and still used across all four units as of end of 2024.

## 6. Results
Adoption hit **100%** within a week of each rollout, against an 80% target. NPS came in at **9** (n=30).

Day-to-day usage settled at:

- `alt` — 185 invocations/day (7-day rolling median), ~6 per engineer per day
- `alt commit` — 112 invocations/day (7-day rolling median), ~3 per engineer per day
- **4 custom plugins** built by teams in the first three months of use

The workflow improvements were concrete:

- Publishing a new npm package went from seven manual steps to three commands (`alt generate`, `alt commit`, `alt deploy`) — about 2 hours saved per publish, based on team self-reports
- New hires deployed their first artifact on their second working day
- Onboarding time for the lifecycle workflows dropped from ~8 hours of process and tool walk-throughs to under an hour of `alt config`
- Zero missed-step incidents reported in the QA channel over the six months following full rollout, previously a recurring source of failed builds and forgotten DEV deploys
- A year on, `alt` had become the most-used internal tool among the frontend engineers and was treated as essential infrastructure

## 7. What I'd do differently
**The custom pipeline feature didn't earn its keep.** Teams barely touched it, because everything they actually needed was already implemented out of the box. Internal processes evolve slowly, and when something did need to change, updating a built-in command's source code turned out to be easier than authoring a new pipeline. The extension point still has strategic value if processes ever change radically, but in retrospect it was over-built for v1. I'd ship that capability only when there's evidence of demand, not on speculation.

**The daemon architecture was over-engineered for v1.** It was designed for a future where multiple front-ends (a web GUI, system notifiers, IDE integrations) would talk to a long-running core. A year of usage showed that the rich CLI is enough for everyone. The daemon still pays off as an architectural option for later, but the complexity wasn't necessary on day one. A simpler stateless CLI would have been faster to build and easier to maintain.


## 8. Further reading
- Three-layer fractal architecture (coming soon)
- IPC communication for Node.js processes with Unix sockets (coming soon)
