@jaypie/fabric 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 (166) hide show
  1. package/README.md +677 -0
  2. package/dist/cjs/commander/FabricCommander.d.ts +94 -0
  3. package/dist/cjs/commander/createCommanderOptions.d.ts +25 -0
  4. package/dist/cjs/commander/fabricCommand.d.ts +43 -0
  5. package/dist/cjs/commander/index.cjs +1487 -0
  6. package/dist/cjs/commander/index.cjs.map +1 -0
  7. package/dist/cjs/commander/index.d.ts +6 -0
  8. package/dist/cjs/commander/parseCommanderOptions.d.ts +32 -0
  9. package/dist/cjs/commander/registerServiceCommand.d.ts +43 -0
  10. package/dist/cjs/commander/types.d.ts +107 -0
  11. package/dist/cjs/constants.d.ts +12 -0
  12. package/dist/cjs/convert-date.d.ts +47 -0
  13. package/dist/cjs/convert.d.ts +69 -0
  14. package/dist/cjs/data/FabricData.d.ts +42 -0
  15. package/dist/cjs/data/index.cjs +1575 -0
  16. package/dist/cjs/data/index.cjs.map +1 -0
  17. package/dist/cjs/data/index.d.ts +5 -0
  18. package/dist/cjs/data/services/archive.d.ts +8 -0
  19. package/dist/cjs/data/services/create.d.ts +8 -0
  20. package/dist/cjs/data/services/delete.d.ts +8 -0
  21. package/dist/cjs/data/services/execute.d.ts +8 -0
  22. package/dist/cjs/data/services/index.d.ts +7 -0
  23. package/dist/cjs/data/services/list.d.ts +8 -0
  24. package/dist/cjs/data/services/read.d.ts +8 -0
  25. package/dist/cjs/data/services/update.d.ts +8 -0
  26. package/dist/cjs/data/transforms.d.ts +80 -0
  27. package/dist/cjs/data/types.d.ts +190 -0
  28. package/dist/cjs/express/FabricRouter.d.ts +29 -0
  29. package/dist/cjs/express/fabricExpress.d.ts +16 -0
  30. package/dist/cjs/express/index.cjs +505 -0
  31. package/dist/cjs/express/index.cjs.map +1 -0
  32. package/dist/cjs/express/index.d.ts +3 -0
  33. package/dist/cjs/express/types.d.ts +51 -0
  34. package/dist/cjs/helpers/fallback.d.ts +21 -0
  35. package/dist/cjs/helpers/index.d.ts +3 -0
  36. package/dist/cjs/helpers/resolvedName.d.ts +24 -0
  37. package/dist/cjs/http/FabricHttpServer.d.ts +31 -0
  38. package/dist/cjs/http/authorization.d.ts +30 -0
  39. package/dist/cjs/http/cors.d.ts +40 -0
  40. package/dist/cjs/http/fabricHttp.d.ts +28 -0
  41. package/dist/cjs/http/httpTransform.d.ts +36 -0
  42. package/dist/cjs/http/index.cjs +1820 -0
  43. package/dist/cjs/http/index.cjs.map +1 -0
  44. package/dist/cjs/http/index.d.ts +10 -0
  45. package/dist/cjs/http/stream.d.ts +185 -0
  46. package/dist/cjs/http/types.d.ts +343 -0
  47. package/dist/cjs/index/index.d.ts +8 -0
  48. package/dist/cjs/index/keyBuilder.d.ts +81 -0
  49. package/dist/cjs/index/registry.d.ts +56 -0
  50. package/dist/cjs/index/types.d.ts +54 -0
  51. package/dist/cjs/index.cjs +1674 -0
  52. package/dist/cjs/index.cjs.map +1 -0
  53. package/dist/cjs/index.d.ts +18 -0
  54. package/dist/cjs/lambda/createLambdaService.d.ts +33 -0
  55. package/dist/cjs/lambda/fabricLambda.d.ts +36 -0
  56. package/dist/cjs/lambda/index.cjs +967 -0
  57. package/dist/cjs/lambda/index.cjs.map +1 -0
  58. package/dist/cjs/lambda/index.d.ts +2 -0
  59. package/dist/cjs/lambda/types.d.ts +68 -0
  60. package/dist/cjs/llm/createLlmTool.d.ts +40 -0
  61. package/dist/cjs/llm/fabricTool.d.ts +40 -0
  62. package/dist/cjs/llm/index.cjs +1107 -0
  63. package/dist/cjs/llm/index.cjs.map +1 -0
  64. package/dist/cjs/llm/index.d.ts +3 -0
  65. package/dist/cjs/llm/inputToJsonSchema.d.ts +32 -0
  66. package/dist/cjs/llm/types.d.ts +61 -0
  67. package/dist/cjs/mcp/fabricMcp.d.ts +38 -0
  68. package/dist/cjs/mcp/index.cjs +938 -0
  69. package/dist/cjs/mcp/index.cjs.map +1 -0
  70. package/dist/cjs/mcp/index.d.ts +2 -0
  71. package/dist/cjs/mcp/registerMcpTool.d.ts +38 -0
  72. package/dist/cjs/mcp/types.d.ts +60 -0
  73. package/dist/cjs/models/base.d.ts +209 -0
  74. package/dist/cjs/resolve-date.d.ts +47 -0
  75. package/dist/cjs/resolve.d.ts +69 -0
  76. package/dist/cjs/resolveService.d.ts +49 -0
  77. package/dist/cjs/service.d.ts +13 -0
  78. package/dist/cjs/status.d.ts +30 -0
  79. package/dist/cjs/types/elementaryTypes.d.ts +84 -0
  80. package/dist/cjs/types/fieldCategory.d.ts +20 -0
  81. package/dist/cjs/types/fieldDefinition.d.ts +46 -0
  82. package/dist/cjs/types/index.d.ts +4 -0
  83. package/dist/cjs/types.d.ts +56 -0
  84. package/dist/esm/commander/FabricCommander.d.ts +94 -0
  85. package/dist/esm/commander/createCommanderOptions.d.ts +25 -0
  86. package/dist/esm/commander/fabricCommand.d.ts +43 -0
  87. package/dist/esm/commander/index.d.ts +6 -0
  88. package/dist/esm/commander/index.js +1482 -0
  89. package/dist/esm/commander/index.js.map +1 -0
  90. package/dist/esm/commander/parseCommanderOptions.d.ts +32 -0
  91. package/dist/esm/commander/registerServiceCommand.d.ts +43 -0
  92. package/dist/esm/commander/types.d.ts +107 -0
  93. package/dist/esm/constants.d.ts +12 -0
  94. package/dist/esm/convert-date.d.ts +47 -0
  95. package/dist/esm/convert.d.ts +69 -0
  96. package/dist/esm/data/FabricData.d.ts +42 -0
  97. package/dist/esm/data/index.d.ts +5 -0
  98. package/dist/esm/data/index.js +1548 -0
  99. package/dist/esm/data/index.js.map +1 -0
  100. package/dist/esm/data/services/archive.d.ts +8 -0
  101. package/dist/esm/data/services/create.d.ts +8 -0
  102. package/dist/esm/data/services/delete.d.ts +8 -0
  103. package/dist/esm/data/services/execute.d.ts +8 -0
  104. package/dist/esm/data/services/index.d.ts +7 -0
  105. package/dist/esm/data/services/list.d.ts +8 -0
  106. package/dist/esm/data/services/read.d.ts +8 -0
  107. package/dist/esm/data/services/update.d.ts +8 -0
  108. package/dist/esm/data/transforms.d.ts +80 -0
  109. package/dist/esm/data/types.d.ts +190 -0
  110. package/dist/esm/express/FabricRouter.d.ts +29 -0
  111. package/dist/esm/express/fabricExpress.d.ts +16 -0
  112. package/dist/esm/express/index.d.ts +3 -0
  113. package/dist/esm/express/index.js +500 -0
  114. package/dist/esm/express/index.js.map +1 -0
  115. package/dist/esm/express/types.d.ts +51 -0
  116. package/dist/esm/helpers/fallback.d.ts +21 -0
  117. package/dist/esm/helpers/index.d.ts +3 -0
  118. package/dist/esm/helpers/resolvedName.d.ts +24 -0
  119. package/dist/esm/http/FabricHttpServer.d.ts +31 -0
  120. package/dist/esm/http/authorization.d.ts +30 -0
  121. package/dist/esm/http/cors.d.ts +40 -0
  122. package/dist/esm/http/fabricHttp.d.ts +28 -0
  123. package/dist/esm/http/httpTransform.d.ts +36 -0
  124. package/dist/esm/http/index.d.ts +10 -0
  125. package/dist/esm/http/index.js +1775 -0
  126. package/dist/esm/http/index.js.map +1 -0
  127. package/dist/esm/http/stream.d.ts +185 -0
  128. package/dist/esm/http/types.d.ts +343 -0
  129. package/dist/esm/index/index.d.ts +8 -0
  130. package/dist/esm/index/keyBuilder.d.ts +81 -0
  131. package/dist/esm/index/registry.d.ts +56 -0
  132. package/dist/esm/index/types.d.ts +54 -0
  133. package/dist/esm/index.d.ts +18 -0
  134. package/dist/esm/index.js +1606 -0
  135. package/dist/esm/index.js.map +1 -0
  136. package/dist/esm/lambda/createLambdaService.d.ts +33 -0
  137. package/dist/esm/lambda/fabricLambda.d.ts +36 -0
  138. package/dist/esm/lambda/index.d.ts +2 -0
  139. package/dist/esm/lambda/index.js +965 -0
  140. package/dist/esm/lambda/index.js.map +1 -0
  141. package/dist/esm/lambda/types.d.ts +68 -0
  142. package/dist/esm/llm/createLlmTool.d.ts +40 -0
  143. package/dist/esm/llm/fabricTool.d.ts +40 -0
  144. package/dist/esm/llm/index.d.ts +3 -0
  145. package/dist/esm/llm/index.js +1104 -0
  146. package/dist/esm/llm/index.js.map +1 -0
  147. package/dist/esm/llm/inputToJsonSchema.d.ts +32 -0
  148. package/dist/esm/llm/types.d.ts +61 -0
  149. package/dist/esm/mcp/fabricMcp.d.ts +38 -0
  150. package/dist/esm/mcp/index.d.ts +2 -0
  151. package/dist/esm/mcp/index.js +936 -0
  152. package/dist/esm/mcp/index.js.map +1 -0
  153. package/dist/esm/mcp/registerMcpTool.d.ts +38 -0
  154. package/dist/esm/mcp/types.d.ts +60 -0
  155. package/dist/esm/models/base.d.ts +209 -0
  156. package/dist/esm/resolve-date.d.ts +47 -0
  157. package/dist/esm/resolve.d.ts +69 -0
  158. package/dist/esm/resolveService.d.ts +49 -0
  159. package/dist/esm/service.d.ts +13 -0
  160. package/dist/esm/status.d.ts +30 -0
  161. package/dist/esm/types/elementaryTypes.d.ts +84 -0
  162. package/dist/esm/types/fieldCategory.d.ts +20 -0
  163. package/dist/esm/types/fieldDefinition.d.ts +46 -0
  164. package/dist/esm/types/index.d.ts +4 -0
  165. package/dist/esm/types.d.ts +56 -0
  166. package/package.json +122 -0
@@ -0,0 +1,1487 @@
1
+ 'use strict';
2
+
3
+ var commander = require('commander');
4
+ var errors = require('@jaypie/errors');
5
+
6
+ // Create Commander.js options from service config
7
+ /**
8
+ * Convert camelCase to kebab-case
9
+ */
10
+ function toKebabCase$1(str) {
11
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
12
+ }
13
+ /**
14
+ * Check if a type is a typed array (e.g., [String], [Number])
15
+ */
16
+ function isTypedArrayType$2(type) {
17
+ if (!Array.isArray(type)) {
18
+ return false;
19
+ }
20
+ if (type.length === 0) {
21
+ return true; // [] is untyped array
22
+ }
23
+ if (type.length !== 1) {
24
+ return false;
25
+ }
26
+ const element = type[0];
27
+ return (element === Boolean ||
28
+ element === Number ||
29
+ element === String ||
30
+ element === Object ||
31
+ element === "boolean" ||
32
+ element === "number" ||
33
+ element === "string" ||
34
+ element === "object" ||
35
+ element === "" ||
36
+ (typeof element === "object" &&
37
+ element !== null &&
38
+ !(element instanceof RegExp) &&
39
+ Object.keys(element).length === 0));
40
+ }
41
+ /**
42
+ * Check if a type represents a boolean
43
+ */
44
+ function isBooleanType$1(type) {
45
+ return type === Boolean || type === "boolean";
46
+ }
47
+ /**
48
+ * Check if a type is variadic (array-like)
49
+ */
50
+ function isVariadicType(type) {
51
+ if (type === Array || type === "array") {
52
+ return true;
53
+ }
54
+ return isTypedArrayType$2(type);
55
+ }
56
+ /**
57
+ * Get default description for a field
58
+ */
59
+ function getDescription(fieldName, definition, override) {
60
+ if (override?.description) {
61
+ return override.description;
62
+ }
63
+ if (definition.description) {
64
+ return definition.description;
65
+ }
66
+ return fieldName;
67
+ }
68
+ /**
69
+ * Build the flags string for an option
70
+ */
71
+ function buildFlags(fieldName, definition, override) {
72
+ if (override?.flags) {
73
+ return override.flags;
74
+ }
75
+ // Priority: override.long > definition.flag > toKebabCase(fieldName)
76
+ const long = override?.long ?? definition.flag ?? toKebabCase$1(fieldName);
77
+ // Priority: override.short > definition.letter
78
+ const short = override?.short ?? definition.letter;
79
+ const isBoolean = isBooleanType$1(definition.type);
80
+ const isVariadic = isVariadicType(definition.type);
81
+ const isRequired = definition.required !== false && definition.default === undefined;
82
+ let flags = "";
83
+ if (short) {
84
+ flags += `-${short}, `;
85
+ }
86
+ flags += `--${long}`;
87
+ if (!isBoolean) {
88
+ if (isVariadic) {
89
+ // Variadic: can take multiple values
90
+ if (isRequired) {
91
+ flags += ` <${fieldName}...>`;
92
+ }
93
+ else {
94
+ flags += ` [${fieldName}...]`;
95
+ }
96
+ }
97
+ else {
98
+ // Scalar value
99
+ if (isRequired) {
100
+ flags += ` <${fieldName}>`;
101
+ }
102
+ else {
103
+ flags += ` [${fieldName}]`;
104
+ }
105
+ }
106
+ }
107
+ return flags;
108
+ }
109
+ /**
110
+ * Create a Commander Option from a field definition
111
+ */
112
+ function createOption(fieldName, definition, override) {
113
+ const flags = buildFlags(fieldName, definition, override);
114
+ const description = getDescription(fieldName, definition, override);
115
+ const option = new commander.Option(flags, description);
116
+ // Set default value if provided
117
+ if (definition.default !== undefined) {
118
+ option.default(definition.default);
119
+ }
120
+ // Hide from help if specified
121
+ if (override?.hidden) {
122
+ option.hideHelp();
123
+ }
124
+ return option;
125
+ }
126
+ /**
127
+ * Create Commander.js Option objects from a service config
128
+ *
129
+ * @param input - The input field definitions from a service config
130
+ * @param config - Optional configuration for excluding fields or overriding options
131
+ * @returns An object containing an array of Commander Option objects
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const handler = createService({
136
+ * input: {
137
+ * name: { type: String, description: "User name" },
138
+ * age: { type: Number, default: 25 },
139
+ * verbose: { type: Boolean },
140
+ * },
141
+ * service: (input) => input,
142
+ * });
143
+ *
144
+ * const { options } = createCommanderOptions(handler.input);
145
+ * options.forEach(opt => command.addOption(opt));
146
+ * ```
147
+ */
148
+ function createCommanderOptions(input, config = {}) {
149
+ if (!input) {
150
+ return { options: [] };
151
+ }
152
+ const { exclude = [], overrides = {} } = config;
153
+ const options = [];
154
+ for (const [fieldName, definition] of Object.entries(input)) {
155
+ // Skip excluded fields
156
+ if (exclude.includes(fieldName)) {
157
+ continue;
158
+ }
159
+ const override = overrides[fieldName];
160
+ const option = createOption(fieldName, definition, override);
161
+ options.push(option);
162
+ // For boolean fields, also create a --no-<flag> option for negation
163
+ // Commander.js requires separate options for --flag and --no-flag
164
+ if (isBooleanType$1(definition.type) && !override?.flags) {
165
+ const long = override?.long ?? definition.flag ?? toKebabCase$1(fieldName);
166
+ const negateOption = new commander.Option(`--no-${long}`, `Disable ${fieldName}`);
167
+ // Hide the negatable option from help to avoid clutter
168
+ negateOption.hideHelp();
169
+ options.push(negateOption);
170
+ }
171
+ }
172
+ return { options };
173
+ }
174
+
175
+ /**
176
+ * Meta-modeling Constants
177
+ */
178
+ // =============================================================================
179
+ // Constants
180
+ // =============================================================================
181
+ /** Root organizational unit */
182
+ /** Fabric version - used to identify pre-instantiated Services */
183
+ const FABRIC_VERSION = "0.1.0";
184
+
185
+ /**
186
+ * Date Type Conversion for @jaypie/fabric
187
+ *
188
+ * Adds Date as a supported type in the fabric type system.
189
+ * Follows the same conversion patterns as String, Number, Boolean.
190
+ */
191
+ /**
192
+ * Convert a value to a Date
193
+ *
194
+ * Supported inputs:
195
+ * - Date: returned as-is (validated)
196
+ * - Number: treated as Unix timestamp (milliseconds)
197
+ * - String: parsed via Date constructor (ISO 8601, etc.)
198
+ * - Object with value property: unwrapped and converted
199
+ *
200
+ * @throws BadRequestError if value cannot be converted to valid Date
201
+ */
202
+ function fabricDate(value) {
203
+ // Already a Date
204
+ if (value instanceof Date) {
205
+ if (Number.isNaN(value.getTime())) {
206
+ throw new errors.BadRequestError("Invalid Date value");
207
+ }
208
+ return value;
209
+ }
210
+ // Null/undefined
211
+ if (value === null || value === undefined) {
212
+ throw new errors.BadRequestError("Cannot convert null or undefined to Date");
213
+ }
214
+ // Object with value property (fabric pattern)
215
+ if (typeof value === "object" && value !== null && "value" in value) {
216
+ return fabricDate(value.value);
217
+ }
218
+ // Number (timestamp in milliseconds)
219
+ if (typeof value === "number") {
220
+ if (Number.isNaN(value)) {
221
+ throw new errors.BadRequestError("Cannot convert NaN to Date");
222
+ }
223
+ const date = new Date(value);
224
+ if (Number.isNaN(date.getTime())) {
225
+ throw new errors.BadRequestError(`Cannot convert ${value} to Date`);
226
+ }
227
+ return date;
228
+ }
229
+ // String (ISO 8601 or parseable format)
230
+ if (typeof value === "string") {
231
+ // Empty string is invalid
232
+ if (value.trim() === "") {
233
+ throw new errors.BadRequestError("Cannot convert empty string to Date");
234
+ }
235
+ const date = new Date(value);
236
+ if (Number.isNaN(date.getTime())) {
237
+ throw new errors.BadRequestError(`Cannot convert "${value}" to Date`);
238
+ }
239
+ return date;
240
+ }
241
+ // Boolean cannot be converted to Date
242
+ if (typeof value === "boolean") {
243
+ throw new errors.BadRequestError("Cannot convert boolean to Date");
244
+ }
245
+ // Arrays - attempt single element extraction
246
+ if (Array.isArray(value)) {
247
+ if (value.length === 1) {
248
+ return fabricDate(value[0]);
249
+ }
250
+ throw new errors.BadRequestError(`Cannot convert array with ${value.length} elements to Date`);
251
+ }
252
+ throw new errors.BadRequestError(`Cannot convert ${typeof value} to Date`);
253
+ }
254
+ /**
255
+ * Type guard for Date type in schema definitions
256
+ */
257
+ function isDateType(type) {
258
+ return type === Date;
259
+ }
260
+
261
+ // Fabric functions for @jaypie/fabric
262
+ /**
263
+ * Try to parse a string as JSON if it looks like JSON
264
+ * Returns the parsed value or the original string if not JSON
265
+ */
266
+ function tryParseJson(value) {
267
+ const trimmed = value.trim();
268
+ if ((trimmed.startsWith("{") && trimmed.endsWith("}")) ||
269
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
270
+ try {
271
+ return JSON.parse(trimmed);
272
+ }
273
+ catch {
274
+ // Not valid JSON, return original
275
+ return value;
276
+ }
277
+ }
278
+ return value;
279
+ }
280
+ /**
281
+ * Unwrap arrays and objects to get to the scalar value
282
+ * - Single-element arrays unwrap to their element
283
+ * - Objects with value property unwrap to that value
284
+ * - Recursively unwraps nested structures
285
+ */
286
+ function unwrapToScalar(value) {
287
+ if (value === undefined || value === null) {
288
+ return value;
289
+ }
290
+ // Unwrap single-element arrays
291
+ if (Array.isArray(value)) {
292
+ if (value.length === 0) {
293
+ return undefined;
294
+ }
295
+ if (value.length === 1) {
296
+ return unwrapToScalar(value[0]);
297
+ }
298
+ throw new errors.BadRequestError("Cannot convert multi-value array to scalar");
299
+ }
300
+ // Unwrap objects with value property
301
+ if (typeof value === "object") {
302
+ const obj = value;
303
+ if ("value" in obj) {
304
+ return unwrapToScalar(obj.value);
305
+ }
306
+ throw new errors.BadRequestError("Object must have a value attribute");
307
+ }
308
+ return value;
309
+ }
310
+ /**
311
+ * Prepare a value for scalar conversion by parsing JSON strings and unwrapping
312
+ */
313
+ function prepareForScalarConversion(value) {
314
+ if (value === undefined || value === null) {
315
+ return value;
316
+ }
317
+ // Try to parse JSON strings
318
+ if (typeof value === "string") {
319
+ const parsed = tryParseJson(value);
320
+ if (parsed !== value) {
321
+ // Successfully parsed, unwrap the result
322
+ return unwrapToScalar(parsed);
323
+ }
324
+ return value;
325
+ }
326
+ // Unwrap arrays and objects
327
+ if (Array.isArray(value) || typeof value === "object") {
328
+ return unwrapToScalar(value);
329
+ }
330
+ return value;
331
+ }
332
+ /**
333
+ * Convert a value to a boolean
334
+ * - Arrays, objects, and JSON strings are unwrapped first
335
+ * - String "true" becomes true
336
+ * - String "false" becomes false
337
+ * - Strings that parse to numbers: positive = true, zero or negative = false
338
+ * - Numbers: positive = true, zero or negative = false
339
+ * - Boolean passes through
340
+ */
341
+ function fabricBoolean(value) {
342
+ // Prepare value by parsing JSON and unwrapping arrays/objects
343
+ const prepared = prepareForScalarConversion(value);
344
+ if (prepared === undefined || prepared === null) {
345
+ return undefined;
346
+ }
347
+ if (typeof prepared === "boolean") {
348
+ return prepared;
349
+ }
350
+ if (typeof prepared === "string") {
351
+ if (prepared === "") {
352
+ return undefined;
353
+ }
354
+ const lower = prepared.toLowerCase();
355
+ if (lower === "true") {
356
+ return true;
357
+ }
358
+ if (lower === "false") {
359
+ return false;
360
+ }
361
+ // Try to parse as number
362
+ const num = parseFloat(prepared);
363
+ if (isNaN(num)) {
364
+ throw new errors.BadRequestError(`Cannot convert "${prepared}" to Boolean`);
365
+ }
366
+ return num > 0;
367
+ }
368
+ if (typeof prepared === "number") {
369
+ if (isNaN(prepared)) {
370
+ throw new errors.BadRequestError("Cannot convert NaN to Boolean");
371
+ }
372
+ return prepared > 0;
373
+ }
374
+ throw new errors.BadRequestError(`Cannot convert ${typeof prepared} to Boolean`);
375
+ }
376
+ /**
377
+ * Convert a value to a number
378
+ * - Arrays, objects, and JSON strings are unwrapped first
379
+ * - String "" becomes undefined
380
+ * - String "true" becomes 1
381
+ * - String "false" becomes 0
382
+ * - Strings that parse to numbers use those values
383
+ * - Strings that parse to NaN throw BadRequestError
384
+ * - Boolean true becomes 1, false becomes 0
385
+ * - Number passes through
386
+ */
387
+ function fabricNumber(value) {
388
+ // Prepare value by parsing JSON and unwrapping arrays/objects
389
+ const prepared = prepareForScalarConversion(value);
390
+ if (prepared === undefined || prepared === null) {
391
+ return undefined;
392
+ }
393
+ if (typeof prepared === "number") {
394
+ if (isNaN(prepared)) {
395
+ throw new errors.BadRequestError("Cannot convert NaN to Number");
396
+ }
397
+ return prepared;
398
+ }
399
+ if (typeof prepared === "boolean") {
400
+ return prepared ? 1 : 0;
401
+ }
402
+ if (typeof prepared === "string") {
403
+ if (prepared === "") {
404
+ return undefined;
405
+ }
406
+ const lower = prepared.toLowerCase();
407
+ if (lower === "true") {
408
+ return 1;
409
+ }
410
+ if (lower === "false") {
411
+ return 0;
412
+ }
413
+ const num = parseFloat(prepared);
414
+ if (isNaN(num)) {
415
+ throw new errors.BadRequestError(`Cannot convert "${prepared}" to Number`);
416
+ }
417
+ return num;
418
+ }
419
+ throw new errors.BadRequestError(`Cannot convert ${typeof prepared} to Number`);
420
+ }
421
+ /**
422
+ * Convert a value to a string
423
+ * - Arrays, objects, and JSON strings are unwrapped first
424
+ * - String "" becomes undefined
425
+ * - Boolean true becomes "true", false becomes "false"
426
+ * - Number converts to string representation
427
+ * - String passes through
428
+ */
429
+ function fabricString(value) {
430
+ // Prepare value by parsing JSON and unwrapping arrays/objects
431
+ const prepared = prepareForScalarConversion(value);
432
+ if (prepared === undefined || prepared === null) {
433
+ return undefined;
434
+ }
435
+ if (typeof prepared === "string") {
436
+ if (prepared === "") {
437
+ return undefined;
438
+ }
439
+ return prepared;
440
+ }
441
+ if (typeof prepared === "boolean") {
442
+ return prepared ? "true" : "false";
443
+ }
444
+ if (typeof prepared === "number") {
445
+ if (isNaN(prepared)) {
446
+ throw new errors.BadRequestError("Cannot convert NaN to String");
447
+ }
448
+ return String(prepared);
449
+ }
450
+ throw new errors.BadRequestError(`Cannot convert ${typeof prepared} to String`);
451
+ }
452
+ /**
453
+ * Convert a value to an array
454
+ * - Non-arrays become arrays containing that value
455
+ * - Arrays of a single value become that value (unwrapped)
456
+ * - Multi-value arrays throw BadRequestError
457
+ * - undefined/null become undefined
458
+ */
459
+ function fabricArray(value) {
460
+ if (value === undefined || value === null) {
461
+ return undefined;
462
+ }
463
+ if (Array.isArray(value)) {
464
+ // Arrays pass through (single-element unwrapping happens when converting FROM array)
465
+ return value;
466
+ }
467
+ // Non-arrays become single-element arrays
468
+ return [value];
469
+ }
470
+ /**
471
+ * Convert a value to an object with a value property
472
+ * - Scalars become { value: scalar }
473
+ * - Arrays become { value: array }
474
+ * - Objects with a value attribute pass through
475
+ * - Objects without a value attribute throw BadRequestError
476
+ * - undefined/null become undefined
477
+ */
478
+ function fabricObject(value) {
479
+ if (value === undefined || value === null) {
480
+ return undefined;
481
+ }
482
+ // Check if already an object (but not an array)
483
+ if (typeof value === "object" && !Array.isArray(value)) {
484
+ const obj = value;
485
+ if ("value" in obj) {
486
+ return obj;
487
+ }
488
+ throw new errors.BadRequestError("Object must have a value attribute");
489
+ }
490
+ // Scalars and arrays become { value: ... }
491
+ return { value };
492
+ }
493
+ /**
494
+ * Check if a type is a typed array (e.g., [String], [Number], [], etc.)
495
+ */
496
+ function isTypedArrayType$1(type) {
497
+ return Array.isArray(type);
498
+ }
499
+ /**
500
+ * Split a string on comma or tab delimiters for typed array conversion.
501
+ * Only splits if the string contains commas or tabs.
502
+ * Returns the original value if not a string or no delimiters found.
503
+ */
504
+ function splitStringForArray(value) {
505
+ if (typeof value !== "string") {
506
+ return value;
507
+ }
508
+ // Check for comma or tab delimiters
509
+ if (value.includes(",")) {
510
+ return value.split(",").map((s) => s.trim());
511
+ }
512
+ if (value.includes("\t")) {
513
+ return value.split("\t").map((s) => s.trim());
514
+ }
515
+ return value;
516
+ }
517
+ /**
518
+ * Try to parse a string as JSON for array context.
519
+ * Returns parsed value if it's an array, otherwise returns original.
520
+ */
521
+ function tryParseJsonArray(value) {
522
+ if (typeof value !== "string") {
523
+ return value;
524
+ }
525
+ const trimmed = value.trim();
526
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
527
+ try {
528
+ const parsed = JSON.parse(trimmed);
529
+ if (Array.isArray(parsed)) {
530
+ return parsed;
531
+ }
532
+ }
533
+ catch {
534
+ // Not valid JSON, fall through
535
+ }
536
+ }
537
+ return value;
538
+ }
539
+ /**
540
+ * Get the element type from a typed array type
541
+ * Returns undefined for untyped arrays ([])
542
+ */
543
+ function getArrayElementType(type) {
544
+ if (type.length === 0) {
545
+ return undefined; // Untyped array
546
+ }
547
+ const elementType = type[0];
548
+ // Handle constructor types
549
+ if (elementType === Boolean)
550
+ return "boolean";
551
+ if (elementType === Number)
552
+ return "number";
553
+ if (elementType === String)
554
+ return "string";
555
+ if (elementType === Object)
556
+ return "object";
557
+ // Handle string types
558
+ if (elementType === "boolean")
559
+ return "boolean";
560
+ if (elementType === "number")
561
+ return "number";
562
+ if (elementType === "string")
563
+ return "string";
564
+ if (elementType === "object")
565
+ return "object";
566
+ // Handle shorthand types
567
+ if (elementType === "")
568
+ return "string"; // "" shorthand for String
569
+ if (typeof elementType === "object" &&
570
+ elementType !== null &&
571
+ Object.keys(elementType).length === 0) {
572
+ return "object"; // {} shorthand for Object
573
+ }
574
+ throw new errors.BadRequestError(`Unknown array element type: ${String(elementType)}`);
575
+ }
576
+ /**
577
+ * Convert a value to a typed array
578
+ * - Tries to parse JSON arrays first
579
+ * - Splits strings on comma/tab if present
580
+ * - Wraps non-arrays in an array
581
+ * - Converts each element to the specified element type
582
+ */
583
+ function fabricTypedArray(value, elementType) {
584
+ // Try to parse JSON array first
585
+ let processed = tryParseJsonArray(value);
586
+ // If still a string, try to split on comma/tab
587
+ processed = splitStringForArray(processed);
588
+ // Convert to array (wraps non-arrays)
589
+ const array = fabricArray(processed);
590
+ if (array === undefined) {
591
+ return undefined;
592
+ }
593
+ // If no element type specified, return as-is
594
+ if (elementType === undefined) {
595
+ return array;
596
+ }
597
+ // Convert each element to the element type
598
+ return array.map((element, index) => {
599
+ try {
600
+ switch (elementType) {
601
+ case "boolean":
602
+ return fabricBoolean(element);
603
+ case "number":
604
+ return fabricNumber(element);
605
+ case "object":
606
+ return fabricObject(element);
607
+ case "string":
608
+ return fabricString(element);
609
+ default:
610
+ throw new errors.BadRequestError(`Unknown element type: ${elementType}`);
611
+ }
612
+ }
613
+ catch (error) {
614
+ if (error instanceof errors.BadRequestError) {
615
+ throw new errors.BadRequestError(`Cannot convert array element at index ${index}: ${error.message}`);
616
+ }
617
+ throw error;
618
+ }
619
+ });
620
+ }
621
+ /**
622
+ * Fabric a value to the specified type
623
+ */
624
+ function fabric(value, type) {
625
+ // Check for Date type first
626
+ if (isDateType(type)) {
627
+ return fabricDate(value);
628
+ }
629
+ // Check for typed array types
630
+ if (isTypedArrayType$1(type)) {
631
+ const elementType = getArrayElementType(type);
632
+ return fabricTypedArray(value, elementType);
633
+ }
634
+ const normalizedType = normalizeType(type);
635
+ switch (normalizedType) {
636
+ case "array":
637
+ return fabricArray(value);
638
+ case "boolean":
639
+ return fabricBoolean(value);
640
+ case "number":
641
+ return fabricNumber(value);
642
+ case "object":
643
+ return fabricObject(value);
644
+ case "string":
645
+ return fabricString(value);
646
+ default:
647
+ throw new errors.BadRequestError(`Unknown type: ${String(type)}`);
648
+ }
649
+ }
650
+ /**
651
+ * Normalize type to string representation
652
+ */
653
+ function normalizeType(type) {
654
+ if (type === Array || type === "array") {
655
+ return "array";
656
+ }
657
+ if (type === Boolean || type === "boolean") {
658
+ return "boolean";
659
+ }
660
+ if (type === Number || type === "number") {
661
+ return "number";
662
+ }
663
+ if (type === Object || type === "object") {
664
+ return "object";
665
+ }
666
+ if (type === String || type === "string") {
667
+ return "string";
668
+ }
669
+ throw new errors.BadRequestError(`Unknown type: ${String(type)}`);
670
+ }
671
+
672
+ // Service for @jaypie/fabric
673
+ /**
674
+ * Check if a single-element array is a typed array type constructor.
675
+ */
676
+ function isTypedArrayConstructor(element) {
677
+ return (element === Boolean ||
678
+ element === Number ||
679
+ element === String ||
680
+ element === Object ||
681
+ element === "boolean" ||
682
+ element === "number" ||
683
+ element === "string" ||
684
+ element === "object" ||
685
+ element === "" ||
686
+ (typeof element === "object" &&
687
+ element !== null &&
688
+ !(element instanceof RegExp) &&
689
+ Object.keys(element).length === 0));
690
+ }
691
+ /**
692
+ * Check if a type is a validated string type (array of string literals and/or RegExp).
693
+ * Distinguishes from typed arrays like [String], [Number], etc.
694
+ */
695
+ function isValidatedStringType(type) {
696
+ if (!Array.isArray(type)) {
697
+ return false;
698
+ }
699
+ // Empty array is untyped array, not validated string
700
+ if (type.length === 0) {
701
+ return false;
702
+ }
703
+ // Single-element arrays with type constructors are typed arrays
704
+ if (type.length === 1 && isTypedArrayConstructor(type[0])) {
705
+ return false;
706
+ }
707
+ // Check that all elements are strings or RegExp
708
+ return type.every((item) => typeof item === "string" || item instanceof RegExp);
709
+ }
710
+ /**
711
+ * Check if a type is a validated number type (array of number literals).
712
+ * Distinguishes from typed arrays like [Number], etc.
713
+ */
714
+ function isValidatedNumberType(type) {
715
+ if (!Array.isArray(type)) {
716
+ return false;
717
+ }
718
+ // Empty array is untyped array, not validated number
719
+ if (type.length === 0) {
720
+ return false;
721
+ }
722
+ // Single-element arrays with type constructors are typed arrays
723
+ if (type.length === 1 && isTypedArrayConstructor(type[0])) {
724
+ return false;
725
+ }
726
+ // Check that all elements are numbers
727
+ return type.every((item) => typeof item === "number");
728
+ }
729
+ /**
730
+ * Parse input string as JSON if it's a string
731
+ */
732
+ function parseInput(input) {
733
+ if (input === undefined || input === null) {
734
+ return {};
735
+ }
736
+ if (typeof input === "string") {
737
+ if (input === "") {
738
+ return {};
739
+ }
740
+ try {
741
+ const parsed = JSON.parse(input);
742
+ if (typeof parsed !== "object" ||
743
+ parsed === null ||
744
+ Array.isArray(parsed)) {
745
+ throw new errors.BadRequestError("Input must be an object");
746
+ }
747
+ return parsed;
748
+ }
749
+ catch (error) {
750
+ if (error instanceof errors.BadRequestError) {
751
+ throw error;
752
+ }
753
+ throw new errors.BadRequestError("Invalid JSON input");
754
+ }
755
+ }
756
+ if (typeof input === "object" && !Array.isArray(input)) {
757
+ return input;
758
+ }
759
+ throw new errors.BadRequestError("Input must be an object or JSON string");
760
+ }
761
+ /**
762
+ * Run validation on a value (supports async validators)
763
+ */
764
+ async function runValidation(value, validate, fieldName) {
765
+ if (typeof validate === "function") {
766
+ const result = await validate(value);
767
+ if (result === false) {
768
+ throw new errors.BadRequestError(`Validation failed for field "${fieldName}"`);
769
+ }
770
+ }
771
+ else if (validate instanceof RegExp) {
772
+ if (typeof value !== "string" || !validate.test(value)) {
773
+ throw new errors.BadRequestError(`Validation failed for field "${fieldName}"`);
774
+ }
775
+ }
776
+ else if (Array.isArray(validate)) {
777
+ // Check if value matches any item in the array
778
+ for (const item of validate) {
779
+ if (item instanceof RegExp) {
780
+ if (typeof value === "string" && item.test(value)) {
781
+ return; // Match found
782
+ }
783
+ }
784
+ else if (typeof item === "function") {
785
+ try {
786
+ const result = await item(value);
787
+ if (result !== false) {
788
+ return; // Match found
789
+ }
790
+ }
791
+ catch {
792
+ // Continue to next item
793
+ }
794
+ }
795
+ else if (value === item) {
796
+ return; // Scalar match found
797
+ }
798
+ }
799
+ throw new errors.BadRequestError(`Validation failed for field "${fieldName}"`);
800
+ }
801
+ }
802
+ /**
803
+ * Check if a field is required
804
+ * A field is required unless it has a default OR required is explicitly false
805
+ */
806
+ function isFieldRequired(definition) {
807
+ if (definition.required === false) {
808
+ return false;
809
+ }
810
+ if (definition.default !== undefined) {
811
+ return false;
812
+ }
813
+ return true;
814
+ }
815
+ /**
816
+ * Process a single field through conversion and validation
817
+ */
818
+ async function processField(fieldName, value, definition) {
819
+ // Apply default if value is undefined
820
+ let processedValue = value;
821
+ if (processedValue === undefined && definition.default !== undefined) {
822
+ processedValue = definition.default;
823
+ }
824
+ // Determine actual type and validation
825
+ let actualType = definition.type;
826
+ let validation = definition.validate;
827
+ // Handle bare RegExp shorthand: /regex/
828
+ if (definition.type instanceof RegExp) {
829
+ actualType = String;
830
+ validation = definition.type; // The RegExp becomes the validation
831
+ }
832
+ // Handle validated string shorthand: ["value1", "value2"] or [/regex/]
833
+ else if (isValidatedStringType(definition.type)) {
834
+ actualType = String;
835
+ validation = definition.type; // The array becomes the validation
836
+ }
837
+ // Handle validated number shorthand: [1, 2, 3]
838
+ else if (isValidatedNumberType(definition.type)) {
839
+ actualType = Number;
840
+ validation = definition.type; // The array becomes the validation
841
+ }
842
+ // Fabric to target type
843
+ const convertedValue = fabric(processedValue, actualType);
844
+ // Check if required field is missing
845
+ if (convertedValue === undefined && isFieldRequired(definition)) {
846
+ throw new errors.BadRequestError(`Missing required field "${fieldName}"`);
847
+ }
848
+ // Run validation if provided
849
+ if (validation !== undefined && convertedValue !== undefined) {
850
+ await runValidation(convertedValue, validation, fieldName);
851
+ }
852
+ return convertedValue;
853
+ }
854
+ /**
855
+ * Fabric a service function
856
+ *
857
+ * Service builds a function that initiates a "controller" step that:
858
+ * - Parses the input if it is a string to object
859
+ * - Fabrics each input field to its type
860
+ * - Calls the validation function or regular expression or checks the array
861
+ * - Calls the service function and returns the response
862
+ *
863
+ * The returned function has config properties for introspection.
864
+ */
865
+ function fabricService(config) {
866
+ const { input: inputDefinitions, service } = config;
867
+ const handler = async (rawInput, context) => {
868
+ // Parse input (handles string JSON)
869
+ const parsedInput = parseInput(rawInput);
870
+ // If no input definitions, pass through to service or return parsed input
871
+ if (!inputDefinitions) {
872
+ if (service) {
873
+ return service(parsedInput, context);
874
+ }
875
+ return parsedInput;
876
+ }
877
+ // Process all fields in parallel
878
+ const entries = Object.entries(inputDefinitions);
879
+ const processedValues = await Promise.all(entries.map(([fieldName, definition]) => processField(fieldName, parsedInput[fieldName], definition)));
880
+ // Build processed input object
881
+ const processedInput = {};
882
+ entries.forEach(([fieldName], index) => {
883
+ processedInput[fieldName] = processedValues[index];
884
+ });
885
+ // Return processed input if no service, otherwise call service
886
+ if (service) {
887
+ return service(processedInput, context);
888
+ }
889
+ return processedInput;
890
+ };
891
+ // Attach config properties directly to handler for flat access
892
+ const typedHandler = handler;
893
+ typedHandler.$fabric = FABRIC_VERSION;
894
+ if (config.alias !== undefined)
895
+ typedHandler.alias = config.alias;
896
+ if (config.description !== undefined)
897
+ typedHandler.description = config.description;
898
+ if (config.input !== undefined)
899
+ typedHandler.input = config.input;
900
+ if (config.service !== undefined)
901
+ typedHandler.service = config.service;
902
+ return typedHandler;
903
+ }
904
+
905
+ // Resolve inline service definitions to full Service objects
906
+ /**
907
+ * Type guard to check if a value is a pre-instantiated Service
908
+ * A Service is a function with the `$fabric` property set by fabricService
909
+ */
910
+ function isService(value) {
911
+ return typeof value === "function" && "$fabric" in value;
912
+ }
913
+ /**
914
+ * Resolve a service configuration to a full Service object
915
+ *
916
+ * Supports two patterns:
917
+ * 1. Inline service definition - pass a plain function as `service` along with
918
+ * `alias`, `description`, and `input` in the config
919
+ * 2. Pre-instantiated Service - pass a Service object as `service`
920
+ *
921
+ * When a pre-instantiated Service is passed, config fields act as overrides:
922
+ * - `alias` overrides service.alias
923
+ * - `description` overrides service.description
924
+ * - `input` overrides service.input
925
+ *
926
+ * The original Service is never mutated - a new Service is created when overrides
927
+ * are applied.
928
+ *
929
+ * @example
930
+ * ```typescript
931
+ * // Inline service definition
932
+ * const service = resolveService({
933
+ * alias: "greet",
934
+ * description: "Greet a user",
935
+ * input: { name: { type: String } },
936
+ * service: ({ name }) => `Hello, ${name}!`,
937
+ * });
938
+ *
939
+ * // Pre-instantiated with override
940
+ * const baseService = fabricService({ alias: "foo", service: (x) => x });
941
+ * const overridden = resolveService({
942
+ * alias: "bar", // Override alias
943
+ * service: baseService,
944
+ * });
945
+ * ```
946
+ */
947
+ function resolveService(config) {
948
+ const { alias, description, input, service } = config;
949
+ if (isService(service)) {
950
+ // Service is pre-instantiated - config fields act as overrides
951
+ // Create new Service with merged properties (config overrides service)
952
+ return fabricService({
953
+ alias: alias ?? service.alias,
954
+ description: description ?? service.description,
955
+ input: input ?? service.input,
956
+ service: service.service,
957
+ });
958
+ }
959
+ // Service is an inline function - create Service from config
960
+ return fabricService({
961
+ alias,
962
+ description,
963
+ input,
964
+ service,
965
+ });
966
+ }
967
+
968
+ // Parse Commander.js options back to handler input format
969
+ /**
970
+ * Convert kebab-case to camelCase
971
+ */
972
+ function toCamelCase(str) {
973
+ return str.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
974
+ }
975
+ /**
976
+ * Convert camelCase to kebab-case
977
+ */
978
+ function toKebabCase(str) {
979
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
980
+ }
981
+ /**
982
+ * Check if a type is a typed array (e.g., [String], [Number])
983
+ */
984
+ function isTypedArrayType(type) {
985
+ if (!Array.isArray(type)) {
986
+ return false;
987
+ }
988
+ if (type.length === 0) {
989
+ return true; // [] is untyped array
990
+ }
991
+ if (type.length !== 1) {
992
+ return false;
993
+ }
994
+ const element = type[0];
995
+ return (element === Boolean ||
996
+ element === Number ||
997
+ element === String ||
998
+ element === Object ||
999
+ element === "boolean" ||
1000
+ element === "number" ||
1001
+ element === "string" ||
1002
+ element === "object" ||
1003
+ element === "" ||
1004
+ (typeof element === "object" &&
1005
+ element !== null &&
1006
+ !(element instanceof RegExp) &&
1007
+ Object.keys(element).length === 0));
1008
+ }
1009
+ /**
1010
+ * Check if a type represents a number (scalar or validated)
1011
+ */
1012
+ function isNumberType(type) {
1013
+ if (type === Number || type === "number") {
1014
+ return true;
1015
+ }
1016
+ // Check for validated number type [1, 2, 3]
1017
+ if (Array.isArray(type) && type.length > 0) {
1018
+ // If it's not a typed array and all elements are numbers, it's a validated number
1019
+ if (!isTypedArrayType(type) &&
1020
+ type.every((item) => typeof item === "number")) {
1021
+ return true;
1022
+ }
1023
+ }
1024
+ return false;
1025
+ }
1026
+ /**
1027
+ * Check if a type represents a boolean
1028
+ */
1029
+ function isBooleanType(type) {
1030
+ return type === Boolean || type === "boolean";
1031
+ }
1032
+ /**
1033
+ * Check if a type is an array type (Array, "array", or typed array)
1034
+ */
1035
+ function isArrayType(type) {
1036
+ if (type === Array || type === "array") {
1037
+ return true;
1038
+ }
1039
+ return isTypedArrayType(type);
1040
+ }
1041
+ /**
1042
+ * Check if a type is an object type
1043
+ */
1044
+ function isObjectType(type) {
1045
+ return type === Object || type === "object";
1046
+ }
1047
+ /**
1048
+ * Convert a single value based on its target type
1049
+ */
1050
+ function convertValue(value, type) {
1051
+ if (value === undefined || value === null) {
1052
+ return undefined;
1053
+ }
1054
+ // Boolean type - Commander handles this automatically
1055
+ if (isBooleanType(type)) {
1056
+ if (typeof value === "boolean") {
1057
+ return value;
1058
+ }
1059
+ if (typeof value === "string") {
1060
+ const lower = value.toLowerCase();
1061
+ if (lower === "true" || lower === "1" || lower === "yes") {
1062
+ return true;
1063
+ }
1064
+ if (lower === "false" || lower === "0" || lower === "no") {
1065
+ return false;
1066
+ }
1067
+ }
1068
+ return Boolean(value);
1069
+ }
1070
+ // Number type
1071
+ if (isNumberType(type)) {
1072
+ if (typeof value === "number") {
1073
+ return value;
1074
+ }
1075
+ if (typeof value === "string") {
1076
+ const num = Number(value);
1077
+ if (!isNaN(num)) {
1078
+ return num;
1079
+ }
1080
+ }
1081
+ return value;
1082
+ }
1083
+ // Date type
1084
+ if (isDateType(type)) {
1085
+ if (value instanceof Date) {
1086
+ return value;
1087
+ }
1088
+ try {
1089
+ return fabricDate(value);
1090
+ }
1091
+ catch {
1092
+ // If conversion fails, return as-is and let createService handle it
1093
+ return value;
1094
+ }
1095
+ }
1096
+ // Array types - handle variadic options
1097
+ if (isArrayType(type)) {
1098
+ if (Array.isArray(value)) {
1099
+ return value;
1100
+ }
1101
+ if (typeof value === "string") {
1102
+ // Try to parse as JSON first
1103
+ try {
1104
+ const parsed = JSON.parse(value);
1105
+ if (Array.isArray(parsed)) {
1106
+ return parsed;
1107
+ }
1108
+ }
1109
+ catch {
1110
+ // Not JSON, check for comma or tab separated
1111
+ if (value.includes(",")) {
1112
+ return value.split(",").map((s) => s.trim());
1113
+ }
1114
+ if (value.includes("\t")) {
1115
+ return value.split("\t").map((s) => s.trim());
1116
+ }
1117
+ }
1118
+ // Single value becomes array
1119
+ return [value];
1120
+ }
1121
+ return [value];
1122
+ }
1123
+ // Object type - try to parse JSON
1124
+ if (isObjectType(type)) {
1125
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
1126
+ return value;
1127
+ }
1128
+ if (typeof value === "string") {
1129
+ try {
1130
+ const parsed = JSON.parse(value);
1131
+ if (typeof parsed === "object" &&
1132
+ parsed !== null &&
1133
+ !Array.isArray(parsed)) {
1134
+ return parsed;
1135
+ }
1136
+ }
1137
+ catch {
1138
+ // Not valid JSON
1139
+ }
1140
+ }
1141
+ return value;
1142
+ }
1143
+ // String type (default) - just return as-is
1144
+ return value;
1145
+ }
1146
+ /**
1147
+ * Parse Commander.js options back to handler input format
1148
+ *
1149
+ * This function converts the options object from Commander.js (which uses
1150
+ * kebab-case converted to camelCase) back to the expected handler input
1151
+ * format with proper type conversion based on the input definitions.
1152
+ *
1153
+ * @param options - The options object from Commander.js (program.opts())
1154
+ * @param config - Configuration including input field definitions
1155
+ * @returns An object suitable for passing to a service
1156
+ *
1157
+ * @example
1158
+ * ```typescript
1159
+ * const handler = createService({
1160
+ * input: {
1161
+ * userName: { type: String },
1162
+ * age: { type: Number },
1163
+ * tags: { type: [String] },
1164
+ * },
1165
+ * service: (input) => input,
1166
+ * });
1167
+ *
1168
+ * program.action((options) => {
1169
+ * const input = parseCommanderOptions(options, {
1170
+ * input: handler.input,
1171
+ * });
1172
+ * await handler(input);
1173
+ * });
1174
+ * ```
1175
+ */
1176
+ function parseCommanderOptions(options, config = {}) {
1177
+ const { exclude = [], input: inputDefinitions } = config;
1178
+ const result = {};
1179
+ // Build reverse mapping from flag name (camelCase) to field name
1180
+ // This handles cases where input.flag overrides the default flag name
1181
+ const flagToFieldMap = new Map();
1182
+ if (inputDefinitions) {
1183
+ for (const [fieldName, definition] of Object.entries(inputDefinitions)) {
1184
+ if (definition.flag) {
1185
+ // Custom flag: map flag (as camelCase) -> fieldName
1186
+ const flagCamelCase = toCamelCase(definition.flag);
1187
+ flagToFieldMap.set(flagCamelCase, fieldName);
1188
+ }
1189
+ else {
1190
+ // Default: kebab-case of fieldName -> fieldName
1191
+ const defaultFlag = toKebabCase(fieldName);
1192
+ const defaultFlagCamelCase = toCamelCase(defaultFlag);
1193
+ flagToFieldMap.set(defaultFlagCamelCase, fieldName);
1194
+ }
1195
+ }
1196
+ }
1197
+ for (const [key, value] of Object.entries(options)) {
1198
+ // Skip excluded fields
1199
+ if (exclude.includes(key)) {
1200
+ continue;
1201
+ }
1202
+ // Convert kebab-case Commander option to camelCase
1203
+ const keyCamelCase = toCamelCase(key);
1204
+ // Look up the actual field name from our mapping
1205
+ const fieldName = flagToFieldMap.get(keyCamelCase) ?? keyCamelCase;
1206
+ // Skip if excluded after resolution
1207
+ if (exclude.includes(fieldName)) {
1208
+ continue;
1209
+ }
1210
+ // Get field definition for type conversion
1211
+ const definition = inputDefinitions?.[fieldName];
1212
+ if (definition) {
1213
+ // Convert based on type definition
1214
+ result[fieldName] = convertValue(value, definition.type);
1215
+ }
1216
+ else {
1217
+ // No definition, pass through as-is
1218
+ result[fieldName] = value;
1219
+ }
1220
+ }
1221
+ return result;
1222
+ }
1223
+
1224
+ // Fabric a service as a Commander command
1225
+ /**
1226
+ * Fabric a service as a Commander.js command
1227
+ *
1228
+ * This function creates a command from a service, automatically:
1229
+ * - Creating the command with the service's alias (or custom name)
1230
+ * - Adding a description from the service's description (or custom)
1231
+ * - Converting input definitions to Commander options
1232
+ * - Wiring up the action to call the service with parsed input
1233
+ *
1234
+ * Error handling:
1235
+ * - Services can call context.onError() for recoverable errors
1236
+ * - Services can call context.onFatal() for fatal errors
1237
+ * - Any error that throws out of the service is treated as fatal
1238
+ *
1239
+ * @param config - Configuration including service, program, and optional overrides
1240
+ * @returns An object containing the created command
1241
+ *
1242
+ * @example
1243
+ * ```typescript
1244
+ * import { Command } from "commander";
1245
+ * import { fabricService } from "@jaypie/fabric";
1246
+ * import { fabricCommand } from "@jaypie/fabric/commander";
1247
+ *
1248
+ * const myService = fabricService({
1249
+ * alias: "greet",
1250
+ * description: "Greet a user",
1251
+ * input: {
1252
+ * userName: { type: String, description: "User name" },
1253
+ * loud: { type: Boolean, description: "Shout greeting" },
1254
+ * },
1255
+ * service: ({ userName, loud }) => {
1256
+ * const greeting = `Hello, ${userName}!`;
1257
+ * return loud ? greeting.toUpperCase() : greeting;
1258
+ * },
1259
+ * });
1260
+ *
1261
+ * const program = new Command();
1262
+ * fabricCommand({ program, service: myService });
1263
+ * program.parse();
1264
+ * ```
1265
+ */
1266
+ function fabricCommand({ alias, description, exclude, input, name, onComplete, onError, onFatal, onMessage, overrides, program, service: serviceOrFunction, }) {
1267
+ // Resolve inline service or apply overrides to pre-instantiated service
1268
+ const service = resolveService({
1269
+ alias,
1270
+ description,
1271
+ input,
1272
+ service: serviceOrFunction,
1273
+ });
1274
+ // Determine command name (priority: name > service.alias > "command")
1275
+ const commandName = name ?? service.alias ?? "command";
1276
+ // Determine command description
1277
+ const commandDescription = service.description;
1278
+ // Create the command
1279
+ const command = program.command(commandName);
1280
+ // Add description if available
1281
+ if (commandDescription) {
1282
+ command.description(commandDescription);
1283
+ }
1284
+ // Create and add options from service input
1285
+ if (service.input) {
1286
+ const { options } = createCommanderOptions(service.input, {
1287
+ exclude,
1288
+ overrides,
1289
+ });
1290
+ options.forEach((opt) => command.addOption(opt));
1291
+ }
1292
+ // Wire up the action
1293
+ command.action(async (options) => {
1294
+ // Parse Commander options to service input format
1295
+ const input = parseCommanderOptions(options, {
1296
+ exclude,
1297
+ input: service.input,
1298
+ });
1299
+ // Create context callbacks that wrap the registration callbacks with error swallowing
1300
+ // Callback failures should never halt service execution
1301
+ const sendMessage = onMessage
1302
+ ? async (message) => {
1303
+ try {
1304
+ await onMessage(message);
1305
+ }
1306
+ catch {
1307
+ // Swallow errors - messaging failures should not halt execution
1308
+ }
1309
+ }
1310
+ : undefined;
1311
+ const contextOnError = onError
1312
+ ? async (error) => {
1313
+ try {
1314
+ await onError(error);
1315
+ }
1316
+ catch {
1317
+ // Swallow errors - callback failures should not halt execution
1318
+ }
1319
+ }
1320
+ : undefined;
1321
+ const contextOnFatal = onFatal
1322
+ ? async (error) => {
1323
+ try {
1324
+ await onFatal(error);
1325
+ }
1326
+ catch {
1327
+ // Swallow errors - callback failures should not halt execution
1328
+ }
1329
+ }
1330
+ : undefined;
1331
+ // Create context for the service
1332
+ const context = {
1333
+ onError: contextOnError,
1334
+ onFatal: contextOnFatal,
1335
+ sendMessage,
1336
+ };
1337
+ try {
1338
+ // Call the service with context
1339
+ const response = await service(input, context);
1340
+ // Call onComplete callback if provided
1341
+ if (onComplete) {
1342
+ await onComplete(response);
1343
+ }
1344
+ }
1345
+ catch (error) {
1346
+ // Any error that escapes the service is treated as fatal
1347
+ // Services should catch recoverable errors and call context.onError() explicitly
1348
+ if (onFatal) {
1349
+ await onFatal(error);
1350
+ }
1351
+ else if (onError) {
1352
+ // Fall back to onError if onFatal not provided
1353
+ await onError(error);
1354
+ }
1355
+ else {
1356
+ // No error callbacks provided, re-throw
1357
+ throw error;
1358
+ }
1359
+ }
1360
+ });
1361
+ return { command, onMessage };
1362
+ }
1363
+
1364
+ // FabricCommander - Convenient wrapper for multi-command CLIs
1365
+ /**
1366
+ * Type guard to check if config is an array of services
1367
+ */
1368
+ function isServicesArray(config) {
1369
+ return Array.isArray(config);
1370
+ }
1371
+ /**
1372
+ * Type guard to check if a service entry is an inline definition
1373
+ */
1374
+ function isInlineDefinition(entry) {
1375
+ return (typeof entry === "object" &&
1376
+ entry !== null &&
1377
+ "alias" in entry &&
1378
+ "service" in entry &&
1379
+ !("$fabric" in entry));
1380
+ }
1381
+ /**
1382
+ * FabricCommander - Convenient wrapper for creating multi-command CLIs
1383
+ *
1384
+ * Creates a Commander program with multiple service commands in a single call.
1385
+ *
1386
+ * @example
1387
+ * ```typescript
1388
+ * // Array form - simple list of services
1389
+ * const cli = new FabricCommander([greetService, farewellService]);
1390
+ * cli.parse();
1391
+ *
1392
+ * // Config form - with description and version
1393
+ * const cli = new FabricCommander({
1394
+ * name: "my-cli",
1395
+ * description: "My CLI application",
1396
+ * version: "1.0.0",
1397
+ * services: [greetService, farewellService],
1398
+ * });
1399
+ * cli.parse();
1400
+ *
1401
+ * // With inline service definitions
1402
+ * const cli = new FabricCommander({
1403
+ * description: "My CLI",
1404
+ * version: "1.0.0",
1405
+ * services: [
1406
+ * existingService,
1407
+ * {
1408
+ * alias: "greet",
1409
+ * description: "Greet a user",
1410
+ * input: { name: { type: String } },
1411
+ * service: ({ name }) => `Hello, ${name}!`,
1412
+ * },
1413
+ * ],
1414
+ * });
1415
+ * cli.parse();
1416
+ * ```
1417
+ */
1418
+ class FabricCommander {
1419
+ constructor(config) {
1420
+ this.command = new commander.Command();
1421
+ // Normalize config
1422
+ const normalizedConfig = isServicesArray(config)
1423
+ ? { services: config }
1424
+ : config;
1425
+ const { description, name, onComplete, onError, onFatal, onMessage, services, version, } = normalizedConfig;
1426
+ // Set program metadata
1427
+ if (name) {
1428
+ this.command.name(name);
1429
+ }
1430
+ if (version) {
1431
+ this.command.version(version);
1432
+ }
1433
+ if (description) {
1434
+ this.command.description(description);
1435
+ }
1436
+ // Register each service as a command
1437
+ for (const entry of services) {
1438
+ if (isInlineDefinition(entry)) {
1439
+ // Inline service definition
1440
+ fabricCommand({
1441
+ alias: entry.alias,
1442
+ description: entry.description,
1443
+ input: entry.input,
1444
+ onComplete,
1445
+ onError,
1446
+ onFatal,
1447
+ onMessage,
1448
+ program: this.command,
1449
+ service: entry.service,
1450
+ });
1451
+ }
1452
+ else {
1453
+ // Pre-instantiated Service
1454
+ fabricCommand({
1455
+ onComplete,
1456
+ onError,
1457
+ onFatal,
1458
+ onMessage,
1459
+ program: this.command,
1460
+ service: entry,
1461
+ });
1462
+ }
1463
+ }
1464
+ }
1465
+ /**
1466
+ * Parse command-line arguments
1467
+ * Delegates to Commander's parse method
1468
+ */
1469
+ parse(argv) {
1470
+ this.command.parse(argv);
1471
+ return this;
1472
+ }
1473
+ /**
1474
+ * Parse command-line arguments asynchronously
1475
+ * Delegates to Commander's parseAsync method
1476
+ */
1477
+ async parseAsync(argv) {
1478
+ await this.command.parseAsync(argv);
1479
+ return this;
1480
+ }
1481
+ }
1482
+
1483
+ exports.FabricCommander = FabricCommander;
1484
+ exports.createCommanderOptions = createCommanderOptions;
1485
+ exports.fabricCommand = fabricCommand;
1486
+ exports.parseCommanderOptions = parseCommanderOptions;
1487
+ //# sourceMappingURL=index.cjs.map