cli-kiss 0.2.4 → 0.2.5
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/dist/index.d.ts +150 -164
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.mts +1 -0
- package/docs/guide/01_getting_started.md +12 -13
- package/docs/guide/02_commands.md +12 -29
- package/docs/guide/03_options.md +16 -25
- package/docs/guide/04_positionals.md +45 -55
- package/docs/guide/05_types.md +66 -66
- package/docs/guide/06_run.md +28 -40
- package/docs/public/favicon.ico +0 -0
- package/docs/public/hero.png +0 -0
- package/package.json +1 -1
- package/src/index.ts +0 -2
- package/src/lib/Command.ts +14 -35
- package/src/lib/Operation.ts +13 -4
- package/src/lib/Option.ts +118 -162
- package/src/lib/Positional.ts +37 -62
- package/src/lib/Reader.ts +3 -3
- package/src/lib/Run.ts +74 -46
- package/src/lib/Type.ts +227 -141
- package/src/lib/Typo.ts +36 -24
- package/src/lib/Usage.ts +27 -42
- package/tests/unit.Reader.parsings.ts +50 -0
- package/tests/unit.command.execute.ts +13 -13
- package/tests/unit.command.usage.ts +60 -54
- package/tests/unit.runner.colors.ts +197 -0
- package/tests/unit.runner.cycle.ts +69 -55
- package/tests/unit.runner.errors.ts +12 -20
package/src/lib/Type.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "fs";
|
|
1
2
|
import {
|
|
2
3
|
TypoError,
|
|
3
4
|
TypoString,
|
|
@@ -8,17 +9,17 @@ import {
|
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Decodes a raw CLI string into a typed value.
|
|
11
|
-
* A pair of a human-readable `content` name
|
|
12
|
+
* A pair of a human-readable `content` name and a `decoder` function.
|
|
12
13
|
*
|
|
13
|
-
* Built-in: {@link
|
|
14
|
-
* {@link typeInteger}, {@link
|
|
15
|
-
* Composite: {@link
|
|
14
|
+
* Built-in: {@link type}, {@link typeBoolean}, {@link typeNumber},
|
|
15
|
+
* {@link typeInteger}, {@link typeDatetime}, {@link typeUrl}.
|
|
16
|
+
* Composite: {@link typeChoice}, {@link typeConverted}, {@link typeTuple}, {@link typeList}.
|
|
16
17
|
*
|
|
17
18
|
* @typeParam Value - Type produced by the decoder.
|
|
18
19
|
*/
|
|
19
20
|
export type Type<Value> = {
|
|
20
21
|
/**
|
|
21
|
-
* Human-readable name shown in help and errors (e.g. `"
|
|
22
|
+
* Human-readable name shown in help and errors (e.g. `"name"`, `"number"`).
|
|
22
23
|
*/
|
|
23
24
|
content: string;
|
|
24
25
|
/**
|
|
@@ -37,32 +38,34 @@ export type Type<Value> = {
|
|
|
37
38
|
*
|
|
38
39
|
* @example
|
|
39
40
|
* ```ts
|
|
40
|
-
* typeBoolean.decoder("true") // → true
|
|
41
|
-
* typeBoolean.decoder("yes") // → true
|
|
42
|
-
* typeBoolean.decoder("y") // → true
|
|
43
|
-
* typeBoolean.decoder("false") // → false
|
|
44
|
-
* typeBoolean.decoder("no") // → false
|
|
45
|
-
* typeBoolean.decoder("n") // → false
|
|
41
|
+
* typeBoolean("flag").decoder("true") // → true
|
|
42
|
+
* typeBoolean("flag").decoder("yes") // → true
|
|
43
|
+
* typeBoolean("flag").decoder("y") // → true
|
|
44
|
+
* typeBoolean("flag").decoder("false") // → false
|
|
45
|
+
* typeBoolean("flag").decoder("no") // → false
|
|
46
|
+
* typeBoolean("flag").decoder("n") // → false
|
|
46
47
|
* ```
|
|
47
48
|
*/
|
|
48
|
-
export
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
new
|
|
60
|
-
new
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
49
|
+
export function typeBoolean(name?: string): Type<boolean> {
|
|
50
|
+
return {
|
|
51
|
+
content: name ?? "boolean",
|
|
52
|
+
decoder(input: string) {
|
|
53
|
+
const lower = input.toLowerCase();
|
|
54
|
+
if (booleanValuesTrue.has(lower)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (booleanValuesFalse.has(lower)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
throw new TypoError(
|
|
61
|
+
new TypoText(
|
|
62
|
+
new TypoString(`Not a boolean: `),
|
|
63
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
64
|
+
),
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
66
69
|
const booleanValuesTrue = new Set(["true", "yes", "on", "1", "y", "t"]);
|
|
67
70
|
const booleanValuesFalse = new Set(["false", "no", "off", "0", "n", "f"]);
|
|
68
71
|
|
|
@@ -72,60 +75,64 @@ const booleanValuesFalse = new Set(["false", "no", "off", "0", "n", "f"]);
|
|
|
72
75
|
*
|
|
73
76
|
* @example
|
|
74
77
|
* ```ts
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
+
* typeDatetime("my-datetime").decoder("2024-01-15") // → Date object for 2024-01-15
|
|
79
|
+
* typeDatetime("my-datetime").decoder("2024-01-15T13:45:30Z") // → Date object for 2024-01-15 13:45:30 UTC
|
|
80
|
+
* typeDatetime("my-datetime").decoder("not a date") // throws TypoError
|
|
78
81
|
* ```
|
|
79
82
|
*/
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
83
|
+
export function typeDatetime(name?: string): Type<Date> {
|
|
84
|
+
return {
|
|
85
|
+
content: name ?? "datetime",
|
|
86
|
+
decoder(input: string) {
|
|
87
|
+
try {
|
|
88
|
+
const timestampMs = Date.parse(input);
|
|
89
|
+
if (isNaN(timestampMs)) {
|
|
90
|
+
throw new Error();
|
|
91
|
+
}
|
|
92
|
+
return new Date(timestampMs);
|
|
93
|
+
} catch {
|
|
94
|
+
throw new TypoError(
|
|
95
|
+
new TypoText(
|
|
96
|
+
new TypoString(`Not a valid ISO_8601 datetime: `),
|
|
97
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
87
100
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
new TypoText(
|
|
92
|
-
new TypoString(`Not a valid ISO_8601: `),
|
|
93
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
94
|
-
),
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
99
104
|
|
|
100
105
|
/**
|
|
101
106
|
* Parses a string to `number` via `Number()`; `NaN` throws {@link TypoError}.
|
|
102
107
|
*
|
|
103
108
|
* @example
|
|
104
109
|
* ```ts
|
|
105
|
-
* typeNumber.decoder("3.14") // → 3.14
|
|
106
|
-
* typeNumber.decoder("-1") // → -1
|
|
107
|
-
* typeNumber.decoder("hello") // throws TypoError
|
|
110
|
+
* typeNumber("my-number").decoder("3.14") // → 3.14
|
|
111
|
+
* typeNumber("my-number").decoder("-1") // → -1
|
|
112
|
+
* typeNumber("my-number").decoder("hello") // throws TypoError
|
|
108
113
|
* ```
|
|
109
114
|
*/
|
|
110
|
-
export
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
export function typeNumber(name?: string): Type<number> {
|
|
116
|
+
return {
|
|
117
|
+
content: name ?? "number",
|
|
118
|
+
decoder(input: string) {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = Number(input);
|
|
121
|
+
if (isNaN(parsed)) {
|
|
122
|
+
throw new Error();
|
|
123
|
+
}
|
|
124
|
+
return parsed;
|
|
125
|
+
} catch {
|
|
126
|
+
throw new TypoError(
|
|
127
|
+
new TypoText(
|
|
128
|
+
new TypoString(`Not a number: `),
|
|
129
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
130
|
+
),
|
|
131
|
+
);
|
|
117
132
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
new TypoText(
|
|
122
|
-
new TypoString(`Unable to parse: `),
|
|
123
|
-
new TypoString(`"${input}"`, typoStyleQuote),
|
|
124
|
-
),
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
};
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
129
136
|
|
|
130
137
|
/**
|
|
131
138
|
* Parses an integer string to `bigint` via `BigInt()`.
|
|
@@ -133,26 +140,28 @@ export const typeNumber: Type<number> = {
|
|
|
133
140
|
*
|
|
134
141
|
* @example
|
|
135
142
|
* ```ts
|
|
136
|
-
* typeInteger.decoder("42") // → 42n
|
|
137
|
-
* typeInteger.decoder("3.14") // throws TypoError
|
|
138
|
-
* typeInteger.decoder("abc") // throws TypoError
|
|
143
|
+
* typeInteger("my-integer").decoder("42") // → 42n
|
|
144
|
+
* typeInteger("my-integer").decoder("3.14") // throws TypoError
|
|
145
|
+
* typeInteger("my-integer").decoder("abc") // throws TypoError
|
|
139
146
|
* ```
|
|
140
147
|
*/
|
|
141
|
-
export
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
new
|
|
149
|
-
new
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
148
|
+
export function typeInteger(name?: string): Type<bigint> {
|
|
149
|
+
return {
|
|
150
|
+
content: name ?? "integer",
|
|
151
|
+
decoder(input: string) {
|
|
152
|
+
try {
|
|
153
|
+
return BigInt(input);
|
|
154
|
+
} catch {
|
|
155
|
+
throw new TypoError(
|
|
156
|
+
new TypoText(
|
|
157
|
+
new TypoString(`Not an integer: `),
|
|
158
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
156
165
|
|
|
157
166
|
/**
|
|
158
167
|
* Parses an absolute URL string to a `URL` object.
|
|
@@ -160,41 +169,43 @@ export const typeInteger: Type<bigint> = {
|
|
|
160
169
|
*
|
|
161
170
|
* @example
|
|
162
171
|
* ```ts
|
|
163
|
-
* typeUrl.decoder("https://example.com") // → URL { href: "https://example.com/", ... }
|
|
164
|
-
* typeUrl.decoder("not-a-url") // throws TypoError
|
|
172
|
+
* typeUrl("my-url").decoder("https://example.com") // → URL { href: "https://example.com/", ... }
|
|
173
|
+
* typeUrl("my-url").decoder("not-a-url") // throws TypoError
|
|
165
174
|
* ```
|
|
166
175
|
*/
|
|
167
|
-
export
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
new
|
|
175
|
-
new
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
176
|
+
export function typeUrl(name?: string): Type<URL> {
|
|
177
|
+
return {
|
|
178
|
+
content: name ?? "url",
|
|
179
|
+
decoder(input: string) {
|
|
180
|
+
try {
|
|
181
|
+
return new URL(input);
|
|
182
|
+
} catch {
|
|
183
|
+
throw new TypoError(
|
|
184
|
+
new TypoText(
|
|
185
|
+
new TypoString(`Not an URL: `),
|
|
186
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
182
193
|
|
|
183
194
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
195
|
+
* A named type that accepts any string as input.
|
|
196
|
+
* @param name - Name shown in help and errors (e.g. `"my-value"`).
|
|
186
197
|
* @example
|
|
187
198
|
* ```ts
|
|
188
|
-
*
|
|
189
|
-
*
|
|
199
|
+
* type("greeting").decoder("hello") // → "hello"
|
|
200
|
+
* type("greeting").decoder("") // → ""
|
|
190
201
|
* ```
|
|
191
202
|
*/
|
|
192
|
-
export
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
}
|
|
203
|
+
export function type(name?: string): Type<string> {
|
|
204
|
+
return {
|
|
205
|
+
content: name ?? "string",
|
|
206
|
+
decoder: (input: string) => input,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
198
209
|
|
|
199
210
|
/**
|
|
200
211
|
* Chains `before`'s decoder with an `after` transformation.
|
|
@@ -203,33 +214,30 @@ export const typeString: Type<string> = {
|
|
|
203
214
|
* @typeParam Before - Intermediate type from `before.decoder`.
|
|
204
215
|
* @typeParam After - Final type from `after.decoder`.
|
|
205
216
|
*
|
|
206
|
-
* @param
|
|
207
|
-
* @param
|
|
208
|
-
* @param
|
|
209
|
-
* @param after.decoder - Converts a `Before` value to `After`.
|
|
217
|
+
* @param name - Name shown in help and errors (e.g. `"my-value"`).
|
|
218
|
+
* @param before - Base type to decode the raw string.
|
|
219
|
+
* @param mapper - Transforms `before`'s output to the final value; errors are wrapped with context.
|
|
210
220
|
* @returns A {@link Type}`<After>`.
|
|
211
221
|
*
|
|
212
222
|
* @example
|
|
213
223
|
* ```ts
|
|
214
|
-
* const typePort =
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* if (n < 1 || n > 65535) throw new Error("Out of range");
|
|
218
|
-
* return n;
|
|
219
|
-
* },
|
|
224
|
+
* const typePort = typeConverted("port", typeNumber(), (n) => {
|
|
225
|
+
* if (n < 1 || n > 65535) throw new Error("Out of range");
|
|
226
|
+
* return n;
|
|
220
227
|
* });
|
|
221
228
|
* // "--port 8080" → 8080
|
|
222
229
|
* // "--port 99999" → TypoError: --port: <PORT>: Port: Out of range
|
|
223
230
|
* ```
|
|
224
231
|
*/
|
|
225
|
-
export function
|
|
232
|
+
export function typeConverted<Before, After>(
|
|
233
|
+
name: string,
|
|
226
234
|
before: Type<Before>,
|
|
227
|
-
|
|
235
|
+
mapper: (value: Before) => After,
|
|
228
236
|
): Type<After> {
|
|
229
237
|
return {
|
|
230
|
-
content:
|
|
238
|
+
content: name,
|
|
231
239
|
decoder: (input: string) => {
|
|
232
|
-
return
|
|
240
|
+
return mapper(
|
|
233
241
|
TypoError.tryWithContext(
|
|
234
242
|
() => before.decoder(input),
|
|
235
243
|
() =>
|
|
@@ -243,32 +251,110 @@ export function typeMapped<Before, After>(
|
|
|
243
251
|
};
|
|
244
252
|
}
|
|
245
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Adds a name to a {@link Type} for clearer error messages and help text.
|
|
256
|
+
*
|
|
257
|
+
* @param name - Name to use for the type.
|
|
258
|
+
* @param type - Base type to name.
|
|
259
|
+
* @returns A {@link Type} with the given name.
|
|
260
|
+
*/
|
|
261
|
+
export function typeRenamed<Value>(
|
|
262
|
+
type: Type<Value>,
|
|
263
|
+
name: string,
|
|
264
|
+
): Type<Value> {
|
|
265
|
+
return {
|
|
266
|
+
content: name,
|
|
267
|
+
decoder: (input: string) => {
|
|
268
|
+
return TypoError.tryWithContext(
|
|
269
|
+
() => type.decoder(input),
|
|
270
|
+
() =>
|
|
271
|
+
new TypoText(
|
|
272
|
+
new TypoString("from: "),
|
|
273
|
+
new TypoString(type.content, typoStyleLogic),
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Creates a {@link Type} for filesystem paths with optional existence checks.
|
|
282
|
+
* @param checks - Optional checks for path existence and type (file/directory).
|
|
283
|
+
* @returns A {@link Type}`<string>` representing the path.
|
|
284
|
+
*/
|
|
285
|
+
export function typePath(
|
|
286
|
+
name?: string,
|
|
287
|
+
checks?: { checkSyncExistAs?: "file" | "directory" | "anything" },
|
|
288
|
+
): Type<string> {
|
|
289
|
+
return {
|
|
290
|
+
content: name ?? "path",
|
|
291
|
+
decoder(input: string) {
|
|
292
|
+
if (input.length === 0) {
|
|
293
|
+
throw new Error(`Path cannot be empty`);
|
|
294
|
+
}
|
|
295
|
+
if (input.includes("\0")) {
|
|
296
|
+
throw new Error(`Path cannot contain null characters`);
|
|
297
|
+
}
|
|
298
|
+
if (checks?.checkSyncExistAs !== undefined) {
|
|
299
|
+
const stats = statSync(input);
|
|
300
|
+
const preview = stats.isDirectory()
|
|
301
|
+
? "directory"
|
|
302
|
+
: stats.isFile()
|
|
303
|
+
? "file"
|
|
304
|
+
: "unknown";
|
|
305
|
+
if (checks.checkSyncExistAs === "file" && !stats.isFile()) {
|
|
306
|
+
throw new TypoError(
|
|
307
|
+
new TypoText(
|
|
308
|
+
new TypoString(`Expected a 'file' but found '${preview}': `),
|
|
309
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
if (checks.checkSyncExistAs === "directory" && !stats.isDirectory()) {
|
|
314
|
+
throw new TypoError(
|
|
315
|
+
new TypoText(
|
|
316
|
+
new TypoString(`Expected a 'directory' but found '${preview}': `),
|
|
317
|
+
new TypoString(`"${input}"`, typoStyleQuote),
|
|
318
|
+
),
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return input;
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
246
327
|
/**
|
|
247
328
|
* Creates a {@link Type}`<string>` that only accepts a fixed set of values.
|
|
248
329
|
* Out-of-set inputs throw {@link TypoError} listing up to 5 valid options.
|
|
249
330
|
*
|
|
250
|
-
* @param
|
|
331
|
+
* @param name - Name shown in help and errors.
|
|
251
332
|
* @param values - Ordered list of accepted values.
|
|
252
333
|
* @returns A {@link Type}`<string>`.
|
|
253
334
|
*
|
|
254
335
|
* @example
|
|
255
336
|
* ```ts
|
|
256
|
-
* const typeEnv =
|
|
337
|
+
* const typeEnv = typeChoice("environment", ["dev", "staging", "prod"]);
|
|
257
338
|
* typeEnv.decoder("prod") // → "prod"
|
|
258
339
|
* typeEnv.decoder("unknown") // throws TypoError: Invalid value: "unknown" (expected one of: "dev" | "staging" | "prod")
|
|
259
340
|
* ```
|
|
260
341
|
*/
|
|
261
|
-
export function
|
|
262
|
-
|
|
342
|
+
export function typeChoice<const Value extends string>(
|
|
343
|
+
name: string,
|
|
263
344
|
values: Array<Value>,
|
|
345
|
+
caseSensitive: boolean = false,
|
|
264
346
|
): Type<Value> {
|
|
347
|
+
const normalize = caseSensitive
|
|
348
|
+
? (s: string) => s
|
|
349
|
+
: (s: string) => s.toLowerCase();
|
|
350
|
+
const valueMap = new Map(values.map((value) => [normalize(value), value]));
|
|
265
351
|
return {
|
|
266
|
-
content:
|
|
352
|
+
content: name,
|
|
267
353
|
decoder(input: string) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
354
|
+
const normalized = normalize(input);
|
|
355
|
+
const original = valueMap.get(normalized);
|
|
356
|
+
if (original !== undefined) {
|
|
357
|
+
return original;
|
|
272
358
|
}
|
|
273
359
|
const valuesPreview = [];
|
|
274
360
|
for (const value of values) {
|
|
@@ -306,7 +392,7 @@ export function typeOneOf<const Value extends string>(
|
|
|
306
392
|
*
|
|
307
393
|
* @example
|
|
308
394
|
* ```ts
|
|
309
|
-
* const typePoint = typeTuple([typeNumber, typeNumber]);
|
|
395
|
+
* const typePoint = typeTuple([typeNumber("x"), typeNumber("y")]);
|
|
310
396
|
* typePoint.decoder("3.14,2.71") // → [3.14, 2.71]
|
|
311
397
|
* typePoint.decoder("1,2,3") // → [1, 2]
|
|
312
398
|
* typePoint.decoder("x,2") // throws TypoError: at 0: Number: Unable to parse: "x"
|
|
@@ -363,7 +449,7 @@ export function typeTuple<const Elements extends Array<any>>(
|
|
|
363
449
|
* typeNumbers.decoder("1,2,3") // → [1, 2, 3]
|
|
364
450
|
* typeNumbers.decoder("1,x,3") // throws TypoError: at 1: Number: Unable to parse: "x"
|
|
365
451
|
*
|
|
366
|
-
* const typePaths = typeList(
|
|
452
|
+
* const typePaths = typeList(typePath(), ":");
|
|
367
453
|
* typePaths.decoder("/usr/bin:/usr/local/bin") // → ["/usr/bin", "/usr/local/bin"]
|
|
368
454
|
* ```
|
|
369
455
|
*/
|
package/src/lib/Typo.ts
CHANGED
|
@@ -25,6 +25,10 @@ export type TypoColor =
|
|
|
25
25
|
* All fields are optional; ignored entirely in `"none"` mode.
|
|
26
26
|
*/
|
|
27
27
|
export type TypoStyle = {
|
|
28
|
+
/**
|
|
29
|
+
* Letter case.
|
|
30
|
+
*/
|
|
31
|
+
case?: "upper" | "lower";
|
|
28
32
|
/**
|
|
29
33
|
* Foreground (text) color.
|
|
30
34
|
*/
|
|
@@ -228,7 +232,7 @@ export class TypoGrid {
|
|
|
228
232
|
* Renders as an array of styled, column-padded strings.
|
|
229
233
|
*
|
|
230
234
|
* @param typoSupport - Rendering mode.
|
|
231
|
-
* @returns
|
|
235
|
+
* @returns Array of styled strings.
|
|
232
236
|
*/
|
|
233
237
|
computeStyledLines(typoSupport: TypoSupport): Array<string> {
|
|
234
238
|
const widths = new Array<number>();
|
|
@@ -346,36 +350,37 @@ export class TypoSupport {
|
|
|
346
350
|
}
|
|
347
351
|
/**
|
|
348
352
|
* Deterministic textual styling for snapshot tests.
|
|
349
|
-
* Style flags appear as suffixes: `{text}@color`, `{text}+` (bold), `{text}-` (dim),
|
|
350
|
-
* `{text}*` (italic), `{text}_` (underline), `{text}~` (strikethrough).
|
|
351
353
|
*/
|
|
352
354
|
static mock(): TypoSupport {
|
|
353
355
|
return new TypoSupport("mock");
|
|
354
356
|
}
|
|
355
357
|
/**
|
|
356
|
-
* Auto-detects styling mode from the process environment.
|
|
357
|
-
* `FORCE_COLOR=0` / `NO_COLOR` → none; `FORCE_COLOR` (truthy) / `isTTY` → tty; else → none.
|
|
358
|
-
* Falls back to none if `process` is unavailable.
|
|
358
|
+
* Auto-detects styling mode from the process environment on best-effort basis.
|
|
359
359
|
*/
|
|
360
|
-
static
|
|
361
|
-
if (!process) {
|
|
360
|
+
static inferFromEnv(): TypoSupport {
|
|
361
|
+
if (!process || !process.env) {
|
|
362
362
|
return TypoSupport.none();
|
|
363
363
|
}
|
|
364
|
-
|
|
365
|
-
if (process.env
|
|
366
|
-
return
|
|
367
|
-
}
|
|
368
|
-
if (process.env["FORCE_COLOR"]) {
|
|
369
|
-
return TypoSupport.tty();
|
|
370
|
-
}
|
|
371
|
-
if ("NO_COLOR" in process.env) {
|
|
372
|
-
return TypoSupport.none();
|
|
364
|
+
function readEnvVar(name: string) {
|
|
365
|
+
if (!(name in process.env)) {
|
|
366
|
+
return undefined;
|
|
373
367
|
}
|
|
368
|
+
return process.env[name];
|
|
369
|
+
}
|
|
370
|
+
const envForceColor = readEnvVar("FORCE_COLOR");
|
|
371
|
+
if (envForceColor === "0") {
|
|
372
|
+
return TypoSupport.none();
|
|
374
373
|
}
|
|
375
|
-
if (
|
|
376
|
-
|
|
374
|
+
if (envForceColor !== undefined) {
|
|
375
|
+
TypoSupport.tty();
|
|
377
376
|
}
|
|
378
|
-
|
|
377
|
+
if (readEnvVar("NO_COLOR") !== undefined) {
|
|
378
|
+
return TypoSupport.none();
|
|
379
|
+
}
|
|
380
|
+
if (readEnvVar("MOCK_COLOR") !== undefined) {
|
|
381
|
+
return TypoSupport.mock();
|
|
382
|
+
}
|
|
383
|
+
return TypoSupport.tty();
|
|
379
384
|
}
|
|
380
385
|
/**
|
|
381
386
|
* Applies `typoStyle` to `value` according to the current mode.
|
|
@@ -385,8 +390,15 @@ export class TypoSupport {
|
|
|
385
390
|
* @returns Styled string.
|
|
386
391
|
*/
|
|
387
392
|
computeStyledString(value: string, typoStyle: TypoStyle): string {
|
|
393
|
+
let styledValue = value;
|
|
394
|
+
if (typoStyle.case === "upper") {
|
|
395
|
+
styledValue = styledValue.toUpperCase();
|
|
396
|
+
}
|
|
397
|
+
if (typoStyle.case === "lower") {
|
|
398
|
+
styledValue = styledValue.toLowerCase();
|
|
399
|
+
}
|
|
388
400
|
if (this.#kind === "none") {
|
|
389
|
-
return
|
|
401
|
+
return styledValue;
|
|
390
402
|
}
|
|
391
403
|
if (this.#kind === "tty") {
|
|
392
404
|
const fgColorCode = typoStyle.fgColor
|
|
@@ -402,12 +414,12 @@ export class TypoSupport {
|
|
|
402
414
|
const strikethroughCode = typoStyle.strikethrough
|
|
403
415
|
? ttyCodeStrikethrough
|
|
404
416
|
: "";
|
|
405
|
-
return `${fgColorCode}${bgColorCode}${boldCode}${dimCode}${italicCode}${underlineCode}${strikethroughCode}${
|
|
417
|
+
return `${fgColorCode}${bgColorCode}${boldCode}${dimCode}${italicCode}${underlineCode}${strikethroughCode}${styledValue}${ttyCodeReset}`;
|
|
406
418
|
}
|
|
407
419
|
if (this.#kind === "mock") {
|
|
408
420
|
const fgColorPart = typoStyle.fgColor
|
|
409
|
-
? `{${
|
|
410
|
-
:
|
|
421
|
+
? `{${styledValue}}@${typoStyle.fgColor}`
|
|
422
|
+
: styledValue;
|
|
411
423
|
const bgColorPart = typoStyle.bgColor
|
|
412
424
|
? `{${fgColorPart}}#${typoStyle.bgColor}`
|
|
413
425
|
: fgColorPart;
|