Entry point: src/presentation/cli/index.ts
The bootstrap() function runs four sequential steps:
initializeContainer() opens SQLite, runs migrations, registers repositories and use cases. Exposes the container on globalThis.__shepContainer for the web UI’s server-side code.InitializeSettingsUseCase from the container, executes it to load/create settings, then calls initializeSettings(settings) to populate the in-memory singleton.onboardingWizard()). The wizard is lazy-imported to avoid startup cost when already complete.Command('shep'), registers subcommands, calls program.parseAsync(). The default action (no subcommand) starts the daemon via startDaemon().async function bootstrap() {
await initializeContainer();
(globalThis as Record<string, unknown>).__shepContainer = container;
const initializeSettingsUseCase = container.resolve(InitializeSettingsUseCase);
const settings = await initializeSettingsUseCase.execute();
initializeSettings(settings);
// First-run onboarding gate (TTY only)
if (process.stdin.isTTY) {
const onboardingCheck = new CheckOnboardingStatusUseCase();
const { isComplete } = await onboardingCheck.execute();
if (!isComplete) {
const { onboardingWizard } = await import('../tui/wizards/onboarding/onboarding.wizard.js');
await onboardingWizard();
}
}
const program = new Command()
.name('shep')
.version(version, '-v, --version')
.action(async () => {
await startDaemon();
});
program.addCommand(createVersionCommand());
program.addCommand(createSettingsCommand());
// ... all other commands
await program.parseAsync();
}
reflect-metadata is imported at the very top of the file (before any other imports) as required by tsyringe.
Every command is a factory function returning a Command instance:
export function createXxxCommand(): Command {
return new Command('name')
.description('...')
.addOption(...)
.addHelpText('after', '...')
.action((options) => { ... });
}
create<Name>Command(), exported from <name>.command.ts.index.ts) adds child commands via .addCommand(). See commands/settings/index.ts.new Option(...) with .choices() for enum-like values, .default() for defaults..addHelpText('after', ...) with leading $ for command examples.async action handlers; Commander calls parseAsync() to support them.commands/
version.command.ts # Top-level command
run.command.ts # shep run
ui.command.ts # shep ui
start.command.ts # shep start (daemon)
stop.command.ts # shep stop (daemon)
restart.command.ts # shep restart (daemon)
status.command.ts # shep status (daemon)
_serve.command.ts # shep serve (hidden, internal)
upgrade.command.ts # shep upgrade
install.command.ts # shep install
ide-open.command.ts # shep ide-open
tools.command.ts # shep tools (group)
settings/
index.ts # settings command group
show.command.ts # shep settings show
init.command.ts # shep settings init
agent.command.ts # shep settings agent
ide.command.ts # shep settings ide
workflow.command.ts # shep settings workflow
model.command.ts # shep settings model
feat/
index.ts # feat command group
new.command.ts # shep feat new
ls.command.ts # shep feat ls
show.command.ts # shep feat show
del.command.ts # shep feat del
resume.command.ts # shep feat resume
review.command.ts # shep feat review
approve.command.ts # shep feat approve
reject.command.ts # shep feat reject
logs.command.ts # shep feat logs
agent/
index.ts # agent command group
ls.command.ts # shep agent ls
show.command.ts # shep agent show
stop.command.ts # shep agent stop
logs.command.ts # shep agent logs
delete.command.ts # shep agent delete
approve.command.ts # shep agent approve
reject.command.ts # shep agent reject
repo/
index.ts # repo command group
ls.command.ts # shep repo ls
show.command.ts # shep repo show
session/
index.ts # session command group
ls.command.ts # shep session ls
show.command.ts # shep session show
daemon/
start-daemon.ts # Daemon start logic
stop-daemon.ts # Daemon stop logic
To add a new command group:
commands/<group>/index.ts with createGroupCommand().<action>.command.ts.program.addCommand(createGroupCommand()) in index.ts.Commands access application services through two mechanisms:
import { container } from '@/infrastructure/di/container';
const useCase = container.resolve(SomeUseCase);
await useCase.execute();
Used during bootstrap for InitializeSettingsUseCase.
import { getSettings } from '@/infrastructure/services/settings.service';
const settings = getSettings(); // Returns Settings object
The getSettings() singleton is the preferred way to access settings in command handlers. It avoids re-resolving from the DI container on every call. The singleton is set once during bootstrap and is read-only thereafter. The settings init command uses resetSettings() + initializeSettings() to replace the singleton in-place.
Each command action wraps its body in try/catch. On error:
messages.error(message, error) to display the error.process.exitCode = 1 (do not call process.exit() from command handlers)..action((options) => {
try {
// command logic
} catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
messages.error('Failed to do X', err);
process.exitCode = 1;
}
});
Bootstrap wraps the entire sequence in try/catch. Each step has its own inner try/catch that logs the specific error with messages.error(), then re-throws. The outer catch calls process.exit(1).
Registered at module level for safety:
process.on('uncaughtException', ...) – logs and exits.process.on('unhandledRejection', ...) – logs and exits.Error stack traces are only printed when the DEBUG environment variable is set. This applies to messages.error() and messages.debug().
--version / -v prints version number only.version subcommand prints detailed info (name, description, Node version, platform).addHelpText('after', ...) use $ shep <command> prefix format.