Add instances command, improve error messages with actionable hints

Add "dclient instances" command to list known instances from config,
with --json support.

Improve "No instance specified" and "No database specified" error
messages to show the exact commands needed to fix the problem.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Simon Willison 2026-02-24 15:25:57 -08:00
commit 7b3a15027e
2 changed files with 86 additions and 2 deletions

View file

@ -60,7 +60,10 @@ def _resolve_instance(instance, config_file):
if env_url:
return env_url.rstrip("/")
raise click.ClickException(
"No instance specified. Use -i, set a default instance, or set DATASETTE_URL."
"No instance specified. Use -i <url-or-alias>, or configure a default:\n\n"
" dclient alias add <name> <url>\n"
" dclient alias default <name>\n\n"
"Or set the DATASETTE_URL environment variable."
)
@ -81,7 +84,9 @@ def _resolve_database(database, instance_alias, config_file):
if env_db:
return env_db
raise click.ClickException(
"No database specified. Use -d, set a default database, or set DATASETTE_DATABASE."
"No database specified. Use -d <name>, or configure a default:\n\n"
" dclient alias default-db <alias> <database>\n\n"
"Or set the DATASETTE_DATABASE environment variable."
)
@ -642,6 +647,31 @@ def default_query(sql, instance, database, token, verbose):
click.echo(json.dumps(response.json()["rows"], indent=2))
@cli.command()
@click.option("--json", "_json", is_flag=True, help="Output raw JSON")
def instances(_json):
"""
List known instances from the config
Example usage:
\b
dclient instances
dclient instances --json
"""
config_file = get_config_dir() / "config.json"
config = _load_config(config_file)
inst_map = config.get("instances", {})
default = config.get("default_instance")
if _json:
click.echo(json.dumps(config, indent=2))
else:
for name, inst in inst_map.items():
marker = "* " if name == default else " "
db_info = f" (db: {inst['default_database']})" if inst.get("default_database") else ""
click.echo(f"{marker}{name} = {inst['url']}{db_info}")
# -- alias command group --

View file

@ -536,3 +536,57 @@ def test_get_command(httpx_mock, mocker, tmpdir):
assert data == {"hello": "world"}
request = httpx_mock.get_request()
assert request.url.path == "/-/plugins.json"
# -- instances command --
def test_instances_plain(mocker, tmpdir):
config_dir = pathlib.Path(tmpdir)
mocker.patch("dclient.cli.get_config_dir", return_value=config_dir)
(config_dir / "config.json").write_text(
json.dumps(
{
"default_instance": "prod",
"instances": {
"prod": {"url": "https://prod.example.com", "default_database": "main"},
"staging": {"url": "https://staging.example.com", "default_database": None},
},
}
)
)
runner = CliRunner()
result = runner.invoke(cli, ["instances"])
assert result.exit_code == 0
assert "* prod = https://prod.example.com (db: main)" in result.output
assert " staging = https://staging.example.com" in result.output
def test_instances_json(mocker, tmpdir):
config_dir = pathlib.Path(tmpdir)
mocker.patch("dclient.cli.get_config_dir", return_value=config_dir)
(config_dir / "config.json").write_text(
json.dumps(
{
"default_instance": "prod",
"instances": {
"prod": {"url": "https://prod.example.com", "default_database": "main"},
},
}
)
)
runner = CliRunner()
result = runner.invoke(cli, ["instances", "--json"])
assert result.exit_code == 0
data = json.loads(result.output)
assert "prod" in data["instances"]
assert data["default_instance"] == "prod"
def test_instances_empty(mocker, tmpdir):
config_dir = pathlib.Path(tmpdir)
mocker.patch("dclient.cli.get_config_dir", return_value=config_dir)
runner = CliRunner()
result = runner.invoke(cli, ["instances"])
assert result.exit_code == 0
assert result.output.strip() == ""