@lewishowles/cli-style 0.13.0 → 0.15.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,428 +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 aligned table from Python values.
163
- #
164
- # @param {list[dict[str, Any]]} columns
165
- # Table column definitions, including per-column overflow settings.
166
- # @param {list[dict[str, Any]]} rows
167
- # Table row values.
168
- # @param {bool|None} unicode
169
- # Optional Unicode border preference.
170
- # @param {int|None} width
171
- # Optional table width.
172
- def table(
173
- columns: list[dict[str, Any]],
174
- rows: list[dict[str, Any]],
175
- *,
176
- unicode: bool | None = None,
177
- width: int | None = None,
178
- **kwargs: Any,
179
- ) -> str:
180
- return render("table", {
181
- "columns": columns,
182
- "rows": rows,
183
- "unicode": unicode,
184
- "width": width,
185
- }, **kwargs)
186
-
187
-
188
- # Render a group of labelled value rows from Python values.
189
- #
190
- # @param {list[dict[str, Any]]} rows
191
- # Labelled values to render together.
192
- # @param {int|None} label_width
193
- # Optional minimum label width.
194
- def row_group(
195
- rows: list[dict[str, Any]],
196
- *,
197
- label_width: int | None = None,
198
- **kwargs: Any,
199
- ) -> str:
200
- return render("row-group", {
201
- "rows": rows,
202
- "labelWidth": label_width,
203
- }, **kwargs)
204
-
205
-
206
- # Render an inline span from Python values.
207
- #
208
- # @param {str} value
209
- # Span text.
210
- # @param {str} tone
211
- # Optional colour tone, defaulting to info.
212
- # @param {str|None} weight
213
- # Optional ANSI text weight.
214
- def span(
215
- value: str,
216
- tone: str = "info",
217
- weight: str | None = None,
218
- **kwargs: Any,
219
- ) -> str:
220
- return render("span", {"value": value, "tone": tone, "weight": weight}, **kwargs)
221
-
222
-
223
- # Render a divider from a Python value.
224
- #
225
- # @param {str} label
226
- # Optional divider label.
227
- # @param {int|None} divider_width
228
- # Optional divider width.
229
- # @param {str|None} divider_colour
230
- # Optional divider colour token.
231
- # @param {str|None} label_colour
232
- # Optional label colour token.
233
- def divider(
234
- label: str = "",
235
- divider_width: int | None = None,
236
- divider_colour: str | None = None,
237
- label_colour: str | None = None,
238
- **kwargs: Any,
239
- ) -> str:
240
- data = {
241
- "label": label,
242
- "dividerWidth": divider_width,
243
- }
244
-
245
- if divider_colour is not None:
246
- data["dividerColour"] = divider_colour
247
-
248
- if label_colour is not None:
249
- data["labelColour"] = label_colour
250
-
251
- return render("divider", data, **kwargs)
252
-
253
-
254
- # Render a hint from a Python value.
255
- #
256
- # @param {str} message
257
- # Hint message.
258
- def hint(
259
- message: str,
260
- **kwargs: Any,
261
- ) -> str:
262
- return render("hint", {"message": message}, **kwargs)
263
-
264
-
265
- # Render a command result pattern from Python values.
266
- #
267
- # @param {str} result
268
- # Result type such as success, failed, warning, or skipped.
269
- # @param {str} summary
270
- # Summary line.
271
- # @param {str} command
272
- # Optional command text.
273
- # @param {int|None} exit_code
274
- # Optional integer exit code.
275
- # @param {str} duration
276
- # Optional duration text.
277
- # @param {str} detail
278
- # Optional detail line.
279
- def command_result(
280
- result: str,
281
- summary: str,
282
- command: str = "",
283
- exit_code: int | None = None,
284
- duration: str = "",
285
- detail: str = "",
286
- **kwargs: Any,
287
- ) -> str:
288
- return render("command-result", {
289
- "result": result,
290
- "summary": summary,
291
- "command": command,
292
- "exitCode": exit_code,
293
- "duration": duration,
294
- "details": [detail] if detail else [],
295
- }, **kwargs)
296
-
297
-
298
- # Render an audit finding pattern from Python values.
299
- #
300
- # @param {str} result
301
- # Result type such as success, failed, warning, or skipped.
302
- # @param {str} finding
303
- # Finding summary.
304
- # @param {str} location
305
- # Optional source location.
306
- # @param {str} recommendation
307
- # Optional recommendation text.
308
- # @param {str} evidence
309
- # Optional evidence line.
310
- # @param {str} reference
311
- # Optional reference line.
312
- def audit_finding(
313
- result: str,
314
- finding: str,
315
- location: str = "",
316
- recommendation: str = "",
317
- evidence: str = "",
318
- reference: str = "",
319
- **kwargs: Any,
320
- ) -> str:
321
- return render("audit-finding", {
322
- "result": result,
323
- "finding": finding,
324
- "location": location,
325
- "recommendation": recommendation,
326
- "evidence": [evidence] if evidence else [],
327
- "references": [reference] if reference else [],
328
- }, **kwargs)
329
-
330
-
331
- # Render a task summary pattern from Python values.
332
- #
333
- # @param {str} result
334
- # Result type such as success, failed, warning, or skipped.
335
- # @param {str} task
336
- # Task label.
337
- # @param {str} summary
338
- # Optional summary detail.
339
- # @param {str} completed
340
- # Optional completed item.
341
- # @param {str} remaining
342
- # Optional remaining item.
343
- def task_summary(
344
- result: str,
345
- task: str,
346
- summary: str = "",
347
- completed: str = "",
348
- remaining: str = "",
349
- **kwargs: Any,
350
- ) -> str:
351
- return render("task-summary", {
352
- "result": result,
353
- "task": task,
354
- "summary": summary,
355
- "completed": [completed] if completed else [],
356
- "remaining": [remaining] if remaining else [],
357
- }, **kwargs)
358
-
359
-
360
- # Render a confirmation result pattern from Python values.
361
- #
362
- # @param {str} state
363
- # Confirmation state such as confirmed, cancelled, or skipped.
364
- # @param {str} action
365
- # Action label.
366
- # @param {str} item
367
- # Optional item label.
368
- # @param {str} detail
369
- # Optional detail text.
370
- def confirmation_result(
371
- state: str,
372
- action: str,
373
- item: str = "",
374
- detail: str = "",
375
- **kwargs: Any,
376
- ) -> str:
377
- return render("confirmation-result", {
378
- "state": state,
379
- "action": action,
380
- "item": item,
381
- "detail": detail,
382
- }, **kwargs)
383
-
384
-
385
- # Render a next-step block pattern from Python values.
386
- #
387
- # @param {str} next_step
388
- # Next step text.
389
- # @param {str} reason
390
- # Optional reason text.
391
- # @param {str} command
392
- # Optional command line.
393
- # @param {str} alternative
394
- # Optional alternative line.
395
- def next_step_block(
396
- next_step: str,
397
- reason: str = "",
398
- command: str = "",
399
- alternative: str = "",
400
- **kwargs: Any,
401
- ) -> str:
402
- return render("next-step-block", {
403
- "next": next_step,
404
- "reason": reason,
405
- "commands": [command] if command else [],
406
- "alternatives": [alternative] if alternative else [],
407
- }, **kwargs)
408
-
409
-
410
- # Resolve a command name or executable path for subprocess use.
411
- #
412
- # @param {str} binary
413
- # Binary path or command name.
414
- def resolve_binary(binary: str) -> str:
415
- if "/" in binary:
416
- path = Path(binary)
417
-
418
- if path.is_file() and os.access(path, os.X_OK):
419
- return str(path)
420
-
421
- raise CliStyleNotFoundError(f"cli-style binary not found: {binary}")
422
-
423
- resolved = shutil.which(binary)
424
-
425
- if resolved is None:
426
- raise CliStyleNotFoundError(f"cli-style binary not found: {binary}")
427
-
428
- return resolved