This document describes the current basectl execution contract. It is about
what happens after a user invokes Base, not the future project-discovery or
Python orchestration layers.
bin/basectl is the public control-plane command for Base. Base exposes one
public executable directory: $BASE_HOME/bin.
basectl is responsible for deciding what kind of invocation the user asked
for. It then delegates runtime setup to base_init.sh.
At a high level, basectl can:
- start a Base-enabled interactive Bash shell
- run the umbrella Base command dispatcher
- run a Base command implementation by convention
- run an explicit Bash script path inside the Base runtime
basectl derives BASE_HOME from its own location. In the normal layout,
bin/basectl lives directly under $BASE_HOME/bin, so the parent of bin/ is
Base home.
This is intentionally a filesystem-layout contract, not a Git contract. Base
does not require $BASE_HOME to be the root of a Git repository. That keeps the
same runtime model usable when Base is checked out as its own repo or embedded
inside a larger repository.
BASE_HOME is always the physical Base installation root. It is not the
developer workspace root, and it is not changed by ~/.base.d/config.yaml.
Project-discovery commands use the configured workspace.root from
~/.base.d/config.yaml when present, then fall back to BASE_HOME's parent for
source-checkout layouts.
The Base home check validates the expected Base files instead of checking for a
.git directory.
When basectl starts, it uses this dispatch order:
- If no arguments are provided and stdin/stdout are attached to a terminal, start a Base-enabled interactive Bash shell with Base's project virtual environment activated, while preserving the caller's current directory.
- If the first argument is path-like or names an existing file, treat it as a Bash script path and run that script inside the Base runtime.
- If the first argument matches a Base command implementation by convention, run that command implementation.
- Otherwise, run the umbrella Base command dispatcher.
This ordering lets explicit script paths win over command names.
A first argument is treated as a script when either condition is true:
- it contains
/, such as./script.sh,scripts/deploy, or/tmp/base-task.sh - it is an existing file in the current directory, such as
deploy.shordeploy
Script files do not need a .sh extension. The script is sourced as Bash and
must define a main function. After sourcing the script, basectl calls
main "$@" with the remaining arguments.
For command naming, a trailing .sh is stripped from
BASE_BASH_COMMAND_NAME. Other extensions are left intact.
Examples:
basectl ./scripts/deploy.sh prod
basectl scripts/deploy prod
basectl deploy.sh prod # works if deploy.sh exists in the current directoryA script can also opt into Base with a shebang:
#!/usr/bin/env basectl
main() {
local project="${1:-}"
if [[ -z "$project" ]]; then
print_error "Project name is required."
return 2
fi
log_info "Checking project '$project'."
run git status --short
}In shebang mode, the script defines main but does not call main "$@"
itself. The operating system runs basectl with the script path as an
argument. basectl then:
- resolves
BASE_HOME - treats the script path as an explicit Bash script
- exports runtime metadata such as
BASE_BASH_COMMAND_NAME,BASE_BASH_COMMAND_DIR, andBASE_BASH_COMMAND_SCRIPT - sources
base_init.sh, which loads Base runtime variables and the Bash standard library - sources the script
- calls
mainwith the remaining user arguments
This makes Base stdlib helpers such as log_info, print_error,
fatal_error, run, assert_command_exists, and import_base_lib available
without the script sourcing lib_std.sh directly.
Standalone Bash scripts that are not intended to run through Base can still source the standard library directly:
#!/usr/bin/env bash
source "/path/to/base/lib/bash/std/lib_std.sh"
main() {
run echo "hello"
}
main "$@"Base command implementations are found by convention:
$BASE_HOME/cli/bash/commands/<command>/<command>.sh
For example:
basectl exampleloads:
$BASE_HOME/cli/bash/commands/example/example.sh
A command implementation is sourced as Bash and must define main.
The umbrella command implementation lives at:
$BASE_HOME/cli/bash/commands/basectl/basectl.sh
It owns the current Base subcommands. The canonical, always-current command
list is basectl --help; this list summarizes the shipped public surface:
setupcheckcleandoctoractivatetestbuildrundemorepologsworkspaceonboardghconfigupdateprojects listupdate-profileversionhelp
Subcommand modules for the umbrella command live under:
$BASE_HOME/cli/bash/commands/basectl/subcommands/
Base-owned convenience commands in $BASE_HOME/bin should be tiny real launcher
files, not symlinks. They delegate to basectl and keep the public command
surface in one place.
Example for a hypothetical Base-owned Bash command:
#!/usr/bin/env bash
exec "$(dirname "$0")/basectl" example "$@"The implementation still lives under cli/bash/commands/<command>/ with its
local README and tests.
Optional utility CLIs such as caff and sort-in-place live in
codeforester/base-platform-tools
instead of Base core.
When base-platform-tools is checked out next to Base and contains both
base_manifest.yaml and bin/, Base's shell startup snippets add its bin/
directory to PATH after $BASE_HOME/bin. Runtime project shells keep project
bin/ entries behind both Base and Base Platform Tools.
base_init.sh is the runtime bootstrap layer. It is sourced after basectl
has decided what should run.
base_init.sh establishes the Base runtime contract, including:
- exported Base environment variables such as
BASE_HOME,BASE_BIN_DIR,BASE_BASH_COMMANDS_DIR, andBASE_BASH_LIB_DIR - OS and host metadata such as
BASE_OSandBASE_HOST - Base's Bash standard library
import_base_lib, the convention-based helper for sourcing Base Bash libraries- PATH additions needed by Base runtime execution
The full variable list, ownership rules, readonly policy, and ~/.baserc
behavior are documented in Runtime Environment.
Downstream Bash scripts should import Base Bash libraries with:
import_base_lib file/lib_file.shimport_base_lib fails through Base standard error handling when the requested
library cannot be found, so callers do not need to duplicate that check.
Running basectl activate <project> starts an interactive Bash shell with the
Base runtime loaded and the project virtual environment activated. Running
basectl with no arguments in a terminal starts the Base project runtime while
preserving the caller's current directory.
That shell uses Base's runtime rcfile:
$BASE_HOME/lib/bash/runtime/bashrc
The runtime rcfile sources base_init.sh, sources the user's ~/.bashrc once
with guardrails, activates the project virtual environment, sources any
manifest-declared activate.source scripts, and then sets the Base runtime
prompt. This gives the user their normal interactive Bash behavior while also
making Base stdlib functions such as import_base_lib available during user
Bash startup. Base still owns the final runtime prompt.
Bash startup paths share lib/shell/baserc_guard.sh for safe ~/.baserc
loading and Base-owned variable protection. See
Runtime Environment for the exact variables users may
set there.
The normal shell-startup snippets under lib/shell/ do not source
base_init.sh. They only manage Bash/Zsh startup concerns, including:
- deriving
BASE_HOMEfor the managed snippet - adding
$BASE_HOME/bintoPATH - adding an optional sibling
base-platform-tools/bintoPATH - loading simple user preferences from
~/.baserc - enabling optional shell defaults when requested
The full Base runtime is loaded only through the basectl command path.
The current execution model does not yet define:
- Python command dispatch
- project discovery
- project activation
- release automation around version numbers
- Linux support beyond the current macOS implementation
Those features should build on this execution contract rather than bypass it.