@lewishowles/cli-style 0.12.1 → 0.14.0

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.
@@ -1,384 +0,0 @@
1
- #!/usr/bin/env python3
2
- # Render caller data through the shared cli-style binary.
3
-
4
- from __future__ import annotations
5
-
6
- import json
7
- import os
8
- import shutil
9
- import subprocess
10
- import sys
11
- from pathlib import Path
12
- from typing import Any, Sequence
13
-
14
-
15
- class CliStyleError(RuntimeError):
16
- """Base error for cli-style adapter failures."""
17
-
18
-
19
- class CliStyleNotFoundError(CliStyleError):
20
- """Raised when the cli-style binary cannot be found."""
21
-
22
-
23
- class CliStyleRenderError(CliStyleError):
24
- """Raised when cli-style rejects a render request."""
25
-
26
-
27
- # Resolve the child environment while preserving explicit caller controls.
28
- def resolve_subprocess_environment() -> dict[str, str]:
29
- environment = os.environ.copy()
30
-
31
- if (
32
- sys.stdout.isatty()
33
- and "FORCE_COLOR" not in environment
34
- and "NO_COLOR" not in environment
35
- ):
36
- environment["FORCE_COLOR"] = "1"
37
-
38
- return environment
39
-
40
-
41
- # Render caller data through `cli-style render`.
42
- #
43
- # @param {str} renderer
44
- # Stable renderer name accepted by `cli-style render`.
45
- # @param {dict} data
46
- # JSON-serialisable renderer input.
47
- def render(
48
- renderer: str,
49
- data: dict[str, Any],
50
- *,
51
- binary: str = "cli-style",
52
- profile: str | None = None,
53
- width: int | None = None,
54
- plain: bool = False,
55
- no_colour: bool = False,
56
- no_unicode: bool = False,
57
- extra_args: Sequence[str] | None = None,
58
- ) -> str:
59
- if not isinstance(renderer, str) or renderer == "":
60
- raise ValueError("renderer must be a non-empty string")
61
-
62
- if not isinstance(data, dict):
63
- raise TypeError("data must be a dict")
64
-
65
- resolved_binary = resolve_binary(binary)
66
- command = [resolved_binary, "render", renderer]
67
-
68
- if profile is not None:
69
- command.extend(["--profile", profile])
70
-
71
- if width is not None:
72
- command.extend(["--width", str(width)])
73
-
74
- if plain:
75
- command.append("--plain")
76
-
77
- if no_colour:
78
- command.append("--no-colour")
79
-
80
- if no_unicode:
81
- command.append("--no-unicode")
82
-
83
- if extra_args is not None:
84
- command.extend(extra_args)
85
-
86
- result = subprocess.run(
87
- command,
88
- capture_output=True,
89
- check=False,
90
- env=resolve_subprocess_environment(),
91
- input=json.dumps(data),
92
- text=True,
93
- )
94
-
95
- if result.returncode != 0:
96
- message = result.stderr.strip() or f"cli-style render failed with exit code {result.returncode}"
97
- raise CliStyleRenderError(message)
98
-
99
- return result.stdout.rstrip("\n")
100
-
101
-
102
- # Render a result status line from Python values.
103
- #
104
- # @param {str} type
105
- # Result type such as success, failed, warning, or skipped.
106
- # @param {str} label
107
- # Status label.
108
- # @param {str} detail
109
- # Optional detail text.
110
- def status(
111
- type: str,
112
- label: str = "",
113
- detail: str = "",
114
- **kwargs: Any,
115
- ) -> str:
116
- return render("status", {"type": type, "label": label, "detail": detail}, **kwargs)
117
-
118
-
119
- # Render a labelled value row from Python values.
120
- #
121
- # @param {str} label
122
- # Row label.
123
- # @param {str} value
124
- # Row value.
125
- # @param {str} result
126
- # Optional result type for a symbol and tone.
127
- # @param {int|None} label_width
128
- # Optional minimum label width.
129
- # @param {str|None} label_colour
130
- # Optional label colour token.
131
- # @param {str|None} value_colour
132
- # Optional value colour token.
133
- # @param {str|None} separator
134
- # Optional text between the label and value.
135
- def row(
136
- label: str,
137
- value: str,
138
- result: str = "",
139
- label_width: int | None = None,
140
- label_colour: str | None = None,
141
- value_colour: str | None = None,
142
- separator: str | None = None,
143
- **kwargs: Any,
144
- ) -> str:
145
- data = {
146
- "label": label,
147
- "value": value,
148
- "result": result,
149
- "labelWidth": label_width,
150
- "separator": separator,
151
- }
152
-
153
- if label_colour is not None:
154
- data["labelColour"] = label_colour
155
-
156
- if value_colour is not None:
157
- data["valueColour"] = value_colour
158
-
159
- return render("row", data, **kwargs)
160
-
161
-
162
- # Render an inline span from Python values.
163
- #
164
- # @param {str} value
165
- # Span text.
166
- # @param {str} tone
167
- # Optional colour tone, defaulting to info.
168
- # @param {str|None} weight
169
- # Optional ANSI text weight.
170
- def span(
171
- value: str,
172
- tone: str = "info",
173
- weight: str | None = None,
174
- **kwargs: Any,
175
- ) -> str:
176
- return render("span", {"value": value, "tone": tone, "weight": weight}, **kwargs)
177
-
178
-
179
- # Render a divider from a Python value.
180
- #
181
- # @param {str} label
182
- # Optional divider label.
183
- # @param {int|None} divider_width
184
- # Optional divider width.
185
- # @param {str|None} divider_colour
186
- # Optional divider colour token.
187
- # @param {str|None} label_colour
188
- # Optional label colour token.
189
- def divider(
190
- label: str = "",
191
- divider_width: int | None = None,
192
- divider_colour: str | None = None,
193
- label_colour: str | None = None,
194
- **kwargs: Any,
195
- ) -> str:
196
- data = {
197
- "label": label,
198
- "dividerWidth": divider_width,
199
- }
200
-
201
- if divider_colour is not None:
202
- data["dividerColour"] = divider_colour
203
-
204
- if label_colour is not None:
205
- data["labelColour"] = label_colour
206
-
207
- return render("divider", data, **kwargs)
208
-
209
-
210
- # Render a hint from a Python value.
211
- #
212
- # @param {str} message
213
- # Hint message.
214
- def hint(
215
- message: str,
216
- **kwargs: Any,
217
- ) -> str:
218
- return render("hint", {"message": message}, **kwargs)
219
-
220
-
221
- # Render a command result pattern from Python values.
222
- #
223
- # @param {str} result
224
- # Result type such as success, failed, warning, or skipped.
225
- # @param {str} summary
226
- # Summary line.
227
- # @param {str} command
228
- # Optional command text.
229
- # @param {int|None} exit_code
230
- # Optional integer exit code.
231
- # @param {str} duration
232
- # Optional duration text.
233
- # @param {str} detail
234
- # Optional detail line.
235
- def command_result(
236
- result: str,
237
- summary: str,
238
- command: str = "",
239
- exit_code: int | None = None,
240
- duration: str = "",
241
- detail: str = "",
242
- **kwargs: Any,
243
- ) -> str:
244
- return render("command-result", {
245
- "result": result,
246
- "summary": summary,
247
- "command": command,
248
- "exitCode": exit_code,
249
- "duration": duration,
250
- "details": [detail] if detail else [],
251
- }, **kwargs)
252
-
253
-
254
- # Render an audit finding pattern from Python values.
255
- #
256
- # @param {str} result
257
- # Result type such as success, failed, warning, or skipped.
258
- # @param {str} finding
259
- # Finding summary.
260
- # @param {str} location
261
- # Optional source location.
262
- # @param {str} recommendation
263
- # Optional recommendation text.
264
- # @param {str} evidence
265
- # Optional evidence line.
266
- # @param {str} reference
267
- # Optional reference line.
268
- def audit_finding(
269
- result: str,
270
- finding: str,
271
- location: str = "",
272
- recommendation: str = "",
273
- evidence: str = "",
274
- reference: str = "",
275
- **kwargs: Any,
276
- ) -> str:
277
- return render("audit-finding", {
278
- "result": result,
279
- "finding": finding,
280
- "location": location,
281
- "recommendation": recommendation,
282
- "evidence": [evidence] if evidence else [],
283
- "references": [reference] if reference else [],
284
- }, **kwargs)
285
-
286
-
287
- # Render a task summary pattern from Python values.
288
- #
289
- # @param {str} result
290
- # Result type such as success, failed, warning, or skipped.
291
- # @param {str} task
292
- # Task label.
293
- # @param {str} summary
294
- # Optional summary detail.
295
- # @param {str} completed
296
- # Optional completed item.
297
- # @param {str} remaining
298
- # Optional remaining item.
299
- def task_summary(
300
- result: str,
301
- task: str,
302
- summary: str = "",
303
- completed: str = "",
304
- remaining: str = "",
305
- **kwargs: Any,
306
- ) -> str:
307
- return render("task-summary", {
308
- "result": result,
309
- "task": task,
310
- "summary": summary,
311
- "completed": [completed] if completed else [],
312
- "remaining": [remaining] if remaining else [],
313
- }, **kwargs)
314
-
315
-
316
- # Render a confirmation result pattern from Python values.
317
- #
318
- # @param {str} state
319
- # Confirmation state such as confirmed, cancelled, or skipped.
320
- # @param {str} action
321
- # Action label.
322
- # @param {str} item
323
- # Optional item label.
324
- # @param {str} detail
325
- # Optional detail text.
326
- def confirmation_result(
327
- state: str,
328
- action: str,
329
- item: str = "",
330
- detail: str = "",
331
- **kwargs: Any,
332
- ) -> str:
333
- return render("confirmation-result", {
334
- "state": state,
335
- "action": action,
336
- "item": item,
337
- "detail": detail,
338
- }, **kwargs)
339
-
340
-
341
- # Render a next-step block pattern from Python values.
342
- #
343
- # @param {str} next_step
344
- # Next step text.
345
- # @param {str} reason
346
- # Optional reason text.
347
- # @param {str} command
348
- # Optional command line.
349
- # @param {str} alternative
350
- # Optional alternative line.
351
- def next_step_block(
352
- next_step: str,
353
- reason: str = "",
354
- command: str = "",
355
- alternative: str = "",
356
- **kwargs: Any,
357
- ) -> str:
358
- return render("next-step-block", {
359
- "next": next_step,
360
- "reason": reason,
361
- "commands": [command] if command else [],
362
- "alternatives": [alternative] if alternative else [],
363
- }, **kwargs)
364
-
365
-
366
- # Resolve a command name or executable path for subprocess use.
367
- #
368
- # @param {str} binary
369
- # Binary path or command name.
370
- def resolve_binary(binary: str) -> str:
371
- if "/" in binary:
372
- path = Path(binary)
373
-
374
- if path.is_file() and os.access(path, os.X_OK):
375
- return str(path)
376
-
377
- raise CliStyleNotFoundError(f"cli-style binary not found: {binary}")
378
-
379
- resolved = shutil.which(binary)
380
-
381
- if resolved is None:
382
- raise CliStyleNotFoundError(f"cli-style binary not found: {binary}")
383
-
384
- return resolved