diff --git a/.claude/rules/api-client.md b/.claude/rules/api-client.md new file mode 100644 index 0000000..b4f32a4 --- /dev/null +++ b/.claude/rules/api-client.md @@ -0,0 +1,49 @@ +--- +paths: + - src/lib/api-client.ts + - src/base-command.ts +--- + +# API Client Contract + +## BaseCommand + +`BaseCommand` provides: +- `apiClient` — built in `init()` from config or flag overrides, so a missing key or unreadable config fails (exit 1) before `run()` and before any confirmation prompt. +- `requireNumericId(value, name)` — validates string is all digits, errors with exit 2. +- `requireUuid(value, name)` — validates a UUID, errors with exit 2. `init()` applies it to `--idempotency-key` before `run()`. +- `requireMetricId(value, name)` — metric keys are opaque strings, so it rejects only what would rewrite the path: empty, blank, `.` and `..` (exit 2). Pair it with `encodeURIComponent`. +- `accountHeaders` getter — returns `{'x-account-id': id}` if `--account-id` set, else `{}`. `init()` validates `--account-id` as numeric first. +- `outputFormat` getter — `'table' | 'json' | 'csv'`; `--json` resolves to `'json'`. +- `color` getter — false under `--no-color` or a non-empty `NO_COLOR`. The CLI's own output has no colour; any colour added must check it. +- `catch()` — renders `ApiRequestError` via `describeApiError` (exit 1) and `ApiConnectionError` (exit 2) before oclif's handler prints them. +- Base flags: `--output table|json|csv` (default `table`), `--json` (shorthand, exclusive with `--output`), `--verbose`, `--no-color`, `--api-key` (hidden), `--api-url` (hidden), `--account-id` (hidden). + +`auth login` extends `Command` directly (not `BaseCommand`) because it works without an existing API key. + +## ApiClient + +- Wraps native `fetch()` — no external HTTP dependencies. +- Methods: `get`, `post`, `patch`, `put`, `delete` — all generic. +- Unwraps V2 envelope: returns `response.data`, not the full `{data, requestId, status}`. +- Auth: `x-api-key` header on every request. Content-Type set only when body is present. +- Errors: parses `errors[]` from the error envelope, throws `ApiRequestError(message, status, errors, requestId)`. + `code`, `field` and `type` are taken from the first error. A non-JSON or `null` body falls back to `API error: `. +- Transport failures — `fetch()` rejecting, a timeout, or the body stream failing mid-read — throw `ApiConnectionError` (exit 2). +- Redirects are refused (`redirect: 'error'`, in `ask-genie.ts` too): fetch strips only `Authorization` and `Cookie` on a + cross-origin redirect, so a followed one would resend `x-api-key`. The refusal is an `ApiConnectionError` (exit 2) with its own message. +- `--verbose`: the `trace` option receives lines built only by `describeRequest(method, url)` and + `describeResponse(status, ms, requestId)`. Neither takes the headers, so the key is never in scope; + the header line is the literal `Headers: x-api-key: `. Trace goes to stderr. + +## Rules + +- Never add external HTTP dependencies (axios, got, node-fetch) — use native `fetch()`. +- API error detail (code, message, field, request ID) is printed by `BaseCommand.catch` in the cli.md §8 layout. Commands must not catch API errors themselves — that loses the detail and the exit code. + - The one sanctioned exception: a command may override `catch()` to append a hint to one specific API error code. + It re-renders that error with `describeApiError(error)` plus the hint line, passes it to `super.catch` as a + `CLIError` with exit 1, and delegates every other error to `super.catch` unchanged. No try/catch goes around the + API call itself. See `src/commands/user/invite.ts` (`duplicate_record` → a pointer to `user update`). +- Never pass headers, or anything holding the key, into a trace or log line. Build such lines from explicit printable values. +- Never bypass `ApiClient` for API calls unless the protocol requires it (SSE streaming). +- The `apiKey` property must not appear in any `this.log()`, `console.log()`, or error output. diff --git a/.claude/rules/commands.md b/.claude/rules/commands.md new file mode 100644 index 0000000..e5f186c --- /dev/null +++ b/.claude/rules/commands.md @@ -0,0 +1,124 @@ +--- +paths: + - src/commands/** + - src/lib/output.ts +--- + +# Command Conventions + +## Structure + +Every command extends `BaseCommand` (exception: `auth login` extends `Command` directly). +Static members are ordered alphabetically: `args`, `description`, `examples`, `flags`. + +```typescript +// Good +export default class DatasetGet extends BaseCommand { + static args = { ... } + static description = 'Get details of a specific dataset' + static examples = [ ... ] + static flags = { ... } + async run(): Promise { ... } +} + +// Bad — wrong order, missing examples +export default class DatasetGet extends BaseCommand { + static description = '...' + static flags = { ... } + static args = { ... } + async run(): Promise { ... } +} +``` + +## Flags and arguments + +- Flag names: kebab-case (`page-size`, `data-source-id`). Body/query params: camelCase (`pageSize`, `dataSourceId`). +- Use `Flags.string()`, `Flags.integer()`, `Flags.boolean()` — match the data type. +- `required: true` on mandatory flags, `options: [...]` for enums, `exclusive: [...]` for mutual exclusion. +- Shared flags from `src/lib/flags.ts`: + - `...paginationFlags` (`--page`, `--page-size` max 100, `--all`) on list endpoints; `...dataPaginationFlags` (`--page-size` max 1000) on the row-data endpoints (`dataset data`, `metric drilldown`). + - `...sortFlags(options)` — pass the sort fields the service validates; `sortFlags()` leaves `--sort-by` free. + - `...idempotencyFlags` on exactly the routes ingestion-api marks `[IdempotencyFilter]`, sending `{...this.accountHeaders, ...idempotencyHeaders(this.flags)}`. +- Boolean flags: `default: false`. +- Examples use `<%= config.bin %>` template, never hardcoded `databox`. At least 2 examples per command. +- Args use `Args.string({ required: true })` — even numeric IDs are accepted as strings and validated later. + +Never drop `--flag ""` silently: the user typed it deliberately. Where the API validates the field, send the empty +value (guard with `!== undefined`, never truthiness) and let the API reject it with a message; where the API would +accept it silently, reject it locally with exit 2. A required name flag rejects blank locally unless the API does +(the create commands check it anyway, to fail before the round trip). `runCommand` refuses an empty-string flag +value, so the regression test for `--flag ""` belongs in `test/e2e/`. + +```typescript +// Good — the API validates timezone, so `--timezone ""` reaches it and is rejected with a message +if (flags.timezone !== undefined) body.timezone = flags.timezone +// Good — the API would accept a blank integration key, so the CLI refuses it +if (flags['integration-key'] !== undefined && flags['integration-key'].trim() === '') { + this.error('--integration-key cannot be empty.', {exit: 2}) +} + +// Bad — `--timezone ""` is dropped and the data source silently gets the default zone +if (flags.timezone) body.timezone = flags.timezone +``` + +## Output by command type + +| Type | Output | Functions | +|---|---|---| +| List | Table + pagination | `formatOutput(data, columns, this.outputFormat)` + `showPagination(pagination, this.outputFormat)` | +| Get / Create / Update | Single record | `formatSingle(data, this.outputFormat)` | +| Delete / Purge / Clear | Confirmation message | `this.log('Resource ID action.')` | +| Set (permissions, timezone) | Confirmation message or single record | `this.log()` or `formatSingle()` | + +Always pass `this.outputFormat`, never `this.flags.json`: it also covers `--output json|csv`. + +`--json` returns what the endpoint returned: + +- A pure `{items}` or `{items, pagination}` list unwraps to a bare array of the items, passed through whole. +- A response whose siblings of `items` carry data returns the whole object: `dataset schema` `{items, primaryKey}`, + `dataset data` `{items, pagination, schema, lastUpdatedAt}`, `dataset preview-modification` `{items, pagination, schema}`. + Branch on `this.outputFormat === 'json'` and print it with `formatSingle(response, this.outputFormat)`. +- A mutation that returns the resource (`set-timezone`, `set-sync-frequency`, `set-verification`) prints it with + `formatSingle` under json and csv, and keeps its confirmation line in table mode. +- Lines that only make sense beside a table (a primary key, a "rows matched" count) are printed in table mode only, + never in CSV, so the stream stays parseable. + +List commands fetch through `fetchPaginated`, which implements `--all` and reports a short result on stderr: + +```typescript +const response = await fetchPaginated(this.flags, query, pageQuery => + this.apiClient.get('/v2/resources', pageQuery, this.accountHeaders), warning => this.warn(warning)) +``` + +## Destructive operations + +Delete, purge, and clear commands require: +1. `--force` flag with `default: false` +2. `confirm()` from `../../lib/prompt.js` when not forced +3. `this.log('Aborted.')` when user declines +4. Success message: `"Resource ID past-tense."` (e.g., `"Dataset 123 deleted."`) + +Off a terminal, `confirm()` writes its question to stderr and reads the answer from the first line +of stdin: `y`/`yes` proceeds, anything else is declined (`Aborted.`, exit 0), and an empty first line +or a stdin that ends with nothing throws exit 2 ("Refusing to prompt: … Pass --force to confirm."). +Commands need no handling of their own. + +## Error codes + +- `this.error(msg, {exit: 1})` — general errors (missing auth, API failures) +- `this.error(msg, {exit: 2})` — input validation errors (`requireNumericId`) + +## API calls + +- Always pass `this.accountHeaders` as the last argument to `apiClient.get/post/patch/put/delete`. +- No try/catch around API calls — errors propagate to `BaseCommand.catch`, which prints their detail and sets the exit code. +- No direct `fetch()` calls (exception: `ask-genie.ts` for SSE streaming). + +## Update commands + +Update commands with optional flags must guard against empty bodies: +```typescript +if (Object.keys(body).length === 0) { + this.error('Provide at least one field to update (--name or --title).', {exit: 1}) +} +``` diff --git a/.claude/rules/e2e-testing.md b/.claude/rules/e2e-testing.md new file mode 100644 index 0000000..08edcf7 --- /dev/null +++ b/.claude/rules/e2e-testing.md @@ -0,0 +1,111 @@ +# E2E Testing Conventions + +End-to-end suites live in `test/e2e/` and spawn the **built** CLI against a **real** API. +They are separate from the mocked unit suite in `test/commands/`. Full usage: +`test/e2e/README.md`. + +## Boundaries + +| | Unit (`test/commands/*.test.ts`) | E2E (`test/e2e/*.e2e.ts`) | +|---|---|---| +| Runs | `runCommand` in-process | `node bin/run.js` as a child process | +| API | `global.fetch` mocked | Real, over the network | +| Asserts | stdout string contents | exit code + stdout/stderr | +| Command | `npm test` | `npm run test:e2e` | + +The `.e2e.ts` suffix is what keeps them apart — `npm test`'s glob is `test/**/*.test.ts`. +Never rename an e2e file to `.test.ts`, and never add `test/e2e` to `.mocharc.yml`. + +## Rules + +- **Everything goes through the CLI.** No suite makes a direct HTTP call. Setup, + assertions and teardown all use `cli()`. The e2e layer has no API client of its own, + so it cannot drift from the one under test. +- **One file per command group**, named for the group: `test/e2e/.e2e.ts`. +- **Name every created resource** with `e2eName(label)` and register it on a + `ResourceTracker`; tear it down in the suite's `after()`. +- **Always pass `--force`** to destructive commands. Child stdin is `'ignore'`, so an + unforced `confirm()` exits 2 at once ("Refusing to prompt: stdin is not a terminal") and + the command does nothing. +- **Match error text with `errorText(result)`**, never `result.stderr` directly — the + CLI hard-wraps messages, so a phrase can be split across lines with padding. +- **Never add mocha `--retries` or `--parallel`.** Mocha retries re-run the whole test, + creating resources twice; parallel suites collide on shared organization state. Retry belongs + in `retryRead` (poll a read until the API's cache catches up) and `cliWithRetry` (re-run + a command whose failure matches a transient environment fault). `cliWithRetry` is safe + for assertions too — a genuine failure does not match a transient pattern, and a matched + one is still returned once attempts run out. Shared dev environments do fail in bursts, + including spurious 401s on a valid key. +- **No API key in the repository** — this repo is public; keys come from + `DATABOX_E2E_API_KEY` only. +- **Never mutate a resource the suite did not create without `withRestore()`.** It + records the undo on disk before the change, so an interrupted run can be repaired + with `npm run test:e2e:cleanup`. A bare `finally` does not survive Ctrl-C. +- **A command rename or removal sweeps `test/e2e/` too.** `npm test` never loads e2e files, + so a stale `cli(['old-topic', 'cmd'])` stays green until the live suite runs. + `test/e2e-commands.test.ts` checks every e2e argv against `src/commands/`; a deliberate + negative test goes on its allowlist. + +## Classifying a failure + +A failing e2e test means one of three things. Say which, in the test: + +1. **A CLI defect** — **fix the command.** A skipped test is green and CI cannot tell it + from a passing one, so parking a known-broken command as a skip makes the suite lie. + Only if the fix is genuinely deferred, use `it.skip` with `[BROKEN: ]` in the + title plus a comment giving the API's real contract, the observed symptom, and the + source file that fixes it — and keep that list short. +2. **An environment outage** — `this.skip()` at runtime via `serviceUnavailable(result)`, + which recognises the API's own 5xx/service-down messages. Never hard-code an outage + as expected behaviour. +3. **A capability the organization lacks** — `this.skip()` with a logged reason: it manages + no accounts, no Advanced Security add-on, no databoards, no connections. + +A skip must always print or carry its reason. A silent skip is worse than a failure. + +## Verifying a suspected CLI defect + +Before marking anything `[BROKEN]`, confirm it against the raw endpoint with `curl`, +so the report distinguishes a CLI bug from an API one. Two defect families found so far, +both invisible to the unit suite: + +- **Envelope drift** — a command reads `response.items` where the API returns a bare + array, or hands `{items: […]}` straight to `formatOutput`. The unit mock defines the + shape, so it always agrees with itself. Only a real response settles it. +- **Request-body drift** — a command sends a field name the API does not accept + (`interval` vs `syncInterval`, `status` vs `isVerified`, `tags` vs `synonyms`). These + commands could never succeed, and every one of them had a passing unit test. + +**Any command that sends a request body needs both**: an e2e test, and a unit test +asserting the body via `lastBody(method, path)` from `test/helpers.ts`. The unit +assertion is the cheap guard that runs on every `npm test`; the e2e test is what proves +the field names are the ones the API actually wants. + +```typescript +it('sends syncInterval, not interval', async () => { + await runCommand(['dataset', 'set-sync-frequency', '123', '--interval', '60'], {root: process.cwd()}) + expect(lastBody('PUT', '/v2/datasets/123/sync-frequency')).to.deep.equal({syncInterval: 60}) +}) +``` + +Note that `@oclif/test`'s `runCommand` refuses an **empty-string** flag value even though +the real binary accepts one, so assertions about clearing a nullable field belong in the +e2e suite. + +## The API is the source of truth + +`ingestion-api` defines the contract; the CLI follows it. Concretely: + +- A command exposes **every field** of its request contract in + `IngestionApi.Core/Contracts/Request/V2/`, and every query parameter its controller + action declares. +- `--json` returns **what the endpoint returned**. Do not hand-pick a subset into a new + object, and do not reshape values. List commands unwrap `response.items` to a bare + array — that is the one established convention — but the item objects pass through whole. +- Optional string fields are guarded with `!== undefined`, never truthiness, so an empty + string can clear a nullable field. +- Response interfaces mirror the response contract, including inherited members (a + `…Detail` type extends its `…ListItem`). + +Re-derive the mapping from the API source rather than from memory or from the existing +CLI code, and confirm anything surprising against a live endpoint with `curl`. diff --git a/.claude/rules/security.md b/.claude/rules/security.md new file mode 100644 index 0000000..e8983a0 --- /dev/null +++ b/.claude/rules/security.md @@ -0,0 +1,44 @@ +--- +paths: + - src/** +--- + +# Security Rules + +## API key handling + +- `apiClient.apiKey` must never appear in `this.log()`, `console.log()`, or error messages. +- The only legitimate direct access to `apiKey` is in `ask-genie.ts` for the SSE `x-api-key` header. +- Hidden flags (`--api-key`, `--api-url`, `--account-id`) must remain `hidden: true` — they do not appear in help output. + +## Config file + +- Config lives at `~/.config/databox-cli/config.json` with `apiKey` and optional `apiUrl`. +- Never log config file contents. Never weaken file permissions. + +## URL interpolation + +- User-provided IDs are interpolated into API paths: `/v2/datasets/${args.datasetId}`. +- Numeric IDs validate with `requireNumericId()`, UUIDs (ingestion IDs) with `requireUuid()` — both safe. +- Metric keys are opaque strings: `requireMetricId()` plus `encodeURIComponent`. Encoding alone is not enough — it leaves `.`/`..` intact and `new URL()` resolves them, so `metric delete ..` would send `DELETE /v2/`. +- Every command that interpolates user input into a URL path validates it first. + +```typescript +// Safe — validated +this.requireNumericId(args.datasetId, 'Dataset ID') +const response = await this.apiClient.get(`/v2/datasets/${args.datasetId}`, ...) + +// Risk — unvalidated +const response = await this.apiClient.get(`/v2/connections/${args.connectionId}`, ...) +``` + +## Input handling + +- `JSON.parse()` on user-provided flag values (--schema, --records, --data) must be wrapped in try/catch with a user-friendly error message. A bare `JSON.parse` leaks a raw `SyntaxError`. +- `fs.readFileSync` on user-provided paths (--file flag) should check file existence first. +- No unbounded stdin reads without size limits in new commands. + +## Error messages + +- Do not include request headers, full URLs, or API keys in error output. +- `ApiRequestError` messages are displayed to the user — they come from the server and are acceptable. diff --git a/.claude/rules/testing.md b/.claude/rules/testing.md new file mode 100644 index 0000000..0c72ae7 --- /dev/null +++ b/.claude/rules/testing.md @@ -0,0 +1,57 @@ +--- +paths: + - test/** +--- + +# Testing Conventions + +## Framework + +Mocha + Chai (expect style) + `@oclif/test` (`runCommand`). ESM with ts-node loader. + +## Test structure + +```typescript +import {runCommand} from '@oclif/test' +import {expect} from 'chai' +import {cleanupTestConfig, mockApi, restoreApi, setupTestConfig} from '../../helpers.js' + +describe('domain action', () => { + beforeEach(() => { + setupTestConfig() + mockApi([{ + method: 'GET', + path: '/v2/resources', + response: {status: 'success', requestId: 'test', data: { ... }}, + }]) + }) + + afterEach(() => { restoreApi(); cleanupTestConfig() }) + + it('does the thing', async () => { + const {stdout} = await runCommand(['domain', 'action'], {root: process.cwd()}) + expect(stdout).to.include('expected value') + }) + + it('outputs JSON with --json', async () => { + const {stdout} = await runCommand(['domain', 'action', '--json'], {root: process.cwd()}) + const json = JSON.parse(stdout) + expect(json).to.have.property('expectedKey') + }) +}) +``` + +## Rules + +- **File location mirrors source**: `src/commands/dataset/get.ts` → `test/commands/dataset/get.test.ts` +- **Setup/teardown**: `setupTestConfig()` in `beforeEach`, `cleanupTestConfig()` + `restoreApi()` in `afterEach`. Always both. +- **Config isolation**: a test that redirects the config directory sets and restores both `HOME` and `USERPROFILE` + (deleting one that was unset). `os.homedir()` reads `USERPROFILE` on win32, so a `HOME`-only override is inert + there and the suite writes the developer's real config. `setupTestConfig()`/`setupEmptyConfig()` do both. +- **Mock envelope**: Full `{status: 'success', requestId: 'test', data: {...}}` — not just `{data}`. +- **Realistic mocks**: Include all fields the command accesses, not empty objects. +- **runCommand**: Always pass `{root: process.cwd()}`. +- **Destructive commands**: Use `--force` to skip interactive prompts. +- **Coverage per command**: At minimum one happy-path test + one `--json` test. +- **Error paths**: New validation logic (`requireNumericId`, empty-body guard) needs tests asserting the exit code. +- **Naming**: `describe('domain action')` matches the CLI invocation. `it('verbs behavior')`. diff --git a/.claude/skills/pr-review/SKILL.md b/.claude/skills/pr-review/SKILL.md new file mode 100644 index 0000000..084b40c --- /dev/null +++ b/.claude/skills/pr-review/SKILL.md @@ -0,0 +1,183 @@ +--- +name: pr-review +description: > + Use when asked to review a pull request, branch, or set of changes in this `databox-cli` + repository — "review PR", "review PR #N", "review my changes", "review branch X", a pasted + GitHub PR URL, or "what do you think of this PR". Always use this instead of a single-pass review. +allowed-tools: Agent, Read, Grep, Glob, Bash +--- + +# Comprehensive PR Review + +You are the **review lead**. You coordinate parallel specialist agents, validate their +findings, and synthesize one prioritized review for this TypeScript/oclif CLI project +(~80 commands, each self-contained: BaseCommand → ApiClient → output formatting). + +## Token discipline (non-negotiable) + +**Agents pull; you never push.** Never paste the diff, `.claude/rules/*`, `project-patterns.md`, +or agent instruction files into a prompt. Every agent has Bash/Read — send it the base SHA, +file paths, and the pointers below. You do not need to read the reference files or rules +yourself; you only read `git diff --stat`. + +## Phase 1: Gather context + +### 1. Identify the target and base + +Input may be a PR number, branch, GitHub URL, or "my changes" (current branch vs `master`). + +```bash +# PR number: make sure the head is checked out locally so agents can read source +gh pr checkout # skip if already on the branch +gh pr view --json title,body,baseRefName,additions,deletions --jq '{title,body,baseRefName,additions,deletions}' + +BASE=$(git merge-base HEAD master) # or the PR's baseRefName +git diff $BASE...HEAD --stat +git log $BASE...HEAD --oneline +REVIEW_DIR=/pr-review && mkdir -p $REVIEW_DIR +``` + +Record `BASE` (full SHA) and `REVIEW_DIR` — every agent prompt uses both. + +### 2. Review history (PRs only) + +```bash +gh pr view --json reviews --jq '.reviews[] | {author: .author.login, state, body}' +gh api repos/databox/databox-cli/pulls//comments --jq '.[] | {path, line, body}' +gh api repos/databox/databox-cli/issues//comments --jq '.[] | {author: .user.login, body}' +``` + +If prior reviews exist this is a **re-review (round N)**. Build a **prior-disposition ledger**: +`pattern/location · disposition (fixed | declined | backlogged) · sha/reason/link`. Used in Phase 4. + +### 3. Size tier and rule selection (from `--stat` only) + +| Tier | `src/` lines changed | Team | +|---|---|---| +| **Config-only** | no `.ts` files in `src/` or `test/` | No agents — you review the content directly | +| **Small** | < 100 | Correctness, Testing (+ Consistency if new command files added under `src/commands/`) | +| **Standard** | 100 – 800 | Correctness, Consistency, Testing (+ Security if `lib/`, `base-command.ts`, or `auth/` touched) | +| **Large** | > 800 | All 4 agents; if 15+ command files changed, split Consistency into 2 agents by command domain | + +Rules to name in prompts (by touched path): + +| Touched | Rule file | +|---|---| +| `src/commands/` | `commands.md` | +| `src/base-command.ts`, `src/lib/api-client.ts` | `api-client.md`, `security.md` | +| `src/lib/config.ts`, `src/lib/prompt.ts` | `security.md` | +| `src/lib/output.ts` | `commands.md` | +| `src/commands/auth/` | `security.md` | +| `test/` | `testing.md` | + +All paths are relative to `.claude/rules/`. Every agent gets the same rule list — the +Testing agent always gets `testing.md` in addition. + +## Phase 2: Spawn specialists (one message, parallel) + +`subagent_type: "general-purpose"`. Model: **Correctness, Security, Validator inherit the session model**; +**Consistency, Testing use `model: "sonnet"`** (sufficient for checklist-style verification). +Prompt template — fill the braces, nothing else: + +``` +You are the {AGENT} reviewer for a PR in the databox-cli repo (cwd is the repo root). +Read, in order: +1. .claude/skills/pr-review/references/agents/{agent}.md (your instructions) +2. .claude/skills/pr-review/references/agents/output-format.md +3. .claude/rules/{rule1}.md, .claude/rules/{rule2}.md ... +4. .claude/skills/pr-review/references/project-patterns.md (only your row of the per-agent table matters) +Diff: `git diff {BASE}...HEAD -- {paths}` (all changed files: {file list}) +Investigate beyond the diff (callers, tests, base classes) before reporting. +Write your full report to {REVIEW_DIR}/{agent}.md and return the same text. +PR: "{title}" — {one-line summary of intent}. {Round N re-review | First review}. Tier: {tier}. +``` + +Diff scoping per agent (`{paths}`): + +| Agent | `{paths}` | +|---|---| +| Correctness | `src/` | +| Consistency | `src/` | +| Security | `src/lib/ src/base-command.ts src/commands/auth/` | +| Testing | `test/ src/` | + +Small tier: use `src/ test/` for everyone. + +## Phase 3: Validate + +Skip if no BLOCKER or WARNING was reported. Otherwise spawn **one** validator: + +``` +You are the validator for a PR review. Read .claude/skills/pr-review/references/agents/validator.md +and .claude/skills/pr-review/references/project-patterns.md. +Specialist reports are in {REVIEW_DIR}/*.md — validate only their BLOCKER and WARNING findings. +Diff: `git diff {BASE}...HEAD`. Return only the validation output block. +``` + +Apply verdicts: **CONFIRMED** keeps severity · **DOWNGRADED** drops one level · +**DISMISSED** is removed. + +## Phase 4: Synthesize + +1. **Merge** findings that share a root cause (cite all agents). **Reconcile** against the + ledger: `backlogged`/`declined` → drop silently; `fixed` → keep, annotate + `(regression — previously fixed in )`. +2. **Severity**: BLOCKER (must fix before merge) · WARNING (should fix) · SUGGESTION · PRAISE. +3. **Action per WARNING** — *Fix in PR* only on a positive signal: finding's `In diff: yes`, + or single-file fix under ~20 lines, or regression/test gap introduced by this PR. + Otherwise **Backlog** (pre-existing code, 3+ files, needs a design decision, systemic). +4. **Report** using this shape (omit empty sections): + +```markdown +# PR Review: {title} +_Round N — reconciled against M prior dispositions (K dropped as settled)._ ← re-reviews only + +## Summary +{2-3 sentences: mergeable? strongest / weakest aspect} + +## Verdict: APPROVE | REQUEST CHANGES | COMMENT +**Scope:** B{n} · W{fix}/{backlog} · S{n} · P{n} — {one-line justification} + +## Blockers (N) +### B1: {title} +**File** `path:L42-L55` · **Found by** {agents} · **Confidence** HIGH +**Issue** … **Impact** … **Fix** … (code when useful) + +## Warnings (N) +### W1: {title} +**File** … · **Found by** … · **Confidence** … +**Issue** … **Impact** … **Fix** … +**Action** Fix in PR | Backlog — {matched signal} + +## Suggestions (N) ← one line each: `S1 path:L — issue → fix` +## Praise (N) ← max 3, one line each, specific + +## Coverage +| Agent | Findings | Note | +|---|---|---| + +## Codification candidates (N) ← Phase 6 +``` + +## Phase 5: Triage confirmation + +If warnings exist, close with: + +> Review the **Action** on each warning and reply with overrides or "confirm". I'll then fix +> the *Fix in PR* items (and blockers if you want) and list *Backlog* items for filing. + +Offer, when relevant: fix blockers · write missing tests. + +## Phase 6: Codification candidates + +Run only when there is at least one BLOCKER or WARNING. A candidate is either +**(A) recurred** — 2+ findings share a root pattern across files/agents — or +**(B) known but ungraduated** — matches a `project-patterns.md` entry with no `.claude/rules/` link. +Draft the text in the target file's style (patterns: bold title + context + "Watch for"; +rules: 2–3 lines + Good/Bad example) and ask which to accept. + +## Finding quality bar + +Specific (exact lines) · actionable (concrete fix) · justified (why) · proportional (severity = +impact). No style nits (linting owns formatting), no restating rules, no generic praise, +and frame uncertain intent as a question rather than a finding. diff --git a/.claude/skills/pr-review/references/agents/consistency.md b/.claude/skills/pr-review/references/agents/consistency.md new file mode 100644 index 0000000..1d784d3 --- /dev/null +++ b/.claude/skills/pr-review/references/agents/consistency.md @@ -0,0 +1,67 @@ +# Consistency Agent + +You are the **consistency reviewer** — verify that commands follow the established patterns +and conventions of this oclif CLI project. The codebase has ~80 commands that all follow the +same patterns; drift is the primary risk. Apply the `.claude/rules/` files you were told to +read; don't restate them. + +## What to look for + +**Command structure** +- Class extends `BaseCommand` (exception: `auth login` extends `Command` directly). +- Static members ordered alphabetically: `args`, `description`, `examples`, `flags`. + Every command must have this exact order. +- Class name matches file path: `data-source/get.ts` exports `DataSourceGet`, + `dataset/list.ts` exports `DatasetList`. +- `async run(): Promise` — the only instance method. + +**Flag conventions** +- Flag names: kebab-case (`page-size`, `data-source-id`). Never camelCase. +- Body/query params: camelCase (`pageSize`, `dataSourceId`). Manual conversion in command body. +- `Flags.string()` / `Flags.integer()` / `Flags.boolean()` — correct type for the data. +- `required: true` on mandatory flags, `options: [...]` for enums, `exclusive: [...]` for mutual exclusion. +- Boolean flags use `default: false`. + +**Import conventions** +- All imports use `.js` extension (ESM requirement): `'../../base-command.js'`, not `'../../base-command'`. +- Import ordering: node builtins (`node:fs`) → oclif (`@oclif/core`) → local (`../../base-command.js`). +- Only import what's used. Destructured imports from oclif: `{Args, Flags}`, `{Args}`, `{Flags}` — only what's needed. + +**Examples** +- Use `<%= config.bin %>` template syntax, never hardcoded `databox`. +- At least 2 examples per command (basic usage + `--json` or variant). +- Examples demonstrate realistic usage, not just flag enumeration. + +**Output formatting by command type** +- List: `formatOutput(data, columns, json)` + `showPagination(pagination, json)`. +- Get / Create / Update: `formatSingle(data, json)`. +- Delete / Purge / Clear: `this.log('Resource ID action.')` — no `formatSingle`. +- Set operations: `this.log()` confirmation or `formatSingle()`. + +**Destructive operations** +- `--force` flag with `default: false`. +- `confirm()` from `../../lib/prompt.js` when not forced. +- `this.log('Aborted.')` when user declines. +- Success: `"Resource ID past-tense."` (e.g., `"Dataset 123 deleted."`, `"Data source 456 purged."`). + +**Error codes** +- `this.error(msg, {exit: 1})` — general errors. +- `this.error(msg, {exit: 2})` — input validation errors. + +**API client usage** +- Always pass `this.accountHeaders` as the last argument to API calls. +- No try/catch around API calls (except `auth login`). +- No direct `fetch()` calls (except `ask-genie.ts`). + +**Description text** +- `static description` is a short sentence fragment (no period, starts with verb or noun). +- Flag `description` properties are short, start lowercase after the flag name. + +## How to review + +1. Compare the changed command's structure against 2-3 existing commands of the same type. +2. Check static member ordering is alphabetical. +3. Verify flag naming (kebab-case) vs body/query property naming (camelCase). +4. Confirm the right output function is used for the command type. +5. Verify examples use `<%= config.bin %>` and are realistic. +6. For new commands, check they follow the canonical pattern for their type (list/get/create/update/delete/set). diff --git a/.claude/skills/pr-review/references/agents/correctness.md b/.claude/skills/pr-review/references/agents/correctness.md new file mode 100644 index 0000000..103198d --- /dev/null +++ b/.claude/skills/pr-review/references/agents/correctness.md @@ -0,0 +1,53 @@ +# Correctness Agent + +You are the **correctness reviewer** — find bugs, logic errors, edge cases, and runtime +behaviour that would misbehave in production or give users confusing errors. +Apply the `.claude/rules/` files you were told to read; don't restate them. + +## What to look for + +**JSON.parse on user input without try/catch** +- `JSON.parse(flags.xxx)` on user-provided values (`--schema`, `--records`, `--data`, + `--date`, `--measure`, `--tags`, `--columns`) must be wrapped in try/catch with + `this.error('Invalid JSON for --flag: ...', {exit: 2})`. +- A bare `JSON.parse` leaks a raw `SyntaxError` with no actionable message. +- `ask-genie.ts` wraps its `JSON.parse` correctly — that is the model to follow. + +**Missing requireNumericId validation** +- Dataset commands call `requireNumericId()`. Other commands interpolate args straight into + URL paths (`/v2/connections/${args.connectionId}`) without validation. +- New commands taking resource IDs as args should validate or document why the ID may be non-numeric. + +**Missing empty-body guard in update commands** +- Update commands with optional flags must check `Object.keys(body).length === 0` and error + with exit code 1. Some existing updates have this, some don't — new ones must. + +**Error propagation** +- Commands intentionally do NOT wrap API calls in try/catch — errors propagate to oclif's handler. +- Verify new commands maintain this pattern: no swallowed errors, no redundant catch blocks. +- Exception: `auth login` has a try/catch for validation (intentional — validation failure is non-fatal). + +**Edge cases** +- Empty string args (e.g., `databox dataset get ""`) — accepted by oclif, passed to API as empty path segment. +- Negative page numbers — `Flags.integer()` accepts negatives with no guard. +- `fs.readFileSync` in `dataset ingest --file` with no file-existence check — raw Node error. +- Stdin detection via `!process.stdin.isTTY` — may incorrectly detect piped input in some environments. + +**Double-parse inconsistency** +- `BaseCommand.init()` parses flags into `this.flags`. Many commands also `await this.parse(ClassName)` + in `run()` to destructure `{args, flags}` locally. +- Mixing `flags.xxx` (local) and `this.flags.xxx` (from init) for the same data is fragile. + Watch for new commands that reference both for overlapping flag names. + +**Type safety** +- Interfaces defined inline per command — verify the interface matches what the API actually returns. +- Optional/nullable fields should use `| null` or `?`, not assume presence. +- `Flags.integer()` returns `number | undefined` — check for `undefined` before using in arithmetic. + +## How to review + +1. Read surrounding code at each changed location, not just the hunk. +2. For new commands, trace the full flow: flag parsing → validation → API call → output formatting. +3. Check if the command type (list/get/create/update/delete/set) follows its established sub-pattern. +4. Look for `JSON.parse` without try/catch on any user-provided input. +5. Check that every user-provided ID interpolated into a URL path is validated. diff --git a/.claude/skills/pr-review/references/agents/output-format.md b/.claude/skills/pr-review/references/agents/output-format.md new file mode 100644 index 0000000..bed774d --- /dev/null +++ b/.claude/skills/pr-review/references/agents/output-format.md @@ -0,0 +1,34 @@ +# Common Output Format + +Return **only** this block — no preamble, no restated rules, no quoted diff. + +``` +## [Agent Name] Review + +### Summary +[1-2 sentences: what you examined, overall assessment] + +### Findings + +#### [BLOCKER|WARNING|SUGGESTION|PRAISE] - [Short title] +- **File**: `src/commands/dataset/list.ts:L42` (or `L42-L55`) +- **In diff**: yes | no (is the flagged line inside a hunk this PR changes?) +- **Confidence**: HIGH | MEDIUM | LOW +- **Description**: [what the issue is] +- **Why it matters**: [impact — what goes wrong, who is affected] +- **Suggested fix**: [concrete recommendation; code only if it clarifies] + +### No-Issue Confirmation +[≤ 5 one-line bullets: areas checked that were clean] +``` + +## Budget + +- At most **8 findings**. If you have more, keep the highest severity / confidence and fold + the rest into one SUGGESTION ("also: …"). +- At most **2 PRAISE**, and only for something specific and non-obvious. +- Merge findings that share a root cause into one entry listing all locations. +- Only report what you verified by reading surrounding code — LOW confidence means you could + not verify, not that you didn't look. +- Do **not** run `npm test` / `npm run build` — CI and the review lead handle that. Spend + your budget reading code. diff --git a/.claude/skills/pr-review/references/agents/security.md b/.claude/skills/pr-review/references/agents/security.md new file mode 100644 index 0000000..c27807b --- /dev/null +++ b/.claude/skills/pr-review/references/agents/security.md @@ -0,0 +1,60 @@ +# Security Agent + +You are the **security reviewer** — find API key exposure, input sanitization issues, +config file security risks, and unsafe URL interpolation. Apply the `.claude/rules/` files +you were told to read; don't restate them. + +## What to look for + +**API key exposure** +- `apiClient.apiKey` must never appear in `this.log()`, `console.log()`, or error messages. +- The only legitimate access to `apiClient.apiKey` is in `ask-genie.ts` for the SSE header. +- Verify no new commands access `apiClient.apiKey` directly. +- Search for string literals containing "apiKey", "api-key", "x-api-key" in log/error output. + +**Hidden flags** +- `--api-key`, `--api-url`, `--account-id` must remain `hidden: true` in `BaseCommand.baseFlags`. +- Verify no command overrides these flags without `hidden: true`. + +**Config file security** +- Config at `~/.config/databox-cli/config.json` contains the API key. +- Never log config file contents or path with key. +- No changes that expose the config file to stdout or error output. + +**URL path interpolation** +- User-provided IDs interpolated into paths: `/v2/datasets/${args.datasetId}`. +- With `requireNumericId` (digits only) this is safe. +- Without validation, a user could pass values containing `/`, `?`, or `..` — producing unexpected API paths. +- New commands that interpolate user input into URL paths must validate the input. + +```typescript +// Safe +this.requireNumericId(args.datasetId, 'Dataset ID') + +// Risk — connectionId could contain path-traversal characters +await this.apiClient.get(`/v2/connections/${args.connectionId}`, ...) +``` + +**Bypassing ApiClient** +- Any `fetch()` call not going through `ApiClient` must be justified (currently only `ask-genie.ts` for SSE). +- New direct `fetch()` calls must not hard-code API keys, must handle errors, must not log headers. + +**Input sanitization** +- `JSON.parse` on user flags without try/catch leaks raw `SyntaxError` stack traces. +- `fs.readFileSync` on user-provided paths — verify no path traversal concern for the use case. +- Stdin reads — verify no unbounded memory allocation. + +**Error message content** +- Error messages displayed to the user must not contain: + - Request headers (especially `x-api-key`) + - Full URL paths with embedded credentials + - Stack traces (except via oclif's default error handler in debug mode) +- `ApiRequestError` messages from the server are acceptable — they're designed for end users. + +## How to review + +1. Search changed files for `apiKey`, `api-key`, `config`, `fetch(`, `process.env`. +2. Check that hidden flags remain hidden. +3. Verify any user input going into URL paths is validated. +4. Look for new `fetch()` calls bypassing `ApiClient`. +5. Check error messages for internal details. diff --git a/.claude/skills/pr-review/references/agents/testing.md b/.claude/skills/pr-review/references/agents/testing.md new file mode 100644 index 0000000..59c2ce9 --- /dev/null +++ b/.claude/skills/pr-review/references/agents/testing.md @@ -0,0 +1,73 @@ +# Testing Agent + +You are the **testing reviewer** — find coverage gaps, mock quality issues, and test +structure problems. Apply the `.claude/rules/` files you were told to read; don't restate them. + +## What to look for + +**Coverage gaps** +- Every new command in `src/commands/` must have a corresponding test at + `test/commands//.test.ts`. +- Every new command must have at least one happy-path test and one `--json` test. +- New validation logic (`requireNumericId`, empty-body guard, `JSON.parse` wrap) needs + tests asserting the correct exit code. +- New flags should have at least one test exercising them. +- Error paths: if the command has a `this.error()` call, there should be a test that + triggers it. + +**Mock quality** +- Mock responses must include the full V2 envelope: + `{status: 'success', requestId: 'test', data: {...}}`. + Missing `status` or `requestId` will not break tests today but violates the contract. +- The `data` field must match the response type the command expects — not just `{}`. + Include all fields the command accesses in its `formatOutput`/`formatSingle` call. +- Mock HTTP method must match what the command actually calls (`GET`, `POST`, `PATCH`, `PUT`, `DELETE`). +- Mock path must match the exact API path including any interpolated IDs. + +```typescript +// Good — realistic mock +mockApi([{ + method: 'GET', + path: '/v2/datasets/123', + response: {status: 'success', requestId: 'test', data: { + id: 123, title: 'Revenue', dataSourceId: 456, createdAt: '2024-01-01', + timezone: 'UTC', primaryKey: null, schema: null, + }}, +}]) + +// Bad — empty data, missing fields +mockApi([{ + method: 'GET', + path: '/v2/datasets/123', + response: {status: 'success', requestId: 'test', data: {}}, +}]) +``` + +**Test structure** +- `setupTestConfig()` in `beforeEach`, `cleanupTestConfig()` + `restoreApi()` in `afterEach`. + Both cleanup calls are required — missing either leaks state. +- `runCommand()` always includes `{root: process.cwd()}`. +- `describe('domain action')` naming matches CLI invocation (e.g., `'dataset list'`). +- `it('verbs behavior')` naming (e.g., `'lists datasets'`, `'deletes with --force'`). + +**Destructive command tests** +- Delete/purge/clear tests must use `--force` to skip interactive prompts. +- Verify the success message matches the pattern: `"Resource ID action."`. + +**Missing test cases to flag** +- List commands: test with empty results (`items: []`). +- Commands with pagination: test that `showPagination` output appears. +- Commands with optional flags: test the default behavior (no flags) and with flags. + +**Tests impacted by diff** +- If a command's interface or output format changed, check that its test still validates + the new shape. +- If `test/helpers.ts` changed, check that all tests still work with the new helpers. + +## How to review + +1. For each changed command file, verify a corresponding test file exists and was updated. +2. Check mock responses include the full API envelope and realistic data. +3. Verify destructive commands test with `--force`. +4. Look for missing error path tests. +5. Check that test names match conventions (`describe`/`it` naming). diff --git a/.claude/skills/pr-review/references/agents/validator.md b/.claude/skills/pr-review/references/agents/validator.md new file mode 100644 index 0000000..eda8cb6 --- /dev/null +++ b/.claude/skills/pr-review/references/agents/validator.md @@ -0,0 +1,51 @@ +# Validator Agent + +You are the **adversarial validator** — the last line of defense against false positives. +A finding is guilty until proven innocent: try to disprove each one against the real code +and confirm only where you cannot find a reasonable counter-argument. + +## Inputs + +- Specialist reports in the review directory you were given (`*.md`). Validate **only** + BLOCKER and WARNING findings; ignore SUGGESTION and PRAISE. +- The diff command you were given. Run it once, scoped to the files the findings cite. +- `project-patterns.md` — a finding that matches a documented pattern is evidence toward CONFIRMED. + +## For each BLOCKER / WARNING + +1. Open the cited file at the cited lines, plus ~50 lines of context each side. +2. Look for mitigation the specialist may have missed: + - callers guarding the input; oclif flag validation (`required`, `options`) handling it + - base class (`BaseCommand`) providing defaults or validation + - the same pattern used safely elsewhere in the codebase + - for "missing test" findings: grep the test directory for the command name and claimed + behaviour before confirming — the test may live in a sibling file +3. Judge: is it real? is the severity right? would the suggested fix break something? + +Order: BLOCKERs first, then WARNINGs; spend the most effort on LOW/MEDIUM confidence +findings and on findings reported by multiple agents (same root cause?). + +## Verdicts + +- **CONFIRMED** — real, no mitigation found, severity appropriate. Add evidence if you found more. +- **DOWNGRADED** — merit, but partial mitigation or rare path; state the lower severity. +- **DISMISSED** — handled elsewhere. Cite the file:line that disproves it. + +## Output + +``` +## Validation Results + +### Finding: [original title] +**Original severity**: BLOCKER|WARNING · **Reported by**: [agents] +**Verdict**: CONFIRMED|DOWNGRADED|DISMISSED +**Evidence**: [file:line references] +**Reasoning**: [specific, not generic] + +### Validation Summary +Validated N · Confirmed N · Downgraded N · Dismissed N — [one sentence on signal quality] +``` + +Don't rubber-stamp ("looks correct" is useless), don't dismiss because a pattern is common, +and don't add new findings except under a brief "Additional concerns" if something critical +was clearly missed. diff --git a/.claude/skills/pr-review/references/project-patterns.md b/.claude/skills/pr-review/references/project-patterns.md new file mode 100644 index 0000000..2c22816 --- /dev/null +++ b/.claude/skills/pr-review/references/project-patterns.md @@ -0,0 +1,116 @@ +# Project-Specific Patterns and Pitfalls + +Known recurring issues and patterns specific to this codebase. Review agents should check for +these actively — they represent real bugs and review feedback, not hypothetical concerns. + +--- + +## Critical + +**Fields guessed from the endpoint name instead of read from the C# contract** +The single largest source of real bugs in this repo. Six commands sent request bodies the API +silently ignored (`interval` for `syncInterval`, `status` for `isVerified`, `tags` for +`synonyms`), four crashed rendering a response shape that never existed, and six table columns +were permanently blank. **Every one of them had a passing unit test**, because the mock encoded +the same guess as the code, so the test and the bug agreed with each other. +- **Watch for**: any request body field, response interface member or table column that cannot + be traced to `ingestion-api/src/IngestionApi.Core/Contracts/{Request,Response}/V2/`. Check the + contract, not the endpoint name, and not the existing CLI code. +- **Watch for**: envelope assumptions — `response.items` where the endpoint returns a bare + array, or `{items: […]}` handed straight to `formatOutput`. +- A command that sends a body needs **both** an e2e test and a unit test asserting the body via + `lastBody(method, path)`. A unit test alone cannot catch this class of bug. +- Inheritance counts: a `…Detail` response extends its `…ListItem`, so a detail view that + returns fewer fields than a list row is a defect, not a design choice. + +**Unwrapped JSON.parse on user-provided flag values** +~10 commands parse flag values with bare `JSON.parse()` (`--schema`, `--records`, `--data`, +`--date`, `--measure`, `--tags`, `--columns`). Malformed JSON produces a raw `SyntaxError` +with no actionable message. Only `ask-genie.ts` wraps its `JSON.parse` correctly. +- **Watch for**: any `JSON.parse(flags.xxx)` or `JSON.parse(args.xxx)` without a try/catch + that calls `this.error('Invalid JSON for --flagname: ...', {exit: 2})`. + +```typescript +// Bad — raw SyntaxError to user +body.schema = JSON.parse(flags.schema) as SchemaType + +// Good — user-friendly error +try { + body.schema = JSON.parse(flags.schema) as SchemaType +} catch { + this.error('Invalid JSON for --schema. Expected format: [{"id":"...","dataType":"..."}]', {exit: 2}) +} +``` + +--- + +## High + +**A flag rename that does not sweep every surface** +Renaming a flag touches five places, and a PR that updates only the first is worse than one that +renames nothing — the docs then actively mislead. `--tags`→`--synonyms` and `--key`→ +`--integration-key` each left stale references behind, and a review was filed against the +*correct* README on the assumption a rename had happened that had not. +- **Watch for**: a changed flag name in `src/commands/` with no matching change in `test/`, + `README.md` (regenerate with `npx oclif readme`), `skills/databox-*/SKILL.md` and + `CHANGELOG.md`. Grep the old name across the repo; the count should be zero. +- Before reporting a rename as a bug, confirm the old flag is actually gone. Different commands + legitimately use different names for the same concept (`--data-source-id` on `dataset create` + and `dataset list`; `--source-id` on `metric list`, `metric drilldown` and + `metric dimension-values`; `--dataset-id` on `metric create`). + +**Double-parse inconsistency** +`BaseCommand.init()` parses flags into `this.flags`. Most commands with args also call +`this.parse(ClassName)` in `run()` to destructure `{args, flags}` locally. Some commands +reference `flags` (local) for domain flags but `this.flags` (from init) for base flags +like `json` and `account-id`. This works today but is fragile. +- **Watch for**: mixing `flags.xxx` and `this.flags.xxx` in the same command for the same + or overlapping data. + +--- + +## Medium + +**Empty update bodies** +Update commands whose flags are all optional must refuse to send an empty PATCH and name the +flags they wanted. Every such command now does; `test/validation/empty-body.test.ts` sweeps them +and its last test walks `src/commands` to assert the table still covers every guard. +- **Watch for**: a new all-optional update command that builds a body conditionally and never + checks whether it stayed empty, or one added without a row in that sweep. +- The guard is **exit 1**, not exit 2, per `.claude/rules/commands.md` — the command is + well-formed, it just has nothing to do. Do not report this as an exit-code bug. + +--- + +## Low + +**ask-genie bypasses ApiClient** +`analyze/ask-genie.ts` makes a direct `fetch()` call to a different service URL, accessing +`this.apiClient.apiKey` directly. Intentional (SSE streaming not supported by ApiClient) +but creates a maintenance risk if ApiClient's header logic changes. +- **Watch for**: new commands that bypass ApiClient for non-standard protocols — they + should document why. + +**Exit codes** +The convention is settled in `.claude/rules/commands.md`: **exit 2** for input validation +(`requireNumericId`, `requireUuid`, `parseJsonFlag`), **exit 1** for general errors, including the +empty-body guard above. +- **Watch for**: new commands using the wrong code. Check the rule before reporting one as wrong. + +**Hand-rolled pagination or sorting flags** +`src/lib/flags.ts` owns `paginationFlags` and `sortFlags` (page is 0-indexed with `min: 0`, +page-size `min: 1`), plus `addPagination`/`addSorting` for the query string. Declaring these +inline per command is how the defaults drifted apart in the first place. +- **Watch for**: a list command declaring its own `page`/`page-size`/`sort` flags, or declaring + pagination flags it then never sends as query params. + +--- + +## Per-agent mapping + +| Agent | Relevant pattern sections | +|---|---| +| **Correctness** | Guessed contract fields, unwrapped JSON.parse, double-parse, empty-body guard, exit codes | +| **Consistency** | Flag-rename sweep, hand-rolled pagination/sorting flags, double-parse, exit codes | +| **Security** | ask-genie bypasses ApiClient (direct apiKey access) | +| **Testing** | Missing `lastBody()` assertion on any command that sends a body; missing error-path tests for JSON.parse, resource IDs and empty bodies — each has a sweep under `test/validation/` whose final test asserts the table still covers every call site | diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..fb44223 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,9 @@ +{ + "extends": ["oclif", "oclif-typescript"], + "ignorePatterns": ["/lib", "node_modules", "/bin"], + "rules": { + "array-bracket-newline": "off", + "array-element-newline": "off", + "valid-jsdoc": "off" + } +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..18c8e84 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Tests +on: + workflow_call: + workflow_dispatch: + pull_request: + types: [opened, reopened, synchronize] + branches: [main] + +jobs: + Test: + runs-on: ubuntu-latest + steps: + - name: "Checkout" + uses: actions/checkout@v4 + - name: "Setup Node" + uses: actions/setup-node@v4 + with: + node-version: '22.x' + cache: 'npm' + - name: "Installing dependencies" + run: npm ci + - name: "Typecheck" + run: | + npx tsc -b + npx tsc -p tsconfig.test.json --noEmit + # Runs the linter first via the pretest hook, then the unit suite. + # The e2e suite is deliberately not run here: it needs a live API and a key. + - name: "Lint and test" + run: npm test diff --git a/.gitignore b/.gitignore index 1a7c1a1..5ab5cbb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,12 @@ node_modules/ oclif.manifest.json .DS_Store .agents/ -.claude/ +.claude/settings.json skills-lock.json + +# Local env files can hold an API key, and this repo is public +.env +.env.* + +# e2e undo log for mutations to shared resources +.e2e-restore.json diff --git a/.mocharc.e2e.yml b/.mocharc.e2e.yml new file mode 100644 index 0000000..432bb15 --- /dev/null +++ b/.mocharc.e2e.yml @@ -0,0 +1,11 @@ +# End-to-end suite: spawns the built CLI against a real API. +# Run with `npm run test:e2e`. See test/e2e/README.md for configuration. +# +# Deliberately NOT set: +# parallel — suites share one account's state +# retries — a re-run `it` would create its resources twice +node-option: + - loader=ts-node/esm +spec: test/e2e/**/*.e2e.ts +timeout: 180000 +slow: 5000 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2768a87 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,221 @@ +# Changelog + +## 1.0.0 — V2 API + +**Breaking change.** The CLI now uses the Databox V2 API exclusively; every V1 call is gone. It needs your personal Databox API key (`pak_…`), created under **Account Management → Security**. Creating one takes an admin role and a plan that includes API access; see [Getting an API Key](https://github.com/databox/databox-cli#getting-an-api-key). + +What breaks for a 0.x user, in short: + +- **Dataset IDs are numeric.** GUIDs are rejected. Find the new ID with `databox dataset list`, or see [Finding IDs](https://github.com/databox/databox-cli#finding-ids). +- **`--title` is now `--name`** on `data-source create` and `dataset create`. +- **`dataset create --primary-keys` is now `--primary-key`** (still repeatable), and each `--schema` column is `{"id", "dataType"}` instead of `{"name", "dataType"}`. +- **`data-source create --key` is now `--integration-key`.** +- **Your organization has its own topic, and `account` means the accounts in it.** `organization info`, `organization update`, `organization usage` and `organization timezones` cover your organization; `account list` lists the accounts in it. `account data-sources` and `account datasets` are gone: use `data-source list` and `dataset list`, with `--account-id`, now a global flag, to target an account on any command. +- **JSON output uses the V2 API's field names.** Scripts that parse `--json` output need updating; see "JSON Output" below. +- **Lists are paginated.** 0.x printed every item; 1.0 prints the first page unless you pass `--all`. See [List Flags](https://github.com/databox/databox-cli#list-flags). +- **Off a terminal, a prompt needs piped input.** A delete, purge or clear without `--force` reads its `y`/`yes` from stdin, as before, and `auth login` without `--api-key` now reads the key from stdin. When nothing is piped, both exit 2 and do nothing. See [Authentication](https://github.com/databox/databox-cli#authentication) and [Errors and Exit Codes](https://github.com/databox/databox-cli#errors-and-exit-codes). + +Beyond that, 1.0.0 adds commands across the whole V2 API, CSV output, `--all` pagination, `--verbose` tracing, idempotent retries, and structured errors with distinct exit codes. The [README](https://github.com/databox/databox-cli#readme) is the reference for all of it; this guide lists what changed. + +### Migration Guide + +#### Authentication + +`databox auth login` at a terminal, the stored key in `~/.config/databox-cli/config.json`, and the `DATABOX_API_KEY` environment variable work as before. New: off a terminal, `auth login` reads the key from stdin (`pass show databox | databox auth login`), and exits 2 when nothing is piped. See [Authentication](https://github.com/databox/databox-cli#authentication) for the details, including when the key is saved. + +#### Command Changes — Where Did My Stuff Go? + +Every 0.x command maps to a 1.0 command: + +| v0.x command | v1.0 equivalent | What changed | +|---|---|---| +| `account list` | `account list` | **Changed.** Lists the accounts in your organization, which `account get`, `create`, `update` and `delete` manage. It works only for an organization that manages accounts; any other gets an error, and reads its own details with `organization info`. To act inside an account, pass `--account-id` to any command. | +| `account data-sources ACCOUNTID` | `data-source list` | **Removed.** Use `data-source list`, with `--account-id` to target an account in your organization. | +| `account datasets ACCOUNTID` | `dataset list` | **Removed.** Use `dataset list`, with `--account-id` to target an account in your organization. The `--type` filter is gone; `--data-source-id`, `--search` and `--sort-by` are new. | +| `account timezones` | `organization timezones` | **Moved** to the `organization` topic. | +| `data-source create` | `data-source create` | `--title` → `--name`. `--key` → `--integration-key` (for third-party integrations such as Datadoo; omit it for a normal ingestion data source, since it now sets the integration type rather than a free-form key). `--account-id` is now the global flag. | +| `data-source datasets ID` | `data-source datasets ID` | Now paginated, and takes `--search`, `--sort-by` and `--sort-order`. | +| `data-source delete ID` | `data-source delete ID` | No change. | +| `dataset create` | `dataset create` | `--title` → `--name`. `--primary-keys` → `--primary-key` (repeat for several). Schema columns are `{"id", "dataType"}`; see the schema example below. | +| `dataset get GUID` | `dataset get NUMERIC_ID` | **IDs are now numeric.** | +| `dataset delete GUID` | `dataset delete NUMERIC_ID` | **IDs are now numeric.** | +| `dataset ingest GUID` | `dataset ingest NUMERIC_ID` | **IDs are now numeric.** | +| `dataset ingestion GUID ING_ID` | `dataset ingestion NUMERIC_ID ING_ID` | **The dataset ID is now numeric.** The ingestion ID is the UUID that `dataset ingest` returns. | +| `dataset ingestions GUID` | `dataset ingestions NUMERIC_ID` | **IDs are now numeric.** | +| `dataset purge GUID` | `dataset purge NUMERIC_ID` | **IDs are now numeric.** | +| `analyze ask-genie` | — | **Temporarily unavailable.** It has returned 403 in every version since 2026-08-20, 0.3.1 included, because the Genie service now requires internal authentication the CLI cannot provide. 1.0.0 hides it and exits 1 with that reason; it will return in a later release. | + +#### Schema Definition Change + +v0.x: +```bash +--schema '[{"name":"date","dataType":"datetime"},{"name":"value","dataType":"number"}]' +``` + +v1.0: +```bash +--schema '[{"id":"date","dataType":"datetime"},{"id":"value","dataType":"number"}]' +``` + +`dataType` is one of `string`, `number` or `datetime`. The column `id` is also how you refer to the column everywhere else: in `--primary-key`, in metric column references, and in modifications. + +#### Dataset ID Migration + +0.x used GUIDs for datasets (e.g. `a1b2c3d4-e5f6-...`). 1.0 uses numeric IDs (e.g. `12345`) and rejects anything else with exit code 2. To find the numeric ID of an existing dataset: + +```bash +databox dataset list --search "My Dataset" +``` + +The README's [Finding IDs](https://github.com/databox/databox-cli#finding-ids) explains every ID the CLI takes, including where the Databox app shows them. + +#### Agent Skills + +The bundled skills follow the new topics. 0.3.1's `databox-accounts` covered account listing, timezones, and an account's data sources and datasets. In 1.0.0: + +- `databox-organization` covers your organization: details, usage, settings and timezones. +- `databox-accounts` covers the accounts in your organization: listing, creating, updating and deleting them. +- `databox-data-sources` and `databox-datasets` cover listing data sources and datasets, with `--account-id` for an account. +- `databox-metrics`, `databox-users`, `databox-connections`, `databox-integrations` and `databox-billing` are new. +- `databox-analyze` is removed for now, until `analyze ask-genie` returns. + +Reinstall them with `npx skills add databox/databox-cli --skill '*'`. + +#### JSON Output + +`--json` still prints JSON on every command, but the objects are the V2 API's own. For example, `dataset ingestions` items carry `id`, `initiatedAt`, `status`, `duration` and `initiatedBy`, and `data-source datasets` items carry `id`, `name`, `dataSourceId`, `createdAt`, `lastActivityAt` and status details. Check the output of the commands your scripts use. + +A list prints a bare array of the API's items; a few commands print the whole response object. The rules are under [Output Formats](https://github.com/databox/databox-cli#output-formats). + +#### Summary of Removed Features + +| Feature | Why | Alternative | +|---|---|---| +| Listing accounts across organizations (`account list`) | V2 scopes every call to one organization or account | `account list` for the accounts in your organization, `organization info` for your own | +| `--type` filter on dataset listing | Not part of the V2 API | `dataset list`, optionally filtered by `--data-source-id` or `--search` | +| GUID dataset IDs | V2 identifies datasets and data sources by numeric ID | `dataset list` to find the numeric ID | +| `ACCOUNTID` positional argument | Replaced by account scoping on every command | The global `--account-id` flag | + +### New Flags + +- **On every command** except `auth login`: `--output table|json|csv` (CSV is new), `--verbose` (request tracing on stderr), `--no-color`, and `--account-id` / `DATABOX_ACCOUNT_ID`, which replaces 0.x's `ACCOUNTID` argument. See [Global Flags](https://github.com/databox/databox-cli#global-flags). +- **On list commands**: `--page`, `--page-size`, `--all`, and on some `--search`, `--sort-by` and `--sort-order`. See [List Flags](https://github.com/databox/databox-cli#list-flags). +- **`--idempotency-key `** on `account create`, `data-source create`, `data-source purge`, `dataset create`, `dataset duplicate`, `dataset ingest`, `dataset purge`, `dataset update-modification`, `metric create` and `user invite`: a retry with the same key within 24 hours returns the first response instead of repeating the action. + +### Errors and Exit Codes + +An API error now prints its code, message, the field at fault and the request ID on stderr, and exit codes tell failures apart: `1` for an API error, `2` for input that never reached the API or a network failure, `130` for Ctrl-C at a prompt. See [Errors and Exit Codes](https://github.com/databox/databox-cli#errors-and-exit-codes). + +An empty flag value is never silently ignored: `--schema ""`, `--records ""`, `--file ""` and `--integration-key ""` fail with exit 2, as does a blank `--name`, and `--timezone ""` is sent for the API to reject. A redirect from the API is refused with exit 2 rather than followed, since following it would resend your API key. + +### New Commands + +91 commands in all, covering the V2 API. New in 1.0.0: + +#### Organization +- `organization info` — Show your organization's details +- `organization update` — Update the organization name, company, address, billing details, metadata and settings (date and number format, first day of week, `gregorian`/`customFiscal`/`weekAlignedFiscal` calendar) +- `organization usage` — Show usage statistics: users, data sources, accounts and AI credits +- `organization countries` — List available countries +- `organization metadata-options` — List the metadata options for organization settings + +With `--account-id`, the `organization` commands answer for that account. + +#### Profile +- `profile info` — Show your profile, with your organization and, if you belong to one, your home account +- `profile update` — Update your name, timezone or metadata (department, title, role) +- `profile metadata-options` — List the departments and roles `profile update --metadata` accepts + +#### Billing +- `billing info` — Show plan and billing details +- `billing invoices` — List invoices, with amounts in USD + +#### Users +- `user list` — List users, filtered by `--role` or `--search` +- `user get` — Get user details +- `user invite` — Invite a user with `--role admin|user|editor|viewer` +- `user update` — Change a user's name or role +- `user delete` — Remove a user + +#### Accounts (organizations that manage accounts) +- `account get` — Get account details +- `account create` — Create an account +- `account update` — Update an account's name, manager or website +- `account delete` — Delete an account + +#### Connections +- `connection list` — List connections +- `connection get` — Get connection details +- `connection update` — Rename a connection +- `connection delete` — Delete a connection +- `connection permissions` — Show permissions +- `connection set-permissions` — Set `--access-level everyone|selectedUsers|private` (with `--access-list` user IDs for `selectedUsers`). `--shared-with-accounts` or `--no-shared-with-accounts` is required, because every call replaces the sharing setting. + +#### Integrations +- `integration list` — Browse the integration catalog +- `integration get` — Get integration details + +#### Data Sources +- `data-source list` — List data sources, with `--search`, `--connection-id` and sorting +- `data-source get` — Get data source details +- `data-source update` — Rename a data source +- `data-source set-timezone` — Set the timezone, optionally for its datasets too +- `data-source sync-frequency-options` — List the sync intervals the data source can use, and which your plan includes +- `data-source set-sync-frequency` — Set `--interval` in minutes: 1, 15, 60, 240, 360, 480 or 1440. For an ingestion data source this sets how often metrics sync; the data itself arrives when you push it +- `data-source permissions` — Show permissions +- `data-source set-permissions` — Set `--access-level everyone|selectedUsers|private` +- `data-source purge` — Purge all data, keeping the data source + +#### Datasets +- `dataset list` — List datasets, with `--search`, `--data-source-id` and sorting +- `dataset update` — Rename a dataset +- `dataset duplicate` — Duplicate a dataset (not supported for datasets created through the API) +- `dataset data` — Page through a dataset's rows, sorted by any column +- `dataset schema` — Show columns and the primary key +- `dataset lineage` — Show what a dataset is built from, and what is built from it +- `dataset set-timezone` — Set the timezone +- `dataset sync-frequency-options` — List the sync intervals the dataset can use, and which your plan includes +- `dataset set-sync-frequency` — Set `--interval` in minutes: 1, 15, 60, 240, 360, 480 or 1440. For an ingestion dataset this sets how often metrics sync; the data itself arrives when you push it +- `dataset sync-history` — Show sync history +- `dataset sync-statistics` — Show sync statistics +- `dataset ingestion-statistics` — Show ingestion statistics +- `dataset permissions` — Show permissions +- `dataset set-permissions` — Set `--access-level everyone|selectedUsers|private` +- `dataset metadata` — Show the description, synonyms and default time dimension +- `dataset set-metadata` — Update them +- `dataset column-metadata` — Show per-column descriptions, concept types and synonyms +- `dataset set-column-metadata` — Update them, by column `id` +- `dataset verification` — Show verification status +- `dataset set-verification` — Mark a dataset verified or unverified +- `dataset modifications` — Show the modification definition: filters, formulas, display names, data types, column order and visibility +- `dataset update-modification` — Create or replace the modification definition. It replaces the whole definition; to change part of it, start from `dataset modifications ID --json`. +- `dataset preview-modification` — Preview a definition on up to 200 rows without saving it +- `dataset clear-modifications` — Remove all modifications +- `dataset modification-rules` — List the filter operators and type conversions modifications accept +- `dataset modification-functions` — List the functions modification formulas can use + +#### Metrics +- `metric list` — List metrics, filtered by `--source-id` (a data source or dataset) or `--search` +- `metric get` — Get a metric's details, including a custom metric's measure, date, aggregation and filters +- `metric create` — Create a custom metric on a dataset. `--measure`, `--date` and `--dimension` take column references as `{"id","displayName"}`; `--filters` takes one group, `{"logicalOperator":"and","conditions":[{"field","operator","values"}]}`; `--aggregation-function` is `sum`, `avg`, `min`, `max` or `count`. +- `metric update` — Update a custom metric; `--clear-dimensions` removes all its dimensions +- `metric delete` — Delete a custom metric +- `metric drilldown` — Get the rows behind a metric's value for a period, by `--source-id`, with repeatable `--dimension-id` and `--filters` as `databoard metrics` reports them. Its `--filters` is a **different shape** from `metric create`'s; [JSON Input](https://github.com/databox/databox-cli#json-input) has an example of each +- `metric dimension-values` — List the values of one dimension (`--metric-id`, `--source-id`, `--dimension-id`) +- `metric lineage` — Show what a metric is built from, and which calculated metrics read it +- `metric usages` — Show where a custom metric is used (databoards, alerts, goals, reports and more) +- `metric verification` — Show verification status +- `metric set-verification` — Mark a metric verified or unverified + +#### Databoards +- `databoard list` — List databoards +- `databoard metrics` — List the metrics on a databoard's datablocks, with their dimensions, date range and filters + +#### Activity Log +- `activity-log list` — List activity log entries, filtered by date, `--resource-type`, `--user-id` or `--search` + +### Unchanged + +- `auth validate`, and `auth login` at a terminal +- The config file location, `~/.config/databox-cli/config.json` +- The `DATABOX_API_KEY` and `DATABOX_API_URL` environment variables diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..faa6ca3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Databox + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index b016d92..00eea65 100644 --- a/README.md +++ b/README.md @@ -1,64 +1,300 @@ # databox-cli -CLI for the [Databox](https://databox.com) public API. Manage accounts, data sources, datasets, push data, and analyze datasets with Genie AI — all from the terminal. +Command-line interface for the [Databox](https://databox.com) API. Manage data sources, datasets and the data in them, custom metrics, databoards, users, your organization and its accounts, connections and billing — from the terminal, from scripts, or through an AI agent. Running it from a script or an agent? Read [Scripts and AI Agents](#scripts-and-ai-agents) first. + +Version 1.0 targets the Databox V2 API. Upgrading from 0.x? The [1.0.0 migration guide](https://github.com/databox/databox-cli/blob/main/CHANGELOG.md) lists every renamed command and flag. ## Installation ```bash npm install -g databox-cli +databox --version ``` +This installs the `databox` command. Requires Node.js 18 or later. + +## How It Fits Together + +A **data source** holds **datasets**. You push rows into a dataset, then build custom **metrics** on it. + +- Data source and dataset IDs are numbers. See [Finding IDs](#finding-ids). +- Metric IDs are strings such as `67890|custom_query_100`. The `|` is special in the shell, so always quote them: `--metric-id "67890|custom_query_100"`. +- Timestamps differ per command: + +| Where | Format | Example | +|---|---|---| +| Datetime values in ingested records | ISO 8601 | `"2026-01-15"`, `"2026-01-15T10:30:00Z"` | +| `metric drilldown --start-timestamp` / `--end-timestamp` | Unix seconds | `1767225600` (2026-01-01 00:00 UTC). Convert with `date -u -d 2026-01-01 +%s` (Linux) or `date -u -j -f '%F %T' '2026-01-01 00:00:00' +%s` (macOS) | +| `activity-log list --date-from` / `--date-to` | Date, `YYYY-MM-DD` | `2026-01-31`. A bare date means the start of that day (UTC), so `--date-to 2026-01-31` leaves out most of the 31st; pass `--date-to 2026-02-01` to include it | + +### Finding IDs + +Most commands take the ID of a data source, a dataset or a metric. Every one of them is shown by a list command, and returned as `id` by the command that creates it. + +| ID | What it is | Where to find it | +|---|---|---| +| Data source ID | A number, e.g. `12345`. The container your datasets live in. | `databox data-source list`; `id` from `data-source create`. In the Databox app, open the data source in **Data Manager**: the number in the page URL, `app.databox.com/data-manager/connected/12345/datasets/view`, is its ID. | +| Dataset ID | A number, e.g. `67890`. Each dataset has its own ID, separate from its data source's. | `databox dataset list --search "Orders"`, or `databox data-source datasets 12345` for one data source's datasets; `id` from `dataset create`. In the Databox app, open the dataset in **Data Manager** and read the number from the URL, as above. | +| Metric ID | A string: `\|`, e.g. `12345\|custom_query_100`. | `databox metric list --search revenue`; `id` from `metric create`. Quote it in the shell. | +| Ingestion ID | A UUID. One ingest request, not a dataset. | Returned as `ingestionId` by `dataset ingest`; `databox dataset ingestions DATASETID` lists them. | +| Account ID | A number. An account your organization manages, for `--account-id`. | `databox account list`. | + +Data source and dataset IDs share one ID space, so a number names exactly one of them: pass a dataset ID to `dataset` commands and a data source ID to `data-source` commands. The dataset GUIDs that 0.x used (`a1b2c3d4-e5f6-…`) are not accepted; look the dataset up by name with `databox dataset list --search`. + ## Getting Started +A complete run, from an empty organization to a metric's rows. It uses [jq](https://jqlang.org) to capture each new ID; without jq, copy the ID from the command's table output. + ```bash -# Authenticate with your API key +# 1. Authenticate, and check the key works (scripts: export DATABOX_API_KEY instead) databox auth login - -# Verify your key works databox auth validate -# List your accounts -databox account list - -# Push data into a dataset -databox dataset ingest DATASET_ID --file data.json +# 2. Create a data source, then a dataset in it +SOURCE=$(databox data-source create --name "My App" --json | jq -r .id) +DATASET=$(databox dataset create --name "Orders" --data-source-id "$SOURCE" \ + --primary-key order_id \ + --schema '[{"id":"order_id","dataType":"string"},{"id":"date","dataType":"datetime"},{"id":"country","dataType":"string"},{"id":"amount","dataType":"number"}]' \ + --json | jq -r .id) + +# 3. Push rows: a bare JSON array of objects keyed by column id (not {"records": [...]}; +# the CLI adds that wrapper). Inline with --records, from a file with --file, or piped on stdin. +INGESTION=$(databox dataset ingest "$DATASET" \ + --records '[{"order_id":"A-1","date":"2026-01-15","country":"US","amount":42}]' \ + --json | jq -r .ingestionId) + +# 4. Ingestion is asynchronous. Poll until the status is success, or failed, in which case +# its errors name each rejected record (purged means the data was purged meanwhile); +# give up after 2 minutes. A poll that fails +# (a network blip) just polls again. Then read the rows. +for attempt in $(seq 1 60); do + STATUS=$(databox dataset ingestion "$DATASET" "$INGESTION" --json | jq -r .status) + case "$STATUS" in success|failed|purged) break ;; esac + sleep 2 +done +databox dataset ingestion "$DATASET" "$INGESTION" +databox dataset data "$DATASET" + +# 5. Build a custom metric on the dataset. Column references are {"id","displayName"}, +# with the id taken from "dataset schema". +METRIC=$(databox metric create --name "Revenue" --dataset-id "$DATASET" \ + --measure '{"id":"amount","displayName":"Amount"}' \ + --date '{"id":"date","displayName":"Date"}' \ + --dimension '{"id":"country","displayName":"Country"}' \ + --json | jq -r .id) + +# 6. Read the rows behind the metric for January 2026, broken down by country +databox metric drilldown --metric-id "$METRIC" --source-id "$DATASET" \ + --start-timestamp 1767225600 --end-timestamp 1769904000 --dimension-id country ``` ## Authentication -All commands (except `auth login`) require an API key. Run `databox auth login` to store your key in `~/.config/databox-cli/config.json`. +### Getting an API Key + +The CLI authenticates with your **personal API key**, a string starting with `pak_`. To create it, in the Databox app open **Account Management → Security** (the page is titled **Password & Security**) and, under **API key**, click **Create**. + +Prerequisites: + +- **You are an admin.** Only admin users can create a key. +- **Your plan includes API access.** If the **API key** section does not appear on the Security page, your plan does not include it, or you are not an admin. + +What to know about the key: + +- **One key per user.** It never expires. To rotate it, delete it on the same page and create a new one. +- **It acts as you.** Every command runs with your user's permissions, in your organization and in any account you can reach with `--account-id`. +- **It can be limited to IP addresses.** Under **Manage allowed IPs**, choose **Selected IPs only** to accept requests from listed IPv4/IPv6 addresses only. A request from anywhere else is rejected as unauthenticated (exit 1), so add the IP of every machine or CI runner that uses the CLI. +- **Treat it like a password.** Anyone holding it can act as you until you delete it. + +### Using the Key + +All commands except `auth login` need the key. There are four ways to supply it: + +| How | Use it for | +|---|---| +| `databox auth login` | Interactive use. Prompts for the key without echoing it, which keeps it out of your shell history. | +| `pass show databox \| databox auth login` | Storing a key read from stdin, e.g. from a password manager or `< keyfile`. | +| `databox auth login --api-key YOUR_API_KEY` | Storing a key without a prompt. | +| `DATABOX_API_KEY=YOUR_API_KEY` in the environment | Scripts, CI and AI agents. Nothing is stored, and it takes precedence over the stored key. | + +`auth login` stores the key in `~/.config/databox-cli/config.json`, readable only by you. It then checks the key, but saves it even if that check fails: it prints `Warning: API key could not be validated.` and still exits 0. Run `databox auth validate` to be sure; exit 0 means the key works. + +Off a terminal (stdin piped or closed, as in scripts and agent shells), `auth login` without `--api-key` does not prompt: it reads the key from the first line of stdin. When nothing is piped, it exits 2 and saves nothing. `auth login` itself does not read `DATABOX_API_KEY`; set that variable instead of logging in. An open stdin that nobody writes to makes it wait, so in agent shells pass `--api-key` or use `DATABOX_API_KEY`. + +## Global Flags + +Every command except `auth login` accepts these: + +| Flag | Env var | Description | +|------|---------|-------------| +| `--output table\|json\|csv` | — | Output format. Default `table`. | +| `--json` | — | Shorthand for `--output json`. Cannot be combined with `--output`. | +| `--verbose` | — | Print each request and response (method, URL, status, duration, request ID) to stderr. The API key is never printed. | +| `--no-color` | `NO_COLOR` | Disable coloured output. A non-empty `NO_COLOR` does the same. | +| `--api-key` | `DATABOX_API_KEY` | Use this API key instead of the stored one. | +| `--api-url` | `DATABOX_API_URL` | Override the API base URL (default `https://api.databox.com`). `auth login` saves the URL it was given to the config file, and later commands keep using it. | +| `--account-id` | `DATABOX_ACCOUNT_ID` | Target an account in your organization (see [Organizations and Accounts](#organizations-and-accounts)). | +| `-h`, `--help` | — | Show help for a command or topic. | + +`--api-key`, `--api-url` and `--account-id` do not appear in each command's `--help`, but work on every command that calls the API. + +One exception: `auth login` takes only `--api-key` (and `--api-url`); `--json` and the other flags are rejected with exit 2. + +### List Flags + +Commands that return a list page by page also take these. `metric dimension-values`, `organization timezones`, `organization countries` and `databoard metrics` return everything at once and take none of them. + +| Flag | Description | +|------|-------------| +| `--page` | Page number, starting at 0. | +| `--page-size` | Items per page: at most 100, or 1000 on `dataset data` and `metric drilldown`. | +| `--all` | Fetch every page and print them as one list. Cannot be combined with `--page`. | +| `--search`, `--sort-by`, `--sort-order` | On the commands that support them; `--help` lists the accepted sort fields. | + +**Without `--page` or `--all`, a list command returns only the first page**: 25 items, or 200 rows for `dataset data` and `metric drilldown`. Table mode prints a `Page 1 of N (T total items)` footer. `--output csv` prints no total, and neither does `--json`, apart from the `pagination` object in `dataset data` and `metric drilldown`. Pass `--all` whenever you need every item. -You can also pass the key inline: +### Safe Retries with `--idempotency-key` + +Commands that create something, or start work that should not happen twice, accept `--idempotency-key `. The key is sent as the `Idempotency-Key` header: a retry with the same key within 24 hours returns the first response instead of repeating the action. Only a successful response is kept, so a retry after an error runs the request again. Keys are scoped to the account (`--account-id`), and the request body is not compared: reuse a key only for a retry of the same request. ```bash -databox auth login --api-key YOUR_API_KEY +KEY=$(uuidgen) +databox dataset ingest 67890 --file orders.json --idempotency-key "$KEY" +# Timed out? Re-running with the same key cannot ingest the rows twice. +databox dataset ingest 67890 --file orders.json --idempotency-key "$KEY" ``` +It is available on `account create`, `data-source create`, `data-source purge`, `dataset create`, `dataset duplicate`, `dataset ingest`, `dataset purge`, `dataset update-modification`, `metric create` and `user invite`. The value must be a UUID. + +## JSON Input + +Several flags take JSON. Quote it in single quotes. Each command's `--help` has a full example. + +| Flag | Shape and example | +|---|---| +| `dataset create --schema` | Array of columns; `dataType` is `string`, `number` or `datetime`. Optional: without it, the first ingest defines the schema. `'[{"id":"amount","dataType":"number"}]'` | +| `dataset ingest --records`, `--file`, stdin | A bare array of row objects, keyed by column `id`. `'[{"order_id":"A-1","amount":42}]'` | +| `metric create` / `update` `--measure`, `--date`, `--dimension` | A column reference. `'{"id":"amount","displayName":"Amount"}'` | +| `metric create` / `update` `--filters` | One group of conditions, lower-case `logicalOperator`, values as strings. `'{"logicalOperator":"and","conditions":[{"field":"country","operator":"ANY_OF","values":["US"]}]}'` | +| `metric drilldown --filters` | **A different shape**: upper-case, nested `groups`, each condition with a `type`. Copy it from `databoard metrics --json`. `'{"logicalOperator":"AND","groups":[{"logicalOperator":"AND","conditions":[{"type":"dimension","field":"country","operator":"ANY_OF","values":["US"]}]}]}'` | +| `dataset update-modification` / `preview-modification` `--data` | Any of `filters`, `formulas`, `displayNames`, `dataTypes`, `order`, `visibility`. `update-modification` replaces the whole definition, so start from `dataset modifications ID --json`. `'{"formulas":{"totalWithTax":"$amount * 1.2"},"displayNames":{"amount":"Revenue"}}'` | +| `dataset set-column-metadata --columns` | Array of `{id, description?, conceptType?, synonyms?}`. `'[{"id":"country","conceptType":"dimension","synonyms":["nation"]}]'` | +| `organization update --settings` | `{dateFormat, numberFormat, firstDayOfWeek, calendar, fiscalYearStart}`. `'{"calendar":"customFiscal","fiscalYearStart":{"month":4,"day":1}}'` | +| `organization update --address` / `--metadata` | `{street, zip, city, state, country}` / `{industry, businessType, companySize, annualRevenue}` | +| `profile update --metadata` | `{department, title, role}`, with values from `profile metadata-options`. `'{"department":"engineering","role":"software_engineer"}'` | + ## Output Formats -By default, commands output human-readable tables. Add `--json` to any command for machine-readable JSON output: +Commands print a table by default. `--output json` (or `--json`) and `--output csv` are for scripts: + +```bash +# JSON, filtered with jq (--all: see List Flags) +databox dataset list --all --json | jq '.[] | {id, name}' + +# Every data source as CSV, across all pages +databox data-source list --all --output csv > data-sources.csv + +# A dataset's rows as CSV +databox dataset data 67890 --all --output csv > orders.csv +``` + +What `--json` prints: + +- **Lists** print a JSON array of the items, each exactly as the API returned it. With `--all`, the array holds every page; without it, only the first (see [List Flags](#list-flags)). +- **Responses that carry more than a list** print the whole response object: `dataset schema` (`{items, primaryKey}`), `dataset data` (`{items, pagination, schema, lastUpdatedAt}`), `dataset preview-modification` and `metric drilldown` (`{items, schema, pagination}`), `dataset lineage` and `metric lineage` (`{parents, children}`), `metric dimension-values`, `dataset modifications`, `dataset modification-rules` and `databoard metrics`. A command's `--help` says when `--json` returns the whole response. Under `--all`, `dataset data` and `metric drilldown` leave out `pagination`. +- **Single resources** print the object the API returned. +- **`metric create` and `metric update`** print the metric as `metric get` does, in every format. +- **`set-timezone`, `set-sync-frequency` and `set-verification`** print a one-line confirmation in table mode, and the updated resource with `--json` or `--output csv`. Every other `set-*` command prints what the API returned in every format: the updated resource, or for `dataset set-column-metadata` the dataset's columns. +- **Deletes, purges and clears** print a one-line confirmation in every format. + +CSV uses the same columns as the table, with a header row even when there are no results. A single resource prints as `field,value` rows. + +Stdout carries only the result. Pagination footers appear in table mode only, and `--verbose` traces, warnings and errors go to stderr, so piping stays clean. + +## Limits + +| Limit | Value | +|---|---| +| Records per `dataset ingest` | 500. The CLI refuses a larger batch before sending it (exit 2); split it. | +| Payload per `dataset ingest` | 30 MB (30,000,000 bytes) of JSON, checked the same way. | +| Columns per dataset | 100 | +| Dataset size | Set per dataset: `maxSize` in `dataset get`. | +| Rate limit, per API key | 10 requests per second, 10,000 per hour, enforced by Databox's gateway. Over it you get HTTP 429 (exit 1), possibly with no error code: back off and retry, with `--idempotency-key` on writes. | +| Request timeout | 30 seconds, or 5 minutes for `dataset ingest` (exit 2). | + +With a primary key (`dataset create --primary-key`), ingesting a row whose key already exists overwrites it. Without one, every ingest appends, so sending the same rows twice duplicates them. + +## Errors and Exit Codes + +When the API rejects a request, the CLI prints the error code, the message, the field at fault (if any) and the request ID, on stderr: + +``` + › Error: invalid_input + › Invalid timezone value + › Field: timezone + › Request ID: 9ea537f4-27bc-4662-a4e2-48744ea9b7bd +``` + +Errors are always plain text on stderr, even with `--json`; on failure, stdout is empty. If you contact Databox support about a failed command, quote the **Request ID**: it identifies the exact request in Databox's logs. `--verbose` prints the request ID of successful requests too. + +| Exit code | Meaning | +|-----------|---------| +| `0` | Success. Answering anything but `y` or `yes` to a confirmation prompt, at a terminal or piped, also exits 0 after printing `Aborted.`. | +| `1` | The API returned an error (4xx or 5xx, including the rate limit). Also: no API key is configured, the stored config file is not valid JSON, the response was not JSON (usually a wrong `--api-url`), an update command was given no field to change, or `dataset ingest` was run at a terminal with no `--records` or `--file`. | +| `2` | The request was never sent, or never reached the API: an unknown flag, a value outside a flag's options, a malformed ID or JSON value, an ingest over the limits, or a network failure, timeout or redirect (the CLI does not follow a redirect: it would resend your API key). Also a command that would prompt when stdin is not a terminal and nothing is piped: a delete, purge or clear without `--force`, or `auth login` without `--api-key` (see [Authentication](#authentication)). | +| `130` | A prompt (a confirmation, or the API key at `auth login`) was interrupted with Ctrl-C. | + +## Scripts and AI Agents + +- **Authenticate with `DATABOX_API_KEY`**, not a bare `auth login` (see [Authentication](#authentication)). +- **Use `--json`, and `--all` on lists** (see [List Flags](#list-flags)). +- **Branch on the exit code**, not the output: `0` ok, `1` API error, `2` bad input or network. Errors are plain text on stderr even with `--json`, and stdout is then empty. +- **Pass `--force`** to `account delete`, `connection delete`, `data-source delete`, `data-source purge`, `dataset delete`, `dataset purge`, `dataset clear-modifications`, `metric delete` and `user delete`. Without it, off a terminal, they read `y`/`yes` from stdin, and exit 2 having done nothing when nothing is piped. +- **`set-timezone --purge-data` deletes data without asking**, on both `data-source` and `dataset`. +- **Give `dataset ingest` its input explicitly** with `--records` or `--file`. With neither, it reads stdin, and an open stdin that nobody writes to waits forever. +- **Ingestion is asynchronous**: poll `dataset ingestion DATASETID INGESTIONID` until its status is `success`, `failed` or `purged` (see [Getting Started](#getting-started)). +- **Make retries safe** with `--idempotency-key "$(uuidgen)"` on creates and ingests, reusing the key when you retry. +- **Report failures with the request ID** from the error. `--verbose` adds a request ID for every request, on stderr. + +## Organizations and Accounts + +Your **organization** is the top level: `databox organization info`, `organization update` and `organization usage` read and change it. An organization that manages several accounts (an agency) lists and manages them with the `account` commands, and `--account-id` scopes any command to one of them: ```bash -databox account list --json +# List the accounts in your organization +databox account list --all + +# List data sources in one account +databox data-source list --account-id 12345 ``` +- `account` commands work only for an organization that manages accounts. Any other organization gets `invalid_input` on field `organization` from `account list` and `account create`. `account get`, `update` and `delete` answer `not_found` for any account your organization does not manage. +- `--account-id` takes a numeric account ID. It matters on list and create commands, where it picks the account to list or create in. A command given a resource ID acts on that resource wherever it lives. +- A `DATABOX_ACCOUNT_ID` in your environment applies to every command. +- With `--account-id`, the `organization` commands answer for that account. `databox profile info` always shows your own organization, and your home account if you belong to one. + ## Agent Skills -This package includes shareable skills for AI agents (like [Claude Code](https://claude.ai/claude-code)) to use the CLI autonomously. +This package includes skills that let AI agents (like [Claude Code](https://claude.com/claude-code)) use the CLI on your behalf. ### Bundled Skills | Skill | Description | |-------|-------------| | `databox-auth` | Authentication setup and API key validation | -| `databox-accounts` | Account discovery, timezones, resource listing | -| `databox-data-sources` | Data source create, delete, and inspection | -| `databox-datasets` | Dataset CRUD, schema definition, data ingestion, monitoring | -| `databox-analyze` | Dataset analysis with Genie AI, conversational data Q&A | +| `databox-organization` | Organization info, usage, settings, timezones | +| `databox-data-sources` | Data source CRUD, timezone, sync frequency, permissions, purge | +| `databox-datasets` | Dataset CRUD, schema, data ingestion, metadata, verification, modifications, lineage | +| `databox-metrics` | Custom metric CRUD, dimension values, drilldown, lineage, usages, verification | +| `databox-users` | User invites, roles, removal | +| `databox-accounts` | Managing the accounts in your organization | +| `databox-connections` | Connection management and permissions | +| `databox-integrations` | Browse the integration catalog | +| `databox-billing` | Plan details and invoices | ### Install Skills -Install all skills at once using [npx skills](https://github.com/anthropics/skills): +Install all skills at once using [npx skills](https://github.com/vercel-labs/skills): ```bash npx skills add databox/databox-cli --skill '*' @@ -68,169 +304,324 @@ Or install individual skills: ```bash npx skills add databox/databox-cli --skill databox-auth -npx skills add databox/databox-cli --skill databox-accounts +npx skills add databox/databox-cli --skill databox-organization npx skills add databox/databox-cli --skill databox-data-sources npx skills add databox/databox-cli --skill databox-datasets -npx skills add databox/databox-cli --skill databox-analyze +npx skills add databox/databox-cli --skill databox-metrics +npx skills add databox/databox-cli --skill databox-users +npx skills add databox/databox-cli --skill databox-accounts +npx skills add databox/databox-cli --skill databox-connections +npx skills add databox/databox-cli --skill databox-integrations +npx skills add databox/databox-cli --skill databox-billing ``` -Once installed, Claude Code can manage your Databox resources directly — creating data sources, defining schemas, pushing data, monitoring ingestions, and analyzing datasets with Genie AI. +Once installed, Claude Code can manage your Databox resources directly — your organization and its accounts, data sources, datasets, metrics, users, connections and billing. ## Commands +`analyze ask-genie` is temporarily unavailable in 1.0: the Genie service now requires authentication the CLI cannot provide yet. It will return in a later release. + -* [`databox account data-sources ACCOUNTID`](#databox-account-data-sources-accountid) -* [`databox account datasets ACCOUNTID`](#databox-account-datasets-accountid) +* [`databox account create`](#databox-account-create) +* [`databox account delete ACCOUNTID`](#databox-account-delete-accountid) +* [`databox account get ACCOUNTID`](#databox-account-get-accountid) * [`databox account list`](#databox-account-list) -* [`databox account timezones`](#databox-account-timezones) -* [`databox analyze ask-genie DATASETID QUESTION`](#databox-analyze-ask-genie-datasetid-question) +* [`databox account update ACCOUNTID`](#databox-account-update-accountid) +* [`databox activity-log list`](#databox-activity-log-list) * [`databox auth login`](#databox-auth-login) * [`databox auth validate`](#databox-auth-validate) +* [`databox billing info`](#databox-billing-info) +* [`databox billing invoices`](#databox-billing-invoices) +* [`databox connection delete CONNECTIONID`](#databox-connection-delete-connectionid) +* [`databox connection get CONNECTIONID`](#databox-connection-get-connectionid) +* [`databox connection list`](#databox-connection-list) +* [`databox connection permissions CONNECTIONID`](#databox-connection-permissions-connectionid) +* [`databox connection set-permissions CONNECTIONID`](#databox-connection-set-permissions-connectionid) +* [`databox connection update CONNECTIONID`](#databox-connection-update-connectionid) * [`databox data-source create`](#databox-data-source-create) * [`databox data-source datasets DATASOURCEID`](#databox-data-source-datasets-datasourceid) * [`databox data-source delete DATASOURCEID`](#databox-data-source-delete-datasourceid) +* [`databox data-source get DATASOURCEID`](#databox-data-source-get-datasourceid) +* [`databox data-source list`](#databox-data-source-list) +* [`databox data-source permissions DATASOURCEID`](#databox-data-source-permissions-datasourceid) +* [`databox data-source purge DATASOURCEID`](#databox-data-source-purge-datasourceid) +* [`databox data-source set-permissions DATASOURCEID`](#databox-data-source-set-permissions-datasourceid) +* [`databox data-source set-sync-frequency DATASOURCEID`](#databox-data-source-set-sync-frequency-datasourceid) +* [`databox data-source set-timezone DATASOURCEID`](#databox-data-source-set-timezone-datasourceid) +* [`databox data-source sync-frequency-options DATASOURCEID`](#databox-data-source-sync-frequency-options-datasourceid) +* [`databox data-source update DATASOURCEID`](#databox-data-source-update-datasourceid) +* [`databox databoard list`](#databox-databoard-list) +* [`databox databoard metrics DATABOARDID`](#databox-databoard-metrics-databoardid) +* [`databox dataset clear-modifications DATASETID`](#databox-dataset-clear-modifications-datasetid) +* [`databox dataset column-metadata DATASETID`](#databox-dataset-column-metadata-datasetid) * [`databox dataset create`](#databox-dataset-create) +* [`databox dataset data DATASETID`](#databox-dataset-data-datasetid) * [`databox dataset delete DATASETID`](#databox-dataset-delete-datasetid) +* [`databox dataset duplicate DATASETID`](#databox-dataset-duplicate-datasetid) * [`databox dataset get DATASETID`](#databox-dataset-get-datasetid) * [`databox dataset ingest DATASETID`](#databox-dataset-ingest-datasetid) * [`databox dataset ingestion DATASETID INGESTIONID`](#databox-dataset-ingestion-datasetid-ingestionid) +* [`databox dataset ingestion-statistics DATASETID`](#databox-dataset-ingestion-statistics-datasetid) * [`databox dataset ingestions DATASETID`](#databox-dataset-ingestions-datasetid) +* [`databox dataset lineage DATASETID`](#databox-dataset-lineage-datasetid) +* [`databox dataset list`](#databox-dataset-list) +* [`databox dataset metadata DATASETID`](#databox-dataset-metadata-datasetid) +* [`databox dataset modification-functions`](#databox-dataset-modification-functions) +* [`databox dataset modification-rules`](#databox-dataset-modification-rules) +* [`databox dataset modifications DATASETID`](#databox-dataset-modifications-datasetid) +* [`databox dataset permissions DATASETID`](#databox-dataset-permissions-datasetid) +* [`databox dataset preview-modification DATASETID`](#databox-dataset-preview-modification-datasetid) * [`databox dataset purge DATASETID`](#databox-dataset-purge-datasetid) +* [`databox dataset schema DATASETID`](#databox-dataset-schema-datasetid) +* [`databox dataset set-column-metadata DATASETID`](#databox-dataset-set-column-metadata-datasetid) +* [`databox dataset set-metadata DATASETID`](#databox-dataset-set-metadata-datasetid) +* [`databox dataset set-permissions DATASETID`](#databox-dataset-set-permissions-datasetid) +* [`databox dataset set-sync-frequency DATASETID`](#databox-dataset-set-sync-frequency-datasetid) +* [`databox dataset set-timezone DATASETID`](#databox-dataset-set-timezone-datasetid) +* [`databox dataset set-verification DATASETID`](#databox-dataset-set-verification-datasetid) +* [`databox dataset sync-frequency-options DATASETID`](#databox-dataset-sync-frequency-options-datasetid) +* [`databox dataset sync-history DATASETID`](#databox-dataset-sync-history-datasetid) +* [`databox dataset sync-statistics DATASETID`](#databox-dataset-sync-statistics-datasetid) +* [`databox dataset update DATASETID`](#databox-dataset-update-datasetid) +* [`databox dataset update-modification DATASETID`](#databox-dataset-update-modification-datasetid) +* [`databox dataset verification DATASETID`](#databox-dataset-verification-datasetid) * [`databox help [COMMAND]`](#databox-help-command) +* [`databox integration get INTEGRATIONID`](#databox-integration-get-integrationid) +* [`databox integration list`](#databox-integration-list) +* [`databox metric create`](#databox-metric-create) +* [`databox metric delete METRICID`](#databox-metric-delete-metricid) +* [`databox metric dimension-values`](#databox-metric-dimension-values) +* [`databox metric drilldown`](#databox-metric-drilldown) +* [`databox metric get METRICID`](#databox-metric-get-metricid) +* [`databox metric lineage METRICID`](#databox-metric-lineage-metricid) +* [`databox metric list`](#databox-metric-list) +* [`databox metric set-verification METRICID`](#databox-metric-set-verification-metricid) +* [`databox metric update METRICID`](#databox-metric-update-metricid) +* [`databox metric usages METRICID`](#databox-metric-usages-metricid) +* [`databox metric verification METRICID`](#databox-metric-verification-metricid) +* [`databox organization countries`](#databox-organization-countries) +* [`databox organization info`](#databox-organization-info) +* [`databox organization metadata-options`](#databox-organization-metadata-options) +* [`databox organization timezones`](#databox-organization-timezones) +* [`databox organization update`](#databox-organization-update) +* [`databox organization usage`](#databox-organization-usage) +* [`databox profile info`](#databox-profile-info) +* [`databox profile metadata-options`](#databox-profile-metadata-options) +* [`databox profile update`](#databox-profile-update) +* [`databox user delete USERID`](#databox-user-delete-userid) +* [`databox user get USERID`](#databox-user-get-userid) +* [`databox user invite`](#databox-user-invite) +* [`databox user list`](#databox-user-list) +* [`databox user update USERID`](#databox-user-update-userid) + +## `databox account create` + +Create an account in your organization + +``` +USAGE + $ databox account create --name [--no-color] [--output table|json|csv | --json] [--verbose] + [--idempotency-key ] [--managed-by-id ] [--website-url ] + +FLAGS + --idempotency-key= A UUID sent as the Idempotency-Key header: a retry with the same key within 24 hours + returns the first response instead of repeating the action + --json Output as JSON (shorthand for --output json) + --managed-by-id= User ID of the account manager + --name= (required) Name of the account + --no-color Disable coloured output (a non-empty NO_COLOR environment variable does the same) + --output=