@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.
- package/.claude/skills/cx-core/ref-entity-commodity.md +22 -0
- package/.claude/skills/cx-workflow/SKILL.md +2 -2
- package/.claude/skills/cx-workflow/ref-entity.md +30 -1
- package/.claude/skills/cx-workflow/ref-expressions.md +3 -0
- package/.claude/skills/cx-workflow/ref-utilities.md +21 -0
- package/dist/cli.js +399 -88
- package/dist/cli.js.map +1 -1
- package/dist/extractUtils.d.ts +11 -0
- package/dist/extractUtils.d.ts.map +1 -0
- package/dist/extractUtils.js +19 -0
- package/dist/extractUtils.js.map +1 -0
- package/dist/validator.js +2 -2
- package/dist/validator.js.map +1 -1
- package/dist/workflowValidator.js +2 -2
- package/dist/workflowValidator.js.map +1 -1
- package/package.json +5 -5
- package/schemas/workflows/tasks/action-event.json +65 -0
- package/schemas/workflows/tasks/all.json +126 -26
- package/schemas/workflows/tasks/appmodule.json +56 -0
- package/schemas/workflows/tasks/attachment.json +4 -1
- package/schemas/workflows/tasks/authentication.json +72 -0
- package/schemas/workflows/tasks/caching.json +68 -0
- package/schemas/workflows/tasks/charge.json +3 -1
- package/schemas/workflows/tasks/commodity.json +3 -0
- package/schemas/workflows/tasks/contact-address.json +72 -0
- package/schemas/workflows/tasks/contact-payment-method.json +72 -0
- package/schemas/workflows/tasks/edi.json +65 -0
- package/schemas/workflows/tasks/filetransfer.json +102 -0
- package/schemas/workflows/tasks/flow-transition.json +68 -0
- package/schemas/workflows/tasks/httpRequest.json +23 -0
- package/schemas/workflows/tasks/import.json +64 -0
- package/schemas/workflows/tasks/inventory.json +67 -0
- package/schemas/workflows/tasks/movement.json +54 -0
- package/schemas/workflows/tasks/note.json +59 -0
- package/schemas/workflows/tasks/number.json +65 -0
- package/schemas/workflows/tasks/order.json +8 -1
- package/schemas/workflows/tasks/pdf-document.json +60 -0
- package/schemas/workflows/tasks/user.json +70 -0
- 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
|
|
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
|
|
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,
|
|
199
|
-
|
|
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 =
|
|
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 =
|
|
863
|
+
const yamlContent = yaml_1.default.stringify(doc, {
|
|
841
864
|
indent: 2,
|
|
842
|
-
lineWidth:
|
|
843
|
-
|
|
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
|
|
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
|
|
980
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
if
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1017
|
-
|
|
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 =
|
|
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
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
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
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
1068
|
-
console.log(chalk_1.default.
|
|
1069
|
-
console.log(chalk_1.default.gray(`
|
|
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 =
|
|
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
|
-
//
|
|
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.
|
|
1261
|
-
//
|
|
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', `${
|
|
1266
|
-
path.join(schemasPath, 'workflows', 'tasks', `${
|
|
1267
|
-
path.join(schemasPath, 'workflows', 'common', `${
|
|
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', `${
|
|
1270
|
-
path.join(schemasPath, 'fields', `${
|
|
1271
|
-
path.join(schemasPath, 'actions', `${
|
|
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', `${
|
|
1276
|
-
path.join(schemasPath, 'fields', `${
|
|
1277
|
-
path.join(schemasPath, 'actions', `${
|
|
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', `${
|
|
1280
|
-
path.join(schemasPath, 'workflows', 'tasks', `${
|
|
1281
|
-
path.join(schemasPath, 'workflows', 'common', `${
|
|
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(
|
|
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
|
-
|
|
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
|