Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions tools/developer_tools/bely-cli/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,11 @@ Conventions that hold across all command modules:
Text output is printed inline as work happens; structured output is accumulated into a
`result` dict and emitted once at the end via `print_result`/`print_items`. Guard
human-readable prints with `if fmt == "text"`.
- **`--format` and `--no-prompt` are per-leaf-command**, applied by the `common_options`
decorator in `cli.py` — never hoisted to the group level. `run_test.sh` asserts this.
The one exception is `tui_group`: it's declared with `@cli.group("tui",
invoke_without_command=True)` and carries `common_options` plus `--limit` itself, because
bare `bely-cli tui` (no subcommand) *is* a leaf invocation — `ctx.invoked_subcommand is
None` is what triggers `cmd_tui(mode="app", ...)`. `tui lookup` is a normal leaf
subcommand underneath it. `run_test.sh` checks both: `--format` on `tui lookup -h` and
`--limit` on the bare `tui -h`.
- **`--format` is per format-capable leaf command**, applied by `common_options` in
`cli.py`; `prompting_options` additionally supplies `--no-prompt` only to commands that
can request input or credentials. Delete commands use `--force` instead. The one group
exception is `tui_group`: it is declared with `invoke_without_command=True` and carries
`common_options` plus `--limit`, because bare `bely-cli tui` is itself a leaf invocation.
- **Non-interactive mode** is a module-level flag in `common.py`
(`set_no_prompt()` / `is_no_prompt()`), set by `--no-prompt` or automatically when
`--file=-`. Any code path that would prompt must check `is_no_prompt()` first and raise
Expand Down Expand Up @@ -182,7 +179,7 @@ Split so that most of it needs no terminal to test:
behavior, and `source` picks drilling in from logbook types vs. starting at the document
level with `core.recent_documents(...)`. Its `_exit_top()` pops back to whatever pushed
this browse (e.g. a command-palette-pushed "My documents" browse), or -- when `root=True`
(the landing screen `app.py` pushes on mount) -- does nothing but notify, since `q` is the
(the landing screen `app.py` pushes on mount) -- does nothing but notify, since `ctrl+q` is the
only quit key and an accidental escape shouldn't drop the user into an empty terminal.
- `app.py` — `BelyTuiApp`, the shared CSS, `ensure_auth()` (see below), and
`get_system_commands()`. Pushes `BrowseScreen` directly on mount for both modes;
Expand Down
22 changes: 10 additions & 12 deletions tools/developer_tools/bely-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ Aliases accept the same arguments and produce the same output as their canonical
- `bely-cli auth verify` checks whether the cached token is accepted by the server.
- `bely-cli auth logout` invalidates the current server session and removes the cached token.

All commands support the shared `--format` and `--no-prompt` options.
Commands that produce output support `--format`. Commands that may request missing input or credentials support `--no-prompt`; delete commands use `--force` instead.

### `doc` — log documents

Expand Down Expand Up @@ -248,12 +248,11 @@ bely-cli doc show --doc-id 99 --format json
#### `bely-cli doc delete` / `rm`

Delete a document selected by `--doc-name` or `--doc-id`. Confirmation is required unless
`--yes` is supplied. A document containing entries requires `--force`; `--force` does not
skip confirmation.
`--force` is supplied. Deleting a document that contains entries also requires `--force`.

```bash
bely-cli doc delete --doc-id 99
bely-cli doc rm --doc-name "Disposable Log" --force --yes
bely-cli doc rm --doc-name "Disposable Log" --force
```

### `tui` — interactive terminal UIs
Expand All @@ -276,7 +275,7 @@ bely-cli tui --limit 50

Browsing needs no authentication. `Enter` on an entry just keeps it in the preview
instead of exiting, and `Esc` at the logbook list does nothing (there's nothing above it to
pop back to) — press `q` to quit. A few keys are available once you've drilled in:
pop back to) — press `ctrl+q` to quit. Pressing `q` alone shows a reminder instead of exiting. A few keys are available once you've drilled in:

| Key | Level | Action |
|-----|-------|--------|
Expand Down Expand Up @@ -419,7 +418,7 @@ only appear there. `t` appears at the entries level only when the document has r
| `Up` / `Down`, `PgUp` / `PgDn` | Move the highlight; the preview/info panel follows. |
| `/` | Reveal and focus the filter box; incrementally filters the current table (case-insensitive substring, matched against the entry text — not the reply tree's glyphs or reply count). |
| `Enter` | In the filter box, return focus to the table. Elsewhere, drill into the highlighted row, or select the entry at the entries level. |
| `Esc` / `Backspace` | Go back one level (from the table); does nothing at the logbook list — press `q` to quit. In the filter box, `Esc` returns focus to the table. |
| `Esc` / `Backspace` | Go back one level (from the table); does nothing at the logbook list — press `ctrl+q` to quit. In the filter box, `Esc` returns focus to the table. |
| `d` | Logbook/document levels only: create a new document (see `bely-cli tui`'s "New document" above) — a mutation, so this is where the app authenticates if it hasn't already. |
| `s` | Entries level only: save the highlighted entry's markdown to a file in the current directory. |
| `y` | Entries level only: copy a `bely-cli entry get` reference for the highlighted entry to the clipboard. |
Expand All @@ -431,7 +430,7 @@ only appear there. `t` appears at the entries level only when the document has r
| `i` | Logbook/document levels only: toggle the side info panel. |
| `f` | Entries level only: toggle the table to widen the preview pane. |
| `r` | Refresh the current level, bypassing the in-session cache. Entry selection, preview scroll, filters, and collapsed threads are preserved when possible. |
| `q` | Quit without selecting. |
| `ctrl+q` | Quit without selecting. Pressing `q` alone shows a reminder. |

Replies only nest one level deep. Pressing `p` on either a top-level entry or one of its
replies targets the top-level thread; `n` still adds a separate top-level entry.
Expand Down Expand Up @@ -552,10 +551,10 @@ bely-cli entry attachment ls -n "Shift Report" --id 42 --format json

#### `bely-cli entry attachment delete` / `rm`

Delete an attachment using its numeric ID from `attachment list`:
Delete an attachment using its numeric ID from `attachment list`. Use `--force` to skip confirmation:

```bash
bely-cli entry attachment delete -d 99 --id 42 --attachment-id 7
bely-cli entry attachment delete -d 99 --id 42 --attachment-id 7 --force
```

#### `bely-cli entry update`
Expand Down Expand Up @@ -593,12 +592,11 @@ Delete a top-level entry or reply by log ID, with confirmation by default:

```bash
bely-cli entry delete -d 99 --id 42
bely-cli entry rm -n "Shift Report" --id 42 --yes
bely-cli entry rm -n "Shift Report" --id 42 --force
```

All delete commands support structured JSON/YAML output. Declining confirmation returns
`status: cancelled`; successful deletion returns `status: deleted`. In `--no-prompt` mode,
`--yes` is required.
`status: cancelled`; successful deletion returns `status: deleted`. Use `--force` to skip confirmation.

#### `bely-cli entry get`

Expand Down
12 changes: 9 additions & 3 deletions tools/developer_tools/bely-cli/run_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,17 @@ $RUNNER bely-cli entry attachment add -h | grep -q -- --format
$RUNNER bely-cli entry attachment list -h | grep -q -- --format
$RUNNER bely-cli entry attachment ls -h | grep -q -- --format
$RUNNER bely-cli doc delete -h | grep -q -- --force
$RUNNER bely-cli doc rm -h | grep -q -- --yes
$RUNNER bely-cli entry delete -h | grep -q -- --yes
$RUNNER bely-cli doc rm -h | grep -q -- --force
$RUNNER bely-cli entry delete -h | grep -q -- --force
$RUNNER bely-cli entry rm -h | grep -q -- --id
$RUNNER bely-cli entry attachment delete -h | grep -q -- --attachment-id
$RUNNER bely-cli entry attachment rm -h | grep -q -- --yes
$RUNNER bely-cli entry attachment rm -h | grep -q -- --force
# --no-prompt is limited to commands with interactive input or credential fallback.
$RUNNER bely-cli doc new -h | grep -q -- --no-prompt
$RUNNER bely-cli entry update -h | grep -q -- --no-prompt
! $RUNNER bely-cli doc show -h | grep -q -- --no-prompt
! $RUNNER bely-cli entry delete -h | grep -q -- --no-prompt
! $RUNNER bely-cli tui -h | grep -q -- --no-prompt
$RUNNER bely-cli tui lookup -h | grep -q -- --format
$RUNNER bely-cli shell -h | grep -q -- cache
$RUNNER bely-cli shell init -h | grep -q -- --shell
Expand Down
43 changes: 28 additions & 15 deletions tools/developer_tools/bely-cli/src/bely_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,19 @@ def decorator(f):


def common_options(f):
"""Options shared by all leaf commands (--format and --no-prompt)."""
return format_option(no_prompt_option(f))
"""Options shared by format-capable leaf commands."""
return format_option(f)


def prompting_options(f):
"""Format and non-interactive options for commands that may prompt."""
return common_options(no_prompt_option(f))


@click.group(context_settings=CONTEXT_SETTINGS)
def cli():
"""BELY logbook CLI"""
pass
set_no_prompt(False)


# -- auth --
Expand All @@ -127,7 +132,7 @@ def auth_group():


@auth_group.command("login")
@common_options
@prompting_options
def auth_login(output_format):
"""Log in and cache an authentication token."""
cmd_auth_login(fmt=output_format)
Expand Down Expand Up @@ -173,7 +178,7 @@ def doc_group():
type=click.Choice(["system", "type", "template"]),
default=None,
help="List available values for the given option and exit")
@common_options
@prompting_options
def doc_new(output_format, **kwargs):
"""Create a new log document."""
if kwargs.get('file') == '-':
Expand All @@ -183,7 +188,7 @@ def doc_new(output_format, **kwargs):

@doc_group.command("list")
@click.option("--limit", default=20, type=int, help="Max documents to return (default 20)")
@common_options
@prompting_options
def doc_list(output_format, **kwargs):
"""List recent log documents created by you."""
cmd_list_docs(fmt=output_format, **kwargs)
Expand All @@ -201,11 +206,15 @@ def doc_show(output_format, **kwargs):
@doc_group.command("delete")
@doc_name_option
@doc_id_option
@click.option("--yes", is_flag=True, help="Delete without confirmation")
@click.option("--force", is_flag=True, help="Allow deletion when the document contains entries")
@click.option(
"--force", is_flag=True,
help="Delete without confirmation, including documents that contain entries",
)
@common_options
def doc_delete(output_format, **kwargs):
"""Delete a log document."""
if kwargs.get("force"):
set_no_prompt()
cmd_delete_doc(fmt=output_format, **kwargs)


Expand Down Expand Up @@ -234,7 +243,7 @@ def entry_attachment_group():
@entry_id_option()
@click.option("--file", "-f", required=True, type=click.Path(path_type=str),
help="File to attach")
@common_options
@prompting_options
def entry_attachment_add(output_format, **kwargs):
"""Upload an attachment to an existing log entry."""
cmd_add_attachment(fmt=output_format, **kwargs)
Expand All @@ -258,10 +267,12 @@ def entry_attachment_list(output_format, **kwargs):
"--attachment-id", required=True, type=int,
shell_complete=complete_attachment_ids, help="Numeric attachment ID",
)
@click.option("--yes", is_flag=True, help="Delete without confirmation")
@click.option("--force", is_flag=True, help="Delete without confirmation")
@common_options
def entry_attachment_delete(output_format, **kwargs):
"""Delete an attachment from a log entry."""
if kwargs.get("force"):
set_no_prompt()
cmd_delete_attachment(fmt=output_format, **kwargs)


Expand All @@ -277,7 +288,7 @@ def entry_attachment_delete(output_format, **kwargs):
@click.option("--text", "-t", default=None, help="Inline text for the entry")
@click.option("--add-attachment", default=None, type=click.Path(path_type=str),
help="File to attach to the entry")
@common_options
@prompting_options
def entry_add(output_format, **kwargs):
"""Add a new log entry to an existing document."""
if kwargs.get('file') == '-':
Expand All @@ -297,7 +308,7 @@ def entry_add(output_format, **kwargs):
@click.option("--text", "-t", default=None, help="Inline reply text")
@click.option("--add-attachment", default=None, type=click.Path(path_type=str),
help="File to attach to the reply")
@common_options
@prompting_options
def entry_reply(output_format, **kwargs):
"""Reply to an existing log entry."""
if kwargs.get('file') == '-':
Expand All @@ -314,7 +325,7 @@ def entry_reply(output_format, **kwargs):
@click.option("--text", "-t", default=None, help="Inline text for the entry")
@click.option("--add-attachment", default=None, type=click.Path(path_type=str),
help="File to attach to the entry")
@common_options
@prompting_options
def entry_update(output_format, **kwargs):
"""Update an existing log entry or reply."""
if kwargs.get('file') == '-':
Expand Down Expand Up @@ -352,10 +363,12 @@ def entry_get(output_format, **kwargs):
@doc_name_option
@doc_id_option
@entry_id_option(help="Log entry or reply ID")
@click.option("--yes", is_flag=True, help="Delete without confirmation")
@click.option("--force", is_flag=True, help="Delete without confirmation")
@common_options
def entry_delete(output_format, **kwargs):
"""Delete a log entry or reply."""
if kwargs.get("force"):
set_no_prompt()
cmd_delete_entry(fmt=output_format, **kwargs)


Expand All @@ -369,7 +382,7 @@ def entry_delete(output_format, **kwargs):

# Bare `bely-cli tui` launches the full interactive app, so the group itself
# is a leaf invocation when no subcommand is given -- the one place
# --format/--no-prompt sit on a group rather than a leaf command.
# --format sits on a group rather than a leaf command.
@cli.group("tui", cls=AliasHelpGroup, invoke_without_command=True)
@click.option("--limit", default=100, type=int,
help="Recent documents to load per logbook (default 100)")
Expand Down
4 changes: 2 additions & 2 deletions tools/developer_tools/bely-cli/src/bely_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def cmd_show_doc(doc_name, doc_id, fmt="text"):
print(f"{labels[key]}: {value}")


def cmd_delete_doc(doc_name, doc_id, yes=False, force=False, fmt="text"):
def cmd_delete_doc(doc_name, doc_id, force=False, fmt="text"):
"""Delete a log document after validation and confirmation."""
factory = auth.get_factory()
logbook_api = factory.get_logbook_api()
Expand All @@ -224,7 +224,7 @@ def cmd_delete_doc(doc_name, doc_id, yes=False, force=False, fmt="text"):
raise ValueError(
f'document "{doc.name}" contains {len(entries)} entries; use --force to delete it')
result = {"doc_id": doc.id, "doc": doc.name, "status": "cancelled"}
if not confirm_delete(f'Delete document "{doc.name}" (id={doc.id})?', yes):
if not confirm_delete(f'Delete document "{doc.name}" (id={doc.id})?', force):
print_result(result, "Deletion cancelled.", fmt)
return result
with auth.get_authenticated_factory() as auth_factory:
Expand Down
8 changes: 3 additions & 5 deletions tools/developer_tools/bely-cli/src/bely_cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@ def is_no_prompt():
return _no_prompt


def confirm_delete(prompt, yes=False):
"""Confirm a destructive operation, respecting non-interactive mode."""
if yes:
def confirm_delete(prompt, force=False):
"""Confirm a destructive operation unless forced."""
if force:
return True
if is_no_prompt():
raise ValueError("--yes is required with --no-prompt for deletion")
return input(f"{prompt} [y/N] ").strip().lower() in ("y", "yes")


Expand Down
8 changes: 4 additions & 4 deletions tools/developer_tools/bely-cli/src/bely_cli/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def cmd_add_attachment(doc_name, doc_id, entry_id, file, fmt="text"):
print_result({"doc": doc.name, "log_id": entry_id, "attachment": info}, "", fmt)


def cmd_delete_attachment(doc_name, doc_id, entry_id, attachment_id, yes=False, fmt="text"):
def cmd_delete_attachment(doc_name, doc_id, entry_id, attachment_id, force=False, fmt="text"):
"""Delete an entry attachment by numeric ID after confirmation."""
factory = auth.get_factory()
logbook_api = factory.get_logbook_api()
Expand All @@ -223,7 +223,7 @@ def cmd_delete_attachment(doc_name, doc_id, entry_id, attachment_id, yes=False,
"attachment_id": attachment_id, "status": "cancelled",
}
if not confirm_delete(
f'Delete attachment {attachment_id} from entry {entry_id}?', yes):
f'Delete attachment {attachment_id} from entry {entry_id}?', force):
print_result(result, "Deletion cancelled.", fmt)
return result
with auth.get_authenticated_factory() as auth_factory:
Expand Down Expand Up @@ -253,7 +253,7 @@ def cmd_list_attachments(doc_name, doc_id, entry_id, fmt="text"):
print_items(items, columns, fmt)


def cmd_delete_entry(doc_name, doc_id, entry_id, yes=False, fmt="text"):
def cmd_delete_entry(doc_name, doc_id, entry_id, force=False, fmt="text"):
"""Delete a log entry or reply after confirmation."""
factory = auth.get_factory()
logbook_api = factory.get_logbook_api()
Expand All @@ -268,7 +268,7 @@ def cmd_delete_entry(doc_name, doc_id, entry_id, yes=False, fmt="text"):
"status": "cancelled",
}
if not confirm_delete(
f'Delete entry/reply {entry_id} from document "{doc.name}"?', yes):
f'Delete entry/reply {entry_id} from document "{doc.name}"?', force):
print_result(result, "Deletion cancelled.", fmt)
return result
with auth.get_authenticated_factory() as auth_factory:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,8 @@ def action_delete(self):
async def _delete(self, attachment):
confirmed = await self.app.push_screen_wait(ConfirmScreen(
f'Delete attachment "{attachment.original_filename}" (id={attachment.id})?',
confirm_label="Delete", cancel_label="Cancel", confirm_variant="error"))
confirm_label="Delete", cancel_label="Cancel", confirm_variant="error",
confirm_shortcut="y"))
if not confirmed:
return
api = await self.app.ensure_auth()
Expand Down
Loading
Loading