@ontrails/cli 1.0.0-beta.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 (70) hide show
  1. package/.turbo/turbo-build.log +1 -0
  2. package/.turbo/turbo-lint.log +3 -0
  3. package/.turbo/turbo-typecheck.log +1 -0
  4. package/CHANGELOG.md +20 -0
  5. package/README.md +166 -0
  6. package/dist/build.d.ts +25 -0
  7. package/dist/build.d.ts.map +1 -0
  8. package/dist/build.js +167 -0
  9. package/dist/build.js.map +1 -0
  10. package/dist/command.d.ts +47 -0
  11. package/dist/command.d.ts.map +1 -0
  12. package/dist/command.js +9 -0
  13. package/dist/command.js.map +1 -0
  14. package/dist/commander/blaze.d.ts +31 -0
  15. package/dist/commander/blaze.d.ts.map +1 -0
  16. package/dist/commander/blaze.js +42 -0
  17. package/dist/commander/blaze.js.map +1 -0
  18. package/dist/commander/index.d.ts +5 -0
  19. package/dist/commander/index.d.ts.map +1 -0
  20. package/dist/commander/index.js +3 -0
  21. package/dist/commander/index.js.map +1 -0
  22. package/dist/commander/to-commander.d.ts +12 -0
  23. package/dist/commander/to-commander.d.ts.map +1 -0
  24. package/dist/commander/to-commander.js +148 -0
  25. package/dist/commander/to-commander.js.map +1 -0
  26. package/dist/flags.d.ts +17 -0
  27. package/dist/flags.d.ts.map +1 -0
  28. package/dist/flags.js +180 -0
  29. package/dist/flags.js.map +1 -0
  30. package/dist/index.d.ts +11 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +13 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/layers.d.ts +21 -0
  35. package/dist/layers.d.ts.map +1 -0
  36. package/dist/layers.js +156 -0
  37. package/dist/layers.js.map +1 -0
  38. package/dist/on-result.d.ts +12 -0
  39. package/dist/on-result.d.ts.map +1 -0
  40. package/dist/on-result.js +21 -0
  41. package/dist/on-result.js.map +1 -0
  42. package/dist/output.d.ts +20 -0
  43. package/dist/output.d.ts.map +1 -0
  44. package/dist/output.js +82 -0
  45. package/dist/output.js.map +1 -0
  46. package/dist/prompt.d.ts +29 -0
  47. package/dist/prompt.d.ts.map +1 -0
  48. package/dist/prompt.js +12 -0
  49. package/dist/prompt.js.map +1 -0
  50. package/package.json +29 -0
  51. package/src/__tests__/blaze.test.ts +78 -0
  52. package/src/__tests__/build.test.ts +219 -0
  53. package/src/__tests__/flags.test.ts +176 -0
  54. package/src/__tests__/layers.test.ts +218 -0
  55. package/src/__tests__/on-result.test.ts +64 -0
  56. package/src/__tests__/output.test.ts +115 -0
  57. package/src/__tests__/to-commander.test.ts +133 -0
  58. package/src/build.ts +267 -0
  59. package/src/command.ts +73 -0
  60. package/src/commander/blaze.ts +67 -0
  61. package/src/commander/index.ts +5 -0
  62. package/src/commander/to-commander.ts +186 -0
  63. package/src/flags.ts +250 -0
  64. package/src/index.ts +28 -0
  65. package/src/layers.ts +231 -0
  66. package/src/on-result.ts +27 -0
  67. package/src/output.ts +101 -0
  68. package/src/prompt.ts +40 -0
  69. package/tsconfig.json +9 -0
  70. package/tsconfig.tsbuildinfo +1 -0
package/src/flags.ts ADDED
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Flag derivation from surface-agnostic fields and reusable flag presets.
3
+ */
4
+
5
+ import type { Field } from '@ontrails/core';
6
+ import type { z } from 'zod';
7
+
8
+ import type { CliFlag } from './command.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** Convert camelCase to kebab-case. */
15
+ const toKebab = (str: string): string =>
16
+ str.replaceAll(/[A-Z]/g, (ch) => `-${ch.toLowerCase()}`);
17
+
18
+ interface ZodInternals {
19
+ readonly _zod: {
20
+ readonly def: Readonly<Record<string, unknown>>;
21
+ };
22
+ readonly description?: string;
23
+ }
24
+
25
+ interface CliFlagShape {
26
+ readonly choices?: string[] | undefined;
27
+ readonly type: CliFlag['type'];
28
+ readonly variadic: boolean;
29
+ }
30
+
31
+ const fieldTypeToCliFlag: Record<Field['type'], CliFlagShape> = {
32
+ boolean: { type: 'boolean', variadic: false },
33
+ enum: { type: 'string', variadic: false },
34
+ multiselect: { type: 'string[]', variadic: true },
35
+ number: { type: 'number', variadic: false },
36
+ string: { type: 'string', variadic: false },
37
+ };
38
+
39
+ /** Convert a derived field into a CLI flag descriptor. */
40
+ const toCliFlag = (field: Field): CliFlag => {
41
+ const shape = fieldTypeToCliFlag[field.type];
42
+ return {
43
+ choices: field.options?.map((option) => option.value),
44
+ default: field.default,
45
+ description: field.label,
46
+ name: toKebab(field.name),
47
+ required: field.required,
48
+ type: shape.type,
49
+ variadic: shape.variadic,
50
+ };
51
+ };
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Public API
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /** Convert derived fields to CLI flags. */
58
+ export const toFlags = (fields: readonly Field[]): CliFlag[] =>
59
+ fields.map(toCliFlag);
60
+
61
+ interface UnwrapState {
62
+ defaultValue: unknown;
63
+ description: string | undefined;
64
+ required: boolean;
65
+ }
66
+
67
+ /** Get the inner type from an optional or default wrapper. */
68
+ const getInnerType = (current: ZodInternals): ZodInternals =>
69
+ current._zod.def['innerType'] as ZodInternals;
70
+
71
+ /** Propagate description from an inner type. */
72
+ const propagateDescription = (
73
+ inner: ZodInternals,
74
+ state: UnwrapState
75
+ ): void => {
76
+ if (inner.description) {
77
+ state.description = inner.description;
78
+ }
79
+ };
80
+
81
+ /** Step one unwrap level for optional/default fields. */
82
+ const unwrapStep = (
83
+ current: ZodInternals,
84
+ state: UnwrapState
85
+ ): ZodInternals | null => {
86
+ const defType = current._zod.def['type'] as string;
87
+ if (defType !== 'optional' && defType !== 'default') {
88
+ return null;
89
+ }
90
+ state.required = false;
91
+ if (defType === 'default') {
92
+ state.defaultValue = current._zod.def['defaultValue'];
93
+ }
94
+ const inner = getInnerType(current);
95
+ propagateDescription(inner, state);
96
+ return inner;
97
+ };
98
+
99
+ /** Unwrap optional/default wrappers, collecting metadata. */
100
+ const unwrap = (
101
+ schema: ZodInternals
102
+ ): {
103
+ defaultValue: unknown;
104
+ description: string | undefined;
105
+ inner: ZodInternals;
106
+ required: boolean;
107
+ } => {
108
+ const state: UnwrapState = {
109
+ defaultValue: undefined,
110
+ description: schema.description,
111
+ required: true,
112
+ };
113
+ let current = schema;
114
+
115
+ // eslint-disable-next-line no-constant-condition
116
+ while (true) {
117
+ const next = unwrapStep(current, state);
118
+ if (next === null) {
119
+ break;
120
+ }
121
+ current = next;
122
+ }
123
+
124
+ return { ...state, inner: current };
125
+ };
126
+
127
+ interface DerivedFlagShape {
128
+ readonly choices?: string[] | undefined;
129
+ readonly type: CliFlag['type'];
130
+ readonly variadic: boolean;
131
+ }
132
+
133
+ const flagShapeByDef: Record<
134
+ string,
135
+ (schema: ZodInternals) => DerivedFlagShape
136
+ > = {
137
+ array: (schema) => {
138
+ const element = schema._zod.def['element'] as ZodInternals;
139
+ const elementType = element._zod.def['type'] as string;
140
+ if (elementType === 'number') {
141
+ return { type: 'number[]', variadic: true };
142
+ }
143
+ return { type: 'string[]', variadic: true };
144
+ },
145
+ boolean: () => ({ type: 'boolean', variadic: false }),
146
+ enum: (schema) => {
147
+ const entries = schema._zod.def['entries'] as Record<string, string>;
148
+ return {
149
+ choices: Object.values(entries),
150
+ type: 'string',
151
+ variadic: false,
152
+ };
153
+ },
154
+ number: () => ({ type: 'number', variadic: false }),
155
+ string: () => ({ type: 'string', variadic: false }),
156
+ };
157
+
158
+ /** Derive the CLI flag shape for an unwrapped Zod field. */
159
+ const deriveFlagShape = (schema: ZodInternals): DerivedFlagShape => {
160
+ const defType = schema._zod.def['type'] as string;
161
+ const deriveShape = flagShapeByDef[defType];
162
+ return deriveShape
163
+ ? deriveShape(schema)
164
+ : { type: 'string', variadic: false };
165
+ };
166
+
167
+ /** Derive a single CLI flag from an object shape entry. */
168
+ const deriveFlag = (key: string, value: ZodInternals): CliFlag => {
169
+ const { inner, required, defaultValue, description } = unwrap(value);
170
+ const { choices, type, variadic } = deriveFlagShape(inner);
171
+ return {
172
+ choices,
173
+ default: defaultValue,
174
+ description: description ?? inner.description,
175
+ name: toKebab(key),
176
+ required,
177
+ type,
178
+ variadic,
179
+ };
180
+ };
181
+
182
+ /** Derive CLI flags from a Zod input schema. */
183
+ export const deriveFlags = (schema: z.ZodType): CliFlag[] => {
184
+ const zod = schema as unknown as ZodInternals;
185
+ if ((zod._zod.def['type'] as string) !== 'object') {
186
+ return [];
187
+ }
188
+ const shape = zod._zod.def['shape'] as
189
+ | Record<string, ZodInternals>
190
+ | undefined;
191
+ if (!shape) {
192
+ return [];
193
+ }
194
+ return Object.entries(shape).map(([key, value]) => deriveFlag(key, value));
195
+ };
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // Presets
199
+ // ---------------------------------------------------------------------------
200
+
201
+ /** Flags for output mode selection: --output, --json, --jsonl */
202
+ export const outputModePreset = (): CliFlag[] => [
203
+ {
204
+ choices: ['text', 'json', 'jsonl'],
205
+ default: 'text',
206
+ description: 'Output format',
207
+ name: 'output',
208
+ required: false,
209
+ short: 'o',
210
+ type: 'string',
211
+ variadic: false,
212
+ },
213
+ {
214
+ description: 'Shorthand for --output json',
215
+ name: 'json',
216
+ required: false,
217
+ type: 'boolean',
218
+ variadic: false,
219
+ },
220
+ {
221
+ description: 'Shorthand for --output jsonl',
222
+ name: 'jsonl',
223
+ required: false,
224
+ type: 'boolean',
225
+ variadic: false,
226
+ },
227
+ ];
228
+
229
+ /** Flag for working directory override: --cwd */
230
+ export const cwdPreset = (): CliFlag[] => [
231
+ {
232
+ description: 'Working directory override',
233
+ name: 'cwd',
234
+ required: false,
235
+ type: 'string',
236
+ variadic: false,
237
+ },
238
+ ];
239
+
240
+ /** Flag for dry-run mode: --dry-run */
241
+ export const dryRunPreset = (): CliFlag[] => [
242
+ {
243
+ default: false,
244
+ description: 'Execute without side effects',
245
+ name: 'dry-run',
246
+ required: false,
247
+ type: 'boolean',
248
+ variadic: false,
249
+ },
250
+ ];
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ // Command model
2
+ export type { AnyTrail, CliCommand, CliFlag, CliArg } from './command.js';
3
+
4
+ // Build
5
+ export { buildCliCommands } from './build.js';
6
+ export type { BuildCliCommandsOptions, ActionResultContext } from './build.js';
7
+
8
+ // Flags
9
+ export {
10
+ deriveFlags,
11
+ outputModePreset,
12
+ cwdPreset,
13
+ dryRunPreset,
14
+ } from './flags.js';
15
+
16
+ // Output
17
+ export { output, resolveOutputMode } from './output.js';
18
+ export type { OutputMode } from './output.js';
19
+
20
+ // onResult
21
+ export { defaultOnResult } from './on-result.js';
22
+
23
+ // Prompt
24
+ export { passthroughResolver, isInteractive } from './prompt.js';
25
+ export type { Field, InputResolver, ResolveInputOptions } from './prompt.js';
26
+
27
+ // Layers
28
+ export { autoIterateLayer, dateShortcutsLayer } from './layers.js';
package/src/layers.ts ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * CLI-specific layers shipped with @ontrails/cli.
3
+ */
4
+
5
+ import type { Implementation, Layer, Trail } from '@ontrails/core';
6
+ import { Result } from '@ontrails/core';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Pagination output shape detection
10
+ // ---------------------------------------------------------------------------
11
+
12
+ interface ZodInternals {
13
+ readonly _zod: {
14
+ readonly def: Readonly<Record<string, unknown>>;
15
+ };
16
+ }
17
+
18
+ /** Check if a trail's output schema looks like a paginated response. */
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ const isPaginatedOutput = (trail: Trail<any, any>): boolean => {
21
+ if (!trail.output) {
22
+ return false;
23
+ }
24
+ const s = trail.output as unknown as ZodInternals;
25
+ const defType = s._zod.def['type'] as string;
26
+ if (defType !== 'object') {
27
+ return false;
28
+ }
29
+
30
+ const shape = s._zod.def['shape'] as Record<string, ZodInternals> | undefined;
31
+ if (!shape) {
32
+ return false;
33
+ }
34
+
35
+ return 'items' in shape && 'hasMore' in shape && 'nextCursor' in shape;
36
+ };
37
+
38
+ /** Check if a trail's input schema has since/until fields. */
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ const hasDateRangeFields = (trail: Trail<any, any>): boolean => {
41
+ const s = trail.input as unknown as ZodInternals;
42
+ const defType = s._zod.def['type'] as string;
43
+ if (defType !== 'object') {
44
+ return false;
45
+ }
46
+
47
+ const shape = s._zod.def['shape'] as Record<string, ZodInternals> | undefined;
48
+ if (!shape) {
49
+ return false;
50
+ }
51
+
52
+ return 'since' in shape || 'until' in shape;
53
+ };
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // autoIterateLayer
57
+ // ---------------------------------------------------------------------------
58
+
59
+ interface PaginatedInput {
60
+ cursor?: string;
61
+ all?: boolean;
62
+ [key: string]: unknown;
63
+ }
64
+
65
+ interface PaginatedOutput {
66
+ items: unknown[];
67
+ hasMore: boolean;
68
+ nextCursor?: string;
69
+ }
70
+
71
+ /** Fetch one page and extract items. Returns error result or page data. */
72
+ const fetchPage = async <I, O>(
73
+ inp: PaginatedInput,
74
+ cursor: string | undefined,
75
+ implementation: Implementation<I, O>,
76
+ ctx: Parameters<Implementation<I, O>>[1]
77
+ ): Promise<Result<PaginatedOutput, Error>> => {
78
+ const pageInput = { ...inp, cursor } as I;
79
+ const result = await implementation(pageInput, ctx);
80
+ if (result.isErr()) {
81
+ return result as Result<PaginatedOutput, Error>;
82
+ }
83
+ return Result.ok(result.value as PaginatedOutput);
84
+ };
85
+
86
+ /** Accumulate items from a page and return the next cursor if there are more. */
87
+ const accumulatePage = (
88
+ page: PaginatedOutput,
89
+ allItems: unknown[]
90
+ ): string | undefined => {
91
+ allItems.push(...page.items);
92
+ return page.hasMore && page.nextCursor ? page.nextCursor : undefined;
93
+ };
94
+
95
+ /** Collect all pages into a single result. */
96
+ const collectAllPages = async <I, O>(
97
+ inp: PaginatedInput,
98
+ implementation: Implementation<I, O>,
99
+ ctx: Parameters<Implementation<I, O>>[1]
100
+ ): Promise<Result<unknown, Error>> => {
101
+ const allItems: unknown[] = [];
102
+ let cursor: string | undefined;
103
+
104
+ for (;;) {
105
+ const pageResult = await fetchPage(inp, cursor, implementation, ctx);
106
+ if (pageResult.isErr()) {
107
+ return pageResult;
108
+ }
109
+ cursor = accumulatePage(pageResult.value as PaginatedOutput, allItems);
110
+ if (cursor === undefined) {
111
+ break;
112
+ }
113
+ }
114
+
115
+ return Result.ok({ hasMore: false, items: allItems });
116
+ };
117
+
118
+ /**
119
+ * Automatically iterates paginated results when --all flag is present.
120
+ *
121
+ * When a trail's output matches the pagination pattern (items, hasMore,
122
+ * nextCursor) and the input contains `all: true`, this layer repeatedly
123
+ * calls the implementation with incrementing cursors and collects all items.
124
+ */
125
+ export const autoIterateLayer: Layer = {
126
+ description: 'Auto-paginate results when --all flag is set',
127
+ name: 'autoIterate',
128
+
129
+ wrap<I, O>(
130
+ trail: Trail<I, O>,
131
+ implementation: Implementation<I, O>
132
+ ): Implementation<I, O> {
133
+ if (!isPaginatedOutput(trail)) {
134
+ return implementation;
135
+ }
136
+
137
+ return (input, ctx) => {
138
+ const inp = input as PaginatedInput;
139
+ if (!inp.all) {
140
+ return implementation(input, ctx);
141
+ }
142
+ return collectAllPages(inp, implementation, ctx) as Promise<
143
+ Result<O, Error>
144
+ >;
145
+ };
146
+ },
147
+ };
148
+
149
+ // ---------------------------------------------------------------------------
150
+ // dateShortcutsLayer
151
+ // ---------------------------------------------------------------------------
152
+
153
+ /** Build a date relative to today with a day offset. */
154
+ const daysAgo = (days: number): string => {
155
+ const now = new Date();
156
+ return new Date(
157
+ now.getFullYear(),
158
+ now.getMonth(),
159
+ now.getDate() - days
160
+ ).toISOString();
161
+ };
162
+
163
+ const dateShortcuts: Record<string, () => string> = {
164
+ 'this-month': () => {
165
+ const now = new Date();
166
+ return new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
167
+ },
168
+ 'this-week': () => {
169
+ const now = new Date();
170
+ const dayOfWeek = now.getDay();
171
+ const diff = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
172
+ return daysAgo(diff);
173
+ },
174
+ today: () => daysAgo(0),
175
+ yesterday: () => daysAgo(1),
176
+ };
177
+
178
+ /** Expand a date shortcut to an ISO date string. */
179
+ const expandDateShortcut = (shortcut: string): string | undefined => {
180
+ const handler = dateShortcuts[shortcut];
181
+ if (handler) {
182
+ return handler();
183
+ }
184
+ const match = /^(\d+)d$/.exec(shortcut);
185
+ if (match?.[1]) {
186
+ return daysAgo(Number(match[1]));
187
+ }
188
+ return undefined;
189
+ };
190
+
191
+ /** Expand since/until shortcuts in an input record. */
192
+ const expandDateFields = (
193
+ inp: Record<string, unknown>
194
+ ): Record<string, unknown> => {
195
+ const modified = { ...inp };
196
+ for (const field of ['since', 'until'] as const) {
197
+ if (typeof inp[field] === 'string') {
198
+ const expanded = expandDateShortcut(inp[field]);
199
+ if (expanded) {
200
+ modified[field] = expanded;
201
+ }
202
+ }
203
+ }
204
+ return modified;
205
+ };
206
+
207
+ /**
208
+ * Expands date shortcut strings into ISO date ranges.
209
+ *
210
+ * When a trail's input has `since` or `until` fields, this layer
211
+ * checks for shortcuts like "today", "yesterday", "7d", "30d",
212
+ * "this-week", "this-month" and expands them to ISO 8601 dates.
213
+ */
214
+ export const dateShortcutsLayer: Layer = {
215
+ description: 'Expand date shortcuts (today, 7d, etc.) to ISO dates',
216
+ name: 'dateShortcuts',
217
+
218
+ wrap<I, O>(
219
+ trail: Trail<I, O>,
220
+ implementation: Implementation<I, O>
221
+ ): Implementation<I, O> {
222
+ if (!hasDateRangeFields(trail)) {
223
+ return implementation;
224
+ }
225
+
226
+ return (input, ctx) => {
227
+ const modified = expandDateFields(input as Record<string, unknown>);
228
+ return implementation(modified as I, ctx);
229
+ };
230
+ },
231
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Default result handler for CLI commands.
3
+ */
4
+
5
+ import type { ActionResultContext } from './build.js';
6
+ import { output, resolveOutputMode } from './output.js';
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // defaultOnResult
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /**
13
+ * The batteries-included result handler.
14
+ *
15
+ * - On error: throws the error (lets the program's error handler produce exit code)
16
+ * - On success: resolves output mode from flags, pipes value through `output()`
17
+ */
18
+ export const defaultOnResult = async (
19
+ ctx: ActionResultContext
20
+ ): Promise<void> => {
21
+ if (ctx.result.isErr()) {
22
+ throw ctx.result.error;
23
+ }
24
+
25
+ const { mode } = resolveOutputMode(ctx.flags);
26
+ await output(ctx.result.value, mode);
27
+ };
package/src/output.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Output formatting and mode resolution for CLI output.
3
+ */
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Types
7
+ // ---------------------------------------------------------------------------
8
+
9
+ export type OutputMode = 'text' | 'json' | 'jsonl';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // output()
13
+ // ---------------------------------------------------------------------------
14
+
15
+ /**
16
+ * Write a value to stdout in the specified format.
17
+ *
18
+ * - **text**: strings written directly; objects JSON-stringified with 2-space indent
19
+ * - **json**: always JSON.stringify with 2-space indent
20
+ * - **jsonl**: arrays emit one JSON line per element; scalars emit one line
21
+ */
22
+ const outputWriters: Record<OutputMode, (value: unknown) => void> = {
23
+ json: (value) => process.stdout.write(`${JSON.stringify(value, null, 2)}\n`),
24
+ jsonl: (value) => {
25
+ if (Array.isArray(value)) {
26
+ for (const item of value) {
27
+ process.stdout.write(`${JSON.stringify(item)}\n`);
28
+ }
29
+ } else {
30
+ process.stdout.write(`${JSON.stringify(value)}\n`);
31
+ }
32
+ },
33
+ text: (value) => {
34
+ if (typeof value === 'string') {
35
+ process.stdout.write(`${value}\n`);
36
+ } else {
37
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
38
+ }
39
+ },
40
+ };
41
+
42
+ export const output = (value: unknown, mode: OutputMode): void => {
43
+ const writer = outputWriters[mode];
44
+ writer(value);
45
+ };
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // resolveOutputMode()
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const VALID_MODES = new Set<OutputMode>(['text', 'json', 'jsonl']);
52
+
53
+ /** Resolve mode from flags alone (--json, --jsonl, --output). */
54
+ const resolveFlagMode = (
55
+ flags: Record<string, unknown>
56
+ ): OutputMode | undefined => {
57
+ if (flags['json'] === true) {
58
+ return 'json';
59
+ }
60
+ if (flags['jsonl'] === true) {
61
+ return 'jsonl';
62
+ }
63
+ if (
64
+ typeof flags['output'] === 'string' &&
65
+ VALID_MODES.has(flags['output'] as OutputMode)
66
+ ) {
67
+ return flags['output'] as OutputMode;
68
+ }
69
+ return undefined;
70
+ };
71
+
72
+ /** Resolve mode from environment variables. */
73
+ const resolveEnvMode = (): OutputMode | undefined => {
74
+ if (process.env['TRAILS_JSON'] === '1') {
75
+ return 'json';
76
+ }
77
+ if (process.env['TRAILS_JSONL'] === '1') {
78
+ return 'jsonl';
79
+ }
80
+ return undefined;
81
+ };
82
+
83
+ /**
84
+ * Determine the output mode from parsed CLI flags and environment.
85
+ *
86
+ * Resolution order (highest priority wins):
87
+ * 1. `flags.json === true` -> "json"
88
+ * 2. `flags.jsonl === true` -> "jsonl"
89
+ * 3. `flags.output` as string -> validate against OutputMode
90
+ * 4. `TRAILS_JSON=1` env var -> "json"
91
+ * 5. `TRAILS_JSONL=1` env var -> "jsonl"
92
+ * 6. Default: "text"
93
+ */
94
+ export const resolveOutputMode = (
95
+ flags: Record<string, unknown>
96
+ ): {
97
+ mode: OutputMode;
98
+ } => {
99
+ const mode = resolveFlagMode(flags) ?? resolveEnvMode() ?? 'text';
100
+ return { mode };
101
+ };
package/src/prompt.ts ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Surface-agnostic input resolution contracts for CLI commands.
3
+ *
4
+ * The core CLI package does not depend on any concrete prompt library.
5
+ * Callers can provide a resolver that gathers missing input however they want
6
+ * (Clack, forms, conversational UI, or no prompting at all).
7
+ */
8
+
9
+ import type { Field } from '@ontrails/core';
10
+
11
+ /**
12
+ * Options passed to an input resolver.
13
+ *
14
+ * `isTTY` is provided so callers can override interactivity in tests.
15
+ */
16
+ export interface ResolveInputOptions {
17
+ readonly isTTY?: boolean | undefined;
18
+ }
19
+
20
+ /**
21
+ * A resolver that fills in missing values for derived schema fields.
22
+ *
23
+ * The resolver receives the current input as field-name keyed values and
24
+ * returns a merged record with any newly gathered answers.
25
+ */
26
+ export type InputResolver = (
27
+ fields: readonly Field[],
28
+ provided: Record<string, unknown>,
29
+ options?: ResolveInputOptions
30
+ ) => Promise<Record<string, unknown>>;
31
+
32
+ /** Default passthrough resolver for non-interactive execution. */
33
+ export const passthroughResolver: InputResolver = async (_fields, provided) =>
34
+ await provided;
35
+
36
+ /** Shared TTY check for resolver implementations. */
37
+ export const isInteractive = (options?: ResolveInputOptions): boolean =>
38
+ options?.isTTY ?? process.stdin.isTTY ?? false;
39
+
40
+ export type { Field };
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["**/__tests__/**", "**/*.test.ts", "dist"]
9
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/build.ts","./src/command.ts","./src/flags.ts","./src/index.ts","./src/layers.ts","./src/on-result.ts","./src/output.ts","./src/prompt.ts","./src/commander/blaze.ts","./src/commander/index.ts","./src/commander/to-commander.ts"],"version":"5.9.3"}