fulfil-cli 0.1.3__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/PKG-INFO +21 -15
  2. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/README.md +20 -14
  3. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/pyproject.toml +1 -1
  4. fulfil_cli-0.2.0/src/fulfil_cli/__init__.py +6 -0
  5. fulfil_cli-0.2.0/src/fulfil_cli/auth/__init__.py +3 -0
  6. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/auth/api_key.py +8 -42
  7. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/app.py +92 -61
  8. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/api.py +18 -16
  9. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/auth.py +14 -24
  10. fulfil_cli-0.2.0/src/fulfil_cli/cli/commands/common.py +54 -0
  11. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/completion.py +4 -2
  12. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/config.py +18 -11
  13. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/model.py +112 -144
  14. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/commands/report.py +30 -49
  15. fulfil_cli-0.2.0/src/fulfil_cli/cli/state.py +106 -0
  16. fulfil_cli-0.2.0/src/fulfil_cli/client/__init__.py +4 -0
  17. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/client/errors.py +12 -0
  18. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/client/http.py +8 -8
  19. fulfil_cli-0.2.0/src/fulfil_cli/config/__init__.py +3 -0
  20. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/config/paths.py +14 -4
  21. fulfil_cli-0.2.0/src/fulfil_cli/output/__init__.py +4 -0
  22. fulfil_cli-0.2.0/src/fulfil_cli/output/formatter.py +125 -0
  23. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/output/json_output.py +6 -7
  24. fulfil_cli-0.1.3/src/fulfil_cli/__init__.py +0 -5
  25. fulfil_cli-0.1.3/src/fulfil_cli/cli/state.py +0 -89
  26. fulfil_cli-0.1.3/src/fulfil_cli/client/__init__.py +0 -0
  27. fulfil_cli-0.1.3/src/fulfil_cli/config/__init__.py +0 -0
  28. fulfil_cli-0.1.3/src/fulfil_cli/output/__init__.py +0 -0
  29. fulfil_cli-0.1.3/src/fulfil_cli/output/formatter.py +0 -129
  30. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/__main__.py +0 -0
  31. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/auth/keyring_store.py +0 -0
  32. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/auth/oauth.py +0 -0
  33. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/cli/__init__.py +0 -0
  34. {fulfil_cli-0.1.3/src/fulfil_cli/auth → fulfil_cli-0.2.0/src/fulfil_cli/cli/commands}/__init__.py +0 -0
  35. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/config/manager.py +0 -0
  36. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/output/describe.py +0 -0
  37. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/output/report.py +0 -0
  38. {fulfil_cli-0.1.3 → fulfil_cli-0.2.0}/src/fulfil_cli/output/table.py +0 -0
  39. /fulfil_cli-0.1.3/src/fulfil_cli/cli/commands/__init__.py → /fulfil_cli-0.2.0/src/fulfil_cli/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fulfil-cli
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: The Fulfil CLI — primary interface for humans and AI agents
5
5
  Keywords: fulfil,cli,erp,ecommerce
6
6
  Author: Fulfil.IO
@@ -108,12 +108,12 @@ fulfil sales_order list --fields reference,state,total_amount
108
108
 
109
109
  # Filter with MongoDB-style queries
110
110
  fulfil sales_order list --where '{"state": "confirmed"}'
111
- fulfil sales_order list --where '{"total_amount": {"gte": 1000}}'
111
+ fulfil sales_order list --where '{"sale_date": {"gte": "2025-01-01"}}'
112
112
  fulfil sales_order list --where '{"or": [{"state": "draft"}, {"state": "confirmed"}]}'
113
113
 
114
114
  # Sort results
115
115
  fulfil sales_order list --order sale_date:desc
116
- fulfil sales_order list --order total_amount:desc,reference:asc
116
+ fulfil sales_order list --order sale_date:desc,reference:asc
117
117
 
118
118
  # Control page size
119
119
  fulfil sales_order list --limit 50
@@ -134,20 +134,23 @@ fulfil sales_order get 1,2,3
134
134
  ### Creating records
135
135
 
136
136
  ```bash
137
- # Single record
138
- fulfil contact create --data '{"name": "Acme Corp"}'
137
+ # Single record (from stdin)
138
+ echo '{"name": "Acme Corp"}' | fulfil contact create
139
139
 
140
140
  # Record with nested lines (e.g. contact with addresses)
141
- fulfil contact create --data '{"name": "Acme Corp", "addresses": [{"street": "100 Broadway", "city": "New York"}, {"street": "45 Industrial Pkwy", "city": "Newark"}]}'
141
+ echo '{"name": "Acme Corp", "addresses": [{"street": "100 Broadway", "city": "New York"}, {"street": "45 Industrial Pkwy", "city": "Newark"}]}' | fulfil contact create
142
142
 
143
- # Multiple records at once
144
- fulfil contact create --data '[{"name": "Alice"}, {"name": "Bob"}]'
143
+ # Multiple records at once (preferred — never loop single creates)
144
+ echo '[{"name": "Alice"}, {"name": "Bob"}]' | fulfil contact create
145
+
146
+ # From a file
147
+ fulfil contact create data.json
145
148
  ```
146
149
 
147
150
  ### Updating records
148
151
 
149
152
  ```bash
150
- fulfil sales_order update 42 --data '{"comment": "Approved by finance"}'
153
+ echo '{"comment": "Approved by finance"}' | fulfil sales_order update 42
151
154
  ```
152
155
 
153
156
  ### Deleting records
@@ -218,14 +221,17 @@ echo '{"method": "model.product.count", "params": {}}' | fulfil api -
218
221
  |---|---|
219
222
  | Terminal | Rich tables with colors |
220
223
  | Piped / redirected / CI | JSON (automatic) |
221
- | `--json` flag | JSON (forced) |
224
+ | `--format` flag | table, json, csv, or ndjson |
222
225
 
223
226
  ```bash
224
227
  # Force JSON and pipe to jq
225
- fulfil sales_order list --fields reference,state --json | jq '.data[].reference'
228
+ fulfil sales_order list --fields reference,state --format json | jq '.data[].reference'
229
+
230
+ # Force format via env var
231
+ FULFIL_FORMAT=json fulfil sales_order list
226
232
 
227
- # Force JSON via env var
228
- FULFIL_JSON=1 fulfil sales_order list
233
+ # CSV output
234
+ fulfil sales_order list --fields reference,state --format csv
229
235
  ```
230
236
 
231
237
  ## Configuration
@@ -245,7 +251,7 @@ Config file location: `~/.config/fulfil/config.toml`
245
251
  --workspace TEXT Workspace domain (e.g. acme.fulfil.io)
246
252
  --debug Show HTTP request/response details
247
253
  --quiet, -q Suppress hints and decorative output
248
- --json Force JSON output
254
+ --format TEXT Output format: table, json, csv, ndjson
249
255
  -h, --help Show help
250
256
  ```
251
257
 
@@ -274,7 +280,7 @@ fulfil completion # auto-detects zsh/bash/fish
274
280
 
275
281
  The CLI is designed for programmatic use:
276
282
 
277
- - **JSON output by default** when stdout is piped or redirected
283
+ - **JSON output by default** when stdout is piped or redirected (override with `--format` or `FULFIL_FORMAT`)
278
284
  - **Structured exit codes** for error handling
279
285
  - **Errors on stderr**, data on stdout — safe to parse stdout directly
280
286
  - **Environment variable auth** — no interactive prompts: `FULFIL_API_KEY` + `FULFIL_WORKSPACE`
@@ -78,12 +78,12 @@ fulfil sales_order list --fields reference,state,total_amount
78
78
 
79
79
  # Filter with MongoDB-style queries
80
80
  fulfil sales_order list --where '{"state": "confirmed"}'
81
- fulfil sales_order list --where '{"total_amount": {"gte": 1000}}'
81
+ fulfil sales_order list --where '{"sale_date": {"gte": "2025-01-01"}}'
82
82
  fulfil sales_order list --where '{"or": [{"state": "draft"}, {"state": "confirmed"}]}'
83
83
 
84
84
  # Sort results
85
85
  fulfil sales_order list --order sale_date:desc
86
- fulfil sales_order list --order total_amount:desc,reference:asc
86
+ fulfil sales_order list --order sale_date:desc,reference:asc
87
87
 
88
88
  # Control page size
89
89
  fulfil sales_order list --limit 50
@@ -104,20 +104,23 @@ fulfil sales_order get 1,2,3
104
104
  ### Creating records
105
105
 
106
106
  ```bash
107
- # Single record
108
- fulfil contact create --data '{"name": "Acme Corp"}'
107
+ # Single record (from stdin)
108
+ echo '{"name": "Acme Corp"}' | fulfil contact create
109
109
 
110
110
  # Record with nested lines (e.g. contact with addresses)
111
- fulfil contact create --data '{"name": "Acme Corp", "addresses": [{"street": "100 Broadway", "city": "New York"}, {"street": "45 Industrial Pkwy", "city": "Newark"}]}'
111
+ echo '{"name": "Acme Corp", "addresses": [{"street": "100 Broadway", "city": "New York"}, {"street": "45 Industrial Pkwy", "city": "Newark"}]}' | fulfil contact create
112
112
 
113
- # Multiple records at once
114
- fulfil contact create --data '[{"name": "Alice"}, {"name": "Bob"}]'
113
+ # Multiple records at once (preferred — never loop single creates)
114
+ echo '[{"name": "Alice"}, {"name": "Bob"}]' | fulfil contact create
115
+
116
+ # From a file
117
+ fulfil contact create data.json
115
118
  ```
116
119
 
117
120
  ### Updating records
118
121
 
119
122
  ```bash
120
- fulfil sales_order update 42 --data '{"comment": "Approved by finance"}'
123
+ echo '{"comment": "Approved by finance"}' | fulfil sales_order update 42
121
124
  ```
122
125
 
123
126
  ### Deleting records
@@ -188,14 +191,17 @@ echo '{"method": "model.product.count", "params": {}}' | fulfil api -
188
191
  |---|---|
189
192
  | Terminal | Rich tables with colors |
190
193
  | Piped / redirected / CI | JSON (automatic) |
191
- | `--json` flag | JSON (forced) |
194
+ | `--format` flag | table, json, csv, or ndjson |
192
195
 
193
196
  ```bash
194
197
  # Force JSON and pipe to jq
195
- fulfil sales_order list --fields reference,state --json | jq '.data[].reference'
198
+ fulfil sales_order list --fields reference,state --format json | jq '.data[].reference'
199
+
200
+ # Force format via env var
201
+ FULFIL_FORMAT=json fulfil sales_order list
196
202
 
197
- # Force JSON via env var
198
- FULFIL_JSON=1 fulfil sales_order list
203
+ # CSV output
204
+ fulfil sales_order list --fields reference,state --format csv
199
205
  ```
200
206
 
201
207
  ## Configuration
@@ -215,7 +221,7 @@ Config file location: `~/.config/fulfil/config.toml`
215
221
  --workspace TEXT Workspace domain (e.g. acme.fulfil.io)
216
222
  --debug Show HTTP request/response details
217
223
  --quiet, -q Suppress hints and decorative output
218
- --json Force JSON output
224
+ --format TEXT Output format: table, json, csv, ndjson
219
225
  -h, --help Show help
220
226
  ```
221
227
 
@@ -244,7 +250,7 @@ fulfil completion # auto-detects zsh/bash/fish
244
250
 
245
251
  The CLI is designed for programmatic use:
246
252
 
247
- - **JSON output by default** when stdout is piped or redirected
253
+ - **JSON output by default** when stdout is piped or redirected (override with `--format` or `FULFIL_FORMAT`)
248
254
  - **Structured exit codes** for error handling
249
255
  - **Errors on stderr**, data on stdout — safe to parse stdout directly
250
256
  - **Environment variable auth** — no interactive prompts: `FULFIL_API_KEY` + `FULFIL_WORKSPACE`
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "fulfil-cli"
7
- version = "0.1.3"
7
+ version = "0.2.0"
8
8
  description = "The Fulfil CLI — primary interface for humans and AI agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,6 @@
1
+ """Fulfil CLI — command-line interface for the Fulfil ERP platform."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("fulfil-cli")
6
+ __all__ = ["__version__"]
@@ -0,0 +1,3 @@
1
+ from fulfil_cli.auth.api_key import Credentials, normalize_workspace, resolve_credentials
2
+
3
+ __all__ = ["Credentials", "normalize_workspace", "resolve_credentials"]
@@ -1,11 +1,13 @@
1
- """API key resolution — env > flag > keyring."""
1
+ """Credential resolution — flag > env > keyring, with workspace normalization."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
6
  from dataclasses import dataclass
7
+ from typing import Literal
7
8
 
8
9
  from fulfil_cli.auth.keyring_store import get_api_key, get_oauth_tokens, store_oauth_tokens
10
+ from fulfil_cli.auth.oauth import OAuthTokens, discover_oidc, refresh_access_token
9
11
  from fulfil_cli.client.errors import AuthError
10
12
 
11
13
 
@@ -13,42 +15,12 @@ from fulfil_cli.client.errors import AuthError
13
15
  class Credentials:
14
16
  """Resolved authentication credentials."""
15
17
 
16
- method: str # "api_key" or "oauth"
18
+ method: Literal["api_key", "oauth"]
17
19
  api_key: str | None = None
18
20
  access_token: str | None = None
19
21
  workspace: str | None = None
20
22
 
21
23
 
22
- def resolve_api_key(
23
- *,
24
- token_flag: str | None = None,
25
- workspace: str | None = None,
26
- ) -> str:
27
- """Resolve API key from: --token flag > FULFIL_API_KEY env > keyring.
28
-
29
- Raises AuthError if no key can be found.
30
- """
31
- # 1. Explicit --token flag
32
- if token_flag:
33
- return token_flag
34
-
35
- # 2. Environment variable
36
- env_key = os.environ.get("FULFIL_API_KEY")
37
- if env_key:
38
- return env_key
39
-
40
- # 3. Keyring (requires workspace)
41
- if workspace:
42
- stored = get_api_key(workspace)
43
- if stored:
44
- return stored
45
-
46
- raise AuthError(
47
- message="No API key found",
48
- hint="Run 'fulfil auth login' or set FULFIL_API_KEY environment variable.",
49
- )
50
-
51
-
52
24
  def resolve_credentials(
53
25
  *,
54
26
  token_flag: str | None = None,
@@ -57,23 +29,18 @@ def resolve_credentials(
57
29
  ) -> Credentials:
58
30
  """Resolve credentials, supporting both API key and OAuth.
59
31
 
60
- Priority: --token flag FULFIL_API_KEY env check auth method keyring.
32
+ Priority: --token flag > FULFIL_API_KEY env > check auth method > keyring.
61
33
  """
62
- # 1. Explicit --token flag (always treated as API key)
63
34
  if token_flag:
64
35
  return Credentials(method="api_key", api_key=token_flag)
65
36
 
66
- # 2. Environment variable
67
37
  env_key = os.environ.get("FULFIL_API_KEY")
68
38
  if env_key:
69
39
  return Credentials(method="api_key", api_key=env_key)
70
40
 
71
- # 3. Based on configured auth method
72
41
  if auth_method == "oauth" and workspace:
73
42
  tokens_json = get_oauth_tokens(workspace)
74
43
  if tokens_json:
75
- from fulfil_cli.auth.oauth import OAuthTokens, discover_oidc, refresh_access_token
76
-
77
44
  tokens = OAuthTokens.from_json(tokens_json)
78
45
  if tokens.is_expired and tokens.refresh_token:
79
46
  oidc = discover_oidc(workspace)
@@ -85,7 +52,6 @@ def resolve_credentials(
85
52
  workspace=workspace,
86
53
  )
87
54
 
88
- # 4. Fall back to API key from keyring
89
55
  if workspace:
90
56
  stored = get_api_key(workspace)
91
57
  if stored:
@@ -108,11 +74,11 @@ def resolve_workspace(
108
74
  Raises AuthError if no workspace can be found.
109
75
  """
110
76
  if workspace_flag:
111
- return _normalize_workspace(workspace_flag)
77
+ return normalize_workspace(workspace_flag)
112
78
 
113
79
  env_workspace = os.environ.get("FULFIL_WORKSPACE")
114
80
  if env_workspace:
115
- return _normalize_workspace(env_workspace)
81
+ return normalize_workspace(env_workspace)
116
82
 
117
83
  if config_workspace:
118
84
  return config_workspace
@@ -123,7 +89,7 @@ def resolve_workspace(
123
89
  )
124
90
 
125
91
 
126
- def _normalize_workspace(value: str) -> str:
92
+ def normalize_workspace(value: str) -> str:
127
93
  """Normalize workspace input — accept slug or full domain.
128
94
 
129
95
  'acme' → 'acme.fulfil.io'
@@ -1,4 +1,12 @@
1
- """Root CLI application with dynamic model subcommands."""
1
+ """Root CLI application with dynamic model and report subcommands.
2
+
3
+ Typer handles the root app and static commands (auth, config, version)
4
+ where type-driven argument parsing shines. Click groups are used for
5
+ dynamic model and report routing because Typer does not support runtime
6
+ subcommand creation via ``get_command`` overrides — the ``FulfilGroup``
7
+ and ``ReportGroup`` classes intercept unknown subcommand names and
8
+ resolve them as Fulfil model or report names respectively.
9
+ """
2
10
 
3
11
  from __future__ import annotations
4
12
 
@@ -10,24 +18,17 @@ from rich.console import Console
10
18
  from fulfil_cli import __version__
11
19
  from fulfil_cli.cli.commands import auth, config
12
20
  from fulfil_cli.cli.commands.api import api_cmd
21
+ from fulfil_cli.cli.commands.common import handle_error
13
22
  from fulfil_cli.cli.commands.completion import completion_install
14
23
  from fulfil_cli.cli.commands.model import create_model_group
15
24
  from fulfil_cli.cli.commands.report import create_report_group
16
- from fulfil_cli.cli.state import get_client, is_quiet, set_globals
17
- from fulfil_cli.client.errors import FulfilError
25
+ from fulfil_cli.cli.state import VALID_FORMATS, AppContext, format_option
26
+ from fulfil_cli.client.errors import EXIT_USAGE, FulfilError
18
27
  from fulfil_cli.output.formatter import output
19
28
 
20
29
  console = Console(stderr=True)
21
30
 
22
31
 
23
- def _handle_error(exc: FulfilError) -> None:
24
- """Print error and exit with appropriate code."""
25
- console.print(f"[red]Error: {exc}[/red]")
26
- if exc.hint and not is_quiet():
27
- console.print(f"[dim]Hint: {exc.hint}[/dim]")
28
- raise typer.Exit(code=exc.exit_code)
29
-
30
-
31
32
  class ReportGroup(click.Group):
32
33
  """Click group that resolves unknown subcommands as report names."""
33
34
 
@@ -41,7 +42,6 @@ class ReportGroup(click.Group):
41
42
  class FulfilGroup(typer.core.TyperGroup):
42
43
  """Custom Click group that resolves unknown subcommands as model or report names."""
43
44
 
44
- # Dynamic commands that should appear in help alongside static ones
45
45
  _dynamic_commands = ("models", "reports")
46
46
 
47
47
  def list_commands(self, ctx: click.Context) -> list[str]:
@@ -52,17 +52,13 @@ class FulfilGroup(typer.core.TyperGroup):
52
52
  return commands
53
53
 
54
54
  def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
55
- # Check static commands first
56
55
  rv = super().get_command(ctx, cmd_name)
57
56
  if rv is not None:
58
57
  return rv
59
- # `fulfil models` / `fulfil models list`
60
58
  if cmd_name == "models":
61
59
  return models_group
62
- # `fulfil reports` / `fulfil reports list` / `fulfil reports <name> execute`
63
60
  if cmd_name == "reports":
64
61
  return reports_group
65
- # Treat as model name → return model sub-group
66
62
  return create_model_group(cmd_name)
67
63
 
68
64
 
@@ -113,34 +109,64 @@ def main_callback(
113
109
  base_url: str | None = typer.Option(None, "--base-url", hidden=True, help="Override base URL"),
114
110
  debug: bool = typer.Option(False, "--debug", help="Show debug output"),
115
111
  quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress hints and decorative output"),
112
+ output_format: str | None = typer.Option(
113
+ None,
114
+ "--format",
115
+ help="Output format: table, json, csv, ndjson (default: table for TTY, json when piped)",
116
+ ),
116
117
  ) -> None:
117
118
  """Root callback — sets global auth state."""
118
- set_globals(token=token, workspace=workspace, base_url=base_url, debug=debug, quiet=quiet)
119
+ if output_format and output_format not in VALID_FORMATS:
120
+ console.print(
121
+ f"[red]Error: Invalid format '{output_format}'. "
122
+ f"Choose from: {', '.join(VALID_FORMATS)}[/red]"
123
+ )
124
+ raise typer.Exit(code=EXIT_USAGE)
125
+ ctx.obj = AppContext(
126
+ token=token,
127
+ workspace=workspace,
128
+ base_url=base_url,
129
+ debug=debug,
130
+ quiet=quiet,
131
+ output_format=output_format,
132
+ )
133
+
134
+
135
+ FORMAT_OPTION = typer.Option(
136
+ None,
137
+ "--format",
138
+ help="Output format: table, json, csv, ndjson",
139
+ )
119
140
 
120
141
 
121
142
  @app.command()
122
143
  def version(
123
- json_flag: bool = typer.Option(False, "--json", help="Output as JSON"),
144
+ ctx: typer.Context,
145
+ output_format: str | None = FORMAT_OPTION,
124
146
  ) -> None:
125
147
  """Show CLI version."""
126
- if json_flag:
127
- output({"version": __version__}, json_flag=True)
128
- else:
148
+ app_ctx: AppContext = ctx.obj
149
+ fmt = app_ctx.get_effective_format(output_format)
150
+ if fmt == "table":
129
151
  typer.echo(f"fulfil-cli {__version__}")
152
+ else:
153
+ output({"version": __version__}, fmt=fmt)
130
154
 
131
155
 
132
156
  @app.command()
133
157
  def whoami(
134
- json_flag: bool = typer.Option(False, "--json", help="Output as JSON"),
158
+ ctx: typer.Context,
159
+ output_format: str | None = FORMAT_OPTION,
135
160
  ) -> None:
136
161
  """Show current user and workspace info."""
162
+ app_ctx: AppContext = ctx.obj
137
163
  try:
138
- client = get_client()
164
+ client = app_ctx.get_client()
139
165
  result = client.call("system.whoami")
140
166
  except FulfilError as exc:
141
- _handle_error(exc)
167
+ handle_error(exc)
142
168
 
143
- output(result, json_flag=json_flag)
169
+ output(result, fmt=app_ctx.get_effective_format(output_format))
144
170
 
145
171
 
146
172
  def _flatten_model_row(row: dict) -> dict:
@@ -157,13 +183,14 @@ def _flatten_model_row(row: dict) -> dict:
157
183
  }
158
184
 
159
185
 
160
- def _list_models(json_flag: bool, search: str | None = None) -> None:
186
+ def _list_models(fmt: str, search: str | None = None) -> None:
161
187
  """Fetch and display all available models."""
188
+ app_ctx: AppContext = click.get_current_context().obj
162
189
  try:
163
- client = get_client()
190
+ client = app_ctx.get_client()
164
191
  result = client.call("system.list_models")
165
192
  except FulfilError as exc:
166
- _handle_error(exc)
193
+ handle_error(exc)
167
194
 
168
195
  if search and isinstance(result, list):
169
196
  term = search.lower()
@@ -178,43 +205,43 @@ def _list_models(json_flag: bool, search: str | None = None) -> None:
178
205
  )
179
206
  ]
180
207
 
181
- if not json_flag and isinstance(result, list):
208
+ if fmt == "table" and isinstance(result, list):
182
209
  result = [_flatten_model_row(r) for r in result if isinstance(r, dict)]
183
210
 
184
- output(result, json_flag=json_flag, title="Available Models")
211
+ output(result, fmt=fmt, title="Available Models")
185
212
 
186
213
 
187
214
  @click.group(name="models", help="List available models.", invoke_without_command=True)
188
- @click.option("--json", "json_flag", is_flag=True, help="Output as JSON")
189
215
  @click.option("--search", "-s", default=None, help="Filter by name, description, or category.")
216
+ @format_option
190
217
  @click.pass_context
191
- def models_group(ctx: click.Context, json_flag: bool, search: str | None) -> None:
218
+ def models_group(ctx: click.Context, search: str | None, output_format: str | None) -> None:
192
219
  """List all available models."""
193
- ctx.ensure_object(dict)
194
- ctx.obj["json_flag"] = json_flag
195
- ctx.obj["search"] = search
196
220
  if ctx.invoked_subcommand is None:
197
- _list_models(json_flag, search=search)
221
+ app_ctx: AppContext = ctx.obj
222
+ _list_models(app_ctx.get_effective_format(output_format), search=search)
198
223
 
199
224
 
200
225
  @models_group.command("list")
201
- @click.option("--json", "json_flag", is_flag=True, help="Output as JSON")
202
226
  @click.option("--search", "-s", default=None, help="Filter by name, description, or category.")
227
+ @format_option
203
228
  @click.pass_context
204
- def models_list_cmd(ctx: click.Context, json_flag: bool, search: str | None) -> None:
229
+ def models_list_cmd(ctx: click.Context, search: str | None, output_format: str | None) -> None:
205
230
  """List all available models."""
206
- _list_models(json_flag, search=search)
231
+ app_ctx: AppContext = ctx.obj
232
+ _list_models(app_ctx.get_effective_format(output_format), search=search)
207
233
 
208
234
 
209
- def _list_reports(json_flag: bool) -> None:
235
+ def _list_reports(fmt: str) -> None:
210
236
  """Fetch and display all available reports."""
237
+ app_ctx: AppContext = click.get_current_context().obj
211
238
  try:
212
- client = get_client()
239
+ client = app_ctx.get_client()
213
240
  result = client.call("system.list_reports")
214
241
  except FulfilError as exc:
215
- _handle_error(exc)
242
+ handle_error(exc)
216
243
 
217
- output(result, json_flag=json_flag, title="Available Reports")
244
+ output(result, fmt=fmt, title="Available Reports")
218
245
 
219
246
 
220
247
  @click.group(
@@ -223,42 +250,46 @@ def _list_reports(json_flag: bool) -> None:
223
250
  help="Interact with reports.",
224
251
  invoke_without_command=True,
225
252
  )
226
- @click.option("--json", "json_flag", is_flag=True, help="Output as JSON")
253
+ @format_option
227
254
  @click.pass_context
228
- def reports_group(ctx: click.Context, json_flag: bool) -> None:
255
+ def reports_group(ctx: click.Context, output_format: str | None) -> None:
229
256
  """List all available reports."""
230
- ctx.ensure_object(dict)
231
- ctx.obj["json_flag"] = json_flag
232
257
  if ctx.invoked_subcommand is None:
233
- _list_reports(json_flag)
258
+ app_ctx: AppContext = ctx.obj
259
+ _list_reports(app_ctx.get_effective_format(output_format))
234
260
 
235
261
 
236
262
  @reports_group.command("list")
237
- @click.option("--json", "json_flag", is_flag=True, help="Output as JSON")
263
+ @format_option
238
264
  @click.pass_context
239
- def reports_list_cmd(ctx: click.Context, json_flag: bool) -> None:
265
+ def reports_list_cmd(ctx: click.Context, output_format: str | None) -> None:
240
266
  """List all available reports."""
241
- _list_reports(json_flag)
267
+ app_ctx: AppContext = ctx.obj
268
+ _list_reports(app_ctx.get_effective_format(output_format))
242
269
 
243
270
 
244
271
  @app.command()
245
272
  def docs(
273
+ ctx: typer.Context,
246
274
  query: str = typer.Argument(..., help="Search query for Fulfil documentation"),
247
- json_flag: bool = typer.Option(False, "--json", help="Output as JSON"),
275
+ output_format: str | None = FORMAT_OPTION,
248
276
  ) -> None:
249
277
  """Search Fulfil documentation."""
278
+ app_ctx: AppContext = ctx.obj
250
279
  try:
251
- client = get_client()
280
+ client = app_ctx.get_client()
252
281
  results = client.call("system.search_docs", query=query)
253
282
  except FulfilError as exc:
254
- _handle_error(exc)
283
+ handle_error(exc)
255
284
 
256
285
  if not results:
257
- console.print("[dim]No results found.[/dim]")
286
+ if not app_ctx.quiet:
287
+ console.print("[dim]No results found.[/dim]")
258
288
  raise typer.Exit()
259
289
 
260
- if json_flag:
261
- output(results, json_flag=True)
290
+ fmt = app_ctx.get_effective_format(output_format)
291
+ if fmt != "table":
292
+ output(results, fmt=fmt)
262
293
  else:
263
294
  from rich.markdown import Markdown
264
295
  from rich.panel import Panel
@@ -315,8 +346,8 @@ fulfil sales_order get 42
315
346
  ## 4. Create and update records
316
347
 
317
348
  ```
318
- fulfil contact create --data '{"name": "Acme Corp"}'
319
- fulfil sales_order update 42 --data '{"comment": "Approved"}'
349
+ echo '{"name": "Acme Corp"}' | fulfil contact create
350
+ fulfil sales_order update 42 updates.json
320
351
  ```
321
352
 
322
353
  ## 5. Call workflow methods
@@ -338,7 +369,8 @@ fulfil reports price_list_report execute --params '{"date_from": "2024-01-01"}'
338
369
 
339
370
  ## Tips
340
371
 
341
- - Use `--json` to force JSON output (automatic when piped)
372
+ - Use `--format json` to force JSON output (automatic when piped)
373
+ - Use `--format csv` or `--format ndjson` for other machine-readable formats
342
374
  - Use `--debug` to see HTTP request/response details
343
375
  - Use `-h` on any command for help: `fulfil sales_order list -h`
344
376
  - Run `fulfil completion` to install shell completions
@@ -350,6 +382,5 @@ fulfil reports price_list_report execute --params '{"date_from": "2024-01-01"}'
350
382
  app.command(name="api")(api_cmd)
351
383
  app.command(name="completion")(completion_install)
352
384
 
353
-
354
385
  # Top-level aliases for frequently-used auth subcommands
355
386
  app.command(name="workspaces")(auth.workspaces)