← Basics
Publishedrole: foundationlicense: BSD-3-Clause

tom_build_base

tom_build_base · v2.6.25

The CLI and tooling framework Tom's build tools are built on: declarative tool, command, and option definitions with workspace traversal, pipelines, and configuration loading.

View repository → See License
Status
Published
LOC
9.2k
Tests
788
Test LOC
11.7k

Overview

From the module readme.md file:

What it enables

Enables Declarative CLI tools, Workspace and git traversal, Pipelines and macros, Build configuration loading.

Relationships

Standalone — no declared relationships.

tom_build_base

> Part of the Tom framework by al-the-bear. > © 2024–2026 Peter Nicolai Alexis Kyaw — BSD-3-Clause, see LICENSE.

Unified CLI framework for workspace traversal, tool definition, pipeline execution, and build configuration.

tom_build_base is the foundation every Tom command-line tool is built on — buildkit, testkit, issuekit, d4rtgen, and the rest. It answers the questions that every workspace tool has to answer and that nobody should have to re-answer: how do I declare commands and options, how do I generate help, how do I find the projects in this workspace, in what order do I process them, and how do I run a pipeline of shell and tool steps over them. A tool author declares what their tool does (a ToolDefinition with commands and an executor per command); tom_build_base supplies how it runs.

This is a deliberately large, single-purpose package. It is also the reason the Tom CLI tools feel like one product rather than a pile of scripts: they share this framework, so they share their navigation flags, their help format, their configuration model, and their end-of-run summaries.

---

Overview

A workspace tool is mostly the same machinery wrapped around a small core of tool-specific logic. Without a shared base, every tool reinvents argument parsing, copies a help formatter, writes its own directory walk, and disagrees with its siblings about what --project means. tom_build_base collapses all of that into one framework with four cooperating layers:

  • Tool definitionToolDefinition, CommandDefinition, OptionDefinition

describe a tool declaratively. From that description the framework derives argument parsing, --help/--version, shell completion, and the standard navigation flags — no imperative wiring per tool. - ExecutionToolRunner takes a definition plus a CommandExecutor per command, parses the arguments, runs the right command across the traversal, and aggregates per-item outcomes into a single ToolResult. - TraversalBuildBase.traverse (and the higher-level runner) scans the filesystem, detects each folder's natures (Dart project, git repo, buildkit folder…), filters by project/module selectors, orders by dependency build order or git depth, and invokes a callback with a typed CommandContext per match. - Pipelines & configuration — multi-command tools get pipelines, runtime macros, and persistent defines automatically; TomBuildConfig reads the two-tier …_master.yaml / project-level YAML configuration.

The package ships two entry points. package:tom_build_base/tom_build_base.dart is the full surface (tool framework + traversal + config + utilities); package:tom_build_base/tom_build_base_v2.dart is the same modern surface without the few legacy utility exports, for tools that only need the v2 framework.

---

Installation

dependencies:
  tom_build_base: ^2.6.0

Or from the command line:

dart pub add tom_build_base

Then import the entry point:

import 'package:tom_build_base/tom_build_base.dart';

Requires Dart SDK ^3.10.4. It depends on args, console_markdown, dcli, glob, path, and yaml. It is a dart:io package — it runs on desktop, server, and CLI hosts, not the web.

---

Features

Tool definition

APIKindPurpose
ToolDefinition class Declares a tool: name, version, mode, commands, features
CommandDefinition class One command: name, aliases, options, nature requirements
OptionDefinition class One flag/option/multi-option, with .flag / .option / .multi constructors
ToolMode enum singleCommand, multiCommand, or hybrid
NavigationFeatures class Which navigation flags a tool exposes ( projectTool , gitTool , all , …)
CommandListOps extension without / replacing / plus for deriving command lists

Execution

APIKindPurpose
ToolRunner class Parses args, routes commands, drives traversal, aggregates results
CommandExecutor abstract The contract: run a command on one folder
CallbackExecutorclassBuild an executor from a closure
SyncExecutor / ListExecutor / ShellExecutor / DartExecutor class Ready-made executors
ItemResult class Outcome for one folder: success / skipped / failure
ToolResult class Aggregated run outcome + renderRunSummary()

Traversal

APIKindPurpose
BuildBase.traverse static Scan → detect → filter → order → run a callback per folder
BaseTraversalInfo abstract Shared traversal config (exclude patterns, test-project toggles)
ProjectTraversalInfo class Project-mode config (scan path, recursive, build-order, selectors)
GitTraversalInfo class Git-mode config (modules, inner/outer-first order)
CommandContext class Per-folder context: path, natures, getNature<T>()
DartProjectFolder / GitFolder / BuildkitFolder / ExtensionFolder nature Detected folder kinds with typed metadata
FilterPipeline / FolderSorter class Project/module filtering and build-order/git-depth ordering

Pipelines, configuration & utilities

APIKindPurpose
PipelineConfig / PipelineExecutor class Multi-step pipelines ( shell , shell-scan , stdin , print , {TOOL} )
TomBuildConfig class Two-tier …_master.yaml + project-level config loading
HelpGenerator / CompletionGenerator class Auto-generated help and shell completion
yamlToMap / yamlListToList / toStringList function YAML-node conversion helpers
MkLinkExecutor / createSymLink API Cross-platform symlink creation for tool commands

---

Quick start

A complete tool is a ToolDefinition, one CommandExecutor per command, and a ToolRunner to glue them together:

import 'dart:io';
import 'package:tom_build_base/tom_build_base.dart';

const myTool = ToolDefinition(
  name: 'mytool',
  description: 'My custom build tool',
  version: '1.0.0',
  mode: ToolMode.multiCommand,
  features: NavigationFeatures.projectTool,
  commands: [
    CommandDefinition(
      name: 'list',
      description: 'List discovered Dart projects',
      requiredNatures: {DartProjectFolder},
    ),
  ],
);

void main(List<String> args) async {
  final runner = ToolRunner(
    tool: myTool,
    executors: {
      'list': CallbackExecutor(
        onExecute: (context, args) async {
          final dart = context.getNature<DartProjectFolder>();
          print('  ${dart.projectName} v${dart.version}');
          return ItemResult.success(path: context.path, name: context.name);
        },
      ),
    },
  );
  final result = await runner.run(args);
  exit(result.success ? 0 : 1);
}

That ~30-line tool already supports mytool :list, mytool --help, mytool --version, the full --project / --exclude / --scan / --build-order navigation flag set, dependency-ordered traversal, and a consolidated end-of-run summary. --version prints:

mytool v1.0.0

and --help prints the tool description, every global option, and the command list — all derived from the definition, none of it hand-written.

> The runnable version of this tool lives in > example/tom_build_base_example.dart.

---

Example projects

ExampleWhat it shows
example/tom_build_base_example.dart A two-command (hello, list) ToolRunner tool
tom_build_base_introduction_sample A simple single-command build tool, built from one ToolDefinition and run over a fixture workspace — the introductory article.
tom_build_base_advanced_sample A nestable, multi-command tool: per-command options, audit/exit codes, sequencing and nested invocation — the advanced article.
tom_build_base_advanced_analyzer_sample A nestable single-command tool whose traversal feeds an analyzer-summary cache ( tom_analyzer_shared ) — the caching article.

Run the local example with:

dart run example/tom_build_base_example.dart --help
dart run example/tom_build_base_example.dart :list

For full worked tutorials, start with the tom_build_base_introduction_sample and progress through the advanced and analyzer-caching samples; the usage sections below and the doc/ guides are the inline reference.

---

Usage

Defining a tool

A ToolDefinition is a const value object. Its mode decides the calling convention:

  • ToolMode.singleCommand — the tool is one operation (mytool [options]).
  • ToolMode.multiCommand — the tool dispatches to named commands, invoked with

a colon prefix (mytool :build, mytool :clean). - ToolMode.hybrid — supports both.

features selects which standard flags appear. The presets cover the common cases — NavigationFeatures.projectTool (project traversal + recursion), NavigationFeatures.gitTool (git traversal), NavigationFeatures.all, NavigationFeatures.minimal — or construct one to enable exactly the flags you want (jsonOutput, interactiveMode, dryRun, …).

const tool = ToolDefinition(
  name: 'mytool',
  description: 'Demonstrates the definition surface',
  version: '2.0.0',
  mode: ToolMode.multiCommand,
  features: NavigationFeatures.projectTool,
  commands: [
    CommandDefinition(
      name: 'build',
      description: 'Compile each package',
      aliases: ['b'],
      requiredNatures: {DartProjectFolder},
      examples: ['mytool :build', 'mytool :build --project app_*'],
    ),
  ],
);

CommandDefinition.requiredNatures is the filter that makes a command run only where it makes sense: {DartProjectFolder} means "only on folders that are Dart projects". Commands resolve by exact name, alias, or unambiguous prefixmytool :b and mytool :bui both reach build, and an ambiguous prefix resolves to nothing rather than guessing.

Options

OptionDefinition has three named constructors for the three kinds of option, and a .usage getter the help generator uses:

const verbose = OptionDefinition.flag(
    name: 'verbose', abbr: 'v', description: 'Verbose output');
const config = OptionDefinition.option(
    name: 'config', abbr: 'c', description: 'Config path', valueName: 'path');
const exclude = OptionDefinition.multi(
    name: 'exclude', description: 'Skip these', valueName: 'pattern');

print(verbose.usage); // -v, --verbose
print(config.usage);  // -c, --config=<path>
print(exclude.type);  // OptionType.multiOption

You rarely define the navigation options yourself — projectTraversalOptions, gitTraversalOptions, and commonOptions are contributed automatically based on the tool's features. Define options only for behaviour unique to your tool.

Deriving a tool from another

Because a ToolDefinition is immutable, you extend one with copyWith, and the CommandListOps extension (without / replacing / plus) edits the command list functionally. This is how a specialised tool reuses a general one without inheritance:

final superTool = baseTool.copyWith(
  name: 'supertool',
  commands: baseTool.commands
      .without({'clean'})                       // drop a command
      .replacing('build', fasterBuildCommand)   // swap one out, keep its slot
      .plus([shipCommand]),                      // add new ones
);

print(superTool.commands.map((c) => c.name)); // (build, ship)
print(superTool.findCommand('sh')?.name);      // ship  (prefix match)

Running commands

ToolRunner ties a definition to behaviour. Each command name maps to a CommandExecutor; the simplest is CallbackExecutor, which wraps a closure. The closure receives a CommandContext (the folder and its natures) and the parsed CliArgs, and returns an ItemResult:

final runner = ToolRunner(
  tool: myTool,
  executors: {
    'build': CallbackExecutor(
      onExecute: (context, args) async {
        if (!context.isDartProject) {
          return ItemResult.skipped(
              path: context.path, name: context.name, message: 'not a package');
        }
        // … do the build …
        return ItemResult.success(path: context.path, name: context.name);
      },
    ),
  },
);
await runner.run(args);

For common shapes there are ready-made executors: ShellExecutor (run a shell command per folder), DartExecutor (a Future<bool> function per folder), SyncExecutor (a synchronous callback), and ListExecutor (just enumerate).

Reading results

Each folder yields an ItemResultsuccess, skipped (a deliberate, non-failing skip), or failure. ToolResult.fromItems aggregates them, and renderRunSummary() produces the uniform end-of-run block every Tom tool prints:

final result = ToolResult.fromItems([
  ItemResult.success(path: '/a', name: 'a', commandName: 'build'),
  ItemResult.skipped(path: '/b', name: 'b', commandName: 'build', message: 'no changes'),
  ItemResult.failure(path: '/c', name: 'c', commandName: 'build', error: 'compile failed'),
]);

print('success=${result.success} failed=${result.failedCount}');
print(result.renderRunSummary());

Output:

success=false failed=1
=== Skipped ===
  b :build — no changes
1 project(s) skipped.

=== Errors ===
  c :build — compile failed
1 error(s) in 1 project(s).

A skipped item is still a success — it never affects the exit code — but it is reported separately from items that did real work, so a long run ends with one readable account of what was built, what was skipped and why, and what failed.

Traversal without the full runner

When you want the scanning and ordering machinery but not the CLI layer — say, inside another tool or a test — call BuildBase.traverse directly. You give it a traversal config and a nature filter; it scans, detects natures, filters, orders, and invokes your callback with a typed CommandContext:

await BuildBase.traverse(
  info: ProjectTraversalInfo(
    scan: workspaceRoot,
    recursive: true,
    executionRoot: workspaceRoot,
  ),
  requiredNatures: {DartProjectFolder},
  run: (ctx) async {
    final dart = ctx.getNature<DartProjectFolder>();
    print('${dart.projectName} v${dart.version}');
    return true;
  },
);

Over a workspace containing alpha and beta packages this prints:

alpha v1.2.3
beta v1.2.3

By default ProjectTraversalInfo.buildOrder is true, so packages arrive in dependency order (a package's dependencies before the package itself) computed across all scanned projects — not just the filtered subset — so ordering stays correct even when --project narrows the run. At least one of requiredNatures / worksWithNatures must be set; use {FsFolder} to match every folder.

Natures

A nature is a capability the framework detects on a folder. One folder can have several — a Dart package inside a git repo is both a DartProjectFolder and a GitFolder. CommandContext exposes them type-safely:

if (ctx.hasNature<DartProjectFolder>()) {
  final dart = ctx.getNature<DartProjectFolder>();      // throws if absent
  print(dart.dependencies.keys);
}
final git = ctx.tryGetNature<GitFolder>();              // null if absent
NatureDetected whenCarries
DartProjectFolder folder has pubspec.yaml projectName , version , dependencies , devDependencies , pubspec
GitFolder folder has .git/ git repository metadata
BuildkitFolder folder has buildkit.yaml tool configuration presence
ExtensionFolder a VS Code / tool extension folder extension metadata

DartProjectFolder is hierarchy-aware: a Flutter package is a FlutterProjectFolder and matches DartProjectFolder, so a requiredNatures: {DartProjectFolder} filter catches every Dart project subtype.

Configuration

Tom tools read configuration from two tiers: a workspace-root master file ({tool}_master.yaml, e.g. buildkit_master.yaml) for shared defaults, and a per-project file (buildkit.yaml) that overrides them. TomBuildConfig.load and TomBuildConfig.loadMaster read these:

# buildkit_master.yaml (workspace root) — shared defaults
navigation:
  scan: .
  recursive: true
  exclude: [.git, build]

mytool:
  verbose: false

# buildkit.yaml (inside a project) — overrides
mytool:
  verbose: true

Pipelines

Multi-command tools get pipelines for free. A pipeline is a list of steps, each with a prefix that decides how the step runs:

PrefixBehaviour
shell <cmd>Run a shell command once
shell-scan <cmd>Run the command once per traversed project
stdin <cmd>Run with multiline stdin content
print <msg>Print exactly one resolved message (no shell noise)
{TOOL} <cmd>Delegate to one of the tool's own commands

Pipelines also support runtime macros ($name, defined on the command line and persisted per tool) and persistent defines (key/value pairs in the master and project YAML). See multiws_pipelines_macros_defines.md and modes_and_placeholders.md for the full model.

---

Architecture

package:tom_build_base/tom_build_base.dart   (full surface)
package:tom_build_base/tom_build_base_v2.dart (framework only, no legacy utils)
        │
   ┌────┴───────────────── Tool definition ──────────────────┐
   │ ToolDefinition ── commands ─▶ CommandDefinition          │
   │      │                             │                     │
   │   features                      options ─▶ OptionDefinition
   │   (NavigationFeatures)                                   │
   └──────────────┬──────────────────────────────────────────┘
                  │ given to
                  ▼
            ToolRunner ── executors: { name → CommandExecutor }
                  │  parse args → route command → traverse → aggregate
                  ▼
            BuildBase.traverse
                  │ scan ─▶ NatureDetector ─▶ FilterPipeline ─▶ FolderSorter
                  ▼
            CommandContext (path + natures) ─▶ executor ─▶ ItemResult
                  │
                  ▼
            ToolResult.fromItems(...) ─▶ renderRunSummary()
TypeRole
ToolDefinition Declarative description of a tool (the single source of truth)
CommandDefinitionOne command within a multi-command tool
OptionDefinitionOne flag/option/multi-option
ToolRunner Parses args, routes to a command, drives traversal, aggregates
CommandExecutor The per-folder behaviour contract (CallbackExecutor et al.)
BuildBase Static traversal engine: scan → detect → filter → order → run
BaseTraversalInfo / ProjectTraversalInfo / GitTraversalInfo Traversal configuration (project vs git mode)
CommandContextTyped per-folder context with nature accessors
FilterPipeline / FolderSorterSelection and ordering
ItemResult / ToolResultPer-item and aggregated outcomes
TomBuildConfigTwo-tier YAML configuration loader

The framework holds no global mutable state across tools: a ToolDefinition is an immutable value, traversal is a pure scan over the filesystem, and a ToolRunner owns only the state of its own invocation.

---

Ecosystem

tom_build_base is the build-framework member of the tom_ai/basics foundation layer:

  • tom_basics — exceptions, logging, the platform seam, and

the runtime model. - tom_basics_console — the standalone/server platform implementation. - tom_basics_network — HTTP retry and LAN server discovery.

Downstream, the Tom CLI tools are all tom_build_base tools: buildkit, testkit, issuekit, and the code generators each ship a ToolDefinition and a set of executors and let this package supply everything else. That is the design rule for the workspace — shared CLI infrastructure lives here, never re-implemented in a tool. New capability that a tool needs is added to tom_build_base, published, and then consumed.

---

Further documentation

The doc/ folder holds the in-depth guides:

guide and API reference. - cli_tools_navigation.md — the navigation flag model and how traversal selection works. - modes_and_placeholders.md — modes and placeholder resolution. - multiws_pipelines_macros_defines.md — pipelines, multi-workspace runs, macros, and defines. - tool_inheritance_and_nesting.md — deriving tools (copyWith + CommandListOps) and nested tool wiring. - test_coverage.md — the test-coverage map.

See also ../README.md, the tom_ai/basics package map, and example/tom_build_base_example.dart.

---

Status

  • Version: 2.6.25
  • Tests: an extensive suite under test/ (dart test / testkit :test)

covering tool definition, argument parsing, help and completion generation, traversal, filtering and build-order, pipelines, macros and defines, and configuration loading. - Analysis: clean under package:lints (dart analyze — no issues). - Platforms: any Dart runtime with dart:io (desktop, server, CLI).

License
BSD 3-Clause License

Copyright (c) 2024-2026, Peter Nicolai Alexis Kyaw
Find me on LinkedIn under Alexis Kyaw
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.