@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.
- package/README.md +436 -0
- package/dist/actions.d.ts +13 -0
- package/dist/actions.js +175 -0
- package/dist/cli.d.ts +104 -0
- package/dist/cli.js +55 -0
- package/dist/command.d.ts +74 -0
- package/dist/command.js +46 -0
- package/dist/index.d.ts +607 -0
- package/dist/index.js +50 -0
- package/dist/input.d.ts +278 -0
- package/dist/input.js +439 -0
- package/dist/output.d.ts +69 -0
- package/dist/output.js +156 -0
- package/dist/pagination.d.ts +96 -0
- package/dist/pagination.js +90 -0
- package/dist/shared/@outfitter/cli-4yy82cmp.js +20 -0
- package/dist/types.d.ts +253 -0
- package/dist/types.js +12 -0
- package/package.json +100 -0
package/README.md
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
# @outfitter/cli
|
|
2
|
+
|
|
3
|
+
Typed CLI runtime with output contracts, input parsing, and pagination for Bun.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @outfitter/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { output, collectIds, loadCursor, saveCursor } from "@outfitter/cli";
|
|
15
|
+
|
|
16
|
+
// Output data with automatic mode detection
|
|
17
|
+
output({ id: "123", name: "Example" });
|
|
18
|
+
|
|
19
|
+
// Collect IDs from various input formats
|
|
20
|
+
const ids = await collectIds("id1,id2,id3");
|
|
21
|
+
|
|
22
|
+
// Handle pagination state
|
|
23
|
+
const cursor = loadCursor({ command: "list", toolName: "myapp" });
|
|
24
|
+
if (cursor) {
|
|
25
|
+
// Continue from last position
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## API Reference
|
|
30
|
+
|
|
31
|
+
### Output Utilities
|
|
32
|
+
|
|
33
|
+
#### `output(data, options?)`
|
|
34
|
+
|
|
35
|
+
Output data to the console with automatic mode selection.
|
|
36
|
+
|
|
37
|
+
Defaults to human-friendly output for TTY, JSON for non-TTY. Override via `mode` option or `OUTFITTER_JSON`/`OUTFITTER_JSONL` environment variables.
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { output } from "@outfitter/cli";
|
|
41
|
+
|
|
42
|
+
// Basic usage - mode auto-detected
|
|
43
|
+
output(results);
|
|
44
|
+
|
|
45
|
+
// Force JSON mode
|
|
46
|
+
output(results, { mode: "json" });
|
|
47
|
+
|
|
48
|
+
// Pretty-print JSON
|
|
49
|
+
output(results, { mode: "json", pretty: true });
|
|
50
|
+
|
|
51
|
+
// Output to stderr
|
|
52
|
+
output(errors, { stream: process.stderr });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Options:**
|
|
56
|
+
|
|
57
|
+
| Option | Type | Default | Description |
|
|
58
|
+
|--------|------|---------|-------------|
|
|
59
|
+
| `mode` | `OutputMode` | auto | Force a specific output mode |
|
|
60
|
+
| `stream` | `WritableStream` | `stdout` | Stream to write to |
|
|
61
|
+
| `pretty` | `boolean` | `false` | Pretty-print JSON output |
|
|
62
|
+
|
|
63
|
+
**Output Modes:**
|
|
64
|
+
|
|
65
|
+
- `human` - Human-readable key: value format
|
|
66
|
+
- `json` - Single JSON object
|
|
67
|
+
- `jsonl` - JSON Lines (one object per line)
|
|
68
|
+
- `tree` - Tree structure (reserved)
|
|
69
|
+
- `table` - Table format (reserved)
|
|
70
|
+
|
|
71
|
+
#### `exitWithError(error)`
|
|
72
|
+
|
|
73
|
+
Exit the process with an error message and appropriate exit code.
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { exitWithError } from "@outfitter/cli";
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
await riskyOperation();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
exitWithError(error instanceof Error ? error : new Error(String(error)));
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Input Utilities
|
|
86
|
+
|
|
87
|
+
#### `collectIds(input, options?)`
|
|
88
|
+
|
|
89
|
+
Collect IDs from various input formats: space-separated, comma-separated, repeated flags, `@file`, and stdin.
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { collectIds } from "@outfitter/cli";
|
|
93
|
+
|
|
94
|
+
// All these produce the same result:
|
|
95
|
+
// myapp show id1 id2 id3
|
|
96
|
+
// myapp show id1,id2,id3
|
|
97
|
+
// myapp show --ids id1 --ids id2
|
|
98
|
+
// myapp show @ids.txt
|
|
99
|
+
// echo "id1\nid2" | myapp show @-
|
|
100
|
+
|
|
101
|
+
const ids = await collectIds(args.ids, {
|
|
102
|
+
allowFile: true,
|
|
103
|
+
allowStdin: true,
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Options:**
|
|
108
|
+
|
|
109
|
+
| Option | Type | Default | Description |
|
|
110
|
+
|--------|------|---------|-------------|
|
|
111
|
+
| `allowFile` | `boolean` | `true` | Allow `@file` expansion |
|
|
112
|
+
| `allowStdin` | `boolean` | `true` | Allow `@-` for stdin |
|
|
113
|
+
|
|
114
|
+
#### `expandFileArg(input, options?)`
|
|
115
|
+
|
|
116
|
+
Expand `@file` references to file contents. Returns input unchanged if not a file reference.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { expandFileArg } from "@outfitter/cli";
|
|
120
|
+
|
|
121
|
+
// myapp create @template.md
|
|
122
|
+
const content = await expandFileArg(args.content);
|
|
123
|
+
|
|
124
|
+
// With options
|
|
125
|
+
const content = await expandFileArg(args.content, {
|
|
126
|
+
maxSize: 1024 * 1024, // 1MB limit
|
|
127
|
+
trim: true,
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Options:**
|
|
132
|
+
|
|
133
|
+
| Option | Type | Default | Description |
|
|
134
|
+
|--------|------|---------|-------------|
|
|
135
|
+
| `encoding` | `BufferEncoding` | `utf-8` | File encoding |
|
|
136
|
+
| `maxSize` | `number` | - | Maximum file size in bytes |
|
|
137
|
+
| `trim` | `boolean` | `false` | Trim whitespace |
|
|
138
|
+
|
|
139
|
+
#### `parseGlob(pattern, options?)`
|
|
140
|
+
|
|
141
|
+
Parse and expand glob patterns using `Bun.Glob`.
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { parseGlob } from "@outfitter/cli";
|
|
145
|
+
|
|
146
|
+
const files = await parseGlob("src/**/*.ts", {
|
|
147
|
+
cwd: workspaceRoot,
|
|
148
|
+
ignore: ["node_modules/**", "**/*.test.ts"],
|
|
149
|
+
onlyFiles: true,
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**Options:**
|
|
154
|
+
|
|
155
|
+
| Option | Type | Default | Description |
|
|
156
|
+
|--------|------|---------|-------------|
|
|
157
|
+
| `cwd` | `string` | `process.cwd()` | Working directory |
|
|
158
|
+
| `ignore` | `string[]` | `[]` | Patterns to exclude |
|
|
159
|
+
| `onlyFiles` | `boolean` | `false` | Only match files |
|
|
160
|
+
| `onlyDirectories` | `boolean` | `false` | Only match directories |
|
|
161
|
+
| `followSymlinks` | `boolean` | `false` | Follow symbolic links |
|
|
162
|
+
|
|
163
|
+
#### `parseKeyValue(input)`
|
|
164
|
+
|
|
165
|
+
Parse `key=value` pairs from CLI input.
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { parseKeyValue } from "@outfitter/cli";
|
|
169
|
+
|
|
170
|
+
// --set key=value --set key2=value2
|
|
171
|
+
// --set key=value,key2=value2
|
|
172
|
+
const result = parseKeyValue(args.set);
|
|
173
|
+
|
|
174
|
+
if (result.isOk()) {
|
|
175
|
+
// [{ key: "key", value: "value" }, { key: "key2", value: "value2" }]
|
|
176
|
+
console.log(result.value);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### `parseRange(input, type)`
|
|
181
|
+
|
|
182
|
+
Parse numeric or date range inputs.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { parseRange } from "@outfitter/cli";
|
|
186
|
+
|
|
187
|
+
// Numeric range
|
|
188
|
+
const numResult = parseRange("1-10", "number");
|
|
189
|
+
// => { type: "number", min: 1, max: 10 }
|
|
190
|
+
|
|
191
|
+
// Date range
|
|
192
|
+
const dateResult = parseRange("2024-01-01..2024-12-31", "date");
|
|
193
|
+
// => { type: "date", start: Date, end: Date }
|
|
194
|
+
|
|
195
|
+
// Single value
|
|
196
|
+
const single = parseRange("5", "number");
|
|
197
|
+
// => { type: "number", min: 5, max: 5 }
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
#### `parseFilter(input)`
|
|
201
|
+
|
|
202
|
+
Parse filter expressions from CLI input.
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { parseFilter } from "@outfitter/cli";
|
|
206
|
+
|
|
207
|
+
const result = parseFilter("status:active,priority:>=high,!archived:true");
|
|
208
|
+
|
|
209
|
+
if (result.isOk()) {
|
|
210
|
+
// [
|
|
211
|
+
// { field: "status", value: "active" },
|
|
212
|
+
// { field: "priority", value: "high", operator: "gte" },
|
|
213
|
+
// { field: "archived", value: "true", operator: "ne" }
|
|
214
|
+
// ]
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Filter Operators:**
|
|
219
|
+
|
|
220
|
+
| Prefix | Operator | Description |
|
|
221
|
+
|--------|----------|-------------|
|
|
222
|
+
| (none) | `eq` | Equals (default) |
|
|
223
|
+
| `!` | `ne` | Not equals |
|
|
224
|
+
| `>` | `gt` | Greater than |
|
|
225
|
+
| `<` | `lt` | Less than |
|
|
226
|
+
| `>=` | `gte` | Greater than or equal |
|
|
227
|
+
| `<=` | `lte` | Less than or equal |
|
|
228
|
+
| `~` | `contains` | Contains substring |
|
|
229
|
+
|
|
230
|
+
#### `parseSortSpec(input)`
|
|
231
|
+
|
|
232
|
+
Parse sort specification from CLI input.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { parseSortSpec } from "@outfitter/cli";
|
|
236
|
+
|
|
237
|
+
const result = parseSortSpec("modified:desc,title:asc");
|
|
238
|
+
|
|
239
|
+
if (result.isOk()) {
|
|
240
|
+
// [
|
|
241
|
+
// { field: "modified", direction: "desc" },
|
|
242
|
+
// { field: "title", direction: "asc" }
|
|
243
|
+
// ]
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### `normalizeId(input, options?)`
|
|
248
|
+
|
|
249
|
+
Normalize an identifier with validation.
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { normalizeId } from "@outfitter/cli";
|
|
253
|
+
|
|
254
|
+
const result = normalizeId(" MY-ID ", {
|
|
255
|
+
trim: true,
|
|
256
|
+
lowercase: true,
|
|
257
|
+
minLength: 3,
|
|
258
|
+
maxLength: 50,
|
|
259
|
+
pattern: /^[a-z0-9-]+$/,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (result.isOk()) {
|
|
263
|
+
// "my-id"
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Options:**
|
|
268
|
+
|
|
269
|
+
| Option | Type | Default | Description |
|
|
270
|
+
|--------|------|---------|-------------|
|
|
271
|
+
| `trim` | `boolean` | `false` | Trim whitespace |
|
|
272
|
+
| `lowercase` | `boolean` | `false` | Convert to lowercase |
|
|
273
|
+
| `minLength` | `number` | - | Minimum length |
|
|
274
|
+
| `maxLength` | `number` | - | Maximum length |
|
|
275
|
+
| `pattern` | `RegExp` | - | Required pattern |
|
|
276
|
+
|
|
277
|
+
#### `confirmDestructive(options)`
|
|
278
|
+
|
|
279
|
+
Prompt for confirmation before destructive operations. Respects `--yes` flag for non-interactive mode.
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { confirmDestructive } from "@outfitter/cli";
|
|
283
|
+
|
|
284
|
+
const result = await confirmDestructive({
|
|
285
|
+
message: "Delete 5 notes?",
|
|
286
|
+
bypassFlag: flags.yes,
|
|
287
|
+
itemCount: 5,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
if (result.isErr()) {
|
|
291
|
+
// User cancelled or non-TTY environment
|
|
292
|
+
console.error("Operation cancelled");
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Proceed with destructive operation
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Pagination Utilities
|
|
300
|
+
|
|
301
|
+
Pagination state persists per-command to support `--next` and `--reset` functionality.
|
|
302
|
+
|
|
303
|
+
**XDG State Directory Pattern:**
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
$XDG_STATE_HOME/{toolName}/cursors/{command}[/{context}]/cursor.json
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### `loadCursor(options)`
|
|
310
|
+
|
|
311
|
+
Load persisted pagination state for a command.
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
import { loadCursor } from "@outfitter/cli";
|
|
315
|
+
|
|
316
|
+
const state = loadCursor({
|
|
317
|
+
command: "list",
|
|
318
|
+
toolName: "waymark",
|
|
319
|
+
context: "workspace-123", // optional
|
|
320
|
+
maxAgeMs: 30 * 60 * 1000, // optional expiration window
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
if (state) {
|
|
324
|
+
// Continue from last position
|
|
325
|
+
const results = await listNotes({ cursor: state.cursor });
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
#### `saveCursor(cursor, options)`
|
|
330
|
+
|
|
331
|
+
Save pagination state for a command.
|
|
332
|
+
|
|
333
|
+
```typescript
|
|
334
|
+
import { saveCursor } from "@outfitter/cli";
|
|
335
|
+
|
|
336
|
+
const results = await listNotes({ limit: 20 });
|
|
337
|
+
|
|
338
|
+
if (results.hasMore) {
|
|
339
|
+
saveCursor(results.cursor, {
|
|
340
|
+
command: "list",
|
|
341
|
+
toolName: "waymark",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
#### `clearCursor(options)`
|
|
347
|
+
|
|
348
|
+
Clear persisted pagination state for a command.
|
|
349
|
+
|
|
350
|
+
```typescript
|
|
351
|
+
import { clearCursor } from "@outfitter/cli";
|
|
352
|
+
|
|
353
|
+
// User passed --reset flag
|
|
354
|
+
if (flags.reset) {
|
|
355
|
+
clearCursor({
|
|
356
|
+
command: "list",
|
|
357
|
+
toolName: "waymark",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
## Configuration
|
|
363
|
+
|
|
364
|
+
### Environment Variables
|
|
365
|
+
|
|
366
|
+
| Variable | Description | Default |
|
|
367
|
+
|----------|-------------|---------|
|
|
368
|
+
| `OUTFITTER_JSON` | Set to `1` to force JSON output | - |
|
|
369
|
+
| `OUTFITTER_JSONL` | Set to `1` to force JSONL output (takes priority over JSON) | - |
|
|
370
|
+
| `XDG_STATE_HOME` | State directory for pagination | Platform-specific |
|
|
371
|
+
|
|
372
|
+
### Output Mode Priority
|
|
373
|
+
|
|
374
|
+
1. Explicit `mode` option in `output()` call
|
|
375
|
+
2. `OUTFITTER_JSONL=1` environment variable (highest env priority)
|
|
376
|
+
3. `OUTFITTER_JSON=1` environment variable
|
|
377
|
+
4. `OUTFITTER_JSON=0` or `OUTFITTER_JSONL=0` forces human mode
|
|
378
|
+
5. TTY detection: `json` for non-TTY, `human` for TTY
|
|
379
|
+
|
|
380
|
+
## Error Handling
|
|
381
|
+
|
|
382
|
+
### Exit Code Mapping
|
|
383
|
+
|
|
384
|
+
Exit codes are automatically determined from error categories:
|
|
385
|
+
|
|
386
|
+
| Category | Exit Code |
|
|
387
|
+
|----------|-----------|
|
|
388
|
+
| `validation` | 1 |
|
|
389
|
+
| `not_found` | 2 |
|
|
390
|
+
| `conflict` | 3 |
|
|
391
|
+
| `permission` | 4 |
|
|
392
|
+
| `timeout` | 5 |
|
|
393
|
+
| `rate_limit` | 6 |
|
|
394
|
+
| `network` | 7 |
|
|
395
|
+
| `internal` | 8 |
|
|
396
|
+
| `auth` | 9 |
|
|
397
|
+
| `cancelled` | 130 |
|
|
398
|
+
|
|
399
|
+
### Tagged Errors
|
|
400
|
+
|
|
401
|
+
Errors with a `category` property are automatically mapped to exit codes:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const error = new Error("File not found") as Error & { category: string };
|
|
405
|
+
error.category = "not_found";
|
|
406
|
+
|
|
407
|
+
exitWithError(error); // Exits with code 2
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## Types
|
|
411
|
+
|
|
412
|
+
All types are exported for TypeScript consumers:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
import type {
|
|
416
|
+
// Core types
|
|
417
|
+
CLIConfig,
|
|
418
|
+
CommandConfig,
|
|
419
|
+
CommandAction,
|
|
420
|
+
CommandFlags,
|
|
421
|
+
// Output types
|
|
422
|
+
OutputMode,
|
|
423
|
+
OutputOptions,
|
|
424
|
+
// Input types
|
|
425
|
+
CollectIdsOptions,
|
|
426
|
+
ExpandFileOptions,
|
|
427
|
+
ParseGlobOptions,
|
|
428
|
+
// Pagination types
|
|
429
|
+
PaginationState,
|
|
430
|
+
CursorOptions,
|
|
431
|
+
} from "@outfitter/cli";
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## License
|
|
435
|
+
|
|
436
|
+
MIT
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ActionRegistry, ActionSurface, AnyActionSpec, HandlerContext } from "@outfitter/contracts";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
interface BuildCliCommandsOptions {
|
|
4
|
+
readonly createContext?: (input: {
|
|
5
|
+
action: AnyActionSpec;
|
|
6
|
+
args: readonly string[];
|
|
7
|
+
flags: Record<string, unknown>;
|
|
8
|
+
}) => HandlerContext;
|
|
9
|
+
readonly includeSurfaces?: readonly ActionSurface[];
|
|
10
|
+
}
|
|
11
|
+
type ActionSource = ActionRegistry | readonly AnyActionSpec[];
|
|
12
|
+
declare function buildCliCommands(source: ActionSource, options?: BuildCliCommandsOptions): Command[];
|
|
13
|
+
export { buildCliCommands, BuildCliCommandsOptions };
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import"./shared/@outfitter/cli-4yy82cmp.js";
|
|
3
|
+
|
|
4
|
+
// packages/cli/src/actions.ts
|
|
5
|
+
import {
|
|
6
|
+
createContext as createHandlerContext,
|
|
7
|
+
DEFAULT_REGISTRY_SURFACES,
|
|
8
|
+
validateInput
|
|
9
|
+
} from "@outfitter/contracts";
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
var ARGUMENT_PREFIXES = ["<", "["];
|
|
12
|
+
function isArgumentToken(token) {
|
|
13
|
+
if (!token) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
return ARGUMENT_PREFIXES.some((prefix) => token.startsWith(prefix));
|
|
17
|
+
}
|
|
18
|
+
function splitCommandSpec(spec) {
|
|
19
|
+
const parts = spec.trim().split(/\s+/).filter(Boolean);
|
|
20
|
+
if (parts.length === 0) {
|
|
21
|
+
return { name: undefined, args: [] };
|
|
22
|
+
}
|
|
23
|
+
return { name: parts[0], args: parts.slice(1) };
|
|
24
|
+
}
|
|
25
|
+
function applyArguments(command, args) {
|
|
26
|
+
for (const arg of args) {
|
|
27
|
+
command.argument(arg);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function applyCliOptions(command, action) {
|
|
31
|
+
const options = action.cli?.options ?? [];
|
|
32
|
+
for (const option of options) {
|
|
33
|
+
if (option.required) {
|
|
34
|
+
command.requiredOption(option.flags, option.description, option.defaultValue);
|
|
35
|
+
} else {
|
|
36
|
+
command.option(option.flags, option.description, option.defaultValue);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function resolveDescription(action) {
|
|
41
|
+
return action.cli?.description ?? action.description ?? action.id;
|
|
42
|
+
}
|
|
43
|
+
function resolveAliases(action) {
|
|
44
|
+
return action.cli?.aliases ?? [];
|
|
45
|
+
}
|
|
46
|
+
function resolveCommandSpec(action) {
|
|
47
|
+
return action.cli?.command ?? action.id;
|
|
48
|
+
}
|
|
49
|
+
function resolveFlags(command) {
|
|
50
|
+
return command.optsWithGlobals?.() ?? command.opts();
|
|
51
|
+
}
|
|
52
|
+
function resolveInput(action, context) {
|
|
53
|
+
if (action.cli?.mapInput) {
|
|
54
|
+
return action.cli.mapInput(context);
|
|
55
|
+
}
|
|
56
|
+
const hasFlags = Object.keys(context.flags).length > 0;
|
|
57
|
+
if (!hasFlags && context.args.length === 0) {
|
|
58
|
+
return {};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
...context.flags,
|
|
62
|
+
...context.args.length > 0 ? { args: context.args } : {}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function runAction(action, command, createContext) {
|
|
66
|
+
const flags = resolveFlags(command);
|
|
67
|
+
const args = command.args;
|
|
68
|
+
const inputContext = { args, flags };
|
|
69
|
+
const input = resolveInput(action, inputContext);
|
|
70
|
+
const validation = validateInput(action.input, input);
|
|
71
|
+
if (validation.isErr()) {
|
|
72
|
+
throw validation.error;
|
|
73
|
+
}
|
|
74
|
+
const ctx = createContext({ action, args, flags });
|
|
75
|
+
const result = await action.handler(validation.value, ctx);
|
|
76
|
+
if (result.isErr()) {
|
|
77
|
+
throw result.error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function createCommand(action, createContext, spec) {
|
|
81
|
+
const commandSpec = spec ?? resolveCommandSpec(action);
|
|
82
|
+
const { name, args } = splitCommandSpec(commandSpec);
|
|
83
|
+
if (!name) {
|
|
84
|
+
throw new Error(`Missing CLI command name for action ${action.id}`);
|
|
85
|
+
}
|
|
86
|
+
const command = new Command(name);
|
|
87
|
+
command.description(resolveDescription(action));
|
|
88
|
+
applyCliOptions(command, action);
|
|
89
|
+
applyArguments(command, args);
|
|
90
|
+
for (const alias of resolveAliases(action)) {
|
|
91
|
+
command.alias(alias);
|
|
92
|
+
}
|
|
93
|
+
command.action(async (...argsList) => {
|
|
94
|
+
const commandInstance = argsList.at(-1);
|
|
95
|
+
await runAction(action, commandInstance, createContext);
|
|
96
|
+
});
|
|
97
|
+
return command;
|
|
98
|
+
}
|
|
99
|
+
function buildCliCommands(source, options = {}) {
|
|
100
|
+
const actions = isActionRegistry(source) ? source.list() : source;
|
|
101
|
+
const includeSurfaces = options.includeSurfaces ?? [
|
|
102
|
+
"cli"
|
|
103
|
+
];
|
|
104
|
+
const commands = [];
|
|
105
|
+
const createContext = options.createContext ?? ((_input) => createHandlerContext({
|
|
106
|
+
cwd: process.cwd(),
|
|
107
|
+
env: process.env
|
|
108
|
+
}));
|
|
109
|
+
const grouped = new Map;
|
|
110
|
+
const ungrouped = [];
|
|
111
|
+
for (const action of actions) {
|
|
112
|
+
const surfaces = action.surfaces ?? DEFAULT_REGISTRY_SURFACES;
|
|
113
|
+
if (!surfaces.some((surface) => includeSurfaces.includes(surface))) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const group = action.cli?.group;
|
|
117
|
+
if (group) {
|
|
118
|
+
const groupActions = grouped.get(group) ?? [];
|
|
119
|
+
groupActions.push(action);
|
|
120
|
+
grouped.set(group, groupActions);
|
|
121
|
+
} else {
|
|
122
|
+
ungrouped.push(action);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const action of ungrouped) {
|
|
126
|
+
commands.push(createCommand(action, createContext));
|
|
127
|
+
}
|
|
128
|
+
for (const [groupName, groupActions] of grouped.entries()) {
|
|
129
|
+
const groupCommand = new Command(groupName);
|
|
130
|
+
let baseAction;
|
|
131
|
+
const subcommands = [];
|
|
132
|
+
for (const action of groupActions) {
|
|
133
|
+
const spec = action.cli?.command?.trim() ?? "";
|
|
134
|
+
const { name, args } = splitCommandSpec(spec);
|
|
135
|
+
if (!name || isArgumentToken(name)) {
|
|
136
|
+
if (baseAction) {
|
|
137
|
+
throw new Error(`Group '${groupName}' defines multiple base actions: '${baseAction.id}' and '${action.id}'.`);
|
|
138
|
+
}
|
|
139
|
+
baseAction = action;
|
|
140
|
+
groupCommand.description(resolveDescription(action));
|
|
141
|
+
applyCliOptions(groupCommand, action);
|
|
142
|
+
applyArguments(groupCommand, name ? [name, ...args] : args);
|
|
143
|
+
for (const alias of resolveAliases(action)) {
|
|
144
|
+
groupCommand.alias(alias);
|
|
145
|
+
}
|
|
146
|
+
groupCommand.action(async (...argsList) => {
|
|
147
|
+
const commandInstance = argsList.at(-1);
|
|
148
|
+
await runAction(action, commandInstance, createContext);
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
subcommands.push(action);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const action of subcommands) {
|
|
155
|
+
const spec = resolveCommandSpec(action);
|
|
156
|
+
const { name, args } = splitCommandSpec(spec);
|
|
157
|
+
if (!name) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const subcommand = createCommand(action, createContext, [name, ...args].join(" "));
|
|
161
|
+
groupCommand.addCommand(subcommand);
|
|
162
|
+
}
|
|
163
|
+
if (!baseAction) {
|
|
164
|
+
groupCommand.description(groupName);
|
|
165
|
+
}
|
|
166
|
+
commands.push(groupCommand);
|
|
167
|
+
}
|
|
168
|
+
return commands;
|
|
169
|
+
}
|
|
170
|
+
function isActionRegistry(source) {
|
|
171
|
+
return "list" in source;
|
|
172
|
+
}
|
|
173
|
+
export {
|
|
174
|
+
buildCliCommands
|
|
175
|
+
};
|