@optique/logtape 1.2.0-dev.2324 → 1.2.0-dev.2330

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/README.md CHANGED
@@ -70,6 +70,42 @@ Features:
70
70
  `"fatal"`
71
71
  - Provides suggestions for shell completion
72
72
 
73
+ ### `textFormatter()`
74
+
75
+ A value parser for LogTape text formatters. Parses `"jsonl"`, `"logfmt"`,
76
+ `"color"`, and `"plain"` into LogTape formatter functions.
77
+
78
+ ~~~~ typescript
79
+ import { textFormatter } from "@optique/logtape";
80
+ import { object, option, parse } from "@optique/core";
81
+
82
+ const parser = object({
83
+ formatter: option("--log-format", textFormatter()),
84
+ });
85
+
86
+ const result = parse(parser, ["--log-format=logfmt"]);
87
+ // result.value.formatter is LogTape's logfmtFormatter
88
+ ~~~~
89
+
90
+ Formats:
91
+
92
+ `"jsonl"`
93
+ : [JSON Lines][LogTape JSON Lines formatter] output
94
+
95
+ `"logfmt"`
96
+ : [logfmt][LogTape logfmt formatter] key-value output
97
+
98
+ `"color"`
99
+ : [ANSI-colored][LogTape ANSI color formatter] console output
100
+
101
+ `"plain"`
102
+ : [LogTape's default text][LogTape default text formatter] output
103
+
104
+ [LogTape JSON Lines formatter]: https://logtape.org/manual/formatters#json-lines-formatter
105
+ [LogTape logfmt formatter]: https://logtape.org/manual/formatters#logfmt-formatter
106
+ [LogTape ANSI color formatter]: https://logtape.org/manual/formatters#ansi-color-formatter
107
+ [LogTape default text formatter]: https://logtape.org/manual/formatters#default-text-formatter
108
+
73
109
  ### `verbosity()`
74
110
 
75
111
  A parser for accumulating `-v` flags to determine log level.
@@ -152,6 +188,37 @@ const result2 = parse(parser, ["--log-output=/var/log/app.log"]);
152
188
  const sink = await createSink(result1.value.output);
153
189
  ~~~~
154
190
 
191
+ Use `formatter` to add a text formatter option and wire it through the
192
+ resulting `LogOutput`:
193
+
194
+ ~~~~ typescript
195
+ import { logOutput } from "@optique/logtape";
196
+ import { object, parse } from "@optique/core";
197
+
198
+ const parser = object({
199
+ output: logOutput({ formatter: "--log-format" }),
200
+ });
201
+
202
+ const result = parse(parser, ["--log-format=logfmt"]);
203
+ // result.value.output is a console LogOutput with logfmtFormatter
204
+ ~~~~
205
+
206
+ You can also pass a fixed LogTape text formatter. In that case no additional
207
+ command-line option is added:
208
+
209
+ ~~~~ typescript
210
+ import { logfmtFormatter } from "@logtape/logtape";
211
+ import { logOutput } from "@optique/logtape";
212
+ import { object, parse } from "@optique/core";
213
+
214
+ const parser = object({
215
+ output: logOutput({ formatter: logfmtFormatter }),
216
+ });
217
+
218
+ const result = parse(parser, ["--log-output=/var/log/app.log"]);
219
+ // result.value.output is a file LogOutput with logfmtFormatter
220
+ ~~~~
221
+
155
222
  ### `loggingOptions()`
156
223
 
157
224
  A preset that combines log level and log output options into a single group.
@@ -206,9 +273,21 @@ Common options:
206
273
 
207
274
  - `output.enabled`: Whether to enable log output option (default: `true`)
208
275
  - `output.long`: Long option name for output (default: `"--log-output"`)
276
+ - `formatter`: Text formatter or long option name for output format
209
277
  - `groupLabel`: Label for option group in help text (default:
210
278
  `"Logging options"`)
211
279
 
280
+ Set `formatter` at the top level when using the preset:
281
+
282
+ ~~~~ typescript
283
+ const parser = object({
284
+ logging: loggingOptions({
285
+ level: "option",
286
+ formatter: "--log-format",
287
+ }),
288
+ });
289
+ ~~~~
290
+
212
291
  ### `createLoggingConfig()`
213
292
 
214
293
  Converts parsed logging options into a LogTape configuration object.
@@ -240,6 +319,7 @@ if (result.success) {
240
319
  Creates a console sink with configurable stream selection.
241
320
 
242
321
  ~~~~ typescript
322
+ import { logfmtFormatter } from "@logtape/logtape";
243
323
  import { createConsoleSink } from "@optique/logtape";
244
324
 
245
325
  // Write to stderr (default)
@@ -253,6 +333,20 @@ const sink3 = createConsoleSink({
253
333
  streamResolver: (level) =>
254
334
  level === "error" || level === "fatal" ? "stderr" : "stdout",
255
335
  });
336
+
337
+ // Structured logfmt output
338
+ const sink4 = createConsoleSink({
339
+ formatter: logfmtFormatter,
340
+ });
341
+
342
+ // Custom console formatting with multiple console arguments
343
+ const sink5 = createConsoleSink({
344
+ formatter: (record) => [
345
+ "%s %o",
346
+ record.level.toUpperCase(),
347
+ record.properties,
348
+ ],
349
+ });
256
350
  ~~~~
257
351
 
258
352
  ### `createSink()`
@@ -260,13 +354,21 @@ const sink3 = createConsoleSink({
260
354
  Creates a LogTape sink from a `LogOutput` value.
261
355
 
262
356
  ~~~~ typescript
357
+ import { logfmtFormatter } from "@logtape/logtape";
263
358
  import { createSink, type LogOutput } from "@optique/logtape";
264
359
 
265
- // Console sink
266
- const consoleSink = await createSink({ type: "console" });
360
+ // Console sink with a formatter selected by logOutput()
361
+ const consoleSink = await createSink({
362
+ type: "console",
363
+ formatter: logfmtFormatter,
364
+ });
267
365
 
268
366
  // File sink (requires @logtape/file package)
269
- const fileSink = await createSink({ type: "file", path: "/var/log/app.log" });
367
+ const fileSink = await createSink({
368
+ type: "file",
369
+ path: "/var/log/app.log",
370
+ formatter: logfmtFormatter,
371
+ });
270
372
  ~~~~
271
373
 
272
374
  > [!NOTE]
package/dist/index.cjs CHANGED
@@ -22,12 +22,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
 
23
23
  //#endregion
24
24
  const __optique_core_valueparser = __toESM(require("@optique/core/valueparser"));
25
+ const __logtape_logtape = __toESM(require("@logtape/logtape"));
25
26
  const __optique_core_primitives = __toESM(require("@optique/core/primitives"));
26
27
  const __optique_core_modifiers = __toESM(require("@optique/core/modifiers"));
27
28
  const __optique_core_message = __toESM(require("@optique/core/message"));
28
29
  const node_path = __toESM(require("node:path"));
29
- const __optique_core_nonempty = __toESM(require("@optique/core/nonempty"));
30
30
  const __optique_core_constructs = __toESM(require("@optique/core/constructs"));
31
+ const __optique_core_nonempty = __toESM(require("@optique/core/nonempty"));
31
32
 
32
33
  //#region src/loglevel.ts
33
34
  /**
@@ -92,6 +93,38 @@ function logLevel(options = {}) {
92
93
  });
93
94
  }
94
95
 
96
+ //#endregion
97
+ //#region src/textformatter.ts
98
+ const textFormatters = {
99
+ jsonl: __logtape_logtape.jsonLinesFormatter,
100
+ logfmt: __logtape_logtape.logfmtFormatter,
101
+ color: __logtape_logtape.ansiColorFormatter,
102
+ plain: __logtape_logtape.defaultTextFormatter
103
+ };
104
+ /**
105
+ * Creates a {@link ValueParser} for LogTape text formatters.
106
+ *
107
+ * This parser accepts `"jsonl"`, `"logfmt"`, `"color"`, and `"plain"` and
108
+ * maps them to LogTape's `jsonLinesFormatter`, `logfmtFormatter`,
109
+ * `ansiColorFormatter`, and `defaultTextFormatter` respectively.
110
+ *
111
+ * @returns A {@link ValueParser} that converts formatter names to
112
+ * LogTape text formatter functions.
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * import { option } from "@optique/core";
117
+ * import { textFormatter } from "@optique/logtape";
118
+ *
119
+ * const parser = option("--log-format", textFormatter());
120
+ * ```
121
+ *
122
+ * @since 1.2.0
123
+ */
124
+ function textFormatter() {
125
+ return (0, __optique_core_valueparser.biject)(textFormatters);
126
+ }
127
+
95
128
  //#endregion
96
129
  //#region src/verbosity.ts
97
130
  /**
@@ -306,9 +339,41 @@ function logOutput(options = {}) {
306
339
  const description = options.description ?? __optique_core_message.message`Log output destination. Use ${"-"} for console.`;
307
340
  if (options.short) {
308
341
  const short = options.short;
309
- return (0, __optique_core_modifiers.optional)((0, __optique_core_primitives.option)(short, long, valueParser, { description }));
342
+ const outputParser$1 = (0, __optique_core_modifiers.optional)((0, __optique_core_primitives.option)(short, long, valueParser, { description }));
343
+ return withFormatter(outputParser$1, options.formatter);
310
344
  }
311
- return (0, __optique_core_modifiers.optional)((0, __optique_core_primitives.option)(long, valueParser, { description }));
345
+ const outputParser = (0, __optique_core_modifiers.optional)((0, __optique_core_primitives.option)(long, valueParser, { description }));
346
+ return withFormatter(outputParser, options.formatter);
347
+ }
348
+ function withFormatter(outputParser, formatter) {
349
+ if (formatter == null) return outputParser;
350
+ if (typeof formatter !== "string") return outputParser.map((output) => output == null ? void 0 : {
351
+ ...output,
352
+ formatter
353
+ });
354
+ const formatterParser = createTextFormatterOption(formatter);
355
+ return (0, __optique_core_constructs.object)({
356
+ output: outputParser,
357
+ formatter: formatterParser
358
+ }).map(({ output, formatter: formatter$1 }) => {
359
+ if (formatter$1 == null) return output;
360
+ return {
361
+ ...output ?? { type: "console" },
362
+ formatter: formatter$1
363
+ };
364
+ });
365
+ }
366
+ /**
367
+ * Creates an optional parser for a text formatter option.
368
+ *
369
+ * @param long The long option name for selecting the formatter.
370
+ * @returns A parser that produces the selected {@link TextFormatter}, or
371
+ * `undefined` when the option is not present.
372
+ * @throws {TypeError} If `long` is not a valid option name.
373
+ * @since 1.2.0
374
+ */
375
+ function createTextFormatterOption(long) {
376
+ return (0, __optique_core_modifiers.optional)((0, __optique_core_primitives.option)(long, textFormatter(), { description: __optique_core_message.message`Log output format.` }));
312
377
  }
313
378
  /**
314
379
  * Creates a console sink with configurable stream selection.
@@ -346,6 +411,7 @@ function logOutput(options = {}) {
346
411
  function createConsoleSink(options = {}) {
347
412
  const streamResolver = options.streamResolver;
348
413
  const defaultStream = options.stream ?? "stderr";
414
+ const formatter = options.formatter ?? defaultConsoleFormatter;
349
415
  const invalidStreamError = (value) => {
350
416
  let repr;
351
417
  if (typeof value === "string") repr = JSON.stringify(value);
@@ -361,22 +427,30 @@ function createConsoleSink(options = {}) {
361
427
  return (record) => {
362
428
  const stream = streamResolver ? streamResolver(record.level) : defaultStream;
363
429
  if (stream !== "stdout" && stream !== "stderr") throw invalidStreamError(stream);
364
- const messageParts = [];
365
- for (let i = 0; i < record.message.length; i++) {
366
- const part = record.message[i];
367
- if (typeof part === "string") messageParts.push(part);
368
- else messageParts.push(String(part));
369
- }
370
- const formattedMessage = messageParts.join("");
371
- const ts = record.timestamp;
372
- const timestamp = new Date(ts != null && !Number.isNaN(ts) ? ts : Date.now()).toISOString();
373
- const category = record.category.join(".");
374
- const level = record.level.toUpperCase().padEnd(7);
375
- const line = `${timestamp} [${level}] ${category}: ${formattedMessage}`;
376
- if (stream === "stderr") console.error(line);
377
- else console.log(line);
430
+ const args = toConsoleArgs(formatter(record));
431
+ if (stream === "stderr") console.error(...args);
432
+ else console.log(...args);
378
433
  };
379
434
  }
435
+ function toConsoleArgs(value) {
436
+ if (typeof value === "string") return [value.replace(/\r?\n$/, "")];
437
+ if (Array.isArray(value)) return value;
438
+ return value == null ? [] : [value];
439
+ }
440
+ function defaultConsoleFormatter(record) {
441
+ const messageParts = [];
442
+ for (let i = 0; i < record.message.length; i++) {
443
+ const part = record.message[i];
444
+ if (typeof part === "string") messageParts.push(part);
445
+ else messageParts.push(String(part));
446
+ }
447
+ const formattedMessage = messageParts.join("");
448
+ const ts = record.timestamp;
449
+ const timestamp = new Date(ts != null && !Number.isNaN(ts) ? ts : Date.now()).toISOString();
450
+ const category = record.category.join(".");
451
+ const level = record.level.toUpperCase().padEnd(7);
452
+ return `${timestamp} [${level}] ${category}: ${formattedMessage}`;
453
+ }
380
454
  /**
381
455
  * Creates a sink from a {@link LogOutput} destination.
382
456
  *
@@ -409,7 +483,10 @@ function createConsoleSink(options = {}) {
409
483
  * @since 0.8.0
410
484
  */
411
485
  async function createSink(output, consoleSinkOptions = {}) {
412
- if (output.type === "console") return createConsoleSink(consoleSinkOptions);
486
+ if (output.type === "console") return createConsoleSink({
487
+ ...consoleSinkOptions,
488
+ formatter: consoleSinkOptions.formatter ?? output.formatter
489
+ });
413
490
  let getFileSink;
414
491
  try {
415
492
  ({getFileSink} = await import("@logtape/file"));
@@ -421,7 +498,7 @@ async function createSink(output, consoleSinkOptions = {}) {
421
498
 
422
499
  Original error: ${e}`);
423
500
  }
424
- return getFileSink(output.path);
501
+ return getFileSink(output.path, output.formatter == null ? void 0 : { formatter: output.formatter });
425
502
  }
426
503
 
427
504
  //#endregion
@@ -482,6 +559,7 @@ function loggingOptions(config) {
482
559
  const groupLabel = config.groupLabel ?? "Logging options";
483
560
  const outputEnabled = config.output?.enabled !== false;
484
561
  const outputLong = config.output?.long ?? "--log-output";
562
+ const outputFormatter = config.formatter;
485
563
  let levelParser;
486
564
  switch (config.level) {
487
565
  case "option": {
@@ -513,35 +591,40 @@ function loggingOptions(config) {
513
591
  }
514
592
  default: throw new TypeError(`Unsupported level configuration: ${String(config.level)}. Expected "option", "verbosity", or "debug".`);
515
593
  }
516
- const defaultOutput = { type: "console" };
517
- const outputParser = (0, __optique_core_modifiers.withDefault)(logOutput({ long: outputLong }), defaultOutput);
518
- if (!outputEnabled) {
519
- const constantOutputParser = {
520
- mode: "sync",
521
- $valueType: [],
522
- $stateType: [],
523
- priority: 0,
524
- usage: [],
525
- leadingNames: /* @__PURE__ */ new Set(),
526
- acceptingAnyToken: false,
527
- initialState: void 0,
528
- parse: (context) => ({
529
- success: true,
530
- next: context,
531
- consumed: []
532
- }),
533
- complete: () => ({
534
- success: true,
535
- value: defaultOutput
536
- }),
537
- *suggest() {},
538
- getDocFragments: () => ({ fragments: [] })
539
- };
540
- return (0, __optique_core_constructs.group)(groupLabel, (0, __optique_core_constructs.object)({
541
- logLevel: levelParser,
542
- logOutput: constantOutputParser
543
- }));
544
- }
594
+ const defaultOutput = typeof outputFormatter === "function" ? {
595
+ type: "console",
596
+ formatter: outputFormatter
597
+ } : { type: "console" };
598
+ let outputParser;
599
+ if (outputEnabled) outputParser = (0, __optique_core_modifiers.withDefault)(logOutput({
600
+ long: outputLong,
601
+ formatter: outputFormatter
602
+ }), defaultOutput);
603
+ else if (typeof outputFormatter === "string") outputParser = createTextFormatterOption(outputFormatter).map((formatter) => formatter == null ? defaultOutput : {
604
+ type: "console",
605
+ formatter
606
+ });
607
+ else outputParser = {
608
+ mode: "sync",
609
+ $valueType: [],
610
+ $stateType: [],
611
+ priority: 0,
612
+ usage: [],
613
+ leadingNames: /* @__PURE__ */ new Set(),
614
+ acceptingAnyToken: false,
615
+ initialState: void 0,
616
+ parse: (context) => ({
617
+ success: true,
618
+ next: context,
619
+ consumed: []
620
+ }),
621
+ complete: () => ({
622
+ success: true,
623
+ value: defaultOutput
624
+ }),
625
+ *suggest() {},
626
+ getDocFragments: () => ({ fragments: [] })
627
+ };
545
628
  const innerParser = (0, __optique_core_constructs.object)({
546
629
  logLevel: levelParser,
547
630
  logOutput: outputParser
@@ -620,4 +703,5 @@ exports.debug = debug;
620
703
  exports.logLevel = logLevel;
621
704
  exports.logOutput = logOutput;
622
705
  exports.loggingOptions = loggingOptions;
706
+ exports.textFormatter = textFormatter;
623
707
  exports.verbosity = verbosity;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { Config, LogLevel, LogLevel as LogLevel$1, LogRecord, Sink, Sink as Sink$1 } from "@logtape/logtape";
1
+ import { Config, ConsoleFormatter, LogLevel, LogLevel as LogLevel$1, LogRecord, Sink, Sink as Sink$1, TextFormatter } from "@logtape/logtape";
2
2
  import { NonEmptyString, ValueParser } from "@optique/core/valueparser";
3
3
  import { Message } from "@optique/core/message";
4
4
  import { FluentParser } from "@optique/core/fluent";
@@ -73,6 +73,34 @@ interface LogLevelOptions {
73
73
  */
74
74
  declare function logLevel(options?: LogLevelOptions): ValueParser<"sync", LogLevel$1>;
75
75
  //#endregion
76
+ //#region src/textformatter.d.ts
77
+ /**
78
+ * The names accepted by {@link textFormatter}.
79
+ * @since 1.2.0
80
+ */
81
+ type TextFormatterName = "jsonl" | "logfmt" | "color" | "plain";
82
+ /**
83
+ * Creates a {@link ValueParser} for LogTape text formatters.
84
+ *
85
+ * This parser accepts `"jsonl"`, `"logfmt"`, `"color"`, and `"plain"` and
86
+ * maps them to LogTape's `jsonLinesFormatter`, `logfmtFormatter`,
87
+ * `ansiColorFormatter`, and `defaultTextFormatter` respectively.
88
+ *
89
+ * @returns A {@link ValueParser} that converts formatter names to
90
+ * LogTape text formatter functions.
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * import { option } from "@optique/core";
95
+ * import { textFormatter } from "@optique/logtape";
96
+ *
97
+ * const parser = option("--log-format", textFormatter());
98
+ * ```
99
+ *
100
+ * @since 1.2.0
101
+ */
102
+ declare function textFormatter(): ValueParser<"sync", TextFormatter>;
103
+ //#endregion
76
104
  //#region src/verbosity.d.ts
77
105
  /**
78
106
  * Options for creating a verbosity parser.
@@ -229,9 +257,11 @@ declare function debug(options?: DebugOptions): FluentParser<"sync", LogLevel$1,
229
257
  */
230
258
  type LogOutput = {
231
259
  readonly type: "console";
260
+ readonly formatter?: TextFormatter;
232
261
  } | {
233
262
  readonly type: "file";
234
263
  readonly path: string;
264
+ readonly formatter?: TextFormatter;
235
265
  };
236
266
  /**
237
267
  * Options for configuring console sink creation.
@@ -258,6 +288,16 @@ interface ConsoleSinkOptions {
258
288
  * ```
259
289
  */
260
290
  readonly streamResolver?: (level: LogLevel$1) => "stdout" | "stderr";
291
+ /**
292
+ * A formatter for converting log records to console output.
293
+ * Text formatters return one string argument, while console formatters
294
+ * return the full argument list passed to the selected console method.
295
+ *
296
+ * If omitted, records are formatted as
297
+ * `ISO_TIMESTAMP [LEVEL] category: message`.
298
+ * @since 1.2.0
299
+ */
300
+ readonly formatter?: TextFormatter | ConsoleFormatter;
261
301
  }
262
302
  /**
263
303
  * Options for creating a log output parser.
@@ -282,6 +322,26 @@ interface LogOutputOptions {
282
322
  * Description to show in help text.
283
323
  */
284
324
  readonly description?: Message;
325
+ /**
326
+ * Text formatter to apply to the selected log output, or a long option name
327
+ * for selecting the text formatter from the command line.
328
+ *
329
+ * When a string is specified, this adds an option that accepts `"jsonl"`,
330
+ * `"logfmt"`, `"color"`, and `"plain"` and stores the selected formatter in
331
+ * the resulting {@link LogOutput}. If the formatter option is specified
332
+ * without a log output option, the output defaults to console.
333
+ *
334
+ * When a formatter function is specified, it is applied to the resulting
335
+ * {@link LogOutput} only when the log output option itself is present.
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * logOutput({ formatter: "--log-format" })
340
+ * ```
341
+ *
342
+ * @since 1.2.0
343
+ */
344
+ readonly formatter?: string | TextFormatter;
285
345
  /**
286
346
  * Custom error messages.
287
347
  */
@@ -318,6 +378,16 @@ interface LogOutputOptions {
318
378
  * @since 0.8.0
319
379
  */
320
380
  declare function logOutput(options?: LogOutputOptions): FluentParser<"sync", LogOutput | undefined, unknown>;
381
+ /**
382
+ * Creates an optional parser for a text formatter option.
383
+ *
384
+ * @param long The long option name for selecting the formatter.
385
+ * @returns A parser that produces the selected {@link TextFormatter}, or
386
+ * `undefined` when the option is not present.
387
+ * @throws {TypeError} If `long` is not a valid option name.
388
+ * @since 1.2.0
389
+ */
390
+
321
391
  /**
322
392
  * Creates a console sink with configurable stream selection.
323
393
  *
@@ -444,6 +514,13 @@ interface LoggingOptionsWithLevel {
444
514
  * Configuration for log output option.
445
515
  */
446
516
  readonly output?: LogOutputConfig;
517
+ /**
518
+ * Text formatter to apply to the generated sink, or a long option name for
519
+ * selecting the text formatter from the command line.
520
+ *
521
+ * @since 1.2.0
522
+ */
523
+ readonly formatter?: string | TextFormatter;
447
524
  /**
448
525
  * Label for the option group in help text.
449
526
  * @default `"Logging options"`
@@ -478,6 +555,13 @@ interface LoggingOptionsWithVerbosity {
478
555
  * Configuration for log output option.
479
556
  */
480
557
  readonly output?: LogOutputConfig;
558
+ /**
559
+ * Text formatter to apply to the generated sink, or a long option name for
560
+ * selecting the text formatter from the command line.
561
+ *
562
+ * @since 1.2.0
563
+ */
564
+ readonly formatter?: string | TextFormatter;
481
565
  /**
482
566
  * Label for the option group in help text.
483
567
  * @default `"Logging options"`
@@ -517,6 +601,13 @@ interface LoggingOptionsWithDebug {
517
601
  * Configuration for log output option.
518
602
  */
519
603
  readonly output?: LogOutputConfig;
604
+ /**
605
+ * Text formatter to apply to the generated sink, or a long option name for
606
+ * selecting the text formatter from the command line.
607
+ *
608
+ * @since 1.2.0
609
+ */
610
+ readonly formatter?: string | TextFormatter;
520
611
  /**
521
612
  * Label for the option group in help text.
522
613
  * @default `"Logging options"`
@@ -637,4 +728,4 @@ declare function loggingOptions(config: LoggingOptionsConfig): FluentParser<"syn
637
728
  */
638
729
  declare function createLoggingConfig(options: LoggingOptionsResult, consoleSinkOptions?: ConsoleSinkOptions, additionalConfig?: Partial<Config<string, string>>): Promise<Config<string, string>>;
639
730
  //#endregion
640
- export { type ConsoleSinkOptions, type DebugOptions, LOG_LEVELS, type LogLevel, type LogLevelOptions, type LogOutput, type LogOutputConfig, type LogOutputOptions, type LogRecord, type LoggingOptionsConfig, type LoggingOptionsResult, type LoggingOptionsWithDebug, type LoggingOptionsWithLevel, type LoggingOptionsWithVerbosity, type Sink, type VerbosityOptions, createConsoleSink, createLoggingConfig, createSink, debug, logLevel, logOutput, loggingOptions, verbosity };
731
+ export { type ConsoleSinkOptions, type DebugOptions, LOG_LEVELS, type LogLevel, type LogLevelOptions, type LogOutput, type LogOutputConfig, type LogOutputOptions, type LogRecord, type LoggingOptionsConfig, type LoggingOptionsResult, type LoggingOptionsWithDebug, type LoggingOptionsWithLevel, type LoggingOptionsWithVerbosity, type Sink, type TextFormatterName, type VerbosityOptions, createConsoleSink, createLoggingConfig, createSink, debug, logLevel, logOutput, loggingOptions, textFormatter, verbosity };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { NonEmptyString, ValueParser } from "@optique/core/valueparser";
2
+ import { Config, ConsoleFormatter, LogLevel, LogLevel as LogLevel$1, LogRecord, Sink, Sink as Sink$1, TextFormatter } from "@logtape/logtape";
2
3
  import { Message } from "@optique/core/message";
3
- import { Config, LogLevel, LogLevel as LogLevel$1, LogRecord, Sink, Sink as Sink$1 } from "@logtape/logtape";
4
4
  import { FluentParser } from "@optique/core/fluent";
5
5
 
6
6
  //#region src/loglevel.d.ts
@@ -73,6 +73,34 @@ interface LogLevelOptions {
73
73
  */
74
74
  declare function logLevel(options?: LogLevelOptions): ValueParser<"sync", LogLevel$1>;
75
75
  //#endregion
76
+ //#region src/textformatter.d.ts
77
+ /**
78
+ * The names accepted by {@link textFormatter}.
79
+ * @since 1.2.0
80
+ */
81
+ type TextFormatterName = "jsonl" | "logfmt" | "color" | "plain";
82
+ /**
83
+ * Creates a {@link ValueParser} for LogTape text formatters.
84
+ *
85
+ * This parser accepts `"jsonl"`, `"logfmt"`, `"color"`, and `"plain"` and
86
+ * maps them to LogTape's `jsonLinesFormatter`, `logfmtFormatter`,
87
+ * `ansiColorFormatter`, and `defaultTextFormatter` respectively.
88
+ *
89
+ * @returns A {@link ValueParser} that converts formatter names to
90
+ * LogTape text formatter functions.
91
+ *
92
+ * @example
93
+ * ```typescript
94
+ * import { option } from "@optique/core";
95
+ * import { textFormatter } from "@optique/logtape";
96
+ *
97
+ * const parser = option("--log-format", textFormatter());
98
+ * ```
99
+ *
100
+ * @since 1.2.0
101
+ */
102
+ declare function textFormatter(): ValueParser<"sync", TextFormatter>;
103
+ //#endregion
76
104
  //#region src/verbosity.d.ts
77
105
  /**
78
106
  * Options for creating a verbosity parser.
@@ -229,9 +257,11 @@ declare function debug(options?: DebugOptions): FluentParser<"sync", LogLevel$1,
229
257
  */
230
258
  type LogOutput = {
231
259
  readonly type: "console";
260
+ readonly formatter?: TextFormatter;
232
261
  } | {
233
262
  readonly type: "file";
234
263
  readonly path: string;
264
+ readonly formatter?: TextFormatter;
235
265
  };
236
266
  /**
237
267
  * Options for configuring console sink creation.
@@ -258,6 +288,16 @@ interface ConsoleSinkOptions {
258
288
  * ```
259
289
  */
260
290
  readonly streamResolver?: (level: LogLevel$1) => "stdout" | "stderr";
291
+ /**
292
+ * A formatter for converting log records to console output.
293
+ * Text formatters return one string argument, while console formatters
294
+ * return the full argument list passed to the selected console method.
295
+ *
296
+ * If omitted, records are formatted as
297
+ * `ISO_TIMESTAMP [LEVEL] category: message`.
298
+ * @since 1.2.0
299
+ */
300
+ readonly formatter?: TextFormatter | ConsoleFormatter;
261
301
  }
262
302
  /**
263
303
  * Options for creating a log output parser.
@@ -282,6 +322,26 @@ interface LogOutputOptions {
282
322
  * Description to show in help text.
283
323
  */
284
324
  readonly description?: Message;
325
+ /**
326
+ * Text formatter to apply to the selected log output, or a long option name
327
+ * for selecting the text formatter from the command line.
328
+ *
329
+ * When a string is specified, this adds an option that accepts `"jsonl"`,
330
+ * `"logfmt"`, `"color"`, and `"plain"` and stores the selected formatter in
331
+ * the resulting {@link LogOutput}. If the formatter option is specified
332
+ * without a log output option, the output defaults to console.
333
+ *
334
+ * When a formatter function is specified, it is applied to the resulting
335
+ * {@link LogOutput} only when the log output option itself is present.
336
+ *
337
+ * @example
338
+ * ```typescript
339
+ * logOutput({ formatter: "--log-format" })
340
+ * ```
341
+ *
342
+ * @since 1.2.0
343
+ */
344
+ readonly formatter?: string | TextFormatter;
285
345
  /**
286
346
  * Custom error messages.
287
347
  */
@@ -318,6 +378,16 @@ interface LogOutputOptions {
318
378
  * @since 0.8.0
319
379
  */
320
380
  declare function logOutput(options?: LogOutputOptions): FluentParser<"sync", LogOutput | undefined, unknown>;
381
+ /**
382
+ * Creates an optional parser for a text formatter option.
383
+ *
384
+ * @param long The long option name for selecting the formatter.
385
+ * @returns A parser that produces the selected {@link TextFormatter}, or
386
+ * `undefined` when the option is not present.
387
+ * @throws {TypeError} If `long` is not a valid option name.
388
+ * @since 1.2.0
389
+ */
390
+
321
391
  /**
322
392
  * Creates a console sink with configurable stream selection.
323
393
  *
@@ -444,6 +514,13 @@ interface LoggingOptionsWithLevel {
444
514
  * Configuration for log output option.
445
515
  */
446
516
  readonly output?: LogOutputConfig;
517
+ /**
518
+ * Text formatter to apply to the generated sink, or a long option name for
519
+ * selecting the text formatter from the command line.
520
+ *
521
+ * @since 1.2.0
522
+ */
523
+ readonly formatter?: string | TextFormatter;
447
524
  /**
448
525
  * Label for the option group in help text.
449
526
  * @default `"Logging options"`
@@ -478,6 +555,13 @@ interface LoggingOptionsWithVerbosity {
478
555
  * Configuration for log output option.
479
556
  */
480
557
  readonly output?: LogOutputConfig;
558
+ /**
559
+ * Text formatter to apply to the generated sink, or a long option name for
560
+ * selecting the text formatter from the command line.
561
+ *
562
+ * @since 1.2.0
563
+ */
564
+ readonly formatter?: string | TextFormatter;
481
565
  /**
482
566
  * Label for the option group in help text.
483
567
  * @default `"Logging options"`
@@ -517,6 +601,13 @@ interface LoggingOptionsWithDebug {
517
601
  * Configuration for log output option.
518
602
  */
519
603
  readonly output?: LogOutputConfig;
604
+ /**
605
+ * Text formatter to apply to the generated sink, or a long option name for
606
+ * selecting the text formatter from the command line.
607
+ *
608
+ * @since 1.2.0
609
+ */
610
+ readonly formatter?: string | TextFormatter;
520
611
  /**
521
612
  * Label for the option group in help text.
522
613
  * @default `"Logging options"`
@@ -637,4 +728,4 @@ declare function loggingOptions(config: LoggingOptionsConfig): FluentParser<"syn
637
728
  */
638
729
  declare function createLoggingConfig(options: LoggingOptionsResult, consoleSinkOptions?: ConsoleSinkOptions, additionalConfig?: Partial<Config<string, string>>): Promise<Config<string, string>>;
639
730
  //#endregion
640
- export { type ConsoleSinkOptions, type DebugOptions, LOG_LEVELS, type LogLevel, type LogLevelOptions, type LogOutput, type LogOutputConfig, type LogOutputOptions, type LogRecord, type LoggingOptionsConfig, type LoggingOptionsResult, type LoggingOptionsWithDebug, type LoggingOptionsWithLevel, type LoggingOptionsWithVerbosity, type Sink, type VerbosityOptions, createConsoleSink, createLoggingConfig, createSink, debug, logLevel, logOutput, loggingOptions, verbosity };
731
+ export { type ConsoleSinkOptions, type DebugOptions, LOG_LEVELS, type LogLevel, type LogLevelOptions, type LogOutput, type LogOutputConfig, type LogOutputOptions, type LogRecord, type LoggingOptionsConfig, type LoggingOptionsResult, type LoggingOptionsWithDebug, type LoggingOptionsWithLevel, type LoggingOptionsWithVerbosity, type Sink, type TextFormatterName, type VerbosityOptions, createConsoleSink, createLoggingConfig, createSink, debug, logLevel, logOutput, loggingOptions, textFormatter, verbosity };
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
- import { choice } from "@optique/core/valueparser";
1
+ import { biject, choice } from "@optique/core/valueparser";
2
+ import { ansiColorFormatter, defaultTextFormatter, jsonLinesFormatter, logfmtFormatter } from "@logtape/logtape";
2
3
  import { flag, option } from "@optique/core/primitives";
3
4
  import { map, multiple, optional, withDefault } from "@optique/core/modifiers";
4
5
  import { message } from "@optique/core/message";
5
6
  import { basename } from "node:path";
6
- import { ensureNonEmptyString } from "@optique/core/nonempty";
7
7
  import { group, object } from "@optique/core/constructs";
8
+ import { ensureNonEmptyString } from "@optique/core/nonempty";
8
9
 
9
10
  //#region src/loglevel.ts
10
11
  /**
@@ -69,6 +70,38 @@ function logLevel(options = {}) {
69
70
  });
70
71
  }
71
72
 
73
+ //#endregion
74
+ //#region src/textformatter.ts
75
+ const textFormatters = {
76
+ jsonl: jsonLinesFormatter,
77
+ logfmt: logfmtFormatter,
78
+ color: ansiColorFormatter,
79
+ plain: defaultTextFormatter
80
+ };
81
+ /**
82
+ * Creates a {@link ValueParser} for LogTape text formatters.
83
+ *
84
+ * This parser accepts `"jsonl"`, `"logfmt"`, `"color"`, and `"plain"` and
85
+ * maps them to LogTape's `jsonLinesFormatter`, `logfmtFormatter`,
86
+ * `ansiColorFormatter`, and `defaultTextFormatter` respectively.
87
+ *
88
+ * @returns A {@link ValueParser} that converts formatter names to
89
+ * LogTape text formatter functions.
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * import { option } from "@optique/core";
94
+ * import { textFormatter } from "@optique/logtape";
95
+ *
96
+ * const parser = option("--log-format", textFormatter());
97
+ * ```
98
+ *
99
+ * @since 1.2.0
100
+ */
101
+ function textFormatter() {
102
+ return biject(textFormatters);
103
+ }
104
+
72
105
  //#endregion
73
106
  //#region src/verbosity.ts
74
107
  /**
@@ -283,9 +316,41 @@ function logOutput(options = {}) {
283
316
  const description = options.description ?? message`Log output destination. Use ${"-"} for console.`;
284
317
  if (options.short) {
285
318
  const short = options.short;
286
- return optional(option(short, long, valueParser, { description }));
319
+ const outputParser$1 = optional(option(short, long, valueParser, { description }));
320
+ return withFormatter(outputParser$1, options.formatter);
287
321
  }
288
- return optional(option(long, valueParser, { description }));
322
+ const outputParser = optional(option(long, valueParser, { description }));
323
+ return withFormatter(outputParser, options.formatter);
324
+ }
325
+ function withFormatter(outputParser, formatter) {
326
+ if (formatter == null) return outputParser;
327
+ if (typeof formatter !== "string") return outputParser.map((output) => output == null ? void 0 : {
328
+ ...output,
329
+ formatter
330
+ });
331
+ const formatterParser = createTextFormatterOption(formatter);
332
+ return object({
333
+ output: outputParser,
334
+ formatter: formatterParser
335
+ }).map(({ output, formatter: formatter$1 }) => {
336
+ if (formatter$1 == null) return output;
337
+ return {
338
+ ...output ?? { type: "console" },
339
+ formatter: formatter$1
340
+ };
341
+ });
342
+ }
343
+ /**
344
+ * Creates an optional parser for a text formatter option.
345
+ *
346
+ * @param long The long option name for selecting the formatter.
347
+ * @returns A parser that produces the selected {@link TextFormatter}, or
348
+ * `undefined` when the option is not present.
349
+ * @throws {TypeError} If `long` is not a valid option name.
350
+ * @since 1.2.0
351
+ */
352
+ function createTextFormatterOption(long) {
353
+ return optional(option(long, textFormatter(), { description: message`Log output format.` }));
289
354
  }
290
355
  /**
291
356
  * Creates a console sink with configurable stream selection.
@@ -323,6 +388,7 @@ function logOutput(options = {}) {
323
388
  function createConsoleSink(options = {}) {
324
389
  const streamResolver = options.streamResolver;
325
390
  const defaultStream = options.stream ?? "stderr";
391
+ const formatter = options.formatter ?? defaultConsoleFormatter;
326
392
  const invalidStreamError = (value) => {
327
393
  let repr;
328
394
  if (typeof value === "string") repr = JSON.stringify(value);
@@ -338,22 +404,30 @@ function createConsoleSink(options = {}) {
338
404
  return (record) => {
339
405
  const stream = streamResolver ? streamResolver(record.level) : defaultStream;
340
406
  if (stream !== "stdout" && stream !== "stderr") throw invalidStreamError(stream);
341
- const messageParts = [];
342
- for (let i = 0; i < record.message.length; i++) {
343
- const part = record.message[i];
344
- if (typeof part === "string") messageParts.push(part);
345
- else messageParts.push(String(part));
346
- }
347
- const formattedMessage = messageParts.join("");
348
- const ts = record.timestamp;
349
- const timestamp = new Date(ts != null && !Number.isNaN(ts) ? ts : Date.now()).toISOString();
350
- const category = record.category.join(".");
351
- const level = record.level.toUpperCase().padEnd(7);
352
- const line = `${timestamp} [${level}] ${category}: ${formattedMessage}`;
353
- if (stream === "stderr") console.error(line);
354
- else console.log(line);
407
+ const args = toConsoleArgs(formatter(record));
408
+ if (stream === "stderr") console.error(...args);
409
+ else console.log(...args);
355
410
  };
356
411
  }
412
+ function toConsoleArgs(value) {
413
+ if (typeof value === "string") return [value.replace(/\r?\n$/, "")];
414
+ if (Array.isArray(value)) return value;
415
+ return value == null ? [] : [value];
416
+ }
417
+ function defaultConsoleFormatter(record) {
418
+ const messageParts = [];
419
+ for (let i = 0; i < record.message.length; i++) {
420
+ const part = record.message[i];
421
+ if (typeof part === "string") messageParts.push(part);
422
+ else messageParts.push(String(part));
423
+ }
424
+ const formattedMessage = messageParts.join("");
425
+ const ts = record.timestamp;
426
+ const timestamp = new Date(ts != null && !Number.isNaN(ts) ? ts : Date.now()).toISOString();
427
+ const category = record.category.join(".");
428
+ const level = record.level.toUpperCase().padEnd(7);
429
+ return `${timestamp} [${level}] ${category}: ${formattedMessage}`;
430
+ }
357
431
  /**
358
432
  * Creates a sink from a {@link LogOutput} destination.
359
433
  *
@@ -386,7 +460,10 @@ function createConsoleSink(options = {}) {
386
460
  * @since 0.8.0
387
461
  */
388
462
  async function createSink(output, consoleSinkOptions = {}) {
389
- if (output.type === "console") return createConsoleSink(consoleSinkOptions);
463
+ if (output.type === "console") return createConsoleSink({
464
+ ...consoleSinkOptions,
465
+ formatter: consoleSinkOptions.formatter ?? output.formatter
466
+ });
390
467
  let getFileSink;
391
468
  try {
392
469
  ({getFileSink} = await import("@logtape/file"));
@@ -398,7 +475,7 @@ async function createSink(output, consoleSinkOptions = {}) {
398
475
 
399
476
  Original error: ${e}`);
400
477
  }
401
- return getFileSink(output.path);
478
+ return getFileSink(output.path, output.formatter == null ? void 0 : { formatter: output.formatter });
402
479
  }
403
480
 
404
481
  //#endregion
@@ -459,6 +536,7 @@ function loggingOptions(config) {
459
536
  const groupLabel = config.groupLabel ?? "Logging options";
460
537
  const outputEnabled = config.output?.enabled !== false;
461
538
  const outputLong = config.output?.long ?? "--log-output";
539
+ const outputFormatter = config.formatter;
462
540
  let levelParser;
463
541
  switch (config.level) {
464
542
  case "option": {
@@ -490,35 +568,40 @@ function loggingOptions(config) {
490
568
  }
491
569
  default: throw new TypeError(`Unsupported level configuration: ${String(config.level)}. Expected "option", "verbosity", or "debug".`);
492
570
  }
493
- const defaultOutput = { type: "console" };
494
- const outputParser = withDefault(logOutput({ long: outputLong }), defaultOutput);
495
- if (!outputEnabled) {
496
- const constantOutputParser = {
497
- mode: "sync",
498
- $valueType: [],
499
- $stateType: [],
500
- priority: 0,
501
- usage: [],
502
- leadingNames: /* @__PURE__ */ new Set(),
503
- acceptingAnyToken: false,
504
- initialState: void 0,
505
- parse: (context) => ({
506
- success: true,
507
- next: context,
508
- consumed: []
509
- }),
510
- complete: () => ({
511
- success: true,
512
- value: defaultOutput
513
- }),
514
- *suggest() {},
515
- getDocFragments: () => ({ fragments: [] })
516
- };
517
- return group(groupLabel, object({
518
- logLevel: levelParser,
519
- logOutput: constantOutputParser
520
- }));
521
- }
571
+ const defaultOutput = typeof outputFormatter === "function" ? {
572
+ type: "console",
573
+ formatter: outputFormatter
574
+ } : { type: "console" };
575
+ let outputParser;
576
+ if (outputEnabled) outputParser = withDefault(logOutput({
577
+ long: outputLong,
578
+ formatter: outputFormatter
579
+ }), defaultOutput);
580
+ else if (typeof outputFormatter === "string") outputParser = createTextFormatterOption(outputFormatter).map((formatter) => formatter == null ? defaultOutput : {
581
+ type: "console",
582
+ formatter
583
+ });
584
+ else outputParser = {
585
+ mode: "sync",
586
+ $valueType: [],
587
+ $stateType: [],
588
+ priority: 0,
589
+ usage: [],
590
+ leadingNames: /* @__PURE__ */ new Set(),
591
+ acceptingAnyToken: false,
592
+ initialState: void 0,
593
+ parse: (context) => ({
594
+ success: true,
595
+ next: context,
596
+ consumed: []
597
+ }),
598
+ complete: () => ({
599
+ success: true,
600
+ value: defaultOutput
601
+ }),
602
+ *suggest() {},
603
+ getDocFragments: () => ({ fragments: [] })
604
+ };
522
605
  const innerParser = object({
523
606
  logLevel: levelParser,
524
607
  logOutput: outputParser
@@ -589,4 +672,4 @@ async function createLoggingConfig(options, consoleSinkOptions = {}, additionalC
589
672
  }
590
673
 
591
674
  //#endregion
592
- export { LOG_LEVELS, createConsoleSink, createLoggingConfig, createSink, debug, logLevel, logOutput, loggingOptions, verbosity };
675
+ export { LOG_LEVELS, createConsoleSink, createLoggingConfig, createSink, debug, logLevel, logOutput, loggingOptions, textFormatter, verbosity };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/logtape",
3
- "version": "1.2.0-dev.2324",
3
+ "version": "1.2.0-dev.2330",
4
4
  "description": "LogTape logging integration for Optique CLI parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -60,8 +60,8 @@
60
60
  },
61
61
  "sideEffects": false,
62
62
  "peerDependencies": {
63
- "@logtape/file": "^2.0.4",
64
- "@logtape/logtape": "^2.0.4"
63
+ "@logtape/file": "^2.2.2",
64
+ "@logtape/logtape": "^2.2.2"
65
65
  },
66
66
  "peerDependenciesMeta": {
67
67
  "@logtape/file": {
@@ -69,11 +69,11 @@
69
69
  }
70
70
  },
71
71
  "dependencies": {
72
- "@optique/core": "1.2.0-dev.2324+747f36a7"
72
+ "@optique/core": "1.2.0-dev.2330+6776ce17"
73
73
  },
74
74
  "devDependencies": {
75
- "@logtape/file": "^2.0.4",
76
- "@logtape/logtape": "^2.0.4",
75
+ "@logtape/file": "^2.2.2",
76
+ "@logtape/logtape": "^2.2.2",
77
77
  "@types/node": "^24.0.0",
78
78
  "tsdown": "^0.13.0",
79
79
  "typescript": "^5.8.3"