Skip to content
Merged
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
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# AGENTS.md — `ditto` CLI (`@dittolive/cli`)
# AGENTS.md — `dittosh` CLI (`@dittolive/cli`)

## What this is

The Ditto CLI: an npm/Homebrew-installable TypeScript CLI (binary `ditto`) whose first command group, `ditto dql`, runs DQL statements against a local, offline-only Ditto store. Canonical spec: `plans/SDKS-4855-dql-cli-tool.md`. Working checklist: `plans/SDKS-4855-implementation-plan.md` (tick boxes as work lands).
The Ditto CLI: an npm/Homebrew-installable TypeScript CLI (binary `dittosh` — renamed from `ditto` to avoid clashing with the macOS/Linux `ditto` tool) whose first command group, `dittosh dql`, runs DQL statements against a local, offline-only Ditto store. Canonical spec: `plans/SDKS-4855-dql-cli-tool.md`. Working checklist: `plans/SDKS-4855-implementation-plan.md` (tick boxes as work lands).

## Hard rules (from the spec — do not regress)

Expand All @@ -16,7 +16,7 @@ The Ditto CLI: an npm/Homebrew-installable TypeScript CLI (binary `ditto`) whose
## Layout

- `src/cli/` — commander entry (`index.ts`), injected version (`version.ts`, tsup `define`), `groups/` per command group (`dql`, later `skills`, `system`)
- `src/config/` — data-dir resolution (`--data-dir` > `DITTO_DATA_DIR` > OS default), config dir (`DITTO_CONFIG_DIR` > OS default; env-paths caches homedir at module load, so tests must use this override, not `$HOME`), persisted state (one-time warnings, update cache)
- `src/config/` — data-dir resolution (`--data-dir` > `DITTOSH_DATA_DIR` > OS default), config dir (`DITTOSH_CONFIG_DIR` > OS default; env-paths caches homedir at module load, so tests must use this override, not `$HOME`), persisted state (one-time warnings, update cache)
- `src/identity/` — token loading (dev env / release reassembly), expiry
- `src/ditto/session.ts` — the only SDK touchpoint: init/open/close, log taming, lock mapping
- `src/query/` — statement classifier, splitter, param binding, result extraction, row cap
Expand Down
80 changes: 40 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Ditto CLI
# dittosh — the Ditto CLI

The command-line tool for [Ditto](https://www.ditto.live) — run DQL statements against a local, offline-only Ditto store, load realistic sample datasets, and get rich diagnostics (timing, EXPLAIN, PROFILE, ADVISE) in your terminal.
The command-line tool for [Ditto](https://www.ditto.live) — run DQL statements against a local, offline-only Ditto store, load realistic sample datasets, and get rich diagnostics (timing, EXPLAIN, PROFILE, ADVISE) in your terminal. The binary is `dittosh` (named to avoid clashing with the `ditto` tool shipped with macOS/Linux).

```
$ ditto dql "SELECT _id.title, _id.year, rated FROM movies WHERE _id.year > '2000' LIMIT 3"
$ dittosh dql "SELECT _id.title, _id.year, rated FROM movies WHERE _id.year > '2000' LIMIT 3"
┌─────────┬───────────────┬──────┐
│ rated │ title │ year │
├─────────┼───────────────┼──────┤
Expand All @@ -17,45 +17,45 @@ $ ditto dql "SELECT _id.title, _id.year, rated FROM movies WHERE _id.year > '200
## Installation

```bash
npm i -g @dittolive/cli # npm (primary)
brew install getditto/tap/ditto # Homebrew (macOS/Linux)
npm i -g @dittolive/cli # npm (primary)
brew install getditto/tap/dittosh # Homebrew (macOS/Linux)
```

The binary is `ditto`. Requires Node.js ≥ 20 for npm installs. Supported platforms (matching the Ditto Node SDK): **macOS arm64, Linux x64/arm64, Windows x64**. Intel Macs (darwin-x64) are not supported by SDK 5.1.0.
The binary is `dittosh`. Requires Node.js ≥ 20 for npm installs. Supported platforms (matching the Ditto Node SDK): **macOS arm64, Linux x64/arm64, Windows x64**. Intel Macs (darwin-x64) are not supported by SDK 5.1.0.

The CLI ships with a built-in offline license and runs entirely locally — no account, no credentials, no sync. `startSync()` is never called. All your data lives in one local directory (see [Data directory](#data-directory)).

## Quickstart

```bash
# check your install
ditto dql doctor
dittosh dql doctor

# load a sample dataset
ditto dql dataset load movies
dittosh dql dataset load movies

# query it
ditto dql "SELECT _id.title, _id.year FROM movies WHERE _id.year > '2000' LIMIT 5"
dittosh dql "SELECT _id.title, _id.year FROM movies WHERE _id.year > '2000' LIMIT 5"

# or run a curated catalog query by name (prints the statement, then results)
ditto dql dataset run single_result --dataset movies
dittosh dql dataset run single_result --dataset movies

# pipe results anywhere — stdout is always clean JSON when piped
ditto dql "SELECT title FROM movies" | jq '.[].title'
dittosh dql "SELECT title FROM movies" | jq '.[].title'
```

## Commands

### `ditto dql` — run DQL
### `dittosh dql` — run DQL

All four input modes:

```bash
ditto dql "SELECT * FROM movies WHERE year = 1994" # one-shot (statement arg)
ditto dql -e "SELECT * FROM movies LIMIT 5" # explicit statement form
ditto dql -f script.dql # run a file of statements
echo "SELECT * FROM movies LIMIT 3;" | ditto dql # piped stdin
ditto dql # interactive REPL
dittosh dql "SELECT * FROM movies WHERE year = 1994" # one-shot (statement arg)
dittosh dql -e "SELECT * FROM movies LIMIT 5" # explicit statement form
dittosh dql -f script.dql # run a file of statements
echo "SELECT * FROM movies LIMIT 3;" | dittosh dql # piped stdin
dittosh dql # interactive REPL
```

| Flag | Description |
Expand All @@ -76,24 +76,24 @@ ditto dql # interactive REPL
| `--apply` | apply ADVISE's suggested `CREATE INDEX` statements (prompts; `-y` skips) |
| `-y, --yes` | skip confirmation prompts |

### `ditto dql doctor`
### `dittosh dql doctor`

Platform/arch, Node version, data-directory writability, token validity + expiry, SDK load, and store-lock probe — with an exit code that says what's wrong.

### `ditto dql collections` / `ditto dql indexes [collection]`
### `dittosh dql collections` / `dittosh dql indexes [collection]`

List collections (`system:collections`) and indexes (`system:indexes`).

### `ditto dql dataset` — sample data
### `dittosh dql dataset` — sample data

Four built-in datasets vendored from Ditto's benchmark suites — movies, retail, retail-joins, pos — generated on the fly (nothing pre-generated ships in the package):

```bash
ditto dql dataset list # available datasets
ditto dql dataset show retail # shapes, setup indexes, full query catalog
ditto dql dataset load retail --docs 5000 # generate + insert (progress on stderr)
ditto dql dataset run stores__select__by_location_city --dataset retail
ditto dql dataset reset retail --yes # evict the dataset's collections
dittosh dql dataset list # available datasets
dittosh dql dataset show retail # shapes, setup indexes, full query catalog
dittosh dql dataset load retail --docs 5000 # generate + insert (progress on stderr)
dittosh dql dataset run stores__select__by_location_city --dataset retail
dittosh dql dataset reset retail --yes # evict the dataset's collections
```

`dataset run` prints the resolved statement (on stderr, so stdout stays clean), then executes it. Query names resolve across datasets; ambiguous names list the matches. `--setup` applies the entry's index DDL first; write-category catalog queries require `--yes` and clean up after themselves. `--seed <n>` reproduces a dataset exactly; changing seeds adds new documents (reset first for a clean slate).
Expand All @@ -102,41 +102,41 @@ ditto dql dataset reset retail --yes # evict the dataset's collection

`--no-color`, `--quiet` (suppress informational notes), `--no-update-check` (planned; update flow lands in a later milestone).

### `ditto skills` — install the DQL agent skill for AI coding agents
### `dittosh skills` — install the DQL agent skill for AI coding agents

```bash
ditto skills add # install the dql skill into all detected agents (global)
ditto skills add --project . # project-local install
ditto skills add --agent claude,opencode # specific agents
ditto skills list # what's installed where (with upstream ref)
ditto skills update # refresh installed skills from the latest upstream release
dittosh skills add # install the dql skill into all detected agents (global)
dittosh skills add --project . # project-local install
dittosh skills add --agent claude,opencode # specific agents
dittosh skills list # what's installed where (with upstream ref)
dittosh skills update # refresh installed skills from the latest upstream release
```

Mirrors the Android CLI's `android skills add` semantics: default skill is `dql`, global scope unless `--project <path>`, all detected agents unless `--agent <list>`. Targets: Claude Code (`~/.claude/skills/dql` or `.claude/skills/dql`), OpenCode (`~/.agents/skills/dql` or `.agents/skills/dql`), Codex (`~/.codex/skills/dql`), Gemini (`~/.gemini/skills/dql`), Cursor (`.cursor/rules/dql`, project-only), Copilot + Windsurf (project instruction files). While `getditto/agent-skills` is private, set `GITHUB_TOKEN` (e.g. `GITHUB_TOKEN=$(gh auth token)`).

### Planned for later milestones

`ditto version`, `ditto update` (self-update banner + channel-aware upgrade).
`dittosh version`, `dittosh update` (self-update banner + channel-aware upgrade).

## Data directory

Resolution order: **`--data-dir` flag → `DITTO_DATA_DIR` env var → OS default** (`~/Library/Application Support/ditto` on macOS, `~/.local/share/ditto` on Linux, `%LOCALAPPDATA%\ditto` on Windows). One process at a time per directory (a second one gets a clear lock error, exit 4).
Resolution order: **`--data-dir` flag → `DITTOSH_DATA_DIR` env var → OS default** (`~/Library/Application Support/dittosh` on macOS, `~/.local/share/dittosh` on Linux, `%LOCALAPPDATA%\dittosh` on Windows). One process at a time per directory (a second one gets a clear lock error, exit 4).

## Output & piping

- **stdout is sacred**: query results are the only thing on stdout (JSON when piped). Warnings, progress, banners, and SDK logs all go to stderr — so `ditto dql "SELECT …" | jq …` always works.
- **stdout is sacred**: query results are the only thing on stdout (JSON when piped). Warnings, progress, banners, and SDK logs all go to stderr — so `dittosh dql "SELECT …" | jq …` always works.
- Diagnostics (`--profile`/`--explain`/`--advise`) render as rich UI on a TTY and route to stderr when piped, so they never corrupt a pipe.
- Colors honor `NO_COLOR`, `CI`, `--no-color`, and non-TTY.
- Attachments appear as `[attachment …]` placeholders (attachment bytes can't flow through DQL).

## Diagnostics

```bash
ditto dql --time "SELECT …" # timing footer
ditto dql --explain "SELECT …" # operator plan tree
ditto dql --profile "SELECT …" # execution profile: summary strip + operator tree + hotspots (▲ = ≥50% of exec time)
ditto dql --advise "SELECT …" # index suggestions + ready-to-run CREATE INDEX statements
ditto dql --advise --apply -y "SELECT …" # apply them
dittosh dql --time "SELECT …" # timing footer
dittosh dql --explain "SELECT …" # operator plan tree
dittosh dql --profile "SELECT …" # execution profile: summary strip + operator tree + hotspots (▲ = ≥50% of exec time)
dittosh dql --advise "SELECT …" # index suggestions + ready-to-run CREATE INDEX statements
dittosh dql --advise --apply -y "SELECT …" # apply them
```

## Exit codes
Expand All @@ -151,7 +151,7 @@ ditto dql --advise --apply -y "SELECT …" # apply them

## REPL

Bare `ditto dql` starts an interactive session: multi-line statements terminated with `;`, history, per-statement timing, dot-commands (`.help`, `.collections`, `.indexes [name]`, `.break`, `.exit`).
Bare `dittosh dql` starts an interactive session: multi-line statements terminated with `;`, history, per-statement timing, dot-commands (`.help`, `.collections`, `.indexes [name]`, `.break`, `.exit`).

## Development

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "@dittolive/cli",
"version": "0.1.0",
"description": "The Ditto CLI \u2014 run DQL against a local Ditto store, load sample datasets, and install DQL skills for AI agents",
"description": "dittosh — the Ditto CLI: run DQL against a local Ditto store, load sample datasets, and install DQL skills for AI agents",
"license": "SEE LICENSE IN LICENSE.md",
"type": "module",
"bin": {
"ditto": "dist/cli.js"
"dittosh": "dist/cli.js"
},
"files": [
"dist"
Expand Down
2 changes: 1 addition & 1 deletion src/cli/default-command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* `ditto dql <stmt>` really means `ditto dql exec <stmt>`.
* `dittosh dql <stmt>` really means `dittosh dql exec <stmt>`.
*
* Commander can't put a default action on a command that also has
* subcommands without same-named options on the parent swallowing the
Expand Down
8 changes: 5 additions & 3 deletions src/cli/groups/dql/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ export function registerDatasetCommands(
scales_on: d.scalingDimension,
}));
console.log(renderRows(rows, format));
note("\n ditto dql dataset show <name> for details · ditto dql dataset load <name> to load");
note(
"\n dittosh dql dataset show <name> for details · dittosh dql dataset load <name> to load",
);
});

dataset
Expand Down Expand Up @@ -101,7 +103,7 @@ export function registerDatasetCommands(
}
console.log(chalk.bold("\nQuery catalog:"));
printQueryCatalog(suite);
note(`\nRun one with: ditto dql dataset run <query-name> --dataset ${suite.name}`);
note(`\nRun one with: dittosh dql dataset run <query-name> --dataset ${suite.name}`);
});

dataset
Expand Down Expand Up @@ -217,7 +219,7 @@ export function registerDatasetCommands(
if (!resolved) {
const hint = opts.dataset ? ` in dataset "${opts.dataset}"` : "";
console.error(
chalk.red(`Unknown query: ${queryName}${hint}. See: ditto dql dataset show <name>`),
chalk.red(`Unknown query: ${queryName}${hint}. See: dittosh dql dataset show <name>`),
);
process.exitCode = 2;
return;
Expand Down
4 changes: 2 additions & 2 deletions src/cli/groups/dql/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ export async function collectDoctorChecks(opts: DoctorOptions = {}): Promise<Doc
if (isBogusDataDir(opts.dataDir)) {
dirOk = false;
dirDetail = "bogus --data-dir value: expected a directory path";
} else if (!opts.dataDir?.trim() && isBogusDataDir(env.DITTO_DATA_DIR)) {
} else if (!opts.dataDir?.trim() && isBogusDataDir(env.DITTOSH_DATA_DIR)) {
dirOk = false;
dirDetail = "bogus DITTO_DATA_DIR value: expected a directory path";
dirDetail = "bogus DITTOSH_DATA_DIR value: expected a directory path";
} else if (fs.existsSync(dataDir)) {
if (!fs.statSync(dataDir).isDirectory()) {
dirOk = false;
Expand Down
12 changes: 6 additions & 6 deletions src/cli/groups/dql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async function openSession(opts: ExecOpts): Promise<DittoSession | null> {
// Bogus data-dir values (commander artifacts like `-d --`) fail fast for
// EVERY store-opening command. Mirror resolveDataDir's fallthrough: an
// empty/whitespace flag means the env var wins — check the EFFECTIVE value.
const rawDir = opts.dataDir?.trim() ? opts.dataDir : process.env.DITTO_DATA_DIR;
const rawDir = opts.dataDir?.trim() ? opts.dataDir : process.env.DITTOSH_DATA_DIR;
if (isBogusDataDir(rawDir)) {
console.error(chalk.red("-d/--data-dir requires a directory path"));
process.exitCode = 2;
Expand All @@ -60,7 +60,7 @@ async function openSession(opts: ExecOpts): Promise<DittoSession | null> {
if (days !== null && days < 0) {
console.error(
chalk.red(
`The embedded license token expired on ${identity.expiresOn}.\nUpdate the CLI: ditto update (or brew upgrade ditto / npm i -g @dittolive/cli@latest).`,
`The embedded license token expired on ${identity.expiresOn}.\nUpdate the CLI: dittosh update (or brew upgrade dittosh / npm i -g @dittolive/cli@latest).`,
),
);
process.exitCode = 3;
Expand All @@ -69,7 +69,7 @@ async function openSession(opts: ExecOpts): Promise<DittoSession | null> {
if (days !== null && days < EXPIRY_NAG_DAYS) {
console.error(
chalk.yellow(
`note: the embedded license token expires ${identity.expiresOn} (${days}d left) — update soon: ditto update`,
`note: the embedded license token expires ${identity.expiresOn} (${days}d left) — update soon: dittosh update`,
),
);
}
Expand Down Expand Up @@ -196,7 +196,7 @@ export function registerDqlGroup(dql: ReturnType<Command["command"]>): void {
});

// Execution subcommand (also the default — see rewriteDefaultSubcommand in
// the CLI entry, which maps `ditto dql <stmt>` → `ditto dql exec <stmt>`;
// the CLI entry, which maps `dittosh dql <stmt>` → `dittosh dql exec <stmt>`;
// an action directly on `dql` would swallow same-named child options).
dql
.command("exec")
Expand Down Expand Up @@ -288,7 +288,7 @@ export function registerDqlGroup(dql: ReturnType<Command["command"]>): void {
} else if (isBlankOrComments(statement)) {
console.error(
chalk.red(
'No statement given (input was only whitespace/comments). Usage: ditto dql "SELECT ..."',
'No statement given (input was only whitespace/comments). Usage: dittosh dql "SELECT ..."',
),
);
process.exitCode = 2;
Expand Down Expand Up @@ -340,7 +340,7 @@ export function registerDqlGroup(dql: ReturnType<Command["command"]>): void {
// REPL: no statement, no file, interactive terminal
if (!statement && !opts.file && !stdinPiped) {
if (!process.stdout.isTTY) {
console.error('No statement given. Usage: ditto dql "SELECT ..." (see --help)');
console.error('No statement given. Usage: dittosh dql "SELECT ..." (see --help)');
process.exitCode = 2;
return;
}
Expand Down
2 changes: 1 addition & 1 deletion src/cli/groups/dql/repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { dotHelp, makeReplEval } from "./repl-core.js";
import { type RunOptions, runStatement } from "./run.js";

/**
* Interactive REPL for `ditto dql` with no statement and a TTY on stdin.
* Interactive REPL for `dittosh dql` with no statement and a TTY on stdin.
* Logic lives in repl-core.ts (unit-tested); this is node:repl wiring.
*/
export async function startRepl(
Expand Down
8 changes: 4 additions & 4 deletions src/cli/groups/dql/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ export function validateOutPath(out: string): string | null {
return null;
}

/** Informational notes on stderr; suppressed by --quiet (DITTO_QUIET=1/true/yes). */
/** Informational notes on stderr; suppressed by --quiet (DITTOSH_QUIET=1/true/yes). */
export function note(message: string): void {
const v = process.env.DITTO_QUIET?.toLowerCase();
const v = process.env.DITTOSH_QUIET?.toLowerCase();
if (v === "1" || v === "true" || v === "yes") return;
console.error(chalk.dim(message));
}
Expand Down Expand Up @@ -223,8 +223,8 @@ export async function runStatement(
// result sets); an explicit --max-rows still caps them.
const rowsForFile = opts.maxRowsExplicit ? shown : rows;
const format = opts.out ? formatForOutFile(opts.out, opts.format) : resolveFormat(opts.format);
if (format === "json") process.env.DITTO_JSON_OUT = "1"; // the update banner never appears in JSON mode
if (format === "json") process.env.DITTO_JSON_OUT = "1"; // the update banner never appears in JSON mode
if (format === "json") process.env.DITTOSH_JSON_OUT = "1"; // the update banner never appears in JSON mode
if (format === "json") process.env.DITTOSH_JSON_OUT = "1"; // the update banner never appears in JSON mode

if (opts.out) {
// Files never get ANSI escapes, even when the terminal is colored.
Expand Down
Loading
Loading