@optique/core 0.1.0-dev.6 → 0.1.0

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.
Files changed (2) hide show
  1. package/README.md +34 -473
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,503 +1,64 @@
1
- Optique: Type-safe combinatorial CLI parser for TypeScript
2
- ==========================================================
1
+ @optique/core
2
+ =============
3
3
 
4
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.
5
+ > Optique is currently in early development and may change significantly.
6
+ > Expect breaking changes as we refine the API and features.
8
7
 
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.
8
+ The core package of Optique which provides the shared types and parser
9
+ combinators. It is designed to be used in universal JavaScript runtimes,
10
+ including Node.js, Deno, Bun, edge functions, and web browsers—although
11
+ you usually won't use it directly in browsers.
26
12
 
27
13
  > [!TIP]
28
14
  > *Building CLI apps?* Consider *@optique/run* for automatic `process.argv`
29
15
  > handling and `process.exit()` integration. This core package is perfect for
30
16
  > libraries, web apps, or when you need full control over argument parsing.
31
17
 
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
- - `constant()` for literal values (essential for discriminated unions)
50
-
51
- - *Modifying combinators* transform and combine parsers:
52
- - `optional()` makes parsers optional
53
- - `withDefault()` provides default values for optional parsers
54
- - `map()` transforms parsed values using mapping functions
55
- - `multiple()` allows repetition
56
- - `or()` creates alternatives
57
- - `merge()` combines object parsers
58
-
59
- - *Construct combinators* build structured results:
60
-
61
- - `object()` groups parsers into objects
62
- - `tuple()` combines parsers into tuples
63
-
64
- The library automatically infers the result type of your parser composition,
65
- ensuring that your parsed CLI arguments are fully typed without manual type
66
- annotations. When parsing fails, you get detailed error messages that help
67
- users understand what went wrong.
68
-
69
-
70
- Example
71
- -------
72
-
73
- ~~~~ typescript
74
- import { run } from "@optique/core/facade";
75
- import { formatMessage } from "@optique/core/message";
76
- import {
77
- argument,
78
- merge,
79
- multiple,
80
- object,
81
- option,
82
- optional,
83
- or,
84
- withDefault
85
- } from "@optique/core/parser";
86
- import { choice, integer, locale, string, url } from "@optique/core/valueparser";
87
- import process from "node:process";
88
-
89
- // Define a sophisticated CLI with grouped and reusable option sets
90
- const networkOptions = object("Network", {
91
- port: option("-p", "--port", integer({ min: 1, max: 65535 })),
92
- host: withDefault(
93
- option("-h", "--host", string({ metavar: "HOST" })),
94
- "localhost",
95
- ),
96
- });
97
-
98
- const loggingOptions = object("Logging", {
99
- verbose: optional(option("-v", "--verbose")),
100
- logFile: optional(option("--log-file", string({ metavar: "FILE" }))),
101
- });
102
-
103
- const parser = or(
104
- // Server mode: merge network and logging options with server-specific config
105
- merge(
106
- networkOptions,
107
- loggingOptions,
108
- object("Server", {
109
- locales: multiple(option("-l", "--locale", locale())),
110
- config: argument(string({ metavar: "CONFIG_FILE" })),
111
- }),
112
- ),
113
- object("Client mode", {
114
- connect: option(
115
- "-c", "--connect",
116
- url({ allowedProtocols: ["http:", "https:"] }),
117
- ),
118
- headers: multiple(option("-H", "--header", string())),
119
- timeout: withDefault(option("-t", "--timeout", integer({ min: 0 })), 30000),
120
- files: multiple(argument(string({ metavar: "FILE" })), { min: 1, max: 5 }),
121
- }),
122
- );
123
-
124
- const result = run(parser, "myapp", process.argv.slice(2), {
125
- colors: true,
126
- help: "both",
127
- onError: process.exit,
128
- });
129
-
130
- // TypeScript automatically infers the complex union type with optional fields
131
- if ("port" in result) {
132
- const server = result;
133
- console.log(`Starting server on ${server.host}:${server.port}`);
134
- console.log(`Supported locales: ${server.locales.join(", ") || "default"}`);
135
- if (server.verbose) console.log("Verbose mode enabled");
136
- if (server.logFile) console.log(`Logging to: ${server.logFile}`);
137
- } else {
138
- const client = result;
139
- console.log(`Connecting to ${client.connect}`);
140
- console.log(`Processing ${client.files.length} files`);
141
- console.log(`Timeout: ${client.timeout}ms`);
142
- }
143
- ~~~~
144
-
145
- This example demonstrates Optique's powerful combinators:
146
-
147
- - **`merge()`** combines multiple `object()` parsers into a single unified
148
- parser, enabling modular and reusable option groups
149
- - **`optional()`** makes parsers optional, returning `undefined` when not
150
- provided
151
- - **`withDefault()`** provides default values instead of `undefined` for
152
- optional parameters, improving usability
153
- - **`multiple()`** allows repeating options/arguments with configurable
154
- constraints
155
- - **`or()`** creates mutually exclusive alternatives
156
- - **`object()`** groups related options into structured data
157
-
158
- The parser demonstrates modular design by:
159
-
160
- - Separating network options (`--port`, `--host`) for reusability
161
- - Grouping logging configuration (`--verbose`, `--log-file`) separately
162
- - Merging reusable groups with server-specific options using `merge()`
163
- - Supporting complex scenarios like multiple locales: `-l en-US -l fr-FR`
164
-
165
- All with full type safety and automatic inference!
166
-
167
-
168
- Parser combinators
169
- ------------------
170
-
171
- Optique provides several types of parsers and combinators for building sophisticated CLI interfaces:
172
-
173
- ### Primitive parsers
174
-
175
- - **`option()`**: Handles command-line flags and their arguments
176
-
177
- ~~~~ typescript
178
- option("-p", "--port", integer({ min: 1, max: 65535 }))
179
- option("-v", "--verbose") // Boolean flag
180
- ~~~~
181
-
182
- - **`argument()`**: Handles positional arguments
183
-
184
- ~~~~ typescript
185
- argument(string({ metavar: "FILE" }))
186
- ~~~~
187
-
188
- - **`command()`**: Matches subcommands for `git`-like CLI interfaces
189
-
190
- ~~~~ typescript
191
- command("add", object({ file: argument(string()) }))
192
- ~~~~
193
-
194
- - **`constant()`**: Produces a constant value without consuming input, essential for discriminated unions
195
-
196
- ~~~~ typescript
197
- constant("add") // Always returns "add"
198
- constant(42) // Always returns 42
199
- ~~~~
200
-
201
- ### Modifying combinators
202
-
203
- - **`optional()`**: Makes any parser optional, returning `undefined` if not
204
- matched
205
-
206
- ~~~~ typescript
207
- const parser = object({
208
- name: option("-n", "--name", string()),
209
- verbose: optional(option("-v", "--verbose")), // undefined if not provided
210
- });
211
- ~~~~
212
-
213
- - **`withDefault()`**: Makes any parser provide a default value instead of
214
- `undefined` when not matched
215
-
216
- ~~~~ typescript
217
- const parser = object({
218
- name: option("-n", "--name", string()),
219
- port: withDefault(option("-p", "--port", integer()), 8080), // 8080 if not provided
220
- host: withDefault(option("-h", "--host", string()), "localhost"),
221
- });
222
- ~~~~
223
-
224
- - **`map()`**: Transforms the parsed value using a mapping function while
225
- preserving the original parsing logic
226
18
 
227
- ~~~~ typescript
228
- const parser = object({
229
- // Transform boolean flag to its inverse
230
- disallow: map(option("--allow"), b => !b),
231
- // Transform string to uppercase
232
- upperName: map(option("-n", "--name", string()), s => s.toUpperCase()),
233
- // Transform number to formatted string
234
- portDesc: map(option("-p", "--port", integer()), n => `port: ${n}`),
235
- });
236
- ~~~~
19
+ When to use @optique/core
20
+ ------------------------
237
21
 
238
- - **`multiple()`**: Allows repeating a parser multiple times with constraints
22
+ Use *@optique/core* instead when:
239
23
 
240
- ~~~~ typescript
241
- const parser = object({
242
- // Multiple locales: -l en -l fr
243
- locales: multiple(option("-l", "--locale", locale())),
244
- // 1-3 input files required
245
- files: multiple(argument(string()), { min: 1, max: 3 }),
246
- });
247
- ~~~~
24
+ - Building web applications or libraries
25
+ - You need full control over argument sources and error handling
26
+ - Working in environments without `process` (browsers, web workers)
27
+ - Building reusable parser components
248
28
 
249
- - **`or()`**: Creates mutually exclusive alternatives
250
- (try first, then second, etc.)
29
+ Use *@optique/run* when:
251
30
 
252
- ~~~~ typescript
253
- or(
254
- command("add", addParser),
255
- command("remove", removeParser)
256
- )
257
- ~~~~
31
+ - Building CLI applications for Node.js, Bun, or Deno
32
+ - You want automatic `process.argv` parsing and `process.exit()` handling
33
+ - You need automatic terminal capability detection (colors, width)
34
+ - You prefer a simple, batteries-included approach
258
35
 
259
- - **`merge()`**: Merges multiple `object()` parsers into a single parser,
260
- enabling modular composition of option groups
261
36
 
262
- ~~~~ typescript
263
- merge(networkOptions, loggingOptions, serverOptions)
264
- ~~~~
265
-
266
- ### Construct combinators
267
-
268
- - **`object()`**: Combines multiple parsers into a structured object
269
-
270
- ~~~~ typescript
271
- object("Server", {
272
- port: option("-p", "--port", integer()),
273
- host: option("-h", "--host", string()),
274
- })
275
- ~~~~
276
-
277
- - **`tuple()`**: Combines multiple parsers into a tuple with preserved order
278
-
279
- ~~~~ typescript
280
- tuple(
281
- option("-u", "--user", string()),
282
- option("-p", "--port", integer())
283
- )
284
- ~~~~
285
-
286
- ### Advanced patterns
287
-
288
- The `merge()` combinator enables powerful modular designs by separating concerns
289
- into reusable option groups:
290
-
291
- ~~~~ typescript
292
- // Define reusable option groups
293
- const databaseOptions = object("Database", {
294
- dbHost: option("--db-host", string()),
295
- dbPort: option("--db-port", integer({ min: 1, max: 65535 })),
296
- dbName: option("--db-name", string()),
297
- });
298
-
299
- const authOptions = object("Authentication", {
300
- username: option("-u", "--user", string()),
301
- password: optional(option("-p", "--password", string())),
302
- token: optional(option("-t", "--token", string())),
303
- });
304
-
305
- const loggingOptions = object("Logging", {
306
- logLevel: option("--log-level", choice(["debug", "info", "warn", "error"])),
307
- logFile: optional(option("--log-file", string())),
308
- });
309
-
310
- // Combine groups differently for different modes
311
- const parser = or(
312
- // Development: all options available
313
- merge(
314
- object("Dev", { dev: option("--dev") }),
315
- databaseOptions,
316
- authOptions,
317
- loggingOptions
318
- ),
319
- // Production: database and auth required, enhanced logging
320
- merge(
321
- object("Prod", { config: option("-c", "--config", string()) }),
322
- databaseOptions,
323
- authOptions,
324
- loggingOptions,
325
- object("Production", {
326
- workers: multiple(option("-w", "--worker", integer({ min: 1 }))),
327
- })
328
- ),
329
- );
330
- ~~~~
331
-
332
- This approach promotes:
333
-
334
- - *Reusability*: Option groups can be shared across different command modes
335
- - *Maintainability*: Changes to option groups automatically propagate
336
- - *Modularity*: Each concern is separated into its own focused parser
337
- - *Flexibility*: Different combinations for different use cases
338
-
339
- The `multiple()` combinator is especially powerful when combined with `object()`
340
- parsers, as it provides empty arrays as defaults when no matches are found,
341
- allowing for clean optional repeated arguments.
342
-
343
- ### Quick subcommand example
344
-
345
- For a quick introduction to subcommands, here's a simple `git`-like interface:
37
+ Quick example
38
+ -------------
346
39
 
347
40
  ~~~~ typescript
348
41
  import { run } from "@optique/core/facade";
349
- import { argument, command, constant, object, option, or } from "@optique/core/parser";
350
- import { string } from "@optique/core/valueparser";
42
+ import { object, option, argument } from "@optique/core/parser";
43
+ import { string, integer } from "@optique/core/valueparser";
351
44
  import process from "node:process";
352
45
 
353
- const parser = or(
354
- command("add", object({
355
- type: constant("add"),
356
- all: option("-A", "--all"),
357
- file: argument(string()),
358
- })),
359
- command("commit", object({
360
- type: constant("commit"),
361
- message: option("-m", "--message", string()),
362
- amend: option("--amend"),
363
- })),
364
- );
365
-
366
- const result = run(parser, "git", ["commit", "-m", "Fix parser bug"], {
367
- help: "both",
368
- onError: process.exit,
46
+ const parser = object({
47
+ name: argument(string()),
48
+ age: option("-a", "--age", integer()),
49
+ verbose: option("-v", "--verbose"),
369
50
  });
370
- // result.type === "commit"
371
- // result.message === "Fix parser bug"
372
- // result.amend === false
373
- ~~~~
374
-
375
- ### Subcommands
376
-
377
- The `command()` combinator enables building git-like CLI interfaces with
378
- subcommands. Each subcommand can have its own set of options and arguments:
379
-
380
- ~~~~ typescript
381
- import { run } from "@optique/core/facade";
382
- import {
383
- argument,
384
- command,
385
- constant,
386
- multiple,
387
- object,
388
- option,
389
- optional,
390
- or,
391
- } from "@optique/core/parser";
392
- import { string } from "@optique/core/valueparser";
393
- import process from "node:process";
394
51
 
395
- const parser = or(
396
- command("show", object({
397
- type: constant("show"),
398
- progress: option("-p", "--progress"),
399
- verbose: optional(option("-v", "--verbose")),
400
- id: argument(string({ metavar: "ITEM_ID" })),
401
- })),
402
- command("edit", object({
403
- type: constant("edit"),
404
- editor: optional(option("-e", "--editor", string({ metavar: "EDITOR" }))),
405
- backup: option("-b", "--backup"),
406
- id: argument(string({ metavar: "ITEM_ID" })),
407
- })),
408
- command("delete", object({
409
- type: constant("delete"),
410
- force: option("-f", "--force"),
411
- recursive: optional(option("-r", "--recursive")),
412
- items: multiple(argument(string({ metavar: "ITEM_ID" })), { min: 1 }),
413
- })),
414
- );
415
-
416
- const result = run(parser, "myapp", process.argv.slice(2), {
417
- colors: true,
52
+ const config = run(parser, "myapp", process.argv.slice(2), {
418
53
  help: "both",
419
54
  onError: process.exit,
420
55
  });
421
56
 
422
- // TypeScript infers a union type with discriminated subcommands
423
- switch (result.type) {
424
- case "show":
425
- console.log(`Showing item: ${result.id}`);
426
- if (result.progress) console.log("Progress enabled");
427
- if (result.verbose) console.log("Verbose mode enabled");
428
- break;
429
- case "edit":
430
- console.log(`Editing item: ${result.id}`);
431
- if (result.editor) console.log(`Using editor: ${result.editor}`);
432
- if (result.backup) console.log("Backup enabled");
433
- break;
434
- case "delete":
435
- console.log(`Deleting items: ${result.items.join(", ")}`);
436
- if (result.force) console.log("Force delete enabled");
437
- if (result.recursive) console.log("Recursive delete enabled");
438
- break;
439
- }
440
- ~~~~
441
-
442
- This example demonstrates:
443
-
444
- - *Subcommand routing*: `command("show", ...)` matches the first argument
445
- and applies the inner parser to remaining arguments
446
- - *Type discrimination*: Using `constant()` with unique values enables
447
- TypeScript to discriminate between subcommand types in the result union
448
- - *Per-subcommand options*: Each subcommand can have its own unique set
449
- of options and arguments
450
- - *Complex arguments*: The `delete` command shows multiple required arguments
451
-
452
- The `constant()` parser is crucial here—it adds a literal value to each
453
- subcommand's result object without consuming any input. This creates
454
- a discriminated union type that TypeScript can use for type narrowing in
455
- `switch` statements or type guards.
456
-
457
- Example usage:
458
-
459
- ~~~~ bash
460
- # Show command with options
461
- $ myapp show --progress --verbose item123
462
-
463
- # Edit command with optional editor
464
- $ myapp edit --editor vim --backup item456
465
-
466
- # Delete command with multiple items
467
- $ myapp delete --force item1 item2 item3
57
+ console.log(`Hello ${config.name}!`);
58
+ if (config.age) console.log(`You are ${config.age} years old.`);
59
+ if (config.verbose) console.log("Verbose mode enabled.");
468
60
  ~~~~
469
61
 
470
- ### Advanced subcommand patterns
471
-
472
- You can also combine subcommands with global options using `object()`:
473
-
474
- ~~~~ typescript
475
- const parser = object({
476
- // Global options available to all subcommands
477
- debug: optional(option("--debug")),
478
- config: optional(option("-c", "--config", string())),
479
-
480
- // Subcommand with its own options
481
- command: or(
482
- command("server", object({
483
- type: constant("server" as const),
484
- port: option("-p", "--port", integer({ min: 1, max: 65535 })),
485
- daemon: option("-d", "--daemon"),
486
- })),
487
- command("client", object({
488
- type: constant("client" as const),
489
- connect: option("--connect", url()),
490
- timeout: optional(option("-t", "--timeout", integer())),
491
- })),
492
- ),
493
- });
494
- ~~~~
495
-
496
- This allows for commands like:
497
-
498
- ~~~~ bash
499
- $ myapp --debug --config app.json server --port 8080 --daemon
500
- $ myapp client --connect http://localhost:8080 --timeout 5000
501
- ~~~~
62
+ For more resources, see the [docs] and the [*examples/*](/examples/) directory.
502
63
 
503
- <!-- cSpell: ignore optparse myapp -->
64
+ [docs]: https://optique.dev/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "0.1.0-dev.6+179f4189",
3
+ "version": "0.1.0",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",