@optique/core 0.1.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/README.md +475 -0
- package/dist/doc.cjs +104 -0
- package/dist/doc.d.cts +129 -0
- package/dist/doc.d.ts +129 -0
- package/dist/doc.js +104 -0
- package/dist/facade.cjs +98 -0
- package/dist/facade.d.cts +113 -0
- package/dist/facade.d.ts +113 -0
- package/dist/facade.js +97 -0
- package/dist/index.cjs +42 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/message.cjs +202 -0
- package/dist/message.d.cts +199 -0
- package/dist/message.d.ts +199 -0
- package/dist/message.js +194 -0
- package/dist/parser.cjs +1065 -0
- package/dist/parser.d.cts +580 -0
- package/dist/parser.d.ts +580 -0
- package/dist/parser.js +1053 -0
- package/dist/usage.cjs +242 -0
- package/dist/usage.d.cts +217 -0
- package/dist/usage.d.ts +217 -0
- package/dist/usage.js +239 -0
- package/dist/valueparser.cjs +332 -0
- package/dist/valueparser.d.cts +332 -0
- package/dist/valueparser.d.ts +332 -0
- package/dist/valueparser.js +325 -0
- package/package.json +117 -0
package/README.md
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
Optique: Type-safe combinatorial CLI parser for TypeScript
|
|
2
|
+
==========================================================
|
|
3
|
+
|
|
4
|
+
> [!CAUTION]
|
|
5
|
+
> Optique is currently in early development for proof of concept purposes,
|
|
6
|
+
> and is not yet ready for production use. The API is subject to change,
|
|
7
|
+
> and there may be bugs or missing features.
|
|
8
|
+
|
|
9
|
+
Optique is a type-safe combinatorial CLI *parser* for TypeScript inspired by
|
|
10
|
+
Haskell's [optparse-applicative] and TypeScript's [Zod]. It provides composable
|
|
11
|
+
parsers for building command-line interfaces with full type safety and
|
|
12
|
+
automatic type inference.
|
|
13
|
+
|
|
14
|
+
> [!NOTE]
|
|
15
|
+
> Optique is a parsing library that focuses on extracting and
|
|
16
|
+
> validating command-line arguments. It doesn't dictate your application's
|
|
17
|
+
> structure, handle command execution, or provide scaffolding—it simply
|
|
18
|
+
> transforms command-line input into well-typed data structures that your
|
|
19
|
+
> application can use.
|
|
20
|
+
|
|
21
|
+
Unlike traditional CLI parsers that rely on configuration objects or
|
|
22
|
+
string-based definitions, Optique uses a functional approach where parsers
|
|
23
|
+
are first-class values that can be combined, transformed, and reused.
|
|
24
|
+
This compositional design makes it easy to express complex argument structures
|
|
25
|
+
while maintaining complete type safety throughout your application.
|
|
26
|
+
|
|
27
|
+
> [!TIP]
|
|
28
|
+
> *Building CLI apps?* Consider *@optique/run* for automatic `process.argv`
|
|
29
|
+
> handling and `process.exit()` integration. This core package is perfect for
|
|
30
|
+
> libraries, web apps, or when you need full control over argument parsing.
|
|
31
|
+
|
|
32
|
+
[optparse-applicative]: https://github.com/pcapriotti/optparse-applicative
|
|
33
|
+
[Zod]: https://zod.dev/
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
Core concepts
|
|
37
|
+
-------------
|
|
38
|
+
|
|
39
|
+
Optique is built around several key concepts:
|
|
40
|
+
|
|
41
|
+
- *Value parsers* convert strings to typed values
|
|
42
|
+
(`string()`, `integer()`, `url()`, etc.)
|
|
43
|
+
|
|
44
|
+
- *Primitive parsers* handle the basic building blocks:
|
|
45
|
+
|
|
46
|
+
- `option()` for command-line flags and their arguments
|
|
47
|
+
- `argument()` for positional arguments
|
|
48
|
+
- `command()` for subcommands
|
|
49
|
+
|
|
50
|
+
- *Modifying combinators* transform and combine parsers:
|
|
51
|
+
- `optional()` makes parsers optional
|
|
52
|
+
- `withDefault()` provides default values for optional parsers
|
|
53
|
+
- `multiple()` allows repetition
|
|
54
|
+
- `or()` creates alternatives
|
|
55
|
+
- `merge()` combines object parsers
|
|
56
|
+
|
|
57
|
+
- *Construct combinators* build structured results:
|
|
58
|
+
|
|
59
|
+
- `object()` groups parsers into objects
|
|
60
|
+
- `tuple()` combines parsers into tuples
|
|
61
|
+
|
|
62
|
+
The library automatically infers the result type of your parser composition,
|
|
63
|
+
ensuring that your parsed CLI arguments are fully typed without manual type
|
|
64
|
+
annotations. When parsing fails, you get detailed error messages that help
|
|
65
|
+
users understand what went wrong.
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
Example
|
|
69
|
+
-------
|
|
70
|
+
|
|
71
|
+
~~~~ typescript
|
|
72
|
+
import { run } from "@optique/core/facade";
|
|
73
|
+
import { formatMessage } from "@optique/core/message";
|
|
74
|
+
import {
|
|
75
|
+
argument,
|
|
76
|
+
merge,
|
|
77
|
+
multiple,
|
|
78
|
+
object,
|
|
79
|
+
option,
|
|
80
|
+
optional,
|
|
81
|
+
or,
|
|
82
|
+
withDefault
|
|
83
|
+
} from "@optique/core/parser";
|
|
84
|
+
import { choice, integer, locale, string, url } from "@optique/core/valueparser";
|
|
85
|
+
import process from "node:process";
|
|
86
|
+
|
|
87
|
+
// Define a sophisticated CLI with grouped and reusable option sets
|
|
88
|
+
const networkOptions = object("Network", {
|
|
89
|
+
port: option("-p", "--port", integer({ min: 1, max: 65535 })),
|
|
90
|
+
host: withDefault(
|
|
91
|
+
option("-h", "--host", string({ metavar: "HOST" })),
|
|
92
|
+
"localhost",
|
|
93
|
+
),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const loggingOptions = object("Logging", {
|
|
97
|
+
verbose: optional(option("-v", "--verbose")),
|
|
98
|
+
logFile: optional(option("--log-file", string({ metavar: "FILE" }))),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const parser = or(
|
|
102
|
+
// Server mode: merge network and logging options with server-specific config
|
|
103
|
+
merge(
|
|
104
|
+
networkOptions,
|
|
105
|
+
loggingOptions,
|
|
106
|
+
object("Server", {
|
|
107
|
+
locales: multiple(option("-l", "--locale", locale())),
|
|
108
|
+
config: argument(string({ metavar: "CONFIG_FILE" })),
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
object("Client mode", {
|
|
112
|
+
connect: option(
|
|
113
|
+
"-c", "--connect",
|
|
114
|
+
url({ allowedProtocols: ["http:", "https:"] }),
|
|
115
|
+
),
|
|
116
|
+
headers: multiple(option("-H", "--header", string())),
|
|
117
|
+
timeout: withDefault(option("-t", "--timeout", integer({ min: 0 })), 30000),
|
|
118
|
+
files: multiple(argument(string({ metavar: "FILE" })), { min: 1, max: 5 }),
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const result = run(parser, "myapp", process.argv.slice(2), {
|
|
123
|
+
colors: true,
|
|
124
|
+
help: "both",
|
|
125
|
+
onError: process.exit,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// TypeScript automatically infers the complex union type with optional fields
|
|
129
|
+
if ("port" in result) {
|
|
130
|
+
const server = result;
|
|
131
|
+
console.log(`Starting server on ${server.host}:${server.port}`);
|
|
132
|
+
console.log(`Supported locales: ${server.locales.join(", ") || "default"}`);
|
|
133
|
+
if (server.verbose) console.log("Verbose mode enabled");
|
|
134
|
+
if (server.logFile) console.log(`Logging to: ${server.logFile}`);
|
|
135
|
+
} else {
|
|
136
|
+
const client = result;
|
|
137
|
+
console.log(`Connecting to ${client.connect}`);
|
|
138
|
+
console.log(`Processing ${client.files.length} files`);
|
|
139
|
+
console.log(`Timeout: ${client.timeout}ms`);
|
|
140
|
+
}
|
|
141
|
+
~~~~
|
|
142
|
+
|
|
143
|
+
This example demonstrates Optique's powerful combinators:
|
|
144
|
+
|
|
145
|
+
- **`merge()`** combines multiple `object()` parsers into a single unified
|
|
146
|
+
parser, enabling modular and reusable option groups
|
|
147
|
+
- **`optional()`** makes parsers optional, returning `undefined` when not
|
|
148
|
+
provided
|
|
149
|
+
- **`withDefault()`** provides default values instead of `undefined` for
|
|
150
|
+
optional parameters, improving usability
|
|
151
|
+
- **`multiple()`** allows repeating options/arguments with configurable
|
|
152
|
+
constraints
|
|
153
|
+
- **`or()`** creates mutually exclusive alternatives
|
|
154
|
+
- **`object()`** groups related options into structured data
|
|
155
|
+
|
|
156
|
+
The parser demonstrates modular design by:
|
|
157
|
+
|
|
158
|
+
- Separating network options (`--port`, `--host`) for reusability
|
|
159
|
+
- Grouping logging configuration (`--verbose`, `--log-file`) separately
|
|
160
|
+
- Merging reusable groups with server-specific options using `merge()`
|
|
161
|
+
- Supporting complex scenarios like multiple locales: `-l en-US -l fr-FR`
|
|
162
|
+
|
|
163
|
+
All with full type safety and automatic inference!
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
Parser combinators
|
|
167
|
+
------------------
|
|
168
|
+
|
|
169
|
+
Optique provides several types of parsers and combinators for building sophisticated CLI interfaces:
|
|
170
|
+
|
|
171
|
+
### Primitive parsers
|
|
172
|
+
|
|
173
|
+
- **`option()`**: Handles command-line flags and their arguments
|
|
174
|
+
|
|
175
|
+
~~~~ typescript
|
|
176
|
+
option("-p", "--port", integer({ min: 1, max: 65535 }))
|
|
177
|
+
option("-v", "--verbose") // Boolean flag
|
|
178
|
+
~~~~
|
|
179
|
+
|
|
180
|
+
- **`argument()`**: Handles positional arguments
|
|
181
|
+
|
|
182
|
+
~~~~ typescript
|
|
183
|
+
argument(string({ metavar: "FILE" }))
|
|
184
|
+
~~~~
|
|
185
|
+
|
|
186
|
+
- **`command()`**: Matches subcommands for `git`-like CLI interfaces
|
|
187
|
+
|
|
188
|
+
~~~~ typescript
|
|
189
|
+
command("add", object({ file: argument(string()) }))
|
|
190
|
+
~~~~
|
|
191
|
+
|
|
192
|
+
### Modifying combinators
|
|
193
|
+
|
|
194
|
+
- **`optional()`**: Makes any parser optional, returning `undefined` if not
|
|
195
|
+
matched
|
|
196
|
+
|
|
197
|
+
~~~~ typescript
|
|
198
|
+
const parser = object({
|
|
199
|
+
name: option("-n", "--name", string()),
|
|
200
|
+
verbose: optional(option("-v", "--verbose")), // undefined if not provided
|
|
201
|
+
});
|
|
202
|
+
~~~~
|
|
203
|
+
|
|
204
|
+
- **`withDefault()`**: Makes any parser provide a default value instead of
|
|
205
|
+
`undefined` when not matched
|
|
206
|
+
|
|
207
|
+
~~~~ typescript
|
|
208
|
+
const parser = object({
|
|
209
|
+
name: option("-n", "--name", string()),
|
|
210
|
+
port: withDefault(option("-p", "--port", integer()), 8080), // 8080 if not provided
|
|
211
|
+
host: withDefault(option("-h", "--host", string()), "localhost"),
|
|
212
|
+
});
|
|
213
|
+
~~~~
|
|
214
|
+
|
|
215
|
+
- **`multiple()`**: Allows repeating a parser multiple times with constraints
|
|
216
|
+
|
|
217
|
+
~~~~ typescript
|
|
218
|
+
const parser = object({
|
|
219
|
+
// Multiple locales: -l en -l fr
|
|
220
|
+
locales: multiple(option("-l", "--locale", locale())),
|
|
221
|
+
// 1-3 input files required
|
|
222
|
+
files: multiple(argument(string()), { min: 1, max: 3 }),
|
|
223
|
+
});
|
|
224
|
+
~~~~
|
|
225
|
+
|
|
226
|
+
- **`or()`**: Creates mutually exclusive alternatives
|
|
227
|
+
(try first, then second, etc.)
|
|
228
|
+
|
|
229
|
+
~~~~ typescript
|
|
230
|
+
or(
|
|
231
|
+
command("add", addParser),
|
|
232
|
+
command("remove", removeParser)
|
|
233
|
+
)
|
|
234
|
+
~~~~
|
|
235
|
+
|
|
236
|
+
- **`merge()`**: Merges multiple `object()` parsers into a single parser,
|
|
237
|
+
enabling modular composition of option groups
|
|
238
|
+
|
|
239
|
+
~~~~ typescript
|
|
240
|
+
merge(networkOptions, loggingOptions, serverOptions)
|
|
241
|
+
~~~~
|
|
242
|
+
|
|
243
|
+
### Construct combinators
|
|
244
|
+
|
|
245
|
+
- **`object()`**: Combines multiple parsers into a structured object
|
|
246
|
+
|
|
247
|
+
~~~~ typescript
|
|
248
|
+
object("Server", {
|
|
249
|
+
port: option("-p", "--port", integer()),
|
|
250
|
+
host: option("-h", "--host", string()),
|
|
251
|
+
})
|
|
252
|
+
~~~~
|
|
253
|
+
|
|
254
|
+
- **`tuple()`**: Combines multiple parsers into a tuple with preserved order
|
|
255
|
+
|
|
256
|
+
~~~~ typescript
|
|
257
|
+
tuple(
|
|
258
|
+
option("-u", "--user", string()),
|
|
259
|
+
option("-p", "--port", integer())
|
|
260
|
+
)
|
|
261
|
+
~~~~
|
|
262
|
+
|
|
263
|
+
### Advanced patterns
|
|
264
|
+
|
|
265
|
+
The `merge()` combinator enables powerful modular designs by separating concerns
|
|
266
|
+
into reusable option groups:
|
|
267
|
+
|
|
268
|
+
~~~~ typescript
|
|
269
|
+
// Define reusable option groups
|
|
270
|
+
const databaseOptions = object("Database", {
|
|
271
|
+
dbHost: option("--db-host", string()),
|
|
272
|
+
dbPort: option("--db-port", integer({ min: 1, max: 65535 })),
|
|
273
|
+
dbName: option("--db-name", string()),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const authOptions = object("Authentication", {
|
|
277
|
+
username: option("-u", "--user", string()),
|
|
278
|
+
password: optional(option("-p", "--password", string())),
|
|
279
|
+
token: optional(option("-t", "--token", string())),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const loggingOptions = object("Logging", {
|
|
283
|
+
logLevel: option("--log-level", choice(["debug", "info", "warn", "error"])),
|
|
284
|
+
logFile: optional(option("--log-file", string())),
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// Combine groups differently for different modes
|
|
288
|
+
const parser = or(
|
|
289
|
+
// Development: all options available
|
|
290
|
+
merge(
|
|
291
|
+
object("Dev", { dev: option("--dev") }),
|
|
292
|
+
databaseOptions,
|
|
293
|
+
authOptions,
|
|
294
|
+
loggingOptions
|
|
295
|
+
),
|
|
296
|
+
// Production: database and auth required, enhanced logging
|
|
297
|
+
merge(
|
|
298
|
+
object("Prod", { config: option("-c", "--config", string()) }),
|
|
299
|
+
databaseOptions,
|
|
300
|
+
authOptions,
|
|
301
|
+
loggingOptions,
|
|
302
|
+
object("Production", {
|
|
303
|
+
workers: multiple(option("-w", "--worker", integer({ min: 1 }))),
|
|
304
|
+
})
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
~~~~
|
|
308
|
+
|
|
309
|
+
This approach promotes:
|
|
310
|
+
|
|
311
|
+
- *Reusability*: Option groups can be shared across different command modes
|
|
312
|
+
- *Maintainability*: Changes to option groups automatically propagate
|
|
313
|
+
- *Modularity*: Each concern is separated into its own focused parser
|
|
314
|
+
- *Flexibility*: Different combinations for different use cases
|
|
315
|
+
|
|
316
|
+
The `multiple()` combinator is especially powerful when combined with `object()`
|
|
317
|
+
parsers, as it provides empty arrays as defaults when no matches are found,
|
|
318
|
+
allowing for clean optional repeated arguments.
|
|
319
|
+
|
|
320
|
+
### Quick subcommand example
|
|
321
|
+
|
|
322
|
+
For a quick introduction to subcommands, here's a simple `git`-like interface:
|
|
323
|
+
|
|
324
|
+
~~~~ typescript
|
|
325
|
+
import { run } from "@optique/core/facade";
|
|
326
|
+
import { argument, command, constant, object, option, or } from "@optique/core/parser";
|
|
327
|
+
import { string } from "@optique/core/valueparser";
|
|
328
|
+
import process from "node:process";
|
|
329
|
+
|
|
330
|
+
const parser = or(
|
|
331
|
+
command("add", object({
|
|
332
|
+
type: constant("add"),
|
|
333
|
+
all: option("-A", "--all"),
|
|
334
|
+
file: argument(string()),
|
|
335
|
+
})),
|
|
336
|
+
command("commit", object({
|
|
337
|
+
type: constant("commit"),
|
|
338
|
+
message: option("-m", "--message", string()),
|
|
339
|
+
amend: option("--amend"),
|
|
340
|
+
})),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const result = run(parser, "git", ["commit", "-m", "Fix parser bug"], {
|
|
344
|
+
help: "both",
|
|
345
|
+
onError: process.exit,
|
|
346
|
+
});
|
|
347
|
+
// result.type === "commit"
|
|
348
|
+
// result.message === "Fix parser bug"
|
|
349
|
+
// result.amend === false
|
|
350
|
+
~~~~
|
|
351
|
+
|
|
352
|
+
### Subcommands
|
|
353
|
+
|
|
354
|
+
The `command()` combinator enables building git-like CLI interfaces with
|
|
355
|
+
subcommands. Each subcommand can have its own set of options and arguments:
|
|
356
|
+
|
|
357
|
+
~~~~ typescript
|
|
358
|
+
import { run } from "@optique/core/facade";
|
|
359
|
+
import {
|
|
360
|
+
argument,
|
|
361
|
+
command,
|
|
362
|
+
constant,
|
|
363
|
+
multiple,
|
|
364
|
+
object,
|
|
365
|
+
option,
|
|
366
|
+
optional,
|
|
367
|
+
or,
|
|
368
|
+
} from "@optique/core/parser";
|
|
369
|
+
import { string } from "@optique/core/valueparser";
|
|
370
|
+
import process from "node:process";
|
|
371
|
+
|
|
372
|
+
const parser = or(
|
|
373
|
+
command("show", object({
|
|
374
|
+
type: constant("show"),
|
|
375
|
+
progress: option("-p", "--progress"),
|
|
376
|
+
verbose: optional(option("-v", "--verbose")),
|
|
377
|
+
id: argument(string({ metavar: "ITEM_ID" })),
|
|
378
|
+
})),
|
|
379
|
+
command("edit", object({
|
|
380
|
+
type: constant("edit"),
|
|
381
|
+
editor: optional(option("-e", "--editor", string({ metavar: "EDITOR" }))),
|
|
382
|
+
backup: option("-b", "--backup"),
|
|
383
|
+
id: argument(string({ metavar: "ITEM_ID" })),
|
|
384
|
+
})),
|
|
385
|
+
command("delete", object({
|
|
386
|
+
type: constant("delete"),
|
|
387
|
+
force: option("-f", "--force"),
|
|
388
|
+
recursive: optional(option("-r", "--recursive")),
|
|
389
|
+
items: multiple(argument(string({ metavar: "ITEM_ID" })), { min: 1 }),
|
|
390
|
+
})),
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const result = run(parser, "myapp", process.argv.slice(2), {
|
|
394
|
+
colors: true,
|
|
395
|
+
help: "both",
|
|
396
|
+
onError: process.exit,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// TypeScript infers a union type with discriminated subcommands
|
|
400
|
+
switch (result.type) {
|
|
401
|
+
case "show":
|
|
402
|
+
console.log(`Showing item: ${result.id}`);
|
|
403
|
+
if (result.progress) console.log("Progress enabled");
|
|
404
|
+
if (result.verbose) console.log("Verbose mode enabled");
|
|
405
|
+
break;
|
|
406
|
+
case "edit":
|
|
407
|
+
console.log(`Editing item: ${result.id}`);
|
|
408
|
+
if (result.editor) console.log(`Using editor: ${result.editor}`);
|
|
409
|
+
if (result.backup) console.log("Backup enabled");
|
|
410
|
+
break;
|
|
411
|
+
case "delete":
|
|
412
|
+
console.log(`Deleting items: ${result.items.join(", ")}`);
|
|
413
|
+
if (result.force) console.log("Force delete enabled");
|
|
414
|
+
if (result.recursive) console.log("Recursive delete enabled");
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
~~~~
|
|
418
|
+
|
|
419
|
+
This example demonstrates:
|
|
420
|
+
|
|
421
|
+
- *Subcommand routing*: `command("show", ...)` matches the first argument
|
|
422
|
+
and applies the inner parser to remaining arguments
|
|
423
|
+
- *Type discrimination*: Using `constant()` with unique values enables
|
|
424
|
+
TypeScript to discriminate between subcommand types
|
|
425
|
+
- *Per-subcommand options*: Each subcommand can have its own unique set
|
|
426
|
+
of options and arguments
|
|
427
|
+
- *Complex arguments*: The `delete` command shows multiple required arguments
|
|
428
|
+
|
|
429
|
+
Example usage:
|
|
430
|
+
|
|
431
|
+
~~~~ bash
|
|
432
|
+
# Show command with options
|
|
433
|
+
$ myapp show --progress --verbose item123
|
|
434
|
+
|
|
435
|
+
# Edit command with optional editor
|
|
436
|
+
$ myapp edit --editor vim --backup item456
|
|
437
|
+
|
|
438
|
+
# Delete command with multiple items
|
|
439
|
+
$ myapp delete --force item1 item2 item3
|
|
440
|
+
~~~~
|
|
441
|
+
|
|
442
|
+
### Advanced subcommand patterns
|
|
443
|
+
|
|
444
|
+
You can also combine subcommands with global options using `object()`:
|
|
445
|
+
|
|
446
|
+
~~~~ typescript
|
|
447
|
+
const parser = object({
|
|
448
|
+
// Global options available to all subcommands
|
|
449
|
+
debug: optional(option("--debug")),
|
|
450
|
+
config: optional(option("-c", "--config", string())),
|
|
451
|
+
|
|
452
|
+
// Subcommand with its own options
|
|
453
|
+
command: or(
|
|
454
|
+
command("server", object({
|
|
455
|
+
type: constant("server" as const),
|
|
456
|
+
port: option("-p", "--port", integer({ min: 1, max: 65535 })),
|
|
457
|
+
daemon: option("-d", "--daemon"),
|
|
458
|
+
})),
|
|
459
|
+
command("client", object({
|
|
460
|
+
type: constant("client" as const),
|
|
461
|
+
connect: option("--connect", url()),
|
|
462
|
+
timeout: optional(option("-t", "--timeout", integer())),
|
|
463
|
+
})),
|
|
464
|
+
),
|
|
465
|
+
});
|
|
466
|
+
~~~~
|
|
467
|
+
|
|
468
|
+
This allows for commands like:
|
|
469
|
+
|
|
470
|
+
~~~~ bash
|
|
471
|
+
$ myapp --debug --config app.json server --port 8080 --daemon
|
|
472
|
+
$ myapp client --connect http://localhost:8080 --timeout 5000
|
|
473
|
+
~~~~
|
|
474
|
+
|
|
475
|
+
<!-- cSpell: ignore optparse myapp -->
|
package/dist/doc.cjs
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const require_message = require('./message.cjs');
|
|
2
|
+
const require_usage = require('./usage.cjs');
|
|
3
|
+
|
|
4
|
+
//#region src/doc.ts
|
|
5
|
+
/**
|
|
6
|
+
* Formats a documentation page into a human-readable string.
|
|
7
|
+
*
|
|
8
|
+
* This function takes a structured {@link DocPage} and converts it into
|
|
9
|
+
* a formatted string suitable for display in terminals or documentation.
|
|
10
|
+
* The formatting includes proper indentation, alignment, and optional
|
|
11
|
+
* color support.
|
|
12
|
+
*
|
|
13
|
+
* @param programName The name of the program, used in usage lines
|
|
14
|
+
* @param page The documentation page to format
|
|
15
|
+
* @param options Formatting options to customize the output
|
|
16
|
+
* @returns A formatted string representation of the documentation page
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const page: DocPage = {
|
|
21
|
+
* brief: "A CLI tool",
|
|
22
|
+
* usage: [{ type: "literal", value: "myapp" }],
|
|
23
|
+
* sections: [{
|
|
24
|
+
* title: "Options",
|
|
25
|
+
* entries: [{
|
|
26
|
+
* term: { type: "option", short: "-v", long: "--verbose" },
|
|
27
|
+
* description: "Enable verbose output"
|
|
28
|
+
* }]
|
|
29
|
+
* }]
|
|
30
|
+
* };
|
|
31
|
+
*
|
|
32
|
+
* const formatted = formatDocPage("myapp", page, { colors: true });
|
|
33
|
+
* console.log(formatted);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
function formatDocPage(programName, page, options = {}) {
|
|
37
|
+
const termIndent = options.termIndent ?? 2;
|
|
38
|
+
const termWidth = options.termWidth ?? 26;
|
|
39
|
+
let output = "";
|
|
40
|
+
if (page.brief != null) {
|
|
41
|
+
output += require_message.formatMessage(page.brief, {
|
|
42
|
+
colors: options.colors,
|
|
43
|
+
maxWidth: options.maxWidth,
|
|
44
|
+
quotes: !options.colors
|
|
45
|
+
});
|
|
46
|
+
output += "\n";
|
|
47
|
+
}
|
|
48
|
+
if (page.usage != null) {
|
|
49
|
+
output += "Usage: ";
|
|
50
|
+
output += indentLines(require_usage.formatUsage(programName, page.usage, {
|
|
51
|
+
colors: options.colors,
|
|
52
|
+
maxWidth: options.maxWidth == null ? void 0 : options.maxWidth - 7,
|
|
53
|
+
expandCommands: true
|
|
54
|
+
}), 7);
|
|
55
|
+
output += "\n";
|
|
56
|
+
}
|
|
57
|
+
if (page.description != null) {
|
|
58
|
+
output += "\n";
|
|
59
|
+
output += require_message.formatMessage(page.description, {
|
|
60
|
+
colors: options.colors,
|
|
61
|
+
maxWidth: options.maxWidth,
|
|
62
|
+
quotes: !options.colors
|
|
63
|
+
});
|
|
64
|
+
output += "\n";
|
|
65
|
+
}
|
|
66
|
+
const sections = page.sections.toSorted((a, b) => a.title == null && b.title == null ? 0 : a.title == null ? -1 : 1);
|
|
67
|
+
for (const section of sections) {
|
|
68
|
+
output += "\n";
|
|
69
|
+
if (section.title != null) output += `${section.title}:\n`;
|
|
70
|
+
for (const entry of section.entries) {
|
|
71
|
+
const term = require_usage.formatUsageTerm(entry.term, {
|
|
72
|
+
colors: options.colors,
|
|
73
|
+
optionsSeparator: ", ",
|
|
74
|
+
maxWidth: options.maxWidth == null ? void 0 : options.maxWidth - termIndent
|
|
75
|
+
});
|
|
76
|
+
output += `${" ".repeat(termIndent)}${ansiAwareRightPad(term, termWidth)} ${entry.description == null ? "" : indentLines(require_message.formatMessage(entry.description, {
|
|
77
|
+
colors: options.colors,
|
|
78
|
+
quotes: !options.colors,
|
|
79
|
+
maxWidth: options.maxWidth == null ? void 0 : options.maxWidth - termIndent - termWidth - 2
|
|
80
|
+
}), termIndent + termWidth + 2)}\n`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (page.footer != null) {
|
|
84
|
+
output += "\n";
|
|
85
|
+
output += require_message.formatMessage(page.footer, {
|
|
86
|
+
colors: options.colors,
|
|
87
|
+
maxWidth: options.maxWidth,
|
|
88
|
+
quotes: !options.colors
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return output;
|
|
92
|
+
}
|
|
93
|
+
function indentLines(text, indent) {
|
|
94
|
+
return text.split("\n").join("\n" + " ".repeat(indent));
|
|
95
|
+
}
|
|
96
|
+
function ansiAwareRightPad(text, length, char = " ") {
|
|
97
|
+
const ansiEscapeCodeRegex = /\x1B\[[0-9;]*[a-zA-Z]/g;
|
|
98
|
+
const strippedText = text.replace(ansiEscapeCodeRegex, "");
|
|
99
|
+
if (strippedText.length >= length) return text;
|
|
100
|
+
return text + char.repeat(length - strippedText.length);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
exports.formatDocPage = formatDocPage;
|
package/dist/doc.d.cts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Message } from "./message.cjs";
|
|
2
|
+
import { Usage, UsageTerm } from "./usage.cjs";
|
|
3
|
+
|
|
4
|
+
//#region src/doc.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A documentation entry which describes a specific usage of a command or
|
|
8
|
+
* option. It includes a subject (the usage), a description, and an optional
|
|
9
|
+
* default value.
|
|
10
|
+
*/
|
|
11
|
+
interface DocEntry {
|
|
12
|
+
/**
|
|
13
|
+
* The subject of the entry, which is typically a command or option
|
|
14
|
+
* usage.
|
|
15
|
+
*/
|
|
16
|
+
readonly term: UsageTerm;
|
|
17
|
+
/**
|
|
18
|
+
* A description of the entry, which provides additional context or
|
|
19
|
+
* information about the usage.
|
|
20
|
+
*/
|
|
21
|
+
readonly description?: Message;
|
|
22
|
+
/**
|
|
23
|
+
* An optional default value for the entry, which can be used to
|
|
24
|
+
* indicate what the default behavior is if the command or option is not
|
|
25
|
+
* specified.
|
|
26
|
+
*/
|
|
27
|
+
readonly default?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A section in a document that groups related entries together.
|
|
31
|
+
*/
|
|
32
|
+
interface DocSection {
|
|
33
|
+
readonly title?: string;
|
|
34
|
+
readonly entries: readonly DocEntry[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A document page that contains multiple sections, each with its own brief
|
|
38
|
+
* and a list of entries. This structure is used to organize documentation
|
|
39
|
+
* for commands, options, and other related information.
|
|
40
|
+
*/
|
|
41
|
+
interface DocPage {
|
|
42
|
+
readonly brief?: Message;
|
|
43
|
+
readonly usage?: Usage;
|
|
44
|
+
readonly description?: Message;
|
|
45
|
+
readonly sections: readonly DocSection[];
|
|
46
|
+
readonly footer?: Message;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A documentation fragment that can be either an entry or a section.
|
|
50
|
+
* Fragments are building blocks used to construct documentation pages.
|
|
51
|
+
*/
|
|
52
|
+
type DocFragment = {
|
|
53
|
+
readonly type: "entry";
|
|
54
|
+
} & DocEntry | {
|
|
55
|
+
readonly type: "section";
|
|
56
|
+
} & DocSection;
|
|
57
|
+
/**
|
|
58
|
+
* A collection of documentation fragments with an optional description.
|
|
59
|
+
* This structure is used to gather fragments before organizing them into
|
|
60
|
+
* a final document page.
|
|
61
|
+
*/
|
|
62
|
+
interface DocFragments {
|
|
63
|
+
/**
|
|
64
|
+
* An optional description that applies to the entire collection of fragments.
|
|
65
|
+
*/
|
|
66
|
+
readonly description?: Message;
|
|
67
|
+
/**
|
|
68
|
+
* An array of documentation fragments that can be entries or sections.
|
|
69
|
+
*/
|
|
70
|
+
readonly fragments: readonly DocFragment[];
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Options for formatting a documentation page.
|
|
74
|
+
*/
|
|
75
|
+
interface DocPageFormatOptions {
|
|
76
|
+
/**
|
|
77
|
+
* Whether to include ANSI color codes in the output.
|
|
78
|
+
* @default `false`
|
|
79
|
+
*/
|
|
80
|
+
colors?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Number of spaces to indent terms in documentation entries.
|
|
83
|
+
* @default `2`
|
|
84
|
+
*/
|
|
85
|
+
termIndent?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Width allocated for terms before descriptions start.
|
|
88
|
+
* @default `26`
|
|
89
|
+
*/
|
|
90
|
+
termWidth?: number;
|
|
91
|
+
/**
|
|
92
|
+
* Maximum width of the entire formatted output.
|
|
93
|
+
*/
|
|
94
|
+
maxWidth?: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Formats a documentation page into a human-readable string.
|
|
98
|
+
*
|
|
99
|
+
* This function takes a structured {@link DocPage} and converts it into
|
|
100
|
+
* a formatted string suitable for display in terminals or documentation.
|
|
101
|
+
* The formatting includes proper indentation, alignment, and optional
|
|
102
|
+
* color support.
|
|
103
|
+
*
|
|
104
|
+
* @param programName The name of the program, used in usage lines
|
|
105
|
+
* @param page The documentation page to format
|
|
106
|
+
* @param options Formatting options to customize the output
|
|
107
|
+
* @returns A formatted string representation of the documentation page
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* ```typescript
|
|
111
|
+
* const page: DocPage = {
|
|
112
|
+
* brief: "A CLI tool",
|
|
113
|
+
* usage: [{ type: "literal", value: "myapp" }],
|
|
114
|
+
* sections: [{
|
|
115
|
+
* title: "Options",
|
|
116
|
+
* entries: [{
|
|
117
|
+
* term: { type: "option", short: "-v", long: "--verbose" },
|
|
118
|
+
* description: "Enable verbose output"
|
|
119
|
+
* }]
|
|
120
|
+
* }]
|
|
121
|
+
* };
|
|
122
|
+
*
|
|
123
|
+
* const formatted = formatDocPage("myapp", page, { colors: true });
|
|
124
|
+
* console.log(formatted);
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
declare function formatDocPage(programName: string, page: DocPage, options?: DocPageFormatOptions): string;
|
|
128
|
+
//#endregion
|
|
129
|
+
export { DocEntry, DocFragment, DocFragments, DocPage, DocPageFormatOptions, DocSection, formatDocPage };
|