cited-cli 0.3.2__py3-none-any.whl
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.
- cited_cli/__init__.py +1 -0
- cited_cli/__main__.py +3 -0
- cited_cli/api/__init__.py +0 -0
- cited_cli/api/client.py +111 -0
- cited_cli/api/endpoints.py +88 -0
- cited_cli/app.py +199 -0
- cited_cli/auth/__init__.py +0 -0
- cited_cli/auth/oauth_server.py +123 -0
- cited_cli/auth/store.py +83 -0
- cited_cli/commands/__init__.py +0 -0
- cited_cli/commands/agent.py +129 -0
- cited_cli/commands/analytics.py +83 -0
- cited_cli/commands/audit.py +177 -0
- cited_cli/commands/auth.py +353 -0
- cited_cli/commands/business.py +217 -0
- cited_cli/commands/config_cmd.py +111 -0
- cited_cli/commands/hq.py +125 -0
- cited_cli/commands/job.py +106 -0
- cited_cli/commands/mcp_cmd.py +30 -0
- cited_cli/commands/named_audit.py +244 -0
- cited_cli/commands/recommend.py +211 -0
- cited_cli/commands/solution.py +154 -0
- cited_cli/config/__init__.py +0 -0
- cited_cli/config/constants.py +48 -0
- cited_cli/config/manager.py +69 -0
- cited_cli/mcp/__init__.py +1 -0
- cited_cli/mcp/context.py +12 -0
- cited_cli/mcp/server.py +54 -0
- cited_cli/mcp/tools/__init__.py +1 -0
- cited_cli/mcp/tools/_helpers.py +33 -0
- cited_cli/mcp/tools/audit.py +129 -0
- cited_cli/mcp/tools/auth.py +23 -0
- cited_cli/mcp/tools/business.py +96 -0
- cited_cli/mcp/tools/job.py +57 -0
- cited_cli/mcp/tools/recommend.py +96 -0
- cited_cli/mcp/tools/solution.py +66 -0
- cited_cli/output/__init__.py +0 -0
- cited_cli/output/formatter.py +61 -0
- cited_cli/output/progress.py +56 -0
- cited_cli/output/tables.py +49 -0
- cited_cli/utils/__init__.py +0 -0
- cited_cli/utils/errors.py +31 -0
- cited_cli/utils/interactive.py +83 -0
- cited_cli-0.3.2.dist-info/METADATA +515 -0
- cited_cli-0.3.2.dist-info/RECORD +48 -0
- cited_cli-0.3.2.dist-info/WHEEL +4 -0
- cited_cli-0.3.2.dist-info/entry_points.txt +2 -0
- cited_cli-0.3.2.dist-info/licenses/LICENSE +4 -0
cited_cli/commands/hq.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from cited_cli.output.formatter import OutputContext, print_error, print_result
|
|
9
|
+
from cited_cli.output.tables import render_bar
|
|
10
|
+
from cited_cli.utils.errors import CitedAPIError, ExitCode, handle_api_error
|
|
11
|
+
from cited_core.api import endpoints
|
|
12
|
+
from cited_core.api.client import CitedClient
|
|
13
|
+
from cited_core.auth.store import TokenStore
|
|
14
|
+
from cited_core.config.manager import ConfigManager
|
|
15
|
+
|
|
16
|
+
hq_app = typer.Typer(name="hq", help="Business HQ dashboard.", invoke_without_command=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_client(ctx: typer.Context) -> tuple[OutputContext, CitedClient]:
|
|
20
|
+
obj = ctx.obj or {}
|
|
21
|
+
out: OutputContext = obj.get("output", OutputContext())
|
|
22
|
+
cfg: ConfigManager = obj.get("config", ConfigManager())
|
|
23
|
+
profile = obj.get("profile", "default")
|
|
24
|
+
env = cfg.get_environment(profile, obj.get("env_override"))
|
|
25
|
+
api_url = cfg.get_api_url(profile, obj.get("env_override"))
|
|
26
|
+
|
|
27
|
+
store = TokenStore()
|
|
28
|
+
token = store.get_token(env)
|
|
29
|
+
if not token:
|
|
30
|
+
print_error(f"Not logged in to {env}. Run: cited login", out)
|
|
31
|
+
raise typer.Exit(ExitCode.AUTH_ERROR)
|
|
32
|
+
|
|
33
|
+
return out, CitedClient(base_url=api_url, token=token)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@hq_app.callback(invoke_without_command=True)
|
|
37
|
+
def hq_dashboard(
|
|
38
|
+
ctx: typer.Context,
|
|
39
|
+
business_id: Annotated[str, typer.Argument(help="Business ID")],
|
|
40
|
+
full: Annotated[bool, typer.Option("--full", help="Include all data (heavy)")] = False,
|
|
41
|
+
personas: Annotated[bool, typer.Option("--personas", help="Include personas")] = False,
|
|
42
|
+
products: Annotated[bool, typer.Option("--products", help="Include products")] = False,
|
|
43
|
+
intents: Annotated[bool, typer.Option("--intents", help="Include buyer intents")] = False,
|
|
44
|
+
actions: Annotated[bool, typer.Option("--actions", help="Include priority actions")] = False,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Show the Business HQ dashboard."""
|
|
47
|
+
if ctx.invoked_subcommand is not None:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
out, client = _get_client(ctx)
|
|
51
|
+
try:
|
|
52
|
+
# Fetch base HQ data
|
|
53
|
+
if full:
|
|
54
|
+
path = endpoints.HQ_HEAVY.format(business_id=business_id)
|
|
55
|
+
else:
|
|
56
|
+
path = endpoints.HQ.format(business_id=business_id)
|
|
57
|
+
data = client.get(path)
|
|
58
|
+
|
|
59
|
+
# Fetch additional sections if requested
|
|
60
|
+
if personas and not full:
|
|
61
|
+
data["personas"] = client.get(endpoints.PERSONAS.format(business_id=business_id))
|
|
62
|
+
if products and not full:
|
|
63
|
+
data["products"] = client.get(endpoints.PRODUCTS.format(business_id=business_id))
|
|
64
|
+
if intents and not full:
|
|
65
|
+
path = endpoints.BUYER_INTENTS.format(business_id=business_id)
|
|
66
|
+
data["buyer_intents"] = client.get(path)
|
|
67
|
+
if actions and not full:
|
|
68
|
+
path = endpoints.HQ_PRIORITY.format(business_id=business_id)
|
|
69
|
+
data["priority_actions"] = client.get(path)
|
|
70
|
+
|
|
71
|
+
def _human(d: object, console: Console) -> None:
|
|
72
|
+
if not isinstance(d, dict):
|
|
73
|
+
return
|
|
74
|
+
name = d.get("business", {}).get("name", d.get("name", "Business HQ"))
|
|
75
|
+
console.print()
|
|
76
|
+
console.rule(f"[bold]{name}[/bold]")
|
|
77
|
+
|
|
78
|
+
# Health scores
|
|
79
|
+
scores = d.get("health_scores", d.get("scores", {}))
|
|
80
|
+
if scores:
|
|
81
|
+
console.print("\n[bold]Health Scores[/bold]")
|
|
82
|
+
if isinstance(scores, dict):
|
|
83
|
+
for label, val in scores.items():
|
|
84
|
+
if isinstance(val, (int, float)):
|
|
85
|
+
render_bar(label.replace("_", " ").title(), val, console=console)
|
|
86
|
+
|
|
87
|
+
# Personas
|
|
88
|
+
if d.get("personas"):
|
|
89
|
+
console.print("\n[bold]Personas[/bold]")
|
|
90
|
+
p_list = (
|
|
91
|
+
d["personas"] if isinstance(d["personas"], list)
|
|
92
|
+
else d["personas"].get("items", [])
|
|
93
|
+
)
|
|
94
|
+
for p in p_list:
|
|
95
|
+
console.print(f" - {p.get('name', 'Unknown')}")
|
|
96
|
+
|
|
97
|
+
# Products
|
|
98
|
+
if d.get("products"):
|
|
99
|
+
console.print("\n[bold]Products[/bold]")
|
|
100
|
+
prod_list = (
|
|
101
|
+
d["products"] if isinstance(d["products"], list)
|
|
102
|
+
else d["products"].get("items", [])
|
|
103
|
+
)
|
|
104
|
+
for p in prod_list:
|
|
105
|
+
console.print(f" - {p.get('name', 'Unknown')}")
|
|
106
|
+
|
|
107
|
+
# Priority actions
|
|
108
|
+
if d.get("priority_actions"):
|
|
109
|
+
console.print("\n[bold]Priority Actions[/bold]")
|
|
110
|
+
pa = d["priority_actions"]
|
|
111
|
+
act_list = (
|
|
112
|
+
pa if isinstance(pa, list)
|
|
113
|
+
else pa.get("items", [])
|
|
114
|
+
)
|
|
115
|
+
for a in act_list:
|
|
116
|
+
label = a.get('title', a.get('description', ''))
|
|
117
|
+
console.print(f" [{a.get('priority', '?')}] {label}")
|
|
118
|
+
|
|
119
|
+
console.print()
|
|
120
|
+
|
|
121
|
+
print_result(data, out, human_formatter=_human)
|
|
122
|
+
except CitedAPIError as e:
|
|
123
|
+
handle_api_error(e, out.json_mode)
|
|
124
|
+
finally:
|
|
125
|
+
client.close()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from cited_cli.output.formatter import OutputContext, print_error, print_result, print_success
|
|
8
|
+
from cited_cli.output.progress import watch_job
|
|
9
|
+
from cited_cli.output.tables import render_kv
|
|
10
|
+
from cited_cli.utils.errors import CitedAPIError, ExitCode, handle_api_error
|
|
11
|
+
from cited_cli.utils.interactive import confirm_action
|
|
12
|
+
from cited_core.api.client import CitedClient
|
|
13
|
+
from cited_core.auth.store import TokenStore
|
|
14
|
+
from cited_core.config.manager import ConfigManager
|
|
15
|
+
|
|
16
|
+
job_app = typer.Typer(name="job", help="Monitor and manage background jobs.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_client(ctx: typer.Context) -> tuple[OutputContext, CitedClient]:
|
|
20
|
+
obj = ctx.obj or {}
|
|
21
|
+
out: OutputContext = obj.get("output", OutputContext())
|
|
22
|
+
cfg: ConfigManager = obj.get("config", ConfigManager())
|
|
23
|
+
profile = obj.get("profile", "default")
|
|
24
|
+
env = cfg.get_environment(profile, obj.get("env_override"))
|
|
25
|
+
api_url = cfg.get_api_url(profile, obj.get("env_override"))
|
|
26
|
+
|
|
27
|
+
store = TokenStore()
|
|
28
|
+
token = store.get_token(env)
|
|
29
|
+
if not token:
|
|
30
|
+
print_error(f"Not logged in to {env}. Run: cited login", out)
|
|
31
|
+
raise typer.Exit(ExitCode.AUTH_ERROR)
|
|
32
|
+
|
|
33
|
+
return out, CitedClient(base_url=api_url, token=token)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _guess_job_type(job_id: str, client: CitedClient) -> str:
|
|
37
|
+
"""Try to determine job type by probing status endpoints."""
|
|
38
|
+
for prefix in ["/audit", "/recommendations", "/solutions"]:
|
|
39
|
+
try:
|
|
40
|
+
client.get(f"{prefix}/{job_id}/status")
|
|
41
|
+
return prefix.strip("/")
|
|
42
|
+
except CitedAPIError:
|
|
43
|
+
continue
|
|
44
|
+
return "audit" # default fallback
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@job_app.command("watch")
|
|
48
|
+
def job_watch(
|
|
49
|
+
ctx: typer.Context,
|
|
50
|
+
job_id: Annotated[str, typer.Argument(help="Job ID to watch")],
|
|
51
|
+
job_type: Annotated[
|
|
52
|
+
str,
|
|
53
|
+
typer.Option("--type", "-t", help="Job type: audit, recommendations, solutions"),
|
|
54
|
+
] = "",
|
|
55
|
+
interval: Annotated[
|
|
56
|
+
float,
|
|
57
|
+
typer.Option("--interval", "-i", help="Poll interval in seconds"),
|
|
58
|
+
] = 2.0,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Watch a job's progress until completion."""
|
|
61
|
+
out, client = _get_client(ctx)
|
|
62
|
+
try:
|
|
63
|
+
if not job_type:
|
|
64
|
+
job_type = _guess_job_type(job_id, client)
|
|
65
|
+
|
|
66
|
+
status_path = f"/{job_type}/{job_id}/status"
|
|
67
|
+
result = watch_job(client, status_path, out.console, poll_interval=interval)
|
|
68
|
+
|
|
69
|
+
status = result.get("status", "unknown")
|
|
70
|
+
if status in ("completed", "complete", "done"):
|
|
71
|
+
print_success(f"Job {job_id[:8]} completed", out)
|
|
72
|
+
elif status in ("failed", "error"):
|
|
73
|
+
print_error(f"Job {job_id[:8]} failed: {result.get('error', '')}", out)
|
|
74
|
+
raise typer.Exit(ExitCode.ERROR)
|
|
75
|
+
else:
|
|
76
|
+
print_result(result, out, human_formatter=lambda d, c: render_kv("Job Result", d, c))
|
|
77
|
+
except CitedAPIError as e:
|
|
78
|
+
handle_api_error(e, out.json_mode)
|
|
79
|
+
finally:
|
|
80
|
+
client.close()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@job_app.command("cancel")
|
|
84
|
+
def job_cancel(
|
|
85
|
+
ctx: typer.Context,
|
|
86
|
+
job_id: Annotated[str, typer.Argument(help="Job ID to cancel")],
|
|
87
|
+
job_type: Annotated[
|
|
88
|
+
str,
|
|
89
|
+
typer.Option("--type", "-t", help="Job type: audit, recommendations, solutions"),
|
|
90
|
+
] = "",
|
|
91
|
+
yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Cancel a running job."""
|
|
94
|
+
out, client = _get_client(ctx)
|
|
95
|
+
confirm_action(f"Cancel job {job_id[:8]}?", out, skip=yes)
|
|
96
|
+
try:
|
|
97
|
+
if not job_type:
|
|
98
|
+
job_type = _guess_job_type(job_id, client)
|
|
99
|
+
|
|
100
|
+
cancel_path = f"/{job_type}/{job_id}/cancel"
|
|
101
|
+
client.post(cancel_path)
|
|
102
|
+
print_success(f"Cancelled job {job_id[:8]}", out)
|
|
103
|
+
except CitedAPIError as e:
|
|
104
|
+
handle_api_error(e, out.json_mode)
|
|
105
|
+
finally:
|
|
106
|
+
client.close()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
mcp_app = typer.Typer(help="MCP server for AI agent integration.", no_args_is_help=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@mcp_app.command("serve")
|
|
13
|
+
def mcp_serve(
|
|
14
|
+
env: Annotated[
|
|
15
|
+
str | None,
|
|
16
|
+
typer.Option("--env", "-e", help="Target environment (dev, prod, local)"),
|
|
17
|
+
] = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Start the MCP server (stdio transport) for AI agent tool calls."""
|
|
20
|
+
try:
|
|
21
|
+
from cited_mcp.server import run_server
|
|
22
|
+
except ImportError:
|
|
23
|
+
print(
|
|
24
|
+
'MCP dependencies not installed. Run: pip install "cited-cli[mcp]"',
|
|
25
|
+
file=sys.stderr,
|
|
26
|
+
)
|
|
27
|
+
raise typer.Exit(1) # noqa: B904
|
|
28
|
+
if env:
|
|
29
|
+
os.environ["CITED_ENV"] = env
|
|
30
|
+
run_server()
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Annotated
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from cited_cli.output.formatter import OutputContext, print_error, print_result, print_success
|
|
9
|
+
from cited_cli.output.tables import render_kv, render_table
|
|
10
|
+
from cited_cli.utils.errors import CitedAPIError, ExitCode, handle_api_error
|
|
11
|
+
from cited_cli.utils.interactive import can_prompt, prompt_if_missing
|
|
12
|
+
from cited_core.api import endpoints
|
|
13
|
+
from cited_core.api.client import CitedClient
|
|
14
|
+
from cited_core.auth.store import TokenStore
|
|
15
|
+
from cited_core.config.manager import ConfigManager
|
|
16
|
+
|
|
17
|
+
named_audit_app = typer.Typer(name="template", help="Manage audit templates.")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_client(ctx: typer.Context) -> tuple[OutputContext, CitedClient]:
|
|
21
|
+
obj = ctx.obj or {}
|
|
22
|
+
out: OutputContext = obj.get("output", OutputContext())
|
|
23
|
+
cfg: ConfigManager = obj.get("config", ConfigManager())
|
|
24
|
+
profile = obj.get("profile", "default")
|
|
25
|
+
env = cfg.get_environment(profile, obj.get("env_override"))
|
|
26
|
+
api_url = cfg.get_api_url(profile, obj.get("env_override"))
|
|
27
|
+
|
|
28
|
+
store = TokenStore()
|
|
29
|
+
token = store.get_token(env)
|
|
30
|
+
if not token:
|
|
31
|
+
print_error(f"Not logged in to {env}. Run: cited login", out)
|
|
32
|
+
raise typer.Exit(ExitCode.AUTH_ERROR)
|
|
33
|
+
|
|
34
|
+
return out, CitedClient(base_url=api_url, token=token)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@named_audit_app.command("list")
|
|
38
|
+
def template_list(
|
|
39
|
+
ctx: typer.Context,
|
|
40
|
+
business_id: Annotated[
|
|
41
|
+
str | None,
|
|
42
|
+
typer.Option("--business", "-b", help="Filter by business ID"),
|
|
43
|
+
] = None,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""List audit templates."""
|
|
46
|
+
out, client = _get_client(ctx)
|
|
47
|
+
try:
|
|
48
|
+
params = {}
|
|
49
|
+
if business_id:
|
|
50
|
+
params["business_id"] = business_id
|
|
51
|
+
data = client.get(endpoints.NAMED_AUDITS, params=params or None)
|
|
52
|
+
templates = (
|
|
53
|
+
data if isinstance(data, list)
|
|
54
|
+
else data.get("named_audits", data.get("items", []))
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def _human(d: object, console: Console) -> None:
|
|
58
|
+
items = d if isinstance(d, list) else []
|
|
59
|
+
rows = []
|
|
60
|
+
for t in items:
|
|
61
|
+
questions = t.get("questions", [])
|
|
62
|
+
rows.append([
|
|
63
|
+
t.get("id", "")[:8],
|
|
64
|
+
t.get("name", ""),
|
|
65
|
+
t.get("business_name", t.get("business_id", "")),
|
|
66
|
+
str(len(questions)),
|
|
67
|
+
t.get("created_at", "")[:19] if t.get("created_at") else "",
|
|
68
|
+
])
|
|
69
|
+
render_table(
|
|
70
|
+
"Audit Templates",
|
|
71
|
+
["ID", "Name", "Business", "Questions", "Created"],
|
|
72
|
+
rows,
|
|
73
|
+
console,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
print_result(templates, out, human_formatter=_human)
|
|
77
|
+
except CitedAPIError as e:
|
|
78
|
+
handle_api_error(e, out.json_mode)
|
|
79
|
+
finally:
|
|
80
|
+
client.close()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@named_audit_app.command("get")
|
|
84
|
+
def template_get(
|
|
85
|
+
ctx: typer.Context,
|
|
86
|
+
named_audit_id: Annotated[str, typer.Argument(help="Audit template ID")],
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Get audit template details."""
|
|
89
|
+
out, client = _get_client(ctx)
|
|
90
|
+
try:
|
|
91
|
+
path = endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id)
|
|
92
|
+
data = client.get(path)
|
|
93
|
+
|
|
94
|
+
def _human(d: object, console: Console) -> None:
|
|
95
|
+
if not isinstance(d, dict):
|
|
96
|
+
console.print(d)
|
|
97
|
+
return
|
|
98
|
+
questions = d.pop("questions", [])
|
|
99
|
+
render_kv("Audit Template", d, console)
|
|
100
|
+
if questions:
|
|
101
|
+
console.print("\n[bold]Questions:[/bold]")
|
|
102
|
+
for i, q in enumerate(questions, 1):
|
|
103
|
+
text = q.get("question", q) if isinstance(q, dict) else q
|
|
104
|
+
console.print(f" {i}. {text}")
|
|
105
|
+
|
|
106
|
+
print_result(data, out, human_formatter=_human)
|
|
107
|
+
except CitedAPIError as e:
|
|
108
|
+
handle_api_error(e, out.json_mode)
|
|
109
|
+
finally:
|
|
110
|
+
client.close()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@named_audit_app.command("create")
|
|
114
|
+
def template_create(
|
|
115
|
+
ctx: typer.Context,
|
|
116
|
+
name: Annotated[str | None, typer.Option("--name", "-n", help="Template name")] = None,
|
|
117
|
+
business_id: Annotated[str | None, typer.Option("--business", "-b", help="Business ID")] = None,
|
|
118
|
+
description: Annotated[
|
|
119
|
+
str | None, typer.Option("--description", "-d", help="Template description")
|
|
120
|
+
] = None,
|
|
121
|
+
questions: Annotated[
|
|
122
|
+
list[str] | None,
|
|
123
|
+
typer.Option("--question", "-q", help="Question to include (repeatable)"),
|
|
124
|
+
] = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Create a new audit template."""
|
|
127
|
+
out, client = _get_client(ctx)
|
|
128
|
+
name = prompt_if_missing(name, "--name", "Template name", out)
|
|
129
|
+
business_id = prompt_if_missing(business_id, "--business", "Business ID", out)
|
|
130
|
+
if not questions and can_prompt(out):
|
|
131
|
+
out.console.print("\n[bold]Enter questions (one per line, empty line to finish):[/bold]")
|
|
132
|
+
entered: list[str] = []
|
|
133
|
+
while True:
|
|
134
|
+
q = typer.prompt(f" Q{len(entered) + 1}", default="")
|
|
135
|
+
if not q:
|
|
136
|
+
break
|
|
137
|
+
entered.append(q)
|
|
138
|
+
if entered:
|
|
139
|
+
questions = entered
|
|
140
|
+
try:
|
|
141
|
+
payload: dict[str, object] = {
|
|
142
|
+
"name": name,
|
|
143
|
+
"business_id": business_id,
|
|
144
|
+
}
|
|
145
|
+
if description:
|
|
146
|
+
payload["description"] = description
|
|
147
|
+
if questions:
|
|
148
|
+
payload["questions"] = [{"question": q} for q in questions]
|
|
149
|
+
|
|
150
|
+
data = client.post(endpoints.NAMED_AUDITS, json=payload)
|
|
151
|
+
template_id = data.get("id", "")
|
|
152
|
+
print_result(data, out, human_formatter=lambda d, c: render_kv("Template Created", d, c))
|
|
153
|
+
if not out.json_mode and template_id:
|
|
154
|
+
out.console.print(
|
|
155
|
+
f"\nTemplate ID: [bold]{template_id}[/bold]"
|
|
156
|
+
f"\nRun audit: [bold]cited audit start {template_id}"
|
|
157
|
+
f" --business {business_id}[/bold]"
|
|
158
|
+
)
|
|
159
|
+
except CitedAPIError as e:
|
|
160
|
+
handle_api_error(e, out.json_mode)
|
|
161
|
+
finally:
|
|
162
|
+
client.close()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@named_audit_app.command("update")
|
|
166
|
+
def template_update(
|
|
167
|
+
ctx: typer.Context,
|
|
168
|
+
named_audit_id: Annotated[str, typer.Argument(help="Audit template ID to update")],
|
|
169
|
+
name: Annotated[
|
|
170
|
+
str | None, typer.Option("--name", "-n", help="New template name")
|
|
171
|
+
] = None,
|
|
172
|
+
description: Annotated[
|
|
173
|
+
str | None, typer.Option("--description", "-d", help="New description")
|
|
174
|
+
] = None,
|
|
175
|
+
questions: Annotated[
|
|
176
|
+
list[str] | None,
|
|
177
|
+
typer.Option(
|
|
178
|
+
"--question", "-q",
|
|
179
|
+
help="Replacement question (repeatable — replaces all existing)",
|
|
180
|
+
),
|
|
181
|
+
] = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
"""Update an audit template's name, description, or questions.
|
|
184
|
+
|
|
185
|
+
When --question flags are provided, they replace ALL existing questions.
|
|
186
|
+
Omit --question to keep existing questions unchanged.
|
|
187
|
+
"""
|
|
188
|
+
out, client = _get_client(ctx)
|
|
189
|
+
try:
|
|
190
|
+
payload: dict[str, object] = {}
|
|
191
|
+
if name is not None:
|
|
192
|
+
payload["name"] = name
|
|
193
|
+
if description is not None:
|
|
194
|
+
payload["description"] = description
|
|
195
|
+
if questions is not None:
|
|
196
|
+
payload["questions"] = [{"question": q} for q in questions]
|
|
197
|
+
|
|
198
|
+
if not payload:
|
|
199
|
+
print_error("Nothing to update. Provide --name, --description, or --question.", out)
|
|
200
|
+
raise typer.Exit(ExitCode.VALIDATION_ERROR)
|
|
201
|
+
|
|
202
|
+
path = endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id)
|
|
203
|
+
data = client.put(path, json=payload)
|
|
204
|
+
|
|
205
|
+
def _human(d: object, console: Console) -> None:
|
|
206
|
+
if not isinstance(d, dict):
|
|
207
|
+
console.print(d)
|
|
208
|
+
return
|
|
209
|
+
questions_list = d.pop("questions", [])
|
|
210
|
+
render_kv("Template Updated", d, console)
|
|
211
|
+
if questions_list:
|
|
212
|
+
console.print("\n[bold]Questions:[/bold]")
|
|
213
|
+
for i, q in enumerate(questions_list, 1):
|
|
214
|
+
text = q.get("question", q) if isinstance(q, dict) else q
|
|
215
|
+
console.print(f" {i}. {text}")
|
|
216
|
+
|
|
217
|
+
print_result(data, out, human_formatter=_human)
|
|
218
|
+
except CitedAPIError as e:
|
|
219
|
+
handle_api_error(e, out.json_mode)
|
|
220
|
+
finally:
|
|
221
|
+
client.close()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@named_audit_app.command("delete")
|
|
225
|
+
def template_delete(
|
|
226
|
+
ctx: typer.Context,
|
|
227
|
+
named_audit_id: Annotated[str, typer.Argument(help="Audit template ID to delete")],
|
|
228
|
+
yes: Annotated[bool, typer.Option("--yes", "-y", help="Skip confirmation")] = False,
|
|
229
|
+
) -> None:
|
|
230
|
+
"""Delete an audit template."""
|
|
231
|
+
out, client = _get_client(ctx)
|
|
232
|
+
try:
|
|
233
|
+
if not yes:
|
|
234
|
+
confirmed = typer.confirm(f"Delete template {named_audit_id}?")
|
|
235
|
+
if not confirmed:
|
|
236
|
+
raise typer.Abort()
|
|
237
|
+
|
|
238
|
+
path = endpoints.NAMED_AUDIT.format(named_audit_id=named_audit_id)
|
|
239
|
+
client.delete(path)
|
|
240
|
+
print_success(f"Template {named_audit_id} deleted.", out)
|
|
241
|
+
except CitedAPIError as e:
|
|
242
|
+
handle_api_error(e, out.json_mode)
|
|
243
|
+
finally:
|
|
244
|
+
client.close()
|