@optique/logtape 0.8.0-dev.1

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.js ADDED
@@ -0,0 +1,539 @@
1
+ import { choice } from "@optique/core/valueparser";
2
+ import { flag, option } from "@optique/core/primitives";
3
+ import { map, multiple, optional, withDefault } from "@optique/core/modifiers";
4
+ import { message } from "@optique/core/message";
5
+ import { group, object } from "@optique/core/constructs";
6
+
7
+ //#region src/loglevel.ts
8
+ /**
9
+ * All valid log levels in order from lowest to highest severity.
10
+ */
11
+ const LOG_LEVELS = [
12
+ "trace",
13
+ "debug",
14
+ "info",
15
+ "warning",
16
+ "error",
17
+ "fatal"
18
+ ];
19
+ /**
20
+ * Creates a {@link ValueParser} for LogTape log levels.
21
+ *
22
+ * This parser validates that the input is one of the valid LogTape severity
23
+ * levels: `"trace"`, `"debug"`, `"info"`, `"warning"`, `"error"`, or `"fatal"`.
24
+ * The parsing is case-insensitive.
25
+ *
26
+ * @param options Configuration options for the log level parser.
27
+ * @returns A {@link ValueParser} that converts string input to {@link LogLevel}.
28
+ *
29
+ * @example Basic usage
30
+ * ```typescript
31
+ * import { logLevel } from "@optique/logtape";
32
+ * import { option, withDefault } from "@optique/core";
33
+ *
34
+ * const parser = object({
35
+ * level: withDefault(
36
+ * option("--log-level", "-l", logLevel()),
37
+ * "info"
38
+ * ),
39
+ * });
40
+ * ```
41
+ *
42
+ * @example Custom metavar
43
+ * ```typescript
44
+ * import { logLevel } from "@optique/logtape";
45
+ *
46
+ * const parser = logLevel({ metavar: "LOG_LEVEL" });
47
+ * ```
48
+ *
49
+ * @since 0.8.0
50
+ */
51
+ function logLevel(options = {}) {
52
+ return choice(LOG_LEVELS, {
53
+ metavar: options.metavar ?? "LEVEL",
54
+ caseInsensitive: true,
55
+ errors: options.errors?.invalidLevel != null ? { invalidChoice: typeof options.errors.invalidLevel === "function" ? (input, _choices) => options.errors.invalidLevel(input) : options.errors.invalidLevel } : void 0
56
+ });
57
+ }
58
+
59
+ //#endregion
60
+ //#region src/verbosity.ts
61
+ /**
62
+ * Mapping from verbosity count (offset from base) to log level.
63
+ * Index 0 is base, 1 is base+1 flag, 2 is base+2 flags, etc.
64
+ */
65
+ const VERBOSITY_LEVELS = [
66
+ "fatal",
67
+ "error",
68
+ "warning",
69
+ "info",
70
+ "debug",
71
+ "trace"
72
+ ];
73
+ /**
74
+ * Index of "warning" in VERBOSITY_LEVELS (the default base level).
75
+ */
76
+ const WARNING_INDEX = 2;
77
+ /**
78
+ * Creates a parser for verbosity flags (`-v`, `-vv`, `-vvv`, etc.).
79
+ *
80
+ * This parser accumulates `-v` flags to determine the log level.
81
+ * Each additional `-v` flag increases the verbosity (decreases the log level
82
+ * severity).
83
+ *
84
+ * Default level mapping with `baseLevel: "warning"`:
85
+ * - No flags: `"warning"`
86
+ * - `-v`: `"info"`
87
+ * - `-vv`: `"debug"`
88
+ * - `-vvv` or more: `"trace"`
89
+ *
90
+ * @param options Configuration options for the verbosity parser.
91
+ * @returns A {@link Parser} that produces a {@link LogLevel}.
92
+ *
93
+ * @example Basic usage
94
+ * ```typescript
95
+ * import { verbosity } from "@optique/logtape";
96
+ * import { object } from "@optique/core/constructs";
97
+ *
98
+ * const parser = object({
99
+ * logLevel: verbosity(),
100
+ * });
101
+ *
102
+ * // No flags -> "warning"
103
+ * // -v -> "info"
104
+ * // -vv -> "debug"
105
+ * // -vvv -> "trace"
106
+ * ```
107
+ *
108
+ * @example Custom base level
109
+ * ```typescript
110
+ * import { verbosity } from "@optique/logtape";
111
+ *
112
+ * const parser = verbosity({ baseLevel: "error" });
113
+ * // No flags -> "error"
114
+ * // -v -> "warning"
115
+ * // -vv -> "info"
116
+ * // -vvv -> "debug"
117
+ * // -vvvv -> "trace"
118
+ * ```
119
+ *
120
+ * @since 0.8.0
121
+ */
122
+ function verbosity(options = {}) {
123
+ const short = options.short ?? "-v";
124
+ const long = options.long ?? "--verbose";
125
+ const baseLevel = options.baseLevel ?? "warning";
126
+ const baseIndex = VERBOSITY_LEVELS.indexOf(baseLevel);
127
+ const effectiveBaseIndex = baseIndex >= 0 ? baseIndex : WARNING_INDEX;
128
+ const flagParser = flag(short, long, { description: options.description });
129
+ const multipleFlags = multiple(flagParser);
130
+ return map(multipleFlags, (flags) => {
131
+ const count = flags.length;
132
+ const targetIndex = Math.min(effectiveBaseIndex + count, VERBOSITY_LEVELS.length - 1);
133
+ return VERBOSITY_LEVELS[targetIndex];
134
+ });
135
+ }
136
+
137
+ //#endregion
138
+ //#region src/debug.ts
139
+ /**
140
+ * Creates a parser for a debug flag (`-d`, `--debug`).
141
+ *
142
+ * This parser provides a simple boolean toggle for enabling debug-level
143
+ * logging. When the flag is present, it returns the debug level; otherwise,
144
+ * it returns the normal level.
145
+ *
146
+ * @param options Configuration options for the debug flag parser.
147
+ * @returns A {@link Parser} that produces a {@link LogLevel}.
148
+ *
149
+ * @example Basic usage
150
+ * ```typescript
151
+ * import { debug } from "@optique/logtape";
152
+ * import { object } from "@optique/core/constructs";
153
+ *
154
+ * const parser = object({
155
+ * logLevel: debug(),
156
+ * });
157
+ *
158
+ * // No flag -> "info"
159
+ * // --debug or -d -> "debug"
160
+ * ```
161
+ *
162
+ * @example Custom levels
163
+ * ```typescript
164
+ * import { debug } from "@optique/logtape";
165
+ *
166
+ * const parser = debug({
167
+ * debugLevel: "trace",
168
+ * normalLevel: "warning",
169
+ * });
170
+ * // No flag -> "warning"
171
+ * // --debug -> "trace"
172
+ * ```
173
+ *
174
+ * @since 0.8.0
175
+ */
176
+ function debug(options = {}) {
177
+ const short = options.short ?? "-d";
178
+ const long = options.long ?? "--debug";
179
+ const debugLevel = options.debugLevel ?? "debug";
180
+ const normalLevel = options.normalLevel ?? "info";
181
+ const flagParser = flag(short, long, { description: options.description });
182
+ return map(optional(flagParser), (value) => {
183
+ return value === true ? debugLevel : normalLevel;
184
+ });
185
+ }
186
+
187
+ //#endregion
188
+ //#region src/output.ts
189
+ /**
190
+ * Creates a value parser for log output destinations.
191
+ *
192
+ * This parser accepts either `-` for console output or a file path for file
193
+ * output. The `-` value follows the common CLI convention for representing
194
+ * standard output/error.
195
+ *
196
+ * @param options Configuration options for the parser.
197
+ * @returns A {@link ValueParser} that produces a {@link LogOutput}.
198
+ */
199
+ function logOutputValueParser(options = {}) {
200
+ return {
201
+ metavar: options.metavar ?? "FILE",
202
+ parse(input) {
203
+ if (input === "-") return {
204
+ success: true,
205
+ value: { type: "console" }
206
+ };
207
+ if (input.trim() === "") return {
208
+ success: false,
209
+ error: options.errors?.emptyPath ? typeof options.errors.emptyPath === "function" ? options.errors.emptyPath(input) : options.errors.emptyPath : message`Log output path cannot be empty.`
210
+ };
211
+ return {
212
+ success: true,
213
+ value: {
214
+ type: "file",
215
+ path: input
216
+ }
217
+ };
218
+ },
219
+ format(value) {
220
+ return value.type === "console" ? "-" : value.path;
221
+ },
222
+ *suggest(prefix) {
223
+ if ("-".startsWith(prefix)) yield {
224
+ kind: "literal",
225
+ text: "-"
226
+ };
227
+ yield {
228
+ kind: "file",
229
+ type: "file",
230
+ pattern: prefix
231
+ };
232
+ }
233
+ };
234
+ }
235
+ /**
236
+ * Creates a parser for log output destination (`--log-output`).
237
+ *
238
+ * This parser accepts either `-` for console output (following CLI convention)
239
+ * or a file path for file output.
240
+ *
241
+ * @param options Configuration options for the log output parser.
242
+ * @returns A {@link Parser} that produces a {@link LogOutput} or `undefined`.
243
+ *
244
+ * @example Basic usage
245
+ * ```typescript
246
+ * import { logOutput } from "@optique/logtape";
247
+ * import { object } from "@optique/core/constructs";
248
+ *
249
+ * const parser = object({
250
+ * output: logOutput(),
251
+ * });
252
+ *
253
+ * // --log-output=- -> console output
254
+ * // --log-output=/var/log/app.log -> file output
255
+ * ```
256
+ *
257
+ * @since 0.8.0
258
+ */
259
+ function logOutput(options = {}) {
260
+ const long = options.long ?? "--log-output";
261
+ const valueParser = logOutputValueParser(options);
262
+ if (options.short) {
263
+ const short = options.short;
264
+ return optional(option(short, long, valueParser, { description: options.description }));
265
+ }
266
+ return optional(option(long, valueParser, { description: options.description }));
267
+ }
268
+ /**
269
+ * Creates a console sink with configurable stream selection.
270
+ *
271
+ * This function creates a LogTape sink that writes to the console. The target
272
+ * stream (stdout or stderr) can be configured statically or dynamically per
273
+ * log record.
274
+ *
275
+ * @param options Configuration options for the console sink.
276
+ * @returns A {@link Sink} function.
277
+ *
278
+ * @example Static stream selection
279
+ * ```typescript
280
+ * import { createConsoleSink } from "@optique/logtape";
281
+ *
282
+ * const sink = createConsoleSink({ stream: "stderr" });
283
+ * ```
284
+ *
285
+ * @example Dynamic stream selection based on level
286
+ * ```typescript
287
+ * import { createConsoleSink } from "@optique/logtape";
288
+ *
289
+ * const sink = createConsoleSink({
290
+ * streamResolver: (level) =>
291
+ * level === "error" || level === "fatal" ? "stderr" : "stdout"
292
+ * });
293
+ * ```
294
+ *
295
+ * @since 0.8.0
296
+ */
297
+ function createConsoleSink(options = {}) {
298
+ const defaultStream = options.stream ?? "stderr";
299
+ const streamResolver = options.streamResolver;
300
+ return (record) => {
301
+ const stream = streamResolver ? streamResolver(record.level) : defaultStream;
302
+ const messageParts = [];
303
+ for (let i = 0; i < record.message.length; i++) {
304
+ const part = record.message[i];
305
+ if (typeof part === "string") messageParts.push(part);
306
+ else messageParts.push(String(part));
307
+ }
308
+ const formattedMessage = messageParts.join("");
309
+ const timestamp = record.timestamp ? new Date(record.timestamp).toISOString() : (/* @__PURE__ */ new Date()).toISOString();
310
+ const category = record.category.join(".");
311
+ const level = record.level.toUpperCase().padEnd(7);
312
+ const line = `${timestamp} [${level}] ${category}: ${formattedMessage}`;
313
+ if (stream === "stderr") console.error(line);
314
+ else console.log(line);
315
+ };
316
+ }
317
+ /**
318
+ * Creates a sink from a {@link LogOutput} destination.
319
+ *
320
+ * For console output, this creates a console sink. For file output, this
321
+ * dynamically imports `@logtape/file` and creates a file sink.
322
+ *
323
+ * @param output The log output destination.
324
+ * @param consoleSinkOptions Options for console sink (only used when output is console).
325
+ * @returns A promise that resolves to a {@link Sink}.
326
+ * @throws {Error} If file output is requested but `@logtape/file` is not installed.
327
+ *
328
+ * @example Console output
329
+ * ```typescript
330
+ * import { createSink } from "@optique/logtape";
331
+ *
332
+ * const sink = await createSink({ type: "console" }, { stream: "stderr" });
333
+ * ```
334
+ *
335
+ * @example File output
336
+ * ```typescript
337
+ * import { createSink } from "@optique/logtape";
338
+ *
339
+ * const sink = await createSink({ type: "file", path: "/var/log/app.log" });
340
+ * ```
341
+ *
342
+ * @since 0.8.0
343
+ */
344
+ async function createSink(output, consoleSinkOptions = {}) {
345
+ if (output.type === "console") return createConsoleSink(consoleSinkOptions);
346
+ try {
347
+ const { getFileSink } = await import("@logtape/file");
348
+ return getFileSink(output.path);
349
+ } catch (e) {
350
+ throw new Error(`File sink requires @logtape/file package. Install it with:
351
+ npm install @logtape/file
352
+ # or
353
+ deno add jsr:@logtape/file
354
+
355
+ Original error: ${e}`);
356
+ }
357
+ }
358
+
359
+ //#endregion
360
+ //#region src/preset.ts
361
+ /**
362
+ * Creates a logging options parser preset.
363
+ *
364
+ * This function creates a parser that combines log level and log output
365
+ * options into a single group. The log level can be configured using one of
366
+ * three methods (mutually exclusive):
367
+ *
368
+ * - `"option"`: Explicit `--log-level=LEVEL` option
369
+ * - `"verbosity"`: `-v`/`-vv`/`-vvv` flags for increasing verbosity
370
+ * - `"debug"`: Simple `--debug` flag toggle
371
+ *
372
+ * @param config Configuration for the logging options.
373
+ * @returns A {@link Parser} that produces a {@link LoggingOptionsResult}.
374
+ *
375
+ * @example Using log level option
376
+ * ```typescript
377
+ * import { loggingOptions } from "@optique/logtape";
378
+ * import { object } from "@optique/core/constructs";
379
+ *
380
+ * const parser = object({
381
+ * logging: loggingOptions({ level: "option" }),
382
+ * });
383
+ * // --log-level=debug --log-output=/var/log/app.log
384
+ * ```
385
+ *
386
+ * @example Using verbosity flags
387
+ * ```typescript
388
+ * import { loggingOptions } from "@optique/logtape";
389
+ * import { object } from "@optique/core/constructs";
390
+ *
391
+ * const parser = object({
392
+ * logging: loggingOptions({ level: "verbosity" }),
393
+ * });
394
+ * // -vv --log-output=-
395
+ * ```
396
+ *
397
+ * @example Using debug flag
398
+ * ```typescript
399
+ * import { loggingOptions } from "@optique/logtape";
400
+ * import { object } from "@optique/core/constructs";
401
+ *
402
+ * const parser = object({
403
+ * logging: loggingOptions({ level: "debug" }),
404
+ * });
405
+ * // --debug
406
+ * ```
407
+ *
408
+ * @since 0.8.0
409
+ */
410
+ function loggingOptions(config) {
411
+ const groupLabel = config.groupLabel ?? "Logging options";
412
+ const outputEnabled = config.output?.enabled !== false;
413
+ const outputLong = config.output?.long ?? "--log-output";
414
+ let levelParser;
415
+ switch (config.level) {
416
+ case "option": {
417
+ const long = config.long ?? "--log-level";
418
+ const short = config.short ?? "-l";
419
+ const defaultLevel = config.default ?? "info";
420
+ levelParser = withDefault(option(short, long, logLevel()), defaultLevel);
421
+ break;
422
+ }
423
+ case "verbosity": {
424
+ const verbosityOptions = {
425
+ short: config.short,
426
+ long: config.long,
427
+ baseLevel: config.baseLevel
428
+ };
429
+ levelParser = verbosity(verbosityOptions);
430
+ break;
431
+ }
432
+ case "debug": {
433
+ const debugOptions = {
434
+ short: config.short,
435
+ long: config.long,
436
+ debugLevel: config.debugLevel,
437
+ normalLevel: config.normalLevel
438
+ };
439
+ levelParser = debug(debugOptions);
440
+ break;
441
+ }
442
+ }
443
+ const defaultOutput = { type: "console" };
444
+ const outputParser = withDefault(logOutput({ long: outputLong }), defaultOutput);
445
+ if (!outputEnabled) {
446
+ const constantOutputParser = {
447
+ $valueType: [],
448
+ $stateType: [],
449
+ priority: 0,
450
+ usage: [],
451
+ initialState: void 0,
452
+ parse: (context) => ({
453
+ success: true,
454
+ next: context,
455
+ consumed: []
456
+ }),
457
+ complete: () => ({
458
+ success: true,
459
+ value: defaultOutput
460
+ }),
461
+ suggest: () => [],
462
+ getDocFragments: () => ({ fragments: [] })
463
+ };
464
+ return group(groupLabel, object({
465
+ logLevel: levelParser,
466
+ logOutput: constantOutputParser
467
+ }));
468
+ }
469
+ const innerParser = object({
470
+ logLevel: levelParser,
471
+ logOutput: outputParser
472
+ });
473
+ return group(groupLabel, innerParser);
474
+ }
475
+ /**
476
+ * Creates a LogTape configuration from parsed logging options.
477
+ *
478
+ * This helper function converts the result of {@link loggingOptions} parser
479
+ * into a configuration object that can be passed to LogTape's `configure()`
480
+ * function.
481
+ *
482
+ * @param options The parsed logging options.
483
+ * @param consoleSinkOptions Options for console sink (only used when output is console).
484
+ * @param additionalConfig Additional LogTape configuration to merge.
485
+ * @returns A promise that resolves to a LogTape {@link Config}.
486
+ *
487
+ * @example Basic usage
488
+ * ```typescript
489
+ * import { loggingOptions, createLoggingConfig } from "@optique/logtape";
490
+ * import { configure } from "@logtape/logtape";
491
+ * import { object, parse } from "@optique/core";
492
+ *
493
+ * const parser = object({
494
+ * logging: loggingOptions({ level: "option" }),
495
+ * });
496
+ *
497
+ * const result = parse(parser, ["--log-level=debug"]);
498
+ * if (result.success) {
499
+ * const config = await createLoggingConfig(result.value.logging);
500
+ * await configure(config);
501
+ * }
502
+ * ```
503
+ *
504
+ * @example With additional configuration
505
+ * ```typescript
506
+ * import { loggingOptions, createLoggingConfig } from "@optique/logtape";
507
+ * import { configure } from "@logtape/logtape";
508
+ *
509
+ * const config = await createLoggingConfig(result.value.logging, {
510
+ * stream: "stderr",
511
+ * }, {
512
+ * loggers: [
513
+ * { category: ["my-app", "database"], lowestLevel: "debug", sinks: ["default"] },
514
+ * ],
515
+ * });
516
+ * await configure(config);
517
+ * ```
518
+ *
519
+ * @since 0.8.0
520
+ */
521
+ async function createLoggingConfig(options, consoleSinkOptions = {}, additionalConfig = {}) {
522
+ const sink = await createSink(options.logOutput, consoleSinkOptions);
523
+ return {
524
+ sinks: {
525
+ default: sink,
526
+ ...additionalConfig.sinks
527
+ },
528
+ loggers: [{
529
+ category: [],
530
+ lowestLevel: options.logLevel,
531
+ sinks: ["default"]
532
+ }, ...additionalConfig.loggers ?? []],
533
+ filters: additionalConfig.filters,
534
+ reset: additionalConfig.reset
535
+ };
536
+ }
537
+
538
+ //#endregion
539
+ export { LOG_LEVELS, createConsoleSink, createLoggingConfig, createSink, debug, logLevel, logOutput, loggingOptions, verbosity };
package/package.json ADDED
@@ -0,0 +1,81 @@
1
+ {
2
+ "name": "@optique/logtape",
3
+ "version": "0.8.0-dev.1",
4
+ "description": "LogTape logging integration for Optique CLI parser",
5
+ "keywords": [
6
+ "CLI",
7
+ "command-line",
8
+ "commandline",
9
+ "parser",
10
+ "logging",
11
+ "logtape"
12
+ ],
13
+ "license": "MIT",
14
+ "author": {
15
+ "name": "Hong Minhee",
16
+ "email": "hong@minhee.org",
17
+ "url": "https://hongminhee.org/"
18
+ },
19
+ "homepage": "https://optique.dev/",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/dahlia/optique.git",
23
+ "directory": "packages/logtape/"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/dahlia/optique/issues"
27
+ },
28
+ "funding": [
29
+ "https://github.com/sponsors/dahlia"
30
+ ],
31
+ "engines": {
32
+ "node": ">=20.0.0",
33
+ "bun": ">=1.2.0",
34
+ "deno": ">=2.3.0"
35
+ },
36
+ "files": [
37
+ "dist/",
38
+ "package.json",
39
+ "README.md"
40
+ ],
41
+ "type": "module",
42
+ "module": "./dist/index.js",
43
+ "main": "./dist/index.cjs",
44
+ "types": "./dist/index.d.ts",
45
+ "exports": {
46
+ ".": {
47
+ "types": {
48
+ "import": "./dist/index.d.ts",
49
+ "require": "./dist/index.d.cts"
50
+ },
51
+ "import": "./dist/index.js",
52
+ "require": "./dist/index.cjs"
53
+ }
54
+ },
55
+ "sideEffects": false,
56
+ "peerDependencies": {
57
+ "@logtape/logtape": "^1.2.2"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "@logtape/file": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "dependencies": {
65
+ "@optique/core": ""
66
+ },
67
+ "devDependencies": {
68
+ "@logtape/logtape": "^1.2.2",
69
+ "@types/node": "^20.19.9",
70
+ "tsdown": "^0.13.0",
71
+ "typescript": "^5.8.3"
72
+ },
73
+ "scripts": {
74
+ "build": "tsdown",
75
+ "prepublish": "tsdown",
76
+ "test": "tsdown && node --experimental-transform-types --test",
77
+ "test:bun": "tsdown && bun test",
78
+ "test:deno": "deno test",
79
+ "test-all": "tsdown && node --experimental-transform-types --test && bun test && deno test"
80
+ }
81
+ }