xPulse
πŸ‡¬πŸ‡§ EN

@xpulse/cli – Component Spec

Status: APPROVED Β· Updated March 2026 Parent: GLOBAL_concept-ecosystem.md ADR: COMP_cli_adr-001-command-registration.md


Principle

@xpulse/cli is a runner – not a command owner.

The package provides the xpulse entry point, the command parser, the Command base class, Input/Output, and autodiscovery. Which commands exist is decided by each xPart itself – by shipping command classes under src/commands/.

xpulse doc:fetch chat -tag="v1.3.0" --dry-run -V
↓
@xpulse/cli discovers node_modules/@xpulse/*/src/commands/*.js
finds FetchCommand (name = 'doc:fetch' from configure())
builds Input + Output, injects keepAlive
calls new FetchCommand().run(input, output)
↓
FetchCommand.run() executes, returns exit code
↓
@xpulse/cli process.exit(code)

Dependencies

@xpulse/cli
└── @xpulse/event ← cli:keepalive (internal, xParts do not see this)

Command Syntax

xpulse namespace:area:command [arguments] [-option="value"] [--flag] [-V] [-q]
Part Description Example
namespace Domain of the responsible xPart repo, doc, project
area Sub-area (optional) changelog, fetch
command Action generate, fetch, show
argument Positional argument chat
-option="value" Named option -tag="v1.3.0"
--flag Boolean, camelCase --dry-run β†’ dryRun: true

Command Base Class (`src/command.js`)

xParts import Command and extend it in src/commands/. A command describes itself in configure() and implements run(). It knows nothing about events, timeouts, or the CLI itself.

import { Command } from '@xpulse/cli/command';
export class FetchCommand extends Command {
configure() {
this
.setName('doc:fetch')
.setAlias('df')
.setDescription('Fetch docs via git archive')
.setHelp('Fetches docs from an xPulse repo and stores them locally.')
.addArgument('tool', null, 'Name of the tool (e.g. chat)')
.addOption('tag', null, 'Git tag to fetch')
.addFlag('dry-run', false, 'Simulate only, write nothing');
}
async run(input, output) {
const tool = input.getArgument('tool');
const dryRun = input.getFlag('dryRun');
while (!done) {
await doWork();
this.keepAlive(); // Reset timeout
}
output.success('Done!');
return 0;
}
}

Fluent API

Method Description
setName(name) Command name – namespace:area:command
setAlias(alias) Short name (e.g. df for doc:fetch)
setDescription(text) Short description for xpulse --help
setHelp(text) Longer text for xpulse --help <command>
addArgument(name, default, description) Positional argument
addOption(name, default, description) Named option (-key=value)
addFlag(name, default, description) Boolean flag (--flag)

Exit Codes

Code Meaning
0 Success
1 Error
2 Misuse – wrong or missing arguments

`keepAlive()`

Resets the timeout timer. Injected by @xpulse/cli before run(). No import of @xpulse/event required – the command abstracts this completely.


Input (`src/input.js`)

input.getArgument('tool') // positional argument by name
input.getOption('tag') // -key=value, kebab-case or camelCase
input.hasOption('tag') // β†’ true | false
input.getFlag('dryRun') // --flag, kebab-case or camelCase
input.isVerbose() // --verbose / -V
input.isQuiet() // --quiet / -q
input.isAnsi() // false if --no-ansi

Output (`src/output.js`)

Respects --no-ansi and --quiet automatically.

output.write('...') // without line break
output.writeln('...') // with line break
output.success('...') // βœ“ green
output.warn('...') // ! yellow
output.error('...') // βœ— red – ignores --quiet
output.table(headers, rows) // formatted table

Global CLI Flags

Flag Shortcut Description
--help [command] -h CLI help or command help
--list – All command names, machine-readable (one per line)
--version -v Print version
--verbose -V Verbose output β†’ input.isVerbose()
--quiet -q Errors only β†’ input.isQuiet()
--no-ansi – No colour β†’ input.isAnsi() = false

`--help` Behaviour

xpulse --help # all commands with name, alias, description
xpulse --help doc:fetch # name, alias, description, help text,
# usage example, arguments, options, flags

`--list` Behaviour

xpulse --list
# doc:fetch
# doc:fetch:changelog
# project:config:show
# repo:changelog:generate

Machine-readable – one command name per line, no ANSI, no padding.


Autodiscovery

Search order on startup:

1. node_modules/@xpulse/*/src/commands/*.js ← installed xParts
2. src/commands/*.js ← local project

Both paths are merged. The local project therefore behaves like an implicit xPart – no symlink, no npm link required.


Timeout

CLI_TIMEOUT=30 # seconds, default: 30

Timer starts with run(). Fully reset by this.keepAlive(). No cli:done – run() resolves with exit code, @xpulse/cli terminates the process.


Flow

xpulse doc:fetch chat -tag="v1.3.0" --dry-run -V
↓
@xpulse/cli starts
β†’ discovers node_modules/@xpulse/*/src/commands/*.js
β†’ parses input β†’ { name: 'doc:fetch', args: { tag: 'v1.3.0', dryRun: true },
rawArguments: ['chat'], verbose: true, ansi: true }
β†’ finds FetchCommand (name = 'doc:fetch')
β†’ builds Input + Output
β†’ injects cmd._keepAliveFn = startTimer
β†’ startTimer()
β†’ exitCode = await cmd.run(input, output)
β†’ clearTimeout + process.exit(exitCode)

Known Commands (Examples)

Command Provided by
doc:fetch @xpulse/doc
project:config:show @xpulse/config
repo:changelog:generate @xpulse/repo (TBD)
release:prepare @xpulse/release (TBD)

Package Structure

@xpulse/cli/
bin/
xpulse.js ← entry point: autodiscovery, parser, Input/Output, timeout
src/
command.js ← Command base class with configure(), keepAlive(), run()
input.js ← Input class: getArgument, getOption, getFlag, isVerbose ...
output.js ← Output class + internal helpers (--help, --list, --version)
loader.js ← find + import node_modules/@xpulse/*/src/commands/*.js
parser.js ← CLI input β†’ { name, args, rawArguments, verbose, quiet, ansi }
docs/
index.md
de/ index, guide, api, schema
en/ index, guide, api, schema
test/
command.test.js
input.test.js
parser.test.js
loader.test.js
README.md
package.json
xpulse.json

Installation & Usage

@xpulse/cli is installed locally in the project – no global install required. It is always invoked via npx.

npm install @xpulse/cli

Important: The project's package.json must contain "type": "module", since the entire xPulse ecosystem uses ESM:

{
"name": "my-project",
"type": "module",
"dependencies": {
"@xpulse/cli": "^1.0.0"
}
}

Invocation – always with `npx`

npx xpulse --version
npx xpulse --list
npx xpulse --help
npx xpulse --help doc:fetch
npx xpulse doc:fetch chat -tag="v1.3.0" --dry-run

npx always uses the local version from node_modules/.bin/xpulse – consistent, no version mismatch with globally installed versions.


Argument vs. Option – Syntax Rules

Type Definition Invocation
Argument addArgument('name', ...) npx xpulse cmd Johnny (positional)
Option addOption('name', ...) npx xpulse cmd -name="Johnny"
Flag addFlag('dry-run', ...) npx xpulse cmd --dry-run

Important: Options must always be passed with =:

npx xpulse cmd -name="Johnny" # βœ“ correct
npx xpulse cmd --name="Johnny" # βœ“ correct
npx xpulse cmd -name Johnny # βœ— warning – Johnny is interpreted as positional argument
npx xpulse cmd --name Johnny # βœ— warning – Johnny is interpreted as positional argument

If the syntax is wrong, @xpulse/cli outputs a warning:

! Invalid syntax: "--name Johnny" – did you mean --name="Johnny"?
"Johnny" is interpreted as a positional argument.
en/spec.md 2026-03-27