Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 67 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

CLI tool for managing projects on [bool.com](https://bool.com).

`bool` follows the [Printing Press](https://printingpress.dev/) agent-native CLI conventions: machine-readable output by default when piped, typed exit codes, and standard flags on every command.

## Installation

```bash
Expand All @@ -15,18 +17,60 @@ This installs the `bool` command globally.
```bash
bool auth login # Paste your API key from the bool.com web UI
bool auth status # Verify connection
bool auth doctor # Diagnose auth + API connectivity
```

Your API key is saved to `~/.config/bool-cli/config.json`. You can also set the `BOOL_API_KEY` environment variable, or pipe a key into login:

```bash
echo "$BOOL_API_KEY" | bool auth login
```

## Agent-native flags

These flags work on every command. They can appear in any position.

| Flag | Description |
|---|---|
| `--json` | Output structured JSON. Implicit when stdout is piped. |
| `--csv` | Output CSV. |
| `--select <fields>` | Comma-separated keys to keep in structured output (e.g. `--select slug,visibility`). |
| `--compact` | Keep only high-gravity fields (`id`, `slug`, `name`, `status`, `visibility`, `version_number`, `file_count`, `created_at`, `updated_at`, `url`). |
| `--quiet` | Suppress status messages on stderr. |
| `--no-color` | Disable ANSI colors (also honors `NO_COLOR`). |
| `--no-input` | Fail instead of prompting for interactive input. |
| `--dry-run` | Show what would happen without making changes. |

Status messages (`✔ ✖ ℹ ⚠`) go to **stderr**. Structured data goes to **stdout**. This means you can pipe JSON without losing log output:

```bash
bool list | jq '.[].slug'
bool show my-project --select slug,visibility,url
bool list --csv > bools.csv
```

Your API key is saved to `~/.config/bool-cli/config.json`. You can also set the `BOOL_API_KEY` environment variable.
## Exit codes

| Code | Meaning |
|---|---|
| `0` | Success |
| `2` | Usage error (missing/invalid argument) |
| `3` | Not found |
| `4` | Authentication failure |
| `5` | API error |
| `7` | Rate limited |

Errors are written to stderr in the format `✖ <message>` followed by an indented hint pointing at the flag, value, or remediation step — so agents can self-correct in one retry without parsing free-text errors.

## Commands

### Authentication

| Command | Description |
|---|---|
| `bool auth login` | Save API key |
| `bool auth login` | Save API key (reads from stdin or prompts) |
| `bool auth status` | Check auth + API health |
| `bool auth doctor` | Diagnose auth + API connectivity |

### Ship It

Expand Down Expand Up @@ -63,11 +107,7 @@ Aliases:
- `bool get` / `bool info` for `bool show`
- `bool rm` for `bool delete`

Deprecated but still supported:
- `bool bools ...` commands now print a deprecation warning and map to top-level commands.
- `bool bools visibility ...` is deprecated; use `bool update [slug] --visibility <value>`.

> **Slug resolution:** When `[slug]` is omitted, commands read it from the `.bool/config` file in the current directory. This file is created automatically by `shipit`, `deploy`, `pull`, and `show` (or `info` alias).
> **Slug resolution:** When `[slug]` is omitted, commands read it from the `.bool/config` file in the current directory. This file is created automatically by `shipit`, `deploy`, `pull`, and `show`.

### Versions & Deployment

Expand Down Expand Up @@ -99,15 +139,22 @@ bool pull my-project ./local-copy --version 3

- `--version` — Specific version number (default: latest)

### JSON output
### Claim

All commands support `--json` for machine-readable output:
```bash
bool claim [slug-or-directory] [--secret <secret>]
```

Transfers an anonymous Bool to your account using the secret stored in `.bool/config`.

### Skill

```bash
bool list --json
bool show my-project --json
bool skill [directory]
```

Installs the bool-cli agent skill into the target directory's `.agents/` folder so coding agents discover the available CLI surface.

## Project Config

Running `shipit`, `deploy`, `pull`, or `show` creates a `.bool/config` file in the project directory. This JSON file stores `slug` and `name` so you can run commands without specifying the slug each time:
Expand All @@ -126,16 +173,20 @@ Add `.bool/` to your `.gitignore`.
```
bool-cli/
bin/
bool.js # Entry point
bool.js # Entry point + global flags
src/
commands/
auth.js # auth login, auth status
bools.js # top-level bool commands (+ deprecated bools wrappers)
auth.js # auth login, status, doctor
bools.js # list, create, show, update, delete, open
shipit.js # shipit (anonymous create + deploy)
versions.js # versions, deploy, pull
claim.js # claim anonymous Bool
skill.js # install agent skill
utils/
api.js # API client (fetch-based)
action.js # action wrapper: typed errors + global flag plumbing
api.js # API client → typed CliError on HTTP failure
config.js # Global config + project-level .bool/config
exit.js # Typed exit codes (Printing Press convention)
files.js # File reading, .boolignore, binary detection
output.js # Output formatting (tables, colors, JSON)
output.js # Output: auto-JSON on pipe, CSV, --select, --compact
```
20 changes: 17 additions & 3 deletions bin/bool.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node

import { createRequire } from 'node:module';
import { Command } from 'commander';
import { Command, Option } from 'commander';
import { register as auth } from '../src/commands/auth.js';
import { register as bools } from '../src/commands/bools.js';
import { register as shipit } from '../src/commands/shipit.js';
Expand All @@ -17,7 +17,18 @@ const program = new Command();
program
.name('bool')
.description('CLI for bool.com')
.version(version);
.version(version)
.configureHelp({ showGlobalOptions: true })
// Agent-native global flags (Printing Press conventions). Subcommands inherit
// these via cmd.optsWithGlobals() in the action wrapper.
.option('--json', 'Output as JSON (auto when stdout is piped)')
.option('--csv', 'Output as CSV')
.option('--select <fields>', 'Comma-separated keys to keep in structured output')
.option('--compact', 'Keep only high-gravity fields (id, slug, name, status, timestamps)')
.option('--quiet', 'Suppress status messages on stderr')
.addOption(new Option('--no-color', 'Disable ANSI colors (also honors NO_COLOR)'))
.option('--no-input', 'Fail instead of prompting for interactive input')
.option('--dry-run', 'Show what would happen without making changes');

auth(program);
bools(program);
Expand All @@ -26,4 +37,7 @@ versions(program);
skill(program);
claim(program);

program.parse();
program.parseAsync(process.argv).catch((err) => {
process.stderr.write(`✖ ${err.message}\n`);
process.exit(2);
});
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

138 changes: 115 additions & 23 deletions src/commands/auth.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { createInterface } from 'node:readline';
import { getApiKey, setApiKey } from '../utils/config.js';
import { healthCheck } from '../utils/api.js';
import { success, error, info } from '../utils/output.js';
import { getApiKey, setApiKey, getApiUrl } from '../utils/config.js';
import { healthCheck, get } from '../utils/api.js';
import { success, info, data as printData } from '../utils/output.js';
import { action } from '../utils/action.js';
import { CliError, EXIT } from '../utils/exit.js';

function prompt(question) {
function prompt(question, { noInput }) {
if (noInput) {
throw new CliError('Interactive prompt required but --no-input is set.', EXIT.USAGE, {
hint: 'Pipe the API key on stdin or set BOOL_API_KEY.',
});
}
const rl = createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(question, (answer) => {
Expand All @@ -13,43 +20,128 @@ function prompt(question) {
});
}

async function readStdin() {
if (process.stdin.isTTY) return null;
let buf = '';
for await (const chunk of process.stdin) buf += chunk;
return buf.trim() || null;
}

export function register(program) {
const auth = program
.command('auth')
.description('Manage authentication');
const auth = program.command('auth').description('Manage authentication');

auth
.command('login')
.description('Save API key')
.action(async () => {
const key = await prompt('Enter your bool.com API key: ');
.description('Save API key (reads from stdin or prompts)')
.action(action(async (opts) => {
const piped = await readStdin();
const key = piped || await prompt('Enter your bool.com API key: ', { noInput: opts.input === false });
if (!key) {
error('No API key provided.');
process.exit(1);
throw new CliError('No API key provided.', EXIT.USAGE, {
hint: 'Pipe the key: echo $KEY | bool auth login',
});
}
if (opts.dryRun) {
info('[dry-run] Would save API key to ~/.config/bool-cli/config.json');
return;
}
setApiKey(key);
success('API key saved to ~/.config/bool-cli/config.json');
});
}));

auth
.command('status')
.description('Check auth and API health')
.action(async () => {
.action(action(async () => {
const key = getApiKey();
if (key) {
const masked = key.slice(0, 8) + '…' + key.slice(-4);
info(`API key: ${masked}`);
} else {
error('No API key configured. Run: bool auth login');
process.exit(1);
if (!key) {
throw new CliError('No API key configured.', EXIT.AUTH, {
hint: 'Run: bool auth login',
});
}
const masked = key.slice(0, 8) + '…' + key.slice(-4);

let healthError = null;
try {
await healthCheck();
} catch (err) {
healthError = err.message;
}
const healthy = healthError == null;

const shaped = printData({
api_key: masked,
api_url: getApiUrl(),
healthy,
...(healthy ? {} : { error: healthError }),
});
if (shaped === undefined) {
if (!healthy) process.exitCode = EXIT.API;
return;
}

info(`API key: ${masked}`);
info(`API URL: ${getApiUrl()}`);
if (healthy) {
success('API is reachable and healthy.');
} else {
throw new CliError(`API health check failed: ${healthError}`, EXIT.API);
}
}));

auth
.command('doctor')
.description('Diagnose auth + API connectivity and report what to fix')
.action(action(async () => {
const checks = [];

// 1. Key present?
const key = getApiKey();
const source = process.env.BOOL_API_KEY ? 'BOOL_API_KEY env var' : (key ? 'config file' : 'none');
checks.push({
check: 'api_key_present',
ok: Boolean(key),
detail: key ? `loaded from ${source}` : 'missing — run: bool auth login',
});

// 2. Health endpoint reachable (no auth)?
try {
await healthCheck();
checks.push({ check: 'api_reachable', ok: true, detail: getApiUrl() });
} catch (err) {
error(`API health check failed: ${err.message}`);
process.exit(1);
checks.push({ check: 'api_reachable', ok: false, detail: err.message });
}
});

// 3. Authenticated request works?
if (key) {
try {
await get('/bools/');
checks.push({ check: 'api_key_valid', ok: true, detail: 'authenticated request succeeded' });
} catch (err) {
checks.push({ check: 'api_key_valid', ok: false, detail: err.message });
}
} else {
checks.push({ check: 'api_key_valid', ok: false, detail: 'skipped — no key configured' });
}

const allOk = checks.every((c) => c.ok);
const report = { ok: allOk, checks };

const shaped = printData(report);
if (shaped === undefined) {
if (!allOk) process.exitCode = EXIT.AUTH;
return;
}

for (const c of checks) {
if (c.ok) success(`${c.check}: ${c.detail}`);
else info(`✖ ${c.check}: ${c.detail}`);
}
if (!allOk) {
throw new CliError('Doctor reported failures.', EXIT.AUTH, {
hint: 'Re-run after fixing the items above, or pass --json for machine-readable output.',
});
}
success('All checks passed.');
}));
}
Loading