Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a4a212f
refactor(cli): isolate product command registration
francoischalifour Jul 16, 2026
7497abc
refactor(cli): give every command an owned directory
francoischalifour Jul 16, 2026
02a243c
refactor(cli): execute commands without operation framework
francoischalifour Jul 16, 2026
0f1bac6
refactor(cli): colocate command code and unit tests
francoischalifour Jul 16, 2026
99999f0
docs(cli): codify the colocated command architecture
francoischalifour Jul 16, 2026
16a18ee
refactor(cli): standardize command module boundaries
francoischalifour Jul 16, 2026
b6fe6dd
refactor(cli): derive shared lakehouse command data
francoischalifour Jul 16, 2026
308fafb
fix(cli): preserve catalog list request failures
francoischalifour Jul 16, 2026
532e311
test(cli): build fixtures through command helpers
francoischalifour Jul 16, 2026
ee8250d
test(cli): cover commands through the public runtime
francoischalifour Jul 16, 2026
e0e4b79
refactor(cli): make shared dependencies explicit
francoischalifour Jul 16, 2026
1cc9d4f
refactor(cli): remove test-only output APIs
francoischalifour Jul 16, 2026
dfb9dc9
refactor(cli): compose arguments by semantic ownership
francoischalifour Jul 16, 2026
f5180c7
refactor(cli): clarify shared runtime boundaries
francoischalifour Jul 16, 2026
8216673
fix(auth): persist the authenticated control plane on login
francoischalifour Jul 16, 2026
de2a98e
refactor(secrets): inject the system credential adapter
francoischalifour Jul 16, 2026
2906263
refactor(cli): bind command runtime once at the root
francoischalifour Jul 16, 2026
9ea198c
test(cli): colocate profile and config behavior
francoischalifour Jul 16, 2026
10fb362
fix(secrets): fail closed on Keychain deletion errors
francoischalifour Jul 16, 2026
7cef337
test(auth): restore login profile behavior coverage
francoischalifour Jul 16, 2026
4e4805e
chore(cli): align dead-code checks after rebase
francoischalifour Jul 16, 2026
b2a2d2b
Merge remote-tracking branch 'origin/main' into fc/simplify-command-a…
francoischalifour Jul 16, 2026
036efbd
Merge branch 'main' into fc/simplify-command-architecture
francoischalifour Jul 16, 2026
c26c8fa
test(profile): initialize keychain backend before profile setup
francoischalifour Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Altertable CLI — a TypeScript/Bun command-line tool for querying and managing
| Path | When to edit |
|------|--------------|
| `cli/src/` | CLI commands, HTTP clients, formatting, config |
| `cli/tests/` | Bun unit tests for CLI logic |
| `cli/src/**/*.test.ts` | Colocated Bun unit tests for CLI logic |
| `tests/` | Black-box end-user CLI tests run through `bin/altertable` |
| `specs/` | Client API specs (submodule — read-only from this repo) |
| `bin/altertable` | Dev launcher — do not edit |
Expand Down
23 changes: 14 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,29 @@ Match existing conventions in `cli/src/`:

## Command Architecture

Commands should keep platform concepts split and composed. The intended flow is:
Commands should keep the execution path local and readable. The intended flow is:

```text
parse args -> build operation plan -> run effects -> decode result -> present output
parse args -> build request -> send request -> decode result -> present output
```

Use these boundaries when adding or changing commands:

- `cli/src/commands/**` owns command shape, argument parsing, operation ids, capability metadata, and presentation choice.
- `cli/src/lib/operation-codec.ts` owns small reusable argument codecs. Prefer these helpers over ad hoc `String(...)` coercion.
- `cli/src/lib/operation-effect.ts` owns executable operation plans and the effect handler registry.
- `cli/src/lib/http-operation.ts` owns named HTTP operation descriptors. Prefer descriptors over command-local request objects.
- `cli/src/lib/operation-transport.ts` owns plane-aware HTTP transport. Do not build management/lakehouse URLs in commands.
- `cli/src/commands/<family>/index.ts` owns the command group; each leaf command has a sibling `<name>.ts` file.
- `cli/src/commands/<family>/lib/**` owns implementation code used only by that command family.
- `cli/src/lib/**` is reserved for code shared by multiple command families.
- `cli/src/lib/args.ts` owns small reusable argument codecs. Prefer these helpers over ad hoc `String(...)` coercion.
- `cli/src/lib/http-request.ts` owns plane-aware HTTP transport. Do not build management/lakehouse URLs in commands.
- Request builders should return data structures. They should not perform transport as a side effect.
- Keep request descriptions declarative so a future dry-run mode can inspect them without performing I/O.
- Presentation should return `CommandOutputMode` or write through the command output sink. Avoid mixing transport, parsing, and terminal output in the same function.
- Register every command surface with a stable operation id, capabilities, effects, planes, mutability, and output shape through `defineOperationCommand`.
- Declare the exported command immediately after imports, then place its helpers and types below it so reviewers see the public shape first.
- Define commands and argument schemas through `cli/src/lib/command.ts`; Citty is an implementation detail of that boundary.
- Derive related argument schemas from one shared definition instead of repeating flags and descriptions.
- Register top-level commands in `cli/src/commands/index.ts`.
- Colocate unit tests beside their subject as `<name>.test.ts`; reserve root `tests/` for black-box behavior.

Avoid adding compatibility wrappers that both build and execute requests. If a shared path is needed, share the request builder, effect builder, parser, or presenter instead.
If a path needs sharing, start at the narrowest common owner and move it to top-level `lib/` only when multiple command families use it.

## Tests

Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ bun test "$PWD"/tests/*.test.ts

### Shell completion

Shell completion scripts are generated from the Citty `CommandDef` tree in `cli/src/cli.ts`. The spec walker lives in `cli/src/lib/completion-spec.ts`; bash/zsh/fish formatters and shared path/flag helpers live in `cli/src/lib/completion-format.ts`. Command-specific flags are taken from `CompletionNode.flags` on each visited node; positional arguments and dynamic API values are not completed. When you change command structure, run `cd cli && bun test cli/tests/completion-spec.test.ts cli/tests/completion.test.ts`.
Shell completion scripts are generated from the Citty `CommandDef` tree in `cli/src/cli.ts`. The spec walker and formatters live in `cli/src/commands/completion/lib/`, beside their tests. Command-specific flags are taken from `CompletionNode.flags` on each visited node; positional arguments and dynamic API values are not completed. When you change command structure, run `cd cli && bun test src/commands/completion/lib/spec.test.ts src/commands/completion/index.test.ts`.

Integration tests against the mock server:

Expand Down
112 changes: 58 additions & 54 deletions cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,18 @@ bun test "$PWD"/tests/integration.e2e.ts
## Architecture

- **Dual-plane auth**: lakehouse data plane (HTTP Basic) vs management REST (Bearer API key). See root README credential tables.
- **`CliRuntime` + `OutputSink`** (`src/lib/runtime.ts`): `defineAltertableCommand` injects `sink` into every command `run` handler. Commands pass `sink` to output helpers — not raw `console.log` / `console.error` (completion scripts are the exception; they must emit raw script text).
- **`writeCommandOutput`** (`src/lib/command-output.ts`): unified success output — raw API, normalized envelopes, tabular management, deletes. Accepts `sink` as the last argument; lib callers may omit it (defaults to `getOutputSink()`).
- Helpers: `writeManagementOutput`, `writeLakehouseOutput`, `writeJsonOrRaw` (thin wrappers around `writeCommandOutput`).
- **`CliRuntime` + `OutputSink`** (`src/lib/runtime.ts`): `defineCommand` injects `runtime`, `sink`, and a lazy `execution` context into every command handler. Commands pass `sink` to output helpers rather than writing to the console.
- **Direct execution**: a leaf command parses its arguments, builds a plain request value, sends it, and presents the result. Request values stay declarative as the seam for a future dry-run mode; there is no operation/effect framework.
- **`writeCommandOutput`** (`src/lib/command-output.ts`): unified success output — raw API, normalized envelopes, tabular management, deletes. Commands pass their injected `sink` explicitly.
- **When to use `sink` directly**: bespoke output (custom tables, metadata messages) — `sink.writeJson`, `sink.writeHuman`, `sink.writeMetadata`.
- **When to use helpers**: API-shaped responses with `--json` parity — pass `sink` as the last argument.
- Lakehouse streaming/query formatting lives in `lakehouse-client.ts` and `query-format.ts`.
- Lakehouse streaming lives in `lakehouse/`; query presentation lives in `query-output.ts` and `query-format.ts`.

## Conventions

- Declare and export each command immediately after its imports; keep supporting helpers and types below it.
- Import command types and `defineArgs` from `src/lib/command.ts`; only that boundary should depend on Citty's types.
- Derive related argument schemas from shared fragments instead of repeating flag definitions.
- Function declarations over const function expressions (except one-liners)
- Types over interfaces
- Explicit variable names; match surrounding file style
Expand Down Expand Up @@ -78,39 +81,42 @@ bun test "$PWD"/tests/integration.e2e.ts
| ------------------------------------- | ---------------------------------------------------------- |
| `src/cli.ts` | Citty root command, global flags, error bootstrap |
| `src/context.ts` | Parsed global flags (`json`, `debug`, `profile`, timeouts) |
| `src/commands/*` | One file per top-level command group |
| `src/lib/*` | Shared clients, formatting, config, completion |
| `src/commands/<family>/index.ts` | One top-level command group |
| `src/commands/<family>/<name>.ts` | One leaf command |
| `src/commands/<family>/lib/*` | Implementation private to that command family |
| `src/lib/*` | Code shared by multiple command families |
| `src/test-utils/*` | Shared CLI test harnesses and temporary workspaces |
| `src/generated/openapi-types.ts` | Generated — run `bun run generate` after OpenAPI changes |
| `src/generated/openapi-operations.ts` | Generated operation index for `api routes` |
| `tests/*.test.ts` | Bun unit tests under `cli/tests/` |
| `src/**/*.test.ts` | Unit tests colocated beside their subject |
| `../tests/*.test.ts` | Black-box end-user CLI tests at repo root |
| `../tests/integration.e2e.ts` | Mock-server lakehouse integration test |

**Largest/hot files** — read before large refactors: `lib/http.ts`, `lib/profile-configure-core.ts`, `lib/query-format.ts`, `lib/api-http.ts`.

| Module | Role |
| -------------------------------------- | -------------------------------------------------------------------------------- |
| `commands/lakehouse.ts` | Data-plane commands (query, upload, append, …) |
| `commands/api.ts` | Management HTTP invoker (`api GET /path`), spec, routes |
| `lib/api-http.ts` | HTTP invoker logic for `api` |
| `lib/api-body.ts` | `--body`, `@file`, `-f key=value` body builders |
| `lib/profile-configure-core.ts` | Credential store (`configureRunSet`, show, clear) |
| `lib/profile-configure.ts` | `profile --configure` dispatch (flags vs wizard, `--scope`) + interactive wizard |
| `lib/profile-configure-interactive.ts` | Wizard prompts + credential collection |
| `lib/profile-status.ts` | Post-configure credential verification (`configureVerify`) |
| `features/profile/model.ts` | Profile store/inspect + credential presence (stored + env) |
| `commands/profile.ts` | Profile subcommands, `profile --configure`, `profile show` |
| `lib/lakehouse-client.ts` | Lakehouse HTTP + query rendering |
| `lib/http.ts` | Shared HTTP transport, logging, mock file support |
| `lib/management-transport.ts` | Management API HTTP transport |
| `lib/management-formatters.ts` | Human formatters for identity and `catalogs` |
| `lib/catalog-rows.ts` | Catalog list row builder for `catalogs list` |
| `lib/errors.ts` | Exit codes, `CliError`, JSON error envelope |
| `lib/completion-spec.ts` | Walks Citty tree for shell completion |
**Largest/hot files** — read before large refactors: `lib/http.ts`, `lib/profile-configure-core.ts`, `lib/query-format.ts`, `commands/api/lib/http.ts`.

| Module | Role |
| --------------------------------------- | -------------------------------------------------------------------------------- |
| `commands/query/`, `append/`, `upload/` | Data-plane commands |
| `commands/api/` | Management HTTP invoker (`api GET /path`), spec, routes |
| `commands/api/lib/http.ts` | HTTP invoker logic for `api` |
| `commands/api/lib/body.ts` | `--body`, `@file`, `-f key=value` body builders |
| `lib/profile-configure-core.ts` | Credential store (`configureRunSet`, show, clear) |
| `lib/profile-configure.ts` | `profile --configure` dispatch (flags vs wizard, `--scope`) + interactive wizard |
| `lib/profile-configure-interactive.ts` | Wizard prompts + credential collection |
| `lib/profile-status.ts` | Post-configure credential verification (`configureVerify`) |
| `lib/profile/model.ts` | Profile store/inspect + credential presence shared by auth commands |
| `commands/profile/` | Profile subcommands, `profile --configure`, `profile show` |
| `lib/query-output.ts` | Shared query output formats and sink dispatch |
| `lib/http.ts` | Shared HTTP transport, logging, mock file support |
| `lib/management/` | Shared management identity, catalogs, and presentation |
| `commands/catalogs/lib/requests.ts` | Declarative catalog create request builder |
| `ui/prompts.ts` | Shared interactive prompt adapter and types |
| `lib/errors.ts` | Exit codes, `CliError`, JSON error envelope |
| `commands/completion/lib/spec.ts` | Walks Citty tree for shell completion |

## Command tree

Source of truth: `src/cli.ts` + `commands/api.ts`. Verify with `bin/altertable --help`.
Source of truth: `src/commands/index.ts`. Verify with `bin/altertable --help`.

```
altertable
Expand All @@ -133,27 +139,25 @@ altertable

### Recipe A — New top-level product command

1. Create `src/commands/myfeature.ts` exporting `myfeatureCommand` via `defineAltertableCommand`
2. Register in `src/cli.ts` `topLevelCommands`
3. Pass `sink` from `run({ sink })` to `writeCommandOutput` or plane-specific wrappers (`writeManagementOutput`, `writeLakehouseOutput`)
4. Management plane: `managementRequest()` from `lib/management-transport.ts`
5. Lakehouse plane: functions from `lib/lakehouse-client.ts`
6. Add unit test in `cli/tests/`; black-box test in `tests/` if integration-worthy
7. Flags on command `args` are picked up by `completion-spec.ts` — run completion tests after structural changes
1. Create `src/commands/myfeature/index.ts` exporting `myfeatureCommand` via `defineCommand`.
2. Put each subcommand in `src/commands/myfeature/<name>.ts`.
3. Put private helpers and request builders in `src/commands/myfeature/lib/`.
4. Register the top-level command in `src/commands/index.ts`.
5. Add `<name>.test.ts` beside the command or library under test; add a root `tests/` case only for black-box behavior.
6. Flags on command `args` are picked up by completion automatically; run completion tests after structural changes.

Minimal pattern (management HTTP command):

```typescript
import { defineAltertableCommand } from "@/lib/command-context.ts";
import { writeJsonOrRaw } from "@/lib/command-output.ts";
import { formatWhoami, type WhoamiResponse } from "@/lib/management-formatters.ts";
import { managementRequest } from "@/lib/management-transport.ts";
import { defineCommand } from "@/lib/command.ts";
import { sendHttp } from "@/lib/http-request.ts";

export const myfeatureCommand = defineAltertableCommand({
export const myfeatureCommand = defineCommand({
meta: { name: "myfeature", description: "…" },
async run({ sink }) {
const response = await managementRequest("GET", "/path");
writeJsonOrRaw(response, (data) => formatWhoami(data as WhoamiResponse), sink);
async run({ execution, sink }) {
const request = { plane: "management", method: "GET", endpoint: "/path" } as const;
const response = await sendHttp(request, execution);
sink.writeJson(JSON.parse(response));
},
});
```
Expand All @@ -174,24 +178,24 @@ Bump the OpenAPI spec and extend `openapi-http-conformance.test.ts` placeholder
### Recipe C — Change exit codes or JSON errors

1. Edit `src/lib/errors.ts` only
2. Update `cli/tests/errors.test.ts` and `tests/scripting.test.ts`
2. Update `cli/src/lib/errors.test.ts` and `tests/scripting.test.ts`
3. Update README scripting table — do not renumber existing codes

## Testing guide

| Change type | Run |
| -------------------- | ------------------------------------------------------------------------- |
| lib pure function | `cd cli && bun test path/to.test.ts` |
| command validation | `commands-*.test.ts` pattern |
| HTTP behavior | mock file via `ALTERTABLE_MOCK_HTTP_FILE` (see `tests/helpers.ts`) |
| end-to-end lakehouse | `./scripts/verify.sh --integration` |
| completion structure | `bun test cli/tests/completion-spec.test.ts cli/tests/completion.test.ts` |
| Change type | Run |
| -------------------- | ------------------------------------------------ |
| lib pure function | `cd cli && bun test path/to.test.ts` |
| command validation | colocated `src/commands/<family>/<name>.test.ts` |
| HTTP behavior | mock file via `ALTERTABLE_MOCK_HTTP_FILE` |
| end-to-end lakehouse | `./scripts/verify.sh --integration` |
| completion structure | `cd cli && bun test src/commands/completion` |

Test env vars: `ALTERTABLE_CONFIG_HOME`, `ALTERTABLE_SECRET_BACKEND=file`, `ALTERTABLE_MOCK_HTTP_FILE`, `ALTERTABLE_HTTP_LOG`.

Lakehouse endpoint coverage: [DEVELOPMENT.md spec conformance table](../DEVELOPMENT.md#cli-spec-conformance-lakehouse).

Example mock HTTP test pattern: `cli/tests/lakehouse.test.ts` sets `ALTERTABLE_MOCK_HTTP_FILE`. Root black-box tests use `tests/helpers.ts`.
Example mock HTTP test pattern: command tests use `src/test-utils/lakehouse.ts` with `ALTERTABLE_MOCK_HTTP_FILE`. Root black-box tests use `tests/helpers.ts`.

## Invariants (do not break)

Expand All @@ -200,7 +204,7 @@ Example mock HTTP test pattern: `cli/tests/lakehouse.test.ts` sets `ALTERTABLE_M
- Dual-plane configure: one authentication mechanism per flag-based invocation; the interactive wizard may configure both planes in one session
- HTTP log redaction in tests (`setupHttpLog` / `readHttpLog` in `tests/helpers.ts`)
- `bin/altertable` launcher unchanged
- No raw `console.log` in commands except `completion.ts`
- No raw `console.log` in commands

## Plans

Expand Down
2 changes: 1 addition & 1 deletion cli/bunfig.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ coverageReporter = ["text", "lcov"]
coverageDir = "coverage"
coverageSkipTestFiles = true
coverageThreshold = { line = 0.8, function = 0.8 }
coveragePathIgnorePatterns = ["src/generated/**", "tests/**"]
coveragePathIgnorePatterns = ["src/generated/**"]
12 changes: 9 additions & 3 deletions cli/knip.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
{
"$schema": "https://unpkg.com/knip@6/schema.json",
"entry": ["tests/**/*.test.ts", "scripts/**/*.ts"],
"project": ["src/**/*.ts", "tests/**/*.test.ts", "scripts/**/*.ts"],
"entry": [
"src/cli.ts!",
"src/**/*.test.ts",
"src/**/fixtures/*.ts",
"scripts/**/*.test.ts",
"scripts/**/*.ts"
],
"project": ["src/**/*.ts!", "!src/test-utils/**!", "scripts/**/*.ts"],
"ignore": ["src/generated/**"],
"ignoreExportsUsedInFile": true,
"ignoreDependencies": ["undici"],
"ignoreBinaries": ["less", "duckdb"]
"ignoreBinaries": ["less"]
}
Loading
Loading