@cxtms/cx-schema 1.1.1 → 1.2.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.
@@ -9,9 +9,9 @@ You are a CargoXplorer workflow YAML builder. You generate schema-valid YAML for
9
9
  **IMPORTANT — use `cx-cli` for all workflow operations:**
10
10
  - **Scaffold**: `npx cx-cli create workflow <name> --template <template>` — generates a schema-valid YAML file. ALWAYS run this first, then read the generated file, then customize. Do NOT write YAML from scratch or copy templates manually.
11
11
  - **Validate**: `npx cx-cli <file.yaml>` — run after every change
12
- - **Schema lookup**: `npx cx-cli schema <task>` — e.g., `cx-cli schema graphql`, `cx-cli schema foreach`
12
+ - **Schema lookup**: `npx cx-cli schema <task>` — e.g., `cx-cli schema graphql`, `cx-cli schema foreach`, `cx-cli schema action-event`. Schema names use kebab-case file names. Case-insensitive: `ActionEvent` resolves to `action-event`.
13
13
  - **Examples**: `npx cx-cli example <task>` — show example YAML for a task
14
- - **List schemas**: `npx cx-cli list --type workflow`
14
+ - **List schemas**: `npx cx-cli list --type workflow` — shows all available task schemas in the Tasks section
15
15
  - **Feature folder**: `npx cx-cli create workflow <name> --template <template> --feature <feature-name>`
16
16
 
17
17
  ## Generation Workflow
@@ -91,6 +91,27 @@ Performs HTTP requests to external APIs.
91
91
 
92
92
  Response available at `ActivityName?.CallApi?.result?`.
93
93
 
94
+ **Action events**: When an HTTP request operates on a specific entity (e.g., sending parcel info for an order), enable `actionEvents` in the inputs so the system can track and notify about the request. Include `eventDataExt` with the entity ID to link the event to the entity.
95
+
96
+ ```yaml
97
+ - task: "Utilities/HttpRequest@1"
98
+ name: CallCarrierApi
99
+ inputs:
100
+ actionEvents:
101
+ enabled: true
102
+ eventName: "carrier.sendParcelInfo"
103
+ eventDataExt:
104
+ orderId: "{{ inputs.orderId }}"
105
+ url: "{{ carrierConfig?.baseUrl? }}/api/shipments"
106
+ method: POST
107
+ contentType: "application/json"
108
+ body:
109
+ trackingNumber: "{{ Data?.GetOrder?.order?.trackingNumber? }}"
110
+ outputs:
111
+ - name: result
112
+ mapping: "response?.body?"
113
+ ```
114
+
94
115
  ## Map@1
95
116
 
96
117
  Extracts/reshapes data from variables into new variables.
package/dist/cli.js CHANGED
@@ -72,8 +72,9 @@ ${chalk_1.default.bold.yellow('COMMANDS:')}
72
72
  ${chalk_1.default.green('validate')} Validate YAML file(s) ${chalk_1.default.gray('(default command)')}
73
73
  ${chalk_1.default.green('report')} Generate validation report for multiple files
74
74
  ${chalk_1.default.green('init')} Initialize a new CX project with app.yaml, README.md, AGENTS.md
75
- ${chalk_1.default.green('create')} Create a new module or workflow from template
75
+ ${chalk_1.default.green('create')} Create a new module, workflow, or task-schema from template
76
76
  ${chalk_1.default.green('extract')} Extract a component (and its routes) to another module
77
+ ${chalk_1.default.green('sync-schemas')} Regenerate all.json from task schema directory
77
78
  ${chalk_1.default.green('schema')} Show JSON schema for a component or task
78
79
  ${chalk_1.default.green('example')} Show example YAML for a component or task
79
80
  ${chalk_1.default.green('list')} List available schemas (modules, workflows, tasks)
@@ -92,6 +93,7 @@ ${chalk_1.default.bold.yellow('OPTIONS:')}
92
93
  ${chalk_1.default.green('--template <name>')} Template variant for create command (e.g., ${chalk_1.default.cyan('basic')})
93
94
  ${chalk_1.default.green('--feature <name>')} Place file under features/<name>/ instead of root
94
95
  ${chalk_1.default.green('--options <json>')} JSON field definitions for create (inline or file path)
96
+ ${chalk_1.default.green('--tasks <list>')} Comma-separated task enums for create task-schema
95
97
  ${chalk_1.default.green('--to <file>')} Target file for extract command
96
98
 
97
99
  ${chalk_1.default.bold.yellow('VALIDATION EXAMPLES:')}
@@ -132,6 +134,12 @@ ${chalk_1.default.bold.yellow('PROJECT COMMANDS:')}
132
134
  ${chalk_1.default.gray('# Create module with custom fields')}
133
135
  ${chalk_1.default.cyan(`${PROGRAM_NAME} create module my-config --template configuration --options '[{"name":"host","type":"text"},{"name":"port","type":"number"}]'`)}
134
136
 
137
+ ${chalk_1.default.gray('# Create a task schema with pre-populated task enums')}
138
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create task-schema filetransfer --tasks "FileTransfer/Connect@1,FileTransfer/Disconnect@1"`)}
139
+
140
+ ${chalk_1.default.gray('# Sync all.json after manually adding/removing task schemas')}
141
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} sync-schemas`)}
142
+
135
143
  ${chalk_1.default.gray('# Extract a component to a new module')}
136
144
  ${chalk_1.default.cyan(`${PROGRAM_NAME} extract modules/orders.yaml Orders/CreateItem --to modules/order-create.yaml`)}
137
145
 
@@ -194,9 +202,13 @@ ${chalk_1.default.bold.yellow('AVAILABLE SCHEMAS:')}
194
202
 
195
203
  ${chalk_1.default.bold('Workflow Tasks:')}
196
204
  foreach, switch, while, validation, graphql, httpRequest,
197
- setVariable, map, log, error, csv, export, template,
198
- order, contact, commodity, job, attachment,
199
- email-send, document-render, charge, workflow-execute
205
+ setVariable, map, log, error, csv, export, template, import,
206
+ order, contact, contact-address, contact-payment-method,
207
+ commodity, job, attachment, charge, payment,
208
+ email-send, document-render, document-send, pdf-document,
209
+ accounting-transaction, number, workflow-execute,
210
+ filetransfer, caching, flow-transition, user, authentication,
211
+ edi, note, appmodule, action-event, inventory, movement
200
212
 
201
213
  ${chalk_1.default.bold.yellow('EXAMPLES:')}
202
214
  ${chalk_1.default.cyan(`${PROGRAM_NAME} schema form`)}
@@ -910,8 +922,13 @@ function runInit() {
910
922
  console.log('');
911
923
  }
912
924
  function runCreate(type, name, template, feature, createOptions) {
925
+ // Handle task-schema creation separately
926
+ if (type === 'task-schema') {
927
+ runCreateTaskSchema(name, createOptions);
928
+ return;
929
+ }
913
930
  if (!type || !['module', 'workflow'].includes(type)) {
914
- console.error(chalk_1.default.red('Error: Invalid or missing type. Use: module or workflow'));
931
+ console.error(chalk_1.default.red('Error: Invalid or missing type. Use: module, workflow, or task-schema'));
915
932
  console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} create module my-module`));
916
933
  process.exit(2);
917
934
  }
@@ -920,13 +937,13 @@ function runCreate(type, name, template, feature, createOptions) {
920
937
  console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} create ${type} my-${type}`));
921
938
  process.exit(2);
922
939
  }
923
- // Sanitize name
924
- const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
940
+ // Sanitize name: replace invalid chars with hyphen, collapse runs, trim edges
941
+ const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-|-$/g, '');
925
942
  // Determine output directory and file
926
943
  // workflows/ or features/<feature>/workflows/ (same for modules)
927
944
  const baseDir = type === 'module' ? 'modules' : 'workflows';
928
945
  const dir = feature
929
- ? path.join('features', feature.toLowerCase().replace(/[^a-z0-9-]/g, '-'), baseDir)
946
+ ? path.join('features', feature.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-|-$/g, ''), baseDir)
930
947
  : baseDir;
931
948
  const fileName = `${safeName}.yaml`;
932
949
  const filePath = path.join(process.cwd(), dir, fileName);
@@ -960,6 +977,160 @@ function runCreate(type, name, template, feature, createOptions) {
960
977
  console.log('');
961
978
  }
962
979
  // ============================================================================
980
+ // Create Task Schema Command
981
+ // ============================================================================
982
+ function runCreateTaskSchema(name, tasks) {
983
+ if (!name) {
984
+ console.error(chalk_1.default.red('Error: Missing name for task-schema'));
985
+ console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} create task-schema filetransfer --tasks "FileTransfer/Connect@1,FileTransfer/Disconnect@1"`));
986
+ process.exit(2);
987
+ }
988
+ const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-|-$/g, '');
989
+ // Find schemas directory — prefer source schemas/ in dev, fall back to standard resolution
990
+ let schemasDir = path.join(process.cwd(), 'schemas');
991
+ if (!fs.existsSync(schemasDir)) {
992
+ const resolved = findSchemasPath();
993
+ if (!resolved) {
994
+ console.error(chalk_1.default.red('Error: Cannot find schemas directory'));
995
+ process.exit(2);
996
+ }
997
+ schemasDir = resolved;
998
+ }
999
+ const tasksDir = path.join(schemasDir, 'workflows', 'tasks');
1000
+ if (!fs.existsSync(tasksDir)) {
1001
+ fs.mkdirSync(tasksDir, { recursive: true });
1002
+ }
1003
+ const filePath = path.join(tasksDir, `${safeName}.json`);
1004
+ if (fs.existsSync(filePath)) {
1005
+ console.error(chalk_1.default.red(`Error: Schema file already exists: ${filePath}`));
1006
+ process.exit(2);
1007
+ }
1008
+ // Parse --tasks flag (passed via --options)
1009
+ const taskEnums = [];
1010
+ if (tasks) {
1011
+ for (const t of tasks.split(',')) {
1012
+ const trimmed = t.trim();
1013
+ if (trimmed)
1014
+ taskEnums.push(trimmed);
1015
+ }
1016
+ }
1017
+ // Derive a title from the name
1018
+ const title = safeName
1019
+ .split('-')
1020
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
1021
+ .join(' ') + ' Tasks';
1022
+ // Build the schema JSON
1023
+ const schema = {
1024
+ $schema: 'http://json-schema.org/draft-07/schema#',
1025
+ title,
1026
+ description: `${title} operations`,
1027
+ type: 'object',
1028
+ properties: {
1029
+ task: {
1030
+ type: 'string',
1031
+ ...(taskEnums.length > 0 ? { enum: taskEnums } : {}),
1032
+ description: 'Task type identifier'
1033
+ },
1034
+ name: {
1035
+ type: 'string',
1036
+ description: 'Step name identifier'
1037
+ },
1038
+ description: {
1039
+ type: 'string',
1040
+ description: 'Step description'
1041
+ },
1042
+ conditions: {
1043
+ type: 'array',
1044
+ items: {
1045
+ type: 'object',
1046
+ properties: {
1047
+ expression: { type: 'string' }
1048
+ },
1049
+ required: ['expression']
1050
+ }
1051
+ },
1052
+ continueOnError: {
1053
+ type: 'boolean'
1054
+ },
1055
+ inputs: {
1056
+ type: 'object',
1057
+ description: `${title} inputs`,
1058
+ additionalProperties: true
1059
+ },
1060
+ outputs: {
1061
+ type: 'array',
1062
+ items: {
1063
+ type: 'object',
1064
+ properties: {
1065
+ name: { type: 'string' },
1066
+ mapping: { type: 'string' }
1067
+ },
1068
+ required: ['name', 'mapping']
1069
+ }
1070
+ }
1071
+ },
1072
+ required: ['task'],
1073
+ additionalProperties: true
1074
+ };
1075
+ fs.writeFileSync(filePath, JSON.stringify(schema, null, 2) + '\n', 'utf-8');
1076
+ // Sync all.json
1077
+ syncAllJson(tasksDir);
1078
+ // Invalidate cache so new schema is immediately discoverable
1079
+ _workflowTaskNamesCache = null;
1080
+ console.log(chalk_1.default.green(`\n✓ Created task schema: ${path.relative(process.cwd(), filePath)}`));
1081
+ console.log(chalk_1.default.gray(`\n Next steps:`));
1082
+ console.log(chalk_1.default.gray(` 1. Edit ${chalk_1.default.white(filePath)} to add typed input properties`));
1083
+ console.log(chalk_1.default.gray(` 2. Verify: ${chalk_1.default.white(`${PROGRAM_NAME} schema ${safeName}`)}`));
1084
+ console.log(chalk_1.default.gray(` 3. all.json has been auto-updated with the new reference`));
1085
+ console.log('');
1086
+ }
1087
+ // ============================================================================
1088
+ // Sync all.json (auto-regenerate $ref entries from task schema directory)
1089
+ // ============================================================================
1090
+ function syncAllJson(tasksDir) {
1091
+ const files = fs.readdirSync(tasksDir)
1092
+ .filter(f => f.endsWith('.json') && f !== 'all.json' && f !== 'generic.json')
1093
+ .sort();
1094
+ const anyOfRefs = files.map(f => ({ $ref: f }));
1095
+ // generic.json always last as fallback
1096
+ if (fs.existsSync(path.join(tasksDir, 'generic.json'))) {
1097
+ anyOfRefs.push({ $ref: 'generic.json' });
1098
+ }
1099
+ const allJson = {
1100
+ $schema: 'http://json-schema.org/draft-07/schema#',
1101
+ title: 'All Workflow Tasks',
1102
+ description: 'Aggregator schema for all workflow task types. Uses anyOf to allow matching any known task type or falling back to generic task structure.',
1103
+ type: 'object',
1104
+ anyOf: anyOfRefs
1105
+ };
1106
+ fs.writeFileSync(path.join(tasksDir, 'all.json'), JSON.stringify(allJson, null, 2) + '\n', 'utf-8');
1107
+ }
1108
+ function runSyncSchemas() {
1109
+ // Find schemas directory
1110
+ let schemasDir = path.join(process.cwd(), 'schemas');
1111
+ if (!fs.existsSync(schemasDir)) {
1112
+ const resolved = findSchemasPath();
1113
+ if (!resolved) {
1114
+ console.error(chalk_1.default.red('Error: Cannot find schemas directory'));
1115
+ process.exit(2);
1116
+ }
1117
+ schemasDir = resolved;
1118
+ }
1119
+ const tasksDir = path.join(schemasDir, 'workflows', 'tasks');
1120
+ if (!fs.existsSync(tasksDir)) {
1121
+ console.error(chalk_1.default.red('Error: Tasks directory not found'));
1122
+ process.exit(2);
1123
+ }
1124
+ syncAllJson(tasksDir);
1125
+ // Invalidate cache
1126
+ _workflowTaskNamesCache = null;
1127
+ const taskCount = fs.readdirSync(tasksDir)
1128
+ .filter(f => f.endsWith('.json') && f !== 'all.json' && f !== 'generic.json')
1129
+ .length;
1130
+ console.log(chalk_1.default.green(`\n✓ Synced all.json with ${taskCount} task schemas (+ generic fallback)`));
1131
+ console.log('');
1132
+ }
1133
+ // ============================================================================
963
1134
  // Extract Command
964
1135
  // ============================================================================
965
1136
  function runExtract(sourceFile, componentName, targetFile) {
@@ -1090,7 +1261,7 @@ function parseArgs(args) {
1090
1261
  reportFormat: 'json'
1091
1262
  };
1092
1263
  // Check for commands
1093
- const commands = ['validate', 'schema', 'example', 'list', 'help', 'report', 'init', 'create', 'extract'];
1264
+ const commands = ['validate', 'schema', 'example', 'list', 'help', 'report', 'init', 'create', 'extract', 'sync-schemas'];
1094
1265
  if (args.length > 0 && commands.includes(args[0])) {
1095
1266
  command = args[0];
1096
1267
  args = args.slice(1);
@@ -1157,6 +1328,9 @@ function parseArgs(args) {
1157
1328
  else if (arg === '--options') {
1158
1329
  options.createOptions = args[++i];
1159
1330
  }
1331
+ else if (arg === '--tasks') {
1332
+ options.createTasks = args[++i];
1333
+ }
1160
1334
  else if (arg === '--to') {
1161
1335
  options.extractTo = args[++i];
1162
1336
  }
@@ -1244,47 +1418,81 @@ function detectFileType(filePath) {
1244
1418
  // ============================================================================
1245
1419
  // Schema Display
1246
1420
  // ============================================================================
1421
+ // Cache for dynamically discovered workflow task schema names
1422
+ let _workflowTaskNamesCache = null;
1423
+ function getWorkflowTaskNames(schemasPath) {
1424
+ if (_workflowTaskNamesCache)
1425
+ return _workflowTaskNamesCache;
1426
+ const tasksDir = path.join(schemasPath, 'workflows', 'tasks');
1427
+ _workflowTaskNamesCache = new Set();
1428
+ if (fs.existsSync(tasksDir)) {
1429
+ for (const file of fs.readdirSync(tasksDir)) {
1430
+ if (file.endsWith('.json') && file !== 'all.json') {
1431
+ _workflowTaskNamesCache.add(file.replace('.json', '').toLowerCase().replace(/[^a-z0-9-]/g, ''));
1432
+ }
1433
+ }
1434
+ }
1435
+ // Also include common definitions
1436
+ const commonDir = path.join(schemasPath, 'workflows', 'common');
1437
+ if (fs.existsSync(commonDir)) {
1438
+ for (const file of fs.readdirSync(commonDir)) {
1439
+ if (file.endsWith('.json')) {
1440
+ _workflowTaskNamesCache.add(file.replace('.json', '').toLowerCase().replace(/[^a-z0-9-]/g, ''));
1441
+ }
1442
+ }
1443
+ }
1444
+ return _workflowTaskNamesCache;
1445
+ }
1247
1446
  function findSchemaFile(schemasPath, name, preferWorkflow = false) {
1248
- // Normalize name
1447
+ // Normalize name: lowercase, strip non-alphanumeric except hyphens
1249
1448
  const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '');
1250
- // Workflow schema names - these should match workflow schemas first
1449
+ // Dynamically detect workflow schema names from directory contents
1251
1450
  const workflowCoreNames = ['workflow', 'activity', 'input', 'output', 'variable', 'trigger', 'schedule'];
1252
- const workflowTaskNames = [
1253
- 'foreach', 'switch', 'while', 'validation', 'map', 'setvariable', 'httprequest',
1254
- 'log', 'error', 'csv', 'export', 'template', 'graphql', 'order', 'contact',
1255
- 'commodity', 'job', 'attachment', 'email-send', 'document-render', 'document-send',
1256
- 'charge', 'accounting-transaction', 'payment', 'workflow-execute', 'condition',
1257
- 'expression', 'mapping'
1258
- ];
1451
+ const workflowTaskNames = getWorkflowTaskNames(schemasPath);
1259
1452
  const isWorkflowSchema = workflowCoreNames.includes(normalizedName) ||
1260
- workflowTaskNames.includes(normalizedName);
1261
- // Search patterns in order of priority
1453
+ workflowTaskNames.has(normalizedName);
1454
+ // Build search paths using normalized name for consistency
1262
1455
  const searchPaths = preferWorkflow || isWorkflowSchema
1263
1456
  ? [
1264
1457
  // Workflow schemas first for workflow-related names
1265
- path.join(schemasPath, 'workflows', `${name}.json`),
1266
- path.join(schemasPath, 'workflows', 'tasks', `${name}.json`),
1267
- path.join(schemasPath, 'workflows', 'common', `${name}.json`),
1458
+ path.join(schemasPath, 'workflows', `${normalizedName}.json`),
1459
+ path.join(schemasPath, 'workflows', 'tasks', `${normalizedName}.json`),
1460
+ path.join(schemasPath, 'workflows', 'common', `${normalizedName}.json`),
1268
1461
  // Then module schemas
1269
- path.join(schemasPath, 'components', `${name}.json`),
1270
- path.join(schemasPath, 'fields', `${name}.json`),
1271
- path.join(schemasPath, 'actions', `${name}.json`)
1462
+ path.join(schemasPath, 'components', `${normalizedName}.json`),
1463
+ path.join(schemasPath, 'fields', `${normalizedName}.json`),
1464
+ path.join(schemasPath, 'actions', `${normalizedName}.json`)
1272
1465
  ]
1273
1466
  : [
1274
1467
  // Module schemas first
1275
- path.join(schemasPath, 'components', `${name}.json`),
1276
- path.join(schemasPath, 'fields', `${name}.json`),
1277
- path.join(schemasPath, 'actions', `${name}.json`),
1468
+ path.join(schemasPath, 'components', `${normalizedName}.json`),
1469
+ path.join(schemasPath, 'fields', `${normalizedName}.json`),
1470
+ path.join(schemasPath, 'actions', `${normalizedName}.json`),
1278
1471
  // Then workflow schemas
1279
- path.join(schemasPath, 'workflows', `${name}.json`),
1280
- path.join(schemasPath, 'workflows', 'tasks', `${name}.json`),
1281
- path.join(schemasPath, 'workflows', 'common', `${name}.json`)
1472
+ path.join(schemasPath, 'workflows', `${normalizedName}.json`),
1473
+ path.join(schemasPath, 'workflows', 'tasks', `${normalizedName}.json`),
1474
+ path.join(schemasPath, 'workflows', 'common', `${normalizedName}.json`)
1282
1475
  ];
1283
1476
  for (const schemaPath of searchPaths) {
1284
1477
  if (fs.existsSync(schemaPath)) {
1285
1478
  return schemaPath;
1286
1479
  }
1287
1480
  }
1481
+ // Also try with the original name (preserving case) for backwards compatibility
1482
+ if (normalizedName !== name) {
1483
+ const caseSensitivePaths = [
1484
+ path.join(schemasPath, 'workflows', 'tasks', `${name}.json`),
1485
+ path.join(schemasPath, 'workflows', `${name}.json`),
1486
+ path.join(schemasPath, 'components', `${name}.json`),
1487
+ path.join(schemasPath, 'fields', `${name}.json`),
1488
+ path.join(schemasPath, 'actions', `${name}.json`)
1489
+ ];
1490
+ for (const schemaPath of caseSensitivePaths) {
1491
+ if (fs.existsSync(schemaPath)) {
1492
+ return schemaPath;
1493
+ }
1494
+ }
1495
+ }
1288
1496
  // Try fuzzy matching
1289
1497
  const allSchemas = getAllSchemas(schemasPath);
1290
1498
  for (const schema of allSchemas) {
@@ -1940,7 +2148,18 @@ async function main() {
1940
2148
  }
1941
2149
  // Handle create command
1942
2150
  if (command === 'create') {
1943
- runCreate(files[0], files[1], options.template, options.feature, options.createOptions);
2151
+ // For task-schema, pass --tasks (from options.createTasks) as the tasks argument
2152
+ if (files[0] === 'task-schema') {
2153
+ runCreate(files[0], files[1], options.template, options.feature, options.createTasks);
2154
+ }
2155
+ else {
2156
+ runCreate(files[0], files[1], options.template, options.feature, options.createOptions);
2157
+ }
2158
+ process.exit(0);
2159
+ }
2160
+ // Handle sync-schemas command
2161
+ if (command === 'sync-schemas') {
2162
+ runSyncSchemas();
1944
2163
  process.exit(0);
1945
2164
  }
1946
2165
  // Handle extract command