@lewishowles/cli-style 0.1.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +153 -0
  3. package/adapters/bash/cli-style.sh +50 -0
  4. package/adapters/python/cli_style.py +104 -0
  5. package/bin/cli-style.js +82 -0
  6. package/examples/diagnostic.mjs +30 -0
  7. package/examples/diagnostic.py +38 -0
  8. package/examples/diagnostic.sh +29 -0
  9. package/examples/gallery.mjs +18 -0
  10. package/package.json +42 -0
  11. package/src/capability/resolve-profile.js +69 -0
  12. package/src/capability/terminal.js +105 -0
  13. package/src/cli/gallery-command.js +238 -0
  14. package/src/cli/help.js +35 -0
  15. package/src/cli/render-command.js +283 -0
  16. package/src/create-cli-style.js +53 -0
  17. package/src/formatters/ansi.js +154 -0
  18. package/src/gallery/render-gallery.js +758 -0
  19. package/src/index.js +52 -0
  20. package/src/patterns/agent-transcript.js +180 -0
  21. package/src/patterns/audit-finding.js +107 -0
  22. package/src/patterns/command-result.js +91 -0
  23. package/src/patterns/compact-data-table.js +67 -0
  24. package/src/patterns/confirmation-result.js +88 -0
  25. package/src/patterns/diagnostic-report.js +144 -0
  26. package/src/patterns/helpers.js +106 -0
  27. package/src/patterns/next-step-block.js +118 -0
  28. package/src/patterns/task-summary.js +59 -0
  29. package/src/primitives/bar-chart.js +131 -0
  30. package/src/primitives/chip.js +33 -0
  31. package/src/primitives/divider.js +64 -0
  32. package/src/primitives/feedback.js +83 -0
  33. package/src/primitives/panel.js +100 -0
  34. package/src/primitives/progress-bar.js +121 -0
  35. package/src/primitives/row.js +80 -0
  36. package/src/primitives/status.js +39 -0
  37. package/src/primitives/step-progress.js +184 -0
  38. package/src/primitives/table.js +239 -0
  39. package/src/profiles/profiles.js +24 -0
  40. package/src/theme/colours.js +141 -0
  41. package/src/theme/results.js +98 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lewis Howles
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # CLI style
2
+
3
+ Shared terminal output styles for tools, diagnostics, and package CLIs.
4
+
5
+ ## Requirements
6
+
7
+ - Bun
8
+ - Node.js 20 or newer
9
+
10
+ ## Install
11
+
12
+ JavaScript projects can install the package directly:
13
+
14
+ ```bash
15
+ bun add @lewishowles/cli-style
16
+ ```
17
+
18
+ Bash and Python projects still need the package available somewhere, because their adapters call the `cli-style` binary. If the project already has Node or Bun tooling, add it as a dev dependency. Otherwise, link or vendor this package in a stable tools directory and point the adapters at that copy:
19
+
20
+ ```bash
21
+ export CLI_STYLE_BIN="/path/to/cli-style/bin/cli-style.js"
22
+ export PYTHONPATH="/path/to/cli-style/adapters/python:$PYTHONPATH"
23
+ ```
24
+
25
+ ## JavaScript
26
+
27
+ ```js
28
+ import { createCliStyle, status } from "@lewishowles/cli-style";
29
+
30
+ const ui = createCliStyle({
31
+ profile: "human",
32
+ width: 80,
33
+ });
34
+
35
+ const output = status("success", "184 tests", {
36
+ ...ui.options,
37
+ label: "Build passed",
38
+ });
39
+
40
+ ui.print(output);
41
+ ```
42
+
43
+ Renderer methods return strings. `ui.print()`, `ui.write()`, and CLI commands handle stdout and stderr.
44
+
45
+ ## CLI
46
+
47
+ Use `cli-style render <renderer>` when a script needs rendered text from JSON input.
48
+
49
+ ```bash
50
+ cli-style render status --profile diagnostic <<'JSON'
51
+ {
52
+ "type": "success",
53
+ "label": "Build passed",
54
+ "detail": "184 tests"
55
+ }
56
+ JSON
57
+ ```
58
+
59
+ ```bash
60
+ cli-style render task-summary --plain <<'JSON'
61
+ {
62
+ "title": "Task summary",
63
+ "completed": ["Added render command"],
64
+ "remaining": ["Add adapters"]
65
+ }
66
+ JSON
67
+ ```
68
+
69
+ `render` supports `--profile`, `--plain`, `--no-colour`, `--no-color`, `--no-unicode`, and `--width`. It rejects `--json`; JSON profile behaviour is for library calls and machine-readable commands.
70
+
71
+ ## Bash
72
+
73
+ ```bash
74
+ source /path/to/node_modules/@lewishowles/cli-style/adapters/bash/cli-style.sh
75
+
76
+ cli_style_render status --profile diagnostic <<'JSON'
77
+ {
78
+ "type": "success",
79
+ "label": "Build passed",
80
+ "detail": "184 tests"
81
+ }
82
+ JSON
83
+ ```
84
+
85
+ Set `CLI_STYLE_BIN` when `cli-style` is not on `PATH`:
86
+
87
+ ```bash
88
+ CLI_STYLE_BIN="/path/to/cli-style" cli_style_render status --plain <<'JSON'
89
+ {
90
+ "type": "success",
91
+ "label": "Build passed"
92
+ }
93
+ JSON
94
+ ```
95
+
96
+ ## Python
97
+
98
+ Add `/path/to/node_modules/@lewishowles/cli-style/adapters/python` to `PYTHONPATH`, then import the helper:
99
+
100
+ ```python
101
+ from cli_style import render
102
+
103
+ output = render(
104
+ "status",
105
+ {
106
+ "type": "success",
107
+ "label": "Build passed",
108
+ "detail": "184 tests",
109
+ },
110
+ profile="diagnostic",
111
+ )
112
+
113
+ print(output)
114
+ ```
115
+
116
+ Pass `binary=` when `cli-style` is not on `PATH`:
117
+
118
+ ```python
119
+ output = render(
120
+ "status",
121
+ {
122
+ "type": "success",
123
+ "label": "Build passed",
124
+ },
125
+ binary="/path/to/cli-style",
126
+ plain=True,
127
+ )
128
+ ```
129
+
130
+ ## Profiles
131
+
132
+ | Profile | Behaviour |
133
+ | ------------ | --------------------------------------------------------------------------------- |
134
+ | `human` | Colour and Unicode when supported; compact but visually pleasant. |
135
+ | `agent` | Markdown-like, structured, easy to quote or parse. |
136
+ | `diagnostic` | Optimised for check summaries, findings, skipped checks, and next actions. |
137
+ | `ci` | Stable, grep-friendly, no decorative panels; colour off unless explicitly forced. |
138
+ | `plain` | No ANSI, no Unicode dependency, simple text layout. |
139
+ | `json` | Machine-readable only; library calls return structured data unchanged. |
140
+
141
+ ## Gallery
142
+
143
+ ```bash
144
+ cli-style gallery
145
+ cli-style gallery no-colour
146
+ cli-style gallery --section patterns
147
+ cli-style gallery --fixture audit-finding
148
+ cli-style gallery --profile agent
149
+ cli-style gallery --width 64
150
+ cli-style gallery --variants
151
+ ```
152
+
153
+ Use the gallery to review current renderer output. Use `--interactive` to select a section or fixture with `fzf` when it is installed.
@@ -0,0 +1,50 @@
1
+ # Sourceable Bash helpers for rendering through the cli-style binary.
2
+
3
+ # Return the configured cli-style binary.
4
+ #
5
+ # @returns {string}
6
+ # Binary path or command name.
7
+ cli_style_binary() {
8
+ printf '%s\n' "${CLI_STYLE_BIN:-cli-style}"
9
+ }
10
+
11
+ # Returns 0 when a binary can be executed.
12
+ #
13
+ # @param {string} binary
14
+ # Binary path or command name.
15
+ cli_style_has_binary() {
16
+ local binary="$1"
17
+
18
+ if [[ "$binary" == */* ]]; then
19
+ [[ -x "$binary" ]]
20
+ return
21
+ fi
22
+
23
+ command -v "$binary" >/dev/null 2>&1
24
+ }
25
+
26
+ # Render stdin JSON through the shared cli-style command.
27
+ #
28
+ # @param {string} renderer
29
+ # Stable renderer name accepted by `cli-style render`.
30
+ # @param {string} ...
31
+ # Render flags passed through to `cli-style render`.
32
+ cli_style_render() {
33
+ local renderer="${1:-}"
34
+ local binary
35
+
36
+ if [[ "$renderer" == "" ]]; then
37
+ printf 'Missing renderer\n' >&2
38
+ return 2
39
+ fi
40
+
41
+ shift
42
+ binary="$(cli_style_binary)"
43
+
44
+ if ! cli_style_has_binary "$binary"; then
45
+ printf 'cli-style binary not found: %s\n' "$binary" >&2
46
+ return 127
47
+ fi
48
+
49
+ "$binary" render "$renderer" "$@"
50
+ }
@@ -0,0 +1,104 @@
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
+ from pathlib import Path
11
+ from typing import Any, Sequence
12
+
13
+
14
+ class CliStyleError(RuntimeError):
15
+ """Base error for cli-style adapter failures."""
16
+
17
+
18
+ class CliStyleNotFoundError(CliStyleError):
19
+ """Raised when the cli-style binary cannot be found."""
20
+
21
+
22
+ class CliStyleRenderError(CliStyleError):
23
+ """Raised when cli-style rejects a render request."""
24
+
25
+
26
+ # Render caller data through `cli-style render`.
27
+ #
28
+ # @param {str} renderer
29
+ # Stable renderer name accepted by `cli-style render`.
30
+ # @param {dict} data
31
+ # JSON-serialisable renderer input.
32
+ def render(
33
+ renderer: str,
34
+ data: dict[str, Any],
35
+ *,
36
+ binary: str = "cli-style",
37
+ profile: str | None = None,
38
+ width: int | None = None,
39
+ plain: bool = False,
40
+ no_colour: bool = False,
41
+ no_unicode: bool = False,
42
+ extra_args: Sequence[str] | None = None,
43
+ ) -> str:
44
+ if not isinstance(renderer, str) or renderer == "":
45
+ raise ValueError("renderer must be a non-empty string")
46
+
47
+ if not isinstance(data, dict):
48
+ raise TypeError("data must be a dict")
49
+
50
+ resolved_binary = resolve_binary(binary)
51
+ command = [resolved_binary, "render", renderer]
52
+
53
+ if profile is not None:
54
+ command.extend(["--profile", profile])
55
+
56
+ if width is not None:
57
+ command.extend(["--width", str(width)])
58
+
59
+ if plain:
60
+ command.append("--plain")
61
+
62
+ if no_colour:
63
+ command.append("--no-colour")
64
+
65
+ if no_unicode:
66
+ command.append("--no-unicode")
67
+
68
+ if extra_args is not None:
69
+ command.extend(extra_args)
70
+
71
+ result = subprocess.run(
72
+ command,
73
+ capture_output=True,
74
+ check=False,
75
+ input=json.dumps(data),
76
+ text=True,
77
+ )
78
+
79
+ if result.returncode != 0:
80
+ message = result.stderr.strip() or f"cli-style render failed with exit code {result.returncode}"
81
+ raise CliStyleRenderError(message)
82
+
83
+ return result.stdout.rstrip("\n")
84
+
85
+
86
+ # Resolve a command name or executable path for subprocess use.
87
+ #
88
+ # @param {str} binary
89
+ # Binary path or command name.
90
+ def resolve_binary(binary: str) -> str:
91
+ if "/" in binary:
92
+ path = Path(binary)
93
+
94
+ if path.is_file() and os.access(path, os.X_OK):
95
+ return str(path)
96
+
97
+ raise CliStyleNotFoundError(f"cli-style binary not found: {binary}")
98
+
99
+ resolved = shutil.which(binary)
100
+
101
+ if resolved is None:
102
+ raise CliStyleNotFoundError(f"cli-style binary not found: {binary}")
103
+
104
+ return resolved
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Buffer } from "node:buffer";
4
+
5
+ import { createCliStyle, renderGallery, renderHelp } from "../src/index.js";
6
+ import {
7
+ parseGalleryRequest,
8
+ selectInteractiveGalleryRequest,
9
+ } from "../src/cli/gallery-command.js";
10
+ import {
11
+ parseRenderRequest,
12
+ renderJsonInput,
13
+ } from "../src/cli/render-command.js";
14
+
15
+ // Command-line arguments passed to the package binary.
16
+ const args = process.argv.slice(2);
17
+
18
+ // First positional command requested by the caller.
19
+ const command = args[0];
20
+
21
+ // Renderer configured from the current terminal.
22
+ const ui = createCliStyle({
23
+ argv: args,
24
+ env: process.env,
25
+ stdout: process.stdout,
26
+ });
27
+
28
+ /**
29
+ * Print text with a trailing newline.
30
+ *
31
+ * @param {string} value
32
+ * Text to print.
33
+ * @returns {void}
34
+ */
35
+ function print(value) {
36
+ console.log(value);
37
+ }
38
+
39
+ if (command === undefined || command === "--help" || command === "-h") {
40
+ print(renderHelp());
41
+ } else if (command === "gallery") {
42
+ try {
43
+ const request = parseGalleryRequest(args.slice(1));
44
+ const selectedRequest = selectInteractiveGalleryRequest(request);
45
+
46
+ print(renderGallery(ui.options, selectedRequest));
47
+ } catch (error) {
48
+ console.error(error.message);
49
+ process.exitCode = 1;
50
+ }
51
+ } else if (command === "render") {
52
+ try {
53
+ const request = parseRenderRequest(args.slice(1));
54
+ const input = await readStdin();
55
+
56
+ print(renderJsonInput(input, request, ui.options));
57
+ } catch (error) {
58
+ console.error(error.message);
59
+ process.exitCode = 1;
60
+ }
61
+ } else {
62
+ console.error(`Unknown command: ${command}`);
63
+ console.error("");
64
+ console.error(renderHelp());
65
+ process.exitCode = 1;
66
+ }
67
+
68
+ /**
69
+ * Read all stdin data for custom render input.
70
+ *
71
+ * @returns {Promise<string>}
72
+ * Stdin text.
73
+ */
74
+ async function readStdin() {
75
+ const chunks = [];
76
+
77
+ for await (const chunk of process.stdin) {
78
+ chunks.push(Buffer.from(chunk));
79
+ }
80
+
81
+ return Buffer.concat(chunks).toString("utf8");
82
+ }
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { diagnosticReport } from "../src/index.js";
4
+
5
+ // Example data shaped like a local diagnostic command summary.
6
+ const report = {
7
+ checks: [
8
+ {
9
+ detail: "30 tests",
10
+ name: "Unit tests",
11
+ result: "success",
12
+ },
13
+ ],
14
+ findings: [],
15
+ nextActions: ["Build Bash and Python adapters"],
16
+ skippedChecks: [
17
+ {
18
+ name: "Full suite",
19
+ reason: "Not needed for adapter smoke test",
20
+ },
21
+ ],
22
+ title: "Diagnostics",
23
+ };
24
+
25
+ console.log(diagnosticReport(report, {
26
+ colour: false,
27
+ profile: "diagnostic",
28
+ unicode: true,
29
+ width: 80,
30
+ }));
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env python3
2
+ # Render a diagnostic report through the Python adapter.
3
+
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ SCRIPT_DIR = Path(__file__).resolve().parent
8
+
9
+ sys.path.insert(0, str(SCRIPT_DIR.parent / "adapters" / "python"))
10
+
11
+ from cli_style import render # noqa: E402
12
+
13
+
14
+ report = {
15
+ "title": "Diagnostics",
16
+ "checks": [
17
+ {
18
+ "result": "success",
19
+ "name": "Unit tests",
20
+ "detail": "30 tests",
21
+ },
22
+ ],
23
+ "findings": [],
24
+ "skippedChecks": [
25
+ {
26
+ "name": "Full suite",
27
+ "reason": "Not needed for adapter smoke test",
28
+ },
29
+ ],
30
+ "nextActions": ["Build Bash and Python adapters"],
31
+ }
32
+
33
+ print(render(
34
+ "diagnostic-report",
35
+ report,
36
+ binary=str(SCRIPT_DIR.parent / "bin" / "cli-style.js"),
37
+ profile="diagnostic",
38
+ ))
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bash
2
+ # Render a diagnostic report through the Bash adapter.
3
+
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+
8
+ source "$SCRIPT_DIR/../adapters/bash/cli-style.sh"
9
+
10
+ CLI_STYLE_BIN="${CLI_STYLE_BIN:-"$SCRIPT_DIR/../bin/cli-style.js"}" cli_style_render diagnostic-report --profile diagnostic <<'JSON'
11
+ {
12
+ "title": "Diagnostics",
13
+ "checks": [
14
+ {
15
+ "result": "success",
16
+ "name": "Unit tests",
17
+ "detail": "30 tests"
18
+ }
19
+ ],
20
+ "findings": [],
21
+ "skippedChecks": [
22
+ {
23
+ "name": "Full suite",
24
+ "reason": "Not needed for adapter smoke test"
25
+ }
26
+ ],
27
+ "nextActions": ["Build Bash and Python adapters"]
28
+ }
29
+ JSON
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { renderGallery } from "../src/index.js";
4
+
5
+ // Stable options for reviewing gallery output outside the current terminal.
6
+ const reviewOptions = {
7
+ colour: false,
8
+ profile: "plain",
9
+ unicode: false,
10
+ width: 64,
11
+ };
12
+
13
+ // Variants request exercises every gallery capability variant with fixed sample data.
14
+ const reviewRequest = {
15
+ variants: true,
16
+ };
17
+
18
+ console.log(renderGallery(reviewOptions, reviewRequest));
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@lewishowles/cli-style",
3
+ "version": "0.1.0",
4
+ "description": "Shared terminal output styles for Lewis Howles tools, diagnostics, and package CLIs.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/lewishowles/cli-style.git"
9
+ },
10
+ "bin": {
11
+ "cli-style": "./bin/cli-style.js"
12
+ },
13
+ "files": [
14
+ "adapters",
15
+ "bin",
16
+ "examples",
17
+ "src",
18
+ "README.md"
19
+ ],
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": "./src/index.js"
24
+ },
25
+ "publishConfig": {
26
+ "registry": "https://npm.pkg.github.com/"
27
+ },
28
+ "scripts": {
29
+ "check": "vp check --fix",
30
+ "cli-style:gallery": "bun ./bin/cli-style.js gallery",
31
+ "test:unit": "bun test"
32
+ },
33
+ "devDependencies": {
34
+ "@stylistic/eslint-plugin": "^5.10.0",
35
+ "vite": "npm:@voidzero-dev/vite-plus-core@0.2.1",
36
+ "vite-plus": "0.2.1"
37
+ },
38
+ "engines": {
39
+ "node": ">=20"
40
+ },
41
+ "packageManager": "bun@1.3.14"
42
+ }
@@ -0,0 +1,69 @@
1
+ import { isProfile, profiles } from "../profiles/profiles.js";
2
+
3
+ /**
4
+ * Resolve the active output profile from explicit options, flags, and environment.
5
+ *
6
+ * @param {object} options
7
+ * Profile resolution inputs.
8
+ * @param {string} options.profile
9
+ * Explicit profile name.
10
+ * @param {string[]} options.argv
11
+ * CLI arguments to inspect.
12
+ * @param {object} options.env
13
+ * Environment values to inspect.
14
+ * @returns {string}
15
+ * Resolved profile name.
16
+ */
17
+ export function resolveProfile(options = {}) {
18
+ const argv = options.argv ?? [];
19
+ const env = options.env ?? {};
20
+
21
+ if (argv.includes("--json")) {
22
+ return profiles.JSON;
23
+ }
24
+
25
+ if (argv.includes("--plain")) {
26
+ return profiles.PLAIN;
27
+ }
28
+
29
+ const flagProfile = readProfileFlag(argv);
30
+
31
+ if (flagProfile !== null) {
32
+ return isProfile(flagProfile) ? flagProfile : profiles.HUMAN;
33
+ }
34
+
35
+ if (isProfile(options.profile)) {
36
+ return options.profile;
37
+ }
38
+
39
+ if (env.CI === "true" || env.CI === "1") {
40
+ return profiles.CI;
41
+ }
42
+
43
+ return profiles.HUMAN;
44
+ }
45
+
46
+ /**
47
+ * Read `--profile value` or `--profile=value` from CLI args.
48
+ *
49
+ * @param {string[]} argv
50
+ * CLI arguments to inspect.
51
+ * @returns {string|null}
52
+ * Profile flag value when present.
53
+ */
54
+ function readProfileFlag(argv) {
55
+ const profilePrefix = "--profile=";
56
+ const inlineProfile = argv.find((arg) => arg.startsWith(profilePrefix));
57
+
58
+ if (inlineProfile !== undefined) {
59
+ return inlineProfile.slice(profilePrefix.length);
60
+ }
61
+
62
+ const profileFlagIndex = argv.indexOf("--profile");
63
+
64
+ if (profileFlagIndex === -1) {
65
+ return null;
66
+ }
67
+
68
+ return argv[profileFlagIndex + 1] ?? "";
69
+ }