diff --git a/AGENTS.md b/AGENTS.md index 086ece1..8af580e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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) @@ -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 diff --git a/README.md b/README.md index f6175eb..c8965db 100644 --- a/README.md +++ b/README.md @@ -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 │ ├─────────┼───────────────┼──────┤ @@ -17,11 +17,11 @@ $ 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)). @@ -29,33 +29,33 @@ The CLI ships with a built-in offline license and runs entirely locally — no a ```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 | @@ -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 ` reproduces a dataset exactly; changing seeds adds new documents (reset first for a clean slate). @@ -102,29 +102,29 @@ 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 `, all detected agents unless `--agent `. 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). @@ -132,11 +132,11 @@ Resolution order: **`--data-dir` flag → `DITTO_DATA_DIR` env var → OS defaul ## 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 @@ -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 diff --git a/package.json b/package.json index 9847756..1cd615d 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/cli/default-command.ts b/src/cli/default-command.ts index 47d4403..156870a 100644 --- a/src/cli/default-command.ts +++ b/src/cli/default-command.ts @@ -1,5 +1,5 @@ /** - * `ditto dql ` really means `ditto dql exec `. + * `dittosh dql ` really means `dittosh dql exec `. * * Commander can't put a default action on a command that also has * subcommands without same-named options on the parent swallowing the diff --git a/src/cli/groups/dql/dataset.ts b/src/cli/groups/dql/dataset.ts index 7a30928..8499cc4 100644 --- a/src/cli/groups/dql/dataset.ts +++ b/src/cli/groups/dql/dataset.ts @@ -59,7 +59,9 @@ export function registerDatasetCommands( scales_on: d.scalingDimension, })); console.log(renderRows(rows, format)); - note("\n ditto dql dataset show for details · ditto dql dataset load to load"); + note( + "\n dittosh dql dataset show for details · dittosh dql dataset load to load", + ); }); dataset @@ -101,7 +103,7 @@ export function registerDatasetCommands( } console.log(chalk.bold("\nQuery catalog:")); printQueryCatalog(suite); - note(`\nRun one with: ditto dql dataset run --dataset ${suite.name}`); + note(`\nRun one with: dittosh dql dataset run --dataset ${suite.name}`); }); dataset @@ -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 `), + chalk.red(`Unknown query: ${queryName}${hint}. See: dittosh dql dataset show `), ); process.exitCode = 2; return; diff --git a/src/cli/groups/dql/doctor.ts b/src/cli/groups/dql/doctor.ts index b580534..3ac541b 100644 --- a/src/cli/groups/dql/doctor.ts +++ b/src/cli/groups/dql/doctor.ts @@ -72,9 +72,9 @@ export async function collectDoctorChecks(opts: DoctorOptions = {}): Promise { // 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; @@ -60,7 +60,7 @@ async function openSession(opts: ExecOpts): Promise { 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; @@ -69,7 +69,7 @@ async function openSession(opts: ExecOpts): Promise { 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`, ), ); } @@ -196,7 +196,7 @@ export function registerDqlGroup(dql: ReturnType): void { }); // Execution subcommand (also the default — see rewriteDefaultSubcommand in - // the CLI entry, which maps `ditto dql ` → `ditto dql exec `; + // the CLI entry, which maps `dittosh dql ` → `dittosh dql exec `; // an action directly on `dql` would swallow same-named child options). dql .command("exec") @@ -288,7 +288,7 @@ export function registerDqlGroup(dql: ReturnType): 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; @@ -340,7 +340,7 @@ export function registerDqlGroup(dql: ReturnType): 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; } diff --git a/src/cli/groups/dql/repl.ts b/src/cli/groups/dql/repl.ts index 8d79fea..622fe75 100644 --- a/src/cli/groups/dql/repl.ts +++ b/src/cli/groups/dql/repl.ts @@ -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( diff --git a/src/cli/groups/dql/run.ts b/src/cli/groups/dql/run.ts index 204565e..c577562 100644 --- a/src/cli/groups/dql/run.ts +++ b/src/cli/groups/dql/run.ts @@ -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)); } @@ -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. diff --git a/src/cli/groups/skills/index.ts b/src/cli/groups/skills/index.ts index 64cee9e..1319ae5 100644 --- a/src/cli/groups/skills/index.ts +++ b/src/cli/groups/skills/index.ts @@ -208,7 +208,7 @@ export function registerSkillsGroup( if (rows.length === 0) { if (format === "json") console.log("[]"); else console.log("(no skills installed)"); - note("no skills installed — install with `ditto skills add`"); + note("no skills installed — install with `dittosh skills add`"); return; } console.log(renderRows(rows, format)); @@ -248,7 +248,7 @@ export function registerSkillsGroup( } if (found.length === 0) { if (format === "json") console.log("[]"); - else console.log(`(no ${opts.skill} skill installed — use \`ditto skills add\`)`); + else console.log(`(no ${opts.skill} skill installed — use \`dittosh skills add\`)`); note(`no ${opts.skill} skill installed — nothing to update`); return; } diff --git a/src/cli/groups/system/index.ts b/src/cli/groups/system/index.ts index 3302282..238476e 100644 --- a/src/cli/groups/system/index.ts +++ b/src/cli/groups/system/index.ts @@ -41,7 +41,7 @@ export function registerSystemGroup(program: Command, deps: SystemDeps = realDep const cached = deps.readCachedUpdate(); if (cached) { updateLine = isNewer(CLI_VERSION, cached.latest) - ? `${cached.latest} available (current ${CLI_VERSION}) — run: ditto update` + ? `${cached.latest} available (current ${CLI_VERSION}) — run: dittosh update` : `up to date (${CLI_VERSION})`; } @@ -102,7 +102,7 @@ export function registerSystemGroup(program: Command, deps: SystemDeps = realDep console.error(chalk.dim(`upgrade with: ${channel.updateCommand}`)); else console.error( - chalk.dim("upgrade manually: brew upgrade ditto · npm i -g @dittolive/cli@latest"), + chalk.dim("upgrade manually: brew upgrade dittosh · npm i -g @dittolive/cli@latest"), ); return; } @@ -110,7 +110,7 @@ export function registerSystemGroup(program: Command, deps: SystemDeps = realDep console.error( chalk.yellow( "Can't tell how this install was made. Upgrade manually:\n" + - " brew update && brew upgrade ditto # Homebrew\n" + + " brew update && brew upgrade dittosh # Homebrew\n" + " npm i -g @dittolive/cli@latest # npm", ), ); diff --git a/src/cli/index.ts b/src/cli/index.ts index 60e9ccf..3f99ceb 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -19,7 +19,7 @@ const program = new Command(); program.exitOverride(); program - .name("ditto") + .name("dittosh") .description("The Ditto CLI — run DQL, load sample datasets, install AI agent skills") .version(CLI_VERSION) .option("--no-color", "disable colored output (also: NO_COLOR, CI, non-TTY)") @@ -41,11 +41,11 @@ program.hook("preSubcommand", () => { // NO_COLOR (scrubbed before load — its native layer panics on it). const opts = program.opts<{ color?: boolean; quiet?: boolean }>(); if (opts.color === false || process.env.CI || "NO_COLOR" in process.env) chalk.level = 0; - if (opts.quiet) process.env.DITTO_QUIET = "1"; + if (opts.quiet) process.env.DITTOSH_QUIET = "1"; }); // After a successful command: the update banner (cached, non-blocking, stderr). -// Opt-outs: --no-update-check, DITTO_NO_UPDATE_CHECK, CI, --quiet, piped/JSON. +// Opt-outs: --no-update-check, DITTOSH_NO_UPDATE_CHECK, CI, --quiet, piped/JSON. // Skipped for system-group commands (version/update handle updates themselves) // and for help/version exits. let commandErrored = false; diff --git a/src/cli/update-banner.ts b/src/cli/update-banner.ts index 7859abb..d52950d 100644 --- a/src/cli/update-banner.ts +++ b/src/cli/update-banner.ts @@ -25,7 +25,7 @@ export async function maybeShowUpdateBanner( if (cached && isNewer(current, cached.latest)) { console.error( chalk.yellow(`update available: ${current} → ${cached.latest}`) + - chalk.dim(" (ditto update)"), + chalk.dim(" (dittosh update)"), ); } diff --git a/src/config/paths.ts b/src/config/paths.ts index d46b72f..f1a6589 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -5,17 +5,17 @@ import envPaths from "env-paths"; // Resolved lazily (not at module load) so tests can redirect via env vars. // NOTE: env-paths captures os.homedir() at module load — never cache its result // at module scope if you want env overrides to work, and prefer the explicit -// DITTO_*_DIR overrides in tests. -const paths = () => envPaths("ditto", { suffix: "" }); +// DITTOSH_*_DIR overrides in tests. +const paths = () => envPaths("dittosh", { suffix: "" }); -/** OS-default data directory (macOS ~/Library/Application Support/ditto, Linux ~/.local/share/ditto, Windows %LOCALAPPDATA%\ditto). */ +/** OS-default data directory (macOS ~/Library/Application Support/dittosh, Linux ~/.local/share/dittosh, Windows %LOCALAPPDATA%\dittosh). */ export function defaultDataDir(): string { return paths().data; } -/** Config directory (update-check cache, one-time-warning flags). `DITTO_CONFIG_DIR` overrides — used by tests, handy for portable setups. */ +/** Config directory (update-check cache, one-time-warning flags). `DITTOSH_CONFIG_DIR` overrides — used by tests, handy for portable setups. */ export function configDir(): string { - const override = process.env.DITTO_CONFIG_DIR; + const override = process.env.DITTOSH_CONFIG_DIR; return override?.trim() ? override : paths().config; } @@ -33,7 +33,7 @@ export function isBogusDataDir(v?: string): boolean { /** * Data directory resolution precedence: - * --data-dir flag > DITTO_DATA_DIR env var > OS default + * --data-dir flag > DITTOSH_DATA_DIR env var > OS default * Empty strings fall through (commander accepts `-d ""`; cwd is never intended). */ export function resolveDataDir(flag?: string, env: NodeJS.ProcessEnv = process.env): string { @@ -41,8 +41,8 @@ export function resolveDataDir(flag?: string, env: NodeJS.ProcessEnv = process.e const clean = (v?: string) => v?.replace(/^=/, "").trim(); const chosen = clean(flag) ? clean(flag) - : clean(env.DITTO_DATA_DIR) - ? clean(env.DITTO_DATA_DIR) + : clean(env.DITTOSH_DATA_DIR) + ? clean(env.DITTOSH_DATA_DIR) : undefined; const dir = chosen ?? defaultDataDir(); return path.resolve(expandTilde(dir)); diff --git a/src/ditto/session.ts b/src/ditto/session.ts index 0e501e8..b65db23 100644 --- a/src/ditto/session.ts +++ b/src/ditto/session.ts @@ -7,7 +7,7 @@ export class LockError extends Error { readonly exitCode = 4; constructor(dir: string) { super( - `The data directory is in use by another ditto process: ${dir}\n` + + `The data directory is in use by another dittosh process: ${dir}\n` + `Close the other process, or pass --data-dir to use a different directory.`, ); this.name = "LockError"; diff --git a/src/render/advise.ts b/src/render/advise.ts index 65fba84..bfc1e90 100644 --- a/src/render/advise.ts +++ b/src/render/advise.ts @@ -40,7 +40,7 @@ export function renderAdvice( if (!applied) { lines.push(""); lines.push( - chalk.dim(' apply with: ditto dql --advise --apply "" (prompts; -y skips)'), + chalk.dim(' apply with: dittosh dql --advise --apply "" (prompts; -y skips)'), ); } return lines.join("\n"); diff --git a/src/skills/github.ts b/src/skills/github.ts index f0ea89b..3f26f0f 100644 --- a/src/skills/github.ts +++ b/src/skills/github.ts @@ -2,10 +2,10 @@ import fs from "node:fs"; /** * GitHub fetch layer for agent skills. Source repo: getditto/agent-skills. - * `GITHUB_TOKEN` (or `DITTO_GITHUB_TOKEN`) authenticates while the repo is + * `GITHUB_TOKEN` (or `DITTOSH_GITHUB_TOKEN`) authenticates while the repo is * private; on 401/404 we produce actionable guidance. * - * Test seam: `DITTO_SKILLS_TARBALL=/path/to/repo.tar.gz` bypasses the network + * Test seam: `DITTOSH_SKILLS_TARBALL=/path/to/repo.tar.gz` bypasses the network * entirely (resolveRef → "fixture", fetchTarball reads the file from disk). */ @@ -23,21 +23,21 @@ export class SkillsFetchError extends Error { function headers(): Record { const h: Record = { - "User-Agent": "ditto-cli", + "User-Agent": "dittosh", Accept: "application/vnd.github+json", }; - const token = process.env.GITHUB_TOKEN ?? process.env.DITTO_GITHUB_TOKEN; + const token = process.env.GITHUB_TOKEN ?? process.env.DITTOSH_GITHUB_TOKEN; if (token) h.Authorization = `Bearer ${token}`; return h; } const PRIVATE_GUIDANCE = `The ${REPO} repo is private (or unreachable). Until it goes public, set GITHUB_TOKEN ` + - `to a token with access, e.g.: GITHUB_TOKEN=$(gh auth token) ditto skills add …`; + `to a token with access, e.g.: GITHUB_TOKEN=$(gh auth token) dittosh skills add …`; /** Resolve the ref to fetch: latest release tag, falling back to `main`. */ export async function resolveRef(fetchFn: typeof fetch = fetch): Promise { - if (process.env.DITTO_SKILLS_TARBALL) return "fixture"; // test seam + if (process.env.DITTOSH_SKILLS_TARBALL) return "fixture"; // test seam const res = await fetchFn(`${API}/repos/${REPO}/releases/latest`, { headers: headers() }); if (res.status === 404) { // No releases yet (or private without a token) — fall back to main. @@ -55,13 +55,13 @@ export async function resolveRef(fetchFn: typeof fetch = fetch): Promise /** Download the repo tarball for a ref. Returns the raw gzipped bytes. */ export async function fetchTarball(ref: string, fetchFn: typeof fetch = fetch): Promise { - const seam = process.env.DITTO_SKILLS_TARBALL; + const seam = process.env.DITTOSH_SKILLS_TARBALL; if (seam) { try { return fs.readFileSync(seam); } catch (err) { throw new SkillsFetchError( - `DITTO_SKILLS_TARBALL unreadable: ${seam} (${(err as Error).message})`, + `DITTOSH_SKILLS_TARBALL unreadable: ${seam} (${(err as Error).message})`, ); } } diff --git a/src/update/channel.ts b/src/update/channel.ts index ca3a052..5d2e254 100644 --- a/src/update/channel.ts +++ b/src/update/channel.ts @@ -14,7 +14,7 @@ export interface ChannelInfo { channel: InstallChannel; /** The command the user should run to update. */ updateCommand: string | null; - /** Human description for `ditto version`. */ + /** Human description for `dittosh version`. */ detail: string; } @@ -39,7 +39,7 @@ export function detectChannel( ) { return { channel: "homebrew", - updateCommand: "brew update && brew upgrade ditto", + updateCommand: "brew update && brew upgrade dittosh", detail: `homebrew (${brewPrefix})`, }; } diff --git a/src/update/check.ts b/src/update/check.ts index 28267ef..34418a6 100644 --- a/src/update/check.ts +++ b/src/update/check.ts @@ -4,7 +4,7 @@ import { readState, writeState } from "../config/state.js"; * Non-blocking update check against the npm registry for `@dittolive/cli`. * Result is cached in state.json with a 24h TTL; failures are silent. * - * Opt-outs (checked by callers): CI, DITTO_NO_UPDATE_CHECK, --no-update-check, + * Opt-outs (checked by callers): CI, DITTOSH_NO_UPDATE_CHECK, --no-update-check, * non-TTY, --format json. */ @@ -109,13 +109,13 @@ export async function checkForUpdate( export function updateCheckAllowed( opts: { ci?: boolean; quiet?: boolean; jsonOut?: boolean; isTTY?: boolean } = {}, ): boolean { - const noCheck = process.env.DITTO_NO_UPDATE_CHECK; + const noCheck = process.env.DITTOSH_NO_UPDATE_CHECK; if (noCheck && noCheck !== "0" && noCheck !== "false") return false; const ci = opts.ci ?? process.env.CI; if (ci && ci !== "false" && ci !== "0") return false; - const quiet = opts.quiet ?? process.env.DITTO_QUIET; + const quiet = opts.quiet ?? process.env.DITTOSH_QUIET; if (quiet === true || quiet === "1" || quiet === "true") return false; - if (opts.jsonOut ?? process.env.DITTO_JSON_OUT === "1") return false; + if (opts.jsonOut ?? process.env.DITTOSH_JSON_OUT === "1") return false; if (!(opts.isTTY ?? process.stderr.isTTY)) return false; return true; } diff --git a/tests/e2e/modes.test.ts b/tests/e2e/modes.test.ts index 028e703..fc5b7d8 100644 --- a/tests/e2e/modes.test.ts +++ b/tests/e2e/modes.test.ts @@ -269,7 +269,7 @@ describe.skipIf(!hasDevCredentials)(`e2e: ditto dql input modes (${NO_CREDENTIAL } }); - it("DITTO_DATA_DIR is honored when -d is absent", async () => { + it("DITTOSH_DATA_DIR is honored when -d is absent", async () => { const dir = tmpDataDir("ditto-e2e-env-"); try { const r = (await execa( @@ -281,7 +281,7 @@ describe.skipIf(!hasDevCredentials)(`e2e: ditto dql input modes (${NO_CREDENTIAL "dql", "INSERT INTO t DOCUMENTS ({'_id':'1'}) ON ID CONFLICT DO UPDATE", ], - { cwd: ROOT, reject: false, all: true, env: { DITTO_DATA_DIR: dir } }, + { cwd: ROOT, reject: false, all: true, env: { DITTOSH_DATA_DIR: dir } }, )) as unknown as RunResult; expect(r.exitCode).toBe(0); expect(fs.existsSync(path.join(dir, "__ditto_lock_file"))).toBe(true); diff --git a/tests/e2e/skills.test.ts b/tests/e2e/skills.test.ts index 03917e3..650fa0d 100644 --- a/tests/e2e/skills.test.ts +++ b/tests/e2e/skills.test.ts @@ -44,7 +44,7 @@ describe("e2e: ditto skills (fixture tarball seam)", () => { const home = tmpDataDir("ditto-e2e-home-"); try { const add = (await cli(["skills", "add", "--agent", "claude", "--project", proj], { - DITTO_SKILLS_TARBALL: tgz, + DITTOSH_SKILLS_TARBALL: tgz, HOME: home, })) as unknown as RunResult; expect(add.exitCode).toBe(0); @@ -56,7 +56,7 @@ describe("e2e: ditto skills (fixture tarball seam)", () => { ).toBe(true); const list = (await cli(["skills", "list", "--project", proj], { - DITTO_SKILLS_TARBALL: tgz, + DITTOSH_SKILLS_TARBALL: tgz, HOME: home, })) as unknown as RunResult; expect(list.exitCode).toBe(0); @@ -76,11 +76,11 @@ describe("e2e: ditto skills (fixture tarball seam)", () => { const home = tmpDataDir("ditto-e2e-home-"); try { await cli(["skills", "add", "--agent", "claude", "--project", proj], { - DITTO_SKILLS_TARBALL: tgz, + DITTOSH_SKILLS_TARBALL: tgz, HOME: home, }); const upd = (await cli(["skills", "update", "--project", proj], { - DITTO_SKILLS_TARBALL: tgz, + DITTOSH_SKILLS_TARBALL: tgz, HOME: home, })) as unknown as RunResult; expect(upd.exitCode).toBe(0); @@ -98,14 +98,14 @@ describe("e2e: ditto skills (fixture tarball seam)", () => { const home = tmpDataDir("ditto-e2e-home-"); try { const badFormat = (await cli(["skills", "list", "--format", "yaml"], { - DITTO_SKILLS_TARBALL: tgz, + DITTOSH_SKILLS_TARBALL: tgz, HOME: home, })) as unknown as RunResult; expect(badFormat.exitCode).toBe(2); const badProject = (await cli( ["skills", "add", "--agent", "claude", "--project", "/nonexistent-xyz"], - { DITTO_SKILLS_TARBALL: tgz, HOME: home }, + { DITTOSH_SKILLS_TARBALL: tgz, HOME: home }, )) as unknown as RunResult; expect(badProject.exitCode).toBe(2); expect(badProject.stderr).toContain("does not exist"); diff --git a/tests/e2e/system.test.ts b/tests/e2e/system.test.ts index a67b357..305eb66 100644 --- a/tests/e2e/system.test.ts +++ b/tests/e2e/system.test.ts @@ -88,7 +88,7 @@ describe("e2e: ditto version / update / banner", () => { "utf8", ); const r = (await cli(["dql", "SELECT 1 FROM system:collections", "-d", dir], { - DITTO_CONFIG_DIR: cfg, + DITTOSH_CONFIG_DIR: cfg, })) as unknown as RunResult; expect(r.exitCode).toBe(0); // piped (non-TTY) → banner suppressed entirely; stdout is pure JSON diff --git a/tests/unit/cli-dataset.test.ts b/tests/unit/cli-dataset.test.ts index 8acc98d..deffb97 100644 --- a/tests/unit/cli-dataset.test.ts +++ b/tests/unit/cli-dataset.test.ts @@ -534,7 +534,7 @@ describe("ditto dql dataset wiring (mocked SDK boundary)", () => { }); it("known-issue warnings survive --quiet (they're the only mitigation for an SDK hang)", async () => { - process.env.DITTO_QUIET = "1"; + process.env.DITTOSH_QUIET = "1"; try { await buildProgram().parseAsync([ "node", @@ -551,7 +551,7 @@ describe("ditto dql dataset wiring (mocked SDK boundary)", () => { expect(stderr()).toContain("known issue"); expect(stderr()).not.toContain("Running"); // banner suppressed, warning not } finally { - delete process.env.DITTO_QUIET; + delete process.env.DITTOSH_QUIET; } }); diff --git a/tests/unit/cli-dql.test.ts b/tests/unit/cli-dql.test.ts index fd0a0b8..0a0be10 100644 --- a/tests/unit/cli-dql.test.ts +++ b/tests/unit/cli-dql.test.ts @@ -533,13 +533,13 @@ describe("ditto dql command wiring (mocked SDK boundary)", () => { }); it("-d '' (empty) falls through to env; bogus env is still guarded", async () => { - process.env.DITTO_DATA_DIR = "--"; + process.env.DITTOSH_DATA_DIR = "--"; try { await buildProgram().parseAsync(["node", "ditto", "dql", "exec", "SELECT 1", "-d", ""]); expect(process.exitCode).toBe(2); expect(h.openedDataDir).toBeNull(); } finally { - delete process.env.DITTO_DATA_DIR; + delete process.env.DITTOSH_DATA_DIR; } }); @@ -562,7 +562,7 @@ describe("ditto dql command wiring (mocked SDK boundary)", () => { dataDir, ]); expect(process.exitCode).toBe(4); - expect(stderr()).toContain("in use by another ditto process"); + expect(stderr()).toContain("in use by another dittosh process"); }); it("collections runs system:collections", async () => { diff --git a/tests/unit/cli-system.test.ts b/tests/unit/cli-system.test.ts index 7c6e2e2..657da0c 100644 --- a/tests/unit/cli-system.test.ts +++ b/tests/unit/cli-system.test.ts @@ -10,14 +10,14 @@ beforeEach(() => { outSpy = vi.spyOn(console, "log").mockImplementation(() => {}); errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); process.exitCode = undefined; - process.env.DITTO_CONFIG_DIR = tmpDataDir("ditto-state-"); + process.env.DITTOSH_CONFIG_DIR = tmpDataDir("ditto-state-"); }); afterEach(() => { outSpy.mockRestore(); errSpy.mockRestore(); - rmrf(process.env.DITTO_CONFIG_DIR!); - delete process.env.DITTO_CONFIG_DIR; + rmrf(process.env.DITTOSH_CONFIG_DIR!); + delete process.env.DITTOSH_CONFIG_DIR; }); const stdout = () => outSpy.mock.calls.flat().join("\n"); @@ -130,14 +130,14 @@ describe("ditto update", () => { readCachedUpdate: () => undefined, detectChannel: () => ({ channel: "homebrew", - updateCommand: "brew update && brew upgrade ditto", + updateCommand: "brew update && brew upgrade dittosh", detail: "homebrew (/opt/homebrew)", }), run, }); await program.parseAsync(["node", "ditto", "update", "--check"]); expect(stdout()).toContain("0.1.0 → 0.2.0"); - expect(stderr()).toContain("brew update && brew upgrade ditto"); + expect(stderr()).toContain("brew update && brew upgrade dittosh"); expect(run).not.toHaveBeenCalled(); }); @@ -168,7 +168,7 @@ describe("ditto update", () => { }); await program.parseAsync(["node", "ditto", "update"]); expect(run).not.toHaveBeenCalled(); - expect(stderr()).toContain("brew update && brew upgrade ditto"); + expect(stderr()).toContain("brew update && brew upgrade dittosh"); expect(stderr()).toContain("npm i -g @dittolive/cli@latest"); expect(process.exitCode).toBe(1); }); diff --git a/tests/unit/doctor.test.ts b/tests/unit/doctor.test.ts index 83c05fd..89de46f 100644 --- a/tests/unit/doctor.test.ts +++ b/tests/unit/doctor.test.ts @@ -141,10 +141,10 @@ describe("collectDoctorChecks", () => { expect(t.detail).toContain("2027-06-01"); }); - it("a valid -d flag wins precedence over a bogus DITTO_DATA_DIR", async () => { + it("a valid -d flag wins precedence over a bogus DITTOSH_DATA_DIR", async () => { const checks = await collectDoctorChecks({ dataDir: dir, - env: { ...ENV, DITTO_DATA_DIR: "--" } as NodeJS.ProcessEnv, + env: { ...ENV, DITTOSH_DATA_DIR: "--" } as NodeJS.ProcessEnv, openStore: openStoreOk, }); expect(checks.find((c) => c.label === "data directory")!.ok).toBe(true); @@ -153,22 +153,22 @@ describe("collectDoctorChecks", () => { it("a whitespace-only -d flag falls through to the (bogus) env and fails", async () => { const checks = await collectDoctorChecks({ dataDir: " ", - env: { ...ENV, DITTO_DATA_DIR: "--" } as NodeJS.ProcessEnv, + env: { ...ENV, DITTOSH_DATA_DIR: "--" } as NodeJS.ProcessEnv, openStore: openStoreOk, }); const d = checks.find((c) => c.label === "data directory")!; expect(d.ok).toBe(false); - expect(d.detail).toContain("DITTO_DATA_DIR"); + expect(d.detail).toContain("DITTOSH_DATA_DIR"); }); - it("flags a bogus DITTO_DATA_DIR when no flag overrides it", async () => { + it("flags a bogus DITTOSH_DATA_DIR when no flag overrides it", async () => { const checks = await collectDoctorChecks({ - env: { ...ENV, DITTO_DATA_DIR: "--" } as NodeJS.ProcessEnv, + env: { ...ENV, DITTOSH_DATA_DIR: "--" } as NodeJS.ProcessEnv, openStore: openStoreOk, }); const d = checks.find((c) => c.label === "data directory")!; expect(d.ok).toBe(false); - expect(d.detail).toContain("DITTO_DATA_DIR"); + expect(d.detail).toContain("DITTOSH_DATA_DIR"); }); it("fails the token check when credentials are missing", async () => { diff --git a/tests/unit/paths.test.ts b/tests/unit/paths.test.ts index 9ccaa8d..6e21d66 100644 --- a/tests/unit/paths.test.ts +++ b/tests/unit/paths.test.ts @@ -5,7 +5,7 @@ import { defaultDataDir, isBogusDataDir, resolveDataDir } from "../../src/config describe("resolveDataDir precedence", () => { it("flag beats env beats default", () => { - const env = { DITTO_DATA_DIR: "/env/dir" } as NodeJS.ProcessEnv; + const env = { DITTOSH_DATA_DIR: "/env/dir" } as NodeJS.ProcessEnv; expect(resolveDataDir("/flag/dir", env)).toBe(path.resolve("/flag/dir")); expect(resolveDataDir(undefined, env)).toBe(path.resolve("/env/dir")); expect(resolveDataDir(undefined, {} as NodeJS.ProcessEnv)).toBe(defaultDataDir()); @@ -13,13 +13,13 @@ describe("resolveDataDir precedence", () => { it("empty-string flag falls through to env", () => { // commander never passes "", but the contract is: undefined means "not provided" - const env = { DITTO_DATA_DIR: "/env/dir" } as NodeJS.ProcessEnv; + const env = { DITTOSH_DATA_DIR: "/env/dir" } as NodeJS.ProcessEnv; expect(resolveDataDir(undefined, env)).toBe(path.resolve("/env/dir")); }); it("trims whitespace around the flag/env value", () => { expect(resolveDataDir(" /tmp/x ")).toBe(path.resolve("/tmp/x")); - expect(resolveDataDir(undefined, { DITTO_DATA_DIR: " /tmp/y " } as NodeJS.ProcessEnv)).toBe( + expect(resolveDataDir(undefined, { DITTOSH_DATA_DIR: " /tmp/y " } as NodeJS.ProcessEnv)).toBe( path.resolve("/tmp/y"), ); }); @@ -37,7 +37,9 @@ describe("resolveDataDir precedence", () => { }); it("empty-string flag AND empty env fall through to default (never cwd)", () => { - expect(resolveDataDir("", { DITTO_DATA_DIR: "" } as NodeJS.ProcessEnv)).toBe(defaultDataDir()); + expect(resolveDataDir("", { DITTOSH_DATA_DIR: "" } as NodeJS.ProcessEnv)).toBe( + defaultDataDir(), + ); expect(resolveDataDir("", {} as NodeJS.ProcessEnv)).toBe(defaultDataDir()); }); @@ -54,10 +56,10 @@ describe("resolveDataDir precedence", () => { expect(resolveDataDir("./rel", {} as NodeJS.ProcessEnv)).toBe(path.resolve("./rel")); }); - it("default is an absolute path containing 'ditto'", () => { + it("default is an absolute path containing 'dittosh'", () => { const def = defaultDataDir(); expect(path.isAbsolute(def)).toBe(true); - expect(def.toLowerCase()).toContain("ditto"); + expect(def.toLowerCase()).toContain("dittosh"); expect(def).not.toContain("~"); }); }); diff --git a/tests/unit/run.test.ts b/tests/unit/run.test.ts index d3c3d2d..74320f0 100644 --- a/tests/unit/run.test.ts +++ b/tests/unit/run.test.ts @@ -159,7 +159,7 @@ describe("runStatement", () => { }); it("warns once about SELECT without LIMIT when interactive, then persists the flag", async () => { - process.env.DITTO_CONFIG_DIR = tmpDataDir("ditto-state-"); + process.env.DITTOSH_CONFIG_DIR = tmpDataDir("ditto-state-"); try { vi.resetModules(); const opts: RunOptions = { ...baseOpts, interactive: true, format: "json" }; @@ -170,13 +170,13 @@ describe("runStatement", () => { await runStatement(fakeExecutor([{ _id: "1" }]), "SELECT * FROM movies", opts); expect(errSpy.mock.calls.flat().join("\n")).not.toContain("no LIMIT"); } finally { - rmrf(process.env.DITTO_CONFIG_DIR); - delete process.env.DITTO_CONFIG_DIR; + rmrf(process.env.DITTOSH_CONFIG_DIR); + delete process.env.DITTOSH_CONFIG_DIR; } }); it("never warns when LIMIT present, --max-rows explicit, or non-interactive", async () => { - process.env.DITTO_CONFIG_DIR = tmpDataDir("ditto-state-"); + process.env.DITTOSH_CONFIG_DIR = tmpDataDir("ditto-state-"); try { const withLimit = await runStatement(fakeExecutor([]), "SELECT * FROM movies LIMIT 5", { ...baseOpts, @@ -195,8 +195,8 @@ describe("runStatement", () => { expect(piped.ok).toBe(true); expect(errSpy.mock.calls.flat().join("\n")).not.toContain("no LIMIT"); } finally { - rmrf(process.env.DITTO_CONFIG_DIR); - delete process.env.DITTO_CONFIG_DIR; + rmrf(process.env.DITTOSH_CONFIG_DIR); + delete process.env.DITTOSH_CONFIG_DIR; } }); }); @@ -381,7 +381,7 @@ describe("runStatement diagnostics (--time/--explain/--profile)", () => { }); it("no-LIMIT warning does not fire when -o already exports everything", async () => { - process.env.DITTO_CONFIG_DIR = tmpDataDir("ditto-state-"); + process.env.DITTOSH_CONFIG_DIR = tmpDataDir("ditto-state-"); try { const dir = tmpDataDir("ditto-run-"); const out = path.join(dir, "all.json"); @@ -393,14 +393,14 @@ describe("runStatement diagnostics (--time/--explain/--profile)", () => { expect(errSpy.mock.calls.flat().join("\n")).not.toContain("no LIMIT"); rmrf(dir); } finally { - rmrf(process.env.DITTO_CONFIG_DIR); - delete process.env.DITTO_CONFIG_DIR; + rmrf(process.env.DITTOSH_CONFIG_DIR); + delete process.env.DITTOSH_CONFIG_DIR; } }); - it("DITTO_QUIET=0/false does NOT silence notes (explicit values only)", async () => { + it("DITTOSH_QUIET=0/false does NOT silence notes (explicit values only)", async () => { const { executor } = profileExecutor([]); - process.env.DITTO_QUIET = "0"; + process.env.DITTOSH_QUIET = "0"; try { await runStatement(executor, "INSERT INTO movies DOCUMENTS ({'_id':'1'})", { ...baseOpts, @@ -408,13 +408,13 @@ describe("runStatement diagnostics (--time/--explain/--profile)", () => { }); expect(errSpy.mock.calls.flat().join(" ")).toContain("only SELECT statements"); } finally { - delete process.env.DITTO_QUIET; + delete process.env.DITTOSH_QUIET; } }); - it("--quiet suppresses dim notes (DITTO_QUIET)", async () => { + it("--quiet suppresses dim notes (DITTOSH_QUIET)", async () => { const { executor } = profileExecutor([]); - process.env.DITTO_QUIET = "1"; + process.env.DITTOSH_QUIET = "1"; try { await runStatement(executor, "INSERT INTO movies DOCUMENTS ({'_id':'1'})", { ...baseOpts, @@ -422,7 +422,7 @@ describe("runStatement diagnostics (--time/--explain/--profile)", () => { }); expect(errSpy.mock.calls.flat().join(" ")).not.toContain("only SELECT statements"); } finally { - delete process.env.DITTO_QUIET; + delete process.env.DITTOSH_QUIET; } }); diff --git a/tests/unit/skills-fetch.test.ts b/tests/unit/skills-fetch.test.ts index e2656cb..43255f4 100644 --- a/tests/unit/skills-fetch.test.ts +++ b/tests/unit/skills-fetch.test.ts @@ -1,5 +1,4 @@ import fs from "node:fs"; -import os from "node:os"; import path from "node:path"; import * as tar from "tar"; import { describe, expect, it } from "vitest"; diff --git a/tests/unit/state.test.ts b/tests/unit/state.test.ts index c52e2c5..9a196f8 100644 --- a/tests/unit/state.test.ts +++ b/tests/unit/state.test.ts @@ -4,14 +4,14 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { rmrf, tmpDataDir } from "../helpers/credentials.js"; // Point the config dir at an isolated location before importing the module -// under test (env-paths caches homedir at module load, so DITTO_CONFIG_DIR is +// under test (env-paths caches homedir at module load, so DITTOSH_CONFIG_DIR is // the reliable lever). let home: string; let state: typeof import("../../src/config/state.js"); beforeEach(async () => { home = tmpDataDir("ditto-state-"); - process.env.DITTO_CONFIG_DIR = home; + process.env.DITTOSH_CONFIG_DIR = home; vi.resetModules(); state = await import("../../src/config/state.js"); }); diff --git a/tests/unit/update-banner.test.ts b/tests/unit/update-banner.test.ts index ff80797..353ebd7 100644 --- a/tests/unit/update-banner.test.ts +++ b/tests/unit/update-banner.test.ts @@ -7,15 +7,15 @@ let state: typeof import("../../src/config/state.js"); beforeEach(async () => { errSpy = vi.spyOn(console, "error").mockImplementation(() => {}); - process.env.DITTO_CONFIG_DIR = tmpDataDir("ditto-state-"); + process.env.DITTOSH_CONFIG_DIR = tmpDataDir("ditto-state-"); vi.resetModules(); state = await import("../../src/config/state.js"); }); afterEach(() => { errSpy.mockRestore(); - rmrf(process.env.DITTO_CONFIG_DIR!); - delete process.env.DITTO_CONFIG_DIR; + rmrf(process.env.DITTOSH_CONFIG_DIR!); + delete process.env.DITTOSH_CONFIG_DIR; }); const stderr = () => errSpy.mock.calls.flat().join("\n"); @@ -28,7 +28,7 @@ describe("update banner", () => { state.writeState({ updateCheck: { checkedAt: Date.now(), latest: "9.9.9" } }); await maybeShowUpdateBanner("1.0.0", { isTTY: true }); expect(stderr()).toContain("update available: 1.0.0 → 9.9.9"); - expect(stderr()).toContain("ditto update"); + expect(stderr()).toContain("dittosh update"); } finally { if (hadCI !== undefined) process.env.CI = hadCI; } @@ -45,7 +45,7 @@ describe("update banner", () => { expect(stderr()).toBe(""); }); - it("opt-outs: --no-update-check, quiet, CI, DITTO_NO_UPDATE_CHECK, non-TTY", async () => { + it("opt-outs: --no-update-check, quiet, CI, DITTOSH_NO_UPDATE_CHECK, non-TTY", async () => { state.writeState({ updateCheck: { checkedAt: Date.now(), latest: "9.9.9" } }); await maybeShowUpdateBanner("1.0.0", { noCheckFlag: true, isTTY: true }); expect(stderr()).toBe(""); @@ -61,12 +61,12 @@ describe("update banner", () => { delete process.env.CI; } - process.env.DITTO_NO_UPDATE_CHECK = "1"; + process.env.DITTOSH_NO_UPDATE_CHECK = "1"; try { await maybeShowUpdateBanner("1.0.0", { isTTY: true }); expect(stderr()).toBe(""); } finally { - delete process.env.DITTO_NO_UPDATE_CHECK; + delete process.env.DITTOSH_NO_UPDATE_CHECK; } }); }); diff --git a/tests/unit/update-channel.test.ts b/tests/unit/update-channel.test.ts index f2aca1d..4bb54b6 100644 --- a/tests/unit/update-channel.test.ts +++ b/tests/unit/update-channel.test.ts @@ -5,14 +5,14 @@ import { detectChannel } from "../../src/update/channel.js"; describe("detectChannel", () => { it("homebrew: resolved path under the Cellar", () => { const c = detectChannel({ - argv1: "/opt/homebrew/bin/ditto", + argv1: "/opt/homebrew/bin/dittosh", brewPrefix: "/opt/homebrew", npmPrefix: "/usr/local", }); // simulate realpath resolution to the Cellar expect( detectChannel({ - argv1: "/opt/homebrew/Cellar/ditto/0.1.0/bin/ditto", + argv1: "/opt/homebrew/Cellar/dittosh/0.1.0/bin/dittosh", brewPrefix: "/opt/homebrew", npmPrefix: "/usr/local", }).channel, @@ -58,7 +58,7 @@ describe("detectChannel", () => { it("truly unknown paths are unknown", () => { const c = detectChannel({ - argv1: "/opt/custom/bin/ditto", + argv1: "/opt/custom/bin/dittosh", brewPrefix: "/opt/homebrew", npmPrefix: "/usr/local", }); @@ -68,7 +68,7 @@ describe("detectChannel", () => { it("missing brew/npm binaries degrade gracefully", () => { const c = detectChannel({ - argv1: "/opt/custom/bin/ditto", + argv1: "/opt/custom/bin/dittosh", brewPrefix: undefined, npmPrefix: undefined, }); diff --git a/tests/unit/update-check.test.ts b/tests/unit/update-check.test.ts index c280686..11e4367 100644 --- a/tests/unit/update-check.test.ts +++ b/tests/unit/update-check.test.ts @@ -5,15 +5,15 @@ let state: typeof import("../../src/config/state.js"); let check: typeof import("../../src/update/check.js"); beforeEach(async () => { - process.env.DITTO_CONFIG_DIR = tmpDataDir("ditto-state-"); + process.env.DITTOSH_CONFIG_DIR = tmpDataDir("ditto-state-"); vi.resetModules(); state = await import("../../src/config/state.js"); check = await import("../../src/update/check.js"); }); afterEach(() => { - rmrf(process.env.DITTO_CONFIG_DIR!); - delete process.env.DITTO_CONFIG_DIR; + rmrf(process.env.DITTOSH_CONFIG_DIR!); + delete process.env.DITTOSH_CONFIG_DIR; }); describe("update check", () => { @@ -94,11 +94,11 @@ describe("updateCheckAllowed opt-outs", () => { expect(check!.updateCheckAllowed({ isTTY: true, quiet: true })).toBe(false); expect(check!.updateCheckAllowed({ isTTY: true, jsonOut: true })).toBe(false); expect(check!.updateCheckAllowed({ isTTY: true, ci: true })).toBe(false); - process.env.DITTO_NO_UPDATE_CHECK = "1"; + process.env.DITTOSH_NO_UPDATE_CHECK = "1"; try { expect(check!.updateCheckAllowed({ isTTY: true })).toBe(false); } finally { - delete process.env.DITTO_NO_UPDATE_CHECK; + delete process.env.DITTOSH_NO_UPDATE_CHECK; } } finally { if (hadCI !== undefined) process.env.CI = hadCI;