Skip to content

Commit be95850

Browse files
authored
Feature: Consistent CLI output (#1597)
1 parent 641717d commit be95850

35 files changed

+1357
-6354
lines changed

libs/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
This directory contains internal libraries that share functionality across the packages. They are not npm workspaces, their inclusion in the published packages is due to:
2+
3+
- TSconfig [paths](../tsconfig.base.json).
4+
- Vitest [aliases](../vitest.config.base.ts) - which are built from the TSconfig paths.
5+
- Using a bundler to bundle the internal library code into the consuming package.
6+
7+
```json
8+
{
9+
"scripts": {
10+
"build": "tsup --config ../../tsup.config.ts",
11+
"dev": "tsup --watch ./src --watch '../../libs'"
12+
}
13+
}
14+
```

libs/output/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Libs/Output
2+
3+
A utility library providing consistent terminal output formatting with support for colors, status icons, and branded prefixes.
4+
5+
## Usage
6+
7+
Instantiate the Output class with the cli name and optionally a version.
8+
9+
### Constructor
10+
11+
```typescript
12+
import { Output } from '@libs/output';
13+
14+
const output = new Output('MyApp', '1.0.0');
15+
```
16+
17+
### Methods
18+
19+
#### Message Output
20+
21+
- `success({ title: string, body?: string[], withPrefix?: boolean })` - Displays a success message in green
22+
- `error({ title: string, body?: string[], link?: string, withPrefix?: boolean })` - Displays an error message in red with optional help link
23+
- `warning({ title: string, body?: string[],link?: string, withPrefix?: boolean })` - Displays a warning message in yellow
24+
- `log({ title: string, body?: string[], color?: Colors, withPrefix?: boolean })` - Displays a message with optional color
25+
26+
#### Formatting
27+
28+
- `addHorizontalLine(color: Colors)` - Adds a horizontal separator line in specified color
29+
- `addNewLine()` - Adds a line break
30+
- `bulletList(list: string[])` - Formats an array of strings as bullet points
31+
- `statusList(status: TaskStatus, list: string[])` - Formats an array of strings with status icons (e.g. ✔️ for success)
32+
33+
#### Types
34+
35+
```typescript
36+
type Colors = 'red' | 'cyan' | 'green' | 'yellow' | 'gray';
37+
type TaskStatus = 'success' | 'failure' | 'skipped';
38+
```
39+
40+
### Example Usage
41+
42+
```typescript
43+
const output = new Output('MyApp', '1.0.0');
44+
45+
output.success({
46+
title: 'Build completed',
47+
body: output.statusList('success', ['dist folder created', 'types generated']),
48+
});
49+
50+
output.error({
51+
title: 'Build failed',
52+
body: ['Unable to resolve dependencies'],
53+
link: 'https://example.com/troubleshooting',
54+
});
55+
```

libs/output/src/index.mts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import chalk from 'chalk';
2+
import { EOL } from 'os';
3+
4+
type Colors = 'red' | 'cyan' | 'green' | 'yellow' | 'gray';
5+
6+
type TaskStatus = 'success' | 'failure' | 'skipped';
7+
8+
function isCI() {
9+
return (
10+
(process.env.CI && process.env.CI !== 'false') || // Drone CI plus others
11+
process.env.GITHUB_ACTIONS === 'true' // GitHub Actions
12+
);
13+
}
14+
15+
if (isCI()) {
16+
// Disable coloring when running in CI environments.
17+
chalk.level = 0;
18+
}
19+
20+
export class Output {
21+
private appName: string;
22+
private appVersion?: string;
23+
24+
constructor(name: string, version?: string) {
25+
this.appName = name;
26+
this.appVersion = version;
27+
}
28+
29+
private write(str: string) {
30+
process.stdout.write(str);
31+
}
32+
33+
private get separator() {
34+
let separator = '';
35+
for (let i = 0; i < process.stdout.columns - 1; i++) {
36+
separator += '\u2014';
37+
}
38+
return separator;
39+
}
40+
41+
addHorizontalLine(color: Colors) {
42+
const separator = chalk.dim[color](this.separator);
43+
this.write(`${separator}${EOL}`);
44+
}
45+
46+
addNewLine() {
47+
this.write(EOL);
48+
}
49+
50+
private writeTitle(color: Colors, title: string, withPrefix = true) {
51+
if (withPrefix) {
52+
this.write(`${this.addPrefix(color, title)}${EOL}`);
53+
} else {
54+
this.write(`${title}${EOL}`);
55+
}
56+
}
57+
58+
private getStatusIcon(taskStatus: TaskStatus) {
59+
switch (taskStatus) {
60+
case 'success':
61+
return chalk.green('✓');
62+
case 'failure':
63+
return chalk.red('⨯');
64+
case 'skipped':
65+
return chalk.yellow('−');
66+
}
67+
}
68+
69+
private addPrefix(color: Colors, text: string) {
70+
const namePrefix = chalk.reset.inverse.bold[color](` ${this.appName} `);
71+
if (!this.appVersion) {
72+
return `${namePrefix} ${text}`;
73+
}
74+
const nameAndVersionPrefix = chalk.reset.inverse.bold[color](` ${this.appName}@${this.appVersion} `);
75+
return `${nameAndVersionPrefix} ${text}`;
76+
}
77+
78+
private writeBody(body?: string[]) {
79+
if (!body) {
80+
return;
81+
}
82+
this.addNewLine();
83+
body.forEach((line) => {
84+
this.write(`${line}${EOL}`);
85+
});
86+
}
87+
88+
error({
89+
title,
90+
body,
91+
link,
92+
withPrefix = true,
93+
}: {
94+
title: string;
95+
body?: string[];
96+
link?: string;
97+
withPrefix?: boolean;
98+
}) {
99+
this.addNewLine();
100+
this.writeTitle('red', chalk.red.bold(title), withPrefix);
101+
this.writeBody(body);
102+
103+
if (link) {
104+
this.addNewLine();
105+
this.write(`${chalk.gray('Learn more about this error: ')}
106+
${chalk.cyan(link)}`);
107+
}
108+
this.addNewLine();
109+
}
110+
111+
warning({
112+
title,
113+
body,
114+
link,
115+
withPrefix = true,
116+
}: {
117+
title: string;
118+
body?: string[];
119+
link?: string;
120+
withPrefix?: boolean;
121+
}) {
122+
this.addNewLine();
123+
this.writeTitle('yellow', chalk.yellow.bold(title), withPrefix);
124+
this.writeBody(body);
125+
126+
if (link) {
127+
this.addNewLine();
128+
this.write(`${chalk.gray('Learn more about this warning: ')}
129+
${this.formatUrl(link)}`);
130+
}
131+
this.addNewLine();
132+
}
133+
134+
success({ title, body, withPrefix = true }: { title: string; body?: string[]; withPrefix?: boolean }) {
135+
this.addNewLine();
136+
this.writeTitle('green', chalk.green.bold(title), withPrefix);
137+
this.writeBody(body);
138+
this.addNewLine();
139+
}
140+
141+
log({ title, body, withPrefix = true }: { title: string; body?: string[]; withPrefix?: boolean }) {
142+
this.addNewLine();
143+
this.writeTitle('cyan', chalk.cyan.bold(title), withPrefix);
144+
this.writeBody(body);
145+
this.addNewLine();
146+
}
147+
148+
bulletList(list: string[]) {
149+
return list.map((item) => {
150+
return ` • ${item}`;
151+
});
152+
}
153+
154+
formatCode(code: string) {
155+
return chalk.italic.cyan(code);
156+
}
157+
158+
formatUrl(url: string) {
159+
return chalk.reset.blue.underline(url);
160+
}
161+
162+
statusList(status: TaskStatus, list: string[]) {
163+
return list.map((item) => {
164+
return ` ${this.getStatusIcon(status)} ${item}`;
165+
});
166+
}
167+
}

libs/version/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Libs/Version
2+
3+
A utility library that provides a simple way to retrieve the version of your application from its package.json file.
4+
5+
## Usage
6+
7+
```ts
8+
import { getVersion } from '@libs/version';
9+
10+
const version = getVersion();
11+
console.log(version); // e.g. "1.0.0"
12+
```
13+
14+
If no package.json file was found or the package.json file does not have a version property it will throw an error.

libs/version/src/index.mts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { findUpSync } from 'find-up';
2+
import { readFileSync } from 'node:fs';
3+
import { fileURLToPath } from 'node:url';
4+
5+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
6+
7+
export function getVersion(): string {
8+
const packageJsonPath = findUpSync('package.json', { cwd: __dirname });
9+
if (!packageJsonPath) {
10+
throw `Could not find package.json`;
11+
}
12+
const pkg = readFileSync(packageJsonPath, 'utf8');
13+
const { version } = JSON.parse(pkg);
14+
if (!version) {
15+
throw `Could not find the version of create-plugin`;
16+
}
17+
return version;
18+
}

0 commit comments

Comments
 (0)