Skip to content

The Pipeline

The CLI runs every command through one pipeline. Each stage is a plain, testable unit; the runtime edge composes them.

Detection is the first step in every command. It reads the project directory and builds a profile that drives which conformance tasks apply.

The profile contains:

  • Framework and framework version, bundler, router, and styling
  • TypeScript usage, runtime target, and Node version
  • Package manager and whether the project uses Vite+
  • Monorepo status, monorepo tool, and whether the current directory is the workspace root
  • Git and GitHub presence
  • Existing configuration files, such as Biome, ESLint, tsconfig, Renovate, commitlint, knip, Vite config, and GitHub workflows

Detection runs on every invocation; there is no profile cache. Each run inspects package.json, root config files, lockfiles, config directories, and ancestor markers, then computes a fresh profile. Signals come from two sources:

  • Dependencies: framework, bundler, router, and styling packages in package.json
  • Filesystem: TypeScript config presence, lockfiles, monorepo markers, bundler config files, and a .github/ directory

Framework, router, styling, runtime, and Vite+ detection run inline. Bundler, monorepo, and package manager detection consume shared declarations from the detection registry, which also supplies lockfile checks for diagnostics. New keyed detectors belong in the registry.

When both react and react-native (or expo) are present in dependencies, detection reports react-native; there is no prompt to disambiguate. The framework is null only when the project has no package.json. workspaceRoot is true when the current directory contains monorepo markers such as pnpm-workspace.yaml or turbo.json, false inside a workspace package discovered by walking up parent directories, and equal to monorepo in non-monorepos.

Detection stays a plain async function and does not compose Effects; the engine lifts it at the nearest orchestration seam. See the configuration guide for the user-facing summary.

Resolution keeps the tasks whose applicability check accepts the profile — a pure filter with no I/O. In a monorepo, it also filters tasks by scope (root, package, or both); non-monorepo projects skip scope filtering.

Status checks run in parallel. Each task inspects the filesystem and reports one of four statuses:

Status Meaning init acts sync acts
new Config does not exist yet Yes No
patch Config exists and can be updated Yes Yes
skip Already conformant No No
conflict Changes need explicit approval Only when selected Only when selected

A check that throws is reported as a conflict with its error detail instead of failing the whole resolution.

Every project command runs preflight before doing work. Preflight requires:

  • A package.json in the project root (MISSING_PACKAGE_JSON when absent)
  • A name field in package.json (INVALID_PACKAGE_JSON when absent)
  • A git repository (MISSING_GIT when absent)

Covered commands are init, sync, diff, check, add, list, query, restore, undo, and doctor. When a check fails, the CLI prints every error with a hint and exits with code 1, and conformance tasks do not run. doctor continues so it can still report diagnostics. The CLI can maintain .gitignore so the internal .xtarterize/ directory stays out of version control even when preflight fails.

check and doctor additionally run tooling diagnostics:

  • Conflicting tools: Biome alongside ESLint or Prettier, and legacy .eslintrc files, are reported as warnings.
  • Tool installation: for each tool declared in package.json (Biome, ESLint, TypeScript, Commitlint, Knip), the tool’s version command confirms it is installed.

check runs the tools and configuration groups. doctor runs every group (environment, tools, project, and configuration), and --verbose adds a system group with host platform information. Checks report pass, warn, or fail with a message; a check that throws becomes a single failure entry, so one broken check never hides the others, and a failing diagnostic sets exit code 1.

In CI (CI=true or CI=1), xtarterize enables quiet mode for all commands. Quiet mode is also enabled by --quiet, --json, and --format json.

@xtarterize/patchers provides the write mechanics. Patchers return new content, and the caller decides whether to write it.

  • JSON merge: deep merges objects with defu — existing keys win, incoming values fill gaps, nested objects merge recursively, and arrays are replaced entirely, never concatenated. Used for tsconfig.json, biome.json, VS Code settings, and other JSON configuration.
  • Vite plugin injection: inserts an import and a plugin call into Vite config source with magicast. Idempotent: an already-imported plugin is not added again. When the config structure is non-standard, such as a factory function or a conditional export, the patcher returns manual instructions instead of rewriting the file.

Object merging alone would discard comments and formatting, so JSON file writes use a second patcher that edits the JSON text directly, preserving comments, key order, whitespace and indentation, and trailing commas in JSONC. JSON merge targets combine both steps: the object merge computes the target state, then the text patch applies it to the original file.

The apply engine is the final stage. It plans a task set, backs up modified files, installs dependencies in one batch, runs the tasks, and reports errors.

Planning is side-effect free, so a preview is exact and execution replays the plan unchanged. The plan collects, for the selected tasks:

  • Each task’s status, reusing statuses computed while the session opened
  • The diffs for tasks that are not skipped or conflicting
  • The dependencies each runnable task declares
  • The unique file paths in the diffs, which are also the backup set

Execution replays the plan:

  1. Back up every affected file once and write a run manifest for the undo command.
  2. Install the collected dependencies in one batch.
  3. Run each task’s apply in sequence, collecting per-task errors instead of aborting.
  4. Return the applied count, skipped count, collected errors, and timing.

Before any file is modified, the engine writes a timestamped copy under .xtarterize/backups/ and indexes it in .xtarterize/backups/.index.json for restore. Each unique file path is backed up once per run, even when several tasks modify it. The .xtarterize/ directory is added to .gitignore automatically.

Errors are collected and reported without aborting the run:

  • A check that throws becomes a conflict with the error message, and the task is skipped.
  • A dry-run failure is reported and the task is skipped without a backup.
  • A failed dependency install is recorded in the result. Tasks that need the missing packages fail individually, while file-only tasks still run.
  • A failed apply is logged with the task id and message, and later tasks continue.

Backup or manifest failures stop execution before any task runs. Tasks declared from a synchronous or Promise-returning spec follow the same rules as Effect-based tasks.