@lewishowles/cli-style 0.14.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.15.0 - 2026-09-20
6
+
7
+ ### Added
8
+
9
+ - The Python adapter is published to PyPI as `lewishowles-cli-style`, so Python projects can install it with `pip install lewishowles-cli-style` instead of from a local path or this repository. The name it is imported under is unchanged: `import cli_style`.
10
+ - `bun run check:adapters` now fails when `package.json` and the Python package declare different versions, or when the Python package's copy of the licence differs from the one at the repository root.
11
+ - Releasing a tag whose version does not match `package.json` now fails before anything is published.
12
+
5
13
  ## 0.14.0 - 2026-09-20
6
14
 
7
15
  ### Added
package/README.md CHANGED
@@ -17,16 +17,22 @@ JavaScript projects can install the package directly:
17
17
  bun add @lewishowles/cli-style
18
18
  ```
19
19
 
20
- Python projects can install the adapter from a local path with `uv`:
20
+ Python projects can install the published package from PyPI:
21
+
22
+ ```bash
23
+ pip install lewishowles-cli-style
24
+ ```
25
+
26
+ They can also install the adapter from a local path with `uv`:
21
27
 
22
28
  ```bash
23
29
  uv add --editable ../cli-style/adapters/python
24
30
  ```
25
31
 
26
- They can also install it from this repository:
32
+ Or install it straight from this repository:
27
33
 
28
34
  ```bash
29
- uv add "cli-style @ git+https://github.com/lewishowles/cli-style.git#subdirectory=adapters/python"
35
+ uv add "lewishowles-cli-style @ git+https://github.com/lewishowles/cli-style.git#subdirectory=adapters/python"
30
36
  ```
31
37
 
32
38
  Swift projects can add `adapters/swift` as a local package dependency and use its `CliStyle` product.
@@ -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.
@@ -0,0 +1,29 @@
1
+ # lewishowles-cli-style
2
+
3
+ `lewishowles-cli-style` is a Python adapter for the shared `cli-style` command-line renderer. It lets Python programs use the same terminal output styles as the JavaScript package.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ python -m pip install lewishowles-cli-style
9
+ ```
10
+
11
+ The package supports Python 3.9 and later. The installed distribution is named `lewishowles-cli-style`, and you import it as `cli_style`.
12
+
13
+ Rendering happens in the separate `cli-style` binary, so that needs to be on your `PATH`. The [main readme](https://github.com/lewishowles/cli-style#install) covers the ways to install it.
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from cli_style import render
19
+
20
+ print(render("status", {"type": "success", "label": "Build passed"}))
21
+ ```
22
+
23
+ If the `cli-style` binary is not available, `render()` returns sorted plain `key: value` text by default. Pass `raise_on_missing=True` to raise `CliStyleNotFoundError` instead.
24
+
25
+ ## More information
26
+
27
+ - [Repository](https://github.com/lewishowles/cli-style)
28
+ - [Issue tracker](https://github.com/lewishowles/cli-style/issues)
29
+ - [MIT licence](https://github.com/lewishowles/cli-style/blob/main/LICENSE)
@@ -1,11 +1,28 @@
1
1
  [project]
2
- name = "cli-style"
3
- version = "0.14.0"
2
+ name = "lewishowles-cli-style"
3
+ version = "0.15.0"
4
4
  description = "Python adapter for the shared cli-style binary."
5
5
  authors = [{ name = "Lewis Howles" }]
6
6
  license = { text = "MIT" }
7
+ license-files = ["LICENSE"]
8
+ readme = "README.md"
7
9
  requires-python = ">=3.9"
8
10
  dependencies = []
11
+ classifiers = [
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3 :: Only",
15
+ "Programming Language :: Python :: 3.9",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+
23
+ [project.urls]
24
+ Repository = "https://github.com/lewishowles/cli-style"
25
+ Issues = "https://github.com/lewishowles/cli-style/issues"
9
26
 
10
27
  [build-system]
11
28
  requires = ["hatchling"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewishowles/cli-style",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Shared terminal output styles for Lewis Howles tools, diagnostics, and package CLIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -6,6 +6,8 @@
6
6
  # dist/adapters/bash/cli-style.sh
7
7
  # dist/adapters/python/cli_style/
8
8
  # dist/adapters/python/pyproject.toml
9
+ # dist/adapters/python/README.md
10
+ # dist/adapters/python/LICENSE
9
11
  # dist/adapters/swift/Package.swift
10
12
  # dist/adapters/swift/Sources/
11
13
 
@@ -29,6 +31,6 @@ cp adapters/bash/cli-style.sh dist/adapters/bash/cli-style.sh
29
31
  # Copy the Python package without the __pycache__ folders Python leaves behind.
30
32
  tar -C "adapters/python" \
31
33
  --exclude="__pycache__" \
32
- -cf - cli_style pyproject.toml | tar -C "dist/adapters/python" -xf -
34
+ -cf - cli_style pyproject.toml README.md LICENSE | tar -C "dist/adapters/python" -xf -
33
35
 
34
36
  cp -R adapters/swift/Package.swift adapters/swift/Sources dist/adapters/swift/
@@ -4,16 +4,19 @@ import { fileURLToPath } from "node:url";
4
4
 
5
5
  import { rendererCatalogue } from "../src/catalogue/renderer-catalogue.js";
6
6
 
7
+ const scriptsDirectoryPath = dirname(fileURLToPath(import.meta.url));
8
+ const repositoryRootPath = join(scriptsDirectoryPath, "..");
9
+
7
10
  // Generated adapter modules are kept beside their hand-written language cores.
8
- const pythonOutputPath = join(
9
- dirname(fileURLToPath(import.meta.url)),
10
- "../adapters/python/cli_style/_wrappers.py",
11
- );
11
+ const pythonOutputPath = join(repositoryRootPath, "adapters/python/cli_style/_wrappers.py");
12
+
13
+ const swiftOutputPath = join(repositoryRootPath, "adapters/swift/Sources/CliStyle/Wrappers.swift");
12
14
 
13
- const swiftOutputPath = join(
14
- dirname(fileURLToPath(import.meta.url)),
15
- "../adapters/swift/Sources/CliStyle/Wrappers.swift",
16
- );
15
+ // Package metadata and licence files checked alongside generated adapter modules.
16
+ const packageJsonPath = join(repositoryRootPath, "package.json");
17
+ const pythonProjectPath = join(repositoryRootPath, "adapters/python/pyproject.toml");
18
+ const repositoryLicensePath = join(repositoryRootPath, "LICENSE");
19
+ const pythonLicensePath = join(repositoryRootPath, "adapters/python/LICENSE");
17
20
 
18
21
  // Map catalogue types to the closest useful Python annotations.
19
22
  const pythonTypes = {
@@ -379,10 +382,59 @@ const generatedFiles = [
379
382
  },
380
383
  ];
381
384
 
385
+ /**
386
+ * Read a licence file, treating an absent file as a reportable state rather than a crash.
387
+ *
388
+ * @param {string} path
389
+ * The licence file to read.
390
+ * @returns {Buffer|null}
391
+ * The file contents, or null when the file is not there.
392
+ */
393
+ function readLicenseFile(path) {
394
+ try {
395
+ return readFileSync(path);
396
+ } catch (error) {
397
+ if (error.code === "ENOENT") {
398
+ return null;
399
+ }
400
+
401
+ throw error;
402
+ }
403
+ }
404
+
382
405
  // `--check` reports committed wrappers that differ from the generated source and writes nothing.
383
406
  const checkOnly = process.argv.includes("--check");
384
407
 
385
408
  if (checkOnly) {
409
+ // Both packages are released from one tag, so a version edited in one file and
410
+ // forgotten in the other would publish a Python package claiming a version that
411
+ // was never released.
412
+ const packageManifest = JSON.parse(readFileSync(packageJsonPath, "utf8"));
413
+ const packageVersion = packageManifest.version;
414
+
415
+ // The version is the only field read out of pyproject.toml, and the file is ours, so
416
+ // a pattern match avoids taking on a TOML parser for one line.
417
+ const pythonVersionMatch = readFileSync(pythonProjectPath, "utf8").match(
418
+ /^version\s*=\s*"([^"]+)"/m,
419
+ );
420
+
421
+ const pythonVersion = pythonVersionMatch?.[1];
422
+ const versionsMatch = packageVersion === pythonVersion;
423
+
424
+ // The Python package carries its own copy of the licence, because a wheel built from
425
+ // the published source archive cannot reach the one at the repository root.
426
+ const repositoryLicense = readLicenseFile(repositoryLicensePath);
427
+ const pythonLicense = readLicenseFile(pythonLicensePath);
428
+
429
+ const missingLicensePaths = [
430
+ ["LICENSE", repositoryLicense],
431
+ ["adapters/python/LICENSE", pythonLicense],
432
+ ]
433
+ .filter(([, contents]) => contents === null)
434
+ .map(([path]) => path);
435
+
436
+ const licensesMatch = missingLicensePaths.length === 0 && repositoryLicense.equals(pythonLicense);
437
+
386
438
  // A committed wrapper is stale when its contents differ from the generated source; a missing file counts as stale.
387
439
  const staleFiles = generatedFiles.filter(({ path, source }) => {
388
440
  try {
@@ -396,13 +448,33 @@ if (checkOnly) {
396
448
  }
397
449
  });
398
450
 
451
+ if (!versionsMatch) {
452
+ console.error("Package versions do not match:");
453
+ console.error(`- package.json: ${packageVersion ?? "missing"}`);
454
+ console.error(`- adapters/python/pyproject.toml: ${pythonVersion ?? "missing"}`);
455
+ }
456
+
457
+ if (missingLicensePaths.length > 0) {
458
+ console.error("Licence files are missing:");
459
+
460
+ for (const path of missingLicensePaths) {
461
+ console.error(`- ${path}`);
462
+ }
463
+ } else if (!licensesMatch) {
464
+ console.error("Licence files do not match:");
465
+ console.error("- LICENSE");
466
+ console.error("- adapters/python/LICENSE");
467
+ }
468
+
399
469
  if (staleFiles.length > 0) {
400
470
  console.error("Generated adapters are stale:");
401
471
 
402
472
  for (const { path } of staleFiles) {
403
473
  console.error(`- ${path}`);
404
474
  }
475
+ }
405
476
 
477
+ if (staleFiles.length > 0 || !versionsMatch || !licensesMatch) {
406
478
  process.exitCode = 1;
407
479
  }
408
480
  } else {
@@ -6,6 +6,8 @@
6
6
  # adapters/bash/cli-style.sh
7
7
  # adapters/python/cli_style/
8
8
  # adapters/python/pyproject.toml
9
+ # adapters/python/README.md
10
+ # adapters/python/LICENSE
9
11
  # adapters/swift/Package.swift
10
12
  # adapters/swift/Sources/
11
13