Skip to content

Commit f4ecae1

Browse files
committed
Release 2.0.4 with versioned CLI summary output.
Show version on every run, add --version, and print aligned human-readable stats.
1 parent 6296786 commit f4ecae1

9 files changed

Lines changed: 156 additions & 59 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ text-compress ./large-file.txt -s 4000
6767
# Inline text
6868
text-compress -t "hello world" -o output.txt
6969

70+
# Show version
71+
text-compress --version
72+
7073
# Force mode when auto-detect is wrong
7174
text-compress --compress ./looks-compressed.txt
7275
text-compress --decompress ./plain.md # errors if not valid payload
@@ -184,6 +187,12 @@ See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) and [docs/LEARNING.md](docs/LEA
184187

185188
## Changelog
186189

190+
### v2.0.4 — `text-compress` (2026-07-09)
191+
192+
- Show package version on every run (`text-compress v2.0.4`)
193+
- Add `-V` / `--version` flag
194+
- Improve CLI summary: aligned stats, human-readable sizes, clearer split output
195+
187196
### v2.0.3 — `text-compress` (2026-07-09)
188197

189198
- Use printable ASCII `;TCP2;` split headers so part files stay copyable in text editors (legacy binary `TCP\x02` headers still accepted on read)

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "text-compress",
3-
"version": "2.0.3",
3+
"version": "2.0.4",
44
"description": "Brotli-compress text or folders to base64/base85 strings for easy sharing",
55
"type": "module",
66
"main": "./dist/index.js",

src/cli/analytics.ts

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,62 @@
11
/**
22
* @module cli/analytics
33
*
4-
* Post-run statistics printed to stdout after each CLI command.
4+
* Post-run summary printed to stdout after each CLI command.
55
*/
66

7-
/** Print a key/value analytics block below the main status line. */
8-
export function printAnalytics(stats: Record<string, string | number>) {
9-
console.log("\n--- Analytics ---")
10-
for (const [key, value] of Object.entries(stats)) {
11-
console.log(`${key}: ${value}`)
7+
import { getVersion } from "./version.js"
8+
9+
/** Print package version and exit (for `-V` / `--version`). */
10+
export function printVersion(): void {
11+
console.log(`text-compress v${getVersion()}`)
12+
}
13+
14+
/** Format a byte count for human-readable CLI output. */
15+
export function formatBytes(bytes: number): string {
16+
if (bytes < 1024) return `${bytes} B`
17+
const units = ["KB", "MB", "GB", "TB"] as const
18+
let value = bytes / 1024
19+
for (const unit of units) {
20+
if (value < 1024) {
21+
const digits = value < 10 ? 1 : 0
22+
return `${value.toFixed(digits)} ${unit}`
23+
}
24+
value /= 1024
25+
}
26+
return `${value.toFixed(1)} TB`
27+
}
28+
29+
/** Format integers with thousands separators. */
30+
export function formatCount(value: number): string {
31+
return value.toLocaleString("en-US")
32+
}
33+
34+
export interface RunSummary {
35+
title: string
36+
outputPaths: string[]
37+
stats: Record<string, string | number>
38+
}
39+
40+
/** Print a versioned, aligned summary block after a successful run. */
41+
export function printRunSummary(summary: RunSummary): void {
42+
console.log(`text-compress v${getVersion()}`)
43+
console.log()
44+
console.log(summary.title)
45+
46+
if (summary.outputPaths.length > 1) {
47+
for (const path of summary.outputPaths) {
48+
console.log(` ${path}`)
49+
}
50+
} else if (summary.outputPaths.length === 1) {
51+
console.log(` ${summary.outputPaths[0]}`)
52+
}
53+
54+
if (Object.keys(summary.stats).length === 0) return
55+
56+
console.log()
57+
const width = Math.max(...Object.keys(summary.stats).map((key) => key.length))
58+
for (const [key, value] of Object.entries(summary.stats)) {
59+
console.log(` ${key.padEnd(width)} ${value}`)
1260
}
1361
}
1462

@@ -19,7 +67,7 @@ export function splitAnalytics(
1967
): Record<string, string | number> {
2068
if (splitChunkSize === undefined) return {}
2169
return {
22-
"Split size (chars)": splitChunkSize,
23-
"Output parts": outputParts,
70+
"Split limit": `${formatCount(splitChunkSize)} chars / part`,
71+
Parts: outputParts,
2472
}
2573
}

src/cli/commands/compress.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { compress } from "../../api/text.js"
88
import { assertDirectory } from "../../fs/paths.js"
99
import { compressFolderToPath } from "../../streaming/folder.js"
10-
import { printAnalytics, splitAnalytics } from "../analytics.js"
10+
import { formatBytes, formatCount, printRunSummary, splitAnalytics } from "../analytics.js"
1111
import { type Args, readInput, resolveEncoding } from "../args.js"
1212
import { writeCompressedOutput } from "../output.js"
1313
import { resolveOutputPath } from "../paths.js"
@@ -32,22 +32,23 @@ export async function runCompress(args: Args): Promise<void> {
3232
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6
3333
const ratio = originalBytes === 0 ? 0 : compressedBytes / originalBytes
3434

35-
console.log(
36-
outputPaths.length === 1
37-
? `Compressed folder written to ${outputPaths[0]}`
38-
: `Compressed folder written to ${outputPaths.length} files:\n ${outputPaths.join("\n ")}`,
39-
)
40-
printAnalytics({
41-
Encoding: `base${encoding}`,
42-
Files: fileCount,
43-
Directories: dirCount,
44-
"Original size (bytes)": originalBytes,
45-
"Archive size before compression (bytes)": archiveBytes,
46-
[`Compressed size (bytes, base${encoding})`]: compressedBytes,
47-
...splitAnalytics(splitChunkSize, outputPaths.length),
48-
"Size ratio (compressed/original)": ratio.toFixed(3),
49-
"Space saved": `${((1 - ratio) * 100).toFixed(1)}%`,
50-
"Time taken (ms)": elapsedMs.toFixed(3),
35+
printRunSummary({
36+
title:
37+
outputPaths.length === 1
38+
? "Compressed folder"
39+
: `Compressed folder → ${outputPaths.length} files`,
40+
outputPaths,
41+
stats: {
42+
Encoding: `base${encoding}`,
43+
Files: formatCount(fileCount),
44+
Directories: formatCount(dirCount),
45+
"Original size": formatBytes(originalBytes),
46+
"Archive (pre-Brotli)": formatBytes(archiveBytes),
47+
[`Compressed (base${encoding})`]: formatBytes(compressedBytes),
48+
...splitAnalytics(splitChunkSize, outputPaths.length),
49+
"Space saved": `${((1 - ratio) * 100).toFixed(1)}%`,
50+
Time: `${elapsedMs.toFixed(0)} ms`,
51+
},
5152
})
5253
return
5354
}
@@ -68,18 +69,20 @@ export async function runCompress(args: Args): Promise<void> {
6869
const outputBytes = Buffer.byteLength(result, "utf-8")
6970
const ratio = inputBytes === 0 ? 0 : outputBytes / inputBytes
7071

71-
console.log(
72-
outputPaths.length === 1
73-
? `Compressed output written to ${outputPath}`
74-
: `Compressed output written to ${outputPaths.length} files:\n ${outputPaths.join("\n ")}`,
75-
)
76-
printAnalytics({
77-
Encoding: `base${encoding}`,
78-
"Original size (bytes)": inputBytes,
79-
[`Compressed size (bytes, base${encoding})`]: outputBytes,
80-
...splitAnalytics(splitChunkSize, outputPaths.length),
81-
"Size ratio (compressed/original)": ratio.toFixed(3),
82-
"Space saved": `${((1 - ratio) * 100).toFixed(1)}%`,
83-
"Time taken (ms)": elapsedMs.toFixed(3),
72+
printRunSummary({
73+
title:
74+
outputPaths.length === 1
75+
? "Compressed text"
76+
: `Compressed text → ${outputPaths.length} files`,
77+
outputPaths,
78+
stats: {
79+
Encoding: `base${encoding}`,
80+
"Original size": formatBytes(inputBytes),
81+
[`Compressed (base${encoding})`]: formatBytes(outputBytes),
82+
...splitAnalytics(splitChunkSize, outputPaths.length),
83+
"Size ratio": ratio.toFixed(3),
84+
"Space saved": `${((1 - ratio) * 100).toFixed(1)}%`,
85+
Time: `${elapsedMs.toFixed(0)} ms`,
86+
},
8487
})
8588
}

src/cli/commands/decompress.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { writeFileSync } from "node:fs"
88
import { unpackDirectory } from "../../archive/unpack.js"
99
import { decompressPayload, TAG_FOLDER, TAG_TEXT } from "../../payload/tags.js"
1010
import { readSplitInput } from "../../split/parts.js"
11-
import { printAnalytics } from "../analytics.js"
11+
import { formatBytes, formatCount, printRunSummary } from "../analytics.js"
1212
import { type Args, readInput, resolveEncoding, resolveEncodingOptional } from "../args.js"
1313
import { resolveDetectedEncoding } from "../detect.js"
1414
import { resolveOutputPath } from "../paths.js"
@@ -47,15 +47,18 @@ export function runDecompress(args: Args): void {
4747
const outputPath = resolveOutputPath(args, "decompressed.de", ".de")
4848
const { files, dirs, bytes } = unpackDirectory(data, outputPath)
4949

50-
console.log(`Decompressed folder recreated at ${outputPath}`)
51-
printAnalytics({
52-
Encoding: `base${encoding}`,
53-
[`Compressed size (bytes, base${encoding})`]: inputBytes,
54-
...(partPaths ? { "Input parts": partPaths.length } : {}),
55-
"Files restored": files,
56-
"Directories restored": dirs,
57-
"Decompressed size (bytes)": bytes,
58-
"Time taken (ms)": elapsedMs.toFixed(3),
50+
printRunSummary({
51+
title: "Decompressed folder",
52+
outputPaths: [outputPath],
53+
stats: {
54+
Encoding: `base${encoding}`,
55+
[`Compressed (base${encoding})`]: formatBytes(inputBytes),
56+
...(partPaths ? { "Input parts": partPaths.length } : {}),
57+
"Files restored": formatCount(files),
58+
"Directories restored": formatCount(dirs),
59+
"Restored size": formatBytes(bytes),
60+
Time: `${elapsedMs.toFixed(0)} ms`,
61+
},
5962
})
6063
return
6164
}
@@ -70,13 +73,16 @@ export function runDecompress(args: Args): void {
7073
const outputBytes = Buffer.byteLength(result, "utf-8")
7174
const ratio = inputBytes === 0 ? 0 : outputBytes / inputBytes
7275

73-
console.log(`Decompressed output written to ${outputPath}`)
74-
printAnalytics({
75-
Encoding: `base${encoding}`,
76-
[`Compressed size (bytes, base${encoding})`]: inputBytes,
77-
...(partPaths ? { "Input parts": partPaths.length } : {}),
78-
"Decompressed size (bytes)": outputBytes,
79-
"Expansion ratio (decompressed/compressed)": ratio.toFixed(3),
80-
"Time taken (ms)": elapsedMs.toFixed(3),
76+
printRunSummary({
77+
title: "Decompressed text",
78+
outputPaths: [outputPath],
79+
stats: {
80+
Encoding: `base${encoding}`,
81+
[`Compressed (base${encoding})`]: formatBytes(inputBytes),
82+
...(partPaths ? { "Input parts": partPaths.length } : {}),
83+
"Restored size": formatBytes(outputBytes),
84+
"Expansion ratio": ratio.toFixed(3),
85+
Time: `${elapsedMs.toFixed(0)} ms`,
86+
},
8187
})
8288
}

src/cli/main.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { readSplitInput } from "../split/parts.js"
8+
import { printVersion } from "./analytics.js"
89
import { type Args, parseArgs, resolveEncodingOptional, resolveInputArgs } from "./args.js"
910
import { runCompress } from "./commands/compress.js"
1011
import { runDecompress } from "./commands/decompress.js"
@@ -15,6 +16,10 @@ function wantsHelp(argv: string[]): boolean {
1516
return argv.length === 0 || argv.includes("-h") || argv.includes("--help")
1617
}
1718

19+
function wantsVersion(argv: string[]): boolean {
20+
return argv.includes("-V") || argv.includes("--version")
21+
}
22+
1823
/** Strip an optional legacy leading compress/decompress command. */
1924
function normalizeArgv(argv: string[]): { argv: string[]; forcedMode?: Args["mode"] } {
2025
const [first, ...rest] = argv
@@ -52,6 +57,11 @@ export function main() {
5257
process.exit(argv.length === 0 ? 1 : 0)
5358
}
5459

60+
if (wantsVersion(argv)) {
61+
printVersion()
62+
process.exit(0)
63+
}
64+
5565
const legacy = normalizeArgv(argv)
5666
argv = legacy.argv
5767
const args = parseArgs(argv)

src/cli/usage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Options:
4040
preserves text verbatim (e.g. a code block)
4141
-p, --password <string> Password-protect on compress, or unlock on decompress
4242
-h, --help Show this usage guide
43+
-V, --version Show package version
4344
4445
Split output (decompress):
4546
Pass any one sibling file. All files sharing the same basename prefix

src/cli/version.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @module cli/version
3+
*
4+
* Package version read from package.json at runtime.
5+
*/
6+
7+
import { readFileSync } from "node:fs"
8+
import { dirname, join } from "node:path"
9+
import { fileURLToPath } from "node:url"
10+
11+
let cachedVersion: string | undefined
12+
13+
/** Return the published `text-compress` version string. */
14+
export function getVersion(): string {
15+
if (cachedVersion) return cachedVersion
16+
const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..")
17+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf-8")) as { version: string }
18+
cachedVersion = pkg.version
19+
return cachedVersion
20+
}

0 commit comments

Comments
 (0)