@lewishowles/cli-style 0.7.0 → 0.8.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.8.1 - 2026-07-15
6
+
7
+ ### Fixed
8
+
9
+ - Fixed the Python and Swift adapters losing ANSI colour: they now set `FORCE_COLOR=1` for the child process when the caller's own stdout is a TTY, unless the caller has already set `FORCE_COLOR` or `NO_COLOR`.
10
+ - Documented that captured Bash output (command substitution) cannot auto-detect its eventual destination and requires an explicit `FORCE_COLOR=1` to opt into colour.
11
+
12
+ ## 0.8.0 - 2026-07-05
13
+
14
+ ### Added
15
+
16
+ - Added Swift adapter with convenience functions for scalar primitives and per-script patterns: `CliStyle.status`, `CliStyle.row`, `CliStyle.span`, `CliStyle.hint`, `CliStyle.divider`, `CliStyle.commandResult`, `CliStyle.auditFinding`, `CliStyle.taskSummary`, `CliStyle.confirmationResult`, and `CliStyle.nextStepBlock`.
17
+ - Added `cli-style adapter-path swift` support for discovering the Swift adapter file path.
18
+
19
+ ### Changed
20
+
21
+ - Extended the README renderer table with a Swift helper column covering all 20 renderers.
22
+
3
23
  ## 0.7.0 - 2026-07-05
4
24
 
5
25
  ### Added
package/README.md CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  Shared terminal output styles for local tools, diagnostics, agent-facing reports, and package CLIs.
4
4
 
5
- Use it directly from JavaScript, or call the same renderers from Bash and Python through the `cli-style` binary.
5
+ Use it directly from JavaScript, or call the same renderers from Bash, Python, and Swift through the `cli-style` binary.
6
6
 
7
7
  ## Requirements
8
8
 
9
9
  - Bun and Node.js 20 or newer for JavaScript projects and local binary builds
10
- - The standalone `cli-style` binary for Bash or Python projects that should not install Bun
10
+ - The standalone `cli-style` binary for Bash, Python, or Swift projects that should not install Bun
11
11
 
12
12
  ## Install
13
13
 
@@ -17,7 +17,7 @@ JavaScript projects can install the package directly:
17
17
  bun add @lewishowles/cli-style
18
18
  ```
19
19
 
20
- Bash and Python projects call the `cli-style` binary through thin adapters. If the project already has Node or Bun tooling, add this package as a dev dependency and use the package binary. Otherwise, install or vendor the standalone binary and put it on `PATH`:
20
+ Bash, Python, and Swift projects call the `cli-style` binary through thin adapters. If the project already has Node or Bun tooling, add this package as a dev dependency and use the package binary. Otherwise, install or vendor the standalone binary and put it on `PATH`:
21
21
 
22
22
  ```bash
23
23
  export PATH="/path/to/cli-style/bin:$PATH"
@@ -41,7 +41,7 @@ The package contains `bin/cli-style` and `adapters/`.
41
41
 
42
42
  ## Choose an integration
43
43
 
44
- All renderers return text. JavaScript imports functions directly. CLI, Bash, and Python integrations pass JSON-compatible data to a named renderer.
44
+ All renderers return text. JavaScript imports functions directly. CLI, Bash, Python, and Swift integrations pass JSON-compatible data to a named renderer.
45
45
 
46
46
  ### JavaScript
47
47
 
@@ -107,6 +107,14 @@ cli_style_next_step_block "Update helpers scripts" "Wrappers are now available"
107
107
 
108
108
  The convenience functions build JSON internally, so callers do not need to escape quotes, backslashes, or paths by hand.
109
109
 
110
+ Direct Bash calls use `cli-style`'s native stdout detection. When a helper's output is captured, the adapter cannot know where the captured text will be written, so set `FORCE_COLOR=1` explicitly when the destination is an interactive terminal:
111
+
112
+ ```bash
113
+ command="$(FORCE_COLOR=1 cli_style_span "npm run docs:readme" info)"
114
+ ```
115
+
116
+ Python and Swift automatically set `FORCE_COLOR=1` for the child render process when their caller's stdout is an interactive terminal. An existing `FORCE_COLOR` or `NO_COLOR` value is preserved. `FORCE_COLOR=0` does not enable colour, while `NO_COLOR`, `TERM=dumb`, and explicit no-colour flags remain authoritative.
117
+
110
118
  Set `CLI_STYLE_BIN` when `cli-style` is not on `PATH`:
111
119
 
112
120
  ```bash
@@ -169,33 +177,75 @@ output = render(
169
177
  print(output)
170
178
  ```
171
179
 
180
+ ### Swift
181
+
182
+ Add the adapter file to your Swift project, then use the `CliStyle` enum for convenience functions or the generic `render` method:
183
+
184
+ ```bash
185
+ cli-style adapter-path swift
186
+ # Returns the path to CliStyle.swift for inclusion in your project
187
+ ```
188
+
189
+ ```swift
190
+ let options = CliStyleOptions(binary: "./bin/cli-style.js", isPlain: true)
191
+
192
+ print(try CliStyle.status(type: "success", label: "Build passed", detail: "184 tests", options: options))
193
+ print(try CliStyle.row(label: "Workspace", value: "/path/to/project", options: options))
194
+ print(try CliStyle.row(label: "Bundle", value: "over budget", result: "failed", options: options))
195
+ let command = try CliStyle.span(value: "npm run docs:readme", tone: "info", options: options)
196
+ print(try CliStyle.hint(message: "Run " + command + " before release", options: options))
197
+ print(try CliStyle.divider(label: "Project setup", options: options))
198
+
199
+ print(try CliStyle.commandResult(result: "success", summary: "Build passed", command: "bun run test:unit", exitCode: 0, duration: "1.2s", detail: "184 tests", options: options))
200
+ print(try CliStyle.taskSummary(result: "partial", task: "Adopt cli-style", summary: "Wrappers added", completed: "Updated adapter", remaining: "Update scripts", options: options))
201
+ print(try CliStyle.confirmationResult(state: "confirmed", action: "Publish release", item: "v0.6.0", detail: "Tag push starts npm publish", options: options))
202
+ print(try CliStyle.nextStepBlock(nextStep: "Update helpers scripts", reason: "Wrappers are now available", command: "scripts/setup.sh --check", alternative: "", options: options))
203
+ ```
204
+
205
+ Convenience functions build the renderer payload internally, so callers pass plain Swift strings without hand-building JSON.
206
+
207
+ Pass `binary:` in `CliStyleOptions` when `cli-style` is not on `PATH`. The generic `CliStyle.render(_:data:options:)` method remains for aggregate patterns or multi-item fields:
208
+
209
+ ```swift
210
+ let output = try CliStyle.render(
211
+ "diagnostic-report",
212
+ data: [
213
+ "title": "Release checks",
214
+ "checks": [["name": "README", "result": "success"]],
215
+ ],
216
+ options: CliStyleOptions(profile: "diagnostic")
217
+ )
218
+
219
+ print(output)
220
+ ```
221
+
172
222
  ## Available renderers
173
223
 
174
224
  Renderer names are stable for `cli-style render`, `cli_style_render`, and Python `render()`. JavaScript uses camel-case function names.
175
225
 
176
- | Renderer | JavaScript | Bash helper | Python helper | Purpose |
177
- | --------------------- | ------------------------------------- | ------------------------------------ | ---------------------------------- | --------------------------------------------------------------- |
178
- | `status` | `status(resultType, detail, options)` | `cli_style_status type label detail` | `status(type, label, detail)` | Result line with symbol, label, and detail. |
179
- | `row` | `row(label, value, options)` | `cli_style_row label value [result]` | `row(label, value, result)` | Aligned label/value row, optionally marked with a result state. |
180
- | `span` | `span(value, tone, options)` | `cli_style_span value [tone]` | `span(value, tone)` | Inline colour or weight for a word, command, file, or value. |
181
- | `hint` | `hint(message, options)` | `cli_style_hint message` | `hint(message)` | Informational hint line. |
182
- | `divider` | `divider(options)` | `cli_style_divider label` | `divider(label)` | Section divider with an optional label. |
183
- | `command-result` | `commandResult(options)` | `cli_style_command_result ...` | `command_result(...)` | Command execution outcome with exit code and duration. |
184
- | `audit-finding` | `auditFinding(options)` | `cli_style_audit_finding ...` | `audit_finding(...)` | Structured audit finding with evidence and recommendation. |
185
- | `task-summary` | `taskSummary(options)` | `cli_style_task_summary ...` | `task_summary(...)` | Task progress with completed and remaining items. |
186
- | `confirmation-result` | `confirmationResult(options)` | `cli_style_confirmation_result ...` | `confirmation_result(...)` | Confirmation outcome with action and item. |
187
- | `next-step-block` | `nextStepBlock(options)` | `cli_style_next_step_block ...` | `next_step_block(...)` | Next-step guidance with command and alternative. |
188
- | `chip` | `chip(label, tone, options)` | Use `cli_style_render` | Use `render("chip", ...)` | Compact labelled state. |
189
- | `panel` | `panel(options)` | Use `cli_style_render` | Use `render("panel", ...)` | Framed content block for grouped output. |
190
- | `table` | `table(options)` | Use `cli_style_render` | Use `render("table", ...)` | Tabular records with narrow-width fallback. |
191
- | `progress-bar` | `progressBar(options)` | Use `cli_style_render` | Use `render("progress-bar", ...)` | Single progress value with numeric text. |
192
- | `bar-chart` | `barChart(options)` | Use `cli_style_render` | Use `render("bar-chart", ...)` | Multi-row bar chart. |
193
- | `step` | `step(label, state, options)` | Use `cli_style_render` | Use `render("step", ...)` | One workflow step. |
194
- | `step-progress` | `stepProgress(options)` | Use `cli_style_render` | Use `render("step-progress", ...)` | Numbered workflow steps. |
195
- | `empty-state` | `emptyState(title, detail, options)` | Use `cli_style_render` | Use `render("empty-state", ...)` | Empty result message. |
196
- | `error-block` | `errorBlock(title, lines, options)` | Use `cli_style_render` | Use `render("error-block", ...)` | Structured error block. |
197
-
198
- Use `render("<renderer>", data, ...)` for aggregate patterns (`diagnostic-report`, `agent-transcript`, `compact-data-table`) or any renderer with multi-item fields.
226
+ | Renderer | JavaScript | Bash helper | Python helper | Swift helper | Purpose |
227
+ | --------------------- | ------------------------------------- | ------------------------------------ | ---------------------------------- | -------------------------------------- | --------------------------------------------------------------- |
228
+ | `status` | `status(resultType, detail, options)` | `cli_style_status type label detail` | `status(type, label, detail)` | `CliStyle.status(type, label, detail)` | Result line with symbol, label, and detail. |
229
+ | `row` | `row(label, value, options)` | `cli_style_row label value [result]` | `row(label, value, result)` | `CliStyle.row(label, value, result)` | Aligned label/value row, optionally marked with a result state. |
230
+ | `span` | `span(value, tone, options)` | `cli_style_span value [tone]` | `span(value, tone)` | `CliStyle.span(value, tone)` | Inline colour or weight for a word, command, file, or value. |
231
+ | `hint` | `hint(message, options)` | `cli_style_hint message` | `hint(message)` | `CliStyle.hint(message)` | Informational hint line. |
232
+ | `divider` | `divider(options)` | `cli_style_divider label` | `divider(label)` | `CliStyle.divider(label)` | Section divider with an optional label. |
233
+ | `command-result` | `commandResult(options)` | `cli_style_command_result ...` | `command_result(...)` | `CliStyle.commandResult(...)` | Command execution outcome with exit code and duration. |
234
+ | `audit-finding` | `auditFinding(options)` | `cli_style_audit_finding ...` | `audit_finding(...)` | `CliStyle.auditFinding(...)` | Structured audit finding with evidence and recommendation. |
235
+ | `task-summary` | `taskSummary(options)` | `cli_style_task_summary ...` | `task_summary(...)` | `CliStyle.taskSummary(...)` | Task progress with completed and remaining items. |
236
+ | `confirmation-result` | `confirmationResult(options)` | `cli_style_confirmation_result ...` | `confirmation_result(...)` | `CliStyle.confirmationResult(...)` | Confirmation outcome with action and item. |
237
+ | `next-step-block` | `nextStepBlock(options)` | `cli_style_next_step_block ...` | `next_step_block(...)` | `CliStyle.nextStepBlock(...)` | Next-step guidance with command and alternative. |
238
+ | `chip` | `chip(label, tone, options)` | Use `cli_style_render` | Use `render("chip", ...)` | Use `CliStyle.render` | Compact labelled state. |
239
+ | `panel` | `panel(options)` | Use `cli_style_render` | Use `render("panel", ...)` | Use `CliStyle.render` | Framed content block for grouped output. |
240
+ | `table` | `table(options)` | Use `cli_style_render` | Use `render("table", ...)` | Use `CliStyle.render` | Tabular records with narrow-width fallback. |
241
+ | `progress-bar` | `progressBar(options)` | Use `cli_style_render` | Use `render("progress-bar", ...)` | Use `CliStyle.render` | Single progress value with numeric text. |
242
+ | `bar-chart` | `barChart(options)` | Use `cli_style_render` | Use `render("bar-chart", ...)` | Use `CliStyle.render` | Multi-row bar chart. |
243
+ | `step` | `step(label, state, options)` | Use `cli_style_render` | Use `render("step", ...)` | Use `CliStyle.render` | One workflow step. |
244
+ | `step-progress` | `stepProgress(options)` | Use `cli_style_render` | Use `render("step-progress", ...)` | Use `CliStyle.render` | Numbered workflow steps. |
245
+ | `empty-state` | `emptyState(title, detail, options)` | Use `cli_style_render` | Use `render("empty-state", ...)` | Use `CliStyle.render` | Empty result message. |
246
+ | `error-block` | `errorBlock(title, lines, options)` | Use `cli_style_render` | Use `render("error-block", ...)` | Use `CliStyle.render` | Structured error block. |
247
+
248
+ Use `render("<renderer>", data, ...)` (Python) or `CliStyle.render(_:data:options:)` (Swift) for aggregate patterns (`diagnostic-report`, `agent-transcript`, `compact-data-table`) or any renderer with multi-item fields.
199
249
 
200
250
  ## Primitive data shapes
201
251
 
@@ -301,6 +351,8 @@ The Bash adapter also exposes scalar convenience wrappers for per-script usage:
301
351
 
302
352
  Pass an empty string for optional positional fields you want to skip. Use `cli_style_render` or `cli_style_render_json` for aggregate reports, tables, transcripts, and multi-item arrays.
303
353
 
354
+ Swift equivalents are available as `CliStyle.commandResult(...)`, `CliStyle.auditFinding(...)`, `CliStyle.taskSummary(...)`, `CliStyle.confirmationResult(...)`, and `CliStyle.nextStepBlock(...)`. Use `CliStyle.render` for aggregate reports, tables, transcripts, and multi-item arrays.
355
+
304
356
  ### Pattern example
305
357
 
306
358
  JavaScript:
@@ -462,7 +514,7 @@ Most renderers accept the same output options:
462
514
  | `unicode` | Whether Unicode symbols should be used. |
463
515
  | `width` | Available output width for renderers that adapt layout. |
464
516
 
465
- Use `createCliStyle()` to resolve these options from `argv`, `env`, `stdout`, CI, TTY, `NO_COLOR`, and terminal capability.
517
+ Use `createCliStyle()` to resolve these options from `argv`, `env`, `stdout`, CI, TTY, `NO_COLOR`, `FORCE_COLOR`, and terminal capability.
466
518
 
467
519
  ### CLI and Bash flags
468
520
 
@@ -488,6 +540,20 @@ Use `createCliStyle()` to resolve these options from `argv`, `env`, `stdout`, CI
488
540
  | `binary` | Path or command name for the `cli-style` binary. |
489
541
  | `extra_args` | Extra CLI arguments to pass through. |
490
542
 
543
+ ### Swift options
544
+
545
+ Swift options are passed via the `CliStyleOptions` struct:
546
+
547
+ | Property | Purpose |
548
+ | ------------- | ------------------------------------------------ |
549
+ | `binary` | Path or command name for the `cli-style` binary. |
550
+ | `profile` | Render with a named profile. |
551
+ | `width` | Override detected output width. |
552
+ | `isPlain` | Disable colour and Unicode decoration. |
553
+ | `isNoColour` | Disable ANSI colour. |
554
+ | `isNoUnicode` | Use ASCII symbols. |
555
+ | `extraArgs` | Extra CLI arguments to pass through. |
556
+
491
557
  ## Profiles
492
558
 
493
559
  | Profile | Behaviour |
@@ -7,6 +7,7 @@ import json
7
7
  import os
8
8
  import shutil
9
9
  import subprocess
10
+ import sys
10
11
  from pathlib import Path
11
12
  from typing import Any, Sequence
12
13
 
@@ -23,6 +24,20 @@ class CliStyleRenderError(CliStyleError):
23
24
  """Raised when cli-style rejects a render request."""
24
25
 
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
+
26
41
  # Render caller data through `cli-style render`.
27
42
  #
28
43
  # @param {str} renderer
@@ -72,6 +87,7 @@ def render(
72
87
  command,
73
88
  capture_output=True,
74
89
  check=False,
90
+ env=resolve_subprocess_environment(),
75
91
  input=json.dumps(data),
76
92
  text=True,
77
93
  )
@@ -0,0 +1,428 @@
1
+ import Foundation
2
+ #if canImport(Darwin)
3
+ import Darwin
4
+ #elseif canImport(Glibc)
5
+ import Glibc
6
+ #endif
7
+
8
+ // Error types for cli-style adapter failures.
9
+ enum CliStyleError: Error {
10
+ case notFound(String)
11
+ case renderFailed(String)
12
+ case invalidInput(String)
13
+ }
14
+
15
+ // Options for controlling cli-style render behaviour.
16
+ struct CliStyleOptions {
17
+ // Binary path or command name.
18
+ var binary: String = "cli-style"
19
+
20
+ // Profile name such as human, agent, or json.
21
+ var profile: String? = nil
22
+
23
+ // Terminal width in characters.
24
+ var width: Int? = nil
25
+
26
+ // Force plain text output.
27
+ var isPlain: Bool = false
28
+
29
+ // Disable colour output.
30
+ var isNoColour: Bool = false
31
+
32
+ // Disable Unicode symbols.
33
+ var isNoUnicode: Bool = false
34
+
35
+ // Additional arguments passed through to the binary.
36
+ var extraArgs: [String] = []
37
+ }
38
+
39
+ // Render caller data through the shared cli-style binary.
40
+ enum CliStyle {
41
+
42
+ /**
43
+ Render a dictionary through `cli-style render`.
44
+
45
+ - parameter renderer: Stable renderer name accepted by `cli-style render`.
46
+ - parameter data: JSON-serialisable renderer input.
47
+ - parameter options: Render options including binary path and output flags.
48
+ - returns: Rendered string output from cli-style.
49
+ - throws: `CliStyleError` if the binary is missing, input is invalid, or rendering fails.
50
+ */
51
+ static func render(
52
+ _ renderer: String,
53
+ data: [String: Any],
54
+ options: CliStyleOptions = .init()
55
+ ) throws -> String {
56
+ if renderer.isEmpty {
57
+ throw CliStyleError.invalidInput("renderer must be a non-empty string")
58
+ }
59
+
60
+ let resolvedBinary = try resolveBinary(options.binary)
61
+
62
+ let jsonData: Data
63
+
64
+ do {
65
+ jsonData = try JSONSerialization.data(withJSONObject: data)
66
+ } catch {
67
+ throw CliStyleError.invalidInput("data is not JSON-serialisable")
68
+ }
69
+
70
+ var arguments = ["render", renderer]
71
+
72
+ if let profile = options.profile {
73
+ arguments += ["--profile", profile]
74
+ }
75
+
76
+ if let width = options.width {
77
+ arguments += ["--width", String(width)]
78
+ }
79
+
80
+ if options.isPlain {
81
+ arguments.append("--plain")
82
+ }
83
+
84
+ if options.isNoColour {
85
+ arguments.append("--no-colour")
86
+ }
87
+
88
+ if options.isNoUnicode {
89
+ arguments.append("--no-unicode")
90
+ }
91
+
92
+ arguments += options.extraArgs
93
+
94
+ let process = Process()
95
+ process.executableURL = URL(fileURLWithPath: resolvedBinary)
96
+ process.arguments = arguments
97
+ process.environment = resolveProcessEnvironment()
98
+
99
+ let stdinPipe = Pipe()
100
+ let stdoutPipe = Pipe()
101
+ let stderrPipe = Pipe()
102
+ process.standardInput = stdinPipe
103
+ process.standardOutput = stdoutPipe
104
+ process.standardError = stderrPipe
105
+
106
+ do {
107
+ try process.run()
108
+ } catch {
109
+ throw CliStyleError.renderFailed("failed to launch cli-style: \(error.localizedDescription)")
110
+ }
111
+
112
+ stdinPipe.fileHandleForWriting.write(jsonData)
113
+ stdinPipe.fileHandleForWriting.closeFile()
114
+
115
+ process.waitUntilExit()
116
+
117
+ let stdoutData = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
118
+ let stderrData = stderrPipe.fileHandleForReading.readDataToEndOfFile()
119
+
120
+ if process.terminationStatus != 0 {
121
+ let stderrText = String(data: stderrData, encoding: .utf8)?
122
+ .trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
123
+ let message = stderrText.isEmpty
124
+ ? "cli-style render failed with exit code \(process.terminationStatus)"
125
+ : stderrText
126
+
127
+ throw CliStyleError.renderFailed(message)
128
+ }
129
+
130
+ let stdoutText = String(data: stdoutData, encoding: .utf8) ?? ""
131
+ return stdoutText.trimmingCharacters(in: .newlines)
132
+ }
133
+
134
+ /**
135
+ * Resolve the child environment for a captured render.
136
+ */
137
+ private static func resolveProcessEnvironment() -> [String: String] {
138
+ var environment = ProcessInfo.processInfo.environment
139
+
140
+ if isatty(STDOUT_FILENO) == 1 &&
141
+ environment["FORCE_COLOR"] == nil &&
142
+ environment["NO_COLOR"] == nil {
143
+ environment["FORCE_COLOR"] = "1"
144
+ }
145
+
146
+ return environment
147
+ }
148
+
149
+ /**
150
+ Render a result status line.
151
+
152
+ - parameter type: Result type such as success, failed, warning, or skipped.
153
+ - parameter label: Status label.
154
+ - parameter detail: Optional detail text.
155
+ - parameter options: Render options.
156
+ - returns: Rendered status line.
157
+ - throws: `CliStyleError` on failure.
158
+ */
159
+ static func status(
160
+ type: String,
161
+ label: String = "",
162
+ detail: String = "",
163
+ options: CliStyleOptions = .init()
164
+ ) throws -> String {
165
+ try render("status", data: [
166
+ "type": type,
167
+ "label": label,
168
+ "detail": detail,
169
+ ], options: options)
170
+ }
171
+
172
+ /**
173
+ Render a labelled value row.
174
+
175
+ - parameter label: Row label.
176
+ - parameter value: Row value.
177
+ - parameter result: Optional result type for a symbol and tone.
178
+ - parameter options: Render options.
179
+ - returns: Rendered row line.
180
+ - throws: `CliStyleError` on failure.
181
+ */
182
+ static func row(
183
+ label: String,
184
+ value: String,
185
+ result: String = "",
186
+ options: CliStyleOptions = .init()
187
+ ) throws -> String {
188
+ try render("row", data: [
189
+ "label": label,
190
+ "value": value,
191
+ "result": result,
192
+ ], options: options)
193
+ }
194
+
195
+ /**
196
+ Render an inline span.
197
+
198
+ - parameter value: Span text.
199
+ - parameter tone: Optional colour tone, defaulting to info.
200
+ - parameter options: Render options.
201
+ - returns: Rendered span string.
202
+ - throws: `CliStyleError` on failure.
203
+ */
204
+ static func span(
205
+ value: String,
206
+ tone: String = "info",
207
+ options: CliStyleOptions = .init()
208
+ ) throws -> String {
209
+ try render("span", data: [
210
+ "value": value,
211
+ "tone": tone,
212
+ ], options: options)
213
+ }
214
+
215
+ /**
216
+ Render a divider.
217
+
218
+ - parameter label: Optional divider label.
219
+ - parameter options: Render options.
220
+ - returns: Rendered divider line.
221
+ - throws: `CliStyleError` on failure.
222
+ */
223
+ static func divider(
224
+ label: String = "",
225
+ options: CliStyleOptions = .init()
226
+ ) throws -> String {
227
+ try render("divider", data: [
228
+ "label": label,
229
+ ], options: options)
230
+ }
231
+
232
+ /**
233
+ Render a hint.
234
+
235
+ - parameter message: Hint message.
236
+ - parameter options: Render options.
237
+ - returns: Rendered hint line.
238
+ - throws: `CliStyleError` on failure.
239
+ */
240
+ static func hint(
241
+ message: String,
242
+ options: CliStyleOptions = .init()
243
+ ) throws -> String {
244
+ try render("hint", data: [
245
+ "message": message,
246
+ ], options: options)
247
+ }
248
+
249
+ /**
250
+ Render a command result pattern.
251
+
252
+ - parameter result: Result type such as success, failed, warning, or skipped.
253
+ - parameter summary: Summary line.
254
+ - parameter command: Optional command text.
255
+ - parameter exitCode: Optional integer exit code.
256
+ - parameter duration: Optional duration text.
257
+ - parameter detail: Optional detail line.
258
+ - parameter options: Render options.
259
+ - returns: Rendered command result block.
260
+ - throws: `CliStyleError` on failure.
261
+ */
262
+ static func commandResult(
263
+ result: String,
264
+ summary: String,
265
+ command: String = "",
266
+ exitCode: Int? = nil,
267
+ duration: String = "",
268
+ detail: String = "",
269
+ options: CliStyleOptions = .init()
270
+ ) throws -> String {
271
+ var data: [String: Any] = [
272
+ "result": result,
273
+ "summary": summary,
274
+ "command": command,
275
+ "duration": duration,
276
+ "details": detail.isEmpty ? [] : [detail],
277
+ ]
278
+
279
+ // JSONSerialization needs NSNull for explicit null values.
280
+ if let exitCode = exitCode {
281
+ data["exitCode"] = exitCode
282
+ } else {
283
+ data["exitCode"] = NSNull()
284
+ }
285
+
286
+ return try render("command-result", data: data, options: options)
287
+ }
288
+
289
+ /**
290
+ Render an audit finding pattern.
291
+
292
+ - parameter result: Result type such as success, failed, warning, or skipped.
293
+ - parameter finding: Finding summary.
294
+ - parameter location: Optional source location.
295
+ - parameter recommendation: Optional recommendation text.
296
+ - parameter evidence: Optional evidence line.
297
+ - parameter reference: Optional reference line.
298
+ - parameter options: Render options.
299
+ - returns: Rendered audit finding block.
300
+ - throws: `CliStyleError` on failure.
301
+ */
302
+ static func auditFinding(
303
+ result: String,
304
+ finding: String,
305
+ location: String = "",
306
+ recommendation: String = "",
307
+ evidence: String = "",
308
+ reference: String = "",
309
+ options: CliStyleOptions = .init()
310
+ ) throws -> String {
311
+ try render("audit-finding", data: [
312
+ "result": result,
313
+ "finding": finding,
314
+ "location": location,
315
+ "recommendation": recommendation,
316
+ "evidence": evidence.isEmpty ? [] : [evidence],
317
+ "references": reference.isEmpty ? [] : [reference],
318
+ ], options: options)
319
+ }
320
+
321
+ /**
322
+ Render a task summary pattern.
323
+
324
+ - parameter result: Result type such as success, failed, warning, or skipped.
325
+ - parameter task: Task label.
326
+ - parameter summary: Optional summary detail.
327
+ - parameter completed: Optional completed item.
328
+ - parameter remaining: Optional remaining item.
329
+ - parameter options: Render options.
330
+ - returns: Rendered task summary block.
331
+ - throws: `CliStyleError` on failure.
332
+ */
333
+ static func taskSummary(
334
+ result: String,
335
+ task: String,
336
+ summary: String = "",
337
+ completed: String = "",
338
+ remaining: String = "",
339
+ options: CliStyleOptions = .init()
340
+ ) throws -> String {
341
+ try render("task-summary", data: [
342
+ "result": result,
343
+ "task": task,
344
+ "summary": summary,
345
+ "completed": completed.isEmpty ? [] : [completed],
346
+ "remaining": remaining.isEmpty ? [] : [remaining],
347
+ ], options: options)
348
+ }
349
+
350
+ /**
351
+ Render a confirmation result pattern.
352
+
353
+ - parameter state: Confirmation state such as confirmed, cancelled, or skipped.
354
+ - parameter action: Action label.
355
+ - parameter item: Optional item label.
356
+ - parameter detail: Optional detail text.
357
+ - parameter options: Render options.
358
+ - returns: Rendered confirmation result block.
359
+ - throws: `CliStyleError` on failure.
360
+ */
361
+ static func confirmationResult(
362
+ state: String,
363
+ action: String,
364
+ item: String = "",
365
+ detail: String = "",
366
+ options: CliStyleOptions = .init()
367
+ ) throws -> String {
368
+ try render("confirmation-result", data: [
369
+ "state": state,
370
+ "action": action,
371
+ "item": item,
372
+ "detail": detail,
373
+ ], options: options)
374
+ }
375
+
376
+ /**
377
+ Render a next-step block pattern.
378
+
379
+ - parameter nextStep: Next step text.
380
+ - parameter reason: Optional reason text.
381
+ - parameter command: Optional command line.
382
+ - parameter alternative: Optional alternative line.
383
+ - parameter options: Render options.
384
+ - returns: Rendered next-step block.
385
+ - throws: `CliStyleError` on failure.
386
+ */
387
+ static func nextStepBlock(
388
+ nextStep: String,
389
+ reason: String = "",
390
+ command: String = "",
391
+ alternative: String = "",
392
+ options: CliStyleOptions = .init()
393
+ ) throws -> String {
394
+ try render("next-step-block", data: [
395
+ "next": nextStep,
396
+ "reason": reason,
397
+ "commands": command.isEmpty ? [] : [command],
398
+ "alternatives": alternative.isEmpty ? [] : [alternative],
399
+ ], options: options)
400
+ }
401
+
402
+ /**
403
+ Resolve a command name or executable path for subprocess use.
404
+
405
+ - parameter binary: Binary path or command name.
406
+ - returns: Resolved executable path.
407
+ - throws: `CliStyleError.notFound` if the binary cannot be found.
408
+ */
409
+ static func resolveBinary(_ binary: String) throws -> String {
410
+ if binary.contains("/") {
411
+ if FileManager.default.isExecutableFile(atPath: binary) {
412
+ return binary
413
+ }
414
+ throw CliStyleError.notFound("cli-style binary not found: \(binary)")
415
+ }
416
+
417
+ let path = ProcessInfo.processInfo.environment["PATH"] ?? ""
418
+
419
+ for directory in path.split(separator: ":") {
420
+ let candidate = "\(directory)/\(binary)"
421
+ if FileManager.default.isExecutableFile(atPath: candidate) {
422
+ return candidate
423
+ }
424
+ }
425
+
426
+ throw CliStyleError.notFound("cli-style binary not found: \(binary)")
427
+ }
428
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewishowles/cli-style",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "Shared terminal output styles for Lewis Howles tools, diagnostics, and package CLIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,12 +23,14 @@ export function resolveTerminalCapabilities(options = {}) {
23
23
  const isTty = stdout.isTTY === true;
24
24
  const isCi = env.CI === "true" || env.CI === "1";
25
25
  const isDumb = env.TERM === "dumb";
26
+ const forceColour = env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "0";
26
27
  const width = resolveWidth(stdout.columns);
27
28
  const usesPlain = argv.includes("--plain");
28
29
 
29
30
  const colour = resolveColour({
30
31
  argv,
31
32
  env,
33
+ forceColour,
32
34
  isDumb,
33
35
  isTty,
34
36
  usesPlain,
@@ -69,7 +71,7 @@ function resolveColour(options) {
69
71
  return false;
70
72
  }
71
73
 
72
- return options.isTty;
74
+ return options.forceColour || options.isTty;
73
75
  }
74
76
 
75
77
  /**
@@ -12,6 +12,10 @@ const adapterPaths = {
12
12
  releasePath: "adapters/python",
13
13
  sourceUrl: new URL("../../adapters/python", import.meta.url),
14
14
  },
15
+ swift: {
16
+ releasePath: "adapters/swift/CliStyle.swift",
17
+ sourceUrl: new URL("../../adapters/swift/CliStyle.swift", import.meta.url),
18
+ },
15
19
  };
16
20
 
17
21
  // Adapter names accepted by the adapter-path command.