@outfitter/cli 0.1.0-rc.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.
@@ -0,0 +1,607 @@
1
+ import { Command } from "commander";
2
+ import { CancelledError, ValidationError } from "@outfitter/contracts";
3
+ /**
4
+ * Configuration for creating a CLI instance.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * const config: CLIConfig = {
9
+ * name: "waymark",
10
+ * version: "1.0.0",
11
+ * description: "A note management CLI",
12
+ * };
13
+ * ```
14
+ */
15
+ interface CLIConfig {
16
+ /** CLI name (used in help output and error messages) */
17
+ readonly name: string;
18
+ /** CLI version (displayed with --version) */
19
+ readonly version: string;
20
+ /** CLI description (displayed in help output) */
21
+ readonly description?: string;
22
+ /** Custom error handler */
23
+ readonly onError?: (error: Error) => void;
24
+ /** Custom exit handler (defaults to process.exit) */
25
+ readonly onExit?: (code: number) => never;
26
+ }
27
+ /**
28
+ * CLI instance returned by createCLI.
29
+ */
30
+ interface CLI {
31
+ /** Register a command with the CLI */
32
+ register(command: CommandBuilder | Command): this;
33
+ /** Parse arguments and execute the matched command */
34
+ parse(argv?: readonly string[]): Promise<void>;
35
+ /** Get the underlying Commander program */
36
+ readonly program: Command;
37
+ }
38
+ /**
39
+ * Configuration for a single command.
40
+ */
41
+ interface CommandConfig {
42
+ /** Command name and argument syntax (e.g., "list" or "get <id>") */
43
+ readonly name: string;
44
+ /** Command description */
45
+ readonly description?: string;
46
+ /** Command aliases */
47
+ readonly aliases?: readonly string[];
48
+ /** Whether to hide from help output */
49
+ readonly hidden?: boolean;
50
+ }
51
+ /**
52
+ * Action function executed when a command is invoked.
53
+ *
54
+ * @typeParam TFlags - Type of parsed command flags
55
+ */
56
+ type CommandAction<TFlags extends CommandFlags = CommandFlags> = (context: {
57
+ /** Parsed command-line arguments */
58
+ readonly args: readonly string[];
59
+ /** Parsed command flags */
60
+ readonly flags: TFlags;
61
+ /** Raw Commander command instance */
62
+ readonly command: Command;
63
+ }) => Promise<void> | void;
64
+ /**
65
+ * Base type for command flags.
66
+ * All flag types must extend this.
67
+ */
68
+ type CommandFlags = Record<string, unknown>;
69
+ /**
70
+ * Builder interface for constructing commands fluently.
71
+ */
72
+ interface CommandBuilder {
73
+ /** Set command description */
74
+ description(text: string): this;
75
+ /** Add a command option/flag */
76
+ option(flags: string, description: string, defaultValue?: unknown): this;
77
+ /** Add a required option */
78
+ requiredOption(flags: string, description: string, defaultValue?: unknown): this;
79
+ /** Add command aliases */
80
+ alias(alias: string): this;
81
+ /** Set the action handler */
82
+ action<TFlags extends CommandFlags = CommandFlags>(handler: CommandAction<TFlags>): this;
83
+ /** Build the underlying Commander command */
84
+ build(): Command;
85
+ }
86
+ /**
87
+ * Available output modes for CLI commands.
88
+ */
89
+ type OutputMode = "human" | "json" | "jsonl" | "tree" | "table";
90
+ /**
91
+ * Options for the output() function.
92
+ */
93
+ interface OutputOptions {
94
+ /** Force a specific output mode (overrides flag detection) */
95
+ readonly mode?: OutputMode;
96
+ /** Stream to write to (defaults to stdout) */
97
+ readonly stream?: NodeJS.WritableStream;
98
+ /** Whether to pretty-print JSON output */
99
+ readonly pretty?: boolean;
100
+ /** Exit code to use after output (undefined = don't exit) */
101
+ readonly exitCode?: number;
102
+ }
103
+ /**
104
+ * Options for collectIds() input utility.
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const ids = await collectIds(args, {
109
+ * allowFile: true, // @file expansion
110
+ * allowStdin: true, // - reads from stdin
111
+ * });
112
+ * ```
113
+ */
114
+ interface CollectIdsOptions {
115
+ /** Allow @file expansion (reads IDs from file) */
116
+ readonly allowFile?: boolean;
117
+ /** Allow glob patterns */
118
+ readonly allowGlob?: boolean;
119
+ /** Allow reading from stdin with "-" */
120
+ readonly allowStdin?: boolean;
121
+ /** Separator for comma-separated values */
122
+ readonly separator?: string;
123
+ }
124
+ /**
125
+ * Options for expandFileArg() input utility.
126
+ */
127
+ interface ExpandFileOptions {
128
+ /** Encoding for file reads (defaults to utf-8) */
129
+ readonly encoding?: BufferEncoding;
130
+ /** Maximum file size to read (in bytes) */
131
+ readonly maxSize?: number;
132
+ /** Whether to trim the file content */
133
+ readonly trim?: boolean;
134
+ }
135
+ /**
136
+ * Options for parseGlob() input utility.
137
+ */
138
+ interface ParseGlobOptions {
139
+ /** Current working directory for glob resolution */
140
+ readonly cwd?: string;
141
+ /** Whether to follow symlinks */
142
+ readonly followSymlinks?: boolean;
143
+ /** Patterns to exclude */
144
+ readonly ignore?: readonly string[];
145
+ /** Only match files (not directories) */
146
+ readonly onlyFiles?: boolean;
147
+ /** Only match directories (not files) */
148
+ readonly onlyDirectories?: boolean;
149
+ }
150
+ /**
151
+ * Options for normalizeId().
152
+ */
153
+ interface NormalizeIdOptions {
154
+ /** Whether to lowercase the ID */
155
+ readonly lowercase?: boolean;
156
+ /** Whether to trim whitespace */
157
+ readonly trim?: boolean;
158
+ /** Minimum length requirement */
159
+ readonly minLength?: number;
160
+ /** Maximum length requirement */
161
+ readonly maxLength?: number;
162
+ /** Pattern the ID must match */
163
+ readonly pattern?: RegExp;
164
+ }
165
+ /**
166
+ * Options for confirmDestructive().
167
+ */
168
+ interface ConfirmDestructiveOptions {
169
+ /** Message to display to the user */
170
+ readonly message: string;
171
+ /** Whether to bypass confirmation (e.g., --yes flag) */
172
+ readonly bypassFlag?: boolean;
173
+ /** Number of items affected (shown in confirmation) */
174
+ readonly itemCount?: number;
175
+ }
176
+ /**
177
+ * Numeric or date range parsed from CLI input.
178
+ */
179
+ type Range = NumericRange | DateRange;
180
+ /**
181
+ * Numeric range (e.g., "1-10").
182
+ */
183
+ interface NumericRange {
184
+ readonly type: "number";
185
+ readonly min: number;
186
+ readonly max: number;
187
+ }
188
+ /**
189
+ * Date range (e.g., "2024-01-01..2024-12-31").
190
+ */
191
+ interface DateRange {
192
+ readonly type: "date";
193
+ readonly start: Date;
194
+ readonly end: Date;
195
+ }
196
+ /**
197
+ * Filter expression parsed from CLI input.
198
+ */
199
+ interface FilterExpression {
200
+ readonly field: string;
201
+ readonly value: string;
202
+ readonly operator?: "eq" | "ne" | "gt" | "lt" | "gte" | "lte" | "contains";
203
+ }
204
+ /**
205
+ * Sort criteria parsed from CLI input.
206
+ */
207
+ interface SortCriteria {
208
+ readonly field: string;
209
+ readonly direction: "asc" | "desc";
210
+ }
211
+ /**
212
+ * Key-value pair parsed from CLI input.
213
+ */
214
+ interface KeyValuePair {
215
+ readonly key: string;
216
+ readonly value: string;
217
+ }
218
+ /**
219
+ * State for paginated command results.
220
+ */
221
+ interface PaginationState {
222
+ /** Cursor for the next page */
223
+ readonly cursor: string;
224
+ /** Command that generated this state */
225
+ readonly command: string;
226
+ /** Context key for scoping pagination */
227
+ readonly context?: string;
228
+ /** Timestamp when state was created */
229
+ readonly timestamp: number;
230
+ /** Whether there are more results */
231
+ readonly hasMore: boolean;
232
+ /** Total count (if known) */
233
+ readonly total?: number;
234
+ }
235
+ /**
236
+ * Options for cursor persistence operations.
237
+ */
238
+ interface CursorOptions {
239
+ /** Command name for cursor scoping */
240
+ readonly command: string;
241
+ /** Context key for additional scoping */
242
+ readonly context?: string;
243
+ /** Tool name for XDG path resolution */
244
+ readonly toolName: string;
245
+ /** Maximum age in milliseconds before a cursor is treated as expired */
246
+ readonly maxAgeMs?: number;
247
+ /** Whether there are more results (defaults to true) */
248
+ readonly hasMore?: boolean;
249
+ /** Total count of results (if known) */
250
+ readonly total?: number;
251
+ }
252
+ /**
253
+ * Output data to the console with automatic mode selection.
254
+ *
255
+ * Respects --json, --jsonl, --tree, --table flags automatically.
256
+ * Defaults to human-friendly output when no flags are present.
257
+ *
258
+ * @param data - The data to output
259
+ * @param options - Output configuration options
260
+ *
261
+ * @example
262
+ * ```typescript
263
+ * import { output } from "@outfitter/cli";
264
+ *
265
+ * // Basic usage - mode auto-detected from flags
266
+ * output(results);
267
+ *
268
+ * // Force JSON mode
269
+ * output(results, { mode: "json" });
270
+ *
271
+ * // Pretty-print JSON
272
+ * output(results, { mode: "json", pretty: true });
273
+ *
274
+ * // Output to stderr
275
+ * output(errors, { stream: process.stderr });
276
+ *
277
+ * // Await for large outputs (recommended)
278
+ * await output(largeDataset, { mode: "jsonl" });
279
+ * ```
280
+ */
281
+ declare function output(data: unknown, options?: OutputOptions): Promise<void>;
282
+ /**
283
+ * Exit the process with an error message.
284
+ *
285
+ * Formats the error according to the current output mode (human or JSON)
286
+ * and exits with an appropriate exit code.
287
+ *
288
+ * @param error - The error to display
289
+ * @returns Never returns (exits the process)
290
+ *
291
+ * @example
292
+ * ```typescript
293
+ * import { exitWithError } from "@outfitter/cli";
294
+ *
295
+ * try {
296
+ * await riskyOperation();
297
+ * } catch (error) {
298
+ * exitWithError(error instanceof Error ? error : new Error(String(error)));
299
+ * }
300
+ * ```
301
+ */
302
+ declare function exitWithError(error: Error, options?: OutputOptions): never;
303
+ /**
304
+ * Load persisted pagination state for a command.
305
+ *
306
+ * @param options - Cursor options specifying command and context
307
+ * @returns The pagination state if it exists, undefined otherwise
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * const state = loadCursor({
312
+ * command: "list",
313
+ * toolName: "waymark",
314
+ * });
315
+ *
316
+ * if (state) {
317
+ * // Continue from last position
318
+ * const results = await listNotes({ cursor: state.cursor });
319
+ * }
320
+ * ```
321
+ */
322
+ declare function loadCursor(options: CursorOptions): PaginationState | undefined;
323
+ /**
324
+ * Save pagination state for a command.
325
+ *
326
+ * The cursor is persisted to XDG state directory, scoped by
327
+ * tool name, command, and optional context.
328
+ *
329
+ * @param cursor - The cursor string to persist
330
+ * @param options - Cursor options specifying command and context
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * const results = await listNotes({ limit: 20 });
335
+ *
336
+ * if (results.hasMore) {
337
+ * saveCursor(results.cursor, {
338
+ * command: "list",
339
+ * toolName: "waymark",
340
+ * });
341
+ * }
342
+ * ```
343
+ */
344
+ declare function saveCursor(cursor: string, options: CursorOptions): void;
345
+ /**
346
+ * Clear persisted pagination state for a command.
347
+ *
348
+ * Called when --reset flag is passed or when pagination completes.
349
+ *
350
+ * @param options - Cursor options specifying command and context
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * // User passed --reset flag
355
+ * if (flags.reset) {
356
+ * clearCursor({
357
+ * command: "list",
358
+ * toolName: "waymark",
359
+ * });
360
+ * }
361
+ * ```
362
+ */
363
+ declare function clearCursor(options: CursorOptions): void;
364
+ import { CancelledError as CancelledError2, ValidationError as ValidationError2 } from "@outfitter/contracts";
365
+ import { Result as Result2 } from "better-result";
366
+ /**
367
+ * Collect IDs from various input formats.
368
+ *
369
+ * Handles space-separated, comma-separated, repeated flags, @file, and stdin.
370
+ *
371
+ * @param input - Raw input from CLI arguments
372
+ * @param options - Collection options
373
+ * @returns Array of collected IDs
374
+ *
375
+ * @example
376
+ * ```typescript
377
+ * // All these produce the same result:
378
+ * // wm show id1 id2 id3
379
+ * // wm show id1,id2,id3
380
+ * // wm show --ids id1 --ids id2
381
+ * // wm show @ids.txt
382
+ * const ids = await collectIds(args.ids, {
383
+ * allowFile: true,
384
+ * allowStdin: true,
385
+ * });
386
+ * ```
387
+ */
388
+ declare function collectIds(input: string | readonly string[], options?: CollectIdsOptions): Promise<string[]>;
389
+ /**
390
+ * Expand @file references to file contents.
391
+ *
392
+ * If the input starts with @, reads the file and returns its contents.
393
+ * Otherwise, returns the input unchanged.
394
+ *
395
+ * @param input - Raw input that may be a @file reference
396
+ * @param options - Expansion options
397
+ * @returns File contents or original input
398
+ *
399
+ * @example
400
+ * ```typescript
401
+ * // wm create @template.md
402
+ * const content = await expandFileArg(args.content);
403
+ * ```
404
+ */
405
+ declare function expandFileArg(input: string, options?: ExpandFileOptions): Promise<string>;
406
+ /**
407
+ * Parse and expand glob patterns.
408
+ *
409
+ * Uses Bun.Glob with workspace constraints.
410
+ *
411
+ * @param pattern - Glob pattern to expand
412
+ * @param options - Glob options
413
+ * @returns Array of matched file paths
414
+ *
415
+ * @example
416
+ * ```typescript
417
+ * // wm index "src/**\/*.ts"
418
+ * const files = await parseGlob(args.pattern, {
419
+ * cwd: workspaceRoot,
420
+ * ignore: ["node_modules/**"],
421
+ * });
422
+ * ```
423
+ */
424
+ declare function parseGlob(pattern: string, options?: ParseGlobOptions): Promise<string[]>;
425
+ /**
426
+ * Parse key=value pairs from CLI input.
427
+ *
428
+ * @param input - Raw input containing key=value pairs
429
+ * @returns Array of parsed key-value pairs
430
+ *
431
+ * @example
432
+ * ```typescript
433
+ * // --set key=value --set key2=value2
434
+ * // --set key=value,key2=value2
435
+ * const pairs = parseKeyValue(args.set);
436
+ * // => [{ key: "key", value: "value" }, { key: "key2", value: "value2" }]
437
+ * ```
438
+ */
439
+ declare function parseKeyValue(input: string | readonly string[]): Result2<KeyValuePair[], InstanceType<typeof ValidationError2>>;
440
+ /**
441
+ * Parse range inputs (numeric or date).
442
+ *
443
+ * @param input - Range string (e.g., "1-10" or "2024-01-01..2024-12-31")
444
+ * @param type - Type of range to parse
445
+ * @returns Parsed range
446
+ *
447
+ * @example
448
+ * ```typescript
449
+ * parseRange("1-10", "number");
450
+ * // => Result<{ type: "number", min: 1, max: 10 }, ValidationError>
451
+ *
452
+ * parseRange("2024-01-01..2024-12-31", "date");
453
+ * // => Result<{ type: "date", start: Date, end: Date }, ValidationError>
454
+ * ```
455
+ */
456
+ declare function parseRange(input: string, type: "number" | "date"): Result2<Range, InstanceType<typeof ValidationError2>>;
457
+ /**
458
+ * Parse filter expressions from CLI input.
459
+ *
460
+ * @param input - Filter string (e.g., "status:active,priority:high")
461
+ * @returns Array of parsed filter expressions
462
+ *
463
+ * @example
464
+ * ```typescript
465
+ * parseFilter("status:active,priority:high");
466
+ * // => Result<[
467
+ * // { field: "status", value: "active" },
468
+ * // { field: "priority", value: "high" }
469
+ * // ], ValidationError>
470
+ * ```
471
+ */
472
+ declare function parseFilter(input: string): Result2<FilterExpression[], InstanceType<typeof ValidationError2>>;
473
+ /**
474
+ * Parse sort specification from CLI input.
475
+ *
476
+ * @param input - Sort string (e.g., "modified:desc,title:asc")
477
+ * @returns Array of parsed sort criteria
478
+ *
479
+ * @example
480
+ * ```typescript
481
+ * parseSortSpec("modified:desc,title:asc");
482
+ * // => Result<[
483
+ * // { field: "modified", direction: "desc" },
484
+ * // { field: "title", direction: "asc" }
485
+ * // ], ValidationError>
486
+ * ```
487
+ */
488
+ declare function parseSortSpec(input: string): Result2<SortCriteria[], InstanceType<typeof ValidationError2>>;
489
+ /**
490
+ * Normalize an identifier (trim, lowercase where appropriate).
491
+ *
492
+ * @param input - Raw identifier input
493
+ * @param options - Normalization options
494
+ * @returns Normalized identifier
495
+ *
496
+ * @example
497
+ * ```typescript
498
+ * normalizeId(" MY-ID ", { lowercase: true, trim: true });
499
+ * // => Result<"my-id", ValidationError>
500
+ * ```
501
+ */
502
+ declare function normalizeId(input: string, options?: NormalizeIdOptions): Result2<string, InstanceType<typeof ValidationError2>>;
503
+ /**
504
+ * Prompt for confirmation before destructive operations.
505
+ *
506
+ * Respects --yes flag for non-interactive mode.
507
+ *
508
+ * @param options - Confirmation options
509
+ * @returns Whether the user confirmed
510
+ *
511
+ * @example
512
+ * ```typescript
513
+ * const confirmed = await confirmDestructive({
514
+ * message: "Delete 5 notes?",
515
+ * bypassFlag: flags.yes,
516
+ * itemCount: 5,
517
+ * });
518
+ *
519
+ * if (confirmed.isErr()) {
520
+ * // User cancelled
521
+ * process.exit(0);
522
+ * }
523
+ * ```
524
+ */
525
+ declare function confirmDestructive(options: ConfirmDestructiveOptions): Promise<Result2<boolean, InstanceType<typeof CancelledError2>>>;
526
+ /**
527
+ * Create a new CLI instance with the given configuration.
528
+ *
529
+ * The CLI wraps Commander.js with typed helpers, output contract enforcement,
530
+ * and pagination state management.
531
+ *
532
+ * @param config - CLI configuration options
533
+ * @returns A CLI instance ready for command registration
534
+ *
535
+ * @example
536
+ * ```typescript
537
+ * import { createCLI, command, output } from "@outfitter/cli";
538
+ *
539
+ * const cli = createCLI({
540
+ * name: "waymark",
541
+ * version: "1.0.0",
542
+ * description: "A note management CLI",
543
+ * });
544
+ *
545
+ * cli.register(
546
+ * command("list")
547
+ * .description("List all notes")
548
+ * .action(async () => {
549
+ * const notes = await getNotes();
550
+ * output(notes);
551
+ * })
552
+ * );
553
+ *
554
+ * await cli.parse();
555
+ * ```
556
+ */
557
+ declare function createCLI(config: CLIConfig): CLI;
558
+ import { ActionRegistry, ActionSurface, AnyActionSpec, HandlerContext } from "@outfitter/contracts";
559
+ import { Command as Command2 } from "commander";
560
+ interface BuildCliCommandsOptions {
561
+ readonly createContext?: (input: {
562
+ action: AnyActionSpec;
563
+ args: readonly string[];
564
+ flags: Record<string, unknown>;
565
+ }) => HandlerContext;
566
+ readonly includeSurfaces?: readonly ActionSurface[];
567
+ }
568
+ type ActionSource = ActionRegistry | readonly AnyActionSpec[];
569
+ declare function buildCliCommands(source: ActionSource, options?: BuildCliCommandsOptions): Command2[];
570
+ /**
571
+ * Create a new command builder with the given name.
572
+ *
573
+ * The command builder provides a fluent API for defining CLI commands
574
+ * with typed flags, arguments, and actions.
575
+ *
576
+ * @param name - Command name and optional argument syntax (e.g., "list" or "get <id>")
577
+ * @returns A CommandBuilder instance for fluent configuration
578
+ *
579
+ * @example
580
+ * ```typescript
581
+ * import { command, output } from "@outfitter/cli";
582
+ *
583
+ * const list = command("list")
584
+ * .description("List all notes")
585
+ * .option("--limit <n>", "Max results", "20")
586
+ * .option("--json", "Output as JSON")
587
+ * .option("--next", "Continue from last position")
588
+ * .action(async ({ flags }) => {
589
+ * const results = await listNotes(flags);
590
+ * output(results);
591
+ * });
592
+ * ```
593
+ *
594
+ * @example
595
+ * ```typescript
596
+ * // Command with required argument
597
+ * const get = command("get <id>")
598
+ * .description("Get a note by ID")
599
+ * .action(async ({ args }) => {
600
+ * const [id] = args;
601
+ * const note = await getNote(id);
602
+ * output(note);
603
+ * });
604
+ * ```
605
+ */
606
+ declare function command(name: string): CommandBuilder;
607
+ export { saveCursor, parseSortSpec, parseRange, parseKeyValue, parseGlob, parseFilter, output, normalizeId, loadCursor, expandFileArg, exitWithError, createCLI, confirmDestructive, command, collectIds, clearCursor, buildCliCommands, ValidationError, SortCriteria, Range, ParseGlobOptions, PaginationState, OutputOptions, OutputMode, NumericRange, NormalizeIdOptions, KeyValuePair, FilterExpression, ExpandFileOptions, DateRange, CursorOptions, ConfirmDestructiveOptions, CommandFlags, CommandConfig, CommandBuilder, CommandAction, CollectIdsOptions, CancelledError, CLIConfig, CLI, BuildCliCommandsOptions };
package/dist/index.js ADDED
@@ -0,0 +1,50 @@
1
+ // @bun
2
+ import {
3
+ exitWithError,
4
+ output
5
+ } from "./output.js";
6
+ import"./shared/@outfitter/cli-4yy82cmp.js";
7
+ import {
8
+ clearCursor,
9
+ loadCursor,
10
+ saveCursor
11
+ } from "./pagination.js";
12
+ import {
13
+ collectIds,
14
+ confirmDestructive,
15
+ expandFileArg,
16
+ normalizeId,
17
+ parseFilter,
18
+ parseGlob,
19
+ parseKeyValue,
20
+ parseRange,
21
+ parseSortSpec
22
+ } from "./input.js";
23
+ import {
24
+ createCLI
25
+ } from "./cli.js";
26
+ import {
27
+ buildCliCommands
28
+ } from "./actions.js";
29
+ import {
30
+ command
31
+ } from "./command.js";
32
+ export {
33
+ saveCursor,
34
+ parseSortSpec,
35
+ parseRange,
36
+ parseKeyValue,
37
+ parseGlob,
38
+ parseFilter,
39
+ output,
40
+ normalizeId,
41
+ loadCursor,
42
+ expandFileArg,
43
+ exitWithError,
44
+ createCLI,
45
+ confirmDestructive,
46
+ command,
47
+ collectIds,
48
+ clearCursor,
49
+ buildCliCommands
50
+ };