@cxtms/cx-schema 1.1.1 → 1.3.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 (39) hide show
  1. package/.claude/skills/cx-core/ref-entity-commodity.md +22 -0
  2. package/.claude/skills/cx-workflow/SKILL.md +2 -2
  3. package/.claude/skills/cx-workflow/ref-entity.md +30 -1
  4. package/.claude/skills/cx-workflow/ref-expressions.md +3 -0
  5. package/.claude/skills/cx-workflow/ref-utilities.md +21 -0
  6. package/dist/cli.js +399 -88
  7. package/dist/cli.js.map +1 -1
  8. package/dist/extractUtils.d.ts +11 -0
  9. package/dist/extractUtils.d.ts.map +1 -0
  10. package/dist/extractUtils.js +19 -0
  11. package/dist/extractUtils.js.map +1 -0
  12. package/dist/validator.js +2 -2
  13. package/dist/validator.js.map +1 -1
  14. package/dist/workflowValidator.js +2 -2
  15. package/dist/workflowValidator.js.map +1 -1
  16. package/package.json +5 -5
  17. package/schemas/workflows/tasks/action-event.json +65 -0
  18. package/schemas/workflows/tasks/all.json +126 -26
  19. package/schemas/workflows/tasks/appmodule.json +56 -0
  20. package/schemas/workflows/tasks/attachment.json +4 -1
  21. package/schemas/workflows/tasks/authentication.json +72 -0
  22. package/schemas/workflows/tasks/caching.json +68 -0
  23. package/schemas/workflows/tasks/charge.json +3 -1
  24. package/schemas/workflows/tasks/commodity.json +3 -0
  25. package/schemas/workflows/tasks/contact-address.json +72 -0
  26. package/schemas/workflows/tasks/contact-payment-method.json +72 -0
  27. package/schemas/workflows/tasks/edi.json +65 -0
  28. package/schemas/workflows/tasks/filetransfer.json +102 -0
  29. package/schemas/workflows/tasks/flow-transition.json +68 -0
  30. package/schemas/workflows/tasks/httpRequest.json +23 -0
  31. package/schemas/workflows/tasks/import.json +64 -0
  32. package/schemas/workflows/tasks/inventory.json +67 -0
  33. package/schemas/workflows/tasks/movement.json +54 -0
  34. package/schemas/workflows/tasks/note.json +59 -0
  35. package/schemas/workflows/tasks/number.json +65 -0
  36. package/schemas/workflows/tasks/order.json +8 -1
  37. package/schemas/workflows/tasks/pdf-document.json +60 -0
  38. package/schemas/workflows/tasks/user.json +70 -0
  39. package/scripts/postinstall.js +3 -3
package/dist/cli.js CHANGED
@@ -43,9 +43,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
43
43
  const fs = __importStar(require("fs"));
44
44
  const path = __importStar(require("path"));
45
45
  const chalk_1 = __importDefault(require("chalk"));
46
- const yaml = __importStar(require("js-yaml"));
46
+ const yaml_1 = __importStar(require("yaml"));
47
47
  const validator_1 = require("./validator");
48
48
  const workflowValidator_1 = require("./workflowValidator");
49
+ const extractUtils_1 = require("./extractUtils");
49
50
  // ============================================================================
50
51
  // Constants
51
52
  // ============================================================================
@@ -72,8 +73,9 @@ ${chalk_1.default.bold.yellow('COMMANDS:')}
72
73
  ${chalk_1.default.green('validate')} Validate YAML file(s) ${chalk_1.default.gray('(default command)')}
73
74
  ${chalk_1.default.green('report')} Generate validation report for multiple files
74
75
  ${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
76
+ ${chalk_1.default.green('create')} Create a new module, workflow, or task-schema from template
76
77
  ${chalk_1.default.green('extract')} Extract a component (and its routes) to another module
78
+ ${chalk_1.default.green('sync-schemas')} Regenerate all.json from task schema directory
77
79
  ${chalk_1.default.green('schema')} Show JSON schema for a component or task
78
80
  ${chalk_1.default.green('example')} Show example YAML for a component or task
79
81
  ${chalk_1.default.green('list')} List available schemas (modules, workflows, tasks)
@@ -92,7 +94,9 @@ ${chalk_1.default.bold.yellow('OPTIONS:')}
92
94
  ${chalk_1.default.green('--template <name>')} Template variant for create command (e.g., ${chalk_1.default.cyan('basic')})
93
95
  ${chalk_1.default.green('--feature <name>')} Place file under features/<name>/ instead of root
94
96
  ${chalk_1.default.green('--options <json>')} JSON field definitions for create (inline or file path)
97
+ ${chalk_1.default.green('--tasks <list>')} Comma-separated task enums for create task-schema
95
98
  ${chalk_1.default.green('--to <file>')} Target file for extract command
99
+ ${chalk_1.default.green('--copy')} Copy component instead of moving (source unchanged, target gets higher priority)
96
100
 
97
101
  ${chalk_1.default.bold.yellow('VALIDATION EXAMPLES:')}
98
102
  ${chalk_1.default.gray('# Validate a module YAML file')}
@@ -132,6 +136,12 @@ ${chalk_1.default.bold.yellow('PROJECT COMMANDS:')}
132
136
  ${chalk_1.default.gray('# Create module with custom fields')}
133
137
  ${chalk_1.default.cyan(`${PROGRAM_NAME} create module my-config --template configuration --options '[{"name":"host","type":"text"},{"name":"port","type":"number"}]'`)}
134
138
 
139
+ ${chalk_1.default.gray('# Create a task schema with pre-populated task enums')}
140
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} create task-schema filetransfer --tasks "FileTransfer/Connect@1,FileTransfer/Disconnect@1"`)}
141
+
142
+ ${chalk_1.default.gray('# Sync all.json after manually adding/removing task schemas')}
143
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} sync-schemas`)}
144
+
135
145
  ${chalk_1.default.gray('# Extract a component to a new module')}
136
146
  ${chalk_1.default.cyan(`${PROGRAM_NAME} extract modules/orders.yaml Orders/CreateItem --to modules/order-create.yaml`)}
137
147
 
@@ -194,9 +204,13 @@ ${chalk_1.default.bold.yellow('AVAILABLE SCHEMAS:')}
194
204
 
195
205
  ${chalk_1.default.bold('Workflow Tasks:')}
196
206
  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
207
+ setVariable, map, log, error, csv, export, template, import,
208
+ order, contact, contact-address, contact-payment-method,
209
+ commodity, job, attachment, charge, payment,
210
+ email-send, document-render, document-send, pdf-document,
211
+ accounting-transaction, number, workflow-execute,
212
+ filetransfer, caching, flow-transition, user, authentication,
213
+ edi, note, appmodule, action-event, inventory, movement
200
214
 
201
215
  ${chalk_1.default.bold.yellow('EXAMPLES:')}
202
216
  ${chalk_1.default.cyan(`${PROGRAM_NAME} schema form`)}
@@ -269,16 +283,25 @@ ${chalk_1.default.bold.yellow('EXTRACT COMMAND')}
269
283
  Extract a component (and its routes) from one module into another.
270
284
 
271
285
  ${chalk_1.default.bold.yellow('USAGE:')}
272
- ${chalk_1.default.cyan(`${PROGRAM_NAME} extract <source-file> <component-name> --to <target-file>`)}
286
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} extract <source-file> <component-name> --to <target-file> [--copy]`)}
287
+
288
+ ${chalk_1.default.bold.yellow('OPTIONS:')}
289
+ ${chalk_1.default.green('--copy')} Copy instead of move. Source stays unchanged, target gets higher priority.
273
290
 
274
291
  ${chalk_1.default.bold.yellow('WHAT GETS MOVED:')}
275
292
  ${chalk_1.default.green('Component')} - The component matching the exact name
276
293
  ${chalk_1.default.green('Routes')} - Any routes whose component field matches the name
277
294
  ${chalk_1.default.gray('Permissions and entities are NOT moved')}
278
295
 
296
+ ${chalk_1.default.bold.yellow('PRIORITY (--copy mode):')}
297
+ When using --copy, the target module gets a priority higher than the source:
298
+ ${chalk_1.default.gray('Source priority 1 → Target priority 2')}
299
+ ${chalk_1.default.gray('Source no priority → Target priority 1')}
300
+
279
301
  ${chalk_1.default.bold.yellow('EXAMPLES:')}
280
302
  ${chalk_1.default.cyan(`${PROGRAM_NAME} extract modules/orders.yaml Orders/CreateItem --to modules/order-create.yaml`)}
281
303
  ${chalk_1.default.cyan(`${PROGRAM_NAME} extract modules/main.yaml Dashboard --to modules/dashboard.yaml`)}
304
+ ${chalk_1.default.cyan(`${PROGRAM_NAME} extract modules/orders.yaml Orders/CreateItem --to modules/order-create.yaml --copy`)}
282
305
  `;
283
306
  // ============================================================================
284
307
  // Templates
@@ -798,7 +821,7 @@ function applyCreateOptions(content, optionsArg) {
798
821
  }
799
822
  }
800
823
  // Parse YAML
801
- const doc = yaml.load(content);
824
+ const doc = yaml_1.default.parse(content);
802
825
  if (!doc)
803
826
  throw new Error('Failed to parse template YAML for --options processing');
804
827
  let applied = false;
@@ -837,12 +860,10 @@ function applyCreateOptions(content, optionsArg) {
837
860
  return content;
838
861
  }
839
862
  // Dump back to YAML
840
- const yamlContent = yaml.dump(doc, {
863
+ const yamlContent = yaml_1.default.stringify(doc, {
841
864
  indent: 2,
842
- lineWidth: -1,
843
- noRefs: true,
844
- quotingType: '"',
845
- forceQuotes: false
865
+ lineWidth: 0,
866
+ singleQuote: false,
846
867
  });
847
868
  return headerLines.join('\n') + yamlContent;
848
869
  }
@@ -910,8 +931,13 @@ function runInit() {
910
931
  console.log('');
911
932
  }
912
933
  function runCreate(type, name, template, feature, createOptions) {
934
+ // Handle task-schema creation separately
935
+ if (type === 'task-schema') {
936
+ runCreateTaskSchema(name, createOptions);
937
+ return;
938
+ }
913
939
  if (!type || !['module', 'workflow'].includes(type)) {
914
- console.error(chalk_1.default.red('Error: Invalid or missing type. Use: module or workflow'));
940
+ console.error(chalk_1.default.red('Error: Invalid or missing type. Use: module, workflow, or task-schema'));
915
941
  console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} create module my-module`));
916
942
  process.exit(2);
917
943
  }
@@ -920,13 +946,13 @@ function runCreate(type, name, template, feature, createOptions) {
920
946
  console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} create ${type} my-${type}`));
921
947
  process.exit(2);
922
948
  }
923
- // Sanitize name
924
- const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
949
+ // Sanitize name: replace invalid chars with hyphen, collapse runs, trim edges
950
+ const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-|-$/g, '');
925
951
  // Determine output directory and file
926
952
  // workflows/ or features/<feature>/workflows/ (same for modules)
927
953
  const baseDir = type === 'module' ? 'modules' : 'workflows';
928
954
  const dir = feature
929
- ? path.join('features', feature.toLowerCase().replace(/[^a-z0-9-]/g, '-'), baseDir)
955
+ ? path.join('features', feature.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-|-$/g, ''), baseDir)
930
956
  : baseDir;
931
957
  const fileName = `${safeName}.yaml`;
932
958
  const filePath = path.join(process.cwd(), dir, fileName);
@@ -960,13 +986,167 @@ function runCreate(type, name, template, feature, createOptions) {
960
986
  console.log('');
961
987
  }
962
988
  // ============================================================================
989
+ // Create Task Schema Command
990
+ // ============================================================================
991
+ function runCreateTaskSchema(name, tasks) {
992
+ if (!name) {
993
+ console.error(chalk_1.default.red('Error: Missing name for task-schema'));
994
+ console.error(chalk_1.default.gray(`Example: ${PROGRAM_NAME} create task-schema filetransfer --tasks "FileTransfer/Connect@1,FileTransfer/Disconnect@1"`));
995
+ process.exit(2);
996
+ }
997
+ const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-{2,}/g, '-').replace(/^-|-$/g, '');
998
+ // Find schemas directory — prefer source schemas/ in dev, fall back to standard resolution
999
+ let schemasDir = path.join(process.cwd(), 'schemas');
1000
+ if (!fs.existsSync(schemasDir)) {
1001
+ const resolved = findSchemasPath();
1002
+ if (!resolved) {
1003
+ console.error(chalk_1.default.red('Error: Cannot find schemas directory'));
1004
+ process.exit(2);
1005
+ }
1006
+ schemasDir = resolved;
1007
+ }
1008
+ const tasksDir = path.join(schemasDir, 'workflows', 'tasks');
1009
+ if (!fs.existsSync(tasksDir)) {
1010
+ fs.mkdirSync(tasksDir, { recursive: true });
1011
+ }
1012
+ const filePath = path.join(tasksDir, `${safeName}.json`);
1013
+ if (fs.existsSync(filePath)) {
1014
+ console.error(chalk_1.default.red(`Error: Schema file already exists: ${filePath}`));
1015
+ process.exit(2);
1016
+ }
1017
+ // Parse --tasks flag (passed via --options)
1018
+ const taskEnums = [];
1019
+ if (tasks) {
1020
+ for (const t of tasks.split(',')) {
1021
+ const trimmed = t.trim();
1022
+ if (trimmed)
1023
+ taskEnums.push(trimmed);
1024
+ }
1025
+ }
1026
+ // Derive a title from the name
1027
+ const title = safeName
1028
+ .split('-')
1029
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
1030
+ .join(' ') + ' Tasks';
1031
+ // Build the schema JSON
1032
+ const schema = {
1033
+ $schema: 'http://json-schema.org/draft-07/schema#',
1034
+ title,
1035
+ description: `${title} operations`,
1036
+ type: 'object',
1037
+ properties: {
1038
+ task: {
1039
+ type: 'string',
1040
+ ...(taskEnums.length > 0 ? { enum: taskEnums } : {}),
1041
+ description: 'Task type identifier'
1042
+ },
1043
+ name: {
1044
+ type: 'string',
1045
+ description: 'Step name identifier'
1046
+ },
1047
+ description: {
1048
+ type: 'string',
1049
+ description: 'Step description'
1050
+ },
1051
+ conditions: {
1052
+ type: 'array',
1053
+ items: {
1054
+ type: 'object',
1055
+ properties: {
1056
+ expression: { type: 'string' }
1057
+ },
1058
+ required: ['expression']
1059
+ }
1060
+ },
1061
+ continueOnError: {
1062
+ type: 'boolean'
1063
+ },
1064
+ inputs: {
1065
+ type: 'object',
1066
+ description: `${title} inputs`,
1067
+ additionalProperties: true
1068
+ },
1069
+ outputs: {
1070
+ type: 'array',
1071
+ items: {
1072
+ type: 'object',
1073
+ properties: {
1074
+ name: { type: 'string' },
1075
+ mapping: { type: 'string' }
1076
+ },
1077
+ required: ['name', 'mapping']
1078
+ }
1079
+ }
1080
+ },
1081
+ required: ['task'],
1082
+ additionalProperties: true
1083
+ };
1084
+ fs.writeFileSync(filePath, JSON.stringify(schema, null, 2) + '\n', 'utf-8');
1085
+ // Sync all.json
1086
+ syncAllJson(tasksDir);
1087
+ // Invalidate cache so new schema is immediately discoverable
1088
+ _workflowTaskNamesCache = null;
1089
+ console.log(chalk_1.default.green(`\n✓ Created task schema: ${path.relative(process.cwd(), filePath)}`));
1090
+ console.log(chalk_1.default.gray(`\n Next steps:`));
1091
+ console.log(chalk_1.default.gray(` 1. Edit ${chalk_1.default.white(filePath)} to add typed input properties`));
1092
+ console.log(chalk_1.default.gray(` 2. Verify: ${chalk_1.default.white(`${PROGRAM_NAME} schema ${safeName}`)}`));
1093
+ console.log(chalk_1.default.gray(` 3. all.json has been auto-updated with the new reference`));
1094
+ console.log('');
1095
+ }
1096
+ // ============================================================================
1097
+ // Sync all.json (auto-regenerate $ref entries from task schema directory)
1098
+ // ============================================================================
1099
+ function syncAllJson(tasksDir) {
1100
+ const files = fs.readdirSync(tasksDir)
1101
+ .filter(f => f.endsWith('.json') && f !== 'all.json' && f !== 'generic.json')
1102
+ .sort();
1103
+ const anyOfRefs = files.map(f => ({ $ref: f }));
1104
+ // generic.json always last as fallback
1105
+ if (fs.existsSync(path.join(tasksDir, 'generic.json'))) {
1106
+ anyOfRefs.push({ $ref: 'generic.json' });
1107
+ }
1108
+ const allJson = {
1109
+ $schema: 'http://json-schema.org/draft-07/schema#',
1110
+ title: 'All Workflow Tasks',
1111
+ description: 'Aggregator schema for all workflow task types. Uses anyOf to allow matching any known task type or falling back to generic task structure.',
1112
+ type: 'object',
1113
+ anyOf: anyOfRefs
1114
+ };
1115
+ fs.writeFileSync(path.join(tasksDir, 'all.json'), JSON.stringify(allJson, null, 2) + '\n', 'utf-8');
1116
+ }
1117
+ function runSyncSchemas() {
1118
+ // Find schemas directory
1119
+ let schemasDir = path.join(process.cwd(), 'schemas');
1120
+ if (!fs.existsSync(schemasDir)) {
1121
+ const resolved = findSchemasPath();
1122
+ if (!resolved) {
1123
+ console.error(chalk_1.default.red('Error: Cannot find schemas directory'));
1124
+ process.exit(2);
1125
+ }
1126
+ schemasDir = resolved;
1127
+ }
1128
+ const tasksDir = path.join(schemasDir, 'workflows', 'tasks');
1129
+ if (!fs.existsSync(tasksDir)) {
1130
+ console.error(chalk_1.default.red('Error: Tasks directory not found'));
1131
+ process.exit(2);
1132
+ }
1133
+ syncAllJson(tasksDir);
1134
+ // Invalidate cache
1135
+ _workflowTaskNamesCache = null;
1136
+ const taskCount = fs.readdirSync(tasksDir)
1137
+ .filter(f => f.endsWith('.json') && f !== 'all.json' && f !== 'generic.json')
1138
+ .length;
1139
+ console.log(chalk_1.default.green(`\n✓ Synced all.json with ${taskCount} task schemas (+ generic fallback)`));
1140
+ console.log('');
1141
+ }
1142
+ // ============================================================================
963
1143
  // Extract Command
964
1144
  // ============================================================================
965
- function runExtract(sourceFile, componentName, targetFile) {
1145
+ function runExtract(sourceFile, componentName, targetFile, copy) {
966
1146
  // Validate args
967
1147
  if (!sourceFile || !componentName || !targetFile) {
968
1148
  console.error(chalk_1.default.red('Error: Missing required arguments'));
969
- console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} extract <source-file> <component-name> --to <target-file>`));
1149
+ console.error(chalk_1.default.gray(`Usage: ${PROGRAM_NAME} extract <source-file> <component-name> --to <target-file> [--copy]`));
970
1150
  process.exit(2);
971
1151
  }
972
1152
  // Check source exists
@@ -974,17 +1154,26 @@ function runExtract(sourceFile, componentName, targetFile) {
974
1154
  console.error(chalk_1.default.red(`Error: Source file not found: ${sourceFile}`));
975
1155
  process.exit(2);
976
1156
  }
977
- // Read and parse source
1157
+ // Read and parse source (Document API preserves comments)
978
1158
  const sourceContent = fs.readFileSync(sourceFile, 'utf-8');
979
- const sourceDoc = yaml.load(sourceContent);
980
- if (!sourceDoc || !Array.isArray(sourceDoc.components)) {
1159
+ const srcDoc = yaml_1.default.parseDocument(sourceContent);
1160
+ const sourceJS = srcDoc.toJS();
1161
+ if (!sourceJS || !Array.isArray(sourceJS.components)) {
981
1162
  console.error(chalk_1.default.red(`Error: Source file is not a valid module (missing components array): ${sourceFile}`));
982
1163
  process.exit(2);
983
1164
  }
1165
+ // Get the AST components sequence
1166
+ const srcComponents = srcDoc.get('components', true);
1167
+ if (!(0, yaml_1.isSeq)(srcComponents)) {
1168
+ console.error(chalk_1.default.red(`Error: Source components is not a sequence: ${sourceFile}`));
1169
+ process.exit(2);
1170
+ }
984
1171
  // Find component by exact name match
985
- const compIndex = sourceDoc.components.findIndex((c) => c.name === componentName);
1172
+ const compIndex = srcComponents.items.findIndex((item) => {
1173
+ return (0, yaml_1.isMap)(item) && item.get('name') === componentName;
1174
+ });
986
1175
  if (compIndex === -1) {
987
- const available = sourceDoc.components.map((c) => c.name).filter(Boolean);
1176
+ const available = sourceJS.components.map((c) => c.name).filter(Boolean);
988
1177
  console.error(chalk_1.default.red(`Error: Component not found: ${componentName}`));
989
1178
  if (available.length > 0) {
990
1179
  console.error(chalk_1.default.gray('Available components:'));
@@ -994,32 +1183,52 @@ function runExtract(sourceFile, componentName, targetFile) {
994
1183
  }
995
1184
  process.exit(2);
996
1185
  }
997
- const component = sourceDoc.components[compIndex];
998
- // Find matching routes
999
- const matchedRoutes = [];
1000
- const remainingRoutes = [];
1001
- if (Array.isArray(sourceDoc.routes)) {
1002
- for (const route of sourceDoc.routes) {
1003
- if (route.component === componentName) {
1004
- matchedRoutes.push(route);
1005
- }
1006
- else {
1007
- remainingRoutes.push(route);
1008
- }
1186
+ // Get the component AST node (clone for copy, take for move)
1187
+ const componentNode = copy
1188
+ ? srcDoc.createNode(sourceJS.components[compIndex])
1189
+ : srcComponents.items[compIndex];
1190
+ // Capture comment: if this is the first item, the comment lives on the parent seq
1191
+ let componentComment;
1192
+ if (compIndex === 0 && srcComponents.commentBefore) {
1193
+ componentComment = srcComponents.commentBefore;
1194
+ if (!copy) {
1195
+ // Transfer the comment away from the source seq (it belongs to the extracted component)
1196
+ srcComponents.commentBefore = undefined;
1009
1197
  }
1010
1198
  }
1011
- // Load or create target
1012
- let targetDoc;
1199
+ else {
1200
+ componentComment = componentNode.commentBefore;
1201
+ }
1202
+ // Find matching routes (by index in AST)
1203
+ const srcRoutes = srcDoc.get('routes', true);
1204
+ const matchedRouteIndices = [];
1205
+ if ((0, yaml_1.isSeq)(srcRoutes)) {
1206
+ srcRoutes.items.forEach((item, idx) => {
1207
+ if ((0, yaml_1.isMap)(item) && item.get('component') === componentName) {
1208
+ matchedRouteIndices.push(idx);
1209
+ }
1210
+ });
1211
+ }
1212
+ // Collect route AST nodes (clone for copy, reference for move)
1213
+ const routeNodes = matchedRouteIndices.map(idx => {
1214
+ if (copy) {
1215
+ return srcDoc.createNode(sourceJS.routes[idx]);
1216
+ }
1217
+ return srcRoutes.items[idx];
1218
+ });
1219
+ // Load or create target document
1220
+ let tgtDoc;
1013
1221
  let targetCreated = false;
1014
1222
  if (fs.existsSync(targetFile)) {
1015
1223
  const targetContent = fs.readFileSync(targetFile, 'utf-8');
1016
- targetDoc = yaml.load(targetContent);
1017
- if (!targetDoc || !Array.isArray(targetDoc.components)) {
1224
+ tgtDoc = yaml_1.default.parseDocument(targetContent);
1225
+ const targetJS = tgtDoc.toJS();
1226
+ if (!targetJS || !Array.isArray(targetJS.components)) {
1018
1227
  console.error(chalk_1.default.red(`Error: Target file is not a valid module (missing components array): ${targetFile}`));
1019
1228
  process.exit(2);
1020
1229
  }
1021
1230
  // Check for duplicate component name
1022
- const duplicate = targetDoc.components.find((c) => c.name === componentName);
1231
+ const duplicate = targetJS.components.find((c) => c.name === componentName);
1023
1232
  if (duplicate) {
1024
1233
  console.error(chalk_1.default.red(`Error: Target already contains a component named "${componentName}"`));
1025
1234
  process.exit(2);
@@ -1032,41 +1241,92 @@ function runExtract(sourceFile, componentName, targetFile) {
1032
1241
  .split('-')
1033
1242
  .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
1034
1243
  .join('');
1035
- targetDoc = {
1036
- module: moduleName,
1244
+ const sourceModule = typeof sourceJS.module === 'object' ? sourceJS.module : null;
1245
+ const displayName = moduleName.replace(/([a-z])([A-Z])/g, '$1 $2');
1246
+ const moduleObj = {
1247
+ name: moduleName,
1037
1248
  appModuleId: generateUUID(),
1038
- application: sourceDoc.application || 'cx',
1249
+ displayName: { 'en-US': displayName },
1250
+ description: { 'en-US': `${displayName} module` },
1251
+ application: sourceModule?.application || sourceJS.application || 'cx',
1252
+ };
1253
+ // In copy mode, set priority higher than source
1254
+ if (copy) {
1255
+ const sourcePriority = sourceModule?.priority;
1256
+ moduleObj.priority = (0, extractUtils_1.computeExtractPriority)(sourcePriority);
1257
+ }
1258
+ // Parse from string so the document has proper AST context for comment preservation
1259
+ const scaffoldStr = yaml_1.default.stringify({
1260
+ module: moduleObj,
1039
1261
  entities: [],
1040
1262
  permissions: [],
1041
1263
  components: [],
1042
1264
  routes: []
1043
- };
1265
+ }, { indent: 2, lineWidth: 0, singleQuote: false });
1266
+ tgtDoc = yaml_1.default.parseDocument(scaffoldStr);
1044
1267
  targetCreated = true;
1045
1268
  }
1046
- // Move component to target
1047
- targetDoc.components.push(component);
1048
- sourceDoc.components.splice(compIndex, 1);
1049
- // Move routes to target
1050
- if (matchedRoutes.length > 0) {
1051
- if (!Array.isArray(targetDoc.routes)) {
1052
- targetDoc.routes = [];
1269
+ // Add component to target (ensure block style so comments are preserved)
1270
+ const tgtComponents = tgtDoc.get('components', true);
1271
+ if ((0, yaml_1.isSeq)(tgtComponents)) {
1272
+ tgtComponents.flow = false;
1273
+ // Apply the captured comment: if it's the first item in target, set on seq; otherwise on node
1274
+ if (componentComment) {
1275
+ if (tgtComponents.items.length === 0) {
1276
+ tgtComponents.commentBefore = componentComment;
1277
+ }
1278
+ else {
1279
+ componentNode.commentBefore = componentComment;
1280
+ }
1281
+ }
1282
+ tgtComponents.items.push(componentNode);
1283
+ }
1284
+ else {
1285
+ tgtDoc.addIn(['components'], componentNode);
1286
+ }
1287
+ // In move mode, remove component from source
1288
+ if (!copy) {
1289
+ srcComponents.items.splice(compIndex, 1);
1290
+ }
1291
+ // Add routes to target
1292
+ if (routeNodes.length > 0) {
1293
+ let tgtRoutes = tgtDoc.get('routes', true);
1294
+ if (!(0, yaml_1.isSeq)(tgtRoutes)) {
1295
+ tgtDoc.set('routes', tgtDoc.createNode([]));
1296
+ tgtRoutes = tgtDoc.get('routes', true);
1297
+ }
1298
+ tgtRoutes.flow = false;
1299
+ for (const routeNode of routeNodes) {
1300
+ tgtRoutes.items.push(routeNode);
1301
+ }
1302
+ // In move mode, remove routes from source (reverse order to preserve indices)
1303
+ if (!copy && (0, yaml_1.isSeq)(srcRoutes)) {
1304
+ for (let i = matchedRouteIndices.length - 1; i >= 0; i--) {
1305
+ srcRoutes.items.splice(matchedRouteIndices[i], 1);
1306
+ }
1053
1307
  }
1054
- targetDoc.routes.push(...matchedRoutes);
1055
- sourceDoc.routes = remainingRoutes;
1056
1308
  }
1057
1309
  // Ensure target directory exists
1058
1310
  const targetDir = path.dirname(targetFile);
1059
1311
  if (!fs.existsSync(targetDir)) {
1060
1312
  fs.mkdirSync(targetDir, { recursive: true });
1061
1313
  }
1062
- // Write both files
1063
- const dumpOptions = { indent: 2, lineWidth: -1, noRefs: true, quotingType: '"', forceQuotes: false };
1064
- fs.writeFileSync(sourceFile, yaml.dump(sourceDoc, dumpOptions), 'utf-8');
1065
- fs.writeFileSync(targetFile, yaml.dump(targetDoc, dumpOptions), 'utf-8');
1314
+ // Write files (toString preserves comments)
1315
+ const toStringOpts = { indent: 2, lineWidth: 0, singleQuote: false };
1316
+ if (!copy) {
1317
+ fs.writeFileSync(sourceFile, srcDoc.toString(toStringOpts), 'utf-8');
1318
+ }
1319
+ fs.writeFileSync(targetFile, tgtDoc.toString(toStringOpts), 'utf-8');
1066
1320
  // Print summary
1067
- console.log(chalk_1.default.green(`\n✓ Extracted component: ${chalk_1.default.bold(componentName)}`));
1068
- console.log(chalk_1.default.gray(` Routes moved: ${matchedRoutes.length}`));
1069
- console.log(chalk_1.default.gray(` Source: ${sourceFile} (updated)`));
1321
+ const action = copy ? 'Copied' : 'Extracted';
1322
+ console.log(chalk_1.default.green(`\n✓ ${action} component: ${chalk_1.default.bold(componentName)}`));
1323
+ console.log(chalk_1.default.gray(` Routes ${copy ? 'copied' : 'moved'}: ${matchedRouteIndices.length}`));
1324
+ if (!copy) {
1325
+ console.log(chalk_1.default.gray(` Source: ${sourceFile} (updated)`));
1326
+ }
1327
+ else {
1328
+ console.log(chalk_1.default.gray(` Source: ${sourceFile} (unchanged)`));
1329
+ }
1070
1330
  console.log(chalk_1.default.gray(` Target: ${targetFile} (${targetCreated ? 'created' : 'updated'})`));
1071
1331
  console.log('');
1072
1332
  }
@@ -1090,7 +1350,7 @@ function parseArgs(args) {
1090
1350
  reportFormat: 'json'
1091
1351
  };
1092
1352
  // Check for commands
1093
- const commands = ['validate', 'schema', 'example', 'list', 'help', 'report', 'init', 'create', 'extract'];
1353
+ const commands = ['validate', 'schema', 'example', 'list', 'help', 'report', 'init', 'create', 'extract', 'sync-schemas'];
1094
1354
  if (args.length > 0 && commands.includes(args[0])) {
1095
1355
  command = args[0];
1096
1356
  args = args.slice(1);
@@ -1157,9 +1417,15 @@ function parseArgs(args) {
1157
1417
  else if (arg === '--options') {
1158
1418
  options.createOptions = args[++i];
1159
1419
  }
1420
+ else if (arg === '--tasks') {
1421
+ options.createTasks = args[++i];
1422
+ }
1160
1423
  else if (arg === '--to') {
1161
1424
  options.extractTo = args[++i];
1162
1425
  }
1426
+ else if (arg === '--copy') {
1427
+ options.extractCopy = true;
1428
+ }
1163
1429
  else if (!arg.startsWith('-')) {
1164
1430
  files.push(arg);
1165
1431
  }
@@ -1218,7 +1484,7 @@ function findSchemasPath() {
1218
1484
  function detectFileType(filePath) {
1219
1485
  try {
1220
1486
  const content = fs.readFileSync(filePath, 'utf-8');
1221
- const data = yaml.load(content);
1487
+ const data = yaml_1.default.parse(content);
1222
1488
  if (data && typeof data === 'object') {
1223
1489
  if ('workflow' in data) {
1224
1490
  return 'workflow';
@@ -1244,47 +1510,81 @@ function detectFileType(filePath) {
1244
1510
  // ============================================================================
1245
1511
  // Schema Display
1246
1512
  // ============================================================================
1513
+ // Cache for dynamically discovered workflow task schema names
1514
+ let _workflowTaskNamesCache = null;
1515
+ function getWorkflowTaskNames(schemasPath) {
1516
+ if (_workflowTaskNamesCache)
1517
+ return _workflowTaskNamesCache;
1518
+ const tasksDir = path.join(schemasPath, 'workflows', 'tasks');
1519
+ _workflowTaskNamesCache = new Set();
1520
+ if (fs.existsSync(tasksDir)) {
1521
+ for (const file of fs.readdirSync(tasksDir)) {
1522
+ if (file.endsWith('.json') && file !== 'all.json') {
1523
+ _workflowTaskNamesCache.add(file.replace('.json', '').toLowerCase().replace(/[^a-z0-9-]/g, ''));
1524
+ }
1525
+ }
1526
+ }
1527
+ // Also include common definitions
1528
+ const commonDir = path.join(schemasPath, 'workflows', 'common');
1529
+ if (fs.existsSync(commonDir)) {
1530
+ for (const file of fs.readdirSync(commonDir)) {
1531
+ if (file.endsWith('.json')) {
1532
+ _workflowTaskNamesCache.add(file.replace('.json', '').toLowerCase().replace(/[^a-z0-9-]/g, ''));
1533
+ }
1534
+ }
1535
+ }
1536
+ return _workflowTaskNamesCache;
1537
+ }
1247
1538
  function findSchemaFile(schemasPath, name, preferWorkflow = false) {
1248
- // Normalize name
1539
+ // Normalize name: lowercase, strip non-alphanumeric except hyphens
1249
1540
  const normalizedName = name.toLowerCase().replace(/[^a-z0-9-]/g, '');
1250
- // Workflow schema names - these should match workflow schemas first
1541
+ // Dynamically detect workflow schema names from directory contents
1251
1542
  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
- ];
1543
+ const workflowTaskNames = getWorkflowTaskNames(schemasPath);
1259
1544
  const isWorkflowSchema = workflowCoreNames.includes(normalizedName) ||
1260
- workflowTaskNames.includes(normalizedName);
1261
- // Search patterns in order of priority
1545
+ workflowTaskNames.has(normalizedName);
1546
+ // Build search paths using normalized name for consistency
1262
1547
  const searchPaths = preferWorkflow || isWorkflowSchema
1263
1548
  ? [
1264
1549
  // 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`),
1550
+ path.join(schemasPath, 'workflows', `${normalizedName}.json`),
1551
+ path.join(schemasPath, 'workflows', 'tasks', `${normalizedName}.json`),
1552
+ path.join(schemasPath, 'workflows', 'common', `${normalizedName}.json`),
1268
1553
  // Then module schemas
1269
- path.join(schemasPath, 'components', `${name}.json`),
1270
- path.join(schemasPath, 'fields', `${name}.json`),
1271
- path.join(schemasPath, 'actions', `${name}.json`)
1554
+ path.join(schemasPath, 'components', `${normalizedName}.json`),
1555
+ path.join(schemasPath, 'fields', `${normalizedName}.json`),
1556
+ path.join(schemasPath, 'actions', `${normalizedName}.json`)
1272
1557
  ]
1273
1558
  : [
1274
1559
  // Module schemas first
1275
- path.join(schemasPath, 'components', `${name}.json`),
1276
- path.join(schemasPath, 'fields', `${name}.json`),
1277
- path.join(schemasPath, 'actions', `${name}.json`),
1560
+ path.join(schemasPath, 'components', `${normalizedName}.json`),
1561
+ path.join(schemasPath, 'fields', `${normalizedName}.json`),
1562
+ path.join(schemasPath, 'actions', `${normalizedName}.json`),
1278
1563
  // 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`)
1564
+ path.join(schemasPath, 'workflows', `${normalizedName}.json`),
1565
+ path.join(schemasPath, 'workflows', 'tasks', `${normalizedName}.json`),
1566
+ path.join(schemasPath, 'workflows', 'common', `${normalizedName}.json`)
1282
1567
  ];
1283
1568
  for (const schemaPath of searchPaths) {
1284
1569
  if (fs.existsSync(schemaPath)) {
1285
1570
  return schemaPath;
1286
1571
  }
1287
1572
  }
1573
+ // Also try with the original name (preserving case) for backwards compatibility
1574
+ if (normalizedName !== name) {
1575
+ const caseSensitivePaths = [
1576
+ path.join(schemasPath, 'workflows', 'tasks', `${name}.json`),
1577
+ path.join(schemasPath, 'workflows', `${name}.json`),
1578
+ path.join(schemasPath, 'components', `${name}.json`),
1579
+ path.join(schemasPath, 'fields', `${name}.json`),
1580
+ path.join(schemasPath, 'actions', `${name}.json`)
1581
+ ];
1582
+ for (const schemaPath of caseSensitivePaths) {
1583
+ if (fs.existsSync(schemaPath)) {
1584
+ return schemaPath;
1585
+ }
1586
+ }
1587
+ }
1288
1588
  // Try fuzzy matching
1289
1589
  const allSchemas = getAllSchemas(schemasPath);
1290
1590
  for (const schema of allSchemas) {
@@ -1342,7 +1642,7 @@ function showExample(schemasPath, name) {
1342
1642
  console.log(chalk_1.default.gray('─'.repeat(70)));
1343
1643
  // Generate example from schema
1344
1644
  const example = generateExampleFromSchema(schema, name);
1345
- console.log(yaml.dump(example, { indent: 2, lineWidth: 100 }));
1645
+ console.log(yaml_1.default.stringify(example, { indent: 2, lineWidth: 100 }));
1346
1646
  console.log(chalk_1.default.gray('─'.repeat(70)));
1347
1647
  }
1348
1648
  function generateExampleFromSchema(schema, name) {
@@ -1940,12 +2240,23 @@ async function main() {
1940
2240
  }
1941
2241
  // Handle create command
1942
2242
  if (command === 'create') {
1943
- runCreate(files[0], files[1], options.template, options.feature, options.createOptions);
2243
+ // For task-schema, pass --tasks (from options.createTasks) as the tasks argument
2244
+ if (files[0] === 'task-schema') {
2245
+ runCreate(files[0], files[1], options.template, options.feature, options.createTasks);
2246
+ }
2247
+ else {
2248
+ runCreate(files[0], files[1], options.template, options.feature, options.createOptions);
2249
+ }
2250
+ process.exit(0);
2251
+ }
2252
+ // Handle sync-schemas command
2253
+ if (command === 'sync-schemas') {
2254
+ runSyncSchemas();
1944
2255
  process.exit(0);
1945
2256
  }
1946
2257
  // Handle extract command
1947
2258
  if (command === 'extract') {
1948
- runExtract(files[0], files[1], options.extractTo);
2259
+ runExtract(files[0], files[1], options.extractTo, options.extractCopy);
1949
2260
  process.exit(0);
1950
2261
  }
1951
2262
  // Validate files